Can connect directly to CLightning via TCP or UNIX socket

This commit is contained in:
nicolas.dorier 2018-03-21 00:31:19 +09:00
parent 392f3a16f1
commit 6d611d7d05
20 changed files with 612 additions and 116 deletions

View File

@ -1,5 +1,7 @@
using BTCPayServer.Configuration;
using BTCPayServer.Hosting;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Tests.Logging;
@ -118,6 +120,7 @@ namespace BTCPayServer.Tests
.Build();
_Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
((LightningLikePaymentHandler)_Host.Services.GetService(typeof(IPaymentMethodHandler<LightningSupportedPaymentMethod>))).SkipP2PTest = !InContainer;
}
public string HostName
@ -127,6 +130,7 @@ namespace BTCPayServer.Tests
}
public InvoiceRepository InvoiceRepository { get; private set; }
public Uri IntegratedLightning { get; internal set; }
public bool InContainer { get; internal set; }
public T GetService<T>()
{

View File

@ -57,7 +57,9 @@ namespace BTCPayServer.Tests
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
CustomerLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "http://127.0.0.1:30992/")), btc);
CustomerLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "tcp://127.0.0.1:30992/")), btc);
MerchantLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "tcp://127.0.0.1:30993/")), btc);
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "merchant_lightningd", btc);
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
@ -69,6 +71,7 @@ namespace BTCPayServer.Tests
};
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
PayTester.InContainer = bool.Parse(GetEnvironment("TESTS_INCONTAINER", "false"));
PayTester.Start();
}
@ -90,8 +93,10 @@ namespace BTCPayServer.Tests
{
while (true)
{
var skippedStates = new[] { "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
var channel = (await CustomerLightningD.ListPeersAsync())
.SelectMany(p => p.Channels)
.Where(c => !skippedStates.Contains(c.State ?? ""))
.FirstOrDefault();
switch (channel?.State)
{
@ -148,6 +153,7 @@ namespace BTCPayServer.Tests
}
public CLightningRPCClient CustomerLightningD { get; set; }
public CLightningRPCClient MerchantLightningD { get; private set; }
public ChargeTester MerchantCharge { get; private set; }
internal string GetEnvironment(string variable, string defaultValue)

View File

@ -1,4 +1,5 @@
using BTCPayServer.Controllers;
using System.Linq;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Invoices;
@ -11,6 +12,8 @@ using System.Text;
using System.Threading.Tasks;
using Xunit;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Tests
{
@ -112,19 +115,23 @@ namespace BTCPayServer.Tests
get; set;
}
public void RegisterLightningNode(string cryptoCode)
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
{
RegisterLightningNodeAsync(cryptoCode).GetAwaiter().GetResult();
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
}
public async Task RegisterLightningNodeAsync(string cryptoCode)
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
{
var storeController = parent.PayTester.GetController<StoresController>(UserId);
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
{
CryptoCurrency = "BTC",
Url = parent.MerchantCharge.Client.Uri.AbsoluteUri
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :
connectionType == LightningConnectionType.CLightning ? parent.MerchantLightningD.Address.AbsoluteUri
: throw new NotSupportedException(connectionType.ToString())
}, "save");
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
}
}
}

View File

@ -324,6 +324,79 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanParseLightningURL()
{
LightningConnectionString conn = null;
Assert.True(LightningConnectionString.TryParse("/test/a", out conn));
Assert.Equal("unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("unix://test/a", out conn));
Assert.Equal("unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("unix://test/a", out conn));
Assert.Equal("unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("tcp://test/a", out conn));
Assert.Equal("tcp://test/a", conn.ToString());
Assert.Equal("tcp://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("tcp://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("http://aaa:bbb@test/a", out conn));
Assert.Equal("http://aaa:bbb@test/a", conn.ToString());
Assert.Equal("http://aaa:bbb@test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("http://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.Charge, conn.ConnectionType);
Assert.Equal("aaa", conn.Username);
Assert.Equal("bbb", conn.Password);
Assert.False(LightningConnectionString.TryParse("lol://aaa:bbb@test/a", out conn));
Assert.False(LightningConnectionString.TryParse("https://test/a", out conn));
Assert.False(LightningConnectionString.TryParse("unix://dwewoi:dwdwqd@test/a", out conn));
}
[Fact]
public void CanSendLightningPayment2()
{
using (var tester = ServerTester.Create())
{
tester.Start();
tester.PrepareLightning();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.01,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description"
});
tester.SendLightningPayment(invoice);
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("complete", localInvoice.Status);
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
});
}
}
[Fact]
public void CanSendLightningPayment()
{
@ -334,7 +407,7 @@ namespace BTCPayServer.Tests
tester.PrepareLightning();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC");
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()

View File

@ -17,14 +17,19 @@ services:
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_PORT: 80
TESTS_HOSTNAME: tests
TEST_CUSTOMERLIGHTNINGD: http://customer_lightningd:9835/
TEST_MERCHANTLIGHTNINGD: "/etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "/etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
TESTS_INCONTAINER: "true"
expose:
- "80"
links:
- dev
extra_hosts:
- "tests:127.0.0.1"
volumes:
- "customer_lightningd_datadir:/etc/customer_lightningd_datadir"
- "merchant_lightningd_datadir:/etc/merchant_lightningd_datadir"
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
@ -90,6 +95,7 @@ services:
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
network=regtest
ipaddr=customer_lightningd
log-level=debug
ports:
- "30992:9835" # api port
@ -128,6 +134,7 @@ services:
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
ipaddr=merchant_lightningd
network=regtest
log-level=debug
ports:

View File

@ -0,0 +1 @@
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli $args

View File

@ -0,0 +1,3 @@
#!/bin/bash
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli "$@"

View File

@ -8,6 +8,7 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning.CLightning;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Payments.Lightning;
using System.Net;
namespace BTCPayServer.Controllers
{
@ -23,7 +24,7 @@ namespace BTCPayServer.Controllers
return NotFound();
LightningNodeViewModel vm = new LightningNodeViewModel();
vm.SetCryptoCurrencies(_NetworkProvider, selectedCrypto);
vm.InternalLightningNode = GetInternalLightningNodeIfAuthorized();
vm.InternalLightningNode = CanUseInternalLightning() ? _BtcpayServerOptions.InternalLightningNode.AbsoluteUri : null;
return View(vm);
}
@ -36,7 +37,7 @@ namespace BTCPayServer.Controllers
return NotFound();
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
vm.SetCryptoCurrencies(_NetworkProvider, vm.CryptoCurrency);
vm.InternalLightningNode = GetInternalLightningNodeIfAuthorized();
vm.InternalLightningNode = CanUseInternalLightning() ? _BtcpayServerOptions.InternalLightningNode.AbsoluteUri : null;
if (network == null)
{
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
@ -47,41 +48,39 @@ namespace BTCPayServer.Controllers
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
if (!string.IsNullOrEmpty(vm.Url))
{
Uri uri;
if (!Uri.TryCreate(vm.Url, UriKind.Absolute, out uri))
if(!LightningConnectionString.TryParse(vm.Url, out var connectionString, out var error))
{
ModelState.AddModelError(nameof(vm.Url), "Invalid URL");
ModelState.AddModelError(nameof(vm.Url), $"Invalid URL ({error})");
return View(vm);
}
var domain = GetDomain(uri.AbsoluteUri);
if (uri.Scheme != "https" && domain != "127.0.0.1" && domain != "localhost")
var internalDomain = _BtcpayServerOptions.InternalLightningNode.DnsSafeHost;
bool isLocal = (internalDomain == "127.0.0.1" || internalDomain == "localhost");
bool isInternalNode = connectionString.ConnectionType == LightningConnectionType.CLightning ||
connectionString.BaseUri.DnsSafeHost == internalDomain ||
isLocal;
if (connectionString.BaseUri.Scheme == "http" && !isLocal)
{
var internalNode = GetInternalLightningNodeIfAuthorized();
if (internalNode == null || GetDomain(internalNode) != domain)
if (!isInternalNode || (isInternalNode && !CanUseInternalLightning()))
{
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
return View(vm);
}
}
if (!CanUseInternalLightning() && GetDomain(_BtcpayServerOptions.InternalLightningNode.AbsoluteUri) == GetDomain(uri.AbsoluteUri))
if (isInternalNode && !CanUseInternalLightning())
{
ModelState.AddModelError(nameof(vm.Url), "Unauthorized url");
return View(vm);
}
if (string.IsNullOrEmpty(uri.UserInfo) || uri.UserInfo.Split(':').Length != 2)
{
ModelState.AddModelError(nameof(vm.Url), "The url is missing user and password");
return View(vm);
}
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
{
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetLightningChargeUrl(uri);
paymentMethod.SetLightningUrl(connectionString);
}
if (command == "save")
{
@ -112,24 +111,9 @@ namespace BTCPayServer.Controllers
}
}
private string GetInternalLightningNodeIfAuthorized()
{
if (_BtcpayServerOptions.InternalLightningNode != null &&
CanUseInternalLightning())
{
return _BtcpayServerOptions.InternalLightningNode.AbsoluteUri;
}
return null;
}
private bool CanUseInternalLightning()
{
return (_BTCPayEnv.IsDevelopping || User.IsInRole(Roles.ServerAdmin));
}
string GetDomain(string uri)
{
return new UriBuilder(uri).Host;
}
}
}

View File

@ -246,7 +246,7 @@ namespace BTCPayServer.Controllers
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
{
CryptoCode = lightning.CryptoCode,
Address = lightning.GetLightningChargeUrl(false).AbsoluteUri
Address = lightning.GetLightningUrl().BaseUri.AbsoluteUri
});
}
}

View File

@ -5,8 +5,10 @@ using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.Charge;
using Mono.Unix;
using NBitcoin;
using NBitcoin.RPC;
using Newtonsoft.Json;
@ -14,7 +16,14 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class CLightningRPCClient
public class LightningRPCException : Exception
{
public LightningRPCException(string message) : base(message)
{
}
}
public class CLightningRPCClient : ILightningInvoiceClient, ILightningListenInvoiceSession
{
public Network Network { get; private set; }
public Uri Address { get; private set; }
@ -25,13 +34,17 @@ namespace BTCPayServer.Payments.Lightning.CLightning
throw new ArgumentNullException(nameof(address));
if (network == null)
throw new ArgumentNullException(nameof(network));
if(address.Scheme == "file")
{
address = new UriBuilder(address) { Scheme = "unix" }.Uri;
}
Address = address;
Network = network;
}
public Task<GetInfoResponse> GetInfoAsync()
public Task<GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
{
return SendCommandAsync<GetInfoResponse>("getinfo");
return SendCommandAsync<GetInfoResponse>("getinfo", cancellation: cancellation);
}
public Task SendAsync(string bolt11)
@ -60,18 +73,11 @@ namespace BTCPayServer.Payments.Lightning.CLightning
}
static Encoding UTF8 = new UTF8Encoding(false);
private async Task<T> SendCommandAsync<T>(string command, object[] parameters = null, bool noReturn = false, bool isArray = false)
private async Task<T> SendCommandAsync<T>(string command, object[] parameters = null, bool noReturn = false, bool isArray = false, CancellationToken cancellation = default(CancellationToken))
{
parameters = parameters ?? Array.Empty<string>();
var domain = Address.DnsSafeHost;
if (!IPAddress.TryParse(domain, out IPAddress address))
using (Socket socket = await Connect())
{
address = (await Dns.GetHostAddressesAsync(domain)).FirstOrDefault();
if (address == null)
throw new Exception("Host not found");
}
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(new IPEndPoint(address, Address.Port));
using (var networkStream = new NetworkStream(socket))
{
using (var textWriter = new StreamWriter(networkStream, UTF8, 1024 * 10, true))
@ -82,21 +88,26 @@ namespace BTCPayServer.Payments.Lightning.CLightning
req.Add("id", 0);
req.Add("method", command);
req.Add("params", new JArray(parameters));
await req.WriteToAsync(jsonWriter);
await jsonWriter.FlushAsync();
await req.WriteToAsync(jsonWriter, cancellation);
await jsonWriter.FlushAsync(cancellation);
}
await textWriter.FlushAsync();
}
await networkStream.FlushAsync();
await networkStream.FlushAsync(cancellation);
using (var textReader = new StreamReader(networkStream, UTF8, false, 1024 * 10, true))
{
using (var jsonReader = new JsonTextReader(textReader))
{
var result = await JObject.LoadAsync(jsonReader);
var resultAsync = JObject.LoadAsync(jsonReader, cancellation);
// without this hack resultAsync is blocking even if cancellation happen
using (cancellation.Register(() => { socket.Dispose(); }))
{
var result = await resultAsync;
var error = result.Property("error");
if (error != null)
{
throw new Exception(error.Value.ToString());
throw new LightningRPCException(error.Value["message"].Value<string>());
}
if (noReturn)
return default(T);
@ -109,11 +120,111 @@ namespace BTCPayServer.Payments.Lightning.CLightning
}
}
}
}
}
private async Task<Socket> Connect()
{
Socket socket = null;
EndPoint endpoint = null;
if (Address.Scheme == "tcp" || Address.Scheme == "tcp")
{
var domain = Address.DnsSafeHost;
if (!IPAddress.TryParse(domain, out IPAddress address))
{
address = (await Dns.GetHostAddressesAsync(domain)).FirstOrDefault();
if (address == null)
throw new Exception("Host not found");
}
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
endpoint = new IPEndPoint(address, Address.Port);
}
else if (Address.Scheme == "unix")
{
var path = Address.AbsoluteUri.Remove(0, "unix:".Length);
if (!path.StartsWith('/'))
path = "/" + path;
while (path.Length >= 2 && (path[0] != '/' || path[1] == '/'))
{
path = path.Remove(0, 1);
}
if (path.Length < 2)
throw new FormatException("Invalid unix url");
socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
endpoint = new UnixEndPoint(path);
}
else
throw new NotSupportedException($"Protocol {Address.Scheme} for clightning not supported");
await socket.ConnectAsync(endpoint);
return socket;
}
public async Task<BitcoinAddress> NewAddressAsync()
{
var obj = await SendCommandAsync<JObject>("newaddr");
return BitcoinAddress.Create(obj.Property("address").Value.Value<string>(), Network);
}
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
{
var invoices = await SendCommandAsync<ChargeInvoice[]>("listinvoices", new[] { invoiceId }, false, true, cancellation);
if (invoices.Length == 0)
return null;
return ChargeClient.ToLightningInvoice(invoices[0]);
}
static NBitcoin.DataEncoders.DataEncoder InvoiceIdEncoder = NBitcoin.DataEncoders.Encoders.Base58;
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation)
{
var id = InvoiceIdEncoder.EncodeData(RandomUtils.GetBytes(20));
var invoice = await SendCommandAsync<CreateInvoiceResponse>("invoice", new object[] { amount.MilliSatoshi, id, "" }, cancellation: cancellation);
invoice.Label = id;
invoice.MilliSatoshi = amount;
invoice.Status = "unpaid";
return ToLightningInvoice(invoice);
}
private static LightningInvoice ToLightningInvoice(CreateInvoiceResponse invoice)
{
return new LightningInvoice()
{
Id = invoice.Label,
Amount = invoice.MilliSatoshi,
BOLT11 = invoice.BOLT11,
Status = invoice.Status,
PaidAt = invoice.PaidAt
};
}
Task<ILightningListenInvoiceSession> ILightningInvoiceClient.Listen(CancellationToken cancellation)
{
return Task.FromResult<ILightningListenInvoiceSession>(this);
}
long lastInvoiceIndex = 99999999999;
async Task<LightningInvoice> ILightningListenInvoiceSession.WaitInvoice(CancellationToken cancellation)
{
var chargeInvoice = await SendCommandAsync<CreateInvoiceResponse>("waitanyinvoice", new object[] { lastInvoiceIndex }, cancellation: cancellation);
lastInvoiceIndex = chargeInvoice.PayIndex.Value;
return ToLightningInvoice(chargeInvoice);
}
async Task<LightningNodeInformation> ILightningInvoiceClient.GetInfo(CancellationToken cancellation)
{
var info = await GetInfoAsync(cancellation);
var address = info.Address.Select(a => a.Address).FirstOrDefault();
var port = info.Port;
return new LightningNodeInformation()
{
P2PPort = port,
Address = address,
BlockHeight = info.BlockHeight
};
}
void IDisposable.Dispose()
{
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class CreateInvoiceResponse
{
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
[JsonProperty("payment_hash")]
public uint256 PaymentHash { get; set; }
[JsonProperty("msatoshi")]
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
public LightMoney MilliSatoshi { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
[JsonProperty("expiry_time")]
public DateTimeOffset ExpiryTime { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
[JsonProperty("expires_at")]
public DateTimeOffset ExpiryAt { get; set; }
[JsonProperty("bolt11")]
public string BOLT11 { get; set; }
[JsonProperty("pay_index")]
public int? PayIndex { get; set; }
public string Label { get; set; }
public string Status { get; set; }
[JsonProperty("paid_at")]
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? PaidAt { get; set; }
}
}

View File

@ -0,0 +1,140 @@
//
// Mono.Unix.UnixEndPoint: EndPoint derived class for AF_UNIX family sockets.
//
// Authors:
// Gonzalo Paniagua Javier (gonzalo@ximian.com)
//
// (C) 2003 Ximian, Inc (http://www.ximian.com)
//
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Mono.Unix
{
[Serializable]
public class UnixEndPoint : EndPoint
{
string filename;
public UnixEndPoint(string filename)
{
if (filename == null)
throw new ArgumentNullException("filename");
if (filename.Length == 0)
throw new ArgumentException("Cannot be empty.", "filename");
this.filename = filename;
}
public string Filename
{
get
{
return (filename);
}
set
{
filename = value;
}
}
public override AddressFamily AddressFamily
{
get { return AddressFamily.Unix; }
}
public override EndPoint Create(SocketAddress socketAddress)
{
/*
* Should also check this
*
int addr = (int) AddressFamily.Unix;
if (socketAddress [0] != (addr & 0xFF))
throw new ArgumentException ("socketAddress is not a unix socket address.");
if (socketAddress [1] != ((addr & 0xFF00) >> 8))
throw new ArgumentException ("socketAddress is not a unix socket address.");
*/
if (socketAddress.Size == 2)
{
// Empty filename.
// Probably from RemoteEndPoint which on linux does not return the file name.
UnixEndPoint uep = new UnixEndPoint("a");
uep.filename = "";
return uep;
}
int size = socketAddress.Size - 2;
byte[] bytes = new byte[size];
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = socketAddress[i + 2];
// There may be junk after the null terminator, so ignore it all.
if (bytes[i] == 0)
{
size = i;
break;
}
}
string name = Encoding.Default.GetString(bytes, 0, size);
return new UnixEndPoint(name);
}
public override SocketAddress Serialize()
{
byte[] bytes = Encoding.Default.GetBytes(filename);
SocketAddress sa = new SocketAddress(AddressFamily, 2 + bytes.Length + 1);
// sa [0] -> family low byte, sa [1] -> family high byte
for (int i = 0; i < bytes.Length; i++)
sa[2 + i] = bytes[i];
//NULL suffix for non-abstract path
sa[2 + bytes.Length] = 0;
return sa;
}
public override string ToString()
{
return (filename);
}
public override int GetHashCode()
{
return filename.GetHashCode(StringComparison.Ordinal);
}
public override bool Equals(object o)
{
UnixEndPoint other = o as UnixEndPoint;
if (other == null)
return false;
return (other.filename == filename);
}
}
}

View File

@ -142,7 +142,7 @@ namespace BTCPayServer.Payments.Lightning.Charge
{
return new LightningInvoice()
{
Id = invoice.Id,
Id = invoice.Id ?? invoice.Label,
Amount = invoice.MilliSatoshi,
BOLT11 = invoice.PaymentRequest,
PaidAt = invoice.PaidAt,
@ -161,7 +161,6 @@ namespace BTCPayServer.Payments.Lightning.Charge
var info = await GetInfoAsync(cancellation);
var address = info.Address.Select(a => a.Address).FirstOrDefault();
var port = info.Port;
address = address ?? Uri.DnsSafeHost;
return new LightningNodeInformation()
{
P2PPort = port,

View File

@ -27,6 +27,7 @@ namespace BTCPayServer.Payments.Lightning.Charge
[JsonProperty("payreq")]
public string PaymentRequest { get; set; }
public string Label { get; set; }
}
public class ChargeSession : ILightningListenInvoiceSession
{

View File

@ -35,6 +35,6 @@ namespace BTCPayServer.Payments.Lightning
public interface ILightningListenInvoiceSession : IDisposable
{
Task<LightningInvoice> WaitInvoice(CancellationToken token);
Task<LightningInvoice> WaitInvoice(CancellationToken cancellation);
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
namespace BTCPayServer.Payments.Lightning
{
@ -10,7 +11,17 @@ namespace BTCPayServer.Payments.Lightning
{
public ILightningInvoiceClient CreateClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
return new ChargeClient(supportedPaymentMethod.GetLightningChargeUrl(true), network.NBitcoinNetwork);
var uri = supportedPaymentMethod.GetLightningUrl();
if (uri.ConnectionType == LightningConnectionType.Charge)
{
return new ChargeClient(uri.ToUri(true), network.NBitcoinNetwork);
}
else if (uri.ConnectionType == LightningConnectionType.CLightning)
{
return new CLightningRPCClient(uri.ToUri(false), network.NBitcoinNetwork);
}
else
throw new NotSupportedException($"Unsupported connection string for lightning server ({uri.ConnectionType})");
}
}
}

View File

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning
{
public enum LightningConnectionType
{
Charge,
CLightning
}
public class LightningConnectionString
{
public static bool TryParse(string str, out LightningConnectionString connectionString)
{
return TryParse(str, out connectionString, out var error);
}
public static bool TryParse(string str, out LightningConnectionString connectionString, out string error)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
if (str.StartsWith('/'))
str = "unix:" + str;
var result = new LightningConnectionString();
connectionString = null;
error = null;
Uri uri;
if (!System.Uri.TryCreate(str, UriKind.Absolute, out uri))
{
error = "Invalid URL";
return false;
}
var supportedDomains = new string[] { "unix", "tcp", "http", "https" };
if (!supportedDomains.Contains(uri.Scheme))
{
var protocols = String.Join(",", supportedDomains);
error = $"The url support the following protocols {protocols}";
return false;
}
if (uri.Scheme == "unix")
{
str = uri.AbsoluteUri.Substring("unix:".Length);
while (str.Length >= 1 && str[0] == '/')
{
str = str.Substring(1);
}
uri = new Uri("unix://" + str, UriKind.Absolute);
}
if (uri.Scheme == "http" || uri.Scheme == "https")
{
var parts = uri.UserInfo.Split(':');
if (string.IsNullOrEmpty(uri.UserInfo) || parts.Length != 2)
{
error = "The url is missing user and password";
return false;
}
result.Username = parts[0];
result.Password = parts[1];
}
else if (!string.IsNullOrEmpty(uri.UserInfo))
{
error = "The url should not have user information";
return false;
}
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
connectionString = result;
return true;
}
public LightningConnectionString()
{
}
public string Username { get; set; }
public string Password { get; set; }
public Uri BaseUri { get; set; }
public LightningConnectionType ConnectionType
{
get
{
return BaseUri.Scheme == "http" || BaseUri.Scheme == "https" ? LightningConnectionType.Charge
: LightningConnectionType.CLightning;
}
}
public Uri ToUri(bool withCredentials)
{
if (withCredentials)
{
return new UriBuilder(BaseUri) { UserName = Username ?? "", Password = Password ?? "" }.Uri;
}
else
{
return BaseUri;
}
}
public override string ToString()
{
return ToUri(true).AbsoluteUri;
}
}
}

View File

@ -49,18 +49,21 @@ namespace BTCPayServer.Payments.Lightning
catch { return false; }
}
/// <summary>
/// Used for testing
/// </summary>
public bool SkipP2PTest { get; set; }
public async Task Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"Full node not available");
var cts = new CancellationTokenSource(5000);
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
LightningNodeInformation info = null;
try
{
info = await client.GetInfo(cts.Token);
}
catch (Exception ex)
@ -68,6 +71,11 @@ namespace BTCPayServer.Payments.Lightning
throw new Exception($"Error while connecting to the API ({ex.Message})");
}
if(info.Address == null)
{
throw new Exception($"No lightning node public address has been configured");
}
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
if (blocksGap > 10)
{
@ -76,6 +84,7 @@ namespace BTCPayServer.Payments.Lightning
try
{
if(!SkipP2PTest)
await TestConnection(info.Address, info.P2PPort, cts.Token);
}
catch (Exception ex)

View File

@ -79,7 +79,7 @@ namespace BTCPayServer.Payments.Lightning
var listenedInvoice = new ListenedInvoice()
{
Uri = lightningSupportedMethod.GetLightningChargeUrl(false).AbsoluteUri,
Uri = lightningSupportedMethod.GetLightningUrl().BaseUri.AbsoluteUri,
PaymentMethodDetails = lightningMethod,
SupportedPaymentMethod = lightningSupportedMethod,
PaymentMethod = paymentMethod,
@ -125,7 +125,7 @@ namespace BTCPayServer.Payments.Lightning
{
try
{
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
var charge = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
var session = await charge.Listen(_Cts.Token);
while (true)
@ -134,7 +134,6 @@ namespace BTCPayServer.Payments.Lightning
ListenedInvoice listenedInvoice = GetListenedInvoice(notification.Id);
if (listenedInvoice == null)
continue;
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11)
{
@ -157,10 +156,10 @@ namespace BTCPayServer.Payments.Lightning
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningChargeUrl(false)}");
DoneListening(supportedPaymentMethod.GetLightningChargeUrl(false));
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningUrl().BaseUri}");
DoneListening(supportedPaymentMethod.GetLightningUrl());
}
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
}
private async Task AddPayment(BTCPayNetwork network, LightningInvoice notification, ListenedInvoice listenedInvoice)
@ -204,8 +203,9 @@ namespace BTCPayServer.Payments.Lightning
/// Stop listening all invoices on this server
/// </summary>
/// <param name="uri"></param>
private void DoneListening(Uri uri)
private void DoneListening(LightningConnectionString connectionString)
{
var uri = connectionString.BaseUri;
lock (_ListenedInvoiceByChargeInvoiceId)
{
foreach (var listenedInvoice in _ListenedInvoiceByLightningUrl[uri.AbsoluteUri])

View File

@ -8,41 +8,36 @@ namespace BTCPayServer.Payments.Lightning
public class LightningSupportedPaymentMethod : ISupportedPaymentMethod
{
public string CryptoCode { get; set; }
[Obsolete("Use Get/SetLightningChargeUrl")]
[Obsolete("Use Get/SetLightningUrl")]
public string LightningChargeUrl { get; set; }
public Uri GetLightningChargeUrl(bool withCredentials)
public LightningConnectionString GetLightningUrl()
{
#pragma warning disable CS0618 // Type or member is obsolete
UriBuilder uri = new UriBuilder(LightningChargeUrl);
if (withCredentials)
var fullUri = new UriBuilder(LightningChargeUrl) { UserName = Username, Password = Password }.Uri.AbsoluteUri;
if(!LightningConnectionString.TryParse(fullUri, out var connectionString, out var error))
{
uri.UserName = Username;
uri.Password = Password;
throw new FormatException(error);
}
#pragma warning restore CS0618 // Type or member is obsolete
return uri.Uri;
}
public void SetLightningChargeUrl(Uri uri)
{
if (uri == null)
throw new ArgumentNullException(nameof(uri));
if (string.IsNullOrEmpty(uri.UserInfo))
throw new ArgumentException(paramName: nameof(uri), message: "Uri should have credential information");
var splitted = uri.UserInfo.Split(':');
if (splitted.Length != 2)
throw new ArgumentException(paramName: nameof(uri), message: "Uri should have credential information");
#pragma warning disable CS0618 // Type or member is obsolete
Username = splitted[0];
Password = splitted[1];
LightningChargeUrl = new UriBuilder(uri) { UserName = "", Password = "" }.Uri.AbsoluteUri;
return connectionString;
#pragma warning restore CS0618 // Type or member is obsolete
}
[Obsolete("Use Get/SetLightningChargeUrl")]
public void SetLightningUrl(LightningConnectionString connectionString)
{
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
#pragma warning disable CS0618 // Type or member is obsolete
Username = connectionString.Username;
Password = connectionString.Password;
LightningChargeUrl = connectionString.BaseUri.AbsoluteUri;
#pragma warning restore CS0618 // Type or member is obsolete
}
[Obsolete("Use Get/SetLightningUrl")]
public string Username { get; set; }
[Obsolete("Use Get/SetLightningChargeUrl")]
[Obsolete("Use Get/SetLightningUrl")]
public string Password { get; set; }
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
}