btcpayserver/BTCPayServer/PayoutProcessors/Lightning/LightningAutomatedPayoutProcessor.cs
Andrew Camilleri 51690b47a3
Transfer Processors (#3476)
* Automated Transfer processors

This PR introduces a few things:
* Payouts can now be directly nested under a store instead of through a pull payment.
* The Wallet Send screen now has an option to "schedule" instead of simply creating a transaction. When you click on schedule, all transaction destinations are converted into approved payouts. Any options relating to fees or coin selection are discarded.
* There is a new concept introduced, called "Transfer Processors".  Transfer Processors are services for stores that process payouts that are awaiting payment. Each processor specifies which payment methods it can handle.  BTCPay Server will have some forms of transfer processors baked in but it has been designed to allow the Plugin System to provide additional processors.
* The initial transfer processors provided are "automated processors", for on chain and lightning payment methods. They can be configured to process payouts every X amount of minutes. For  on-chain, this means payments are batched into one transaction, resulting in more efficient and cheaper fees for processing.
*

* fix build

* extract

* remove magic string stuff

* fix error message when scheduling

* Paginate migration

* add payout count to payment method tab

* remove unused var

* add protip

* optimzie payout migration dramatically

* Remove useless double condition

* Fix bunch of warnings

* Remove warning

* Remove warnigns

* Rename to Payout processors

* fix typo

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2022-04-24 12:19:34 +09:00

149 lines
6.2 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Data.Data;
using BTCPayServer.Data.Payouts.LightningLike;
using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.PayoutProcessors.Settings;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using LNURL;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PayoutData = BTCPayServer.Data.PayoutData;
using PayoutProcessorData = BTCPayServer.Data.Data.PayoutProcessorData;
namespace BTCPayServer.PayoutProcessors.Lightning;
public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<AutomatedPayoutBlob>
{
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly LightningClientFactoryService _lightningClientFactoryService;
private readonly UserService _userService;
private readonly IOptions<LightningNetworkOptions> _options;
private readonly LightningLikePayoutHandler _payoutHandler;
private readonly BTCPayNetwork _network;
public LightningAutomatedPayoutProcessor(
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
LightningClientFactoryService lightningClientFactoryService,
IEnumerable<IPayoutHandler> payoutHandlers,
UserService userService,
ILoggerFactory logger, IOptions<LightningNetworkOptions> options,
StoreRepository storeRepository, PayoutProcessorData payoutProcesserSettings,
ApplicationDbContextFactory applicationDbContextFactory, BTCPayNetworkProvider btcPayNetworkProvider) :
base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory,
btcPayNetworkProvider)
{
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
_lightningClientFactoryService = lightningClientFactoryService;
_userService = userService;
_options = options;
_payoutHandler = (LightningLikePayoutHandler)payoutHandlers.FindPayoutHandler(PaymentMethodId);
_network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(_PayoutProcesserSettings.GetPaymentMethodId().CryptoCode);
}
protected override async Task Process(ISupportedPaymentMethod paymentMethod, PayoutData[] payouts)
{
await using var ctx = _applicationDbContextFactory.CreateContext();
var lightningSupportedPaymentMethod = (LightningSupportedPaymentMethod)paymentMethod;
if (lightningSupportedPaymentMethod.IsInternalNode &&
!(await Task.WhenAll((await _storeRepository.GetStoreUsers(_PayoutProcesserSettings.StoreId))
.Where(user => user.Role == StoreRoles.Owner).Select(user => user.Id)
.Select(s => _userService.IsAdminUser(s)))).Any(b => b))
{
return;
}
var client =
lightningSupportedPaymentMethod.CreateLightningClient(_network, _options.Value,
_lightningClientFactoryService);
foreach (var payoutData in payouts)
{
var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings);
var claim = await _payoutHandler.ParseClaimDestination(PaymentMethodId, blob.Destination);
try
{
switch (claim.destination)
{
case LNURLPayClaimDestinaton lnurlPayClaimDestinaton:
var endpoint = LNURL.LNURL.Parse(lnurlPayClaimDestinaton.LNURL, out var tag);
var httpClient = _payoutHandler.CreateClient(endpoint);
var lnurlInfo =
(LNURLPayRequest)await LNURL.LNURL.FetchInformation(endpoint, "payRequest",
httpClient);
var lm = new LightMoney(blob.CryptoAmount.Value, LightMoneyUnit.BTC);
if (lm > lnurlInfo.MaxSendable || lm < lnurlInfo.MinSendable)
{
continue;
}
else
{
try
{
var lnurlPayRequestCallbackResponse =
await lnurlInfo.SendRequest(lm, _network.NBitcoinNetwork, httpClient);
if (await TrypayBolt(client, blob, payoutData,
lnurlPayRequestCallbackResponse
.GetPaymentRequest(_network.NBitcoinNetwork)))
{
ctx.Attach(payoutData);
payoutData.State = PayoutState.Completed;
}
}
catch (LNUrlException)
{
continue;
}
}
break;
case BoltInvoiceClaimDestination item1:
if (await TrypayBolt(client, blob, payoutData, item1.PaymentRequest))
{
ctx.Attach(payoutData);
payoutData.State = PayoutState.Completed;
}
break;
}
}
catch (Exception e)
{
Logs.PayServer.LogError(e, $"Could not process payout {payoutData.Id}");
}
}
await ctx.SaveChangesAsync();
}
//we group per store and init the transfers by each
async Task<bool> TrypayBolt(ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData,
BOLT11PaymentRequest bolt11PaymentRequest)
{
var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
if (boltAmount != payoutBlob.CryptoAmount)
{
return false;
}
var result = await lightningClient.Pay(bolt11PaymentRequest.ToString());
return result.Result == PayResult.Ok;
}
}