Support Lnurl Withdraw in pull payments (#3709)

* Support Lnurl Withdraw

* cleanup and small fixes

* remove putin brace
This commit is contained in:
Andrew Camilleri
2022-06-28 16:02:17 +02:00
committed by GitHub
parent c63529ea99
commit 3d7f628014
13 changed files with 335 additions and 61 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>();

View File

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

View File

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

View File

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

View File

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

View File

@@ -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:&nbsp;<span class="float-end">@pp.Progress.Awaiting</span></span> <span>Awaiting:&nbsp;<span class="float-end">@pp.Progress.AwaitingFormatted</span></span>
<br /> <br />
<span>Completed:&nbsp;<span class="float-end">@pp.Progress.Completed</span></span> <span>Completed:&nbsp;<span class="float-end">@pp.Progress.CompletedFormatted</span></span>
<br /> <br />
<span>Limit:&nbsp;<span class="float-end">@pp.Progress.Limit</span></span> <span>Limit:&nbsp;<span class="float-end">@pp.Progress.LimitFormatted</span></span>
@if (pp.Progress.ResetIn != null) @if (pp.Progress.ResetIn != null)
{ {
<br /> <br />