mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
Remove Lightning Specific logic from BTCPay, and use BTCPayServer.Lightning packages instead
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
using BTCPayServer.Lightning.Charge;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Payments.Lightning.Charge;
|
|
||||||
using BTCPayServer.Payments.Lightning.CLightning;
|
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Payments.Lightning.CLightning;
|
using BTCPayServer.Lightning.CLightning;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
@@ -13,10 +13,10 @@ namespace BTCPayServer.Tests
|
|||||||
public LightningDTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost, Network network)
|
public LightningDTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost, Network network)
|
||||||
{
|
{
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
RPC = new CLightningRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
|
RPC = new CLightningClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CLightningRPCClient RPC { get; }
|
public CLightningClient RPC { get; }
|
||||||
public string P2PHost { get; }
|
public string P2PHost { get; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using BTCPayServer.Payments.Lightning.Lnd;
|
using BTCPayServer.Lightning.LND;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests.Lnd
|
namespace BTCPayServer.Tests.Lnd
|
||||||
@@ -16,12 +16,12 @@ namespace BTCPayServer.Tests.Lnd
|
|||||||
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
||||||
|
|
||||||
Swagger = new LndSwaggerClient(new LndRestSettings(new Uri(url)) { AllowInsecure = true });
|
Swagger = new LndSwaggerClient(new LndRestSettings(new Uri(url)) { AllowInsecure = true });
|
||||||
Client = new LndInvoiceClient(Swagger);
|
Client = new LndClient(Swagger, network);
|
||||||
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LndSwaggerClient Swagger { get; set; }
|
public LndSwaggerClient Swagger { get; set; }
|
||||||
public LndInvoiceClient Client { get; set; }
|
public LndClient Client { get; set; }
|
||||||
public string P2PHost { get; }
|
public string P2PHost { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,246 +0,0 @@
|
|||||||
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;
|
|
||||||
using Xunit.Sdk;
|
|
||||||
|
|
||||||
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 = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53280")) { AllowInsecure = true });
|
|
||||||
InvoiceClient = new LndInvoiceClient(MerchantLnd);
|
|
||||||
|
|
||||||
CustomerLnd = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53281")) { AllowInsecure = true });
|
|
||||||
}
|
|
||||||
|
|
||||||
private LndSwaggerClient MerchantLnd { get; set; }
|
|
||||||
private LndInvoiceClient InvoiceClient { get; set; }
|
|
||||||
|
|
||||||
private LndSwaggerClient 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Play()
|
|
||||||
{
|
|
||||||
var seq = new System.Buffers.ReadOnlySequence<byte>(new ReadOnlyMemory<byte>(new byte[1000]));
|
|
||||||
var seq2 = seq.Slice(3);
|
|
||||||
var pos = seq2.GetPosition(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// integration tests
|
|
||||||
[Fact]
|
|
||||||
public async Task TestWaitListenInvoice()
|
|
||||||
{
|
|
||||||
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
|
||||||
var merchantInvoice2 = 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);
|
|
||||||
|
|
||||||
var waitTask2 = listener.WaitInvoice(waitToken);
|
|
||||||
|
|
||||||
payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
|
||||||
{
|
|
||||||
Payment_request = merchantInvoice2.BOLT11
|
|
||||||
});
|
|
||||||
|
|
||||||
invoice = await waitTask2;
|
|
||||||
Assert.True(invoice.PaidAt.HasValue);
|
|
||||||
|
|
||||||
var waitTask3 = listener.WaitInvoice(waitToken);
|
|
||||||
await Task.Delay(100);
|
|
||||||
listener.Dispose();
|
|
||||||
Assert.Throws<TaskCanceledException>(() => waitTask3.GetAwaiter().GetResult());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CreateLndInvoiceAndPay()
|
|
||||||
{
|
|
||||||
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
|
||||||
|
|
||||||
await EnsureLightningChannelAsync();
|
|
||||||
|
|
||||||
await EventuallyAsync(async () =>
|
|
||||||
{
|
|
||||||
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
|
||||||
{
|
|
||||||
Payment_request = merchantInvoice.BOLT11
|
|
||||||
});
|
|
||||||
|
|
||||||
var invoice = await InvoiceClient.GetInvoice(merchantInvoice.Id);
|
|
||||||
Assert.True(invoice.PaidAt.HasValue);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task EventuallyAsync(Func<Task> act)
|
|
||||||
{
|
|
||||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await act();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
await Task.Delay(500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,10 +18,10 @@ using System.Text;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using BTCPayServer.Payments.Lightning.CLightning;
|
|
||||||
using BTCPayServer.Payments.Lightning.Charge;
|
|
||||||
using BTCPayServer.Tests.Lnd;
|
using BTCPayServer.Tests.Lnd;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
using BTCPayServer.Lightning.CLightning;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
@@ -49,8 +49,8 @@ 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 = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
|
CustomerLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
|
||||||
MerchantLightningD = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
|
MerchantLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
|
||||||
|
|
||||||
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
|
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
|
||||||
|
|
||||||
@@ -78,88 +78,24 @@ namespace BTCPayServer.Tests
|
|||||||
PayTester.Start();
|
PayTester.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Connect a customer LN node to the merchant LN node
|
|
||||||
/// </summary>
|
|
||||||
public void PrepareLightning(LightningConnectionType lndBackend)
|
|
||||||
{
|
|
||||||
ILightningInvoiceClient client = MerchantCharge.Client;
|
|
||||||
if (lndBackend == LightningConnectionType.LndREST)
|
|
||||||
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>
|
/// <summary>
|
||||||
/// Connect a customer LN node to the merchant LN node
|
/// Connect a customer LN node to the merchant LN node
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private async Task PrepareLightningAsync(ILightningInvoiceClient client)
|
public Task EnsureConnectedToDestinations()
|
||||||
{
|
{
|
||||||
bool awaitingLocking = false;
|
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
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 => !SKIPPED_STATES.Contains(c.State ?? ""))
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
switch (channel?.State)
|
|
||||||
{
|
|
||||||
case null:
|
|
||||||
var address = await CustomerLightningD.NewAddressAsync();
|
|
||||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.5m));
|
|
||||||
ExplorerNode.Generate(1);
|
|
||||||
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
|
||||||
await Task.Delay(1000);
|
|
||||||
|
|
||||||
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(awaitingLocking ? 1 : 10);
|
|
||||||
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
|
||||||
awaitingLocking = true;
|
|
||||||
break;
|
|
||||||
case "CHANNELD_NORMAL":
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
throw new NotSupportedException(channel?.State ?? "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<LightningNodeInformation> WaitLNSynched(params ILightningInvoiceClient[] clients)
|
private IEnumerable<ILightningClient> GetLightningSenderClients()
|
||||||
{
|
{
|
||||||
while (true)
|
yield return CustomerLightningD;
|
||||||
{
|
|
||||||
var blockCount = await ExplorerNode.GetBlockCountAsync();
|
|
||||||
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)
|
private IEnumerable<ILightningClient> GetLightningDestClients()
|
||||||
{
|
{
|
||||||
var merchantInfo = await client.GetInfo();
|
yield return MerchantLightningD;
|
||||||
if (merchantInfo.BlockHeight == blockCount)
|
yield return MerchantLnd.Client;
|
||||||
{
|
|
||||||
return merchantInfo;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SendLightningPayment(Invoice invoice)
|
public void SendLightningPayment(Invoice invoice)
|
||||||
@@ -171,12 +107,12 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
|
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
|
||||||
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
|
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
|
||||||
await CustomerLightningD.SendAsync(bolt11);
|
await CustomerLightningD.Pay(bolt11);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CLightningRPCClient CustomerLightningD { get; set; }
|
public ILightningClient CustomerLightningD { get; set; }
|
||||||
|
|
||||||
public CLightningRPCClient MerchantLightningD { get; private set; }
|
public ILightningClient MerchantLightningD { get; private set; }
|
||||||
public ChargeTester MerchantCharge { get; private set; }
|
public ChargeTester MerchantCharge { get; private set; }
|
||||||
public LndMockTester MerchantLnd { get; set; }
|
public LndMockTester MerchantLnd { get; set; }
|
||||||
|
|
||||||
@@ -218,7 +154,7 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
foreach(var store in Stores)
|
foreach (var store in Stores)
|
||||||
{
|
{
|
||||||
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
|
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ using NBXplorer.DerivationStrategy;
|
|||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Tests.Logging;
|
using BTCPayServer.Tests.Logging;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
using BTCPayServer.Lightning.CLightning;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
@@ -133,7 +135,7 @@ namespace BTCPayServer.Tests
|
|||||||
if (connectionType == LightningConnectionType.Charge)
|
if (connectionType == LightningConnectionType.Charge)
|
||||||
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
|
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
|
||||||
else if (connectionType == LightningConnectionType.CLightning)
|
else if (connectionType == LightningConnectionType.CLightning)
|
||||||
connectionString = "type=clightning;server=" + parent.MerchantLightningD.Address.AbsoluteUri;
|
connectionString = "type=clightning;server=" + ((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
|
||||||
else if (connectionType == LightningConnectionType.LndREST)
|
else if (connectionType == LightningConnectionType.LndREST)
|
||||||
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
|
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ using BTCPayServer.Rating;
|
|||||||
using BTCPayServer.Validation;
|
using BTCPayServer.Validation;
|
||||||
using ExchangeSharp;
|
using ExchangeSharp;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
@@ -387,18 +388,6 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanUseLightMoney()
|
|
||||||
{
|
|
||||||
var light = LightMoney.MilliSatoshis(1);
|
|
||||||
Assert.Equal("0.00000000001", light.ToString());
|
|
||||||
|
|
||||||
light = LightMoney.MilliSatoshis(200000);
|
|
||||||
Assert.Equal(200m, light.ToDecimal(LightMoneyUnit.Satoshi));
|
|
||||||
Assert.Equal(0.00000001m * 200m, light.ToDecimal(LightMoneyUnit.BTC));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
[Trait("Flaky", "Flaky")]
|
|
||||||
public void CanSetLightningServer()
|
public void CanSetLightningServer()
|
||||||
{
|
{
|
||||||
using (var tester = ServerTester.Create())
|
using (var tester = ServerTester.Create())
|
||||||
@@ -435,139 +424,24 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanParseLightningURL()
|
public async Task CanSendLightningPaymentCLightning()
|
||||||
{
|
{
|
||||||
LightningConnectionString conn = null;
|
await ProcessLightningPayment(LightningConnectionType.CLightning);
|
||||||
Assert.True(LightningConnectionString.TryParse("/test/a", true, out conn));
|
|
||||||
for (int i = 0; i < 2; i++)
|
|
||||||
{
|
|
||||||
if (i == 1)
|
|
||||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
|
||||||
Assert.Equal(i == 0, conn.IsLegacy);
|
|
||||||
Assert.Equal("type=clightning;server=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", true, out conn));
|
|
||||||
for (int i = 0; i < 2; i++)
|
|
||||||
{
|
|
||||||
if (i == 1)
|
|
||||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
|
||||||
Assert.Equal("type=clightning;server=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", true, out conn));
|
|
||||||
for (int i = 0; i < 2; i++)
|
|
||||||
{
|
|
||||||
if (i == 1)
|
|
||||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
|
||||||
Assert.Equal("type=clightning;server=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", true, out conn));
|
|
||||||
for (int i = 0; i < 2; i++)
|
|
||||||
{
|
|
||||||
if (i == 1)
|
|
||||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
|
||||||
Assert.Equal("type=clightning;server=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", true, out conn));
|
|
||||||
for (int i = 0; i < 2; i++)
|
|
||||||
{
|
|
||||||
if (i == 1)
|
|
||||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
|
||||||
Assert.Equal("type=charge;server=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.True(LightningConnectionString.TryParse("http://api-token:bbb@test/a", true, out conn));
|
|
||||||
for (int i = 0; i < 2; i++)
|
|
||||||
{
|
|
||||||
if (i == 1)
|
|
||||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
|
||||||
Assert.Equal("type=charge;server=http://test/a;api-token=bbb", conn.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert.False(LightningConnectionString.TryParse("lol://aaa:bbb@test/a", true, out conn));
|
|
||||||
Assert.False(LightningConnectionString.TryParse("https://test/a", true, out conn));
|
|
||||||
Assert.False(LightningConnectionString.TryParse("unix://dwewoi:dwdwqd@test/a", true, out conn));
|
|
||||||
Assert.False(LightningConnectionString.TryParse("tcp://test/a", false, out conn));
|
|
||||||
Assert.False(LightningConnectionString.TryParse("type=charge;server=http://aaa:bbb@test/a;unk=lol", false, out conn));
|
|
||||||
Assert.False(LightningConnectionString.TryParse("type=charge;server=tcp://aaa:bbb@test/a", false, out conn));
|
|
||||||
Assert.False(LightningConnectionString.TryParse("type=charge", false, out conn));
|
|
||||||
Assert.False(LightningConnectionString.TryParse("type=clightning", false, out conn));
|
|
||||||
Assert.True(LightningConnectionString.TryParse("type=clightning;server=tcp://aaa:bbb@test/a", false, out conn));
|
|
||||||
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 certthumbprint = "c51bb1d402306d0da00e85581b32aa56166bcbab7eb888ff925d7167eb436d06";
|
|
||||||
|
|
||||||
// We get this format from "openssl x509 -noout -fingerprint -sha256 -inform pem -in <certificate>"
|
|
||||||
var certthumbprint2 = "C5:1B:B1:D4:02:30:6D:0D:A0:0E:85:58:1B:32:AA:56:16:6B:CB:AB:7E:B8:88:FF:92:5D:71:67:EB:43:6D:06";
|
|
||||||
|
|
||||||
var lndUri = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint}";
|
|
||||||
var lndUri2 = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint2}";
|
|
||||||
|
|
||||||
var certificateHash = new X509Certificate2(Encoders.Hex.DecodeData("2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494942396a4343415a7967417749424167495156397a62474252724e54716b4e4b55676d72524d377a414b42676771686b6a4f50515144416a41784d5238770a485159445651514b45785a73626d5167595856306232646c626d56795958526c5a43426a5a584a304d51347744415944565151444577564754304e56557a41650a467730784f4441304d6a55794d7a517a4d6a4261467730784f5441324d6a41794d7a517a4d6a42614d444578487a416442674e5642416f54466d78755a4342680a645852765a3256755a584a686447566b49474e6c636e5178446a414d42674e5642414d5442555a50513156544d466b77457759484b6f5a497a6a3043415159490a4b6f5a497a6a304441516344516741454b7557424568564f75707965434157476130766e713262712f59396b41755a78616865646d454553482b753936436d450a397577486b4b2b4a7667547a66385141783550513741357254637155374b57595170303175364f426c5443426b6a414f42674e56485138424166384542414d430a4171517744775944565230544151482f42415577417745422f7a427642674e56485245456144426d6767564754304e565534494a6247396a5957786f62334e300a6877522f4141414268784141414141414141414141414141414141414141414268775373474f69786877514b41457342687753702f717473687754417141724c0a687753702f6d4a72687753702f754f77687753702f714e59687753702f6874436877514b70514157687753702f6c42514d416f4743437147534d343942414d430a413067414d45554349464866716d595a5043647a4a5178386b47586859473834394c31766541364c784d6f7a4f5774356d726835416945413662756e51556c710a6558553070474168776c3041654d726a4d4974394c7652736179756162565a593278343d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a"))
|
|
||||||
.GetCertHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
|
|
||||||
|
|
||||||
|
|
||||||
Assert.True(LightningConnectionString.TryParse(lndUri, false, out conn));
|
|
||||||
Assert.True(LightningConnectionString.TryParse(lndUri2, false, out var conn2));
|
|
||||||
Assert.Equal(conn2.ToString(), conn.ToString());
|
|
||||||
Assert.Equal(lndUri, conn.ToString());
|
|
||||||
Assert.Equal(LightningConnectionType.LndREST, conn.ConnectionType);
|
|
||||||
Assert.Equal(macaroon, Encoders.Hex.EncodeData(conn.Macaroon));
|
|
||||||
Assert.Equal(certthumbprint.Replace(":", "", StringComparison.OrdinalIgnoreCase).ToLowerInvariant(), Encoders.Hex.EncodeData(conn.CertificateThumbprint));
|
|
||||||
Assert.True(certificateHash.SequenceEqual(conn.CertificateThumbprint));
|
|
||||||
|
|
||||||
// AllowInsecure can be set to allow http
|
|
||||||
Assert.False(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=false", false, out conn2));
|
|
||||||
Assert.True(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=true", false, out conn2));
|
|
||||||
Assert.True(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=true", false, out conn2));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Flaky", "Flaky")]
|
public async Task CanSendLightningPaymentCharge()
|
||||||
public void CanSendLightningPaymentCLightning()
|
|
||||||
{
|
{
|
||||||
ProcessLightningPayment(LightningConnectionType.CLightning);
|
await ProcessLightningPayment(LightningConnectionType.Charge);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Flaky", "Flaky")]
|
public async Task CanSendLightningPaymentLnd()
|
||||||
public void CanSendLightningPaymentCharge()
|
|
||||||
{
|
{
|
||||||
ProcessLightningPayment(LightningConnectionType.Charge);
|
await ProcessLightningPayment(LightningConnectionType.LndREST);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
async Task ProcessLightningPayment(LightningConnectionType type)
|
||||||
[Trait("Flaky", "Flaky")]
|
|
||||||
public void CanSendLightningPaymentLnd()
|
|
||||||
{
|
|
||||||
ProcessLightningPayment(LightningConnectionType.LndREST);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProcessLightningPayment(LightningConnectionType type)
|
|
||||||
{
|
{
|
||||||
// For easier debugging and testing
|
// For easier debugging and testing
|
||||||
// LightningLikePaymentHandler.LIGHTNING_TIMEOUT = int.MaxValue;
|
// LightningLikePaymentHandler.LIGHTNING_TIMEOUT = int.MaxValue;
|
||||||
@@ -580,11 +454,11 @@ namespace BTCPayServer.Tests
|
|||||||
user.RegisterLightningNode("BTC", type);
|
user.RegisterLightningNode("BTC", type);
|
||||||
user.RegisterDerivationScheme("BTC");
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
|
||||||
tester.PrepareLightning(type);
|
await tester.EnsureConnectedToDestinations();
|
||||||
|
|
||||||
Task.WaitAll(CanSendLightningPaymentCore(tester, user));
|
await CanSendLightningPaymentCore(tester, user);
|
||||||
|
|
||||||
Task.WaitAll(Enumerable.Range(0, 5)
|
await Task.WhenAll(Enumerable.Range(0, 5)
|
||||||
.Select(_ => CanSendLightningPaymentCore(tester, user))
|
.Select(_ => CanSendLightningPaymentCore(tester, user))
|
||||||
.ToArray());
|
.ToArray());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
<EmbeddedResource Include="Currencies.txt" />
|
<EmbeddedResource Include="Currencies.txt" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.0.0.4" />
|
||||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
|
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
|
||||||
<PackageReference Include="Hangfire" Version="1.6.19" />
|
<PackageReference Include="Hangfire" Version="1.6.19" />
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using BTCPayServer.Payments.Lightning;
|
|||||||
using Renci.SshNet;
|
using Renci.SshNet;
|
||||||
using NBitcoin.DataEncoders;
|
using NBitcoin.DataEncoders;
|
||||||
using BTCPayServer.SSH;
|
using BTCPayServer.SSH;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
|
||||||
namespace BTCPayServer.Configuration
|
namespace BTCPayServer.Configuration
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ using System.Net.Mail;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Renci.SshNet;
|
using Renci.SshNet;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Lightning.CLightning;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ namespace BTCPayServer.Hosting
|
|||||||
}
|
}
|
||||||
return dbContext;
|
return dbContext;
|
||||||
});
|
});
|
||||||
services.TryAddSingleton<Payments.Lightning.LightningClientFactory>();
|
|
||||||
|
|
||||||
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
|
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System.Reflection;
|
|
||||||
using BTCPayServer.Payments.Lightning;
|
|
||||||
using NBitcoin.JsonConverters;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace BTCPayServer.JsonConverters
|
|
||||||
{
|
|
||||||
public class LightMoneyJsonConverter : JsonConverter
|
|
||||||
{
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
return typeof(LightMoneyJsonConverter).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
|
||||||
}
|
|
||||||
|
|
||||||
Type longType = typeof(long).GetTypeInfo();
|
|
||||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return reader.TokenType == JsonToken.Null ? null :
|
|
||||||
reader.TokenType == JsonToken.Integer ?
|
|
||||||
longType.IsAssignableFrom(reader.ValueType) ? new LightMoney((long)reader.Value)
|
|
||||||
: new LightMoney(long.MaxValue) :
|
|
||||||
reader.TokenType == JsonToken.String ? new LightMoney(long.Parse((string)reader.Value, CultureInfo.InvariantCulture))
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
catch (InvalidCastException)
|
|
||||||
{
|
|
||||||
throw new JsonObjectException("Money amount should be in millisatoshi", reader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
writer.WriteValue(((LightMoney)value).MilliSatoshi);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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 CLightningInvoice
|
|
||||||
{
|
|
||||||
[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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Mono.Unix;
|
|
||||||
using NBitcoin;
|
|
||||||
using NBitcoin.RPC;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
|
||||||
{
|
|
||||||
public class LightningRPCException : Exception
|
|
||||||
{
|
|
||||||
public LightningRPCException(string message) : base(message)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public class CLightningRPCClient : ILightningInvoiceClient, ILightningListenInvoiceSession
|
|
||||||
{
|
|
||||||
public Network Network { get; private set; }
|
|
||||||
public Uri Address { get; private set; }
|
|
||||||
|
|
||||||
public CLightningRPCClient(Uri address, Network network)
|
|
||||||
{
|
|
||||||
if (address == null)
|
|
||||||
throw new ArgumentNullException(nameof(address));
|
|
||||||
if (network == null)
|
|
||||||
throw new ArgumentNullException(nameof(network));
|
|
||||||
if(address.Scheme == "file")
|
|
||||||
{
|
|
||||||
address = new UriBuilder(address) { Scheme = "unix" }.Uri;
|
|
||||||
}
|
|
||||||
Address = address;
|
|
||||||
Network = network;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Charge.GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
|
|
||||||
{
|
|
||||||
return SendCommandAsync<Charge.GetInfoResponse>("getinfo", cancellation: cancellation);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SendAsync(string bolt11)
|
|
||||||
{
|
|
||||||
return SendCommandAsync<object>("pay", new[] { bolt11 }, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<PeerInfo[]> ListPeersAsync()
|
|
||||||
{
|
|
||||||
var peers = await SendCommandAsync<PeerInfo[]>("listpeers", isArray: true);
|
|
||||||
foreach (var peer in peers)
|
|
||||||
{
|
|
||||||
peer.Channels = peer.Channels ?? Array.Empty<ChannelInfo>();
|
|
||||||
}
|
|
||||||
return peers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task FundChannelAsync(NodeInfo nodeInfo, Money money)
|
|
||||||
{
|
|
||||||
return SendCommandAsync<object>("fundchannel", new object[] { nodeInfo.NodeId, money.Satoshi }, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task ConnectAsync(NodeInfo nodeInfo)
|
|
||||||
{
|
|
||||||
return SendCommandAsync<object>("connect", new[] { $"{nodeInfo.NodeId}@{nodeInfo.Host}:{nodeInfo.Port}" }, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Encoding UTF8 = new UTF8Encoding(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>();
|
|
||||||
using (Socket socket = await Connect())
|
|
||||||
{
|
|
||||||
using (var networkStream = new NetworkStream(socket))
|
|
||||||
{
|
|
||||||
using (var textWriter = new StreamWriter(networkStream, UTF8, 1024 * 10, true))
|
|
||||||
{
|
|
||||||
using (var jsonWriter = new JsonTextWriter(textWriter))
|
|
||||||
{
|
|
||||||
var req = new JObject();
|
|
||||||
req.Add("id", 0);
|
|
||||||
req.Add("method", command);
|
|
||||||
req.Add("params", new JArray(parameters));
|
|
||||||
await req.WriteToAsync(jsonWriter, cancellation);
|
|
||||||
await jsonWriter.FlushAsync(cancellation);
|
|
||||||
}
|
|
||||||
await textWriter.FlushAsync();
|
|
||||||
}
|
|
||||||
await networkStream.FlushAsync(cancellation);
|
|
||||||
using (var textReader = new StreamReader(networkStream, UTF8, false, 1024 * 10, true))
|
|
||||||
{
|
|
||||||
using (var jsonReader = new JsonTextReader(textReader))
|
|
||||||
{
|
|
||||||
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>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Socket> Connect()
|
|
||||||
{
|
|
||||||
Socket socket = null;
|
|
||||||
EndPoint endpoint = null;
|
|
||||||
if (Address.Scheme == "tcp" || Address.Scheme == "tcp")
|
|
||||||
{
|
|
||||||
var domain = Address.DnsSafeHost;
|
|
||||||
if (!IPAddress.TryParse(domain, out IPAddress address))
|
|
||||||
{
|
|
||||||
address = (await Dns.GetHostAddressesAsync(domain)).FirstOrDefault();
|
|
||||||
if (address == null)
|
|
||||||
throw new Exception("Host not found");
|
|
||||||
}
|
|
||||||
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
||||||
endpoint = new IPEndPoint(address, Address.Port);
|
|
||||||
}
|
|
||||||
else if (Address.Scheme == "unix")
|
|
||||||
{
|
|
||||||
var path = Address.AbsoluteUri.Remove(0, "unix:".Length);
|
|
||||||
if (!path.StartsWith('/'))
|
|
||||||
path = "/" + path;
|
|
||||||
while (path.Length >= 2 && (path[0] != '/' || path[1] == '/'))
|
|
||||||
{
|
|
||||||
path = path.Remove(0, 1);
|
|
||||||
}
|
|
||||||
if (path.Length < 2)
|
|
||||||
throw new FormatException("Invalid unix url");
|
|
||||||
socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
|
|
||||||
endpoint = new UnixEndPoint(path);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
throw new NotSupportedException($"Protocol {Address.Scheme} for clightning not supported");
|
|
||||||
|
|
||||||
await socket.ConnectAsync(endpoint);
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<BitcoinAddress> NewAddressAsync()
|
|
||||||
{
|
|
||||||
var obj = await SendCommandAsync<JObject>("newaddr");
|
|
||||||
return BitcoinAddress.Create(obj.Property("address").Value.Value<string>(), Network);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
|
|
||||||
{
|
|
||||||
var invoices = await SendCommandAsync<CLightningInvoice[]>("listinvoices", new[] { invoiceId }, false, true, cancellation);
|
|
||||||
if (invoices.Length == 0)
|
|
||||||
return null;
|
|
||||||
return ToLightningInvoice(invoices[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
static NBitcoin.DataEncoders.DataEncoder InvoiceIdEncoder = NBitcoin.DataEncoders.Encoders.Base58;
|
|
||||||
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
|
|
||||||
{
|
|
||||||
var id = InvoiceIdEncoder.EncodeData(RandomUtils.GetBytes(20));
|
|
||||||
var invoice = await SendCommandAsync<CLightningInvoice>("invoice", new object[] { amount.MilliSatoshi, id, description ?? "", Math.Max(0, (int)expiry.TotalSeconds) }, cancellation: cancellation);
|
|
||||||
invoice.Label = id;
|
|
||||||
invoice.MilliSatoshi = amount;
|
|
||||||
invoice.Status = "unpaid";
|
|
||||||
return ToLightningInvoice(invoice);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LightningInvoice ToLightningInvoice(CLightningInvoice 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 invoice = await SendCommandAsync<CLightningInvoice>("waitanyinvoice", new object[] { lastInvoiceIndex }, cancellation: cancellation);
|
|
||||||
lastInvoiceIndex = invoice.PayIndex.Value;
|
|
||||||
return ToLightningInvoice(invoice);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<LightningNodeInformation> ILightningInvoiceClient.GetInfo(CancellationToken cancellation)
|
|
||||||
{
|
|
||||||
var info = await GetInfoAsync(cancellation);
|
|
||||||
return ToLightningNodeInformation(info);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static LightningNodeInformation ToLightningNodeInformation(Charge.GetInfoResponse info)
|
|
||||||
{
|
|
||||||
var addr = info.Address.FirstOrDefault();
|
|
||||||
if (addr == null)
|
|
||||||
{
|
|
||||||
addr = new Charge.GetInfoResponse.GetInfoAddress();
|
|
||||||
addr.Address = "127.0.0.1";
|
|
||||||
}
|
|
||||||
if (addr.Port == 0)
|
|
||||||
{
|
|
||||||
addr.Port = 9735;
|
|
||||||
}
|
|
||||||
return new LightningNodeInformation()
|
|
||||||
{
|
|
||||||
NodeId = info.Id,
|
|
||||||
P2PPort = addr.Port,
|
|
||||||
Address = addr.Address,
|
|
||||||
BlockHeight = info.BlockHeight
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void IDisposable.Dispose()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
|
||||||
{
|
|
||||||
public class NodeInfo
|
|
||||||
{
|
|
||||||
public NodeInfo(string nodeId, string host, int port)
|
|
||||||
{
|
|
||||||
if (host == null)
|
|
||||||
throw new ArgumentNullException(nameof(host));
|
|
||||||
if (nodeId == null)
|
|
||||||
throw new ArgumentNullException(nameof(nodeId));
|
|
||||||
Port = port;
|
|
||||||
Host = host;
|
|
||||||
NodeId = nodeId;
|
|
||||||
}
|
|
||||||
public string NodeId { get; private set; }
|
|
||||||
public string Host { get; private set; }
|
|
||||||
public int Port { get; private set; }
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"{NodeId}@{Host}:{Port}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
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 ChannelInfo
|
|
||||||
{
|
|
||||||
public string State { get; set; }
|
|
||||||
public string Owner { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("funding_txid")]
|
|
||||||
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
|
|
||||||
public uint256 FundingTxId { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("msatoshi_to_us")]
|
|
||||||
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
|
||||||
public LightMoney ToUs { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("msatoshi_total")]
|
|
||||||
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
|
||||||
public LightMoney Total { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("dust_limit_satoshis")]
|
|
||||||
[JsonConverter(typeof(NBitcoin.JsonConverters.MoneyJsonConverter))]
|
|
||||||
public Money DustLimit { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("max_htlc_value_in_flight_msat")]
|
|
||||||
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
|
||||||
public LightMoney MaxHTLCValueInFlight { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("channel_reserve_satoshis")]
|
|
||||||
[JsonConverter(typeof(NBitcoin.JsonConverters.MoneyJsonConverter))]
|
|
||||||
public Money ChannelReserve { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("htlc_minimum_msat")]
|
|
||||||
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
|
||||||
public LightMoney HTLCMinimum { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("to_self_delay")]
|
|
||||||
public int ToSelfDelay { get; set; }
|
|
||||||
[JsonProperty("max_accepted_htlcs")]
|
|
||||||
public int MaxAcceptedHTLCS { get; set; }
|
|
||||||
public string[] Status { get; set; }
|
|
||||||
}
|
|
||||||
public class PeerInfo
|
|
||||||
{
|
|
||||||
public string State { get; set; }
|
|
||||||
public string Id { get; set; }
|
|
||||||
[JsonProperty("netaddr")]
|
|
||||||
public string[] NetworkAddresses { get; set; }
|
|
||||||
public bool Connected { get; set; }
|
|
||||||
public string Owner { get; set; }
|
|
||||||
public ChannelInfo[] Channels { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
//
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Net.WebSockets;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using NBitcoin;
|
|
||||||
using NBXplorer;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.Charge
|
|
||||||
{
|
|
||||||
public class ChargeClient : ILightningInvoiceClient
|
|
||||||
{
|
|
||||||
private Uri _Uri;
|
|
||||||
public Uri Uri
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return _Uri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private Network _Network;
|
|
||||||
static HttpClient _Client = new HttpClient();
|
|
||||||
|
|
||||||
public ChargeClient(Uri uri, Network network)
|
|
||||||
{
|
|
||||||
if (uri == null)
|
|
||||||
throw new ArgumentNullException(nameof(uri));
|
|
||||||
if (network == null)
|
|
||||||
throw new ArgumentNullException(nameof(network));
|
|
||||||
this._Uri = uri;
|
|
||||||
this._Network = network;
|
|
||||||
if (uri.UserInfo == null)
|
|
||||||
throw new ArgumentException(paramName: nameof(uri), message: "User information not present in uri");
|
|
||||||
var userInfo = uri.UserInfo.Split(':');
|
|
||||||
if (userInfo.Length != 2)
|
|
||||||
throw new ArgumentException(paramName: nameof(uri), message: "User information not present in uri");
|
|
||||||
Credentials = new NetworkCredential(userInfo[0], userInfo[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CreateInvoiceResponse> CreateInvoiceAsync(CreateInvoiceRequest request, CancellationToken cancellation = default(CancellationToken))
|
|
||||||
{
|
|
||||||
var message = CreateMessage(HttpMethod.Post, "invoice");
|
|
||||||
Dictionary<string, string> parameters = new Dictionary<string, string>();
|
|
||||||
parameters.Add("msatoshi", request.Amount.MilliSatoshi.ToString(CultureInfo.InvariantCulture));
|
|
||||||
parameters.Add("expiry", ((int)request.Expiry.TotalSeconds).ToString(CultureInfo.InvariantCulture));
|
|
||||||
if(request.Description != null)
|
|
||||||
parameters.Add("description", request.Description);
|
|
||||||
message.Content = new FormUrlEncodedContent(parameters);
|
|
||||||
var result = await _Client.SendAsync(message, cancellation);
|
|
||||||
result.EnsureSuccessStatusCode();
|
|
||||||
var content = await result.Content.ReadAsStringAsync();
|
|
||||||
return JsonConvert.DeserializeObject<CreateInvoiceResponse>(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ChargeSession> Listen(CancellationToken cancellation = default(CancellationToken))
|
|
||||||
{
|
|
||||||
var socket = new ClientWebSocket();
|
|
||||||
socket.Options.SetRequestHeader("Authorization", $"Basic {GetBase64Creds()}");
|
|
||||||
var uri = new UriBuilder(Uri) { UserName = null, Password = null }.Uri.AbsoluteUri;
|
|
||||||
if (!uri.EndsWith('/'))
|
|
||||||
uri += "/";
|
|
||||||
uri += "ws";
|
|
||||||
uri = ToWebsocketUri(uri);
|
|
||||||
await socket.ConnectAsync(new Uri(uri), cancellation);
|
|
||||||
return new ChargeSession(socket);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ToWebsocketUri(string uri)
|
|
||||||
{
|
|
||||||
if (uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
|
||||||
uri = uri.Replace("https://", "wss://", StringComparison.OrdinalIgnoreCase);
|
|
||||||
if (uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
|
|
||||||
uri = uri.Replace("http://", "ws://", StringComparison.OrdinalIgnoreCase);
|
|
||||||
return uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public NetworkCredential Credentials { get; set; }
|
|
||||||
|
|
||||||
public GetInfoResponse GetInfo()
|
|
||||||
{
|
|
||||||
return GetInfoAsync().GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ChargeInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken))
|
|
||||||
{
|
|
||||||
var request = CreateMessage(HttpMethod.Get, $"invoice/{invoiceId}");
|
|
||||||
var message = await _Client.SendAsync(request, cancellation);
|
|
||||||
if (message.StatusCode == HttpStatusCode.NotFound)
|
|
||||||
return null;
|
|
||||||
message.EnsureSuccessStatusCode();
|
|
||||||
var content = await message.Content.ReadAsStringAsync();
|
|
||||||
return JsonConvert.DeserializeObject<ChargeInvoice>(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
|
|
||||||
{
|
|
||||||
var request = CreateMessage(HttpMethod.Get, "info");
|
|
||||||
var message = await _Client.SendAsync(request, cancellation);
|
|
||||||
message.EnsureSuccessStatusCode();
|
|
||||||
var content = await message.Content.ReadAsStringAsync();
|
|
||||||
return JsonConvert.DeserializeObject<GetInfoResponse>(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpRequestMessage CreateMessage(HttpMethod method, string path)
|
|
||||||
{
|
|
||||||
var uri = GetFullUri(path);
|
|
||||||
var request = new HttpRequestMessage(method, uri);
|
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", GetBase64Creds());
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetBase64Creds()
|
|
||||||
{
|
|
||||||
return Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Credentials.UserName}:{Credentials.Password}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Uri GetFullUri(string partialUrl)
|
|
||||||
{
|
|
||||||
var uri = _Uri.AbsoluteUri;
|
|
||||||
if (!uri.EndsWith("/", StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
uri += "/";
|
|
||||||
return new Uri(uri + partialUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
|
|
||||||
{
|
|
||||||
var invoice = await GetInvoice(invoiceId, cancellation);
|
|
||||||
if (invoice == null)
|
|
||||||
return null;
|
|
||||||
return ChargeClient.ToLightningInvoice(invoice);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<ILightningListenInvoiceSession> ILightningInvoiceClient.Listen(CancellationToken cancellation)
|
|
||||||
{
|
|
||||||
return await Listen(cancellation);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static LightningInvoice ToLightningInvoice(ChargeInvoice invoice)
|
|
||||||
{
|
|
||||||
return new LightningInvoice()
|
|
||||||
{
|
|
||||||
Id = invoice.Id ?? invoice.Label,
|
|
||||||
Amount = invoice.MilliSatoshi,
|
|
||||||
BOLT11 = invoice.PaymentRequest,
|
|
||||||
PaidAt = invoice.PaidAt,
|
|
||||||
Status = invoice.Status
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
|
|
||||||
{
|
|
||||||
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" }, cancellation);
|
|
||||||
return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" };
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<LightningNodeInformation> ILightningInvoiceClient.GetInfo(CancellationToken cancellation)
|
|
||||||
{
|
|
||||||
var info = await GetInfoAsync(cancellation);
|
|
||||||
return CLightning.CLightningRPCClient.ToLightningNodeInformation(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.WebSockets;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using NBXplorer;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.Charge
|
|
||||||
{
|
|
||||||
public class ChargeInvoice
|
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("msatoshi")]
|
|
||||||
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
|
||||||
public LightMoney MilliSatoshi { get; set; }
|
|
||||||
[JsonProperty("paid_at")]
|
|
||||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
|
||||||
public DateTimeOffset? PaidAt { get; set; }
|
|
||||||
[JsonProperty("expires_at")]
|
|
||||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
|
||||||
public DateTimeOffset? ExpiresAt { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("payreq")]
|
|
||||||
public string PaymentRequest { get; set; }
|
|
||||||
public string Label { get; set; }
|
|
||||||
}
|
|
||||||
public class ChargeSession : ILightningListenInvoiceSession
|
|
||||||
{
|
|
||||||
private ClientWebSocket socket;
|
|
||||||
|
|
||||||
const int ORIGINAL_BUFFER_SIZE = 1024 * 5;
|
|
||||||
const int MAX_BUFFER_SIZE = 1024 * 1024 * 5;
|
|
||||||
public ChargeSession(ClientWebSocket socket)
|
|
||||||
{
|
|
||||||
this.socket = socket;
|
|
||||||
var buffer = new byte[ORIGINAL_BUFFER_SIZE];
|
|
||||||
_Buffer = new ArraySegment<byte>(buffer, 0, buffer.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
ArraySegment<byte> _Buffer;
|
|
||||||
public async Task<ChargeInvoice> WaitInvoice(CancellationToken cancellation = default(CancellationToken))
|
|
||||||
{
|
|
||||||
var buffer = _Buffer;
|
|
||||||
var array = _Buffer.Array;
|
|
||||||
var originalSize = _Buffer.Array.Length;
|
|
||||||
var newSize = _Buffer.Array.Length;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var message = await socket.ReceiveAsync(buffer, cancellation);
|
|
||||||
if (message.MessageType == WebSocketMessageType.Close)
|
|
||||||
{
|
|
||||||
await CloseSocketAndThrow(WebSocketCloseStatus.NormalClosure, "Close message received from the peer", cancellation);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (message.MessageType != WebSocketMessageType.Text)
|
|
||||||
{
|
|
||||||
await CloseSocketAndThrow(WebSocketCloseStatus.InvalidMessageType, "Only Text is supported", cancellation);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (message.EndOfMessage)
|
|
||||||
{
|
|
||||||
buffer = new ArraySegment<byte>(array, 0, buffer.Offset + message.Count);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var o = ParseMessage(buffer);
|
|
||||||
if (newSize != originalSize)
|
|
||||||
{
|
|
||||||
Array.Resize(ref array, originalSize);
|
|
||||||
}
|
|
||||||
return o;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await CloseSocketAndThrow(WebSocketCloseStatus.InvalidPayloadData, $"Invalid payload: {ex.Message}", cancellation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (buffer.Count - message.Count <= 0)
|
|
||||||
{
|
|
||||||
newSize *= 2;
|
|
||||||
if (newSize > MAX_BUFFER_SIZE)
|
|
||||||
await CloseSocketAndThrow(WebSocketCloseStatus.MessageTooBig, "Message is too big", cancellation);
|
|
||||||
Array.Resize(ref array, newSize);
|
|
||||||
buffer = new ArraySegment<byte>(array, buffer.Offset, newSize - buffer.Offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer = buffer.Slice(message.Count, buffer.Count - message.Count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new InvalidOperationException("Should never happen");
|
|
||||||
}
|
|
||||||
|
|
||||||
UTF8Encoding UTF8 = new UTF8Encoding(false, true);
|
|
||||||
private ChargeInvoice ParseMessage(ArraySegment<byte> buffer)
|
|
||||||
{
|
|
||||||
var str = UTF8.GetString(buffer.Array, 0, buffer.Count);
|
|
||||||
return JsonConvert.DeserializeObject<ChargeInvoice>(str, new JsonSerializerSettings());
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CloseSocketAndThrow(WebSocketCloseStatus status, string description, CancellationToken cancellation)
|
|
||||||
{
|
|
||||||
var array = _Buffer.Array;
|
|
||||||
if (array.Length != ORIGINAL_BUFFER_SIZE)
|
|
||||||
Array.Resize(ref array, ORIGINAL_BUFFER_SIZE);
|
|
||||||
await socket.CloseSocket(status, description, cancellation);
|
|
||||||
throw new WebSocketException($"The socket has been closed ({status}: {description})");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void Dispose()
|
|
||||||
{
|
|
||||||
await this.socket.CloseSocket();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DisposeAsync()
|
|
||||||
{
|
|
||||||
await this.socket.CloseSocket();
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<LightningInvoice> ILightningListenInvoiceSession.WaitInvoice(CancellationToken token)
|
|
||||||
{
|
|
||||||
return ChargeClient.ToLightningInvoice(await WaitInvoice(token));
|
|
||||||
}
|
|
||||||
|
|
||||||
void IDisposable.Dispose()
|
|
||||||
{
|
|
||||||
Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.Charge
|
|
||||||
{
|
|
||||||
public class CreateInvoiceRequest
|
|
||||||
{
|
|
||||||
public LightMoney Amount { get; set; }
|
|
||||||
public TimeSpan Expiry { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.Charge
|
|
||||||
{
|
|
||||||
public class CreateInvoiceResponse
|
|
||||||
{
|
|
||||||
public string PayReq { get; set; }
|
|
||||||
public string Id { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.Charge
|
|
||||||
{
|
|
||||||
//[{"type":"ipv4","address":"52.166.90.122","port":9735}]
|
|
||||||
public class GetInfoResponse
|
|
||||||
{
|
|
||||||
public class GetInfoAddress
|
|
||||||
{
|
|
||||||
public string Type { get; set; }
|
|
||||||
public string Address { get; set; }
|
|
||||||
public int Port { get; set; }
|
|
||||||
}
|
|
||||||
public string Id { get; set; }
|
|
||||||
public GetInfoAddress[] Address { get; set; }
|
|
||||||
public string Version { get; set; }
|
|
||||||
public int BlockHeight { get; set; }
|
|
||||||
public string Network { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using BTCPayServer.Payments.Lightning.Charge;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning
|
|
||||||
{
|
|
||||||
public class LightningInvoice
|
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
public string BOLT11 { get; set; }
|
|
||||||
public DateTimeOffset? PaidAt
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
public LightMoney Amount { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LightningNodeInformation
|
|
||||||
{
|
|
||||||
public string NodeId { get; set; }
|
|
||||||
public string Address { get; internal set; }
|
|
||||||
public int P2PPort { get; internal set; }
|
|
||||||
public int BlockHeight { get; set; }
|
|
||||||
}
|
|
||||||
public interface ILightningInvoiceClient
|
|
||||||
{
|
|
||||||
Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken));
|
|
||||||
Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation = default(CancellationToken));
|
|
||||||
Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken));
|
|
||||||
Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default(CancellationToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface ILightningListenInvoiceSession : IDisposable
|
|
||||||
{
|
|
||||||
Task<LightningInvoice> WaitInvoice(CancellationToken cancellation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,573 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using NBitcoin;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning
|
|
||||||
{
|
|
||||||
public enum LightMoneyUnit : ulong
|
|
||||||
{
|
|
||||||
BTC = 100000000000,
|
|
||||||
MilliBTC = 100000000,
|
|
||||||
Bit = 100000,
|
|
||||||
Satoshi = 1000,
|
|
||||||
MilliSatoshi = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LightMoney : IComparable, IComparable<LightMoney>, IEquatable<LightMoney>
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
// for decimal.TryParse. None of the NumberStyles' composed values is useful for bitcoin style
|
|
||||||
private const NumberStyles BitcoinStyle =
|
|
||||||
NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite
|
|
||||||
| NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint;
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse a bitcoin amount (Culture Invariant)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="bitcoin"></param>
|
|
||||||
/// <param name="nRet"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static bool TryParse(string bitcoin, out LightMoney nRet)
|
|
||||||
{
|
|
||||||
nRet = null;
|
|
||||||
|
|
||||||
decimal value;
|
|
||||||
if (!decimal.TryParse(bitcoin, BitcoinStyle, CultureInfo.InvariantCulture, out value))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
nRet = new LightMoney(value, LightMoneyUnit.BTC);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (OverflowException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse a bitcoin amount (Culture Invariant)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="bitcoin"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static LightMoney Parse(string bitcoin)
|
|
||||||
{
|
|
||||||
LightMoney result;
|
|
||||||
if (TryParse(bitcoin, out result))
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
throw new FormatException("Impossible to parse the string in a bitcoin amount");
|
|
||||||
}
|
|
||||||
|
|
||||||
long _MilliSatoshis;
|
|
||||||
public long MilliSatoshi
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return _MilliSatoshis;
|
|
||||||
}
|
|
||||||
// used as a central point where long.MinValue checking can be enforced
|
|
||||||
private set
|
|
||||||
{
|
|
||||||
CheckLongMinValue(value);
|
|
||||||
_MilliSatoshis = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get absolute value of the instance
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public LightMoney Abs()
|
|
||||||
{
|
|
||||||
var a = this;
|
|
||||||
if (a < LightMoney.Zero)
|
|
||||||
a = -a;
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LightMoney(int msatoshis)
|
|
||||||
{
|
|
||||||
MilliSatoshi = msatoshis;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LightMoney(uint msatoshis)
|
|
||||||
{
|
|
||||||
MilliSatoshi = msatoshis;
|
|
||||||
}
|
|
||||||
public LightMoney(Money money)
|
|
||||||
{
|
|
||||||
MilliSatoshi = checked(money.Satoshi * 1000);
|
|
||||||
}
|
|
||||||
public LightMoney(long msatoshis)
|
|
||||||
{
|
|
||||||
MilliSatoshi = msatoshis;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LightMoney(ulong msatoshis)
|
|
||||||
{
|
|
||||||
// overflow check.
|
|
||||||
// ulong.MaxValue is greater than long.MaxValue
|
|
||||||
checked
|
|
||||||
{
|
|
||||||
MilliSatoshi = (long)msatoshis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public LightMoney(decimal amount, LightMoneyUnit unit)
|
|
||||||
{
|
|
||||||
// sanity check. Only valid units are allowed
|
|
||||||
CheckMoneyUnit(unit, "unit");
|
|
||||||
checked
|
|
||||||
{
|
|
||||||
var satoshi = amount * (long)unit;
|
|
||||||
MilliSatoshi = (long)satoshi;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Split the Money in parts without loss
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="parts">The number of parts (must be more than 0)</param>
|
|
||||||
/// <returns>The splitted money</returns>
|
|
||||||
public IEnumerable<LightMoney> Split(int parts)
|
|
||||||
{
|
|
||||||
if (parts <= 0)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(parts), "Parts should be more than 0");
|
|
||||||
long remain;
|
|
||||||
long result = DivRem(_MilliSatoshis, parts, out remain);
|
|
||||||
|
|
||||||
for (int i = 0; i < parts; i++)
|
|
||||||
{
|
|
||||||
yield return LightMoney.Satoshis(result + (remain > 0 ? 1 : 0));
|
|
||||||
remain--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long DivRem(long a, long b, out long result)
|
|
||||||
{
|
|
||||||
result = a % b;
|
|
||||||
return a / b;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LightMoney FromUnit(decimal amount, LightMoneyUnit unit)
|
|
||||||
{
|
|
||||||
return new LightMoney(amount, unit);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert Money to decimal (same as ToDecimal)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="unit"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public decimal ToUnit(LightMoneyUnit unit)
|
|
||||||
{
|
|
||||||
CheckMoneyUnit(unit, "unit");
|
|
||||||
// overflow safe because (long / int) always fit in decimal
|
|
||||||
// decimal operations are checked by default
|
|
||||||
return (decimal)MilliSatoshi / (ulong)unit;
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// Convert Money to decimal (same as ToUnit)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="unit"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public decimal ToDecimal(LightMoneyUnit unit)
|
|
||||||
{
|
|
||||||
return ToUnit(unit);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LightMoney Coins(decimal coins)
|
|
||||||
{
|
|
||||||
// overflow safe.
|
|
||||||
// decimal operations are checked by default
|
|
||||||
return new LightMoney(coins * (ulong)LightMoneyUnit.BTC, LightMoneyUnit.MilliBTC);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LightMoney Bits(decimal bits)
|
|
||||||
{
|
|
||||||
// overflow safe.
|
|
||||||
// decimal operations are checked by default
|
|
||||||
return new LightMoney(bits * (ulong)LightMoneyUnit.Bit, LightMoneyUnit.MilliBTC);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LightMoney Cents(decimal cents)
|
|
||||||
{
|
|
||||||
// overflow safe.
|
|
||||||
// decimal operations are checked by default
|
|
||||||
return new LightMoney(cents * (ulong)LightMoneyUnit.Bit, LightMoneyUnit.MilliBTC);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LightMoney Satoshis(decimal sats)
|
|
||||||
{
|
|
||||||
return new LightMoney(sats * (ulong)LightMoneyUnit.Satoshi, LightMoneyUnit.MilliBTC);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LightMoney Satoshis(ulong sats)
|
|
||||||
{
|
|
||||||
return new LightMoney(sats);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LightMoney Satoshis(long sats)
|
|
||||||
{
|
|
||||||
return new LightMoney(sats);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LightMoney MilliSatoshis(long msats)
|
|
||||||
{
|
|
||||||
return new LightMoney(msats);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LightMoney MilliSatoshis(ulong msats)
|
|
||||||
{
|
|
||||||
return new LightMoney(msats);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region IEquatable<Money> Members
|
|
||||||
|
|
||||||
public bool Equals(LightMoney other)
|
|
||||||
{
|
|
||||||
if (other == null)
|
|
||||||
return false;
|
|
||||||
return _MilliSatoshis.Equals(other._MilliSatoshis);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int CompareTo(LightMoney other)
|
|
||||||
{
|
|
||||||
if (other == null)
|
|
||||||
return 1;
|
|
||||||
return _MilliSatoshis.CompareTo(other._MilliSatoshis);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region IComparable Members
|
|
||||||
|
|
||||||
public int CompareTo(object obj)
|
|
||||||
{
|
|
||||||
if (obj == null)
|
|
||||||
return 1;
|
|
||||||
LightMoney m = obj as LightMoney;
|
|
||||||
if (m != null)
|
|
||||||
return _MilliSatoshis.CompareTo(m._MilliSatoshis);
|
|
||||||
#if !(PORTABLE || NETCORE)
|
|
||||||
return _MilliSatoshis.CompareTo(obj);
|
|
||||||
#else
|
|
||||||
return _Satoshis.CompareTo((long)obj);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
public static LightMoney operator -(LightMoney left, LightMoney right)
|
|
||||||
{
|
|
||||||
if (left == null)
|
|
||||||
throw new ArgumentNullException("left");
|
|
||||||
if (right == null)
|
|
||||||
throw new ArgumentNullException("right");
|
|
||||||
return new LightMoney(checked(left._MilliSatoshis - right._MilliSatoshis));
|
|
||||||
}
|
|
||||||
public static LightMoney operator -(LightMoney left)
|
|
||||||
{
|
|
||||||
if (left == null)
|
|
||||||
throw new ArgumentNullException("left");
|
|
||||||
return new LightMoney(checked(-left._MilliSatoshis));
|
|
||||||
}
|
|
||||||
public static LightMoney operator +(LightMoney left, LightMoney right)
|
|
||||||
{
|
|
||||||
if (left == null)
|
|
||||||
throw new ArgumentNullException("left");
|
|
||||||
if (right == null)
|
|
||||||
throw new ArgumentNullException("right");
|
|
||||||
return new LightMoney(checked(left._MilliSatoshis + right._MilliSatoshis));
|
|
||||||
}
|
|
||||||
public static LightMoney operator *(int left, LightMoney right)
|
|
||||||
{
|
|
||||||
if (right == null)
|
|
||||||
throw new ArgumentNullException("right");
|
|
||||||
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LightMoney operator *(LightMoney right, int left)
|
|
||||||
{
|
|
||||||
if (right == null)
|
|
||||||
throw new ArgumentNullException("right");
|
|
||||||
return LightMoney.Satoshis(checked(right._MilliSatoshis * left));
|
|
||||||
}
|
|
||||||
public static LightMoney operator *(long left, LightMoney right)
|
|
||||||
{
|
|
||||||
if (right == null)
|
|
||||||
throw new ArgumentNullException("right");
|
|
||||||
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
|
|
||||||
}
|
|
||||||
public static LightMoney operator *(LightMoney right, long left)
|
|
||||||
{
|
|
||||||
if (right == null)
|
|
||||||
throw new ArgumentNullException("right");
|
|
||||||
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LightMoney operator /(LightMoney left, long right)
|
|
||||||
{
|
|
||||||
if (left == null)
|
|
||||||
throw new ArgumentNullException("left");
|
|
||||||
return new LightMoney(checked(left._MilliSatoshis / right));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator <(LightMoney left, LightMoney right)
|
|
||||||
{
|
|
||||||
if (left == null)
|
|
||||||
throw new ArgumentNullException("left");
|
|
||||||
if (right == null)
|
|
||||||
throw new ArgumentNullException("right");
|
|
||||||
return left._MilliSatoshis < right._MilliSatoshis;
|
|
||||||
}
|
|
||||||
public static bool operator >(LightMoney left, LightMoney right)
|
|
||||||
{
|
|
||||||
if (left == null)
|
|
||||||
throw new ArgumentNullException("left");
|
|
||||||
if (right == null)
|
|
||||||
throw new ArgumentNullException("right");
|
|
||||||
return left._MilliSatoshis > right._MilliSatoshis;
|
|
||||||
}
|
|
||||||
public static bool operator <=(LightMoney left, LightMoney right)
|
|
||||||
{
|
|
||||||
if (left == null)
|
|
||||||
throw new ArgumentNullException("left");
|
|
||||||
if (right == null)
|
|
||||||
throw new ArgumentNullException("right");
|
|
||||||
return left._MilliSatoshis <= right._MilliSatoshis;
|
|
||||||
}
|
|
||||||
public static bool operator >=(LightMoney left, LightMoney right)
|
|
||||||
{
|
|
||||||
if (left == null)
|
|
||||||
throw new ArgumentNullException("left");
|
|
||||||
if (right == null)
|
|
||||||
throw new ArgumentNullException("right");
|
|
||||||
return left._MilliSatoshis >= right._MilliSatoshis;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator LightMoney(long value)
|
|
||||||
{
|
|
||||||
return new LightMoney(value);
|
|
||||||
}
|
|
||||||
public static implicit operator LightMoney(int value)
|
|
||||||
{
|
|
||||||
return new LightMoney(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator LightMoney(uint value)
|
|
||||||
{
|
|
||||||
return new LightMoney(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator LightMoney(ulong value)
|
|
||||||
{
|
|
||||||
return new LightMoney(checked((long)value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator long(LightMoney value)
|
|
||||||
{
|
|
||||||
return value.MilliSatoshi;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator ulong(LightMoney value)
|
|
||||||
{
|
|
||||||
return checked((ulong)value.MilliSatoshi);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator LightMoney(string value)
|
|
||||||
{
|
|
||||||
return LightMoney.Parse(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
LightMoney item = obj as LightMoney;
|
|
||||||
if (item == null)
|
|
||||||
return false;
|
|
||||||
return _MilliSatoshis.Equals(item._MilliSatoshis);
|
|
||||||
}
|
|
||||||
public static bool operator ==(LightMoney a, LightMoney b)
|
|
||||||
{
|
|
||||||
if (Object.ReferenceEquals(a, b))
|
|
||||||
return true;
|
|
||||||
if (((object)a == null) || ((object)b == null))
|
|
||||||
return false;
|
|
||||||
return a._MilliSatoshis == b._MilliSatoshis;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(LightMoney a, LightMoney b)
|
|
||||||
{
|
|
||||||
return !(a == b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return _MilliSatoshis.GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a culture invariant string representation of Bitcoin amount
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return ToString(false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a culture invariant string representation of Bitcoin amount
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="fplus">True if show + for a positive amount</param>
|
|
||||||
/// <param name="trimExcessZero">True if trim excess zeroes</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public string ToString(bool fplus, bool trimExcessZero = true)
|
|
||||||
{
|
|
||||||
var fmt = string.Format(CultureInfo.InvariantCulture, "{{0:{0}{1}B}}",
|
|
||||||
(fplus ? "+" : null),
|
|
||||||
(trimExcessZero ? "2" : "11"));
|
|
||||||
return string.Format(BitcoinFormatter.Formatter, fmt, _MilliSatoshis);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static LightMoney _Zero = new LightMoney(0);
|
|
||||||
public static LightMoney Zero
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return _Zero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class BitcoinFormatter : IFormatProvider, ICustomFormatter
|
|
||||||
{
|
|
||||||
public static readonly BitcoinFormatter Formatter = new BitcoinFormatter();
|
|
||||||
|
|
||||||
public object GetFormat(Type formatType)
|
|
||||||
{
|
|
||||||
return formatType == typeof(ICustomFormatter) ? this : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Format(string format, object arg, IFormatProvider formatProvider)
|
|
||||||
{
|
|
||||||
if (!this.Equals(formatProvider))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var i = 0;
|
|
||||||
var plus = format[i] == '+';
|
|
||||||
if (plus)
|
|
||||||
i++;
|
|
||||||
int decPos = 0;
|
|
||||||
if (int.TryParse(format.Substring(i, 1), out decPos))
|
|
||||||
{
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
var unit = format[i];
|
|
||||||
var unitToUseInCalc = LightMoneyUnit.BTC;
|
|
||||||
switch (unit)
|
|
||||||
{
|
|
||||||
case 'B':
|
|
||||||
unitToUseInCalc = LightMoneyUnit.BTC;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
var val = Convert.ToDecimal(arg, CultureInfo.InvariantCulture) / (long)unitToUseInCalc;
|
|
||||||
var zeros = new string('0', decPos);
|
|
||||||
var rest = new string('#', 11 - decPos);
|
|
||||||
var fmt = plus && val > 0 ? "+" : string.Empty;
|
|
||||||
|
|
||||||
fmt += "{0:0" + (decPos > 0 ? "." + zeros + rest : string.Empty) + "}";
|
|
||||||
return string.Format(CultureInfo.InvariantCulture, fmt, val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tell if amount is almost equal to this instance
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="amount"></param>
|
|
||||||
/// <param name="dust">more or less amount</param>
|
|
||||||
/// <returns>true if equals, else false</returns>
|
|
||||||
public bool Almost(LightMoney amount, LightMoney dust)
|
|
||||||
{
|
|
||||||
if (amount == null)
|
|
||||||
throw new ArgumentNullException("amount");
|
|
||||||
if (dust == null)
|
|
||||||
throw new ArgumentNullException("dust");
|
|
||||||
return (amount - this).Abs() <= dust;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tell if amount is almost equal to this instance
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="amount"></param>
|
|
||||||
/// <param name="margin">error margin (between 0 and 1)</param>
|
|
||||||
/// <returns>true if equals, else false</returns>
|
|
||||||
public bool Almost(LightMoney amount, decimal margin)
|
|
||||||
{
|
|
||||||
if (amount == null)
|
|
||||||
throw new ArgumentNullException("amount");
|
|
||||||
if (margin < 0.0m || margin > 1.0m)
|
|
||||||
throw new ArgumentOutOfRangeException("margin", "margin should be between 0 and 1");
|
|
||||||
var dust = LightMoney.Satoshis((decimal)this.MilliSatoshi * margin);
|
|
||||||
return Almost(amount, dust);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LightMoney Min(LightMoney a, LightMoney b)
|
|
||||||
{
|
|
||||||
if (a == null)
|
|
||||||
throw new ArgumentNullException("a");
|
|
||||||
if (b == null)
|
|
||||||
throw new ArgumentNullException("b");
|
|
||||||
if (a <= b)
|
|
||||||
return a;
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LightMoney Max(LightMoney a, LightMoney b)
|
|
||||||
{
|
|
||||||
if (a == null)
|
|
||||||
throw new ArgumentNullException("a");
|
|
||||||
if (b == null)
|
|
||||||
throw new ArgumentNullException("b");
|
|
||||||
if (a >= b)
|
|
||||||
return a;
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void CheckLongMinValue(long value)
|
|
||||||
{
|
|
||||||
if (value == long.MinValue)
|
|
||||||
throw new OverflowException("satoshis amount should be greater than long.MinValue");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void CheckMoneyUnit(LightMoneyUnit value, string paramName)
|
|
||||||
{
|
|
||||||
var typeOfMoneyUnit = typeof(LightMoneyUnit);
|
|
||||||
if (!Enum.IsDefined(typeOfMoneyUnit, value))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Invalid value for MoneyUnit", paramName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region IComparable Members
|
|
||||||
|
|
||||||
int IComparable.CompareTo(object obj)
|
|
||||||
{
|
|
||||||
return this.CompareTo(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
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
|
|
||||||
{
|
|
||||||
public class LightningClientFactory
|
|
||||||
{
|
|
||||||
public ILightningInvoiceClient CreateClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
|
||||||
{
|
|
||||||
var uri = supportedPaymentMethod.GetLightningUrl();
|
|
||||||
return CreateClient(uri, network.NBitcoinNetwork);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ILightningInvoiceClient CreateClient(LightningConnectionString connString, Network network)
|
|
||||||
{
|
|
||||||
if (connString.ConnectionType == LightningConnectionType.Charge)
|
|
||||||
{
|
|
||||||
return new ChargeClient(connString.ToUri(true), network);
|
|
||||||
}
|
|
||||||
else if (connString.ConnectionType == LightningConnectionType.CLightning)
|
|
||||||
{
|
|
||||||
return new CLightningRPCClient(connString.ToUri(false), network);
|
|
||||||
}
|
|
||||||
else if (connString.ConnectionType == LightningConnectionType.LndREST)
|
|
||||||
{
|
|
||||||
return new LndInvoiceClient(new LndSwaggerClient(new LndRestSettings(connString.BaseUri)
|
|
||||||
{
|
|
||||||
Macaroon = connString.Macaroon,
|
|
||||||
MacaroonFilePath = connString.MacaroonFilePath,
|
|
||||||
CertificateThumbprint = connString.CertificateThumbprint,
|
|
||||||
AllowInsecure = connString.AllowInsecure,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
throw new NotSupportedException($"Unsupported connection string for lightning server ({connString.ConnectionType})");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ILightningInvoiceClient CreateClient(string connectionString, Network network)
|
|
||||||
{
|
|
||||||
if (!Payments.Lightning.LightningConnectionString.TryParse(connectionString, false, out var conn, out string error))
|
|
||||||
throw new FormatException($"Invalid format ({error})");
|
|
||||||
return Payments.Lightning.LightningClientFactory.CreateClient(conn, network);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,438 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.WebUtilities;
|
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
using NBitcoin.DataEncoders;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning
|
|
||||||
{
|
|
||||||
public enum LightningConnectionType
|
|
||||||
{
|
|
||||||
Charge,
|
|
||||||
CLightning,
|
|
||||||
LndREST,
|
|
||||||
LndGRPC
|
|
||||||
}
|
|
||||||
public class LightningConnectionString
|
|
||||||
{
|
|
||||||
static Dictionary<string, LightningConnectionType> typeMapping;
|
|
||||||
static Dictionary<LightningConnectionType, string> typeMappingReverse;
|
|
||||||
static LightningConnectionString()
|
|
||||||
{
|
|
||||||
typeMapping = new Dictionary<string, LightningConnectionType>();
|
|
||||||
typeMapping.Add("clightning", LightningConnectionType.CLightning);
|
|
||||||
typeMapping.Add("charge", LightningConnectionType.Charge);
|
|
||||||
typeMapping.Add("lnd-rest", LightningConnectionType.LndREST);
|
|
||||||
typeMapping.Add("lnd-grpc", LightningConnectionType.LndGRPC);
|
|
||||||
typeMappingReverse = new Dictionary<LightningConnectionType, string>();
|
|
||||||
foreach (var kv in typeMapping)
|
|
||||||
{
|
|
||||||
typeMappingReverse.Add(kv.Value, kv.Key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public static bool TryParse(string str, bool supportLegacy, out LightningConnectionString connectionString)
|
|
||||||
{
|
|
||||||
return TryParse(str, supportLegacy, out connectionString, out var error);
|
|
||||||
}
|
|
||||||
public static bool TryParse(string str, bool supportLegacy, out LightningConnectionString connectionString, out string error)
|
|
||||||
{
|
|
||||||
if (str == null)
|
|
||||||
throw new ArgumentNullException(nameof(str));
|
|
||||||
|
|
||||||
if (supportLegacy)
|
|
||||||
{
|
|
||||||
var parsed = TryParseLegacy(str, out connectionString, out error);
|
|
||||||
if (!parsed)
|
|
||||||
{
|
|
||||||
parsed = TryParseNewFormat(str, out connectionString, out error);
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return TryParseNewFormat(str, out connectionString, out error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryParseNewFormat(string str, out LightningConnectionString connectionString, out string error)
|
|
||||||
{
|
|
||||||
connectionString = null;
|
|
||||||
error = null;
|
|
||||||
var parts = str.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
Dictionary<string, string> keyValues = new Dictionary<string, string>();
|
|
||||||
foreach (var part in parts.Select(p => p.Trim()))
|
|
||||||
{
|
|
||||||
var idx = part.IndexOf('=', StringComparison.OrdinalIgnoreCase);
|
|
||||||
if (idx == -1)
|
|
||||||
{
|
|
||||||
error = "The format of the connectionString should a list of key=value delimited by semicolon";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var key = part.Substring(0, idx).Trim().ToLowerInvariant();
|
|
||||||
var value = part.Substring(idx + 1).Trim();
|
|
||||||
if (keyValues.ContainsKey(key))
|
|
||||||
{
|
|
||||||
error = $"Duplicate key {key}";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
keyValues.Add(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
var possibleTypes = String.Join(", ", typeMapping.Select(k => k.Key).ToArray());
|
|
||||||
|
|
||||||
LightningConnectionString result = new LightningConnectionString();
|
|
||||||
var type = Take(keyValues, "type");
|
|
||||||
if (type == null)
|
|
||||||
{
|
|
||||||
error = $"The key 'type' is mandatory, possible values are {possibleTypes}";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!typeMapping.TryGetValue(type.ToLowerInvariant(), out var connectionType))
|
|
||||||
{
|
|
||||||
error = $"The key 'type' is invalid, possible values are {possibleTypes}";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.ConnectionType = connectionType;
|
|
||||||
|
|
||||||
switch (connectionType)
|
|
||||||
{
|
|
||||||
case LightningConnectionType.Charge:
|
|
||||||
{
|
|
||||||
var server = Take(keyValues, "server");
|
|
||||||
if (server == null)
|
|
||||||
{
|
|
||||||
error = $"The key 'server' is mandatory for charge 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];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var apiToken = Take(keyValues, "api-token");
|
|
||||||
if (apiToken == null)
|
|
||||||
{
|
|
||||||
error = "The key 'api-token' is not found";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
result.Username = "api-token";
|
|
||||||
result.Password = apiToken;
|
|
||||||
}
|
|
||||||
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case LightningConnectionType.CLightning:
|
|
||||||
{
|
|
||||||
var server = Take(keyValues, "server");
|
|
||||||
if (server == null)
|
|
||||||
{
|
|
||||||
error = $"The key 'server' is mandatory for charge connection strings";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (server.StartsWith("//", StringComparison.OrdinalIgnoreCase))
|
|
||||||
server = "unix:" + str;
|
|
||||||
else if (server.StartsWith("/", StringComparison.OrdinalIgnoreCase))
|
|
||||||
server = "unix:/" + str;
|
|
||||||
|
|
||||||
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|
|
||||||
|| (uri.Scheme != "tcp" && uri.Scheme != "unix"))
|
|
||||||
{
|
|
||||||
error = $"The key 'server' should be an URI starting by tcp:// or unix:// or a path to the 'lightning-rpc' unix socket";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
result.BaseUri = uri;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case LightningConnectionType.LndREST:
|
|
||||||
case LightningConnectionType.LndGRPC:
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
result.Macaroon = Encoder.DecodeData(macaroon);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
error = $"The key 'macaroon' format should be in hex";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var macaroonFilePath = Take(keyValues, "macaroonfilepath");
|
|
||||||
if (macaroonFilePath != null)
|
|
||||||
{
|
|
||||||
if (macaroon != null)
|
|
||||||
{
|
|
||||||
error = $"The key 'macaroon' is already specified";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!macaroonFilePath.EndsWith(".macaroon", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
error = $"The key 'macaroonfilepath' should point to a .macaroon file";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
result.MacaroonFilePath = macaroonFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
string securitySet = null;
|
|
||||||
var certthumbprint = Take(keyValues, "certthumbprint");
|
|
||||||
if (certthumbprint != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var bytes = Encoders.Hex.DecodeData(certthumbprint.Replace(":", string.Empty, StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (bytes.Length != 32)
|
|
||||||
{
|
|
||||||
error = $"The key 'certthumbprint' has invalid length: it should be the SHA256 of the PEM format of the certificate (32 bytes)";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
result.CertificateThumbprint = bytes;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
error = $"The key 'certthumbprint' has invalid format: it should be the SHA256 of the PEM format of the certificate";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
securitySet = "certthumbprint";
|
|
||||||
}
|
|
||||||
|
|
||||||
var allowinsecureStr = Take(keyValues, "allowinsecure");
|
|
||||||
|
|
||||||
if (allowinsecureStr != null)
|
|
||||||
{
|
|
||||||
var allowedValues = new[] { "true", "false" };
|
|
||||||
if (!allowedValues.Any(v => v.Equals(allowinsecureStr, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
error = $"The key 'allowinsecure' should be true or false";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool allowInsecure = allowinsecureStr.Equals("true", StringComparison.OrdinalIgnoreCase);
|
|
||||||
if (securitySet != null && allowInsecure)
|
|
||||||
{
|
|
||||||
error = $"The key 'allowinsecure' conflict with '{securitySet}'";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
result.AllowInsecure = allowInsecure;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.AllowInsecure && result.BaseUri.Scheme == "http")
|
|
||||||
{
|
|
||||||
error = $"The key 'allowinsecure' is false, but server's Uri is not using https";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new NotSupportedException(connectionType.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyValues.Count != 0)
|
|
||||||
{
|
|
||||||
error = $"Unknown keys ({String.Join(", ", keyValues.Select(k => k.Key).ToArray())})";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectionString = result;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LightningConnectionString Clone()
|
|
||||||
{
|
|
||||||
LightningConnectionString.TryParse(this.ToString(), false, out var result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Take(Dictionary<string, string> keyValues, string key)
|
|
||||||
{
|
|
||||||
if (keyValues.TryGetValue(key, out var v))
|
|
||||||
keyValues.Remove(key);
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryParseLegacy(string str, out LightningConnectionString connectionString, out string error)
|
|
||||||
{
|
|
||||||
if (str.StartsWith('/'))
|
|
||||||
str = "unix:" + str;
|
|
||||||
var result = new LightningConnectionString();
|
|
||||||
connectionString = null;
|
|
||||||
error = null;
|
|
||||||
|
|
||||||
Uri uri;
|
|
||||||
if (!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);
|
|
||||||
result.ConnectionType = LightningConnectionType.CLightning;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uri.Scheme == "tcp")
|
|
||||||
result.ConnectionType = LightningConnectionType.CLightning;
|
|
||||||
|
|
||||||
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];
|
|
||||||
result.ConnectionType = LightningConnectionType.Charge;
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(uri.UserInfo))
|
|
||||||
{
|
|
||||||
error = "The url should not have user information";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
|
|
||||||
result.IsLegacy = true;
|
|
||||||
connectionString = result;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LightningConnectionString()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Username { get; set; }
|
|
||||||
public string Password { get; set; }
|
|
||||||
public Uri BaseUri { get; set; }
|
|
||||||
public bool IsLegacy { get; private set; }
|
|
||||||
|
|
||||||
public LightningConnectionType ConnectionType
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
public byte[] Macaroon { get; set; }
|
|
||||||
public string MacaroonFilePath { get; set; }
|
|
||||||
public byte[] CertificateThumbprint { get; set; }
|
|
||||||
public bool AllowInsecure { get; set; }
|
|
||||||
|
|
||||||
public Uri ToUri(bool withCredentials)
|
|
||||||
{
|
|
||||||
if (withCredentials)
|
|
||||||
{
|
|
||||||
return new UriBuilder(BaseUri) { UserName = Username ?? "", Password = Password ?? "" }.Uri;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return BaseUri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static NBitcoin.DataEncoders.DataEncoder Encoder = NBitcoin.DataEncoders.Encoders.Hex;
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
var type = typeMappingReverse[ConnectionType];
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
builder.Append($"type={type}");
|
|
||||||
switch (ConnectionType)
|
|
||||||
{
|
|
||||||
case LightningConnectionType.Charge:
|
|
||||||
if (Username == null || Username == "api-token")
|
|
||||||
{
|
|
||||||
builder.Append($";server={BaseUri};api-token={Password}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
builder.Append($";server={ToUri(true)}");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case LightningConnectionType.CLightning:
|
|
||||||
builder.Append($";server={BaseUri}");
|
|
||||||
break;
|
|
||||||
case LightningConnectionType.LndREST:
|
|
||||||
case LightningConnectionType.LndGRPC:
|
|
||||||
if (Username == null)
|
|
||||||
{
|
|
||||||
builder.Append($";server={BaseUri}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
builder.Append($";server={ToUri(true)}");
|
|
||||||
}
|
|
||||||
if (Macaroon != null)
|
|
||||||
{
|
|
||||||
builder.Append($";macaroon={Encoder.EncodeData(Macaroon)}");
|
|
||||||
}
|
|
||||||
if (MacaroonFilePath != null)
|
|
||||||
{
|
|
||||||
builder.Append($";macaroonfilepath={MacaroonFilePath}");
|
|
||||||
}
|
|
||||||
if (CertificateThumbprint != null)
|
|
||||||
{
|
|
||||||
builder.Append($";certthumbprint={Encoders.Hex.EncodeData(CertificateThumbprint)}");
|
|
||||||
}
|
|
||||||
if (AllowInsecure)
|
|
||||||
{
|
|
||||||
builder.Append($";allowinsecure=true");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new NotSupportedException(type);
|
|
||||||
}
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,8 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.JsonConverters;
|
using BTCPayServer.JsonConverters;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
using BTCPayServer.Lightning.JsonConverters;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Payments.Lightning.Charge;
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Payments.Lightning.CLightning;
|
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning
|
namespace BTCPayServer.Payments.Lightning
|
||||||
@@ -18,12 +17,9 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
public static int LIGHTNING_TIMEOUT = 5000;
|
public static int LIGHTNING_TIMEOUT = 5000;
|
||||||
|
|
||||||
NBXplorerDashboard _Dashboard;
|
NBXplorerDashboard _Dashboard;
|
||||||
LightningClientFactory _LightningClientFactory;
|
|
||||||
public LightningLikePaymentHandler(
|
public LightningLikePaymentHandler(
|
||||||
LightningClientFactory lightningClientFactory,
|
|
||||||
NBXplorerDashboard dashboard)
|
NBXplorerDashboard dashboard)
|
||||||
{
|
{
|
||||||
_LightningClientFactory = lightningClientFactory;
|
|
||||||
_Dashboard = dashboard;
|
_Dashboard = dashboard;
|
||||||
}
|
}
|
||||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
|
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
|
||||||
@@ -32,7 +28,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
var test = Test(supportedPaymentMethod, network);
|
var test = Test(supportedPaymentMethod, network);
|
||||||
var invoice = paymentMethod.ParentEntity;
|
var invoice = paymentMethod.ParentEntity;
|
||||||
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
|
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
|
||||||
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
var client = supportedPaymentMethod.CreateClient(network);
|
||||||
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
||||||
if (expiry < TimeSpan.Zero)
|
if (expiry < TimeSpan.Zero)
|
||||||
expiry = TimeSpan.FromSeconds(1);
|
expiry = TimeSpan.FromSeconds(1);
|
||||||
@@ -74,7 +70,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
|
|
||||||
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
|
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
|
||||||
{
|
{
|
||||||
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
var client = supportedPaymentMethod.CreateClient(network);
|
||||||
LightningNodeInformation info = null;
|
LightningNodeInformation info = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -89,7 +85,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
|
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.Address == null)
|
if (info.NodeInfo == null)
|
||||||
{
|
{
|
||||||
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
|
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
|
||||||
}
|
}
|
||||||
@@ -100,7 +96,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
|
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
|
return info.NodeInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using BTCPayServer.Logging;
|
|||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning
|
namespace BTCPayServer.Payments.Lightning
|
||||||
{
|
{
|
||||||
@@ -27,16 +28,13 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
EventAggregator _Aggregator;
|
EventAggregator _Aggregator;
|
||||||
InvoiceRepository _InvoiceRepository;
|
InvoiceRepository _InvoiceRepository;
|
||||||
BTCPayNetworkProvider _NetworkProvider;
|
BTCPayNetworkProvider _NetworkProvider;
|
||||||
LightningClientFactory _LightningClientFactory;
|
|
||||||
public LightningListener(EventAggregator aggregator,
|
public LightningListener(EventAggregator aggregator,
|
||||||
InvoiceRepository invoiceRepository,
|
InvoiceRepository invoiceRepository,
|
||||||
LightningClientFactory lightningClientFactory,
|
|
||||||
BTCPayNetworkProvider networkProvider)
|
BTCPayNetworkProvider networkProvider)
|
||||||
{
|
{
|
||||||
_Aggregator = aggregator;
|
_Aggregator = aggregator;
|
||||||
_InvoiceRepository = invoiceRepository;
|
_InvoiceRepository = invoiceRepository;
|
||||||
_NetworkProvider = networkProvider;
|
_NetworkProvider = networkProvider;
|
||||||
_LightningClientFactory = lightningClientFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CompositeDisposable leases = new CompositeDisposable();
|
CompositeDisposable leases = new CompositeDisposable();
|
||||||
@@ -100,22 +98,22 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
|
|
||||||
if (poll)
|
if (poll)
|
||||||
{
|
{
|
||||||
var charge = _LightningClientFactory.CreateClient(lightningSupportedMethod, network);
|
var charge = lightningSupportedMethod.CreateClient(network);
|
||||||
LightningInvoice chargeInvoice = null;
|
LightningInvoice chargeInvoice = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
|
chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
|
||||||
}
|
}
|
||||||
catch(Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logs.PayServer.LogError(ex, $"{lightningSupportedMethod.CryptoCode} (Lightning): Can't connect to the lightning server");
|
Logs.PayServer.LogError(ex, $"{lightningSupportedMethod.CryptoCode} (Lightning): Can't connect to the lightning server");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (chargeInvoice == null)
|
if (chargeInvoice == null)
|
||||||
continue;
|
continue;
|
||||||
if (chargeInvoice.Status == "paid")
|
if (chargeInvoice.Status == LightningInvoiceStatus.Paid)
|
||||||
await AddPayment(network, chargeInvoice, listenedInvoice);
|
await AddPayment(network, chargeInvoice, listenedInvoice);
|
||||||
if (chargeInvoice.Status == "paid" || chargeInvoice.Status == "expired")
|
if (chargeInvoice.Status == LightningInvoiceStatus.Paid || chargeInvoice.Status == LightningInvoiceStatus.Expired)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,11 +141,11 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
CancellationTokenSource _Cts = new CancellationTokenSource();
|
CancellationTokenSource _Cts = new CancellationTokenSource();
|
||||||
private async Task Listen(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
private async Task Listen(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||||
{
|
{
|
||||||
ILightningListenInvoiceSession session = null;
|
ILightningInvoiceListener session = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||||
var lightningClient = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
var lightningClient = supportedPaymentMethod.CreateClient(network);
|
||||||
session = await lightningClient.Listen(_Cts.Token);
|
session = await lightningClient.Listen(_Cts.Token);
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
@@ -158,13 +156,13 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
|
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
|
||||||
notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11)
|
notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11)
|
||||||
{
|
{
|
||||||
if (notification.Status == "paid" && notification.PaidAt.HasValue)
|
if (notification.Status == LightningInvoiceStatus.Paid && notification.PaidAt.HasValue)
|
||||||
{
|
{
|
||||||
await AddPayment(network, notification, listenedInvoice);
|
await AddPayment(network, notification, listenedInvoice);
|
||||||
if (DoneListening(listenedInvoice))
|
if (DoneListening(listenedInvoice))
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (notification.Status == "expired")
|
if (notification.Status == LightningInvoiceStatus.Expired)
|
||||||
{
|
{
|
||||||
if (DoneListening(listenedInvoice))
|
if (DoneListening(listenedInvoice))
|
||||||
break;
|
break;
|
||||||
@@ -197,7 +195,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
if (payment != null)
|
if (payment != null)
|
||||||
{
|
{
|
||||||
var invoice = await _InvoiceRepository.GetInvoice(null, listenedInvoice.InvoiceId);
|
var invoice = await _InvoiceRepository.GetInvoice(null, listenedInvoice.InvoiceId);
|
||||||
if(invoice != null)
|
if (invoice != null)
|
||||||
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
|
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning
|
namespace BTCPayServer.Payments.Lightning
|
||||||
{
|
{
|
||||||
@@ -28,7 +30,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
if (!string.IsNullOrEmpty(LightningConnectionString))
|
if (!string.IsNullOrEmpty(LightningConnectionString))
|
||||||
{
|
{
|
||||||
if (!BTCPayServer.Payments.Lightning.LightningConnectionString.TryParse(LightningConnectionString, false, out var connectionString, out var error))
|
if (!BTCPayServer.Lightning.LightningConnectionString.TryParse(LightningConnectionString, false, out var connectionString, out var error))
|
||||||
{
|
{
|
||||||
throw new FormatException(error);
|
throw new FormatException(error);
|
||||||
}
|
}
|
||||||
@@ -37,7 +39,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var fullUri = new UriBuilder(LightningChargeUrl) { UserName = Username, Password = Password }.Uri.AbsoluteUri;
|
var fullUri = new UriBuilder(LightningChargeUrl) { UserName = Username, Password = Password }.Uri.AbsoluteUri;
|
||||||
if (!BTCPayServer.Payments.Lightning.LightningConnectionString.TryParse(fullUri, true, out var connectionString, out var error))
|
if (!BTCPayServer.Lightning.LightningConnectionString.TryParse(fullUri, true, out var connectionString, out var error))
|
||||||
{
|
{
|
||||||
throw new FormatException(error);
|
throw new FormatException(error);
|
||||||
}
|
}
|
||||||
@@ -58,5 +60,10 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
LightningChargeUrl = null;
|
LightningChargeUrl = null;
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ILightningClient CreateClient(BTCPayNetwork network)
|
||||||
|
{
|
||||||
|
return LightningClientFactory.CreateClient(this.GetLightningUrl(), network.NBitcoinNetwork);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using NBitcoin.DataEncoders;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
|
||||||
{
|
|
||||||
public abstract class LndAuthentication
|
|
||||||
{
|
|
||||||
public class FixedMacaroonAuthentication : LndAuthentication
|
|
||||||
{
|
|
||||||
public FixedMacaroonAuthentication(byte[] macaroon)
|
|
||||||
{
|
|
||||||
if (macaroon == null)
|
|
||||||
throw new ArgumentNullException(nameof(macaroon));
|
|
||||||
Macaroon = macaroon;
|
|
||||||
}
|
|
||||||
public byte[] Macaroon { get; set; }
|
|
||||||
public override void AddAuthentication(HttpRequestMessage httpRequest)
|
|
||||||
{
|
|
||||||
httpRequest.Headers.Add("Grpc-Metadata-macaroon", Encoders.Hex.EncodeData(Macaroon));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public class NullAuthentication : LndAuthentication
|
|
||||||
{
|
|
||||||
public static NullAuthentication Instance { get; } = new NullAuthentication();
|
|
||||||
|
|
||||||
private NullAuthentication()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
public override void AddAuthentication(HttpRequestMessage httpRequest)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MacaroonFileAuthentication : LndAuthentication
|
|
||||||
{
|
|
||||||
public MacaroonFileAuthentication(string filePath)
|
|
||||||
{
|
|
||||||
if (filePath == null)
|
|
||||||
throw new ArgumentNullException(nameof(filePath));
|
|
||||||
// Because this dump the whole file, let's make sure it is indeed the macaroon
|
|
||||||
if (!filePath.EndsWith(".macaroon", StringComparison.OrdinalIgnoreCase))
|
|
||||||
throw new ArgumentException(message: "filePath is not a macaroon file", paramName: nameof(filePath));
|
|
||||||
FilePath = filePath;
|
|
||||||
}
|
|
||||||
public string FilePath { get; set; }
|
|
||||||
public override void AddAuthentication(HttpRequestMessage httpRequest)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var bytes = File.ReadAllBytes(FilePath);
|
|
||||||
httpRequest.Headers.Add("Grpc-Metadata-macaroon", Encoders.Hex.EncodeData(bytes));
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void AddAuthentication(HttpRequestMessage httpRequest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
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.Runtime.ExceptionServices;
|
|
||||||
using System.Security.Authentication;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Channels;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using NBitcoin;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
|
||||||
{
|
|
||||||
public class LndInvoiceClient : ILightningInvoiceClient
|
|
||||||
{
|
|
||||||
class LndInvoiceClientSession : ILightningListenInvoiceSession
|
|
||||||
{
|
|
||||||
private LndSwaggerClient _Parent;
|
|
||||||
Channel<LightningInvoice> _Invoices = Channel.CreateBounded<LightningInvoice>(50);
|
|
||||||
CancellationTokenSource _Cts = new CancellationTokenSource();
|
|
||||||
|
|
||||||
|
|
||||||
HttpClient _Client;
|
|
||||||
HttpResponseMessage _Response;
|
|
||||||
Stream _Body;
|
|
||||||
StreamReader _Reader;
|
|
||||||
Task _ListenLoop;
|
|
||||||
|
|
||||||
public LndInvoiceClientSession(LndSwaggerClient parent)
|
|
||||||
{
|
|
||||||
_Parent = parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task StartListening()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_Client = _Parent.CreateHttpClient();
|
|
||||||
_Client.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite);
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, _Parent.BaseUrl.WithTrailingSlash() + "v1/invoices/subscribe");
|
|
||||||
_Parent._Authentication.AddAuthentication(request);
|
|
||||||
_ListenLoop = ListenLoop(request);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
Dispose();
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ListenLoop(HttpRequestMessage request)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_Response = await _Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _Cts.Token);
|
|
||||||
_Body = await _Response.Content.ReadAsStreamAsync();
|
|
||||||
_Reader = new StreamReader(_Body);
|
|
||||||
while (!_Cts.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
string line = await _Reader.ReadLineAsync().WithCancellation(_Cts.Token);
|
|
||||||
if (line != null)
|
|
||||||
{
|
|
||||||
if (line.StartsWith("{\"result\":", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var invoiceString = JObject.Parse(line)["result"].ToString();
|
|
||||||
LnrpcInvoice parsedInvoice = _Parent.Deserialize<LnrpcInvoice>(invoiceString);
|
|
||||||
await _Invoices.Writer.WriteAsync(ConvertLndInvoice(parsedInvoice), _Cts.Token);
|
|
||||||
}
|
|
||||||
else if (line.StartsWith("{\"error\":", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var errorString = JObject.Parse(line)["error"].ToString();
|
|
||||||
var error = _Parent.Deserialize<LndError>(errorString);
|
|
||||||
throw new LndException(error);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new LndException("Unknown result from LND: " + line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch when (_Cts.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_Invoices.Writer.TryComplete(ex);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Dispose(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public async Task<LightningInvoice> WaitInvoice(CancellationToken cancellation)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await _Invoices.Reader.ReadAsync(cancellation);
|
|
||||||
}
|
|
||||||
catch (ChannelClosedException ex) when (ex.InnerException == null)
|
|
||||||
{
|
|
||||||
throw new TaskCanceledException();
|
|
||||||
}
|
|
||||||
catch (ChannelClosedException ex)
|
|
||||||
{
|
|
||||||
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
}
|
|
||||||
void Dispose(bool waitLoop)
|
|
||||||
{
|
|
||||||
if (_Cts.IsCancellationRequested)
|
|
||||||
return;
|
|
||||||
_Cts.Cancel();
|
|
||||||
_Reader?.Dispose();
|
|
||||||
_Reader = null;
|
|
||||||
_Body?.Dispose();
|
|
||||||
_Body = null;
|
|
||||||
_Response?.Dispose();
|
|
||||||
_Response = null;
|
|
||||||
_Client?.Dispose();
|
|
||||||
_Client = null;
|
|
||||||
if (waitLoop)
|
|
||||||
_ListenLoop?.Wait();
|
|
||||||
_Invoices.Writer.TryComplete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public LndSwaggerClient _rpcClient;
|
|
||||||
|
|
||||||
public LndInvoiceClient(LndSwaggerClient swaggerClient)
|
|
||||||
{
|
|
||||||
if (swaggerClient == null)
|
|
||||||
throw new ArgumentNullException(nameof(swaggerClient));
|
|
||||||
_rpcClient = swaggerClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
catch (SwaggerException ex) when (!string.IsNullOrEmpty(ex.Response))
|
|
||||||
{
|
|
||||||
throw new Exception("LND threw an error: " + ex.Response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken))
|
|
||||||
{
|
|
||||||
var resp = await _rpcClient.LookupInvoiceAsync(invoiceId, null, cancellation);
|
|
||||||
return ConvertLndInvoice(resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken))
|
|
||||||
{
|
|
||||||
var session = new LndInvoiceClientSession(this._rpcClient);
|
|
||||||
await session.StartListening();
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// utility static methods... maybe move to separate class
|
|
||||||
private static string BitString(byte[] bytes)
|
|
||||||
{
|
|
||||||
return BitConverter.ToString(bytes)
|
|
||||||
.Replace("-", "", StringComparison.InvariantCulture)
|
|
||||||
.ToLower(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
|
||||||
{
|
|
||||||
public class LndRestSettings
|
|
||||||
{
|
|
||||||
public LndRestSettings()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
public LndRestSettings(Uri uri)
|
|
||||||
{
|
|
||||||
Uri = uri;
|
|
||||||
}
|
|
||||||
public Uri Uri { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The SHA256 of the PEM certificate
|
|
||||||
/// </summary>
|
|
||||||
public byte[] CertificateThumbprint { get; set; }
|
|
||||||
public byte[] Macaroon { get; set; }
|
|
||||||
public bool AllowInsecure { get; set; }
|
|
||||||
public string MacaroonFilePath { get; set; }
|
|
||||||
|
|
||||||
public LndAuthentication CreateLndAuthentication()
|
|
||||||
{
|
|
||||||
if (Macaroon != null)
|
|
||||||
return new LndAuthentication.FixedMacaroonAuthentication(Macaroon);
|
|
||||||
if (!string.IsNullOrEmpty(MacaroonFilePath))
|
|
||||||
return new LndAuthentication.MacaroonFileAuthentication(MacaroonFilePath);
|
|
||||||
return LndAuthentication.NullAuthentication.Instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,119 +0,0 @@
|
|||||||
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.DataEncoders;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
|
||||||
{
|
|
||||||
public class LndException : Exception
|
|
||||||
{
|
|
||||||
public LndException(string message) : base(message)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
public LndException(LndError error) : base(error.Message)
|
|
||||||
{
|
|
||||||
if (error == null)
|
|
||||||
throw new ArgumentNullException(nameof(error));
|
|
||||||
_Error = error;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private readonly LndError _Error;
|
|
||||||
public LndError Error
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return _Error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// {"grpc_code":2,"http_code":500,"message":"rpc error: code = Unknown desc = expected 1 macaroon, got 0","http_status":"Internal Server Error"}
|
|
||||||
public class LndError
|
|
||||||
{
|
|
||||||
[JsonProperty("grpc_code")]
|
|
||||||
public int GRPCCode { get; set; }
|
|
||||||
[JsonProperty("http_code")]
|
|
||||||
public int HttpCode { get; set; }
|
|
||||||
[JsonProperty("message")]
|
|
||||||
public string Message { get; set; }
|
|
||||||
[JsonProperty("http_status")]
|
|
||||||
public string HttpStatus { get; set; }
|
|
||||||
}
|
|
||||||
public partial class LndSwaggerClient
|
|
||||||
{
|
|
||||||
public LndSwaggerClient(LndRestSettings settings)
|
|
||||||
{
|
|
||||||
if (settings == null)
|
|
||||||
throw new ArgumentNullException(nameof(settings));
|
|
||||||
_LndSettings = settings;
|
|
||||||
_Authentication = settings.CreateLndAuthentication();
|
|
||||||
BaseUrl = settings.Uri.AbsoluteUri.TrimEnd('/');
|
|
||||||
_httpClient = CreateHttpClient(settings);
|
|
||||||
_settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(() =>
|
|
||||||
{
|
|
||||||
var json = new Newtonsoft.Json.JsonSerializerSettings();
|
|
||||||
UpdateJsonSerializerSettings(json);
|
|
||||||
return json;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
LndRestSettings _LndSettings;
|
|
||||||
internal LndAuthentication _Authentication;
|
|
||||||
|
|
||||||
partial void PrepareRequest(HttpClient client, HttpRequestMessage request, string url)
|
|
||||||
{
|
|
||||||
_Authentication.AddAuthentication(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static HttpClient CreateHttpClient(LndRestSettings settings)
|
|
||||||
{
|
|
||||||
var handler = new HttpClientHandler
|
|
||||||
{
|
|
||||||
SslProtocols = SslProtocols.Tls12
|
|
||||||
};
|
|
||||||
|
|
||||||
var expectedThumbprint = settings.CertificateThumbprint?.ToArray();
|
|
||||||
if (expectedThumbprint != null)
|
|
||||||
{
|
|
||||||
handler.ServerCertificateCustomValidationCallback = (request, cert, chain, errors) =>
|
|
||||||
{
|
|
||||||
var actualCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
|
|
||||||
var hash = actualCert.GetCertHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
|
|
||||||
return hash.SequenceEqual(expectedThumbprint);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.AllowInsecure)
|
|
||||||
{
|
|
||||||
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (settings.Uri.Scheme == "http")
|
|
||||||
throw new InvalidOperationException("AllowInsecure is set to false, but the URI is not using https");
|
|
||||||
}
|
|
||||||
return new HttpClient(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal HttpClient CreateHttpClient()
|
|
||||||
{
|
|
||||||
return LndSwaggerClient.CreateHttpClient(_LndSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal T Deserialize<T>(string str)
|
|
||||||
{
|
|
||||||
return JsonConvert.DeserializeObject<T>(str, _settings.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user