Compare commits
342 Commits
form_build
...
plugins
Author | SHA1 | Date | |
---|---|---|---|
3c7751ae80 | |||
6d9bbb50d6 | |||
948bae9f95 | |||
a1c10b4ea3 | |||
f36df81d9a | |||
2fd9eb6c68 | |||
508002503e | |||
8894d14130 | |||
4039e74a82 | |||
0af3faf6ff | |||
0520b69c18 | |||
e11a775bed | |||
b4ed4623e1 | |||
9ee9653c7d | |||
e55a16d917 | |||
3458a0b22c | |||
ddcfa735e0 | |||
3370240541 | |||
c0cec4716e | |||
08b239e87a | |||
84132e794a | |||
425d70f261 | |||
420954ed00 | |||
45edd330f5 | |||
6a0e2bcad3 | |||
d67d3e0167 | |||
031bb7b224 | |||
cd4f3d9a66 | |||
5c6db35c9b | |||
887bea4328 | |||
def5095d77 | |||
ab66662ff6 | |||
2d84433a62 | |||
b8e61787d4 | |||
669825a35d | |||
31b25ca169 | |||
a6ee92fbd5 | |||
5ff1a59a99 | |||
4f65eb4d65 | |||
39328c7368 | |||
2f5f3e1b51 | |||
022285806b | |||
2d6827dd19 | |||
5e00bc43d5 | |||
db8a2930a4 | |||
7325610ac5 | |||
36b064b50f | |||
7ecf6504d0 | |||
7c971df109 | |||
c7f6784364 | |||
b816c4462e | |||
ca750f4152 | |||
6be968f7bf | |||
89cd1079fe | |||
fa5470c587 | |||
820421fcbc | |||
a4ab2dcd84 | |||
d14f4f8173 | |||
386c18b897 | |||
5d98fe7b34 | |||
2008d15dda | |||
d206a88e3a | |||
bbce88f269 | |||
edbfafcaf2 | |||
b64ddab08b | |||
c34b4d64e5 | |||
90308ce6b9 | |||
a3b702c360 | |||
4f9820ae1b | |||
6884db431f | |||
349dbdc85d | |||
dc795311fa | |||
7ff2f58f34 | |||
c736babd40 | |||
b5174dcbcd | |||
ffe40daaa6 | |||
43649026c1 | |||
84212de830 | |||
d958ef2ec7 | |||
b60dcb6927 | |||
9af32e7089 | |||
7b10067ce5 | |||
7746e5beef | |||
38fd1d338b | |||
b0e4186733 | |||
c6c0a7b6ac | |||
f2e35529e9 | |||
1deddbcf14 | |||
c14f26340e | |||
ab5497eb3d | |||
1b1fdd4360 | |||
fbc4dde974 | |||
4d1f8c1d16 | |||
524ad8393d | |||
0def52416d | |||
375405ba51 | |||
9ce1ba0019 | |||
ff6043cf67 | |||
e6e3efb709 | |||
ec029fa5cd | |||
5f9926896a | |||
e68cb1805f | |||
f554478254 | |||
45a8852799 | |||
92dae8dff6 | |||
beb58cb90b | |||
f379593a2f | |||
15ddf9d619 | |||
82b479cc7c | |||
ba2db52859 | |||
8f5af29f23 | |||
7b5049099c | |||
5d37f31d72 | |||
05aac2801c | |||
160a19f6c7 | |||
afedb48eeb | |||
25716c8ab7 | |||
61c9466374 | |||
4b3d7fd004 | |||
e8657da952 | |||
b4e3c8f0b5 | |||
405def199f | |||
ea9fe8386d | |||
5dd8b3701f | |||
e6b4b83bc0 | |||
253cfda4b4 | |||
f03063e390 | |||
5e295bab89 | |||
92cf26dc3b | |||
a3ed557244 | |||
475f16b280 | |||
b3b54ccaf7 | |||
07cba914bb | |||
7e569c044f | |||
7bb32e3fdf | |||
8aea701f6c | |||
177628d05b | |||
fe644bb979 | |||
70168f73ce | |||
5a27155984 | |||
b38c05f33f | |||
2ff1c80661 | |||
6f7bce216c | |||
f7981b2b12 | |||
2b81ea5040 | |||
733dc7561a | |||
c323ef51fa | |||
4d97de8208 | |||
dd99fec838 | |||
539352f598 | |||
a63288fce8 | |||
0f57c09aff | |||
15068c1398 | |||
6ef4822ad2 | |||
f2f3e472b0 | |||
57e42df769 | |||
0918c79305 | |||
ef0f61dafa | |||
4e1c841615 | |||
713e0bf5e5 | |||
aa6f41bace | |||
d44c06320e | |||
0d2603dd5a | |||
c87a4511cd | |||
3b61be1b82 | |||
28d05edd96 | |||
21d159d5f9 | |||
d2d02e18e7 | |||
47208db8db | |||
ea6ad0995f | |||
0c1b474c35 | |||
0e90f1219c | |||
abb21c68cb | |||
9c1404c261 | |||
ceb722c71f | |||
d863dfa0c8 | |||
0cc849be91 | |||
110524391b | |||
d452c1e578 | |||
86fead2809 | |||
6c80ca1d18 | |||
aa742b3079 | |||
01eb93bc43 | |||
e6d29afed6 | |||
3f88ea1e15 | |||
409b6df8a2 | |||
8304afd36c | |||
56400dcc27 | |||
d59ab2ef01 | |||
458f8979fb | |||
ca7b306dea | |||
f509b937fd | |||
19c75f51bd | |||
ca821a643e | |||
d8d3172b17 | |||
14ce5dd41f | |||
505b1d8310 | |||
c60a95a6fe | |||
63119c63f1 | |||
c5f4db4df6 | |||
77932f6e20 | |||
2d67e0e75c | |||
db8b7a4621 | |||
a6ac322246 | |||
8b5ea9fd56 | |||
d9aa8f7dbc | |||
5341b4eb08 | |||
935577de71 | |||
3d15ff31f5 | |||
e68942056d | |||
eff872148c | |||
3692602b15 | |||
3de38298b6 | |||
eb7d8694f5 | |||
8e53b3c9f0 | |||
6a714e6d9c | |||
f227b9aec5 | |||
a741ad446c | |||
e4d45798f1 | |||
e5b4f2a399 | |||
55fb4ec233 | |||
c5b4b1b9e6 | |||
ad6effa3bb | |||
c5e48eb975 | |||
675bb63d14 | |||
5c64a4ac40 | |||
0adb2cf233 | |||
316ed2a9bb | |||
cea2a3f513 | |||
5d01e07e2d | |||
ee0547448e | |||
08580eb244 | |||
e127478c2e | |||
9b7bd57cb1 | |||
f771b6dd70 | |||
f4ed031f99 | |||
b7d8077467 | |||
8197375845 | |||
74a3c6739c | |||
392023219b | |||
97420dfd3e | |||
30d1bcfa4d | |||
36b5a13d1c | |||
0dadac914e | |||
3a76bebcbc | |||
53f46d4b8d | |||
5911b998b5 | |||
45885c5137 | |||
1b5a7ed1e1 | |||
9be0810fb7 | |||
a3efae4cc5 | |||
2141eafa37 | |||
b403f6f1e8 | |||
0055746be6 | |||
2885335780 | |||
d1524a5e5d | |||
775101a31f | |||
48a0ed7b98 | |||
7843b6173c | |||
4446d506e9 | |||
7b824d5ec0 | |||
967c91f711 | |||
f7271f6ffb | |||
0b83d2b859 | |||
fc4a7a26fe | |||
f6e3ca32fb | |||
0a3b4f5d61 | |||
60d8dd81a4 | |||
0e2a7b5efd | |||
a664937526 | |||
00354de289 | |||
c70f58fd70 | |||
21c08acfd5 | |||
f38432e15e | |||
58309dd7aa | |||
59017e77eb | |||
be323ba147 | |||
f3350bcdbf | |||
79df9a027a | |||
3fe981d6f5 | |||
80265b56b8 | |||
90ce5acf99 | |||
eef2780e70 | |||
cedb04cc42 | |||
1fd8f48bda | |||
4f63ca4af4 | |||
6d72d7f6de | |||
78e4cb868d | |||
560117fe59 | |||
ee0e199add | |||
fb48b9fa52 | |||
c888a845ab | |||
c3e43cb5b3 | |||
9a855deca4 | |||
41bdbd784d | |||
e2e87652e1 | |||
c29b1be070 | |||
82fc59cc76 | |||
1f1f2ca819 | |||
d43119e4ac | |||
2b4c3e68b1 | |||
a35e8fa69a | |||
e855441455 | |||
62f426fea8 | |||
094db3dab9 | |||
14770d7a6b | |||
ac0722246b | |||
14785278ec | |||
359247542e | |||
e028f5d0c1 | |||
66c90ae5f7 | |||
72c876b62b | |||
89204f2256 | |||
0580e5bf9b | |||
6bfcb02a8c | |||
b9d73415a4 | |||
beaac40222 | |||
17bd4a3d9c | |||
ce61a07111 | |||
cadd197dd9 | |||
f97275ae16 | |||
6845c511c6 | |||
34c482a991 | |||
250762c758 | |||
06df50c5d0 | |||
240cb72d24 | |||
22678d2b50 | |||
1f593d0710 | |||
ec41e806dd | |||
bac5ad4bbc | |||
28ae60faf3 | |||
c9544b22d1 | |||
01d7ff2525 | |||
4f67a443c5 | |||
1f333058c9 | |||
6ef90503df | |||
cece43a921 | |||
8aae72f63e | |||
d3df78e71b | |||
4f6bd1a523 | |||
4b2d70e3fa | |||
ab729b0f7c |
.github/ISSUE_TEMPLATE
.gitmodulesBTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Data
BTCPayServer.Rating
BTCPayServer.Tests
AltcoinTests
Checkoutv2Tests.csFastTests.csGreenfieldAPITests.csLNbankTests.csdocker-compose.altcoins.ymldocker-compose.ymldocker-customer-holdinvoice.shdocker-lightning-channel-setup.shBTCPayServer
BTCPayServer.csproj
Components
Configuration
Controllers
GreenField
GreenfieldInvoiceController.csGreenfieldPullPaymentController.csGreenfieldStoreOnChainWalletsController.cs
UIInvoiceController.UI.csUIInvoiceController.csUILNURLController.csUIPaymentRequestController.csUIStorePullPaymentsController.PullPayments.csUIStoresController.Onchain.csUIWalletsController.csData
DerivationSchemeSettings.csForms
FormComponentProvider.csFormComponentProviders.csFormDataExtensions.csFormDataService.csHtmlFieldsetFormProvider.csHtmlInputFormProvider.csIFormComponentProvider.cs
Models
UIFormsController.csHostedServices
DbMigrationsHostedService.csPullPaymentHostedService.csTransactionLabelMarkerHostedService.csWebhookSender.cs
Hosting
Models/PaymentRequestViewModels
PaymentRequest
PayoutProcessors
Plugins/PointOfSale/Controllers
Properties
Services
Storage/Services/Providers/FileSystemStorage
Views
Shared
UICustodianAccounts
UIForms
UIInvoice
Checkout.cshtmlCheckoutV2.cshtmlCreateInvoice.cshtmlInvoice.cshtmlListInvoices.cshtml_RefundModal.cshtml
UIManage
UIPaymentRequest
UIPullPayment
UIServer
UIStores
CheckoutAppearance.cshtmlCreateToken.cshtmlDashboard.cshtmlGeneralSettings.cshtmlLightningSettings.cshtmlModifyWebhook.cshtmlRates.cshtmlStoreEmails.cshtmlWalletSettings.cshtml
UIWallets
wwwroot
Build
Changelog.mdPlugins
BTCPayServer.Plugins.Custodians.FakeCustodian
BTCPayServer.Plugins.LNbankBTCPayServer.Plugins.PodServer
58
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
58
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
---
|
||||
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
68
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,68 +0,0 @@
|
||||
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
Normal file
6
.gitmodules
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
[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
|
18
BTCPayServer.Abstractions/CamelCaseSerializerSettings.cs
Normal file
18
BTCPayServer.Abstractions/CamelCaseSerializerSettings.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Abstractions
|
||||
{
|
||||
public class CamelCaseSerializerSettings
|
||||
{
|
||||
static CamelCaseSerializerSettings()
|
||||
{
|
||||
Settings = new JsonSerializerSettings()
|
||||
{
|
||||
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
|
||||
};
|
||||
Serializer = JsonSerializer.Create(Settings);
|
||||
}
|
||||
public static readonly JsonSerializerSettings Settings;
|
||||
public static readonly JsonSerializer Serializer;
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
@ -9,16 +10,50 @@ 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>();
|
||||
|
||||
@ -26,9 +61,4 @@ 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();
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
namespace BTCPayServer.Abstractions.Form;
|
||||
|
||||
public class Fieldset : Field
|
||||
{
|
||||
public bool Hidden { get; set; }
|
||||
public string Label { get; set; }
|
||||
|
||||
public Fieldset()
|
||||
{
|
||||
Type = "fieldset";
|
||||
}
|
||||
}
|
@ -2,12 +2,24 @@ 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();
|
||||
|
||||
@ -17,7 +29,7 @@ public class Form
|
||||
// Are all the fields valid in the form?
|
||||
public bool IsValid()
|
||||
{
|
||||
return Fields.All(field => field.IsValid());
|
||||
return Fields.Select(f => f.IsValid()).All(o => o);
|
||||
}
|
||||
|
||||
public Field GetFieldByName(string name)
|
||||
@ -52,6 +64,7 @@ public class Form
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<string> GetAllNames()
|
||||
{
|
||||
return GetAllNames(Fields);
|
||||
|
@ -1,27 +0,0 @@
|
||||
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.
|
||||
}
|
@ -1,5 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
@ -8,7 +15,7 @@ namespace BTCPayServer.Abstractions.TagHelpers;
|
||||
|
||||
// Make sure that <svg><use href=/ are correctly working if rootpath is present
|
||||
[HtmlTargetElement("use", Attributes = "href")]
|
||||
public class SVGUse : UrlResolutionTagHelper
|
||||
public class SVGUse : UrlResolutionTagHelper2
|
||||
{
|
||||
private readonly IFileVersionProvider _fileVersionProvider;
|
||||
|
||||
@ -21,5 +28,6 @@ public class SVGUse : UrlResolutionTagHelper
|
||||
var attr = output.Attributes["href"].Value.ToString();
|
||||
attr = _fileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, attr);
|
||||
output.Attributes.SetAttribute("href", attr);
|
||||
}
|
||||
base.Process(context, output);
|
||||
}
|
||||
}
|
||||
|
314
BTCPayServer.Abstractions/TagHelpers/UrlResolutionTagHelper2.cs
Normal file
314
BTCPayServer.Abstractions/TagHelpers/UrlResolutionTagHelper2.cs
Normal file
@ -0,0 +1,314 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
|
||||
|
||||
namespace BTCPayServer.Abstractions.TagHelpers
|
||||
{
|
||||
// A copy of https://github.com/dotnet/aspnetcore/blob/39f0e0b8f40b4754418f81aef0de58a9204a1fe5/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs
|
||||
// slightly modified to also work on use tag.
|
||||
public class UrlResolutionTagHelper2 : TagHelper
|
||||
{
|
||||
// Valid whitespace characters defined by the HTML5 spec.
|
||||
private static readonly char[] ValidAttributeWhitespaceChars =
|
||||
new[] { '\t', '\n', '\u000C', '\r', ' ' };
|
||||
private static readonly Dictionary<string, string[]> ElementAttributeLookups =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "use", new[] { "href" } },
|
||||
{ "a", new[] { "href" } },
|
||||
{ "applet", new[] { "archive" } },
|
||||
{ "area", new[] { "href" } },
|
||||
{ "audio", new[] { "src" } },
|
||||
{ "base", new[] { "href" } },
|
||||
{ "blockquote", new[] { "cite" } },
|
||||
{ "button", new[] { "formaction" } },
|
||||
{ "del", new[] { "cite" } },
|
||||
{ "embed", new[] { "src" } },
|
||||
{ "form", new[] { "action" } },
|
||||
{ "html", new[] { "manifest" } },
|
||||
{ "iframe", new[] { "src" } },
|
||||
{ "img", new[] { "src", "srcset" } },
|
||||
{ "input", new[] { "src", "formaction" } },
|
||||
{ "ins", new[] { "cite" } },
|
||||
{ "link", new[] { "href" } },
|
||||
{ "menuitem", new[] { "icon" } },
|
||||
{ "object", new[] { "archive", "data" } },
|
||||
{ "q", new[] { "cite" } },
|
||||
{ "script", new[] { "src" } },
|
||||
{ "source", new[] { "src", "srcset" } },
|
||||
{ "track", new[] { "src" } },
|
||||
{ "video", new[] { "poster", "src" } },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="UrlResolutionTagHelper"/>.
|
||||
/// </summary>
|
||||
/// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param>
|
||||
/// <param name="htmlEncoder">The <see cref="HtmlEncoder"/>.</param>
|
||||
public UrlResolutionTagHelper2(IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder)
|
||||
{
|
||||
UrlHelperFactory = urlHelperFactory;
|
||||
HtmlEncoder = htmlEncoder;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int Order => -1000 - 999;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IUrlHelperFactory"/>.
|
||||
/// </summary>
|
||||
protected IUrlHelperFactory UrlHelperFactory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="HtmlEncoder"/>.
|
||||
/// </summary>
|
||||
protected HtmlEncoder HtmlEncoder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ViewContext"/>.
|
||||
/// </summary>
|
||||
[HtmlAttributeNotBound]
|
||||
[ViewContext]
|
||||
public ViewContext ViewContext { get; set; } = default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(output);
|
||||
|
||||
if (output.TagName == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ElementAttributeLookups.TryGetValue(output.TagName, out var attributeNames))
|
||||
{
|
||||
for (var i = 0; i < attributeNames.Length; i++)
|
||||
{
|
||||
ProcessUrlAttribute(attributeNames[i], output);
|
||||
}
|
||||
}
|
||||
|
||||
// itemid can be present on any HTML element.
|
||||
ProcessUrlAttribute("itemid", output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves and updates URL values starting with '~/' (relative to the application's 'webroot' setting) for
|
||||
/// <paramref name="output"/>'s <see cref="TagHelperOutput.Attributes"/> whose
|
||||
/// <see cref="TagHelperAttribute.Name"/> is <paramref name="attributeName"/>.
|
||||
/// </summary>
|
||||
/// <param name="attributeName">The attribute name used to lookup values to resolve.</param>
|
||||
/// <param name="output">The <see cref="TagHelperOutput"/>.</param>
|
||||
protected void ProcessUrlAttribute(string attributeName, TagHelperOutput output)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(attributeName);
|
||||
ArgumentNullException.ThrowIfNull(output);
|
||||
|
||||
var attributes = output.Attributes;
|
||||
// Read interface .Count once rather than per iteration
|
||||
var attributesCount = attributes.Count;
|
||||
for (var i = 0; i < attributesCount; i++)
|
||||
{
|
||||
var attribute = attributes[i];
|
||||
if (!string.Equals(attribute.Name, attributeName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attribute.Value is string stringValue)
|
||||
{
|
||||
if (TryResolveUrl(stringValue, resolvedUrl: out string? resolvedUrl))
|
||||
{
|
||||
attributes[i] = new TagHelperAttribute(
|
||||
attribute.Name,
|
||||
resolvedUrl,
|
||||
attribute.ValueStyle);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (attribute.Value is IHtmlContent htmlContent)
|
||||
{
|
||||
var htmlString = htmlContent as HtmlString;
|
||||
if (htmlString != null)
|
||||
{
|
||||
// No need for a StringWriter in this case.
|
||||
stringValue = htmlString.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
using var writer = new StringWriter();
|
||||
htmlContent.WriteTo(writer, HtmlEncoder);
|
||||
stringValue = writer.ToString();
|
||||
}
|
||||
|
||||
if (TryResolveUrl(stringValue, resolvedUrl: out IHtmlContent? resolvedUrl))
|
||||
{
|
||||
attributes[i] = new TagHelperAttribute(
|
||||
attribute.Name,
|
||||
resolvedUrl,
|
||||
attribute.ValueStyle);
|
||||
}
|
||||
else if (htmlString == null)
|
||||
{
|
||||
// Not a ~/ URL. Just avoid re-encoding the attribute value later.
|
||||
attributes[i] = new TagHelperAttribute(
|
||||
attribute.Name,
|
||||
new HtmlString(stringValue),
|
||||
attribute.ValueStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to resolve the given <paramref name="url"/> value relative to the application's 'webroot' setting.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to resolve.</param>
|
||||
/// <param name="resolvedUrl">Absolute URL beginning with the application's virtual root. <c>null</c> if
|
||||
/// <paramref name="url"/> could not be resolved.</param>
|
||||
/// <returns><c>true</c> if the <paramref name="url"/> could be resolved; <c>false</c> otherwise.</returns>
|
||||
protected bool TryResolveUrl(string url, out string? resolvedUrl)
|
||||
{
|
||||
resolvedUrl = null;
|
||||
var start = FindRelativeStart(url);
|
||||
if (start == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var trimmedUrl = CreateTrimmedString(url, start);
|
||||
|
||||
var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
|
||||
resolvedUrl = urlHelper.Content(trimmedUrl);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to resolve the given <paramref name="url"/> value relative to the application's 'webroot' setting.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to resolve.</param>
|
||||
/// <param name="resolvedUrl">
|
||||
/// Absolute URL beginning with the application's virtual root. <c>null</c> if <paramref name="url"/> could
|
||||
/// not be resolved.
|
||||
/// </param>
|
||||
/// <returns><c>true</c> if the <paramref name="url"/> could be resolved; <c>false</c> otherwise.</returns>
|
||||
protected bool TryResolveUrl(string url, [NotNullWhen(true)] out IHtmlContent? resolvedUrl)
|
||||
{
|
||||
resolvedUrl = null;
|
||||
var start = FindRelativeStart(url);
|
||||
if (start == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var trimmedUrl = CreateTrimmedString(url, start);
|
||||
|
||||
var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
|
||||
var appRelativeUrl = urlHelper.Content(trimmedUrl);
|
||||
var postTildeSlashUrlValue = trimmedUrl.Substring(2);
|
||||
|
||||
if (!appRelativeUrl.EndsWith(postTildeSlashUrlValue, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
resolvedUrl = new EncodeFirstSegmentContent(
|
||||
appRelativeUrl,
|
||||
appRelativeUrl.Length - postTildeSlashUrlValue.Length,
|
||||
postTildeSlashUrlValue);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int FindRelativeStart(string url)
|
||||
{
|
||||
if (url == null || url.Length < 2)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var maxTestLength = url.Length - 2;
|
||||
|
||||
var start = 0;
|
||||
for (; start < url.Length; start++)
|
||||
{
|
||||
if (start > maxTestLength)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!IsCharWhitespace(url[start]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Before doing more work, ensure that the URL we're looking at is app-relative.
|
||||
if (url[start] != '~' || url[start + 1] != '/')
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
private static string CreateTrimmedString(string input, int start)
|
||||
{
|
||||
var end = input.Length - 1;
|
||||
for (; end >= start; end--)
|
||||
{
|
||||
if (!IsCharWhitespace(input[end]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var len = end - start + 1;
|
||||
|
||||
// Substring returns same string if start == 0 && len == Length
|
||||
return input.Substring(start, len);
|
||||
}
|
||||
|
||||
private static bool IsCharWhitespace(char ch)
|
||||
{
|
||||
return ValidAttributeWhitespaceChars.AsSpan().IndexOf(ch) != -1;
|
||||
}
|
||||
|
||||
private sealed class EncodeFirstSegmentContent : IHtmlContent
|
||||
{
|
||||
private readonly string _firstSegment;
|
||||
private readonly int _firstSegmentLength;
|
||||
private readonly string _secondSegment;
|
||||
|
||||
public EncodeFirstSegmentContent(string firstSegment, int firstSegmentLength, string secondSegment)
|
||||
{
|
||||
_firstSegment = firstSegment;
|
||||
_firstSegmentLength = firstSegmentLength;
|
||||
_secondSegment = secondSegment;
|
||||
}
|
||||
|
||||
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
||||
{
|
||||
encoder.Encode(writer, _firstSegment, 0, _firstSegmentLength);
|
||||
writer.Write(_secondSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
<RepositoryType>git</RepositoryType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.0</Version>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.1</Version>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
@ -28,8 +28,8 @@
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.15" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.14" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.16" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.20" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -128,5 +128,18 @@ namespace BTCPayServer.Client
|
||||
method: HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<PullPaymentData> RefundInvoice(
|
||||
string storeId,
|
||||
string invoiceId,
|
||||
RefundInvoiceRequest request,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/refund", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<PullPaymentData>(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,6 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
public string FormId { get; set; }
|
||||
|
||||
public string FormResponse { get; set; }
|
||||
public JObject FormResponse { get; set; }
|
||||
}
|
||||
}
|
||||
|
27
BTCPayServer.Client/Models/RefundInvoiceRequest.cs
Normal file
27
BTCPayServer.Client/Models/RefundInvoiceRequest.cs
Normal file
@ -0,0 +1,27 @@
|
||||
#nullable enable
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public enum RefundVariant
|
||||
{
|
||||
RateThen,
|
||||
CurrentRate,
|
||||
Fiat,
|
||||
Custom
|
||||
}
|
||||
|
||||
public class RefundInvoiceRequest
|
||||
{
|
||||
public string? Name { get; set; } = null;
|
||||
public string? PaymentMethod { get; set; }
|
||||
public string? Description { get; set; } = null;
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public RefundVariant? RefundVariant { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? CustomAmount { get; set; }
|
||||
public string? CustomCurrency { get; set; }
|
||||
}
|
||||
}
|
@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
@ -19,6 +16,7 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
[JsonProperty(Order = 1)] public string StoreId { get; set; }
|
||||
[JsonProperty(Order = 2)] public string InvoiceId { get; set; }
|
||||
[JsonProperty(Order = 3)] public JObject Metadata { get; set; }
|
||||
}
|
||||
|
||||
public class WebhookInvoiceSettledEvent : WebhookInvoiceEvent
|
||||
|
@ -105,10 +105,10 @@ namespace BTCPayServer.Data
|
||||
//PlannedTransaction.OnModelCreating(builder);
|
||||
PullPaymentData.OnModelCreating(builder);
|
||||
RefundData.OnModelCreating(builder);
|
||||
//SettingData.OnModelCreating(builder);
|
||||
SettingData.OnModelCreating(builder, Database);
|
||||
StoreSettingData.OnModelCreating(builder, Database);
|
||||
StoreWebhookData.OnModelCreating(builder);
|
||||
//StoreData.OnModelCreating(builder);
|
||||
StoreData.OnModelCreating(builder, Database);
|
||||
U2FDevice.OnModelCreating(builder);
|
||||
Fido2Credential.OnModelCreating(builder);
|
||||
BTCPayServer.Data.UserStore.OnModelCreating(builder);
|
||||
|
@ -1,3 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class SettingData
|
||||
@ -5,5 +8,15 @@ namespace BTCPayServer.Data
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Value { get; set; }
|
||||
|
||||
public static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<SettingData>()
|
||||
.Property(o => o.Value)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using PayoutProcessorData = BTCPayServer.Data.Data.PayoutProcessorData;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -36,7 +38,7 @@ namespace BTCPayServer.Data
|
||||
|
||||
[NotMapped] public string Role { get; set; }
|
||||
|
||||
public byte[] StoreBlob { get; set; }
|
||||
public string StoreBlob { get; set; }
|
||||
|
||||
[Obsolete("Use GetDefaultPaymentId instead")]
|
||||
public string DefaultCrypto { get; set; }
|
||||
@ -48,5 +50,15 @@ namespace BTCPayServer.Data
|
||||
public IEnumerable<PayoutData> Payouts { get; set; }
|
||||
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
|
||||
public IEnumerable<StoreSettingData> Settings { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<StoreData>()
|
||||
.Property(o => o.StoreBlob)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,42 +21,44 @@ namespace BTCPayServer.Data
|
||||
public const string PayjoinExposed = "pj-exposed";
|
||||
public const string Payout = "payout";
|
||||
public const string PullPayment = "pull-payment";
|
||||
public const string 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> ChildLinks { get; set; }
|
||||
public List<WalletObjectLinkData> ParentLinks { get; set; }
|
||||
public List<WalletObjectLinkData> Bs { get; set; }
|
||||
public List<WalletObjectLinkData> As { get; set; }
|
||||
|
||||
public IEnumerable<(string type, string id, string linkdata, string objectdata)> GetLinks()
|
||||
{
|
||||
if (ChildLinks is not null)
|
||||
foreach (var c in ChildLinks)
|
||||
if (Bs is not null)
|
||||
foreach (var c in Bs)
|
||||
{
|
||||
yield return (c.ChildType, c.ChildId, c.Data, c.Child?.Data);
|
||||
yield return (c.BType, c.BId, c.Data, c.B?.Data);
|
||||
}
|
||||
if (ParentLinks is not null)
|
||||
foreach (var c in ParentLinks)
|
||||
if (As is not null)
|
||||
foreach (var c in As)
|
||||
{
|
||||
yield return (c.ParentType, c.ParentId, c.Data, c.Parent?.Data);
|
||||
yield return (c.AType, c.AId, c.Data, c.A?.Data);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<WalletObjectData> GetNeighbours()
|
||||
{
|
||||
if (ChildLinks != null)
|
||||
foreach (var c in ChildLinks)
|
||||
if (Bs != null)
|
||||
foreach (var c in Bs)
|
||||
{
|
||||
if (c.Child != null)
|
||||
yield return c.Child;
|
||||
if (c.B != null)
|
||||
yield return c.B;
|
||||
}
|
||||
if (ParentLinks != null)
|
||||
foreach (var c in ParentLinks)
|
||||
if (As != null)
|
||||
foreach (var c in As)
|
||||
{
|
||||
if (c.Parent != null)
|
||||
yield return c.Parent;
|
||||
if (c.A != null)
|
||||
yield return c.A;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,14 +11,14 @@ namespace BTCPayServer.Data
|
||||
public class WalletObjectLinkData
|
||||
{
|
||||
public string WalletId { get; set; }
|
||||
public string ParentType { get; set; }
|
||||
public string ParentId { get; set; }
|
||||
public string ChildType { get; set; }
|
||||
public string ChildId { get; set; }
|
||||
public string AType { get; set; }
|
||||
public string AId { get; set; }
|
||||
public string BType { get; set; }
|
||||
public string BId { get; set; }
|
||||
public string Data { get; set; }
|
||||
|
||||
public WalletObjectData Parent { get; set; }
|
||||
public WalletObjectData Child { get; set; }
|
||||
public WalletObjectData A { get; set; }
|
||||
public WalletObjectData B { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
@ -26,28 +26,28 @@ namespace BTCPayServer.Data
|
||||
new
|
||||
{
|
||||
o.WalletId,
|
||||
o.ParentType,
|
||||
o.ParentId,
|
||||
o.ChildType,
|
||||
o.ChildId,
|
||||
o.AType,
|
||||
o.AId,
|
||||
o.BType,
|
||||
o.BId,
|
||||
});
|
||||
builder.Entity<WalletObjectLinkData>().HasIndex(o => new
|
||||
{
|
||||
o.WalletId,
|
||||
o.ChildType,
|
||||
o.ChildId,
|
||||
o.BType,
|
||||
o.BId,
|
||||
});
|
||||
|
||||
builder.Entity<WalletObjectLinkData>()
|
||||
.HasOne(o => o.Parent)
|
||||
.WithMany(o => o.ChildLinks)
|
||||
.HasForeignKey(o => new { o.WalletId, o.ParentType, o.ParentId })
|
||||
.HasOne(o => o.A)
|
||||
.WithMany(o => o.Bs)
|
||||
.HasForeignKey(o => new { o.WalletId, o.AType, o.AId })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<WalletObjectLinkData>()
|
||||
.HasOne(o => o.Child)
|
||||
.WithMany(o => o.ParentLinks)
|
||||
.HasForeignKey(o => new { o.WalletId, o.ChildType, o.ChildId })
|
||||
.HasOne(o => o.B)
|
||||
.WithMany(o => o.As)
|
||||
.HasForeignKey(o => new { o.WalletId, o.BType, o.BId })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
if (databaseFacade.IsNpgsql())
|
||||
|
@ -40,33 +40,33 @@ namespace BTCPayServer.Migrations
|
||||
columns: table => new
|
||||
{
|
||||
WalletId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ParentType = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ParentId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ChildType = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ChildId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
AType = table.Column<string>(type: "TEXT", nullable: false),
|
||||
AId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
BType = table.Column<string>(type: "TEXT", nullable: false),
|
||||
BId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Data = table.Column<string>(type: migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WalletObjectLinks", x => new { x.WalletId, x.ParentType, x.ParentId, x.ChildType, x.ChildId });
|
||||
table.PrimaryKey("PK_WalletObjectLinks", x => new { x.WalletId, x.AType, x.AId, x.BType, x.BId });
|
||||
table.ForeignKey(
|
||||
name: "FK_WalletObjectLinks_WalletObjects_WalletId_ChildType_ChildId",
|
||||
columns: x => new { x.WalletId, x.ChildType, x.ChildId },
|
||||
name: "FK_WalletObjectLinks_WalletObjects_WalletId_BType_BId",
|
||||
columns: x => new { x.WalletId, x.BType, x.BId },
|
||||
principalTable: "WalletObjects",
|
||||
principalColumns: new[] { "WalletId", "Type", "Id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_WalletObjectLinks_WalletObjects_WalletId_ParentType_ParentId",
|
||||
columns: x => new { x.WalletId, x.ParentType, x.ParentId },
|
||||
name: "FK_WalletObjectLinks_WalletObjects_WalletId_AType_AId",
|
||||
columns: x => new { x.WalletId, x.AType, x.AId },
|
||||
principalTable: "WalletObjects",
|
||||
principalColumns: new[] { "WalletId", "Type", "Id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WalletObjectLinks_WalletId_ChildType_ChildId",
|
||||
name: "IX_WalletObjectLinks_WalletId_BType_BId",
|
||||
table: "WalletObjectLinks",
|
||||
columns: new[] { "WalletId", "ChildType", "ChildId" });
|
||||
columns: new[] { "WalletId", "BType", "BId" });
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
|
31
BTCPayServer.Data/Migrations/20221128062447_jsonb.cs
Normal file
31
BTCPayServer.Data/Migrations/20221128062447_jsonb.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20221128062447_jsonb")]
|
||||
public partial class jsonb : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder.IsNpgsql())
|
||||
{
|
||||
migrationBuilder.Sql("ALTER TABLE \"Settings\" ALTER COLUMN \"Value\" TYPE JSONB USING \"Value\"::JSONB");
|
||||
migrationBuilder.Sql("ALTER TABLE \"Stores\" ALTER COLUMN \"StoreBlob\" TYPE JSONB USING regexp_replace(convert_from(\"StoreBlob\",'UTF8'), '\\\\u0000', '', 'g')::JSONB");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Not supported
|
||||
}
|
||||
}
|
||||
}
|
@ -872,24 +872,24 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("WalletId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ParentType")
|
||||
b.Property<string>("AType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ParentId")
|
||||
b.Property<string>("AId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ChildType")
|
||||
b.Property<string>("BType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ChildId")
|
||||
b.Property<string>("BId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("WalletId", "ParentType", "ParentId", "ChildType", "ChildId");
|
||||
b.HasKey("WalletId", "AType", "AId", "BType", "BId");
|
||||
|
||||
b.HasIndex("WalletId", "ChildType", "ChildId");
|
||||
b.HasIndex("WalletId", "BType", "BId");
|
||||
|
||||
b.ToTable("WalletObjectLinks");
|
||||
});
|
||||
@ -1384,21 +1384,21 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.WalletObjectData", "Child")
|
||||
.WithMany("ParentLinks")
|
||||
.HasForeignKey("WalletId", "ChildType", "ChildId")
|
||||
b.HasOne("BTCPayServer.Data.WalletObjectData", "A")
|
||||
.WithMany("Bs")
|
||||
.HasForeignKey("WalletId", "AType", "AId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("BTCPayServer.Data.WalletObjectData", "Parent")
|
||||
.WithMany("ChildLinks")
|
||||
.HasForeignKey("WalletId", "ParentType", "ParentId")
|
||||
b.HasOne("BTCPayServer.Data.WalletObjectData", "B")
|
||||
.WithMany("As")
|
||||
.HasForeignKey("WalletId", "BType", "BId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Child");
|
||||
b.Navigation("A");
|
||||
|
||||
b.Navigation("Parent");
|
||||
b.Navigation("B");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
|
||||
@ -1545,9 +1545,9 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectData", b =>
|
||||
{
|
||||
b.Navigation("ChildLinks");
|
||||
b.Navigation("As");
|
||||
|
||||
b.Navigation("ParentLinks");
|
||||
b.Navigation("Bs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WebhookData", b =>
|
||||
|
@ -6,7 +6,7 @@
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.14" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.20" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
@ -1,6 +1,6 @@
|
||||
[
|
||||
{
|
||||
"name":"Afghani",
|
||||
"name":"Afghan Afghani",
|
||||
"code":"AFN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -21,7 +21,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Lek",
|
||||
"name":"Albanian Lek",
|
||||
"code":"ALL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -42,7 +42,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Kwanza",
|
||||
"name":"Angolan Kwanza",
|
||||
"code":"AOA",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -84,7 +84,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Azerbaijanian Manat",
|
||||
"name":"Azerbaijani Manat",
|
||||
"code":"AZN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -105,14 +105,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Taka",
|
||||
"name":"Bangladeshi Taka",
|
||||
"code":"BDT",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Barbados Dollar",
|
||||
"name":"Barbadian Dollar",
|
||||
"code":"BBD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -161,21 +161,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Ngultrum",
|
||||
"name":"Bhutanese Ngultrum",
|
||||
"code":"BTN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Boliviano",
|
||||
"name":"Bolivian Boliviano",
|
||||
"code":"BOB",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Mvdol",
|
||||
"name":"Bolivian Mvdol",
|
||||
"code":"BOV",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -189,7 +189,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Pula",
|
||||
"name":"Botswana Pula",
|
||||
"code":"BWP",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -224,21 +224,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Burundi Franc",
|
||||
"name":"Burundian Franc",
|
||||
"code":"BIF",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Cabo Verde Escudo",
|
||||
"name":"Cape Verdean Escudo",
|
||||
"code":"CVE",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Riel",
|
||||
"name":"Cambodian Riel",
|
||||
"code":"KHR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -301,7 +301,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Comoro Franc",
|
||||
"name":"Comorian Franc",
|
||||
"code":"KMF",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
@ -329,7 +329,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Kuna",
|
||||
"name":"Croatian Kuna",
|
||||
"code":"HRK",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -371,7 +371,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Djibouti Franc",
|
||||
"name":"Djiboutian Franc",
|
||||
"code":"DJF",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
@ -392,14 +392,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"El Salvador Colon",
|
||||
"name":"Salvadoran Colon",
|
||||
"code":"SVC",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Nakfa",
|
||||
"name":"Eritrean Nakfa",
|
||||
"code":"ERN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -420,7 +420,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Fiji Dollar",
|
||||
"name":"Fijian Dollar",
|
||||
"code":"FJD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -434,21 +434,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Dalasi",
|
||||
"name":"Gambian Dalasi",
|
||||
"code":"GMD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Lari",
|
||||
"name":"Georgian Lari",
|
||||
"code":"GEL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Ghana Cedi",
|
||||
"name":"Ghanaian Cedi",
|
||||
"code":"GHS",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -462,7 +462,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Quetzal",
|
||||
"name":"Guatemalan Quetzal",
|
||||
"code":"GTQ",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -476,28 +476,28 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Guinea Franc",
|
||||
"name":"Guinean Franc",
|
||||
"code":"GNF",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Guyana Dollar",
|
||||
"name":"Guyanese Dollar",
|
||||
"code":"GYD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Gourde",
|
||||
"name":"Haitian Gourde",
|
||||
"code":"HTG",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Lempira",
|
||||
"name":"Honduran Lempira",
|
||||
"code":"HNL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -511,21 +511,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Forint",
|
||||
"name":"Hungarian Forint",
|
||||
"code":"HUF",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Iceland Krona",
|
||||
"name":"Icelandic Krona",
|
||||
"code":"ISK",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Rupiah",
|
||||
"name":"Indonesian Rupiah",
|
||||
"code":"IDR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -546,7 +546,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"New Israeli Sheqel",
|
||||
"name":"New Israeli Shekel",
|
||||
"code":"ILS",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -560,7 +560,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Yen",
|
||||
"name":"Japanese Yen",
|
||||
"code":"JPY",
|
||||
"divisibility":0,
|
||||
"symbol":"¥",
|
||||
@ -574,7 +574,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Tenge",
|
||||
"name":"Kazakhstani Tenge",
|
||||
"code":"KZT",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -595,7 +595,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Won",
|
||||
"name":"South Korean Won",
|
||||
"code":"KRW",
|
||||
"divisibility":0,
|
||||
"symbol":"₩",
|
||||
@ -609,14 +609,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Som",
|
||||
"name":"Kyrgyzstani Som",
|
||||
"code":"KGS",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Kip",
|
||||
"name":"Lao Kip",
|
||||
"code":"LAK",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -630,14 +630,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Loti",
|
||||
"name":"Lesotho Loti",
|
||||
"code":"LSL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Rand",
|
||||
"name":"South African Rand",
|
||||
"code":"ZAR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -665,14 +665,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Pataca",
|
||||
"name":"Macanese Pataca",
|
||||
"code":"MOP",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Denar",
|
||||
"name":"Macedonian Denar",
|
||||
"code":"MKD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -686,7 +686,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Malawi Kwacha",
|
||||
"name":"Malawian Kwacha",
|
||||
"code":"MWK",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -700,21 +700,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Rufiyaa",
|
||||
"name":"Maldivian Rufiyaa",
|
||||
"code":"MVR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Ouguiya",
|
||||
"name":"Mauritanian Ouguiya",
|
||||
"code":"MRO",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Mauritius Rupee",
|
||||
"name":"Mauritian Rupee",
|
||||
"code":"MUR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -742,7 +742,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Tugrik",
|
||||
"name":"Mongolian Tugrik",
|
||||
"code":"MNT",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -756,21 +756,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Mozambique Metical",
|
||||
"name":"Mozambican Metical",
|
||||
"code":"MZN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Kyat",
|
||||
"name":"Myanmar Kyat",
|
||||
"code":"MMK",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Namibia Dollar",
|
||||
"name":"Namibian dollar",
|
||||
"code":"NAD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -784,56 +784,56 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Cordoba Oro",
|
||||
"name":"Nicaraguan Cordoba",
|
||||
"code":"NIO",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Naira",
|
||||
"name":"Nigerian Naira",
|
||||
"code":"NGN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Rial Omani",
|
||||
"name":"Omani Rial",
|
||||
"code":"OMR",
|
||||
"divisibility":3,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Pakistan Rupee",
|
||||
"name":"Pakistani Rupee",
|
||||
"code":"PKR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Balboa",
|
||||
"name":"Panamanian Balboa",
|
||||
"code":"PAB",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Kina",
|
||||
"name":"Papua New Guinean Kina",
|
||||
"code":"PGK",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Guarani",
|
||||
"name":"Paraguayan Guarani",
|
||||
"code":"PYG",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Sol",
|
||||
"name":"Peruvian Sol",
|
||||
"code":"PEN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -847,7 +847,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Zloty",
|
||||
"name":"Polish Zloty",
|
||||
"code":"PLN",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -875,7 +875,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Rwanda Franc",
|
||||
"name":"Rwandan Franc",
|
||||
"code":"RWF",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
@ -889,14 +889,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Tala",
|
||||
"name":"Samoan Tala",
|
||||
"code":"WST",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Dobra",
|
||||
"name":"São Tomé and Príncipe sDobra",
|
||||
"code":"STD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -917,14 +917,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Seychelles Rupee",
|
||||
"name":"Seychellois Rupee",
|
||||
"code":"SCR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Leone",
|
||||
"name":"Sierra Leonean Leone",
|
||||
"code":"SLL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -959,7 +959,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Sri Lanka Rupee",
|
||||
"name":"Sri Lankan Rupee",
|
||||
"code":"LKR",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -973,14 +973,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Surinam Dollar",
|
||||
"name":"Surinamese Dollar",
|
||||
"code":"SRD",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Lilangeni",
|
||||
"name":"Swazi Lilangeni",
|
||||
"code":"SZL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -1022,7 +1022,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Somoni",
|
||||
"name":"Tajikistani Somoni",
|
||||
"code":"TJS",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -1036,14 +1036,14 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Baht",
|
||||
"name":"Thai Baht",
|
||||
"code":"THB",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Pa’anga",
|
||||
"name":"Tongan paʻanga",
|
||||
"code":"TOP",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -1071,21 +1071,21 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Turkmenistan New Manat",
|
||||
"name":"Turkmenistani Manat",
|
||||
"code":"TMT",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Uganda Shilling",
|
||||
"name":"Ugandan Shilling",
|
||||
"code":"UGX",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Hryvnia",
|
||||
"name":"Ukrainian Hryvnia",
|
||||
"code":"UAH",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -1106,7 +1106,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Peso Uruguayo",
|
||||
"name":"Uruguayan Peso",
|
||||
"code":"UYU",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
@ -1120,28 +1120,28 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Uzbekistan Sum",
|
||||
"name":"Uzbekistani Sum",
|
||||
"code":"UZS",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Vatu",
|
||||
"name":"Vanuatu Vatu",
|
||||
"code":"VUV",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Bolívar",
|
||||
"name":"Venezuelan Bolívar",
|
||||
"code":"VEF",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Dong",
|
||||
"name":"Vietnamese Dong",
|
||||
"code":"VND",
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
@ -1162,7 +1162,7 @@
|
||||
"crypto":false
|
||||
},
|
||||
{
|
||||
"name":"Zimbabwe Dollar",
|
||||
"name":"Zimbabwean Dollar",
|
||||
"code":"ZWL",
|
||||
"divisibility":2,
|
||||
"symbol":null,
|
||||
|
@ -171,8 +171,9 @@ namespace BTCPayServer.Tests
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
.OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
DerivationSchemeSettings.TryParseFromWalletFile(content, onchainBTC.Network, out var expected);
|
||||
DerivationSchemeSettings.TryParseFromWalletFile(content, onchainBTC.Network, out var expected, out var error);
|
||||
Assert.Equal(expected.ToJson(), onchainBTC.ToJson());
|
||||
Assert.Null(error);
|
||||
|
||||
// Let's check that the root hdkey and account key path are taken into account when making a PSBT
|
||||
invoice = await user.BitPay.CreateInvoiceAsync(
|
||||
|
@ -151,6 +151,15 @@ namespace BTCPayServer.Tests
|
||||
Assert.StartsWith("bitcoin:", payUrl);
|
||||
Assert.Contains("&LIGHTNING=", payUrl);
|
||||
|
||||
// BIP21 with LN as default payment method
|
||||
s.GoToHome();
|
||||
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
|
||||
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||
Assert.StartsWith("bitcoin:", payUrl);
|
||||
Assert.Contains("&LIGHTNING=", payUrl);
|
||||
|
||||
// BIP21 with topup invoice (which is only available with Bitcoin onchain)
|
||||
s.GoToHome();
|
||||
invoiceId = s.CreateInvoice(amount: null);
|
||||
|
@ -656,7 +656,8 @@ namespace BTCPayServer.Tests
|
||||
// ColdCard
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
mainnet, out var settings));
|
||||
mainnet, out var settings, out var error));
|
||||
Assert.Null(error);
|
||||
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint);
|
||||
Assert.Equal(settings.AccountKeySettings[0].RootFingerprint,
|
||||
HDFingerprint.TryParse("8bafd160", out var hd) ? hd : default);
|
||||
@ -672,30 +673,41 @@ namespace BTCPayServer.Tests
|
||||
// Should be legacy
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings));
|
||||
testnet, out settings, out error));
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit);
|
||||
Assert.Null(error);
|
||||
|
||||
// Should be segwit p2sh
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings));
|
||||
testnet, out settings, out error));
|
||||
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p &&
|
||||
p.Inner is DirectDerivationStrategy s2 && s2.Segwit);
|
||||
Assert.Null(error);
|
||||
|
||||
// Should be segwit
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings));
|
||||
testnet, out settings, out error));
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit);
|
||||
Assert.Null(error);
|
||||
|
||||
// Specter
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"label\": \"Specter\", \"blockheight\": 123456, \"descriptor\": \"wpkh([8bafd160/49h/0h/0h]xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw/0/*)#9x4vkw48\"}",
|
||||
mainnet, out var specter));
|
||||
mainnet, out var specter, out error));
|
||||
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), specter.AccountKeySettings[0].RootFingerprint);
|
||||
Assert.Equal(specter.AccountKeySettings[0].RootFingerprint, hd);
|
||||
Assert.Equal("49'/0'/0'", specter.AccountKeySettings[0].AccountKeyPath.ToString());
|
||||
Assert.Equal("Specter", specter.Label);
|
||||
Assert.Null(error);
|
||||
|
||||
// Failure case
|
||||
Assert.False(DerivationSchemeSettings.TryParseFromWalletFile(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubFailure\", \"xpub\": \"tpubFailure\", \"label\": \"Failure\"}, \"wallet_type\": \"standard\"}",
|
||||
testnet, out settings, out error));
|
||||
Assert.Null(settings);
|
||||
Assert.NotNull(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -1749,8 +1761,7 @@ namespace BTCPayServer.Tests
|
||||
PaymentMethod = new PaymentMethodId("BTC", PaymentTypes.BTCLike)
|
||||
}
|
||||
};
|
||||
var newBlob = Encoding.UTF8.GetBytes(
|
||||
new Serializer(null).ToString(blob).Replace( "paymentMethod\":\"BTC\"","paymentMethod\":\"ETH_ZYC\""));
|
||||
var newBlob = new Serializer(null).ToString(blob).Replace( "paymentMethod\":\"BTC\"","paymentMethod\":\"ETH_ZYC\"");
|
||||
Assert.Empty(StoreDataExtensions.GetStoreBlob(new StoreData() {StoreBlob = newBlob}).PaymentMethodCriteria);
|
||||
}
|
||||
}
|
||||
|
@ -1561,6 +1561,127 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanRefundInvoice()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
var client = await user.CreateClient();
|
||||
var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = 5000.0m, Currency = "USD" });
|
||||
var methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
|
||||
var method = methods.First();
|
||||
var amount = method.Amount;
|
||||
Assert.Equal(amount, method.Due);
|
||||
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||
{
|
||||
await tester.ExplorerNode.SendToAddressAsync(
|
||||
BitcoinAddress.Create(method.Destination, tester.NetworkProvider.BTC.NBitcoinNetwork),
|
||||
Money.Coins(method.Due)
|
||||
);
|
||||
});
|
||||
|
||||
// test validation that the invoice exists
|
||||
await AssertHttpError(404, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, "lol fake invoice id", new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
});
|
||||
});
|
||||
|
||||
// test validation error for when invoice is not yet in the state in which it can be refunded
|
||||
var apiError = await AssertAPIError("non-refundable", () => client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
}));
|
||||
Assert.Equal("Cannot refund this invoice", apiError.Message);
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
|
||||
Assert.True(invoice.Status == InvoiceStatus.Processing);
|
||||
});
|
||||
|
||||
// need to set the status to the one in which we can actually refund the invoice
|
||||
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest() {
|
||||
Status = InvoiceStatus.Settled
|
||||
});
|
||||
|
||||
// test validation for the payment method
|
||||
var validationError = await AssertValidationError(new[] { "PaymentMethod" }, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = "fake payment method",
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
});
|
||||
});
|
||||
Assert.Contains("PaymentMethod: Please select one of the payment methods which were available for the original invoice", validationError.Message);
|
||||
|
||||
// test RefundVariant.RateThen
|
||||
var pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(1, pp.Amount);
|
||||
Assert.Equal(pp.Name, $"Refund {invoice.Id}");
|
||||
|
||||
// test RefundVariant.CurrentRate
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.CurrentRate
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(1, pp.Amount);
|
||||
|
||||
// test RefundVariant.Fiat
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.Fiat,
|
||||
Name = "my test name"
|
||||
});
|
||||
Assert.Equal("USD", pp.Currency);
|
||||
Assert.False(pp.AutoApproveClaims);
|
||||
Assert.Equal(5000, pp.Amount);
|
||||
Assert.Equal("my test name", pp.Name);
|
||||
|
||||
// test RefundVariant.Custom
|
||||
validationError = await AssertValidationError(new[] { "CustomAmount", "CustomCurrency" }, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.Custom,
|
||||
});
|
||||
});
|
||||
Assert.Contains("CustomAmount: Amount must be greater than 0", validationError.Message);
|
||||
Assert.Contains("CustomCurrency: Invalid currency", validationError.Message);
|
||||
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.Custom,
|
||||
CustomAmount = 69420,
|
||||
CustomCurrency = "JPY"
|
||||
});
|
||||
Assert.Equal("JPY", pp.Currency);
|
||||
Assert.False(pp.AutoApproveClaims);
|
||||
Assert.Equal(69420, pp.Amount);
|
||||
|
||||
// should auto-approve if currencies match
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.Custom,
|
||||
CustomAmount = 0.00069420m,
|
||||
CustomCurrency = "BTC"
|
||||
});
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task InvoiceTests()
|
||||
@ -3073,7 +3194,7 @@ namespace BTCPayServer.Tests
|
||||
// Only the node `test` `test` is connected to `test1`
|
||||
var wid = new WalletId(admin.StoreId, "BTC");
|
||||
var repo = tester.PayTester.GetService<WalletRepository>();
|
||||
var allObjects = await repo.GetWalletObjects((new(wid, null) { UseInefficientPath = useInefficient }));
|
||||
var allObjects = await repo.GetWalletObjects(new(wid) { UseInefficientPath = useInefficient });
|
||||
var allObjectsNoWallet = await repo.GetWalletObjects((new() { UseInefficientPath = useInefficient }));
|
||||
var allObjectsNoWalletAndType = await repo.GetWalletObjects((new() { Type = "test", UseInefficientPath = useInefficient }));
|
||||
var allTests = await repo.GetWalletObjects((new(wid, "test") { UseInefficientPath = useInefficient }));
|
||||
|
250
BTCPayServer.Tests/LNbankTests.cs
Normal file
250
BTCPayServer.Tests/LNbankTests.cs
Normal file
@ -0,0 +1,250 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -71,7 +71,7 @@ services:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:23.0-1
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -126,7 +126,7 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:23.0-1
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
|
@ -68,7 +68,7 @@ services:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:23.0-1
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -113,7 +113,7 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:23.0-1
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
|
16
BTCPayServer.Tests/docker-customer-holdinvoice.sh
Executable file
16
BTCPayServer.Tests/docker-customer-holdinvoice.sh
Executable file
@ -0,0 +1,16 @@
|
||||
#!/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"
|
@ -97,8 +97,17 @@ 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
|
||||
|
@ -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.8" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.10" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
@ -133,6 +133,8 @@
|
||||
<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>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
@model BTCPayServer.Components.Icon.IconViewModel
|
||||
|
||||
<svg role="img" class="icon icon-@Model.Symbol">
|
||||
<use href="/img/icon-sprite.svg#@Model.Symbol"></use>
|
||||
<use href="~/img/icon-sprite.svg#@Model.Symbol"></use>
|
||||
</svg>
|
||||
|
@ -14,7 +14,7 @@
|
||||
else
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" role="img" alt="BTCPay Server" class="main-logo main-logo-btcpay @Model.CssClass">
|
||||
<use href="/img/logo.svg#small" class="main-logo-btcpay--small"/>
|
||||
<use href="/img/logo.svg#large" class="main-logo-btcpay--large"/>
|
||||
<use href="~/img/logo.svg#small" class="main-logo-btcpay--small"/>
|
||||
<use href="~/img/logo.svg#large" class="main-logo-btcpay--large"/>
|
||||
</svg>
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ else
|
||||
@if (Model.Options.Count > 0)
|
||||
{
|
||||
<div id="StoreSelectorDropdown" class="dropdown only-for-js">
|
||||
<button id="StoreSelectorToggle" class="btn btn-secondary dropdown-toggle rounded-pill px-3 @(Model.CurrentStoreId == null ? "text-secondary" : "")" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<button id="StoreSelectorToggle" class="btn btn-secondary dropdown-toggle rounded-pill px-3 @(Model.CurrentStoreId == null ? "empty-state" : "")" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@if (!string.IsNullOrEmpty(Model.CurrentStoreLogoFileId))
|
||||
{
|
||||
<img class="logo" src="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.CurrentStoreLogoFileId))" alt="@Model.CurrentDisplayName" />
|
||||
|
@ -65,6 +65,10 @@ namespace BTCPayServer.Configuration
|
||||
if (conf.GetOrDefault<bool>("launchsettings", false) && NetworkType != ChainName.Regtest)
|
||||
throw new ConfigException($"You need to run BTCPayServer with the run.sh or run.ps1 script");
|
||||
|
||||
if (conf.GetOrDefault<string>("SQLITEFILE", null) != null)
|
||||
Logs.Configuration.LogWarning("SQLITE backend support is deprecated and will be soon out of support");
|
||||
if (conf.GetOrDefault<string>("MYSQL", null) != null)
|
||||
Logs.Configuration.LogWarning("MYSQL backend support is deprecated and will be soon out of support");
|
||||
DockerDeployment = conf.GetOrDefault<bool>("dockerdeployment", true);
|
||||
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
||||
TorServices = conf.GetOrDefault<string>("torservices", null)
|
||||
|
@ -27,9 +27,9 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--signet | -signet", $"Use signet (deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
|
||||
app.Option("--postgres", $"Connection string to a PostgreSQL database", CommandOptionType.SingleValue);
|
||||
app.Option("--mysql", $"Connection string to a MySQL database", CommandOptionType.SingleValue);
|
||||
app.Option("--mysql", $"DEPRECATED: Connection string to a MySQL database", CommandOptionType.SingleValue);
|
||||
app.Option("--nocsp", $"Disable CSP (default false)", CommandOptionType.BoolValue);
|
||||
app.Option("--sqlitefile", $"File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
|
||||
app.Option("--sqlitefile", $"DEPRECATED: File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
|
||||
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshconnection", "SSH server to manage BTCPay under the form user@server:port (default: root@externalhost or empty)", CommandOptionType.SingleValue);
|
||||
|
@ -2,14 +2,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Rating;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@ -32,12 +37,19 @@ 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)
|
||||
EventAggregator eventAggregator, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||
CurrencyNameTable currencyNameTable, BTCPayNetworkProvider networkProvider, RateFetcher rateProvider,
|
||||
PullPaymentHostedService pullPaymentService, ApplicationDbContextFactory dbContextFactory)
|
||||
{
|
||||
_invoiceController = invoiceController;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
@ -45,6 +57,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_eventAggregator = eventAggregator;
|
||||
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_networkProvider = networkProvider;
|
||||
_rateProvider = rateProvider;
|
||||
_pullPaymentService = pullPaymentService;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
LanguageService = languageService;
|
||||
}
|
||||
|
||||
@ -333,6 +350,175 @@ 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");
|
||||
|
@ -460,12 +460,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ModelState.AddModelError(nameof(approvePayoutRequest.RateRule), "Invalid RateRule");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
var result = await _pullPaymentService.Approve(new PullPaymentHostedService.PayoutApproval()
|
||||
var result = (await _pullPaymentService.Approve(new PullPaymentHostedService.PayoutApproval()
|
||||
{
|
||||
PayoutId = payoutId,
|
||||
Revision = revision!.Value,
|
||||
Rate = rateResult.BidAsk.Ask
|
||||
});
|
||||
})).Result;
|
||||
var errorMessage = PullPaymentHostedService.PayoutApproval.GetErrorMessage(result);
|
||||
switch (result)
|
||||
{
|
||||
|
@ -189,7 +189,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId);
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId, (string[] ) null);
|
||||
|
||||
var preFiltering = true;
|
||||
if (statusFilter?.Any() is true || !string.IsNullOrWhiteSpace(labelFilter))
|
||||
|
@ -24,7 +24,6 @@ using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Invoices.Export;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
@ -134,7 +133,7 @@ namespace BTCPayServer.Controllers
|
||||
Events = invoice.Events,
|
||||
PosData = PosDataParser.ParsePosData(invoice.Metadata.PosData),
|
||||
Archived = invoice.Archived,
|
||||
CanRefund = CanRefund(invoiceState),
|
||||
CanRefund = invoiceState.CanRefund(),
|
||||
Refunds = invoice.Refunds,
|
||||
ShowCheckout = invoice.Status == InvoiceStatusLegacy.New,
|
||||
ShowReceipt = invoice.Status.ToModernStatus() == InvoiceStatus.Settled && (invoice.ReceiptOptions?.Enabled ?? receipt.Enabled is true),
|
||||
@ -179,11 +178,6 @@ 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 =>
|
||||
@ -240,16 +234,6 @@ 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)]
|
||||
@ -268,7 +252,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
if (invoice.CurrentRefund?.PullPaymentDataId is null && GetUserId() is null)
|
||||
return NotFound();
|
||||
if (!CanRefund(invoice.GetInvoiceState()))
|
||||
if (!invoice.GetInvoiceState().CanRefund())
|
||||
return NotFound();
|
||||
if (invoice.CurrentRefund?.PullPaymentDataId is string ppId && !invoice.CurrentRefund.PullPaymentData.Archived)
|
||||
{
|
||||
@ -324,7 +308,7 @@ namespace BTCPayServer.Controllers
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
|
||||
if (!CanRefund(invoice.GetInvoiceState()))
|
||||
if (!invoice.GetInvoiceState().CanRefund())
|
||||
return NotFound();
|
||||
|
||||
var store = GetCurrentStore();
|
||||
@ -659,9 +643,23 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
|
||||
bool isDefaultPaymentId = false;
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var btcId = PaymentMethodId.Parse("BTC");
|
||||
var lnId = PaymentMethodId.Parse("BTC_LightningLike");
|
||||
if (paymentMethodId is null)
|
||||
{
|
||||
var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider);
|
||||
var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider)
|
||||
// Exclude LNURL for Checkout v2
|
||||
.Where(pmId => storeBlob.CheckoutType == CheckoutType.V1 || pmId.PaymentType is not LNURLPayPaymentType)
|
||||
.ToArray();
|
||||
|
||||
// Exclude Lightning if OnChainWithLnInvoiceFallback is active and we have both payment methods
|
||||
if (storeBlob.CheckoutType == CheckoutType.V2 && storeBlob.OnChainWithLnInvoiceFallback &&
|
||||
enabledPaymentIds.Contains(btcId) && enabledPaymentIds.Contains(lnId))
|
||||
{
|
||||
enabledPaymentIds = enabledPaymentIds.Where(pmId => pmId != lnId).ToArray();
|
||||
}
|
||||
|
||||
PaymentMethodId? invoicePaymentId = invoice.GetDefaultPaymentMethod();
|
||||
PaymentMethodId? storePaymentId = store.GetDefaultPaymentId();
|
||||
if (invoicePaymentId is not null)
|
||||
@ -692,6 +690,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if (paymentMethodId is null)
|
||||
return null;
|
||||
|
||||
BTCPayNetworkBase network = _NetworkProvider.GetNetwork<BTCPayNetworkBase>(paymentMethodId.CryptoCode);
|
||||
if (network is null || !invoice.Support(paymentMethodId))
|
||||
{
|
||||
@ -718,12 +717,10 @@ 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())
|
||||
@ -826,16 +823,18 @@ namespace BTCPayServer.Controllers
|
||||
.OrderByDescending(a => a.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode).ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
|
||||
.ToList()
|
||||
};
|
||||
|
||||
// Exclude Lightning if OnChainWithLnInvoiceFallback is active and we have both payment methods
|
||||
if (storeBlob.CheckoutType == CheckoutType.V2 && storeBlob.OnChainWithLnInvoiceFallback)
|
||||
{
|
||||
var onchainPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == "BTC");
|
||||
var lightningPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == "BTC_LightningLike");
|
||||
var onchainPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == btcId.ToString());
|
||||
var lightningPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == lnId.ToString());
|
||||
if (onchainPM != null && lightningPM != null)
|
||||
{
|
||||
model.AvailableCryptos.Remove(lightningPM);
|
||||
}
|
||||
}
|
||||
|
||||
paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob, paymentMethod);
|
||||
model.UISettings = paymentMethodHandler.GetCheckoutUISettings();
|
||||
model.PaymentMethodId = paymentMethodId.ToString();
|
||||
|
@ -193,7 +193,8 @@ namespace BTCPayServer.Controllers
|
||||
Metadata = invoiceMetadata.ToJObject(),
|
||||
Currency = pr.Currency,
|
||||
Amount = amount,
|
||||
Checkout = { RedirectURL = redirectUrl }
|
||||
Checkout = { RedirectURL = redirectUrl },
|
||||
Receipt = new InvoiceDataBase.ReceiptOptions { Enabled = false }
|
||||
};
|
||||
|
||||
var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(pr.Id) };
|
||||
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
@ -20,6 +21,7 @@ using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -51,6 +53,7 @@ namespace BTCPayServer
|
||||
private readonly LightningAddressService _lightningAddressService;
|
||||
private readonly LightningLikePayoutHandler _lightningLikePayoutHandler;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
|
||||
|
||||
public UILNURLController(InvoiceRepository invoiceRepository,
|
||||
EventAggregator eventAggregator,
|
||||
@ -62,7 +65,8 @@ namespace BTCPayServer
|
||||
LinkGenerator linkGenerator,
|
||||
LightningAddressService lightningAddressService,
|
||||
LightningLikePayoutHandler lightningLikePayoutHandler,
|
||||
PullPaymentHostedService pullPaymentHostedService)
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings)
|
||||
{
|
||||
_invoiceRepository = invoiceRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
@ -75,11 +79,12 @@ namespace BTCPayServer
|
||||
_lightningAddressService = lightningAddressService;
|
||||
_lightningLikePayoutHandler = lightningLikePayoutHandler;
|
||||
_pullPaymentHostedService = pullPaymentHostedService;
|
||||
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("withdraw/pp/{pullPaymentId}")]
|
||||
public async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr)
|
||||
public async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
@ -155,25 +160,28 @@ namespace BTCPayServer
|
||||
{
|
||||
var client =
|
||||
_lightningLikePaymentHandler.CreateLightningClient(pm, network);
|
||||
PayResponse payResult;
|
||||
try
|
||||
{
|
||||
payResult = await client.Pay(pr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
payResult = new PayResponse(PayResult.Error, e.Message);
|
||||
}
|
||||
|
||||
var payResult = await UILightningLikePayoutController.TrypayBolt(client,
|
||||
claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings),
|
||||
claimResponse.PayoutData, result, pmi, cancellationToken);
|
||||
|
||||
switch (payResult.Result)
|
||||
{
|
||||
case PayResult.Ok:
|
||||
case PayResult.Unknown:
|
||||
await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest()
|
||||
{
|
||||
PayoutId = claimResponse.PayoutData.Id, State = PayoutState.Completed
|
||||
PayoutId = claimResponse.PayoutData.Id,
|
||||
State = claimResponse.PayoutData.State,
|
||||
Proof = claimResponse.PayoutData.GetProofBlobJson()
|
||||
});
|
||||
|
||||
return Ok(new LNUrlStatusResponse {Status = "OK"});
|
||||
return Ok(new LNUrlStatusResponse
|
||||
{
|
||||
Status = "OK",
|
||||
Reason = payResult.Message
|
||||
});
|
||||
case PayResult.CouldNotFindRoute:
|
||||
case PayResult.Error:
|
||||
default:
|
||||
await _pullPaymentHostedService.Cancel(
|
||||
new PullPaymentHostedService.CancelRequest(new string[]
|
||||
@ -184,7 +192,7 @@ namespace BTCPayServer
|
||||
return Ok(new LNUrlStatusResponse
|
||||
{
|
||||
Status = "ERROR",
|
||||
Reason = $"Pr could not be paid because {payResult.ErrorDetail}"
|
||||
Reason = payResult.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,15 @@
|
||||
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;
|
||||
@ -17,10 +18,8 @@ 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;
|
||||
@ -40,6 +39,8 @@ namespace BTCPayServer.Controllers
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
private FormComponentProviders FormProviders { get; }
|
||||
|
||||
public UIPaymentRequestController(
|
||||
UIInvoiceController invoiceController,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
@ -48,7 +49,8 @@ namespace BTCPayServer.Controllers
|
||||
EventAggregator eventAggregator,
|
||||
CurrencyNameTable currencies,
|
||||
StoreRepository storeRepository,
|
||||
InvoiceRepository invoiceRepository)
|
||||
InvoiceRepository invoiceRepository,
|
||||
FormComponentProviders formProviders)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
@ -58,6 +60,7 @@ namespace BTCPayServer.Controllers
|
||||
_Currencies = currencies;
|
||||
_storeRepository = storeRepository;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
FormProviders = formProviders;
|
||||
}
|
||||
|
||||
[BitpayAPIConstraint(false)]
|
||||
@ -181,7 +184,7 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet("{payReqId}/form")]
|
||||
[HttpPost("{payReqId}/form")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ViewPaymentRequestForm(string payReqId, [FromForm] string formId, [FromForm] string formData)
|
||||
public async Task<IActionResult> ViewPaymentRequestForm(string payReqId)
|
||||
{
|
||||
var result = await _PaymentRequestRepository.FindPaymentRequest(payReqId, GetUserId());
|
||||
if (result == null)
|
||||
@ -191,36 +194,41 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var prBlob = result.GetBlob();
|
||||
var prFormId = prBlob.FormId;
|
||||
switch (prFormId)
|
||||
var formConfig = prFormId is null ? null : Forms.UIFormsController.GetFormData(prFormId)?.Config;
|
||||
switch (formConfig)
|
||||
{
|
||||
case null:
|
||||
case { } when string.IsNullOrEmpty(prFormId):
|
||||
case { } when !this.Request.HasFormContentType && prBlob.FormResponse is not null:
|
||||
return RedirectToAction("ViewPaymentRequest", new { payReqId });
|
||||
case { } when !this.Request.HasFormContentType && prBlob.FormResponse is null:
|
||||
break;
|
||||
|
||||
default:
|
||||
// POST case: Handle form submit
|
||||
if (!string.IsNullOrEmpty(formData) && formId == prFormId)
|
||||
var formData = Form.Parse(formConfig);
|
||||
formData.ApplyValuesFromForm(Request.Form);
|
||||
if (FormProviders.Validate(formData, ModelState))
|
||||
{
|
||||
prBlob.FormResponse = formData;
|
||||
prBlob.FormResponse = JObject.FromObject(formData.GetValues());
|
||||
result.SetBlob(prBlob);
|
||||
await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(result);
|
||||
return RedirectToAction("PayPaymentRequest", new { payReqId });
|
||||
}
|
||||
|
||||
// GET or empty form data case: Redirect to form
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
AspController = "UIForms",
|
||||
AspAction = "ViewPublicForm",
|
||||
FormParameters =
|
||||
{
|
||||
{ "formId", prFormId },
|
||||
{ "redirectUrl", Request.GetCurrentUrl() }
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return RedirectToAction("ViewPaymentRequest", new { payReqId });
|
||||
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
AspController = "UIForms",
|
||||
AspAction = "ViewPublicForm",
|
||||
RouteParameters =
|
||||
{
|
||||
{ "formId", prFormId }
|
||||
},
|
||||
FormParameters =
|
||||
{
|
||||
{ "redirectUrl", Request.GetCurrentUrl() }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{payReqId}/pay")]
|
||||
|
@ -338,11 +338,11 @@ namespace BTCPayServer.Controllers
|
||||
Revision = payout.GetBlob(_jsonSerializerSettings).Revision,
|
||||
Rate = rateResult.BidAsk.Ask
|
||||
});
|
||||
if (approveResult != PullPaymentHostedService.PayoutApproval.Result.Ok)
|
||||
if (approveResult.Result != PullPaymentHostedService.PayoutApproval.Result.Ok)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult),
|
||||
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult.Result),
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
});
|
||||
failed = true;
|
||||
|
@ -89,17 +89,17 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (vm.WalletFile != null)
|
||||
{
|
||||
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy))
|
||||
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy, out var error))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.WalletFile), "Wallet file was not in the correct format");
|
||||
ModelState.AddModelError(nameof(vm.WalletFile), $"Importing wallet failed: {error}");
|
||||
return View(vm.ViewName, vm);
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(vm.WalletFileContent))
|
||||
{
|
||||
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy))
|
||||
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy, out var error))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.WalletFileContent), "QR import was not in the correct format");
|
||||
ModelState.AddModelError(nameof(vm.WalletFileContent), $"QR import failed: {error}");
|
||||
return View(vm.ViewName, vm);
|
||||
}
|
||||
}
|
||||
|
@ -578,17 +578,24 @@ namespace BTCPayServer.Controllers
|
||||
var utxos = await _walletProvider.GetWallet(network)
|
||||
.GetUnspentCoins(schemeSettings.AccountDerivation, false, cancellation);
|
||||
|
||||
var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId, utxos.Select(u => u.OutPoint.Hash.ToString()).Distinct().ToArray());
|
||||
var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId,
|
||||
utxos.SelectMany(u => GetWalletObjectsQuery.Get(u)).Distinct().ToArray());
|
||||
vm.InputsAvailable = utxos.Select(coin =>
|
||||
{
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
|
||||
var labels = CreateTransactionTagModels(info).ToList();
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.ScriptPubKey.ToHex(), out var info2);
|
||||
|
||||
if (info is not null && info2 is not null)
|
||||
{
|
||||
info.Merge(info2);
|
||||
}
|
||||
info ??= info2;
|
||||
return new WalletSendModel.InputSelectionOption()
|
||||
{
|
||||
Outpoint = coin.OutPoint.ToString(),
|
||||
Amount = coin.Value.GetValue(network),
|
||||
Comment = info?.Comment,
|
||||
Labels = labels,
|
||||
Labels = CreateTransactionTagModels(info),
|
||||
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
|
||||
coin.OutPoint.Hash.ToString()),
|
||||
Confirmations = coin.Confirmations
|
||||
@ -1291,7 +1298,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId);
|
||||
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId, (string[] ) null);
|
||||
var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, null, null);
|
||||
var walletTransactionsInfo = await walletTransactionsInfoAsync;
|
||||
var export = new TransactionsExport(wallet, walletTransactionsInfo);
|
||||
|
@ -257,9 +257,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static readonly TimeSpan SendTimeout = TimeSpan.FromSeconds(20);
|
||||
|
||||
public static async Task<ResultVM> TrypayBolt(
|
||||
ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest,
|
||||
PaymentMethodId pmi, CancellationToken cancellationToken)
|
||||
@ -281,17 +279,13 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
var proofBlob = new PayoutLightningBlob() { PaymentHash = bolt11PaymentRequest.PaymentHash.ToString() };
|
||||
try
|
||||
{
|
||||
// TODO: Incorporate the changes from this PR here:
|
||||
// https://github.com/btcpayserver/BTCPayServer.Lightning/pull/106
|
||||
using var timeout = new CancellationTokenSource(SendTimeout);
|
||||
using var c = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
|
||||
var result = await lightningClient.Pay(bolt11PaymentRequest.ToString(),
|
||||
new PayInvoiceParams()
|
||||
{
|
||||
Amount = bolt11PaymentRequest.MinimumAmount == LightMoney.Zero
|
||||
? new LightMoney((decimal)payoutBlob.CryptoAmount, LightMoneyUnit.BTC)
|
||||
: null
|
||||
}, c.Token);
|
||||
}, cancellationToken);
|
||||
string message = null;
|
||||
if (result.Result == PayResult.Ok)
|
||||
{
|
||||
@ -309,6 +303,11 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
else if(result.Result == PayResult.Unknown)
|
||||
{
|
||||
payoutData.State = PayoutState.InProgress;
|
||||
message = "The payment has been initiated but is still in-flight.";
|
||||
}
|
||||
|
||||
payoutData.SetProofBlob(proofBlob, null);
|
||||
return new ResultVM
|
||||
|
@ -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>(Encoding.UTF8.GetString(storeData.StoreBlob));
|
||||
var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(storeData.StoreBlob);
|
||||
if (result.PreferredExchange == null)
|
||||
result.PreferredExchange = CoinGeckoRateProvider.CoinGeckoName;
|
||||
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 = Encoding.UTF8.GetBytes(newBlob);
|
||||
storeData.StoreBlob = newBlob;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -83,5 +82,23 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using BTCPayServer.Payments;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
@ -16,17 +17,18 @@ namespace BTCPayServer
|
||||
{
|
||||
public static DerivationSchemeSettings Parse(string derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
string error = null;
|
||||
ArgumentNullException.ThrowIfNull(network);
|
||||
ArgumentNullException.ThrowIfNull(derivationStrategy);
|
||||
var result = new DerivationSchemeSettings();
|
||||
result.Network = network;
|
||||
var parser = new DerivationSchemeParser(network);
|
||||
if (TryParseXpub(derivationStrategy, parser, ref result, false) || TryParseXpub(derivationStrategy, parser, ref result, true))
|
||||
if (TryParseXpub(derivationStrategy, parser, ref result, ref error, false) || TryParseXpub(derivationStrategy, parser, ref result, ref error, true))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new FormatException("Invalid Derivation Scheme");
|
||||
throw new FormatException($"Invalid Derivation Scheme: {error}");
|
||||
}
|
||||
|
||||
public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy)
|
||||
@ -47,10 +49,11 @@ namespace BTCPayServer
|
||||
{
|
||||
return AccountDerivation is null ? null : DBUtils.nbxv1_get_wallet_id(Network.CryptoCode, AccountDerivation.ToString());
|
||||
}
|
||||
private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, bool electrum = true)
|
||||
private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, ref string error, bool electrum = true)
|
||||
{
|
||||
if (!electrum)
|
||||
{
|
||||
var isOD = Regex.Match(xpub, @"\(.*?\)").Success;
|
||||
try
|
||||
{
|
||||
var result = derivationSchemeParser.ParseOutputDescriptor(xpub);
|
||||
@ -64,9 +67,13 @@ namespace BTCPayServer
|
||||
}).ToArray();
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception exception)
|
||||
{
|
||||
// ignored
|
||||
error = exception.Message;
|
||||
if (isOD)
|
||||
{
|
||||
return false;
|
||||
} // otherwise continue and try to parse input as xpub
|
||||
}
|
||||
}
|
||||
try
|
||||
@ -82,20 +89,22 @@ namespace BTCPayServer
|
||||
derivationSchemeSettings.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception exception)
|
||||
{
|
||||
error = exception.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryParseFromWalletFile(string fileContents, BTCPayNetwork network, out DerivationSchemeSettings settings)
|
||||
public static bool TryParseFromWalletFile(string fileContents, BTCPayNetwork network, out DerivationSchemeSettings settings, out string error)
|
||||
{
|
||||
settings = null;
|
||||
error = null;
|
||||
ArgumentNullException.ThrowIfNull(fileContents);
|
||||
ArgumentNullException.ThrowIfNull(network);
|
||||
var result = new DerivationSchemeSettings();
|
||||
var derivationSchemeParser = new DerivationSchemeParser(network);
|
||||
JObject jobj = null;
|
||||
JObject jobj;
|
||||
try
|
||||
{
|
||||
if (HexEncoder.IsWellFormed(fileContents))
|
||||
@ -107,8 +116,8 @@ namespace BTCPayServer
|
||||
catch
|
||||
{
|
||||
result.Source = "GenericFile";
|
||||
if (TryParseXpub(fileContents, derivationSchemeParser, ref result) ||
|
||||
TryParseXpub(fileContents, derivationSchemeParser, ref result, false))
|
||||
if (TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error) ||
|
||||
TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error, false))
|
||||
{
|
||||
settings = result;
|
||||
settings.Network = network;
|
||||
@ -125,7 +134,7 @@ namespace BTCPayServer
|
||||
jobj = (JObject)jobj["keystore"];
|
||||
|
||||
if (!jobj.ContainsKey("xpub") ||
|
||||
!TryParseXpub(jobj["xpub"].Value<string>(), derivationSchemeParser, ref result))
|
||||
!TryParseXpub(jobj["xpub"].Value<string>(), derivationSchemeParser, ref result, ref error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -162,7 +171,7 @@ namespace BTCPayServer
|
||||
{
|
||||
result.Source = "SpecterFile";
|
||||
|
||||
if (!TryParseXpub(jobj["descriptor"].Value<string>(), derivationSchemeParser, ref result, false))
|
||||
if (!TryParseXpub(jobj["descriptor"].Value<string>(), derivationSchemeParser, ref result, ref error, false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -181,7 +190,7 @@ namespace BTCPayServer
|
||||
{
|
||||
result.Source = "WasabiFile";
|
||||
if (!jobj.ContainsKey("ExtPubKey") ||
|
||||
!TryParseXpub(jobj["ExtPubKey"].Value<string>(), derivationSchemeParser, ref result, false))
|
||||
!TryParseXpub(jobj["ExtPubKey"].Value<string>(), derivationSchemeParser, ref result, ref error, false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
34
BTCPayServer/Forms/FormComponentProviders.cs
Normal file
34
BTCPayServer/Forms/FormComponentProviders.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class FormComponentProviders
|
||||
{
|
||||
private readonly IEnumerable<IFormComponentProvider> _formComponentProviders;
|
||||
|
||||
public Dictionary<string, IFormComponentProvider> TypeToComponentProvider = new Dictionary<string, IFormComponentProvider>();
|
||||
|
||||
public FormComponentProviders(IEnumerable<IFormComponentProvider> formComponentProviders)
|
||||
{
|
||||
_formComponentProviders = formComponentProviders;
|
||||
foreach (var prov in _formComponentProviders)
|
||||
prov.Register(TypeToComponentProvider);
|
||||
}
|
||||
|
||||
public bool Validate(Form form, ModelStateDictionary modelState)
|
||||
{
|
||||
foreach (var field in form.Fields)
|
||||
{
|
||||
if (TypeToComponentProvider.TryGetValue(field.Type, out var provider))
|
||||
{
|
||||
provider.Validate(form, field);
|
||||
foreach (var err in field.ValidationErrors)
|
||||
modelState.TryAddModelError(field.Name, err);
|
||||
}
|
||||
}
|
||||
return modelState.IsValid;
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ public static class FormDataExtensions
|
||||
public static void AddForms(this IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddSingleton<FormDataService>();
|
||||
serviceCollection.AddSingleton<FormComponentProvider>();
|
||||
serviceCollection.AddSingleton<FormComponentProviders>();
|
||||
serviceCollection.AddSingleton<IFormComponentProvider, HtmlInputFormProvider>();
|
||||
serviceCollection.AddSingleton<IFormComponentProvider, HtmlFieldsetFormProvider>();
|
||||
}
|
||||
|
@ -15,21 +15,21 @@ public class FormDataService
|
||||
|
||||
public static readonly Form StaticFormEmail = new()
|
||||
{
|
||||
Fields = new List<Field>() {new HtmlInputField("Enter your email", "buyerEmail", null, true, null)}
|
||||
Fields = new List<Field>() {Field.Create("Enter your email", "buyerEmail", null, true, null, "email")}
|
||||
};
|
||||
|
||||
public static readonly Form StaticFormAddress = new()
|
||||
{
|
||||
Fields = new List<Field>()
|
||||
{
|
||||
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)
|
||||
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)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,12 +1,23 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class HtmlFieldsetFormProvider: IFormComponentProvider
|
||||
{
|
||||
public string CanHandle(Field field)
|
||||
public string View => "Forms/FieldSetElement";
|
||||
|
||||
public void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
|
||||
{
|
||||
return new[] { "fieldset"}.Contains(field.Type) ? "Forms/FieldSetElement" : null;
|
||||
typeToComponentProvider.Add("fieldset", this);
|
||||
}
|
||||
}
|
||||
|
||||
public void Validate(Field field)
|
||||
{
|
||||
}
|
||||
|
||||
public void Validate(Form form, Field field)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,16 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Validation;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class HtmlInputFormProvider: IFormComponentProvider
|
||||
public class HtmlInputFormProvider: FormComponentProviderBase
|
||||
{
|
||||
public string CanHandle(Field field)
|
||||
public override void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
|
||||
{
|
||||
return new[] {
|
||||
foreach (var t in new[] {
|
||||
"text",
|
||||
"radio",
|
||||
"checkbox",
|
||||
@ -29,6 +32,20 @@ public class HtmlInputFormProvider: IFormComponentProvider
|
||||
"search",
|
||||
"url",
|
||||
"tel",
|
||||
"reset"}.Contains(field.Type) ? "Forms/InputElement" : null;
|
||||
"reset"})
|
||||
typeToComponentProvider.Add(t, this);
|
||||
}
|
||||
}
|
||||
public override string View => "Forms/InputElement";
|
||||
|
||||
public override void Validate(Form form, Field field)
|
||||
{
|
||||
if (field.Required)
|
||||
{
|
||||
ValidateField<RequiredAttribute>(field);
|
||||
}
|
||||
if (field.Type == "email")
|
||||
{
|
||||
ValidateField<MailboxAddressAttribute>(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,26 @@
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public interface IFormComponentProvider
|
||||
{
|
||||
public string CanHandle(Field field);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -8,5 +8,6 @@ public class FormViewModel
|
||||
{
|
||||
public string RedirectUrl { get; set; }
|
||||
public FormData FormData { get; set; }
|
||||
public Form Form { get => JObject.Parse(FormData.Config).ToObject<Form>(); }
|
||||
Form _Form;
|
||||
public Form Form { get => _Form ??= Form.Parse(FormData.Config); }
|
||||
}
|
||||
|
@ -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,25 +39,35 @@ public class UIFormsController : Controller
|
||||
: Redirect(redirectUrl);
|
||||
}
|
||||
|
||||
return View("View", new FormViewModel { FormData = formData, RedirectUrl = 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,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
[FromServices] UIInvoiceController invoiceController)
|
||||
public IActionResult SubmitForm(string formId, string? redirectUrl, string? command)
|
||||
{
|
||||
if (!IsValidRedirectUri(redirectUrl))
|
||||
return BadRequest();
|
||||
|
||||
var formData = GetFormData(formId);
|
||||
if (formData is null)
|
||||
{
|
||||
if (formData?.Config is null)
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!Request.HasFormContentType)
|
||||
return GetFormView(formData, redirectUrl);
|
||||
|
||||
var dbForm = JObject.Parse(formData.Config!).ToObject<Form>()!;
|
||||
dbForm.ApplyValuesFromForm(Request.Form);
|
||||
Dictionary<string, object> data = dbForm.GetValues();
|
||||
var conf = Form.Parse(formData.Config);
|
||||
conf.ApplyValuesFromForm(Request.Form);
|
||||
if (!FormProviders.Validate(conf, ModelState))
|
||||
return GetFormView(formData, redirectUrl);
|
||||
|
||||
var form = new MultiValueDictionary<string, string>();
|
||||
foreach (var kv in Request.Form)
|
||||
form.Add(kv.Key, kv.Value);
|
||||
|
||||
// With redirect, the form comes from another entity that we need to send the data back to
|
||||
if (!string.IsNullOrEmpty(redirectUrl))
|
||||
@ -65,30 +75,26 @@ public class UIFormsController : Controller
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
FormUrl = redirectUrl,
|
||||
FormParameters =
|
||||
{
|
||||
{ "formId", formData.Id },
|
||||
{ "formData", JsonConvert.SerializeObject(data) }
|
||||
}
|
||||
FormParameters = form
|
||||
});
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
private FormData? GetFormData(string id)
|
||||
internal static FormData? GetFormData(string id)
|
||||
{
|
||||
FormData? form = id switch
|
||||
{
|
||||
{ } formId when formId == GenericFormOption.Address.ToString() => new FormData
|
||||
{
|
||||
Config = JObject.FromObject(FormDataService.StaticFormAddress).ToString(),
|
||||
Config = FormDataService.StaticFormAddress.ToString(),
|
||||
Id = GenericFormOption.Address.ToString(),
|
||||
Name = "Provide your address",
|
||||
},
|
||||
{ } formId when formId == GenericFormOption.Email.ToString() => new FormData
|
||||
{
|
||||
Config = JObject.FromObject(FormDataService.StaticFormEmail).ToString(),
|
||||
Config = FormDataService.StaticFormEmail.ToString(),
|
||||
Id = GenericFormOption.Email.ToString(),
|
||||
Name = "Provide your email address",
|
||||
},
|
||||
@ -96,4 +102,8 @@ 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));
|
||||
}
|
||||
|
@ -173,10 +173,10 @@ next:
|
||||
db.WalletObjectLinks.Add(new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = tx.WalletDataId,
|
||||
ChildType = Data.WalletObjectData.Types.Tx,
|
||||
ChildId = tx.TransactionId,
|
||||
ParentType = Data.WalletObjectData.Types.Label,
|
||||
ParentId = labelId
|
||||
BType = Data.WalletObjectData.Types.Tx,
|
||||
BId = tx.TransactionId,
|
||||
AType = Data.WalletObjectData.Types.Label,
|
||||
AId = labelId
|
||||
});
|
||||
|
||||
if (label.Value is ReferenceLabel reflabel)
|
||||
@ -195,10 +195,10 @@ next:
|
||||
db.WalletObjectLinks.Add(new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = tx.WalletDataId,
|
||||
ChildType = Data.WalletObjectData.Types.Tx,
|
||||
ChildId = tx.TransactionId,
|
||||
ParentType = reflabel.Type,
|
||||
ParentId = reflabel.Reference ?? String.Empty
|
||||
BType = Data.WalletObjectData.Types.Tx,
|
||||
BId = tx.TransactionId,
|
||||
AType = reflabel.Type,
|
||||
AId = reflabel.Reference ?? String.Empty
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -224,10 +224,10 @@ next:
|
||||
db.WalletObjectLinks.Add(new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = tx.WalletDataId,
|
||||
ChildType = Data.WalletObjectData.Types.Tx,
|
||||
ChildId = tx.TransactionId,
|
||||
ParentType = "payout",
|
||||
ParentId = payout
|
||||
BType = Data.WalletObjectData.Types.Tx,
|
||||
BId = tx.TransactionId,
|
||||
AType = "payout",
|
||||
AId = payout
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -79,10 +79,12 @@ namespace BTCPayServer.HostedServices
|
||||
OldRevision
|
||||
}
|
||||
|
||||
public record ApprovalResult(Result Result, decimal? CryptoAmount);
|
||||
|
||||
public string PayoutId { get; set; }
|
||||
public int Revision { get; set; }
|
||||
public decimal Rate { get; set; }
|
||||
internal TaskCompletionSource<Result> Completion { get; set; }
|
||||
internal TaskCompletionSource<ApprovalResult> Completion { get; set; }
|
||||
|
||||
public static string GetErrorMessage(Result result)
|
||||
{
|
||||
@ -333,10 +335,10 @@ namespace BTCPayServer.HostedServices
|
||||
return _rateFetcher.FetchRate(rule, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<PayoutApproval.Result> Approve(PayoutApproval approval)
|
||||
public Task<PayoutApproval.ApprovalResult> Approve(PayoutApproval approval)
|
||||
{
|
||||
approval.Completion =
|
||||
new TaskCompletionSource<PayoutApproval.Result>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
new TaskCompletionSource<PayoutApproval.ApprovalResult>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
if (!_Channel.Writer.TryWrite(approval))
|
||||
throw new ObjectDisposedException(nameof(PullPaymentHostedService));
|
||||
return approval.Completion.Task;
|
||||
@ -351,26 +353,26 @@ namespace BTCPayServer.HostedServices
|
||||
.FirstOrDefaultAsync();
|
||||
if (payout is null)
|
||||
{
|
||||
req.Completion.SetResult(PayoutApproval.Result.NotFound);
|
||||
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.NotFound, null));
|
||||
return;
|
||||
}
|
||||
|
||||
if (payout.State != PayoutState.AwaitingApproval)
|
||||
{
|
||||
req.Completion.SetResult(PayoutApproval.Result.InvalidState);
|
||||
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.InvalidState, null));
|
||||
return;
|
||||
}
|
||||
|
||||
var payoutBlob = payout.GetBlob(this._jsonSerializerSettings);
|
||||
if (payoutBlob.Revision != req.Revision)
|
||||
{
|
||||
req.Completion.SetResult(PayoutApproval.Result.OldRevision);
|
||||
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.OldRevision, null));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PaymentMethodId.TryParse(payout.PaymentMethodId, out var paymentMethod))
|
||||
{
|
||||
req.Completion.SetResult(PayoutApproval.Result.NotFound);
|
||||
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.NotFound, null));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -388,7 +390,7 @@ namespace BTCPayServer.HostedServices
|
||||
await payoutHandler.GetMinimumPayoutAmount(paymentMethod, dest.destination);
|
||||
if (cryptoAmount < minimumCryptoAmount)
|
||||
{
|
||||
req.Completion.TrySetResult(PayoutApproval.Result.TooLowAmount);
|
||||
req.Completion.TrySetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.TooLowAmount, null));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -397,7 +399,7 @@ namespace BTCPayServer.HostedServices
|
||||
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
req.Completion.SetResult(PayoutApproval.Result.Ok);
|
||||
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.Ok, payoutBlob.CryptoAmount));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -566,18 +568,20 @@ namespace BTCPayServer.HostedServices
|
||||
var rateResult = await GetRate(payout, null, CancellationToken.None);
|
||||
if (rateResult.BidAsk != null)
|
||||
{
|
||||
var approveResult = new TaskCompletionSource<PayoutApproval.Result>();
|
||||
var approveResultTask = new TaskCompletionSource<PayoutApproval.ApprovalResult>();
|
||||
await HandleApproval(new PayoutApproval()
|
||||
{
|
||||
PayoutId = payout.Id,
|
||||
Revision = payoutBlob.Revision,
|
||||
Rate = rateResult.BidAsk.Ask,
|
||||
Completion = approveResult
|
||||
Completion = approveResultTask
|
||||
});
|
||||
|
||||
if ((await approveResult.Task) == PayoutApproval.Result.Ok)
|
||||
var approveResult = await approveResultTask.Task;
|
||||
if (approveResult.Result == PayoutApproval.Result.Ok)
|
||||
{
|
||||
payout.State = PayoutState.AwaitingPayment;
|
||||
payoutBlob.CryptoAmount = approveResult.CryptoAmount;
|
||||
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
@ -22,42 +23,79 @@ 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)
|
||||
{
|
||||
if (evt is InvoiceEvent invoiceEvent && invoiceEvent.Name == InvoiceEvent.ReceivedPayment &&
|
||||
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance &&
|
||||
invoiceEvent.Payment.GetCryptoPaymentData() is BitcoinLikePaymentData bitcoinLikePaymentData)
|
||||
switch (evt)
|
||||
{
|
||||
var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode());
|
||||
var transactionId = bitcoinLikePaymentData.Outpoint.Hash;
|
||||
var labels = new List<Attachment>
|
||||
// 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:
|
||||
{
|
||||
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));
|
||||
}
|
||||
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();
|
||||
|
||||
await _walletRepository.AddWalletTransactionAttachment(walletId, transactionId, labels);
|
||||
var objs = await _walletRepository.GetWalletObjects(new GetWalletObjectsQuery(){TypesIds = matchedObjects});
|
||||
|
||||
foreach (var walletObjectDatas in objs.GroupBy(data => data.Key.WalletId))
|
||||
{
|
||||
var txWalletObject = new WalletObjectId(walletObjectDatas.Key,
|
||||
WalletObjectData.Types.Tx, txHash);
|
||||
await _walletRepository.EnsureWalletObject(txWalletObject);
|
||||
foreach (var walletObjectData in walletObjectDatas)
|
||||
{
|
||||
await _walletRepository.EnsureWalletObjectLink(txWalletObject, walletObjectData.Key);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case InvoiceEvent {Name: InvoiceEvent.ReceivedPayment} invoiceEvent when
|
||||
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance &&
|
||||
invoiceEvent.Payment.GetCryptoPaymentData() is BitcoinLikePaymentData bitcoinLikePaymentData:
|
||||
{
|
||||
var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode());
|
||||
var transactionId = bitcoinLikePaymentData.Outpoint.Hash;
|
||||
var labels = new List<Attachment>
|
||||
{
|
||||
Attachment.Invoice(invoiceEvent.Invoice.Id)
|
||||
};
|
||||
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
|
||||
{
|
||||
labels.Add(Attachment.PaymentRequest(paymentId));
|
||||
}
|
||||
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice))
|
||||
{
|
||||
labels.Add(Attachment.App(appId));
|
||||
}
|
||||
|
||||
await _walletRepository.AddWalletTransactionAttachment(walletId, transactionId, labels);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -157,6 +157,7 @@ namespace BTCPayServer.HostedServices
|
||||
webhookEvent.DeliveryId = delivery.Id;
|
||||
webhookEvent.WebhookId = webhook.Id;
|
||||
webhookEvent.OriginalDeliveryId = delivery.Id;
|
||||
webhookEvent.Metadata = invoiceEvent.Invoice.Metadata.ToJObject();
|
||||
webhookEvent.IsRedelivery = false;
|
||||
webhookEvent.Timestamp = delivery.Timestamp;
|
||||
var context = new WebhookDeliveryRequest(webhook.Id, webhookEvent, delivery, webhookBlob);
|
||||
|
@ -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 };
|
||||
settings = new MigrationSettings() { MigratedInvoiceTextSearchPages = int.MaxValue, MigratedTransactionLabels = int.MaxValue };
|
||||
foreach (var prop in settings.GetType().GetProperties().Where(p => p.CanWrite && p.PropertyType == typeof(bool)))
|
||||
{
|
||||
prop.SetValue(settings, true);
|
||||
|
@ -46,9 +46,9 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
CustomCSSLink = blob.CustomCSSLink;
|
||||
EmbeddedCSS = blob.EmbeddedCSS;
|
||||
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
|
||||
FormResponse = string.IsNullOrEmpty(blob.FormResponse)
|
||||
FormResponse = blob.FormResponse is null
|
||||
? null
|
||||
: JObject.Parse(blob.FormResponse).ToObject<Dictionary<string, object>>();
|
||||
: blob.FormResponse.ToObject<Dictionary<string, object>>();
|
||||
}
|
||||
|
||||
[Display(Name = "Request customer data on checkout")]
|
||||
|
@ -98,7 +98,7 @@ namespace BTCPayServer.PaymentRequest
|
||||
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
|
||||
LastUpdated = DateTime.UtcNow,
|
||||
FormId = blob.FormId,
|
||||
FormSubmitted = !string.IsNullOrEmpty(blob.FormResponse),
|
||||
FormSubmitted = blob.FormResponse is not null,
|
||||
AnyPendingInvoice = pendingInvoice != null,
|
||||
PendingInvoiceHasPayments = pendingInvoice != null &&
|
||||
pendingInvoice.ExceptionStatus != InvoiceExceptionStatus.None,
|
||||
|
@ -14,7 +14,6 @@ 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>());
|
||||
}
|
||||
|
@ -9,11 +9,13 @@ 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;
|
||||
@ -39,19 +41,23 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
AppService appService,
|
||||
CurrencyNameTable currencies,
|
||||
StoreRepository storeRepository,
|
||||
UIInvoiceController invoiceController)
|
||||
UIInvoiceController invoiceController,
|
||||
FormComponentProviders formProviders)
|
||||
{
|
||||
_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)]
|
||||
@ -118,8 +124,6 @@ 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)
|
||||
@ -221,18 +225,21 @@ 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 (posFormId)
|
||||
switch (formConfig)
|
||||
{
|
||||
case null:
|
||||
case { } when string.IsNullOrEmpty(posFormId):
|
||||
case { } when !this.Request.HasFormContentType:
|
||||
break;
|
||||
|
||||
default:
|
||||
// POST case: Handle form submit
|
||||
if (!string.IsNullOrEmpty(formData) && formId == posFormId)
|
||||
var formData = Form.Parse(formConfig);
|
||||
formData.ApplyValuesFromForm(this.Request.Form);
|
||||
|
||||
if (FormProviders.Validate(formData, ModelState))
|
||||
{
|
||||
formResponse = JObject.Parse(formData);
|
||||
formResponse = JObject.FromObject(formData.GetValues());
|
||||
break;
|
||||
}
|
||||
|
||||
@ -247,9 +254,12 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
{
|
||||
AspController = "UIForms",
|
||||
AspAction = "ViewPublicForm",
|
||||
RouteParameters =
|
||||
{
|
||||
{ "formId", posFormId }
|
||||
},
|
||||
FormParameters =
|
||||
{
|
||||
{ "formId", posFormId },
|
||||
{ "redirectUrl", Request.GetCurrentUrl() + query }
|
||||
}
|
||||
});
|
||||
|
@ -7,6 +7,7 @@
|
||||
"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",
|
||||
@ -31,7 +32,7 @@
|
||||
"BTCPAY_CHEATMODE": "true",
|
||||
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"
|
||||
},
|
||||
"applicationUrl": "http://localhost:14142/"
|
||||
"applicationUrl": "http://0.0.0.0:14142/"
|
||||
},
|
||||
"Bitcoin-HTTPS": {
|
||||
"commandName": "Project",
|
||||
@ -42,7 +43,8 @@
|
||||
"BTCPAY_PORT": "14142",
|
||||
"BTCPAY_HttpsUseDefaultCertificate": "true",
|
||||
"BTCPAY_VERBOSE": "true",
|
||||
"BTCPAY_BTCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993",
|
||||
"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_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",
|
||||
@ -68,7 +70,7 @@
|
||||
"BTCPAY_CHEATMODE": "true",
|
||||
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"
|
||||
},
|
||||
"applicationUrl": "https://localhost:14142/"
|
||||
"applicationUrl": "https://0.0.0.0:14142/"
|
||||
},
|
||||
"Altcoins-HTTPS": {
|
||||
"commandName": "Project",
|
||||
@ -107,7 +109,7 @@
|
||||
"BTCPAY_CHEATMODE": "true",
|
||||
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"
|
||||
},
|
||||
"applicationUrl": "https://localhost:14142/"
|
||||
"applicationUrl": "https://0.0.0.0:14142/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -835,6 +835,17 @@ 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);
|
||||
|
@ -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.ToString() };
|
||||
new() { Text = DisplayName(opt), Value = opt == GenericFormOption.None ? null : opt.ToString() };
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ 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;
|
||||
@ -37,8 +38,9 @@ namespace BTCPayServer.Services
|
||||
Type = type;
|
||||
Ids = ids;
|
||||
}
|
||||
public GetWalletObjectsQuery(ObjectTypeId[]? typesIds)
|
||||
public GetWalletObjectsQuery(WalletId? walletId,ObjectTypeId[]? typesIds)
|
||||
{
|
||||
WalletId = walletId;
|
||||
TypesIds = typesIds;
|
||||
}
|
||||
|
||||
@ -50,6 +52,18 @@ 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
|
||||
@ -78,7 +92,7 @@ namespace BTCPayServer.Services
|
||||
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
|
||||
// If we are using postgres, the `transactionIds.Contains(w.ChildId)` result in a long query like `ANY(@txId1, @txId2, @txId3, @txId4)`
|
||||
// If we are using postgres, the `transactionIds.Contains(w.BId)` 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.
|
||||
@ -106,9 +120,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 \"ParentType\" AS \"Type2\", \"ParentId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"ChildType\"=wos.\"Type\" AND \"ChildId\"=wos.\"Id\" " +
|
||||
"SELECT \"AType\" AS \"Type2\", \"AId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"BType\"=wos.\"Type\" AND \"BId\"=wos.\"Id\" " +
|
||||
"UNION " +
|
||||
"SELECT \"ChildType\" AS \"Type2\", \"ChildId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"ParentType\"=wos.\"Type\" AND \"ParentId\"=wos.\"Id\"" +
|
||||
"SELECT \"BType\" AS \"Type2\", \"BId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"AType\"=wos.\"Type\" AND \"AId\"=wos.\"Id\"" +
|
||||
$" ) wol ON true " + includeNeighbourJoin;
|
||||
cmd.CommandText = query;
|
||||
if (queryObject.WalletId is not null)
|
||||
@ -177,21 +191,21 @@ namespace BTCPayServer.Services
|
||||
else
|
||||
{
|
||||
wosById.Add(id, wo);
|
||||
wo.ChildLinks = new List<WalletObjectLinkData>();
|
||||
wo.Bs = new List<WalletObjectLinkData>();
|
||||
}
|
||||
if (reader["Type2"] is not DBNull)
|
||||
{
|
||||
var l = new WalletObjectLinkData()
|
||||
{
|
||||
ChildType = (string)reader["Type2"],
|
||||
ChildId = (string)reader["Id2"],
|
||||
BType = (string)reader["Type2"],
|
||||
BId = (string)reader["Id2"],
|
||||
Data = reader["LinkData"] is DBNull ? null : (string)reader["LinkData"]
|
||||
};
|
||||
wo.ChildLinks.Add(l);
|
||||
l.Child = new WalletObjectData()
|
||||
wo.Bs.Add(l);
|
||||
l.B = new WalletObjectData()
|
||||
{
|
||||
Type = l.ChildType,
|
||||
Id = l.ChildId,
|
||||
Type = l.BType,
|
||||
Id = l.BId,
|
||||
Data = (!queryObject.IncludeNeighbours || reader["Data2"] is DBNull) ? null : (string)reader["Data2"]
|
||||
};
|
||||
}
|
||||
@ -215,8 +229,8 @@ namespace BTCPayServer.Services
|
||||
}
|
||||
if (queryObject.IncludeNeighbours)
|
||||
{
|
||||
q = q.Include(o => o.ChildLinks).ThenInclude(o => o.Child)
|
||||
.Include(o => o.ParentLinks).ThenInclude(o => o.Parent);
|
||||
q = q.Include(o => o.Bs).ThenInclude(o => o.B)
|
||||
.Include(o => o.As).ThenInclude(o => o.A);
|
||||
}
|
||||
q = q.AsNoTracking();
|
||||
|
||||
@ -230,9 +244,28 @@ 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((GetWalletObjectsQuery)(new(walletId, WalletObjectData.Types.Tx, transactionIds)));
|
||||
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 result = new Dictionary<string, WalletTransactionInfo>(wos.Count);
|
||||
foreach (var obj in wos.Values)
|
||||
{
|
||||
@ -299,10 +332,10 @@ namespace BTCPayServer.Services
|
||||
var l = new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = a.WalletId.ToString(),
|
||||
ParentType = a.Type,
|
||||
ParentId = a.Id,
|
||||
ChildType = b.Type,
|
||||
ChildId = b.Id,
|
||||
AType = a.Type,
|
||||
AId = a.Id,
|
||||
BType = b.Type,
|
||||
BId = b.Id,
|
||||
Data = data?.ToString(Formatting.None)
|
||||
};
|
||||
ctx.WalletObjectLinks.Add(l);
|
||||
@ -345,10 +378,10 @@ namespace BTCPayServer.Services
|
||||
var l = new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = a.WalletId.ToString(),
|
||||
ParentType = a.Type,
|
||||
ParentId = a.Id,
|
||||
ChildType = b.Type,
|
||||
ChildId = b.Id,
|
||||
AType = a.Type,
|
||||
AId = a.Id,
|
||||
BType = b.Type,
|
||||
BId = b.Id,
|
||||
Data = data?.ToString(Formatting.None)
|
||||
};
|
||||
var e = ctx.WalletObjectLinks.Add(l);
|
||||
@ -421,13 +454,20 @@ namespace BTCPayServer.Services
|
||||
}
|
||||
public Task AddWalletTransactionAttachment(WalletId walletId, uint256 txId, Attachment attachment)
|
||||
{
|
||||
return AddWalletTransactionAttachment(walletId, txId, new[] { attachment });
|
||||
return AddWalletTransactionAttachment(walletId, txId.ToString(), new []{attachment}, WalletObjectData.Types.Tx);
|
||||
}
|
||||
public async Task AddWalletTransactionAttachment(WalletId walletId, uint256 txId, IEnumerable<Attachment> attachments)
|
||||
|
||||
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)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(walletId);
|
||||
ArgumentNullException.ThrowIfNull(txId);
|
||||
var txObjId = new WalletObjectId(walletId, WalletObjectData.Types.Tx, txId.ToString());
|
||||
var txObjId = new WalletObjectId(walletId, type, txId.ToString());
|
||||
await EnsureWalletObject(txObjId);
|
||||
foreach (var attachment in attachments)
|
||||
{
|
||||
@ -453,10 +493,10 @@ namespace BTCPayServer.Services
|
||||
ctx.WalletObjectLinks.Remove(new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = a.WalletId.ToString(),
|
||||
ParentId = a.Id,
|
||||
ParentType = a.Type,
|
||||
ChildId = b.Id,
|
||||
ChildType = b.Type
|
||||
AId = a.Id,
|
||||
AType = a.Type,
|
||||
BId = b.Id,
|
||||
BType = b.Type
|
||||
});
|
||||
try
|
||||
{
|
||||
|
@ -4,6 +4,7 @@ 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;
|
||||
@ -22,19 +23,21 @@ 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)
|
||||
StoreRepository storeRepository, WalletRepository walletRepository )
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
_explorerClientProvider = explorerClientProvider;
|
||||
_btcPayWalletProvider = btcPayWalletProvider;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_storeRepository = storeRepository;
|
||||
_walletRepository = walletRepository;
|
||||
}
|
||||
|
||||
public async Task<string> UnReserveAddress(WalletId walletId)
|
||||
@ -73,6 +76,8 @@ 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;
|
||||
}
|
||||
|
@ -40,8 +40,11 @@ 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);
|
||||
return baseResult.Replace(new DirectoryInfo(_datadirs.Value.StorageDir).FullName, url.AbsoluteUri,
|
||||
var r = 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,
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.CustomCSSLink))
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true"/>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||
{
|
||||
|
@ -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 />
|
||||
<small class="d-inline-block form-text text-muted">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</small>
|
||||
<div class="form-text">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</div>
|
||||
<span asp-validation-for="TargetCurrency" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -47,9 +47,7 @@
|
||||
<div class="form-group">
|
||||
<label asp-for="Settings.Login" class="form-label"></label>
|
||||
<input asp-for="Settings.Login" class="form-control"/>
|
||||
<small class="form-text text-muted">
|
||||
For many email providers (like Gmail) your login is your email address.
|
||||
</small>
|
||||
<div class="form-text">For many email providers (like Gmail) your login is your email address.</div>
|
||||
<span asp-validation-for="Settings.Login" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -1,27 +1,19 @@
|
||||
@using BTCPayServer.Abstractions.Form
|
||||
@using BTCPayServer.Abstractions.Form
|
||||
@using BTCPayServer.Forms
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using Newtonsoft.Json.Linq
|
||||
@inject FormComponentProvider FormComponentProvider
|
||||
@inject FormComponentProviders FormComponentProviders
|
||||
@model BTCPayServer.Abstractions.Form.Field
|
||||
@{
|
||||
if (Model is not Fieldset fieldset)
|
||||
{
|
||||
fieldset = JObject.FromObject(Model).ToObject<Fieldset>();
|
||||
}
|
||||
}
|
||||
@if (!fieldset.Hidden)
|
||||
@if (!Model.Hidden)
|
||||
{
|
||||
<fieldset>
|
||||
<legend class="h3 mt-4 mb-3">@fieldset.Label</legend>
|
||||
@foreach (var field in fieldset.Fields)
|
||||
<legend class="h3 mt-4 mb-3">@Model.Label</legend>
|
||||
@foreach (var field in Model.Fields)
|
||||
{
|
||||
var partial = FormComponentProvider.CanHandle(field);
|
||||
if (string.IsNullOrEmpty(partial))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
<partial name="@partial" for="@field"></partial>
|
||||
if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial))
|
||||
{
|
||||
<partial name="@partial.View" for="@field"></partial>
|
||||
}
|
||||
}
|
||||
</fieldset>
|
||||
}
|
||||
|
@ -1,32 +1,33 @@
|
||||
@using BTCPayServer.Abstractions.Form
|
||||
@using BTCPayServer.Abstractions.Form
|
||||
@using Newtonsoft.Json.Linq
|
||||
@model BTCPayServer.Abstractions.Form.Field
|
||||
@{
|
||||
if (Model is not HtmlInputField field)
|
||||
{
|
||||
field = JObject.FromObject(Model).ToObject<HtmlInputField>();
|
||||
}
|
||||
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;
|
||||
|
||||
}
|
||||
<div class="form-group">
|
||||
@if (field.Required)
|
||||
@if (Model.Required)
|
||||
{
|
||||
<label class="form-label" for="@field.Name" data-required>
|
||||
@field.Label
|
||||
<label class="form-label" for="@Model.Name" data-required>
|
||||
@Model.Label
|
||||
</label>
|
||||
}
|
||||
else
|
||||
{
|
||||
<label class="form-label" for="@field.Name">
|
||||
@field.Label
|
||||
<label class="form-label" for="@Model.Name">
|
||||
@Model.Label
|
||||
</label>
|
||||
}
|
||||
|
||||
<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))
|
||||
<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))
|
||||
{
|
||||
<small id="@("HelpText" + field.Name)" class="form-text text-muted">
|
||||
@field.HelpText
|
||||
</small>
|
||||
<div id="@("HelpText" + Model.Name)" class="form-text">@Model.HelpText</div>
|
||||
}
|
||||
|
||||
|
||||
|
@ -305,9 +305,7 @@
|
||||
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>
|
||||
<p class="form-text text-muted">
|
||||
The URL to post purchase data.
|
||||
</p>
|
||||
<div class="form-text">The URL to post purchase data.</div>
|
||||
</div>
|
||||
<div class="form-group" v-if="!srvModel.appIdEndpoint">
|
||||
<label class="form-label" for="email-notifications">Email Notifications</label>
|
||||
@ -316,9 +314,7 @@
|
||||
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>
|
||||
<p class="form-text text-muted">
|
||||
Receive email notification updates.
|
||||
</p>
|
||||
<div class="form-text">Receive email notification updates.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="browser-redirect">Browser Redirect</label>
|
||||
@ -326,9 +322,7 @@
|
||||
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>
|
||||
<p class="form-text text-muted">
|
||||
Where to redirect the customer after payment is complete.
|
||||
</p>
|
||||
<div class="form-text">Where to redirect the customer after payment is complete</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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">
|
||||
<img src="@Model.CustomLogoLink" height="40" asp-append-version="true">
|
||||
} else {
|
||||
<h1 class="mb-0">@Model.Title</h1>
|
||||
}
|
||||
|
@ -36,7 +36,6 @@
|
||||
{
|
||||
<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>
|
||||
}
|
||||
|
@ -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"/>
|
||||
<img src="@Model.CustomLogoLink" height="40" asp-append-version="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -25,7 +25,12 @@
|
||||
var jObject = JObject.Parse(await reader.ReadToEndAsync());
|
||||
jObject["short_name"] = title;
|
||||
jObject["name"] = $"BTCPay Server: {title}";
|
||||
return $"data:application/manifest+json, {jObject.ToString(Formatting.None)}";
|
||||
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)}";
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,11 +50,10 @@
|
||||
<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" />
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true" />
|
||||
}
|
||||
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" asp-append-version="true" />
|
||||
|
||||
|
@ -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 />
|
||||
<small class="d-inline-block form-text text-muted">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</small>
|
||||
<div class="form-text">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</div>
|
||||
<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>
|
||||
<p class="form-text text-muted">Choose the point of sale style for your customers.</p>
|
||||
<div class="form-text">Choose the point of sale style for your customers.</div>
|
||||
</div>
|
||||
<div class="form-group" id="button-price-text">
|
||||
<label asp-for="ButtonText" class="form-label" data-required></label>
|
||||
|
@ -93,16 +93,12 @@
|
||||
<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" />
|
||||
<p class="form-text text-muted">
|
||||
Leave blank to not use this feature.
|
||||
</p>
|
||||
<div class="form-text">Leave blank to not use this feature.</div>
|
||||
</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" />
|
||||
<p class="form-text text-muted">
|
||||
Leave blank to generate ID from title.
|
||||
</p>
|
||||
<div class="form-text">Leave blank to generate ID from title.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Buy Button Text</label>
|
||||
|
@ -1,14 +1,12 @@
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Forms
|
||||
@model BTCPayServer.Abstractions.Form.Form
|
||||
@inject FormComponentProvider FormComponentProvider
|
||||
@inject FormComponentProviders FormComponentProviders
|
||||
|
||||
@foreach (var field in Model.Fields)
|
||||
{
|
||||
var partial = FormComponentProvider.CanHandle(field);
|
||||
if (string.IsNullOrEmpty(partial))
|
||||
if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial))
|
||||
{
|
||||
continue;
|
||||
<partial name="@partial.View" for="@field"></partial>
|
||||
}
|
||||
<partial name="@partial" for="@field"></partial>
|
||||
}
|
||||
|
@ -364,7 +364,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
-->
|
||||
<small class="form-text text-muted">Final results may vary due to trading fees and slippage.</small>
|
||||
<div class="form-text">Final results may vary due to trading fees and slippage.</div>
|
||||
</div>
|
||||
<div v-if="trade.results !== null">
|
||||
<p class="alert alert-success">Successfully traded {{ trade.results.fromAsset}} into {{ trade.results.toAsset}}.</p>
|
||||
|
@ -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" value="Submit"/>
|
||||
<input type="submit" class="btn btn-primary" name="command" value="Submit"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -45,7 +45,7 @@
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.CustomCSSLink))
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" />
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true"/>
|
||||
}
|
||||
|
||||
@if (Model.IsModal)
|
||||
|
@ -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" :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>
|
||||
<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>
|
||||
</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" :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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -135,9 +135,7 @@
|
||||
<label asp-for="NotificationEmail" class="form-label"></label>
|
||||
<input asp-for="NotificationEmail" class="form-control" />
|
||||
<span asp-validation-for="NotificationEmail" class="text-danger"></span>
|
||||
<p id="InvoiceEmailHelpBlock" class="form-text text-muted">
|
||||
Receive updates for this invoice.
|
||||
</p>
|
||||
<div id="InvoiceEmailHelpBlock" class="form-text">Receive updates for this invoice.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -186,15 +186,15 @@
|
||||
<div class="dropdown-menu" aria-labelledby="markStatusDropdownMenuButton">
|
||||
@if (Model.CanMarkInvalid)
|
||||
{
|
||||
<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>
|
||||
<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>
|
||||
}
|
||||
@if (Model.CanMarkSettled)
|
||||
{
|
||||
<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>
|
||||
<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>
|
||||
}
|
||||
</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 class="thead-inverse">
|
||||
<thead>
|
||||
<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 class="thead-inverse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Pull Payment</th>
|
||||
<th>Amount</th>
|
||||
@ -526,9 +526,9 @@
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
<h3 class="mb-0">Events</h3>
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-inverse">
|
||||
<h3 class="mb-0 mt-5">Events</h3>
|
||||
<table class="table table-hover mt-3 mb-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Message</th>
|
||||
|
@ -41,7 +41,7 @@
|
||||
pavpill.replaceWith(statusHtml);
|
||||
})
|
||||
.fail(function (data) {
|
||||
pavpill.html(originalHtml.replace("dropdown-menu pull-right show", "dropdown-menu pull-right"));
|
||||
pavpill.html(originalHtml.replace("dropdown-menu show", "dropdown-menu"));
|
||||
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 pull-right">
|
||||
<div class="dropdown-menu">
|
||||
@if (invoice.CanMarkInvalid)
|
||||
{
|
||||
<button class="dropdown-item cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="invalid">
|
||||
<button type="button" class="dropdown-item lh-base changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="invalid">
|
||||
Mark as invalid
|
||||
</button>
|
||||
}
|
||||
@if (invoice.CanMarkSettled)
|
||||
{
|
||||
<button class="dropdown-item cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="settled">
|
||||
<button type="button" class="dropdown-item lh-base changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="settled">
|
||||
Mark as settled
|
||||
</button>
|
||||
}
|
||||
|
@ -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 text-muted">The crypto currency price, at the rate the invoice got paid.</div>
|
||||
<div class="form-text">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 text-muted">The crypto currency price, at the current rate.</div>
|
||||
<div class="form-text">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 text-muted">The invoice currency, at the rate when the refund will be sent.</div>
|
||||
<div class="form-text">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 text-muted">The specified amount with the specified currency, at the rate when the refund will be sent.</div>
|
||||
<div class="form-text">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">
|
||||
|
@ -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 text-muted">@Model.PermissionValues[i].Description</div>
|
||||
<div class="form-text">@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
Reference in New Issue
Block a user