Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/BTCPayServer.Lightning.Common/CloseChannelRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;

namespace BTCPayServer.Lightning
{
public class CloseChannelRequest
{
public NodeInfo NodeInfo
{
get; set;
}
public string ChannelPointFundingTxIdStr
{
get; set;
}
public long ChannelPointOutputIndex
{
get; set;
}
public static void AssertIsSane(CloseChannelRequest closeChannelRequest)
{
if (closeChannelRequest == null)
throw new ArgumentNullException(nameof(closeChannelRequest));
if (closeChannelRequest.ChannelPointFundingTxIdStr == null)
throw new ArgumentNullException(nameof(closeChannelRequest.ChannelPointFundingTxIdStr));
}
}
}
20 changes: 20 additions & 0 deletions src/BTCPayServer.Lightning.Common/CloseChannelResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace BTCPayServer.Lightning
{
public enum CloseChannelResult
{
Ok,
Failed,
}

public class CloseChannelResponse
{
public CloseChannelResponse(CloseChannelResult result)
{
Result = result;
}
public CloseChannelResult Result
{
get; set;
}
}
}
4 changes: 4 additions & 0 deletions src/BTCPayServer.Lightning.Common/OpenChannelResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,9 @@ public OpenChannelResult Result
{
get; set;
}
public string FundingTxIdIfAvailable
{
get; set;
}
}
}
20 changes: 19 additions & 1 deletion src/BTCPayServer.Lightning.LND/LndClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,9 @@ async Task<OpenChannelResponse> ILightningClient.OpenChannel(OpenChannelRequest
req.Sat_per_byte = ((int)openChannelRequest.FeeRate.SatoshiPerByte).ToString();
}
var result = await this.SwaggerClient.OpenChannelSyncAsync(req, cancellation);
return new OpenChannelResponse(OpenChannelResult.Ok);
var res = new OpenChannelResponse(OpenChannelResult.Ok);
res.FundingTxIdIfAvailable = result.Funding_txid_str;
return res;
}
catch(SwaggerException ex) when
(ex.AsLNDError() is LndError2 lndError &&
Expand Down Expand Up @@ -427,6 +429,22 @@ async Task<OpenChannelResponse> ILightningClient.OpenChannel(OpenChannelRequest
}
}

public async Task<CloseChannelResponse> CloseChannel(CloseChannelRequest closeChannelRequest, CancellationToken cancellation)
{
CloseChannelRequest.AssertIsSane(closeChannelRequest);
cancellation.ThrowIfCancellationRequested();
try
{
var result = await this.SwaggerClient.CloseChannelAsync(closeChannelRequest.ChannelPointFundingTxIdStr, closeChannelRequest.ChannelPointOutputIndex, cancellation);
return new CloseChannelResponse(CloseChannelResult.Ok);
}
catch (Exception)
{
throw;
//return new CloseChannelResponse(CloseChannelResult.Failed);
}
}

async Task<BitcoinAddress> ILightningClient.GetDepositAddress()
{
return BitcoinAddress.Create((await SwaggerClient.NewWitnessAddressAsync()).Address, Network);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ namespace BTCPayServer.Lightning.Tests
/// <summary>
/// Utilities to connect channels on regtest
/// </summary>
public static class ConnectChannels
public static class ChannelSetup
{
static ConnectChannels()
static ChannelSetup()
{
Logs = NullLogger.Instance;
}
Expand All @@ -26,13 +26,13 @@ public static ILogger Logs
set;
}
/// <summary>
/// Create channels from all senders to all receivers while mining on cashCow
/// Open channels from all senders to all receivers while mining on cashCow
/// </summary>
/// <param name="cashCow">The miner and liquidity source</param>
/// <param name="senders">Senders of payment on Lightning Network</param>
/// <param name="receivers">Receivers of payment on Lightning Network</param>
/// <returns></returns>
public static async Task ConnectAll(RPCClient cashCow, IEnumerable<ILightningClient> senders, IEnumerable<ILightningClient> receivers)
public static async Task OpenAll(RPCClient cashCow, IEnumerable<ILightningClient> senders, IEnumerable<ILightningClient> receivers)
{
if(await cashCow.GetBlockCountAsync() <= cashCow.Network.Consensus.CoinbaseMaturity)
{
Expand All @@ -44,11 +44,20 @@ public static async Task ConnectAll(RPCClient cashCow, IEnumerable<ILightningCli
{
foreach(var dest in receivers)
{
await CreateChannel(cashCow, sender, dest);
await OpenChannel(cashCow, sender, dest);
}
}
}
public static async Task CreateChannel(RPCClient cashCow, ILightningClient sender, ILightningClient dest)

private static Dictionary<(ILightningClient, ILightningClient), string> availableFundingTxIds
= new Dictionary<(ILightningClient, ILightningClient), string>();

internal static string GetFundingTxIdForChannel(ILightningClient sender, ILightningClient dest)
{
return availableFundingTxIds.GetValueOrDefault((sender, dest));
}

public static async Task OpenChannel(RPCClient cashCow, ILightningClient sender, ILightningClient dest)
{
var destInfo = await dest.GetInfo();
var destInvoice = await dest.CreateInvoice(1000, "EnsureConnectedToDestination", TimeSpan.FromSeconds(5000));
Expand Down Expand Up @@ -115,6 +124,8 @@ public static async Task CreateChannel(RPCClient cashCow, ILightningClient sende
}
if(openChannel.Result == OpenChannelResult.Ok)
{
if (!String.IsNullOrEmpty(openChannel.FundingTxIdIfAvailable))
availableFundingTxIds[(sender, dest)] = openChannel.FundingTxIdIfAvailable;
// generate one block and a bit more time to confirm channel opening
await cashCow.GenerateAsync(1);
await WaitLNSynched(cashCow, sender);
Expand Down
99 changes: 97 additions & 2 deletions tests/CommonTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,76 @@ namespace BTCPayServer.Lightning.Tests
{
public class CommonTests
{
uint CountJsonObjects(string jsonString)
{
uint countJsonInner(char? head, string tail, uint braceNestingLevel, bool insideSubString, uint acc)
{
if (!head.HasValue)
{
if (braceNestingLevel != 0)
throw new ArgumentException("jsonString seems to be invalid");
return acc;
}

char? newHead = null;
if (!String.IsNullOrEmpty(tail))
newHead = tail.First();
var newTail = newHead == null ? null : tail.Substring(1);

if (!insideSubString && head == '"') {
return countJsonInner(newHead, newTail, braceNestingLevel, true, acc);
}
if (insideSubString)
{
if (head == '"')
return countJsonInner(newHead, newTail, braceNestingLevel, false, acc);
return countJsonInner(newHead, newTail, braceNestingLevel, insideSubString, acc);
}

if (head == '{')
return countJsonInner(newHead, newTail, braceNestingLevel + 1, insideSubString, acc);
else if (head == '}')
{
if (braceNestingLevel == 0)
throw new ArgumentException("jsonString seems to be invalid");
var newNestingLevel = braceNestingLevel - 1;
var newAcc = (newNestingLevel == 0) ? acc + 1 : acc;
return countJsonInner(newHead, newTail, newNestingLevel, insideSubString, newAcc);
}
else
return countJsonInner(newHead, newTail, braceNestingLevel, insideSubString, acc);
}

if (jsonString == string.Empty)
return 0;
if (!jsonString.StartsWith("{"))
throw new ArgumentException("jsonString should start with {");
if (!jsonString.EndsWith("}"))
throw new ArgumentException("jsonString should end with }");

return countJsonInner(jsonString.First(), jsonString.Substring(1), 0, false, 0);
}

[Fact()]
public void JsonCounter()
{
Assert.Equal(0u, CountJsonObjects(String.Empty));

Assert.Throws<ArgumentException>(() => CountJsonObjects("x")); // not startswith {
Assert.Throws<ArgumentException>(() => CountJsonObjects("{x")); // not endswith }

Assert.Equal(1u, CountJsonObjects("{ \"foo\": 1 }"));
Assert.Equal(2u, CountJsonObjects("{ \"foo\": 1 }{ \"bar\": 2 }"));
Assert.Equal(3u, CountJsonObjects("{ \"foo\": 1 }{ \"bar\": 2 }{ \"baz\": 3 }"));

// ends with bad nesting level
Assert.Throws<ArgumentException>(() => CountJsonObjects("{{x}"));
Assert.Throws<ArgumentException>(() => CountJsonObjects("{x}}"));

// edge case
Assert.Equal(1u, CountJsonObjects("{ \"foo\": \"{\" }"));
}

#if DEBUG
public const int Timeout = 20 * 60 * 1000;
#else
Expand All @@ -25,7 +95,7 @@ public CommonTests(ITestOutputHelper helper)
Docker = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("IN_DOCKER_CONTAINER"));
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
ConnectChannels.Logs = Logs.LogProvider.CreateLogger("Tests");
ChannelSetup.Logs = Logs.LogProvider.CreateLogger("Tests");
}

public static bool Docker { get; set; }
Expand Down Expand Up @@ -271,7 +341,7 @@ private async Task EnsureConnectedToDestinations((string Name, ILightningClient
{
await Task.WhenAll(WaitServersAreUp($"{test.Name} (Customer)", test.Customer), WaitServersAreUp($"{test.Name} (Merchant)", test.Merchant));
Tests.Logs.Tester.LogInformation($"{test.Name}: Connecting channels...");
await ConnectChannels.ConnectAll(Tester.CreateRPC(), new[] { test.Customer }, new[] { test.Merchant });
await ChannelSetup.OpenAll(Tester.CreateRPC(), new[] { test.Customer }, new[] { test.Merchant });
Tests.Logs.Tester.LogInformation($"{test.Name}: Channels connected");
}

Expand Down Expand Up @@ -536,5 +606,30 @@ public void CanParseLightningURL()
Assert.True(LightningConnectionString.TryParse("type=charge;server=http://api-token:foiewnccewuify@127.0.0.1:54938/;allowinsecure=true", out conn));
Assert.Equal("type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify;allowinsecure=true", conn.ToString());
}

[Fact(Timeout = Timeout)]
public async Task CanCloseLndChannel()
{
// TODO: test in all LN implementations, not just LND
//foreach (var test in Tester.GetTestedPairs())
//{
var test = Tester.GetTestedLndPair();
await EnsureConnectedToDestinations(test);
var customer = (ILightningClient)test.Customer;
var senderChannel = (await customer.ListChannels()).First();

var senderInfo = await customer.GetInfo();
var fundingTxIdForChannel = ChannelSetup.GetFundingTxIdForChannel(customer, test.Merchant);
if (String.IsNullOrEmpty(fundingTxIdForChannel))
throw new InvalidOperationException("Could not find funding Tx ID of channel");
var closeChannelRequest = new CloseChannelRequest()
{
NodeInfo = senderInfo.NodeInfoList.First(),
ChannelPointOutputIndex = senderChannel.ChannelPoint.N,
ChannelPointFundingTxIdStr = fundingTxIdForChannel
};
await test.Merchant.CloseChannel(closeChannelRequest, CancellationToken.None);
//}
}
}
}
13 changes: 10 additions & 3 deletions tests/Tester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,18 @@ public static LndClient CreateLndClientDest()
yield return ("Eclair (Client)", CreateEclairClient());
}

private static (string Name, ILightningClient Customer, ILightningClient Merchant) GetTestedCLightningPair()
=> ("C-Lightning", CreateCLightningClient(), CreateCLightningClientDest());
internal static (string Name, LndClient Customer, LndClient Merchant) GetTestedLndPair()
=> ("LND", CreateLndClient(), CreateLndClientDest());
private static (string Name, ILightningClient Customer, ILightningClient Merchant) GetTestedEclairPair()
=> ("Eclair", CreateEclairClient(), CreateEclairClientDest());

public static IEnumerable<(string Name, ILightningClient Customer, ILightningClient Merchant)> GetTestedPairs()
{
yield return ("C-Lightning", CreateCLightningClient(), CreateCLightningClientDest());
yield return ("LND", CreateLndClient(), CreateLndClientDest());
yield return ("Eclair", CreateEclairClient(), CreateEclairClientDest());
yield return GetTestedCLightningPair();
yield return GetTestedLndPair();
yield return GetTestedEclairPair();
}
}
}