From 3d7f62801423408c2d411880c525e4292f6a643c Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Tue, 28 Jun 2022 16:02:17 +0200 Subject: [PATCH] Support Lnurl Withdraw in pull payments (#3709) * Support Lnurl Withdraw * cleanup and small fixes * remove putin brace --- BTCPayServer.Data/Data/PayoutData.cs | 8 +- BTCPayServer.Tests/SeleniumTests.cs | 64 +++++++++ BTCPayServer/BTCPayServer.csproj | 2 +- BTCPayServer/Controllers/UILNURLController.cs | 136 +++++++++++++++++- ...torePullPaymentsController.PullPayments.cs | 55 ++----- .../PullPayments/PullPaymentsExtensions.cs | 2 +- .../PullPaymentHostedService.cs | 50 ++++++- BTCPayServer/Hosting/BTCPayServerServices.cs | 3 +- BTCPayServer/Models/ViewPullPaymentModel.cs | 2 + .../WalletViewModels/PullPaymentsModel.cs | 9 +- BTCPayServer/Views/Shared/ShowQR.cshtml | 14 +- .../UIPullPayment/ViewPullPayment.cshtml | 45 +++++- .../UIStorePullPayments/PullPayments.cshtml | 6 +- 13 files changed, 335 insertions(+), 61 deletions(-) diff --git a/BTCPayServer.Data/Data/PayoutData.cs b/BTCPayServer.Data/Data/PayoutData.cs index d8a6a6bf8..53e4dbfbb 100644 --- a/BTCPayServer.Data/Data/PayoutData.cs +++ b/BTCPayServer.Data/Data/PayoutData.cs @@ -51,12 +51,10 @@ namespace BTCPayServer.Data var period = pp.GetPeriod(now); if (period is { } p) { - return p.Start <= Date && (p.End is DateTimeOffset end ? Date < end : true); - } - else - { - return false; + return p.Start <= Date && (p.End is not { } end || Date < end); } + + return false; } } } diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 30d7bf073..47126348c 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -1489,6 +1489,70 @@ namespace BTCPayServer.Tests 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(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(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(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(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] diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 0323ba951..b3e1e40ae 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -57,7 +57,7 @@ - + diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index 901d966c2..e3debec5c 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -12,7 +12,9 @@ using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Controllers; using BTCPayServer.Data; +using BTCPayServer.Data.Payouts.LightningLike; using BTCPayServer.Events; +using BTCPayServer.HostedServices; using BTCPayServer.Lightning; using BTCPayServer.Models.AppViewModels; using BTCPayServer.Payments; @@ -45,6 +47,8 @@ namespace BTCPayServer private readonly UIInvoiceController _invoiceController; private readonly LinkGenerator _linkGenerator; private readonly LightningAddressService _lightningAddressService; + private readonly LightningLikePayoutHandler _lightningLikePayoutHandler; + private readonly PullPaymentHostedService _pullPaymentHostedService; public UILNURLController(InvoiceRepository invoiceRepository, EventAggregator eventAggregator, @@ -54,7 +58,9 @@ namespace BTCPayServer AppService appService, UIInvoiceController invoiceController, LinkGenerator linkGenerator, - LightningAddressService lightningAddressService) + LightningAddressService lightningAddressService, + LightningLikePayoutHandler lightningLikePayoutHandler, + PullPaymentHostedService pullPaymentHostedService) { _invoiceRepository = invoiceRepository; _eventAggregator = eventAggregator; @@ -65,9 +71,137 @@ namespace BTCPayServer _invoiceController = invoiceController; _linkGenerator = linkGenerator; _lightningAddressService = lightningAddressService; + _lightningLikePayoutHandler = lightningLikePayoutHandler; + _pullPaymentHostedService = pullPaymentHostedService; } + [HttpGet("withdraw/pp/{pullPaymentId}")] + public async Task GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr) + { + + var network = _btcPayNetworkProvider.GetNetwork(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() + .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}")] public async Task GetLNURLForApp(string cryptoCode, string appId, string itemCode = null) { diff --git a/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs b/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs index 1e51187cd..192ba56ee 100644 --- a/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs +++ b/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs @@ -21,6 +21,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; using PayoutData = BTCPayServer.Data.PayoutData; +using PullPaymentData = BTCPayServer.Data.PullPaymentData; using StoreData = BTCPayServer.Data.StoreData; namespace BTCPayServer.Controllers @@ -216,53 +217,27 @@ namespace BTCPayServer.Controllers break; } - var pps = (await ppsQuery - .Skip(vm.Skip) - .Take(vm.Count) - .ToListAsync() - ); - foreach (var pp in pps) + var pps = await ppsQuery + .Skip(vm.Skip) + .Take(vm.Count) + .ToListAsync(); + vm.PullPayments.AddRange(pps.Select(pp => { - 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(); - 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() + var blob = pp.GetBlob(); + return new PullPaymentsModel.PullPaymentModel() { StartDate = pp.StartDate, EndDate = pp.EndDate, Id = pp.Id, - Name = ppBlob.Name, - Progress = new PullPaymentsModel.PullPaymentModel.ProgressModel() - { - CompletedPercent = (int)(totalCompleted / ppBlob.Limit * 100m), - 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 - }); - } + Name = blob.Name, + AutoApproveClaims = blob.AutoApproveClaims, + Progress = _pullPaymentService.CalculatePullPaymentProgress(pp, now), + Archived = pp.Archived + }; + })); + return View(vm); } - public TimeSpan ZeroIfNegative(TimeSpan time) - { - if (time < TimeSpan.Zero) - time = TimeSpan.Zero; - return time; - } [HttpGet("stores/{storeId}/pull-payments/{pullPaymentId}/archive")] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] diff --git a/BTCPayServer/Data/PullPayments/PullPaymentsExtensions.cs b/BTCPayServer/Data/PullPayments/PullPaymentsExtensions.cs index a4989a90e..86f05bd81 100644 --- a/BTCPayServer/Data/PullPayments/PullPaymentsExtensions.cs +++ b/BTCPayServer/Data/PullPayments/PullPaymentsExtensions.cs @@ -17,7 +17,7 @@ namespace BTCPayServer.Data 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); } diff --git a/BTCPayServer/HostedServices/PullPaymentHostedService.cs b/BTCPayServer/HostedServices/PullPaymentHostedService.cs index 43254c700..253c74963 100644 --- a/BTCPayServer/HostedServices/PullPaymentHostedService.cs +++ b/BTCPayServer/HostedServices/PullPaymentHostedService.cs @@ -4,10 +4,13 @@ using System.Linq; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Logging; +using BTCPayServer.Models.WalletViewModels; using BTCPayServer.Payments; +using BTCPayServer.Rating; using BTCPayServer.Services; using BTCPayServer.Services.Notifications; using BTCPayServer.Services.Notifications.Blobs; @@ -18,6 +21,7 @@ using NBitcoin; using NBitcoin.DataEncoders; using NBXplorer; using PayoutData = BTCPayServer.Data.PayoutData; +using PullPaymentData = BTCPayServer.Data.PullPaymentData; namespace BTCPayServer.HostedServices @@ -167,7 +171,8 @@ namespace BTCPayServer.HostedServices RateFetcher rateFetcher, IEnumerable payoutHandlers, ILogger logger, - Logs logs) : base(logs) + Logs logs, + CurrencyNameTable currencyNameTable) : base(logs) { _dbContextFactory = dbContextFactory; _jsonSerializerSettings = jsonSerializerSettings; @@ -177,6 +182,7 @@ namespace BTCPayServer.HostedServices _rateFetcher = rateFetcher; _payoutHandlers = payoutHandlers; _logger = logger; + _currencyNameTable = currencyNameTable; } Channel _Channel; @@ -188,6 +194,7 @@ namespace BTCPayServer.HostedServices private readonly RateFetcher _rateFetcher; private readonly IEnumerable _payoutHandlers; private readonly ILogger _logger; + private readonly CurrencyNameTable _currencyNameTable; private readonly CompositeDisposable _subscriptions = new CompositeDisposable(); 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 { public InternalPayoutPaidRequest(TaskCompletionSource completionSource, diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 572699aba..daa4812d0 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -339,7 +339,8 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(provider => provider.GetRequiredService()); - services.AddSingleton(); + services.AddSingleton(provider => provider.GetRequiredService()); + services.AddSingleton(); services.AddHttpClient(LightningLikePayoutHandler.LightningLikePayoutHandlerOnionNamedClient) .ConfigurePrimaryHttpMessageHandler(); diff --git a/BTCPayServer/Models/ViewPullPaymentModel.cs b/BTCPayServer/Models/ViewPullPaymentModel.cs index c833736fa..4d411bfe9 100644 --- a/BTCPayServer/Models/ViewPullPaymentModel.cs +++ b/BTCPayServer/Models/ViewPullPaymentModel.cs @@ -24,6 +24,7 @@ namespace BTCPayServer.Models PaymentMethods = blob.SupportedPaymentMethods; SelectedPaymentMethod = PaymentMethods.First().ToString(); Archived = data.Archived; + AutoApprove = blob.AutoApproveClaims; Title = blob.View.Title; Description = blob.View.Description; Amount = blob.Limit; @@ -97,6 +98,7 @@ namespace BTCPayServer.Models public string AmountCollectedFormatted { get; set; } public string AmountFormatted { get; set; } public bool Archived { get; set; } + public bool AutoApprove { get; set; } public class PayoutLine { diff --git a/BTCPayServer/Models/WalletViewModels/PullPaymentsModel.cs b/BTCPayServer/Models/WalletViewModels/PullPaymentsModel.cs index 1af095475..a1d649588 100644 --- a/BTCPayServer/Models/WalletViewModels/PullPaymentsModel.cs +++ b/BTCPayServer/Models/WalletViewModels/PullPaymentsModel.cs @@ -16,11 +16,14 @@ namespace BTCPayServer.Models.WalletViewModels { public int CompletedPercent { get; set; } public int AwaitingPercent { get; set; } - public string Completed { get; set; } - public string Awaiting { get; set; } - public string Limit { get; set; } + public string CompletedFormatted { get; set; } + public string AwaitingFormatted { get; set; } + public string LimitFormatted { get; set; } public string ResetIn { 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 Name { get; set; } diff --git a/BTCPayServer/Views/Shared/ShowQR.cshtml b/BTCPayServer/Views/Shared/ShowQR.cshtml index 41a554985..9c8e15acd 100644 --- a/BTCPayServer/Views/Shared/ShowQR.cshtml +++ b/BTCPayServer/Views/Shared/ShowQR.cshtml @@ -8,11 +8,11 @@ -