2021-08-22 23:13:26 -07:00
#nullable enable
2020-06-28 21:44:35 -05:00
using System ;
2018-02-26 00:48:12 +09:00
using System.Collections.Generic ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
2024-04-04 16:31:04 +09:00
using AngleSharp.Dom ;
using BTCPayServer.Abstractions.Extensions ;
using BTCPayServer.Client ;
2021-12-31 16:59:02 +09:00
using BTCPayServer.Client.Models ;
2021-03-02 11:11:58 +09:00
using BTCPayServer.Configuration ;
2018-04-07 16:27:46 +09:00
using BTCPayServer.Data ;
2018-03-02 14:03:18 -05:00
using BTCPayServer.HostedServices ;
2018-08-30 11:50:39 +09:00
using BTCPayServer.Lightning ;
2023-06-20 10:28:16 +02:00
using BTCPayServer.Lightning.LndHub ;
2020-06-28 17:55:27 +09:00
using BTCPayServer.Logging ;
2019-05-29 14:33:31 +00:00
using BTCPayServer.Models ;
using BTCPayServer.Models.InvoicingModels ;
2024-04-04 16:31:04 +09:00
using BTCPayServer.Payments.Bitcoin ;
using BTCPayServer.Security ;
2019-03-18 00:03:02 +09:00
using BTCPayServer.Services ;
2020-06-28 17:55:27 +09:00
using BTCPayServer.Services.Invoices ;
2024-04-04 16:31:04 +09:00
using Microsoft.AspNetCore.Authorization ;
2021-03-02 11:11:58 +09:00
using Microsoft.Extensions.Options ;
2019-03-31 13:16:05 +09:00
using NBitcoin ;
2024-04-04 16:31:04 +09:00
using NBitcoin.Protocol ;
using Newtonsoft.Json ;
using Newtonsoft.Json.Linq ;
2018-02-26 00:48:12 +09:00
namespace BTCPayServer.Payments.Lightning
{
2024-04-04 16:31:04 +09:00
public interface ILightningPaymentHandler : IHasNetwork
2018-02-26 00:48:12 +09:00
{
2024-04-04 16:31:04 +09:00
LightningPaymentData ParsePaymentDetails ( JToken details ) ;
}
public class LightningLikePaymentHandler : IPaymentMethodHandler , ILightningPaymentHandler
{
public JsonSerializer Serializer { get ; }
2023-03-21 14:22:10 +01:00
public static readonly int LightningTimeout = 5000 ;
2020-06-28 22:07:48 -05:00
readonly NBXplorerDashboard _Dashboard ;
2019-04-11 01:10:29 +09:00
private readonly LightningClientFactoryService _lightningClientFactory ;
2024-04-04 16:31:04 +09:00
private readonly BTCPayNetwork _Network ;
2019-03-18 00:03:02 +09:00
private readonly SocketFactory _socketFactory ;
2023-03-13 02:12:58 +01:00
private readonly DisplayFormatter _displayFormatter ;
2024-04-04 16:31:04 +09:00
private readonly ISettingsAccessor < PoliciesSettings > _policies ;
private readonly IOptions < LightningNetworkOptions > _lightningNetworkOptions ;
2019-03-18 00:03:02 +09:00
2018-03-20 12:10:35 +09:00
public LightningLikePaymentHandler (
2024-04-04 16:31:04 +09:00
PaymentMethodId paymentMethodId ,
2019-03-18 00:03:02 +09:00
NBXplorerDashboard dashboard ,
2019-04-11 01:10:29 +09:00
LightningClientFactoryService lightningClientFactory ,
2024-04-04 16:31:04 +09:00
BTCPayNetwork network ,
2021-12-31 16:59:02 +09:00
SocketFactory socketFactory ,
2023-03-13 02:12:58 +01:00
DisplayFormatter displayFormatter ,
2024-04-04 16:31:04 +09:00
IOptions < LightningNetworkOptions > options ,
ISettingsAccessor < PoliciesSettings > policies ,
IOptions < LightningNetworkOptions > lightningNetworkOptions )
2018-02-26 00:48:12 +09:00
{
2024-04-04 16:31:04 +09:00
Serializer = BlobSerializer . CreateSerializer ( network . NBitcoinNetwork ) . Serializer ;
2018-03-02 14:03:18 -05:00
_Dashboard = dashboard ;
2019-04-11 01:10:29 +09:00
_lightningClientFactory = lightningClientFactory ;
2024-04-04 16:31:04 +09:00
_Network = network ;
2019-03-18 00:03:02 +09:00
_socketFactory = socketFactory ;
2023-03-13 02:12:58 +01:00
_displayFormatter = displayFormatter ;
2021-03-02 11:11:58 +09:00
Options = options ;
2024-04-04 16:31:04 +09:00
_policies = policies ;
_lightningNetworkOptions = lightningNetworkOptions ;
PaymentMethodId = paymentMethodId ;
}
public Task BeforeFetchingRates ( PaymentMethodContext context )
{
context . Prompt . Currency = _Network . CryptoCode ;
context . Prompt . PaymentMethodFee = 0 m ;
context . Prompt . Divisibility = 11 ;
return Task . CompletedTask ;
2018-02-26 00:48:12 +09:00
}
2019-05-24 06:38:47 +00:00
2024-04-04 16:31:04 +09:00
public PaymentMethodId PaymentMethodId { get ; private set ; }
2021-03-02 11:11:58 +09:00
public IOptions < LightningNetworkOptions > Options { get ; }
2024-04-04 16:31:04 +09:00
public BTCPayNetwork Network = > _Network ;
public async Task ConfigurePrompt ( PaymentMethodContext context )
2018-02-26 00:48:12 +09:00
{
2024-04-04 16:31:04 +09:00
if ( context . InvoiceEntity . Type = = InvoiceType . TopUp )
2021-12-31 16:59:02 +09:00
{
2021-08-22 23:13:26 -07:00
throw new PaymentMethodUnavailableException ( "Lightning Network payment method is not available for top-up invoices" ) ;
}
2024-04-04 16:31:04 +09:00
var paymentPrompt = context . Prompt ;
var preferOnion = Uri . TryCreate ( context . InvoiceEntity . ServerUrl , UriKind . Absolute , out var u ) & & u . IsOnion ( ) ;
var storeBlob = context . StoreBlob ;
var store = context . Store ;
2021-12-31 16:59:02 +09:00
2024-04-04 16:31:04 +09:00
var config = ParsePaymentMethodConfig ( context . PaymentMethodConfig ) ;
var nodeInfo = GetNodeInfo ( config , context . Logs , preferOnion ) ;
var invoice = context . InvoiceEntity ;
decimal due = Extensions . RoundUp ( invoice . Price / paymentPrompt . Rate , _Network . Divisibility ) ;
2020-09-15 13:46:45 +02:00
try
{
2024-04-04 16:31:04 +09:00
due = paymentPrompt . Calculate ( ) . Due ;
2020-09-15 13:46:45 +02:00
}
catch ( Exception )
{
// ignored
}
2024-04-04 16:31:04 +09:00
var client = config . CreateLightningClient ( _Network , Options . Value , _lightningClientFactory ) ;
2018-02-26 00:48:12 +09:00
var expiry = invoice . ExpirationTime - DateTimeOffset . UtcNow ;
2018-03-20 11:59:43 +09:00
if ( expiry < TimeSpan . Zero )
expiry = TimeSpan . FromSeconds ( 1 ) ;
2018-02-26 00:48:12 +09:00
2023-01-13 09:29:41 +01:00
LightningInvoice ? lightningInvoice ;
2018-05-12 00:14:39 +09:00
string description = storeBlob . LightningDescriptionTemplate ;
description = description . Replace ( "{StoreName}" , store . StoreName ? ? "" , StringComparison . OrdinalIgnoreCase )
2020-08-25 14:33:00 +09:00
. Replace ( "{ItemDescription}" , invoice . Metadata . ItemDesc ? ? "" , StringComparison . OrdinalIgnoreCase )
. Replace ( "{OrderId}" , invoice . Metadata . OrderId ? ? "" , StringComparison . OrdinalIgnoreCase ) ;
2023-03-21 14:22:10 +01:00
using ( var cts = new CancellationTokenSource ( LightningTimeout ) )
2018-03-28 22:37:01 +09:00
{
2018-05-12 00:14:39 +09:00
try
{
2020-05-19 16:47:26 -05:00
var request = new CreateInvoiceParams ( new LightMoney ( due , LightMoneyUnit . BTC ) , description , expiry ) ;
request . PrivateRouteHints = storeBlob . LightningPrivateRouteHints ;
lightningInvoice = await client . CreateInvoice ( request , cts . Token ) ;
2018-05-12 00:14:39 +09:00
}
catch ( OperationCanceledException ) when ( cts . IsCancellationRequested )
{
2020-11-17 08:57:14 +01:00
throw new PaymentMethodUnavailableException ( "The lightning node did not reply in a timely manner" ) ;
2018-05-12 00:14:39 +09:00
}
catch ( Exception ex )
{
throw new PaymentMethodUnavailableException ( $"Impossible to create lightning invoice ({ex.Message})" , ex ) ;
}
2018-02-26 00:48:12 +09:00
}
2021-09-23 13:36:42 +02:00
2024-04-04 16:31:04 +09:00
paymentPrompt . Destination = lightningInvoice . BOLT11 ;
var details = new LigthningPaymentPromptDetails
2018-03-28 22:37:01 +09:00
{
2024-04-04 16:31:04 +09:00
PaymentHash = BOLT11PaymentRequest . Parse ( lightningInvoice . BOLT11 , _Network . NBitcoinNetwork ) . PaymentHash ,
2023-01-13 09:29:41 +01:00
Preimage = string . IsNullOrEmpty ( lightningInvoice . Preimage ) ? null : uint256 . Parse ( lightningInvoice . Preimage ) ,
2018-03-28 22:37:01 +09:00
InvoiceId = lightningInvoice . Id ,
2021-10-25 08:18:02 +02:00
NodeInfo = ( await nodeInfo ) . FirstOrDefault ( ) ? . ToString ( )
2018-03-28 22:37:01 +09:00
} ;
2024-04-04 16:31:04 +09:00
paymentPrompt . Details = JObject . FromObject ( details , Serializer ) ;
2018-02-26 00:48:12 +09:00
}
2024-04-04 16:31:04 +09:00
public async Task < NodeInfo [ ] > GetNodeInfo ( LightningPaymentMethodConfig supportedPaymentMethod , PrefixedInvoiceLogs ? invoiceLogs , bool? preferOnion = null , bool throws = false )
2018-02-26 00:48:12 +09:00
{
2024-04-04 16:31:04 +09:00
var synced = _Dashboard . IsFullySynched ( _Network . CryptoCode , out var summary ) ;
2023-08-22 13:45:50 +02:00
if ( supportedPaymentMethod . IsInternalNode & & ! synced )
2024-04-04 16:31:04 +09:00
throw new PaymentMethodUnavailableException ( "Full node not available" ) ;
;
2021-10-25 08:18:02 +02:00
try
2018-02-26 00:48:12 +09:00
{
2023-03-21 14:22:10 +01:00
using var cts = new CancellationTokenSource ( LightningTimeout ) ;
2024-04-04 16:31:04 +09:00
var client = CreateLightningClient ( supportedPaymentMethod ) ;
2023-06-20 10:28:16 +02:00
// LNDhub-compatible implementations might not offer all of GetInfo data.
// Skip checks in those cases, see https://github.com/lnbits/lnbits/issues/1182
var isLndHub = client is LndHubLightningClient ;
2024-03-22 10:06:38 +01:00
2022-01-14 17:50:29 +09:00
LightningNodeInformation info ;
try
{
info = await client . GetInfo ( cts . Token ) ;
}
catch ( OperationCanceledException ) when ( cts . IsCancellationRequested )
{
throw new PaymentMethodUnavailableException ( "The lightning node did not reply in a timely manner" ) ;
}
2023-12-13 13:40:18 +01:00
catch ( NotSupportedException )
2023-06-20 10:28:16 +02:00
{
2023-12-20 11:23:46 +01:00
// LNDhub, LNbits and others might not support this call, yet we can create invoices.
2024-04-04 16:31:04 +09:00
return new NodeInfo [ ] { } ;
2023-12-20 11:23:46 +01:00
}
catch ( UnauthorizedAccessException )
{
// LND might return this with restricted macaroon, support this nevertheless..
2024-04-04 16:31:04 +09:00
return new NodeInfo [ ] { } ;
2023-06-20 10:28:16 +02:00
}
2022-01-14 17:50:29 +09:00
catch ( Exception ex )
2018-05-12 00:14:39 +09:00
{
2022-01-14 17:50:29 +09:00
throw new PaymentMethodUnavailableException ( $"Error while connecting to the API: {ex.Message}" +
( ! string . IsNullOrEmpty ( ex . InnerException ? . Message ) ? $" ({ex.InnerException.Message})" : "" ) ) ;
2018-05-12 00:14:39 +09:00
}
2022-01-14 17:50:29 +09:00
2022-11-05 12:21:24 +01:00
// Node info might be empty if there are no public URIs to announce. The UI also supports this.
2022-01-14 17:50:29 +09:00
var nodeInfo = preferOnion ! = null & & info . NodeInfoList . Any ( i = > i . IsTor = = preferOnion )
? info . NodeInfoList . Where ( i = > i . IsTor = = preferOnion . Value ) . ToArray ( )
: info . NodeInfoList . Select ( i = > i ) . ToArray ( ) ;
2023-08-22 13:45:50 +02:00
2024-03-22 10:06:38 +01:00
if ( summary . Status is not null )
2022-01-14 17:50:29 +09:00
{
2024-03-22 10:06:38 +01:00
var blocksGap = summary . Status . ChainHeight - info . BlockHeight ;
if ( blocksGap > 10 & & ! ( isLndHub & & info . BlockHeight = = 0 ) )
{
throw new PaymentMethodUnavailableException (
$"The lightning node is not synched ({blocksGap} blocks left)" ) ;
}
2022-01-14 17:50:29 +09:00
}
return nodeInfo ;
2021-10-25 08:18:02 +02:00
}
2021-12-31 16:59:02 +09:00
catch ( Exception e ) when ( ! throws )
2021-10-25 08:18:02 +02:00
{
2024-04-04 16:31:04 +09:00
invoiceLogs ? . Write ( $"NodeInfo failed to be fetched: {e.Message}" , InvoiceEventData . EventSeverity . Error ) ;
2021-10-25 08:18:02 +02:00
}
2018-03-21 00:31:19 +09:00
2021-10-25 08:18:02 +02:00
return Array . Empty < NodeInfo > ( ) ;
}
2018-02-26 00:48:12 +09:00
2024-04-04 16:31:04 +09:00
public ILightningClient CreateLightningClient ( LightningPaymentMethodConfig supportedPaymentMethod )
2021-10-25 08:18:02 +02:00
{
2024-04-04 16:31:04 +09:00
return supportedPaymentMethod . CreateLightningClient ( _Network , Options . Value , _lightningClientFactory ) ;
2018-02-26 00:48:12 +09:00
}
2018-04-09 16:25:31 +09:00
public async Task TestConnection ( NodeInfo nodeInfo , CancellationToken cancellation )
2018-02-26 00:48:12 +09:00
{
try
{
2019-03-31 13:16:05 +09:00
if ( ! Utils . TryParseEndpoint ( nodeInfo . Host , nodeInfo . Port , out var endpoint ) )
2019-03-18 00:03:02 +09:00
throw new PaymentMethodUnavailableException ( $"Could not parse the endpoint {nodeInfo.Host}" ) ;
2018-02-26 00:48:12 +09:00
2022-01-14 17:50:29 +09:00
using var tcp = await _socketFactory . ConnectAsync ( endpoint , cancellation ) ;
2018-04-09 16:25:31 +09:00
}
catch ( Exception ex )
2018-02-26 00:48:12 +09:00
{
2018-04-09 16:25:31 +09:00
throw new PaymentMethodUnavailableException ( $"Error while connecting to the lightning node via {nodeInfo.Host}:{nodeInfo.Port} ({ex.Message})" ) ;
2018-02-26 00:48:12 +09:00
}
}
2024-04-04 16:31:04 +09:00
public LightningPaymentMethodConfig ParsePaymentMethodConfig ( JToken config )
2019-05-29 14:33:31 +00:00
{
2024-04-04 16:31:04 +09:00
return config . ToObject < LightningPaymentMethodConfig > ( Serializer ) ? ? throw new FormatException ( $"Invalid {nameof(LightningPaymentMethodConfig)}" ) ;
2019-05-29 14:33:31 +00:00
}
2024-04-04 16:31:04 +09:00
object IPaymentMethodHandler . ParsePaymentMethodConfig ( JToken config )
2019-05-29 14:33:31 +00:00
{
2024-04-04 16:31:04 +09:00
return ParsePaymentMethodConfig ( config ) ;
2019-05-29 14:33:31 +00:00
}
2024-04-04 16:31:04 +09:00
object IPaymentMethodHandler . ParsePaymentPromptDetails ( JToken details )
2019-05-29 14:33:31 +00:00
{
2024-04-04 16:31:04 +09:00
return ParsePaymentPromptDetails ( details ) ;
2019-05-29 14:33:31 +00:00
}
2024-04-04 16:31:04 +09:00
public LigthningPaymentPromptDetails ParsePaymentPromptDetails ( JToken details )
2019-05-29 14:33:31 +00:00
{
2024-04-04 16:31:04 +09:00
return details . ToObject < LigthningPaymentPromptDetails > ( Serializer ) ? ? throw new FormatException ( $"Invalid {nameof(LigthningPaymentPromptDetails)}" ) ;
2019-05-29 14:33:31 +00:00
}
2024-04-04 16:31:04 +09:00
public LightningPaymentData ParsePaymentDetails ( JToken details )
2019-05-29 14:33:31 +00:00
{
2024-04-04 16:31:04 +09:00
return details . ToObject < LightningPaymentData > ( Serializer ) ? ? throw new FormatException ( $"Invalid {nameof(LightningPaymentData)}" ) ;
2019-05-29 14:33:31 +00:00
}
2024-04-04 16:31:04 +09:00
object IPaymentMethodHandler . ParsePaymentDetails ( JToken details )
2020-11-06 08:41:03 +01:00
{
2024-04-04 16:31:04 +09:00
return ParsePaymentDetails ( details ) ;
2020-11-06 08:41:03 +01:00
}
2024-04-04 16:31:04 +09:00
public async Task ValidatePaymentMethodConfig ( PaymentMethodConfigValidationContext validationContext )
2019-05-29 14:33:31 +00:00
{
2024-04-04 16:31:04 +09:00
if ( validationContext . Config is JValue { Type : JTokenType . String } )
validationContext . Config = new JObject ( ) { [ "connectionString" ] = validationContext . Config . Value < string > ( ) ! } ;
#pragma warning disable CS0618 // Type or member is obsolete
var config = ParsePaymentMethodConfig ( validationContext . Config ) ;
if ( config . ConnectionString = = LightningPaymentMethodConfig . InternalNode )
config . SetInternalNode ( ) ;
LightningPaymentMethodConfig ? oldConfig = null ;
if ( validationContext . PreviousConfig is not null )
oldConfig = ParsePaymentMethodConfig ( validationContext . PreviousConfig ) ;
var connectionStringChanged = oldConfig ? . ConnectionString ! = config . ConnectionString ;
if ( connectionStringChanged & & ! string . IsNullOrEmpty ( config . ConnectionString ) )
{
// Let's check the connection string can be parsed and is safe to use for non-admin.
try
{
var client = _lightningClientFactory . Create ( config . ConnectionString , _Network ) ;
if ( ! client . IsSafe ( ) )
{
var canManage = ( await validationContext . AuthorizationService . AuthorizeAsync ( validationContext . User , null ,
new PolicyRequirement ( Policies . CanModifyServerSettings ) ) ) . Succeeded ;
if ( ! canManage )
{
validationContext . ModelState . AddModelError ( nameof ( config . ConnectionString ) , $"You do not have 'btcpay.server.canmodifyserversettings' rights, so the connection string should not contain 'cookiefilepath', 'macaroondirectorypath', 'macaroonfilepath', and should not point to a local ip or to a dns name ending with '.internal', '.local', '.lan' or '.'." ) ;
return ;
}
}
}
catch
{
validationContext . ModelState . AddModelError ( nameof ( config . ConnectionString ) , "Invalid connection string" ) ;
return ;
}
}
2021-04-07 06:08:42 +02:00
2024-04-04 16:31:04 +09:00
if ( oldConfig ? . IsInternalNode ! = config . IsInternalNode & & config . IsInternalNode )
{
var canUseInternalNode = _policies . Settings . AllowLightningInternalNodeForAll | |
( await validationContext . AuthorizationService . AuthorizeAsync ( validationContext . User , null ,
new PolicyRequirement ( Policies . CanUseInternalLightningNode ) ) ) . Succeeded & & _lightningNetworkOptions . Value . InternalLightningByCryptoCode . ContainsKey ( _Network . CryptoCode ) ;
if ( ! canUseInternalNode )
{
validationContext . SetMissingPermission ( Policies . CanUseInternalLightningNode , $"You are not authorized to use the internal lightning node. Either add '{Policies.CanUseInternalLightningNode}' to an API Key, or allow non-admin users to use the internal lightning node in the server settings." ) ;
return ;
}
}
if ( ! config . IsInternalNode & & string . IsNullOrEmpty ( config . ConnectionString ) )
{
validationContext . ModelState . AddModelError ( nameof ( config . ConnectionString ) , "The connection string or setting the internal node is required" ) ;
return ;
}
validationContext . Config = JToken . FromObject ( config , Serializer ) ;
#pragma warning restore CS0618 // Type or member is obsolete
2021-04-07 06:08:42 +02:00
}
2018-02-26 00:48:12 +09:00
}
}