Compare commits

..

47 Commits

Author SHA1 Message Date
71c2bfffc1 Remove untested and unfinished code 2022-11-25 10:02:01 +09:00
4b952648a4 Refactoring: Use PostRedirect instead of TempData for data transfer 2022-11-25 00:24:02 +01:00
8b7b772d34 Add index - needs migration 2022-11-24 19:54:55 +01:00
713b87d07b Update test 2022-11-24 19:43:28 +01:00
82a690645e More minor syntax cleanups 2022-11-24 19:33:43 +01:00
dcade8b4a9 Remove custom form options from select for now 2022-11-24 19:09:41 +01:00
ad17234a86 Minor cleanups 2022-11-24 19:03:00 +01:00
7218c3077d Hide custom form feature in UI 2022-11-24 17:14:23 +01:00
3e1511c0a8 Merge branch 'master' into form_builder 2022-11-24 17:03:47 +01:00
3cb7c64321 Remove useless storeId parameter 2022-11-24 21:36:24 +09:00
9c88c53798 Remove storeId from step form 2022-11-24 21:15:55 +09:00
50d08de78b Fix modify 2022-11-24 21:15:27 +09:00
15ab7c051b Fix query to forms, ensure no permission bypass 2022-11-24 20:34:05 +09:00
4e8126a734 Fix ef request 2022-11-24 18:07:08 +09:00
8195acbbf7 Fix warnings 2022-11-24 17:53:43 +09:00
ec35b75324 Put the Authorize at controller level on UIForms 2022-11-24 17:48:21 +09:00
d65835b0fe Refactor FormQuery to only be able to query single store and single form 2022-11-24 17:42:55 +09:00
426a65d3fe Merge remote-tracking branch 'origin/master' into form_builder
# Conflicts:
#	BTCPayServer/Controllers/UIInvoiceController.UI.cs
#	BTCPayServer/Models/StoreViewModels/CheckoutAppearanceViewModel.cs
#	BTCPayServer/Views/UIInvoice/CheckoutV2.cshtml
#	BTCPayServer/Views/UIStores/CheckoutAppearance.cshtml
2022-11-24 07:23:32 +01:00
2fe2efcb7a make pay request form submission redirect to invoice 2022-11-23 21:53:08 +01:00
f671f8fd26 fix 2022-11-23 07:43:39 +01:00
e0f15c8792 fix migration for forms 2022-11-22 22:58:37 +01:00
4e828f4398 Merge remote-tracking branch 'origin/master' into form_builder
# Conflicts:
#	BTCPayServer/Services/BTCPayServerEnvironment.cs
2022-11-22 22:52:44 +01:00
399fbe2827 add form test 2022-11-22 22:47:01 +01:00
6f547750ae Remove invoice and store level form 2022-11-22 22:04:50 +01:00
f0f09180d5 Fix flaky test (#4330)
* Fix flaky test

* Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs

Co-authored-by: d11n <mail@dennisreimann.de>

Co-authored-by: d11n <mail@dennisreimann.de>
2022-11-22 22:04:34 +01:00
95c6817d2a Add documentation link to plugins (#4329)
* Add documentation link to plugins

* Minor UI updates

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-11-22 22:04:34 +01:00
bb9c0989c6 POS: Fix null pointer
Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`.
2022-11-22 22:04:34 +01:00
fa2a3dd7f3 Change confirmed to settled. (#4328) 2022-11-22 22:04:34 +01:00
86361d0f75 Server Settings: Update Policies page (#4326)
Handles the multiple submit buttons on that page and closes #4319.

Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS.
2022-11-22 22:04:33 +01:00
a056e9b570 fix monero issue 2022-11-22 22:04:33 +01:00
b89918ff5d Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) 2022-11-22 22:04:33 +01:00
37693399d6 Fix warnings in Form builder (#4331)
* Fix build warnings about string?

Enable nullable on UIFormsController.cs
Fixes CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

* Clean up lack of space in injected services in Submit() of UIFormsController.cs

* Remove unused variables (CS0219) and assignment of nullable value to nullable type (CS8600)

* Cleanup double semicolons while we're at tit
2022-11-22 20:26:25 +01:00
bc652507d8 UI updates 2022-11-21 22:44:14 +01:00
d8094e9fee fix pav bug 2022-11-21 11:55:42 +01:00
33c92c1428 general fixes for form system 2022-11-21 11:51:46 +01:00
6747f91d29 move checkoutform id in checkout appearance outside of checkotu v2 toggle 2022-11-21 11:51:46 +01:00
469ff5b11f fix up code 2022-11-21 11:51:46 +01:00
b3bc4e5745 Do not request additional forms on invoice from pay request 2022-11-21 11:51:46 +01:00
44b0addff5 Display form name in inherit from store setting 2022-11-21 11:51:46 +01:00
383520c453 invoice form through receipt page 2022-11-21 11:51:45 +01:00
0c3b345d1b pay request form rough support 2022-11-21 11:51:45 +01:00
3513e602f4 Add support for pos app + forms 2022-11-21 11:51:45 +01:00
cec83ab4ec Make predefined forms usable statically 2022-11-21 11:51:45 +01:00
16537509d9 Update UIFormsController.cs 2022-11-21 11:51:45 +01:00
6bd6a045f4 UI updates 2022-11-21 11:51:45 +01:00
5de665dfff Cleanups 2022-11-21 11:51:45 +01:00
e329368e21 wip 2022-11-21 11:51:38 +01:00
138 changed files with 2444 additions and 4352 deletions

View File

@ -1,58 +0,0 @@
---
name: "\U0001F41B Bug report"
about: Report a bug or a technical issue
---
<!--
Thank you for reporting a technical issue with one of my BTCPay Server plugins, like LNbank or PodServer.
For general issues with BTCPay Server please visit https://github.com/btcpayserver/btcpayserver/issues
General support is available on our community chat chat.btcpayserver.org
Please fill in as much of the template below as you're able.
-->
**Plugin**
Name and version of the plugin. <!--[available on the Server Settings > Plugins page] -->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce the bug**
Steps to reproduce the reported bug:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
-->
**Screenshots**
<!--
If applicable, add screenshots to help explain your problem.
-->
**Your BTCPay Environment (please complete the following information):**
- BTCPay Server Version: <!--[available in the right bottom corner of footer] -->
- Lightning implementation <!--[e.g. LND, Core Lightning] -->
- Deployment Method: <!--[e.g. Docker, Manual, Third-Party-host]-->
- Browser: <!--[e.g. Chrome, Safari]-->
**Logs (if applicable)**
<!--
If you are using the Docker setup, please post the output of the following command:
docker logs generated_btcpayserver_1
Otherwise, basic logs can be found in Server Settings > Logs.
More logs https://docs.btcpayserver.org/Troubleshooting/#2-looking-through-the-logs
-->

68
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,68 @@
name: 🐛 Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! Please provide as much information as you can. It helps us better understand the problem and fix it faster.
- type: textarea
id: version
attributes:
label: What is your BTCPay version?
description: You can see the version in the footer's bottom right corner
placeholder: I'm running BTCPay v1.X.X.X
validations:
required: true
- type: textarea
id: deployment
attributes:
label: How did you deploy BTCPay Server?
description: Docker, manual, third-party host? Read more on deployment methods [here](https://docs.btcpayserver.org/Deployment/)
placeholder: I'm running BTCPay Server on a...
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: A clear and concise description of what the bug is.
placeholder: Tell us what you see!
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: How did you encounter this bug?
description: Step by step describe how did you encounter the bug?
placeholder: 1. I clicked X 2. Then I clicked Y 3. See error
validations:
required: true
- type: textarea
id: logoutput
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. Logs can be found in Server Settings > Logs. Here's how you can [troubleshoot an issue](https://docs.btcpayserver.org/Troubleshooting/)
render: shell
- type: textarea
id: browser
attributes:
label: What browser do you use?
description: Provide your browser and it's version. If you replicated issues on multiple browsers, let us know which ones.
placeholder: For example Safari 15.00, Chrome 10.0, Tor, Edge, etc
validations:
required: false
- type: textarea
id: additonal
attributes:
label: Additional information
description: Feel free to provide additional information. Screenshots are always helpful.
- type: checkboxes
id: terms
attributes:
label: Are you sure this is a bug report?
description: By submitting this report, you agree that this is not a support or a feature request. For general questions please read our [documentation](https://docs.btcpayserver.org). You can ask questions in [discussions](https://github.com/btcpayserver/btcpayserver/discussions) and [on our community chat](https://chat.btcpayserver.org)
options:
- label: I confirm this is a bug report
required: true

6
.gitmodules vendored
View File

@ -1,6 +0,0 @@
[submodule "LNbank"]
path = Plugins/BTCPayServer.Plugins.LNbank
url = git@github.com:dennisreimann/btcpayserver-plugin-lnbank.git
[submodule "PodServer"]
path = Plugins/BTCPayServer.Plugins.PodServer
url = git@github.com:dennisreimann/btcpayserver-plugin-podserver.git

View File

@ -1,18 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Abstractions
{
public class CamelCaseSerializerSettings
{
static CamelCaseSerializerSettings()
{
Settings = new JsonSerializerSettings()
{
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
};
Serializer = JsonSerializer.Create(Settings);
}
public static readonly JsonSerializerSettings Settings;
public static readonly JsonSerializer Serializer;
}
}

View File

@ -2,7 +2,6 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -10,50 +9,16 @@ namespace BTCPayServer.Abstractions.Form;
public class Field
{
public static Field Create(string label, string name, string value, bool required, string helpText, string type = "text")
{
return new Field()
{
Label = label,
Name = name,
Value = value,
OriginalValue = value,
Required = required,
HelpText = helpText,
Type = type
};
}
// The name of the HTML5 node. Should be used as the key for the posted data.
public string Name;
public bool Hidden;
// HTML5 compatible type string like "text", "textarea", "email", "password", etc. Each type is a class and may contain more fields (i.e. "select" would have options).
public string Type;
public static Field CreateFieldset()
{
return new Field() { Type = "fieldset" };
}
// The 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>();
@ -61,4 +26,9 @@ public class Field
{
return ValidationErrors.Count == 0 && Fields.All(field => field.IsValid());
}
[JsonExtensionData] public IDictionary<string, JToken> AdditionalData { get; set; }
public List<Field> Fields { get; set; } = new();
}

View File

@ -0,0 +1,12 @@
namespace BTCPayServer.Abstractions.Form;
public class Fieldset : Field
{
public bool Hidden { get; set; }
public string Label { get; set; }
public Fieldset()
{
Type = "fieldset";
}
}

View File

@ -2,24 +2,12 @@ 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();
@ -29,7 +17,7 @@ public class Form
// Are all the fields valid in the form?
public bool IsValid()
{
return Fields.Select(f => f.IsValid()).All(o => o);
return Fields.All(field => field.IsValid());
}
public Field GetFieldByName(string name)
@ -64,7 +52,6 @@ public class Form
}
return null;
}
public List<string> GetAllNames()
{
return GetAllNames(Fields);

View File

@ -0,0 +1,27 @@
namespace BTCPayServer.Abstractions.Form;
public class HtmlInputField : Field
{
// 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;
public bool Required;
public HtmlInputField(string label, string name, string value, bool required, string helpText, string type = "text")
{
Label = label;
Name = name;
Value = value;
OriginalValue = value;
Required = required;
HelpText = helpText;
Type = type;
}
// TODO JSON parsing from string to objects again probably won't work out of the box because of the different field types.
}

View File

@ -1,12 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
@ -15,7 +8,7 @@ namespace BTCPayServer.Abstractions.TagHelpers;
// Make sure that <svg><use href=/ are correctly working if rootpath is present
[HtmlTargetElement("use", Attributes = "href")]
public class SVGUse : UrlResolutionTagHelper2
public class SVGUse : UrlResolutionTagHelper
{
private readonly IFileVersionProvider _fileVersionProvider;
@ -28,6 +21,5 @@ public class SVGUse : UrlResolutionTagHelper2
var attr = output.Attributes["href"].Value.ToString();
attr = _fileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, attr);
output.Attributes.SetAttribute("href", attr);
base.Process(context, output);
}
}
}

View File

@ -1,314 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace BTCPayServer.Abstractions.TagHelpers
{
// A copy of https://github.com/dotnet/aspnetcore/blob/39f0e0b8f40b4754418f81aef0de58a9204a1fe5/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs
// slightly modified to also work on use tag.
public class UrlResolutionTagHelper2 : TagHelper
{
// Valid whitespace characters defined by the HTML5 spec.
private static readonly char[] ValidAttributeWhitespaceChars =
new[] { '\t', '\n', '\u000C', '\r', ' ' };
private static readonly Dictionary<string, string[]> ElementAttributeLookups =
new(StringComparer.OrdinalIgnoreCase)
{
{ "use", new[] { "href" } },
{ "a", new[] { "href" } },
{ "applet", new[] { "archive" } },
{ "area", new[] { "href" } },
{ "audio", new[] { "src" } },
{ "base", new[] { "href" } },
{ "blockquote", new[] { "cite" } },
{ "button", new[] { "formaction" } },
{ "del", new[] { "cite" } },
{ "embed", new[] { "src" } },
{ "form", new[] { "action" } },
{ "html", new[] { "manifest" } },
{ "iframe", new[] { "src" } },
{ "img", new[] { "src", "srcset" } },
{ "input", new[] { "src", "formaction" } },
{ "ins", new[] { "cite" } },
{ "link", new[] { "href" } },
{ "menuitem", new[] { "icon" } },
{ "object", new[] { "archive", "data" } },
{ "q", new[] { "cite" } },
{ "script", new[] { "src" } },
{ "source", new[] { "src", "srcset" } },
{ "track", new[] { "src" } },
{ "video", new[] { "poster", "src" } },
};
/// <summary>
/// Creates a new <see cref="UrlResolutionTagHelper"/>.
/// </summary>
/// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param>
/// <param name="htmlEncoder">The <see cref="HtmlEncoder"/>.</param>
public UrlResolutionTagHelper2(IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder)
{
UrlHelperFactory = urlHelperFactory;
HtmlEncoder = htmlEncoder;
}
/// <inheritdoc />
public override int Order => -1000 - 999;
/// <summary>
/// The <see cref="IUrlHelperFactory"/>.
/// </summary>
protected IUrlHelperFactory UrlHelperFactory { get; }
/// <summary>
/// The <see cref="HtmlEncoder"/>.
/// </summary>
protected HtmlEncoder HtmlEncoder { get; }
/// <summary>
/// The <see cref="ViewContext"/>.
/// </summary>
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; } = default!;
/// <inheritdoc />
public override void Process(TagHelperContext context, TagHelperOutput output)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(output);
if (output.TagName == null)
{
return;
}
if (ElementAttributeLookups.TryGetValue(output.TagName, out var attributeNames))
{
for (var i = 0; i < attributeNames.Length; i++)
{
ProcessUrlAttribute(attributeNames[i], output);
}
}
// itemid can be present on any HTML element.
ProcessUrlAttribute("itemid", output);
}
/// <summary>
/// Resolves and updates URL values starting with '~/' (relative to the application's 'webroot' setting) for
/// <paramref name="output"/>'s <see cref="TagHelperOutput.Attributes"/> whose
/// <see cref="TagHelperAttribute.Name"/> is <paramref name="attributeName"/>.
/// </summary>
/// <param name="attributeName">The attribute name used to lookup values to resolve.</param>
/// <param name="output">The <see cref="TagHelperOutput"/>.</param>
protected void ProcessUrlAttribute(string attributeName, TagHelperOutput output)
{
ArgumentNullException.ThrowIfNull(attributeName);
ArgumentNullException.ThrowIfNull(output);
var attributes = output.Attributes;
// Read interface .Count once rather than per iteration
var attributesCount = attributes.Count;
for (var i = 0; i < attributesCount; i++)
{
var attribute = attributes[i];
if (!string.Equals(attribute.Name, attributeName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (attribute.Value is string stringValue)
{
if (TryResolveUrl(stringValue, resolvedUrl: out string? resolvedUrl))
{
attributes[i] = new TagHelperAttribute(
attribute.Name,
resolvedUrl,
attribute.ValueStyle);
}
}
else
{
if (attribute.Value is IHtmlContent htmlContent)
{
var htmlString = htmlContent as HtmlString;
if (htmlString != null)
{
// No need for a StringWriter in this case.
stringValue = htmlString.ToString();
}
else
{
using var writer = new StringWriter();
htmlContent.WriteTo(writer, HtmlEncoder);
stringValue = writer.ToString();
}
if (TryResolveUrl(stringValue, resolvedUrl: out IHtmlContent? resolvedUrl))
{
attributes[i] = new TagHelperAttribute(
attribute.Name,
resolvedUrl,
attribute.ValueStyle);
}
else if (htmlString == null)
{
// Not a ~/ URL. Just avoid re-encoding the attribute value later.
attributes[i] = new TagHelperAttribute(
attribute.Name,
new HtmlString(stringValue),
attribute.ValueStyle);
}
}
}
}
}
/// <summary>
/// Tries to resolve the given <paramref name="url"/> value relative to the application's 'webroot' setting.
/// </summary>
/// <param name="url">The URL to resolve.</param>
/// <param name="resolvedUrl">Absolute URL beginning with the application's virtual root. <c>null</c> if
/// <paramref name="url"/> could not be resolved.</param>
/// <returns><c>true</c> if the <paramref name="url"/> could be resolved; <c>false</c> otherwise.</returns>
protected bool TryResolveUrl(string url, out string? resolvedUrl)
{
resolvedUrl = null;
var start = FindRelativeStart(url);
if (start == -1)
{
return false;
}
var trimmedUrl = CreateTrimmedString(url, start);
var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
resolvedUrl = urlHelper.Content(trimmedUrl);
return true;
}
/// <summary>
/// Tries to resolve the given <paramref name="url"/> value relative to the application's 'webroot' setting.
/// </summary>
/// <param name="url">The URL to resolve.</param>
/// <param name="resolvedUrl">
/// Absolute URL beginning with the application's virtual root. <c>null</c> if <paramref name="url"/> could
/// not be resolved.
/// </param>
/// <returns><c>true</c> if the <paramref name="url"/> could be resolved; <c>false</c> otherwise.</returns>
protected bool TryResolveUrl(string url, [NotNullWhen(true)] out IHtmlContent? resolvedUrl)
{
resolvedUrl = null;
var start = FindRelativeStart(url);
if (start == -1)
{
return false;
}
var trimmedUrl = CreateTrimmedString(url, start);
var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
var appRelativeUrl = urlHelper.Content(trimmedUrl);
var postTildeSlashUrlValue = trimmedUrl.Substring(2);
if (!appRelativeUrl.EndsWith(postTildeSlashUrlValue, StringComparison.Ordinal))
{
throw new InvalidOperationException();
}
resolvedUrl = new EncodeFirstSegmentContent(
appRelativeUrl,
appRelativeUrl.Length - postTildeSlashUrlValue.Length,
postTildeSlashUrlValue);
return true;
}
private static int FindRelativeStart(string url)
{
if (url == null || url.Length < 2)
{
return -1;
}
var maxTestLength = url.Length - 2;
var start = 0;
for (; start < url.Length; start++)
{
if (start > maxTestLength)
{
return -1;
}
if (!IsCharWhitespace(url[start]))
{
break;
}
}
// Before doing more work, ensure that the URL we're looking at is app-relative.
if (url[start] != '~' || url[start + 1] != '/')
{
return -1;
}
return start;
}
private static string CreateTrimmedString(string input, int start)
{
var end = input.Length - 1;
for (; end >= start; end--)
{
if (!IsCharWhitespace(input[end]))
{
break;
}
}
var len = end - start + 1;
// Substring returns same string if start == 0 && len == Length
return input.Substring(start, len);
}
private static bool IsCharWhitespace(char ch)
{
return ValidAttributeWhitespaceChars.AsSpan().IndexOf(ch) != -1;
}
private sealed class EncodeFirstSegmentContent : IHtmlContent
{
private readonly string _firstSegment;
private readonly int _firstSegmentLength;
private readonly string _secondSegment;
public EncodeFirstSegmentContent(string firstSegment, int firstSegmentLength, string secondSegment)
{
_firstSegment = firstSegment;
_firstSegmentLength = firstSegmentLength;
_secondSegment = secondSegment;
}
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{
encoder.Encode(writer, _firstSegment, 0, _firstSegmentLength);
writer.Write(_secondSegment);
}
}
}
}

View File

@ -14,7 +14,7 @@
<RepositoryType>git</RepositoryType>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.7.1</Version>
<Version Condition=" '$(Version)' == '' ">1.7.0</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@ -28,8 +28,8 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.16" />
<PackageReference Include="NBitcoin" Version="7.0.20" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.15" />
<PackageReference Include="NBitcoin" Version="7.0.14" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>

View File

@ -128,18 +128,5 @@ namespace BTCPayServer.Client
method: HttpMethod.Post), token);
await HandleResponse(response);
}
public virtual async Task<PullPaymentData> RefundInvoice(
string storeId,
string invoiceId,
RefundInvoiceRequest request,
CancellationToken token = default
)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/refund", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<PullPaymentData>(response);
}
}
}

View File

@ -27,6 +27,6 @@ namespace BTCPayServer.Client.Models
public string FormId { get; set; }
public JObject FormResponse { get; set; }
public string FormResponse { get; set; }
}
}

View File

@ -1,27 +0,0 @@
#nullable enable
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models
{
public enum RefundVariant
{
RateThen,
CurrentRate,
Fiat,
Custom
}
public class RefundInvoiceRequest
{
public string? Name { get; set; } = null;
public string? PaymentMethod { get; set; }
public string? Description { get; set; } = null;
[JsonConverter(typeof(StringEnumConverter))]
public RefundVariant? RefundVariant { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? CustomAmount { get; set; }
public string? CustomCurrency { get; set; }
}
}

View File

@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models
{
@ -16,7 +19,6 @@ namespace BTCPayServer.Client.Models
[JsonProperty(Order = 1)] public string StoreId { get; set; }
[JsonProperty(Order = 2)] public string InvoiceId { get; set; }
[JsonProperty(Order = 3)] public JObject Metadata { get; set; }
}
public class WebhookInvoiceSettledEvent : WebhookInvoiceEvent

View File

@ -105,10 +105,10 @@ namespace BTCPayServer.Data
//PlannedTransaction.OnModelCreating(builder);
PullPaymentData.OnModelCreating(builder);
RefundData.OnModelCreating(builder);
SettingData.OnModelCreating(builder, Database);
//SettingData.OnModelCreating(builder);
StoreSettingData.OnModelCreating(builder, Database);
StoreWebhookData.OnModelCreating(builder);
StoreData.OnModelCreating(builder, Database);
//StoreData.OnModelCreating(builder);
U2FDevice.OnModelCreating(builder);
Fido2Credential.OnModelCreating(builder);
BTCPayServer.Data.UserStore.OnModelCreating(builder);

View File

@ -1,6 +1,3 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class SettingData
@ -8,15 +5,5 @@ namespace BTCPayServer.Data
public string Id { get; set; }
public string Value { get; set; }
public static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
if (databaseFacade.IsNpgsql())
{
builder.Entity<SettingData>()
.Property(o => o.Value)
.HasColumnType("JSONB");
}
}
}
}

View File

@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using BTCPayServer.Client.Models;
using BTCPayServer.Data.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using PayoutProcessorData = BTCPayServer.Data.Data.PayoutProcessorData;
namespace BTCPayServer.Data
@ -38,7 +36,7 @@ namespace BTCPayServer.Data
[NotMapped] public string Role { get; set; }
public string StoreBlob { get; set; }
public byte[] StoreBlob { get; set; }
[Obsolete("Use GetDefaultPaymentId instead")]
public string DefaultCrypto { get; set; }
@ -50,15 +48,5 @@ namespace BTCPayServer.Data
public IEnumerable<PayoutData> Payouts { get; set; }
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
public IEnumerable<StoreSettingData> Settings { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
if (databaseFacade.IsNpgsql())
{
builder.Entity<StoreData>()
.Property(o => o.StoreBlob)
.HasColumnType("JSONB");
}
}
}
}

View File

@ -21,44 +21,42 @@ namespace BTCPayServer.Data
public const string PayjoinExposed = "pj-exposed";
public const string Payout = "payout";
public const string PullPayment = "pull-payment";
public const string Script = "script";
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> Bs { get; set; }
public List<WalletObjectLinkData> As { get; set; }
public List<WalletObjectLinkData> ChildLinks { get; set; }
public List<WalletObjectLinkData> ParentLinks { get; set; }
public IEnumerable<(string type, string id, string linkdata, string objectdata)> GetLinks()
{
if (Bs is not null)
foreach (var c in Bs)
if (ChildLinks is not null)
foreach (var c in ChildLinks)
{
yield return (c.BType, c.BId, c.Data, c.B?.Data);
yield return (c.ChildType, c.ChildId, c.Data, c.Child?.Data);
}
if (As is not null)
foreach (var c in As)
if (ParentLinks is not null)
foreach (var c in ParentLinks)
{
yield return (c.AType, c.AId, c.Data, c.A?.Data);
yield return (c.ParentType, c.ParentId, c.Data, c.Parent?.Data);
}
}
public IEnumerable<WalletObjectData> GetNeighbours()
{
if (Bs != null)
foreach (var c in Bs)
if (ChildLinks != null)
foreach (var c in ChildLinks)
{
if (c.B != null)
yield return c.B;
if (c.Child != null)
yield return c.Child;
}
if (As != null)
foreach (var c in As)
if (ParentLinks != null)
foreach (var c in ParentLinks)
{
if (c.A != null)
yield return c.A;
if (c.Parent != null)
yield return c.Parent;
}
}

View File

@ -11,14 +11,14 @@ namespace BTCPayServer.Data
public class WalletObjectLinkData
{
public string WalletId { get; set; }
public string AType { get; set; }
public string AId { get; set; }
public string BType { get; set; }
public string BId { get; set; }
public string ParentType { get; set; }
public string ParentId { get; set; }
public string ChildType { get; set; }
public string ChildId { get; set; }
public string Data { get; set; }
public WalletObjectData A { get; set; }
public WalletObjectData B { get; set; }
public WalletObjectData Parent { get; set; }
public WalletObjectData Child { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
@ -26,28 +26,28 @@ namespace BTCPayServer.Data
new
{
o.WalletId,
o.AType,
o.AId,
o.BType,
o.BId,
o.ParentType,
o.ParentId,
o.ChildType,
o.ChildId,
});
builder.Entity<WalletObjectLinkData>().HasIndex(o => new
{
o.WalletId,
o.BType,
o.BId,
o.ChildType,
o.ChildId,
});
builder.Entity<WalletObjectLinkData>()
.HasOne(o => o.A)
.WithMany(o => o.Bs)
.HasForeignKey(o => new { o.WalletId, o.AType, o.AId })
.HasOne(o => o.Parent)
.WithMany(o => o.ChildLinks)
.HasForeignKey(o => new { o.WalletId, o.ParentType, o.ParentId })
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<WalletObjectLinkData>()
.HasOne(o => o.B)
.WithMany(o => o.As)
.HasForeignKey(o => new { o.WalletId, o.BType, o.BId })
.HasOne(o => o.Child)
.WithMany(o => o.ParentLinks)
.HasForeignKey(o => new { o.WalletId, o.ChildType, o.ChildId })
.OnDelete(DeleteBehavior.Cascade);
if (databaseFacade.IsNpgsql())

View File

@ -40,33 +40,33 @@ namespace BTCPayServer.Migrations
columns: table => new
{
WalletId = table.Column<string>(type: "TEXT", nullable: false),
AType = table.Column<string>(type: "TEXT", nullable: false),
AId = table.Column<string>(type: "TEXT", nullable: false),
BType = table.Column<string>(type: "TEXT", nullable: false),
BId = table.Column<string>(type: "TEXT", nullable: false),
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),
Data = table.Column<string>(type: migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_WalletObjectLinks", x => new { x.WalletId, x.AType, x.AId, x.BType, x.BId });
table.PrimaryKey("PK_WalletObjectLinks", x => new { x.WalletId, x.ParentType, x.ParentId, x.ChildType, x.ChildId });
table.ForeignKey(
name: "FK_WalletObjectLinks_WalletObjects_WalletId_BType_BId",
columns: x => new { x.WalletId, x.BType, x.BId },
name: "FK_WalletObjectLinks_WalletObjects_WalletId_ChildType_ChildId",
columns: x => new { x.WalletId, x.ChildType, x.ChildId },
principalTable: "WalletObjects",
principalColumns: new[] { "WalletId", "Type", "Id" },
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_WalletObjectLinks_WalletObjects_WalletId_AType_AId",
columns: x => new { x.WalletId, x.AType, x.AId },
name: "FK_WalletObjectLinks_WalletObjects_WalletId_ParentType_ParentId",
columns: x => new { x.WalletId, x.ParentType, x.ParentId },
principalTable: "WalletObjects",
principalColumns: new[] { "WalletId", "Type", "Id" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_WalletObjectLinks_WalletId_BType_BId",
name: "IX_WalletObjectLinks_WalletId_ChildType_ChildId",
table: "WalletObjectLinks",
columns: new[] { "WalletId", "BType", "BId" });
columns: new[] { "WalletId", "ChildType", "ChildId" });
}
protected override void Down(MigrationBuilder migrationBuilder)

View File

@ -1,31 +0,0 @@
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("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
}
}
}

View File

@ -872,24 +872,24 @@ namespace BTCPayServer.Migrations
b.Property<string>("WalletId")
.HasColumnType("TEXT");
b.Property<string>("AType")
b.Property<string>("ParentType")
.HasColumnType("TEXT");
b.Property<string>("AId")
b.Property<string>("ParentId")
.HasColumnType("TEXT");
b.Property<string>("BType")
b.Property<string>("ChildType")
.HasColumnType("TEXT");
b.Property<string>("BId")
b.Property<string>("ChildId")
.HasColumnType("TEXT");
b.Property<string>("Data")
.HasColumnType("TEXT");
b.HasKey("WalletId", "AType", "AId", "BType", "BId");
b.HasKey("WalletId", "ParentType", "ParentId", "ChildType", "ChildId");
b.HasIndex("WalletId", "BType", "BId");
b.HasIndex("WalletId", "ChildType", "ChildId");
b.ToTable("WalletObjectLinks");
});
@ -1384,21 +1384,21 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
{
b.HasOne("BTCPayServer.Data.WalletObjectData", "A")
.WithMany("Bs")
.HasForeignKey("WalletId", "AType", "AId")
b.HasOne("BTCPayServer.Data.WalletObjectData", "Child")
.WithMany("ParentLinks")
.HasForeignKey("WalletId", "ChildType", "ChildId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BTCPayServer.Data.WalletObjectData", "B")
.WithMany("As")
.HasForeignKey("WalletId", "BType", "BId")
b.HasOne("BTCPayServer.Data.WalletObjectData", "Parent")
.WithMany("ChildLinks")
.HasForeignKey("WalletId", "ParentType", "ParentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("A");
b.Navigation("Child");
b.Navigation("B");
b.Navigation("Parent");
});
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
@ -1545,9 +1545,9 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.WalletObjectData", b =>
{
b.Navigation("As");
b.Navigation("ChildLinks");
b.Navigation("Bs");
b.Navigation("ParentLinks");
});
modelBuilder.Entity("BTCPayServer.Data.WebhookData", b =>

View File

@ -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.20" />
<PackageReference Include="NBitcoin" Version="7.0.14" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.2" />
</ItemGroup>

View File

@ -1,6 +1,6 @@
[
{
"name":"Afghan Afghani",
"name":"Afghani",
"code":"AFN",
"divisibility":2,
"symbol":null,
@ -21,7 +21,7 @@
"crypto":false
},
{
"name":"Albanian Lek",
"name":"Lek",
"code":"ALL",
"divisibility":2,
"symbol":null,
@ -42,7 +42,7 @@
"crypto":false
},
{
"name":"Angolan Kwanza",
"name":"Kwanza",
"code":"AOA",
"divisibility":2,
"symbol":null,
@ -84,7 +84,7 @@
"crypto":false
},
{
"name":"Azerbaijani Manat",
"name":"Azerbaijanian Manat",
"code":"AZN",
"divisibility":2,
"symbol":null,
@ -105,14 +105,14 @@
"crypto":false
},
{
"name":"Bangladeshi Taka",
"name":"Taka",
"code":"BDT",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Barbadian Dollar",
"name":"Barbados Dollar",
"code":"BBD",
"divisibility":2,
"symbol":null,
@ -161,21 +161,21 @@
"crypto":false
},
{
"name":"Bhutanese Ngultrum",
"name":"Ngultrum",
"code":"BTN",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Bolivian Boliviano",
"name":"Boliviano",
"code":"BOB",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Bolivian Mvdol",
"name":"Mvdol",
"code":"BOV",
"divisibility":2,
"symbol":null,
@ -189,7 +189,7 @@
"crypto":false
},
{
"name":"Botswana Pula",
"name":"Pula",
"code":"BWP",
"divisibility":2,
"symbol":null,
@ -224,21 +224,21 @@
"crypto":false
},
{
"name":"Burundian Franc",
"name":"Burundi Franc",
"code":"BIF",
"divisibility":0,
"symbol":null,
"crypto":false
},
{
"name":"Cape Verdean Escudo",
"name":"Cabo Verde Escudo",
"code":"CVE",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Cambodian Riel",
"name":"Riel",
"code":"KHR",
"divisibility":2,
"symbol":null,
@ -301,7 +301,7 @@
"crypto":false
},
{
"name":"Comorian Franc",
"name":"Comoro Franc",
"code":"KMF",
"divisibility":0,
"symbol":null,
@ -329,7 +329,7 @@
"crypto":false
},
{
"name":"Croatian Kuna",
"name":"Kuna",
"code":"HRK",
"divisibility":2,
"symbol":null,
@ -371,7 +371,7 @@
"crypto":false
},
{
"name":"Djiboutian Franc",
"name":"Djibouti Franc",
"code":"DJF",
"divisibility":0,
"symbol":null,
@ -392,14 +392,14 @@
"crypto":false
},
{
"name":"Salvadoran Colon",
"name":"El Salvador Colon",
"code":"SVC",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Eritrean Nakfa",
"name":"Nakfa",
"code":"ERN",
"divisibility":2,
"symbol":null,
@ -420,7 +420,7 @@
"crypto":false
},
{
"name":"Fijian Dollar",
"name":"Fiji Dollar",
"code":"FJD",
"divisibility":2,
"symbol":null,
@ -434,21 +434,21 @@
"crypto":false
},
{
"name":"Gambian Dalasi",
"name":"Dalasi",
"code":"GMD",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Georgian Lari",
"name":"Lari",
"code":"GEL",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Ghanaian Cedi",
"name":"Ghana Cedi",
"code":"GHS",
"divisibility":2,
"symbol":null,
@ -462,7 +462,7 @@
"crypto":false
},
{
"name":"Guatemalan Quetzal",
"name":"Quetzal",
"code":"GTQ",
"divisibility":2,
"symbol":null,
@ -476,28 +476,28 @@
"crypto":false
},
{
"name":"Guinean Franc",
"name":"Guinea Franc",
"code":"GNF",
"divisibility":0,
"symbol":null,
"crypto":false
},
{
"name":"Guyanese Dollar",
"name":"Guyana Dollar",
"code":"GYD",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Haitian Gourde",
"name":"Gourde",
"code":"HTG",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Honduran Lempira",
"name":"Lempira",
"code":"HNL",
"divisibility":2,
"symbol":null,
@ -511,21 +511,21 @@
"crypto":false
},
{
"name":"Hungarian Forint",
"name":"Forint",
"code":"HUF",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Icelandic Krona",
"name":"Iceland Krona",
"code":"ISK",
"divisibility":0,
"symbol":null,
"crypto":false
},
{
"name":"Indonesian Rupiah",
"name":"Rupiah",
"code":"IDR",
"divisibility":2,
"symbol":null,
@ -546,7 +546,7 @@
"crypto":false
},
{
"name":"New Israeli Shekel",
"name":"New Israeli Sheqel",
"code":"ILS",
"divisibility":2,
"symbol":null,
@ -560,7 +560,7 @@
"crypto":false
},
{
"name":"Japanese Yen",
"name":"Yen",
"code":"JPY",
"divisibility":0,
"symbol":"¥",
@ -574,7 +574,7 @@
"crypto":false
},
{
"name":"Kazakhstani Tenge",
"name":"Tenge",
"code":"KZT",
"divisibility":2,
"symbol":null,
@ -595,7 +595,7 @@
"crypto":false
},
{
"name":"South Korean Won",
"name":"Won",
"code":"KRW",
"divisibility":0,
"symbol":"₩",
@ -609,14 +609,14 @@
"crypto":false
},
{
"name":"Kyrgyzstani Som",
"name":"Som",
"code":"KGS",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Lao Kip",
"name":"Kip",
"code":"LAK",
"divisibility":2,
"symbol":null,
@ -630,14 +630,14 @@
"crypto":false
},
{
"name":"Lesotho Loti",
"name":"Loti",
"code":"LSL",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"South African Rand",
"name":"Rand",
"code":"ZAR",
"divisibility":2,
"symbol":null,
@ -665,14 +665,14 @@
"crypto":false
},
{
"name":"Macanese Pataca",
"name":"Pataca",
"code":"MOP",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Macedonian Denar",
"name":"Denar",
"code":"MKD",
"divisibility":2,
"symbol":null,
@ -686,7 +686,7 @@
"crypto":false
},
{
"name":"Malawian Kwacha",
"name":"Malawi Kwacha",
"code":"MWK",
"divisibility":2,
"symbol":null,
@ -700,21 +700,21 @@
"crypto":false
},
{
"name":"Maldivian Rufiyaa",
"name":"Rufiyaa",
"code":"MVR",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Mauritanian Ouguiya",
"name":"Ouguiya",
"code":"MRO",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Mauritian Rupee",
"name":"Mauritius Rupee",
"code":"MUR",
"divisibility":2,
"symbol":null,
@ -742,7 +742,7 @@
"crypto":false
},
{
"name":"Mongolian Tugrik",
"name":"Tugrik",
"code":"MNT",
"divisibility":2,
"symbol":null,
@ -756,21 +756,21 @@
"crypto":false
},
{
"name":"Mozambican Metical",
"name":"Mozambique Metical",
"code":"MZN",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Myanmar Kyat",
"name":"Kyat",
"code":"MMK",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Namibian dollar",
"name":"Namibia Dollar",
"code":"NAD",
"divisibility":2,
"symbol":null,
@ -784,56 +784,56 @@
"crypto":false
},
{
"name":"Nicaraguan Cordoba",
"name":"Cordoba Oro",
"code":"NIO",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Nigerian Naira",
"name":"Naira",
"code":"NGN",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Omani Rial",
"name":"Rial Omani",
"code":"OMR",
"divisibility":3,
"symbol":null,
"crypto":false
},
{
"name":"Pakistani Rupee",
"name":"Pakistan Rupee",
"code":"PKR",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Panamanian Balboa",
"name":"Balboa",
"code":"PAB",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Papua New Guinean Kina",
"name":"Kina",
"code":"PGK",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Paraguayan Guarani",
"name":"Guarani",
"code":"PYG",
"divisibility":0,
"symbol":null,
"crypto":false
},
{
"name":"Peruvian Sol",
"name":"Sol",
"code":"PEN",
"divisibility":2,
"symbol":null,
@ -847,7 +847,7 @@
"crypto":false
},
{
"name":"Polish Zloty",
"name":"Zloty",
"code":"PLN",
"divisibility":2,
"symbol":null,
@ -875,7 +875,7 @@
"crypto":false
},
{
"name":"Rwandan Franc",
"name":"Rwanda Franc",
"code":"RWF",
"divisibility":0,
"symbol":null,
@ -889,14 +889,14 @@
"crypto":false
},
{
"name":"Samoan Tala",
"name":"Tala",
"code":"WST",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"São Tomé and Príncipe sDobra",
"name":"Dobra",
"code":"STD",
"divisibility":2,
"symbol":null,
@ -917,14 +917,14 @@
"crypto":false
},
{
"name":"Seychellois Rupee",
"name":"Seychelles Rupee",
"code":"SCR",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Sierra Leonean Leone",
"name":"Leone",
"code":"SLL",
"divisibility":2,
"symbol":null,
@ -959,7 +959,7 @@
"crypto":false
},
{
"name":"Sri Lankan Rupee",
"name":"Sri Lanka Rupee",
"code":"LKR",
"divisibility":2,
"symbol":null,
@ -973,14 +973,14 @@
"crypto":false
},
{
"name":"Surinamese Dollar",
"name":"Surinam Dollar",
"code":"SRD",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Swazi Lilangeni",
"name":"Lilangeni",
"code":"SZL",
"divisibility":2,
"symbol":null,
@ -1022,7 +1022,7 @@
"crypto":false
},
{
"name":"Tajikistani Somoni",
"name":"Somoni",
"code":"TJS",
"divisibility":2,
"symbol":null,
@ -1036,14 +1036,14 @@
"crypto":false
},
{
"name":"Thai Baht",
"name":"Baht",
"code":"THB",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Tongan paʻanga",
"name":"Paanga",
"code":"TOP",
"divisibility":2,
"symbol":null,
@ -1071,21 +1071,21 @@
"crypto":false
},
{
"name":"Turkmenistani Manat",
"name":"Turkmenistan New Manat",
"code":"TMT",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Ugandan Shilling",
"name":"Uganda Shilling",
"code":"UGX",
"divisibility":0,
"symbol":null,
"crypto":false
},
{
"name":"Ukrainian Hryvnia",
"name":"Hryvnia",
"code":"UAH",
"divisibility":2,
"symbol":null,
@ -1106,7 +1106,7 @@
"crypto":false
},
{
"name":"Uruguayan Peso",
"name":"Peso Uruguayo",
"code":"UYU",
"divisibility":2,
"symbol":null,
@ -1120,28 +1120,28 @@
"crypto":false
},
{
"name":"Uzbekistani Sum",
"name":"Uzbekistan Sum",
"code":"UZS",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Vanuatu Vatu",
"name":"Vatu",
"code":"VUV",
"divisibility":0,
"symbol":null,
"crypto":false
},
{
"name":"Venezuelan Bolívar",
"name":"Bolívar",
"code":"VEF",
"divisibility":2,
"symbol":null,
"crypto":false
},
{
"name":"Vietnamese Dong",
"name":"Dong",
"code":"VND",
"divisibility":0,
"symbol":null,
@ -1162,7 +1162,7 @@
"crypto":false
},
{
"name":"Zimbabwean Dollar",
"name":"Zimbabwe Dollar",
"code":"ZWL",
"divisibility":2,
"symbol":null,

View File

@ -171,9 +171,8 @@ 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, out var error);
DerivationSchemeSettings.TryParseFromWalletFile(content, onchainBTC.Network, out var expected);
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(

View File

@ -151,15 +151,6 @@ 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);

View File

@ -656,8 +656,7 @@ 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, out var error));
Assert.Null(error);
mainnet, out var settings));
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint);
Assert.Equal(settings.AccountKeySettings[0].RootFingerprint,
HDFingerprint.TryParse("8bafd160", out var hd) ? hd : default);
@ -673,41 +672,30 @@ 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, out error));
testnet, out settings));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit);
Assert.Null(error);
// Should be segwit p2sh
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings, out error));
testnet, out settings));
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p &&
p.Inner is DirectDerivationStrategy s2 && s2.Segwit);
Assert.Null(error);
// Should be segwit
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings, out error));
testnet, out settings));
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, out error));
mainnet, out var specter));
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]
@ -1761,7 +1749,8 @@ namespace BTCPayServer.Tests
PaymentMethod = new PaymentMethodId("BTC", PaymentTypes.BTCLike)
}
};
var newBlob = new Serializer(null).ToString(blob).Replace( "paymentMethod\":\"BTC\"","paymentMethod\":\"ETH_ZYC\"");
var newBlob = Encoding.UTF8.GetBytes(
new Serializer(null).ToString(blob).Replace( "paymentMethod\":\"BTC\"","paymentMethod\":\"ETH_ZYC\""));
Assert.Empty(StoreDataExtensions.GetStoreBlob(new StoreData() {StoreBlob = newBlob}).PaymentMethodCriteria);
}
}

View File

@ -1561,127 +1561,6 @@ namespace BTCPayServer.Tests
});
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanRefundInvoice()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.RegisterDerivationSchemeAsync("BTC");
var client = await user.CreateClient();
var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = 5000.0m, Currency = "USD" });
var methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
var method = methods.First();
var amount = method.Amount;
Assert.Equal(amount, method.Due);
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
{
await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(method.Destination, tester.NetworkProvider.BTC.NBitcoinNetwork),
Money.Coins(method.Due)
);
});
// test validation that the invoice exists
await AssertHttpError(404, async () =>
{
await client.RefundInvoice(user.StoreId, "lol fake invoice id", new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.RateThen
});
});
// test validation error for when invoice is not yet in the state in which it can be refunded
var apiError = await AssertAPIError("non-refundable", () => client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.RateThen
}));
Assert.Equal("Cannot refund this invoice", apiError.Message);
await TestUtils.EventuallyAsync(async () =>
{
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
Assert.True(invoice.Status == InvoiceStatus.Processing);
});
// need to set the status to the one in which we can actually refund the invoice
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest() {
Status = InvoiceStatus.Settled
});
// test validation for the payment method
var validationError = await AssertValidationError(new[] { "PaymentMethod" }, async () =>
{
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = "fake payment method",
RefundVariant = RefundVariant.RateThen
});
});
Assert.Contains("PaymentMethod: Please select one of the payment methods which were available for the original invoice", validationError.Message);
// test RefundVariant.RateThen
var pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.RateThen
});
Assert.Equal("BTC", pp.Currency);
Assert.True(pp.AutoApproveClaims);
Assert.Equal(1, pp.Amount);
Assert.Equal(pp.Name, $"Refund {invoice.Id}");
// test RefundVariant.CurrentRate
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.CurrentRate
});
Assert.Equal("BTC", pp.Currency);
Assert.True(pp.AutoApproveClaims);
Assert.Equal(1, pp.Amount);
// test RefundVariant.Fiat
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.Fiat,
Name = "my test name"
});
Assert.Equal("USD", pp.Currency);
Assert.False(pp.AutoApproveClaims);
Assert.Equal(5000, pp.Amount);
Assert.Equal("my test name", pp.Name);
// test RefundVariant.Custom
validationError = await AssertValidationError(new[] { "CustomAmount", "CustomCurrency" }, async () =>
{
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.Custom,
});
});
Assert.Contains("CustomAmount: Amount must be greater than 0", validationError.Message);
Assert.Contains("CustomCurrency: Invalid currency", validationError.Message);
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.Custom,
CustomAmount = 69420,
CustomCurrency = "JPY"
});
Assert.Equal("JPY", pp.Currency);
Assert.False(pp.AutoApproveClaims);
Assert.Equal(69420, pp.Amount);
// should auto-approve if currencies match
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.Custom,
CustomAmount = 0.00069420m,
CustomCurrency = "BTC"
});
Assert.True(pp.AutoApproveClaims);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task InvoiceTests()
@ -3194,7 +3073,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) { UseInefficientPath = useInefficient });
var allObjects = await repo.GetWalletObjects((new(wid, null) { UseInefficientPath = useInefficient }));
var allObjectsNoWallet = await repo.GetWalletObjects((new() { UseInefficientPath = useInefficient }));
var allObjectsNoWalletAndType = await repo.GetWalletObjects((new() { Type = "test", UseInefficientPath = useInefficient }));
var allTests = await repo.GetWalletObjects((new(wid, "test") { UseInefficientPath = useInefficient }));

View File

@ -1,250 +0,0 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Lightning;
using BTCPayServer.Plugins.LNbank.Data.Models;
using NBitcoin;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
[Trait("Selenium", "Selenium")]
[Collection(nameof(NonParallelizableCollectionDefinition))]
public class LNbankTests : UnitTestBase
{
private const int TestTimeout = TestUtils.TestTimeout;
public LNbankTests(ITestOutputHelper helper) : base(helper)
{
}
[Fact(Timeout = TestTimeout)]
[Trait("Lightning", "Lightning")]
public async Task CanUseLNbank()
{
var implementations = new []
{
LightningConnectionType.CLightning,
LightningConnectionType.LndREST
};
foreach (var nodeType in implementations)
{
using var s = CreateSeleniumTester();
s.Server.ActivateLightning(nodeType);
await s.StartAsync();
s.RegisterNewUser(true);
// Setup store LN node with LNbank
s.CreateNewStore();
s.Driver.FindElement(By.Id("StoreNav-LightningBTC")).Click();
s.Driver.FindElement(By.CssSelector("label[for=\"LightningNodeType-LNbank\"]")).Click();
s.Driver.WaitForElement(By.Id("LNbank-CreateWallet"));
Assert.Equal("", s.Driver.FindElement(By.Id("LNbankWallet")).GetAttribute("value"));
// Create new wallet, which is pre-selected afterwards
s.Driver.FindElement(By.Id("LNbank-CreateWallet")).Click();
var walletName = "Wallet" + RandomUtils.GetUInt64();
s.Driver.FindElement(By.Id("Wallet_Name")).SendKeys(walletName);
s.Driver.FindElement(By.Id("LNbank-Create")).Click();
s.Driver.WaitForElement(By.Id("LNbankWallet"));
var walletSelect = new SelectElement(s.Driver.FindElement(By.Id("LNbankWallet")));
Assert.Equal(walletName, walletSelect.SelectedOption.Text);
// Finish and validate setup
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains("LNbank", s.Driver.FindElement(By.Id("CustomNodeInfo")).Text);
// LNbank wallets
s.Driver.FindElement(By.Id("Nav-LNbank")).Click();
Assert.Contains(walletName, s.Driver.FindElement(By.Id("LNbank-Wallets")).Text);
Assert.Single(s.Driver.FindElements(By.CssSelector("#LNbank-Wallets a")));
s.Driver.FindElement(By.CssSelector("#LNbank-Wallets a")).Click();
// Wallet
Assert.Contains("0 sats", s.Driver.FindElement(By.Id("LNbank-WalletBalance")).Text);
Assert.Contains("There are no transactions yet.", s.Driver.FindElement(By.Id("LNbank-WalletTransactions")).Text);
s.Driver.FindElement(By.Id("LNbank-WalletSettings")).Click();
Assert.Contains(walletName, s.Driver.FindElement(By.Id("LNbank-WalletName")).Text);
// Receive
var description = "First invoice";
s.Driver.FindElement(By.Id("LNbank-WalletReceive")).Click();
s.Driver.FindElement(By.Id("Description")).SendKeys(description);
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("21");
s.Driver.FindElement(By.Id("LNbank-CreateInvoice")).Click();
// Details
Assert.Contains(description, s.Driver.FindElement(By.Id("LNbank-TransactionDescription")).Text);
Assert.Contains("21 sats unpaid", s.Driver.FindElement(By.Id("LNbank-TransactionAmount")).Text);
var bolt11 = s.Driver.FindElement(By.Id("LNbank-CopyPaymentRequest")).GetAttribute("data-clipboard");
var shareUrl = s.Driver.FindElement(By.Id("LNbank-CopyShareUrl")).GetAttribute("data-clipboard");
Assert.StartsWith("ln", bolt11);
// List
s.Driver.FindElement(By.Id("LNbank-WalletOverview")).Click();
var listUrl = s.Driver.Url;
Assert.Single(s.Driver.FindElements(By.CssSelector("#LNbank-WalletTransactions tr")));
Assert.Contains("21 sats", s.Driver.FindElement(By.CssSelector("#LNbank-WalletTransactions tr .transaction-amount")).Text);
Assert.Contains(description, s.Driver.FindElement(By.CssSelector("#LNbank-WalletTransactions tr .transaction-description")).Text);
Assert.Contains("unpaid", s.Driver.FindElement(By.CssSelector("#LNbank-WalletTransactions tr .transaction-status")).Text);
Assert.Contains("0 sats", s.Driver.FindElement(By.Id("LNbank-WalletBalance")).Text);
// Share
s.GoToUrl(shareUrl);
Assert.Contains(description, s.Driver.FindElement(By.Id("LNbank-TransactionDescription")).Text);
Assert.Contains("21 sats unpaid", s.Driver.FindElement(By.Id("LNbank-TransactionAmount")).Text);
// Pay invoice
var resp = await s.Server.CustomerLightningD.Pay(bolt11);
Assert.Equal(PayResult.Ok, resp.Result);
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
Assert.Contains("21 sats settled", s.Driver.FindElement(By.Id("LNbank-TransactionSettled")).Text);
});
// List
s.GoToUrl(listUrl);
Assert.Single(s.Driver.FindElements(By.CssSelector("#LNbank-WalletTransactions tr")));
Assert.Contains("21 sats", s.Driver.FindElement(By.CssSelector("#LNbank-WalletTransactions tr .transaction-settled")).Text);
Assert.Contains("21 sats", s.Driver.FindElement(By.Id("LNbank-WalletBalance")).Text);
// Send
var memo = "Donation";
var amount = LightMoney.Satoshis(5);
var invoice = await s.Server.CustomerLightningD.CreateInvoice(amount, memo, TimeSpan.FromHours(1));
s.Driver.FindElement(By.Id("LNbank-WalletSend")).Click();
s.Driver.FindElement(By.Id("Destination")).SendKeys(invoice.BOLT11);
s.Driver.FindElement(By.Id("LNbank-Decode")).Click();
// Confirm
Assert.Contains(memo, s.Driver.FindElement(By.Id("Description")).GetAttribute("value"));
Assert.Contains("5 sats", s.Driver.FindElement(By.Id("LNbank-Amount")).Text);
s.Driver.FindElement(By.Id("Description")).Clear();
s.Driver.FindElement(By.Id("Description")).SendKeys("For Uncle Jim");
s.Driver.FindElement(By.Id("LNbank-Send")).Click();
Assert.Contains("Payment successfully sent and settled.", s.FindAlertMessage().Text);
// List
s.Driver.FindElement(By.Id("LNbank-WalletOverview")).Click();
var amountEl = s.Driver.FindElement(By.CssSelector("#LNbank-WalletTransactions tr .transaction-amount"));
var settledEl = s.Driver.FindElement(By.CssSelector("#LNbank-WalletTransactions tr .transaction-settled"));
var amountMoney = LightMoney.MilliSatoshis(long.Parse(amountEl.GetAttribute("data-amount")));
var amountSettledMoney = LightMoney.MilliSatoshis(long.Parse(settledEl.GetAttribute("data-amount-settled")));
var feeMoney = LightMoney.MilliSatoshis(long.Parse(settledEl.GetAttribute("data-transaction-fee")));
var amountSettled = (amountMoney + feeMoney) * -1;
var balance = LightMoney.Satoshis(21) + amountSettled;
Assert.Equal(2, s.Driver.FindElements(By.CssSelector("#LNbank-WalletTransactions tr")).Count);
Assert.Equal(amount, amountMoney);
Assert.Equal(amountSettled, amountSettledMoney);
Assert.Contains("For Uncle Jim", s.Driver.FindElement(By.CssSelector("#LNbank-WalletTransactions tr .transaction-description")).Text);
Assert.Contains($"{amount.ToUnit(LightMoneyUnit.Satoshi)} sats", amountEl.Text);
Assert.Contains($"{amountSettled.ToUnit(LightMoneyUnit.Satoshi)} sats", settledEl.Text);
Assert.Contains($"{balance.ToUnit(LightMoneyUnit.Satoshi)} sats", s.Driver.FindElement(By.Id("LNbank-WalletBalance")).Text);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Lightning", "Lightning")]
public async Task CanUseLNbankAccessKeys()
{
using var s = CreateSeleniumTester();
s.Server.ActivateLightning(LightningConnectionType.CLightning);
await s.StartAsync();
s.GoToRegister();
var user = s.RegisterNewUser();
s.GoToRegister();
var admin = s.RegisterNewUser(true);
// Create new wallet
s.Driver.FindElement(By.Id("Nav-LNbank")).Click();
var walletName = "AccessKeys" + RandomUtils.GetUInt64();
s.Driver.FindElement(By.Id("Wallet_Name")).SendKeys(walletName);
s.Driver.FindElement(By.Id("LNbank-Create")).Click();
Assert.Contains("Wallet successfully created.", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("LNbank-WalletSettings")).Click();
var walletId = s.Driver.FindElement(By.Id("LNbank-WalletId")).Text;
var walletNavId = $"Nav-LNbank-Wallet-{walletId}";
// Check if the user sees it
s.Logout();
s.LogIn(user);
s.Driver.AssertElementNotFound(By.Id(walletNavId));
void SetAccessLevel(AccessLevel level)
{
s.Logout();
s.LogIn(admin);
s.Driver.FindElement(By.Id(walletNavId)).Click();
s.Driver.FindElement(By.Id("LNbank-WalletSettings")).Click();
s.Driver.FindElement(By.Id("SectionNav-WalletAccessKeys")).Click();
s.Driver.FindElement(By.Id("AccessKey_Email")).SendKeys(user);
var levelSelect = new SelectElement(s.Driver.FindElement(By.Id("AccessKey_Level")));
levelSelect.SelectByValue(level.ToString());
s.Driver.FindElement(By.Id("LNbank-CreateAccessKey")).Click();
Assert.Contains("Access key added successfully.", s.FindAlertMessage().Text);
// Switch user
s.Logout();
s.LogIn(user);
s.Driver.FindElement(By.Id(walletNavId)).Click();
}
// Add read-only access key for user
SetAccessLevel(AccessLevel.ReadOnly);
s.Driver.AssertElementNotFound(By.Id("LNbank-WalletSend"));
s.Driver.AssertElementNotFound(By.Id("LNbank-WalletReceive"));
s.Driver.AssertElementNotFound(By.Id("LNbank-WalletSettings"));
// Update access key for user: Invoice
SetAccessLevel(AccessLevel.Invoice);
s.Driver.AssertElementNotFound(By.Id("LNbank-WalletSend"));
s.Driver.AssertElementNotFound(By.Id("LNbank-WalletSettings"));
// Receive is allowed now
var description = "My invoice";
s.Driver.FindElement(By.Id("LNbank-WalletReceive")).Click();
s.Driver.FindElement(By.Id("Description")).SendKeys(description);
s.Driver.SetCheckbox(By.Id("AttachDescription"), true);
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("21");
s.Driver.FindElement(By.Id("LNbank-CreateInvoice")).Click();
Assert.Contains(description, s.Driver.FindElement(By.Id("LNbank-TransactionDescription")).Text);
Assert.Contains("21 sats unpaid", s.Driver.FindElement(By.Id("LNbank-TransactionAmount")).Text);
var bolt11 = s.Driver.FindElement(By.Id("LNbank-CopyPaymentRequest")).GetAttribute("data-clipboard");
Assert.StartsWith("ln", bolt11);
// Update access key for user: Send
SetAccessLevel(AccessLevel.Send);
s.Driver.AssertElementNotFound(By.Id("LNbank-WalletSettings"));
// Send is allowed now
s.Driver.FindElement(By.Id("LNbank-WalletSend")).Click();
s.Driver.FindElement(By.Id("Destination")).SendKeys(bolt11);
s.Driver.FindElement(By.Id("LNbank-Decode")).Click();
Assert.Contains(description, s.Driver.FindElement(By.Id("Description")).GetAttribute("value"));
Assert.Contains("21 sats", s.Driver.FindElement(By.Id("LNbank-Amount")).Text);
s.Driver.FindElement(By.Id("LNbank-Send")).Click();
Assert.Contains("Insufficient balance: 0 sats — tried to send 21 sats.", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error).Text);
// Update access key for user: Send
SetAccessLevel(AccessLevel.Admin);
s.Driver.FindElement(By.Id("LNbank-WalletSettings")).Click();
}
}
}

View File

@ -71,7 +71,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:24.0
image: btcpayserver/bitcoin:23.0-1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -126,7 +126,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:24.0
image: btcpayserver/bitcoin:23.0-1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"

View File

@ -68,7 +68,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:24.0
image: btcpayserver/bitcoin:23.0-1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -113,7 +113,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:24.0
image: btcpayserver/bitcoin:23.0-1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"

View File

@ -1,16 +0,0 @@
#!/bin/bash
PREIMAGE=$(cat /dev/urandom | tr -dc 'a-f0-9' | fold -w 64 | head -n 1)
HASH=`node -e "console.log(require('crypto').createHash('sha256').update(Buffer.from('$PREIMAGE', 'hex')).digest('hex'))"`
PAYREQ=$(./docker-customer-lncli.sh addholdinvoice --memo "hodl invoice $@" $HASH "$@" | jq -r ".payment_request")
echo "HASH: $HASH"
echo "PREIMAGE: $PREIMAGE"
echo "PAY REQ: $PAYREQ"
echo ""
echo "SETTLE: ./docker-customer-lncli.sh settleinvoice $PREIMAGE"
echo "CANCEL: ./docker-customer-lncli.sh cancelinvoice $HASH"
echo "LOOKUP: ./docker-customer-lncli.sh lookupinvoice $HASH"
echo ""
echo "TRACK: ./docker-merchant-lncli.sh trackpayment $HASH"
echo "PAY: ./docker-merchant-lncli.sh payinvoice $PAYREQ"

View File

@ -97,17 +97,8 @@ connect $C_LN $m_ln_uri "Customer (LND) to Merchant (LND)"
# Channels
printf "\n\rEstablishing channels\n\r----------------------\n\r"
create_channel $M_LN $c_ln_id "Merchant (LND) to Customer (LND)"
create_channel $C_LN $c_cl_id "Customer (LND) to Customer (c-lightning)"
create_channel $C_CL $m_cl_id "Customer (c-lightning) to Merchant (c-lightning)"
create_channel $C_CL $m_cl_id "Customer (c-lightning) to Merchant (c-lightning)"
create_channel $C_CL $m_ln_id "Customer (c-lightning) to Merchant (LND)"
create_channel $C_LN $c_cl_id "Customer (LND) to Customer (c-lightning)"
create_channel $C_LN $m_cl_id "Customer (LND) to Merchant (c-lightning)"
create_channel $M_CL $m_ln_id "Merchant (c-lightning) to Merchant (LND)" "announce=false"
create_channel $M_CL $c_ln_id "Merchant (c-lightning) to Customer (LND)" "announce=false"
create_channel $M_CL $c_cl_id "Merchant (c-lightning) to Customer (c-lightning)" "announce=false"
create_channel $M_LN $c_ln_id "Merchant (LND) to Customer (LND)"
create_channel $M_LN $c_cl_id "Merchant (LND) to Customer (c-lightning)"
create_channel $C_LN $m_ln_id "Customer (LND) to Merchant (LND)" --private

View File

@ -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" />
@ -47,7 +47,7 @@
<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.10" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.8" />
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Fido2" Version="2.0.2" />
@ -133,8 +133,6 @@
<ProjectReference Include="..\BTCPayServer.Data\BTCPayServer.Data.csproj" />
<ProjectReference Include="..\BTCPayServer.Rating\BTCPayServer.Rating.csproj" />
<ProjectReference Include="..\BTCPayServer.Common\BTCPayServer.Common.csproj" />
<ProjectReference Include="..\Plugins\BTCPayServer.Plugins.LNbank\BTCPayServer.Plugins.LNbank.csproj" />
<ProjectReference Include="..\Plugins\BTCPayServer.Plugins.PodServer\BTCPayServer.Plugins.PodServer.csproj" />
<ProjectReference Include="..\Plugins\BTCPayServer.Plugins.Custodians.FakeCustodian\BTCPayServer.Plugins.Custodians.FakeCustodian.csproj" />
</ItemGroup>

View File

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

View File

@ -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>
}

View File

@ -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 ? "empty-state" : "")" 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 ? "text-secondary" : "")" type="button" data-bs-toggle="dropdown" aria-expanded="false">
@if (!string.IsNullOrEmpty(Model.CurrentStoreLogoFileId))
{
<img class="logo" src="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.CurrentStoreLogoFileId))" alt="@Model.CurrentDisplayName" />

View File

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

View File

@ -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", $"DEPRECATED: Connection string to a MySQL database", CommandOptionType.SingleValue);
app.Option("--mysql", $"Connection string to a MySQL database", CommandOptionType.SingleValue);
app.Option("--nocsp", $"Disable CSP (default false)", CommandOptionType.BoolValue);
app.Option("--sqlitefile", $"DEPRECATED: File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
app.Option("--sqlitefile", $"File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
app.Option("--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);

View File

@ -2,19 +2,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Rating;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
@ -37,19 +32,12 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly EventAggregator _eventAggregator;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
private readonly CurrencyNameTable _currencyNameTable;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly PullPaymentHostedService _pullPaymentService;
private readonly RateFetcher _rateProvider;
private readonly ApplicationDbContextFactory _dbContextFactory;
public LanguageService LanguageService { get; }
public GreenfieldInvoiceController(UIInvoiceController invoiceController, InvoiceRepository invoiceRepository,
LinkGenerator linkGenerator, LanguageService languageService, BTCPayNetworkProvider btcPayNetworkProvider,
EventAggregator eventAggregator, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
CurrencyNameTable currencyNameTable, BTCPayNetworkProvider networkProvider, RateFetcher rateProvider,
PullPaymentHostedService pullPaymentService, ApplicationDbContextFactory dbContextFactory)
EventAggregator eventAggregator, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary)
{
_invoiceController = invoiceController;
_invoiceRepository = invoiceRepository;
@ -57,11 +45,6 @@ namespace BTCPayServer.Controllers.Greenfield
_btcPayNetworkProvider = btcPayNetworkProvider;
_eventAggregator = eventAggregator;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
_currencyNameTable = currencyNameTable;
_networkProvider = networkProvider;
_rateProvider = rateProvider;
_pullPaymentService = pullPaymentService;
_dbContextFactory = dbContextFactory;
LanguageService = languageService;
}
@ -350,175 +333,6 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateValidationError(ModelState);
}
[Authorize(Policy = Policies.CanModifyStoreSettings,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/refund")]
public async Task<IActionResult> RefundInvoice(
string storeId,
string invoiceId,
RefundInvoiceRequest request,
CancellationToken cancellationToken = default
)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return StoreNotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice == null)
{
return InvoiceNotFound();
}
if (invoice.StoreId != store.Id)
{
return InvoiceNotFound();
}
if (!invoice.GetInvoiceState().CanRefund())
{
return this.CreateAPIError("non-refundable", "Cannot refund this invoice");
}
PaymentMethod? invoicePaymentMethod = null;
PaymentMethodId? paymentMethodId = null;
if (request.PaymentMethod is not null && PaymentMethodId.TryParse(request.PaymentMethod, out paymentMethodId))
{
invoicePaymentMethod = invoice.GetPaymentMethods().SingleOrDefault(method => method.GetId() == paymentMethodId);
}
if (invoicePaymentMethod is null)
{
this.ModelState.AddModelError(nameof(request.PaymentMethod), "Please select one of the payment methods which were available for the original invoice");
}
if (request.RefundVariant is null)
this.ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory");
if (!ModelState.IsValid || invoicePaymentMethod is null || paymentMethodId is null)
return this.CreateValidationError(ModelState);
var cryptoPaid = invoicePaymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC);
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility);
var rateResult = await _rateProvider.FetchRate(
new CurrencyPair(paymentMethodId.CryptoCode, invoice.Currency),
store.GetStoreBlob().GetRateRules(_networkProvider),
cancellationToken
);
var paymentMethodDivisibility = _currencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
var createPullPayment = new HostedServices.CreatePullPayment()
{
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration,
Name = request.Name ?? $"Refund {invoice.Id}",
Description = request.Description,
StoreId = storeId,
PaymentMethodIds = new[] { paymentMethodId },
};
if (request.RefundVariant != RefundVariant.Custom)
{
if (request.CustomAmount is not null)
this.ModelState.AddModelError(nameof(request.CustomAmount), "CustomAmount should only be set if the refundVariant is Custom");
if (request.CustomCurrency is not null)
this.ModelState.AddModelError(nameof(request.CustomCurrency), "CustomCurrency should only be set if the refundVariant is Custom");
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
}
switch (request.RefundVariant)
{
case RefundVariant.RateThen:
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
createPullPayment.Amount = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
createPullPayment.AutoApproveClaims = true;
break;
case RefundVariant.CurrentRate:
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
createPullPayment.Amount = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
createPullPayment.AutoApproveClaims = true;
break;
case RefundVariant.Fiat:
createPullPayment.Currency = invoice.Currency;
createPullPayment.Amount = paidCurrency;
createPullPayment.AutoApproveClaims = false;
break;
case RefundVariant.Custom:
if (request.CustomAmount is null || (request.CustomAmount is decimal v && v <= 0)) {
this.ModelState.AddModelError(nameof(request.CustomAmount), "Amount must be greater than 0");
}
if (
string.IsNullOrEmpty(request.CustomCurrency) ||
_currencyNameTable.GetCurrencyData(request.CustomCurrency, false) == null
)
{
ModelState.AddModelError(nameof(request.CustomCurrency), "Invalid currency");
}
if (rateResult.BidAsk is null)
{
ModelState.AddModelError(nameof(request.RefundVariant),
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
}
if (!ModelState.IsValid || request.CustomAmount is null)
{
return this.CreateValidationError(ModelState);
}
createPullPayment.Currency = request.CustomCurrency;
createPullPayment.Amount = request.CustomAmount.Value;
createPullPayment.AutoApproveClaims = paymentMethodId.CryptoCode == request.CustomCurrency;
break;
default:
ModelState.AddModelError(nameof(request.RefundVariant), "Please select a valid refund option");
return this.CreateValidationError(ModelState);
}
var ppId = await _pullPaymentService.CreatePullPayment(createPullPayment);
await using var ctx = _dbContextFactory.CreateContext();
(await ctx.Invoices.FindAsync(new[] { invoice.Id }, cancellationToken))!.CurrentRefundId = ppId;
ctx.Refunds.Add(new RefundData
{
InvoiceDataId = invoice.Id,
PullPaymentDataId = ppId
});
await ctx.SaveChangesAsync(cancellationToken);
var pp = await _pullPaymentService.GetPullPayment(ppId, false);
return this.Ok(CreatePullPaymentData(pp));
}
private Client.Models.PullPaymentData CreatePullPaymentData(Data.PullPaymentData pp)
{
var ppBlob = pp.GetBlob();
return new BTCPayServer.Client.Models.PullPaymentData()
{
Id = pp.Id,
StartsAt = pp.StartDate,
ExpiresAt = pp.EndDate,
Amount = ppBlob.Limit,
Name = ppBlob.Name,
Description = ppBlob.Description,
Currency = ppBlob.Currency,
Period = ppBlob.Period,
Archived = pp.Archived,
AutoApproveClaims = ppBlob.AutoApproveClaims,
BOLT11Expiration = ppBlob.BOLT11Expiration,
ViewLink = _linkGenerator.GetUriByAction(
nameof(UIPullPaymentController.ViewPullPayment),
"UIPullPayment",
new { pullPaymentId = pp.Id },
Request.Scheme,
Request.Host,
Request.PathBase)
};
}
private IActionResult InvoiceNotFound()
{
return this.CreateAPIError(404, "invoice-not-found", "The invoice was not found");

View File

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

View File

@ -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, (string[] ) null);
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId);
var preFiltering = true;
if (statusFilter?.Any() is true || !string.IsNullOrWhiteSpace(labelFilter))

View File

@ -24,6 +24,7 @@ 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;
@ -133,7 +134,7 @@ namespace BTCPayServer.Controllers
Events = invoice.Events,
PosData = PosDataParser.ParsePosData(invoice.Metadata.PosData),
Archived = invoice.Archived,
CanRefund = invoiceState.CanRefund(),
CanRefund = CanRefund(invoiceState),
Refunds = invoice.Refunds,
ShowCheckout = invoice.Status == InvoiceStatusLegacy.New,
ShowReceipt = invoice.Status.ToModernStatus() == InvoiceStatus.Settled && (invoice.ReceiptOptions?.Enabled ?? receipt.Enabled is true),
@ -178,6 +179,11 @@ namespace BTCPayServer.Controllers
}
JToken? receiptData = null;
i.Metadata?.AdditionalData?.TryGetValue("receiptData", out receiptData);
string? formResponse = null;
if (i.Metadata?.AdditionalData?.TryGetValue("formResponse", out var formResponseRaw)is true)
{
formResponseRaw.Value<string>();
}
var payments = i.GetPayments(true)
.Select(paymentEntity =>
@ -234,6 +240,16 @@ namespace BTCPayServer.Controllers
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)]
@ -252,7 +268,7 @@ namespace BTCPayServer.Controllers
return NotFound();
if (invoice.CurrentRefund?.PullPaymentDataId is null && GetUserId() is null)
return NotFound();
if (!invoice.GetInvoiceState().CanRefund())
if (!CanRefund(invoice.GetInvoiceState()))
return NotFound();
if (invoice.CurrentRefund?.PullPaymentDataId is string ppId && !invoice.CurrentRefund.PullPaymentData.Archived)
{
@ -308,7 +324,7 @@ namespace BTCPayServer.Controllers
if (invoice == null)
return NotFound();
if (!invoice.GetInvoiceState().CanRefund())
if (!CanRefund(invoice.GetInvoiceState()))
return NotFound();
var store = GetCurrentStore();
@ -643,23 +659,9 @@ 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)
// 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();
}
var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider);
PaymentMethodId? invoicePaymentId = invoice.GetDefaultPaymentMethod();
PaymentMethodId? storePaymentId = store.GetDefaultPaymentId();
if (invoicePaymentId is not null)
@ -690,7 +692,6 @@ namespace BTCPayServer.Controllers
}
if (paymentMethodId is null)
return null;
BTCPayNetworkBase network = _NetworkProvider.GetNetwork<BTCPayNetworkBase>(paymentMethodId.CryptoCode);
if (network is null || !invoice.Support(paymentMethodId))
{
@ -717,10 +718,12 @@ namespace BTCPayServer.Controllers
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())
@ -823,18 +826,16 @@ 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 == btcId.ToString());
var lightningPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == lnId.ToString());
var onchainPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == "BTC");
var lightningPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == "BTC_LightningLike");
if (onchainPM != null && lightningPM != null)
{
model.AvailableCryptos.Remove(lightningPM);
}
}
paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob, paymentMethod);
model.UISettings = paymentMethodHandler.GetCheckoutUISettings();
model.PaymentMethodId = paymentMethodId.ToString();

View File

@ -193,8 +193,7 @@ namespace BTCPayServer.Controllers
Metadata = invoiceMetadata.ToJObject(),
Currency = pr.Currency,
Amount = amount,
Checkout = { RedirectURL = redirectUrl },
Receipt = new InvoiceDataBase.ReceiptOptions { Enabled = false }
Checkout = { RedirectURL = redirectUrl }
};
var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(pr.Id) };

View File

@ -4,7 +4,6 @@ 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;
@ -21,7 +20,6 @@ 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;
@ -53,7 +51,6 @@ namespace BTCPayServer
private readonly LightningAddressService _lightningAddressService;
private readonly LightningLikePayoutHandler _lightningLikePayoutHandler;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
public UILNURLController(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
@ -65,8 +62,7 @@ namespace BTCPayServer
LinkGenerator linkGenerator,
LightningAddressService lightningAddressService,
LightningLikePayoutHandler lightningLikePayoutHandler,
PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings)
PullPaymentHostedService pullPaymentHostedService)
{
_invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
@ -79,12 +75,11 @@ 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, CancellationToken cancellationToken)
public async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
@ -160,28 +155,25 @@ namespace BTCPayServer
{
var client =
_lightningLikePaymentHandler.CreateLightningClient(pm, network);
var payResult = await UILightningLikePayoutController.TrypayBolt(client,
claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings),
claimResponse.PayoutData, result, pmi, cancellationToken);
PayResponse payResult;
try
{
payResult = await client.Pay(pr);
}
catch (Exception e)
{
payResult = new PayResponse(PayResult.Error, e.Message);
}
switch (payResult.Result)
{
case PayResult.Ok:
case PayResult.Unknown:
await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest()
{
PayoutId = claimResponse.PayoutData.Id,
State = claimResponse.PayoutData.State,
Proof = claimResponse.PayoutData.GetProofBlobJson()
PayoutId = claimResponse.PayoutData.Id, State = PayoutState.Completed
});
return Ok(new LNUrlStatusResponse
{
Status = "OK",
Reason = payResult.Message
});
case PayResult.CouldNotFindRoute:
case PayResult.Error:
return Ok(new LNUrlStatusResponse {Status = "OK"});
default:
await _pullPaymentHostedService.Cancel(
new PullPaymentHostedService.CancelRequest(new string[]
@ -192,7 +184,7 @@ namespace BTCPayServer
return Ok(new LNUrlStatusResponse
{
Status = "ERROR",
Reason = payResult.Message
Reason = $"Pr could not be paid because {payResult.ErrorDetail}"
});
}
}

View File

@ -1,15 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.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;
@ -18,8 +17,10 @@ using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Extensions;
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;
@ -39,8 +40,6 @@ namespace BTCPayServer.Controllers
private readonly InvoiceRepository _InvoiceRepository;
private readonly StoreRepository _storeRepository;
private FormComponentProviders FormProviders { get; }
public UIPaymentRequestController(
UIInvoiceController invoiceController,
UserManager<ApplicationUser> userManager,
@ -49,8 +48,7 @@ namespace BTCPayServer.Controllers
EventAggregator eventAggregator,
CurrencyNameTable currencies,
StoreRepository storeRepository,
InvoiceRepository invoiceRepository,
FormComponentProviders formProviders)
InvoiceRepository invoiceRepository)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
@ -60,7 +58,6 @@ namespace BTCPayServer.Controllers
_Currencies = currencies;
_storeRepository = storeRepository;
_InvoiceRepository = invoiceRepository;
FormProviders = formProviders;
}
[BitpayAPIConstraint(false)]
@ -184,7 +181,7 @@ namespace BTCPayServer.Controllers
[HttpGet("{payReqId}/form")]
[HttpPost("{payReqId}/form")]
[AllowAnonymous]
public async Task<IActionResult> ViewPaymentRequestForm(string payReqId)
public async Task<IActionResult> ViewPaymentRequestForm(string payReqId, [FromForm] string formId, [FromForm] string formData)
{
var result = await _PaymentRequestRepository.FindPaymentRequest(payReqId, GetUserId());
if (result == null)
@ -194,41 +191,36 @@ namespace BTCPayServer.Controllers
var prBlob = result.GetBlob();
var prFormId = prBlob.FormId;
var formConfig = prFormId is null ? null : Forms.UIFormsController.GetFormData(prFormId)?.Config;
switch (formConfig)
switch (prFormId)
{
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:
case { } when string.IsNullOrEmpty(prFormId):
break;
default:
// POST case: Handle form submit
var formData = Form.Parse(formConfig);
formData.ApplyValuesFromForm(Request.Form);
if (FormProviders.Validate(formData, ModelState))
if (!string.IsNullOrEmpty(formData) && formId == prFormId)
{
prBlob.FormResponse = JObject.FromObject(formData.GetValues());
prBlob.FormResponse = formData;
result.SetBlob(prBlob);
await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(result);
return RedirectToAction("PayPaymentRequest", new { payReqId });
}
break;
// GET or empty form data case: Redirect to form
return View("PostRedirect", new PostRedirectViewModel
{
AspController = "UIForms",
AspAction = "ViewPublicForm",
FormParameters =
{
{ "formId", prFormId },
{ "redirectUrl", Request.GetCurrentUrl() }
}
});
}
return View("PostRedirect", new PostRedirectViewModel
{
AspController = "UIForms",
AspAction = "ViewPublicForm",
RouteParameters =
{
{ "formId", prFormId }
},
FormParameters =
{
{ "redirectUrl", Request.GetCurrentUrl() }
}
});
return RedirectToAction("ViewPaymentRequest", new { payReqId });
}
[HttpGet("{payReqId}/pay")]

View File

@ -338,11 +338,11 @@ namespace BTCPayServer.Controllers
Revision = payout.GetBlob(_jsonSerializerSettings).Revision,
Rate = rateResult.BidAsk.Ask
});
if (approveResult.Result != PullPaymentHostedService.PayoutApproval.Result.Ok)
if (approveResult != PullPaymentHostedService.PayoutApproval.Result.Ok)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult.Result),
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult),
Severity = StatusMessageModel.StatusSeverity.Error
});
failed = true;

View File

@ -89,17 +89,17 @@ namespace BTCPayServer.Controllers
if (vm.WalletFile != null)
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy, out var error))
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy))
{
ModelState.AddModelError(nameof(vm.WalletFile), $"Importing wallet failed: {error}");
ModelState.AddModelError(nameof(vm.WalletFile), "Wallet file was not in the correct format");
return View(vm.ViewName, vm);
}
}
else if (!string.IsNullOrEmpty(vm.WalletFileContent))
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy, out var error))
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy))
{
ModelState.AddModelError(nameof(vm.WalletFileContent), $"QR import failed: {error}");
ModelState.AddModelError(nameof(vm.WalletFileContent), "QR import was not in the correct format");
return View(vm.ViewName, vm);
}
}

View File

@ -578,24 +578,17 @@ namespace BTCPayServer.Controllers
var utxos = await _walletProvider.GetWallet(network)
.GetUnspentCoins(schemeSettings.AccountDerivation, false, cancellation);
var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId,
utxos.SelectMany(u => GetWalletObjectsQuery.Get(u)).Distinct().ToArray());
var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId, utxos.Select(u => u.OutPoint.Hash.ToString()).Distinct().ToArray());
vm.InputsAvailable = utxos.Select(coin =>
{
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
walletTransactionsInfoAsync.TryGetValue(coin.ScriptPubKey.ToHex(), out var info2);
if (info is not null && info2 is not null)
{
info.Merge(info2);
}
info ??= info2;
var labels = CreateTransactionTagModels(info).ToList();
return new WalletSendModel.InputSelectionOption()
{
Outpoint = coin.OutPoint.ToString(),
Amount = coin.Value.GetValue(network),
Comment = info?.Comment,
Labels = CreateTransactionTagModels(info),
Labels = labels,
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
coin.OutPoint.Hash.ToString()),
Confirmations = coin.Confirmations
@ -1298,7 +1291,7 @@ namespace BTCPayServer.Controllers
return NotFound();
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId, (string[] ) null);
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId);
var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, null, null);
var walletTransactionsInfo = await walletTransactionsInfoAsync;
var export = new TransactionsExport(wallet, walletTransactionsInfo);

View File

@ -257,7 +257,9 @@ 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)
@ -279,13 +281,17 @@ 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
}, cancellationToken);
}, c.Token);
string message = null;
if (result.Result == PayResult.Ok)
{
@ -303,11 +309,6 @@ 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

View File

@ -47,7 +47,7 @@ namespace BTCPayServer.Data
public static StoreBlob GetStoreBlob(this StoreData storeData)
{
var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(storeData.StoreBlob);
var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(Encoding.UTF8.GetString(storeData.StoreBlob));
if (result.PreferredExchange == null)
result.PreferredExchange = CoinGeckoRateProvider.CoinGeckoName;
if (result.PaymentMethodCriteria is null)
@ -62,7 +62,7 @@ namespace BTCPayServer.Data
var newBlob = new Serializer(null).ToString(storeBlob);
if (original == newBlob)
return false;
storeData.StoreBlob = newBlob;
storeData.StoreBlob = Encoding.UTF8.GetBytes(newBlob);
return true;
}

View File

@ -7,6 +7,7 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Labels;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace BTCPayServer.Data
{
@ -82,23 +83,5 @@ namespace BTCPayServer.Data
}
}
public string Type { get; set; }
public void Merge(WalletTransactionInfo? value)
{
if (value is null)
return;
foreach (var valueLabelColor in value.LabelColors)
{
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)))
{
Attachments.Add(valueAttachment);
}
}
}
}

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using BTCPayServer.Payments;
using NBitcoin;
using NBitcoin.DataEncoders;
@ -17,18 +16,17 @@ 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, ref error, false) || TryParseXpub(derivationStrategy, parser, ref result, ref error, true))
if (TryParseXpub(derivationStrategy, parser, ref result, false) || TryParseXpub(derivationStrategy, parser, ref result, true))
{
return result;
}
throw new FormatException($"Invalid Derivation Scheme: {error}");
throw new FormatException("Invalid Derivation Scheme");
}
public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy)
@ -49,11 +47,10 @@ 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, ref string error, bool electrum = true)
private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, bool electrum = true)
{
if (!electrum)
{
var isOD = Regex.Match(xpub, @"\(.*?\)").Success;
try
{
var result = derivationSchemeParser.ParseOutputDescriptor(xpub);
@ -67,13 +64,9 @@ namespace BTCPayServer
}).ToArray();
return true;
}
catch (Exception exception)
catch (Exception)
{
error = exception.Message;
if (isOD)
{
return false;
} // otherwise continue and try to parse input as xpub
// ignored
}
}
try
@ -89,22 +82,20 @@ 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 exception)
catch (Exception)
{
error = exception.Message;
return false;
}
}
public static bool TryParseFromWalletFile(string fileContents, BTCPayNetwork network, out DerivationSchemeSettings settings, out string error)
public static bool TryParseFromWalletFile(string fileContents, BTCPayNetwork network, out DerivationSchemeSettings settings)
{
settings = null;
error = null;
ArgumentNullException.ThrowIfNull(fileContents);
ArgumentNullException.ThrowIfNull(network);
var result = new DerivationSchemeSettings();
var derivationSchemeParser = new DerivationSchemeParser(network);
JObject jobj;
JObject jobj = null;
try
{
if (HexEncoder.IsWellFormed(fileContents))
@ -116,8 +107,8 @@ namespace BTCPayServer
catch
{
result.Source = "GenericFile";
if (TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error) ||
TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error, false))
if (TryParseXpub(fileContents, derivationSchemeParser, ref result) ||
TryParseXpub(fileContents, derivationSchemeParser, ref result, false))
{
settings = result;
settings.Network = network;
@ -134,7 +125,7 @@ namespace BTCPayServer
jobj = (JObject)jobj["keystore"];
if (!jobj.ContainsKey("xpub") ||
!TryParseXpub(jobj["xpub"].Value<string>(), derivationSchemeParser, ref result, ref error))
!TryParseXpub(jobj["xpub"].Value<string>(), derivationSchemeParser, ref result))
{
return false;
}
@ -171,7 +162,7 @@ namespace BTCPayServer
{
result.Source = "SpecterFile";
if (!TryParseXpub(jobj["descriptor"].Value<string>(), derivationSchemeParser, ref result, ref error, false))
if (!TryParseXpub(jobj["descriptor"].Value<string>(), derivationSchemeParser, ref result, false))
{
return false;
}
@ -190,7 +181,7 @@ namespace BTCPayServer
{
result.Source = "WasabiFile";
if (!jobj.ContainsKey("ExtPubKey") ||
!TryParseXpub(jobj["ExtPubKey"].Value<string>(), derivationSchemeParser, ref result, ref error, false))
!TryParseXpub(jobj["ExtPubKey"].Value<string>(), derivationSchemeParser, ref result, false))
{
return false;
}

View File

@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Form;
namespace BTCPayServer.Forms;
public class FormComponentProvider : IFormComponentProvider
{
private readonly IEnumerable<IFormComponentProvider> _formComponentProviders;
public FormComponentProvider(IEnumerable<IFormComponentProvider> formComponentProviders)
{
_formComponentProviders = formComponentProviders;
}
public string CanHandle(Field field)
{
return _formComponentProviders.Select(formComponentProvider => formComponentProvider.CanHandle(field)).FirstOrDefault(result => !string.IsNullOrEmpty(result));
}
}

View File

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

View File

@ -10,7 +10,7 @@ public static class FormDataExtensions
public static void AddForms(this IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<FormDataService>();
serviceCollection.AddSingleton<FormComponentProviders>();
serviceCollection.AddSingleton<FormComponentProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlInputFormProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlFieldsetFormProvider>();
}

View File

@ -15,21 +15,21 @@ public class FormDataService
public static readonly Form StaticFormEmail = new()
{
Fields = new List<Field>() {Field.Create("Enter your email", "buyerEmail", null, true, null, "email")}
Fields = new List<Field>() {new HtmlInputField("Enter your email", "buyerEmail", null, true, null)}
};
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)
new HtmlInputField("Enter your email", "buyerEmail", null, true, null, "email"),
new HtmlInputField("Name", "buyerName", null, true, null),
new HtmlInputField("Address Line 1", "buyerAddress1", null, true, null),
new HtmlInputField("Address Line 2", "buyerAddress2", null, false, null),
new HtmlInputField("City", "buyerCity", null, true, null),
new HtmlInputField("Postcode", "buyerZip", null, false, null),
new HtmlInputField("State", "buyerState", null, false, null),
new HtmlInputField("Country", "buyerCountry", null, true, null)
}
};
}

View File

@ -1,23 +1,12 @@
using System.Collections.Generic;
using System.Linq;
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)
public string CanHandle(Field field)
{
typeToComponentProvider.Add("fieldset", this);
return new[] { "fieldset"}.Contains(field.Type) ? "Forms/FieldSetElement" : null;
}
public void Validate(Field field)
{
}
public void Validate(Form form, Field field)
{
}
}
}

View File

@ -1,16 +1,13 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Validation;
namespace BTCPayServer.Forms;
public class HtmlInputFormProvider: FormComponentProviderBase
public class HtmlInputFormProvider: IFormComponentProvider
{
public override void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
public string CanHandle(Field field)
{
foreach (var t in new[] {
return new[] {
"text",
"radio",
"checkbox",
@ -32,20 +29,6 @@ public class HtmlInputFormProvider: FormComponentProviderBase
"search",
"url",
"tel",
"reset"})
typeToComponentProvider.Add(t, this);
"reset"}.Contains(field.Type) ? "Forms/InputElement" : null;
}
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);
}
}
}
}

View File

@ -1,26 +1,8 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Abstractions.Form;
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);
}
public string CanHandle(Field field);
}

View File

@ -8,6 +8,5 @@ public class FormViewModel
{
public string RedirectUrl { get; set; }
public FormData FormData { get; set; }
Form _Form;
public Form Form { get => _Form ??= Form.Parse(FormData.Config); }
public Form Form { get => JObject.Parse(FormData.Config).ToObject<Form>(); }
}

View File

@ -1,36 +1,36 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Data.Data;
using BTCPayServer.Forms.Models;
using BTCPayServer.Models;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
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)
{
@ -39,35 +39,25 @@ public class UIFormsController : Controller
: 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)
public IActionResult SubmitForm(
string formId, string? redirectUrl,
[FromServices] StoreRepository storeRepository,
[FromServices] UIInvoiceController invoiceController)
{
if (!IsValidRedirectUri(redirectUrl))
return BadRequest();
var formData = GetFormData(formId);
if (formData?.Config is null)
if (formData 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);
var dbForm = JObject.Parse(formData.Config!).ToObject<Form>()!;
dbForm.ApplyValuesFromForm(Request.Form);
Dictionary<string, object> data = dbForm.GetValues();
// With redirect, the form comes from another entity that we need to send the data back to
if (!string.IsNullOrEmpty(redirectUrl))
@ -75,26 +65,30 @@ public class UIFormsController : Controller
return View("PostRedirect", new PostRedirectViewModel
{
FormUrl = redirectUrl,
FormParameters = form
FormParameters =
{
{ "formId", formData.Id },
{ "formData", JsonConvert.SerializeObject(data) }
}
});
}
return NotFound();
}
internal static FormData? GetFormData(string id)
private FormData? GetFormData(string id)
{
FormData? form = id switch
{
{ } formId when formId == GenericFormOption.Address.ToString() => new FormData
{
Config = FormDataService.StaticFormAddress.ToString(),
Config = JObject.FromObject(FormDataService.StaticFormAddress).ToString(),
Id = GenericFormOption.Address.ToString(),
Name = "Provide your address",
},
{ } formId when formId == GenericFormOption.Email.ToString() => new FormData
{
Config = FormDataService.StaticFormEmail.ToString(),
Config = JObject.FromObject(FormDataService.StaticFormEmail).ToString(),
Id = GenericFormOption.Email.ToString(),
Name = "Provide your email address",
},
@ -102,8 +96,4 @@ public class UIFormsController : Controller
};
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));
}

View File

@ -173,10 +173,10 @@ next:
db.WalletObjectLinks.Add(new WalletObjectLinkData()
{
WalletId = tx.WalletDataId,
BType = Data.WalletObjectData.Types.Tx,
BId = tx.TransactionId,
AType = Data.WalletObjectData.Types.Label,
AId = labelId
ChildType = Data.WalletObjectData.Types.Tx,
ChildId = tx.TransactionId,
ParentType = Data.WalletObjectData.Types.Label,
ParentId = labelId
});
if (label.Value is ReferenceLabel reflabel)
@ -195,10 +195,10 @@ next:
db.WalletObjectLinks.Add(new WalletObjectLinkData()
{
WalletId = tx.WalletDataId,
BType = Data.WalletObjectData.Types.Tx,
BId = tx.TransactionId,
AType = reflabel.Type,
AId = reflabel.Reference ?? String.Empty
ChildType = Data.WalletObjectData.Types.Tx,
ChildId = tx.TransactionId,
ParentType = reflabel.Type,
ParentId = reflabel.Reference ?? String.Empty
});
}
}
@ -224,10 +224,10 @@ next:
db.WalletObjectLinks.Add(new WalletObjectLinkData()
{
WalletId = tx.WalletDataId,
BType = Data.WalletObjectData.Types.Tx,
BId = tx.TransactionId,
AType = "payout",
AId = payout
ChildType = Data.WalletObjectData.Types.Tx,
ChildId = tx.TransactionId,
ParentType = "payout",
ParentId = payout
});
}
}

View File

@ -79,12 +79,10 @@ 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<ApprovalResult> Completion { get; set; }
internal TaskCompletionSource<Result> Completion { get; set; }
public static string GetErrorMessage(Result result)
{
@ -335,10 +333,10 @@ namespace BTCPayServer.HostedServices
return _rateFetcher.FetchRate(rule, cancellationToken);
}
public Task<PayoutApproval.ApprovalResult> Approve(PayoutApproval approval)
public Task<PayoutApproval.Result> Approve(PayoutApproval approval)
{
approval.Completion =
new TaskCompletionSource<PayoutApproval.ApprovalResult>(TaskCreationOptions.RunContinuationsAsynchronously);
new TaskCompletionSource<PayoutApproval.Result>(TaskCreationOptions.RunContinuationsAsynchronously);
if (!_Channel.Writer.TryWrite(approval))
throw new ObjectDisposedException(nameof(PullPaymentHostedService));
return approval.Completion.Task;
@ -353,26 +351,26 @@ namespace BTCPayServer.HostedServices
.FirstOrDefaultAsync();
if (payout is null)
{
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.NotFound, null));
req.Completion.SetResult(PayoutApproval.Result.NotFound);
return;
}
if (payout.State != PayoutState.AwaitingApproval)
{
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.InvalidState, null));
req.Completion.SetResult(PayoutApproval.Result.InvalidState);
return;
}
var payoutBlob = payout.GetBlob(this._jsonSerializerSettings);
if (payoutBlob.Revision != req.Revision)
{
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.OldRevision, null));
req.Completion.SetResult(PayoutApproval.Result.OldRevision);
return;
}
if (!PaymentMethodId.TryParse(payout.PaymentMethodId, out var paymentMethod))
{
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.NotFound, null));
req.Completion.SetResult(PayoutApproval.Result.NotFound);
return;
}
@ -390,7 +388,7 @@ namespace BTCPayServer.HostedServices
await payoutHandler.GetMinimumPayoutAmount(paymentMethod, dest.destination);
if (cryptoAmount < minimumCryptoAmount)
{
req.Completion.TrySetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.TooLowAmount, null));
req.Completion.TrySetResult(PayoutApproval.Result.TooLowAmount);
return;
}
@ -399,7 +397,7 @@ namespace BTCPayServer.HostedServices
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
await ctx.SaveChangesAsync();
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.Ok, payoutBlob.CryptoAmount));
req.Completion.SetResult(PayoutApproval.Result.Ok);
}
catch (Exception ex)
{
@ -568,20 +566,18 @@ namespace BTCPayServer.HostedServices
var rateResult = await GetRate(payout, null, CancellationToken.None);
if (rateResult.BidAsk != null)
{
var approveResultTask = new TaskCompletionSource<PayoutApproval.ApprovalResult>();
var approveResult = new TaskCompletionSource<PayoutApproval.Result>();
await HandleApproval(new PayoutApproval()
{
PayoutId = payout.Id,
Revision = payoutBlob.Revision,
Rate = rateResult.BidAsk.Ask,
Completion = approveResultTask
Completion = approveResult
});
var approveResult = await approveResultTask.Task;
if (approveResult.Result == PayoutApproval.Result.Ok)
if ((await approveResult.Task) == PayoutApproval.Result.Ok)
{
payout.State = PayoutState.AwaitingPayment;
payoutBlob.CryptoAmount = approveResult.CryptoAmount;
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
}
}
}

View File

@ -5,7 +5,6 @@ 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;
@ -23,79 +22,42 @@ namespace BTCPayServer.HostedServices
{
public class TransactionLabelMarkerHostedService : EventHostedServiceBase
{
private readonly EventAggregator _eventAggregator;
private readonly WalletRepository _walletRepository;
public TransactionLabelMarkerHostedService(EventAggregator eventAggregator, WalletRepository walletRepository, Logs logs) :
base(eventAggregator, logs)
{
_eventAggregator = eventAggregator;
_walletRepository = walletRepository;
}
protected override void SubscribeToEvents()
{
Subscribe<InvoiceEvent>();
Subscribe<NewOnChainTransactionEvent>();
}
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
switch (evt)
if (evt is InvoiceEvent invoiceEvent && invoiceEvent.Name == InvoiceEvent.ReceivedPayment &&
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance &&
invoiceEvent.Payment.GetCryptoPaymentData() is BitcoinLikePaymentData bitcoinLikePaymentData)
{
// 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 walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode());
var transactionId = bitcoinLikePaymentData.Outpoint.Hash;
var labels = new List<Attachment>
{
var txHash = transactionEvent.NewTransactionEvent.TransactionData.TransactionHash.ToString();
// 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 => new ObjectTypeId(WalletObjectData.Types.Utxo, txIn.PrevOut.ToString()))
.Concat(transactionEvent.NewTransactionEvent.TransactionData.Transaction.Outputs.AsIndexedOutputs().SelectMany(txOut =>
new[]{
new ObjectTypeId(WalletObjectData.Types.Script,txOut.TxOut.ScriptPubKey.ToHex()),
new ObjectTypeId(WalletObjectData.Types.Utxo,txOut.ToCoin().Outpoint.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:
Attachment.Invoice(invoiceEvent.Invoice.Id)
};
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
{
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;
labels.Add(Attachment.PaymentRequest(paymentId));
}
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice))
{
labels.Add(Attachment.App(appId));
}
await _walletRepository.AddWalletTransactionAttachment(walletId, transactionId, labels);
}
}
}

View File

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

View File

@ -88,7 +88,7 @@ namespace BTCPayServer.Hosting
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, MigratedTransactionLabels = int.MaxValue };
settings = new MigrationSettings() { MigratedInvoiceTextSearchPages = int.MaxValue };
foreach (var prop in settings.GetType().GetProperties().Where(p => p.CanWrite && p.PropertyType == typeof(bool)))
{
prop.SetValue(settings, true);

View File

@ -46,9 +46,9 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
CustomCSSLink = blob.CustomCSSLink;
EmbeddedCSS = blob.EmbeddedCSS;
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
FormResponse = blob.FormResponse is null
FormResponse = string.IsNullOrEmpty(blob.FormResponse)
? null
: blob.FormResponse.ToObject<Dictionary<string, object>>();
: JObject.Parse(blob.FormResponse).ToObject<Dictionary<string, object>>();
}
[Display(Name = "Request customer data on checkout")]

View File

@ -98,7 +98,7 @@ namespace BTCPayServer.PaymentRequest
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
LastUpdated = DateTime.UtcNow,
FormId = blob.FormId,
FormSubmitted = blob.FormResponse is not null,
FormSubmitted = !string.IsNullOrEmpty(blob.FormResponse),
AnyPendingInvoice = pendingInvoice != null,
PendingInvoiceHasPayments = pendingInvoice != null &&
pendingInvoice.ExceptionStatus != InvoiceExceptionStatus.None,

View File

@ -14,6 +14,7 @@ public static class PayoutProcessorsExtensions
serviceCollection.AddSingleton<IPayoutProcessorFactory>(provider => provider.GetRequiredService<OnChainAutomatedPayoutSenderFactory>());
serviceCollection.AddSingleton<LightningAutomatedPayoutSenderFactory>();
serviceCollection.AddSingleton<IPayoutProcessorFactory>(provider => provider.GetRequiredService<LightningAutomatedPayoutSenderFactory>());
serviceCollection.AddHostedService<PayoutProcessorService>();
serviceCollection.AddSingleton<PayoutProcessorService>();
serviceCollection.AddHostedService(s=> s.GetRequiredService<PayoutProcessorService>());
}

View File

@ -9,13 +9,11 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Forms;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Plugins.PointOfSale.Models;
@ -41,23 +39,19 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
AppService appService,
CurrencyNameTable currencies,
StoreRepository storeRepository,
UIInvoiceController invoiceController,
FormComponentProviders formProviders)
UIInvoiceController invoiceController)
{
_currencies = currencies;
_appService = appService;
_storeRepository = storeRepository;
_invoiceController = invoiceController;
FormProviders = formProviders;
}
private readonly CurrencyNameTable _currencies;
private readonly StoreRepository _storeRepository;
private readonly AppService _appService;
private readonly UIInvoiceController _invoiceController;
public FormComponentProviders FormProviders { get; }
[HttpGet("/")]
[HttpGet("/apps/{appId}/pos/{viewType?}")]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
@ -124,6 +118,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
string notificationUrl,
string redirectUrl,
string choiceKey,
string formId = null,
string formData = null,
string posData = null,
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
CancellationToken cancellationToken = default)
@ -225,21 +221,18 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
var store = await _appService.GetStore(app);
var posFormId = settings.FormId;
var formConfig = posFormId is null ? null : Forms.UIFormsController.GetFormData(posFormId)?.Config;
JObject formResponse = null;
switch (formConfig)
switch (posFormId)
{
case null:
case { } when !this.Request.HasFormContentType:
case { } when string.IsNullOrEmpty(posFormId):
break;
default:
var formData = Form.Parse(formConfig);
formData.ApplyValuesFromForm(this.Request.Form);
if (FormProviders.Validate(formData, ModelState))
// POST case: Handle form submit
if (!string.IsNullOrEmpty(formData) && formId == posFormId)
{
formResponse = JObject.FromObject(formData.GetValues());
formResponse = JObject.Parse(formData);
break;
}
@ -254,12 +247,9 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
{
AspController = "UIForms",
AspAction = "ViewPublicForm",
RouteParameters =
{
{ "formId", posFormId }
},
FormParameters =
{
{ "formId", posFormId },
{ "redirectUrl", Request.GetCurrentUrl() + query }
}
});

View File

@ -7,7 +7,6 @@
"BTCPAY_NETWORK": "regtest",
"BTCPAY_LAUNCHSETTINGS": "true",
"BTCPAY_BTCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993",
"BTCPAY_BTCEXTERNALCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993",
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=http://lnd:lnd@127.0.0.1:35531/;allowinsecure=true",
"BTCPAY_BTCEXTERNALLNDSEEDBACKUP": "../BTCPayServer.Tests/TestData/LndSeedBackup/walletunlock.json",
"BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake",
@ -32,7 +31,7 @@
"BTCPAY_CHEATMODE": "true",
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"
},
"applicationUrl": "http://0.0.0.0:14142/"
"applicationUrl": "http://localhost:14142/"
},
"Bitcoin-HTTPS": {
"commandName": "Project",
@ -43,8 +42,7 @@
"BTCPAY_PORT": "14142",
"BTCPAY_HttpsUseDefaultCertificate": "true",
"BTCPAY_VERBOSE": "true",
"BTCPAY_BTCLIGHTNING": "type=lnd-rest;server=http://lnd:lnd@127.0.0.1:35531/;allowinsecure=true",
"BTCPAY_BTCEXTERNALCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993",
"BTCPAY_BTCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993",
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=http://lnd:lnd@127.0.0.1:35531/;allowinsecure=true",
"BTCPAY_BTCEXTERNALLNDSEEDBACKUP": "../BTCPayServer.Tests/TestData/LndSeedBackup/walletunlock.json",
"BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake",
@ -70,7 +68,7 @@
"BTCPAY_CHEATMODE": "true",
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"
},
"applicationUrl": "https://0.0.0.0:14142/"
"applicationUrl": "https://localhost:14142/"
},
"Altcoins-HTTPS": {
"commandName": "Project",
@ -109,7 +107,7 @@
"BTCPAY_CHEATMODE": "true",
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"
},
"applicationUrl": "https://0.0.0.0:14142/"
"applicationUrl": "https://localhost:14142/"
}
}
}

View File

@ -835,17 +835,6 @@ namespace BTCPayServer.Services.Invoices
(Status != InvoiceStatusLegacy.Invalid && ExceptionStatus == InvoiceExceptionStatus.Marked);
}
public bool CanRefund()
{
return Status == InvoiceStatusLegacy.Confirmed ||
Status == InvoiceStatusLegacy.Complete ||
(Status == InvoiceStatusLegacy.Expired &&
(ExceptionStatus == InvoiceExceptionStatus.PaidLate ||
ExceptionStatus == InvoiceExceptionStatus.PaidOver ||
ExceptionStatus == InvoiceExceptionStatus.PaidPartial)) ||
Status == InvoiceStatusLegacy.Invalid;
}
public override int GetHashCode()
{
return HashCode.Combine(Status, ExceptionStatus);

View File

@ -36,5 +36,5 @@ public static class CheckoutFormSelectList
typeof(GenericFormOption).DisplayName(opt.ToString());
private static SelectListItem GenericOptionItem(GenericFormOption opt) =>
new() { Text = DisplayName(opt), Value = opt == GenericFormOption.None ? null : opt.ToString() };
new() { Text = DisplayName(opt), Value = opt.ToString() };
}

View File

@ -5,7 +5,6 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Services.Wallets;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using Newtonsoft.Json;
@ -38,9 +37,8 @@ namespace BTCPayServer.Services
Type = type;
Ids = ids;
}
public GetWalletObjectsQuery(WalletId? walletId,ObjectTypeId[]? typesIds)
public GetWalletObjectsQuery(ObjectTypeId[]? typesIds)
{
WalletId = walletId;
TypesIds = typesIds;
}
@ -52,18 +50,6 @@ namespace BTCPayServer.Services
public string[]? Ids { get; set; }
public bool IncludeNeighbours { get; set; } = true;
public bool UseInefficientPath { get; set; }
public static ObjectTypeId Get(Script script)
{
return new ObjectTypeId(WalletObjectData.Types.Script, script.ToHex());
}
public static IEnumerable<ObjectTypeId> Get(ReceivedCoin coin)
{
yield return new ObjectTypeId(WalletObjectData.Types.Tx, coin.OutPoint.Hash.ToString());
yield return Get(coin.ScriptPubKey);
yield return new ObjectTypeId(WalletObjectData.Types.Utxo, coin.OutPoint.ToString());
}
}
#nullable restore
@ -92,7 +78,7 @@ namespace BTCPayServer.Services
using var ctx = _ContextFactory.CreateContext();
// If we are using postgres, the `transactionIds.Contains(w.BId)` result in a long query like `ANY(@txId1, @txId2, @txId3, @txId4)`
// If we are using postgres, the `transactionIds.Contains(w.ChildId)` result in a long query like `ANY(@txId1, @txId2, @txId3, @txId4)`
// Such request isn't well optimized by postgres, and create different requests clogging up
// pg_stat_statements output, making it impossible to analyze the performance impact of this query.
// On top of this, the entity version is doing 2 left join to satisfy the Include queries, resulting in n*m row returned for each transaction.
@ -120,9 +106,9 @@ namespace BTCPayServer.Services
var query =
$"SELECT wos.\"WalletId\", wos.\"Id\", wos.\"Type\", wos.\"Data\", wol.\"LinkData\", wol.\"Type2\", wol.\"Id2\"{includeNeighbourSelect} FROM ({selectWalletObjects}) wos " +
$"LEFT JOIN LATERAL ( " +
"SELECT \"AType\" AS \"Type2\", \"AId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"BType\"=wos.\"Type\" AND \"BId\"=wos.\"Id\" " +
"SELECT \"ParentType\" AS \"Type2\", \"ParentId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"ChildType\"=wos.\"Type\" AND \"ChildId\"=wos.\"Id\" " +
"UNION " +
"SELECT \"BType\" AS \"Type2\", \"BId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"AType\"=wos.\"Type\" AND \"AId\"=wos.\"Id\"" +
"SELECT \"ChildType\" AS \"Type2\", \"ChildId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"ParentType\"=wos.\"Type\" AND \"ParentId\"=wos.\"Id\"" +
$" ) wol ON true " + includeNeighbourJoin;
cmd.CommandText = query;
if (queryObject.WalletId is not null)
@ -191,21 +177,21 @@ namespace BTCPayServer.Services
else
{
wosById.Add(id, wo);
wo.Bs = new List<WalletObjectLinkData>();
wo.ChildLinks = new List<WalletObjectLinkData>();
}
if (reader["Type2"] is not DBNull)
{
var l = new WalletObjectLinkData()
{
BType = (string)reader["Type2"],
BId = (string)reader["Id2"],
ChildType = (string)reader["Type2"],
ChildId = (string)reader["Id2"],
Data = reader["LinkData"] is DBNull ? null : (string)reader["LinkData"]
};
wo.Bs.Add(l);
l.B = new WalletObjectData()
wo.ChildLinks.Add(l);
l.Child = new WalletObjectData()
{
Type = l.BType,
Id = l.BId,
Type = l.ChildType,
Id = l.ChildId,
Data = (!queryObject.IncludeNeighbours || reader["Data2"] is DBNull) ? null : (string)reader["Data2"]
};
}
@ -229,8 +215,8 @@ namespace BTCPayServer.Services
}
if (queryObject.IncludeNeighbours)
{
q = q.Include(o => o.Bs).ThenInclude(o => o.B)
.Include(o => o.As).ThenInclude(o => o.A);
q = q.Include(o => o.ChildLinks).ThenInclude(o => o.Child)
.Include(o => o.ParentLinks).ThenInclude(o => o.Parent);
}
q = q.AsNoTracking();
@ -244,28 +230,9 @@ namespace BTCPayServer.Services
}
}
#nullable restore
public async Task<Dictionary<string, WalletTransactionInfo>> GetWalletTransactionsInfo(WalletId walletId,
string[] transactionIds = null)
public async Task<Dictionary<string, WalletTransactionInfo>> GetWalletTransactionsInfo(WalletId walletId, string[] transactionIds = null)
{
var wos = await GetWalletObjects(
new GetWalletObjectsQuery(walletId, WalletObjectData.Types.Tx, transactionIds));
return await GetWalletTransactionsInfoCore(walletId, wos);
}
public async Task<Dictionary<string, WalletTransactionInfo>> GetWalletTransactionsInfo(WalletId walletId,
ObjectTypeId[] transactionIds = null)
{
var wos = await GetWalletObjects(
new GetWalletObjectsQuery(walletId, transactionIds));
return await GetWalletTransactionsInfoCore(walletId, wos);
}
private async Task<Dictionary<string, WalletTransactionInfo>> GetWalletTransactionsInfoCore(WalletId walletId,
Dictionary<WalletObjectId, WalletObjectData> wos)
{
var wos = await GetWalletObjects((GetWalletObjectsQuery)(new(walletId, WalletObjectData.Types.Tx, transactionIds)));
var result = new Dictionary<string, WalletTransactionInfo>(wos.Count);
foreach (var obj in wos.Values)
{
@ -332,10 +299,10 @@ namespace BTCPayServer.Services
var l = new WalletObjectLinkData()
{
WalletId = a.WalletId.ToString(),
AType = a.Type,
AId = a.Id,
BType = b.Type,
BId = b.Id,
ParentType = a.Type,
ParentId = a.Id,
ChildType = b.Type,
ChildId = b.Id,
Data = data?.ToString(Formatting.None)
};
ctx.WalletObjectLinks.Add(l);
@ -378,10 +345,10 @@ namespace BTCPayServer.Services
var l = new WalletObjectLinkData()
{
WalletId = a.WalletId.ToString(),
AType = a.Type,
AId = a.Id,
BType = b.Type,
BId = b.Id,
ParentType = a.Type,
ParentId = a.Id,
ChildType = b.Type,
ChildId = b.Id,
Data = data?.ToString(Formatting.None)
};
var e = ctx.WalletObjectLinks.Add(l);
@ -454,20 +421,13 @@ namespace BTCPayServer.Services
}
public Task AddWalletTransactionAttachment(WalletId walletId, uint256 txId, Attachment attachment)
{
return AddWalletTransactionAttachment(walletId, txId.ToString(), new []{attachment}, WalletObjectData.Types.Tx);
return AddWalletTransactionAttachment(walletId, txId, new[] { attachment });
}
public Task AddWalletTransactionAttachment(WalletId walletId, uint256 txId,
IEnumerable<Attachment> attachments)
{
return AddWalletTransactionAttachment(walletId, txId.ToString(), attachments, WalletObjectData.Types.Tx);
}
public async Task AddWalletTransactionAttachment(WalletId walletId, string txId, IEnumerable<Attachment> attachments, string type)
public async Task AddWalletTransactionAttachment(WalletId walletId, uint256 txId, IEnumerable<Attachment> attachments)
{
ArgumentNullException.ThrowIfNull(walletId);
ArgumentNullException.ThrowIfNull(txId);
var txObjId = new WalletObjectId(walletId, type, txId.ToString());
var txObjId = new WalletObjectId(walletId, WalletObjectData.Types.Tx, txId.ToString());
await EnsureWalletObject(txObjId);
foreach (var attachment in attachments)
{
@ -493,10 +453,10 @@ namespace BTCPayServer.Services
ctx.WalletObjectLinks.Remove(new WalletObjectLinkData()
{
WalletId = a.WalletId.ToString(),
AId = a.Id,
AType = a.Type,
BId = b.Id,
BType = b.Type
ParentId = a.Id,
ParentType = a.Type,
ChildId = b.Id,
ChildType = b.Type
});
try
{

View File

@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Services.Stores;
using Microsoft.Extensions.Hosting;
@ -23,21 +22,19 @@ namespace BTCPayServer.Services.Wallets
private readonly BTCPayWalletProvider _btcPayWalletProvider;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly StoreRepository _storeRepository;
private readonly WalletRepository _walletRepository;
private readonly ConcurrentDictionary<WalletId, KeyPathInformation> _walletReceiveState =
new ConcurrentDictionary<WalletId, KeyPathInformation>();
public WalletReceiveService(EventAggregator eventAggregator, ExplorerClientProvider explorerClientProvider,
BTCPayWalletProvider btcPayWalletProvider, BTCPayNetworkProvider btcPayNetworkProvider,
StoreRepository storeRepository, WalletRepository walletRepository )
StoreRepository storeRepository)
{
_eventAggregator = eventAggregator;
_explorerClientProvider = explorerClientProvider;
_btcPayWalletProvider = btcPayWalletProvider;
_btcPayNetworkProvider = btcPayNetworkProvider;
_storeRepository = storeRepository;
_walletRepository = walletRepository;
}
public async Task<string> UnReserveAddress(WalletId walletId)
@ -76,8 +73,6 @@ namespace BTCPayServer.Services.Wallets
}
var reserve = (await wallet.ReserveAddressAsync(derivationScheme.AccountDerivation));
await _walletRepository.AddWalletTransactionAttachment(walletId, reserve.ScriptPubKey.ToString(), new []{new Attachment("receive")},
WalletObjectData.Types.Script);
Set(walletId, reserve);
return reserve;
}

View File

@ -40,11 +40,8 @@ namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage
// Set the relative URL to the directory name if the root path is default, otherwise add root path before the directory name
var relativeUrl = baseUri.AbsolutePath == "/" ? LocalStorageDirectoryName : $"{baseUri.AbsolutePath}/{LocalStorageDirectoryName}";
var url = new Uri(baseUri, relativeUrl);
var r = baseResult.Replace(new DirectoryInfo(_datadirs.Value.StorageDir).FullName, url.AbsoluteUri,
return baseResult.Replace(new DirectoryInfo(_datadirs.Value.StorageDir).FullName, url.AbsoluteUri,
StringComparison.InvariantCultureIgnoreCase);
if (Path.DirectorySeparatorChar == '\\')
r = r.Replace(Path.DirectorySeparatorChar, '/');
return r;
}
public override async Task<string> GetTemporaryFileUrl(Uri baseUri, StoredFile storedFile,

View File

@ -16,7 +16,7 @@
@if (!string.IsNullOrEmpty(Model.CustomCSSLink))
{
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true"/>
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
}
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
{

View File

@ -92,7 +92,7 @@
<div class="form-group">
<label asp-for="TargetCurrency" class="form-label"></label>
<input asp-for="TargetCurrency" class="form-control w-auto" currency-selection />
<div class="form-text">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</div>
<small class="d-inline-block form-text text-muted">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</small>
<span asp-validation-for="TargetCurrency" class="text-danger"></span>
</div>
</div>

View File

@ -47,7 +47,9 @@
<div class="form-group">
<label asp-for="Settings.Login" class="form-label"></label>
<input asp-for="Settings.Login" class="form-control"/>
<div class="form-text">For many email providers (like Gmail) your login is your email address.</div>
<small class="form-text text-muted">
For many email providers (like Gmail) your login is your email address.
</small>
<span asp-validation-for="Settings.Login" class="text-danger"></span>
</div>
<div class="form-group">

View File

@ -1,19 +1,27 @@
@using BTCPayServer.Abstractions.Form
@using BTCPayServer.Abstractions.Form
@using BTCPayServer.Forms
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using Newtonsoft.Json.Linq
@inject FormComponentProviders FormComponentProviders
@inject FormComponentProvider FormComponentProvider
@model BTCPayServer.Abstractions.Form.Field
@if (!Model.Hidden)
@{
if (Model is not Fieldset fieldset)
{
fieldset = JObject.FromObject(Model).ToObject<Fieldset>();
}
}
@if (!fieldset.Hidden)
{
<fieldset>
<legend class="h3 mt-4 mb-3">@Model.Label</legend>
@foreach (var field in Model.Fields)
<legend class="h3 mt-4 mb-3">@fieldset.Label</legend>
@foreach (var field in fieldset.Fields)
{
if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial))
{
<partial name="@partial.View" for="@field"></partial>
}
var partial = FormComponentProvider.CanHandle(field);
if (string.IsNullOrEmpty(partial))
{
continue;
}
<partial name="@partial" for="@field"></partial>
}
</fieldset>
}

View File

@ -1,33 +1,32 @@
@using BTCPayServer.Abstractions.Form
@using BTCPayServer.Abstractions.Form
@using Newtonsoft.Json.Linq
@model BTCPayServer.Abstractions.Form.Field
@{
var isInvalid = this.ViewContext.ModelState[Model.Name]?.ValidationState is Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid;
var error = isInvalid ? this.ViewContext.ModelState[Model.Name].Errors[0].ErrorMessage : null;
if (Model is not HtmlInputField field)
{
field = JObject.FromObject(Model).ToObject<HtmlInputField>();
}
}
<div class="form-group">
@if (Model.Required)
@if (field.Required)
{
<label class="form-label" for="@Model.Name" data-required>
@Model.Label
<label class="form-label" for="@field.Name" data-required>
@field.Label
</label>
}
else
{
<label class="form-label" for="@Model.Name">
@Model.Label
<label class="form-label" for="@field.Name">
@field.Label
</label>
}
<input class="form-control @(Model.IsValid() ? "" : "is-invalid")" id="@Model.Name" type="@Model.Type" required="@Model.Required" name="@Model.Name" value="@Model.Value" aria-describedby="@("HelpText" + Model.Name)"/>
@if(isInvalid)
{
<span class="text-danger">@error</span>
}
@if (!string.IsNullOrEmpty(Model.HelpText))
<input class="form-control @(field.IsValid() ? "" : "is-invalid")" id="@field.Name" type="@field.Type" required="@field.Required" name="@field.Name" value="@field.Value" aria-describedby="@("HelpText" + field.Name)"/>
@if (!string.IsNullOrEmpty(field.HelpText))
{
<div id="@("HelpText" + Model.Name)" class="form-text">@Model.HelpText</div>
<small id="@("HelpText" + field.Name)" class="form-text text-muted">
@field.HelpText
</small>
}

View File

@ -305,7 +305,9 @@
v-model="srvModel.serverIpn" v-on:change="inputChanges"
v-validate="'url'" :class="{'is-invalid': errors.has('serverIpn') }">
<small class="text-danger">{{ errors.first('serverIpn') }}</small>
<div class="form-text">The URL to post purchase data.</div>
<p class="form-text text-muted">
The URL to post purchase data.
</p>
</div>
<div class="form-group" v-if="!srvModel.appIdEndpoint">
<label class="form-label" for="email-notifications">Email Notifications</label>
@ -314,7 +316,9 @@
v-model="srvModel.notifyEmail" v-on:change="inputChanges"
v-validate="'email'" :class="{'is-invalid': errors.has('notifyEmail') }">
<small class="text-danger">{{ errors.first('notifyEmail') }}</small>
<div class="form-text">Receive email notification updates.</div>
<p class="form-text text-muted">
Receive email notification updates.
</p>
</div>
<div class="form-group">
<label class="form-label" for="browser-redirect">Browser Redirect</label>
@ -322,7 +326,9 @@
v-model="srvModel.browserRedirect" v-on:change="inputChanges"
v-validate="'url'" :class="{'is-invalid': errors.has('browserRedirect') }">
<small class="text-danger">{{ errors.first('browserRedirect') }}</small>
<div class="form-text">Where to redirect the customer after payment is complete</div>
<p class="form-text text-muted">
Where to redirect the customer after payment is complete.
</p>
</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
<div class="container p-0 l-pos-wrapper">
<div class="l-pos-header bg-primary py-3 px-3">
@if (!string.IsNullOrEmpty(Model.CustomLogoLink)) {
<img src="@Model.CustomLogoLink" height="40" asp-append-version="true">
<img src="@Model.CustomLogoLink" height="40">
} else {
<h1 class="mb-0">@Model.Title</h1>
}

View File

@ -36,6 +36,7 @@
{
<form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id"/>
<input type="hidden" name="requiresRefundEmail" value="@Model.RequiresRefundEmail.ToString()" />
@{PayFormInputContent(item.BuyButtonText ?? Model.CustomButtonText, item.Price.Type, item.Price.Value, item.Price.Value);}
</form>
}

View File

@ -4,7 +4,7 @@
<div class="l-pos-header bg-primary py-3 px-3">
@if (!string.IsNullOrEmpty(Model.CustomLogoLink))
{
<img src="@Model.CustomLogoLink" height="40" asp-append-version="true" />
<img src="@Model.CustomLogoLink" height="40"/>
}
else
{

View File

@ -25,12 +25,7 @@
var jObject = JObject.Parse(await reader.ReadToEndAsync());
jObject["short_name"] = title;
jObject["name"] = $"BTCPay Server: {title}";
foreach (var jToken in jObject["icons"]!)
{
var icon = (JObject)jToken;
icon["src"] = $"{Context.Request.GetAbsoluteRoot()}/{icon["src"]}";
}
return $"data:application/manifest+json,{Safe.Json(jObject)}";
return $"data:application/manifest+json, {jObject.ToString(Formatting.None)}";
}
}
@ -50,10 +45,11 @@
<link href="~/main/fonts/OpenSans.css" asp-append-version="true" rel="stylesheet" />
<link href="~/main/layout.css" asp-append-version="true" rel="stylesheet" />
<link href="~/main/site.css" asp-append-version="true" rel="stylesheet" />
<link href="@Context.Request.GetRelativePathOrAbsolute(Theme.CssUri)" rel="stylesheet" asp-append-version="true"/>
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true" />
<link href="@Model.CustomCSSLink" rel="stylesheet" />
}
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" asp-append-version="true" />

View File

@ -40,7 +40,7 @@
<div class="form-group">
<label asp-for="Currency" class="form-label"></label>
<input asp-for="Currency" class="form-control w-auto" currency-selection />
<div class="form-text">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</div>
<small class="d-inline-block form-text text-muted">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</small>
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
</div>
@ -75,7 +75,7 @@
<label asp-for="DefaultView" class="form-label" data-required></label>
<select asp-for="DefaultView" asp-items="@Html.GetEnumSelectList<PosViewType>()" class="form-select" required></select>
<span asp-validation-for="DefaultView" class="text-danger"></span>
<div class="form-text">Choose the point of sale style for your customers.</div>
<p class="form-text text-muted">Choose the point of sale style for your customers.</p>
</div>
<div class="form-group" id="button-price-text">
<label asp-for="ButtonText" class="form-label" data-required></label>

View File

@ -93,12 +93,16 @@
<div class="form-group">
<label class="form-label">Inventory</label>
<input type="number" inputmode="numeric" min="0" step="1" class="form-control mb-2" v-model="editingItem.inventory" ref="txtInventory" />
<div class="form-text">Leave blank to not use this feature.</div>
<p class="form-text text-muted">
Leave blank to not use this feature.
</p>
</div>
<div class="form-group">
<label class="form-label">ID</label>
<input type="text" required pattern="[^\*#]+" class="form-control mb-2" v-model="editingItem.id" ref="txtId" />
<div class="form-text">Leave blank to generate ID from title.</div>
<p class="form-text text-muted">
Leave blank to generate ID from title.
</p>
</div>
<div class="form-group">
<label class="form-label">Buy Button Text</label>

View File

@ -1,12 +1,14 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Forms
@model BTCPayServer.Abstractions.Form.Form
@inject FormComponentProviders FormComponentProviders
@inject FormComponentProvider FormComponentProvider
@foreach (var field in Model.Fields)
{
if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial))
var partial = FormComponentProvider.CanHandle(field);
if (string.IsNullOrEmpty(partial))
{
<partial name="@partial.View" for="@field"></partial>
continue;
}
<partial name="@partial" for="@field"></partial>
}

View File

@ -364,7 +364,7 @@
</tbody>
</table>
-->
<div class="form-text">Final results may vary due to trading fees and slippage.</div>
<small class="form-text text-muted">Final results may vary due to trading fees and slippage.</small>
</div>
<div v-if="trade.results !== null">
<p class="alert alert-success">Successfully traded {{ trade.results.fromAsset}} into {{ trade.results.toAsset}}.</p>

View File

@ -33,7 +33,7 @@
<input type="hidden" asp-for="RedirectUrl" value="@Model.RedirectUrl"/>
}
<partial name="_Form" model="@Model.Form"/>
<input type="submit" class="btn btn-primary" name="command" value="Submit"/>
<input type="submit" class="btn btn-primary" value="Submit"/>
</form>
</div>
</div>

View File

@ -45,7 +45,7 @@
@if (!string.IsNullOrEmpty(Model.CustomCSSLink))
{
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true"/>
<link href="@Model.CustomCSSLink" rel="stylesheet" />
}
@if (Model.IsModal)

View File

@ -134,9 +134,9 @@
<payment-details :srv-model="srvModel" :is-active="isActive" class="mb-5"></payment-details>
</div>
<div class="buttons">
<a v-if="srvModel.receiptLink" class="btn btn-primary rounded-pill w-100" :href="srvModel.receiptLink" :target="isModal ? '_top' : null" v-t="'view_receipt'" id="ReceiptLink"></a>
<a v-if="storeLink" class="btn btn-secondary rounded-pill w-100" :href="storeLink" :target="isModal ? '_top' : null" v-t="{ path: 'return_to_store', args: { storeName: srvModel.storeName }}" id="StoreLink"></a>
<button v-else-if="isModal" class="btn btn-secondary rounded-pill w-100" v-on:click="close" v-t="'Close'"></button>
<a v-if="srvModel.receiptLink" class="btn btn-primary" :href="srvModel.receiptLink" :target="isModal ? '_top' : null" v-t="'view_receipt'" id="ReceiptLink"></a>
<a v-if="storeLink" class="btn btn-secondary" :href="storeLink" :target="isModal ? '_top' : null" v-t="{ path: 'return_to_store', args: { storeName: srvModel.storeName }}" id="StoreLink"></a>
<button v-else-if="isModal" class="btn btn-secondary" v-on:click="close" v-t="'Close'"></button>
</div>
</div>
<div id="expired" v-if="isUnpayable">
@ -165,8 +165,8 @@
<p class="text-center mt-3" v-html="replaceNewlines($t('invoice_expired_body', { storeName: srvModel.storeName, minutes: @Model.MaxTimeMinutes }))"></p>
</div>
<div class="buttons">
<a v-if="storeLink" class="btn btn-primary rounded-pill w-100" :href="storeLink" :target="isModal ? '_top' : null" v-t="{ path: 'return_to_store', args: { storeName: srvModel.storeName }}" id="StoreLink"></a>
<button v-else-if="isModal" class="btn btn-primary rounded-pill w-100" v-on:click="close" v-t="'Close'"></button>
<a v-if="storeLink" class="btn btn-primary" :href="storeLink" :target="isModal ? '_top' : null" v-t="{ path: 'return_to_store', args: { storeName: srvModel.storeName }}" id="StoreLink"></a>
<button v-else-if="isModal" class="btn btn-primary" v-on:click="close" v-t="'Close'"></button>
</div>
</div>
</section>

View File

@ -135,7 +135,9 @@
<label asp-for="NotificationEmail" class="form-label"></label>
<input asp-for="NotificationEmail" class="form-control" />
<span asp-validation-for="NotificationEmail" class="text-danger"></span>
<div id="InvoiceEmailHelpBlock" class="form-text">Receive updates for this invoice.</div>
<p id="InvoiceEmailHelpBlock" class="form-text text-muted">
Receive updates for this invoice.
</p>
</div>
</div>
</div>

View File

@ -186,15 +186,15 @@
<div class="dropdown-menu" aria-labelledby="markStatusDropdownMenuButton">
@if (Model.CanMarkInvalid)
{
<button type="button" class="dropdown-item lh-base changeInvoiceState" data-id="@Model.Id" data-status="invalid" data-change-invoice-status-button>
Mark as invalid
</button>
<a class="dropdown-item changeInvoiceState" href="#" data-id="@Model.Id" data-status="invalid" data-change-invoice-status-button>
Mark as invalid <span class="fa fa-times"></span>
</a>
}
@if (Model.CanMarkSettled)
{
<button type="button" class="dropdown-item lh-base changeInvoiceState" href="#" data-id="@Model.Id" data-status="settled" data-change-invoice-status-button>
Mark as settled
</button>
<a class="dropdown-item changeInvoiceState" href="#" data-id="@Model.Id" data-status="settled" data-change-invoice-status-button>
Mark as settled <span class="fa fa-check-circle"></span>
</a>
}
</div>
</div>
@ -416,7 +416,7 @@
<h3 class="mb-3 mt-4">Webhooks</h3>
<div class="table-responsive-xl">
<table class="table table-hover table-responsive-md mb-5">
<thead>
<thead class="thead-inverse">
<tr>
<th>Status</th>
<th>ID</th>
@ -491,7 +491,7 @@
<h3 class="mb-3 mt-4">Refunds</h3>
<div class="table-responsive-xl">
<table class="table table-hover table-responsive-md mb-5">
<thead>
<thead class="thead-inverse">
<tr>
<th>Pull Payment</th>
<th>Amount</th>
@ -526,9 +526,9 @@
</table>
</div>
}
<h3 class="mb-0 mt-5">Events</h3>
<table class="table table-hover mt-3 mb-4">
<thead>
<h3 class="mb-0">Events</h3>
<table class="table table-hover">
<thead class="thead-inverse">
<tr>
<th>Date</th>
<th>Message</th>

View File

@ -41,7 +41,7 @@
pavpill.replaceWith(statusHtml);
})
.fail(function (data) {
pavpill.html(originalHtml.replace("dropdown-menu show", "dropdown-menu"));
pavpill.html(originalHtml.replace("dropdown-menu pull-right show", "dropdown-menu pull-right"));
alert("Invoice state update failed");
});
})
@ -325,16 +325,16 @@
<span class="dropdown-toggle changeInvoiceStateToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@invoice.Status.ToString()
</span>
<div class="dropdown-menu">
<div class="dropdown-menu pull-right">
@if (invoice.CanMarkInvalid)
{
<button type="button" class="dropdown-item lh-base changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="invalid">
<button class="dropdown-item cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="invalid">
Mark as invalid
</button>
}
@if (invoice.CanMarkSettled)
{
<button type="button" class="dropdown-item lh-base changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="settled">
<button class="dropdown-item cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="settled">
Mark as settled
</button>
}

View File

@ -50,21 +50,21 @@
<div class="form-check">
<input id="RateThenOption" asp-for="SelectedRefundOption" type="radio" value="RateThen" class="form-check-input"/>
<label for="RateThenOption" class="form-check-label">@Model.RateThenText</label>
<div class="form-text">The crypto currency price, at the rate the invoice got paid.</div>
<div class="form-text text-muted">The crypto currency price, at the rate the invoice got paid.</div>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input id="CurrentRateOption" asp-for="SelectedRefundOption" type="radio" value="CurrentRate" class="form-check-input"/>
<label for="CurrentRateOption" class="form-check-label">@Model.CurrentRateText</label>
<div class="form-text">The crypto currency price, at the current rate.</div>
<div class="form-text text-muted">The crypto currency price, at the current rate.</div>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input id="FiatOption" asp-for="SelectedRefundOption" type="radio" value="Fiat" class="form-check-input"/>
<label for="FiatOption" class="form-check-label">@Model.FiatText</label>
<div class="form-text">The invoice currency, at the rate when the refund will be sent.</div>
<div class="form-text text-muted">The invoice currency, at the rate when the refund will be sent.</div>
</div>
</div>
@ -72,7 +72,7 @@
<div class="form-check">
<input id="CustomOption" asp-for="SelectedRefundOption" type="radio" value="Custom" class="form-check-input"/>
<label for="CustomOption" class="form-check-label">Custom amount</label>
<div class="form-text">The specified amount with the specified currency, at the rate when the refund will be sent.</div>
<div class="form-text text-muted">The specified amount with the specified currency, at the rate when the refund will be sent.</div>
<div class="form-group pt-2">
<label asp-for="CustomAmount" class="form-label"></label>
<div class="input-group">

View File

@ -128,7 +128,7 @@
<div>
<label for="@Model.PermissionValues[i].Permission" class="form-check-label">@Model.PermissionValues[i].Title</label>
</div>
<div class="form-text">@Model.PermissionValues[i].Description</div>
<div class="form-text text-muted">@Model.PermissionValues[i].Description</div>
<span asp-validation-for="PermissionValues[i].Value" class="text-danger"></span>
@if (Model.PermissionValues[i].Forbidden)
{

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