mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-11 01:35:22 +01:00
Pull Payment: Support LNURL Withdraw with SATS denomination (#5041)
* Pull Payment: Support LNURL Withdraw with SATS denomination * Refactor and add tests
This commit is contained in:
parent
fa8b977016
commit
f11424f73a
8 changed files with 89 additions and 35 deletions
|
@ -1074,6 +1074,22 @@ namespace BTCPayServer.Tests
|
|||
var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id);
|
||||
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||
Assert.IsType<string>(lnrURLs.LNURLUri);
|
||||
Assert.Equal(12.303228134m, test4.Amount);
|
||||
Assert.Equal("BTC", test4.Currency);
|
||||
|
||||
// Test with SATS denomination values
|
||||
var testSats = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
|
||||
{
|
||||
Name = "Test SATS",
|
||||
Amount = 21000,
|
||||
Currency = "SATS",
|
||||
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
});
|
||||
lnrURLs = await unauthenticated.GetPullPaymentLNURL(testSats.Id);
|
||||
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||
Assert.IsType<string>(lnrURLs.LNURLUri);
|
||||
Assert.Equal(21000, testSats.Amount);
|
||||
Assert.Equal("SATS", testSats.Currency);
|
||||
|
||||
//permission test around auto approved pps and payouts
|
||||
var nonApproved = await acc.CreateClient(Policies.CanCreateNonApprovedPullPayments);
|
||||
|
|
|
@ -1748,7 +1748,6 @@ namespace BTCPayServer.Tests
|
|||
Assert.Contains(labels, element => element.Text == "pull-payment");
|
||||
});
|
||||
|
||||
|
||||
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.InProgress}-view")).Click();
|
||||
ReadOnlyCollection<IWebElement> txs;
|
||||
|
@ -1932,8 +1931,7 @@ namespace BTCPayServer.Tests
|
|||
|
||||
Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource);
|
||||
|
||||
//lnurl-w support check
|
||||
|
||||
// LNURL Withdraw support check with BTC denomination
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
|
||||
|
@ -2001,6 +1999,42 @@ namespace BTCPayServer.Tests
|
|||
|
||||
Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource);
|
||||
});
|
||||
|
||||
// LNURL Withdraw support check with SATS denomination
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP SATS");
|
||||
s.Driver.SetCheckbox(By.Id("AutoApproveClaims"), true);
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("21021");
|
||||
s.Driver.FindElement(By.Id("Currency")).Clear();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("SATS" + Keys.Enter);
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
|
||||
lnurl = new Uri(LNURL.LNURL.Parse(s.Driver.FindElement(By.Id("qr-code-data-input")).GetAttribute("value"), out _).ToString().Replace("https", "http"));
|
||||
s.Driver.FindElement(By.CssSelector("button[data-bs-dismiss='modal']")).Click();
|
||||
var amount = new LightMoney(21021, LightMoneyUnit.Satoshi);
|
||||
info = Assert.IsType<LNURLWithdrawRequest>(await LNURL.LNURL.FetchInformation(lnurl, s.Server.PayTester.HttpClient));
|
||||
Assert.Equal(amount, info.MaxWithdrawable);
|
||||
Assert.Equal(amount, info.CurrentBalance);
|
||||
info = Assert.IsType<LNURLWithdrawRequest>(await LNURL.LNURL.FetchInformation(info.BalanceCheck, s.Server.PayTester.HttpClient));
|
||||
Assert.Equal(amount, info.MaxWithdrawable);
|
||||
Assert.Equal(amount, info.CurrentBalance);
|
||||
|
||||
bolt2 = (await s.Server.CustomerLightningD.CreateInvoice(
|
||||
amount,
|
||||
$"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.Completed.GetStateString(), s.Driver.PageSource);
|
||||
Assert.Equal(LightningInvoiceStatus.Paid, (await s.Server.CustomerLightningD.GetInvoice(bolt2.Id)).Status);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
@ -255,16 +255,15 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
return PullPaymentNotFound();
|
||||
|
||||
var blob = pp.GetBlob();
|
||||
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id => id.PaymentType == LightningPaymentType.Instance && _networkProvider.DefaultNetwork.CryptoCode == id.CryptoCode);
|
||||
if (pms is not null && blob.Currency.Equals(pms.CryptoCode, StringComparison.InvariantCultureIgnoreCase))
|
||||
if (_pullPaymentService.SupportsLNURL(blob))
|
||||
{
|
||||
var lnurlEndpoint = new Uri(Url.Action("GetLNURLForPullPayment", "UILNURL", new
|
||||
{
|
||||
cryptoCode = _networkProvider.DefaultNetwork.CryptoCode,
|
||||
pullPaymentId = pullPaymentId
|
||||
pullPaymentId
|
||||
}, Request.Scheme, Request.Host.ToString())!);
|
||||
|
||||
return base.Ok(new PullPaymentLNURL()
|
||||
return base.Ok(new PullPaymentLNURL
|
||||
{
|
||||
LNURLBech32 = LNURL.LNURL.EncodeUri(lnurlEndpoint, "withdrawRequest", true).ToString(),
|
||||
LNURLUri = LNURL.LNURL.EncodeUri(lnurlEndpoint, "withdrawRequest", false).ToString()
|
||||
|
|
|
@ -109,24 +109,24 @@ namespace BTCPayServer
|
|||
}
|
||||
|
||||
var blob = pp.GetBlob();
|
||||
if (!blob.Currency.Equals(cryptoCode, StringComparison.InvariantCultureIgnoreCase))
|
||||
if (!_pullPaymentHostedService.SupportsLNURL(blob))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var unit = blob.Currency == "SATS" ? LightMoneyUnit.Satoshi : LightMoneyUnit.BTC;
|
||||
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),
|
||||
MaxWithdrawable = LightMoney.FromUnit(remaining, unit),
|
||||
K1 = pullPaymentId,
|
||||
BalanceCheck = new Uri(Request.GetCurrentUrl()),
|
||||
CurrentBalance = LightMoney.FromUnit(remaining, LightMoneyUnit.BTC),
|
||||
CurrentBalance = LightMoney.FromUnit(remaining, unit),
|
||||
MinWithdrawable =
|
||||
LightMoney.FromUnit(
|
||||
Math.Min(await _lightningLikePayoutHandler.GetMinimumPayoutAmount(pmi, null), remaining),
|
||||
LightMoneyUnit.BTC),
|
||||
unit),
|
||||
Tag = "withdrawRequest",
|
||||
Callback = new Uri(Request.GetCurrentUrl()),
|
||||
// It's not `pp.GetBlob().Description` because this would be HTML
|
||||
|
@ -154,13 +154,13 @@ namespace BTCPayServer
|
|||
return NotFound();
|
||||
}
|
||||
|
||||
var claimResponse = await _pullPaymentHostedService.Claim(new ClaimRequest()
|
||||
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)
|
||||
Value = result.MinimumAmount.ToDecimal(unit)
|
||||
});
|
||||
|
||||
if (claimResponse.Result != ClaimRequest.ClaimResult.Ok)
|
||||
|
|
|
@ -29,6 +29,7 @@ namespace BTCPayServer.Controllers
|
|||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
|
||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
@ -37,6 +38,7 @@ namespace BTCPayServer.Controllers
|
|||
CurrencyNameTable currencyNameTable,
|
||||
DisplayFormatter displayFormatter,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayNetworkJsonSerializerSettings serializerSettings,
|
||||
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||
StoreRepository storeRepository)
|
||||
|
@ -48,6 +50,7 @@ namespace BTCPayServer.Controllers
|
|||
_serializerSettings = serializerSettings;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
_storeRepository = storeRepository;
|
||||
_networkProvider = networkProvider;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
|
@ -102,6 +105,13 @@ namespace BTCPayServer.Controllers
|
|||
}).ToList()
|
||||
};
|
||||
vm.IsPending &= vm.AmountDue > 0.0m;
|
||||
|
||||
if (_pullPaymentHostedService.SupportsLNURL(blob))
|
||||
{
|
||||
var url = Url.Action("GetLNURLForPullPayment", "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
|
||||
vm.LnurlEndpoint = url != null ? new Uri(url) : null;
|
||||
}
|
||||
|
||||
return View(nameof(ViewPullPayment), vm);
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,8 @@ namespace BTCPayServer.HostedServices
|
|||
|
||||
public class PullPaymentHostedService : BaseAsyncService
|
||||
{
|
||||
private readonly string[] _lnurlSupportedCurrencies = { "BTC", "SATS" };
|
||||
|
||||
public class CancelRequest
|
||||
{
|
||||
public CancelRequest(string pullPaymentId)
|
||||
|
@ -337,6 +339,14 @@ namespace BTCPayServer.HostedServices
|
|||
}
|
||||
}
|
||||
|
||||
public bool SupportsLNURL(PullPaymentBlob blob)
|
||||
{
|
||||
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id =>
|
||||
id.PaymentType == LightningPaymentType.Instance &&
|
||||
_networkProvider.DefaultNetwork.CryptoCode == id.CryptoCode);
|
||||
return pms is not null && _lnurlSupportedCurrencies.Contains(blob.Currency);
|
||||
}
|
||||
|
||||
public Task<RateResult> GetRate(PayoutData payout, string explicitRateRule, CancellationToken cancellationToken)
|
||||
{
|
||||
var ppBlob = payout.PullPaymentData?.GetBlob();
|
||||
|
|
|
@ -96,6 +96,7 @@ namespace BTCPayServer.Models
|
|||
public DateTimeOffset StartDate { get; set; }
|
||||
public DateTime LastRefreshed { get; set; }
|
||||
public CurrencyData CurrencyData { get; set; }
|
||||
public Uri LnurlEndpoint { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
public bool AutoApprove { get; set; }
|
||||
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Payments
|
||||
@using BTCPayServer.Services
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
||||
@inject BTCPayServerEnvironment Env
|
||||
@inject BTCPayNetworkProvider BtcPayNetworkProvider
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@model BTCPayServer.Models.ViewPullPaymentModel
|
||||
@{
|
||||
|
@ -25,22 +23,6 @@
|
|||
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>
|
||||
<html lang="en" @(Env.IsDeveloping ? " data-devenv" : "")>
|
||||
|
@ -62,7 +44,7 @@
|
|||
<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="input-group">
|
||||
@if (lnurl is not null)
|
||||
@if (Model.LnurlEndpoint is not null)
|
||||
{
|
||||
<button type="button" class="input-group-prepend btn btn-outline-secondary" id="lnurlwithdraw-button" data-bs-toggle="modal" data-bs-target="#scan-qr-modal">
|
||||
<span class="fa fa-qrcode fa-2x" title="LNURL-Withdraw"></span>
|
||||
|
@ -223,8 +205,10 @@
|
|||
</footer>
|
||||
</div>
|
||||
<partial name="LayoutFoot" />
|
||||
@if (lnurl is not null)
|
||||
@if (Model.LnurlEndpoint is not null)
|
||||
{
|
||||
var lnurlUri = LNURL.LNURL.EncodeUri(Model.LnurlEndpoint, "withdrawRequest", false).ToString();
|
||||
var lnurlBech32 = LNURL.LNURL.EncodeUri(Model.LnurlEndpoint, "withdrawRequest", true).ToString();
|
||||
var note = "You can scan or open this link with a <a href='https://github.com/fiatjaf/lnurl-rfc#lnurl-documents' target='_blank' rel='noreferrer noopener'>LNURL-Withdraw</a> enabled wallet.";
|
||||
if (!Model.AutoApprove)
|
||||
{
|
||||
|
@ -237,7 +221,7 @@
|
|||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const modes = {
|
||||
uri: { title: "URI", fragments: [@Safe.Json(lnurlUri)], showData: true, href: @Safe.Json(lnurlUri) },
|
||||
bech32: { title: "Bech32", fragments: [@Safe.Json(lnurl)], showData: true, href: @Safe.Json(lnurl) }
|
||||
bech32: { title: "Bech32", fragments: [@Safe.Json(lnurlBech32)], showData: true, href: @Safe.Json(lnurlBech32) }
|
||||
};
|
||||
initQRShow({ title: "LNURL Withdraw", note: @Safe.Json(note), modes })
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue