diff --git a/LNUnit.Tests/Fixture/PostgresLightningFixture.cs b/LNUnit.Tests/Fixture/PostgresLightningFixture.cs new file mode 100644 index 0000000..8a5b847 --- /dev/null +++ b/LNUnit.Tests/Fixture/PostgresLightningFixture.cs @@ -0,0 +1,224 @@ +using System.Net; +using System.Net.Sockets; +using Docker.DotNet; +using Docker.DotNet.Models; +using LNUnit.Setup; +using Npgsql; +using ServiceStack; +using Xunit; +using Assert = NUnit.Framework.Assert; +using HostConfig = Docker.DotNet.Models.HostConfig; + +namespace LNUnit.Fixtures; + +// ReSharper disable once ClassNeverInstantiated.Global +public class PostgresLightningFixture : IDisposable +{ + public string DbContainerName { get; set; }= "postgres"; + private readonly DockerClient _client = new DockerClientConfiguration().CreateClient(); + private string _containerId; + private string _ip; + + public PostgresLightningFixture() + { + StartPostgres().Wait(); + AddDb("alice"); + AddDb("bob"); + AddDb("carol"); + SetupNetwork().Wait(); + } + + private void AddDb(string dbName) + { + using (NpgsqlConnection connection = new(DbConnectionString)) + { + connection.Open(); + using var checkIfExistsCommand = new NpgsqlCommand($"SELECT 1 FROM pg_catalog.pg_database WHERE datname = '{dbName}'", connection); + var result = checkIfExistsCommand.ExecuteScalar(); + + if (result == null) + { + + using var command = new NpgsqlCommand($"CREATE DATABASE \"{dbName}\"", connection); + command.ExecuteNonQuery(); + } + } + LNDConnectionStrings.Add(dbName, + $"postgresql://superuser:superuser@{_ip}:5432/{dbName}?sslmode=disable"); + } + + public string DbConnectionStringLND { get; private set; } + public string DbConnectionString { get; private set; } + + public Dictionary LNDConnectionStrings = new(); + public void Dispose() + { + GC.SuppressFinalize(this); + + // Remove containers + RemoveContainer(DbContainerName).Wait(); + RemoveContainer("miner").Wait(); + RemoveContainer("alice").Wait(); + RemoveContainer("bob").Wait(); + RemoveContainer("carol").Wait(); + + Builder?.Destroy(); + _client.Dispose(); + } + + public LNUnitBuilder? Builder { get; private set; } + + + public async Task SetupNetwork() + { + await RemoveContainer("miner"); + await RemoveContainer("alice"); + await RemoveContainer("bob"); + await RemoveContainer("carol"); + + await _client.CreateDockerImageFromPath("../../../../Docker/lnd", ["custom_lnd", "custom_lnd:latest"]); + Builder = new LNUnitBuilder(); + + Builder.AddBitcoinCoreNode(); + + Builder.AddPolarLNDNode("alice", + [ + new() + { + ChannelSize = 10_000_000, //10MSat + RemoteName = "bob" + } + ], imageName: "custom_lnd", tagName: "latest", pullImage: false,postgresDSN: LNDConnectionStrings["alice"]); + + Builder.AddPolarLNDNode("bob", + [ + new() + { + ChannelSize = 10_000_000, //10MSat + RemotePushOnStart = 1_000_000, // 1MSat + RemoteName = "alice" + } + ], imageName: "custom_lnd", tagName: "latest", pullImage: false,postgresDSN: LNDConnectionStrings["bob"]); + + Builder.AddPolarLNDNode("carol", + [ + new() + { + ChannelSize = 10_000_000, //10MSat + RemotePushOnStart = 1_000_000, // 1MSat + RemoteName = "alice" + }, + new() + { + ChannelSize = 10_000_000, //10MSat + RemotePushOnStart = 1_000_000, // 1MSat + RemoteName = "bob" + } + ], imageName: "custom_lnd", tagName: "latest", pullImage: false,postgresDSN: LNDConnectionStrings["carol"]); + + await Builder.Build(); + } + + public async Task StartPostgres() + { + await _client.PullImageAndWaitForCompleted("postgres", "16.2-alpine"); + await RemoveContainer(DbContainerName); + var nodeContainer = await _client.Containers.CreateContainerAsync(new CreateContainerParameters + { + Image = "postgres:16.2-alpine", + HostConfig = new HostConfig + { + NetworkMode = "bridge" + }, + Name = $"{DbContainerName}", + Hostname = $"{DbContainerName}", + Env = + [ + "POSTGRES_PASSWORD=superuser", + "POSTGRES_USER=superuser", + "POSTGRES_DB=postgres" + ] + }); + Assert.NotNull(nodeContainer); + _containerId = nodeContainer.ID; + var started = await _client.Containers.StartContainerAsync(_containerId, new ContainerStartParameters()); + + //Build connection string + var ipAddressReady = false; + while (!ipAddressReady) + { + var listContainers = await _client.Containers.ListContainersAsync(new ContainersListParameters()); + + var db = listContainers.FirstOrDefault(x => x.ID == nodeContainer.ID); + if (db != null) + { + _ip = db.NetworkSettings.Networks.First().Value.IPAddress; + DbConnectionString = $"Host={_ip};Database=postgres;Username=superuser;Password=superuser"; + ipAddressReady = true; + } + else + { + await Task.Delay(100); + } + + } + //wait for TCP socket to open + var tcpConnectable = false; + while (!tcpConnectable) + { + try + { + TcpClient c = new() + { + ReceiveTimeout = 1, + SendTimeout = 1 + }; + await c.ConnectAsync(new IPEndPoint(IPAddress.Parse(_ip), 5432)); + if (c.Connected) + { + tcpConnectable = true; + } + } + catch (Exception e) + { + await Task.Delay(50); + } + } + } + + private async Task RemoveContainer(string name) + { + try + { + await _client.Containers.RemoveContainerAsync(name, + new ContainerRemoveParameters { Force = true, RemoveVolumes = true }); + } + catch + { + // ignored + } + } + + public async Task IsRunning() + { + try + { + var inspect = await _client.Containers.InspectContainerAsync(DbContainerName); + return inspect.State.Running; + } + catch + { + // ignored + } + + return false; + } +} + +[CollectionDefinition("postgres")] +public class PostgresLightningFixtureCollection : ICollectionFixture +{ + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. +} \ No newline at end of file diff --git a/LNUnit.Tests/LNUnit.Tests.csproj b/LNUnit.Tests/LNUnit.Tests.csproj index f9c4ed3..2fe05e0 100644 --- a/LNUnit.Tests/LNUnit.Tests.csproj +++ b/LNUnit.Tests/LNUnit.Tests.csproj @@ -15,6 +15,7 @@ + @@ -35,6 +36,7 @@ + diff --git a/LNUnit.Tests/PostgresLightningScenario.cs b/LNUnit.Tests/PostgresLightningScenario.cs new file mode 100644 index 0000000..561eb34 --- /dev/null +++ b/LNUnit.Tests/PostgresLightningScenario.cs @@ -0,0 +1,53 @@ +using System.Collections.Immutable; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using Lnrpc; +using LNUnit.Fixtures; +using ServiceStack; +using ServiceStack.Text; +using Xunit; +using Xunit.Abstractions; +using Assert = NUnit.Framework.Assert; + +namespace NLightning.Bolts.Tests.Docker; + +#pragma warning disable xUnit1033 // Test classes decorated with 'Xunit.IClassFixture' or 'Xunit.ICollectionFixture' should add a constructor argument of type TFixture +[Collection("postgres")] +public class PostgresXUnitTest +{ + private readonly PostgresLightningFixture _lightningRegtestNetworkFixture; + + public PostgresXUnitTest(PostgresLightningFixture fixture, ITestOutputHelper output) + { + _lightningRegtestNetworkFixture = fixture; + Console.SetOut(new TestOutputWriter(output)); + } + + + + [Fact] + public async Task Verify_Alice_Bob_Carol_Setup() + { + var readyNodes = _lightningRegtestNetworkFixture.Builder!.LNDNodePool!.ReadyNodes.ToImmutableList(); + var nodeCount = readyNodes.Count; + Assert.AreEqual(3, nodeCount); + $"LND Nodes in Ready State: {nodeCount}".Print(); + foreach (var node in readyNodes) + { + var walletBalanceResponse = await node.LightningClient.WalletBalanceAsync(new WalletBalanceRequest()); + var channels = await node.LightningClient.ListChannelsAsync(new ListChannelsRequest()); + $"Node {node.LocalAlias} ({node.LocalNodePubKey})".Print(); + walletBalanceResponse.PrintDump(); + channels.PrintDump(); + } + $"Bitcoin Node Balance: {(await _lightningRegtestNetworkFixture.Builder!.BitcoinRpcClient!.GetBalanceAsync()).Satoshi / 1e8}".Print(); + } + + // [Fact] + // public async Task KeepRunning5Min() + // { + // await Task.Delay(5 * 60 * 1000); + // } +} +#pragma warning restore xUnit1033 // Test classes decorated with 'Xunit.IClassFixture' or 'Xunit.ICollectionFixture' should add a constructor argument of type TFixture \ No newline at end of file diff --git a/LNUnit.Tests/TestOutputWriter.cs b/LNUnit.Tests/TestOutputWriter.cs new file mode 100644 index 0000000..66b9445 --- /dev/null +++ b/LNUnit.Tests/TestOutputWriter.cs @@ -0,0 +1,21 @@ +using System.Text; +using Xunit.Abstractions; + +namespace NLightning.Bolts.Tests.Docker; + +public class TestOutputWriter : TextWriter +{ + private readonly ITestOutputHelper _output; + + public TestOutputWriter(ITestOutputHelper output) + { + _output = output; + } + + public override Encoding Encoding => Encoding.UTF8; + + public override void Write(char[] buffer, int index, int count) + { + _output.WriteLine(new string(buffer, index, count)); + } +} \ No newline at end of file diff --git a/LNUnit.sln.DotSettings b/LNUnit.sln.DotSettings index f6f79f7..0cfa4df 100644 --- a/LNUnit.sln.DotSettings +++ b/LNUnit.sln.DotSettings @@ -17,6 +17,7 @@ True True True + True True True True diff --git a/LNUnit/Setup/LNUnitBuilder.cs b/LNUnit/Setup/LNUnitBuilder.cs index 7c151bc..27fbbac 100644 --- a/LNUnit/Setup/LNUnitBuilder.cs +++ b/LNUnit/Setup/LNUnitBuilder.cs @@ -262,8 +262,15 @@ await _dockerClient.Containers.StartContainerAsync(loopServer?.ID, n.DockerContainerId = nodeContainer.ID; var success = await _dockerClient.Containers.StartContainerAsync(nodeContainer.ID, new ContainerStartParameters()); - var inspectionResponse = await _dockerClient.Containers.InspectContainerAsync(n.DockerContainerId); - var ipAddress = inspectionResponse.NetworkSettings.Networks.First().Value.IPAddress; + //Not always having IP yet. + var ipAddress = string.Empty; + ContainerInspectResponse? inspectionResponse = null; + while (ipAddress.IsEmpty()) + { + inspectionResponse = await _dockerClient.Containers.InspectContainerAsync(n.DockerContainerId); + ipAddress = inspectionResponse.NetworkSettings.Networks.First().Value.IPAddress; + } + var basePath = !n.Image.Contains("lightning-terminal") ? "/home/lnd/.lnd" : "/root/lnd/.lnd"; if (n.Image.Contains("lightning-terminal")) await Task.Delay(2000); var txt = await GetStringFromFS(n.DockerContainerId, $"{basePath}/tls.cert"); @@ -826,8 +833,8 @@ public static LNUnitBuilder AddBitcoinCoreNode(this LNUnitBuilder b, LNUnitNetwo public static LNUnitBuilder AddPolarLNDNode(this LNUnitBuilder b, string aliasHostname, List? channels = null, string bitcoinMinerHost = "miner", string rpcUser = "bitcoin", string rpcPass = "bitcoin", string imageName = "polarlightning/lnd", - string tagName = "0.16.2-beta", bool acceptKeysend = true, bool pullImage = true, bool mapTotmp = false, - bool gcInvoiceOnStartup = false, bool gcInvoiceOnFly = false) + string tagName = "0.17.4-beta", bool acceptKeysend = true, bool pullImage = true, bool mapTotmp = false, + bool gcInvoiceOnStartup = false, bool gcInvoiceOnFly = false, string postgresDSN = null) { var cmd = new List { @@ -855,6 +862,14 @@ public static LNUnitBuilder AddPolarLNDNode(this LNUnitBuilder b, string aliasHo "--gossip.max-channel-update-burst=100", "--gossip.channel-update-interval=1s" }; + + if (!postgresDSN.IsEmpty()) + { + cmd.Add("--db.backend=postgres"); + cmd.Add($"--db.postgres.dsn={postgresDSN}"); + cmd.Add($"--db.postgres.timeout=300s"); + cmd.Add($"--db.postgres.maxconnections=16"); + } if (gcInvoiceOnStartup) cmd.Add("--gc-canceled-invoices-on-startup"); if (gcInvoiceOnFly) cmd.Add("--gc-canceled-invoices-on-the-fly");