GreenField: Cross-implemenation Lightning Node API (#1566)

* GreenField: Cross-implemenation Lightning Node API

* switch to hard unrsstricted check

* fix

* set LightningPrivateRouteHints in swagger + stores api

* add priv route hint

* rename models and add swagger defs to models
This commit is contained in:
Andrew Camilleri 2020-05-29 02:00:13 +02:00 committed by GitHub
parent 114ab98059
commit 1e3f62718d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1140 additions and 21 deletions

View File

@ -6,6 +6,7 @@
<ItemGroup>
<PackageReference Include="NBitcoin" Version="5.0.39" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.1.0.22" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>

View File

@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public async Task<LightningNodeInformationData> GetLightningNodeInfo(string cryptoCode,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/info",
method: HttpMethod.Get), token);
return await HandleResponse<LightningNodeInformationData>(response);
}
public async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request,
CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/connect", bodyPayload: request,
method: HttpMethod.Post), token);
HandleResponse(response);
}
public async Task<IEnumerable<LightningChannelData>> GetLightningNodeChannels(string cryptoCode,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/channels",
method: HttpMethod.Get), token);
return await HandleResponse<IEnumerable<LightningChannelData>>(response);
}
public async Task<string> OpenLightningChannel(string cryptoCode, OpenLightningChannelRequest request,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/channels", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<string>(response);
}
public async Task<string> GetLightningDepositAddress(string cryptoCode, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/address", method: HttpMethod.Post), token);
return await HandleResponse<string>(response);
}
public async Task PayLightningInvoice(string cryptoCode, PayLightningInvoiceRequest request,
CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/pay", bodyPayload: request,
method: HttpMethod.Post), token);
HandleResponse(response);
}
public async Task<LightningInvoiceData> GetLightningInvoice(string cryptoCode,
string invoiceId, CancellationToken token = default)
{
if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices/{invoiceId}",
method: HttpMethod.Get), token);
return await HandleResponse<LightningInvoiceData>(response);
}
public async Task<LightningInvoiceData> CreateLightningInvoice(string cryptoCode, CreateLightningInvoiceRequest request,
CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<LightningInvoiceData>(response);
}
}
}

View File

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public async Task<LightningNodeInformationData> GetLightningNodeInfo(string storeId, string cryptoCode,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/info",
method: HttpMethod.Get), token);
return await HandleResponse<LightningNodeInformationData>(response);
}
public async Task ConnectToLightningNode(string storeId, string cryptoCode, ConnectToNodeRequest request,
CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/connect", bodyPayload: request,
method: HttpMethod.Post), token);
HandleResponse(response);
}
public async Task<IEnumerable<LightningChannelData>> GetLightningNodeChannels(string storeId, string cryptoCode,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/channels",
method: HttpMethod.Get), token);
return await HandleResponse<IEnumerable<LightningChannelData>>(response);
}
public async Task<string> OpenLightningChannel(string storeId, string cryptoCode, OpenLightningChannelRequest request,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/channels", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<string>(response);
}
public async Task<string> GetLightningDepositAddress(string storeId, string cryptoCode,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/address", method: HttpMethod.Post),
token);
return await HandleResponse<string>(response);
}
public async Task PayLightningInvoice(string storeId, string cryptoCode, PayLightningInvoiceRequest request,
CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/pay", bodyPayload: request,
method: HttpMethod.Post), token);
HandleResponse(response);
}
public async Task<LightningInvoiceData> GetLightningInvoice(string storeId, string cryptoCode,
string invoiceId, CancellationToken token = default)
{
if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/{invoiceId}",
method: HttpMethod.Get), token);
return await HandleResponse<LightningInvoiceData>(response);
}
public async Task<LightningInvoiceData> CreateLightningInvoice(string storeId, string cryptoCode,
CreateLightningInvoiceRequest request, CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<LightningInvoiceData>(response);
}
}
}

View File

@ -0,0 +1,10 @@
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; }
}
}

View File

@ -0,0 +1,17 @@
using System;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class CreateLightningInvoiceRequest
{
[JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))]
public LightMoney Amount { get; set; }
public string Description { get; set; }
public TimeSpan Expiry { get; set; }
public bool LightningPrivateRouteHints { get; set; }
}
}

View File

@ -0,0 +1,28 @@
using System;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models
{
public class LightningInvoiceData
{
public string Id { get; set; }
[JsonProperty(ItemConverterType = typeof(StringEnumConverter))]
public LightningInvoiceStatus Status { get; set; }
public string BOLT11 { get; set; }
public DateTimeOffset? PaidAt { get; set; }
public DateTimeOffset ExpiresAt { get; set; }
[JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))]
public LightMoney Amount { get; set; }
[JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))]
public LightMoney AmountReceived { get; set; }
}
}

View File

@ -0,0 +1,31 @@
using System.Collections.Generic;
using BTCPayServer.Lightning;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class LightningNodeInformationData
{
public IEnumerable<string> NodeInfoList { get; set; }
public int BlockHeight { get; set; }
}
public class LightningChannelData
{
public string RemoteNode { get; set; }
public bool IsPublic { get; set; }
public bool IsActive { get; set; }
[JsonProperty(ItemConverterType = typeof(MoneyJsonConverter))]
public LightMoney Capacity { get; set; }
[JsonProperty(ItemConverterType = typeof(MoneyJsonConverter))]
public LightMoney LocalBalance { get; set; }
public string ChannelPoint { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OpenLightningChannelRequest
{
public ConnectToNodeRequest Node { get; set; }
[JsonProperty(ItemConverterType = typeof(MoneyJsonConverter))]
public Money ChannelAmount { get; set; }
[JsonProperty(ItemConverterType = typeof(FeeRateJsonConverter))]
public FeeRate FeeRate { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace BTCPayServer.Client.Models
{
public class PayLightningInvoiceRequest
{
public string Invoice { get; set; }
}
}

View File

@ -44,6 +44,7 @@ namespace BTCPayServer.Client.Models
public NetworkFeeMode NetworkFeeMode { get; set; }
public bool PayJoinEnabled { get; set; }
public bool LightningPrivateRouteHints { get; set; }
}

View File

@ -6,6 +6,10 @@ namespace BTCPayServer.Client
{
public class Policies
{
public const string CanCreateLightningInvoiceInternalNode = "btcpay.server.cancreatelightninginvoiceinternalnode";
public const string CanCreateLightningInvoiceInStore = "btcpay.store.cancreatelightninginvoice";
public const string CanUseInternalLightningNode = "btcpay.server.canuseinternallightningnode";
public const string CanUseLightningNodeInStore = "btcpay.store.canuselightningnode";
public const string CanModifyServerSettings = "btcpay.server.canmodifyserversettings";
public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings";
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
@ -30,6 +34,10 @@ namespace BTCPayServer.Client
yield return CanViewProfile;
yield return CanCreateUser;
yield return Unrestricted;
yield return CanUseInternalLightningNode;
yield return CanCreateLightningInvoiceInternalNode;
yield return CanUseLightningNodeInStore;
yield return CanCreateLightningInvoiceInStore;
}
}
public static bool IsValidPolicy(string policy)
@ -100,8 +108,6 @@ namespace BTCPayServer.Client
}
}
internal Permission(string policy, string storeId)
{
Policy = policy;
@ -147,7 +153,8 @@ namespace BTCPayServer.Client
case Policies.CanModifyPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanViewStoreSettings:
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyPaymentRequests:
case Policies.CanCreateLightningInvoiceInternalNode when this.Policy == Policies.CanUseInternalLightningNode:
case Policies.CanCreateLightningInvoiceInStore when this.Policy == Policies.CanUseLightningNodeInStore:
return true;
default:
return false;

View File

@ -0,0 +1,110 @@
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class InternalLightningNodeApiInternalController : LightningNodeApiController
{
private readonly BTCPayServerOptions _btcPayServerOptions;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly LightningClientFactoryService _lightningClientFactory;
public InternalLightningNodeApiInternalController(BTCPayServerOptions btcPayServerOptions,
BTCPayNetworkProvider btcPayNetworkProvider, BTCPayServerEnvironment btcPayServerEnvironment,
CssThemeManager cssThemeManager, LightningClientFactoryService lightningClientFactory) : base(
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager)
{
_btcPayServerOptions = btcPayServerOptions;
_btcPayNetworkProvider = btcPayNetworkProvider;
_lightningClientFactory = lightningClientFactory;
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/info")]
public override Task<IActionResult> GetInfo(string cryptoCode)
{
return base.GetInfo(cryptoCode);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/connect")]
public override Task<IActionResult> ConnectToNode(string cryptoCode, ConnectToNodeRequest request)
{
return base.ConnectToNode(cryptoCode, request);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/channels")]
public override Task<IActionResult> GetChannels(string cryptoCode)
{
return base.GetChannels(cryptoCode);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/channels")]
public override Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request)
{
return base.OpenChannel(cryptoCode, request);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/address")]
public override Task<IActionResult> GetDepositAddress(string cryptoCode)
{
return base.GetDepositAddress(cryptoCode);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/invoices/{id}")]
public override Task<IActionResult> GetInvoice(string cryptoCode, string id)
{
return base.GetInvoice(cryptoCode, id);
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/invoices/pay")]
public override Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice)
{
return base.PayInvoice(cryptoCode, lightningInvoice);
}
[Authorize(Policy = Policies.CanCreateLightningInvoiceInternalNode,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/invoices")]
public override Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request)
{
return base.CreateInvoice(cryptoCode, request);
}
protected override Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings)
{
_btcPayServerOptions.InternalLightningByCryptoCode.TryGetValue(cryptoCode,
out var internalLightningNode);
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network == null || !CanUseInternalLightning(doingAdminThings) || internalLightningNode == null)
{
return null;
}
return Task.FromResult(_lightningClientFactory.Create(internalLightningNode, network));
}
}
}

View File

@ -0,0 +1,124 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class StoreLightningNodeApiController : LightningNodeApiController
{
private readonly BTCPayServerOptions _btcPayServerOptions;
private readonly LightningClientFactoryService _lightningClientFactory;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
public StoreLightningNodeApiController(
BTCPayServerOptions btcPayServerOptions,
LightningClientFactoryService lightningClientFactory, BTCPayNetworkProvider btcPayNetworkProvider,
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager) : base(
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager)
{
_btcPayServerOptions = btcPayServerOptions;
_lightningClientFactory = lightningClientFactory;
_btcPayNetworkProvider = btcPayNetworkProvider;
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/info")]
public override Task<IActionResult> GetInfo(string cryptoCode)
{
return base.GetInfo(cryptoCode);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/connect")]
public override Task<IActionResult> ConnectToNode(string cryptoCode, ConnectToNodeRequest request)
{
return base.ConnectToNode(cryptoCode, request);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]
public override Task<IActionResult> GetChannels(string cryptoCode)
{
return base.GetChannels(cryptoCode);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]
public override Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request)
{
return base.OpenChannel(cryptoCode, request);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/address")]
public override Task<IActionResult> GetDepositAddress(string cryptoCode)
{
return base.GetDepositAddress(cryptoCode);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/pay")]
public override Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice)
{
return base.PayInvoice(cryptoCode, lightningInvoice);
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/{id}")]
public override Task<IActionResult> GetInvoice(string cryptoCode, string id)
{
return base.GetInvoice(cryptoCode, id);
}
[Authorize(Policy = Policies.CanCreateLightningInvoiceInStore,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices")]
public override Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request)
{
return base.CreateInvoice(cryptoCode, request);
}
protected override Task<ILightningClient> GetLightningClient(string cryptoCode,
bool doingAdminThings)
{
_btcPayServerOptions.InternalLightningByCryptoCode.TryGetValue(cryptoCode,
out var internalLightningNode);
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var store = HttpContext.GetStoreData();
if (network == null || store == null)
{
return null;
}
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
if (existing == null || (existing.GetLightningUrl().IsInternalNode(internalLightningNode) &&
!CanUseInternalLightning(doingAdminThings)))
{
return null;
}
return Task.FromResult(_lightningClientFactory.Create(existing.GetLightningUrl(), network));
}
}
}

View File

@ -0,0 +1,327 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
namespace BTCPayServer.Controllers.GreenField
{
public abstract class LightningNodeApiController : Controller
{
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
private readonly CssThemeManager _cssThemeManager;
protected LightningNodeApiController(BTCPayNetworkProvider btcPayNetworkProvider,
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_btcPayServerEnvironment = btcPayServerEnvironment;
_cssThemeManager = cssThemeManager;
}
public virtual async Task<IActionResult> GetInfo(string cryptoCode)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
if (lightningClient == null)
{
return NotFound();
}
try
{
var info = await lightningClient.GetInfo();
return Ok(new LightningNodeInformationData()
{
BlockHeight = info.BlockHeight,
NodeInfoList = info.NodeInfoList.Select(nodeInfo => nodeInfo.ToString())
});
}
catch (Exception e)
{
ModelState.AddModelError(string.Empty, e.Message);
return BadRequest(new ValidationProblemDetails(ModelState));
}
}
public virtual async Task<IActionResult> ConnectToNode(string cryptoCode, ConnectToNodeRequest request)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
if (lightningClient == null)
{
return NotFound();
}
if (TryGetNodeInfo(request, out var nodeInfo))
{
ModelState.AddModelError(nameof(request.NodeId), "A valid node info was not provided to connect to");
}
if (CheckValidation(out var errorActionResult))
{
return errorActionResult;
}
try
{
await lightningClient.ConnectTo(nodeInfo);
}
catch (Exception e)
{
ModelState.AddModelError(string.Empty, e.Message);
return BadRequest(new ValidationProblemDetails(ModelState));
}
return Ok();
}
public virtual async Task<IActionResult> GetChannels(string cryptoCode)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
if (lightningClient == null)
{
return NotFound();
}
try
{
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)
{
ModelState.AddModelError(string.Empty, e.Message);
return BadRequest(new ValidationProblemDetails(ModelState));
}
}
public virtual async Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
if (lightningClient == null)
{
return NotFound();
}
if (TryGetNodeInfo(request.Node, out var nodeInfo))
{
ModelState.AddModelError(nameof(request.Node),
"A valid node info was not provided to open a channel with");
}
if (request.ChannelAmount == null)
{
ModelState.AddModelError(nameof(request.ChannelAmount), "ChannelAmount is missing");
}
else if (request.ChannelAmount.Satoshi <= 0)
{
ModelState.AddModelError(nameof(request.ChannelAmount), "ChannelAmount must be more than 0");
}
if (request.FeeRate == null)
{
ModelState.AddModelError(nameof(request.FeeRate), "FeeRate is missing");
}
else if (request.FeeRate.SatoshiPerByte <= 0)
{
ModelState.AddModelError(nameof(request.FeeRate), "FeeRate must be more than 0");
}
if (CheckValidation(out var errorActionResult))
{
return errorActionResult;
}
try
{
var response = await lightningClient.OpenChannel(new Lightning.OpenChannelRequest()
{
ChannelAmount = request.ChannelAmount, FeeRate = request.FeeRate, NodeInfo = nodeInfo
});
if (response.Result == OpenChannelResult.Ok)
{
return Ok();
}
ModelState.AddModelError(string.Empty, response.Result.ToString());
return BadRequest(new ValidationProblemDetails(ModelState));
}
catch (Exception e)
{
ModelState.AddModelError(string.Empty, e.Message);
return BadRequest(new ValidationProblemDetails(ModelState));
}
}
public virtual async Task<IActionResult> GetDepositAddress(string cryptoCode)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
if (lightningClient == null)
{
return NotFound();
}
return Ok((await lightningClient.GetDepositAddress()).ToString());
}
public virtual async Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (lightningClient == null || network == null)
{
return NotFound();
}
try
{
BOLT11PaymentRequest.TryParse(lightningInvoice.Invoice, out var bolt11PaymentRequest, network.NBitcoinNetwork);
}
catch (Exception)
{
ModelState.AddModelError(nameof(lightningInvoice), "The BOLT11 invoice was invalid.");
}
if (CheckValidation(out var errorActionResult))
{
return errorActionResult;
}
var result = await lightningClient.Pay(lightningInvoice.Invoice);
switch (result.Result)
{
case PayResult.Ok:
return Ok();
case PayResult.CouldNotFindRoute:
ModelState.AddModelError(nameof(lightningInvoice.Invoice), "Could not find route");
break;
case PayResult.Error:
ModelState.AddModelError(nameof(lightningInvoice.Invoice), result.ErrorDetail);
break;
}
return BadRequest(new ValidationProblemDetails(ModelState));
}
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)
{
return NotFound();
}
return Ok(ToModel(inv));
}
catch (Exception e)
{
ModelState.AddModelError(string.Empty, e.Message);
return BadRequest(new ValidationProblemDetails(ModelState));
}
}
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))
{
return errorActionResult;
}
try
{
var invoice = await lightningClient.CreateInvoice(
new CreateInvoiceParams(request.Amount, request.Description, request.Expiry)
{
PrivateRouteHints = request.LightningPrivateRouteHints
},
CancellationToken.None);
return Ok(ToModel(invoice));
}
catch (Exception e)
{
ModelState.AddModelError(string.Empty, e.Message);
return BadRequest(new ValidationProblemDetails(ModelState));
}
}
private LightningInvoiceData ToModel(LightningInvoice invoice)
{
return new LightningInvoiceData()
{
Amount = invoice.Amount,
Id = invoice.Id,
Status = invoice.Status,
AmountReceived = invoice.AmountReceived,
PaidAt = invoice.PaidAt,
BOLT11 = invoice.BOLT11,
ExpiresAt = invoice.ExpiresAt
};
}
private bool CheckValidation(out IActionResult result)
{
if (!ModelState.IsValid)
{
result = BadRequest(new ValidationProblemDetails(ModelState));
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

@ -132,7 +132,8 @@ namespace BTCPayServer.Controllers.GreenField
LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate,
PaymentTolerance = storeBlob.PaymentTolerance,
RedirectAutomatically = storeBlob.RedirectAutomatically,
PayJoinEnabled = storeBlob.PayJoinEnabled
PayJoinEnabled = storeBlob.PayJoinEnabled,
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints
};
}
@ -168,6 +169,7 @@ namespace BTCPayServer.Controllers.GreenField
blob.PaymentTolerance = restModel.PaymentTolerance;
blob.RedirectAutomatically = restModel.RedirectAutomatically;
blob.PayJoinEnabled = restModel.PayJoinEnabled;
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;
model.SetStoreBlob(blob);
}

View File

@ -24,6 +24,7 @@ using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitpayClient;
using Newtonsoft.Json;
using CreateInvoiceRequest = BTCPayServer.Models.CreateInvoiceRequest;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers

View File

@ -368,6 +368,12 @@ namespace BTCPayServer.Controllers
{$"{BTCPayServer.Client.Policies.CanModifyPaymentRequests}:", ("Manage selected stores' payment requests", "The app will be able to view, modify, delete and create new payment requests on the selected stores.")},
{BTCPayServer.Client.Policies.CanViewPaymentRequests, ("View your payment requests", "The app will be able to view payment requests.")},
{$"{BTCPayServer.Client.Policies.CanViewPaymentRequests}:", ("View your payment requests", "The app will be able to view the selected stores' payment requests.")},
{BTCPayServer.Client.Policies.CanUseInternalLightningNode, ("Use the internal lightning node", "The app will be able to use the internal BTCPay Server lightning node to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
{BTCPayServer.Client.Policies.CanCreateLightningInvoiceInternalNode, ("Create invoices with internal lightning node", "The app will be able to use the internal BTCPay Server lightning node to create BOLT11 invoices.")},
{BTCPayServer.Client.Policies.CanUseLightningNodeInStore, ("Use the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to all your stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
{BTCPayServer.Client.Policies.CanCreateLightningInvoiceInStore, ("Create invoices the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to all your stores to create BOLT11 invoices.")},
{$"{BTCPayServer.Client.Policies.CanUseLightningNodeInStore}:", ("Use the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to the selected stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
{$"{BTCPayServer.Client.Policies.CanCreateLightningInvoiceInStore}:", ("Create invoices the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to the selected stores to create BOLT11 invoices.")},
};
public string Title
{

View File

@ -91,11 +91,7 @@ namespace BTCPayServer.Controllers
return View(vm);
}
var internalDomain = internalLightning?.BaseUri?.DnsSafeHost;
bool isInternalNode = connectionString.ConnectionType == LightningConnectionType.CLightning ||
connectionString.BaseUri.DnsSafeHost == internalDomain ||
(internalDomain == "127.0.0.1" || internalDomain == "localhost");
bool isInternalNode = connectionString.IsInternalNode(internalLightning);
if (connectionString.BaseUri.Scheme == "http")
{

View File

@ -33,6 +33,7 @@ using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using NBXplorer.DerivationStrategy;
using System.Net;
using BTCPayServer.Lightning;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Newtonsoft.Json.Linq;
@ -42,6 +43,15 @@ namespace BTCPayServer
{
public static class Extensions
{
public static bool IsInternalNode(this LightningConnectionString connectionString, LightningConnectionString internalLightning)
{
var internalDomain = internalLightning?.BaseUri?.DnsSafeHost;
return connectionString.ConnectionType == LightningConnectionType.CLightning ||
connectionString.BaseUri.DnsSafeHost == internalDomain ||
(internalDomain == "127.0.0.1" || internalDomain == "localhost");
}
public static IQueryable<TEntity> Where<TEntity>(this Microsoft.EntityFrameworkCore.DbSet<TEntity> obj, System.Linq.Expressions.Expression<Func<TEntity, bool>> predicate) where TEntity : class
{
return System.Linq.Queryable.Where(obj, predicate);

View File

@ -36,16 +36,7 @@ namespace BTCPayServer.Security.GreenField
bool success = false;
switch (requirement.Policy)
{
case Policies.CanModifyProfile:
case Policies.CanViewProfile:
case Policies.Unrestricted:
success = context.HasPermission(Permission.Create(requirement.Policy));
break;
case Policies.CanViewPaymentRequests:
case Policies.CanModifyPaymentRequests:
case Policies.CanViewStoreSettings:
case Policies.CanModifyStoreSettings:
case { } policy when Policies.IsStorePolicy(policy):
var storeId = _HttpContext.GetImplicitStoreId();
var userid = _userManager.GetUserId(context.User);
// Specific store action
@ -75,8 +66,7 @@ namespace BTCPayServer.Security.GreenField
success = true;
}
break;
case Policies.CanCreateUser:
case Policies.CanModifyServerSettings:
case { } policy when Policies.IsServerPolicy(policy):
if (context.HasPermission(Permission.Create(requirement.Policy)))
{
var user = await _userManager.GetUserAsync(context.User);
@ -87,6 +77,11 @@ namespace BTCPayServer.Security.GreenField
success = true;
}
break;
case Policies.CanModifyProfile:
case Policies.CanViewProfile:
case Policies.Unrestricted:
success = context.HasPermission(Permission.Create(requirement.Policy));
break;
}
if (success)

View File

@ -0,0 +1,244 @@
{
"components": {
"schemas": {
"ConnectToNodeRequest": {
"oneOf": [
{
"$ref": "#/components/schemas/ConnectToNodeRequest1"
},
{
"$ref": "#/components/schemas/ConnectToNodeRequest2"
}
]
},
"ConnectToNodeRequest1": {
"type": "object",
"additionalProperties": false,
"properties": {
"nodeInfo": {
"type": "string",
"nullable": true
}
}
},
"ConnectToNodeRequest2": {
"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": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/LightMoney"
}
]
},
"description": {
"type": "string",
"nullable": true
},
"expiry": {
"type": "string",
"format": "time-span"
},
"lightningPrivateRouteHints": {
"type": "boolean"
}
}
},
"LightMoney": {
"type": "integer",
"format": "int64",
"additionalProperties": false
},
"LightningChannelData": {
"type": "object",
"additionalProperties": false,
"properties": {
"remoteNode": {
"type": "string",
"nullable": true
},
"isPublic": {
"type": "boolean"
},
"isActive": {
"type": "boolean"
},
"capacity": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/LightMoney"
}
]
},
"localBalance": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/LightMoney"
}
]
},
"channelPoint": {
"type": "string",
"nullable": true
}
}
},
"LightningInvoiceData": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"nullable": true
},
"status": {
"$ref": "#/components/schemas/LightningInvoiceStatus"
},
"bolT11": {
"type": "string",
"nullable": true
},
"paidAt": {
"type": "string",
"format": "date-time",
"nullable": true
},
"expiresAt": {
"type": "string",
"format": "date-time"
},
"amount": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/LightMoney"
}
]
},
"amountReceived": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/LightMoney"
}
]
}
}
},
"LightningInvoiceStatus": {
"type": "string",
"description": "",
"x-enumNames": [
"Unpaid",
"Paid",
"Expired"
],
"enum": [
"Unpaid",
"Paid",
"Expired"
]
},
"LightningNodeInformationData": {
"type": "object",
"additionalProperties": false,
"properties": {
"nodeInfoList": {
"type": "array",
"nullable": true,
"items": {
"type": "string"
}
},
"blockHeight": {
"type": "integer",
"format": "int32"
}
}
},
"PayLightningInvoiceRequest": {
"type": "object",
"additionalProperties": false,
"properties": {
"invoice": {
"type": "string",
"nullable": true
}
}
},
"OpenLightningChannelRequest": {
"type": "object",
"additionalProperties": false,
"properties": {
"node": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/ConnectToNodeRequest"
}
]
},
"channelAmount": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/Money"
}
]
},
"feeRate": {
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/FeeRate"
}
]
}
}
},
"Money": {
"type": "integer",
"format": "int64"
},
"FeeRate": {
"oneOf": [
{
"type": "integer",
"format": "int64"
},
{
"type": "number",
"format": "float"
}
]
}
}
},
"tags": [
{
"name": "Lightning"
}
]
}

View File

@ -347,6 +347,9 @@
},
"payJoinEnabled": {
"type": "boolean"
},
"lightningPrivateRouteHints": {
"type": "boolean"
}
}
},