Fix LN Address payouts (#3960)

* Fix LN Address payouts

LN Address was validated when creating the claim but the paying sdection did not support it.

* reuse code

* reuse code

* do not use mail directly

* fix email validator
This commit is contained in:
Andrew Camilleri 2022-07-15 05:37:47 +02:00 committed by GitHub
parent 83c35328ed
commit f8619e382b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 118 additions and 125 deletions

View file

@ -9,7 +9,6 @@ using BTCPayServer.Lightning;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Validation;
using LNURL; using LNURL;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -64,7 +63,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
try try
{ {
string lnurlTag = null; string lnurlTag = null;
var lnurl = MailboxAddressValidator.IsMailboxAddress(destination) var lnurl = destination.IsValidEmail()
? LNURL.LNURL.ExtractUriFromInternetIdentifier(destination) ? LNURL.LNURL.ExtractUriFromInternetIdentifier(destination)
: LNURL.LNURL.Parse(destination, out lnurlTag); : LNURL.LNURL.Parse(destination, out lnurlTag);

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client; using BTCPayServer.Client;
@ -12,7 +11,6 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security; using BTCPayServer.Security;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using LNURL; using LNURL;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -20,6 +18,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NBitcoin;
namespace BTCPayServer.Data.Payouts.LightningLike namespace BTCPayServer.Data.Payouts.LightningLike
{ {
@ -36,7 +35,6 @@ namespace BTCPayServer.Data.Payouts.LightningLike
private readonly IOptions<LightningNetworkOptions> _options; private readonly IOptions<LightningNetworkOptions> _options;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly CurrencyNameTable _currencyNameTable;
public UILightningLikePayoutController(ApplicationDbContextFactory applicationDbContextFactory, public UILightningLikePayoutController(ApplicationDbContextFactory applicationDbContextFactory,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
@ -44,7 +42,6 @@ namespace BTCPayServer.Data.Payouts.LightningLike
IEnumerable<IPayoutHandler> payoutHandlers, IEnumerable<IPayoutHandler> payoutHandlers,
BTCPayNetworkProvider btcPayNetworkProvider, BTCPayNetworkProvider btcPayNetworkProvider,
StoreRepository storeRepository, StoreRepository storeRepository,
CurrencyNameTable currencyNameTable,
LightningClientFactoryService lightningClientFactoryService, LightningClientFactoryService lightningClientFactoryService,
IOptions<LightningNetworkOptions> options, IAuthorizationService authorizationService) IOptions<LightningNetworkOptions> options, IAuthorizationService authorizationService)
{ {
@ -56,7 +53,6 @@ namespace BTCPayServer.Data.Payouts.LightningLike
_lightningClientFactoryService = lightningClientFactoryService; _lightningClientFactoryService = lightningClientFactoryService;
_options = options; _options = options;
_storeRepository = storeRepository; _storeRepository = storeRepository;
_currencyNameTable = currencyNameTable;
_authorizationService = authorizationService; _authorizationService = authorizationService;
} }
@ -123,7 +119,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
await SetStoreContext(); await SetStoreContext();
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var payoutHandler = _payoutHandlers.FindPayoutHandler(pmi); var payoutHandler = (LightningLikePayoutHandler) _payoutHandlers.FindPayoutHandler(pmi);
await using var ctx = _applicationDbContextFactory.CreateContext(); await using var ctx = _applicationDbContextFactory.CreateContext();
@ -132,46 +128,6 @@ namespace BTCPayServer.Data.Payouts.LightningLike
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(pmi.CryptoCode); var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(pmi.CryptoCode);
//we group per store and init the transfers by each //we group per store and init the transfers by each
async Task TrypayBolt(ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest)
{
var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
if (boltAmount != payoutBlob.CryptoAmount)
{
results.Add(new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Message = $"The BOLT11 invoice amount ({_currencyNameTable.DisplayFormatCurrency(boltAmount, pmi.CryptoCode)}) did not match the payout's amount ({_currencyNameTable.DisplayFormatCurrency(payoutBlob.CryptoAmount.GetValueOrDefault(), pmi.CryptoCode)})",
Destination = payoutBlob.Destination
});
return;
}
var result = await lightningClient.Pay(bolt11PaymentRequest.ToString());
if (result.Result == PayResult.Ok)
{
var message = result.Details?.TotalAmount != null
? $"Paid out {result.Details.TotalAmount.ToDecimal(LightMoneyUnit.BTC)}"
: null;
results.Add(new ResultVM
{
PayoutId = payoutData.Id,
Result = result.Result,
Destination = payoutBlob.Destination,
Message = message
});
payoutData.State = PayoutState.Completed;
}
else
{
results.Add(new ResultVM
{
PayoutId = payoutData.Id,
Result = result.Result,
Destination = payoutBlob.Destination,
Message = result.ErrorDetail
});
}
}
var authorizedForInternalNode = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded; var authorizedForInternalNode = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded;
foreach (var payoutDatas in payouts) foreach (var payoutDatas in payouts)
@ -205,6 +161,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
_lightningClientFactoryService); _lightningClientFactoryService);
foreach (var payoutData in payoutDatas) foreach (var payoutData in payoutDatas)
{ {
ResultVM result;
var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings); var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings);
var claim = await payoutHandler.ParseClaimDestination(pmi, blob.Destination); var claim = await payoutHandler.ParseClaimDestination(pmi, blob.Destination);
try try
@ -212,16 +169,65 @@ namespace BTCPayServer.Data.Payouts.LightningLike
switch (claim.destination) switch (claim.destination)
{ {
case LNURLPayClaimDestinaton lnurlPayClaimDestinaton: case LNURLPayClaimDestinaton lnurlPayClaimDestinaton:
var endpoint = LNURL.LNURL.Parse(lnurlPayClaimDestinaton.LNURL, out _); var lnurlResult = await GetInvoiceFromLNURL(payoutData, payoutHandler, blob,
var lightningPayoutHandler = (LightningLikePayoutHandler)payoutHandler; lnurlPayClaimDestinaton, network.NBitcoinNetwork);
var httpClient = lightningPayoutHandler.CreateClient(endpoint); if (lnurlResult.Item2 is not null)
{
result = lnurlResult.Item2;
}
else
{
result = await TrypayBolt(client, blob, payoutData, lnurlResult.Item1, pmi);
}
break;
case BoltInvoiceClaimDestination item1:
result = await TrypayBolt(client, blob, payoutData, item1.PaymentRequest, pmi);
break;
default:
result= new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Destination = blob.Destination,
Message = claim.error
};
break;
}
}
catch (Exception exception)
{
result = new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Destination = blob.Destination,
Message = exception.Message
};
}
results.Add(result);
}
}
await ctx.SaveChangesAsync();
return View("LightningPayoutResult", results);
}
public static async Task<(BOLT11PaymentRequest, ResultVM)> GetInvoiceFromLNURL(PayoutData payoutData,
LightningLikePayoutHandler handler,PayoutBlob blob, LNURLPayClaimDestinaton lnurlPayClaimDestinaton, Network network)
{
var endpoint = lnurlPayClaimDestinaton.LNURL.IsValidEmail()
? LNURL.LNURL.ExtractUriFromInternetIdentifier(lnurlPayClaimDestinaton.LNURL)
: LNURL.LNURL.Parse(lnurlPayClaimDestinaton.LNURL, out _);
var httpClient = handler.CreateClient(endpoint);
var lnurlInfo = var lnurlInfo =
(LNURLPayRequest)await LNURL.LNURL.FetchInformation(endpoint, "payRequest", (LNURLPayRequest)await LNURL.LNURL.FetchInformation(endpoint, "payRequest",
httpClient); httpClient);
var lm = new LightMoney(blob.CryptoAmount.Value, LightMoneyUnit.BTC); var lm = new LightMoney(blob.CryptoAmount.Value, LightMoneyUnit.BTC);
if (lm > lnurlInfo.MaxSendable || lm < lnurlInfo.MinSendable) if (lm > lnurlInfo.MaxSendable || lm < lnurlInfo.MinSendable)
{ {
results.Add(new ResultVM return (null, new ResultVM
{ {
PayoutId = payoutData.Id, PayoutId = payoutData.Id,
Result = PayResult.Error, Result = PayResult.Error,
@ -230,18 +236,18 @@ namespace BTCPayServer.Data.Payouts.LightningLike
$"The LNURL provided would not generate an invoice of {lm.MilliSatoshi}msats" $"The LNURL provided would not generate an invoice of {lm.MilliSatoshi}msats"
}); });
} }
else
{
try try
{ {
var lnurlPayRequestCallbackResponse = var lnurlPayRequestCallbackResponse =
await lnurlInfo.SendRequest(lm, network.NBitcoinNetwork, httpClient); await lnurlInfo.SendRequest(lm, network, httpClient);
await TrypayBolt(client, blob, payoutData, lnurlPayRequestCallbackResponse.GetPaymentRequest(network.NBitcoinNetwork)); return (lnurlPayRequestCallbackResponse.GetPaymentRequest(network), null);
} }
catch (LNUrlException e) catch (LNUrlException e)
{ {
results.Add(new ResultVM return (null,
new ResultVM
{ {
PayoutId = payoutData.Id, PayoutId = payoutData.Id,
Result = PayResult.Error, Result = PayResult.Error,
@ -251,40 +257,46 @@ namespace BTCPayServer.Data.Payouts.LightningLike
} }
} }
break;
case BoltInvoiceClaimDestination item1: public static async Task<ResultVM> TrypayBolt(ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest, PaymentMethodId pmi)
await TrypayBolt(client, blob, payoutData, item1.PaymentRequest); {
var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
break; if (boltAmount != payoutBlob.CryptoAmount)
default: {
results.Add(new ResultVM return new ResultVM
{ {
PayoutId = payoutData.Id, PayoutId = payoutData.Id,
Result = PayResult.Error, Result = PayResult.Error,
Destination = blob.Destination, Message = $"The BOLT11 invoice amount ({boltAmount} {pmi.CryptoCode}) did not match the payout's amount ({payoutBlob.CryptoAmount.GetValueOrDefault()} {pmi.CryptoCode})",
Message = claim.error Destination = payoutBlob.Destination
}); };
break;
} }
} var result = await lightningClient.Pay(bolt11PaymentRequest.ToString(), new PayInvoiceParams());
catch (Exception exception) if (result.Result == PayResult.Ok)
{ {
results.Add(new ResultVM var message = result.Details?.TotalAmount != null
? $"Paid out {result.Details.TotalAmount.ToDecimal(LightMoneyUnit.BTC)}"
: null;
payoutData.State = PayoutState.Completed;
return new ResultVM
{ {
PayoutId = payoutData.Id, PayoutId = payoutData.Id,
Result = PayResult.Error, Result = result.Result,
Destination = blob.Destination, Destination = payoutBlob.Destination,
Message = exception.Message Message = message
}); };
}
}
} }
await ctx.SaveChangesAsync(); return new ResultVM
return View("LightningPayoutResult", results); {
PayoutId = payoutData.Id,
Result = result.Result,
Destination = payoutBlob.Destination,
Message = result.ErrorDetail
};
} }
private async Task SetStoreContext() private async Task SetStoreContext()
{ {
var storeId = HttpContext.GetUserPrefsCookie()?.CurrentStoreId; var storeId = HttpContext.GetUserPrefsCookie()?.CurrentStoreId;

View file

@ -10,9 +10,6 @@ using System.Security.Claims;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.BIP78.Sender; using BTCPayServer.BIP78.Sender;
using BTCPayServer.Configuration; using BTCPayServer.Configuration;
using BTCPayServer.Data; using BTCPayServer.Data;
@ -27,20 +24,28 @@ using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
using NBitcoin.Payment; using NBitcoin.Payment;
using NBXplorer.Models; using NBXplorer.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo; using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo;
namespace BTCPayServer namespace BTCPayServer
{ {
public static class Extensions public static class Extensions
{ {
public static bool IsValidEmail(this string email)
{
if (string.IsNullOrEmpty(email))
{
return false;
}
return MailboxAddressValidator.TryParse(email, out var ma) && ma.ToString() == ma.Address;
}
public static bool TryGetPayjoinEndpoint(this BitcoinUrlBuilder bip21, out Uri endpoint) public static bool TryGetPayjoinEndpoint(this BitcoinUrlBuilder bip21, out Uri endpoint)
{ {
endpoint = bip21.UnknownParameters.TryGetValue($"{PayjoinClient.BIP21EndpointKey}", out var uri) ? new Uri(uri, UriKind.Absolute) : null; endpoint = bip21.UnknownParameters.TryGetValue($"{PayjoinClient.BIP21EndpointKey}", out var uri) ? new Uri(uri, UriKind.Absolute) : null;

View file

@ -80,36 +80,19 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
switch (claim.destination) switch (claim.destination)
{ {
case LNURLPayClaimDestinaton lnurlPayClaimDestinaton: case LNURLPayClaimDestinaton lnurlPayClaimDestinaton:
var endpoint = LNURL.LNURL.Parse(lnurlPayClaimDestinaton.LNURL, out var tag); var lnurlResult = await UILightningLikePayoutController.GetInvoiceFromLNURL(payoutData, _payoutHandler, blob,
var httpClient = _payoutHandler.CreateClient(endpoint); lnurlPayClaimDestinaton, _network.NBitcoinNetwork);
var lnurlInfo = if (lnurlResult.Item2 is not null)
(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; continue;
} }
else
{
try
{
var lnurlPayRequestCallbackResponse =
await lnurlInfo.SendRequest(lm, _network.NBitcoinNetwork, httpClient);
if (await TrypayBolt(client, blob, payoutData, if (await TrypayBolt(client, blob, payoutData,
lnurlPayRequestCallbackResponse lnurlResult.Item1))
.GetPaymentRequest(_network.NBitcoinNetwork)))
{ {
ctx.Attach(payoutData); ctx.Attach(payoutData);
payoutData.State = PayoutState.Completed; payoutData.State = PayoutState.Completed;
} }
}
catch (LNUrlException)
{
continue;
}
}
break; break;
@ -137,13 +120,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
async Task<bool> TrypayBolt(ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, async Task<bool> TrypayBolt(ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData,
BOLT11PaymentRequest bolt11PaymentRequest) BOLT11PaymentRequest bolt11PaymentRequest)
{ {
var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC); return (await UILightningLikePayoutController.TrypayBolt(lightningClient, payoutBlob, payoutData, bolt11PaymentRequest,
if (boltAmount != payoutBlob.CryptoAmount) payoutData.GetPaymentMethodId())).Result == PayResult.Ok;
{
return false;
}
var result = await lightningClient.Pay(bolt11PaymentRequest.ToString());
return result.Result == PayResult.Ok;
} }
} }