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 ;
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.HostedServices ;
using BTCPayServer.Lightning ;
2021-03-02 11:11:58 +09:00
using BTCPayServer.Security ;
2020-05-29 02:00:13 +02: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
namespace BTCPayServer.Controllers.GreenField
{
2020-06-08 23:40:58 +09:00
public class LightningUnavailableExceptionFilter : Attribute , IExceptionFilter
{
public void OnException ( ExceptionContext context )
{
2021-12-16 12:32:13 +09:00
context . Result = new ObjectResult ( new GreenfieldAPIError ( "ligthning-node-unavailable" , $"The lightning node is unavailable ({context.Exception.GetType().Name}: {context.Exception.Message})" ) ) { StatusCode = 503 } ;
// Do not mark handled, it is possible filters above have better errors
2020-06-08 23:40:58 +09: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 ;
2021-07-27 14:08:54 +02:00
private readonly ISettingsRepository _settingsRepository ;
2021-03-02 11:11:58 +09:00
private readonly IAuthorizationService _authorizationService ;
2022-01-07 12:17:59 +09:00
protected GreenfieldLightningNodeApiController ( BTCPayNetworkProvider btcPayNetworkProvider ,
2021-07-27 14:08:54 +02:00
ISettingsRepository settingsRepository ,
2021-03-02 11:11:58 +09:00
IAuthorizationService authorizationService )
2020-05-29 02:00:13 +02:00
{
_btcPayNetworkProvider = btcPayNetworkProvider ;
2021-07-27 14:08:54 +02:00
_settingsRepository = settingsRepository ;
2021-03-02 11:11:58 +09:00
_authorizationService = authorizationService ;
2020-05-29 02:00:13 +02:00
}
public virtual async Task < IActionResult > GetInfo ( string cryptoCode )
{
var lightningClient = await GetLightningClient ( cryptoCode , true ) ;
2020-06-08 23:40:58 +09:00
var info = await lightningClient . GetInfo ( ) ;
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
}
public virtual async Task < IActionResult > ConnectToNode ( string cryptoCode , ConnectToNodeRequest request )
{
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
}
2020-06-08 23:40:58 +09:00
var result = await lightningClient . ConnectTo ( request . NodeURI ) ;
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 ( ) ;
}
public virtual async Task < IActionResult > GetChannels ( string cryptoCode )
{
var lightningClient = await GetLightningClient ( cryptoCode , true ) ;
2020-06-08 23:40:58 +09:00
var channels = await lightningClient . ListChannels ( ) ;
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
2020-05-29 02:00:13 +02:00
public virtual async Task < IActionResult > OpenChannel ( string cryptoCode , OpenLightningChannelRequest request )
{
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
}
2020-06-08 23:40:58 +09:00
var response = await lightningClient . OpenChannel ( new Lightning . 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
} ) ;
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
}
public virtual async Task < IActionResult > GetDepositAddress ( string cryptoCode )
{
var lightningClient = await GetLightningClient ( cryptoCode , true ) ;
2020-06-08 23:40:58 +09:00
return Ok ( new JValue ( ( await lightningClient . GetDepositAddress ( ) ) . ToString ( ) ) ) ;
2020-05-29 02:00:13 +02:00
}
public virtual async Task < IActionResult > PayInvoice ( string cryptoCode , PayLightningInvoiceRequest lightningInvoice )
{
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
}
2020-06-08 23:40:58 +09:00
var result = await lightningClient . Pay ( lightningInvoice . BOLT11 ) ;
2020-05-29 02:00:13 +02:00
switch ( result . Result )
{
case PayResult . CouldNotFindRoute :
2020-06-08 23:40:58 +09:00
return this . CreateAPIError ( "could-not-find-route" , "Impossible to find a route to the peer" ) ;
2020-05-29 02:00:13 +02:00
case PayResult . Error :
2020-06-08 23:40:58 +09:00
return this . CreateAPIError ( "generic-error" , result . ErrorDetail ) ;
case PayResult . Ok :
return Ok ( ) ;
default :
throw new NotSupportedException ( "Unsupported Payresult" ) ;
2020-05-29 02:00:13 +02:00
}
}
public virtual async Task < IActionResult > GetInvoice ( string cryptoCode , string id )
{
var lightningClient = await GetLightningClient ( cryptoCode , false ) ;
2020-06-08 23:40:58 +09:00
var inv = await lightningClient . GetInvoice ( id ) ;
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
}
public virtual async Task < IActionResult > CreateInvoice ( string cryptoCode , CreateLightningInvoiceRequest request )
{
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
{
var invoice = await lightningClient . CreateInvoice (
new CreateInvoiceParams ( request . Amount , request . Description , request . Expiry )
{
PrivateRouteHints = request . PrivateRouteHints
} ,
CancellationToken . None ) ;
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 )
{
return new LightningInvoiceData ( )
{
Amount = invoice . Amount ,
Id = invoice . Id ,
Status = invoice . Status ,
AmountReceived = invoice . AmountReceived ,
PaidAt = invoice . PaidAt ,
BOLT11 = invoice . BOLT11 ,
ExpiresAt = invoice . ExpiresAt
} ;
}
2021-03-02 11:11:58 +09:00
protected async Task < bool > CanUseInternalLightning ( bool doingAdminThings )
2020-05-29 02:00:13 +02:00
{
2021-12-31 16:59:02 +09:00
2021-07-27 14:08:54 +02:00
return ( ! doingAdminThings & & ( await _settingsRepository . GetPolicies ( ) ) . 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 ) ;
}
}