Greenfield: Get Lightning invoices (#4180)

* Greenfield: Get Lightning invoices

Matching the data added in btcpayserver/BTCPayServer.Lightning#98 and btcpayserver/BTCPayServer.Lightning#99.

* Small adjustments

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
d11n 2022-10-17 09:51:15 +02:00 committed by GitHub
parent 0286c72256
commit 6d7c11f1b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 270 additions and 13 deletions

View file

@ -95,6 +95,24 @@ namespace BTCPayServer.Client
method: HttpMethod.Get), token);
return await HandleResponse<LightningInvoiceData>(response);
}
public virtual async Task<LightningInvoiceData[]> GetLightningInvoices(string cryptoCode,
bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default)
{
var queryPayload = new Dictionary<string, object>();
if (pendingOnly is bool v)
{
queryPayload.Add("pendingOnly", v.ToString());
}
if (offsetIndex is > 0)
{
queryPayload.Add("offsetIndex", offsetIndex);
}
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices", queryPayload), token);
return await HandleResponse<LightningInvoiceData[]>(response);
}
public virtual async Task<LightningInvoiceData> CreateLightningInvoice(string cryptoCode, CreateLightningInvoiceRequest request,
CancellationToken token = default)

View file

@ -97,6 +97,24 @@ namespace BTCPayServer.Client
method: HttpMethod.Get), token);
return await HandleResponse<LightningInvoiceData>(response);
}
public virtual async Task<LightningInvoiceData[]> GetLightningInvoices(string storeId, string cryptoCode,
bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default)
{
var queryPayload = new Dictionary<string, object>();
if (pendingOnly is bool v)
{
queryPayload.Add("pendingOnly", v.ToString());
}
if (offsetIndex is > 0)
{
queryPayload.Add("offsetIndex", offsetIndex);
}
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices", queryPayload), token);
return await HandleResponse<LightningInvoiceData[]>(response);
}
public virtual async Task<LightningInvoiceData> CreateLightningInvoice(string storeId, string cryptoCode,
CreateLightningInvoiceRequest request, CancellationToken token = default)

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Lightning;
using Newtonsoft.Json;
@ -26,5 +27,8 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney AmountReceived { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<ulong, string> CustomRecords { get; set; }
}
}

View file

@ -1662,9 +1662,9 @@ namespace BTCPayServer.Tests
var merchantClient = await merchant.CreateClient($"{Policies.CanUseLightningNodeInStore}:{merchant.StoreId}");
var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(LightMoney.Satoshis(1_000), "hey", TimeSpan.FromSeconds(60)));
// The default client is using charge, so we should not be able to query channels
var client = await user.CreateClient(Policies.CanUseInternalLightningNode);
var chargeClient = await user.CreateClient(Policies.CanUseInternalLightningNode);
var info = await client.GetLightningNodeInfo("BTC");
var info = await chargeClient.GetLightningNodeInfo("BTC");
Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight);
Assert.NotNull(info.Alias);
@ -1675,10 +1675,10 @@ namespace BTCPayServer.Tests
Assert.NotNull(info.InactiveChannelsCount);
Assert.NotNull(info.PendingChannelsCount);
await AssertAPIError("lightning-node-unavailable", () => client.GetLightningNodeChannels("BTC"));
await AssertAPIError("lightning-node-unavailable", () => chargeClient.GetLightningNodeChannels("BTC"));
// Not permission for the store!
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC"));
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
await AssertAPIError("missing-permission", () => chargeClient.GetLightningNodeChannels(user.StoreId, "BTC"));
var invoiceData = await chargeClient.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
{
Amount = LightMoney.Satoshis(1000),
Description = "lol",
@ -1686,9 +1686,17 @@ namespace BTCPayServer.Tests
PrivateRouteHints = false
});
var chargeInvoice = invoiceData;
Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
Assert.NotNull(await chargeClient.GetLightningInvoice("BTC", invoiceData.Id));
client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
// check list for internal node
var invoices = await chargeClient.GetLightningInvoices("BTC");
var pendingInvoices = await chargeClient.GetLightningInvoices("BTC", true);
Assert.NotEmpty(invoices);
Assert.Contains(invoices, i => i.Id == invoiceData.Id);
Assert.NotEmpty(pendingInvoices);
Assert.Contains(pendingInvoices, i => i.Id == invoiceData.Id);
var client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
// Not permission for the server
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC"));
@ -1706,6 +1714,11 @@ namespace BTCPayServer.Tests
Assert.NotNull(await client.GetLightningInvoice(user.StoreId, "BTC", invoiceData.Id));
// check pending list
var merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true);
Assert.NotEmpty(merchantPendingInvoices);
Assert.Contains(merchantPendingInvoices, i => i.Id == merchantInvoice.Id);
await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
{
BOLT11 = merchantInvoice.BOLT11
@ -1726,6 +1739,15 @@ namespace BTCPayServer.Tests
var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id);
Assert.NotNull(invoice.PaidAt);
Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount);
// check list for store with paid invoice
var merchantInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC");
merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true);
Assert.NotEmpty(merchantInvoices);
Assert.Empty(merchantPendingInvoices);
// if the test ran too many times the invoice might be on a later page
if (merchantInvoices.Length < 100) Assert.Contains(merchantInvoices, i => i.Id == merchantInvoice.Id);
// Amount received might be bigger because of internal implementation shit from lightning
Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived);
@ -1733,7 +1755,6 @@ namespace BTCPayServer.Tests
Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight);
// As admin, can use the internal node through our store.
await user.MakeAdmin(true);
await user.RegisterInternalLightningNodeAsync("BTC");
@ -1743,7 +1764,7 @@ namespace BTCPayServer.Tests
await AssertPermissionError("btcpay.server.canuseinternallightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC"));
// However, even as a guest, you should be able to create an invoice
var guest = tester.NewAccount();
guest.GrantAccess(false);
await guest.GrantAccessAsync();
await user.AddGuest(guest.UserId);
client = await guest.CreateClient(Policies.CanCreateLightningInvoiceInStore);
await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest()

View file

@ -101,6 +101,14 @@ namespace BTCPayServer.Controllers.Greenfield
return base.GetInvoice(cryptoCode, id, cancellationToken);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/invoices")]
public override Task<IActionResult> GetInvoices(string cryptoCode, [FromQuery] bool? pendingOnly, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default)
{
return base.GetInvoices(cryptoCode, pendingOnly, offsetIndex, cancellationToken);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/invoices/pay")]

View file

@ -111,6 +111,14 @@ namespace BTCPayServer.Controllers.Greenfield
return base.GetInvoice(cryptoCode, id, cancellationToken);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices")]
public override Task<IActionResult> GetInvoices(string cryptoCode, [FromQuery] bool? pendingOnly, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default)
{
return base.GetInvoices(cryptoCode, pendingOnly, offsetIndex, cancellationToken);
}
[Authorize(Policy = Policies.CanCreateLightningInvoiceInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices")]

View file

@ -246,6 +246,14 @@ namespace BTCPayServer.Controllers.Greenfield
return inv == null ? this.CreateAPIError(404, "invoice-not-found", "Impossible to find a lightning invoice with this id") : Ok(ToModel(inv));
}
public virtual async Task<IActionResult> GetInvoices(string cryptoCode, [FromQuery] bool? pendingOnly, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default)
{
var lightningClient = await GetLightningClient(cryptoCode, false);
var param = new ListInvoicesParams { PendingOnly = pendingOnly, OffsetIndex = offsetIndex };
var invoices = await lightningClient.ListInvoices(param, cancellationToken);
return Ok(invoices.Select(ToModel));
}
public virtual async Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request, CancellationToken cancellationToken = default)
{
var lightningClient = await GetLightningClient(cryptoCode, false);
@ -303,7 +311,7 @@ namespace BTCPayServer.Controllers.Greenfield
private LightningInvoiceData ToModel(LightningInvoice invoice)
{
return new LightningInvoiceData
var data = new LightningInvoiceData
{
Amount = invoice.Amount,
Id = invoice.Id,
@ -313,6 +321,12 @@ namespace BTCPayServer.Controllers.Greenfield
BOLT11 = invoice.BOLT11,
ExpiresAt = invoice.ExpiresAt
};
if (invoice.CustomRecords != null)
{
data.CustomRecords = invoice.CustomRecords;
}
return data;
}
private LightningPaymentData ToModel(LightningPayment payment)

View file

@ -351,7 +351,7 @@ namespace BTCPayServer.Controllers.Greenfield
CancellationToken token = default)
{
return GetFromActionResult<LightningNodeBalanceData>(
await GetController<GreenfieldStoreLightningNodeApiController>().GetBalance(cryptoCode));
await GetController<GreenfieldStoreLightningNodeApiController>().GetBalance(cryptoCode, token));
}
public override async Task ConnectToLightningNode(string storeId, string cryptoCode,
@ -394,6 +394,13 @@ namespace BTCPayServer.Controllers.Greenfield
await GetController<GreenfieldStoreLightningNodeApiController>().GetInvoice(cryptoCode, invoiceId, token));
}
public override async Task<LightningInvoiceData[]> GetLightningInvoices(string storeId, string cryptoCode,
bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default)
{
return GetFromActionResult<LightningInvoiceData[]>(
await GetController<GreenfieldStoreLightningNodeApiController>().GetInvoices(cryptoCode, pendingOnly, offsetIndex, token));
}
public override async Task<LightningInvoiceData> CreateLightningInvoice(string storeId, string cryptoCode,
CreateLightningInvoiceRequest request, CancellationToken token = default)
{
@ -455,6 +462,13 @@ namespace BTCPayServer.Controllers.Greenfield
await GetController<GreenfieldInternalLightningNodeApiController>().GetInvoice(cryptoCode, invoiceId, token));
}
public override async Task<LightningInvoiceData[]> GetLightningInvoices(string cryptoCode,
bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default)
{
return GetFromActionResult<LightningInvoiceData[]>(
await GetController<GreenfieldInternalLightningNodeApiController>().GetInvoices(cryptoCode, pendingOnly, offsetIndex, token));
}
public override async Task<LightningInvoiceData> CreateLightningInvoice(string cryptoCode,
CreateLightningInvoiceRequest request,
CancellationToken token = default)

View file

@ -148,6 +148,11 @@
"amountReceived": {
"type": "string",
"description": "The amount received in millisatoshi"
},
"customRecords": {
"type": "object",
"nullable": true,
"description": "The custom TLV records attached to a keysend payment"
}
}
},

View file

@ -389,7 +389,6 @@
}
},
"/api/v1/server/lightning/{cryptoCode}/invoices/{id}": {
"get": {
"tags": [
@ -406,7 +405,8 @@
"type": "string"
},
"example": "BTC"
} , {
},
{
"name": "id",
"in": "path",
"required": true,
@ -446,6 +446,7 @@
]
}
},
"/api/v1/server/lightning/{cryptoCode}/invoices/pay": {
"post": {
"tags": [
@ -524,7 +525,76 @@
]
}
},
"/api/v1/server/lightning/{cryptoCode}/invoices": {
"get": {
"tags": [
"Lightning (Internal Node)"
],
"summary": "Get invoices",
"parameters": [
{
"name": "cryptoCode",
"in": "path",
"required": true,
"description": "The cryptoCode of the lightning-node to query",
"schema": {
"type": "string"
},
"example": "BTC"
},
{
"name": "pendingOnly",
"in": "query",
"required": false,
"description": "Limit to pending invoices only",
"schema": {
"type": "boolean",
"nullable": true,
"default": false
}
},
{
"name": "offsetIndex",
"in": "query",
"required": false,
"description": "The index of an invoice that will be used as the start of the list",
"schema": {
"type": "number",
"nullable": true,
"default": 0
}
}
],
"description": "View information about the lightning invoices",
"operationId": "InternalLightningNodeApi_GetInvoices",
"responses": {
"200": {
"description": "Lightning invoice data",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LightningInvoiceData"
}
}
}
}
},
"503": {
"description": "Unable to access the lightning node"
}
},
"security": [
{
"API_Key": [
"btcpay.server.canuseinternallightningnode"
],
"Basic": []
}
]
},
"post": {
"tags": [
"Lightning (Internal Node)"

View file

@ -609,6 +609,83 @@
}
},
"/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices": {
"get": {
"tags": [
"Lightning (Store)"
],
"summary": "Get invoices",
"parameters": [
{
"name": "cryptoCode",
"in": "path",
"required": true,
"description": "The cryptoCode of the lightning-node to query",
"schema": {
"type": "string"
},
"example": "BTC"
},
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store id with the lightning-node configuration to query",
"schema": {
"type": "string"
}
},
{
"name": "pendingOnly",
"in": "query",
"required": false,
"description": "Limit to pending invoices only",
"schema": {
"type": "boolean",
"nullable": true,
"default": false
}
},
{
"name": "offsetIndex",
"in": "query",
"required": false,
"description": "The index of an invoice that will be used as the start of the list",
"schema": {
"type": "number",
"nullable": true,
"default": 0
}
}
],
"description": "View information about the lightning invoices",
"operationId": "StoreLightningNodeApi_GetInvoices",
"responses": {
"200": {
"description": "Lightning invoice data",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LightningInvoiceData"
}
}
}
}
},
"503": {
"description": "Unable to access the lightning node"
}
},
"security": [
{
"API_Key": [
"btcpay.store.cancreatelightninginvoice"
],
"Basic": []
}
]
},
"post": {
"tags": [
"Lightning (Store)"