2021-10-18 05:37:59 +02:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
2022-08-17 09:45:51 +02:00
using System.Threading ;
2021-10-18 05:37:59 +02:00
using System.Threading.Tasks ;
using BTCPayServer.Abstractions.Constants ;
2021-11-04 08:21:01 +01:00
using BTCPayServer.Client ;
2021-10-18 05:37:59 +02:00
using BTCPayServer.Client.Models ;
using BTCPayServer.Configuration ;
using BTCPayServer.Lightning ;
using BTCPayServer.Payments ;
using BTCPayServer.Payments.Lightning ;
2021-11-04 08:21:01 +01:00
using BTCPayServer.Security ;
2021-10-18 05:37:59 +02:00
using BTCPayServer.Services ;
2022-02-24 13:57:02 +01:00
using BTCPayServer.Services.Stores ;
2021-10-18 05:37:59 +02:00
using LNURL ;
using Microsoft.AspNetCore.Authorization ;
using Microsoft.AspNetCore.Identity ;
using Microsoft.AspNetCore.Mvc ;
using Microsoft.EntityFrameworkCore ;
using Microsoft.Extensions.Options ;
2022-07-15 05:37:47 +02:00
using NBitcoin ;
2021-10-18 05:37:59 +02:00
namespace BTCPayServer.Data.Payouts.LightningLike
{
2022-02-24 13:57:02 +01:00
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2021-10-18 05:37:59 +02:00
[AutoValidateAntiforgeryToken]
2022-01-07 12:32:00 +09:00
public class UILightningLikePayoutController : Controller
2021-10-18 05:37:59 +02:00
{
private readonly ApplicationDbContextFactory _applicationDbContextFactory ;
private readonly UserManager < ApplicationUser > _userManager ;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings ;
private readonly IEnumerable < IPayoutHandler > _payoutHandlers ;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider ;
private readonly LightningClientFactoryService _lightningClientFactoryService ;
private readonly IOptions < LightningNetworkOptions > _options ;
2021-11-04 08:21:01 +01:00
private readonly IAuthorizationService _authorizationService ;
2022-02-24 13:57:02 +01:00
private readonly StoreRepository _storeRepository ;
2021-10-18 05:37:59 +02:00
2022-01-07 12:32:00 +09:00
public UILightningLikePayoutController ( ApplicationDbContextFactory applicationDbContextFactory ,
2021-10-18 05:37:59 +02:00
UserManager < ApplicationUser > userManager ,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings ,
IEnumerable < IPayoutHandler > payoutHandlers ,
BTCPayNetworkProvider btcPayNetworkProvider ,
2022-02-24 13:57:02 +01:00
StoreRepository storeRepository ,
2021-10-18 05:37:59 +02:00
LightningClientFactoryService lightningClientFactoryService ,
2021-11-04 08:21:01 +01:00
IOptions < LightningNetworkOptions > options , IAuthorizationService authorizationService )
2021-10-18 05:37:59 +02:00
{
_applicationDbContextFactory = applicationDbContextFactory ;
_userManager = userManager ;
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings ;
_payoutHandlers = payoutHandlers ;
_btcPayNetworkProvider = btcPayNetworkProvider ;
_lightningClientFactoryService = lightningClientFactoryService ;
_options = options ;
2022-02-24 13:57:02 +01:00
_storeRepository = storeRepository ;
2021-11-04 08:21:01 +01:00
_authorizationService = authorizationService ;
2021-10-18 05:37:59 +02:00
}
private async Task < List < PayoutData > > GetPayouts ( ApplicationDbContext dbContext , PaymentMethodId pmi ,
string [ ] payoutIds )
{
var userId = _userManager . GetUserId ( User ) ;
if ( string . IsNullOrEmpty ( userId ) )
{
return new List < PayoutData > ( ) ;
}
var pmiStr = pmi . ToString ( ) ;
var approvedStores = new Dictionary < string , bool > ( ) ;
return ( await dbContext . Payouts
. Include ( data = > data . PullPaymentData )
. ThenInclude ( data = > data . StoreData )
. ThenInclude ( data = > data . UserStores )
2023-05-26 16:49:32 +02:00
. ThenInclude ( data = > data . StoreRole )
2021-10-18 05:37:59 +02:00
. Where ( data = >
payoutIds . Contains ( data . Id ) & &
data . State = = PayoutState . AwaitingPayment & &
data . PaymentMethodId = = pmiStr )
. ToListAsync ( ) )
. Where ( payout = >
{
2021-12-31 16:59:02 +09:00
if ( approvedStores . TryGetValue ( payout . PullPaymentData . StoreId , out var value ) )
return value ;
2021-10-18 05:37:59 +02:00
value = payout . PullPaymentData . StoreData . UserStores
2023-05-26 16:49:32 +02:00
. Any ( store = > store . ApplicationUserId = = userId & & store . StoreRole . Permissions . Contains ( Policies . CanModifyStoreSettings ) ) ;
2021-10-18 05:37:59 +02:00
approvedStores . Add ( payout . PullPaymentData . StoreId , value ) ;
return value ;
} ) . ToList ( ) ;
}
[HttpGet("pull-payments/payouts/lightning/{cryptoCode}")]
public async Task < IActionResult > ConfirmLightningPayout ( string cryptoCode , string [ ] payoutIds )
{
2022-02-24 13:57:02 +01:00
await SetStoreContext ( ) ;
2022-11-23 21:02:47 +09:00
2021-10-18 05:37:59 +02:00
var pmi = new PaymentMethodId ( cryptoCode , PaymentTypes . LightningLike ) ;
await using var ctx = _applicationDbContextFactory . CreateContext ( ) ;
var payouts = await GetPayouts ( ctx , pmi , payoutIds ) ;
var vm = payouts . Select ( payoutData = >
{
var blob = payoutData . GetBlob ( _btcPayNetworkJsonSerializerSettings ) ;
2022-06-02 11:03:06 +02:00
return new ConfirmVM
2021-10-18 05:37:59 +02:00
{
2021-12-31 16:59:02 +09:00
Amount = blob . CryptoAmount . Value ,
Destination = blob . Destination ,
PayoutId = payoutData . Id
2021-10-18 05:37:59 +02:00
} ;
} ) . ToList ( ) ;
return View ( vm ) ;
}
[HttpPost("pull-payments/payouts/lightning/{cryptoCode}")]
2022-11-22 20:17:29 +09:00
public async Task < IActionResult > ProcessLightningPayout ( string cryptoCode , string [ ] payoutIds , CancellationToken cancellationToken )
2021-10-18 05:37:59 +02:00
{
2022-02-24 13:57:02 +01:00
await SetStoreContext ( ) ;
2022-11-23 21:02:47 +09:00
2021-10-18 05:37:59 +02:00
var pmi = new PaymentMethodId ( cryptoCode , PaymentTypes . LightningLike ) ;
2022-11-23 21:02:47 +09:00
var payoutHandler = ( LightningLikePayoutHandler ) _payoutHandlers . FindPayoutHandler ( pmi ) ;
2021-10-18 05:37:59 +02:00
await using var ctx = _applicationDbContextFactory . CreateContext ( ) ;
var payouts = ( await GetPayouts ( ctx , pmi , payoutIds ) ) . GroupBy ( data = > data . PullPaymentData . StoreId ) ;
var results = new List < ResultVM > ( ) ;
var network = _btcPayNetworkProvider . GetNetwork < BTCPayNetwork > ( pmi . CryptoCode ) ;
//we group per store and init the transfers by each
2022-11-23 21:02:47 +09:00
2021-12-31 16:59:02 +09:00
var authorizedForInternalNode = ( await _authorizationService . AuthorizeAsync ( User , null , new PolicyRequirement ( Policies . CanModifyServerSettings ) ) ) . Succeeded ;
2021-10-18 05:37:59 +02:00
foreach ( var payoutDatas in payouts )
{
var store = payoutDatas . First ( ) . PullPaymentData . StoreData ;
2021-12-31 16:59:02 +09:00
2021-10-18 05:37:59 +02:00
var lightningSupportedPaymentMethod = store . GetSupportedPaymentMethods ( _btcPayNetworkProvider )
. OfType < LightningSupportedPaymentMethod > ( )
. FirstOrDefault ( method = > method . PaymentId = = pmi ) ;
2021-11-04 08:21:01 +01:00
if ( lightningSupportedPaymentMethod . IsInternalNode & & ! authorizedForInternalNode )
{
foreach ( PayoutData payoutData in payoutDatas )
{
2021-12-31 16:59:02 +09:00
2021-11-04 08:21:01 +01:00
var blob = payoutData . GetBlob ( _btcPayNetworkJsonSerializerSettings ) ;
2022-02-24 13:57:02 +01:00
results . Add ( new ResultVM
2021-11-04 08:21:01 +01:00
{
PayoutId = payoutData . Id ,
Result = PayResult . Error ,
Destination = blob . Destination ,
2022-06-02 11:03:06 +02:00
Message = "You are currently using the internal Lightning node for this payout's store but you are not a server admin."
2021-11-04 08:21:01 +01:00
} ) ;
}
continue ;
}
2021-12-31 16:59:02 +09:00
2021-10-18 05:37:59 +02:00
var client =
lightningSupportedPaymentMethod . CreateLightningClient ( network , _options . Value ,
_lightningClientFactoryService ) ;
foreach ( var payoutData in payoutDatas )
{
2022-07-15 05:37:47 +02:00
ResultVM result ;
2021-10-18 05:37:59 +02:00
var blob = payoutData . GetBlob ( _btcPayNetworkJsonSerializerSettings ) ;
2022-11-22 20:17:29 +09:00
var claim = await payoutHandler . ParseClaimDestination ( pmi , blob . Destination , cancellationToken ) ;
2021-10-18 05:37:59 +02:00
try
{
switch ( claim . destination )
{
case LNURLPayClaimDestinaton lnurlPayClaimDestinaton :
2022-07-15 05:37:47 +02:00
var lnurlResult = await GetInvoiceFromLNURL ( payoutData , payoutHandler , blob ,
2022-11-22 20:17:29 +09:00
lnurlPayClaimDestinaton , network . NBitcoinNetwork , cancellationToken ) ;
2022-07-15 05:37:47 +02:00
if ( lnurlResult . Item2 is not null )
2021-10-18 05:37:59 +02:00
{
2022-07-15 05:37:47 +02:00
result = lnurlResult . Item2 ;
2021-10-18 05:37:59 +02:00
}
else
{
2022-11-22 20:17:29 +09:00
result = await TrypayBolt ( client , blob , payoutData , lnurlResult . Item1 , pmi , cancellationToken ) ;
2021-10-18 05:37:59 +02:00
}
break ;
case BoltInvoiceClaimDestination item1 :
2022-11-23 21:02:47 +09:00
result = await TrypayBolt ( client , blob , payoutData , item1 . PaymentRequest , pmi , cancellationToken ) ;
2021-10-18 05:37:59 +02:00
break ;
default :
2022-11-23 21:02:47 +09:00
result = new ResultVM
2021-10-18 05:37:59 +02:00
{
PayoutId = payoutData . Id ,
Result = PayResult . Error ,
Destination = blob . Destination ,
Message = claim . error
2022-07-15 05:37:47 +02:00
} ;
2021-10-18 05:37:59 +02:00
break ;
}
}
2022-06-02 11:03:06 +02:00
catch ( Exception exception )
2021-10-18 05:37:59 +02:00
{
2022-07-15 05:37:47 +02:00
result = new ResultVM
2021-10-18 05:37:59 +02:00
{
2021-12-31 16:59:02 +09:00
PayoutId = payoutData . Id ,
Result = PayResult . Error ,
2022-06-02 11:03:06 +02:00
Destination = blob . Destination ,
Message = exception . Message
2022-07-15 05:37:47 +02:00
} ;
2021-10-18 05:37:59 +02:00
}
2022-07-15 05:37:47 +02:00
results . Add ( result ) ;
2021-10-18 05:37:59 +02:00
}
}
await ctx . SaveChangesAsync ( ) ;
return View ( "LightningPayoutResult" , results ) ;
}
2022-07-15 05:37:47 +02:00
public static async Task < ( BOLT11PaymentRequest , ResultVM ) > GetInvoiceFromLNURL ( PayoutData payoutData ,
2022-11-23 21:02:47 +09:00
LightningLikePayoutHandler handler , PayoutBlob blob , LNURLPayClaimDestinaton lnurlPayClaimDestinaton , Network network , CancellationToken cancellationToken )
2022-07-15 05:37:47 +02: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" ,
2022-11-22 20:17:29 +09:00
httpClient , cancellationToken ) ;
2022-07-15 05:37:47 +02:00
var lm = new LightMoney ( blob . CryptoAmount . Value , LightMoneyUnit . BTC ) ;
if ( lm > lnurlInfo . MaxSendable | | lm < lnurlInfo . MinSendable )
{
return ( null , new ResultVM
{
PayoutId = payoutData . Id ,
Result = PayResult . Error ,
Destination = blob . Destination ,
Message =
$"The LNURL provided would not generate an invoice of {lm.MilliSatoshi}msats"
} ) ;
}
2022-11-23 21:02:47 +09:00
2022-07-15 05:37:47 +02:00
try
{
var lnurlPayRequestCallbackResponse =
2022-11-22 20:17:29 +09:00
await lnurlInfo . SendRequest ( lm , network , httpClient , cancellationToken : cancellationToken ) ;
2022-07-15 05:37:47 +02:00
return ( lnurlPayRequestCallbackResponse . GetPaymentRequest ( network ) , null ) ;
}
catch ( LNUrlException e )
{
return ( null ,
new ResultVM
{
PayoutId = payoutData . Id ,
Result = PayResult . Error ,
Destination = blob . Destination ,
Message = e . Message
} ) ;
}
}
2023-01-06 14:18:07 +01:00
2022-08-17 09:45:51 +02:00
public static async Task < ResultVM > TrypayBolt (
2022-11-23 21:02:47 +09:00
ILightningClient lightningClient , PayoutBlob payoutBlob , PayoutData payoutData , BOLT11PaymentRequest bolt11PaymentRequest ,
2022-11-22 20:17:29 +09:00
PaymentMethodId pmi , CancellationToken cancellationToken )
2022-07-15 05:37:47 +02:00
{
var boltAmount = bolt11PaymentRequest . MinimumAmount . ToDecimal ( LightMoneyUnit . BTC ) ;
2023-07-24 13:40:26 +02:00
if ( boltAmount > payoutBlob . CryptoAmount )
2022-07-15 05:37:47 +02:00
{
2022-11-23 21:02:47 +09:00
2022-08-17 09:45:51 +02:00
payoutData . State = PayoutState . Cancelled ;
2022-07-15 05:37:47 +02:00
return new ResultVM
{
PayoutId = payoutData . Id ,
Result = PayResult . Error ,
Message = $"The BOLT11 invoice amount ({boltAmount} {pmi.CryptoCode}) did not match the payout's amount ({payoutBlob.CryptoAmount.GetValueOrDefault()} {pmi.CryptoCode})" ,
Destination = payoutBlob . Destination
} ;
}
2022-08-17 09:45:51 +02:00
2023-07-20 15:05:14 +02:00
if ( bolt11PaymentRequest . ExpiryDate < DateTimeOffset . Now )
{
payoutData . State = PayoutState . Cancelled ;
return new ResultVM
{
PayoutId = payoutData . Id ,
Result = PayResult . Error ,
Message = $"The BOLT11 invoice expiry date ({bolt11PaymentRequest.ExpiryDate}) has expired" ,
Destination = payoutBlob . Destination
} ;
}
2022-11-23 21:02:47 +09:00
var proofBlob = new PayoutLightningBlob ( ) { PaymentHash = bolt11PaymentRequest . PaymentHash . ToString ( ) } ;
2022-08-17 09:45:51 +02:00
try
2022-07-15 05:37:47 +02:00
{
2022-08-17 09:45:51 +02:00
var result = await lightningClient . Pay ( bolt11PaymentRequest . ToString ( ) ,
new PayInvoiceParams ( )
{
2023-07-24 13:40:26 +02:00
// CLN does not support explicit amount param if it is the same as the invoice amount
Amount = payoutBlob . CryptoAmount = = bolt11PaymentRequest . MinimumAmount . ToDecimal ( LightMoneyUnit . BTC ) ? null : new LightMoney ( ( decimal ) payoutBlob . CryptoAmount , LightMoneyUnit . BTC )
2022-12-04 13:23:59 +01:00
} , cancellationToken ) ;
2022-08-17 09:45:51 +02:00
string message = null ;
if ( result . Result = = PayResult . Ok )
{
message = result . Details ? . TotalAmount ! = null
? $"Paid out {result.Details.TotalAmount.ToDecimal(LightMoneyUnit.BTC)}"
: null ;
payoutData . State = PayoutState . Completed ;
try
{
2022-11-23 21:02:47 +09:00
var payment = await lightningClient . GetPayment ( bolt11PaymentRequest . PaymentHash . ToString ( ) , cancellationToken ) ;
proofBlob . Preimage = payment . Preimage ;
2022-08-17 09:45:51 +02:00
}
2022-09-23 16:23:14 +02:00
catch ( Exception )
2022-08-17 09:45:51 +02:00
{
2022-09-23 16:23:14 +02:00
// ignored
2022-08-17 09:45:51 +02:00
}
}
2023-01-06 14:18:07 +01:00
else if ( result . Result = = PayResult . Unknown )
2022-12-04 13:23:59 +01:00
{
payoutData . State = PayoutState . InProgress ;
message = "The payment has been initiated but is still in-flight." ;
}
2022-11-23 21:02:47 +09:00
2022-08-17 09:45:51 +02:00
payoutData . SetProofBlob ( proofBlob , null ) ;
2022-07-15 05:37:47 +02:00
return new ResultVM
{
PayoutId = payoutData . Id ,
Result = result . Result ,
Destination = payoutBlob . Destination ,
Message = message
} ;
}
2022-08-17 09:45:51 +02:00
catch ( Exception ex ) when ( ex is TaskCanceledException or OperationCanceledException )
2022-07-15 05:37:47 +02:00
{
2022-08-17 09:45:51 +02:00
// Timeout, potentially caused by hold invoices
// Payment will be saved as pending, the LightningPendingPayoutListener will handle settling/cancelling
payoutData . State = PayoutState . InProgress ;
2022-11-23 21:02:47 +09:00
2022-08-17 09:45:51 +02:00
payoutData . SetProofBlob ( proofBlob , null ) ;
return new ResultVM
{
PayoutId = payoutData . Id ,
Result = PayResult . Ok ,
Destination = payoutBlob . Destination ,
Message = "The payment timed out. We will verify if it completed later."
} ;
}
2022-07-15 05:37:47 +02:00
}
2021-10-18 05:37:59 +02:00
2022-02-24 13:57:02 +01:00
private async Task SetStoreContext ( )
{
var storeId = HttpContext . GetUserPrefsCookie ( ) ? . CurrentStoreId ;
2022-11-23 21:02:47 +09:00
if ( string . IsNullOrEmpty ( storeId ) )
return ;
2022-02-24 13:57:02 +01:00
var userId = _userManager . GetUserId ( User ) ;
var store = await _storeRepository . FindStore ( storeId , userId ) ;
if ( store ! = null )
{
HttpContext . SetStoreData ( store ) ;
}
}
2021-10-18 05:37:59 +02:00
public class ResultVM
{
public string PayoutId { get ; set ; }
public string Destination { get ; set ; }
public PayResult Result { get ; set ; }
public string Message { get ; set ; }
}
public class ConfirmVM
{
public string PayoutId { get ; set ; }
public string Destination { get ; set ; }
public decimal Amount { get ; set ; }
}
}
}