Fix lightning implementation, docs and tests

This commit is contained in:
nicolas.dorier 2020-06-08 23:40:58 +09:00
parent a9dbbe1955
commit 8dd6ecc0b8
No known key found for this signature in database
GPG key ID: 6618763EF09186FE
36 changed files with 615 additions and 528 deletions

View file

@ -5,8 +5,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NBitcoin" Version="5.0.39" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.1.0.22" />
<PackageReference Include="NBitcoin" Version="5.0.40" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>

View file

@ -25,7 +25,7 @@ namespace BTCPayServer.Client
public virtual async Task RevokeCurrentAPIKeyInfo(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current", null, HttpMethod.Delete), token);
HandleResponse(response);
await HandleResponse(response);
}
public virtual async Task RevokeAPIKey(string apikey, CancellationToken token = default)
@ -33,7 +33,7 @@ namespace BTCPayServer.Client
if (apikey == null)
throw new ArgumentNullException(nameof(apikey));
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/api-keys/{apikey}", null, HttpMethod.Delete), token);
HandleResponse(response);
await HandleResponse(response);
}
}
}

View file

@ -26,7 +26,7 @@ namespace BTCPayServer.Client
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/connect", bodyPayload: request,
method: HttpMethod.Post), token);
HandleResponse(response);
await HandleResponse(response);
}
public async Task<IEnumerable<LightningChannelData>> GetLightningNodeChannels(string cryptoCode,
@ -61,9 +61,9 @@ namespace BTCPayServer.Client
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/pay", bodyPayload: request,
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices/pay", bodyPayload: request,
method: HttpMethod.Post), token);
HandleResponse(response);
await HandleResponse(response);
}
public async Task<LightningInvoiceData> GetLightningInvoice(string cryptoCode,

View file

@ -4,6 +4,7 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
namespace BTCPayServer.Client
{
@ -26,7 +27,7 @@ namespace BTCPayServer.Client
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/connect", bodyPayload: request,
method: HttpMethod.Post), token);
HandleResponse(response);
await HandleResponse(response);
}
public async Task<IEnumerable<LightningChannelData>> GetLightningNodeChannels(string storeId, string cryptoCode,
@ -62,9 +63,9 @@ namespace BTCPayServer.Client
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/pay", bodyPayload: request,
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/pay", bodyPayload: request,
method: HttpMethod.Post), token);
HandleResponse(response);
await HandleResponse(response);
}
public async Task<LightningInvoiceData> GetLightningInvoice(string storeId, string cryptoCode,

View file

@ -31,7 +31,7 @@ namespace BTCPayServer.Client
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}",
method: HttpMethod.Delete), token);
HandleResponse(response);
await HandleResponse(response);
}
public virtual async Task<PaymentRequestData> CreatePaymentRequest(string storeId,

View file

@ -26,7 +26,7 @@ namespace BTCPayServer.Client
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}", method: HttpMethod.Delete), token);
HandleResponse(response);
await HandleResponse(response);
}
public virtual async Task<StoreData> CreateStore(CreateStoreRequest request, CancellationToken token = default)

View file

@ -43,14 +43,25 @@ namespace BTCPayServer.Client
_httpClient = httpClient ?? new HttpClient();
}
protected void HandleResponse(HttpResponseMessage message)
protected async Task HandleResponse(HttpResponseMessage message)
{
if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
{
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(await message.Content.ReadAsStringAsync()); ;
throw new GreenFieldValidationException(err);
}
else if (message.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
var err = JsonConvert.DeserializeObject<Models.GreenfieldAPIError>(await message.Content.ReadAsStringAsync());
throw new GreenFieldAPIException(err);
}
message.EnsureSuccessStatusCode();
}
protected async Task<T> HandleResponse<T>(HttpResponseMessage message)
{
HandleResponse(message);
await HandleResponse(message);
return JsonConvert.DeserializeObject<T>(await message.Content.ReadAsStringAsync());
}

View file

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Linq;
namespace BTCPayServer.Client
{
public class GreenFieldAPIException : Exception
{
public GreenFieldAPIException(Models.GreenfieldAPIError error):base(error.Message)
{
if (error == null)
throw new ArgumentNullException(nameof(error));
APIError = error;
}
public Models.GreenfieldAPIError APIError { get; }
}
}

View file

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public class GreenFieldValidationException : Exception
{
public GreenFieldValidationException(Models.GreenfieldValidationError[] errors) : base(BuildMessage(errors))
{
ValidationErrors = errors;
}
private static string BuildMessage(GreenfieldValidationError[] errors)
{
if (errors == null)
throw new ArgumentNullException(nameof(errors));
StringBuilder builder = new StringBuilder();
foreach (var error in errors)
{
builder.AppendLine($"{error.Path}: {error.Message}");
}
return builder.ToString();
}
public Models.GreenfieldValidationError[] ValidationErrors { get; }
}
}

View file

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.JsonConverters
{
public class NodeUriJsonConverter : JsonConverter<NodeInfo>
{
public override NodeInfo ReadJson(JsonReader reader, Type objectType, [AllowNull] NodeInfo existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.String)
throw new JsonObjectException(reader.Path, "Unexpected token type for NodeUri");
if (NodeInfo.TryParse((string)reader.Value, out var info))
return info;
throw new JsonObjectException(reader.Path, "Invalid NodeUri");
}
public override void WriteJson(JsonWriter writer, [AllowNull] NodeInfo value, JsonSerializer serializer)
{
if (value is NodeInfo)
writer.WriteValue(value.ToString());
}
}
}

View file

@ -1,10 +1,24 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Lightning;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class ConnectToNodeRequest
{
public string NodeInfo { get; set; }
public string NodeId { get; set; }
public string NodeHost { get; set; }
public int NodePort { get; set; }
public ConnectToNodeRequest()
{
}
public ConnectToNodeRequest(NodeInfo nodeInfo)
{
NodeURI = nodeInfo;
}
[JsonConverter(typeof(NodeUriJsonConverter))]
[JsonProperty("nodeURI")]
public NodeInfo NodeURI { get; set; }
}
}

View file

@ -1,4 +1,5 @@
using System;
using System.Security.Cryptography;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.JsonConverters;
using Newtonsoft.Json;
@ -7,9 +8,20 @@ namespace BTCPayServer.Client.Models
{
public class CreateLightningInvoiceRequest
{
[JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))]
public CreateLightningInvoiceRequest()
{
}
public CreateLightningInvoiceRequest(LightMoney amount, string description, TimeSpan expiry)
{
Amount = amount;
Description = description;
Expiry = expiry;
}
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney Amount { get; set; }
public string Description { get; set; }
[JsonConverter(typeof(JsonConverters.TimeSpanJsonConverter))]
public TimeSpan Expiry { get; set; }
public bool PrivateRouteHints { get; set; }

View file

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Client.Models
{
public class GreenfieldAPIError
{
public GreenfieldAPIError()
{
}
public GreenfieldAPIError(string code, string message)
{
if (code == null)
throw new ArgumentNullException(nameof(code));
if (message == null)
throw new ArgumentNullException(nameof(message));
Code = code;
Message = message;
}
public string Code { get; set; }
public string Message { get; set; }
}
}

View file

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Client.Models
{
public class GreenfieldValidationError
{
public GreenfieldValidationError()
{
}
public GreenfieldValidationError(string path, string message)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
if (message == null)
throw new ArgumentNullException(nameof(message));
Path = path;
Message = message;
}
public string Path { get; set; }
public string Message { get; set; }
}
}

View file

@ -10,19 +10,21 @@ namespace BTCPayServer.Client.Models
{
public string Id { get; set; }
[JsonProperty(ItemConverterType = typeof(StringEnumConverter))]
[JsonConverter(typeof(StringEnumConverter))]
public LightningInvoiceStatus Status { get; set; }
[JsonProperty("BOLT11")]
public string BOLT11 { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? PaidAt { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset ExpiresAt { get; set; }
[JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))]
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney Amount { get; set; }
[JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))]
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney AmountReceived { get; set; }
}
}

View file

@ -9,7 +9,8 @@ namespace BTCPayServer.Client.Models
{
public class LightningNodeInformationData
{
public IEnumerable<string> NodeInfoList { get; set; }
[JsonProperty("nodeURIs", ItemConverterType = typeof(NodeUriJsonConverter))]
public NodeInfo[] NodeURIs { get; set; }
public int BlockHeight { get; set; }
}
@ -21,10 +22,10 @@ namespace BTCPayServer.Client.Models
public bool IsActive { get; set; }
[JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))]
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney Capacity { get; set; }
[JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))]
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney LocalBalance { get; set; }
public string ChannelPoint { get; set; }

View file

@ -1,3 +1,5 @@
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Lightning;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
@ -7,11 +9,13 @@ namespace BTCPayServer.Client.Models
{
public class OpenLightningChannelRequest
{
public ConnectToNodeRequest Node { get; set; }
[JsonProperty(ItemConverterType = typeof(MoneyJsonConverter))]
[JsonConverter(typeof(NodeUriJsonConverter))]
[JsonProperty("nodeURI")]
public NodeInfo NodeURI { get; set; }
[JsonConverter(typeof(MoneyJsonConverter))]
public Money ChannelAmount { get; set; }
[JsonProperty(ItemConverterType = typeof(FeeRateJsonConverter))]
[JsonConverter(typeof(FeeRateJsonConverter))]
public FeeRate FeeRate { get; set; }
}
}

View file

@ -2,6 +2,7 @@ namespace BTCPayServer.Client.Models
{
public class PayLightningInvoiceRequest
{
public string Invoice { get; set; }
[Newtonsoft.Json.JsonProperty("BOLT11")]
public string BOLT11 { get; set; }
}
}

View file

@ -8,7 +8,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.4" />
</ItemGroup>

View file

@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection.Metadata;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
@ -16,6 +17,7 @@ using NBitpayClient;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using Org.BouncyCastle.Utilities.Collections;
using Xunit;
using Xunit.Abstractions;
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
@ -105,13 +107,13 @@ namespace BTCPayServer.Tests
tester.PayTester.DisableRegistration = true;
await tester.StartAsync();
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
await AssertHttpError(422,
await AssertValidationError(new[] { "Email", "Password" },
async () => await unauthClient.CreateUser(new CreateApplicationUserRequest()));
await AssertHttpError(422,
await AssertValidationError(new[] { "Password" },
async () => await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test@gmail.com"}));
// Pass too simple
await AssertHttpError(422,
await AssertValidationError(new[] { "Password" },
async () => await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test3@gmail.com", Password = "a"}));
@ -123,7 +125,7 @@ namespace BTCPayServer.Tests
new CreateApplicationUserRequest() {Email = "test2@gmail.com", Password = "abceudhqw"});
// Duplicate email
await AssertHttpError(422,
await AssertValidationError(new[] { "Email" },
async () => await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test2@gmail.com", Password = "abceudhqw"}));
@ -252,6 +254,18 @@ namespace BTCPayServer.Tests
}
}
private async Task AssertValidationError(string[] fields, Func<Task> act)
{
var remainingFields = fields.ToHashSet();
var ex = await Assert.ThrowsAsync<GreenFieldValidationException>(act);
foreach (var field in fields)
{
Assert.Contains(field, ex.ValidationErrors.Select(e => e.Path).ToArray());
remainingFields.Remove(field);
}
Assert.Empty(remainingFields);
}
private async Task AssertHttpError(int code, Func<Task> act)
{
var ex = await Assert.ThrowsAsync<HttpRequestException>(act);
@ -303,17 +317,17 @@ namespace BTCPayServer.Tests
});
Assert.NotNull(newUser2);
await Assert.ThrowsAsync<HttpRequestException>(async () =>
await AssertValidationError(new[] { "Email" }, async () =>
await clientServer.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}", Password = Guid.NewGuid().ToString()
}));
await Assert.ThrowsAsync<HttpRequestException>(async () =>
await AssertValidationError(new[] { "Password" }, async () =>
await clientServer.CreateUser(
new CreateApplicationUserRequest() {Email = $"{Guid.NewGuid()}@g.com",}));
await Assert.ThrowsAsync<HttpRequestException>(async () =>
await AssertValidationError(new[] { "Email" }, async () =>
await clientServer.CreateUser(
new CreateApplicationUserRequest() {Password = Guid.NewGuid().ToString()}));
}
@ -375,16 +389,16 @@ namespace BTCPayServer.Tests
//create payment request
//validation errors
await AssertHttpError(422, async () =>
await AssertValidationError(new[] { "Amount", "Currency" }, async () =>
{
await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() {Title = "A"});
});
await AssertHttpError(422, async () =>
await AssertValidationError(new[] { "Amount" }, async () =>
{
await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() {Title = "A", Currency = "BTC", Amount = 0});
});
await AssertHttpError(422, async () =>
await AssertValidationError(new[] { "Currency" }, async () =>
{
await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() {Title = "A", Currency = "helloinvalid", Amount = 1});

View file

@ -252,23 +252,39 @@ namespace BTCPayServer.Tests
public bool IsAdmin { get; internal set; }
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true)
{
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult();
}
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true)
{
var storeController = this.GetController<StoresController>();
string connectionString = null;
if (connectionType == LightningConnectionType.Charge)
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
{
if (isMerchant)
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
else
throw new NotSupportedException();
}
else if (connectionType == LightningConnectionType.CLightning)
connectionString = "type=clightning;server=" +
((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
{
if (isMerchant)
connectionString = "type=clightning;server=" +
((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
else
connectionString = "type=clightning;server=" +
((CLightningClient)parent.CustomerLightningD).Address.AbsoluteUri;
}
else if (connectionType == LightningConnectionType.LndREST)
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
{
if (isMerchant)
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else
throw new NotSupportedException();
}
else
throw new NotSupportedException(connectionType.ToString());

View file

@ -64,6 +64,7 @@ using MemoryCache = Microsoft.Extensions.Caching.Memory.MemoryCache;
using Newtonsoft.Json.Schema;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using TwentyTwenty.Storage;
namespace BTCPayServer.Tests
{
@ -742,6 +743,91 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanUseLightningAPI()
{
using (var tester = ServerTester.Create())
{
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess(true);
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false);
var merchant = tester.NewAccount();
merchant.GrantAccess(true);
merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST);
var merchantClient = await merchant.CreateClient($"btcpay.store.canuselightningnode:{merchant.StoreId}");
var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(new LightMoney(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("btcpay.server.canuseinternallightningnode");
var err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels("BTC"));
Assert.Contains("503", err.Message);
// Not permission for the store!
err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels(user.StoreId, "BTC"));
Assert.Contains("403", err.Message);
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
{
Amount = LightMoney.Satoshis(1000),
Description = "lol",
Expiry = TimeSpan.FromSeconds(400),
PrivateRouteHints = false
});
var chargeInvoice = invoiceData;
Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
client = await user.CreateClient($"btcpay.store.canuselightningnode:{user.StoreId}");
// Not permission for the server
err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels("BTC"));
Assert.Contains("403", err.Message);
var data = await client.GetLightningNodeChannels(user.StoreId, "BTC");
Assert.Equal(2, data.Count());
BitcoinAddress.Create(await client.GetLightningDepositAddress(user.StoreId, "BTC"), Network.RegTest);
invoiceData = await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest()
{
Amount = LightMoney.Satoshis(1000),
Description = "lol",
Expiry = TimeSpan.FromSeconds(400),
PrivateRouteHints = false
});
Assert.NotNull(await client.GetLightningInvoice(user.StoreId, "BTC", invoiceData.Id));
await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
{
BOLT11 = merchantInvoice.BOLT11
});
await Assert.ThrowsAsync<GreenFieldValidationException>(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
{
BOLT11 = "lol"
}));
var validationErr = await Assert.ThrowsAsync<GreenFieldValidationException>(async () => await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest()
{
Amount = -1,
Expiry = TimeSpan.FromSeconds(-1),
Description = null
}));
Assert.Equal(2, validationErr.ValidationErrors.Length);
var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id);
Assert.NotNull(invoice.PaidAt);
Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount);
// Amount received might be bigger because of internal implementation shit from lightning
Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived);
var info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight);
}
}
async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user)
{
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()

View file

@ -30,7 +30,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Hwi" Version="1.1.3" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.15" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.0" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />

View file

@ -42,7 +42,7 @@ namespace BTCPayServer.Controllers.GreenField
public async Task<ActionResult<ApiKeyData>> CreateKey(CreateApiKeyRequest request)
{
if (request is null)
return BadRequest();
return NotFound();
var key = new APIKeyData()
{
Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)),
@ -74,7 +74,7 @@ namespace BTCPayServer.Controllers.GreenField
public async Task<IActionResult> RevokeKey(string apikey)
{
if (string.IsNullOrEmpty(apikey))
return BadRequest();
return NotFound();
if (await _apiKeyRepository.Remove(apikey, _userManager.GetUserId(User)))
return Ok();
else

View file

@ -1,26 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Client.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace BTCPayServer.Controllers.GreenField
{
public static class GreenFieldUtils
{
public static IActionResult GetValidationResponse(this ControllerBase controller)
public static IActionResult CreateValidationError(this ControllerBase controller, ModelStateDictionary modelState)
{
return controller.UnprocessableEntity( new ValidationProblemDetails(controller.ModelState));
}
public static IActionResult GetExceptionResponse(this ControllerBase controller, Exception e)
{
return GetGeneralErrorResponse(controller, e.Message);
}
public static IActionResult GetGeneralErrorResponse(this ControllerBase controller, string error)
{
return controller.BadRequest( new ProblemDetails()
List<GreenfieldValidationError> errors = new List<GreenfieldValidationError>();
foreach (var error in modelState)
{
Detail = error
});
foreach (var errorMessage in error.Value.Errors)
{
errors.Add(new GreenfieldValidationError(error.Key, errorMessage.ErrorMessage));
}
}
return controller.UnprocessableEntity(errors.ToArray());
}
public static IActionResult CreateAPIError(this ControllerBase controller, string errorCode, string errorMessage)
{
return controller.BadRequest(new GreenfieldAPIError(errorCode, errorMessage));
}
}
}

View file

@ -13,6 +13,7 @@ namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[LightningUnavailableExceptionFilter]
public class InternalLightningNodeApiController : LightningNodeApiController
{
private readonly BTCPayServerOptions _btcPayServerOptions;
@ -64,7 +65,7 @@ namespace BTCPayServer.Controllers.GreenField
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/address")]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/address")]
public override Task<IActionResult> GetDepositAddress(string cryptoCode)
{
return base.GetDepositAddress(cryptoCode);

View file

@ -17,6 +17,7 @@ namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[LightningUnavailableExceptionFilter]
public class StoreLightningNodeApiController : LightningNodeApiController
{
private readonly BTCPayServerOptions _btcPayServerOptions;
@ -65,7 +66,7 @@ namespace BTCPayServer.Controllers.GreenField
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/address")]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/address")]
public override Task<IActionResult> GetDepositAddress(string cryptoCode)
{
return base.GetDepositAddress(cryptoCode);
@ -81,7 +82,7 @@ namespace BTCPayServer.Controllers.GreenField
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/{id}")]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/{id}")]
public override Task<IActionResult> GetInvoice(string cryptoCode, string id)
{
return base.GetInvoice(cryptoCode, id);

View file

@ -5,12 +5,30 @@ using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Payments.Changelly.Models;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using NBitcoin;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers.GreenField
{
public class LightningUnavailableExceptionFilter : Attribute, IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if (context.Exception is NBitcoin.JsonConverters.JsonObjectException jsonObject)
{
context.Result = new ObjectResult(new GreenfieldValidationError(jsonObject.Path, jsonObject.Message));
}
else
{
context.Result = new StatusCodeResult(503);
}
context.ExceptionHandled = true;
}
}
public abstract class LightningNodeApiController : Controller
{
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
@ -32,20 +50,12 @@ namespace BTCPayServer.Controllers.GreenField
{
return NotFound();
}
try
var info = await lightningClient.GetInfo();
return Ok(new LightningNodeInformationData()
{
var info = await lightningClient.GetInfo();
return Ok(new LightningNodeInformationData()
{
BlockHeight = info.BlockHeight,
NodeInfoList = info.NodeInfoList.Select(nodeInfo => nodeInfo.ToString())
});
}
catch (Exception e)
{
return this.GetExceptionResponse(e);
}
BlockHeight = info.BlockHeight,
NodeURIs = info.NodeInfoList.Select(nodeInfo => nodeInfo).ToArray()
});
}
public virtual async Task<IActionResult> ConnectToNode(string cryptoCode, ConnectToNodeRequest request)
@ -56,23 +66,23 @@ namespace BTCPayServer.Controllers.GreenField
return NotFound();
}
if (TryGetNodeInfo(request, out var nodeInfo))
if (request?.NodeURI is null)
{
ModelState.AddModelError(nameof(request.NodeId), "A valid node info was not provided to connect to");
ModelState.AddModelError(nameof(request.NodeURI), "A valid node info was not provided to connect to");
}
if (CheckValidation(out var errorActionResult))
if (!ModelState.IsValid)
{
return errorActionResult;
return this.CreateValidationError(ModelState);
}
try
var result = await lightningClient.ConnectTo(request.NodeURI);
switch (result)
{
await lightningClient.ConnectTo(nodeInfo);
}
catch (Exception e)
{
return this.GetExceptionResponse(e);
case ConnectionResult.Ok:
return Ok();
case ConnectionResult.CouldNotConnect:
return this.CreateAPIError("could-not-connect", "Could not connect to the remote node");
}
return Ok();
@ -86,26 +96,19 @@ namespace BTCPayServer.Controllers.GreenField
return NotFound();
}
try
var channels = await lightningClient.ListChannels();
return Ok(channels.Select(channel => new LightningChannelData()
{
var channels = await lightningClient.ListChannels();
return Ok(channels.Select(channel => new LightningChannelData()
{
Capacity = channel.Capacity,
ChannelPoint = channel.ChannelPoint.ToString(),
IsActive = channel.IsActive,
IsPublic = channel.IsPublic,
LocalBalance = channel.LocalBalance,
RemoteNode = channel.RemoteNode.ToString()
}));
}
catch (Exception e)
{
return this.GetExceptionResponse(e);
}
Capacity = channel.Capacity,
ChannelPoint = channel.ChannelPoint.ToString(),
IsActive = channel.IsActive,
IsPublic = channel.IsPublic,
LocalBalance = channel.LocalBalance,
RemoteNode = channel.RemoteNode.ToString()
}));
}
public virtual async Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
@ -114,9 +117,9 @@ namespace BTCPayServer.Controllers.GreenField
return NotFound();
}
if (TryGetNodeInfo(request.Node, out var nodeInfo))
if (request?.NodeURI is null)
{
ModelState.AddModelError(nameof(request.Node),
ModelState.AddModelError(nameof(request.NodeURI),
"A valid node info was not provided to open a channel with");
}
@ -138,28 +141,43 @@ namespace BTCPayServer.Controllers.GreenField
ModelState.AddModelError(nameof(request.FeeRate), "FeeRate must be more than 0");
}
if (CheckValidation(out var errorActionResult))
if (ModelState.IsValid)
{
return errorActionResult;
return this.CreateValidationError(ModelState);
}
try
var response = await lightningClient.OpenChannel(new Lightning.OpenChannelRequest()
{
var response = await lightningClient.OpenChannel(new Lightning.OpenChannelRequest()
{
ChannelAmount = request.ChannelAmount, FeeRate = request.FeeRate, NodeInfo = nodeInfo
});
if (response.Result == OpenChannelResult.Ok)
{
ChannelAmount = request.ChannelAmount,
FeeRate = request.FeeRate,
NodeInfo = request.NodeURI
});
string errorCode, errorMessage;
switch (response.Result)
{
case OpenChannelResult.Ok:
return Ok();
}
return this.GetGeneralErrorResponse(response.Result.ToString());
}
catch (Exception e)
{
return this.GetExceptionResponse(e);
case OpenChannelResult.AlreadyExists:
errorCode = "channel-already-exists";
errorMessage = "The channel already exists";
break;
case OpenChannelResult.CannotAffordFunding:
errorCode = "cannot-afford-funding";
errorMessage = "Not enough money to open a channel";
break;
case OpenChannelResult.NeedMoreConf:
errorCode = "need-more-confirmations";
errorMessage = "Need to wait for more confirmations";
break;
case OpenChannelResult.PeerNotConnected:
errorCode = "peer-not-connected";
errorMessage = "Not connected to peer";
break;
default:
throw new NotSupportedException("Unknown OpenChannelResult");
}
return this.CreateAPIError(errorCode, errorMessage);
}
public virtual async Task<IActionResult> GetDepositAddress(string cryptoCode)
@ -170,7 +188,7 @@ namespace BTCPayServer.Controllers.GreenField
return NotFound();
}
return Ok((await lightningClient.GetDepositAddress()).ToString());
return Ok(new JValue((await lightningClient.GetDepositAddress()).ToString()));
}
public virtual async Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice)
@ -182,85 +200,78 @@ namespace BTCPayServer.Controllers.GreenField
return NotFound();
}
try
if (lightningInvoice?.BOLT11 is null ||
!BOLT11PaymentRequest.TryParse(lightningInvoice.BOLT11, out _, network.NBitcoinNetwork))
{
BOLT11PaymentRequest.TryParse(lightningInvoice.Invoice, out var bolt11PaymentRequest, network.NBitcoinNetwork);
}
catch (Exception)
{
ModelState.AddModelError(nameof(lightningInvoice.Invoice), "The BOLT11 invoice was invalid.");
ModelState.AddModelError(nameof(lightningInvoice.BOLT11), "The BOLT11 invoice was invalid.");
}
if (CheckValidation(out var errorActionResult))
if (!ModelState.IsValid)
{
return errorActionResult;
return this.CreateValidationError(ModelState);
}
var result = await lightningClient.Pay(lightningInvoice.Invoice);
var result = await lightningClient.Pay(lightningInvoice.BOLT11);
switch (result.Result)
{
case PayResult.CouldNotFindRoute:
return this.GetGeneralErrorResponse("Could not find route");
return this.CreateAPIError("could-not-find-route", "Impossible to find a route to the peer");
case PayResult.Error:
return this.GetGeneralErrorResponse(result.ErrorDetail);
return this.CreateAPIError("generic-error", result.ErrorDetail);
case PayResult.Ok:
return Ok();
default:
throw new NotSupportedException("Unsupported Payresult");
}
return Ok();
}
public virtual async Task<IActionResult> GetInvoice(string cryptoCode, string id)
{
var lightningClient = await GetLightningClient(cryptoCode, false);
if (lightningClient == null)
{
return NotFound();
}
try
var inv = await lightningClient.GetInvoice(id);
if (inv == null)
{
var inv = await lightningClient.GetInvoice(id);
if (inv == null)
{
return NotFound();
}
return Ok(ToModel(inv));
}
catch (Exception e)
{
return this.GetExceptionResponse(e);
return NotFound();
}
return Ok(ToModel(inv));
}
public virtual async Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request)
{
var lightningClient = await GetLightningClient(cryptoCode, false);
if (lightningClient == null)
{
return NotFound();
}
if (CheckValidation(out var errorActionResult))
if (request.Amount < LightMoney.Zero)
{
return errorActionResult;
ModelState.AddModelError(nameof(request.Amount), "Amount should be more or equals to 0");
}
try
if (request.Expiry <= TimeSpan.Zero)
{
var invoice = await lightningClient.CreateInvoice(
new CreateInvoiceParams(request.Amount, request.Description, request.Expiry)
{
PrivateRouteHints = request.PrivateRouteHints
},
CancellationToken.None);
ModelState.AddModelError(nameof(request.Expiry), "Expiry should be more than 0");
}
return Ok(ToModel(invoice));
}
catch (Exception e)
if (!ModelState.IsValid)
{
return this.GetExceptionResponse(e);
return this.CreateValidationError(ModelState);
}
var invoice = await lightningClient.CreateInvoice(
new CreateInvoiceParams(request.Amount, request.Description, request.Expiry)
{
PrivateRouteHints = request.PrivateRouteHints
},
CancellationToken.None);
return Ok(ToModel(invoice));
}
private LightningInvoiceData ToModel(LightningInvoice invoice)
@ -277,40 +288,12 @@ namespace BTCPayServer.Controllers.GreenField
};
}
private bool CheckValidation(out IActionResult result)
{
if (!ModelState.IsValid)
{
result = this.GetValidationResponse();
return true;
}
result = null;
return false;
}
protected bool CanUseInternalLightning(bool doingAdminThings)
{
return (_btcPayServerEnvironment.IsDevelopping || User.IsInRole(Roles.ServerAdmin) ||
(_cssThemeManager.AllowLightningInternalNodeForAll && !doingAdminThings));
}
private bool TryGetNodeInfo(ConnectToNodeRequest request, out NodeInfo nodeInfo)
{
nodeInfo = null;
if (!string.IsNullOrEmpty(request.NodeInfo)) return NodeInfo.TryParse(request.NodeInfo, out nodeInfo);
try
{
nodeInfo = new NodeInfo(new PubKey(request.NodeId), request.NodeHost, request.NodePort);
return true;
}
catch (Exception)
{
return false;
}
}
protected abstract Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings);
}
}

View file

@ -137,7 +137,7 @@ namespace BTCPayServer.Controllers.GreenField
if (!string.IsNullOrEmpty(data.CustomCSSLink) && data.CustomCSSLink.Length > 500)
ModelState.AddModelError(nameof(data.CustomCSSLink), "CustomCSSLink is 500 chars max");
return !ModelState.IsValid ? this.GetValidationResponse() :null;
return !ModelState.IsValid ? this.CreateValidationError(ModelState) :null;
}
private static Client.Models.PaymentRequestData FromModel(PaymentRequestData data)

View file

@ -57,7 +57,7 @@ namespace BTCPayServer.Controllers.GreenField
if (!_storeRepository.CanDeleteStores())
{
return this.GetGeneralErrorResponse(
return this.CreateAPIError("unsupported",
"BTCPay Server is using a database server that does not allow you to remove stores.");
}
await _storeRepository.RemoveStore(storeId, _userManager.GetUserId(User));
@ -195,7 +195,7 @@ namespace BTCPayServer.Controllers.GreenField
if(request.PaymentTolerance < 0 && request.PaymentTolerance > 100)
ModelState.AddModelError(nameof(request.PaymentTolerance), "PaymentTolerance can only be between 0 and 100 percent");
return !ModelState.IsValid ? this.GetValidationResponse() : null;
return !ModelState.IsValid ? this.CreateValidationError(ModelState) : null;
}
}
}

View file

@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NicolasDorier.RateLimits;
using BTCPayServer.Client;
using System.Reflection;
namespace BTCPayServer.Controllers.GreenField
{
@ -75,7 +76,7 @@ namespace BTCPayServer.Controllers.GreenField
if (!ModelState.IsValid)
{
return this.GetValidationResponse();
return this.CreateValidationError(ModelState);
}
var anyAdmin = (await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Any();
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
@ -118,7 +119,7 @@ namespace BTCPayServer.Controllers.GreenField
{
ModelState.AddModelError(nameof(request.Password), error.Description);
}
return this.GetValidationResponse();
return this.CreateValidationError(ModelState);
}
if (!isAdmin)
{
@ -130,9 +131,12 @@ namespace BTCPayServer.Controllers.GreenField
{
foreach (var error in identityResult.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
if (error.Code == "DuplicateUserName")
ModelState.AddModelError(nameof(request.Email), error.Description);
else
ModelState.AddModelError(string.Empty, error.Description);
}
return this.GetValidationResponse();
return this.CreateValidationError(ModelState);
}
if (request.IsAdministrator is true)

View file

@ -51,7 +51,7 @@ namespace BTCPayServer
derivationSchemeSettings.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
return true;
}
catch (Exception e)
catch (Exception)
{
return false;
}

View file

@ -14,57 +14,38 @@
"components": {
"schemas": {
"ValidationProblemDetails": {
"allOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"errors": {
"type": "object",
"nullable": true,
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
}
"type": "array",
"description": "An array of validation errors of the request",
"items": {
"type": "object",
"description": "A specific validation error on a json property",
"properties": {
"path": {
"type": "string",
"nullable": false,
"description": "The json path of the property which failed validation"
},
"message": {
"type": "string",
"nullable": false,
"description": "User friendly error message about the validation"
}
}
]
}
},
"ProblemDetails": {
"type": "object",
"additionalProperties": false,
"description": "Description of an error happening during processing of the request",
"properties": {
"type": {
"code": {
"type": "string",
"nullable": true
"nullable": false,
"description": "An error code describing the error"
},
"title": {
"message": {
"type": "string",
"nullable": true
},
"status": {
"type": "integer",
"format": "int32",
"nullable": true
},
"detail": {
"type": "string",
"nullable": true
},
"instance": {
"type": "string",
"nullable": true
},
"extensions": {
"type": "object",
"nullable": true,
"additionalProperties": {}
"nullable": false,
"description": "User friendly error message about the error"
}
}
}

View file

@ -2,68 +2,40 @@
"components": {
"schemas": {
"ConnectToNodeRequest": {
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"properties": {
"nodeInfo": {
"type": "string",
"nullable": true
}
}
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"nodeId": {
"type": "string",
"nullable": true
},
"nodeHost": {
"type": "string",
"nullable": true
},
"nodePort": {
"type": "integer",
"format": "int32"
}
}
}
]
},
"CreateLightningInvoiceRequest": {
"type": "object",
"additionalProperties": false,
"properties": {
"amount": {
"nodeURI": {
"type": "string",
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/LightMoney"
}
]
},
"description": {
"type": "string",
"nullable": true
},
"expiry": {
"type": "string",
"format": "time-span"
},
"privateRouteHints": {
"type": "boolean",
"nullable": true
"description": "Node URI in the form `pubkey@endpoint[:port]`"
}
}
},
"LightMoney": {
"type": "string",
"format": "int64",
"description": "a number amount wrapped in a string, represented in millistatoshi (00000000001BTC = 1 mSAT)",
"additionalProperties": false
"CreateLightningInvoiceRequest": {
"type": "object",
"properties": {
"amount": {
"type": "string",
"description": "Amount wrapped in a string, represented in a millistatoshi string. (1000 millisatoshi = 1 satoshi)",
"nullable": false
},
"description": {
"type": "string",
"nullable": true,
"description": "Description of the invoice in the BOLT11"
},
"expiry": {
"type": "integer",
"description": "Expiration time in seconds"
},
"privateRouteHints": {
"type": "boolean",
"nullable": true,
"default": false,
"description": "True if the invoice should include private route hints"
}
}
},
"LightningChannelData": {
"type": "object",
@ -71,29 +43,26 @@
"properties": {
"remoteNode": {
"type": "string",
"nullable": true
"nullable": false,
"description": "The public key of the node (Node ID)"
},
"isPublic": {
"type": "boolean"
"type": "boolean",
"description": "Whether the node is public"
},
"isActive": {
"type": "boolean"
"type": "boolean",
"description": "Whether the node is online"
},
"capacity": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/LightMoney"
}
]
"type": "string",
"description": "The capacity of the channel in millisatoshi",
"nullable": false
},
"localBalance": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/LightMoney"
}
]
"type": "string",
"description": "The local balance of the channel in millisatoshi",
"nullable": false
},
"channelPoint": {
"type": "string",
@ -107,39 +76,32 @@
"properties": {
"id": {
"type": "string",
"nullable": true
"description": "The invoice's ID"
},
"status": {
"$ref": "#/components/schemas/LightningInvoiceStatus"
},
"bolT11": {
"BOLT11": {
"type": "string",
"nullable": true
"description": "The BOLT11 representation of the invoice",
"nullable": false
},
"paidAt": {
"type": "string",
"format": "date-time",
"type": "integer",
"description": "The unix timestamp when the invoice got paid",
"nullable": true
},
"expiresAt": {
"type": "string",
"format": "date-time"
"type": "integer",
"description": "The unix timestamp when the invoice expires"
},
"amount": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/LightMoney"
}
]
"type": "string",
"description": "The amount of the invoice in millisatoshi"
},
"amountReceived": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/LightMoney"
}
]
"type": "string",
"description": "The amount received in millisatoshi"
}
}
},
@ -159,28 +121,26 @@
},
"LightningNodeInformationData": {
"type": "object",
"additionalProperties": false,
"properties": {
"nodeInfoList": {
"nodeURIs": {
"type": "array",
"nullable": true,
"description": "Node URIs to connect to this node in the form `pubkey@endpoint[:port]`",
"items": {
"type": "string"
}
},
"blockHeight": {
"type": "integer",
"format": "int32"
"description": "The block height of the lightning node"
}
}
},
"PayLightningInvoiceRequest": {
"type": "object",
"additionalProperties": false,
"properties": {
"invoice": {
"BOLT11": {
"type": "string",
"nullable": true
"description": "The BOLT11 of the invoice to pay"
}
}
},
@ -188,48 +148,19 @@
"type": "object",
"additionalProperties": false,
"properties": {
"node": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/ConnectToNodeRequest"
}
]
"nodeURI": {
"type": "string",
"description": "Node URI in the form `pubkey@endpoint[:port]`"
},
"channelAmount": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/Money"
}
]
"type": "string",
"description": "The amount to fund (in satoshi)"
},
"feeRate": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/FeeRate"
}
]
"type": "number",
"description": "The amount to fund (in satoshi per byte)"
}
}
},
"Money": {
"type": "string",
"format": "int64",
"description": "a number amount wrapped in a string, represented in satoshi (00000001BTC = 1 sat)"
},
"FeeRate": {
"oneOf": [
{
"type": "integer",
"format": "int64"
},
{
"type": "number",
"format": "float"
}
]
}
}
}

View file

@ -30,28 +30,8 @@
}
}
},
"422": {
"description": "A list of errors that occurred",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationProblemDetails"
}
}
}
},
"400": {
"description": "An error occurred",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
"503": {
"description": "Unable to access the lightning node"
},
"404": {
"description": "The lightning node configuration was not found"
@ -91,7 +71,7 @@
"description": "Successfully connected"
},
"422": {
"description": "A list of errors that occurred",
"description": "Unable to validate the request",
"content": {
"application/json": {
"schema": {
@ -101,7 +81,7 @@
}
},
"400": {
"description": "An error occurred",
"description": "Wellknown error codes are: `could-not-connect`",
"content": {
"application/json": {
"schema": {
@ -110,8 +90,8 @@
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
"503": {
"description": "Unable to access the lightning node"
},
"404": {
"description": "The lightning node configuration was not found"
@ -170,19 +150,6 @@
}
}
},
"400": {
"description": "An error occurred",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
},
"404": {
"description": "The lightning node configuration was not found"
}
@ -219,7 +186,7 @@
"description": "Successfully opened"
},
"422": {
"description": "A list of errors that occurred",
"description": "Unable to validate the request",
"content": {
"application/json": {
"schema": {
@ -229,7 +196,7 @@
}
},
"400": {
"description": "An error occurred",
"description": "Wellknown error codes are: `channel-already-exists`, `cannot-afford-funding`, `need-more-confirmations`, `peer-not-connected`",
"content": {
"application/json": {
"schema": {
@ -238,9 +205,6 @@
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
},
"404": {
"description": "The lightning node configuration was not found"
}
@ -266,7 +230,7 @@
}
},
"/api/v1/server/lightning/{cryptoCode}/address": {
"get": {
"post": {
"tags": [
"Lightning (Internal Node)"
],
@ -290,13 +254,14 @@
"content": {
"application/json": {
"schema": {
"type": "string"
"type": "string",
"description": "A bitcoin address belonging to the lightning node"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
"503": {
"description": "Unable to access the lightning node"
},
"404": {
"description": "The lightning node configuration was not found"
@ -351,8 +316,8 @@
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
"503": {
"description": "Unable to access the lightning node"
},
"404": {
"description": "The lightning node configuration or the specified invoice was not found "
@ -392,7 +357,7 @@
"description": "Successfully paid"
},
"422": {
"description": "A list of errors that occurred",
"description": "Unable to validate the request",
"content": {
"application/json": {
"schema": {
@ -402,7 +367,7 @@
}
},
"400": {
"description": "An error occurred",
"description": "Wellknown error codes are: `could-not-find-route`, `generic-error`",
"content": {
"application/json": {
"schema": {
@ -411,8 +376,8 @@
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
"503": {
"description": "Unable to access the lightning node"
},
"404": {
"description": "The lightning node configuration was not found"
@ -468,28 +433,8 @@
}
}
},
"422": {
"description": "A list of errors that occurred",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationProblemDetails"
}
}
}
},
"400": {
"description": "An error occurred",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
"503": {
"description": "Unable to access the lightning node"
},
"404": {
"description": "The lightning node configuration was not found"
@ -500,7 +445,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayLightningInvoiceRequest"
"$ref": "#/components/schemas/CreateLightningInvoiceRequest"
}
}
}

View file

@ -39,28 +39,8 @@
}
}
},
"422": {
"description": "A list of errors that occurred",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationProblemDetails"
}
}
}
},
"400": {
"description": "An error occurred",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
"503": {
"description": "Unable to access the lightning node"
},
"404": {
"description": "The lightning node configuration was not found"
@ -109,7 +89,7 @@
"description": "Successfully connected"
},
"422": {
"description": "A list of errors that occurred",
"description": "Unable to validate the request",
"content": {
"application/json": {
"schema": {
@ -119,7 +99,7 @@
}
},
"400": {
"description": "An error occurred",
"description": "Wellknown error codes are: `could-not-connect`",
"content": {
"application/json": {
"schema": {
@ -128,8 +108,8 @@
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
"503": {
"description": "Unable to access the lightning node"
},
"404": {
"description": "The lightning node configuration was not found"
@ -197,19 +177,6 @@
}
}
},
"400": {
"description": "An error occurred",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
},
"404": {
"description": "The lightning node configuration was not found"
}
@ -255,7 +222,7 @@
"description": "Successfully opened"
},
"422": {
"description": "A list of errors that occurred",
"description": "Unable to validate the request",
"content": {
"application/json": {
"schema": {
@ -265,7 +232,7 @@
}
},
"400": {
"description": "An error occurred",
"description": "Wellknown error codes are: `channel-already-exists`, `cannot-afford-funding`, `need-more-confirmations`, `peer-not-connected`",
"content": {
"application/json": {
"schema": {
@ -274,8 +241,8 @@
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
"503": {
"description": "Unable to access the lightning node"
},
"404": {
"description": "The lightning node configuration was not found"
@ -302,7 +269,7 @@
}
},
"/api/v1/stores/{storeId}/lightning/{cryptoCode}/address": {
"get": {
"post": {
"tags": [
"Lightning (Store)"
],
@ -314,7 +281,8 @@
"required": true,
"description": "The cryptoCode of the lightning-node to query",
"schema": {
"type": "string"
"type": "string",
"description": "A bitcoin address belonging to the lightning node"
}
},
{
@ -340,8 +308,8 @@
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
"503": {
"description": "Unable to access the lightning node"
},
"404": {
"description": "The lightning node configuration was not found"
@ -406,8 +374,8 @@
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
"503": {
"description": "Unable to access the lightning node"
},
"404": {
"description": "The lightning node configuration or the specified invoice was not found "
@ -456,7 +424,7 @@
"description": "Successfully paid"
},
"422": {
"description": "A list of errors that occurred",
"description": "Unable to validate the request",
"content": {
"application/json": {
"schema": {
@ -466,7 +434,7 @@
}
},
"400": {
"description": "An error occurred",
"description": "Wellknown error codes are: `could-not-find-route`, `generic-error`",
"content": {
"application/json": {
"schema": {
@ -475,8 +443,8 @@
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
"503": {
"description": "Unable to access the lightning node"
},
"404": {
"description": "The lightning node configuration was not found"
@ -541,28 +509,8 @@
}
}
},
"422": {
"description": "A list of errors that occurred",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationProblemDetails"
}
}
}
},
"400": {
"description": "An error occurred",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden"
"503": {
"description": "Unable to access the lightning node"
},
"404": {
"description": "The lightning node configuration was not found"