Files
zap-stream-api/NostrStreamer/Services/LndNode.cs
2024-08-28 11:02:47 +01:00

186 lines
5.8 KiB
C#

using System.Security.Cryptography.X509Certificates;
using BTCPayServer.Lightning;
using Google.Protobuf;
using Grpc.Core;
using Grpc.Net.Client;
using NBitcoin;
using Lnrpc;
using Routerrpc;
namespace NostrStreamer.Services;
public class LndNode
{
private readonly Network _network;
private readonly ILogger<LndNode> _logger;
public LndNode(Config config, ILogger<LndNode> logger)
{
_logger = logger;
_network = Network.GetNetwork(config.Network) ?? Network.RegTest;
var channelOptions = new GrpcChannelOptions();
ConfigureClient(channelOptions, config.Lnd);
var channel = GrpcChannel.ForAddress(config.Lnd.Endpoint, channelOptions);
LightningClient = new(channel);
InvoicesClient = new(channel);
RouterClient = new(channel);
}
public Lightning.LightningClient LightningClient { get; }
public Router.RouterClient RouterClient { get; }
public Invoicesrpc.Invoices.InvoicesClient InvoicesClient { get; }
public async Task<AddInvoiceResponse> AddInvoice(ulong mSats, TimeSpan? expire = null, string? memo = null,
string? descriptionHash = null)
{
var req = new Invoice()
{
ValueMsat = (long)mSats,
Expiry = (long)(expire ?? TimeSpan.FromHours(1)).TotalSeconds
};
if (!string.IsNullOrEmpty(descriptionHash))
{
req.DescriptionHash = ByteString.CopyFrom(Convert.FromHexString(descriptionHash));
}
else if (!string.IsNullOrEmpty(memo))
{
req.Memo = memo;
}
return await LightningClient.AddInvoiceAsync(req);
}
public async Task<Payment?> GetPayment(string paymentHash)
{
using var trackPayment = RouterClient.TrackPaymentV2(new()
{
NoInflightUpdates = true,
PaymentHash = ByteString.CopyFrom(Convert.FromHexString(paymentHash))
});
if (await trackPayment.ResponseStream.MoveNext())
{
return trackPayment.ResponseStream.Current;
}
return default;
}
public async Task<Payment?> SendPayment(string paymentRequest, long feeLimit = 0)
{
using var payment = RouterClient.SendPaymentV2(new()
{
PaymentRequest = paymentRequest,
TimeoutSeconds = 120,
FeeLimitMsat = feeLimit,
NoInflightUpdates = true
});
if (await payment.ResponseStream.MoveNext())
{
return payment.ResponseStream.Current;
}
return default;
}
public async Task<string> GenerateAddress()
{
var rsp = await LightningClient.NewAddressAsync(new());
return rsp.Address;
}
public async Task<Invoicesrpc.AddHoldInvoiceResp?> WrapInvoice(string pr, ulong fee)
{
const double minExpireMins = 8;
const long minCltvDelta = 10;
const long maxCltvDelta = 2016;
var now = DateTimeOffset.UtcNow;
var decoded = BOLT11PaymentRequest.Parse(pr, _network);
var newAmount = decoded.MinimumAmount.MilliSatoshi + (long)fee;
if (decoded.ExpiryDate <= now)
{
throw new InvalidOperationException("Invoice already expired");
}
if ((decoded.ExpiryDate - now).TotalMinutes < minExpireMins)
{
throw new InvalidOperationException("Expiry too soon");
}
if (decoded.MinFinalCLTVExpiry < minCltvDelta)
{
throw new InvalidOperationException("CLTV delta too low");
}
if (decoded.MinFinalCLTVExpiry > maxCltvDelta)
{
throw new InvalidOperationException("CLTV delta too high");
}
var knownBits = new[] { 8, 9, 14, 15, 16, 17 };
for (var x = 0; x < 64; x++)
{
var n = 1L << x;
var fb = (FeatureBits)n;
if (decoded.FeatureBits.HasFlag(fb) && (!knownBits.Contains(x) || !Enum.IsDefined(fb)))
{
throw new InvalidCastException("Unknown feature bit set");
}
}
var req = new Invoicesrpc.AddHoldInvoiceRequest
{
CltvExpiry = (ulong)decoded.MinFinalCLTVExpiry + 18,
Expiry = (long)(decoded.ExpiryDate - DateTimeOffset.UtcNow).TotalSeconds,
Hash = ByteString.CopyFrom(decoded.PaymentHash!.ToBytes(false)),
ValueMsat = newAmount
};
if (!string.IsNullOrEmpty(decoded.ShortDescription))
{
req.Memo = decoded.ShortDescription;
}
else if (decoded.DescriptionHash != null)
{
req.DescriptionHash = ByteString.CopyFrom(decoded.DescriptionHash.ToBytes(false));
}
return await InvoicesClient.AddHoldInvoiceAsync(req);
}
private void ConfigureClient(GrpcChannelOptions opt, LndConfig conf)
{
try
{
var macaroon = File.ReadAllBytes(Environment.ExpandEnvironmentVariables(conf.MacaroonPath));
var cert = File.ReadAllBytes(Environment.ExpandEnvironmentVariables(conf.CertPath));
var asyncInterceptor = new AsyncAuthInterceptor((_, meta) =>
{
meta.Add("macaroon", Convert.ToHexString(macaroon));
return Task.CompletedTask;
});
var httpHandler = new HttpClientHandler();
httpHandler.ServerCertificateCustomValidationCallback = (_, certificate2, _, _) =>
{
var serverCert = new X509Certificate2(cert);
return certificate2!.Thumbprint == serverCert.Thumbprint;
};
opt.HttpHandler = httpHandler;
opt.Credentials = ChannelCredentials.Create(ChannelCredentials.SecureSsl,
CallCredentials.FromInterceptor(asyncInterceptor));
}
catch (Exception ex)
{
_logger.LogError(ex, "Configure failed");
}
}
}