2022-04-24 05:19:34 +02:00
using System ;
2023-07-20 15:05:14 +02:00
using System.Collections.Concurrent ;
2022-04-24 05:19:34 +02:00
using System.Collections.Generic ;
using System.Linq ;
2024-10-19 21:33:34 +09:00
using System.Threading ;
2022-04-24 05:19:34 +02:00
using System.Threading.Tasks ;
2023-03-17 13:50:37 +01:00
using BTCPayServer.Abstractions.Contracts ;
2023-05-26 16:49:32 +02:00
using BTCPayServer.Client ;
2022-04-24 05:19:34 +02:00
using BTCPayServer.Client.Models ;
using BTCPayServer.Configuration ;
using BTCPayServer.Data ;
using BTCPayServer.Data.Payouts.LightningLike ;
2022-08-17 09:45:51 +02:00
using BTCPayServer.HostedServices ;
2022-04-24 05:19:34 +02:00
using BTCPayServer.Lightning ;
using BTCPayServer.Payments ;
2024-04-04 16:31:04 +09:00
using BTCPayServer.Payments.Bitcoin ;
2022-04-24 05:19:34 +02:00
using BTCPayServer.Payments.Lightning ;
2024-05-01 10:22:07 +09:00
using BTCPayServer.Payouts ;
2022-04-24 05:19:34 +02:00
using BTCPayServer.Services ;
2024-04-04 16:31:04 +09:00
using BTCPayServer.Services.Invoices ;
2022-04-24 05:19:34 +02:00
using BTCPayServer.Services.Stores ;
2024-10-19 21:33:34 +09:00
using LNURL ;
2024-03-14 10:29:14 +01:00
using Microsoft.EntityFrameworkCore ;
2022-04-24 05:19:34 +02:00
using Microsoft.Extensions.Logging ;
using Microsoft.Extensions.Options ;
2023-07-20 15:05:14 +02:00
using NBitcoin ;
2024-10-19 21:33:34 +09:00
using Newtonsoft.Json.Linq ;
using static BTCPayServer . Data . Payouts . LightningLike . UILightningLikePayoutController ;
2024-03-14 10:29:14 +01:00
using MarkPayoutRequest = BTCPayServer . HostedServices . MarkPayoutRequest ;
2022-04-24 05:19:34 +02:00
using PayoutData = BTCPayServer . Data . PayoutData ;
2023-02-21 15:06:34 +09:00
using PayoutProcessorData = BTCPayServer . Data . PayoutProcessorData ;
2022-04-24 05:19:34 +02:00
namespace BTCPayServer.PayoutProcessors.Lightning ;
2023-07-20 15:05:14 +02:00
public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor < LightningAutomatedPayoutBlob >
2022-04-24 05:19:34 +02:00
{
2024-10-19 21:33:34 +09:00
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings ;
private readonly LightningClientFactoryService _lightningClientFactoryService ;
private readonly UserService _userService ;
private readonly IOptions < LightningNetworkOptions > _options ;
private readonly PullPaymentHostedService _pullPaymentHostedService ;
private readonly LightningLikePayoutHandler _payoutHandler ;
public BTCPayNetwork Network = > _payoutHandler . Network ;
private readonly PaymentMethodHandlerDictionary _handlers ;
public LightningAutomatedPayoutProcessor (
PayoutMethodId payoutMethodId ,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings ,
LightningClientFactoryService lightningClientFactoryService ,
PayoutMethodHandlerDictionary payoutHandlers ,
UserService userService ,
ILoggerFactory logger , IOptions < LightningNetworkOptions > options ,
StoreRepository storeRepository , PayoutProcessorData payoutProcessorSettings ,
ApplicationDbContextFactory applicationDbContextFactory ,
PaymentMethodHandlerDictionary handlers ,
IPluginHookService pluginHookService ,
EventAggregator eventAggregator ,
PullPaymentHostedService pullPaymentHostedService ) :
base ( PaymentTypes . LN . GetPaymentMethodId ( GetPayoutHandler ( payoutHandlers , payoutMethodId ) . Network . CryptoCode ) , logger , storeRepository , payoutProcessorSettings , applicationDbContextFactory ,
handlers , pluginHookService , eventAggregator )
{
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings ;
_lightningClientFactoryService = lightningClientFactoryService ;
_userService = userService ;
_options = options ;
_pullPaymentHostedService = pullPaymentHostedService ;
_payoutHandler = GetPayoutHandler ( payoutHandlers , payoutMethodId ) ;
_handlers = handlers ;
}
private static LightningLikePayoutHandler GetPayoutHandler ( PayoutMethodHandlerDictionary payoutHandlers , PayoutMethodId payoutMethodId )
{
return ( LightningLikePayoutHandler ) payoutHandlers [ payoutMethodId ] ;
}
public async Task < ResultVM > HandlePayout ( PayoutData payoutData , ILightningClient lightningClient , CancellationToken cancellationToken )
{
using var scope = _payoutHandler . PayoutsPaymentProcessing . StartTracking ( ) ;
if ( payoutData . State ! = PayoutState . AwaitingPayment | | ! scope . TryTrack ( payoutData . Id ) )
return InvalidState ( payoutData . Id ) ;
var blob = payoutData . GetBlob ( _btcPayNetworkJsonSerializerSettings ) ;
var res = await _pullPaymentHostedService . MarkPaid ( new MarkPayoutRequest ( )
{
State = PayoutState . InProgress ,
PayoutId = payoutData . Id ,
Proof = null
} ) ;
if ( res ! = MarkPayoutRequest . PayoutPaidResult . Ok )
return InvalidState ( payoutData . Id ) ;
ResultVM result ;
var claim = await _payoutHandler . ParseClaimDestination ( blob . Destination , cancellationToken ) ;
switch ( claim . destination )
{
case LNURLPayClaimDestinaton lnurlPayClaimDestinaton :
var lnurlResult = await GetInvoiceFromLNURL ( payoutData , _payoutHandler , blob ,
lnurlPayClaimDestinaton , cancellationToken ) ;
if ( lnurlResult . Item2 is not null )
{
result = lnurlResult . Item2 ;
}
else
{
result = await TrypayBolt ( lightningClient , blob , payoutData , lnurlResult . Item1 , cancellationToken ) ;
}
break ;
case BoltInvoiceClaimDestination item1 :
result = await TrypayBolt ( lightningClient , blob , payoutData , item1 . PaymentRequest , cancellationToken ) ;
break ;
default :
result = new ResultVM
{
PayoutId = payoutData . Id ,
2024-11-08 16:13:13 +09:00
Success = false ,
2024-10-19 21:33:34 +09:00
Destination = blob . Destination ,
Message = claim . error
} ;
break ;
}
2024-11-12 09:58:10 +09:00
if ( result . Success is false & & blob . NonInteractiveOnly )
payoutData . State = PayoutState . Cancelled ;
2024-10-19 21:33:34 +09:00
bool updateBlob = false ;
2024-11-08 16:13:13 +09:00
if ( result . Success is false & & payoutData . State = = PayoutState . AwaitingPayment )
2024-10-19 21:33:34 +09:00
{
updateBlob = true ;
2024-10-20 00:08:28 +09:00
if ( blob . IncrementErrorCount ( ) > = 10 )
blob . DisableProcessor ( LightningAutomatedPayoutSenderFactory . ProcessorName ) ;
2024-10-19 21:33:34 +09:00
}
if ( payoutData . State ! = PayoutState . InProgress | | payoutData . Proof is not null )
{
await _pullPaymentHostedService . MarkPaid ( new MarkPayoutRequest ( )
{
State = payoutData . State ,
PayoutId = payoutData . Id ,
Proof = payoutData . GetProofBlobJson ( ) ,
2024-10-19 22:07:20 +09:00
UpdateBlob = updateBlob ? blob : null
2024-10-19 21:33:34 +09:00
} ) ;
}
return result ;
}
private ResultVM InvalidState ( string payoutId ) = >
new ResultVM
{
PayoutId = payoutId ,
2024-11-08 16:13:13 +09:00
Success = false ,
2024-10-19 21:33:34 +09:00
Message = "The payout isn't in a valid state"
} ;
async Task < ( BOLT11PaymentRequest , ResultVM ) > GetInvoiceFromLNURL ( PayoutData payoutData ,
LightningLikePayoutHandler handler , PayoutBlob blob , LNURLPayClaimDestinaton lnurlPayClaimDestinaton , CancellationToken cancellationToken )
2024-05-01 10:22:07 +09:00
{
2024-10-19 21:33:34 +09:00
var endpoint = lnurlPayClaimDestinaton . LNURL . IsValidEmail ( )
? LNURL . LNURL . ExtractUriFromInternetIdentifier ( lnurlPayClaimDestinaton . LNURL )
: LNURL . LNURL . Parse ( lnurlPayClaimDestinaton . LNURL , out _ ) ;
var httpClient = handler . CreateClient ( endpoint ) ;
var lnurlInfo =
( LNURLPayRequest ) await LNURL . LNURL . FetchInformation ( endpoint , "payRequest" ,
httpClient , cancellationToken ) ;
var lm = new LightMoney ( payoutData . Amount . Value , LightMoneyUnit . BTC ) ;
if ( lm > lnurlInfo . MaxSendable | | lm < lnurlInfo . MinSendable )
{
payoutData . State = PayoutState . Cancelled ;
return ( null , new ResultVM
{
PayoutId = payoutData . Id ,
2024-11-08 16:13:13 +09:00
Success = false ,
2024-10-19 21:33:34 +09:00
Destination = blob . Destination ,
Message =
$"The LNURL provided would not generate an invoice of {lm.ToDecimal(LightMoneyUnit.Satoshi)} sats"
} ) ;
}
try
{
var lnurlPayRequestCallbackResponse =
await lnurlInfo . SendRequest ( lm , this . Network . NBitcoinNetwork , httpClient , cancellationToken : cancellationToken ) ;
return ( lnurlPayRequestCallbackResponse . GetPaymentRequest ( this . Network . NBitcoinNetwork ) , null ) ;
}
catch ( LNUrlException e )
{
return ( null ,
new ResultVM
{
PayoutId = payoutData . Id ,
2024-11-08 16:13:13 +09:00
Success = false ,
2024-10-19 21:33:34 +09:00
Destination = blob . Destination ,
Message = e . Message
} ) ;
}
2024-05-01 10:22:07 +09:00
}
2022-04-24 05:19:34 +02:00
2024-10-19 21:33:34 +09:00
async Task < ResultVM > TrypayBolt (
ILightningClient lightningClient , PayoutBlob payoutBlob , PayoutData payoutData , BOLT11PaymentRequest bolt11PaymentRequest , CancellationToken cancellationToken )
2022-04-24 05:19:34 +02:00
{
2024-10-19 21:33:34 +09:00
var boltAmount = bolt11PaymentRequest . MinimumAmount . ToDecimal ( LightMoneyUnit . BTC ) ;
// BoltAmount == 0: Any amount is OK.
// While we could allow paying more than the minimum amount from the boltAmount,
// Core-Lightning do not support it! It would just refuse to pay more than the boltAmount.
if ( boltAmount ! = payoutData . Amount . Value & & boltAmount ! = 0.0 m )
2024-03-14 10:29:14 +01:00
{
2024-10-19 21:33:34 +09:00
payoutData . State = PayoutState . Cancelled ;
return new ResultVM
{
PayoutId = payoutData . Id ,
2024-11-08 16:13:13 +09:00
Success = false ,
2024-10-19 21:33:34 +09:00
Message = $"The BOLT11 invoice amount ({boltAmount} {payoutData.Currency}) did not match the payout's amount ({payoutData.Amount.GetValueOrDefault()} {payoutData.Currency})" ,
Destination = payoutBlob . Destination
} ;
}
if ( bolt11PaymentRequest . ExpiryDate < DateTimeOffset . Now )
2022-04-24 05:19:34 +02:00
{
2024-10-19 21:33:34 +09:00
payoutData . State = PayoutState . Cancelled ;
return new ResultVM
{
PayoutId = payoutData . Id ,
2024-11-08 16:13:13 +09:00
Success = false ,
2024-10-19 21:33:34 +09:00
Message = $"The BOLT11 invoice expiry date ({bolt11PaymentRequest.ExpiryDate}) has expired" ,
Destination = payoutBlob . Destination
} ;
2022-04-24 05:19:34 +02:00
}
2024-10-19 21:33:34 +09:00
var proofBlob = new PayoutLightningBlob { PaymentHash = bolt11PaymentRequest . PaymentHash . ToString ( ) } ;
2024-11-08 16:13:13 +09:00
string errorReason = null ;
string preimage = null ;
2024-11-09 23:09:31 +09:00
// If success:
// * Is null, we don't know the status. The payout should become pending. (LightningPendingPayoutListener will monitor the situation)
// * Is true, we knew the transfer was done. The payout should be completed.
// * Is false, we knew it didn't happen. The payout can be retried.
2024-11-08 16:13:13 +09:00
bool? success = null ;
LightMoney amountSent = null ;
2024-03-14 10:29:14 +01:00
try
2022-04-24 05:19:34 +02:00
{
2024-11-08 16:13:13 +09:00
var pay = await lightningClient . Pay ( bolt11PaymentRequest . ToString ( ) ,
2024-11-05 09:54:58 +01:00
new PayInvoiceParams ( )
2024-10-19 21:33:34 +09:00
{
2024-11-05 09:54:58 +01:00
Amount = new LightMoney ( ( decimal ) payoutData . Amount , LightMoneyUnit . BTC )
} , cancellationToken ) ;
2024-11-08 16:13:13 +09:00
if ( pay is { Result : PayResult . CouldNotFindRoute } )
{
2024-11-11 12:09:17 +09:00
var err = pay . ErrorDetail is null ? "" : $" ({pay.ErrorDetail})" ;
errorReason ? ? = $"Unable to find a route for the payment, check your channel liquidity{err}" ;
2024-11-08 16:13:13 +09:00
success = false ;
}
else if ( pay is { Result : PayResult . Error } )
{
errorReason ? ? = pay . ErrorDetail ;
success = false ;
}
else if ( pay is { Result : PayResult . Ok } )
2024-10-19 21:33:34 +09:00
{
2024-11-08 16:13:13 +09:00
if ( pay . Details is { } details )
2024-10-19 21:33:34 +09:00
{
2024-11-08 16:13:13 +09:00
preimage = details . Preimage ? . ToString ( ) ;
amountSent = details . TotalAmount ;
}
success = true ;
2024-10-19 21:33:34 +09:00
}
2024-11-05 09:54:58 +01:00
}
catch ( Exception ex )
{
2024-11-08 16:13:13 +09:00
errorReason ? ? = ex . Message ;
2024-11-05 09:54:58 +01:00
}
2024-10-19 21:33:34 +09:00
2024-11-08 16:13:13 +09:00
if ( success is null | | preimage is null | | amountSent is null )
2024-11-05 09:54:58 +01:00
{
2024-11-08 16:13:13 +09:00
LightningPayment payment = null ;
try
2024-10-19 21:33:34 +09:00
{
2024-11-08 16:13:13 +09:00
payment = await lightningClient . GetPayment ( bolt11PaymentRequest . PaymentHash . ToString ( ) , cancellationToken ) ;
}
catch ( Exception ex )
{
errorReason ? ? = ex . Message ;
}
success ? ? = payment ? . Status switch
{
LightningPaymentStatus . Complete = > true ,
LightningPaymentStatus . Failed = > false ,
_ = > null
2024-11-05 09:54:58 +01:00
} ;
2024-11-08 16:13:13 +09:00
amountSent ? ? = payment ? . AmountSent ;
preimage ? ? = payment ? . Preimage ;
2024-11-05 09:54:58 +01:00
}
2024-11-08 16:13:13 +09:00
if ( preimage is not null )
proofBlob . Preimage = preimage ;
var vm = new ResultVM
{
PayoutId = payoutData . Id ,
Success = success ,
Destination = payoutBlob . Destination
} ;
if ( success is true )
2024-11-05 09:54:58 +01:00
{
payoutData . State = PayoutState . Completed ;
payoutData . SetProofBlob ( proofBlob , null ) ;
2024-11-08 16:13:13 +09:00
vm . Message = amountSent ! = null
? $"Paid out {amountSent.ToDecimal(LightMoneyUnit.BTC)} {payoutData.Currency}"
: "Paid out" ;
2024-11-05 09:54:58 +01:00
}
2024-11-08 16:13:13 +09:00
else if ( success is false )
2024-11-05 09:54:58 +01:00
{
payoutData . State = PayoutState . AwaitingPayment ;
2024-11-08 16:13:13 +09:00
var err = errorReason is null ? "" : $" ({errorReason})" ;
vm . Message = $"The payment failed{err}" ;
2024-10-19 21:33:34 +09:00
}
2024-11-05 09:54:58 +01:00
else
2024-03-14 10:29:14 +01:00
{
2024-10-19 21:33:34 +09:00
// Payment will be saved as pending, the LightningPendingPayoutListener will handle settling/cancelling
payoutData . State = PayoutState . InProgress ;
payoutData . SetProofBlob ( proofBlob , null ) ;
2024-11-08 16:13:13 +09:00
vm . Message = "The payment has been initiated but is still in-flight." ;
2022-04-24 05:19:34 +02:00
}
2024-11-08 16:13:13 +09:00
return vm ;
2022-04-24 05:19:34 +02:00
}
2023-01-06 14:18:07 +01:00
2024-04-04 16:31:04 +09:00
protected override async Task < bool > ProcessShouldSave ( object paymentMethodConfig , List < PayoutData > payouts )
2024-10-19 21:33:34 +09:00
{
var lightningSupportedPaymentMethod = ( LightningPaymentMethodConfig ) paymentMethodConfig ;
if ( lightningSupportedPaymentMethod . IsInternalNode & &
! await _storeRepository . InternalNodePayoutAuthorized ( PayoutProcessorSettings . StoreId ) )
{
return false ;
}
2024-03-14 10:29:14 +01:00
2024-10-19 21:33:34 +09:00
var client =
lightningSupportedPaymentMethod . CreateLightningClient ( Network , _options . Value ,
_lightningClientFactoryService ) ;
await Task . WhenAll ( payouts . Select ( data = > HandlePayout ( data , client , CancellationToken ) ) ) ;
2024-03-14 10:29:14 +01:00
2024-10-19 21:33:34 +09:00
//we return false because this processor handles db updates on its own
return false ;
}
2022-04-24 05:19:34 +02:00
}