2020-05-29 02:00:13 +02:00
using System ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
2021-07-27 14:08:54 +02:00
using BTCPayServer.Abstractions.Contracts ;
2022-02-24 09:00:44 +01:00
using BTCPayServer.Abstractions.Extensions ;
2021-03-02 11:11:58 +09:00
using BTCPayServer.Client ;
2020-05-29 02:00:13 +02:00
using BTCPayServer.Client.Models ;
using BTCPayServer.Lightning ;
2021-03-02 11:11:58 +09:00
using BTCPayServer.Security ;
2022-05-24 13:18:16 +09:00
using BTCPayServer.Services ;
2021-03-02 11:11:58 +09:00
using Microsoft.AspNetCore.Authorization ;
2020-05-29 02:00:13 +02:00
using Microsoft.AspNetCore.Mvc ;
2020-06-08 23:40:58 +09:00
using Microsoft.AspNetCore.Mvc.Filters ;
using Newtonsoft.Json.Linq ;
2020-05-29 02:00:13 +02:00
2022-01-14 13:05:23 +09:00
namespace BTCPayServer.Controllers.Greenfield
2020-05-29 02:00:13 +02:00
{
2020-06-08 23:40:58 +09:00
public class LightningUnavailableExceptionFilter : Attribute , IExceptionFilter
{
public void OnException ( ExceptionContext context )
{
2022-04-12 11:01:58 +02:00
context . Result = new ObjectResult ( new GreenfieldAPIError ( "lightning-node-unavailable" , $"The lightning node is unavailable ({context.Exception.GetType().Name}: {context.Exception.Message})" ) ) { StatusCode = 503 } ;
2021-12-16 12:32:13 +09:00
// Do not mark handled, it is possible filters above have better errors
2020-06-08 23:40:58 +09:00
}
}
2022-06-23 06:42:28 +02:00
2022-01-07 12:17:59 +09:00
public abstract class GreenfieldLightningNodeApiController : Controller
2020-05-29 02:00:13 +02:00
{
private readonly BTCPayNetworkProvider _btcPayNetworkProvider ;
2022-05-24 13:18:16 +09:00
private readonly PoliciesSettings _policiesSettings ;
2021-03-02 11:11:58 +09:00
private readonly IAuthorizationService _authorizationService ;
2022-01-07 12:17:59 +09:00
protected GreenfieldLightningNodeApiController ( BTCPayNetworkProvider btcPayNetworkProvider ,
2022-05-24 13:18:16 +09:00
PoliciesSettings policiesSettings ,
2021-03-02 11:11:58 +09:00
IAuthorizationService authorizationService )
2020-05-29 02:00:13 +02:00
{
_btcPayNetworkProvider = btcPayNetworkProvider ;
2022-05-24 13:18:16 +09:00
_policiesSettings = policiesSettings ;
2021-03-02 11:11:58 +09:00
_authorizationService = authorizationService ;
2020-05-29 02:00:13 +02:00
}
2022-04-26 03:29:20 +02:00
public virtual async Task < IActionResult > GetInfo ( string cryptoCode , CancellationToken cancellationToken = default )
2020-05-29 02:00:13 +02:00
{
var lightningClient = await GetLightningClient ( cryptoCode , true ) ;
2022-04-26 03:29:20 +02:00
var info = await lightningClient . GetInfo ( cancellationToken ) ;
return Ok ( new LightningNodeInformationData
2020-05-29 02:00:13 +02:00
{
2020-06-08 23:40:58 +09:00
BlockHeight = info . BlockHeight ,
NodeURIs = info . NodeInfoList . Select ( nodeInfo = > nodeInfo ) . ToArray ( )
} ) ;
2020-05-29 02:00:13 +02:00
}
2022-06-23 06:42:28 +02:00
public virtual async Task < IActionResult > GetBalance ( string cryptoCode , CancellationToken cancellationToken = default )
{
var lightningClient = await GetLightningClient ( cryptoCode , true ) ;
var balance = await lightningClient . GetBalance ( cancellationToken ) ;
return Ok ( new LightningNodeBalanceData
{
OnchainBalance = balance . OnchainBalance ! = null
? new OnchainBalanceData
{
Confirmed = balance . OnchainBalance . Confirmed ,
Unconfirmed = balance . OnchainBalance . Unconfirmed ,
Reserved = balance . OnchainBalance . Reserved
}
: null ,
OffchainBalance = balance . OffchainBalance ! = null
? new OffchainBalanceData
{
Opening = balance . OffchainBalance . Opening ,
Local = balance . OffchainBalance . Local ,
Remote = balance . OffchainBalance . Remote ,
Closing = balance . OffchainBalance . Closing ,
}
: null
} ) ;
}
2022-05-18 15:22:08 +02:00
public virtual async Task < IActionResult > ConnectToNode ( string cryptoCode , ConnectToNodeRequest request , CancellationToken cancellationToken = default )
2020-05-29 02:00:13 +02:00
{
var lightningClient = await GetLightningClient ( cryptoCode , true ) ;
2020-06-08 23:40:58 +09:00
if ( request ? . NodeURI is null )
2020-05-29 02:00:13 +02:00
{
2020-06-08 23:40:58 +09:00
ModelState . AddModelError ( nameof ( request . NodeURI ) , "A valid node info was not provided to connect to" ) ;
2020-05-29 02:00:13 +02:00
}
2020-06-08 23:40:58 +09:00
if ( ! ModelState . IsValid )
2020-05-29 02:00:13 +02:00
{
2020-06-08 23:40:58 +09:00
return this . CreateValidationError ( ModelState ) ;
2020-05-29 02:00:13 +02:00
}
2022-05-18 15:22:08 +02:00
var result = await lightningClient . ConnectTo ( request . NodeURI , cancellationToken ) ;
2020-06-08 23:40:58 +09:00
switch ( result )
2020-05-29 02:00:13 +02:00
{
2020-06-08 23:40:58 +09:00
case ConnectionResult . Ok :
return Ok ( ) ;
case ConnectionResult . CouldNotConnect :
return this . CreateAPIError ( "could-not-connect" , "Could not connect to the remote node" ) ;
2020-05-29 02:00:13 +02:00
}
return Ok ( ) ;
}
2022-04-26 03:29:20 +02:00
public virtual async Task < IActionResult > GetChannels ( string cryptoCode , CancellationToken cancellationToken = default )
2020-05-29 02:00:13 +02:00
{
var lightningClient = await GetLightningClient ( cryptoCode , true ) ;
2022-04-26 03:29:20 +02:00
var channels = await lightningClient . ListChannels ( cancellationToken ) ;
return Ok ( channels . Select ( channel = > new LightningChannelData
2020-05-29 02:00:13 +02:00
{
2020-06-08 23:40:58 +09:00
Capacity = channel . Capacity ,
ChannelPoint = channel . ChannelPoint . ToString ( ) ,
IsActive = channel . IsActive ,
IsPublic = channel . IsPublic ,
LocalBalance = channel . LocalBalance ,
RemoteNode = channel . RemoteNode . ToString ( )
} ) ) ;
2020-05-29 02:00:13 +02:00
}
2020-06-08 23:40:58 +09:00
2022-04-26 03:29:20 +02:00
public virtual async Task < IActionResult > OpenChannel ( string cryptoCode , OpenLightningChannelRequest request , CancellationToken cancellationToken = default )
2020-05-29 02:00:13 +02:00
{
var lightningClient = await GetLightningClient ( cryptoCode , true ) ;
2020-06-08 23:40:58 +09:00
if ( request ? . NodeURI is null )
2020-05-29 02:00:13 +02:00
{
2020-06-08 23:40:58 +09:00
ModelState . AddModelError ( nameof ( request . NodeURI ) ,
2020-05-29 02:00:13 +02:00
"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" ) ;
}
2020-11-06 16:52:58 +01:00
if ( ! ModelState . IsValid )
2020-05-29 02:00:13 +02:00
{
2020-06-08 23:40:58 +09:00
return this . CreateValidationError ( ModelState ) ;
2020-05-29 02:00:13 +02:00
}
2022-04-26 03:29:20 +02:00
var response = await lightningClient . OpenChannel ( new OpenChannelRequest
2020-05-29 02:00:13 +02:00
{
2020-06-08 23:40:58 +09:00
ChannelAmount = request . ChannelAmount ,
FeeRate = request . FeeRate ,
NodeInfo = request . NodeURI
2022-04-26 03:29:20 +02:00
} , cancellationToken ) ;
2020-05-29 02:00:13 +02:00
2020-06-08 23:40:58 +09:00
string errorCode , errorMessage ;
switch ( response . Result )
2020-05-29 02:00:13 +02:00
{
2020-06-08 23:40:58 +09:00
case OpenChannelResult . Ok :
return Ok ( ) ;
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 ) ;
2020-05-29 02:00:13 +02:00
}
2022-05-18 15:22:08 +02:00
public virtual async Task < IActionResult > GetDepositAddress ( string cryptoCode , CancellationToken cancellationToken = default )
2020-05-29 02:00:13 +02:00
{
var lightningClient = await GetLightningClient ( cryptoCode , true ) ;
2022-05-31 12:15:38 +02:00
var addr = await lightningClient . GetDepositAddress ( cancellationToken ) ;
return Ok ( new JValue ( addr . ToString ( ) ) ) ;
2020-05-29 02:00:13 +02:00
}
2022-04-26 03:29:20 +02:00
public virtual async Task < IActionResult > GetPayment ( string cryptoCode , string paymentHash , CancellationToken cancellationToken = default )
2022-04-12 11:01:58 +02:00
{
var lightningClient = await GetLightningClient ( cryptoCode , false ) ;
2022-04-26 03:29:20 +02:00
var payment = await lightningClient . GetPayment ( paymentHash , cancellationToken ) ;
2022-04-12 11:01:58 +02:00
return payment = = null ? this . CreateAPIError ( 404 , "payment-not-found" , "Impossible to find a lightning payment with this payment hash" ) : Ok ( ToModel ( payment ) ) ;
}
2022-04-26 03:29:20 +02:00
public virtual async Task < IActionResult > PayInvoice ( string cryptoCode , PayLightningInvoiceRequest lightningInvoice , CancellationToken cancellationToken = default )
2020-05-29 02:00:13 +02:00
{
var lightningClient = await GetLightningClient ( cryptoCode , true ) ;
var network = _btcPayNetworkProvider . GetNetwork < BTCPayNetwork > ( cryptoCode ) ;
2020-06-08 23:40:58 +09:00
if ( lightningInvoice ? . BOLT11 is null | |
! BOLT11PaymentRequest . TryParse ( lightningInvoice . BOLT11 , out _ , network . NBitcoinNetwork ) )
2020-05-29 02:00:13 +02:00
{
2020-06-08 23:40:58 +09:00
ModelState . AddModelError ( nameof ( lightningInvoice . BOLT11 ) , "The BOLT11 invoice was invalid." ) ;
2020-05-29 02:00:13 +02:00
}
2020-06-08 23:40:58 +09:00
if ( ! ModelState . IsValid )
2020-05-29 02:00:13 +02:00
{
2020-06-08 23:40:58 +09:00
return this . CreateValidationError ( ModelState ) ;
2020-05-29 02:00:13 +02:00
}
2022-02-17 10:01:39 +01:00
2022-05-18 07:57:36 +02:00
var param = lightningInvoice ? . MaxFeeFlat ! = null | | lightningInvoice ? . MaxFeePercent ! = null | | lightningInvoice ? . Amount ! = null
? new PayInvoiceParams { MaxFeePercent = lightningInvoice . MaxFeePercent , MaxFeeFlat = lightningInvoice . MaxFeeFlat , Amount = lightningInvoice . Amount }
2022-02-17 10:01:39 +01:00
: null ;
2022-04-26 03:29:20 +02:00
var result = await lightningClient . Pay ( lightningInvoice . BOLT11 , param , cancellationToken ) ;
2022-02-17 10:01:39 +01:00
return result . Result switch
2020-05-29 02:00:13 +02:00
{
2022-02-17 10:01:39 +01:00
PayResult . CouldNotFindRoute = > this . CreateAPIError ( "could-not-find-route" , "Impossible to find a route to the peer" ) ,
PayResult . Error = > this . CreateAPIError ( "generic-error" , result . ErrorDetail ) ,
PayResult . Ok = > Ok ( new LightningPaymentData
{
TotalAmount = result . Details ? . TotalAmount ,
FeeAmount = result . Details ? . FeeAmount
} ) ,
_ = > throw new NotSupportedException ( "Unsupported Payresult" )
} ;
2020-05-29 02:00:13 +02:00
}
2022-04-26 03:29:20 +02:00
public virtual async Task < IActionResult > GetInvoice ( string cryptoCode , string id , CancellationToken cancellationToken = default )
2020-05-29 02:00:13 +02:00
{
var lightningClient = await GetLightningClient ( cryptoCode , false ) ;
2022-04-26 03:29:20 +02:00
var inv = await lightningClient . GetInvoice ( id , cancellationToken ) ;
2021-12-23 05:32:08 +01:00
return inv = = null ? this . CreateAPIError ( 404 , "invoice-not-found" , "Impossible to find a lightning invoice with this id" ) : Ok ( ToModel ( inv ) ) ;
2020-05-29 02:00:13 +02:00
}
2022-04-26 03:29:20 +02:00
public virtual async Task < IActionResult > CreateInvoice ( string cryptoCode , CreateLightningInvoiceRequest request , CancellationToken cancellationToken = default )
2020-05-29 02:00:13 +02:00
{
var lightningClient = await GetLightningClient ( cryptoCode , false ) ;
2020-06-08 23:40:58 +09:00
if ( request . Amount < LightMoney . Zero )
2020-05-29 02:00:13 +02:00
{
2020-06-08 23:40:58 +09:00
ModelState . AddModelError ( nameof ( request . Amount ) , "Amount should be more or equals to 0" ) ;
2020-05-29 02:00:13 +02:00
}
2020-06-08 23:40:58 +09:00
if ( request . Expiry < = TimeSpan . Zero )
2020-05-29 02:00:13 +02:00
{
2020-06-08 23:40:58 +09:00
ModelState . AddModelError ( nameof ( request . Expiry ) , "Expiry should be more than 0" ) ;
2020-05-29 02:00:13 +02:00
}
2020-06-08 23:40:58 +09:00
if ( ! ModelState . IsValid )
2020-05-29 02:00:13 +02:00
{
2020-06-08 23:40:58 +09:00
return this . CreateValidationError ( ModelState ) ;
2020-05-29 02:00:13 +02:00
}
2020-11-23 06:40:13 +01:00
try
{
2022-03-17 10:15:27 +01:00
var param = request . DescriptionHash ! = null
? new CreateInvoiceParams ( request . Amount , request . DescriptionHash , request . Expiry )
2020-11-23 06:40:13 +01:00
{
2022-03-17 10:15:27 +01:00
PrivateRouteHints = request . PrivateRouteHints , Description = request . Description
}
: new CreateInvoiceParams ( request . Amount , request . Description , request . Expiry )
{
PrivateRouteHints = request . PrivateRouteHints , DescriptionHash = request . DescriptionHash
} ;
2022-04-26 03:29:20 +02:00
var invoice = await lightningClient . CreateInvoice ( param , cancellationToken ) ;
2020-11-23 06:40:13 +01:00
return Ok ( ToModel ( invoice ) ) ;
}
catch ( Exception ex )
{
2020-11-23 19:39:53 +09:00
return this . CreateAPIError ( "generic-error" , ex . Message ) ;
2020-11-23 06:40:13 +01:00
}
2020-05-29 02:00:13 +02:00
}
2021-12-16 12:32:13 +09:00
protected JsonHttpException ErrorLightningNodeNotConfiguredForStore ( )
{
return new JsonHttpException ( this . CreateAPIError ( 404 , "lightning-not-configured" , "The lightning node is not set up" ) ) ;
}
protected JsonHttpException ErrorInternalLightningNodeNotConfigured ( )
{
return new JsonHttpException ( this . CreateAPIError ( 404 , "lightning-not-configured" , "The internal lightning node is not set up" ) ) ;
}
protected JsonHttpException ErrorCryptoCodeNotFound ( )
{
return new JsonHttpException ( this . CreateAPIError ( 404 , "unknown-cryptocode" , "This crypto code isn't set up in this BTCPay Server instance" ) ) ;
}
protected JsonHttpException ErrorShouldBeAdminForInternalNode ( )
{
2022-01-11 17:22:10 +09:00
return new JsonHttpException ( this . CreateAPIPermissionError ( "btcpay.server.canuseinternallightningnode" , "The user should be admin to use the internal lightning node" ) ) ;
2021-12-16 12:32:13 +09:00
}
2020-05-29 02:00:13 +02:00
private LightningInvoiceData ToModel ( LightningInvoice invoice )
{
2022-02-17 10:01:39 +01:00
return new LightningInvoiceData
2020-05-29 02:00:13 +02:00
{
Amount = invoice . Amount ,
Id = invoice . Id ,
Status = invoice . Status ,
AmountReceived = invoice . AmountReceived ,
PaidAt = invoice . PaidAt ,
BOLT11 = invoice . BOLT11 ,
ExpiresAt = invoice . ExpiresAt
} ;
}
2022-04-12 11:01:58 +02:00
private LightningPaymentData ToModel ( LightningPayment payment )
{
return new LightningPaymentData
{
TotalAmount = payment . AmountSent ,
FeeAmount = payment . Amount ! = null & & payment . AmountSent ! = null ? payment . AmountSent - payment . Amount : null ,
Id = payment . Id ,
Status = payment . Status ,
CreatedAt = payment . CreatedAt ,
BOLT11 = payment . BOLT11 ,
PaymentHash = payment . PaymentHash ,
Preimage = payment . Preimage
} ;
}
2021-03-02 11:11:58 +09:00
protected async Task < bool > CanUseInternalLightning ( bool doingAdminThings )
2020-05-29 02:00:13 +02:00
{
2022-05-24 13:18:16 +09:00
return ( ! doingAdminThings & & this . _policiesSettings . AllowLightningInternalNodeForAll ) | |
2021-03-02 11:11:58 +09:00
( await _authorizationService . AuthorizeAsync ( User , null ,
new PolicyRequirement ( Policies . CanUseInternalLightningNode ) ) ) . Succeeded ;
2020-05-29 02:00:13 +02:00
}
protected abstract Task < ILightningClient > GetLightningClient ( string cryptoCode , bool doingAdminThings ) ;
}
}