mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Add Lightning payout support (#2517)
* Add Lightning payout support * Adjust Greenfield API to allow other payment types for Payouts * Pull payment view: Improve payment method select * Pull payments view: Update JS * Pull payments view: Table improvements * Pull payment form: Remove duplicate name field * Cleanup Lightning branch after rebasing * Update swagger documnetation for Lightning support * Remove required requirement for amount in pull payments * Adapt Refund endpoint to support multiple playment methods * Support LNURL Pay for Pull Payments * Revert "Remove required requirement for amount in pull payments" This reverts commit 96cb78939d43b7be61ee2d257800ccd1cce45c4c. * Support Lightning address payout claims * Fix lightning claim handling and provide better error messages * Fix tests Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
@@ -16,6 +16,7 @@ using BTCPayServer.Views.Wallets;
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using BTCPayServer.BIP78.Sender;
|
using BTCPayServer.BIP78.Sender;
|
||||||
|
using Microsoft.EntityFrameworkCore.Internal;
|
||||||
using OpenQA.Selenium;
|
using OpenQA.Selenium;
|
||||||
using OpenQA.Selenium.Chrome;
|
using OpenQA.Selenium.Chrome;
|
||||||
using OpenQA.Selenium.Support.Extensions;
|
using OpenQA.Selenium.Support.Extensions;
|
||||||
@@ -95,8 +96,12 @@ namespace BTCPayServer.Tests
|
|||||||
public Uri ServerUri;
|
public Uri ServerUri;
|
||||||
internal IWebElement FindAlertMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
|
internal IWebElement FindAlertMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
|
||||||
{
|
{
|
||||||
var className = $"alert-{StatusMessageModel.ToString(severity)}";
|
return FindAlertMessage(new[] {severity});
|
||||||
var el = Driver.FindElement(By.ClassName(className)) ?? Driver.WaitForElement(By.ClassName(className));
|
}
|
||||||
|
internal IWebElement FindAlertMessage(params StatusMessageModel.StatusSeverity[] severity)
|
||||||
|
{
|
||||||
|
var className = string.Join(", ", severity.Select(statusSeverity => $".alert-{StatusMessageModel.ToString(statusSeverity)}"));
|
||||||
|
var el = Driver.FindElement(By.CssSelector(className)) ?? Driver.WaitForElement(By.CssSelector(className));
|
||||||
if (el is null)
|
if (el is null)
|
||||||
throw new NoSuchElementException($"Unable to find {className}");
|
throw new NoSuchElementException($"Unable to find {className}");
|
||||||
return el;
|
return el;
|
||||||
|
|||||||
@@ -4,10 +4,15 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Models;
|
using BTCPayServer.Abstractions.Models;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
using BTCPayServer.Lightning.Charge;
|
||||||
|
using BTCPayServer.Lightning.LND;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
using BTCPayServer.Tests.Logging;
|
using BTCPayServer.Tests.Logging;
|
||||||
@@ -27,6 +32,7 @@ using OpenQA.Selenium.Support.UI;
|
|||||||
using Renci.SshNet.Security.Cryptography;
|
using Renci.SshNet.Security.Cryptography;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
using CreateInvoiceRequest = BTCPayServer.Lightning.Charge.CreateInvoiceRequest;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
@@ -977,9 +983,11 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Selenium", "Selenium")]
|
[Trait("Selenium", "Selenium")]
|
||||||
|
[Trait("Lightning", "Lightning")]
|
||||||
public async Task CanUsePullPaymentsViaUI()
|
public async Task CanUsePullPaymentsViaUI()
|
||||||
{
|
{
|
||||||
using var s = SeleniumTester.Create();
|
using var s = SeleniumTester.Create();
|
||||||
|
s.Server.ActivateLightning();
|
||||||
await s.StartAsync();
|
await s.StartAsync();
|
||||||
s.RegisterNewUser(true);
|
s.RegisterNewUser(true);
|
||||||
s.CreateNewStore();
|
s.CreateNewStore();
|
||||||
@@ -1129,8 +1137,88 @@ namespace BTCPayServer.Tests
|
|||||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-mark-paid")).Click();
|
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-mark-paid")).Click();
|
||||||
s.FindAlertMessage();
|
s.FindAlertMessage();
|
||||||
|
|
||||||
s.Driver.FindElement(By.Id("InProgress-view")).Click();
|
s.Driver.FindElement(By.Id($"{PayoutState.InProgress}-view")).Click();
|
||||||
Assert.Contains(tx.ToString(), s.Driver.PageSource);
|
Assert.Contains(tx.ToString(), s.Driver.PageSource);
|
||||||
|
|
||||||
|
|
||||||
|
//lightning tests
|
||||||
|
newStore = s.CreateNewStore();
|
||||||
|
s.AddLightningNode("BTC");
|
||||||
|
//Currently an onchain wallet is required to use the Lightning payouts feature..
|
||||||
|
s.GenerateWallet("BTC", "", true, true);
|
||||||
|
newWalletId = new WalletId(newStore.storeId, "BTC");
|
||||||
|
s.GoToWallet(newWalletId, WalletsNavPages.PullPayments);
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||||
|
|
||||||
|
var paymentMethodOptions = s.Driver.FindElements(By.CssSelector("#PaymentMethods option"));
|
||||||
|
Assert.Equal(2, paymentMethodOptions.Count);
|
||||||
|
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id("Name")).SendKeys("Lightning Test");
|
||||||
|
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||||
|
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.00001");
|
||||||
|
s.Driver.FindElement(By.Id("Currency")).Clear();
|
||||||
|
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
|
||||||
|
s.Driver.FindElement(By.Id("Create")).Click();
|
||||||
|
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||||
|
|
||||||
|
var bolt = (await s.Server.MerchantLnd.Client.CreateInvoice(
|
||||||
|
LightMoney.FromUnit(0.00001m, LightMoneyUnit.BTC),
|
||||||
|
$"LN payout test {DateTime.Now.Ticks}",
|
||||||
|
TimeSpan.FromHours(1), CancellationToken.None)).BOLT11;
|
||||||
|
s.Driver.FindElement(By.Id("Destination")).SendKeys(bolt);
|
||||||
|
s.Driver.FindElement(By.Id("SelectedPaymentMethod")).Click();
|
||||||
|
s.Driver.FindElement(By.CssSelector($"#SelectedPaymentMethod option[value={new PaymentMethodId("BTC", PaymentTypes.LightningLike )}]")).Click();
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter);
|
||||||
|
//we do not allow short-life bolts.
|
||||||
|
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
|
||||||
|
|
||||||
|
bolt = (await s.Server.MerchantLnd.Client.CreateInvoice(
|
||||||
|
LightMoney.FromUnit(0.00001m, LightMoneyUnit.BTC),
|
||||||
|
$"LN payout test {DateTime.Now.Ticks}",
|
||||||
|
TimeSpan.FromDays(31), CancellationToken.None)).BOLT11;
|
||||||
|
s.Driver.FindElement(By.Id("Destination")).Clear();
|
||||||
|
s.Driver.FindElement(By.Id("Destination")).SendKeys(bolt);
|
||||||
|
s.Driver.FindElement(By.Id("SelectedPaymentMethod")).Click();
|
||||||
|
s.Driver.FindElement(By.CssSelector($"#SelectedPaymentMethod option[value={new PaymentMethodId("BTC", PaymentTypes.LightningLike )}]")).Click();
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter);
|
||||||
|
s.FindAlertMessage();
|
||||||
|
|
||||||
|
Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource);
|
||||||
|
s.GoToWallet(newWalletId, WalletsNavPages.Payouts);
|
||||||
|
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike )}-view")).Click();
|
||||||
|
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-view")).Click();
|
||||||
|
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
|
||||||
|
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
|
||||||
|
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
|
||||||
|
Assert.Contains(bolt, s.Driver.PageSource);
|
||||||
|
Assert.Contains("0.00001 BTC", s.Driver.PageSource);
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.CssSelector("#pay-invoices-form")).Submit();
|
||||||
|
//lightning config in tests is very unstable so we can just go ahead and handle it as both
|
||||||
|
s.FindAlertMessage(new []{StatusMessageModel.StatusSeverity.Error, StatusMessageModel.StatusSeverity.Success});
|
||||||
|
s.GoToWallet(newWalletId, WalletsNavPages.Payouts);
|
||||||
|
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike )}-view")).Click();
|
||||||
|
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id($"{PayoutState.Completed}-view")).Click();
|
||||||
|
if (!s.Driver.PageSource.Contains(bolt))
|
||||||
|
{
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-view")).Click();
|
||||||
|
Assert.Contains(bolt, s.Driver.PageSource);
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-selectAllCheckbox")).Click();
|
||||||
|
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-actions")).Click();
|
||||||
|
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-mark-paid")).Click();
|
||||||
|
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike )}-view")).Click();
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id($"{PayoutState.Completed}-view")).Click();
|
||||||
|
Assert.Contains(bolt, s.Driver.PageSource);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CanBrowseContent(SeleniumTester s)
|
private static void CanBrowseContent(SeleniumTester s)
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
<PackageReference Include="Fido2" Version="2.0.1" />
|
<PackageReference Include="Fido2" Version="2.0.1" />
|
||||||
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
|
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||||
|
<PackageReference Include="LNURL" Version="0.0.7" />
|
||||||
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.4.0" />
|
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.4.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="3.3.2">
|
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="3.3.2">
|
||||||
@@ -63,7 +64,7 @@
|
|||||||
<PackageReference Include="QRCoder" Version="1.4.1" />
|
<PackageReference Include="QRCoder" Version="1.4.1" />
|
||||||
<PackageReference Include="System.IO.Pipelines" Version="4.7.4" />
|
<PackageReference Include="System.IO.Pipelines" Version="4.7.4" />
|
||||||
<PackageReference Include="NBitpayClient" Version="1.0.0.39" />
|
<PackageReference Include="NBitpayClient" Version="1.0.0.39" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.1.0" />
|
<PackageReference Include="NicolasDorier.RateLimits" Version="1.1.0" />
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer;
|
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Security;
|
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -101,19 +99,21 @@ namespace BTCPayServer.Controllers.GreenField
|
|||||||
ModelState.AddModelError(nameof(request.Period), $"The period should be positive");
|
ModelState.AddModelError(nameof(request.Period), $"The period should be positive");
|
||||||
}
|
}
|
||||||
PaymentMethodId[] paymentMethods = null;
|
PaymentMethodId[] paymentMethods = null;
|
||||||
if (request.PaymentMethods is string[] paymentMethodsStr)
|
if (request.PaymentMethods is { } paymentMethodsStr)
|
||||||
{
|
{
|
||||||
paymentMethods = paymentMethodsStr.Select(p => new PaymentMethodId(p, PaymentTypes.BTCLike)).ToArray();
|
paymentMethods = paymentMethodsStr.Select(s =>
|
||||||
foreach (var p in paymentMethods)
|
|
||||||
{
|
{
|
||||||
var n = _networkProvider.GetNetwork<BTCPayNetwork>(p.CryptoCode);
|
PaymentMethodId.TryParse(s, out var pmi);
|
||||||
if (n is null)
|
return pmi;
|
||||||
ModelState.AddModelError(nameof(request.PaymentMethods), "Invalid payment method");
|
}).ToArray();
|
||||||
if (n.ReadonlyWallet)
|
var supported = _payoutHandlers.GetSupportedPaymentMethods().ToArray();
|
||||||
ModelState.AddModelError(nameof(request.PaymentMethods), "Invalid payment method (We do not support the crypto currency for refund)");
|
for (int i = 0; i < paymentMethods.Length; i++)
|
||||||
|
{
|
||||||
|
if (!supported.Contains(paymentMethods[i]))
|
||||||
|
{
|
||||||
|
request.AddModelError(paymentRequest => paymentRequest.PaymentMethods[i], "Invalid or unsupported payment method", this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (paymentMethods.Any(p => _networkProvider.GetNetwork<BTCPayNetwork>(p.CryptoCode) is null))
|
|
||||||
ModelState.AddModelError(nameof(request.PaymentMethods), "Invalid payment method");
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -245,14 +245,23 @@ namespace BTCPayServer.Controllers.GreenField
|
|||||||
if (pp is null)
|
if (pp is null)
|
||||||
return PullPaymentNotFound();
|
return PullPaymentNotFound();
|
||||||
var ppBlob = pp.GetBlob();
|
var ppBlob = pp.GetBlob();
|
||||||
IClaimDestination destination = await payoutHandler.ParseClaimDestination(paymentMethodId,request.Destination);
|
var destination = await payoutHandler.ParseClaimDestination(paymentMethodId,request.Destination, true);
|
||||||
if (destination is null)
|
if (destination.destination is null)
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(request.Destination), "The destination must be an address or a BIP21 URI");
|
ModelState.AddModelError(nameof(request.Destination), destination.error??"The destination is invalid for the payment specified");
|
||||||
return this.CreateValidationError(ModelState);
|
return this.CreateValidationError(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Amount is decimal v && (v < ppBlob.MinimumClaim || v == 0.0m))
|
if (request.Amount is null && destination.destination.Amount != null)
|
||||||
|
{
|
||||||
|
request.Amount = destination.destination.Amount;
|
||||||
|
}
|
||||||
|
else if (request.Amount != null && destination.destination.Amount != null && request.Amount != destination.destination.Amount)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(request.Amount), $"Amount is implied in destination ({destination.destination.Amount}) that does not match the payout amount provided {request.Amount})");
|
||||||
|
return this.CreateValidationError(ModelState);
|
||||||
|
}
|
||||||
|
if (request.Amount is { } v && (v < ppBlob.MinimumClaim || v == 0.0m))
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(request.Amount), $"Amount too small (should be at least {ppBlob.MinimumClaim})");
|
ModelState.AddModelError(nameof(request.Amount), $"Amount too small (should be at least {ppBlob.MinimumClaim})");
|
||||||
return this.CreateValidationError(ModelState);
|
return this.CreateValidationError(ModelState);
|
||||||
@@ -260,7 +269,7 @@ namespace BTCPayServer.Controllers.GreenField
|
|||||||
var cd = _currencyNameTable.GetCurrencyData(pp.GetBlob().Currency, false);
|
var cd = _currencyNameTable.GetCurrencyData(pp.GetBlob().Currency, false);
|
||||||
var result = await _pullPaymentService.Claim(new ClaimRequest()
|
var result = await _pullPaymentService.Claim(new ClaimRequest()
|
||||||
{
|
{
|
||||||
Destination = destination,
|
Destination = destination.destination,
|
||||||
PullPaymentId = pullPaymentId,
|
PullPaymentId = pullPaymentId,
|
||||||
Value = request.Amount,
|
Value = request.Amount,
|
||||||
PaymentMethodId = paymentMethodId
|
PaymentMethodId = paymentMethodId
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ namespace BTCPayServer.Controllers
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("invoices/{invoiceId}/refund")]
|
[Route("invoices/{invoiceId}/refund")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> Refund(string invoiceId, CancellationToken cancellationToken)
|
public async Task<IActionResult> Refund([FromServices]IEnumerable<IPayoutHandler> payoutHandlers, string invoiceId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using var ctx = _dbContextFactory.CreateContext();
|
using var ctx = _dbContextFactory.CreateContext();
|
||||||
ctx.ChangeTracker.QueryTrackingBehavior = Microsoft.EntityFrameworkCore.QueryTrackingBehavior.NoTracking;
|
ctx.ChangeTracker.QueryTrackingBehavior = Microsoft.EntityFrameworkCore.QueryTrackingBehavior.NoTracking;
|
||||||
@@ -189,22 +189,16 @@ namespace BTCPayServer.Controllers
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var paymentMethods = invoice.GetBlob(_NetworkProvider).GetPaymentMethods();
|
var paymentMethods = invoice.GetBlob(_NetworkProvider).GetPaymentMethods();
|
||||||
var options = paymentMethods
|
var pmis = paymentMethods.Select(method => method.GetId()).ToList();
|
||||||
.Select(o => o.GetId())
|
var options = payoutHandlers.GetSupportedPaymentMethods(pmis);
|
||||||
.Select(o => o.CryptoCode)
|
|
||||||
.Where(o => _NetworkProvider.GetNetwork<BTCPayNetwork>(o) is BTCPayNetwork n && !n.ReadonlyWallet)
|
|
||||||
.Distinct()
|
|
||||||
.OrderBy(o => o)
|
|
||||||
.Select(o => new PaymentMethodId(o, PaymentTypes.BTCLike))
|
|
||||||
.ToList();
|
|
||||||
var defaultRefund = invoice.Payments
|
var defaultRefund = invoice.Payments
|
||||||
.Select(p => p.GetBlob(_NetworkProvider))
|
.Select(p => p.GetBlob(_NetworkProvider))
|
||||||
.Select(p => p?.GetPaymentMethodId())
|
.Select(p => p?.GetPaymentMethodId())
|
||||||
.FirstOrDefault(p => p != null && p.PaymentType == BitcoinPaymentType.Instance);
|
.FirstOrDefault(p => p != null && options.Contains(p));
|
||||||
// TODO: What if no option?
|
// TODO: What if no option?
|
||||||
var refund = new RefundModel();
|
var refund = new RefundModel();
|
||||||
refund.Title = "Select a payment method";
|
refund.Title = "Select a payment method";
|
||||||
refund.AvailablePaymentMethods = new SelectList(options, nameof(PaymentMethodId.CryptoCode), nameof(PaymentMethodId.CryptoCode));
|
refund.AvailablePaymentMethods = new SelectList(options.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString())));
|
||||||
refund.SelectedPaymentMethod = defaultRefund?.ToString() ?? options.Select(o => o.CryptoCode).First();
|
refund.SelectedPaymentMethod = defaultRefund?.ToString() ?? options.Select(o => o.CryptoCode).First();
|
||||||
|
|
||||||
// Nothing to select, skip to next
|
// Nothing to select, skip to next
|
||||||
@@ -229,7 +223,7 @@ namespace BTCPayServer.Controllers
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
if (!CanRefund(invoice.GetInvoiceState()))
|
if (!CanRefund(invoice.GetInvoiceState()))
|
||||||
return NotFound();
|
return NotFound();
|
||||||
var paymentMethodId = new PaymentMethodId(model.SelectedPaymentMethod, PaymentTypes.BTCLike);
|
var paymentMethodId = PaymentMethodId.Parse(model.SelectedPaymentMethod);
|
||||||
var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.Currency, true);
|
var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.Currency, true);
|
||||||
var paymentMethodDivisibility = _CurrencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
|
var paymentMethodDivisibility = _CurrencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
|
||||||
RateRules rules;
|
RateRules rules;
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ namespace BTCPayServer.Controllers
|
|||||||
Currency = blob.Currency,
|
Currency = blob.Currency,
|
||||||
Status = entity.Entity.State,
|
Status = entity.Entity.State,
|
||||||
Destination = entity.Blob.Destination,
|
Destination = entity.Blob.Destination,
|
||||||
|
PaymentMethod = PaymentMethodId.Parse(entity.Entity.PaymentMethodId),
|
||||||
Link = entity.ProofBlob?.Link,
|
Link = entity.ProofBlob?.Link,
|
||||||
TransactionId = entity.ProofBlob?.Id
|
TransactionId = entity.ProofBlob?.Id
|
||||||
}).ToList()
|
}).ToList()
|
||||||
@@ -105,14 +106,31 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ppBlob = pp.GetBlob();
|
var ppBlob = pp.GetBlob();
|
||||||
var network = _networkProvider.GetNetwork<BTCPayNetwork>(ppBlob.SupportedPaymentMethods.Single().CryptoCode);
|
|
||||||
|
|
||||||
var paymentMethodId = ppBlob.SupportedPaymentMethods.Single();
|
var paymentMethodId = ppBlob.SupportedPaymentMethods.FirstOrDefault(id => vm.SelectedPaymentMethod == id.ToString());
|
||||||
var payoutHandler = _payoutHandlers.FirstOrDefault(handler => handler.CanHandle(paymentMethodId));
|
|
||||||
IClaimDestination destination = await payoutHandler?.ParseClaimDestination(paymentMethodId, vm.Destination);
|
var payoutHandler = paymentMethodId is null? null: _payoutHandlers.FirstOrDefault(handler => handler.CanHandle(paymentMethodId));
|
||||||
if (destination is null)
|
if (payoutHandler is null)
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(vm.Destination), $"Invalid destination");
|
ModelState.AddModelError(nameof(vm.SelectedPaymentMethod), $"Invalid destination with selected payment method");
|
||||||
|
return await ViewPullPayment(pullPaymentId);
|
||||||
|
}
|
||||||
|
var destination = await payoutHandler?.ParseClaimDestination(paymentMethodId, vm.Destination, true);
|
||||||
|
if (destination.destination is null)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.Destination), destination.error??"Invalid destination with selected payment method");
|
||||||
|
return await ViewPullPayment(pullPaymentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vm.ClaimedAmount == 0)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.ClaimedAmount),
|
||||||
|
$"Amount is required");
|
||||||
|
}
|
||||||
|
else if (vm.ClaimedAmount != 0 && destination.destination.Amount != null && vm.ClaimedAmount != destination.destination.Amount)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.ClaimedAmount),
|
||||||
|
$"Amount is implied in destination ({destination.destination.Amount}) that does not match the payout amount provided {vm.ClaimedAmount})");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
@@ -122,10 +140,10 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
var result = await _pullPaymentHostedService.Claim(new ClaimRequest()
|
var result = await _pullPaymentHostedService.Claim(new ClaimRequest()
|
||||||
{
|
{
|
||||||
Destination = destination,
|
Destination = destination.destination,
|
||||||
PullPaymentId = pullPaymentId,
|
PullPaymentId = pullPaymentId,
|
||||||
Value = vm.ClaimedAmount,
|
Value = vm.ClaimedAmount,
|
||||||
PaymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike)
|
PaymentMethodId = paymentMethodId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.Result != ClaimRequest.ClaimResult.Ok)
|
if (result.Result != ClaimRequest.ClaimResult.Ok)
|
||||||
@@ -150,12 +168,5 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
return RedirectToAction(nameof(ViewPullPayment), new { pullPaymentId = pullPaymentId });
|
return RedirectToAction(nameof(ViewPullPayment), new { pullPaymentId = pullPaymentId });
|
||||||
}
|
}
|
||||||
|
|
||||||
string GetTransactionLink(BTCPayNetworkBase network, string txId)
|
|
||||||
{
|
|
||||||
if (txId is null)
|
|
||||||
return string.Empty;
|
|
||||||
return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ using BTCPayServer.Payments;
|
|||||||
using BTCPayServer.Rating;
|
using BTCPayServer.Rating;
|
||||||
using BTCPayServer.Views;
|
using BTCPayServer.Views;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||||
@@ -29,15 +30,18 @@ namespace BTCPayServer.Controllers
|
|||||||
public IActionResult NewPullPayment([ModelBinder(typeof(WalletIdModelBinder))]
|
public IActionResult NewPullPayment([ModelBinder(typeof(WalletIdModelBinder))]
|
||||||
WalletId walletId)
|
WalletId walletId)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (GetDerivationSchemeSettings(walletId) == null)
|
if (GetDerivationSchemeSettings(walletId) == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
var storeMethods = CurrentStore.GetSupportedPaymentMethods(NetworkProvider).Select(method => method.PaymentId).ToList();
|
||||||
|
var paymentMethodOptions = _payoutHandlers.GetSupportedPaymentMethods(storeMethods);
|
||||||
return View(new NewPullPaymentModel
|
return View(new NewPullPaymentModel
|
||||||
{
|
{
|
||||||
Name = "",
|
Name = "",
|
||||||
Currency = "BTC",
|
Currency = "BTC",
|
||||||
CustomCSSLink = "",
|
CustomCSSLink = "",
|
||||||
EmbeddedCSS = "",
|
EmbeddedCSS = "",
|
||||||
|
PaymentMethodItems = paymentMethodOptions.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), true))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,8 +52,16 @@ namespace BTCPayServer.Controllers
|
|||||||
if (GetDerivationSchemeSettings(walletId) == null)
|
if (GetDerivationSchemeSettings(walletId) == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
|
var storeMethods = CurrentStore.GetSupportedPaymentMethods(NetworkProvider).Select(method => method.PaymentId).ToList();
|
||||||
|
var paymentMethodOptions = _payoutHandlers.GetSupportedPaymentMethods(storeMethods);
|
||||||
|
model.PaymentMethodItems =
|
||||||
|
paymentMethodOptions.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), true));
|
||||||
model.Name ??= string.Empty;
|
model.Name ??= string.Empty;
|
||||||
model.Currency = model.Currency.ToUpperInvariant().Trim();
|
model.Currency = model.Currency.ToUpperInvariant().Trim();
|
||||||
|
if (!model.PaymentMethods.Any())
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(model.PaymentMethods), "You need at least one payment method");
|
||||||
|
}
|
||||||
if (_currencyTable.GetCurrencyData(model.Currency, false) is null)
|
if (_currencyTable.GetCurrencyData(model.Currency, false) is null)
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(model.Currency), "Invalid currency");
|
ModelState.AddModelError(nameof(model.Currency), "Invalid currency");
|
||||||
@@ -62,10 +74,12 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(model.Name), "The name should be maximum 50 characters.");
|
ModelState.AddModelError(nameof(model.Name), "The name should be maximum 50 characters.");
|
||||||
}
|
}
|
||||||
var paymentMethodId = walletId.GetPaymentMethodId();
|
|
||||||
var n = this.NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
|
var selectedPaymentMethodIds = model.PaymentMethods.Select(PaymentMethodId.Parse).ToArray();
|
||||||
if (n is null || paymentMethodId.PaymentType != PaymentTypes.BTCLike || n.ReadonlyWallet)
|
if (!selectedPaymentMethodIds.All(id => selectedPaymentMethodIds.Contains(id)))
|
||||||
ModelState.AddModelError(nameof(model.Name), "Pull payments are not supported with this wallet");
|
{
|
||||||
|
ModelState.AddModelError(nameof(model.Name), "Not all payment methods are supported");
|
||||||
|
}
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
return View(model);
|
return View(model);
|
||||||
await _pullPaymentService.CreatePullPayment(new HostedServices.CreatePullPayment()
|
await _pullPaymentService.CreatePullPayment(new HostedServices.CreatePullPayment()
|
||||||
@@ -74,7 +88,7 @@ namespace BTCPayServer.Controllers
|
|||||||
Amount = model.Amount,
|
Amount = model.Amount,
|
||||||
Currency = model.Currency,
|
Currency = model.Currency,
|
||||||
StoreId = walletId.StoreId,
|
StoreId = walletId.StoreId,
|
||||||
PaymentMethodIds = new[] { paymentMethodId },
|
PaymentMethodIds = selectedPaymentMethodIds,
|
||||||
EmbeddedCSS = model.EmbeddedCSS,
|
EmbeddedCSS = model.EmbeddedCSS,
|
||||||
CustomCSSLink = model.CustomCSSLink
|
CustomCSSLink = model.CustomCSSLink
|
||||||
});
|
});
|
||||||
@@ -180,13 +194,14 @@ namespace BTCPayServer.Controllers
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
var storeId = walletId.StoreId;
|
var storeId = walletId.StoreId;
|
||||||
var paymentMethodId = new PaymentMethodId(walletId.CryptoCode, PaymentTypes.BTCLike);
|
var paymentMethodId = PaymentMethodId.Parse(vm.PaymentMethodId);
|
||||||
|
var handler = _payoutHandlers
|
||||||
|
.FirstOrDefault(handler => handler.CanHandle(paymentMethodId));
|
||||||
var commandState = Enum.Parse<PayoutState>(vm.Command.Split("-").First());
|
var commandState = Enum.Parse<PayoutState>(vm.Command.Split("-").First());
|
||||||
var payoutIds = vm.GetSelectedPayouts(commandState);
|
var payoutIds = vm.GetSelectedPayouts(commandState);
|
||||||
if (payoutIds.Length == 0)
|
if (payoutIds.Length == 0)
|
||||||
{
|
{
|
||||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
{
|
{
|
||||||
Message = "No payout selected",
|
Message = "No payout selected",
|
||||||
Severity = StatusMessageModel.StatusSeverity.Error
|
Severity = StatusMessageModel.StatusSeverity.Error
|
||||||
@@ -194,12 +209,11 @@ namespace BTCPayServer.Controllers
|
|||||||
return RedirectToAction(nameof(Payouts), new
|
return RedirectToAction(nameof(Payouts), new
|
||||||
{
|
{
|
||||||
walletId = walletId.ToString(),
|
walletId = walletId.ToString(),
|
||||||
pullPaymentId = vm.PullPaymentId
|
pullPaymentId = vm.PullPaymentId,
|
||||||
|
paymentMethodId = paymentMethodId.ToString()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
var command = vm.Command.Substring(vm.Command.IndexOf('-', StringComparison.InvariantCulture) + 1);
|
var command = vm.Command.Substring(vm.Command.IndexOf('-', StringComparison.InvariantCulture) + 1);
|
||||||
var handler = _payoutHandlers
|
|
||||||
.FirstOrDefault(handler => handler.CanHandle(paymentMethodId));
|
|
||||||
if (handler != null)
|
if (handler != null)
|
||||||
{
|
{
|
||||||
var result = await handler.DoSpecificAction(command, payoutIds, walletId.StoreId);
|
var result = await handler.DoSpecificAction(command, payoutIds, walletId.StoreId);
|
||||||
@@ -215,8 +229,9 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
await using var ctx = this._dbContextFactory.CreateContext();
|
await using var ctx = this._dbContextFactory.CreateContext();
|
||||||
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||||
var payouts = await GetPayoutsForPaymentMethod(walletId.GetPaymentMethodId(), ctx, payoutIds, storeId, cancellationToken);
|
var payouts = await GetPayoutsForPaymentMethod(paymentMethodId, ctx, payoutIds, storeId, cancellationToken);
|
||||||
|
|
||||||
|
var failed = false;
|
||||||
for (int i = 0; i < payouts.Count; i++)
|
for (int i = 0; i < payouts.Count; i++)
|
||||||
{
|
{
|
||||||
var payout = payouts[i];
|
var payout = payouts[i];
|
||||||
@@ -230,11 +245,8 @@ namespace BTCPayServer.Controllers
|
|||||||
Message = $"Rate unavailable: {rateResult.EvaluatedRule}",
|
Message = $"Rate unavailable: {rateResult.EvaluatedRule}",
|
||||||
Severity = StatusMessageModel.StatusSeverity.Error
|
Severity = StatusMessageModel.StatusSeverity.Error
|
||||||
});
|
});
|
||||||
return RedirectToAction(nameof(Payouts), new
|
failed = true;
|
||||||
{
|
break;
|
||||||
walletId = walletId.ToString(),
|
|
||||||
pullPaymentId = vm.PullPaymentId
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
var approveResult = await _pullPaymentService.Approve(new HostedServices.PullPaymentHostedService.PayoutApproval()
|
var approveResult = await _pullPaymentService.Approve(new HostedServices.PullPaymentHostedService.PayoutApproval()
|
||||||
{
|
{
|
||||||
@@ -242,26 +254,26 @@ namespace BTCPayServer.Controllers
|
|||||||
Revision = payout.GetBlob(_jsonSerializerSettings).Revision,
|
Revision = payout.GetBlob(_jsonSerializerSettings).Revision,
|
||||||
Rate = rateResult.BidAsk.Ask
|
Rate = rateResult.BidAsk.Ask
|
||||||
});
|
});
|
||||||
if (approveResult != HostedServices.PullPaymentHostedService.PayoutApproval.Result.Ok)
|
if (approveResult != PullPaymentHostedService.PayoutApproval.Result.Ok)
|
||||||
{
|
{
|
||||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
{
|
{
|
||||||
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult),
|
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult),
|
||||||
Severity = StatusMessageModel.StatusSeverity.Error
|
Severity = StatusMessageModel.StatusSeverity.Error
|
||||||
});
|
});
|
||||||
return RedirectToAction(nameof(Payouts), new
|
failed = true;
|
||||||
{
|
break;
|
||||||
walletId = walletId.ToString(),
|
|
||||||
pullPaymentId = vm.PullPaymentId
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (failed)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (command == "approve-pay")
|
if (command == "approve-pay")
|
||||||
{
|
{
|
||||||
goto case "pay";
|
goto case "pay";
|
||||||
}
|
}
|
||||||
|
|
||||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
{
|
{
|
||||||
Message = "Payouts approved", Severity = StatusMessageModel.StatusSeverity.Success
|
Message = "Payouts approved", Severity = StatusMessageModel.StatusSeverity.Success
|
||||||
@@ -271,47 +283,19 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
case "pay":
|
case "pay":
|
||||||
{
|
{
|
||||||
await using var ctx = this._dbContextFactory.CreateContext();
|
if (handler is { }) return await handler?.InitiatePayment(paymentMethodId, payoutIds);
|
||||||
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
|
||||||
var payouts = await GetPayoutsForPaymentMethod(walletId.GetPaymentMethodId(), ctx, payoutIds, storeId, cancellationToken);
|
|
||||||
|
|
||||||
var walletSend = (WalletSendModel)((ViewResult)(await this.WalletSend(walletId))).Model;
|
|
||||||
walletSend.Outputs.Clear();
|
|
||||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
|
||||||
List<string> bip21 = new List<string>();
|
|
||||||
|
|
||||||
foreach (var payout in payouts)
|
|
||||||
{
|
|
||||||
if (payout.Proof != null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var blob = payout.GetBlob(_jsonSerializerSettings);
|
|
||||||
bip21.Add(network.GenerateBIP21(payout.Destination, new Money(blob.CryptoAmount.Value, MoneyUnit.BTC)).ToString());
|
|
||||||
|
|
||||||
}
|
|
||||||
if(bip21.Any())
|
|
||||||
{
|
|
||||||
TempData.SetStatusMessageModel(null);
|
|
||||||
return RedirectToAction(nameof(WalletSend), new {walletId, bip21});
|
|
||||||
}
|
|
||||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
{
|
{
|
||||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
Message = "Paying via this payment method is not supported", Severity = StatusMessageModel.StatusSeverity.Error
|
||||||
Message = "There were no payouts eligible to pay from the selection. You may have selected payouts which have detected a transaction to the payout address with the payout amount that you need to accept or reject as the payout."
|
|
||||||
});
|
|
||||||
return RedirectToAction(nameof(Payouts), new
|
|
||||||
{
|
|
||||||
walletId = walletId.ToString(),
|
|
||||||
pullPaymentId = vm.PullPaymentId
|
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "mark-paid":
|
case "mark-paid":
|
||||||
{
|
{
|
||||||
await using var ctx = this._dbContextFactory.CreateContext();
|
await using var ctx = this._dbContextFactory.CreateContext();
|
||||||
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||||
var payouts = await GetPayoutsForPaymentMethod(walletId.GetPaymentMethodId(), ctx, payoutIds, storeId, cancellationToken);
|
var payouts = await GetPayoutsForPaymentMethod(paymentMethodId, ctx, payoutIds, storeId, cancellationToken);
|
||||||
for (int i = 0; i < payouts.Count; i++)
|
for (int i = 0; i < payouts.Count; i++)
|
||||||
{
|
{
|
||||||
var payout = payouts[i];
|
var payout = payouts[i];
|
||||||
@@ -332,7 +316,8 @@ namespace BTCPayServer.Controllers
|
|||||||
return RedirectToAction(nameof(Payouts), new
|
return RedirectToAction(nameof(Payouts), new
|
||||||
{
|
{
|
||||||
walletId = walletId.ToString(),
|
walletId = walletId.ToString(),
|
||||||
pullPaymentId = vm.PullPaymentId
|
pullPaymentId = vm.PullPaymentId,
|
||||||
|
paymentMethodId = paymentMethodId.ToString()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,15 +331,21 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
case "cancel":
|
case "cancel":
|
||||||
await _pullPaymentService.Cancel(
|
await _pullPaymentService.Cancel(
|
||||||
new HostedServices.PullPaymentHostedService.CancelRequest(payoutIds));
|
new PullPaymentHostedService.CancelRequest(payoutIds));
|
||||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
{
|
{
|
||||||
Message = "Payouts archived", Severity = StatusMessageModel.StatusSeverity.Success
|
Message = "Payouts archived", Severity = StatusMessageModel.StatusSeverity.Success
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return RedirectToAction(nameof(Payouts),
|
return RedirectToAction(nameof(Payouts),
|
||||||
new {walletId = walletId.ToString(), pullPaymentId = vm.PullPaymentId});
|
new
|
||||||
|
{
|
||||||
|
walletId = walletId.ToString(),
|
||||||
|
pullPaymentId = vm.PullPaymentId,
|
||||||
|
paymentMethodId = paymentMethodId.ToString()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<List<PayoutData>> GetPayoutsForPaymentMethod(PaymentMethodId paymentMethodId,
|
private static async Task<List<PayoutData>> GetPayoutsForPaymentMethod(PaymentMethodId paymentMethodId,
|
||||||
@@ -375,12 +366,12 @@ namespace BTCPayServer.Controllers
|
|||||||
[HttpGet("{walletId}/payouts")]
|
[HttpGet("{walletId}/payouts")]
|
||||||
public async Task<IActionResult> Payouts(
|
public async Task<IActionResult> Payouts(
|
||||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||||
WalletId walletId, string pullPaymentId, PayoutState payoutState,
|
WalletId walletId, string pullPaymentId, string paymentMethodId, PayoutState payoutState,
|
||||||
int skip = 0, int count = 50)
|
int skip = 0, int count = 50)
|
||||||
{
|
{
|
||||||
var vm = this.ParseListQuery(new PayoutsModel
|
var vm = this.ParseListQuery(new PayoutsModel
|
||||||
{
|
{
|
||||||
PaymentMethodId = new PaymentMethodId(walletId.CryptoCode, PaymentTypes.BTCLike),
|
PaymentMethodId = paymentMethodId?? new PaymentMethodId(walletId.CryptoCode, PaymentTypes.BTCLike).ToString(),
|
||||||
PullPaymentId = pullPaymentId,
|
PullPaymentId = pullPaymentId,
|
||||||
PayoutState = payoutState,
|
PayoutState = payoutState,
|
||||||
Skip = skip,
|
Skip = skip,
|
||||||
@@ -390,14 +381,14 @@ namespace BTCPayServer.Controllers
|
|||||||
await using var ctx = _dbContextFactory.CreateContext();
|
await using var ctx = _dbContextFactory.CreateContext();
|
||||||
var storeId = walletId.StoreId;
|
var storeId = walletId.StoreId;
|
||||||
var payoutRequest = ctx.Payouts.Where(p => p.PullPaymentData.StoreId == storeId && !p.PullPaymentData.Archived);
|
var payoutRequest = ctx.Payouts.Where(p => p.PullPaymentData.StoreId == storeId && !p.PullPaymentData.Archived);
|
||||||
if (vm.PullPaymentId != null)
|
if (pullPaymentId != null)
|
||||||
{
|
{
|
||||||
payoutRequest = payoutRequest.Where(p => p.PullPaymentDataId == vm.PullPaymentId);
|
payoutRequest = payoutRequest.Where(p => p.PullPaymentDataId == vm.PullPaymentId);
|
||||||
vm.PullPaymentName = (await ctx.PullPayments.FindAsync(pullPaymentId)).GetBlob().Name;
|
vm.PullPaymentName = (await ctx.PullPayments.FindAsync(pullPaymentId)).GetBlob().Name;
|
||||||
}
|
}
|
||||||
if (vm.PaymentMethodId != null)
|
if (vm.PaymentMethodId != null)
|
||||||
{
|
{
|
||||||
var pmiStr = vm.PaymentMethodId.ToString();
|
var pmiStr = vm.PaymentMethodId;
|
||||||
payoutRequest = payoutRequest.Where(p => p.PaymentMethodId == pmiStr);
|
payoutRequest = payoutRequest.Where(p => p.PaymentMethodId == pmiStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,5 +18,7 @@ namespace BTCPayServer.Data
|
|||||||
{
|
{
|
||||||
return _bitcoinAddress.ToString();
|
return _bitcoinAddress.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public decimal? Amount => null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -11,13 +10,11 @@ using BTCPayServer.Data;
|
|||||||
using BTCPayServer.Events;
|
using BTCPayServer.Events;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
using BTCPayServer.Models;
|
|
||||||
using BTCPayServer.Models.WalletViewModels;
|
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Notifications;
|
using BTCPayServer.Services.Notifications;
|
||||||
using BTCPayServer.Services.Notifications.Blobs;
|
using BTCPayServer.Services.Notifications.Blobs;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
@@ -39,8 +36,11 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||||||
private readonly NotificationSender _notificationSender;
|
private readonly NotificationSender _notificationSender;
|
||||||
|
|
||||||
public BitcoinLikePayoutHandler(BTCPayNetworkProvider btcPayNetworkProvider,
|
public BitcoinLikePayoutHandler(BTCPayNetworkProvider btcPayNetworkProvider,
|
||||||
ExplorerClientProvider explorerClientProvider, BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
|
ExplorerClientProvider explorerClientProvider,
|
||||||
ApplicationDbContextFactory dbContextFactory, EventAggregator eventAggregator, NotificationSender notificationSender)
|
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
|
||||||
|
ApplicationDbContextFactory dbContextFactory,
|
||||||
|
EventAggregator eventAggregator,
|
||||||
|
NotificationSender notificationSender)
|
||||||
{
|
{
|
||||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||||
_explorerClientProvider = explorerClientProvider;
|
_explorerClientProvider = explorerClientProvider;
|
||||||
@@ -64,7 +64,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||||||
await explorerClient.TrackAsync(TrackedSource.Create(bitcoinLikeClaimDestination.Address));
|
await explorerClient.TrackAsync(TrackedSource.Create(bitcoinLikeClaimDestination.Address));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IClaimDestination> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination)
|
public Task<(IClaimDestination destination, string error)> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination, bool validate)
|
||||||
{
|
{
|
||||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
|
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
|
||||||
destination = destination.Trim();
|
destination = destination.Trim();
|
||||||
@@ -76,11 +76,12 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||||||
// return Task.FromResult<IClaimDestination>(new UriClaimDestination(new BitcoinUrlBuilder(destination, network.NBitcoinNetwork)));
|
// return Task.FromResult<IClaimDestination>(new UriClaimDestination(new BitcoinUrlBuilder(destination, network.NBitcoinNetwork)));
|
||||||
//}
|
//}
|
||||||
|
|
||||||
return Task.FromResult<IClaimDestination>(new AddressClaimDestination(BitcoinAddress.Create(destination, network.NBitcoinNetwork)));
|
return Task.FromResult<(IClaimDestination, string)>((new AddressClaimDestination(BitcoinAddress.Create(destination, network.NBitcoinNetwork)), null));
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return Task.FromResult<IClaimDestination>(null);
|
return Task.FromResult<(IClaimDestination, string)>(
|
||||||
|
(null, "A valid address was not provided"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +176,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||||||
valueTuple.data.State = PayoutState.InProgress;
|
valueTuple.data.State = PayoutState.InProgress;
|
||||||
SetProofBlob(valueTuple.data, valueTuple.Item2);
|
SetProofBlob(valueTuple.data, valueTuple.Item2);
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,8 +202,10 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||||||
valueTuple.Item2.TransactionId = null;
|
valueTuple.Item2.TransactionId = null;
|
||||||
SetProofBlob(valueTuple.data, valueTuple.Item2);
|
SetProofBlob(valueTuple.data, valueTuple.Item2);
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new StatusMessageModel()
|
return new StatusMessageModel()
|
||||||
{
|
{
|
||||||
Message = "Payout payments have been unmarked",
|
Message = "Payout payments have been unmarked",
|
||||||
@@ -209,11 +213,50 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return new StatusMessageModel()
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<PaymentMethodId> GetSupportedPaymentMethods()
|
||||||
{
|
{
|
||||||
Message = "Unknown action",
|
return _btcPayNetworkProvider.GetAll().OfType<BTCPayNetwork>()
|
||||||
Severity = StatusMessageModel.StatusSeverity.Error
|
.Where(network => network.ReadonlyWallet is false)
|
||||||
};;
|
.Select(network => new PaymentMethodId(network.CryptoCode, BitcoinPaymentType.Instance));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> InitiatePayment(PaymentMethodId paymentMethodId ,string[] payoutIds)
|
||||||
|
{
|
||||||
|
await using var ctx = this._dbContextFactory.CreateContext();
|
||||||
|
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||||
|
var pmi = paymentMethodId.ToString();
|
||||||
|
|
||||||
|
var payouts = await ctx.Payouts.Include(data => data.PullPaymentData)
|
||||||
|
.Where(data => payoutIds.Contains(data.Id)
|
||||||
|
&& pmi == data.PaymentMethodId
|
||||||
|
&& data.State == PayoutState.AwaitingPayment)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var pullPaymentIds = payouts.Select(data => data.PullPaymentDataId).Distinct().ToArray();
|
||||||
|
var storeId = payouts.First().PullPaymentData.StoreId;
|
||||||
|
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
|
||||||
|
List<string> bip21 = new List<string>();
|
||||||
|
foreach (var payout in payouts)
|
||||||
|
{
|
||||||
|
if (payout.Proof != null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var blob = payout.GetBlob(_jsonSerializerSettings);
|
||||||
|
if (payout.GetPaymentMethodId() != paymentMethodId)
|
||||||
|
continue;
|
||||||
|
bip21.Add(network.GenerateBIP21(payout.Destination, new Money(blob.CryptoAmount.Value, MoneyUnit.BTC)).ToString());
|
||||||
|
}
|
||||||
|
if(bip21.Any())
|
||||||
|
return new RedirectToActionResult("WalletSend", "Wallets", new {walletId = new WalletId(storeId, paymentMethodId.CryptoCode).ToString(), bip21});
|
||||||
|
return new RedirectToActionResult("Payouts", "Wallets", new
|
||||||
|
{
|
||||||
|
walletId = new WalletId(storeId, paymentMethodId.CryptoCode).ToString(),
|
||||||
|
pullPaymentId = pullPaymentIds.Length == 1? pullPaymentIds.First(): null
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdatePayoutsInProgress()
|
private async Task UpdatePayoutsInProgress()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.Payment;
|
using NBitcoin.Payment;
|
||||||
@@ -23,5 +24,7 @@ namespace BTCPayServer.Data
|
|||||||
{
|
{
|
||||||
return _bitcoinUrl.ToString();
|
return _bitcoinUrl.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public decimal? Amount => _bitcoinUrl.Amount?.ToDecimal(MoneyUnit.BTC);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using System;
|
#nullable enable
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
{
|
{
|
||||||
public interface IClaimDestination
|
public interface IClaimDestination
|
||||||
{
|
{
|
||||||
|
decimal? Amount { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ using BTCPayServer.Client.Models;
|
|||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
public interface IPayoutHandler
|
public interface IPayoutHandler
|
||||||
{
|
{
|
||||||
public bool CanHandle(PaymentMethodId paymentMethod);
|
public bool CanHandle(PaymentMethodId paymentMethod);
|
||||||
public Task TrackClaim(PaymentMethodId paymentMethodId, IClaimDestination claimDestination);
|
public Task TrackClaim(PaymentMethodId paymentMethodId, IClaimDestination claimDestination);
|
||||||
//Allows payout handler to parse payout destinations on its own
|
//Allows payout handler to parse payout destinations on its own
|
||||||
public Task<IClaimDestination> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination);
|
public Task<(IClaimDestination destination, string error)> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination, bool validate);
|
||||||
public IPayoutProof ParseProof(PayoutData payout);
|
public IPayoutProof ParseProof(PayoutData payout);
|
||||||
//Allows you to subscribe the main pull payment hosted service to events and prepare the handler
|
//Allows you to subscribe the main pull payment hosted service to events and prepare the handler
|
||||||
void StartBackgroundCheck(Action<Type[]> subscribe);
|
void StartBackgroundCheck(Action<Type[]> subscribe);
|
||||||
@@ -21,4 +22,6 @@ public interface IPayoutHandler
|
|||||||
Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethod, IClaimDestination claimDestination);
|
Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethod, IClaimDestination claimDestination);
|
||||||
Dictionary<PayoutState, List<(string Action, string Text)>> GetPayoutSpecificActions();
|
Dictionary<PayoutState, List<(string Action, string Text)>> GetPayoutSpecificActions();
|
||||||
Task<StatusMessageModel> DoSpecificAction(string action, string[] payoutIds, string storeId);
|
Task<StatusMessageModel> DoSpecificAction(string action, string[] payoutIds, string storeId);
|
||||||
|
IEnumerable<PaymentMethodId> GetSupportedPaymentMethods();
|
||||||
|
Task<IActionResult> InitiatePayment(PaymentMethodId paymentMethodId, string[] payoutIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Data.Payouts.LightningLike
|
||||||
|
{
|
||||||
|
public class BoltInvoiceClaimDestination : ILightningLikeLikeClaimDestination
|
||||||
|
{
|
||||||
|
private readonly string _bolt11;
|
||||||
|
private readonly decimal _amount;
|
||||||
|
|
||||||
|
public BoltInvoiceClaimDestination(string bolt11, Network network)
|
||||||
|
{
|
||||||
|
_bolt11 = bolt11 ?? throw new ArgumentNullException(nameof(bolt11));
|
||||||
|
_amount = BOLT11PaymentRequest.Parse(bolt11, network).MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BoltInvoiceClaimDestination(string bolt11, BOLT11PaymentRequest invoice)
|
||||||
|
{
|
||||||
|
_bolt11 = bolt11 ?? throw new ArgumentNullException(nameof(bolt11));
|
||||||
|
_amount = invoice?.MinimumAmount.ToDecimal(LightMoneyUnit.BTC) ?? throw new ArgumentNullException(nameof(invoice));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return _bolt11;
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal? Amount => _amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace BTCPayServer.Data.Payouts.LightningLike
|
||||||
|
{
|
||||||
|
public interface ILightningLikeLikeClaimDestination : IClaimDestination
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace BTCPayServer.Data.Payouts.LightningLike
|
||||||
|
{
|
||||||
|
public class LNURLPayClaimDestinaton: ILightningLikeLikeClaimDestination
|
||||||
|
{
|
||||||
|
public LNURLPayClaimDestinaton(string lnurl)
|
||||||
|
{
|
||||||
|
LNURL = lnurl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal? Amount { get; } = null;
|
||||||
|
public string LNURL { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return LNURL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Abstractions.Constants;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
using BTCPayServer.Configuration;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
using BTCPayServer.Services;
|
||||||
|
using LNURL;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Data.Payouts.LightningLike
|
||||||
|
{
|
||||||
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
|
[AutoValidateAntiforgeryToken]
|
||||||
|
public class LightningLikePayoutController : Controller
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
|
||||||
|
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||||
|
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||||
|
private readonly LightningClientFactoryService _lightningClientFactoryService;
|
||||||
|
private readonly IOptions<LightningNetworkOptions> _options;
|
||||||
|
|
||||||
|
public LightningLikePayoutController(ApplicationDbContextFactory applicationDbContextFactory,
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
|
||||||
|
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||||
|
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||||
|
LightningClientFactoryService lightningClientFactoryService,
|
||||||
|
IOptions<LightningNetworkOptions> options)
|
||||||
|
{
|
||||||
|
_applicationDbContextFactory = applicationDbContextFactory;
|
||||||
|
_userManager = userManager;
|
||||||
|
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
|
||||||
|
_payoutHandlers = payoutHandlers;
|
||||||
|
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||||
|
_lightningClientFactoryService = lightningClientFactoryService;
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<PayoutData>> GetPayouts(ApplicationDbContext dbContext, PaymentMethodId pmi,
|
||||||
|
string[] payoutIds)
|
||||||
|
{
|
||||||
|
var userId = _userManager.GetUserId(User);
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return new List<PayoutData>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var pmiStr = pmi.ToString();
|
||||||
|
|
||||||
|
var approvedStores = new Dictionary<string, bool>();
|
||||||
|
|
||||||
|
return (await dbContext.Payouts
|
||||||
|
.Include(data => data.PullPaymentData)
|
||||||
|
.ThenInclude(data => data.StoreData)
|
||||||
|
.ThenInclude(data => data.UserStores)
|
||||||
|
.Where(data =>
|
||||||
|
payoutIds.Contains(data.Id) &&
|
||||||
|
data.State == PayoutState.AwaitingPayment &&
|
||||||
|
data.PaymentMethodId == pmiStr)
|
||||||
|
.ToListAsync())
|
||||||
|
.Where(payout =>
|
||||||
|
{
|
||||||
|
if (approvedStores.TryGetValue(payout.PullPaymentData.StoreId, out var value)) return value;
|
||||||
|
value = payout.PullPaymentData.StoreData.UserStores
|
||||||
|
.Any(store => store.Role == StoreRoles.Owner && store.ApplicationUserId == userId);
|
||||||
|
approvedStores.Add(payout.PullPaymentData.StoreId, value);
|
||||||
|
return value;
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("pull-payments/payouts/lightning/{cryptoCode}")]
|
||||||
|
public async Task<IActionResult> ConfirmLightningPayout(string cryptoCode, string[] payoutIds)
|
||||||
|
{
|
||||||
|
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||||
|
|
||||||
|
await using var ctx = _applicationDbContextFactory.CreateContext();
|
||||||
|
var payouts = await GetPayouts(ctx, pmi, payoutIds);
|
||||||
|
|
||||||
|
var vm = payouts.Select(payoutData =>
|
||||||
|
{
|
||||||
|
var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings);
|
||||||
|
|
||||||
|
return new ConfirmVM()
|
||||||
|
{
|
||||||
|
Amount = blob.CryptoAmount.Value, Destination = blob.Destination, PayoutId = payoutData.Id
|
||||||
|
};
|
||||||
|
}).ToList();
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("pull-payments/payouts/lightning/{cryptoCode}")]
|
||||||
|
public async Task<IActionResult> ProcessLightningPayout(string cryptoCode, string[] payoutIds)
|
||||||
|
{
|
||||||
|
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||||
|
var payoutHandler = _payoutHandlers.FirstOrDefault(handler => handler.CanHandle(pmi));
|
||||||
|
|
||||||
|
await using var ctx = _applicationDbContextFactory.CreateContext();
|
||||||
|
|
||||||
|
var payouts = (await GetPayouts(ctx, pmi, payoutIds)).GroupBy(data => data.PullPaymentData.StoreId);
|
||||||
|
var results = new List<ResultVM>();
|
||||||
|
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(pmi.CryptoCode);
|
||||||
|
|
||||||
|
//we group per store and init the transfers by each
|
||||||
|
async Task TrypayBolt(ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData,
|
||||||
|
string destination)
|
||||||
|
{
|
||||||
|
var result = await lightningClient.Pay(destination);
|
||||||
|
if (result.Result == PayResult.Ok)
|
||||||
|
{
|
||||||
|
results.Add(new ResultVM()
|
||||||
|
{
|
||||||
|
PayoutId = payoutData.Id, Result = result.Result, Destination = payoutBlob.Destination
|
||||||
|
});
|
||||||
|
payoutData.State = PayoutState.Completed;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
results.Add(new ResultVM()
|
||||||
|
{
|
||||||
|
PayoutId = payoutData.Id, Result = result.Result, Destination = payoutBlob.Destination
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var payoutDatas in payouts)
|
||||||
|
{
|
||||||
|
var store = payoutDatas.First().PullPaymentData.StoreData;
|
||||||
|
var lightningSupportedPaymentMethod = store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
|
||||||
|
.OfType<LightningSupportedPaymentMethod>()
|
||||||
|
.FirstOrDefault(method => method.PaymentId == pmi);
|
||||||
|
var client =
|
||||||
|
lightningSupportedPaymentMethod.CreateLightningClient(network, _options.Value,
|
||||||
|
_lightningClientFactoryService);
|
||||||
|
foreach (var payoutData in payoutDatas)
|
||||||
|
{
|
||||||
|
var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings);
|
||||||
|
var claim = await payoutHandler.ParseClaimDestination(pmi, blob.Destination, false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (claim.destination)
|
||||||
|
{
|
||||||
|
case LNURLPayClaimDestinaton lnurlPayClaimDestinaton:
|
||||||
|
var endpoint = LNURL.LNURL.Parse(lnurlPayClaimDestinaton.LNURL, out var tag);
|
||||||
|
var lightningPayoutHandler = (LightningLikePayoutHandler)payoutHandler;
|
||||||
|
var httpClient = lightningPayoutHandler.CreateClient(endpoint);
|
||||||
|
var lnurlInfo =
|
||||||
|
(LNURLPayRequest)await LNURL.LNURL.FetchInformation(endpoint, "payRequest",
|
||||||
|
httpClient);
|
||||||
|
var lm = new LightMoney(blob.CryptoAmount.Value, LightMoneyUnit.BTC);
|
||||||
|
if (lm > lnurlInfo.MaxSendable || lm < lnurlInfo.MinSendable)
|
||||||
|
{
|
||||||
|
results.Add(new ResultVM()
|
||||||
|
{
|
||||||
|
PayoutId = payoutData.Id,
|
||||||
|
Result = PayResult.Error,
|
||||||
|
Destination = blob.Destination,
|
||||||
|
Message =
|
||||||
|
$"The LNURL provided would not generate an invoice of {lm.MilliSatoshi}msats"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var lnurlPayRequestCallbackResponse =
|
||||||
|
await lnurlInfo.SendRequest(lm, network.NBitcoinNetwork, httpClient);
|
||||||
|
|
||||||
|
|
||||||
|
await TrypayBolt(client, blob, payoutData, lnurlPayRequestCallbackResponse.Pr);
|
||||||
|
}
|
||||||
|
catch (LNUrlException e)
|
||||||
|
{
|
||||||
|
results.Add(new ResultVM()
|
||||||
|
{
|
||||||
|
PayoutId = payoutData.Id,
|
||||||
|
Result = PayResult.Error,
|
||||||
|
Destination = blob.Destination,
|
||||||
|
Message = e.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BoltInvoiceClaimDestination item1:
|
||||||
|
await TrypayBolt(client, blob, payoutData, payoutData.Destination);
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
results.Add(new ResultVM()
|
||||||
|
{
|
||||||
|
PayoutId = payoutData.Id,
|
||||||
|
Result = PayResult.Error,
|
||||||
|
Destination = blob.Destination,
|
||||||
|
Message = claim.error
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
results.Add(new ResultVM()
|
||||||
|
{
|
||||||
|
PayoutId = payoutData.Id, Result = PayResult.Error, Destination = blob.Destination
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
return View("LightningPayoutResult", results);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResultVM
|
||||||
|
{
|
||||||
|
public string PayoutId { get; set; }
|
||||||
|
public string Destination { get; set; }
|
||||||
|
public PayResult Result { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConfirmVM
|
||||||
|
{
|
||||||
|
public string PayoutId { get; set; }
|
||||||
|
public string Destination { get; set; }
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Abstractions.Models;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Validation;
|
||||||
|
using LNURL;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Data.Payouts.LightningLike
|
||||||
|
{
|
||||||
|
public class LightningLikePayoutHandler : IPayoutHandler
|
||||||
|
{
|
||||||
|
public const string LightningLikePayoutHandlerOnionNamedClient =
|
||||||
|
nameof(LightningLikePayoutHandlerOnionNamedClient);
|
||||||
|
|
||||||
|
public const string LightningLikePayoutHandlerClearnetNamedClient =
|
||||||
|
nameof(LightningLikePayoutHandlerClearnetNamedClient);
|
||||||
|
|
||||||
|
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
|
||||||
|
public LightningLikePayoutHandler(BTCPayNetworkProvider btcPayNetworkProvider,
|
||||||
|
IHttpClientFactory httpClientFactory)
|
||||||
|
{
|
||||||
|
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanHandle(PaymentMethodId paymentMethod)
|
||||||
|
{
|
||||||
|
return paymentMethod.PaymentType == LightningPaymentType.Instance &&
|
||||||
|
_btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethod.CryptoCode)?.SupportLightning is true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task TrackClaim(PaymentMethodId paymentMethodId, IClaimDestination claimDestination)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClient CreateClient(Uri uri)
|
||||||
|
{
|
||||||
|
return _httpClientFactory.CreateClient(uri.IsOnion()
|
||||||
|
? LightningLikePayoutHandlerOnionNamedClient
|
||||||
|
: LightningLikePayoutHandlerClearnetNamedClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(IClaimDestination destination, string error)> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination, bool validate)
|
||||||
|
{
|
||||||
|
destination = destination.Trim();
|
||||||
|
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string lnurlTag = null;
|
||||||
|
var lnurl = EmailValidator.IsEmail(destination)
|
||||||
|
? LNURL.LNURL.ExtractUriFromInternetIdentifier(destination)
|
||||||
|
: LNURL.LNURL.Parse(destination, out lnurlTag);
|
||||||
|
|
||||||
|
if (lnurlTag is null)
|
||||||
|
{
|
||||||
|
var info = (LNURLPayRequest)(await LNURL.LNURL.FetchInformation(lnurl, CreateClient(lnurl)));
|
||||||
|
lnurlTag = info.Tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lnurlTag.Equals("payRequest", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
return (new LNURLPayClaimDestinaton(destination), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
catch (HttpRequestException)
|
||||||
|
{
|
||||||
|
return (null, "The LNURL / Lightning Address provided was not online.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result =
|
||||||
|
BOLT11PaymentRequest.TryParse(destination, out var invoice, network.NBitcoinNetwork)
|
||||||
|
? new BoltInvoiceClaimDestination(destination, invoice)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (result == null) return (null, "A valid BOLT11 invoice (with 30+ day expiry) or LNURL Pay or Lightning address was not provided.");
|
||||||
|
if (validate && (invoice.ExpiryDate.UtcDateTime - DateTime.UtcNow).Days < 30)
|
||||||
|
{
|
||||||
|
return (null,
|
||||||
|
$"The BOLT11 invoice must have an expiry date of at least 30 days from submission (Provided was only {(invoice.ExpiryDate.UtcDateTime - DateTime.UtcNow).Days}).");
|
||||||
|
}
|
||||||
|
if (invoice.ExpiryDate.UtcDateTime < DateTime.UtcNow)
|
||||||
|
{
|
||||||
|
return (null,
|
||||||
|
"The BOLT11 invoice submitted has expired.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (result, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPayoutProof ParseProof(PayoutData payout)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartBackgroundCheck(Action<Type[]> subscribe)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task BackgroundCheck(object o)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethodId, IClaimDestination claimDestination)
|
||||||
|
{
|
||||||
|
return Task.FromResult(Money.Satoshis(1).ToDecimal(MoneyUnit.BTC));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<PayoutState, List<(string Action, string Text)>> GetPayoutSpecificActions()
|
||||||
|
{
|
||||||
|
return new Dictionary<PayoutState, List<(string Action, string Text)>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<StatusMessageModel> DoSpecificAction(string action, string[] payoutIds, string storeId)
|
||||||
|
{
|
||||||
|
return Task.FromResult<StatusMessageModel>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<PaymentMethodId> GetSupportedPaymentMethods()
|
||||||
|
{
|
||||||
|
return _btcPayNetworkProvider.GetAll().OfType<BTCPayNetwork>().Where(network => network.SupportLightning)
|
||||||
|
.Select(network => new PaymentMethodId(network.CryptoCode, LightningPaymentType.Instance));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IActionResult> InitiatePayment(PaymentMethodId paymentMethodId, string[] payoutIds)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IActionResult>(new RedirectToActionResult("ConfirmLightningPayout",
|
||||||
|
"LightningLikePayout", new { cryptoCode = paymentMethodId.CryptoCode, payoutIds }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace BTCPayServer.Data.Payouts.LightningLike
|
||||||
|
{
|
||||||
|
public class PayoutLightningBlob: IPayoutProof
|
||||||
|
{
|
||||||
|
public string Bolt11Invoice { get; set; }
|
||||||
|
public string Preimage { get; set; }
|
||||||
|
public string PaymentHash { get; set; }
|
||||||
|
|
||||||
|
public string ProofType { get; }
|
||||||
|
public string Link { get; } = null;
|
||||||
|
public string Id => PaymentHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -50,5 +51,12 @@ namespace BTCPayServer.Data
|
|||||||
data.Proof = bytes;
|
data.Proof = bytes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<PaymentMethodId> GetSupportedPaymentMethods(
|
||||||
|
this IEnumerable<IPayoutHandler> payoutHandlers, List<PaymentMethodId> paymentMethodIds = null)
|
||||||
|
{
|
||||||
|
return payoutHandlers.SelectMany(handler => handler.GetSupportedPaymentMethods())
|
||||||
|
.Where(id => paymentMethodIds is null || paymentMethodIds.Contains(id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace BTCPayServer
|
|||||||
public static void AddModelError<TModel, TProperty>(this TModel source,
|
public static void AddModelError<TModel, TProperty>(this TModel source,
|
||||||
Expression<Func<TModel, TProperty>> ex,
|
Expression<Func<TModel, TProperty>> ex,
|
||||||
string message,
|
string message,
|
||||||
Controller controller)
|
ControllerBase controller)
|
||||||
{
|
{
|
||||||
var provider = (ModelExpressionProvider)controller.HttpContext.RequestServices.GetService(typeof(ModelExpressionProvider));
|
var provider = (ModelExpressionProvider)controller.HttpContext.RequestServices.GetService(typeof(ModelExpressionProvider));
|
||||||
var key = provider.GetExpressionText(ex);
|
var key = provider.GetExpressionText(ex);
|
||||||
|
|||||||
@@ -297,9 +297,8 @@ namespace BTCPayServer.HostedServices
|
|||||||
req.Rate = 1.0m;
|
req.Rate = 1.0m;
|
||||||
var cryptoAmount = payoutBlob.Amount / req.Rate;
|
var cryptoAmount = payoutBlob.Amount / req.Rate;
|
||||||
var payoutHandler = _payoutHandlers.First(handler => handler.CanHandle(paymentMethod));
|
var payoutHandler = _payoutHandlers.First(handler => handler.CanHandle(paymentMethod));
|
||||||
var dest = await payoutHandler.ParseClaimDestination(paymentMethod, payoutBlob.Destination);
|
var dest = await payoutHandler.ParseClaimDestination(paymentMethod, payoutBlob.Destination, false);
|
||||||
|
decimal minimumCryptoAmount = await payoutHandler.GetMinimumPayoutAmount(paymentMethod, dest.destination);
|
||||||
decimal minimumCryptoAmount = await payoutHandler.GetMinimumPayoutAmount(paymentMethod, dest);
|
|
||||||
if (cryptoAmount < minimumCryptoAmount)
|
if (cryptoAmount < minimumCryptoAmount)
|
||||||
{
|
{
|
||||||
req.Completion.TrySetResult(PayoutApproval.Result.TooLowAmount);
|
req.Completion.TrySetResult(PayoutApproval.Result.TooLowAmount);
|
||||||
@@ -384,7 +383,6 @@ namespace BTCPayServer.HostedServices
|
|||||||
Entity = o,
|
Entity = o,
|
||||||
Blob = o.GetBlob(_jsonSerializerSettings)
|
Blob = o.GetBlob(_jsonSerializerSettings)
|
||||||
});
|
});
|
||||||
var cd = _currencyNameTable.GetCurrencyData(pp.GetBlob().Currency, false);
|
|
||||||
var limit = ppBlob.Limit;
|
var limit = ppBlob.Limit;
|
||||||
var totalPayout = payouts.Select(p => p.Blob.Amount).Sum();
|
var totalPayout = payouts.Select(p => p.Blob.Amount).Sum();
|
||||||
var claimed = req.ClaimRequest.Value is decimal v ? v : limit - totalPayout;
|
var claimed = req.ClaimRequest.Value is decimal v ? v : limit - totalPayout;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using BTCPayServer.Configuration;
|
|||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
using BTCPayServer.Controllers.GreenField;
|
using BTCPayServer.Controllers.GreenField;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Data.Payouts.LightningLike;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
@@ -314,6 +315,11 @@ namespace BTCPayServer.Hosting
|
|||||||
|
|
||||||
|
|
||||||
services.AddSingleton<IPayoutHandler, BitcoinLikePayoutHandler>();
|
services.AddSingleton<IPayoutHandler, BitcoinLikePayoutHandler>();
|
||||||
|
services.AddSingleton<IPayoutHandler, LightningLikePayoutHandler>();
|
||||||
|
|
||||||
|
services.AddHttpClient(LightningLikePayoutHandler.LightningLikePayoutHandlerOnionNamedClient)
|
||||||
|
.ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true)
|
||||||
|
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||||
services.AddSingleton<HostedServices.PullPaymentHostedService>();
|
services.AddSingleton<HostedServices.PullPaymentHostedService>();
|
||||||
services.AddSingleton<IHostedService, HostedServices.PullPaymentHostedService>(o => o.GetRequiredService<PullPaymentHostedService>());
|
services.AddSingleton<IHostedService, HostedServices.PullPaymentHostedService>(o => o.GetRequiredService<PullPaymentHostedService>());
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
|
using System.Linq;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
|
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
|
||||||
|
|
||||||
@@ -18,6 +20,8 @@ namespace BTCPayServer.Models
|
|||||||
{
|
{
|
||||||
Id = data.Id;
|
Id = data.Id;
|
||||||
var blob = data.GetBlob();
|
var blob = data.GetBlob();
|
||||||
|
PaymentMethods = blob.SupportedPaymentMethods;
|
||||||
|
SelectedPaymentMethod = PaymentMethods.First().ToString();
|
||||||
Archived = data.Archived;
|
Archived = data.Archived;
|
||||||
Title = blob.View.Title;
|
Title = blob.View.Title;
|
||||||
Amount = blob.Limit;
|
Amount = blob.Limit;
|
||||||
@@ -58,6 +62,11 @@ namespace BTCPayServer.Models
|
|||||||
ResetIn = resetIn.TimeString();
|
ResetIn = resetIn.TimeString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string SelectedPaymentMethod { get; set; }
|
||||||
|
|
||||||
|
public PaymentMethodId[] PaymentMethods { get; set; }
|
||||||
|
|
||||||
public string HubPath { get; set; }
|
public string HubPath { get; set; }
|
||||||
public string ResetIn { get; set; }
|
public string ResetIn { get; set; }
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
@@ -95,6 +104,7 @@ namespace BTCPayServer.Models
|
|||||||
public string Currency { get; set; }
|
public string Currency { get; set; }
|
||||||
public string Link { get; set; }
|
public string Link { get; set; }
|
||||||
public string TransactionId { get; set; }
|
public string TransactionId { get; set; }
|
||||||
|
public PaymentMethodId PaymentMethod { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||||||
public string PullPaymentId { get; set; }
|
public string PullPaymentId { get; set; }
|
||||||
public string Command { get; set; }
|
public string Command { get; set; }
|
||||||
public Dictionary<PayoutState, int> PayoutStateCount { get; set; }
|
public Dictionary<PayoutState, int> PayoutStateCount { get; set; }
|
||||||
public PaymentMethodId PaymentMethodId { get; set; }
|
public string PaymentMethodId { get; set; }
|
||||||
|
|
||||||
public List<PayoutModel> Payouts { get; set; }
|
public List<PayoutModel> Payouts { get; set; }
|
||||||
public PayoutState PayoutState { get; set; }
|
public PayoutState PayoutState { get; set; }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.WalletViewModels
|
namespace BTCPayServer.Models.WalletViewModels
|
||||||
{
|
{
|
||||||
@@ -49,5 +50,8 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||||||
public string CustomCSSLink { get; set; }
|
public string CustomCSSLink { get; set; }
|
||||||
[Display(Name = "Custom CSS Code")]
|
[Display(Name = "Custom CSS Code")]
|
||||||
public string EmbeddedCSS { get; set; }
|
public string EmbeddedCSS { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<string> PaymentMethods { get; set; }
|
||||||
|
public IEnumerable<SelectListItem> PaymentMethodItems { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
BTCPayServer/Payments/Lightning/LightningExtensions.cs
Normal file
26
BTCPayServer/Payments/Lightning/LightningExtensions.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using BTCPayServer.Configuration;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
using BTCPayServer.Services;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Payments.Lightning
|
||||||
|
{
|
||||||
|
public static class LightningExtensions
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
public static ILightningClient CreateLightningClient(this LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, LightningNetworkOptions options, LightningClientFactoryService lightningClientFactory)
|
||||||
|
{
|
||||||
|
var external = supportedPaymentMethod.GetExternalLightningUrl();
|
||||||
|
if (external != null)
|
||||||
|
{
|
||||||
|
return lightningClientFactory.Create(external, network);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!options.InternalLightningByCryptoCode.TryGetValue(network.CryptoCode, out var connectionString))
|
||||||
|
throw new PaymentMethodUnavailableException("No internal node configured");
|
||||||
|
return lightningClientFactory.Create(connectionString, network);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,7 +80,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
{
|
{
|
||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
var client = CreateLightningClient(supportedPaymentMethod, network);
|
var client = supportedPaymentMethod.CreateLightningClient(network, Options.Value, _lightningClientFactory);
|
||||||
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
||||||
if (expiry < TimeSpan.Zero)
|
if (expiry < TimeSpan.Zero)
|
||||||
expiry = TimeSpan.FromSeconds(1);
|
expiry = TimeSpan.FromSeconds(1);
|
||||||
@@ -127,7 +127,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
|
|
||||||
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
|
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
|
||||||
{
|
{
|
||||||
var client = CreateLightningClient(supportedPaymentMethod, network);
|
var client = supportedPaymentMethod.CreateLightningClient(network, Options.Value, _lightningClientFactory);
|
||||||
LightningNodeInformation info;
|
LightningNodeInformation info;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -162,21 +162,6 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ILightningClient CreateLightningClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
|
||||||
{
|
|
||||||
var external = supportedPaymentMethod.GetExternalLightningUrl();
|
|
||||||
if (external != null)
|
|
||||||
{
|
|
||||||
return _lightningClientFactory.Create(external, network);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!Options.Value.InternalLightningByCryptoCode.TryGetValue(network.CryptoCode, out var connectionString))
|
|
||||||
throw new PaymentMethodUnavailableException("No internal node configured");
|
|
||||||
return _lightningClientFactory.Create(connectionString, network);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task TestConnection(NodeInfo nodeInfo, CancellationToken cancellation)
|
public async Task TestConnection(NodeInfo nodeInfo, CancellationToken cancellation)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
@model System.Collections.Generic.List<BTCPayServer.Data.Payouts.LightningLike.LightningLikePayoutController.ConfirmVM>
|
||||||
|
@{
|
||||||
|
Layout = "../Shared/_Layout.cshtml";
|
||||||
|
ViewData["Title"] = "Confirm Lightning Payout";
|
||||||
|
var cryptoCode = Context.GetRouteValue("cryptoCode");
|
||||||
|
}
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<h2 class="mb-4">@ViewData["Title"]</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<ul class="list-group">
|
||||||
|
@foreach (var item in Model)
|
||||||
|
{
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<div data-bs-toggle="tooltip" class="text-break" title="@item.Destination">@item.Destination</div>
|
||||||
|
|
||||||
|
<span class="text-capitalize badge bg-secondary">@item.Amount @cryptoCode</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<form method="post" class="list-group-item justify-content-center" id="pay-invoices-form">
|
||||||
|
<button type="submit" class="btn btn-primary xmx-2" style="min-width:25%;" id="Pay">Pay</button>
|
||||||
|
<button type="button" class="btn btn-secondary mx-2" onclick="history.back(); return false;" style="min-width:25%;">Go back</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@section PageFootContent {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
$("#pay-invoices-form").on("submit", function() {
|
||||||
|
$(this).find("input[type='submit']").prop('disabled', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#pay-invoices-form input").on("input", function () {
|
||||||
|
// Give it a timeout to make sure all form validation has completed by the time we run our callback
|
||||||
|
setTimeout(function() {
|
||||||
|
var validationErrors = $('.field-validation-error');
|
||||||
|
if (validationErrors.length === 0) {
|
||||||
|
$("input[type='submit']#Create").removeAttr('disabled');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
@using BTCPayServer.Lightning
|
||||||
|
@model System.Collections.Generic.List<BTCPayServer.Data.Payouts.LightningLike.LightningLikePayoutController.ResultVM>
|
||||||
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
ViewData["Title"] = $"Lightning Payout Result";
|
||||||
|
}
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="mb-4">@ViewData["Title"]</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<ul class="list-group">
|
||||||
|
@foreach (var item in Model)
|
||||||
|
{
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center @(item.Result == PayResult.Ok ? "bg-success" : "bg-danger")">
|
||||||
|
<div class="text-break" title="@item.Destination">@item.Destination</div>
|
||||||
|
<span class="badge bg-secondary">@(item.Result == PayResult.Ok ? "Sent" : "Failed")</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -15,11 +15,11 @@
|
|||||||
{
|
{
|
||||||
case "Completed":
|
case "Completed":
|
||||||
case "In Progress":
|
case "In Progress":
|
||||||
return "text-success";
|
return "bg-success";
|
||||||
case "Cancelled":
|
case "Cancelled":
|
||||||
return "text-danger";
|
return "bg-danger";
|
||||||
default:
|
default:
|
||||||
return "text-warning";
|
return "bg-warning";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,8 +55,12 @@
|
|||||||
<form asp-action="ClaimPullPayment" asp-route-pullPaymentId="@Model.Id" class="w-100">
|
<form asp-action="ClaimPullPayment" asp-route-pullPaymentId="@Model.Id" class="w-100">
|
||||||
<div class="row align-items-center" style="width:calc(100% + 30px)">
|
<div class="row align-items-center" style="width:calc(100% + 30px)">
|
||||||
<div class="col-12 mb-3 col-lg-6 mb-lg-0">
|
<div class="col-12 mb-3 col-lg-6 mb-lg-0">
|
||||||
<input class="form-control form-control-lg font-monospace w-100" asp-for="Destination" placeholder="Enter destination address to claim funds …" required style="font-size:.9rem;height:42px;">
|
<div class="input-group">
|
||||||
|
<input class="form-control form-control-lg font-monospace" asp-for="Destination" placeholder="Enter destination to claim funds" required style="font-size:.9rem;height:42px;">
|
||||||
|
<select class="form-select w-auto" asp-for="SelectedPaymentMethod" asp-items="Model.PaymentMethods.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString()))"></select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-12 mb-3 col-sm-6 mb-sm-0 col-lg-3">
|
<div class="col-12 mb-3 col-sm-6 mb-sm-0 col-lg-3">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="number" class="form-control form-control-lg text-end hide-number-spin" asp-for="ClaimedAmount" max="@Model.AmountDue" min="@Model.MinimumClaim" step="any" placeholder="Amount" required>
|
<input type="number" class="form-control form-control-lg text-end hide-number-spin" asp-for="ClaimedAmount" max="@Model.AmountDue" min="@Model.MinimumClaim" step="any" placeholder="Amount" required>
|
||||||
@@ -147,6 +151,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr class="table-borderless">
|
<tr class="table-borderless">
|
||||||
<th class="fw-normal text-secondary" scope="col">Destination</th>
|
<th class="fw-normal text-secondary" scope="col">Destination</th>
|
||||||
|
<th class="fw-normal text-secondary" scope="col">Method</th>
|
||||||
<th class="fw-normal text-secondary text-end text-nowrap">Amount requested</th>
|
<th class="fw-normal text-secondary text-end text-nowrap">Amount requested</th>
|
||||||
<th class="fw-normal text-secondary text-end">Status</th>
|
<th class="fw-normal text-secondary text-end">Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -158,15 +163,16 @@
|
|||||||
<td class="text-break">
|
<td class="text-break">
|
||||||
@invoice.Destination
|
@invoice.Destination
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">@invoice.AmountFormatted</td>
|
<td class="text-nowrap">@invoice.PaymentMethod.ToPrettyString()</td>
|
||||||
|
<td class="text-end text-nowrap">@invoice.AmountFormatted</td>
|
||||||
<td class="text-end text-nowrap">
|
<td class="text-end text-nowrap">
|
||||||
@if (!string.IsNullOrEmpty(invoice.Link))
|
@if (!string.IsNullOrEmpty(invoice.Link))
|
||||||
{
|
{
|
||||||
<a class="transaction-link text-print-default @StatusTextClass(invoice.Status.ToString())" href="@invoice.Link" rel="noreferrer noopener">@invoice.Status.GetStateString()</a>
|
<a class="transaction-link text-print-default badge @StatusTextClass(invoice.Status.ToString())" href="@invoice.Link" rel="noreferrer noopener">@invoice.Status.GetStateString()</a>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="text-print-default @StatusTextClass(invoice.Status.ToString())">@invoice.Status.GetStateString()</span>
|
<span class="text-print-default badge @StatusTextClass(invoice.Status.ToString())">@invoice.Status.GetStateString()</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -190,6 +196,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
<partial name="LayoutFoot" />
|
||||||
<script src="~/js/copy-to-clipboard.js" asp-append-version="true"></script>
|
<script src="~/js/copy-to-clipboard.js" asp-append-version="true"></script>
|
||||||
<script>
|
<script>
|
||||||
document.getElementById("copyLink").addEventListener("click", window.copyUrlToClipboard);
|
document.getElementById("copyLink").addEventListener("click", window.copyUrlToClipboard);
|
||||||
|
|||||||
@@ -72,6 +72,10 @@
|
|||||||
<span asp-validation-for="Currency" class="text-danger"></span>
|
<span asp-validation-for="Currency" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="PaymentMethods"></label>
|
||||||
|
<select asp-for="PaymentMethods" asp-items="Model.PaymentMethodItems" class="form-select" multiple></select>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="CustomCSSLink" class="form-label"></label>
|
<label asp-for="CustomCSSLink" class="form-label"></label>
|
||||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener">
|
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener">
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
@using BTCPayServer.Client.Models
|
@using BTCPayServer.Client.Models
|
||||||
|
@using BTCPayServer.Payments
|
||||||
@model PayoutsModel
|
@model PayoutsModel
|
||||||
|
|
||||||
@inject IEnumerable<IPayoutHandler> PayoutHandlers;
|
@inject IEnumerable<IPayoutHandler> PayoutHandlers;
|
||||||
@{
|
@{
|
||||||
Layout = "../Shared/_NavLayout.cshtml";
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
ViewData.SetActivePageAndTitle(WalletsNavPages.Payouts, $"Manage {Model.PaymentMethodId.ToPrettyString()} payouts{(string.IsNullOrEmpty(Model.PullPaymentName) ? string.Empty : " for pull payment " + Model.PullPaymentName)}", Context.GetStoreData().StoreName);
|
ViewData.SetActivePageAndTitle(WalletsNavPages.Payouts, $"Manage payouts{(string.IsNullOrEmpty(Model.PullPaymentName) ? string.Empty : " for pull payment " + Model.PullPaymentName)}", Context.GetStoreData().StoreName);
|
||||||
|
|
||||||
|
var paymentMethods = PayoutHandlers.GetSupportedPaymentMethods();
|
||||||
|
|
||||||
var stateActions = new List<(string Action, string Text)>();
|
var stateActions = new List<(string Action, string Text)>();
|
||||||
var payoutHandler = PayoutHandlers.First(handler => handler.CanHandle(Model.PaymentMethodId));
|
var payoutHandler = PayoutHandlers.First(handler => handler.CanHandle(PaymentMethodId.Parse(Model.PaymentMethodId)));
|
||||||
stateActions.AddRange(payoutHandler.GetPayoutSpecificActions().Where(pair => pair.Key == Model.PayoutState).SelectMany(pair => pair.Value));
|
stateActions.AddRange(payoutHandler.GetPayoutSpecificActions().Where(pair => pair.Key == Model.PayoutState).SelectMany(pair => pair.Value));
|
||||||
switch (Model.PayoutState)
|
switch (Model.PayoutState)
|
||||||
{
|
{
|
||||||
@@ -42,14 +45,37 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<ul class="nav nav-pills">
|
<ul class="nav nav-pills bg-tile mb-2" style="border-radius:4px; border: 1px solid var(--btcpay-body-border-medium)">
|
||||||
@foreach (var state in Model.PayoutStateCount)
|
@foreach (var state in paymentMethods)
|
||||||
{
|
{
|
||||||
<li class="nav-item py-0">
|
<li class="nav-item py-0">
|
||||||
<a id="@state.Key-view" asp-action="Payouts" asp-route-walletId="@Context.GetRouteValue("walletId")" asp-route-payoutState="@state.Key" asp-route-pullPaymentId="@Model.PullPaymentId" class="nav-link me-1 @(state.Key == Model.PayoutState ? "active" : "")" role="tab">@state.Key.GetStateString() (@state.Value)</a>
|
<a asp-action="Payouts" asp-route-walletId="@Context.GetRouteValue("walletId")"
|
||||||
|
asp-route-payoutState="@Model.PayoutState"
|
||||||
|
asp-route-paymentMethodId="@state.ToString()"
|
||||||
|
asp-route-pullPaymentId="@Model.PullPaymentId"
|
||||||
|
class="nav-link me-1 @(state.ToString() == Model.PaymentMethodId ? "active" : "")"
|
||||||
|
id="@state.ToString()-view"
|
||||||
|
role="tab">
|
||||||
|
@state.ToPrettyString()
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
<ul class="nav nav-pills bg-tile" style="border: 1px solid var(--btcpay-body-border-medium)">
|
||||||
|
@foreach (var state in Model.PayoutStateCount)
|
||||||
|
{
|
||||||
|
<li class="nav-item py-0">
|
||||||
|
<a id="@state.Key-view"
|
||||||
|
asp-action="Payouts"
|
||||||
|
asp-route-walletId="@Context.GetRouteValue("walletId")"
|
||||||
|
asp-route-payoutState="@state.Key"
|
||||||
|
asp-route-pullPaymentId="@Model.PullPaymentId"
|
||||||
|
asp-route-paymentMethodId="@Model.PaymentMethodId"
|
||||||
|
class="nav-link me-1 @(state.Key == Model.PayoutState ? "active" : "")" role="tab">@state.Key.GetStateString() (@state.Value)</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@if (Model.Payouts.Any() && stateActions.Any())
|
@if (Model.Payouts.Any() && stateActions.Any())
|
||||||
{
|
{
|
||||||
@@ -69,7 +95,6 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
||||||
<div>
|
|
||||||
@if (Model.Payouts.Any())
|
@if (Model.Payouts.Any())
|
||||||
{
|
{
|
||||||
<table class="table table-hover table-responsive-lg">
|
<table class="table table-hover table-responsive-lg">
|
||||||
@@ -107,8 +132,8 @@
|
|||||||
<td class="mw-100">
|
<td class="mw-100">
|
||||||
<span>@pp.PullPaymentName</span>
|
<span>@pp.PullPaymentName</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td title="@pp.Destination">
|
||||||
<span>@pp.Destination</span>
|
<span class="text-break">@pp.Destination</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<span>@pp.Amount</span>
|
<span>@pp.Amount</span>
|
||||||
@@ -131,7 +156,6 @@
|
|||||||
{
|
{
|
||||||
<p class="mb-0 py-4" id="@Model.PayoutState-no-payouts">There are no payouts matching this criteria.</p>
|
<p class="mb-0 py-4" id="@Model.PayoutState-no-payouts">There are no payouts matching this criteria.</p>
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
<vc:pager view-model="Model"/>
|
<vc:pager view-model="Model"/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
"currency": {
|
"currency": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "BTC",
|
"example": "BTC",
|
||||||
"description": "The currency of the amount. In this current release, this parameter must be set to a cryptoCode like (`BTC`)."
|
"description": "The currency of the amount."
|
||||||
},
|
},
|
||||||
"period": {
|
"period": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
},
|
},
|
||||||
"paymentMethods": {
|
"paymentMethods": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "The list of supported payment methods supported. In this current release, this must be set to an array with a single entry equals to `currency` (eg. `[ \"BTC\" ]`)",
|
"description": "The list of supported payment methods supported by this pull payment. Available options can be queried from the `StorePaymentMethods_GetStorePaymentMethods` endpoint",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "BTC"
|
"example": "BTC"
|
||||||
|
|||||||
Reference in New Issue
Block a user