From 438dcc4c6fc7e3475b5999ac9eb7b685e82aa25e Mon Sep 17 00:00:00 2001 From: Umar Bolatov Date: Wed, 25 Jan 2023 21:43:07 -0800 Subject: [PATCH] Add Greenfield API endpoint for pull payment LNURL items (#4472) * Add Greenfield API endpoint for pull payment LNURL items close #4365 * Rename GetLNURLs to GetPullPaymentLNURL * update "ln-url-not-supported" to "lnurl-not-supported" * remove hardcoding of "BTC" * update "PullPayments_LNURL" to "PullPayments_GetPullPaymentLNURL" * update description of 400 status code response Co-authored-by: Nicolas Dorier --- .../BTCPayServerClient.PullPayments.cs | 10 ++++ .../Models/PullPaymentLNURL.cs | 8 +++ BTCPayServer.Tests/GreenfieldAPITests.cs | 21 ++++++- .../GreenfieldPullPaymentController.cs | 30 ++++++++++ .../GreenField/LocalBTCPayServerClient.cs | 5 ++ .../v1/swagger.template.pull-payments.json | 55 +++++++++++++++++++ 6 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 BTCPayServer.Client/Models/PullPaymentLNURL.cs diff --git a/BTCPayServer.Client/BTCPayServerClient.PullPayments.cs b/BTCPayServer.Client/BTCPayServerClient.PullPayments.cs index 45ac8d31e..78aedaa68 100644 --- a/BTCPayServer.Client/BTCPayServerClient.PullPayments.cs +++ b/BTCPayServer.Client/BTCPayServerClient.PullPayments.cs @@ -97,5 +97,15 @@ namespace BTCPayServer.Client method: HttpMethod.Post, bodyPayload: request), cancellationToken); await HandleResponse(response); } + + public virtual async Task GetPullPaymentLNURL(string pullPaymentId, + CancellationToken cancellationToken = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest( + $"/api/v1/pull-payments/{pullPaymentId}/lnurl", + method: HttpMethod.Get), cancellationToken); + return await HandleResponse(response); + } } } diff --git a/BTCPayServer.Client/Models/PullPaymentLNURL.cs b/BTCPayServer.Client/Models/PullPaymentLNURL.cs new file mode 100644 index 000000000..0c644a9aa --- /dev/null +++ b/BTCPayServer.Client/Models/PullPaymentLNURL.cs @@ -0,0 +1,8 @@ +namespace BTCPayServer.Client.Models +{ + public class PullPaymentLNURL + { + public string LNURLBech32 { get; set; } + public string LNURLUri { get; set; } + } +} diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 2637e4e21..92a93e855 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -672,10 +672,12 @@ namespace BTCPayServer.Tests public async Task CanUsePullPaymentViaAPI() { using var tester = CreateServerTester(); + tester.ActivateLightning(); await tester.StartAsync(); + await tester.EnsureChannelsSetup(); var acc = tester.NewAccount(); - acc.Register(); - await acc.CreateStoreAsync(); + await acc.GrantAccessAsync(true); + acc.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false); var storeId = (await acc.RegisterDerivationSchemeAsync("BTC", importKeysToNBX: true)).StoreId; var client = await acc.CreateClient(); var result = await client.CreatePullPayment(storeId, new CreatePullPaymentRequest() @@ -856,6 +858,8 @@ namespace BTCPayServer.Tests PaymentMethods = new[] { "BTC" } }); + await this.AssertAPIError("lnurl-not-supported", async () => await unauthenticated.GetPullPaymentLNURL(pp.Id)); + destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(); TestLogs.LogInformation("Try to pay it in BTC"); payout = await unauthenticated.CreatePayout(pp.Id, new CreatePayoutRequest() @@ -906,7 +910,18 @@ namespace BTCPayServer.Tests payout = (await client.GetPayouts(payout.PullPaymentId)).First(data => data.Id == payout.Id); Assert.Equal(PayoutState.Completed, payout.State); await AssertAPIError("invalid-state", async () => await client.MarkPayoutPaid(storeId, payout.Id)); - + + // Test LNURL values + var test4 = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() + { + Name = "Test 3", + Amount = 12.303228134m, + Currency = "BTC", + PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" } + }); + var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id); + Assert.IsType(lnrURLs.LNURLBech32); + Assert.IsType(lnrURLs.LNURLUri); //permission test around auto approved pps and payouts var nonApproved = await acc.CreateClient(Policies.CanCreateNonApprovedPullPayments); diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs index c878009fd..23c9fcbf6 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs @@ -34,6 +34,7 @@ namespace BTCPayServer.Controllers.Greenfield private readonly CurrencyNameTable _currencyNameTable; private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings; private readonly IEnumerable _payoutHandlers; + private readonly BTCPayNetworkProvider _networkProvider; private readonly IAuthorizationService _authorizationService; public GreenfieldPullPaymentController(PullPaymentHostedService pullPaymentService, @@ -42,6 +43,7 @@ namespace BTCPayServer.Controllers.Greenfield CurrencyNameTable currencyNameTable, Services.BTCPayNetworkJsonSerializerSettings serializerSettings, IEnumerable payoutHandlers, + BTCPayNetworkProvider btcPayNetworkProvider, IAuthorizationService authorizationService) { _pullPaymentService = pullPaymentService; @@ -50,6 +52,7 @@ namespace BTCPayServer.Controllers.Greenfield _currencyNameTable = currencyNameTable; _serializerSettings = serializerSettings; _payoutHandlers = payoutHandlers; + _networkProvider = btcPayNetworkProvider; _authorizationService = authorizationService; } @@ -243,6 +246,33 @@ namespace BTCPayServer.Controllers.Greenfield return base.Ok(ToModel(payout)); } + [HttpGet("~/api/v1/pull-payments/{pullPaymentId}/lnurl")] + [AllowAnonymous] + public async Task GetPullPaymentLNURL(string pullPaymentId) + { + var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, false); + if (pp is null) + 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)) + { + var lnurlEndpoint = new Uri(Url.Action("GetLNURLForPullPayment", "UILNURL", new + { + cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, + pullPaymentId = pullPaymentId + }, Request.Scheme, Request.Host.ToString())); + + return base.Ok(new PullPaymentLNURL() { + LNURLBech32 = LNURL.LNURL.EncodeUri(lnurlEndpoint, "withdrawRequest", true).ToString(), + LNURLUri = LNURL.LNURL.EncodeUri(lnurlEndpoint, "withdrawRequest", false).ToString() + }); + } + + return this.CreateAPIError("lnurl-not-supported", "LNURL not supported for this pull payment"); + } + private Client.Models.PayoutData ToModel(Data.PayoutData p) { var blob = p.GetBlob(_serializerSettings); diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index d6cee62f3..65d342fe6 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -1242,6 +1242,11 @@ namespace BTCPayServer.Controllers.Greenfield return GetFromActionResult(await GetController().GetPayout(pullPaymentId, payoutId)); } + public override async Task GetPullPaymentLNURL(string pullPaymentId, CancellationToken cancellationToken = default) + { + return GetFromActionResult(await GetController().GetPullPaymentLNURL(pullPaymentId)); + } + public override async Task GetStorePayout(string storeId, string payoutId, CancellationToken cancellationToken = default) { diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.pull-payments.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.pull-payments.json index 3aaee25b5..eaca1725e 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.pull-payments.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.pull-payments.json @@ -402,6 +402,46 @@ "security": [] } }, + "/api/v1/pull-payments/{pullPaymentId}/lnurl": { + "parameters": [ + { + "name": "pullPaymentId", + "in": "path", + "required": true, + "description": "The ID of the pull payment", + "schema": { + "type": "string" + } + } + ], + "get": { + "summary": "Get Pull Payment LNURL details", + "operationId": "PullPayments_GetPullPaymentLNURL", + "description": "Get Pull Payment LNURL details", + "responses": { + "200": { + "description": "Pull payment LNURL details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LNURLData" + } + } + } + }, + "404": { + "description": "Pull payment not found" + }, + "400": { + "description": "Pull payment found but does not support LNURL" + } + }, + "tags": [ + "Pull payments (Public)" + ], + "security": [] + } + }, "/api/v1/stores/{storeId}/payouts": { "parameters": [ { @@ -1028,6 +1068,21 @@ "description": "The link to a page to claim payouts to this pull payment" } } + }, + "LNURLData": { + "type": "object", + "properties": { + "lnurlBech32": { + "type": "string", + "description": "Bech32 representation of LNRURL", + "example": "lightning:lnurl1dp68gup69uhnzv3h9cczuvpwxyarzdp3xsez7sj5gvh42j2vfe24ynp0wa5hg6rywfshwtmswqhngvntdd6x6uzvx4jrvu2kvvur23n8v46rwjpexcc45563fn53w7" + }, + "lnurlUri": { + "type": "string", + "description": "Bech32 representation of LNURL", + "example": "lnurlw://example.com/BTC/UILNURL/withdraw/pp/42kktmpL5d6qVc85Fget7H961ZSQ" + } + } } } },