2020-06-28 21:44:35 -05:00
|
|
|
using System;
|
2018-02-26 00:48:12 +09:00
|
|
|
using System.Collections.Generic;
|
2020-06-28 17:55:27 +09:00
|
|
|
using System.Globalization;
|
2018-02-26 00:48:12 +09:00
|
|
|
using System.Linq;
|
|
|
|
using System.Threading;
|
|
|
|
using System.Threading.Tasks;
|
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;
|
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;
|
|
|
|
using BTCPayServer.Rating;
|
2019-03-18 00:03:02 +09:00
|
|
|
using BTCPayServer.Services;
|
2020-06-28 17:55:27 +09:00
|
|
|
using BTCPayServer.Services.Invoices;
|
2019-05-29 14:33:31 +00:00
|
|
|
using BTCPayServer.Services.Rates;
|
2019-03-31 13:16:05 +09:00
|
|
|
using NBitcoin;
|
2018-02-26 00:48:12 +09:00
|
|
|
|
|
|
|
namespace BTCPayServer.Payments.Lightning
|
|
|
|
{
|
2019-05-29 14:33:31 +00:00
|
|
|
public class LightningLikePaymentHandler : PaymentMethodHandlerBase<LightningSupportedPaymentMethod, BTCPayNetwork>
|
2018-02-26 00:48:12 +09:00
|
|
|
{
|
2018-05-31 16:31:39 -05:00
|
|
|
public static int LIGHTNING_TIMEOUT = 5000;
|
2020-06-28 22:07:48 -05:00
|
|
|
readonly NBXplorerDashboard _Dashboard;
|
2019-04-11 01:10:29 +09:00
|
|
|
private readonly LightningClientFactoryService _lightningClientFactory;
|
2019-05-29 14:33:31 +00:00
|
|
|
private readonly BTCPayNetworkProvider _networkProvider;
|
2019-03-18 00:03:02 +09:00
|
|
|
private readonly SocketFactory _socketFactory;
|
|
|
|
|
2018-03-20 12:10:35 +09:00
|
|
|
public LightningLikePaymentHandler(
|
2019-03-18 00:03:02 +09:00
|
|
|
NBXplorerDashboard dashboard,
|
2019-04-11 01:10:29 +09:00
|
|
|
LightningClientFactoryService lightningClientFactory,
|
2019-05-29 14:33:31 +00:00
|
|
|
BTCPayNetworkProvider networkProvider,
|
2019-03-18 00:03:02 +09:00
|
|
|
SocketFactory socketFactory)
|
2018-02-26 00:48:12 +09:00
|
|
|
{
|
2018-03-02 14:03:18 -05:00
|
|
|
_Dashboard = dashboard;
|
2019-04-11 01:10:29 +09:00
|
|
|
_lightningClientFactory = lightningClientFactory;
|
2019-05-29 14:33:31 +00:00
|
|
|
_networkProvider = networkProvider;
|
2019-03-18 00:03:02 +09:00
|
|
|
_socketFactory = socketFactory;
|
2018-02-26 00:48:12 +09:00
|
|
|
}
|
2019-05-24 06:38:47 +00:00
|
|
|
|
2019-06-04 08:59:01 +09:00
|
|
|
public override PaymentType PaymentType => PaymentTypes.LightningLike;
|
2019-05-29 09:43:50 +00:00
|
|
|
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(
|
2020-03-30 00:28:22 +09:00
|
|
|
InvoiceLogs logs,
|
2019-05-29 09:43:50 +00:00
|
|
|
LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store,
|
2019-05-29 14:33:31 +00:00
|
|
|
BTCPayNetwork network, object preparePaymentObject)
|
2018-02-26 00:48:12 +09:00
|
|
|
{
|
2019-05-29 09:43:50 +00:00
|
|
|
//direct casting to (BTCPayNetwork) is fixed in other pull requests with better generic interfacing for handlers
|
2018-04-07 16:27:46 +09:00
|
|
|
var storeBlob = store.GetStoreBlob();
|
2020-06-28 22:07:48 -05:00
|
|
|
var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, network);
|
2020-09-15 13:46:45 +02:00
|
|
|
|
2018-02-26 00:48:12 +09:00
|
|
|
var invoice = paymentMethod.ParentEntity;
|
2020-09-15 13:46:45 +02:00
|
|
|
decimal due = Extensions.RoundUp(invoice.Price / paymentMethod.Rate, network.Divisibility);
|
|
|
|
try
|
|
|
|
{
|
|
|
|
due = paymentMethod.Calculate().Due.ToDecimal(MoneyUnit.BTC);
|
|
|
|
}
|
|
|
|
catch (Exception)
|
|
|
|
{
|
|
|
|
// ignored
|
|
|
|
}
|
2020-06-28 22:07:48 -05:00
|
|
|
var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network);
|
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
|
|
|
|
2018-03-28 22:37:01 +09:00
|
|
|
LightningInvoice lightningInvoice = null;
|
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);
|
2018-05-31 16:31:39 -05:00
|
|
|
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
|
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)
|
|
|
|
{
|
2019-09-21 16:39:44 +02: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
|
|
|
}
|
2018-03-28 22:37:01 +09:00
|
|
|
var nodeInfo = await test;
|
|
|
|
return new LightningLikePaymentMethodDetails()
|
|
|
|
{
|
|
|
|
BOLT11 = lightningInvoice.BOLT11,
|
|
|
|
InvoiceId = lightningInvoice.Id,
|
|
|
|
NodeInfo = nodeInfo.ToString()
|
|
|
|
};
|
2018-02-26 00:48:12 +09:00
|
|
|
}
|
|
|
|
|
2019-03-17 21:28:47 +09:00
|
|
|
public async Task<NodeInfo> GetNodeInfo(bool preferOnion, LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
2018-02-26 00:48:12 +09:00
|
|
|
{
|
2018-03-02 14:03:18 -05:00
|
|
|
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
2018-03-28 22:37:01 +09:00
|
|
|
throw new PaymentMethodUnavailableException($"Full node not available");
|
|
|
|
|
2018-05-31 16:31:39 -05:00
|
|
|
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
|
2018-02-26 00:48:12 +09:00
|
|
|
{
|
2019-04-11 01:10:29 +09:00
|
|
|
var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network);
|
2018-05-12 00:14:39 +09:00
|
|
|
LightningNodeInformation info = null;
|
|
|
|
try
|
|
|
|
{
|
|
|
|
info = await client.GetInfo(cts.Token);
|
|
|
|
}
|
|
|
|
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
|
|
|
{
|
2019-01-15 12:21:31 +00: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($"Error while connecting to the API ({ex.Message})");
|
|
|
|
}
|
2019-03-17 21:28:47 +09:00
|
|
|
var nodeInfo = info.NodeInfoList.FirstOrDefault(i => i.IsTor == preferOnion) ?? info.NodeInfoList.FirstOrDefault();
|
|
|
|
if (nodeInfo == null)
|
2018-05-12 00:14:39 +09:00
|
|
|
{
|
|
|
|
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
|
|
|
|
}
|
2018-03-21 00:31:19 +09:00
|
|
|
|
2018-10-28 22:46:03 +09:00
|
|
|
var blocksGap = summary.Status.ChainHeight - info.BlockHeight;
|
2018-05-12 00:14:39 +09:00
|
|
|
if (blocksGap > 10)
|
|
|
|
{
|
2018-10-28 22:46:03 +09:00
|
|
|
throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)");
|
2018-05-12 00:14:39 +09:00
|
|
|
}
|
2018-02-26 00:48:12 +09:00
|
|
|
|
2019-03-17 21:28:47 +09:00
|
|
|
return nodeInfo;
|
2018-05-12 00:14:39 +09:00
|
|
|
}
|
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
|
|
|
|
2019-03-31 13:16:05 +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
|
|
|
}
|
|
|
|
}
|
2019-05-29 14:33:31 +00:00
|
|
|
|
|
|
|
public override IEnumerable<PaymentMethodId> GetSupportedPaymentMethods()
|
|
|
|
{
|
2019-09-21 16:39:44 +02:00
|
|
|
return _networkProvider
|
|
|
|
.GetAll()
|
|
|
|
.OfType<BTCPayNetwork>()
|
2020-04-27 11:15:38 +02:00
|
|
|
.Where(network => network.NBitcoinNetwork.Consensus.SupportSegwit && network.SupportLightning)
|
2019-05-29 14:33:31 +00:00
|
|
|
.Select(network => new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike));
|
|
|
|
}
|
2020-06-28 17:55:27 +09:00
|
|
|
|
2019-09-11 07:49:06 +02:00
|
|
|
public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse,
|
|
|
|
StoreBlob storeBlob)
|
2019-05-29 14:33:31 +00:00
|
|
|
{
|
|
|
|
var paymentMethodId = new PaymentMethodId(model.CryptoCode, PaymentTypes.LightningLike);
|
2020-06-28 17:55:27 +09:00
|
|
|
|
2019-05-29 14:33:31 +00:00
|
|
|
var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
|
|
|
var network = _networkProvider.GetNetwork<BTCPayNetwork>(model.CryptoCode);
|
|
|
|
model.IsLightning = true;
|
|
|
|
model.PaymentMethodName = GetPaymentMethodName(network);
|
|
|
|
model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BOLT11;
|
2019-10-17 00:52:19 +09:00
|
|
|
model.InvoiceBitcoinUrlQR = $"lightning:{cryptoInfo.PaymentUrls.BOLT11.ToUpperInvariant().Substring("LIGHTNING:".Length)}";
|
2019-09-11 07:49:06 +02:00
|
|
|
model.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
|
2020-06-28 17:55:27 +09:00
|
|
|
if (storeBlob.LightningAmountInSatoshi && model.CryptoCode == "BTC")
|
2019-09-11 07:49:06 +02:00
|
|
|
{
|
2020-01-27 04:57:46 -06:00
|
|
|
var satoshiCulture = new CultureInfo(CultureInfo.InvariantCulture.Name);
|
|
|
|
satoshiCulture.NumberFormat.NumberGroupSeparator = " ";
|
|
|
|
|
2019-09-11 07:49:06 +02:00
|
|
|
model.CryptoCode = "Sats";
|
2020-01-27 04:57:46 -06:00
|
|
|
model.BtcDue = Money.Parse(model.BtcDue).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
|
2020-06-28 17:55:27 +09:00
|
|
|
model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
|
2020-01-27 04:57:46 -06:00
|
|
|
model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
|
|
|
|
|
2019-09-11 07:49:06 +02:00
|
|
|
model.NetworkFee = new Money(model.NetworkFee, MoneyUnit.BTC).ToUnit(MoneyUnit.Satoshi);
|
|
|
|
}
|
2019-05-29 14:33:31 +00:00
|
|
|
}
|
|
|
|
public override string GetCryptoImage(PaymentMethodId paymentMethodId)
|
|
|
|
{
|
|
|
|
var network = _networkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
|
|
|
|
return GetCryptoImage(network);
|
|
|
|
}
|
2020-06-28 17:55:27 +09:00
|
|
|
|
2019-05-29 14:33:31 +00:00
|
|
|
private string GetCryptoImage(BTCPayNetworkBase network)
|
|
|
|
{
|
|
|
|
return ((BTCPayNetwork)network).LightningImagePath;
|
|
|
|
}
|
|
|
|
public override string GetPaymentMethodName(PaymentMethodId paymentMethodId)
|
|
|
|
{
|
|
|
|
var network = _networkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
|
|
|
|
return GetPaymentMethodName(network);
|
|
|
|
}
|
2020-06-28 17:55:27 +09:00
|
|
|
|
2020-11-06 08:41:03 +01:00
|
|
|
public override CheckoutUIPaymentMethodSettings GetCheckoutUISettings()
|
|
|
|
{
|
|
|
|
return new CheckoutUIPaymentMethodSettings()
|
|
|
|
{
|
|
|
|
ExtensionPartial = "Lightning/LightningLikeMethodCheckout",
|
|
|
|
CheckoutBodyVueComponentName = "LightningLikeMethodCheckout",
|
|
|
|
CheckoutHeaderVueComponentName = "LightningLikeMethodCheckoutHeader",
|
|
|
|
NoScriptPartialName = "Lightning/LightningLikeMethodCheckoutNoScript"
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-05-29 14:33:31 +00:00
|
|
|
private string GetPaymentMethodName(BTCPayNetworkBase network)
|
|
|
|
{
|
|
|
|
return $"{network.DisplayName} (Lightning)";
|
|
|
|
}
|
2018-02-26 00:48:12 +09:00
|
|
|
}
|
|
|
|
}
|