Greenfield: Lightning addresses API (#4546)

* Greenfield: Lightning addresses API

* add docs

* Apply suggestions from code review

Co-authored-by: d11n <mail@dennisreimann.de>
This commit is contained in:
Andrew Camilleri 2023-01-23 10:11:34 +01:00 committed by GitHub
parent 9086822b94
commit 1d2bebf17a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 478 additions and 2 deletions

View File

@ -0,0 +1,48 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<LightningAddressData[]> GetStoreLightningAddresses(string storeId,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning-addresses",
method: HttpMethod.Get), token);
return await HandleResponse<LightningAddressData[]>(response);
}
public virtual async Task<LightningAddressData> GetStoreLightningAddress(string storeId, string username,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning-addresses/{username}",
method: HttpMethod.Get), token);
return await HandleResponse<LightningAddressData>(response);
}
public virtual async Task RemoveStoreLightningAddress(string storeId, string username,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning-addresses/{username}",
method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<LightningAddressData> AddOrUpdateStoreLightningAddress(string storeId,
string username, LightningAddressData data,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning-addresses/{username}",
method: HttpMethod.Post, bodyPayload: data), token);
return await HandleResponse<LightningAddressData>(response);
}
}
}

View File

@ -0,0 +1,10 @@
namespace BTCPayServer.Client.Models;
public class LightningAddressData
{
public string Username { get; set; }
public string? CurrencyCode { get; set; }
public decimal? Min { get; set; }
public decimal? Max { get; set; }
}

View File

@ -2961,6 +2961,50 @@ namespace BTCPayServer.Tests
}
[Fact(Timeout =TestTimeout)]
[Trait("Integration", "Integration")]
public async Task StoreLightningAddressesAPITests()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var admin = tester.NewAccount();
await admin.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.Unrestricted);
var store = await adminClient.GetStore(admin.StoreId);
Assert.Empty(await adminClient.GetStorePaymentMethods(store.Id));
var store2 = (await adminClient.CreateStore(new CreateStoreRequest() {Name = "test2"})).Id;
var address1 = Guid.NewGuid().ToString("n").Substring(0, 8);
var address2 = Guid.NewGuid().ToString("n").Substring(0, 8);
Assert.Empty(await adminClient.GetStoreLightningAddresses(store.Id));
Assert.Empty(await adminClient.GetStoreLightningAddresses(store2));
await adminClient.AddOrUpdateStoreLightningAddress(store.Id, address1, new LightningAddressData());
await adminClient.AddOrUpdateStoreLightningAddress(store.Id, address1, new LightningAddressData()
{
Max = 1
});
await AssertAPIError("username-already-used", async () =>
{
await adminClient.AddOrUpdateStoreLightningAddress(store2, address1, new LightningAddressData());
});
Assert.Equal(1,Assert.Single(await adminClient.GetStoreLightningAddresses(store.Id)).Max);
Assert.Empty(await adminClient.GetStoreLightningAddresses(store2));
await adminClient.AddOrUpdateStoreLightningAddress(store2, address2, new LightningAddressData());
Assert.Single(await adminClient.GetStoreLightningAddresses(store.Id));
Assert.Single(await adminClient.GetStoreLightningAddresses(store2));
await AssertHttpError(404, async () =>
{
await adminClient.RemoveStoreLightningAddress(store2, address1);
});
await adminClient.RemoveStoreLightningAddress(store2, address2);
Assert.Empty(await adminClient.GetStoreLightningAddresses(store2));
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task StoreUsersAPITest()

View File

@ -0,0 +1,96 @@
#nullable enable
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.AuthenticationSchemes;
using LightningAddressData = BTCPayServer.Client.Models.LightningAddressData;
namespace BTCPayServer.Controllers.Greenfield
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class GreenfieldStoreLightningAddressesController : ControllerBase
{
private readonly LightningAddressService _lightningAddressService;
public GreenfieldStoreLightningAddressesController(
LightningAddressService lightningAddressService)
{
_lightningAddressService = lightningAddressService;
}
private LightningAddressData ToModel(BTCPayServer.Data.LightningAddressData data)
{
var blob = data.Blob.GetBlob<LightningAddressDataBlob>();
return new LightningAddressData()
{
Username = data.Username, Max = blob.Max, Min = blob.Min, CurrencyCode = blob.CurrencyCode
};
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning-addresses")]
public async Task<IActionResult> GetStoreLightningAddresses(string storeId)
{
return Ok((await _lightningAddressService.Get(new LightningAddressQuery() {StoreIds = new[] {storeId}}))
.Select(ToModel).ToArray());
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/lightning-addresses/{username}")]
public async Task<IActionResult> RemoveStoreLightningAddress(string storeId, string username)
{
if (await _lightningAddressService.Remove(username, storeId))
{
return Ok();
}
return
this.CreateAPIError(404, "lightning-address-not-found", "The lightning address was not present.");
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning-addresses/{username}")]
public async Task<IActionResult> GetStoreLightningAddress(string storeId, string username)
{
var res = await _lightningAddressService.Get(new LightningAddressQuery()
{
Usernames = new[] {username}, StoreIds = new[] {storeId},
});
return res?.Any() is true ? Ok(ToModel(res.First())) : this.CreateAPIError(404, "lightning-address-not-found", "The lightning address was not present.");
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning-addresses/{username}")]
public async Task<IActionResult> AddOrUpdateStoreLightningAddress(
string storeId, string username, LightningAddressData data)
{
if (data.Min <= 0)
{
ModelState.AddModelError(nameof(data.Min), "Minimum must be greater than 0 if provided.");
return this.CreateValidationError(ModelState);
}
if (await _lightningAddressService.Set(new Data.LightningAddressData()
{
StoreDataId = storeId,
Username = username,
Blob = new LightningAddressDataBlob()
{
Max = data.Max, Min = data.Min, CurrencyCode = data.CurrencyCode
}.SerializeBlob()
}))
{
return await GetStoreLightningAddress(storeId, username);
}
return this.CreateAPIError((int)HttpStatusCode.BadRequest, "username-already-used",
"The username is already in use by another store.");
}
}
}

View File

@ -26,6 +26,7 @@ using NBXplorer.Models;
using Newtonsoft.Json.Linq;
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
using Language = BTCPayServer.Client.Models.Language;
using LightningAddressData = BTCPayServer.Client.Models.LightningAddressData;
using NotificationData = BTCPayServer.Client.Models.NotificationData;
using PaymentRequestData = BTCPayServer.Client.Models.PaymentRequestData;
using PayoutData = BTCPayServer.Client.Models.PayoutData;
@ -1232,5 +1233,27 @@ namespace BTCPayServer.Controllers.Greenfield
{
return GetFromActionResult<PayoutData>(await GetController<GreenfieldPullPaymentController>().GetStorePayout(storeId, payoutId));
}
public override async Task<LightningAddressData[]> GetStoreLightningAddresses(string storeId,
CancellationToken token = default)
{
return GetFromActionResult<LightningAddressData[]>(await GetController<GreenfieldStoreLightningAddressesController>().GetStoreLightningAddresses(storeId));
}
public override async Task<LightningAddressData> GetStoreLightningAddress(string storeId, string username, CancellationToken token = default)
{
return GetFromActionResult<LightningAddressData>(await GetController<GreenfieldStoreLightningAddressesController>().GetStoreLightningAddress(storeId, username));
}
public override async Task<LightningAddressData> AddOrUpdateStoreLightningAddress(string storeId, string username, LightningAddressData data,
CancellationToken token = default)
{
return GetFromActionResult<LightningAddressData>(await GetController<GreenfieldStoreLightningAddressesController>().AddOrUpdateStoreLightningAddress(storeId, username, data));
}
public override async Task RemoveStoreLightningAddress(string storeId, string username, CancellationToken token = default)
{
HandleActionResult(await GetController<GreenfieldStoreLightningAddressesController>().RemoveStoreLightningAddress(storeId, username));
}
}
}

View File

@ -60,8 +60,9 @@ public class LightningAddressService
public async Task<bool> Set(LightningAddressData data)
{
data.Username = NormalizeUsername(data.Username);
await using var context = _applicationDbContextFactory.CreateContext();
var result = (await GetCore(context, new LightningAddressQuery() { Usernames = new[] { data.Username } }))
var result = (await GetCore(context, new LightningAddressQuery() { Usernames = new[] { data.Username} }))
.FirstOrDefault();
if (result is not null)
{
@ -73,7 +74,6 @@ public class LightningAddressService
context.Remove(result);
}
data.Username = NormalizeUsername(data.Username);
await context.AddAsync(data);
await context.SaveChangesAsync();
_memoryCache.Remove(GetKey(data.Username));
@ -82,6 +82,7 @@ public class LightningAddressService
public async Task<bool> Remove(string username, string? storeId = null)
{
username = NormalizeUsername(username);
await using var context = _applicationDbContextFactory.CreateContext();
var x = (await GetCore(context, new LightningAddressQuery() { Usernames = new[] { username } })).FirstOrDefault();
if (x is null)

View File

@ -30,6 +30,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using NBitcoin;
using Newtonsoft.Json;
using LightningAddressData = BTCPayServer.Data.LightningAddressData;
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
namespace BTCPayServer

View File

@ -33,6 +33,7 @@ using NBXplorer;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PeterO.Cbor;
using LightningAddressData = BTCPayServer.Data.LightningAddressData;
using PayoutData = BTCPayServer.Data.PayoutData;
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
using StoreData = BTCPayServer.Data.StoreData;

View File

@ -0,0 +1,252 @@
{
"paths": {
"/api/v1/stores/{storeId}/lightning-addresses": {
"get": {
"tags": [
"Lightning address"
],
"summary": "Get store configured lightning addresses",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store to fetch",
"schema": {
"type": "string"
}
}
],
"description": "Get store configured lightning addresses",
"operationId": "StoreLightningAddresses_GetStoreLightningAddresses",
"responses": {
"200": {
"description": "The lightning addresses configured in the store",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LightningAddressData"
}
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden to view the specified store"
},
"404": {
"description": "The key is not found for this store/wallet"
}
},
"security": [
{
"API_Key": [
"btcpay.store.canviewstoresettings"
],
"Basic": []
}
]
}
},
"/api/v1/stores/{storeId}/lightning-addresses/{username}": {
"get": {
"tags": [
"Lightning address"
],
"summary": "Get store configured lightning address",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store to fetch",
"schema": {
"type": "string"
}
},
{
"name": "username",
"in": "path",
"required": true,
"description": "The lightning address username",
"schema": {
"type": "string"
}
}
],
"description": "Get store configured lightning address",
"operationId": "StoreLightningAddresses_GetStoreLightningAddress",
"responses": {
"200": {
"description": "The lightning address configured in the store",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LightningAddressData"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden to view the specified store"
},
"404": {
"description": "The key is not found for this store/wallet"
}
},
"security": [
{
"API_Key": [
"btcpay.store.canviewstoresettings"
],
"Basic": []
}
]
},
"post": {
"tags": [
"Lightning address"
],
"summary": "Add or update store configured lightning address",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store to fetch",
"schema": {
"type": "string"
}
},
{
"name": "username",
"in": "path",
"required": true,
"description": "the lightning address username",
"schema": {
"type": "string"
}
}
],
"description": "Add or update store configured lightning address",
"operationId": "StoreLightningAddresses_AddOrUpdateStoreLightningAddress",
"requestBody": {
"x-name": "request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LightningAddressData"
}
}
},
"required": true,
"x-position": 1
},
"responses": {
"200": {
"description": "The lightning address configured in the store",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LightningAddressData"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden to view the specified store"
},
"404": {
"description": "The key is not found for this store/wallet"
}
},
"security": [
{
"API_Key": [
"btcpay.store.canmodifystoresettings"
],
"Basic": []
}
]
},
"delete": {
"tags": [
"Lightning address"
],
"summary": "Remove configured lightning address",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store to fetch",
"schema": {
"type": "string"
}
},
{
"name": "username",
"in": "path",
"required": true,
"description": "The lightning address username",
"schema": {
"type": "string"
}
}
],
"description": "Remove store configured lightning address",
"operationId": "StoreLightningAddresses_RemoveStoreLightningAddress",
"responses": {
"200": {
"description": "Lightning address removed"
},
"403": {
"description": "If you are authenticated but forbidden to view the specified store"
},
"404": {
"description": "The key is not found for this store/wallet"
}
},
"security": [
{
"API_Key": [
"btcpay.store.canmodifystoresettings"
],
"Basic": []
}
]
}
},
"components": {
"schemas": {
"LightningAddressData": {
"type": "object",
"additionalProperties": false,
"properties": {
"username": {
"type": "string",
"description": "The username of the lightning address"
},
"currencyCode": {
"type": "string",
"nullable": true,
"description": "The currency to generate the invoices for this lightning address in. Leave null lto use the store default."
},
"min": {
"type": "string",
"nullable": true,
"description": "The minimum amount in sats this ln address allows"
},
"max": {
"type": "string",
"nullable": true,
"description": "The maximum amount in sats this ln address allows"
}
}
}
}
}
}
}