mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 14:04:26 +01:00
Support Lnurl Withdraw in pull payments (#3709)
* Support Lnurl Withdraw * cleanup and small fixes * remove putin brace
This commit is contained in:
@@ -51,12 +51,10 @@ namespace BTCPayServer.Data
|
|||||||
var period = pp.GetPeriod(now);
|
var period = pp.GetPeriod(now);
|
||||||
if (period is { } p)
|
if (period is { } p)
|
||||||
{
|
{
|
||||||
return p.Start <= Date && (p.End is DateTimeOffset end ? Date < end : true);
|
return p.Start <= Date && (p.End is not { } end || Date < end);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1489,6 +1489,70 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource);
|
Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource);
|
||||||
|
|
||||||
|
//lnurl-w support check
|
||||||
|
|
||||||
|
s.GoToStore(s.StoreId,StoreNavPages.PullPayments);
|
||||||
|
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||||
|
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
|
||||||
|
s.Driver.SetCheckbox(By.Id("AutoApproveClaims"), true);
|
||||||
|
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||||
|
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.0000001");
|
||||||
|
s.Driver.FindElement(By.Id("Currency")).Clear();
|
||||||
|
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC" + Keys.Enter);
|
||||||
|
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||||
|
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||||
|
var lnurl = new Uri(LNURL.LNURL.Parse(s.Driver.FindElement(By.CssSelector("button[data-lnurl]")).GetAttribute("data-lnurl-uri"), out _).ToString().Replace("https", "http"));
|
||||||
|
var info = Assert.IsType<LNURLWithdrawRequest>(await LNURL.LNURL.FetchInformation(lnurl, s.Server.PayTester.HttpClient));
|
||||||
|
Assert.Equal(info.MaxWithdrawable, new LightMoney(0.0000001m, LightMoneyUnit.BTC));
|
||||||
|
Assert.Equal(info.CurrentBalance, new LightMoney(0.0000001m, LightMoneyUnit.BTC));
|
||||||
|
info = Assert.IsType<LNURLWithdrawRequest>(await LNURL.LNURL.FetchInformation(info.BalanceCheck, s.Server.PayTester.HttpClient));
|
||||||
|
Assert.Equal(info.MaxWithdrawable, new LightMoney(0.0000001m, LightMoneyUnit.BTC));
|
||||||
|
Assert.Equal(info.CurrentBalance, new LightMoney(0.0000001m, LightMoneyUnit.BTC));
|
||||||
|
|
||||||
|
var bolt2 = (await s.Server.CustomerLightningD.CreateInvoice(
|
||||||
|
new LightMoney(0.0000001m, LightMoneyUnit.BTC),
|
||||||
|
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
|
||||||
|
TimeSpan.FromHours(1), CancellationToken.None));
|
||||||
|
var response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient);
|
||||||
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
|
{
|
||||||
|
s.Driver.Navigate().Refresh();
|
||||||
|
Assert.Contains(bolt2.BOLT11, s.Driver.PageSource);
|
||||||
|
|
||||||
|
Assert.Contains(PayoutState.Completed.GetStateString(), s.Driver.PageSource);
|
||||||
|
Assert.Equal( LightningInvoiceStatus.Paid, (await s.Server.CustomerLightningD.GetInvoice(bolt2.Id)).Status );
|
||||||
|
});
|
||||||
|
|
||||||
|
s.GoToStore(s.StoreId,StoreNavPages.PullPayments);
|
||||||
|
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||||
|
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
|
||||||
|
s.Driver.SetCheckbox(By.Id("AutoApproveClaims"), false);
|
||||||
|
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||||
|
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.0000001");
|
||||||
|
s.Driver.FindElement(By.Id("Currency")).Clear();
|
||||||
|
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC" + Keys.Enter);
|
||||||
|
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||||
|
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||||
|
lnurl = new Uri(LNURL.LNURL.Parse(s.Driver.FindElement(By.CssSelector("button[data-lnurl]")).GetAttribute("data-lnurl-uri"), out _).ToString().Replace("https", "http"));
|
||||||
|
info = Assert.IsType<LNURLWithdrawRequest>(await LNURL.LNURL.FetchInformation(lnurl, s.Server.PayTester.HttpClient));
|
||||||
|
Assert.Equal(info.MaxWithdrawable, new LightMoney(0.0000001m, LightMoneyUnit.BTC));
|
||||||
|
Assert.Equal(info.CurrentBalance, new LightMoney(0.0000001m, LightMoneyUnit.BTC));
|
||||||
|
info = Assert.IsType<LNURLWithdrawRequest>(await LNURL.LNURL.FetchInformation(info.BalanceCheck, s.Server.PayTester.HttpClient));
|
||||||
|
Assert.Equal(info.MaxWithdrawable, new LightMoney(0.0000001m, LightMoneyUnit.BTC));
|
||||||
|
Assert.Equal(info.CurrentBalance, new LightMoney(0.0000001m, LightMoneyUnit.BTC));
|
||||||
|
|
||||||
|
bolt2 = (await s.Server.CustomerLightningD.CreateInvoice(
|
||||||
|
new LightMoney(0.0000001m, LightMoneyUnit.BTC),
|
||||||
|
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
|
||||||
|
TimeSpan.FromHours(1), CancellationToken.None));
|
||||||
|
response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient);
|
||||||
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
|
{
|
||||||
|
s.Driver.Navigate().Refresh();
|
||||||
|
Assert.Contains(bolt2.BOLT11, s.Driver.PageSource);
|
||||||
|
|
||||||
|
Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -57,7 +57,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.18" />
|
<PackageReference Include="LNURL" Version="0.0.22" />
|
||||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||||
<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" />
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ using BTCPayServer.Client;
|
|||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Data.Payouts.LightningLike;
|
||||||
using BTCPayServer.Events;
|
using BTCPayServer.Events;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Models.AppViewModels;
|
using BTCPayServer.Models.AppViewModels;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
@@ -45,6 +47,8 @@ namespace BTCPayServer
|
|||||||
private readonly UIInvoiceController _invoiceController;
|
private readonly UIInvoiceController _invoiceController;
|
||||||
private readonly LinkGenerator _linkGenerator;
|
private readonly LinkGenerator _linkGenerator;
|
||||||
private readonly LightningAddressService _lightningAddressService;
|
private readonly LightningAddressService _lightningAddressService;
|
||||||
|
private readonly LightningLikePayoutHandler _lightningLikePayoutHandler;
|
||||||
|
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||||
|
|
||||||
public UILNURLController(InvoiceRepository invoiceRepository,
|
public UILNURLController(InvoiceRepository invoiceRepository,
|
||||||
EventAggregator eventAggregator,
|
EventAggregator eventAggregator,
|
||||||
@@ -54,7 +58,9 @@ namespace BTCPayServer
|
|||||||
AppService appService,
|
AppService appService,
|
||||||
UIInvoiceController invoiceController,
|
UIInvoiceController invoiceController,
|
||||||
LinkGenerator linkGenerator,
|
LinkGenerator linkGenerator,
|
||||||
LightningAddressService lightningAddressService)
|
LightningAddressService lightningAddressService,
|
||||||
|
LightningLikePayoutHandler lightningLikePayoutHandler,
|
||||||
|
PullPaymentHostedService pullPaymentHostedService)
|
||||||
{
|
{
|
||||||
_invoiceRepository = invoiceRepository;
|
_invoiceRepository = invoiceRepository;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
@@ -65,9 +71,137 @@ namespace BTCPayServer
|
|||||||
_invoiceController = invoiceController;
|
_invoiceController = invoiceController;
|
||||||
_linkGenerator = linkGenerator;
|
_linkGenerator = linkGenerator;
|
||||||
_lightningAddressService = lightningAddressService;
|
_lightningAddressService = lightningAddressService;
|
||||||
|
_lightningLikePayoutHandler = lightningLikePayoutHandler;
|
||||||
|
_pullPaymentHostedService = pullPaymentHostedService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("withdraw/pp/{pullPaymentId}")]
|
||||||
|
public async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr)
|
||||||
|
{
|
||||||
|
|
||||||
|
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||||
|
if (network is null || !network.SupportLightning)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||||
|
var pp = await _pullPaymentHostedService.GetPullPayment(pullPaymentId, true);
|
||||||
|
if (!pp.IsRunning() || !pp.IsSupported(pmi))
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var blob = pp.GetBlob();
|
||||||
|
if (!blob.Currency.Equals(cryptoCode, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress = _pullPaymentHostedService.CalculatePullPaymentProgress(pp, DateTimeOffset.UtcNow);
|
||||||
|
|
||||||
|
var remaining = progress.Limit - progress.Completed - progress.Awaiting;
|
||||||
|
var request = new LNURLWithdrawRequest()
|
||||||
|
{
|
||||||
|
MaxWithdrawable = LightMoney.FromUnit(remaining, LightMoneyUnit.BTC),
|
||||||
|
K1 = pullPaymentId,
|
||||||
|
BalanceCheck = new Uri(Request.GetCurrentUrl()),
|
||||||
|
CurrentBalance = LightMoney.FromUnit(remaining, LightMoneyUnit.BTC),
|
||||||
|
MinWithdrawable =
|
||||||
|
LightMoney.FromUnit(
|
||||||
|
Math.Min(await _lightningLikePayoutHandler.GetMinimumPayoutAmount(pmi, null), remaining),
|
||||||
|
LightMoneyUnit.BTC),
|
||||||
|
Tag = "withdrawRequest",
|
||||||
|
Callback = new Uri(Request.GetCurrentUrl()),
|
||||||
|
};
|
||||||
|
if (pr is null)
|
||||||
|
{
|
||||||
|
return Ok(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BOLT11PaymentRequest.TryParse(pr, out var result, network.NBitcoinNetwork) || result is null)
|
||||||
|
{
|
||||||
|
return BadRequest(new LNUrlStatusResponse {Status = "ERROR", Reason = "Pr was not a valid BOLT11"});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.MinimumAmount < request.MinWithdrawable || result.MinimumAmount > request.MaxWithdrawable)
|
||||||
|
return BadRequest(new LNUrlStatusResponse {Status = "ERROR", Reason = "Pr was not within bounds"});
|
||||||
|
var store = await _storeRepository.FindStore(pp.StoreId);
|
||||||
|
var pm = store!.GetSupportedPaymentMethods(_btcPayNetworkProvider)
|
||||||
|
.OfType<LightningSupportedPaymentMethod>()
|
||||||
|
.FirstOrDefault(method => method.PaymentId == pmi);
|
||||||
|
if (pm is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var claimResponse = await _pullPaymentHostedService.Claim(new ClaimRequest()
|
||||||
|
{
|
||||||
|
Destination = new BoltInvoiceClaimDestination(pr, result),
|
||||||
|
PaymentMethodId = pmi,
|
||||||
|
PullPaymentId = pullPaymentId,
|
||||||
|
StoreId = pp.StoreId,
|
||||||
|
Value = result.MinimumAmount.ToDecimal(LightMoneyUnit.BTC)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (claimResponse.Result != ClaimRequest.ClaimResult.Ok)
|
||||||
|
return BadRequest(new LNUrlStatusResponse {Status = "ERROR", Reason = "Pr could not be paid"});
|
||||||
|
switch (claimResponse.PayoutData.State)
|
||||||
|
{
|
||||||
|
case PayoutState.AwaitingPayment:
|
||||||
|
{
|
||||||
|
var client =
|
||||||
|
_lightningLikePaymentHandler.CreateLightningClient(pm, network);
|
||||||
|
PayResponse payResult;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
payResult = await client.Pay(pr);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
payResult = new PayResponse(PayResult.Error, e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (payResult.Result)
|
||||||
|
{
|
||||||
|
case PayResult.Ok:
|
||||||
|
await _pullPaymentHostedService.MarkPaid(new PayoutPaidRequest()
|
||||||
|
{
|
||||||
|
PayoutId = claimResponse.PayoutData.Id, Proof = new ManualPayoutProof { }
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(new LNUrlStatusResponse {Status = "OK"});
|
||||||
|
default:
|
||||||
|
await _pullPaymentHostedService.Cancel(
|
||||||
|
new PullPaymentHostedService.CancelRequest(new string[]
|
||||||
|
{
|
||||||
|
claimResponse.PayoutData.Id
|
||||||
|
}));
|
||||||
|
|
||||||
|
return Ok(new LNUrlStatusResponse
|
||||||
|
{
|
||||||
|
Status = "ERROR",
|
||||||
|
Reason = $"Pr could not be paid because {payResult.ErrorDetail}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case PayoutState.AwaitingApproval:
|
||||||
|
return Ok(new LNUrlStatusResponse
|
||||||
|
{
|
||||||
|
Status = "OK",
|
||||||
|
Reason =
|
||||||
|
"The payment request has been recorded, but still needs to be approved before execution."
|
||||||
|
});
|
||||||
|
case PayoutState.InProgress:
|
||||||
|
case PayoutState.Completed:
|
||||||
|
return Ok(new LNUrlStatusResponse {Status = "OK"});
|
||||||
|
case PayoutState.Cancelled:
|
||||||
|
return BadRequest(new LNUrlStatusResponse {Status = "ERROR", Reason = "Pr could not be paid"});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(request);
|
||||||
|
}
|
||||||
[HttpGet("pay/app/{appId}/{itemCode}")]
|
[HttpGet("pay/app/{appId}/{itemCode}")]
|
||||||
public async Task<IActionResult> GetLNURLForApp(string cryptoCode, string appId, string itemCode = null)
|
public async Task<IActionResult> GetLNURLForApp(string cryptoCode, string appId, string itemCode = null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||||
|
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
|
||||||
using StoreData = BTCPayServer.Data.StoreData;
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
@@ -216,53 +217,27 @@ namespace BTCPayServer.Controllers
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pps = (await ppsQuery
|
var pps = await ppsQuery
|
||||||
.Skip(vm.Skip)
|
.Skip(vm.Skip)
|
||||||
.Take(vm.Count)
|
.Take(vm.Count)
|
||||||
.ToListAsync()
|
.ToListAsync();
|
||||||
);
|
vm.PullPayments.AddRange(pps.Select(pp =>
|
||||||
foreach (var pp in pps)
|
|
||||||
{
|
{
|
||||||
var totalCompleted = pp.Payouts.Where(p => (p.State == PayoutState.Completed ||
|
var blob = pp.GetBlob();
|
||||||
p.State == PayoutState.InProgress) && p.IsInPeriod(pp, now))
|
return new PullPaymentsModel.PullPaymentModel()
|
||||||
.Select(o => o.GetBlob(_jsonSerializerSettings).Amount).Sum();
|
|
||||||
var totalAwaiting = pp.Payouts.Where(p => (p.State == PayoutState.AwaitingPayment ||
|
|
||||||
p.State == PayoutState.AwaitingApproval) &&
|
|
||||||
p.IsInPeriod(pp, now)).Select(o =>
|
|
||||||
o.GetBlob(_jsonSerializerSettings).Amount).Sum();
|
|
||||||
;
|
|
||||||
var ppBlob = pp.GetBlob();
|
|
||||||
var ni = _currencyNameTable.GetCurrencyData(ppBlob.Currency, true);
|
|
||||||
var nfi = _currencyNameTable.GetNumberFormatInfo(ppBlob.Currency, true);
|
|
||||||
var period = pp.GetPeriod(now);
|
|
||||||
vm.PullPayments.Add(new PullPaymentsModel.PullPaymentModel()
|
|
||||||
{
|
{
|
||||||
StartDate = pp.StartDate,
|
StartDate = pp.StartDate,
|
||||||
EndDate = pp.EndDate,
|
EndDate = pp.EndDate,
|
||||||
Id = pp.Id,
|
Id = pp.Id,
|
||||||
Name = ppBlob.Name,
|
Name = blob.Name,
|
||||||
Progress = new PullPaymentsModel.PullPaymentModel.ProgressModel()
|
AutoApproveClaims = blob.AutoApproveClaims,
|
||||||
{
|
Progress = _pullPaymentService.CalculatePullPaymentProgress(pp, now),
|
||||||
CompletedPercent = (int)(totalCompleted / ppBlob.Limit * 100m),
|
Archived = pp.Archived
|
||||||
AwaitingPercent = (int)(totalAwaiting / ppBlob.Limit * 100m),
|
};
|
||||||
Awaiting = totalAwaiting.RoundToSignificant(ni.Divisibility).ToString("C", nfi),
|
}));
|
||||||
Completed = totalCompleted.RoundToSignificant(ni.Divisibility).ToString("C", nfi),
|
|
||||||
Limit = _currencyNameTable.DisplayFormatCurrency(ppBlob.Limit, ppBlob.Currency),
|
|
||||||
ResetIn = period?.End is DateTimeOffset nr ? ZeroIfNegative(nr - now).TimeString() : null,
|
|
||||||
EndIn = pp.EndDate is DateTimeOffset end ? ZeroIfNegative(end - now).TimeString() : null,
|
|
||||||
},
|
|
||||||
Archived = pp.Archived,
|
|
||||||
AutoApproveClaims = ppBlob.AutoApproveClaims
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
public TimeSpan ZeroIfNegative(TimeSpan time)
|
|
||||||
{
|
|
||||||
if (time < TimeSpan.Zero)
|
|
||||||
time = TimeSpan.Zero;
|
|
||||||
return time;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("stores/{storeId}/pull-payments/{pullPaymentId}/archive")]
|
[HttpGet("stores/{storeId}/pull-payments/{pullPaymentId}/archive")]
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace BTCPayServer.Data
|
|||||||
data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob));
|
data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsSupported(this PullPaymentData data, BTCPayServer.Payments.PaymentMethodId paymentId)
|
public static bool IsSupported(this PullPaymentData data, Payments.PaymentMethodId paymentId)
|
||||||
{
|
{
|
||||||
return data.GetBlob().SupportedPaymentMethods.Contains(paymentId);
|
return data.GetBlob().SupportedPaymentMethods.Contains(paymentId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
|
using BTCPayServer.Models.WalletViewModels;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Notifications;
|
using BTCPayServer.Services.Notifications;
|
||||||
using BTCPayServer.Services.Notifications.Blobs;
|
using BTCPayServer.Services.Notifications.Blobs;
|
||||||
@@ -18,6 +21,7 @@ using NBitcoin;
|
|||||||
using NBitcoin.DataEncoders;
|
using NBitcoin.DataEncoders;
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||||
|
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
|
||||||
|
|
||||||
|
|
||||||
namespace BTCPayServer.HostedServices
|
namespace BTCPayServer.HostedServices
|
||||||
@@ -167,7 +171,8 @@ namespace BTCPayServer.HostedServices
|
|||||||
RateFetcher rateFetcher,
|
RateFetcher rateFetcher,
|
||||||
IEnumerable<IPayoutHandler> payoutHandlers,
|
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||||
ILogger<PullPaymentHostedService> logger,
|
ILogger<PullPaymentHostedService> logger,
|
||||||
Logs logs) : base(logs)
|
Logs logs,
|
||||||
|
CurrencyNameTable currencyNameTable) : base(logs)
|
||||||
{
|
{
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
_jsonSerializerSettings = jsonSerializerSettings;
|
_jsonSerializerSettings = jsonSerializerSettings;
|
||||||
@@ -177,6 +182,7 @@ namespace BTCPayServer.HostedServices
|
|||||||
_rateFetcher = rateFetcher;
|
_rateFetcher = rateFetcher;
|
||||||
_payoutHandlers = payoutHandlers;
|
_payoutHandlers = payoutHandlers;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_currencyNameTable = currencyNameTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
Channel<object> _Channel;
|
Channel<object> _Channel;
|
||||||
@@ -188,6 +194,7 @@ namespace BTCPayServer.HostedServices
|
|||||||
private readonly RateFetcher _rateFetcher;
|
private readonly RateFetcher _rateFetcher;
|
||||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||||
private readonly ILogger<PullPaymentHostedService> _logger;
|
private readonly ILogger<PullPaymentHostedService> _logger;
|
||||||
|
private readonly CurrencyNameTable _currencyNameTable;
|
||||||
private readonly CompositeDisposable _subscriptions = new CompositeDisposable();
|
private readonly CompositeDisposable _subscriptions = new CompositeDisposable();
|
||||||
|
|
||||||
internal override Task[] InitializeTasks()
|
internal override Task[] InitializeTasks()
|
||||||
@@ -610,6 +617,47 @@ namespace BTCPayServer.HostedServices
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public PullPaymentsModel.PullPaymentModel.ProgressModel CalculatePullPaymentProgress(PullPaymentData pp, DateTimeOffset now)
|
||||||
|
{
|
||||||
|
var ppBlob = pp.GetBlob();
|
||||||
|
|
||||||
|
var ni = _currencyNameTable.GetCurrencyData(ppBlob.Currency, true);
|
||||||
|
var nfi = _currencyNameTable.GetNumberFormatInfo(ppBlob.Currency, true);
|
||||||
|
var totalCompleted = pp.Payouts.Where(p => (p.State == PayoutState.Completed ||
|
||||||
|
p.State == PayoutState.InProgress) && p.IsInPeriod(pp, now))
|
||||||
|
.Select(o => o.GetBlob(_jsonSerializerSettings).Amount).Sum().RoundToSignificant(ni.Divisibility);
|
||||||
|
var period = pp.GetPeriod(now);
|
||||||
|
var totalAwaiting = pp.Payouts.Where(p => (p.State == PayoutState.AwaitingPayment ||
|
||||||
|
p.State == PayoutState.AwaitingApproval) &&
|
||||||
|
p.IsInPeriod(pp, now)).Select(o =>
|
||||||
|
o.GetBlob(_jsonSerializerSettings).Amount).Sum().RoundToSignificant(ni.Divisibility);
|
||||||
|
;
|
||||||
|
var currencyData = _currencyNameTable.GetCurrencyData(ppBlob.Currency, true);
|
||||||
|
return new PullPaymentsModel.PullPaymentModel.ProgressModel()
|
||||||
|
{
|
||||||
|
CompletedPercent = (int)(totalCompleted / ppBlob.Limit * 100m),
|
||||||
|
AwaitingPercent = (int)(totalAwaiting / ppBlob.Limit * 100m),
|
||||||
|
AwaitingFormatted = totalAwaiting.ToString("C", nfi),
|
||||||
|
Awaiting = totalAwaiting,
|
||||||
|
Completed = totalCompleted,
|
||||||
|
CompletedFormatted = totalCompleted.ToString("C", nfi),
|
||||||
|
Limit = ppBlob.Limit.RoundToSignificant(currencyData.Divisibility),
|
||||||
|
LimitFormatted = _currencyNameTable.DisplayFormatCurrency(ppBlob.Limit, ppBlob.Currency),
|
||||||
|
ResetIn = period?.End is { } nr ? ZeroIfNegative(nr - now).TimeString() : null,
|
||||||
|
EndIn = pp.EndDate is { } end ? ZeroIfNegative(end - now).TimeString() : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public TimeSpan ZeroIfNegative(TimeSpan time)
|
||||||
|
{
|
||||||
|
if (time < TimeSpan.Zero)
|
||||||
|
time = TimeSpan.Zero;
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class InternalPayoutPaidRequest
|
class InternalPayoutPaidRequest
|
||||||
{
|
{
|
||||||
public InternalPayoutPaidRequest(TaskCompletionSource<PayoutPaidRequest.PayoutPaidResult> completionSource,
|
public InternalPayoutPaidRequest(TaskCompletionSource<PayoutPaidRequest.PayoutPaidResult> completionSource,
|
||||||
|
|||||||
@@ -339,7 +339,8 @@ namespace BTCPayServer.Hosting
|
|||||||
|
|
||||||
services.AddSingleton<BitcoinLikePayoutHandler>();
|
services.AddSingleton<BitcoinLikePayoutHandler>();
|
||||||
services.AddSingleton<IPayoutHandler>(provider => provider.GetRequiredService<BitcoinLikePayoutHandler>());
|
services.AddSingleton<IPayoutHandler>(provider => provider.GetRequiredService<BitcoinLikePayoutHandler>());
|
||||||
services.AddSingleton<IPayoutHandler, LightningLikePayoutHandler>();
|
services.AddSingleton<IPayoutHandler>(provider => provider.GetRequiredService<LightningLikePayoutHandler>());
|
||||||
|
services.AddSingleton<LightningLikePayoutHandler>();
|
||||||
|
|
||||||
services.AddHttpClient(LightningLikePayoutHandler.LightningLikePayoutHandlerOnionNamedClient)
|
services.AddHttpClient(LightningLikePayoutHandler.LightningLikePayoutHandlerOnionNamedClient)
|
||||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ namespace BTCPayServer.Models
|
|||||||
PaymentMethods = blob.SupportedPaymentMethods;
|
PaymentMethods = blob.SupportedPaymentMethods;
|
||||||
SelectedPaymentMethod = PaymentMethods.First().ToString();
|
SelectedPaymentMethod = PaymentMethods.First().ToString();
|
||||||
Archived = data.Archived;
|
Archived = data.Archived;
|
||||||
|
AutoApprove = blob.AutoApproveClaims;
|
||||||
Title = blob.View.Title;
|
Title = blob.View.Title;
|
||||||
Description = blob.View.Description;
|
Description = blob.View.Description;
|
||||||
Amount = blob.Limit;
|
Amount = blob.Limit;
|
||||||
@@ -97,6 +98,7 @@ namespace BTCPayServer.Models
|
|||||||
public string AmountCollectedFormatted { get; set; }
|
public string AmountCollectedFormatted { get; set; }
|
||||||
public string AmountFormatted { get; set; }
|
public string AmountFormatted { get; set; }
|
||||||
public bool Archived { get; set; }
|
public bool Archived { get; set; }
|
||||||
|
public bool AutoApprove { get; set; }
|
||||||
|
|
||||||
public class PayoutLine
|
public class PayoutLine
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,11 +16,14 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||||||
{
|
{
|
||||||
public int CompletedPercent { get; set; }
|
public int CompletedPercent { get; set; }
|
||||||
public int AwaitingPercent { get; set; }
|
public int AwaitingPercent { get; set; }
|
||||||
public string Completed { get; set; }
|
public string CompletedFormatted { get; set; }
|
||||||
public string Awaiting { get; set; }
|
public string AwaitingFormatted { get; set; }
|
||||||
public string Limit { get; set; }
|
public string LimitFormatted { get; set; }
|
||||||
public string ResetIn { get; set; }
|
public string ResetIn { get; set; }
|
||||||
public string EndIn { get; set; }
|
public string EndIn { get; set; }
|
||||||
|
public decimal Awaiting { get; set; }
|
||||||
|
public decimal Completed { get; set; }
|
||||||
|
public decimal Limit { get; set; }
|
||||||
}
|
}
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
<vc:icon symbol="close"/>
|
<vc:icon symbol="close"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body ">
|
<div class="modal-body text-center ">
|
||||||
<div class="qr-container text-center mt-3" style="min-height: 256px;">
|
<component :is="clickable? 'a': 'div'" class="qr-container text-center mt-3" :href="data" style="min-height: 256px;">
|
||||||
<qrcode v-bind:value="currentFragment" :options="{ width: 256,height:256, margin: 1, color: {dark:'#000', light:'#f5f5f7'} }">
|
<qrcode v-bind:value="currentFragment" :options="{ width: 256,height:256, margin: 1, color: {dark:'#000', light:'#f5f5f7'} }">
|
||||||
</qrcode>
|
</qrcode>
|
||||||
</div>
|
</component>
|
||||||
<ul class="nav justify-content-center mt-4 mb-3" v-if="allowedModes.length > 1">
|
<ul class="nav justify-content-center mt-4 mb-3" v-if="allowedModes.length > 1">
|
||||||
<li class="nav-item" v-for="allowedMode in allowedModes">
|
<li class="nav-item" v-for="allowedMode in allowedModes">
|
||||||
<a class="btcpay-pill"
|
<a class="btcpay-pill"
|
||||||
@@ -45,6 +45,8 @@
|
|||||||
title: title,
|
title: title,
|
||||||
speed: 500,
|
speed: 500,
|
||||||
data: data,
|
data: data,
|
||||||
|
dataPerMode: {},
|
||||||
|
clickable: false,
|
||||||
fragments: [],
|
fragments: [],
|
||||||
active: false,
|
active: false,
|
||||||
modalId: modalId,
|
modalId: modalId,
|
||||||
@@ -75,7 +77,11 @@
|
|||||||
watch:
|
watch:
|
||||||
{
|
{
|
||||||
currentMode: function(){
|
currentMode: function(){
|
||||||
|
if (this.dataPerMode && this.dataPerMode[this.currentMode]){
|
||||||
|
this.data = this.dataPerMode[this.currentMode];
|
||||||
|
}
|
||||||
this.setFragments();
|
this.setFragments();
|
||||||
|
|
||||||
},
|
},
|
||||||
data: function ()
|
data: function ()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
@inject BTCPayServer.Services.BTCPayServerEnvironment Env
|
@inject BTCPayServer.Services.BTCPayServerEnvironment Env
|
||||||
@inject BTCPayServer.Services.ThemeSettings Theme
|
@inject BTCPayServer.Services.ThemeSettings Theme
|
||||||
|
@inject BTCPayNetworkProvider BtcPayNetworkProvider
|
||||||
@using NUglify.Helpers
|
@using NUglify.Helpers
|
||||||
@using BTCPayServer.Client
|
@using BTCPayServer.Client
|
||||||
@using BTCPayServer.Components.ThemeSwitch
|
@using BTCPayServer.Components.ThemeSwitch
|
||||||
|
@using BTCPayServer.Payments
|
||||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
@using BundlerMinifier.TagHelpers
|
@using BundlerMinifier.TagHelpers
|
||||||
@using BTCPayServer.TagHelpers
|
@using BTCPayServer.TagHelpers
|
||||||
@@ -26,6 +27,22 @@
|
|||||||
return "bg-warning";
|
return "bg-warning";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
string lnurl = null;
|
||||||
|
string lnurlUri = null;
|
||||||
|
|
||||||
|
var pms = Model.PaymentMethods.FirstOrDefault(id => id.PaymentType == LightningPaymentType.Instance && BtcPayNetworkProvider.DefaultNetwork.CryptoCode == id.CryptoCode);
|
||||||
|
if (pms is not null && Model.Currency.Equals(pms.CryptoCode, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
var lnurlEndpoint = new Uri(Url.Action("GetLNURLForPullPayment", "UILNURL", new
|
||||||
|
{
|
||||||
|
cryptoCode = pms.CryptoCode,
|
||||||
|
pullPaymentId = Model.Id
|
||||||
|
}, Context.Request.Scheme, Context.Request.Host.ToString()));
|
||||||
|
lnurl = LNURL.LNURL.EncodeUri(lnurlEndpoint, "withdrawRequest", true).ToString();
|
||||||
|
lnurlUri = LNURL.LNURL.EncodeUri(lnurlEndpoint, "withdrawRequest", false).ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" @(Env.IsDeveloping ? " data-devenv" : "")>
|
<html lang="en" @(Env.IsDeveloping ? " data-devenv" : "")>
|
||||||
@@ -51,6 +68,12 @@
|
|||||||
<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">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
@if (lnurl is not null)
|
||||||
|
{
|
||||||
|
<button type="button" class="input-group-prepend btn btn-outline-secondary" data-lnurl data-lnurl-bech32="@lnurl" data-lnurl-uri="@lnurlUri">
|
||||||
|
<span class="fa fa-qrcode fa-2x" title="LNURL-Withdraw"></span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
<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;">
|
<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;">
|
||||||
@if (Model.PaymentMethods.Length == 1)
|
@if (Model.PaymentMethods.Length == 1)
|
||||||
{
|
{
|
||||||
@@ -212,8 +235,28 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<partial name="LayoutFoot" />
|
<partial name="LayoutFoot" />
|
||||||
|
@if (lnurl is not null)
|
||||||
|
{
|
||||||
|
<script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
|
||||||
|
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||||
|
<partial name="ShowQR"/>
|
||||||
|
}
|
||||||
<script>
|
<script>
|
||||||
document.getElementById("copyLink").addEventListener("click", window.copyUrlToClipboard);
|
document.getElementById("copyLink").addEventListener("click", window.copyUrlToClipboard);
|
||||||
|
const qrApp = initQRShow("LNURL Withdraw", "", "scan-qr-modal")
|
||||||
|
delegate('click', 'button[data-lnurl]', event => {
|
||||||
|
const { lnurlBech32,lnurlUri } = event.target.dataset
|
||||||
|
qrApp.data = null;
|
||||||
|
qrApp.dataPerMode = {
|
||||||
|
Bech32: lnurlBech32,
|
||||||
|
URI: lnurlUri
|
||||||
|
};
|
||||||
|
qrApp.clickable = true;
|
||||||
|
qrApp.allowedModes = ["Bech32", "URI"];
|
||||||
|
qrApp.currentMode = "URI";
|
||||||
|
$("#scan-qr-modal").modal("show")
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -71,11 +71,11 @@
|
|||||||
@foreach (var pp in Model.PullPayments)
|
@foreach (var pp in Model.PullPayments)
|
||||||
{
|
{
|
||||||
<script id="tooptip_template_@pp.Id" type="text/template">
|
<script id="tooptip_template_@pp.Id" type="text/template">
|
||||||
<span>Awaiting: <span class="float-end">@pp.Progress.Awaiting</span></span>
|
<span>Awaiting: <span class="float-end">@pp.Progress.AwaitingFormatted</span></span>
|
||||||
<br />
|
<br />
|
||||||
<span>Completed: <span class="float-end">@pp.Progress.Completed</span></span>
|
<span>Completed: <span class="float-end">@pp.Progress.CompletedFormatted</span></span>
|
||||||
<br />
|
<br />
|
||||||
<span>Limit: <span class="float-end">@pp.Progress.Limit</span></span>
|
<span>Limit: <span class="float-end">@pp.Progress.LimitFormatted</span></span>
|
||||||
@if (pp.Progress.ResetIn != null)
|
@if (pp.Progress.ResetIn != null)
|
||||||
{
|
{
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
Reference in New Issue
Block a user