mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 22:44:29 +01:00
Can connect directly to CLightning via TCP or UNIX socket
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using BTCPayServer.Hosting;
|
using BTCPayServer.Hosting;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Tests.Logging;
|
using BTCPayServer.Tests.Logging;
|
||||||
@@ -118,6 +120,7 @@ namespace BTCPayServer.Tests
|
|||||||
.Build();
|
.Build();
|
||||||
_Host.Start();
|
_Host.Start();
|
||||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
||||||
|
((LightningLikePaymentHandler)_Host.Services.GetService(typeof(IPaymentMethodHandler<LightningSupportedPaymentMethod>))).SkipP2PTest = !InContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string HostName
|
public string HostName
|
||||||
@@ -127,6 +130,7 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
public InvoiceRepository InvoiceRepository { get; private set; }
|
public InvoiceRepository InvoiceRepository { get; private set; }
|
||||||
public Uri IntegratedLightning { get; internal set; }
|
public Uri IntegratedLightning { get; internal set; }
|
||||||
|
public bool InContainer { get; internal set; }
|
||||||
|
|
||||||
public T GetService<T>()
|
public T GetService<T>()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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/")));
|
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||||
|
|
||||||
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
|
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);
|
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"))
|
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.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
|
||||||
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
|
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
|
||||||
|
PayTester.InContainer = bool.Parse(GetEnvironment("TESTS_INCONTAINER", "false"));
|
||||||
PayTester.Start();
|
PayTester.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,8 +93,10 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
var skippedStates = new[] { "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
|
||||||
var channel = (await CustomerLightningD.ListPeersAsync())
|
var channel = (await CustomerLightningD.ListPeersAsync())
|
||||||
.SelectMany(p => p.Channels)
|
.SelectMany(p => p.Channels)
|
||||||
|
.Where(c => !skippedStates.Contains(c.State ?? ""))
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
switch (channel?.State)
|
switch (channel?.State)
|
||||||
{
|
{
|
||||||
@@ -148,6 +153,7 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CLightningRPCClient CustomerLightningD { get; set; }
|
public CLightningRPCClient CustomerLightningD { get; set; }
|
||||||
|
public CLightningRPCClient MerchantLightningD { get; private set; }
|
||||||
public ChargeTester MerchantCharge { get; private set; }
|
public ChargeTester MerchantCharge { get; private set; }
|
||||||
|
|
||||||
internal string GetEnvironment(string variable, string defaultValue)
|
internal string GetEnvironment(string variable, string defaultValue)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
|
using System.Linq;
|
||||||
using BTCPayServer.Models.AccountViewModels;
|
using BTCPayServer.Models.AccountViewModels;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
@@ -11,6 +12,8 @@ using System.Text;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
@@ -111,20 +114,24 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
get; set;
|
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);
|
var storeController = parent.PayTester.GetController<StoresController>(UserId);
|
||||||
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
|
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
|
||||||
{
|
{
|
||||||
CryptoCurrency = "BTC",
|
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");
|
}, "save");
|
||||||
|
if (storeController.ModelState.ErrorCount != 0)
|
||||||
|
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
[Fact]
|
||||||
public void CanSendLightningPayment()
|
public void CanSendLightningPayment()
|
||||||
{
|
{
|
||||||
@@ -334,7 +407,7 @@ namespace BTCPayServer.Tests
|
|||||||
tester.PrepareLightning();
|
tester.PrepareLightning();
|
||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
user.GrantAccess();
|
user.GrantAccess();
|
||||||
user.RegisterLightningNode("BTC");
|
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
|
||||||
user.RegisterDerivationScheme("BTC");
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
|
||||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||||
|
|||||||
@@ -17,14 +17,19 @@ services:
|
|||||||
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
|
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
|
||||||
TESTS_PORT: 80
|
TESTS_PORT: 80
|
||||||
TESTS_HOSTNAME: tests
|
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/
|
TEST_MERCHANTCHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
|
||||||
|
TESTS_INCONTAINER: "true"
|
||||||
expose:
|
expose:
|
||||||
- "80"
|
- "80"
|
||||||
links:
|
links:
|
||||||
- dev
|
- dev
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "tests:127.0.0.1"
|
- "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
|
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
|
||||||
dev:
|
dev:
|
||||||
@@ -90,6 +95,7 @@ services:
|
|||||||
bitcoin-datadir=/etc/bitcoin
|
bitcoin-datadir=/etc/bitcoin
|
||||||
bitcoin-rpcconnect=bitcoind
|
bitcoin-rpcconnect=bitcoind
|
||||||
network=regtest
|
network=regtest
|
||||||
|
ipaddr=customer_lightningd
|
||||||
log-level=debug
|
log-level=debug
|
||||||
ports:
|
ports:
|
||||||
- "30992:9835" # api port
|
- "30992:9835" # api port
|
||||||
@@ -128,6 +134,7 @@ services:
|
|||||||
LIGHTNINGD_OPT: |
|
LIGHTNINGD_OPT: |
|
||||||
bitcoin-datadir=/etc/bitcoin
|
bitcoin-datadir=/etc/bitcoin
|
||||||
bitcoin-rpcconnect=bitcoind
|
bitcoin-rpcconnect=bitcoind
|
||||||
|
ipaddr=merchant_lightningd
|
||||||
network=regtest
|
network=regtest
|
||||||
log-level=debug
|
log-level=debug
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
1
BTCPayServer.Tests/docker-merchant-lightning-cli.ps1
Executable file
1
BTCPayServer.Tests/docker-merchant-lightning-cli.ps1
Executable file
@@ -0,0 +1 @@
|
|||||||
|
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli $args
|
||||||
3
BTCPayServer.Tests/docker-merchant-lightning-cli.sh
Executable file
3
BTCPayServer.Tests/docker-merchant-lightning-cli.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli "$@"
|
||||||
@@ -8,6 +8,7 @@ using BTCPayServer.Payments;
|
|||||||
using BTCPayServer.Payments.Lightning.CLightning;
|
using BTCPayServer.Payments.Lightning.CLightning;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
@@ -23,7 +24,7 @@ namespace BTCPayServer.Controllers
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
LightningNodeViewModel vm = new LightningNodeViewModel();
|
LightningNodeViewModel vm = new LightningNodeViewModel();
|
||||||
vm.SetCryptoCurrencies(_NetworkProvider, selectedCrypto);
|
vm.SetCryptoCurrencies(_NetworkProvider, selectedCrypto);
|
||||||
vm.InternalLightningNode = GetInternalLightningNodeIfAuthorized();
|
vm.InternalLightningNode = CanUseInternalLightning() ? _BtcpayServerOptions.InternalLightningNode.AbsoluteUri : null;
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ namespace BTCPayServer.Controllers
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
|
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
|
||||||
vm.SetCryptoCurrencies(_NetworkProvider, vm.CryptoCurrency);
|
vm.SetCryptoCurrencies(_NetworkProvider, vm.CryptoCurrency);
|
||||||
vm.InternalLightningNode = GetInternalLightningNodeIfAuthorized();
|
vm.InternalLightningNode = CanUseInternalLightning() ? _BtcpayServerOptions.InternalLightningNode.AbsoluteUri : null;
|
||||||
if (network == null)
|
if (network == null)
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
|
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
|
||||||
@@ -47,41 +48,39 @@ namespace BTCPayServer.Controllers
|
|||||||
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
|
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
|
||||||
if (!string.IsNullOrEmpty(vm.Url))
|
if (!string.IsNullOrEmpty(vm.Url))
|
||||||
{
|
{
|
||||||
Uri uri;
|
if(!LightningConnectionString.TryParse(vm.Url, out var connectionString, out var error))
|
||||||
if (!Uri.TryCreate(vm.Url, UriKind.Absolute, out uri))
|
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(vm.Url), "Invalid URL");
|
ModelState.AddModelError(nameof(vm.Url), $"Invalid URL ({error})");
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
var domain = GetDomain(uri.AbsoluteUri);
|
var internalDomain = _BtcpayServerOptions.InternalLightningNode.DnsSafeHost;
|
||||||
if (uri.Scheme != "https" && domain != "127.0.0.1" && domain != "localhost")
|
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 (!isInternalNode || (isInternalNode && !CanUseInternalLightning()))
|
||||||
if (internalNode == null || GetDomain(internalNode) != domain)
|
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
|
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!CanUseInternalLightning() && GetDomain(_BtcpayServerOptions.InternalLightningNode.AbsoluteUri) == GetDomain(uri.AbsoluteUri))
|
if (isInternalNode && !CanUseInternalLightning())
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(vm.Url), "Unauthorized url");
|
ModelState.AddModelError(nameof(vm.Url), "Unauthorized url");
|
||||||
return View(vm);
|
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()
|
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
|
||||||
{
|
{
|
||||||
CryptoCode = paymentMethodId.CryptoCode
|
CryptoCode = paymentMethodId.CryptoCode
|
||||||
};
|
};
|
||||||
paymentMethod.SetLightningChargeUrl(uri);
|
paymentMethod.SetLightningUrl(connectionString);
|
||||||
}
|
}
|
||||||
if (command == "save")
|
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()
|
private bool CanUseInternalLightning()
|
||||||
{
|
{
|
||||||
return (_BTCPayEnv.IsDevelopping || User.IsInRole(Roles.ServerAdmin));
|
return (_BTCPayEnv.IsDevelopping || User.IsInRole(Roles.ServerAdmin));
|
||||||
}
|
}
|
||||||
|
|
||||||
string GetDomain(string uri)
|
|
||||||
{
|
|
||||||
return new UriBuilder(uri).Host;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ namespace BTCPayServer.Controllers
|
|||||||
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
|
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
|
||||||
{
|
{
|
||||||
CryptoCode = lightning.CryptoCode,
|
CryptoCode = lightning.CryptoCode,
|
||||||
Address = lightning.GetLightningChargeUrl(false).AbsoluteUri
|
Address = lightning.GetLightningUrl().BaseUri.AbsoluteUri
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ using System.Linq;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Payments.Lightning.Charge;
|
using BTCPayServer.Payments.Lightning.Charge;
|
||||||
|
using Mono.Unix;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.RPC;
|
using NBitcoin.RPC;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -14,7 +16,14 @@ using Newtonsoft.Json.Linq;
|
|||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
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 Network Network { get; private set; }
|
||||||
public Uri Address { get; private set; }
|
public Uri Address { get; private set; }
|
||||||
@@ -25,13 +34,17 @@ namespace BTCPayServer.Payments.Lightning.CLightning
|
|||||||
throw new ArgumentNullException(nameof(address));
|
throw new ArgumentNullException(nameof(address));
|
||||||
if (network == null)
|
if (network == null)
|
||||||
throw new ArgumentNullException(nameof(network));
|
throw new ArgumentNullException(nameof(network));
|
||||||
|
if(address.Scheme == "file")
|
||||||
|
{
|
||||||
|
address = new UriBuilder(address) { Scheme = "unix" }.Uri;
|
||||||
|
}
|
||||||
Address = address;
|
Address = address;
|
||||||
Network = network;
|
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)
|
public Task SendAsync(string bolt11)
|
||||||
@@ -42,7 +55,7 @@ namespace BTCPayServer.Payments.Lightning.CLightning
|
|||||||
public async Task<PeerInfo[]> ListPeersAsync()
|
public async Task<PeerInfo[]> ListPeersAsync()
|
||||||
{
|
{
|
||||||
var peers = await SendCommandAsync<PeerInfo[]>("listpeers", isArray: true);
|
var peers = await SendCommandAsync<PeerInfo[]>("listpeers", isArray: true);
|
||||||
foreach(var peer in peers)
|
foreach (var peer in peers)
|
||||||
{
|
{
|
||||||
peer.Channels = peer.Channels ?? Array.Empty<ChannelInfo>();
|
peer.Channels = peer.Channels ?? Array.Empty<ChannelInfo>();
|
||||||
}
|
}
|
||||||
@@ -60,60 +73,158 @@ namespace BTCPayServer.Payments.Lightning.CLightning
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Encoding UTF8 = new UTF8Encoding(false);
|
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>();
|
parameters = parameters ?? Array.Empty<string>();
|
||||||
var domain = Address.DnsSafeHost;
|
using (Socket socket = await Connect())
|
||||||
if (!IPAddress.TryParse(domain, out IPAddress address))
|
|
||||||
{
|
{
|
||||||
address = (await Dns.GetHostAddressesAsync(domain)).FirstOrDefault();
|
using (var networkStream = new NetworkStream(socket))
|
||||||
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))
|
|
||||||
{
|
{
|
||||||
using (var jsonWriter = new JsonTextWriter(textWriter))
|
using (var textWriter = new StreamWriter(networkStream, UTF8, 1024 * 10, true))
|
||||||
{
|
{
|
||||||
var req = new JObject();
|
using (var jsonWriter = new JsonTextWriter(textWriter))
|
||||||
req.Add("id", 0);
|
{
|
||||||
req.Add("method", command);
|
var req = new JObject();
|
||||||
req.Add("params", new JArray(parameters));
|
req.Add("id", 0);
|
||||||
await req.WriteToAsync(jsonWriter);
|
req.Add("method", command);
|
||||||
await jsonWriter.FlushAsync();
|
req.Add("params", new JArray(parameters));
|
||||||
|
await req.WriteToAsync(jsonWriter, cancellation);
|
||||||
|
await jsonWriter.FlushAsync(cancellation);
|
||||||
|
}
|
||||||
|
await textWriter.FlushAsync();
|
||||||
}
|
}
|
||||||
await textWriter.FlushAsync();
|
await networkStream.FlushAsync(cancellation);
|
||||||
}
|
using (var textReader = new StreamReader(networkStream, UTF8, false, 1024 * 10, true))
|
||||||
await networkStream.FlushAsync();
|
|
||||||
using (var textReader = new StreamReader(networkStream, UTF8, false, 1024 * 10, true))
|
|
||||||
{
|
|
||||||
using (var jsonReader = new JsonTextReader(textReader))
|
|
||||||
{
|
{
|
||||||
var result = await JObject.LoadAsync(jsonReader);
|
using (var jsonReader = new JsonTextReader(textReader))
|
||||||
var error = result.Property("error");
|
|
||||||
if(error != null)
|
|
||||||
{
|
{
|
||||||
throw new Exception(error.Value.ToString());
|
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 LightningRPCException(error.Value["message"].Value<string>());
|
||||||
|
}
|
||||||
|
if (noReturn)
|
||||||
|
return default(T);
|
||||||
|
if (isArray)
|
||||||
|
{
|
||||||
|
return result["result"].Children().First().Children().First().ToObject<T>();
|
||||||
|
}
|
||||||
|
return result["result"].ToObject<T>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (noReturn)
|
|
||||||
return default(T);
|
|
||||||
if (isArray)
|
|
||||||
{
|
|
||||||
return result["result"].Children().First().Children().First().ToObject<T>();
|
|
||||||
}
|
|
||||||
return result["result"].ToObject<T>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
public async Task<BitcoinAddress> NewAddressAsync()
|
||||||
{
|
{
|
||||||
var obj = await SendCommandAsync<JObject>("newaddr");
|
var obj = await SendCommandAsync<JObject>("newaddr");
|
||||||
return BitcoinAddress.Create(obj.Property("address").Value.Value<string>(), Network);
|
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()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
140
BTCPayServer/Payments/Lightning/CLightning/UnixEndPoint.cs
Normal file
140
BTCPayServer/Payments/Lightning/CLightning/UnixEndPoint.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -142,7 +142,7 @@ namespace BTCPayServer.Payments.Lightning.Charge
|
|||||||
{
|
{
|
||||||
return new LightningInvoice()
|
return new LightningInvoice()
|
||||||
{
|
{
|
||||||
Id = invoice.Id,
|
Id = invoice.Id ?? invoice.Label,
|
||||||
Amount = invoice.MilliSatoshi,
|
Amount = invoice.MilliSatoshi,
|
||||||
BOLT11 = invoice.PaymentRequest,
|
BOLT11 = invoice.PaymentRequest,
|
||||||
PaidAt = invoice.PaidAt,
|
PaidAt = invoice.PaidAt,
|
||||||
@@ -161,7 +161,6 @@ namespace BTCPayServer.Payments.Lightning.Charge
|
|||||||
var info = await GetInfoAsync(cancellation);
|
var info = await GetInfoAsync(cancellation);
|
||||||
var address = info.Address.Select(a => a.Address).FirstOrDefault();
|
var address = info.Address.Select(a => a.Address).FirstOrDefault();
|
||||||
var port = info.Port;
|
var port = info.Port;
|
||||||
address = address ?? Uri.DnsSafeHost;
|
|
||||||
return new LightningNodeInformation()
|
return new LightningNodeInformation()
|
||||||
{
|
{
|
||||||
P2PPort = port,
|
P2PPort = port,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ namespace BTCPayServer.Payments.Lightning.Charge
|
|||||||
|
|
||||||
[JsonProperty("payreq")]
|
[JsonProperty("payreq")]
|
||||||
public string PaymentRequest { get; set; }
|
public string PaymentRequest { get; set; }
|
||||||
|
public string Label { get; set; }
|
||||||
}
|
}
|
||||||
public class ChargeSession : ILightningListenInvoiceSession
|
public class ChargeSession : ILightningListenInvoiceSession
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,6 +35,6 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
|
|
||||||
public interface ILightningListenInvoiceSession : IDisposable
|
public interface ILightningListenInvoiceSession : IDisposable
|
||||||
{
|
{
|
||||||
Task<LightningInvoice> WaitInvoice(CancellationToken token);
|
Task<LightningInvoice> WaitInvoice(CancellationToken cancellation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Payments.Lightning.Charge;
|
using BTCPayServer.Payments.Lightning.Charge;
|
||||||
|
using BTCPayServer.Payments.Lightning.CLightning;
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning
|
namespace BTCPayServer.Payments.Lightning
|
||||||
{
|
{
|
||||||
@@ -10,7 +11,17 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
{
|
{
|
||||||
public ILightningInvoiceClient CreateClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
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})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
110
BTCPayServer/Payments/Lightning/LightningConnectionString.cs
Normal file
110
BTCPayServer/Payments/Lightning/LightningConnectionString.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,18 +49,21 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
catch { return false; }
|
catch { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used for testing
|
||||||
|
/// </summary>
|
||||||
|
public bool SkipP2PTest { get; set; }
|
||||||
|
|
||||||
public async Task Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
public async Task Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||||
{
|
{
|
||||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||||
throw new Exception($"Full node not available");
|
throw new Exception($"Full node not available");
|
||||||
|
|
||||||
|
|
||||||
var cts = new CancellationTokenSource(5000);
|
var cts = new CancellationTokenSource(5000);
|
||||||
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||||
LightningNodeInformation info = null;
|
LightningNodeInformation info = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
||||||
info = await client.GetInfo(cts.Token);
|
info = await client.GetInfo(cts.Token);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -68,6 +71,11 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
throw new Exception($"Error while connecting to the API ({ex.Message})");
|
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);
|
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
|
||||||
if (blocksGap > 10)
|
if (blocksGap > 10)
|
||||||
{
|
{
|
||||||
@@ -76,7 +84,8 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await TestConnection(info.Address, info.P2PPort, cts.Token);
|
if(!SkipP2PTest)
|
||||||
|
await TestConnection(info.Address, info.P2PPort, cts.Token);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
|
|
||||||
var listenedInvoice = new ListenedInvoice()
|
var listenedInvoice = new ListenedInvoice()
|
||||||
{
|
{
|
||||||
Uri = lightningSupportedMethod.GetLightningChargeUrl(false).AbsoluteUri,
|
Uri = lightningSupportedMethod.GetLightningUrl().BaseUri.AbsoluteUri,
|
||||||
PaymentMethodDetails = lightningMethod,
|
PaymentMethodDetails = lightningMethod,
|
||||||
SupportedPaymentMethod = lightningSupportedMethod,
|
SupportedPaymentMethod = lightningSupportedMethod,
|
||||||
PaymentMethod = paymentMethod,
|
PaymentMethod = paymentMethod,
|
||||||
@@ -125,7 +125,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
{
|
{
|
||||||
try
|
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 charge = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||||
var session = await charge.Listen(_Cts.Token);
|
var session = await charge.Listen(_Cts.Token);
|
||||||
while (true)
|
while (true)
|
||||||
@@ -134,7 +134,6 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
ListenedInvoice listenedInvoice = GetListenedInvoice(notification.Id);
|
ListenedInvoice listenedInvoice = GetListenedInvoice(notification.Id);
|
||||||
if (listenedInvoice == null)
|
if (listenedInvoice == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
|
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
|
||||||
notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11)
|
notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11)
|
||||||
{
|
{
|
||||||
@@ -157,10 +156,10 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningChargeUrl(false)}");
|
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||||
DoneListening(supportedPaymentMethod.GetLightningChargeUrl(false));
|
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)
|
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
|
/// Stop listening all invoices on this server
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uri"></param>
|
/// <param name="uri"></param>
|
||||||
private void DoneListening(Uri uri)
|
private void DoneListening(LightningConnectionString connectionString)
|
||||||
{
|
{
|
||||||
|
var uri = connectionString.BaseUri;
|
||||||
lock (_ListenedInvoiceByChargeInvoiceId)
|
lock (_ListenedInvoiceByChargeInvoiceId)
|
||||||
{
|
{
|
||||||
foreach (var listenedInvoice in _ListenedInvoiceByLightningUrl[uri.AbsoluteUri])
|
foreach (var listenedInvoice in _ListenedInvoiceByLightningUrl[uri.AbsoluteUri])
|
||||||
|
|||||||
@@ -8,41 +8,36 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
public class LightningSupportedPaymentMethod : ISupportedPaymentMethod
|
public class LightningSupportedPaymentMethod : ISupportedPaymentMethod
|
||||||
{
|
{
|
||||||
public string CryptoCode { get; set; }
|
public string CryptoCode { get; set; }
|
||||||
[Obsolete("Use Get/SetLightningChargeUrl")]
|
[Obsolete("Use Get/SetLightningUrl")]
|
||||||
public string LightningChargeUrl { get; set; }
|
public string LightningChargeUrl { get; set; }
|
||||||
|
|
||||||
public Uri GetLightningChargeUrl(bool withCredentials)
|
public LightningConnectionString GetLightningUrl()
|
||||||
{
|
{
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
UriBuilder uri = new UriBuilder(LightningChargeUrl);
|
var fullUri = new UriBuilder(LightningChargeUrl) { UserName = Username, Password = Password }.Uri.AbsoluteUri;
|
||||||
if (withCredentials)
|
if(!LightningConnectionString.TryParse(fullUri, out var connectionString, out var error))
|
||||||
{
|
{
|
||||||
uri.UserName = Username;
|
throw new FormatException(error);
|
||||||
uri.Password = Password;
|
|
||||||
}
|
}
|
||||||
|
return connectionString;
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
return uri.Uri;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetLightningChargeUrl(Uri uri)
|
public void SetLightningUrl(LightningConnectionString connectionString)
|
||||||
{
|
{
|
||||||
if (uri == null)
|
if (connectionString == null)
|
||||||
throw new ArgumentNullException(nameof(uri));
|
throw new ArgumentNullException(nameof(connectionString));
|
||||||
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
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
Username = splitted[0];
|
Username = connectionString.Username;
|
||||||
Password = splitted[1];
|
Password = connectionString.Password;
|
||||||
LightningChargeUrl = new UriBuilder(uri) { UserName = "", Password = "" }.Uri.AbsoluteUri;
|
LightningChargeUrl = connectionString.BaseUri.AbsoluteUri;
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("Use Get/SetLightningChargeUrl")]
|
[Obsolete("Use Get/SetLightningUrl")]
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
[Obsolete("Use Get/SetLightningChargeUrl")]
|
[Obsolete("Use Get/SetLightningUrl")]
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
|
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user