Compare commits
87 Commits
form_build
...
v1.7.3
Author | SHA1 | Date | |
---|---|---|---|
09d5f5a083 | |||
1a41b3fb64 | |||
f958550061 | |||
1e8e7ec4a4 | |||
83c4e38fa5 | |||
607d2fedb7 | |||
627ada56b7 | |||
9ce06fdc4e | |||
bb63ae6d87 | |||
a4182621da | |||
0534261759 | |||
c7baa66a4d | |||
1732606581 | |||
68cdd2c2c8 | |||
ea03b6c19c | |||
b83eb41df3 | |||
e6c68dc5bc | |||
76a953819e | |||
3a2ad48bd6 | |||
674d5bae8a | |||
5e983641b6 | |||
96d4665880 | |||
889ddf6a38 | |||
158e613e29 | |||
255c52db26 | |||
072c81177f | |||
e5c7fc93e2 | |||
5b7b217b9c | |||
06cedaef4b | |||
6972e8a3db | |||
18ba0148ae | |||
dea019ebdc | |||
e27e93aa9a | |||
c9ee7d477d | |||
e9deb13ce4 | |||
cdac238f6d | |||
e2c5e2c7fb | |||
0c3f819200 | |||
3673230fdf | |||
f2cb07ac95 | |||
484cf9d8a2 | |||
5b20be8cfd | |||
4dbe622a4a | |||
9a4dec57d1 | |||
f5c5178f95 | |||
727cf84080 | |||
80a257e85f | |||
ad3c15df9b | |||
c665bd2321 | |||
948bae9f95 | |||
a1c10b4ea3 | |||
f36df81d9a | |||
2fd9eb6c68 | |||
8894d14130 | |||
4039e74a82 | |||
0af3faf6ff | |||
0520b69c18 | |||
e11a775bed | |||
b4ed4623e1 | |||
9ee9653c7d | |||
e55a16d917 | |||
3458a0b22c | |||
ddcfa735e0 | |||
3370240541 | |||
c0cec4716e | |||
08b239e87a | |||
84132e794a | |||
425d70f261 | |||
420954ed00 | |||
45edd330f5 | |||
6a0e2bcad3 | |||
d67d3e0167 | |||
cd4f3d9a66 | |||
5c6db35c9b | |||
887bea4328 | |||
def5095d77 | |||
ab66662ff6 | |||
2d84433a62 | |||
b8e61787d4 | |||
669825a35d | |||
31b25ca169 | |||
a6ee92fbd5 | |||
5ff1a59a99 | |||
4f65eb4d65 | |||
39328c7368 | |||
2f5f3e1b51 | |||
022285806b |
@ -14,8 +14,8 @@
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value=".NETCoreApp,Version=v3.1" />
|
||||
<method v="2">
|
||||
<option name="Build" default="false" projectName="BTCPayServer.Plugins.Test" projectPath="C:\Git\btcpayserver\BTCPayServer.Plugins.Test\BTCPayServer.Plugins.Test.csproj" />
|
||||
<option name="Build" default="false" projectName="BTCPayServer.Plugins.Test" projectPath="C:\Git\btcpayserver\Plugins\BTCPayServer.Plugins.Test\BTCPayServer.Plugins.Test.csproj" />
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
</component>
|
||||
|
18
BTCPayServer.Abstractions/CamelCaseSerializerSettings.cs
Normal file
18
BTCPayServer.Abstractions/CamelCaseSerializerSettings.cs
Normal file
@ -0,0 +1,18 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,35 +1,64 @@
|
||||
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 abstract class Field
|
||||
public class Field
|
||||
{
|
||||
// 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 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;
|
||||
|
||||
// The translated label of the field.
|
||||
public string Label;
|
||||
|
||||
|
||||
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 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 bool Required = false;
|
||||
|
||||
public bool IsValid()
|
||||
public virtual bool IsValid()
|
||||
{
|
||||
return ValidationErrors.Count == 0;
|
||||
return ValidationErrors.Count == 0 && Fields.All(field => field.IsValid());
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
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,60 +1,156 @@
|
||||
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<Fieldset> Fieldsets { get; set; } = new();
|
||||
public List<Field> Fields { get; set; } = new();
|
||||
|
||||
|
||||
// Are all the fields valid in the form?
|
||||
public bool IsValid()
|
||||
{
|
||||
foreach (var fieldset in Fieldsets)
|
||||
{
|
||||
foreach (var field in fieldset.Fields)
|
||||
{
|
||||
if (!field.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return Fields.Select(f => f.IsValid()).All(o => o);
|
||||
}
|
||||
|
||||
public Field GetFieldByName(string name)
|
||||
{
|
||||
foreach (var fieldset in Fieldsets)
|
||||
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 field in fieldset.Fields)
|
||||
var currentPrefix = prefix;
|
||||
if (!string.IsNullOrEmpty(field.Name))
|
||||
{
|
||||
if (name.Equals(field.Name))
|
||||
|
||||
currentPrefix = $"{prefix}{field.Name}";
|
||||
if (currentPrefix.Equals(name, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return field;
|
||||
}
|
||||
|
||||
currentPrefix += "_";
|
||||
}
|
||||
|
||||
var subFieldResult = GetFieldByName(name, field.Fields, currentPrefix);
|
||||
if (subFieldResult is not null)
|
||||
{
|
||||
return subFieldResult;
|
||||
}
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<string> GetAllNames()
|
||||
{
|
||||
return GetAllNames(Fields);
|
||||
}
|
||||
|
||||
private static List<string> GetAllNames(List<Field> fields)
|
||||
{
|
||||
var names = new List<string>();
|
||||
foreach (var fieldset in Fieldsets)
|
||||
|
||||
foreach (var field in fields)
|
||||
{
|
||||
foreach (var field in fieldset.Fields)
|
||||
string prefix = string.Empty;
|
||||
if (!string.IsNullOrEmpty(field.Name))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +0,0 @@
|
||||
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,5 +1,12 @@
|
||||
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;
|
||||
@ -8,7 +15,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 : UrlResolutionTagHelper
|
||||
public class SVGUse : UrlResolutionTagHelper2
|
||||
{
|
||||
private readonly IFileVersionProvider _fileVersionProvider;
|
||||
|
||||
@ -21,5 +28,6 @@ public class SVGUse : UrlResolutionTagHelper
|
||||
var attr = output.Attributes["href"].Value.ToString();
|
||||
attr = _fileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, attr);
|
||||
output.Attributes.SetAttribute("href", attr);
|
||||
}
|
||||
base.Process(context, output);
|
||||
}
|
||||
}
|
||||
|
314
BTCPayServer.Abstractions/TagHelpers/UrlResolutionTagHelper2.cs
Normal file
314
BTCPayServer.Abstractions/TagHelpers/UrlResolutionTagHelper2.cs
Normal file
@ -0,0 +1,314 @@
|
||||
#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.0</Version>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.2</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.19" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.23" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -128,5 +128,18 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,11 +33,12 @@ namespace BTCPayServer.Client
|
||||
return await HandleResponse<ApplicationUserData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task LockUser(string idOrEmail, bool locked, CancellationToken token = default)
|
||||
public virtual async Task<bool> LockUser(string idOrEmail, bool locked, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{idOrEmail}/lock", null,
|
||||
new LockUserRequest() {Locked = locked}, HttpMethod.Post), token);
|
||||
new LockUserRequest {Locked = locked}, HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public virtual async Task<ApplicationUserData[]> GetUsers( CancellationToken token = default)
|
||||
|
@ -37,7 +37,7 @@ namespace BTCPayServer.Client.Models
|
||||
public string RedirectUrl { get; set; } = null;
|
||||
public bool? RedirectAutomatically { get; set; } = null;
|
||||
public bool? RequiresRefundEmail { get; set; } = null;
|
||||
public string CheckoutFormId { get; set; } = null;
|
||||
public string FormId { get; set; } = null;
|
||||
public string EmbeddedCSS { get; set; } = null;
|
||||
public CheckoutType? CheckoutType { get; set; } = null;
|
||||
}
|
||||
|
@ -22,8 +22,7 @@ namespace BTCPayServer.Client.Models
|
||||
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
||||
public LightMoney Amount { get; set; }
|
||||
public string Description { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
|
||||
public uint256 DescriptionHash { get; set; }
|
||||
public bool DescriptionHashOnly { get; set; }
|
||||
[JsonConverter(typeof(JsonConverters.TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan Expiry { get; set; }
|
||||
public bool PrivateRouteHints { get; set; }
|
||||
|
@ -85,8 +85,6 @@ namespace BTCPayServer.Client.Models
|
||||
public bool? RedirectAutomatically { get; set; }
|
||||
public bool? RequiresRefundEmail { get; set; } = null;
|
||||
public string DefaultLanguage { get; set; }
|
||||
[JsonProperty("checkoutFormId")]
|
||||
public string CheckoutFormId { get; set; }
|
||||
public CheckoutType? CheckoutType { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
[JsonConverter(typeof(NodeUriJsonConverter))]
|
||||
[JsonProperty("nodeURI")]
|
||||
public NodeInfo NodeURI { get; set; }
|
||||
public BTCPayServer.Lightning.NodeInfo NodeURI { get; set; }
|
||||
[JsonConverter(typeof(MoneyJsonConverter))]
|
||||
public Money ChannelAmount { get; set; }
|
||||
|
||||
|
@ -24,5 +24,9 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
|
||||
public string FormId { get; set; }
|
||||
|
||||
public JObject FormResponse { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ namespace BTCPayServer.Client.Models
|
||||
public DateTimeOffset CreatedTime { get; set; }
|
||||
public string Id { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
|
||||
public enum PaymentRequestStatus
|
||||
{
|
||||
Pending = 0,
|
||||
|
27
BTCPayServer.Client/Models/RefundInvoiceRequest.cs
Normal file
27
BTCPayServer.Client/Models/RefundInvoiceRequest.cs
Normal file
@ -0,0 +1,27 @@
|
||||
#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; }
|
||||
|
||||
public string CheckoutFormId { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public CheckoutType CheckoutType { get; set; }
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
|
@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
@ -19,6 +16,7 @@ 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
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.2.1" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.2.2" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
|
@ -105,10 +105,10 @@ namespace BTCPayServer.Data
|
||||
//PlannedTransaction.OnModelCreating(builder);
|
||||
PullPaymentData.OnModelCreating(builder);
|
||||
RefundData.OnModelCreating(builder);
|
||||
//SettingData.OnModelCreating(builder);
|
||||
SettingData.OnModelCreating(builder, Database);
|
||||
StoreSettingData.OnModelCreating(builder, Database);
|
||||
StoreWebhookData.OnModelCreating(builder);
|
||||
//StoreData.OnModelCreating(builder);
|
||||
StoreData.OnModelCreating(builder, Database);
|
||||
U2FDevice.OnModelCreating(builder);
|
||||
Fido2Credential.OnModelCreating(builder);
|
||||
BTCPayServer.Data.UserStore.OnModelCreating(builder);
|
||||
|
12
BTCPayServer.Data/Data/FormData.cs
Normal file
12
BTCPayServer.Data/Data/FormData.cs
Normal file
@ -0,0 +1,12 @@
|
||||
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,3 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class SettingData
|
||||
@ -5,5 +8,15 @@ 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,6 +3,8 @@ 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
|
||||
@ -36,7 +38,7 @@ namespace BTCPayServer.Data
|
||||
|
||||
[NotMapped] public string Role { get; set; }
|
||||
|
||||
public byte[] StoreBlob { get; set; }
|
||||
public string StoreBlob { get; set; }
|
||||
|
||||
[Obsolete("Use GetDefaultPaymentId instead")]
|
||||
public string DefaultCrypto { get; set; }
|
||||
@ -48,5 +50,15 @@ 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,42 +21,44 @@ namespace BTCPayServer.Data
|
||||
public const string PayjoinExposed = "pj-exposed";
|
||||
public const string Payout = "payout";
|
||||
public const string PullPayment = "pull-payment";
|
||||
public const string Address = "address";
|
||||
public const string Utxo = "utxo";
|
||||
}
|
||||
public string WalletId { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Data { get; set; }
|
||||
|
||||
public List<WalletObjectLinkData> ChildLinks { get; set; }
|
||||
public List<WalletObjectLinkData> ParentLinks { 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 (ChildLinks is not null)
|
||||
foreach (var c in ChildLinks)
|
||||
if (Bs is not null)
|
||||
foreach (var c in Bs)
|
||||
{
|
||||
yield return (c.ChildType, c.ChildId, c.Data, c.Child?.Data);
|
||||
yield return (c.BType, c.BId, c.Data, c.B?.Data);
|
||||
}
|
||||
if (ParentLinks is not null)
|
||||
foreach (var c in ParentLinks)
|
||||
if (As is not null)
|
||||
foreach (var c in As)
|
||||
{
|
||||
yield return (c.ParentType, c.ParentId, c.Data, c.Parent?.Data);
|
||||
yield return (c.AType, c.AId, c.Data, c.A?.Data);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<WalletObjectData> GetNeighbours()
|
||||
{
|
||||
if (ChildLinks != null)
|
||||
foreach (var c in ChildLinks)
|
||||
if (Bs != null)
|
||||
foreach (var c in Bs)
|
||||
{
|
||||
if (c.Child != null)
|
||||
yield return c.Child;
|
||||
if (c.B != null)
|
||||
yield return c.B;
|
||||
}
|
||||
if (ParentLinks != null)
|
||||
foreach (var c in ParentLinks)
|
||||
if (As != null)
|
||||
foreach (var c in As)
|
||||
{
|
||||
if (c.Parent != null)
|
||||
yield return c.Parent;
|
||||
if (c.A != null)
|
||||
yield return c.A;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,14 +11,14 @@ namespace BTCPayServer.Data
|
||||
public class WalletObjectLinkData
|
||||
{
|
||||
public string WalletId { get; set; }
|
||||
public string ParentType { get; set; }
|
||||
public string ParentId { get; set; }
|
||||
public string ChildType { get; set; }
|
||||
public string ChildId { 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 Parent { get; set; }
|
||||
public WalletObjectData Child { get; set; }
|
||||
public WalletObjectData A { get; set; }
|
||||
public WalletObjectData B { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
@ -26,28 +26,28 @@ namespace BTCPayServer.Data
|
||||
new
|
||||
{
|
||||
o.WalletId,
|
||||
o.ParentType,
|
||||
o.ParentId,
|
||||
o.ChildType,
|
||||
o.ChildId,
|
||||
o.AType,
|
||||
o.AId,
|
||||
o.BType,
|
||||
o.BId,
|
||||
});
|
||||
builder.Entity<WalletObjectLinkData>().HasIndex(o => new
|
||||
{
|
||||
o.WalletId,
|
||||
o.ChildType,
|
||||
o.ChildId,
|
||||
o.BType,
|
||||
o.BId,
|
||||
});
|
||||
|
||||
builder.Entity<WalletObjectLinkData>()
|
||||
.HasOne(o => o.Parent)
|
||||
.WithMany(o => o.ChildLinks)
|
||||
.HasForeignKey(o => new { o.WalletId, o.ParentType, o.ParentId })
|
||||
.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.Child)
|
||||
.WithMany(o => o.ParentLinks)
|
||||
.HasForeignKey(o => new { o.WalletId, o.ChildType, o.ChildId })
|
||||
.HasOne(o => o.B)
|
||||
.WithMany(o => o.As)
|
||||
.HasForeignKey(o => new { o.WalletId, o.BType, o.BId })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
if (databaseFacade.IsNpgsql())
|
||||
|
@ -40,33 +40,33 @@ namespace BTCPayServer.Migrations
|
||||
columns: table => new
|
||||
{
|
||||
WalletId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ParentType = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ParentId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ChildType = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ChildId = 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.ParentType, x.ParentId, x.ChildType, x.ChildId });
|
||||
table.PrimaryKey("PK_WalletObjectLinks", x => new { x.WalletId, x.AType, x.AId, x.BType, x.BId });
|
||||
table.ForeignKey(
|
||||
name: "FK_WalletObjectLinks_WalletObjects_WalletId_ChildType_ChildId",
|
||||
columns: x => new { x.WalletId, x.ChildType, x.ChildId },
|
||||
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_ParentType_ParentId",
|
||||
columns: x => new { x.WalletId, x.ParentType, x.ParentId },
|
||||
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_ChildType_ChildId",
|
||||
name: "IX_WalletObjectLinks_WalletId_BType_BId",
|
||||
table: "WalletObjectLinks",
|
||||
columns: new[] { "WalletId", "ChildType", "ChildId" });
|
||||
columns: new[] { "WalletId", "BType", "BId" });
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
|
31
BTCPayServer.Data/Migrations/20221128062447_jsonb.cs
Normal file
31
BTCPayServer.Data/Migrations/20221128062447_jsonb.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("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
|
||||
}
|
||||
}
|
||||
}
|
@ -872,24 +872,24 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("WalletId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ParentType")
|
||||
b.Property<string>("AType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ParentId")
|
||||
b.Property<string>("AId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ChildType")
|
||||
b.Property<string>("BType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ChildId")
|
||||
b.Property<string>("BId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("WalletId", "ParentType", "ParentId", "ChildType", "ChildId");
|
||||
b.HasKey("WalletId", "AType", "AId", "BType", "BId");
|
||||
|
||||
b.HasIndex("WalletId", "ChildType", "ChildId");
|
||||
b.HasIndex("WalletId", "BType", "BId");
|
||||
|
||||
b.ToTable("WalletObjectLinks");
|
||||
});
|
||||
@ -1384,21 +1384,21 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.WalletObjectData", "Child")
|
||||
.WithMany("ParentLinks")
|
||||
.HasForeignKey("WalletId", "ChildType", "ChildId")
|
||||
b.HasOne("BTCPayServer.Data.WalletObjectData", "A")
|
||||
.WithMany("Bs")
|
||||
.HasForeignKey("WalletId", "AType", "AId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("BTCPayServer.Data.WalletObjectData", "Parent")
|
||||
.WithMany("ChildLinks")
|
||||
.HasForeignKey("WalletId", "ParentType", "ParentId")
|
||||
b.HasOne("BTCPayServer.Data.WalletObjectData", "B")
|
||||
.WithMany("As")
|
||||
.HasForeignKey("WalletId", "BType", "BId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Child");
|
||||
b.Navigation("A");
|
||||
|
||||
b.Navigation("Parent");
|
||||
b.Navigation("B");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
|
||||
@ -1545,9 +1545,9 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectData", b =>
|
||||
{
|
||||
b.Navigation("ChildLinks");
|
||||
b.Navigation("As");
|
||||
|
||||
b.Navigation("ParentLinks");
|
||||
b.Navigation("Bs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WebhookData", b =>
|
||||
|
@ -26,7 +26,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.4.0" />
|
||||
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />
|
||||
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||
<None Include="icon.png" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
@ -1,2 +0,0 @@
|
||||
|
||||
<li class="nav-item"><a asp-controller="UITestExtension" asp-action="Index" class="nav-link js-scroll-trigger" >Dear Nicolas Dorier</a></li>
|
@ -6,7 +6,7 @@
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.14" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.23" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
@ -1,6 +1,6 @@
|
||||
[
|
||||
{
|
||||
"name":"Afghani",
|
||||
"name":"Afghan Afghani",
|
||||
"code":"AFN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -21,7 +21,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Lek",
|
||||
"name":"Albanian Lek",
|
||||
"code":"ALL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -42,7 +42,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Kwanza",
|
||||
"name":"Angolan Kwanza",
|
||||
"code":"AOA",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -84,7 +84,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Azerbaijanian Manat",
|
||||
"name":"Azerbaijani Manat",
|
||||
"code":"AZN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -105,14 +105,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Taka",
|
||||
"name":"Bangladeshi Taka",
|
||||
"code":"BDT",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Barbados Dollar",
|
||||
"name":"Barbadian Dollar",
|
||||
"code":"BBD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -161,21 +161,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Ngultrum",
|
||||
"name":"Bhutanese Ngultrum",
|
||||
"code":"BTN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Boliviano",
|
||||
"name":"Bolivian Boliviano",
|
||||
"code":"BOB",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Mvdol",
|
||||
"name":"Bolivian Mvdol",
|
||||
"code":"BOV",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -189,7 +189,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Pula",
|
||||
"name":"Botswana Pula",
|
||||
"code":"BWP",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -224,21 +224,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Burundi Franc",
|
||||
"name":"Burundian Franc",
|
||||
"code":"BIF",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Cabo Verde Escudo",
|
||||
"name":"Cape Verdean Escudo",
|
||||
"code":"CVE",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Riel",
|
||||
"name":"Cambodian Riel",
|
||||
"code":"KHR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -301,7 +301,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Comoro Franc",
|
||||
"name":"Comorian Franc",
|
||||
"code":"KMF",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
@ -329,7 +329,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Kuna",
|
||||
"name":"Croatian Kuna",
|
||||
"code":"HRK",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -371,7 +371,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Djibouti Franc",
|
||||
"name":"Djiboutian Franc",
|
||||
"code":"DJF",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
@ -392,14 +392,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"El Salvador Colon",
|
||||
"name":"Salvadoran Colon",
|
||||
"code":"SVC",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Nakfa",
|
||||
"name":"Eritrean Nakfa",
|
||||
"code":"ERN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -420,7 +420,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Fiji Dollar",
|
||||
"name":"Fijian Dollar",
|
||||
"code":"FJD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -434,21 +434,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Dalasi",
|
||||
"name":"Gambian Dalasi",
|
||||
"code":"GMD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Lari",
|
||||
"name":"Georgian Lari",
|
||||
"code":"GEL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Ghana Cedi",
|
||||
"name":"Ghanaian Cedi",
|
||||
"code":"GHS",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -462,7 +462,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Quetzal",
|
||||
"name":"Guatemalan Quetzal",
|
||||
"code":"GTQ",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -476,28 +476,28 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Guinea Franc",
|
||||
"name":"Guinean Franc",
|
||||
"code":"GNF",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Guyana Dollar",
|
||||
"name":"Guyanese Dollar",
|
||||
"code":"GYD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Gourde",
|
||||
"name":"Haitian Gourde",
|
||||
"code":"HTG",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Lempira",
|
||||
"name":"Honduran Lempira",
|
||||
"code":"HNL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -511,21 +511,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Forint",
|
||||
"name":"Hungarian Forint",
|
||||
"code":"HUF",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Iceland Krona",
|
||||
"name":"Icelandic Krona",
|
||||
"code":"ISK",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Rupiah",
|
||||
"name":"Indonesian Rupiah",
|
||||
"code":"IDR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -546,7 +546,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"New Israeli Sheqel",
|
||||
"name":"New Israeli Shekel",
|
||||
"code":"ILS",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -560,7 +560,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Yen",
|
||||
"name":"Japanese Yen",
|
||||
"code":"JPY",
|
||||
"divisibility":0,
|
||||
"symbol":"¥",
|
||||
@ -574,7 +574,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Tenge",
|
||||
"name":"Kazakhstani Tenge",
|
||||
"code":"KZT",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -595,7 +595,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Won",
|
||||
"name":"South Korean Won",
|
||||
"code":"KRW",
|
||||
"divisibility":0,
|
||||
"symbol":"₩",
|
||||
@ -609,14 +609,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Som",
|
||||
"name":"Kyrgyzstani Som",
|
||||
"code":"KGS",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Kip",
|
||||
"name":"Lao Kip",
|
||||
"code":"LAK",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -630,14 +630,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Loti",
|
||||
"name":"Lesotho Loti",
|
||||
"code":"LSL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Rand",
|
||||
"name":"South African Rand",
|
||||
"code":"ZAR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -665,14 +665,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Pataca",
|
||||
"name":"Macanese Pataca",
|
||||
"code":"MOP",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Denar",
|
||||
"name":"Macedonian Denar",
|
||||
"code":"MKD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -686,7 +686,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Malawi Kwacha",
|
||||
"name":"Malawian Kwacha",
|
||||
"code":"MWK",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -700,21 +700,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Rufiyaa",
|
||||
"name":"Maldivian Rufiyaa",
|
||||
"code":"MVR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Ouguiya",
|
||||
"name":"Mauritanian Ouguiya",
|
||||
"code":"MRO",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Mauritius Rupee",
|
||||
"name":"Mauritian Rupee",
|
||||
"code":"MUR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -742,7 +742,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Tugrik",
|
||||
"name":"Mongolian Tugrik",
|
||||
"code":"MNT",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -756,21 +756,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Mozambique Metical",
|
||||
"name":"Mozambican Metical",
|
||||
"code":"MZN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Kyat",
|
||||
"name":"Myanmar Kyat",
|
||||
"code":"MMK",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Namibia Dollar",
|
||||
"name":"Namibian dollar",
|
||||
"code":"NAD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -784,56 +784,56 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Cordoba Oro",
|
||||
"name":"Nicaraguan Cordoba",
|
||||
"code":"NIO",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Naira",
|
||||
"name":"Nigerian Naira",
|
||||
"code":"NGN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Rial Omani",
|
||||
"name":"Omani Rial",
|
||||
"code":"OMR",
|
||||
"divisibility":3,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Pakistan Rupee",
|
||||
"name":"Pakistani Rupee",
|
||||
"code":"PKR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Balboa",
|
||||
"name":"Panamanian Balboa",
|
||||
"code":"PAB",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Kina",
|
||||
"name":"Papua New Guinean Kina",
|
||||
"code":"PGK",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Guarani",
|
||||
"name":"Paraguayan Guarani",
|
||||
"code":"PYG",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Sol",
|
||||
"name":"Peruvian Sol",
|
||||
"code":"PEN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -847,7 +847,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Zloty",
|
||||
"name":"Polish Zloty",
|
||||
"code":"PLN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -875,7 +875,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Rwanda Franc",
|
||||
"name":"Rwandan Franc",
|
||||
"code":"RWF",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
@ -889,14 +889,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Tala",
|
||||
"name":"Samoan Tala",
|
||||
"code":"WST",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Dobra",
|
||||
"name":"São Tomé and Príncipe sDobra",
|
||||
"code":"STD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -917,14 +917,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Seychelles Rupee",
|
||||
"name":"Seychellois Rupee",
|
||||
"code":"SCR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Leone",
|
||||
"name":"Sierra Leonean Leone",
|
||||
"code":"SLL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -959,7 +959,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Sri Lanka Rupee",
|
||||
"name":"Sri Lankan Rupee",
|
||||
"code":"LKR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -973,14 +973,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Surinam Dollar",
|
||||
"name":"Surinamese Dollar",
|
||||
"code":"SRD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Lilangeni",
|
||||
"name":"Swazi Lilangeni",
|
||||
"code":"SZL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -1022,7 +1022,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Somoni",
|
||||
"name":"Tajikistani Somoni",
|
||||
"code":"TJS",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -1036,14 +1036,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Baht",
|
||||
"name":"Thai Baht",
|
||||
"code":"THB",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Pa’anga",
|
||||
"name":"Tongan paʻanga",
|
||||
"code":"TOP",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -1071,21 +1071,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Turkmenistan New Manat",
|
||||
"name":"Turkmenistani Manat",
|
||||
"code":"TMT",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Uganda Shilling",
|
||||
"name":"Ugandan Shilling",
|
||||
"code":"UGX",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Hryvnia",
|
||||
"name":"Ukrainian Hryvnia",
|
||||
"code":"UAH",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -1106,7 +1106,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Peso Uruguayo",
|
||||
"name":"Uruguayan Peso",
|
||||
"code":"UYU",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -1120,28 +1120,28 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Uzbekistan Sum",
|
||||
"name":"Uzbekistani Sum",
|
||||
"code":"UZS",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Vatu",
|
||||
"name":"Vanuatu Vatu",
|
||||
"code":"VUV",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Bolívar",
|
||||
"name":"Venezuelan Bolívar",
|
||||
"code":"VEF",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Dong",
|
||||
"name":"Vietnamese Dong",
|
||||
"code":"VND",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
@ -1162,7 +1162,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Zimbabwe Dollar",
|
||||
"name":"Zimbabwean Dollar",
|
||||
"code":"ZWL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
|
37
BTCPayServer.Rating/Providers/BTCTurkRateProvider.cs
Normal file
37
BTCPayServer.Rating/Providers/BTCTurkRateProvider.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Rating.Providers
|
||||
{
|
||||
public class BtcTurkRateProvider : IRateProvider
|
||||
{
|
||||
class Ticker
|
||||
{
|
||||
public string pairNormalized { get; set; }
|
||||
public decimal? bid { get; set; }
|
||||
public decimal? ask { get; set; }
|
||||
}
|
||||
private readonly HttpClient _httpClient;
|
||||
public BtcTurkRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync("https://api.btcturk.com/api/v2/ticker", cancellationToken);
|
||||
var jarray = (JArray)(await response.Content.ReadAsAsync<JObject>(cancellationToken))["data"];
|
||||
var tickers = jarray.ToObject<Ticker[]>();
|
||||
return tickers
|
||||
.Where(t => t.bid is not null && t.ask is not null)
|
||||
.Select(t => new PairRate(CurrencyPair.Parse(t.pairNormalized), new BidAsk(t.bid.Value, t.ask.Value))).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
@ -28,6 +28,7 @@ namespace BTCPayServer.Services.Rates
|
||||
$"BitBank Rates API Error: {errorCode}. See https://github.com/bitbankinc/bitbank-api-docs/blob/master/errors.md for more details.");
|
||||
}
|
||||
return ((data as JArray) ?? new JArray())
|
||||
.Where(p => p["buy"].Type != JTokenType.Null && p["sell"].Type != JTokenType.Null)
|
||||
.Select(item => new PairRate(CurrencyPair.Parse(item["pair"].ToString()), CreateBidAsk(item as JObject)))
|
||||
.ToArray();
|
||||
}
|
||||
|
@ -31,8 +31,7 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
|
||||
string name = ((JProperty)item).Name;
|
||||
int value = results[name].Value<int>();
|
||||
|
||||
var value = results[name].Value<decimal>();
|
||||
list.Add(new PairRate(new CurrencyPair("BTC", name), new BidAsk(value)));
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Rating.Providers;
|
||||
using ExchangeSharp;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
@ -78,6 +79,7 @@ namespace BTCPayServer.Services.Rates
|
||||
yield return new AvailableRateProvider("bitpay", "Bitpay", "https://bitpay.com/rates");
|
||||
yield return new AvailableRateProvider("ripio", "Ripio", "https://api.exchange.ripio.com/api/v1/rate/all/");
|
||||
yield return new AvailableRateProvider("cryptomarket", "CryptoMarket", "https://api.exchange.cryptomkt.com/api/3/public/ticker/");
|
||||
yield return new AvailableRateProvider("btcturk", "BtcTurk", "https://api.btcturk.com/api/v2/ticker");
|
||||
|
||||
yield return new AvailableRateProvider("bitfinex", "Bitfinex", "https://api.bitfinex.com/v2/tickers?symbols=tBTCUSD,tLTCUSD,tLTCBTC,tETHUSD,tETHBTC,tETCBTC,tETCUSD,tRRTUSD,tRRTBTC,tZECUSD,tZECBTC,tXMRUSD,tXMRBTC,tDSHUSD,tDSHBTC,tBTCEUR,tBTCJPY,tXRPUSD,tXRPBTC,tIOTUSD,tIOTBTC,tIOTETH,tEOSUSD,tEOSBTC,tEOSETH,tSANUSD,tSANBTC,tSANETH,tOMGUSD,tOMGBTC,tOMGETH,tNEOUSD,tNEOBTC,tNEOETH,tETPUSD,tETPBTC,tETPETH,tQTMUSD,tQTMBTC,tQTMETH,tAVTUSD,tAVTBTC,tAVTETH,tEDOUSD,tEDOBTC,tEDOETH,tBTGUSD,tBTGBTC,tDATUSD,tDATBTC,tDATETH,tQSHUSD,tQSHBTC,tQSHETH,tYYWUSD,tYYWBTC,tYYWETH,tGNTUSD,tGNTBTC,tGNTETH,tSNTUSD,tSNTBTC,tSNTETH,tIOTEUR,tBATUSD,tBATBTC,tBATETH,tMNAUSD,tMNABTC,tMNAETH,tFUNUSD,tFUNBTC,tFUNETH,tZRXUSD,tZRXBTC,tZRXETH,tTNBUSD,tTNBBTC,tTNBETH,tSPKUSD,tSPKBTC,tSPKETH,tTRXUSD,tTRXBTC,tTRXETH,tRCNUSD,tRCNBTC,tRCNETH,tRLCUSD,tRLCBTC,tRLCETH,tAIDUSD,tAIDBTC,tAIDETH,tSNGUSD,tSNGBTC,tSNGETH,tREPUSD,tREPBTC,tREPETH,tELFUSD,tELFBTC,tELFETH,tNECUSD,tNECBTC,tNECETH,tBTCGBP,tETHEUR,tETHJPY,tETHGBP,tNEOEUR,tNEOJPY,tNEOGBP,tEOSEUR,tEOSJPY,tEOSGBP,tIOTJPY,tIOTGBP,tIOSUSD,tIOSBTC,tIOSETH,tAIOUSD,tAIOBTC,tAIOETH,tREQUSD,tREQBTC,tREQETH,tRDNUSD,tRDNBTC,tRDNETH,tLRCUSD,tLRCBTC,tLRCETH,tWAXUSD,tWAXBTC,tWAXETH,tDAIUSD,tDAIBTC,tDAIETH,tAGIUSD,tAGIBTC,tAGIETH,tBFTUSD,tBFTBTC,tBFTETH,tMTNUSD,tMTNBTC,tMTNETH,tODEUSD,tODEBTC,tODEETH,tANTUSD,tANTBTC,tANTETH,tDTHUSD,tDTHBTC,tDTHETH,tMITUSD,tMITBTC,tMITETH,tSTJUSD,tSTJBTC,tSTJETH,tXLMUSD,tXLMEUR,tXLMJPY,tXLMGBP,tXLMBTC,tXLMETH,tXVGUSD,tXVGEUR,tXVGJPY,tXVGGBP,tXVGBTC,tXVGETH,tBCIUSD,tBCIBTC,tMKRUSD,tMKRBTC,tMKRETH,tKNCUSD,tKNCBTC,tKNCETH,tPOAUSD,tPOABTC,tPOAETH,tEVTUSD,tLYMUSD,tLYMBTC,tLYMETH,tUTKUSD,tUTKBTC,tUTKETH,tVEEUSD,tVEEBTC,tVEEETH,tDADUSD,tDADBTC,tDADETH,tORSUSD,tORSBTC,tORSETH,tAUCUSD,tAUCBTC,tAUCETH,tPOYUSD,tPOYBTC,tPOYETH,tFSNUSD,tFSNBTC,tFSNETH,tCBTUSD,tCBTBTC,tCBTETH,tZCNUSD,tZCNBTC,tZCNETH,tSENUSD,tSENBTC,tSENETH,tNCAUSD,tNCABTC,tNCAETH,tCNDUSD,tCNDBTC,tCNDETH,tCTXUSD,tCTXBTC,tCTXETH,tPAIUSD,tPAIBTC,tSEEUSD,tSEEBTC,tSEEETH,tESSUSD,tESSBTC,tESSETH,tATMUSD,tATMBTC,tATMETH,tHOTUSD,tHOTBTC,tHOTETH,tDTAUSD,tDTABTC,tDTAETH,tIQXUSD,tIQXBTC,tIQXEOS,tWPRUSD,tWPRBTC,tWPRETH,tZILUSD,tZILBTC,tZILETH,tBNTUSD,tBNTBTC,tBNTETH,tABSUSD,tABSETH,tXRAUSD,tXRAETH,tMANUSD,tMANETH,tBBNUSD,tBBNETH,tNIOUSD,tNIOETH,tDGXUSD,tDGXETH,tVETUSD,tVETBTC,tVETETH,tUTNUSD,tUTNETH,tTKNUSD,tTKNETH,tGOTUSD,tGOTEUR,tGOTETH,tXTZUSD,tXTZBTC,tCNNUSD,tCNNETH,tBOXUSD,tBOXETH,tTRXEUR,tTRXGBP,tTRXJPY,tMGOUSD,tMGOETH,tRTEUSD,tRTEETH,tYGGUSD,tYGGETH,tMLNUSD,tMLNETH,tWTCUSD,tWTCETH,tCSXUSD,tCSXETH,tOMNUSD,tOMNBTC,tINTUSD,tINTETH,tDRNUSD,tDRNETH,tPNKUSD,tPNKETH,tDGBUSD,tDGBBTC,tBSVUSD,tBSVBTC,tBABUSD,tBABBTC,tWLOUSD,tWLOXLM,tVLDUSD,tVLDETH,tENJUSD,tENJETH,tONLUSD,tONLETH,tRBTUSD,tRBTBTC,tUSTUSD,tEUTEUR,tEUTUSD,tGSDUSD,tUDCUSD,tTSDUSD,tPAXUSD,tRIFUSD,tRIFBTC,tPASUSD,tPASETH,tVSYUSD,tVSYBTC,tZRXDAI,tMKRDAI,tOMGDAI,tBTTUSD,tBTTBTC,tBTCUST,tETHUST,tCLOUSD,tCLOBTC,tIMPUSD,tIMPETH,tLTCUST,tEOSUST,tBABUST,tSCRUSD,tSCRETH,tGNOUSD,tGNOETH,tGENUSD,tGENETH,tATOUSD,tATOBTC,tATOETH,tWBTUSD,tXCHUSD,tEUSUSD,tWBTETH,tXCHETH,tEUSETH,tLEOUSD,tLEOBTC,tLEOUST,tLEOEOS,tLEOETH,tASTUSD,tASTETH,tFOAUSD,tFOAETH,tUFRUSD,tUFRETH,tZBTUSD,tZBTUST,tOKBUSD,tUSKUSD,tGTXUSD,tKANUSD,tOKBUST,tOKBETH,tOKBBTC,tUSKUST,tUSKETH,tUSKBTC,tUSKEOS,tGTXUST,tKANUST,tAMPUSD,tALGUSD,tALGBTC,tALGUST,tBTCXCH,tSWMUSD,tSWMETH,tTRIUSD,tTRIETH,tLOOUSD,tLOOETH,tAMPUST,tDUSK:USD,tDUSK:BTC,tUOSUSD,tUOSBTC,tRRBUSD,tRRBUST,tDTXUSD,tDTXUST,tAMPBTC,tFTTUSD,tFTTUST,tPAXUST,tUDCUST,tTSDUST,tBTC:CNHT,tUST:CNHT,tCNH:CNHT,tCHZUSD,tCHZUST,tBTCF0:USTF0,tETHF0:USTF0");
|
||||
yield return new AvailableRateProvider("okex", "OKEx", "https://www.okex.com/api/futures/v3/instruments/ticker");
|
||||
@ -106,6 +108,7 @@ namespace BTCPayServer.Services.Rates
|
||||
Providers.Add("cryptomarket", new CryptoMarketExchangeRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_CRYPTOMARKET")));
|
||||
Providers.Add("bitflyer", new BitflyerRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITFLYER")));
|
||||
Providers.Add("yadio", new YadioRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_YADIO")));
|
||||
Providers.Add("btcturk", new BtcTurkRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BTCTURK")));
|
||||
// Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM")));
|
||||
|
||||
|
||||
|
@ -171,8 +171,9 @@ namespace BTCPayServer.Tests
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
.OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
DerivationSchemeSettings.TryParseFromWalletFile(content, onchainBTC.Network, out var expected);
|
||||
DerivationSchemeSettings.TryParseFromWalletFile(content, onchainBTC.Network, out var expected, out var error);
|
||||
Assert.Equal(expected.ToJson(), onchainBTC.ToJson());
|
||||
Assert.Null(error);
|
||||
|
||||
// Let's check that the root hdkey and account key path are taken into account when making a PSBT
|
||||
invoice = await user.BitPay.CreateInvoiceAsync(
|
||||
|
@ -23,7 +23,7 @@
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
|
||||
<PackageReference Include="Selenium.Support" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="105.0.5195.5200" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="108.0.5359.7100" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -46,6 +46,9 @@ namespace BTCPayServer.Tests
|
||||
var invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
|
||||
// Ensure we are seeing Checkout v2
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
|
||||
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
|
||||
Assert.Contains("Lightning", s.Driver.WaitForElement(By.CssSelector(".payment-method.active")).Text);
|
||||
Assert.DoesNotContain("LNURL", s.Driver.PageSource);
|
||||
@ -151,6 +154,15 @@ namespace BTCPayServer.Tests
|
||||
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);
|
||||
@ -186,10 +198,13 @@ namespace BTCPayServer.Tests
|
||||
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);
|
||||
});
|
||||
s.Driver.WaitUntilAvailable(By.Name("btcpay"));
|
||||
|
||||
var frameElement = s.Driver.FindElement(By.Name("btcpay"));
|
||||
Assert.True(frameElement.Displayed);
|
||||
var iframe = s.Driver.SwitchTo().Frame(frameElement);
|
||||
iframe.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
|
||||
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(invoice
|
||||
.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike))
|
||||
.GetPaymentMethodDetails().GetPaymentDestination(), Network.RegTest),
|
||||
@ -198,8 +213,6 @@ namespace BTCPayServer.Tests
|
||||
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);
|
||||
});
|
||||
|
@ -2,8 +2,11 @@ using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using OpenQA.Selenium;
|
||||
@ -15,6 +18,10 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static Task<KeyPathInformation> ReserveAddressAsync(this BTCPayWallet wallet, DerivationStrategyBase derivationStrategyBase)
|
||||
{
|
||||
return wallet.ReserveAddressAsync(null, derivationStrategyBase, "test");
|
||||
}
|
||||
private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
|
||||
public static string ToJson(this object o) => JsonConvert.SerializeObject(o, Formatting.None, JsonSettings);
|
||||
|
||||
|
@ -645,6 +645,49 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(4, tor.Services.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseDerivationSchemes()
|
||||
{
|
||||
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||
var parser = new DerivationSchemeParser(networkProvider.BTC);
|
||||
|
||||
// xpub
|
||||
var xpub = "xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw";
|
||||
DerivationStrategyBase strategyBase = parser.Parse(xpub);
|
||||
Assert.IsType<DirectDerivationStrategy>(strategyBase);
|
||||
Assert.True(((DirectDerivationStrategy)strategyBase).Segwit);
|
||||
Assert.Equal("tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS", strategyBase.ToString());
|
||||
|
||||
// Multisig
|
||||
var multisig = "wsh(sortedmulti(2,[62a7956f/84'/1'/0']tpubDDXgATYzdQkHHhZZCMcNJj8BGDENvzMVou5v9NdxiP4rxDLj33nS233dGFW4htpVZSJ6zds9eVqAV9RyRHHiKtwQKX8eR4n4KN3Dwmj7A3h/0/*,[11312aa2/84'/1'/0']tpubDC8a54NFtQtMQAZ97VhoU9V6jVTvi9w4Y5SaAXJSBYETKg3AoX5CCKndznhPWxJUBToPCpT44s86QbKdGpKAnSjcMTGW4kE6UQ8vpBjcybW/0/*,[8f71b834/84'/1'/0']tpubDChjnP9LXNrJp43biqjY7FH93wgRRNrNxB4Q8pH7PPRy8UPcH2S6V46WGVJ47zVGF7SyBJNCpnaogsFbsybVQckGtVhCkng3EtFn8qmxptS/0/*))";
|
||||
var expected = "2-of-tpubDDXgATYzdQkHHhZZCMcNJj8BGDENvzMVou5v9NdxiP4rxDLj33nS233dGFW4htpVZSJ6zds9eVqAV9RyRHHiKtwQKX8eR4n4KN3Dwmj7A3h-tpubDC8a54NFtQtMQAZ97VhoU9V6jVTvi9w4Y5SaAXJSBYETKg3AoX5CCKndznhPWxJUBToPCpT44s86QbKdGpKAnSjcMTGW4kE6UQ8vpBjcybW-tpubDChjnP9LXNrJp43biqjY7FH93wgRRNrNxB4Q8pH7PPRy8UPcH2S6V46WGVJ47zVGF7SyBJNCpnaogsFbsybVQckGtVhCkng3EtFn8qmxptS";
|
||||
(strategyBase, RootedKeyPath[] rootedKeyPath) = parser.ParseOutputDescriptor(multisig);
|
||||
Assert.Equal(3, rootedKeyPath.Length);
|
||||
Assert.IsType<P2WSHDerivationStrategy>(strategyBase);
|
||||
Assert.IsType<MultisigDerivationStrategy>(((P2WSHDerivationStrategy)strategyBase).Inner);
|
||||
Assert.Equal(expected, strategyBase.ToString());
|
||||
|
||||
var inner = (MultisigDerivationStrategy)((P2WSHDerivationStrategy)strategyBase).Inner;
|
||||
Assert.False(inner.IsLegacy);
|
||||
Assert.Equal(3, inner.Keys.Count);
|
||||
Assert.Equal(2, inner.RequiredSignatures);
|
||||
Assert.Equal(expected, inner.ToString());
|
||||
|
||||
// Output Descriptor
|
||||
networkProvider = new BTCPayNetworkProvider(ChainName.Mainnet);
|
||||
parser = new DerivationSchemeParser(networkProvider.BTC);
|
||||
var od = "wpkh([8bafd160/49h/0h/0h]xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw/0/*)#9x4vkw48";
|
||||
(strategyBase, rootedKeyPath) = parser.ParseOutputDescriptor(od);
|
||||
Assert.Single(rootedKeyPath);
|
||||
Assert.IsType<DirectDerivationStrategy>(strategyBase);
|
||||
Assert.True(((DirectDerivationStrategy)strategyBase).Segwit);
|
||||
|
||||
// Failure cases
|
||||
Assert.Throws<FormatException>(() => { parser.Parse("xpub 661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); }); // invalid format because of space
|
||||
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("invalid"); }); // invalid in general
|
||||
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseDerivationSchemeSettings()
|
||||
{
|
||||
@ -656,7 +699,8 @@ namespace BTCPayServer.Tests
|
||||
// ColdCard
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
mainnet, out var settings));
|
||||
mainnet, out var settings, out var error));
|
||||
Assert.Null(error);
|
||||
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint);
|
||||
Assert.Equal(settings.AccountKeySettings[0].RootFingerprint,
|
||||
HDFingerprint.TryParse("8bafd160", out var hd) ? hd : default);
|
||||
@ -672,30 +716,41 @@ namespace BTCPayServer.Tests
|
||||
// Should be legacy
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings));
|
||||
testnet, out settings, out error));
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit);
|
||||
Assert.Null(error);
|
||||
|
||||
// Should be segwit p2sh
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings));
|
||||
testnet, out settings, out error));
|
||||
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p &&
|
||||
p.Inner is DirectDerivationStrategy s2 && s2.Segwit);
|
||||
Assert.Null(error);
|
||||
|
||||
// Should be segwit
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings));
|
||||
testnet, out settings, out error));
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit);
|
||||
Assert.Null(error);
|
||||
|
||||
// Specter
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"label\": \"Specter\", \"blockheight\": 123456, \"descriptor\": \"wpkh([8bafd160/49h/0h/0h]xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw/0/*)#9x4vkw48\"}",
|
||||
mainnet, out var specter));
|
||||
mainnet, out var specter, out error));
|
||||
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), specter.AccountKeySettings[0].RootFingerprint);
|
||||
Assert.Equal(specter.AccountKeySettings[0].RootFingerprint, hd);
|
||||
Assert.Equal("49'/0'/0'", specter.AccountKeySettings[0].AccountKeyPath.ToString());
|
||||
Assert.Equal("Specter", specter.Label);
|
||||
Assert.Null(error);
|
||||
|
||||
// Failure case
|
||||
Assert.False(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubFailure\", \"xpub\": \"tpubFailure\", \"label\": \"Failure\"}, \"wallet_type\": \"standard\"}",
|
||||
testnet, out settings, out error));
|
||||
Assert.Null(settings);
|
||||
Assert.NotNull(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -1749,8 +1804,7 @@ namespace BTCPayServer.Tests
|
||||
PaymentMethod = new PaymentMethodId("BTC", PaymentTypes.BTCLike)
|
||||
}
|
||||
};
|
||||
var newBlob = Encoding.UTF8.GetBytes(
|
||||
new Serializer(null).ToString(blob).Replace( "paymentMethod\":\"BTC\"","paymentMethod\":\"ETH_ZYC\""));
|
||||
var newBlob = new Serializer(null).ToString(blob).Replace( "paymentMethod\":\"BTC\"","paymentMethod\":\"ETH_ZYC\"");
|
||||
Assert.Empty(StoreDataExtensions.GetStoreBlob(new StoreData() {StoreBlob = newBlob}).PaymentMethodCriteria);
|
||||
}
|
||||
}
|
||||
|
@ -1409,6 +1409,12 @@ namespace BTCPayServer.Tests
|
||||
var invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest());
|
||||
await Pay(invoiceData.Id);
|
||||
|
||||
// Can't update amount once invoice has been created
|
||||
await AssertValidationError(new[] { "Amount" }, () => client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
|
||||
{
|
||||
Amount = 294m
|
||||
}));
|
||||
|
||||
// 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" });
|
||||
@ -1561,6 +1567,127 @@ 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()
|
||||
@ -1599,13 +1726,11 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
RedirectAutomatically = true,
|
||||
RequiresRefundEmail = true,
|
||||
CheckoutFormId = GenericFormOption.Email.ToString()
|
||||
},
|
||||
AdditionalSearchTerms = new string[] { "Banana" }
|
||||
});
|
||||
Assert.True(newInvoice.Checkout.RedirectAutomatically);
|
||||
Assert.True(newInvoice.Checkout.RequiresRefundEmail);
|
||||
Assert.Equal(GenericFormOption.Email.ToString(), newInvoice.Checkout.CheckoutFormId);
|
||||
Assert.Equal(user.StoreId, newInvoice.StoreId);
|
||||
//list
|
||||
var invoices = await viewOnly.GetInvoices(user.StoreId);
|
||||
@ -1816,6 +1941,9 @@ namespace BTCPayServer.Tests
|
||||
RedirectURL = "http://toto.com/lol"
|
||||
}
|
||||
});
|
||||
var invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", newInvoice.Id), false);
|
||||
Assert.Contains(invoiceObject.Links.Select(l => l.Type), t => t == "address");
|
||||
|
||||
Assert.EndsWith($"/i/{newInvoice.Id}", newInvoice.CheckoutLink);
|
||||
var controller = tester.PayTester.GetController<UIInvoiceController>(user.UserId, user.StoreId);
|
||||
var model = (PaymentModel)((ViewResult)await controller.Checkout(newInvoice.Id)).Model;
|
||||
@ -1849,11 +1977,18 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(store.LazyPaymentMethods);
|
||||
|
||||
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = 1, Currency = "USD" });
|
||||
invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", invoice.Id), false);
|
||||
Assert.DoesNotContain(invoiceObject.Links.Select(l => l.Type), t => t == "address");
|
||||
|
||||
|
||||
paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id);
|
||||
Assert.Single(paymentMethods);
|
||||
Assert.False(paymentMethods.First().Activated);
|
||||
await client.ActivateInvoicePaymentMethod(user.StoreId, invoice.Id,
|
||||
paymentMethods.First().PaymentMethod);
|
||||
invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", invoice.Id), false);
|
||||
Assert.Contains(invoiceObject.Links.Select(l => l.Type), t => t == "address");
|
||||
|
||||
paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id);
|
||||
Assert.Single(paymentMethods);
|
||||
Assert.True(paymentMethods.First().Activated);
|
||||
@ -1911,11 +2046,15 @@ namespace BTCPayServer.Tests
|
||||
BitcoinAddress.Create(pm.Destination, tester.ExplorerClient.Network.NBitcoinNetwork),
|
||||
new Money(0.0002m, MoneyUnit.BTC));
|
||||
});
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id));
|
||||
Assert.Single(pm.Payments);
|
||||
Assert.Equal(-0.0001m, pm.Due);
|
||||
|
||||
invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", invoice.Id), false);
|
||||
Assert.Contains(invoiceObject.Links.Select(l => l.Type), t => t == "tx");
|
||||
});
|
||||
}
|
||||
|
||||
@ -2064,6 +2203,49 @@ namespace BTCPayServer.Tests
|
||||
await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC"));
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 20 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
public async Task CanUseLightningAPI2()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
tester.ActivateLightning();
|
||||
await tester.StartAsync();
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync(true);
|
||||
|
||||
var types = new[] { LightningConnectionType.LndREST, LightningConnectionType.CLightning };
|
||||
foreach (var type in types)
|
||||
{
|
||||
user.RegisterLightningNode("BTC", type);
|
||||
var client = await user.CreateClient("btcpay.store.cancreatelightninginvoice");
|
||||
var amount = LightMoney.Satoshis(1000);
|
||||
var expiry = TimeSpan.FromSeconds(600);
|
||||
|
||||
var invoice = await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest
|
||||
{
|
||||
Amount = amount,
|
||||
Expiry = expiry,
|
||||
Description = "Hashed description",
|
||||
DescriptionHashOnly = true
|
||||
});
|
||||
var bolt11 = BOLT11PaymentRequest.Parse(invoice.BOLT11, Network.RegTest);
|
||||
Assert.NotNull(bolt11.DescriptionHash);
|
||||
Assert.Null(bolt11.ShortDescription);
|
||||
|
||||
invoice = await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest
|
||||
{
|
||||
Amount = amount,
|
||||
Expiry = expiry,
|
||||
Description = "Standard description",
|
||||
});
|
||||
bolt11 = BOLT11PaymentRequest.Parse(invoice.BOLT11, Network.RegTest);
|
||||
Assert.Null(bolt11.DescriptionHash);
|
||||
Assert.NotNull(bolt11.ShortDescription);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task NotificationAPITests()
|
||||
@ -2801,8 +2983,7 @@ namespace BTCPayServer.Tests
|
||||
var newUserClient = await newUser.CreateClient(Policies.Unrestricted);
|
||||
Assert.False((await newUserClient.GetCurrentUser()).Disabled);
|
||||
|
||||
await adminClient.LockUser(newUser.UserId, true, CancellationToken.None);
|
||||
|
||||
Assert.True(await adminClient.LockUser(newUser.UserId, true, CancellationToken.None));
|
||||
Assert.True((await adminClient.GetUserByIdOrEmail(newUser.UserId)).Disabled);
|
||||
await AssertAPIError("unauthenticated",async () =>
|
||||
{
|
||||
@ -2815,12 +2996,12 @@ namespace BTCPayServer.Tests
|
||||
await newUserBasicClient.GetCurrentUser();
|
||||
});
|
||||
|
||||
await adminClient.LockUser(newUser.UserId, false, CancellationToken.None);
|
||||
Assert.True(await adminClient.LockUser(newUser.UserId, false, CancellationToken.None));
|
||||
Assert.False((await adminClient.GetUserByIdOrEmail(newUser.UserId)).Disabled);
|
||||
await newUserClient.GetCurrentUser();
|
||||
await newUserBasicClient.GetCurrentUser();
|
||||
// Twice for good measure
|
||||
await adminClient.LockUser(newUser.UserId, false, CancellationToken.None);
|
||||
Assert.True(await adminClient.LockUser(newUser.UserId, false, CancellationToken.None));
|
||||
Assert.False((await adminClient.GetUserByIdOrEmail(newUser.UserId)).Disabled);
|
||||
await newUserClient.GetCurrentUser();
|
||||
await newUserBasicClient.GetCurrentUser();
|
||||
@ -3075,7 +3256,7 @@ namespace BTCPayServer.Tests
|
||||
// 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 allObjects = await repo.GetWalletObjects(new(wid) { 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 }));
|
||||
|
@ -53,7 +53,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Permission guard for guests editing
|
||||
Assert
|
||||
.IsType<NotFoundResult>(guestpaymentRequestController.EditPaymentRequest(user.StoreId, id));
|
||||
.IsType<NotFoundResult>(await guestpaymentRequestController.EditPaymentRequest(user.StoreId, id));
|
||||
|
||||
request.Title = "update";
|
||||
Assert.IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(id, request));
|
||||
|
@ -176,6 +176,7 @@ namespace BTCPayServer.Tests
|
||||
var name = "Store" + RandomUtils.GetUInt64();
|
||||
TestLogs.LogInformation($"Created store {name}");
|
||||
Driver.WaitForElement(By.Id("Name")).SendKeys(name);
|
||||
new SelectElement(Driver.FindElement(By.Id("PreferredExchange"))).SelectByText("CoinGecko");
|
||||
Driver.WaitForElement(By.Id("Create")).Click();
|
||||
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();
|
||||
Driver.FindElement(By.Id($"SectionNav-{StoreNavPages.General.ToString()}")).Click();
|
||||
@ -191,7 +192,9 @@ namespace BTCPayServer.Tests
|
||||
Driver.SetCheckbox(By.Id("UseNewCheckout"), true);
|
||||
Driver.WaitForElement(By.Id("OnChainWithLnInvoiceFallback"));
|
||||
Driver.SetCheckbox(By.Id("OnChainWithLnInvoiceFallback"), bip21);
|
||||
Driver.FindElement(By.Id("Save")).Click();
|
||||
Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);
|
||||
Assert.Contains("Store successfully updated", FindAlertMessage().Text);
|
||||
Assert.True(Driver.FindElement(By.Id("UseNewCheckout")).Selected);
|
||||
}
|
||||
|
||||
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool? importkeys = null, bool isHotWallet = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit)
|
||||
|
@ -64,6 +64,61 @@ 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()
|
||||
{
|
||||
@ -887,6 +942,11 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("ClearExpiryDate")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click();
|
||||
|
||||
// amount and currency should be editable, because no invoice exists
|
||||
s.GoToUrl(editUrl);
|
||||
Assert.True(s.Driver.FindElement(By.Id("Amount")).Enabled);
|
||||
Assert.True(s.Driver.FindElement(By.Id("Currency")).Enabled);
|
||||
|
||||
s.GoToUrl(viewUrl);
|
||||
s.Driver.AssertElementNotFound(By.CssSelector("[data-test='status']"));
|
||||
@ -898,8 +958,12 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.WaitForElement(By.CssSelector("invoice"));
|
||||
Assert.Contains("Awaiting Payment", s.Driver.PageSource);
|
||||
|
||||
// archive (from details page)
|
||||
// amount and currency should not be editable, because invoice exists
|
||||
s.GoToUrl(editUrl);
|
||||
Assert.False(s.Driver.FindElement(By.Id("Amount")).Enabled);
|
||||
Assert.False(s.Driver.FindElement(By.Id("Currency")).Enabled);
|
||||
|
||||
// archive (from details page)
|
||||
var payReqId = s.Driver.Url.Split('/').Last();
|
||||
s.Driver.FindElement(By.Id("ArchivePaymentRequest")).Click();
|
||||
Assert.Contains("The payment request has been archived", s.FindAlertMessage().Text);
|
||||
|
@ -174,7 +174,7 @@ namespace BTCPayServer.Tests
|
||||
await RegisterAsync();
|
||||
}
|
||||
var store = GetController<UIUserStoresController>();
|
||||
await store.CreateStore(new CreateStoreViewModel { Name = "Test Store" });
|
||||
await store.CreateStore(new CreateStoreViewModel { Name = "Test Store", PreferredExchange = "coingecko" });
|
||||
StoreId = store.CreatedStoreId;
|
||||
parent.Stores.Add(StoreId);
|
||||
}
|
||||
|
@ -235,15 +235,22 @@ namespace BTCPayServer.Tests
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0");
|
||||
using var cts = new CancellationTokenSource(5_000);
|
||||
var response = await httpClient.SendAsync(request, cts.Token);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
if (uri.Fragment.Length != 0)
|
||||
if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
var fragment = uri.Fragment.Substring(1);
|
||||
var contents = await response.Content.ReadAsStringAsync();
|
||||
Assert.Matches($"id=\"{fragment}\"", contents);
|
||||
TestLogs.LogInformation($"TooManyRequests, skipping: {url} ({file})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
if (uri.Fragment.Length != 0)
|
||||
{
|
||||
var fragment = uri.Fragment.Substring(1);
|
||||
var contents = await response.Content.ReadAsStringAsync();
|
||||
Assert.Matches($"id=\"{fragment}\"", contents);
|
||||
}
|
||||
|
||||
TestLogs.LogInformation($"OK: {url} ({file})");
|
||||
TestLogs.LogInformation($"OK: {url} ({file})");
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is MatchesException)
|
||||
{
|
||||
|
@ -71,7 +71,7 @@ services:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:23.0-1
|
||||
image: btcpayserver/bitcoin:24.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.54
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -126,7 +126,7 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:23.0-1
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -154,7 +154,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v0.10.1-1-dev
|
||||
image: btcpayserver/lightning:v22.11-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -164,7 +164,7 @@ services:
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=customer_lightningd
|
||||
announce-addr=customer_lightningd:9735
|
||||
log-level=debug
|
||||
funding-confirms=1
|
||||
dev-fast-gossip
|
||||
@ -203,7 +203,7 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v0.10.1-1-dev
|
||||
image: btcpayserver/lightning:v22.11-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
@ -212,7 +212,7 @@ services:
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=merchant_lightningd
|
||||
announce-addr=merchant_lightningd:9735
|
||||
funding-confirms=1
|
||||
log-level=debug
|
||||
dev-fast-gossip
|
||||
|
@ -68,7 +68,7 @@ services:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:23.0-1
|
||||
image: btcpayserver/bitcoin:24.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.54
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -113,7 +113,7 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:23.0-1
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -141,7 +141,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v0.10.1-1-dev
|
||||
image: btcpayserver/lightning:v22.11-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -151,7 +151,7 @@ services:
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=customer_lightningd
|
||||
announce-addr=customer_lightningd:9735
|
||||
log-level=debug
|
||||
funding-confirms=1
|
||||
dev-fast-gossip
|
||||
@ -190,7 +190,7 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v0.10.1-1-dev
|
||||
image: btcpayserver/lightning:v22.11-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
@ -199,7 +199,7 @@ services:
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=merchant_lightningd
|
||||
announce-addr=merchant_lightningd:9735
|
||||
funding-confirms=1
|
||||
log-level=debug
|
||||
dev-fast-gossip
|
||||
|
@ -1,4 +1,4 @@
|
||||
<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" />
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
<RunAnalyzersDuringBuild>False</RunAnalyzersDuringBuild>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Condition="'$(GitCommit)' != ''" Include="BTCPayServer.GitCommitAttribute">
|
||||
<AssemblyAttribute Condition="'$(GitCommit)' != ''" Include="BTCPayServer.GitCommitAttribute">
|
||||
<_Parameter1>$(GitCommit)</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
@ -47,15 +47,15 @@
|
||||
<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.8" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.14" />
|
||||
<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.27" />
|
||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.4.0" />
|
||||
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />
|
||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||
<PackageReference Include="System.IO.Pipelines" Version="6.0.3" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.39" />
|
||||
|
@ -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>
|
||||
|
@ -14,7 +14,7 @@
|
||||
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"/>
|
||||
<use href="~/img/logo.svg#small" class="main-logo-btcpay--small"/>
|
||||
<use href="~/img/logo.svg#large" class="main-logo-btcpay--large"/>
|
||||
</svg>
|
||||
}
|
||||
|
@ -27,13 +27,13 @@
|
||||
<div class="accordion-body">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UIStores" asp-action="Dashboard" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(StoreNavPages.Dashboard)" id="StoreNav-Dashboard">
|
||||
<a asp-area="" asp-controller="UIStores" asp-action="Dashboard" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.Dashboard)" id="StoreNav-Dashboard">
|
||||
<vc:icon symbol="home"/>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(new [] {StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.General, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Plugins, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails})" id="StoreNav-StoreSettings">
|
||||
<a asp-area="" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(new [] {StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.General, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Plugins, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails})" id="StoreNav-StoreSettings">
|
||||
<vc:icon symbol="settings"/>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
@ -56,7 +56,7 @@
|
||||
<li class="nav-item">
|
||||
@if (isSetUp && scheme.WalletSupported)
|
||||
{
|
||||
<a asp-area="" asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@scheme.WalletId" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(WalletsNavPages), scheme.WalletId.ToString()) @ViewData.IsActivePage(StoreNavPages.OnchainSettings)" id="@($"StoreNav-Wallet{scheme.Crypto}")">
|
||||
<a asp-area="" asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@scheme.WalletId" class="nav-link @ViewData.IsActiveCategory(typeof(WalletsNavPages), scheme.WalletId.ToString()) @ViewData.IsActivePage(StoreNavPages.OnchainSettings)" id="@($"StoreNav-Wallet{scheme.Crypto}")">
|
||||
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "pending")"></span>
|
||||
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} Wallet" : "Bitcoin")</span>
|
||||
</a>
|
||||
@ -97,7 +97,7 @@
|
||||
@foreach (var custodianAccount in Model.CustodianAccounts)
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UICustodianAccounts" asp-action="ViewCustodianAccount" asp-route-storeId="@custodianAccount.StoreId" asp-route-accountId="@custodianAccount.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(CustodianAccountsNavPages.View, custodianAccount.Id)" id="@($"StoreNav-CustodianAccount-{custodianAccount.Id}")">
|
||||
<a asp-area="" asp-controller="UICustodianAccounts" asp-action="ViewCustodianAccount" asp-route-storeId="@custodianAccount.StoreId" asp-route-accountId="@custodianAccount.Id" class="nav-link @ViewData.IsActivePage(CustodianAccountsNavPages.View, custodianAccount.Id)" id="@($"StoreNav-CustodianAccount-{custodianAccount.Id}")">
|
||||
@* TODO which icon should we use? *@
|
||||
<span>@custodianAccount.Name</span>
|
||||
<span class="badge bg-warning ms-1" style="font-size:10px;">Experimental</span>
|
||||
@ -105,7 +105,7 @@
|
||||
</li>
|
||||
}
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UICustodianAccounts" asp-action="CreateCustodianAccount" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(CustodianAccountsNavPages.Create)" id="StoreNav-CreateCustodianAccount">
|
||||
<a asp-area="" asp-controller="UICustodianAccounts" asp-action="CreateCustodianAccount" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(CustodianAccountsNavPages.Create)" id="StoreNav-CreateCustodianAccount">
|
||||
<vc:icon symbol="new"/>
|
||||
<span>Add Custodian</span>
|
||||
<span class="badge bg-warning ms-1" style="font-size:10px;">Experimental</span>
|
||||
@ -127,13 +127,13 @@
|
||||
<div class="accordion-body">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item" permission="@Policies.CanViewInvoices">
|
||||
<a asp-area="" asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(InvoiceNavPages))" id="StoreNav-Invoices">
|
||||
<a asp-area="" asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActiveCategory(typeof(InvoiceNavPages))" id="StoreNav-Invoices">
|
||||
<vc:icon symbol="invoice"/>
|
||||
<span>Invoices</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
|
||||
<a asp-area="" asp-controller="UIPaymentRequest" asp-action="GetPaymentRequests" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(PaymentRequestsNavPages))" id="StoreNav-PaymentRequests">
|
||||
<a asp-area="" asp-controller="UIPaymentRequest" asp-action="GetPaymentRequests" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActiveCategory(typeof(PaymentRequestsNavPages))" id="StoreNav-PaymentRequests">
|
||||
<vc:icon symbol="payment-requests"/>
|
||||
<span>Requests</span>
|
||||
</a>
|
||||
@ -170,7 +170,7 @@
|
||||
<vc:ui-extension-point location="apps-nav" model="@app"/>
|
||||
}
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UIApps" asp-action="CreateApp" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(AppsNavPages.Create)" id="StoreNav-CreateApp">
|
||||
<a asp-area="" asp-controller="UIApps" asp-action="CreateApp" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(AppsNavPages.Create)" id="StoreNav-CreateApp">
|
||||
<vc:icon symbol="new"/>
|
||||
<span>New App</span>
|
||||
</a>
|
||||
@ -196,7 +196,7 @@
|
||||
<vc:ui-extension-point location="store-integrations-nav" model="@Model"/>
|
||||
}
|
||||
<li class="nav-item" permission="@Policies.CanModifyServerSettings">
|
||||
<a asp-area="" asp-controller="UIServer" asp-action="ListPlugins" class="nav-link js-scroll-trigger @ViewData.IsActivePage(ServerNavPages.Plugins)" id="Nav-ManagePlugins">
|
||||
<a asp-area="" asp-controller="UIServer" asp-action="ListPlugins" class="nav-link @ViewData.IsActivePage(ServerNavPages.Plugins)" id="Nav-ManagePlugins">
|
||||
<vc:icon symbol="new"/>
|
||||
<span>Manage Plugins</span>
|
||||
</a>
|
||||
@ -234,11 +234,11 @@
|
||||
@if (!PoliciesSettings.LockSubscription)
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UIAccount" asp-action="Register" class="nav-link js-scroll-trigger" id="Nav-Register">Register</a>
|
||||
<a asp-area="" asp-controller="UIAccount" asp-action="Register" class="nav-link" id="Nav-Register">Register</a>
|
||||
</li>
|
||||
}
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UIAccount" asp-action="Login" class="nav-link js-scroll-trigger" id="Nav-Login">Log in</a>
|
||||
<a asp-area="" asp-controller="UIAccount" asp-action="Login" class="nav-link" id="Nav-Login">Log in</a>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
@ -247,13 +247,13 @@
|
||||
{
|
||||
<ul id="mainNavSettings" class="navbar-nav border-top p-3 px-lg-4">
|
||||
<li class="nav-item" permission="@Policies.CanModifyServerSettings">
|
||||
<a asp-area="" asp-controller="UIServer" asp-action="ListUsers" class="nav-link js-scroll-trigger @ViewData.IsActivePage(ServerNavPages.Users) @ViewData.IsActivePage(ServerNavPages.Emails) @ViewData.IsActivePage(ServerNavPages.Policies) @ViewData.IsActivePage(ServerNavPages.Services) @ViewData.IsActivePage(ServerNavPages.Theme) @ViewData.IsActivePage(ServerNavPages.Maintenance) @ViewData.IsActivePage(ServerNavPages.Logs) @ViewData.IsActivePage(ServerNavPages.Files)" id="Nav-ServerSettings">
|
||||
<a asp-area="" asp-controller="UIServer" asp-action="ListUsers" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users) @ViewData.IsActivePage(ServerNavPages.Emails) @ViewData.IsActivePage(ServerNavPages.Policies) @ViewData.IsActivePage(ServerNavPages.Services) @ViewData.IsActivePage(ServerNavPages.Theme) @ViewData.IsActivePage(ServerNavPages.Maintenance) @ViewData.IsActivePage(ServerNavPages.Logs) @ViewData.IsActivePage(ServerNavPages.Files)" id="Nav-ServerSettings">
|
||||
<vc:icon symbol="server-settings"/>
|
||||
<span>Server Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropup">
|
||||
<a class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(ManageNavPages))" role="button" data-bs-toggle="dropdown" aria-expanded="false" id="Nav-Account">
|
||||
<a class="nav-link @ViewData.IsActiveCategory(typeof(ManageNavPages))" role="button" data-bs-toggle="dropdown" aria-expanded="false" id="Nav-Account">
|
||||
<vc:icon symbol="account"/>
|
||||
<span>Account</span>
|
||||
</a>
|
||||
|
@ -41,7 +41,7 @@ else
|
||||
@if (Model.Options.Count > 0)
|
||||
{
|
||||
<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">
|
||||
<button id="StoreSelectorToggle" class="btn btn-secondary dropdown-toggle rounded-pill px-3 @(Model.CurrentStoreId == null ? "empty-state" : "")" 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" />
|
||||
|
@ -65,6 +65,10 @@ 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");
|
||||
DockerDeployment = conf.GetOrDefault<bool>("dockerdeployment", true);
|
||||
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
||||
TorServices = conf.GetOrDefault<string>("torservices", null)
|
||||
|
@ -27,9 +27,9 @@ 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", $"Connection string to a MySQL database", CommandOptionType.SingleValue);
|
||||
app.Option("--mysql", $"DEPRECATED: Connection string to a MySQL database", CommandOptionType.SingleValue);
|
||||
app.Option("--nocsp", $"Disable CSP (default false)", CommandOptionType.BoolValue);
|
||||
app.Option("--sqlitefile", $"File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
|
||||
app.Option("--sqlitefile", $"DEPRECATED: 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("--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);
|
||||
|
@ -233,7 +233,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
EmbeddedCSS = request.EmbeddedCSS,
|
||||
RedirectAutomatically = request.RedirectAutomatically,
|
||||
RequiresRefundEmail = BoolToRequiresRefundEmail(request.RequiresRefundEmail) ?? RequiresRefundEmail.InheritFromStore,
|
||||
CheckoutFormId = request.CheckoutFormId,
|
||||
FormId = request.FormId,
|
||||
CheckoutType = request.CheckoutType ?? CheckoutType.V1
|
||||
};
|
||||
}
|
||||
|
@ -2,14 +2,19 @@
|
||||
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;
|
||||
@ -29,22 +34,30 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly UIInvoiceController _invoiceController;
|
||||
private readonly InvoiceRepository _invoiceRepository;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
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 InvoiceActivator _invoiceActivator;
|
||||
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, RateFetcher rateProvider,
|
||||
InvoiceActivator invoiceActivator,
|
||||
PullPaymentHostedService pullPaymentService, ApplicationDbContextFactory dbContextFactory)
|
||||
{
|
||||
_invoiceController = invoiceController;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
_linkGenerator = linkGenerator;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_eventAggregator = eventAggregator;
|
||||
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_networkProvider = btcPayNetworkProvider;
|
||||
_rateProvider = rateProvider;
|
||||
_invoiceActivator = invoiceActivator;
|
||||
_pullPaymentService = pullPaymentService;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
LanguageService = languageService;
|
||||
}
|
||||
|
||||
@ -325,14 +338,182 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
if (PaymentMethodId.TryParse(paymentMethod, out var paymentMethodId))
|
||||
{
|
||||
await _invoiceRepository.ActivateInvoicePaymentMethod(_eventAggregator, _btcPayNetworkProvider,
|
||||
_paymentMethodHandlerDictionary, store, invoice, paymentMethodId);
|
||||
await _invoiceActivator.ActivateInvoicePaymentMethod(paymentMethodId, invoice, store);
|
||||
return Ok();
|
||||
}
|
||||
ModelState.AddModelError(nameof(paymentMethod), "Invalid payment method");
|
||||
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");
|
||||
@ -437,7 +618,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
DefaultLanguage = entity.DefaultLanguage,
|
||||
RedirectAutomatically = entity.RedirectAutomatically,
|
||||
RequiresRefundEmail = entity.RequiresRefundEmail,
|
||||
CheckoutFormId = entity.CheckoutFormId,
|
||||
CheckoutType = entity.CheckoutType,
|
||||
RedirectURL = entity.RedirectURLTemplate
|
||||
},
|
||||
|
@ -295,27 +295,28 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ModelState.AddModelError(nameof(request.Amount), "Amount should be more or equals to 0");
|
||||
}
|
||||
|
||||
if (request.Description is null && request.DescriptionHashOnly)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Description), "Description is required when `descriptionHashOnly` is true");
|
||||
}
|
||||
|
||||
if (request.Expiry <= TimeSpan.Zero)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Expiry), "Expiry should be more than 0");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
|
||||
request.Description ??= "";
|
||||
try
|
||||
{
|
||||
var param = request.DescriptionHash != null
|
||||
? new CreateInvoiceParams(request.Amount, request.DescriptionHash, request.Expiry)
|
||||
var param = new CreateInvoiceParams(request.Amount, request.Description, request.Expiry)
|
||||
{
|
||||
PrivateRouteHints = request.PrivateRouteHints, Description = request.Description
|
||||
}
|
||||
: new CreateInvoiceParams(request.Amount, request.Description, request.Expiry)
|
||||
{
|
||||
PrivateRouteHints = request.PrivateRouteHints, DescriptionHash = request.DescriptionHash
|
||||
};
|
||||
PrivateRouteHints = request.PrivateRouteHints,
|
||||
DescriptionHashOnly = request.DescriptionHashOnly
|
||||
};
|
||||
var invoice = await lightningClient.CreateInvoice(param, cancellationToken);
|
||||
return Ok(ToModel(invoice));
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
@ -15,6 +16,7 @@ using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||
@ -30,6 +32,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly UIInvoiceController _invoiceController;
|
||||
private readonly PaymentRequestRepository _paymentRequestRepository;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
|
||||
public GreenfieldPaymentRequestsController(
|
||||
@ -38,6 +41,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
PaymentRequestRepository paymentRequestRepository,
|
||||
PaymentRequestService paymentRequestService,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
LinkGenerator linkGenerator)
|
||||
{
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
@ -45,6 +49,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_paymentRequestRepository = paymentRequestRepository;
|
||||
PaymentRequestService = paymentRequestService;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_userManager = userManager;
|
||||
_linkGenerator = linkGenerator;
|
||||
}
|
||||
|
||||
@ -152,7 +157,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public async Task<IActionResult> CreatePaymentRequest(string storeId,
|
||||
CreatePaymentRequestRequest request)
|
||||
{
|
||||
var validationResult = Validate(request);
|
||||
var validationResult = await Validate(null, request);
|
||||
if (validationResult != null)
|
||||
{
|
||||
return validationResult;
|
||||
@ -178,7 +183,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public async Task<IActionResult> UpdatePaymentRequest(string storeId,
|
||||
string paymentRequestId, [FromBody] UpdatePaymentRequestRequest request)
|
||||
{
|
||||
var validationResult = Validate(request);
|
||||
var validationResult = await Validate(paymentRequestId, request);
|
||||
if (validationResult != null)
|
||||
{
|
||||
return validationResult;
|
||||
@ -196,11 +201,22 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
return Ok(FromModel(await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr)));
|
||||
}
|
||||
private string GetUserId() => _userManager.GetUserId(User);
|
||||
|
||||
private IActionResult Validate(PaymentRequestBaseData data)
|
||||
private async Task<IActionResult> Validate(string id, PaymentRequestBaseData data)
|
||||
{
|
||||
if (data is null)
|
||||
return BadRequest();
|
||||
|
||||
if (id != null)
|
||||
{
|
||||
var pr = await this.PaymentRequestService.GetPaymentRequest(id, GetUserId());
|
||||
if (pr.Amount != data.Amount)
|
||||
{
|
||||
if (pr.Invoices.Any())
|
||||
ModelState.AddModelError(nameof(data.Amount), "Amount and currency are not editable once payment request has invoices");
|
||||
}
|
||||
}
|
||||
if (data.Amount <= 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(data.Amount), "Please provide an amount greater than 0");
|
||||
|
@ -460,12 +460,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ModelState.AddModelError(nameof(approvePayoutRequest.RateRule), "Invalid RateRule");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
var result = await _pullPaymentService.Approve(new PullPaymentHostedService.PayoutApproval()
|
||||
var result = (await _pullPaymentService.Approve(new PullPaymentHostedService.PayoutApproval()
|
||||
{
|
||||
PayoutId = payoutId,
|
||||
Revision = revision!.Value,
|
||||
Rate = rateResult.BidAsk.Ask
|
||||
});
|
||||
})).Result;
|
||||
var errorMessage = PullPaymentHostedService.PayoutApproval.GetErrorMessage(result);
|
||||
switch (result)
|
||||
{
|
||||
|
@ -189,7 +189,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId);
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId, (string[] ) null);
|
||||
|
||||
var preFiltering = true;
|
||||
if (statusFilter?.Any() is true || !string.IsNullOrWhiteSpace(labelFilter))
|
||||
@ -307,11 +307,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation);
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId,
|
||||
utxos.Select(u => u.OutPoint.Hash.ToString()).ToHashSet().ToArray());
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId,
|
||||
utxos.SelectMany(GetWalletObjectsQuery.Get).Distinct().ToArray());
|
||||
return Ok(utxos.Select(coin =>
|
||||
{
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info1);
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.Address.ToString(), out var info2);
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.ToString(), out var info3);
|
||||
var info = _walletRepository.Merge(info1, info2, info3);
|
||||
|
||||
return new OnChainWalletUTXOData()
|
||||
{
|
||||
|
@ -12,6 +12,7 @@ using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
@ -127,7 +128,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
//we do not include PaymentMethodCriteria because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
|
||||
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||
CheckoutFormId = storeBlob.CheckoutFormId,
|
||||
CheckoutType = storeBlob.CheckoutType,
|
||||
Receipt = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, null),
|
||||
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
||||
@ -150,7 +150,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
};
|
||||
}
|
||||
|
||||
private static void ToModel(StoreBaseData restModel, Data.StoreData model, PaymentMethodId defaultPaymentMethod)
|
||||
private void ToModel(StoreBaseData restModel, Data.StoreData model, PaymentMethodId defaultPaymentMethod)
|
||||
{
|
||||
var blob = model.GetStoreBlob();
|
||||
model.StoreName = restModel.Name;
|
||||
@ -167,7 +167,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
blob.NetworkFeeMode = restModel.NetworkFeeMode;
|
||||
blob.DefaultCurrency = restModel.DefaultCurrency;
|
||||
blob.RequiresRefundEmail = restModel.RequiresRefundEmail;
|
||||
blob.CheckoutFormId = restModel.CheckoutFormId;
|
||||
blob.CheckoutType = restModel.CheckoutType;
|
||||
blob.ReceiptOptions = InvoiceDataBase.ReceiptOptions.Merge(restModel.Receipt, null);
|
||||
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
|
||||
@ -187,6 +186,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
blob.LightningDescriptionTemplate = restModel.LightningDescriptionTemplate;
|
||||
blob.PaymentTolerance = restModel.PaymentTolerance;
|
||||
blob.PayJoinEnabled = restModel.PayJoinEnabled;
|
||||
blob.NormalizeToRelativeLinks(Request);
|
||||
model.SetStoreBlob(blob);
|
||||
}
|
||||
|
||||
|
@ -76,18 +76,20 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
return UserNotFound();
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/users/{idOrEmail}/lock")]
|
||||
public async Task<IActionResult> LockUser(string idOrEmail, LockUserRequest request )
|
||||
public async Task<IActionResult> LockUser(string idOrEmail, LockUserRequest request)
|
||||
{
|
||||
var user = (await _userManager.FindByIdAsync(idOrEmail) ) ?? await _userManager.FindByEmailAsync(idOrEmail);
|
||||
var user = await _userManager.FindByIdAsync(idOrEmail) ?? await _userManager.FindByEmailAsync(idOrEmail);
|
||||
if (user is null)
|
||||
{
|
||||
return UserNotFound();
|
||||
}
|
||||
|
||||
await _userService.ToggleUser(user.Id, request.Locked ? DateTimeOffset.MaxValue : null);
|
||||
return Ok();
|
||||
var success = await _userService.ToggleUser(user.Id, request.Locked ? DateTimeOffset.MaxValue : null);
|
||||
return success.HasValue && success.Value ? Ok() : this.CreateAPIError("invalid-state",
|
||||
$"{(request.Locked ? "Locking" : "Unlocking")} user failed");
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
|
@ -1047,9 +1047,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return GetFromActionResult<ApplicationUserData>(await GetController<GreenfieldUsersController>().GetUser(idOrEmail));
|
||||
}
|
||||
|
||||
public override async Task LockUser(string idOrEmail, bool disabled, CancellationToken token = default)
|
||||
public override async Task<bool> LockUser(string idOrEmail, bool disabled, CancellationToken token = default)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldUsersController>().LockUser(idOrEmail, new LockUserRequest() {Locked = disabled}));
|
||||
return GetFromActionResult<bool>(
|
||||
await GetController<GreenfieldUsersController>().LockUser(idOrEmail,
|
||||
new LockUserRequest { Locked = disabled }));
|
||||
}
|
||||
|
||||
public override async Task<OnChainWalletTransactionData> PatchOnChainWalletTransaction(string storeId,
|
||||
|
@ -24,7 +24,6 @@ using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Invoices.Export;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
@ -134,7 +133,7 @@ namespace BTCPayServer.Controllers
|
||||
Events = invoice.Events,
|
||||
PosData = PosDataParser.ParsePosData(invoice.Metadata.PosData),
|
||||
Archived = invoice.Archived,
|
||||
CanRefund = CanRefund(invoiceState),
|
||||
CanRefund = invoiceState.CanRefund(),
|
||||
Refunds = invoice.Refunds,
|
||||
ShowCheckout = invoice.Status == InvoiceStatusLegacy.New,
|
||||
ShowReceipt = invoice.Status.ToModernStatus() == InvoiceStatus.Settled && (invoice.ReceiptOptions?.Enabled ?? receipt.Enabled is true),
|
||||
@ -179,7 +178,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
JToken? receiptData = null;
|
||||
i.Metadata?.AdditionalData?.TryGetValue("receiptData", out receiptData);
|
||||
|
||||
|
||||
var payments = i.GetPayments(true)
|
||||
.Select(paymentEntity =>
|
||||
{
|
||||
@ -216,6 +215,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(new InvoiceReceiptViewModel
|
||||
{
|
||||
StoreName = store.StoreName,
|
||||
StoreLogoFileId = store.GetStoreBlob().LogoFileId,
|
||||
Status = i.Status.ToModernStatus(),
|
||||
Amount = payments.Sum(p => p!.Paid),
|
||||
Currency = i.Currency,
|
||||
@ -229,23 +229,12 @@ namespace BTCPayServer.Controllers
|
||||
? new Dictionary<string, object>()
|
||||
: PosDataParser.ParsePosData(receiptData.ToString())
|
||||
});
|
||||
|
||||
}
|
||||
private string? GetTransactionLink(PaymentMethodId paymentMethodId, string txId)
|
||||
{
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
||||
return network == null ? null : paymentMethodId.PaymentType.GetTransactionLink(network, txId);
|
||||
}
|
||||
bool CanRefund(InvoiceState invoiceState)
|
||||
{
|
||||
return invoiceState.Status == InvoiceStatusLegacy.Confirmed ||
|
||||
invoiceState.Status == InvoiceStatusLegacy.Complete ||
|
||||
(invoiceState.Status == InvoiceStatusLegacy.Expired &&
|
||||
(invoiceState.ExceptionStatus == InvoiceExceptionStatus.PaidLate ||
|
||||
invoiceState.ExceptionStatus == InvoiceExceptionStatus.PaidOver ||
|
||||
invoiceState.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)) ||
|
||||
invoiceState.Status == InvoiceStatusLegacy.Invalid;
|
||||
}
|
||||
|
||||
[HttpGet("invoices/{invoiceId}/refund")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
@ -264,7 +253,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
if (invoice.CurrentRefund?.PullPaymentDataId is null && GetUserId() is null)
|
||||
return NotFound();
|
||||
if (!CanRefund(invoice.GetInvoiceState()))
|
||||
if (!invoice.GetInvoiceState().CanRefund())
|
||||
return NotFound();
|
||||
if (invoice.CurrentRefund?.PullPaymentDataId is string ppId && !invoice.CurrentRefund.PullPaymentData.Archived)
|
||||
{
|
||||
@ -320,7 +309,7 @@ namespace BTCPayServer.Controllers
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
|
||||
if (!CanRefund(invoice.GetInvoiceState()))
|
||||
if (!invoice.GetInvoiceState().CanRefund())
|
||||
return NotFound();
|
||||
|
||||
var store = GetCurrentStore();
|
||||
@ -655,9 +644,23 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
|
||||
bool isDefaultPaymentId = false;
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var btcId = PaymentMethodId.Parse("BTC");
|
||||
var lnId = PaymentMethodId.Parse("BTC_LightningLike");
|
||||
if (paymentMethodId is null)
|
||||
{
|
||||
var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider);
|
||||
var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider)
|
||||
// Exclude LNURL for Checkout v2
|
||||
.Where(pmId => storeBlob.CheckoutType == CheckoutType.V1 || pmId.PaymentType is not LNURLPayPaymentType)
|
||||
.ToArray();
|
||||
|
||||
// Exclude Lightning if OnChainWithLnInvoiceFallback is active and we have both payment methods
|
||||
if (storeBlob.CheckoutType == CheckoutType.V2 && storeBlob.OnChainWithLnInvoiceFallback &&
|
||||
enabledPaymentIds.Contains(btcId) && enabledPaymentIds.Contains(lnId))
|
||||
{
|
||||
enabledPaymentIds = enabledPaymentIds.Where(pmId => pmId != lnId).ToArray();
|
||||
}
|
||||
|
||||
PaymentMethodId? invoicePaymentId = invoice.GetDefaultPaymentMethod();
|
||||
PaymentMethodId? storePaymentId = store.GetDefaultPaymentId();
|
||||
if (invoicePaymentId is not null)
|
||||
@ -688,6 +691,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if (paymentMethodId is null)
|
||||
return null;
|
||||
|
||||
BTCPayNetworkBase network = _NetworkProvider.GetNetwork<BTCPayNetworkBase>(paymentMethodId.CryptoCode);
|
||||
if (network is null || !invoice.Support(paymentMethodId))
|
||||
{
|
||||
@ -708,18 +712,15 @@ namespace BTCPayServer.Controllers
|
||||
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
|
||||
if (!paymentMethodDetails.Activated)
|
||||
{
|
||||
if (await _InvoiceRepository.ActivateInvoicePaymentMethod(_EventAggregator, _NetworkProvider,
|
||||
_paymentMethodHandlerDictionary, store, invoice, paymentMethod.GetId()))
|
||||
if (await _invoiceActivator.ActivateInvoicePaymentMethod(paymentMethod.GetId(), invoice, store))
|
||||
{
|
||||
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
|
||||
}
|
||||
}
|
||||
|
||||
var dto = invoice.EntityToDTO();
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var accounting = paymentMethod.Calculate();
|
||||
|
||||
var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId];
|
||||
|
||||
var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits;
|
||||
|
||||
switch (lang?.ToLowerInvariant())
|
||||
@ -760,7 +761,6 @@ namespace BTCPayServer.Controllers
|
||||
CustomLogoLink = storeBlob.CustomLogo,
|
||||
LogoFileId = storeBlob.LogoFileId,
|
||||
BrandColor = storeBlob.BrandColor,
|
||||
CheckoutFormId = invoice.CheckoutFormId ?? storeBlob.CheckoutFormId,
|
||||
CheckoutType = invoice.CheckoutType ?? storeBlob.CheckoutType,
|
||||
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
||||
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
|
||||
@ -823,16 +823,18 @@ namespace BTCPayServer.Controllers
|
||||
.OrderByDescending(a => a.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode).ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
|
||||
.ToList()
|
||||
};
|
||||
|
||||
// Exclude Lightning if OnChainWithLnInvoiceFallback is active and we have both payment methods
|
||||
if (storeBlob.CheckoutType == CheckoutType.V2 && storeBlob.OnChainWithLnInvoiceFallback)
|
||||
{
|
||||
var onchainPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == "BTC");
|
||||
var lightningPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == "BTC_LightningLike");
|
||||
var onchainPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == btcId.ToString());
|
||||
var lightningPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == lnId.ToString());
|
||||
if (onchainPM != null && lightningPM != null)
|
||||
{
|
||||
model.AvailableCryptos.Remove(lightningPM);
|
||||
}
|
||||
}
|
||||
|
||||
paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob, paymentMethod);
|
||||
model.UISettings = paymentMethodHandler.GetCheckoutUISettings();
|
||||
model.PaymentMethodId = paymentMethodId.ToString();
|
||||
@ -1140,9 +1142,6 @@ namespace BTCPayServer.Controllers
|
||||
RequiresRefundEmail = model.RequiresRefundEmail == RequiresRefundEmail.InheritFromStore
|
||||
? storeBlob.RequiresRefundEmail
|
||||
: model.RequiresRefundEmail == RequiresRefundEmail.On,
|
||||
CheckoutFormId = model.CheckoutFormId == GenericFormOption.InheritFromStore.ToString()
|
||||
? storeBlob.CheckoutFormId
|
||||
: model.CheckoutFormId
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Invoice {result.Data.Id} just created!";
|
||||
|
@ -14,6 +14,7 @@ using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
@ -27,6 +28,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
@ -37,6 +39,7 @@ namespace BTCPayServer.Controllers
|
||||
public partial class UIInvoiceController : Controller
|
||||
{
|
||||
readonly InvoiceRepository _InvoiceRepository;
|
||||
private readonly WalletRepository _walletRepository;
|
||||
readonly RateFetcher _RateProvider;
|
||||
readonly StoreRepository _StoreRepository;
|
||||
readonly UserManager<ApplicationUser> _UserManager;
|
||||
@ -49,12 +52,14 @@ namespace BTCPayServer.Controllers
|
||||
private readonly LanguageService _languageService;
|
||||
private readonly ExplorerClientProvider _ExplorerClients;
|
||||
private readonly UIWalletsController _walletsController;
|
||||
private readonly InvoiceActivator _invoiceActivator;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
|
||||
public WebhookSender WebhookNotificationManager { get; }
|
||||
|
||||
public UIInvoiceController(
|
||||
InvoiceRepository invoiceRepository,
|
||||
WalletRepository walletRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
RateFetcher rateProvider,
|
||||
@ -69,11 +74,13 @@ namespace BTCPayServer.Controllers
|
||||
LanguageService languageService,
|
||||
ExplorerClientProvider explorerClients,
|
||||
UIWalletsController walletsController,
|
||||
InvoiceActivator invoiceActivator,
|
||||
LinkGenerator linkGenerator)
|
||||
{
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_walletRepository = walletRepository;
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
_UserManager = userManager;
|
||||
_EventAggregator = eventAggregator;
|
||||
@ -85,6 +92,7 @@ namespace BTCPayServer.Controllers
|
||||
_languageService = languageService;
|
||||
this._ExplorerClients = explorerClients;
|
||||
_walletsController = walletsController;
|
||||
_invoiceActivator = invoiceActivator;
|
||||
_linkGenerator = linkGenerator;
|
||||
}
|
||||
|
||||
@ -142,7 +150,6 @@ namespace BTCPayServer.Controllers
|
||||
entity.RedirectAutomatically =
|
||||
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
|
||||
entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
|
||||
entity.CheckoutFormId = invoice.CheckoutFormId;
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
IPaymentFilter? excludeFilter = null;
|
||||
@ -194,7 +201,8 @@ namespace BTCPayServer.Controllers
|
||||
Metadata = invoiceMetadata.ToJObject(),
|
||||
Currency = pr.Currency,
|
||||
Amount = amount,
|
||||
Checkout = { RedirectURL = redirectUrl }
|
||||
Checkout = { RedirectURL = redirectUrl },
|
||||
Receipt = new InvoiceDataBase.ReceiptOptions { Enabled = false }
|
||||
};
|
||||
|
||||
var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(pr.Id) };
|
||||
@ -227,7 +235,6 @@ namespace BTCPayServer.Controllers
|
||||
entity.DefaultLanguage = invoice.Checkout.DefaultLanguage;
|
||||
entity.DefaultPaymentMethod = invoice.Checkout.DefaultPaymentMethod;
|
||||
entity.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically;
|
||||
entity.CheckoutFormId = invoice.Checkout.CheckoutFormId;
|
||||
entity.CheckoutType = invoice.Checkout.CheckoutType;
|
||||
entity.RequiresRefundEmail = invoice.Checkout.RequiresRefundEmail;
|
||||
IPaymentFilter? excludeFilter = null;
|
||||
@ -369,6 +376,30 @@ namespace BTCPayServer.Controllers
|
||||
using (logs.Measure("Saving invoice"))
|
||||
{
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, additionalSearchTerms);
|
||||
foreach (var method in paymentMethods)
|
||||
{
|
||||
if (method.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod bp)
|
||||
{
|
||||
var walletId = new WalletId(store.Id, method.GetId().CryptoCode);
|
||||
await _walletRepository.EnsureWalletObject(new WalletObjectId(
|
||||
walletId,
|
||||
WalletObjectData.Types.Invoice,
|
||||
entity.Id
|
||||
));
|
||||
if (bp.GetDepositAddress(((BTCPayNetwork)method.Network).NBitcoinNetwork) is BitcoinAddress address)
|
||||
{
|
||||
await _walletRepository.EnsureWalletObjectLink(
|
||||
new WalletObjectId(
|
||||
walletId,
|
||||
WalletObjectData.Types.Address,
|
||||
address.ToString()),
|
||||
new WalletObjectId(
|
||||
walletId,
|
||||
WalletObjectData.Types.Invoice,
|
||||
entity.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
@ -20,6 +21,7 @@ using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -51,6 +53,7 @@ namespace BTCPayServer
|
||||
private readonly LightningAddressService _lightningAddressService;
|
||||
private readonly LightningLikePayoutHandler _lightningLikePayoutHandler;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
|
||||
|
||||
public UILNURLController(InvoiceRepository invoiceRepository,
|
||||
EventAggregator eventAggregator,
|
||||
@ -62,7 +65,8 @@ namespace BTCPayServer
|
||||
LinkGenerator linkGenerator,
|
||||
LightningAddressService lightningAddressService,
|
||||
LightningLikePayoutHandler lightningLikePayoutHandler,
|
||||
PullPaymentHostedService pullPaymentHostedService)
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings)
|
||||
{
|
||||
_invoiceRepository = invoiceRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
@ -75,11 +79,12 @@ namespace BTCPayServer
|
||||
_lightningAddressService = lightningAddressService;
|
||||
_lightningLikePayoutHandler = lightningLikePayoutHandler;
|
||||
_pullPaymentHostedService = pullPaymentHostedService;
|
||||
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("withdraw/pp/{pullPaymentId}")]
|
||||
public async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr)
|
||||
public async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
@ -116,6 +121,7 @@ namespace BTCPayServer
|
||||
LightMoneyUnit.BTC),
|
||||
Tag = "withdrawRequest",
|
||||
Callback = new Uri(Request.GetCurrentUrl()),
|
||||
DefaultDescription = pp.GetBlob().Description ?? String.Empty,
|
||||
};
|
||||
if (pr is null)
|
||||
{
|
||||
@ -155,25 +161,28 @@ namespace BTCPayServer
|
||||
{
|
||||
var client =
|
||||
_lightningLikePaymentHandler.CreateLightningClient(pm, network);
|
||||
PayResponse payResult;
|
||||
try
|
||||
{
|
||||
payResult = await client.Pay(pr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
payResult = new PayResponse(PayResult.Error, e.Message);
|
||||
}
|
||||
|
||||
var payResult = await UILightningLikePayoutController.TrypayBolt(client,
|
||||
claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings),
|
||||
claimResponse.PayoutData, result, pmi, cancellationToken);
|
||||
|
||||
switch (payResult.Result)
|
||||
{
|
||||
case PayResult.Ok:
|
||||
case PayResult.Unknown:
|
||||
await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest()
|
||||
{
|
||||
PayoutId = claimResponse.PayoutData.Id, State = PayoutState.Completed
|
||||
PayoutId = claimResponse.PayoutData.Id,
|
||||
State = claimResponse.PayoutData.State,
|
||||
Proof = claimResponse.PayoutData.GetProofBlobJson()
|
||||
});
|
||||
|
||||
return Ok(new LNUrlStatusResponse {Status = "OK"});
|
||||
return Ok(new LNUrlStatusResponse
|
||||
{
|
||||
Status = "OK",
|
||||
Reason = payResult.Message
|
||||
});
|
||||
case PayResult.CouldNotFindRoute:
|
||||
case PayResult.Error:
|
||||
default:
|
||||
await _pullPaymentHostedService.Cancel(
|
||||
new PullPaymentHostedService.CancelRequest(new string[]
|
||||
@ -184,7 +193,7 @@ namespace BTCPayServer
|
||||
return Ok(new LNUrlStatusResponse
|
||||
{
|
||||
Status = "ERROR",
|
||||
Reason = $"Pr could not be paid because {payResult.ErrorDetail}"
|
||||
Reason = payResult.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -557,14 +566,14 @@ namespace BTCPayServer
|
||||
}
|
||||
}
|
||||
|
||||
var descriptionHash = new uint256(Hashes.SHA256(Encoding.UTF8.GetBytes(metadata)), false);
|
||||
LightningInvoice invoice;
|
||||
try
|
||||
{
|
||||
var expiry = i.ExpirationTime.ToUniversalTime() - DateTimeOffset.UtcNow;
|
||||
var param = new CreateInvoiceParams(amount.Value, descriptionHash, expiry)
|
||||
var param = new CreateInvoiceParams(amount.Value, metadata, expiry)
|
||||
{
|
||||
PrivateRouteHints = blob.LightningPrivateRouteHints
|
||||
PrivateRouteHints = blob.LightningPrivateRouteHints,
|
||||
DescriptionHashOnly = true
|
||||
};
|
||||
invoice = await client.CreateInvoice(param);
|
||||
if (!BOLT11PaymentRequest.Parse(invoice.BOLT11, network.NBitcoinNetwork)
|
||||
|
@ -1,14 +1,16 @@
|
||||
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.Abstractions.Form;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Forms;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -18,7 +20,7 @@ using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
@ -37,6 +39,8 @@ namespace BTCPayServer.Controllers
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
private FormComponentProviders FormProviders { get; }
|
||||
|
||||
public UIPaymentRequestController(
|
||||
UIInvoiceController invoiceController,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
@ -45,7 +49,8 @@ namespace BTCPayServer.Controllers
|
||||
EventAggregator eventAggregator,
|
||||
CurrencyNameTable currencies,
|
||||
StoreRepository storeRepository,
|
||||
InvoiceRepository invoiceRepository)
|
||||
InvoiceRepository invoiceRepository,
|
||||
FormComponentProviders formProviders)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
@ -55,6 +60,7 @@ namespace BTCPayServer.Controllers
|
||||
_Currencies = currencies;
|
||||
_storeRepository = storeRepository;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
FormProviders = formProviders;
|
||||
}
|
||||
|
||||
[BitpayAPIConstraint(false)]
|
||||
@ -87,7 +93,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/payment-requests/edit/{payReqId?}")]
|
||||
public IActionResult EditPaymentRequest(string storeId, string payReqId)
|
||||
public async Task<IActionResult> EditPaymentRequest(string storeId, string payReqId)
|
||||
{
|
||||
var store = GetCurrentStore();
|
||||
var paymentRequest = GetCurrentPaymentRequest();
|
||||
@ -96,9 +102,11 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var prInvoices = payReqId is null ? null : (await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId())).Invoices;
|
||||
var vm = new UpdatePaymentRequestViewModel(paymentRequest)
|
||||
{
|
||||
StoreId = store.Id
|
||||
StoreId = store.Id,
|
||||
AmountAndCurrencyEditable = payReqId is null || !prInvoices.Any()
|
||||
};
|
||||
|
||||
vm.Currency ??= store.GetStoreBlob().DefaultCurrency;
|
||||
@ -125,17 +133,24 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "You cannot edit an archived payment request.");
|
||||
}
|
||||
var data = paymentRequest ?? new PaymentRequestData();
|
||||
data.StoreDataId = viewModel.StoreId;
|
||||
data.Archived = viewModel.Archived;
|
||||
var blob = data.GetBlob();
|
||||
|
||||
if (blob.Amount != viewModel.Amount && payReqId != null)
|
||||
{
|
||||
var prInvoices = (await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId())).Invoices;
|
||||
if (prInvoices.Any())
|
||||
ModelState.AddModelError(nameof(viewModel.Amount), "Amount and currency are not editable once payment request has invoices");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(nameof(EditPaymentRequest), viewModel);
|
||||
}
|
||||
|
||||
var data = paymentRequest ?? new PaymentRequestData();
|
||||
data.StoreDataId = viewModel.StoreId;
|
||||
data.Archived = viewModel.Archived;
|
||||
|
||||
var blob = data.GetBlob();
|
||||
|
||||
blob.Title = viewModel.Title;
|
||||
blob.Email = viewModel.Email;
|
||||
blob.Description = viewModel.Description;
|
||||
@ -145,6 +160,7 @@ namespace BTCPayServer.Controllers
|
||||
blob.EmbeddedCSS = viewModel.EmbeddedCSS;
|
||||
blob.CustomCSSLink = viewModel.CustomCSSLink;
|
||||
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
|
||||
blob.FormId = viewModel.FormId;
|
||||
|
||||
data.SetBlob(blob);
|
||||
var isNewPaymentRequest = string.IsNullOrEmpty(payReqId);
|
||||
@ -174,6 +190,56 @@ namespace BTCPayServer.Controllers
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[HttpGet("{payReqId}/form")]
|
||||
[HttpPost("{payReqId}/form")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ViewPaymentRequestForm(string payReqId)
|
||||
{
|
||||
var result = await _PaymentRequestRepository.FindPaymentRequest(payReqId, GetUserId());
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var prBlob = result.GetBlob();
|
||||
var prFormId = prBlob.FormId;
|
||||
var formConfig = prFormId is null ? null : Forms.UIFormsController.GetFormData(prFormId)?.Config;
|
||||
switch (formConfig)
|
||||
{
|
||||
case null:
|
||||
case { } when !this.Request.HasFormContentType && prBlob.FormResponse is not null:
|
||||
return RedirectToAction("ViewPaymentRequest", new { payReqId });
|
||||
case { } when !this.Request.HasFormContentType && prBlob.FormResponse is null:
|
||||
break;
|
||||
default:
|
||||
// POST case: Handle form submit
|
||||
var formData = Form.Parse(formConfig);
|
||||
formData.ApplyValuesFromForm(Request.Form);
|
||||
if (FormProviders.Validate(formData, ModelState))
|
||||
{
|
||||
prBlob.FormResponse = JObject.FromObject(formData.GetValues());
|
||||
result.SetBlob(prBlob);
|
||||
await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(result);
|
||||
return RedirectToAction("PayPaymentRequest", new { payReqId });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
AspController = "UIForms",
|
||||
AspAction = "ViewPublicForm",
|
||||
RouteParameters =
|
||||
{
|
||||
{ "formId", prFormId }
|
||||
},
|
||||
FormParameters =
|
||||
{
|
||||
{ "redirectUrl", Request.GetCurrentUrl() }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{payReqId}/pay")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> PayPaymentRequest(string payReqId, bool redirectToInvoice = true,
|
||||
@ -286,10 +352,10 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{payReqId}/clone")]
|
||||
public IActionResult ClonePaymentRequest(string payReqId)
|
||||
public async Task<IActionResult> ClonePaymentRequest(string payReqId)
|
||||
{
|
||||
var store = GetCurrentStore();
|
||||
var result = EditPaymentRequest(store.Id, payReqId);
|
||||
var result = await EditPaymentRequest(store.Id, payReqId);
|
||||
if (result is ViewResult viewResult)
|
||||
{
|
||||
var model = (UpdatePaymentRequestViewModel)viewResult.Model;
|
||||
@ -307,7 +373,7 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> TogglePaymentRequestArchival(string payReqId)
|
||||
{
|
||||
var store = GetCurrentStore();
|
||||
var result = EditPaymentRequest(store.Id, payReqId);
|
||||
var result = await EditPaymentRequest(store.Id, payReqId);
|
||||
if (result is ViewResult viewResult)
|
||||
{
|
||||
var model = (UpdatePaymentRequestViewModel)viewResult.Model;
|
||||
|
@ -982,7 +982,10 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("server/theme")]
|
||||
public async Task<IActionResult> Theme(ThemeSettings model, [FromForm] bool RemoveLogoFile)
|
||||
public async Task<IActionResult> Theme(
|
||||
ThemeSettings model,
|
||||
[FromForm] bool RemoveLogoFile,
|
||||
[FromForm] bool RemoveCustomThemeFile)
|
||||
{
|
||||
var settingsChanged = false;
|
||||
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
|
||||
@ -991,6 +994,40 @@ namespace BTCPayServer.Controllers
|
||||
if (userId is null)
|
||||
return NotFound();
|
||||
|
||||
if (model.CustomThemeFile != null)
|
||||
{
|
||||
if (model.CustomThemeFile.ContentType.Equals("text/css", StringComparison.InvariantCulture))
|
||||
{
|
||||
// delete existing file
|
||||
if (!string.IsNullOrEmpty(settings.CustomThemeFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(settings.CustomThemeFileId, userId);
|
||||
}
|
||||
|
||||
// add new file
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(model.CustomThemeFile, userId);
|
||||
settings.CustomThemeFileId = storedFile.Id;
|
||||
settingsChanged = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(nameof(settings.CustomThemeFile), $"Could not save theme file: {e.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(settings.CustomThemeFile), "The uploaded theme file needs to be a CSS file");
|
||||
}
|
||||
}
|
||||
else if (RemoveCustomThemeFile && !string.IsNullOrEmpty(settings.CustomThemeFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(settings.CustomThemeFileId, userId);
|
||||
settings.CustomThemeFileId = null;
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
if (model.LogoFile != null)
|
||||
{
|
||||
if (model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
@ -1010,12 +1047,12 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.LogoFile), $"Could not save logo: {e.Message}");
|
||||
ModelState.AddModelError(nameof(settings.LogoFile), $"Could not save logo: {e.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image");
|
||||
ModelState.AddModelError(nameof(settings.LogoFile), "The uploaded logo file needs to be an image");
|
||||
}
|
||||
}
|
||||
else if (RemoveLogoFile && !string.IsNullOrEmpty(settings.LogoFileId))
|
||||
@ -1025,14 +1062,28 @@ namespace BTCPayServer.Controllers
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
if (model.CustomTheme && !Uri.IsWellFormedUriString(model.CssUri, UriKind.RelativeOrAbsolute))
|
||||
if (model.CustomTheme && !string.IsNullOrEmpty(model.CustomThemeCssUri) && !Uri.IsWellFormedUriString(model.CustomThemeCssUri, UriKind.RelativeOrAbsolute))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CustomTheme), "Please provide a non-empty theme URI");
|
||||
ModelState.AddModelError(nameof(settings.CustomThemeCssUri), "Please provide a non-empty theme URI");
|
||||
}
|
||||
else if (settings.CustomTheme != model.CustomTheme)
|
||||
|
||||
if (settings.CustomThemeExtension != model.CustomThemeExtension)
|
||||
{
|
||||
// Require a custom theme to be defined in that case
|
||||
if (string.IsNullOrEmpty(model.CustomThemeCssUri) && string.IsNullOrEmpty(settings.CustomThemeFileId))
|
||||
{
|
||||
ModelState.AddModelError(nameof(settings.CustomThemeFile), "Please provide a custom theme");
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.CustomThemeExtension = model.CustomThemeExtension;
|
||||
settingsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.CustomTheme != model.CustomTheme)
|
||||
{
|
||||
settings.CustomTheme = model.CustomTheme;
|
||||
settings.CustomThemeCssUri = model.CustomThemeCssUri;
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
|
@ -338,11 +338,11 @@ namespace BTCPayServer.Controllers
|
||||
Revision = payout.GetBlob(_jsonSerializerSettings).Revision,
|
||||
Rate = rateResult.BidAsk.Ask
|
||||
});
|
||||
if (approveResult != PullPaymentHostedService.PayoutApproval.Result.Ok)
|
||||
if (approveResult.Result != PullPaymentHostedService.PayoutApproval.Result.Ok)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult),
|
||||
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult.Result),
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
});
|
||||
failed = true;
|
||||
|
@ -89,17 +89,17 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (vm.WalletFile != null)
|
||||
{
|
||||
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy))
|
||||
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy, out var error))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.WalletFile), "Wallet file was not in the correct format");
|
||||
ModelState.AddModelError(nameof(vm.WalletFile), $"Importing wallet failed: {error}");
|
||||
return View(vm.ViewName, vm);
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(vm.WalletFileContent))
|
||||
{
|
||||
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy))
|
||||
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy, out var error))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.WalletFileContent), "QR import was not in the correct format");
|
||||
ModelState.AddModelError(nameof(vm.WalletFileContent), $"QR import failed: {error}");
|
||||
return View(vm.ViewName, vm);
|
||||
}
|
||||
}
|
||||
|
@ -201,7 +201,7 @@ namespace BTCPayServer.Controllers
|
||||
var exchanges = GetSupportedExchanges();
|
||||
var storeBlob = CurrentStore.GetStoreBlob();
|
||||
var vm = new RatesViewModel();
|
||||
vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? CoinGeckoRateProvider.CoinGeckoName);
|
||||
vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? storeBlob.GetRecommendedExchange());
|
||||
vm.Spread = (double)(storeBlob.Spread * 100m);
|
||||
vm.StoreId = CurrentStore.Id;
|
||||
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
|
||||
@ -225,7 +225,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var exchanges = GetSupportedExchanges();
|
||||
model.SetExchangeRates(exchanges, model.PreferredExchange);
|
||||
model.SetExchangeRates(exchanges, model.PreferredExchange ?? this.HttpContext.GetStoreData().GetStoreBlob().GetRecommendedExchange());
|
||||
model.StoreId = storeId ?? model.StoreId;
|
||||
CurrencyPair[]? currencyPairs = null;
|
||||
try
|
||||
@ -505,7 +505,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback;
|
||||
}
|
||||
|
||||
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LazyPaymentMethods = model.LazyPaymentMethods;
|
||||
blob.RedirectAutomatically = model.RedirectAutomatically;
|
||||
@ -515,7 +515,7 @@ namespace BTCPayServer.Controllers
|
||||
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
|
||||
blob.AutoDetectLanguage = model.AutoDetectLanguage;
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
|
||||
blob.NormalizeToRelativeLinks(Request);
|
||||
if (CurrentStore.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
|
@ -44,7 +44,7 @@ namespace BTCPayServer.Controllers
|
||||
var vm = new CreateStoreViewModel
|
||||
{
|
||||
DefaultCurrency = StoreBlob.StandardDefaultCurrency,
|
||||
Exchanges = GetExchangesSelectList(CoinGeckoRateProvider.CoinGeckoName)
|
||||
Exchanges = GetExchangesSelectList(null)
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
@ -99,7 +99,9 @@ namespace BTCPayServer.Controllers
|
||||
var exchanges = _rateFactory.RateProviderFactory
|
||||
.GetSupportedExchanges()
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r.Name))
|
||||
.OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase);
|
||||
.OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
exchanges.Insert(0, new AvailableRateProvider(null, "Recommended", ""));
|
||||
var chosen = exchanges.FirstOrDefault(f => f.Id == selected) ?? exchanges.First();
|
||||
return new SelectList(exchanges, nameof(chosen.Id), nameof(chosen.Name), chosen.Id);
|
||||
}
|
||||
|
@ -578,17 +578,20 @@ namespace BTCPayServer.Controllers
|
||||
var utxos = await _walletProvider.GetWallet(network)
|
||||
.GetUnspentCoins(schemeSettings.AccountDerivation, false, cancellation);
|
||||
|
||||
var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId, utxos.Select(u => u.OutPoint.Hash.ToString()).Distinct().ToArray());
|
||||
var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId,
|
||||
utxos.SelectMany(u => GetWalletObjectsQuery.Get(u)).Distinct().ToArray());
|
||||
vm.InputsAvailable = utxos.Select(coin =>
|
||||
{
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
|
||||
var labels = CreateTransactionTagModels(info).ToList();
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info1);
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.Address.ToString(), out var info2);
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.ToString(), out var info3);
|
||||
var info = WalletRepository.Merge(info1, info2, info3);
|
||||
return new WalletSendModel.InputSelectionOption()
|
||||
{
|
||||
Outpoint = coin.OutPoint.ToString(),
|
||||
Amount = coin.Value.GetValue(network),
|
||||
Comment = info?.Comment,
|
||||
Labels = labels,
|
||||
Labels = CreateTransactionTagModels(info),
|
||||
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
|
||||
coin.OutPoint.Hash.ToString()),
|
||||
Confirmations = coin.Confirmations
|
||||
@ -1291,7 +1294,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId);
|
||||
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId, (string[] ) null);
|
||||
var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, null, null);
|
||||
var walletTransactionsInfo = await walletTransactionsInfoAsync;
|
||||
var export = new TransactionsExport(wallet, walletTransactionsInfo);
|
||||
|
@ -1,3 +1,4 @@
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Data;
|
||||
@ -8,7 +9,7 @@ public static class CustodianAccountDataExtensions
|
||||
{
|
||||
var result = custodianAccountData.Blob == null
|
||||
? new JObject()
|
||||
: JObject.Parse(ZipUtils.Unzip(custodianAccountData.Blob));
|
||||
: InvoiceRepository.FromBytes<JObject>(custodianAccountData.Blob);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -17,7 +18,8 @@ public static class CustodianAccountDataExtensions
|
||||
var original = custodianAccountData.GetBlob();
|
||||
if (JToken.DeepEquals(original, blob))
|
||||
return false;
|
||||
custodianAccountData.Blob = blob is null ? null : ZipUtils.Zip(blob.ToString(Newtonsoft.Json.Formatting.None));
|
||||
|
||||
custodianAccountData.Blob = blob is null ? null : InvoiceRepository.ToBytes(blob);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -257,9 +257,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static readonly TimeSpan SendTimeout = TimeSpan.FromSeconds(20);
|
||||
|
||||
public static async Task<ResultVM> TrypayBolt(
|
||||
ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest,
|
||||
PaymentMethodId pmi, CancellationToken cancellationToken)
|
||||
@ -281,17 +279,13 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
var proofBlob = new PayoutLightningBlob() { PaymentHash = bolt11PaymentRequest.PaymentHash.ToString() };
|
||||
try
|
||||
{
|
||||
// TODO: Incorporate the changes from this PR here:
|
||||
// https://github.com/btcpayserver/BTCPayServer.Lightning/pull/106
|
||||
using var timeout = new CancellationTokenSource(SendTimeout);
|
||||
using var c = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
|
||||
var result = await lightningClient.Pay(bolt11PaymentRequest.ToString(),
|
||||
new PayInvoiceParams()
|
||||
{
|
||||
Amount = bolt11PaymentRequest.MinimumAmount == LightMoney.Zero
|
||||
? new LightMoney((decimal)payoutBlob.CryptoAmount, LightMoneyUnit.BTC)
|
||||
: null
|
||||
}, c.Token);
|
||||
}, cancellationToken);
|
||||
string message = null;
|
||||
if (result.Result == PayResult.Ok)
|
||||
{
|
||||
@ -309,6 +303,11 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
else if(result.Result == PayResult.Unknown)
|
||||
{
|
||||
payoutData.State = PayoutState.InProgress;
|
||||
message = "The payment has been initiated but is still in-flight.";
|
||||
}
|
||||
|
||||
payoutData.SetProofBlob(proofBlob, null);
|
||||
return new ResultVM
|
||||
|
@ -12,6 +12,7 @@ using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
@ -38,7 +39,6 @@ namespace BTCPayServer.Data
|
||||
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public CheckoutType CheckoutType { get; set; }
|
||||
public string CheckoutFormId { get; set; }
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
public bool LightningPrivateRouteHints { get; set; }
|
||||
@ -171,7 +171,7 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? CoinGeckoRateProvider.CoinGeckoName : PreferredExchange;
|
||||
var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? GetRecommendedExchange() : PreferredExchange;
|
||||
builder.AppendLine(CultureInfo.InvariantCulture, $"X_X = {preferredExchange}(X_X);");
|
||||
|
||||
BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules);
|
||||
@ -179,6 +179,21 @@ namespace BTCPayServer.Data
|
||||
return rules;
|
||||
}
|
||||
|
||||
public static JObject RecommendedExchanges = new ()
|
||||
{
|
||||
{ "EUR", "kraken" },
|
||||
{ "USD", "kraken" },
|
||||
{ "GBP", "kraken" },
|
||||
{ "CHF", "kraken" },
|
||||
{ "GTQ", "bitpay" },
|
||||
{ "COP", "yadio" },
|
||||
{ "JPY", "bitbank" },
|
||||
{ "TRY", "btcturk" }
|
||||
};
|
||||
|
||||
public string GetRecommendedExchange() =>
|
||||
RecommendedExchanges.Property(DefaultCurrency)?.Value.ToString() ?? "coingecko";
|
||||
|
||||
[Obsolete("Use GetExcludedPaymentMethods instead")]
|
||||
public string[] ExcludedPaymentMethods { get; set; }
|
||||
|
||||
@ -225,6 +240,30 @@ namespace BTCPayServer.Data
|
||||
ExcludedPaymentMethods = methods.ToArray();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
// Replace absolute URL with relative to avoid this issue: https://github.com/btcpayserver/btcpayserver/discussions/4195
|
||||
public void NormalizeToRelativeLinks(HttpRequest request)
|
||||
{
|
||||
var schemeAndHost = $"{request.Scheme}://{request.Host.ToString()}/";
|
||||
this.CustomLogo = EnsureRelativeLinks(this.CustomLogo, schemeAndHost);
|
||||
this.CustomCSS = EnsureRelativeLinks(this.CustomCSS, schemeAndHost);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make a link relative if possible
|
||||
/// </summary>
|
||||
/// <param name="value">Example: https://mystore.com/toto.png</param>
|
||||
/// <param name="schemeAndHost">Example: https://mystore.com/</param>
|
||||
/// <returns>/toto.png</returns>
|
||||
private string EnsureRelativeLinks(string value, string schemeAndHost)
|
||||
{
|
||||
if (value is null)
|
||||
return null;
|
||||
value = value.Trim();
|
||||
if (value.StartsWith(schemeAndHost, StringComparison.OrdinalIgnoreCase))
|
||||
return value.Substring(schemeAndHost.Length - 1);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
public class PaymentMethodCriteria
|
||||
{
|
||||
|
@ -47,9 +47,9 @@ namespace BTCPayServer.Data
|
||||
|
||||
public static StoreBlob GetStoreBlob(this StoreData storeData)
|
||||
{
|
||||
var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(Encoding.UTF8.GetString(storeData.StoreBlob));
|
||||
var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(storeData.StoreBlob);
|
||||
if (result.PreferredExchange == null)
|
||||
result.PreferredExchange = CoinGeckoRateProvider.CoinGeckoName;
|
||||
result.PreferredExchange = result.GetRecommendedExchange();
|
||||
if (result.PaymentMethodCriteria is null)
|
||||
result.PaymentMethodCriteria = new List<PaymentMethodCriteria>();
|
||||
result.PaymentMethodCriteria.RemoveAll(criteria => criteria?.PaymentMethod is null);
|
||||
@ -62,7 +62,7 @@ namespace BTCPayServer.Data
|
||||
var newBlob = new Serializer(null).ToString(storeBlob);
|
||||
if (original == newBlob)
|
||||
return false;
|
||||
storeData.StoreBlob = Encoding.UTF8.GetBytes(newBlob);
|
||||
storeData.StoreBlob = newBlob;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -83,5 +82,33 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
public string Type { get; set; }
|
||||
|
||||
public WalletTransactionInfo Merge(WalletTransactionInfo? value)
|
||||
{
|
||||
var result = new WalletTransactionInfo(WalletId);
|
||||
if (value is null)
|
||||
return result;
|
||||
|
||||
if (result.WalletId != value.WalletId)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result.LabelColors = new Dictionary<string, string>(LabelColors);
|
||||
result.Attachments = new List<Attachment>(Attachments);
|
||||
foreach (var valueLabelColor in value.LabelColors)
|
||||
{
|
||||
result.LabelColors.TryAdd(valueLabelColor.Key, valueLabelColor.Value);
|
||||
}
|
||||
|
||||
foreach (var valueAttachment in value.Attachments.Where(valueAttachment => !Attachments.Any(attachment =>
|
||||
attachment.Id == valueAttachment.Id && attachment.Type == valueAttachment.Type)))
|
||||
{
|
||||
result.Attachments.Add(valueAttachment);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ namespace BTCPayServer
|
||||
{
|
||||
throw new FormatException("Custom change paths are not supported.");
|
||||
}
|
||||
|
||||
return (Parse($"{hd.Extkey}{suffix}"), null);
|
||||
case PubKeyProvider.Origin origin:
|
||||
var innerResult = ExtractFromPkProvider(origin.Inner, suffix);
|
||||
@ -42,7 +41,16 @@ namespace BTCPayServer
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
(DerivationStrategyBase, RootedKeyPath[]) ExtractFromMulti(OutputDescriptor.Multi multi)
|
||||
{
|
||||
var xpubs = multi.PkProviders.Select(provider => ExtractFromPkProvider(provider));
|
||||
return (
|
||||
Parse(
|
||||
$"{multi.Threshold}-of-{(string.Join('-', xpubs.Select(tuple => tuple.Item1.ToString())))}{(multi.IsSorted ? "" : "-[keeporder]")}"),
|
||||
xpubs.SelectMany(tuple => tuple.Item2).ToArray());
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(str);
|
||||
str = str.Trim();
|
||||
var outputDescriptor = OutputDescriptor.Parse(str, Network);
|
||||
@ -55,11 +63,7 @@ namespace BTCPayServer
|
||||
case OutputDescriptor.Combo _:
|
||||
throw new FormatException("Only output descriptors of one format are supported.");
|
||||
case OutputDescriptor.Multi multi:
|
||||
var xpubs = multi.PkProviders.Select(provider => ExtractFromPkProvider(provider));
|
||||
return (
|
||||
Parse(
|
||||
$"{multi.Threshold}-of-{(string.Join('-', xpubs.Select(tuple => tuple.Item1.ToString())))}{(multi.IsSorted ? "" : "-[keeporder]")}"),
|
||||
xpubs.SelectMany(tuple => tuple.Item2).ToArray());
|
||||
return ExtractFromMulti(multi);
|
||||
case OutputDescriptor.PKH pkh:
|
||||
return ExtractFromPkProvider(pkh.PkProvider, "-[legacy]");
|
||||
case OutputDescriptor.SH sh:
|
||||
@ -79,11 +83,9 @@ namespace BTCPayServer
|
||||
throw new FormatException("sh descriptors are only supported with multsig(legacy or p2wsh) and segwit(p2wpkh)");
|
||||
case OutputDescriptor.WPKH wpkh:
|
||||
return ExtractFromPkProvider(wpkh.PkProvider, "");
|
||||
case OutputDescriptor.WSH wsh:
|
||||
if (wsh.Inner is OutputDescriptor.Multi)
|
||||
{
|
||||
return ParseOutputDescriptor(wsh.Inner.ToString());
|
||||
}
|
||||
case OutputDescriptor.WSH { Inner: OutputDescriptor.Multi multi }:
|
||||
return ExtractFromMulti(multi);
|
||||
case OutputDescriptor.WSH:
|
||||
throw new FormatException("wsh descriptors are only supported with multisig");
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(outputDescriptor));
|
||||
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using BTCPayServer.Payments;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
@ -16,17 +17,18 @@ namespace BTCPayServer
|
||||
{
|
||||
public static DerivationSchemeSettings Parse(string derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
string error = null;
|
||||
ArgumentNullException.ThrowIfNull(network);
|
||||
ArgumentNullException.ThrowIfNull(derivationStrategy);
|
||||
var result = new DerivationSchemeSettings();
|
||||
result.Network = network;
|
||||
var parser = new DerivationSchemeParser(network);
|
||||
if (TryParseXpub(derivationStrategy, parser, ref result, false) || TryParseXpub(derivationStrategy, parser, ref result, true))
|
||||
if (TryParseXpub(derivationStrategy, parser, ref result, ref error, false) || TryParseXpub(derivationStrategy, parser, ref result, ref error, true))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new FormatException("Invalid Derivation Scheme");
|
||||
throw new FormatException($"Invalid Derivation Scheme: {error}");
|
||||
}
|
||||
|
||||
public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy)
|
||||
@ -47,10 +49,11 @@ namespace BTCPayServer
|
||||
{
|
||||
return AccountDerivation is null ? null : DBUtils.nbxv1_get_wallet_id(Network.CryptoCode, AccountDerivation.ToString());
|
||||
}
|
||||
private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, bool electrum = true)
|
||||
private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, ref string error, bool electrum = true)
|
||||
{
|
||||
if (!electrum)
|
||||
{
|
||||
var isOD = Regex.Match(xpub, @"\(.*?\)").Success;
|
||||
try
|
||||
{
|
||||
var result = derivationSchemeParser.ParseOutputDescriptor(xpub);
|
||||
@ -64,9 +67,13 @@ namespace BTCPayServer
|
||||
}).ToArray();
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception exception)
|
||||
{
|
||||
// ignored
|
||||
error = exception.Message;
|
||||
if (isOD)
|
||||
{
|
||||
return false;
|
||||
} // otherwise continue and try to parse input as xpub
|
||||
}
|
||||
}
|
||||
try
|
||||
@ -82,20 +89,22 @@ namespace BTCPayServer
|
||||
derivationSchemeSettings.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception exception)
|
||||
{
|
||||
error = exception.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryParseFromWalletFile(string fileContents, BTCPayNetwork network, out DerivationSchemeSettings settings)
|
||||
public static bool TryParseFromWalletFile(string fileContents, BTCPayNetwork network, out DerivationSchemeSettings settings, out string error)
|
||||
{
|
||||
settings = null;
|
||||
error = null;
|
||||
ArgumentNullException.ThrowIfNull(fileContents);
|
||||
ArgumentNullException.ThrowIfNull(network);
|
||||
var result = new DerivationSchemeSettings();
|
||||
var derivationSchemeParser = new DerivationSchemeParser(network);
|
||||
JObject jobj = null;
|
||||
JObject jobj;
|
||||
try
|
||||
{
|
||||
if (HexEncoder.IsWellFormed(fileContents))
|
||||
@ -107,8 +116,8 @@ namespace BTCPayServer
|
||||
catch
|
||||
{
|
||||
result.Source = "GenericFile";
|
||||
if (TryParseXpub(fileContents, derivationSchemeParser, ref result) ||
|
||||
TryParseXpub(fileContents, derivationSchemeParser, ref result, false))
|
||||
if (TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error) ||
|
||||
TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error, false))
|
||||
{
|
||||
settings = result;
|
||||
settings.Network = network;
|
||||
@ -125,7 +134,7 @@ namespace BTCPayServer
|
||||
jobj = (JObject)jobj["keystore"];
|
||||
|
||||
if (!jobj.ContainsKey("xpub") ||
|
||||
!TryParseXpub(jobj["xpub"].Value<string>(), derivationSchemeParser, ref result))
|
||||
!TryParseXpub(jobj["xpub"].Value<string>(), derivationSchemeParser, ref result, ref error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -162,7 +171,7 @@ namespace BTCPayServer
|
||||
{
|
||||
result.Source = "SpecterFile";
|
||||
|
||||
if (!TryParseXpub(jobj["descriptor"].Value<string>(), derivationSchemeParser, ref result, false))
|
||||
if (!TryParseXpub(jobj["descriptor"].Value<string>(), derivationSchemeParser, ref result, ref error, false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -181,7 +190,7 @@ namespace BTCPayServer
|
||||
{
|
||||
result.Source = "WasabiFile";
|
||||
if (!jobj.ContainsKey("ExtPubKey") ||
|
||||
!TryParseXpub(jobj["ExtPubKey"].Value<string>(), derivationSchemeParser, ref result, false))
|
||||
!TryParseXpub(jobj["ExtPubKey"].Value<string>(), derivationSchemeParser, ref result, ref error, false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
34
BTCPayServer/Forms/FormComponentProviders.cs
Normal file
34
BTCPayServer/Forms/FormComponentProviders.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class FormComponentProviders
|
||||
{
|
||||
private readonly IEnumerable<IFormComponentProvider> _formComponentProviders;
|
||||
|
||||
public Dictionary<string, IFormComponentProvider> TypeToComponentProvider = new Dictionary<string, IFormComponentProvider>();
|
||||
|
||||
public FormComponentProviders(IEnumerable<IFormComponentProvider> formComponentProviders)
|
||||
{
|
||||
_formComponentProviders = formComponentProviders;
|
||||
foreach (var prov in _formComponentProviders)
|
||||
prov.Register(TypeToComponentProvider);
|
||||
}
|
||||
|
||||
public bool Validate(Form form, ModelStateDictionary modelState)
|
||||
{
|
||||
foreach (var field in form.Fields)
|
||||
{
|
||||
if (TypeToComponentProvider.TryGetValue(field.Type, out var provider))
|
||||
{
|
||||
provider.Validate(form, field);
|
||||
foreach (var err in field.ValidationErrors)
|
||||
modelState.TryAddModelError(field.Name, err);
|
||||
}
|
||||
}
|
||||
return modelState.IsValid;
|
||||
}
|
||||
}
|
22
BTCPayServer/Forms/FormDataExtensions.cs
Normal file
22
BTCPayServer/Forms/FormDataExtensions.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using BTCPayServer.Data.Data;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public static class FormDataExtensions
|
||||
{
|
||||
public static void AddForms(this IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddSingleton<FormDataService>();
|
||||
serviceCollection.AddSingleton<FormComponentProviders>();
|
||||
serviceCollection.AddSingleton<IFormComponentProvider, HtmlInputFormProvider>();
|
||||
serviceCollection.AddSingleton<IFormComponentProvider, HtmlFieldsetFormProvider>();
|
||||
}
|
||||
|
||||
public static string Serialize(this JObject form)
|
||||
{
|
||||
return JsonConvert.SerializeObject(form);
|
||||
}
|
||||
}
|
35
BTCPayServer/Forms/FormDataService.cs
Normal file
35
BTCPayServer/Forms/FormDataService.cs
Normal file
@ -0,0 +1,35 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Data.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class FormDataService
|
||||
{
|
||||
|
||||
public static readonly Form StaticFormEmail = new()
|
||||
{
|
||||
Fields = new List<Field>() {Field.Create("Enter your email", "buyerEmail", null, true, null, "email")}
|
||||
};
|
||||
|
||||
public static readonly Form StaticFormAddress = new()
|
||||
{
|
||||
Fields = new List<Field>()
|
||||
{
|
||||
Field.Create("Enter your email", "buyerEmail", null, true, null, "email"),
|
||||
Field.Create("Name", "buyerName", null, true, null),
|
||||
Field.Create("Address Line 1", "buyerAddress1", null, true, null),
|
||||
Field.Create("Address Line 2", "buyerAddress2", null, false, null),
|
||||
Field.Create("City", "buyerCity", null, true, null),
|
||||
Field.Create("Postcode", "buyerZip", null, false, null),
|
||||
Field.Create("State", "buyerState", null, false, null),
|
||||
Field.Create("Country", "buyerCountry", null, true, null)
|
||||
}
|
||||
};
|
||||
}
|
23
BTCPayServer/Forms/HtmlFieldsetFormProvider.cs
Normal file
23
BTCPayServer/Forms/HtmlFieldsetFormProvider.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class HtmlFieldsetFormProvider: IFormComponentProvider
|
||||
{
|
||||
public string View => "Forms/FieldSetElement";
|
||||
|
||||
public void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
|
||||
{
|
||||
typeToComponentProvider.Add("fieldset", this);
|
||||
}
|
||||
|
||||
public void Validate(Field field)
|
||||
{
|
||||
}
|
||||
|
||||
public void Validate(Form form, Field field)
|
||||
{
|
||||
}
|
||||
}
|
51
BTCPayServer/Forms/HtmlInputFormProvider.cs
Normal file
51
BTCPayServer/Forms/HtmlInputFormProvider.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Validation;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class HtmlInputFormProvider: FormComponentProviderBase
|
||||
{
|
||||
public override void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
|
||||
{
|
||||
foreach (var t in new[] {
|
||||
"text",
|
||||
"radio",
|
||||
"checkbox",
|
||||
"password",
|
||||
"file",
|
||||
"hidden",
|
||||
"button",
|
||||
"submit",
|
||||
"color",
|
||||
"date",
|
||||
"datetime-local",
|
||||
"month",
|
||||
"week",
|
||||
"time",
|
||||
"email",
|
||||
"image",
|
||||
"number",
|
||||
"range",
|
||||
"search",
|
||||
"url",
|
||||
"tel",
|
||||
"reset"})
|
||||
typeToComponentProvider.Add(t, this);
|
||||
}
|
||||
public override string View => "Forms/InputElement";
|
||||
|
||||
public override void Validate(Form form, Field field)
|
||||
{
|
||||
if (field.Required)
|
||||
{
|
||||
ValidateField<RequiredAttribute>(field);
|
||||
}
|
||||
if (field.Type == "email")
|
||||
{
|
||||
ValidateField<MailboxAddressAttribute>(field);
|
||||
}
|
||||
}
|
||||
}
|
26
BTCPayServer/Forms/IFormComponentProvider.cs
Normal file
26
BTCPayServer/Forms/IFormComponentProvider.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public interface IFormComponentProvider
|
||||
{
|
||||
string View { get; }
|
||||
void Validate(Form form, Field field);
|
||||
void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider);
|
||||
}
|
||||
|
||||
public abstract class FormComponentProviderBase : IFormComponentProvider
|
||||
{
|
||||
public abstract string View { get; }
|
||||
public abstract void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider);
|
||||
public abstract void Validate(Form form, Field field);
|
||||
|
||||
public void ValidateField<T>(Field field) where T : ValidationAttribute, new()
|
||||
{
|
||||
var result = new T().GetValidationResult(field.Value, new ValidationContext(field) { DisplayName = field.Label, MemberName = field.Name });
|
||||
if (result != null)
|
||||
field.ValidationErrors.Add(result.ErrorMessage);
|
||||
}
|
||||
}
|
13
BTCPayServer/Forms/Models/FormViewModel.cs
Normal file
13
BTCPayServer/Forms/Models/FormViewModel.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Data.Data;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Forms.Models;
|
||||
|
||||
public class FormViewModel
|
||||
{
|
||||
public string RedirectUrl { get; set; }
|
||||
public FormData FormData { get; set; }
|
||||
Form _Form;
|
||||
public Form Form { get => _Form ??= Form.Parse(FormData.Config); }
|
||||
}
|
11
BTCPayServer/Forms/ModifyForm.cs
Normal file
11
BTCPayServer/Forms/ModifyForm.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class ModifyForm
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
[DisplayName("Form configuration (JSON)")]
|
||||
public string FormConfig { get; set; }
|
||||
}
|
109
BTCPayServer/Forms/UIFormsController.cs
Normal file
109
BTCPayServer/Forms/UIFormsController.cs
Normal file
@ -0,0 +1,109 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data.Data;
|
||||
using BTCPayServer.Forms.Models;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public class UIFormsController : Controller
|
||||
{
|
||||
private FormComponentProviders FormProviders { get; }
|
||||
|
||||
public UIFormsController(FormComponentProviders formProviders)
|
||||
{
|
||||
FormProviders = formProviders;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("~/forms/{formId}")]
|
||||
[HttpPost("~/forms")]
|
||||
public IActionResult ViewPublicForm(string? formId, string? redirectUrl)
|
||||
{
|
||||
if (!IsValidRedirectUri(redirectUrl))
|
||||
return BadRequest();
|
||||
|
||||
FormData? formData = string.IsNullOrEmpty(formId) ? null : GetFormData(formId);
|
||||
if (formData == null)
|
||||
{
|
||||
return string.IsNullOrEmpty(redirectUrl)
|
||||
? NotFound()
|
||||
: Redirect(redirectUrl);
|
||||
}
|
||||
|
||||
return GetFormView(formData, redirectUrl);
|
||||
}
|
||||
|
||||
ViewResult GetFormView(FormData formData, string? redirectUrl)
|
||||
{
|
||||
return View("View", new FormViewModel { FormData = formData, RedirectUrl = redirectUrl });
|
||||
}
|
||||
[AllowAnonymous]
|
||||
[HttpPost("~/forms/{formId}")]
|
||||
public IActionResult SubmitForm(string formId, string? redirectUrl, string? command)
|
||||
{
|
||||
if (!IsValidRedirectUri(redirectUrl))
|
||||
return BadRequest();
|
||||
|
||||
var formData = GetFormData(formId);
|
||||
if (formData?.Config is null)
|
||||
return NotFound();
|
||||
|
||||
if (!Request.HasFormContentType)
|
||||
return GetFormView(formData, redirectUrl);
|
||||
|
||||
var conf = Form.Parse(formData.Config);
|
||||
conf.ApplyValuesFromForm(Request.Form);
|
||||
if (!FormProviders.Validate(conf, ModelState))
|
||||
return GetFormView(formData, redirectUrl);
|
||||
|
||||
var form = new MultiValueDictionary<string, string>();
|
||||
foreach (var kv in Request.Form)
|
||||
form.Add(kv.Key, kv.Value);
|
||||
|
||||
// With redirect, the form comes from another entity that we need to send the data back to
|
||||
if (!string.IsNullOrEmpty(redirectUrl))
|
||||
{
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
FormUrl = redirectUrl,
|
||||
FormParameters = form
|
||||
});
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
internal static FormData? GetFormData(string id)
|
||||
{
|
||||
FormData? form = id switch
|
||||
{
|
||||
{ } formId when formId == GenericFormOption.Address.ToString() => new FormData
|
||||
{
|
||||
Config = FormDataService.StaticFormAddress.ToString(),
|
||||
Id = GenericFormOption.Address.ToString(),
|
||||
Name = "Provide your address",
|
||||
},
|
||||
{ } formId when formId == GenericFormOption.Email.ToString() => new FormData
|
||||
{
|
||||
Config = FormDataService.StaticFormEmail.ToString(),
|
||||
Id = GenericFormOption.Email.ToString(),
|
||||
Name = "Provide your email address",
|
||||
},
|
||||
_ => null
|
||||
};
|
||||
return form;
|
||||
}
|
||||
|
||||
private bool IsValidRedirectUri(string? redirectUrl) =>
|
||||
!string.IsNullOrEmpty(redirectUrl) && Uri.TryCreate(redirectUrl, UriKind.RelativeOrAbsolute, out var uri) &&
|
||||
(Url.IsLocalUrl(redirectUrl) || uri.Host.Equals(Request.Host.Host));
|
||||
}
|
@ -14,6 +14,8 @@ namespace BTCPayServer.HostedServices
|
||||
protected Task[] _Tasks;
|
||||
public readonly Logs Logs;
|
||||
|
||||
public bool NoLogsOnExit { get; set; }
|
||||
|
||||
protected BaseAsyncService(Logs logs)
|
||||
{
|
||||
Logs = logs;
|
||||
@ -77,7 +79,8 @@ namespace BTCPayServer.HostedServices
|
||||
if (_Tasks != null)
|
||||
await Task.WhenAll(_Tasks);
|
||||
}
|
||||
Logs.PayServer.LogInformation($"{this.GetType().Name} successfully exited...");
|
||||
if (!NoLogsOnExit)
|
||||
Logs.PayServer.LogInformation($"{this.GetType().Name} successfully exited...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -173,10 +173,10 @@ next:
|
||||
db.WalletObjectLinks.Add(new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = tx.WalletDataId,
|
||||
ChildType = Data.WalletObjectData.Types.Tx,
|
||||
ChildId = tx.TransactionId,
|
||||
ParentType = Data.WalletObjectData.Types.Label,
|
||||
ParentId = labelId
|
||||
BType = Data.WalletObjectData.Types.Tx,
|
||||
BId = tx.TransactionId,
|
||||
AType = Data.WalletObjectData.Types.Label,
|
||||
AId = labelId
|
||||
});
|
||||
|
||||
if (label.Value is ReferenceLabel reflabel)
|
||||
@ -195,10 +195,10 @@ next:
|
||||
db.WalletObjectLinks.Add(new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = tx.WalletDataId,
|
||||
ChildType = Data.WalletObjectData.Types.Tx,
|
||||
ChildId = tx.TransactionId,
|
||||
ParentType = reflabel.Type,
|
||||
ParentId = reflabel.Reference ?? String.Empty
|
||||
BType = Data.WalletObjectData.Types.Tx,
|
||||
BId = tx.TransactionId,
|
||||
AType = reflabel.Type,
|
||||
AId = reflabel.Reference ?? String.Empty
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -224,10 +224,10 @@ next:
|
||||
db.WalletObjectLinks.Add(new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = tx.WalletDataId,
|
||||
ChildType = Data.WalletObjectData.Types.Tx,
|
||||
ChildId = tx.TransactionId,
|
||||
ParentType = "payout",
|
||||
ParentId = payout
|
||||
BType = Data.WalletObjectData.Types.Tx,
|
||||
BId = tx.TransactionId,
|
||||
AType = "payout",
|
||||
AId = payout
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -79,10 +79,12 @@ namespace BTCPayServer.HostedServices
|
||||
OldRevision
|
||||
}
|
||||
|
||||
public record ApprovalResult(Result Result, decimal? CryptoAmount);
|
||||
|
||||
public string PayoutId { get; set; }
|
||||
public int Revision { get; set; }
|
||||
public decimal Rate { get; set; }
|
||||
internal TaskCompletionSource<Result> Completion { get; set; }
|
||||
internal TaskCompletionSource<ApprovalResult> Completion { get; set; }
|
||||
|
||||
public static string GetErrorMessage(Result result)
|
||||
{
|
||||
@ -333,10 +335,10 @@ namespace BTCPayServer.HostedServices
|
||||
return _rateFetcher.FetchRate(rule, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<PayoutApproval.Result> Approve(PayoutApproval approval)
|
||||
public Task<PayoutApproval.ApprovalResult> Approve(PayoutApproval approval)
|
||||
{
|
||||
approval.Completion =
|
||||
new TaskCompletionSource<PayoutApproval.Result>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
new TaskCompletionSource<PayoutApproval.ApprovalResult>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
if (!_Channel.Writer.TryWrite(approval))
|
||||
throw new ObjectDisposedException(nameof(PullPaymentHostedService));
|
||||
return approval.Completion.Task;
|
||||
@ -351,26 +353,26 @@ namespace BTCPayServer.HostedServices
|
||||
.FirstOrDefaultAsync();
|
||||
if (payout is null)
|
||||
{
|
||||
req.Completion.SetResult(PayoutApproval.Result.NotFound);
|
||||
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.NotFound, null));
|
||||
return;
|
||||
}
|
||||
|
||||
if (payout.State != PayoutState.AwaitingApproval)
|
||||
{
|
||||
req.Completion.SetResult(PayoutApproval.Result.InvalidState);
|
||||
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.InvalidState, null));
|
||||
return;
|
||||
}
|
||||
|
||||
var payoutBlob = payout.GetBlob(this._jsonSerializerSettings);
|
||||
if (payoutBlob.Revision != req.Revision)
|
||||
{
|
||||
req.Completion.SetResult(PayoutApproval.Result.OldRevision);
|
||||
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.OldRevision, null));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PaymentMethodId.TryParse(payout.PaymentMethodId, out var paymentMethod))
|
||||
{
|
||||
req.Completion.SetResult(PayoutApproval.Result.NotFound);
|
||||
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.NotFound, null));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -388,7 +390,7 @@ namespace BTCPayServer.HostedServices
|
||||
await payoutHandler.GetMinimumPayoutAmount(paymentMethod, dest.destination);
|
||||
if (cryptoAmount < minimumCryptoAmount)
|
||||
{
|
||||
req.Completion.TrySetResult(PayoutApproval.Result.TooLowAmount);
|
||||
req.Completion.TrySetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.TooLowAmount, null));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -397,7 +399,7 @@ namespace BTCPayServer.HostedServices
|
||||
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
req.Completion.SetResult(PayoutApproval.Result.Ok);
|
||||
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.Ok, payoutBlob.CryptoAmount));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -566,18 +568,20 @@ namespace BTCPayServer.HostedServices
|
||||
var rateResult = await GetRate(payout, null, CancellationToken.None);
|
||||
if (rateResult.BidAsk != null)
|
||||
{
|
||||
var approveResult = new TaskCompletionSource<PayoutApproval.Result>();
|
||||
var approveResultTask = new TaskCompletionSource<PayoutApproval.ApprovalResult>();
|
||||
await HandleApproval(new PayoutApproval()
|
||||
{
|
||||
PayoutId = payout.Id,
|
||||
Revision = payoutBlob.Revision,
|
||||
Rate = rateResult.BidAsk.Ask,
|
||||
Completion = approveResult
|
||||
Completion = approveResultTask
|
||||
});
|
||||
|
||||
if ((await approveResult.Task) == PayoutApproval.Result.Ok)
|
||||
var approveResult = await approveResultTask.Task;
|
||||
if (approveResult.Result == PayoutApproval.Result.Ok)
|
||||
{
|
||||
payout.State = PayoutState.AwaitingPayment;
|
||||
payoutBlob.CryptoAmount = approveResult.CryptoAmount;
|
||||
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
@ -15,6 +16,7 @@ using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
@ -22,43 +24,93 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class TransactionLabelMarkerHostedService : EventHostedServiceBase
|
||||
{
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly WalletRepository _walletRepository;
|
||||
|
||||
public TransactionLabelMarkerHostedService(EventAggregator eventAggregator, WalletRepository walletRepository, Logs logs) :
|
||||
public BTCPayNetworkProvider NetworkProvider { get; }
|
||||
|
||||
public TransactionLabelMarkerHostedService(BTCPayNetworkProvider networkProvider, EventAggregator eventAggregator, WalletRepository walletRepository, Logs logs) :
|
||||
base(eventAggregator, logs)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
NetworkProvider = networkProvider;
|
||||
_walletRepository = walletRepository;
|
||||
}
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
Subscribe<InvoiceEvent>();
|
||||
Subscribe<NewOnChainTransactionEvent>();
|
||||
}
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is InvoiceEvent invoiceEvent && invoiceEvent.Name == InvoiceEvent.ReceivedPayment &&
|
||||
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance &&
|
||||
invoiceEvent.Payment.GetCryptoPaymentData() is BitcoinLikePaymentData bitcoinLikePaymentData)
|
||||
switch (evt)
|
||||
{
|
||||
var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode());
|
||||
var transactionId = bitcoinLikePaymentData.Outpoint.Hash;
|
||||
var labels = new List<Attachment>
|
||||
{
|
||||
Attachment.Invoice(invoiceEvent.Invoice.Id)
|
||||
};
|
||||
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
|
||||
{
|
||||
labels.Add(Attachment.PaymentRequest(paymentId));
|
||||
}
|
||||
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice))
|
||||
{
|
||||
labels.Add(Attachment.App(appId));
|
||||
}
|
||||
// For each new transaction that we detect, we check if we can find
|
||||
// any utxo or script object matching it.
|
||||
// If we find, then we create a link between them and the tx object.
|
||||
case NewOnChainTransactionEvent transactionEvent:
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(transactionEvent.CryptoCode);
|
||||
var derivation = transactionEvent.NewTransactionEvent.DerivationStrategy;
|
||||
if (network is null || derivation is null)
|
||||
break;
|
||||
var txHash = transactionEvent.NewTransactionEvent.TransactionData.TransactionHash.ToString();
|
||||
|
||||
await _walletRepository.AddWalletTransactionAttachment(walletId, transactionId, labels);
|
||||
// find all wallet objects that fit this transaction
|
||||
// that means see if there are any utxo objects that match in/outs and scripts/addresses that match outs
|
||||
var matchedObjects = transactionEvent.NewTransactionEvent.TransactionData.Transaction.Inputs
|
||||
.Select<TxIn, ObjectTypeId>(txIn => new ObjectTypeId(WalletObjectData.Types.Utxo, txIn.PrevOut.ToString()))
|
||||
.Concat(transactionEvent.NewTransactionEvent.Outputs.SelectMany<NBXplorer.Models.MatchedOutput, ObjectTypeId>(txOut =>
|
||||
|
||||
new[]{
|
||||
new ObjectTypeId(WalletObjectData.Types.Address, GetAddress(derivation, txOut, network).ToString()),
|
||||
new ObjectTypeId(WalletObjectData.Types.Utxo, new OutPoint(transactionEvent.NewTransactionEvent.TransactionData.TransactionHash, (uint)txOut.Index).ToString())
|
||||
|
||||
})).Distinct().ToArray();
|
||||
|
||||
var objs = await _walletRepository.GetWalletObjects(new GetWalletObjectsQuery() { TypesIds = matchedObjects });
|
||||
|
||||
foreach (var walletObjectDatas in objs.GroupBy(data => data.Key.WalletId))
|
||||
{
|
||||
var txWalletObject = new WalletObjectId(walletObjectDatas.Key,
|
||||
WalletObjectData.Types.Tx, txHash);
|
||||
await _walletRepository.EnsureWalletObject(txWalletObject);
|
||||
foreach (var walletObjectData in walletObjectDatas)
|
||||
{
|
||||
await _walletRepository.EnsureWalletObjectLink(txWalletObject, walletObjectData.Key);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case InvoiceEvent {Name: InvoiceEvent.ReceivedPayment} invoiceEvent when
|
||||
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance &&
|
||||
invoiceEvent.Payment.GetCryptoPaymentData() is BitcoinLikePaymentData bitcoinLikePaymentData:
|
||||
{
|
||||
var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode());
|
||||
var transactionId = bitcoinLikePaymentData.Outpoint.Hash;
|
||||
var labels = new List<Attachment>
|
||||
{
|
||||
Attachment.Invoice(invoiceEvent.Invoice.Id)
|
||||
};
|
||||
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
|
||||
{
|
||||
labels.Add(Attachment.PaymentRequest(paymentId));
|
||||
}
|
||||
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice))
|
||||
{
|
||||
labels.Add(Attachment.App(appId));
|
||||
}
|
||||
|
||||
await _walletRepository.AddWalletTransactionAttachment(walletId, transactionId, labels);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BitcoinAddress GetAddress(DerivationStrategyBase derivationStrategy, NBXplorer.Models.MatchedOutput txOut, BTCPayNetwork network)
|
||||
{
|
||||
// Old version of NBX doesn't give address in the event, so we need to guess
|
||||
return (txOut.Address ?? network.NBXplorerNetwork.CreateAddress(derivationStrategy, txOut.KeyPath, txOut.ScriptPubKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -157,6 +157,7 @@ namespace BTCPayServer.HostedServices
|
||||
webhookEvent.DeliveryId = delivery.Id;
|
||||
webhookEvent.WebhookId = webhook.Id;
|
||||
webhookEvent.OriginalDeliveryId = delivery.Id;
|
||||
webhookEvent.Metadata = invoiceEvent.Invoice.Metadata.ToJObject();
|
||||
webhookEvent.IsRedelivery = false;
|
||||
webhookEvent.Timestamp = delivery.Timestamp;
|
||||
var context = new WebhookDeliveryRequest(webhook.Id, webhookEvent, delivery, webhookBlob);
|
||||
|
@ -15,6 +15,7 @@ using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Controllers.Greenfield;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Data.Payouts.LightningLike;
|
||||
using BTCPayServer.Forms;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Logging;
|
||||
@ -431,12 +432,14 @@ namespace BTCPayServer.Hosting
|
||||
services.AddTransient<UIPaymentRequestController>();
|
||||
// Add application services.
|
||||
services.AddSingleton<EmailSenderFactory>();
|
||||
services.AddSingleton<InvoiceActivator>();
|
||||
|
||||
//create a simple client which hooks up to the http scope
|
||||
services.AddScoped<BTCPayServerClient, LocalBTCPayServerClient>();
|
||||
//also provide a factory that can impersonate user/store id
|
||||
services.AddSingleton<IBTCPayServerClientFactory, BTCPayServerClientFactory>();
|
||||
services.AddPayoutProcesors();
|
||||
services.AddForms();
|
||||
|
||||
services.AddAPIKeyAuthentication();
|
||||
services.AddBtcPayServerAuthenticationSchemes();
|
||||
|
@ -19,6 +19,8 @@ using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Storage.Models;
|
||||
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
|
||||
using ExchangeSharp;
|
||||
using Fido2NetLib.Objects;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -87,13 +89,15 @@ namespace BTCPayServer.Hosting
|
||||
var settings = (await _Settings.GetSettingAsync<MigrationSettings>());
|
||||
if (settings is null)
|
||||
{
|
||||
// If it is null, then it's the first run: let's skip all the migrations by migration flags to true
|
||||
settings = new MigrationSettings() { MigratedInvoiceTextSearchPages = int.MaxValue };
|
||||
// If it is null, then it's the first run: let's skip all the migrations by setting flags to true
|
||||
settings = new MigrationSettings() { MigratedInvoiceTextSearchPages = int.MaxValue, MigratedTransactionLabels = int.MaxValue };
|
||||
foreach (var prop in settings.GetType().GetProperties().Where(p => p.CanWrite && p.PropertyType == typeof(bool)))
|
||||
{
|
||||
prop.SetValue(settings, true);
|
||||
}
|
||||
// Ensure these checks still get run
|
||||
settings.CheckedFirstRun = false;
|
||||
settings.FileSystemStorageAsDefault = false;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
|
||||
@ -222,6 +226,21 @@ namespace BTCPayServer.Hosting
|
||||
settings.MigrateWalletColors = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.FileSystemStorageAsDefault)
|
||||
{
|
||||
var storageSettings = await _Settings.GetSettingAsync<StorageSettings>();
|
||||
if (storageSettings is null)
|
||||
{
|
||||
storageSettings = new StorageSettings
|
||||
{
|
||||
Provider = StorageProvider.FileSystem,
|
||||
Configuration = JObject.FromObject(new FileSystemStorageConfiguration())
|
||||
};
|
||||
await _Settings.UpdateSetting(storageSettings);
|
||||
}
|
||||
settings.FileSystemStorageAsDefault = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user