LND Support

This commit is contained in:
rockstardev 2018-07-01 21:41:06 +09:00 committed by nicolas.dorier
commit 25dbf6445f
16 changed files with 9590 additions and 97 deletions

View file

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Payments.Lightning.Lnd;
using NBitcoin;
namespace BTCPayServer.Tests.Lnd
{
public class LndMockTester
{
private ServerTester _Parent;
public LndMockTester(ServerTester serverTester, string environmentName, string defaultValue, string defaultHost, Network network)
{
this._Parent = serverTester;
var url = serverTester.GetEnvironment(environmentName, defaultValue);
Swagger = LndSwaggerClientCustomHttp.Create(new Uri(url), network);
Client = new LndInvoiceClient(Swagger);
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
}
public LndSwaggerClientCustomHttp Swagger { get; set; }
public LndInvoiceClient Client { get; set; }
public string P2PHost { get; }
}
}

View file

@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payments.Lightning.Lnd;
using NBitcoin;
using NBitcoin.RPC;
using Xunit;
using Xunit.Abstractions;
using System.Linq;
using System.Threading;
using NBitpayClient;
using System.Globalization;
namespace BTCPayServer.Tests.Lnd
{
// this depends for now on `docker-compose up devlnd`
public class UnitTests
{
private readonly ITestOutputHelper output;
public UnitTests(ITestOutputHelper output)
{
this.output = output;
initializeEnvironment();
MerchantLnd = LndSwaggerClientCustomHttp.Create(new Uri("http://127.0.0.1:53280"), Network.RegTest);
InvoiceClient = new LndInvoiceClient(MerchantLnd);
CustomerLnd = LndSwaggerClientCustomHttp.Create(new Uri("http://127.0.0.1:53281"), Network.RegTest);
}
private LndSwaggerClientCustomHttp MerchantLnd { get; set; }
private LndInvoiceClient InvoiceClient { get; set; }
private LndSwaggerClientCustomHttp CustomerLnd { get; set; }
[Fact]
public async Task GetInfo()
{
var res = await InvoiceClient.GetInfo();
output.WriteLine("Result: " + res.ToJson());
}
[Fact]
public async Task CreateInvoice()
{
var res = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
output.WriteLine("Result: " + res.ToJson());
}
[Fact]
public async Task GetInvoice()
{
var createInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
var getInvoice = await InvoiceClient.GetInvoice(createInvoice.Id);
Assert.Equal(createInvoice.BOLT11, getInvoice.BOLT11);
}
// integration tests
[Fact]
public async Task TestWaitListenInvoice()
{
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
var waitToken = default(CancellationToken);
var listener = await InvoiceClient.Listen(waitToken);
var waitTask = listener.WaitInvoice(waitToken);
await EnsureLightningChannelAsync();
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
{
Payment_request = merchantInvoice.BOLT11
});
var invoice = await waitTask;
Assert.True(invoice.PaidAt.HasValue);
}
[Fact]
public async Task CreateLndInvoiceAndPay()
{
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
await EnsureLightningChannelAsync();
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
{
Payment_request = merchantInvoice.BOLT11
});
var invoice = await InvoiceClient.GetInvoice(merchantInvoice.Id);
Assert.True(invoice.PaidAt.HasValue);
}
public async Task<LnrpcChannel> EnsureLightningChannelAsync()
{
var merchantInfo = await WaitLNSynched();
var merchantNodeAddress = new LnrpcLightningAddress
{
Pubkey = merchantInfo.NodeId,
Host = "merchant_lnd:9735"
};
while (true)
{
// if channel is pending generate blocks until confirmed
var pendingResponse = await CustomerLnd.PendingChannelsAsync();
if (pendingResponse.Pending_open_channels?
.Any(a => a.Channel?.Remote_node_pub == merchantNodeAddress.Pubkey) == true)
{
ExplorerNode.Generate(1);
await WaitLNSynched();
continue;
}
// check if channel is established
var chanResponse = await CustomerLnd.ListChannelsAsync(null, null, null, null);
LnrpcChannel channelToMerchant = null;
if (chanResponse != null && chanResponse.Channels != null)
{
channelToMerchant = chanResponse.Channels
.Where(a => a.Remote_pubkey == merchantNodeAddress.Pubkey)
.FirstOrDefault();
}
if (channelToMerchant == null)
{
// create new channel
var isConnected = await CustomerLnd.ListPeersAsync();
if (isConnected.Peers == null ||
!isConnected.Peers.Any(a => a.Pub_key == merchantInfo.NodeId))
{
var connectResp = await CustomerLnd.ConnectPeerAsync(new LnrpcConnectPeerRequest
{
Addr = merchantNodeAddress
});
}
var addressResponse = await CustomerLnd.NewWitnessAddressAsync();
var address = BitcoinAddress.Create(addressResponse.Address, Network.RegTest);
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
ExplorerNode.Generate(1);
await WaitLNSynched();
var channelReq = new LnrpcOpenChannelRequest
{
Local_funding_amount = 16777215.ToString(CultureInfo.InvariantCulture),
Node_pubkey_string = merchantInfo.NodeId
};
var channelResp = await CustomerLnd.OpenChannelSyncAsync(channelReq);
}
else
{
// channel exists, return it
ExplorerNode.Generate(1);
await WaitLNSynched();
return channelToMerchant;
}
}
}
private async Task<LightningNodeInformation> WaitLNSynched()
{
while (true)
{
var merchantInfo = await InvoiceClient.GetInfo();
var blockCount = await ExplorerNode.GetBlockCountAsync();
if (merchantInfo.BlockHeight != blockCount)
{
await Task.Delay(500);
}
else
{
return merchantInfo;
}
}
}
//
private void initializeEnvironment()
{
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
}
public BTCPayNetworkProvider NetworkProvider { get; private set; }
public RPCClient ExplorerNode { get; set; }
internal string GetEnvironment(string variable, string defaultValue)
{
var var = Environment.GetEnvironmentVariable(variable);
return String.IsNullOrEmpty(var) ? defaultValue : var;
}
}
}

View file

@ -20,6 +20,7 @@ using System.Threading;
using System.Globalization;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Tests.Lnd;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Tests
@ -53,6 +54,8 @@ namespace BTCPayServer.Tests
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "http://lnd:lnd@127.0.0.1:53280/", "merchant_lnd", btc);
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
{
NBXplorerUri = ExplorerClient.Address,
@ -79,41 +82,52 @@ namespace BTCPayServer.Tests
/// <summary>
/// Connect a customer LN node to the merchant LN node
/// </summary>
public void PrepareLightning()
public void PrepareLightning(LightningConnectionType lndBackend)
{
PrepareLightningAsync().GetAwaiter().GetResult();
ILightningInvoiceClient client = MerchantCharge.Client;
if (lndBackend == LightningConnectionType.Lnd)
client = MerchantLnd.Client;
PrepareLightningAsync(client).GetAwaiter().GetResult();
}
private static readonly string[] SKIPPED_STATES =
{ "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
/// <summary>
/// Connect a customer LN node to the merchant LN node
/// </summary>
/// <returns></returns>
public async Task PrepareLightningAsync()
private async Task PrepareLightningAsync(ILightningInvoiceClient client)
{
while (true)
{
var skippedStates = new[] { "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
var channel = (await CustomerLightningD.ListPeersAsync())
var merchantInfo = await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
var peers = await CustomerLightningD.ListPeersAsync();
var filteringToTargetedPeers = peers.Where(a => a.Id == merchantInfo.NodeId);
var channel = filteringToTargetedPeers
.SelectMany(p => p.Channels)
.Where(c => !skippedStates.Contains(c.State ?? ""))
.Where(c => !SKIPPED_STATES.Contains(c.State ?? ""))
.FirstOrDefault();
switch (channel?.State)
{
case null:
var merchantInfo = await WaitLNSynched();
var clightning = new NodeInfo(merchantInfo.Id, MerchantCharge.P2PHost, merchantInfo.Port);
await CustomerLightningD.ConnectAsync(clightning);
var address = await CustomerLightningD.NewAddressAsync();
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.5m));
ExplorerNode.Generate(1);
await WaitLNSynched();
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
await Task.Delay(1000);
await CustomerLightningD.FundChannelAsync(clightning, Money.Satoshis(16777215));
var merchantNodeInfo = new NodeInfo(merchantInfo.NodeId, merchantInfo.Address, merchantInfo.P2PPort);
await CustomerLightningD.ConnectAsync(merchantNodeInfo);
await CustomerLightningD.FundChannelAsync(merchantNodeInfo, Money.Satoshis(16777215));
break;
case "CHANNELD_AWAITING_LOCKIN":
ExplorerNode.Generate(1);
await WaitLNSynched();
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
break;
case "CHANNELD_NORMAL":
return;
@ -123,23 +137,29 @@ namespace BTCPayServer.Tests
}
}
private async Task<GetInfoResponse> WaitLNSynched()
private async Task<LightningNodeInformation> WaitLNSynched(params ILightningInvoiceClient[] clients)
{
while (true)
{
var merchantInfo = await MerchantCharge.Client.GetInfoAsync();
var blockCount = await ExplorerNode.GetBlockCountAsync();
if (merchantInfo.BlockHeight != blockCount)
{
await Task.Delay(1000);
}
else
{
return merchantInfo;
}
var synching = clients.Select(c => WaitLNSynchedCore(blockCount, c)).ToArray();
await Task.WhenAll(synching);
if (synching.All(c => c.Result != null))
return synching[0].Result;
await Task.Delay(1000);
}
}
private async Task<LightningNodeInformation> WaitLNSynchedCore(int blockCount, ILightningInvoiceClient client)
{
var merchantInfo = await client.GetInfo();
if (merchantInfo.BlockHeight == blockCount)
{
return merchantInfo;
}
return null;
}
public void SendLightningPayment(Invoice invoice)
{
SendLightningPaymentAsync(invoice).GetAwaiter().GetResult();
@ -153,8 +173,10 @@ namespace BTCPayServer.Tests
}
public CLightningRPCClient CustomerLightningD { get; set; }
public CLightningRPCClient MerchantLightningD { get; private set; }
public ChargeTester MerchantCharge { get; private set; }
public LndMockTester MerchantLnd { get; set; }
internal string GetEnvironment(string variable, string defaultValue)
{

View file

@ -126,11 +126,20 @@ namespace BTCPayServer.Tests
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
{
var storeController = this.GetController<StoresController>();
string connectionString = null;
if (connectionType == LightningConnectionType.Charge)
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
else if (connectionType == LightningConnectionType.CLightning)
connectionString = "type=clightning;server=" + parent.MerchantLightningD.Address.AbsoluteUri;
else if (connectionType == LightningConnectionType.Lnd)
connectionString = $"type=lnd;server={parent.MerchantLnd.Swagger.BaseUrl}";
else
throw new NotSupportedException(connectionType.ToString());
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
{
ConnectionString = connectionType == LightningConnectionType.Charge ? "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri :
connectionType == LightningConnectionType.CLightning ? "type=clightning;server=" + parent.MerchantLightningD.Address.AbsoluteUri
: throw new NotSupportedException(connectionType.ToString()),
ConnectionString = connectionString,
SkipPortTest = true
}, "save", "BTC");
if (storeController.ModelState.ErrorCount != 0)

View file

@ -512,71 +512,52 @@ namespace BTCPayServer.Tests
Assert.True(LightningConnectionString.TryParse("type=clightning;server=/aaa:bbb@test/a", false, out conn));
Assert.True(LightningConnectionString.TryParse("type=clightning;server=unix://aaa:bbb@test/a", false, out conn));
Assert.False(LightningConnectionString.TryParse("type=clightning;server=wtf://aaa:bbb@test/a", false, out conn));
var macaroon = "0201036c6e640247030a10b0dbbde28f009f83d330bde05075ca251201301a160a0761646472657373120472656164120577726974651a170a08696e766f6963657312047265616412057772697465000006200ae088692e67cf14e767c3d2a4a67ce489150bf810654ff980e1b7a7e263d5e8";
var tls = "2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494942396a4343415a7967417749424167495156397a62474252724e54716b4e4b55676d72524d377a414b42676771686b6a4f50515144416a41784d5238770a485159445651514b45785a73626d5167595856306232646c626d56795958526c5a43426a5a584a304d51347744415944565151444577564754304e56557a41650a467730784f4441304d6a55794d7a517a4d6a4261467730784f5441324d6a41794d7a517a4d6a42614d444578487a416442674e5642416f54466d78755a4342680a645852765a3256755a584a686447566b49474e6c636e5178446a414d42674e5642414d5442555a50513156544d466b77457759484b6f5a497a6a3043415159490a4b6f5a497a6a304441516344516741454b7557424568564f75707965434157476130766e713262712f59396b41755a78616865646d454553482b753936436d450a397577486b4b2b4a7667547a66385141783550513741357254637155374b57595170303175364f426c5443426b6a414f42674e56485138424166384542414d430a4171517744775944565230544151482f42415577417745422f7a427642674e56485245456144426d6767564754304e565534494a6247396a5957786f62334e300a6877522f4141414268784141414141414141414141414141414141414141414268775373474f69786877514b41457342687753702f717473687754417141724c0a687753702f6d4a72687753702f754f77687753702f714e59687753702f6874436877514b70514157687753702f6c42514d416f4743437147534d343942414d430a413067414d45554349464866716d595a5043647a4a5178386b47586859473834394c31766541364c784d6f7a4f5774356d726835416945413662756e51556c710a6558553070474168776c3041654d726a4d4974394c7652736179756162565a593278343d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a";
var lndUri = $"type=lnd;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};tls={tls}";
Assert.True(LightningConnectionString.TryParse(lndUri, false, out conn));
Assert.Equal(lndUri, conn.ToString());
Assert.Equal(LightningConnectionType.Lnd, conn.ConnectionType);
Assert.Equal(macaroon, Encoders.Hex.EncodeData(conn.Macaroon));
Assert.Equal(tls, Encoders.Hex.EncodeData(conn.Tls));
}
[Fact]
public void CanSendLightningPaymentCLightning()
{
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.01m,
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());
});
}
ProcessLightningPayment(LightningConnectionType.CLightning);
}
[Fact]
public void CanSendLightningPaymentCharge()
{
ProcessLightningPayment(LightningConnectionType.Charge);
}
[Fact]
public void CanSendLightningPaymentLnd()
{
ProcessLightningPayment(LightningConnectionType.Lnd);
}
void ProcessLightningPayment(LightningConnectionType type)
{
// For easier debugging and testing
// LightningLikePaymentHandler.LIGHTNING_TIMEOUT = int.MaxValue;
using (var tester = ServerTester.Create())
{
tester.Start();
tester.PrepareLightning();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
user.RegisterLightningNode("BTC", type);
user.RegisterDerivationScheme("BTC");
tester.PrepareLightning(type);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.01m,
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());
});
Task.WaitAll(CanSendLightningPaymentCore(tester, user));
Task.WaitAll(Enumerable.Range(0, 5)
.Select(_ => CanSendLightningPaymentCore(tester, user))
@ -586,7 +567,10 @@ namespace BTCPayServer.Tests
async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user)
{
await Task.Delay(TimeSpan.FromSeconds(RandomUtils.GetUInt32() % 5));
// TODO: If this parameter is less than 1 second we start having concurrency problems
await Task.Delay(TimeSpan.FromMilliseconds(1000));
//
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
{
Price = 0.01m,

View file

@ -0,0 +1,48 @@
using System;
using NBitcoin;
using Xunit;
namespace BTCPayServer.Tests
{
// Helper class for testing functionality and generating data needed during coding/debuging
public class UnitTestPeusa
{
// Unit test that generates temorary checkout Bitpay page
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
// Testnet of Bitpay down
//[Fact]
//public void BitpayCheckout()
//{
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
// var url = new Uri("https://test.bitpay.com/");
// var btcpay = new Bitpay(key, url);
// var invoice = btcpay.CreateInvoice(new Invoice()
// {
// Price = 5.0,
// Currency = "USD",
// PosData = "posData",
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
// ItemDesc = "Hello from the otherside"
// }, Facade.Merchant);
// // go to invoice.Url
// Console.WriteLine(invoice.Url);
//}
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
[Fact]
public void GeneratePubkey()
{
var network = Network.RegTest;
ExtKey masterKey = new ExtKey();
Console.WriteLine("Master key : " + masterKey.ToString(network));
ExtPubKey masterPubKey = masterKey.Neuter();
ExtPubKey pubkey = masterPubKey.Derive(0);
Console.WriteLine("PubKey " + 0 + " : " + pubkey.ToString(network));
}
}
}

View file

@ -20,6 +20,7 @@ services:
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=/etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=/etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify"
TEST_MERCHANTLND: "type=lnd;server=http://lnd:lnd@127.0.0.1:53280/"
TESTS_INCONTAINER: "true"
expose:
- "80"
@ -44,6 +45,22 @@ services:
- customer_lightningd
- merchant_lightningd
- lightning-charged
- customer_lnd
- merchant_lnd
devlnd:
image: nicolasdorier/docker-bitcoin:0.16.0
environment:
BITCOIN_EXTRA_ARGS: |
regtest=1
connect=bitcoind:39388
links:
- nbxplorer
- postgres
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.2.8
@ -80,8 +97,13 @@ services:
rpcport=43782
port=39388
whitelist=0.0.0.0/0
zmqpubrawtx=tcp://0.0.0.0:28332
zmqpubrawblock=tcp://0.0.0.0:28332
zmqpubrawtxlock=tcp://0.0.0.0:28332
zmqpubhashblock=tcp://0.0.0.0:28332
ports:
- "43782:43782"
- "28332:28332"
expose:
- "43782" # RPC
- "39388" # P2P
@ -177,8 +199,52 @@ services:
expose:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:0.4.2.0
environment:
RPCHOST: bitcoind:43782
RPCUSER: ceiwHEbqWI83
RPCPASS: DwubwWsoo3
ZMQPATH: tcp://bitcoind:28332
NETWORK: regtest
CHAIN: bitcoin
BACKEND: bitcoind
DEBUG: debug
EXTERNALIP: merchant_lnd:9735
ports:
- "53280:8080"
expose:
- "9735"
volumes:
- "merchant_lnd_datadir:/root/.lnd"
links:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:0.4.2.0
environment:
RPCHOST: bitcoind:43782
RPCUSER: ceiwHEbqWI83
RPCPASS: DwubwWsoo3
ZMQPATH: tcp://bitcoind:28332
NETWORK: regtest
CHAIN: bitcoin
BACKEND: bitcoind
DEBUG: debug
ports:
- "53281:8080"
expose:
- "8080"
- "10009"
volumes:
- "customer_lnd_datadir:/root/.lnd"
links:
- bitcoind
volumes:
bitcoin_datadir:
customer_lightningd_datadir:
merchant_lightningd_datadir:
lightning_charge_datadir:
customer_lnd_datadir:
merchant_lnd_datadir:

View file

@ -81,7 +81,8 @@ namespace BTCPayServer.Configuration
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
$"If you have a lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'");
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
$"If you have a lnd server: 'type=lnd;server=https://lnd:lnd@lnd.example.com;macaron=abf239...;tls=2abdf302...'");
}
if(connectionString.IsLegacy)
{

View file

@ -35,7 +35,6 @@ namespace BTCPayServer.Controllers
{
vm.ConnectionString = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
@ -82,7 +81,7 @@ namespace BTCPayServer.Controllers
return View(vm);
}
var internalDomain = internalLightning?.ToUri(false)?.DnsSafeHost;
var internalDomain = internalLightning.BaseUri?.DnsSafeHost;
bool isLocal = (internalDomain == "127.0.0.1" || internalDomain == "localhost");
bool isInternalNode = connectionString.ConnectionType == LightningConnectionType.CLightning ||
@ -110,6 +109,7 @@ namespace BTCPayServer.Controllers
};
paymentMethod.SetLightningUrl(connectionString);
}
if (command == "save")
{
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
@ -135,7 +135,7 @@ namespace BTCPayServer.Controllers
await handler.TestConnection(info, cts.Token);
}
}
vm.StatusMessage = $"Connection to the lightning node succeed ({info})";
vm.StatusMessage = $"Connection to the lightning node succeeded ({info})";
}
catch (Exception ex)
{

View file

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
using NBitcoin;
using BTCPayServer.Payments.Lightning.Lnd;
namespace BTCPayServer.Payments.Lightning
{
@ -16,18 +17,24 @@ namespace BTCPayServer.Payments.Lightning
return CreateClient(uri, network.NBitcoinNetwork);
}
public static ILightningInvoiceClient CreateClient(LightningConnectionString uri, Network network)
public static ILightningInvoiceClient CreateClient(LightningConnectionString connString, Network network)
{
if (uri.ConnectionType == LightningConnectionType.Charge)
if (connString.ConnectionType == LightningConnectionType.Charge)
{
return new ChargeClient(uri.ToUri(true), network);
return new ChargeClient(connString.ToUri(true), network);
}
else if (uri.ConnectionType == LightningConnectionType.CLightning)
else if (connString.ConnectionType == LightningConnectionType.CLightning)
{
return new CLightningRPCClient(uri.ToUri(false), network);
return new CLightningRPCClient(connString.ToUri(false), network);
}
else if (connString.ConnectionType == LightningConnectionType.Lnd)
{
var swagger = LndSwaggerClientCustomHttp.Create(connString.BaseUri, network, connString.Tls, connString.Macaroon);
return new LndInvoiceClient(swagger);
}
else
throw new NotSupportedException($"Unsupported connection string for lightning server ({uri.ConnectionType})");
throw new NotSupportedException($"Unsupported connection string for lightning server ({connString.ConnectionType})");
}
public static ILightningInvoiceClient CreateClient(string connectionString, Network network)

View file

@ -3,13 +3,17 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Primitives;
namespace BTCPayServer.Payments.Lightning
{
public enum LightningConnectionType
{
Charge,
CLightning
CLightning,
Lnd
}
public class LightningConnectionString
{
@ -20,6 +24,7 @@ namespace BTCPayServer.Payments.Lightning
typeMapping = new Dictionary<string, LightningConnectionType>();
typeMapping.Add("clightning", LightningConnectionType.CLightning);
typeMapping.Add("charge", LightningConnectionType.Charge);
typeMapping.Add("lnd", LightningConnectionType.Lnd);
typeMappingReverse = new Dictionary<LightningConnectionType, string>();
foreach (var kv in typeMapping)
{
@ -153,6 +158,56 @@ namespace BTCPayServer.Payments.Lightning
result.BaseUri = uri;
}
break;
case LightningConnectionType.Lnd:
{
var server = Take(keyValues, "server");
if (server == null)
{
error = $"The key 'server' is mandatory for lnd connection strings";
return false;
}
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|| (uri.Scheme != "http" && uri.Scheme != "https"))
{
error = $"The key 'server' should be an URI starting by http:// or https://";
return false;
}
parts = uri.UserInfo.Split(':');
if (!string.IsNullOrEmpty(uri.UserInfo) && parts.Length == 2)
{
result.Username = parts[0];
result.Password = parts[1];
}
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
var macaroon = Take(keyValues, "macaroon");
//if(macaroon == null)
//{
// error = $"The key 'macaroon' is mandatory for lnd connection strings";
// return false;
//}
//try
//{
// result.Macaroon = Encoder.DecodeData(macaroon);
//}
//catch
//{
// error = $"The key 'macaroon' format should be in hex";
// return false;
//}
try
{
var tls = Take(keyValues, "tls");
if (tls != null)
result.Tls = Encoder.DecodeData(tls);
}
catch
{
error = $"The key 'tls' format should be in hex";
return false;
}
}
break;
default:
throw new NotSupportedException(connectionType.ToString());
}
@ -182,7 +237,7 @@ namespace BTCPayServer.Payments.Lightning
error = null;
Uri uri;
if (!System.Uri.TryCreate(str, UriKind.Absolute, out uri))
if (!Uri.TryCreate(str, UriKind.Absolute, out uri))
{
error = "Invalid URL";
return false;
@ -195,7 +250,6 @@ namespace BTCPayServer.Payments.Lightning
error = $"The url support the following protocols {protocols}";
return false;
}
if (uri.Scheme == "unix")
{
str = uri.AbsoluteUri.Substring("unix:".Length);
@ -248,6 +302,8 @@ namespace BTCPayServer.Payments.Lightning
get;
private set;
}
public byte[] Macaroon { get; set; }
public byte[] Tls { get; set; }
public Uri ToUri(bool withCredentials)
{
@ -260,7 +316,7 @@ namespace BTCPayServer.Payments.Lightning
return BaseUri;
}
}
static NBitcoin.DataEncoders.DataEncoder Encoder = NBitcoin.DataEncoders.Encoders.Hex;
public override string ToString()
{
var type = typeMappingReverse[ConnectionType];
@ -269,7 +325,7 @@ namespace BTCPayServer.Payments.Lightning
switch (ConnectionType)
{
case LightningConnectionType.Charge:
if(Username == null || Username == "api-token")
if (Username == null || Username == "api-token")
{
builder.Append($";server={BaseUri};api-token={Password}");
}
@ -281,6 +337,24 @@ namespace BTCPayServer.Payments.Lightning
case LightningConnectionType.CLightning:
builder.Append($";server={BaseUri}");
break;
case LightningConnectionType.Lnd:
if (Username == null)
{
builder.Append($";server={BaseUri}");
}
else
{
builder.Append($";server={ToUri(true)}");
}
if (Macaroon != null)
{
builder.Append($";macaroon={Encoder.EncodeData(Macaroon)}");
}
if (Tls != null)
{
builder.Append($";tls={Encoder.EncodeData(Tls)}");
}
break;
default:
throw new NotSupportedException(type);
}

View file

@ -15,6 +15,8 @@ namespace BTCPayServer.Payments.Lightning
{
public class LightningLikePaymentHandler : PaymentMethodHandlerBase<LightningSupportedPaymentMethod>
{
public static int LIGHTNING_TIMEOUT = 5000;
NBXplorerDashboard _Dashboard;
LightningClientFactory _LightningClientFactory;
public LightningLikePaymentHandler(
@ -41,7 +43,7 @@ namespace BTCPayServer.Payments.Lightning
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
using (var cts = new CancellationTokenSource(5000))
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
{
try
{
@ -70,7 +72,7 @@ namespace BTCPayServer.Payments.Lightning
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new PaymentMethodUnavailableException($"Full node not available");
using (var cts = new CancellationTokenSource(5000))
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
{
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
LightningNodeInformation info = null;

View file

@ -8,6 +8,15 @@ namespace BTCPayServer.Payments.Lightning
public class LightningSupportedPaymentMethod : ISupportedPaymentMethod
{
public string CryptoCode { get; set; }
[Obsolete("Use Get/SetLightningUrl")]
public string Username { get; set; }
[Obsolete("Use Get/SetLightningUrl")]
public string Password { get; set; }
// This property MUST be after CryptoCode or else JSON serialization fails
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
[Obsolete("Use Get/SetLightningUrl")]
public string LightningChargeUrl { get; set; }
@ -49,11 +58,5 @@ namespace BTCPayServer.Payments.Lightning
LightningChargeUrl = null;
#pragma warning restore CS0618 // Type or member is obsolete
}
[Obsolete("Use Get/SetLightningUrl")]
public string Username { get; set; }
[Obsolete("Use Get/SetLightningUrl")]
public string Password { get; set; }
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
}
}

View file

@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Payments.Lightning.Lnd
{
public class LndInvoiceClient : ILightningInvoiceClient, ILightningListenInvoiceSession
{
public LndSwaggerClient _rpcClient;
public LndInvoiceClient(LndSwaggerClient rpcClient)
{
_rpcClient = rpcClient;
}
public async Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry,
CancellationToken cancellation = default(CancellationToken))
{
var strAmount = ConvertInv.ToString(amount.ToUnit(LightMoneyUnit.Satoshi));
var strExpiry = ConvertInv.ToString(Math.Round(expiry.TotalSeconds, 0));
// lnd requires numbers sent as strings. don't ask
var resp = await _rpcClient.AddInvoiceAsync(new LnrpcInvoice
{
Value = strAmount,
Memo = description,
Expiry = strExpiry
});
var invoice = new LightningInvoice
{
Id = BitString(resp.R_hash),
Amount = amount,
BOLT11 = resp.Payment_request,
Status = "unpaid"
};
return invoice;
}
public async Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default(CancellationToken))
{
var resp = await _rpcClient.GetInfoAsync(cancellation);
var nodeInfo = new LightningNodeInformation
{
BlockHeight = (int?)resp.Block_height ?? 0,
NodeId = resp.Identity_pubkey
};
var node = await _rpcClient.GetNodeInfoAsync(resp.Identity_pubkey, cancellation);
if (node.Node.Addresses == null || node.Node.Addresses.Count == 0)
throw new Exception("Lnd External IP not set, make sure you use --externalip=$EXTERNALIP parameter on lnd");
var firstNodeInfo = node.Node.Addresses.First();
var externalHostPort = firstNodeInfo.Addr.Split(':');
nodeInfo.Address = externalHostPort[0];
nodeInfo.P2PPort = ConvertInv.ToInt32(externalHostPort[1]);
return nodeInfo;
}
public async Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken))
{
var resp = await _rpcClient.LookupInvoiceAsync(invoiceId, null, cancellation);
return ConvertLndInvoice(resp);
}
public Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken))
{
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
_rpcClient.StartSubscribeInvoiceThread(cancellation);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
return Task.FromResult<ILightningListenInvoiceSession>(this);
}
async Task<LightningInvoice> ILightningListenInvoiceSession.WaitInvoice(CancellationToken cancellation)
{
var resp = await _rpcClient.InvoiceResponse.Task;
return ConvertLndInvoice(resp);
}
// utility static methods... maybe move to separate class
private static string BitString(byte[] bytes)
{
return BitConverter.ToString(bytes)
.Replace("-", "", StringComparison.InvariantCulture)
.ToLower(CultureInfo.InvariantCulture);
}
private static LightningInvoice ConvertLndInvoice(LnrpcInvoice resp)
{
var invoice = new LightningInvoice
{
// TODO: Verify id corresponds to R_hash
Id = BitString(resp.R_hash),
Amount = new LightMoney(ConvertInv.ToInt64(resp.Value), LightMoneyUnit.Satoshi),
BOLT11 = resp.Payment_request,
Status = "unpaid"
};
if (resp.Settle_date != null)
{
invoice.PaidAt = DateTimeOffset.FromUnixTimeSeconds(ConvertInv.ToInt64(resp.Settle_date));
invoice.Status = "paid";
}
else
{
var invoiceExpiry = ConvertInv.ToInt64(resp.Creation_date) + ConvertInv.ToInt64(resp.Expiry);
if (DateTimeOffset.FromUnixTimeSeconds(invoiceExpiry) > DateTimeOffset.UtcNow)
{
invoice.Status = "expired";
}
}
return invoice;
}
public void Dispose()
{
//
}
// Invariant culture conversion
public static class ConvertInv
{
public static int ToInt32(string str)
{
return Convert.ToInt32(str, CultureInfo.InvariantCulture.NumberFormat);
}
public static long ToInt64(string str)
{
return Convert.ToInt64(str, CultureInfo.InvariantCulture.NumberFormat);
}
public static string ToString(decimal d)
{
return d.ToString(CultureInfo.InvariantCulture);
}
public static string ToString(double d)
{
return d.ToString(CultureInfo.InvariantCulture);
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Payments.Lightning.Lnd
{
public class LndSwaggerClientCustomHttp : LndSwaggerClient, IDisposable
{
protected LndSwaggerClientCustomHttp(string baseUrl, HttpClient httpClient)
: base(baseUrl, httpClient)
{
_HttpClient = httpClient;
}
private HttpClient _HttpClient;
public void Dispose()
{
_HttpClient.Dispose();
}
//
public static LndSwaggerClientCustomHttp Create(Uri uri, Network network, byte[] tlsCertificate = null, byte[] grpcMacaroon = null)
{
var factory = new HttpClientFactoryForLnd(tlsCertificate, grpcMacaroon);
var httpClient = factory.Generate();
var swagger = new LndSwaggerClientCustomHttp(uri.ToString().TrimEnd('/'), httpClient);
swagger.HttpClientFactory = factory;
return swagger;
}
}
internal class HttpClientFactoryForLnd
{
public HttpClientFactoryForLnd(byte[] tlsCertificate = null, byte[] grpcMacaroon = null)
{
TlsCertificate = tlsCertificate;
GrpcMacaroon = grpcMacaroon;
}
public byte[] TlsCertificate { get; set; }
public byte[] GrpcMacaroon { get; set; }
public HttpClient Generate()
{
var httpClient = new HttpClient(GetCertificate(TlsCertificate));
if (GrpcMacaroon != null)
{
var macaroonHex = BitConverter.ToString(GrpcMacaroon).Replace("-", "", StringComparison.InvariantCulture);
httpClient.DefaultRequestHeaders.Add("Grpc-Metadata-macaroon", macaroonHex);
}
return httpClient;
}
private static HttpClientHandler GetCertificate(byte[] certFile)
{
var handler = new HttpClientHandler
{
SslProtocols = SslProtocols.Tls12
};
if (certFile == null)
{
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
return handler;
}
// if certificate is not null, try with custom accepting logic
X509Certificate2 clientCertificate = null;
if (certFile != null)
clientCertificate = new X509Certificate2(certFile);
handler.ServerCertificateCustomValidationCallback = (request, cert, chain, errors) =>
{
const SslPolicyErrors unforgivableErrors =
SslPolicyErrors.RemoteCertificateNotAvailable |
SslPolicyErrors.RemoteCertificateNameMismatch;
if ((errors & unforgivableErrors) != 0)
{
return false;
}
if (clientCertificate == null)
return true;
X509Certificate2 remoteRoot = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
var res = clientCertificate.RawData.SequenceEqual(remoteRoot.RawData);
return res;
};
return handler;
}
}
public partial class LndSwaggerClient
{
internal HttpClientFactoryForLnd HttpClientFactory { get; set; }
public TaskCompletionSource<LnrpcInvoice> InvoiceResponse = new TaskCompletionSource<LnrpcInvoice>();
public TaskCompletionSource<LndSwaggerClient> SubscribeLost = new TaskCompletionSource<LndSwaggerClient>();
// TODO: Refactor swagger generated wrapper to include this method directly
public async Task StartSubscribeInvoiceThread(CancellationToken token)
{
var urlBuilder = new StringBuilder();
urlBuilder.Append(BaseUrl).Append("/v1/invoices/subscribe");
using (var client = HttpClientFactory.Generate())
{
client.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite);
var request = new HttpRequestMessage(HttpMethod.Get, urlBuilder.ToString());
using (var response = await client.SendAsync(
request, HttpCompletionOption.ResponseHeadersRead, token))
{
using (var body = await response.Content.ReadAsStreamAsync())
using (var reader = new StreamReader(body))
{
try
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
if (line != null && line.Contains("\"result\":", StringComparison.OrdinalIgnoreCase))
{
dynamic parsedJson = JObject.Parse(line);
var result = parsedJson.result;
var invoiceString = result.ToString();
LnrpcInvoice parsedInvoice = JsonConvert.DeserializeObject<LnrpcInvoice>(invoiceString, _settings.Value);
InvoiceResponse.SetResult(parsedInvoice);
}
}
}
catch (Exception e)
{
// TODO: check that the exception type is actually from a closed stream.
Debug.WriteLine(e.Message);
SubscribeLost.SetResult(this);
}
}
}
}
}
}
}