From c2b1b8eca4162d3b4a1ed5616447d98dd2874967 Mon Sep 17 00:00:00 2001 From: Richard Safier Date: Sun, 21 Apr 2024 00:24:13 -0400 Subject: [PATCH] use direct client, establishes connections now --- LNUnit.Eclair/EclairClientV2.cs | 383 ++++++++++++++++++++++++++ LNUnit.Eclair/EclairNodeConnection.cs | 12 +- LNUnit.Eclair/EclairNodePool.cs | 2 +- LNUnit.Eclair/EclairSettings.cs | 2 + LNUnit.Tests/EclairLightningTests.cs | 21 +- LNUnit/Setup/LNUnitBuilder.cs | 42 ++- 6 files changed, 440 insertions(+), 22 deletions(-) create mode 100644 LNUnit.Eclair/EclairClientV2.cs diff --git a/LNUnit.Eclair/EclairClientV2.cs b/LNUnit.Eclair/EclairClientV2.cs new file mode 100644 index 0000000..ba6fff2 --- /dev/null +++ b/LNUnit.Eclair/EclairClientV2.cs @@ -0,0 +1,383 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Lightning.Eclair.Models; +using NBitcoin; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Lightning.Eclair +{ + public class EclairClientV2 + { + private readonly Uri _address; + private readonly string _username; + private readonly string _password; + private readonly HttpClient _httpClient; + private static readonly HttpClient SharedClient = new (); + + public Network Network { get; } + + public EclairClientV2(Uri address, string password, Network network, HttpClient httpClient = null):this(address,null, password,network, httpClient){} + public EclairClientV2(Uri address, string username, string password, Network network, HttpClient httpClient = null) + { + if (address == null) + throw new ArgumentNullException(nameof(address)); + if (network == null) + throw new ArgumentNullException(nameof(network)); + _address = address; + _username = username; + _password = password; + Network = network; + _httpClient = httpClient ?? SharedClient; + } + + public async Task GetInfo(CancellationToken cts = default) + { + return await SendCommandAsync("getinfo", NoRequestModel.Instance, cts); + } + + public async Task GlobalBalance(CancellationToken cts = default) + { + return await SendCommandAsync("globalbalance", NoRequestModel.Instance, cts); + } + + public async Task UsableBalances(CancellationToken cts = default) + { + return await SendCommandAsync("usablebalances", NoRequestModel.Instance, cts); + } + + public async Task Connect(string uri, CancellationToken cts = default) + { + return await SendCommandAsync("connect", new ConnectUriRequest() + { + Uri = uri + }, cts); + } + + public async Task Connect(PubKey nodeId, string host, int? port = null, + CancellationToken cts = default) + { + return await SendCommandAsync("connect", new ConnectManualRequest() + { + Host = host, + Port = port, + NodeId = nodeId.ToString() + }, cts); + } + + public async Task Open(PubKey nodeId, long fundingSatoshis, int? pushMsat = null, + long? fundingFeerateSatByte = null, ChannelFlags? channelFlags = null, + CancellationToken cts = default) + { + return await SendCommandAsync("open", new OpenRequest() + { + NodeId = nodeId.ToString(), + FundingSatoshis = fundingSatoshis, + ChannelFlags = channelFlags, + PushMsat = pushMsat, + FundingFeerateSatByte = fundingFeerateSatByte + }, cts); + + } + + public async Task Close(string channelId, string shortChannelId = null, Script scriptPubKey = null, + CancellationToken cts = default) + { + return await SendCommandAsync("close", new CloseRequest() + { + ChannelId = channelId, + ShortChannelId = shortChannelId, + ScriptPubKey = scriptPubKey?.ToHex() + }, cts); + + } + + public async Task ForceClose(string channelId, string shortChannelId = null, + CancellationToken cts = default) + { + return await SendCommandAsync("forceclose", new ForceCloseRequest() + { + ChannelId = channelId, + ShortChannelId = shortChannelId, + }, cts); + } + + public async Task UpdateRelayFee(string channelId, int feeBaseMsat, int feeProportionalMillionths, + CancellationToken cts = default) + { + return await SendCommandAsync("updaterelayfee", new UpdateRelayFeeRequest() + { + ChannelId = channelId, + FeeBaseMsat = feeBaseMsat, + FeeProportionalMillionths = feeProportionalMillionths + }, cts); + } + + public async Task> Peers(CancellationToken cts = default) + { + return await SendCommandAsync>("peers", NoRequestModel.Instance, cts); + } + + public async Task> Channels(PubKey nodeId = null, + CancellationToken cts = default) + { + return await SendCommandAsync>("channels", new ChannelsRequest() + { + NodeId = nodeId?.ToString() + }, cts); + } + + public async Task Channel(string channelId, CancellationToken cts = default) + { + return await SendCommandAsync("channel", new ChannelRequest() + { + ChannelId = channelId + }, cts); + } + + public async Task> AllNodes(CancellationToken cts = default) + { + return await SendCommandAsync>("allnodes", NoRequestModel.Instance, + cts); + } + + public async Task> AllChannels(CancellationToken cts = default) + { + return await SendCommandAsync>("allchannels", + NoRequestModel.Instance, cts); + } + + public async Task> AllUpdates(CancellationToken cts = default) + { + return await SendCommandAsync>("allupdates", + NoRequestModel.Instance, cts); + } + + public async Task CreateInvoice(string description, long? amountMsat = null, + int? expireIn = null, BitcoinAddress fallbackAddress = null, + CancellationToken cts = default) + { + return await SendCommandAsync("createinvoice", + new CreateInvoiceRequest + { + Description = description, + ExpireIn = expireIn, + AmountMsat = amountMsat == 0 ? null : amountMsat, + FallbackAddress = fallbackAddress?.ToString() + }, cts); + } + + public async Task ParseInvoice(string invoice, + CancellationToken cts = default) + { + return await SendCommandAsync("parseinvoice", + new ParseInvoiceRequest() + { + Invoice = invoice + }, cts); + } + + public async Task PayInvoice(PayInvoiceRequest payInvoiceRequest, CancellationToken cts = default) + { + return await SendCommandAsync("payinvoice", payInvoiceRequest, cts); + } + + public async Task SendToNode(SendToNodeRequest sendToNodeRequest, CancellationToken cts = default) + { + return await SendCommandAsync("sendtonode", sendToNodeRequest, cts); + } + + public Task GetNewAddress(CancellationToken cancellationToken = default) + { + return SendCommandAsync("getnewaddress", new NoRequestModel(), cancellationToken); + } + + public async Task> GetSentInfo(string paymentHash, string id = null, + CancellationToken cts = default) + { + return await SendCommandAsync>("getsentinfo", + new GetSentInfoRequest + { + PaymentHash = paymentHash, + Id = id + }, cts); + } + + public async Task GetReceivedInfo(string paymentHash, string invoice = null, + CancellationToken cts = default) + { + return await SendCommandAsync("getreceivedinfo", + new GetReceivedInfoRequest + { + PaymentHash = paymentHash, + Invoice = invoice + }, cts); + } + + public async Task GetInvoice(string paymentHash, + CancellationToken cts = default) + { + return await SendCommandAsync("getinvoice", + new GetReceivedInfoRequest() + { + PaymentHash = paymentHash + }, cts); + } + + public async Task> ListInvoices(int? from = null, int? to = null, + CancellationToken cts = default) + { + return await SendCommandAsync>("listinvoices", + new ListInvoicesRequest { From = from, To = to }, cts); + } + + public async Task> ListPendingInvoices(int? from = null, int? to = null, + CancellationToken cts = default) + { + return await SendCommandAsync>("listpendinginvoices", + new ListInvoicesRequest { From = from, To = to }, cts); + } + + public async Task> FindRoute(string invoice, int? amountMsat = null, + CancellationToken cts = default) + { + return await SendCommandAsync>("findroute", + new FindRouteRequest() + { + Invoice = invoice, + AmountMsat = amountMsat + }, cts); + } + + public async Task> FindRouteToNode(PubKey nodeId, int amountMsat, + CancellationToken cts = default) + { + return await SendCommandAsync>("findroutetonode", + new FindRouteToNodeRequest() + { + NodeId = nodeId.ToString(), + AmountMsat = amountMsat + }, cts); + } + + public async Task Audit(DateTime? from = null, DateTime? to = null, + CancellationToken cts = default) + { + return await SendCommandAsync("audit", + new AuditRequest() + { + From = from?.ToUnixTimestamp(), + To = to?.ToUnixTimestamp() + }, cts); + } + + public async Task> NetworkFees(DateTime? from = null, DateTime? to = null, + CancellationToken cts = default) + { + return await SendCommandAsync>("networkfees", + new NetworkFeesRequest() + { + From = from?.ToUnixTimestamp(), + To = to?.ToUnixTimestamp() + }, cts); + } + + public async Task> ChannelStats(DateTime? from = null, DateTime? to = null, + CancellationToken cts = default) + { + return await SendCommandAsync>("channelstats", NoRequestModel.Instance, cts); + } + + + JsonSerializer _Serializer; + JsonSerializerSettings _SerializerSettings; + JsonSerializerSettings SerializerSettings + { + get + { + if (_SerializerSettings == null) + { + var jsonSerializer = new JsonSerializerSettings(); + NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(jsonSerializer, Network); + _SerializerSettings = jsonSerializer; + } + return _SerializerSettings; + } + } + JsonSerializer Serializer + { + get + { + if (_Serializer == null) + { + _Serializer = JsonSerializer.Create(SerializerSettings); + } + return _Serializer; + } + } + + private async Task SendCommandAsync(string method, TRequest data, CancellationToken cts) + { + HttpContent content = null; + if (data != null && !(data is NoRequestModel)) + { + var jobj = JObject.FromObject(data, Serializer); + Dictionary x = new Dictionary(); + foreach (var item in jobj) + { + if (item.Value == null || (item.Value.Type == JTokenType.Null)) + { + continue; + } + x.Add(item.Key, item.Value.ToString()); + } + content = new FormUrlEncodedContent(x.Select(pair => pair)); + } + + var httpRequest = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri(_address, method), + Content = content + }; + httpRequest.Headers.Accept.Clear(); + httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.Default.GetBytes($"{_username??string.Empty}:{_password}"))); + + var rawResult = await _httpClient.SendAsync(httpRequest, cts); + var rawJson = await rawResult.Content.ReadAsStringAsync(); + if (!rawResult.IsSuccessStatusCode) + { + throw new EclairApiException + { + Error = JsonConvert.DeserializeObject(rawJson, SerializerSettings) + }; + } + return JsonConvert.DeserializeObject(rawJson, SerializerSettings); + } + + + internal class NoRequestModel + { + public static NoRequestModel Instance = new NoRequestModel(); + } + + internal class EclairApiException : Exception + { + public EclairApiError Error { get; set; } + + public override string Message => Error?.Error; + } + + internal class EclairApiError + { + public string Error { get; set; } + } + } +} \ No newline at end of file diff --git a/LNUnit.Eclair/EclairNodeConnection.cs b/LNUnit.Eclair/EclairNodeConnection.cs index ba20c0f..c54c41f 100644 --- a/LNUnit.Eclair/EclairNodeConnection.cs +++ b/LNUnit.Eclair/EclairNodeConnection.cs @@ -1,4 +1,5 @@ using BTCPayServer.Lightning; +using BTCPayServer.Lightning.Eclair; namespace LNUnit.Eclair; @@ -17,7 +18,7 @@ public EclairNodeConnection(EclairSettings s) public byte[] LocalNodePubKeyBytes => Convert.FromHexString(LocalNodePubKey); public string LocalAlias { get; internal set; } - public ILightningClient Client { get; internal set; } + public EclairClientV2 NodeClient { get; internal set; } public string Host { get; internal set; } public void Dispose() @@ -27,10 +28,11 @@ public void Dispose() private async Task Setup() { - ILightningClientFactory factory = new LightningClientFactory(_settings.Network); - Client = factory.Create(_settings.BtcPayConnectionString); - var info = await Client.GetInfo(); + // ILightningClientFactory factory = new LightningClientFactory(_settings.Network); + //NodeClient = factory.Create(_settings.BtcPayConnectionString); + NodeClient = new EclairClientV2(_settings.Uri, _settings.EclairPassword, _settings.Network); + var info = await NodeClient.GetInfo(); LocalAlias = info.Alias; - LocalNodePubKey = info.NodeInfoList.First().NodeId.ToString(); + LocalNodePubKey = info.NodeId.ToString(); } } \ No newline at end of file diff --git a/LNUnit.Eclair/EclairNodePool.cs b/LNUnit.Eclair/EclairNodePool.cs index 0952568..9c31dcd 100644 --- a/LNUnit.Eclair/EclairNodePool.cs +++ b/LNUnit.Eclair/EclairNodePool.cs @@ -156,7 +156,7 @@ private bool IsServerActive(EclairNodeConnection node) //TODO: Surely a better method but probably have to expose calls. try { - var info = node.Client.GetInfo().GetAwaiter().GetResult(); + var info = node.NodeClient.GetInfo().GetAwaiter().GetResult(); if (!info.IsErrorResponse()) return true; } diff --git a/LNUnit.Eclair/EclairSettings.cs b/LNUnit.Eclair/EclairSettings.cs index 1f4e531..b5b94ea 100644 --- a/LNUnit.Eclair/EclairSettings.cs +++ b/LNUnit.Eclair/EclairSettings.cs @@ -4,6 +4,8 @@ namespace LNUnit.Eclair; public class EclairSettings { + public Uri Uri => new Uri($"http://{Host}:{Port}"); + public string Host { get; set; } public int Port { get; set; } public string EclairPassword { get; set; } diff --git a/LNUnit.Tests/EclairLightningTests.cs b/LNUnit.Tests/EclairLightningTests.cs index e3f456f..d0c029f 100644 --- a/LNUnit.Tests/EclairLightningTests.cs +++ b/LNUnit.Tests/EclairLightningTests.cs @@ -9,7 +9,7 @@ namespace LNUnit.Tests; -[TestFixture("sqlite", "polarlightning/eclair", "0.10.0", "/root/.lnd", true)] +[TestFixture("sqlite", "polarlightning/eclair", "0.10.0", true)] public class EclairLightningTests : IDisposable { [SetUp] @@ -46,20 +46,18 @@ public async Task OneTimeSetup() await _client.CreateDockerImageFromPath("./../../../../Docker/bitcoin/27.0", ["bitcoin:latest", "bitcoin:27.0"]); - await SetupNetwork(_lndImage, _tag, _lndRoot, _pullImage); + await SetupNetwork(_lndImage, _tag, _pullImage); } public EclairLightningTests(string dbType, string lndImage = "custom_lnd", - string tag = "latest", - string lndRoot = "/root/.lnd", + string tag = "latest", bool pullImage = false ) { _dbType = dbType; _lndImage = lndImage; - _tag = tag; - _lndRoot = lndRoot; + _tag = tag; _pullImage = pullImage; } @@ -70,8 +68,7 @@ public EclairLightningTests(string dbType, private ServiceProvider _serviceProvider; private readonly string _dbType; private readonly string _lndImage; - private readonly string _tag; - private readonly string _lndRoot; + private readonly string _tag; private readonly bool _pullImage; public void Dispose() @@ -91,8 +88,8 @@ public void Dispose() public LNUnitBuilder? Builder { get; private set; } - public async Task SetupNetwork(string lndImage = "polarlightning/eclair", string lndTag = "0.10.0", - string lndRoot = "/root/.lnd", bool pullLndImage = false, string bitcoinImage = "bitcoin", + public async Task SetupNetwork(string eclairImage = "polarlightning/eclair", string eclairTag = "0.10.0", + bool pullEclairImage = false, string bitcoinImage = "bitcoin", string bitcoinTag = "27.0", bool pullBitcoinImage = false) { @@ -103,7 +100,7 @@ public async Task SetupNetwork(string lndImage = "polarlightning/eclair", string Builder.AddBitcoinCoreNode(image: bitcoinImage, tag: bitcoinTag, pullImage: pullBitcoinImage); - if (pullLndImage) await _client.PullImageAndWaitForCompleted(lndImage, lndTag); + if (pullEclairImage) await _client.PullImageAndWaitForCompleted(eclairImage, eclairTag); Builder.AddPolarEclairNode("alice", @@ -128,7 +125,7 @@ public async Task SetupNetwork(string lndImage = "polarlightning/eclair", string ChannelSize = 10_000_000, //10MSat RemoteName = "bob" } - ],pullImage:true, + ],pullImage:true, tagName:eclairTag, imageName: eclairImage, postgresDSN: _dbType == "postgres" ? PostgresFixture.LNDConnectionStrings["alice"] : null); Builder.AddPolarEclairNode("bob", diff --git a/LNUnit/Setup/LNUnitBuilder.cs b/LNUnit/Setup/LNUnitBuilder.cs index 7eaf70f..141a0a1 100644 --- a/LNUnit/Setup/LNUnitBuilder.cs +++ b/LNUnit/Setup/LNUnitBuilder.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Text; using BTCPayServer.Lightning; +using BTCPayServer.Lightning.Eclair.Models; using Dasync.Collections; using Docker.DotNet; using Docker.DotNet.Models; @@ -16,8 +17,10 @@ using NBitcoin.RPC; using Routerrpc; using ServiceStack; +using ServiceStack.Text; using SharpCompress.Readers; using AddressType = Lnrpc.AddressType; +using GetInfoResponse = Lnrpc.GetInfoResponse; using HostConfig = Docker.DotNet.Models.HostConfig; using Network = NBitcoin.Network; using NodeInfo = BTCPayServer.Lightning.NodeInfo; @@ -421,6 +424,32 @@ await GetTarStreamFromFS(n.DockerContainerId, await Task.Delay(250); if (cancelSource.IsCancellationRequested) throw new Exception("CANCELED"); } + + //Setup Channels (this includes sending funds and waiting) + foreach (var node in Configuration.EclairNodes) + { + var eclair = EclairNodePool.ReadyNodes.First(x => x.LocalAlias == node.Name); + + foreach (var c in node.Channels) + { + var remoteNode = EclairNodePool.ReadyNodes.First(x => x.LocalAlias == c.RemoteName); + var result = await eclair.NodeClient.Open(new PubKey(remoteNode.LocalNodePubKey), c.ChannelSize, + null, 10, ChannelFlags.Public, cancelSource.Token); + if (result != "") + result.PrintDump(); + // + // if (result.Result == OpenChannelResult.Ok) + // { + // _logger.LogInformation("Opened {channel_size} sat channel from {local} to {remote}", eclair.LocalAlias, remoteNode.LocalAlias, c.ChannelSize); + // } + // else + // { + // _logger.LogError("Could not create eclair channel {error}", result.Result); + // throw new Exception("Could not create eclair channel"); + // } + await BitcoinRpcClient.GenerateAsync(6); + } + } } if (Configuration.LNDNodes.Any()) @@ -667,12 +696,16 @@ private async Task ConnectPeers(EclairNodeConnection node, EclairNodeConnection do_again: try { - var nodeInfo = new NodeInfo(new PubKey(remoteNode.LocalNodePubKey), remoteNode.Host, 9735); - var result = await node.Client.ConnectTo(nodeInfo, cancelToken); - if (result == ConnectionResult.Ok) + // var nodeInfo = new NodeInfo(new PubKey(remoteNode.LocalNodePubKey), remoteNode.Host, 9735); + var result = await node.NodeClient.Connect(new PubKey(remoteNode.LocalNodePubKey),remoteNode.Host,9735, cancelToken); + if (result == "connected") { _logger.LogInformation("Connected: {localAlias} -> {remoteAlias}", node.LocalAlias, remoteNode.LocalAlias); } + else if (result == "already connected") + { + _logger.LogInformation("Already Connected: {localAlias} -> {remoteAlias}", node.LocalAlias, remoteNode.LocalAlias); + } else { _logger.LogWarning("Could NOT Connect: {localAlias} -> {remoteAlias}", node.LocalAlias, remoteNode.LocalAlias); @@ -1165,7 +1198,8 @@ public static LNUnitBuilder AddPolarEclairNode(this LNUnitBuilder b, string alia "--datadir=/home/eclair/.eclair", "--printToConsole=true", "--on-chain-fees.feerate-tolerance.ratio-low=0.00001", - "--on-chain-fees.feerate-tolerance.ratio-high=10000.0" + "--on-chain-fees.feerate-tolerance.ratio-high=10000.0", + "--eclair.relay.fees=1000" };