mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 09:54:30 +01:00
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:
parent
9086822b94
commit
1d2bebf17a
48
BTCPayServer.Client/BTCPayServerClient.LightningAddresses.cs
Normal file
48
BTCPayServer.Client/BTCPayServerClient.LightningAddresses.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
10
BTCPayServer.Client/Models/LightningAddressData.cs
Normal file
10
BTCPayServer.Client/Models/LightningAddressData.cs
Normal 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; }
|
||||
|
||||
}
|
@ -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()
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user