diff --git a/BTCPayApp.Core/Attempt2/LDKNode.cs b/BTCPayApp.Core/Attempt2/LDKNode.cs
index 885b8c1..ce14083 100644
--- a/BTCPayApp.Core/Attempt2/LDKNode.cs
+++ b/BTCPayApp.Core/Attempt2/LDKNode.cs
@@ -3,6 +3,7 @@
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Helpers;
using BTCPayApp.Core.LDK;
+using BTCPayApp.Core.LSP.JIT;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
@@ -98,6 +99,18 @@ public async Task OpenChannel(Money amount, PubKey no
}
}
+
+ public async Task GetJITLSPService()
+ {
+ var config = await GetConfig();
+ var lsp = config.JITLSP;
+ if(lsp is null)
+ {
+ return null;
+ }
+ var jits = ServiceProvider.GetServices();
+ return jits.FirstOrDefault(jit => jit.ProviderName == lsp);
+ }
}
public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
@@ -181,12 +194,17 @@ public async Task GetConfig()
await _configLoaded.Task;
return _config!;
}
-
- private async Task UpdateConfig(LightningConfig config)
+ public async Task GetJITLSPs()
+ {
+ return ServiceProvider.GetServices().Select(jit => jit.ProviderName).ToArray();
+ }
+
+ public async Task UpdateConfig(LightningConfig config)
{
await _started.Task;
await _configProvider.Set(LightningConfig.Key, config);
_config = config;
+
ConfigUpdated?.Invoke(this, config);
}
@@ -366,9 +384,9 @@ await context.LightningChannels.AddAsync(new Channel()
}
- public async Task Peer(string toString, PeerInfo? value)
+ public async Task Peer(PubKey key, PeerInfo? value)
{
- toString = toString.ToLowerInvariant();
+ var toString = key.ToString().ToLowerInvariant();
var config = await GetConfig();
if (value is null)
{
diff --git a/BTCPayApp.Core/Data/AppDbContext.cs b/BTCPayApp.Core/Data/AppDbContext.cs
index d235951..9572ea0 100644
--- a/BTCPayApp.Core/Data/AppDbContext.cs
+++ b/BTCPayApp.Core/Data/AppDbContext.cs
@@ -32,9 +32,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
}
}
-public class SpendableCoin
-{
- public string Script { get; set; }
- [Key] public string Outpoint { get; set; }
- public byte[] Data { get; set; }
-}
\ No newline at end of file
+// public class SpendableCoin
+// {
+// public string Script { get; set; }
+// [Key] public string Outpoint { get; set; }
+// public byte[] Data { get; set; }
+// }
\ No newline at end of file
diff --git a/BTCPayApp.Core/LDK/LDKExtensions.cs b/BTCPayApp.Core/LDK/LDKExtensions.cs
index 66b6ed9..f2275d5 100644
--- a/BTCPayApp.Core/LDK/LDKExtensions.cs
+++ b/BTCPayApp.Core/LDK/LDKExtensions.cs
@@ -3,6 +3,7 @@
using BTCPayApp.Core.Attempt2;
using BTCPayApp.Core.Contracts;
using BTCPayApp.Core.Helpers;
+using BTCPayApp.Core.LSP.JIT;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NBitcoin;
@@ -213,6 +214,7 @@ public static IServiceCollection AddLDK(this IServiceCollection services)
// services.AddScoped(provider =>
// provider.GetRequiredService());
services.AddScoped(provider => provider.GetRequiredService());
+ services.AddScoped(provider => provider.GetRequiredService());
services.AddScoped(provider => provider.GetRequiredService());
services.AddScoped(provider => provider.GetRequiredService());
services.AddScoped(provider => provider.GetRequiredService());
@@ -300,6 +302,11 @@ public static IServiceCollection AddLDK(this IServiceCollection services)
ProbabilisticScoringFeeParameters.with_default()));
services.AddScoped(provider => provider.GetRequiredService().as_Router());
+
+ services.AddScoped();
+ services.AddScoped(provider => provider.GetRequiredService());
+ services.AddScoped(provider => provider.GetRequiredService());
+
return services;
}
diff --git a/BTCPayApp.Core/LDK/LDKOpenChannelRequestEventHandler.cs b/BTCPayApp.Core/LDK/LDKOpenChannelRequestEventHandler.cs
index efad161..4353cae 100644
--- a/BTCPayApp.Core/LDK/LDKOpenChannelRequestEventHandler.cs
+++ b/BTCPayApp.Core/LDK/LDKOpenChannelRequestEventHandler.cs
@@ -1,4 +1,5 @@
using BTCPayApp.Core.Attempt2;
+using BTCPayApp.Core.Helpers;
using org.ldk.structs;
using UInt128 = org.ldk.util.UInt128;
@@ -33,6 +34,7 @@ public async Task Handle(Event.Event_OpenChannelRequest eventOpenChannelRequest)
eventOpenChannelRequest.counterparty_node_id,
userChannelId
);
+ AcceptedChannel?.Invoke(this, eventOpenChannelRequest);
return;
}
}
@@ -43,8 +45,11 @@ public async Task Handle(Event.Event_OpenChannelRequest eventOpenChannelRequest)
eventOpenChannelRequest.counterparty_node_id,
userChannelId);
+ AcceptedChannel?.Invoke(this, eventOpenChannelRequest);
//TODO: if we want to reject the channel, we can call reject_channel
//_channelManager.force_close_without_broadcasting_txn(eventOpenChannelRequest.temporary_channel_id, eventOpenChannelRequest.counterparty_node_id);
}
+
+ public AsyncEventHandler? AcceptedChannel;
}
\ No newline at end of file
diff --git a/BTCPayApp.Core/LDK/LDKPeerHandler.cs b/BTCPayApp.Core/LDK/LDKPeerHandler.cs
index 8728ba0..742f818 100644
--- a/BTCPayApp.Core/LDK/LDKPeerHandler.cs
+++ b/BTCPayApp.Core/LDK/LDKPeerHandler.cs
@@ -65,7 +65,7 @@ private async Task BtcPayAppServerClientOnOnServerNodeInfo(object? sender, strin
if (config.Peers.ContainsKey(nodeInfo.NodeId.ToString()))
return;
var endpoint = new IPEndPoint(IPAddress.Parse(nodeInfo.Host), nodeInfo.Port);
- await _node.Peer(nodeInfo.NodeId.ToString(), new PeerInfo()
+ await _node.Peer(nodeInfo.NodeId, new PeerInfo()
{
Endpoint = endpoint.ToString(),
Persistent = true,
@@ -224,7 +224,7 @@ await listener.AcceptTcpClientAsync(cancellationToken),
if (peer.Endpoint != remote.ToString())
{
peer.Endpoint = remote.ToString()!;
- await _node.Peer(theirNodeId.ToString(), peer);
+ await _node.Peer(theirNodeId, peer);
}
}
diff --git a/BTCPayApp.Core/LDK/PaymentsManager.cs b/BTCPayApp.Core/LDK/PaymentsManager.cs
index c4fd5a6..3a8cc45 100644
--- a/BTCPayApp.Core/LDK/PaymentsManager.cs
+++ b/BTCPayApp.Core/LDK/PaymentsManager.cs
@@ -1,41 +1,55 @@
-using System.Security.Cryptography;
+using System.Collections.Concurrent;
+using System.Security.Cryptography;
+using System.Text.Json;
+using BTCPayApp.Core.Attempt2;
using BTCPayApp.Core.Data;
using BTCPayApp.Core.Helpers;
+using BTCPayApp.Core.LSP.JIT;
using BTCPayServer.Lightning;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using org.ldk.structs;
+using org.ldk.util;
// using BTCPayServer.Lightning;
using LightningPayment = BTCPayApp.CommonServer.Models.LightningPayment;
namespace BTCPayApp.Core.LDK;
public class PaymentsManager :
+ IScopedHostedService,
ILDKEventHandler,
ILDKEventHandler,
ILDKEventHandler,
ILDKEventHandler
{
+ public const string LightningPaymentDescriptionKey = "DescriptionHash";
+ public const string LightningPaymentExpiryKey = "Expiry";
+
private readonly IDbContextFactory _dbContextFactory;
private readonly ChannelManager _channelManager;
+ private readonly LDKOpenChannelRequestEventHandler _openChannelRequestEventHandler;
private readonly Logger _logger;
private readonly NodeSigner _nodeSigner;
private readonly Network _network;
+ private readonly LDKNode _ldkNode;
public PaymentsManager(
IDbContextFactory dbContextFactory,
ChannelManager channelManager,
+ LDKOpenChannelRequestEventHandler openChannelRequestEventHandler,
Logger logger,
NodeSigner nodeSigner,
- Network network
- )
+ Network network,
+ LDKNode ldkNode)
{
_dbContextFactory = dbContextFactory;
_channelManager = channelManager;
+ _openChannelRequestEventHandler = openChannelRequestEventHandler;
_logger = logger;
_nodeSigner = nodeSigner;
_network = network;
+ _ldkNode = ldkNode;
}
public async Task> List(
@@ -47,61 +61,107 @@ public async Task> List(
.ToListAsync(cancellationToken: cancellationToken))!;
}
+ // private Bolt11Invoice CreateInvoice(long? amt, int expirySeconds, byte[] descHash)
+ // {
+ // var keyMaterial = _nodeSigner.get_inbound_payment_key_material();
+ // var preimage = RandomUtils.GetBytes(32);
+ // var paymentHash = SHA256.HashData(preimage);
+ // var expandedKey = ExpandedKey.of(keyMaterial);
+ // var inboundPayment = _channelManager.create_inbound_payment_for_hash(paymentHash,
+ // amt is null ? Option_u64Z.none() : Option_u64Z.some(amt.Value), expirySeconds, Option_u16Z.none()));
+ // var paymentSecret = inboundPayment is Result_ThirtyTwoBytesNoneZ.Result_ThirtyTwoBytesNoneZ_OK ok
+ // ? ok.res
+ // : throw new Exception("Error creating inbound payment");
+ //
+ // _nodeSigner.
+ // var invoice = Bolt11Invoice.from_signed(_channelManager, _nodeSigner, _logger, _network.GetLdkCurrency(),
+ // }
+
public async Task RequestPayment(LightMoney amount, TimeSpan expiry, uint256 descriptionHash)
{
var amt = amount == LightMoney.Zero ? Option_u64Z.none() : Option_u64Z.some(amount.MilliSatoshi);
-
- var epoch = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
- // var result =
- // org.ldk.util.UtilMethods.create_invoice_from_channelmanager_and_duration_since_epoch_with_payment_hash(
- // _channelManager, _nodeSigner, _logger,
- // _network.GetLdkCurrency(), amt, description, epoch, (int) Math.Ceiling(expiry.TotalSeconds),
- // paymentHash, Option_u16Z.none());
+ var now = DateTimeOffset.UtcNow;
+ var epoch = now.ToUnixTimeSeconds();
var descHashBytes = Sha256.from_bytes(descriptionHash.ToBytes());
-
- var result = await Task.Run(() =>
- org.ldk.util.UtilMethods.create_invoice_from_channelmanager_with_description_hash_and_duration_since_epoch(
- _channelManager, _nodeSigner, _logger,
- _network.GetLdkCurrency(), amt, descHashBytes, epoch, (int) Math.Ceiling(expiry.TotalSeconds),
- Option_u16Z.none()));
- if (result is Result_Bolt11InvoiceSignOrCreationErrorZ.Result_Bolt11InvoiceSignOrCreationErrorZ_Err err)
- {
- throw new Exception(err.err.to_str());
- }
-
- var invoice = ((Result_Bolt11InvoiceSignOrCreationErrorZ.Result_Bolt11InvoiceSignOrCreationErrorZ_OK) result)
- .res;
- var preimageResult = _channelManager.get_payment_preimage(invoice.payment_hash(), invoice.payment_secret());
- byte[] preimage = null;
- if (preimageResult is
- Result_ThirtyTwoBytesAPIErrorZ.Result_ThirtyTwoBytesAPIErrorZ_Err errx)
- {
-
- throw new Exception(errx.err.GetError());
- }else if (preimageResult is Result_ThirtyTwoBytesAPIErrorZ.Result_ThirtyTwoBytesAPIErrorZ_OK ok)
- {
- preimage = ok.res;
- }
- var bolt11 = invoice.to_str();
- var lp = new LightningPayment()
+ var lsp = await _ldkNode.GetJITLSPService();
+
+ generateInvoice:
+ JITFeeResponse? jitFeeReponse = null;
+ if (lsp is not null)
+ {
+ jitFeeReponse = await lsp.CalculateInvoiceAmount(amount);
+ if (jitFeeReponse is not null)
+ {
+
+ amt = Option_u64Z.some(jitFeeReponse.AmountToGenerateOurInvoice);
+
+ }
+ else
+ {
+ lsp = null;
+ }
+ }
+
+ var result = await Task.Run(() =>
+ org.ldk.util.UtilMethods.create_invoice_from_channelmanager_with_description_hash_and_duration_since_epoch(
+ _channelManager, _nodeSigner, _logger,
+ _network.GetLdkCurrency(), amt, descHashBytes, epoch, (int) Math.Ceiling(expiry.TotalSeconds),
+ Option_u16Z.none()));
+ if (result is Result_Bolt11InvoiceSignOrCreationErrorZ.Result_Bolt11InvoiceSignOrCreationErrorZ_Err err)
+ {
+ throw new Exception(err.err.to_str());
+ }
+ var originalInvoice = ((Result_Bolt11InvoiceSignOrCreationErrorZ.Result_Bolt11InvoiceSignOrCreationErrorZ_OK) result)
+ .res;
+
+
+ var preimageResult = _channelManager.get_payment_preimage(originalInvoice.payment_hash(), originalInvoice.payment_secret());
+ var preimage = preimageResult switch
+ {
+ Result_ThirtyTwoBytesAPIErrorZ.Result_ThirtyTwoBytesAPIErrorZ_Err errx => throw new Exception(
+ errx.err.GetError()),
+ Result_ThirtyTwoBytesAPIErrorZ.Result_ThirtyTwoBytesAPIErrorZ_OK ok => ok.res,
+ _ => throw new Exception("Unknown error retrieving preimage")
+ };
+
+
+ var parsedOriginalInvoice= BOLT11PaymentRequest.Parse(originalInvoice.to_str(), _network);
+ var lp = new LightningPayment()
{
Inbound = true,
PaymentId = "default",
Value = amount.MilliSatoshi,
- PaymentHash = Convert.ToHexString(invoice.payment_hash()).ToLower(),
- Secret = Convert.ToHexString(invoice.payment_secret()).ToLower(),
+ PaymentHash = parsedOriginalInvoice.PaymentHash!.ToString(),
+ Secret = parsedOriginalInvoice.PaymentSecret!.ToString(),
Preimage = Convert.ToHexString(preimage!).ToLower(),
Status = LightningPaymentStatus.Pending,
- Timestamp = DateTimeOffset.FromUnixTimeSeconds(epoch),
- PaymentRequests = [bolt11]
+ Timestamp = now,
+ PaymentRequests = [parsedOriginalInvoice.ToString()],
+ AdditionalData = new Dictionary()
+ {
+ [LightningPaymentDescriptionKey] = JsonSerializer.SerializeToDocument(descriptionHash.ToString()),
+ [LightningPaymentExpiryKey] = JsonSerializer.SerializeToDocument(originalInvoice.expires_at())
+ }
};
+
+ if (lsp is not null)
+ {
+ if(!await lsp.WrapInvoice(lp,jitFeeReponse ))
+ {
+
+ amt = amount == LightMoney.Zero ? Option_u64Z.none() : Option_u64Z.some(amount.MilliSatoshi);
+ lsp = null;
+ goto generateInvoice;
+ }
+ }
+
await Payment(lp);
return lp;
}
-
+
public async Task PayInvoice(BOLT11PaymentRequest paymentRequest,
LightMoney? explicitAmount = null)
{
@@ -270,13 +330,45 @@ public async Task Handle(Event.Event_PaymentClaimable eventPaymentClaimable)
payment.Inbound && payment.Status == LightningPaymentStatus.Pending);
+
var preimage = eventPaymentClaimable.purpose.GetPreimage(out _) ??
(accept?.Preimage is not null ? Convert.FromHexString(accept.Preimage) : null);
- if (accept is not null && preimage is not null)
+
+ if (accept is null || preimage is null)
+ {
+
+ _channelManager.fail_htlc_backwards(eventPaymentClaimable.payment_hash);
+ return;
+ }
+
+ if (accept.Value == eventPaymentClaimable.amount_msat)
+ {
+
_channelManager.claim_funds(preimage);
+ return;
+ }
+ //this discrepancy could have been used to pay for a JIT channel opening
+ else if(_acceptedChannels.TryGetValue(eventPaymentClaimable.via_channel_id.hash(), out var channelRequest) &&
+ accept.AdditionalData.TryGetValue(VoltageFlow2Jit.LightningPaymentLSPKey, out var lspDoc ) &&
+ lspDoc.Deserialize() is { } lsp &&
+ await _ldkNode.GetJITLSPService() is { } lspService &&
+ lspService.ProviderName == lsp &&
+ accept.AdditionalData.TryGetValue(VoltageFlow2Jit.LightningPaymentJITFeeKey, out var lspFee ) && lspFee.Deserialize() is { } fee)
+ {
+ if (fee.AmountToGenerateOurInvoice == eventPaymentClaimable.amount_msat)
+ {
+ _acceptedChannels.Remove(eventPaymentClaimable.via_channel_id.hash(), out _);
+ _channelManager.claim_funds(preimage);
+ return;
+ }
+ }
else
-
+ {
_channelManager.fail_htlc_backwards(eventPaymentClaimable.payment_hash);
+ }
+
+
+ _channelManager.fail_htlc_backwards(eventPaymentClaimable.payment_hash);
}
public async Task Handle(Event.Event_PaymentClaimed eventPaymentClaimed)
@@ -299,4 +391,21 @@ await PaymentUpdate(Convert.ToHexString(eventPaymentSent.payment_hash).ToLower()
((Option_ThirtyTwoBytesZ.Option_ThirtyTwoBytesZ_Some) eventPaymentSent.payment_id).some).ToLower(), false,
Convert.ToHexString(eventPaymentSent.payment_preimage).ToLower());
}
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ _openChannelRequestEventHandler.AcceptedChannel += AcceptedChannel;
+ }
+
+ private ConcurrentDictionary _acceptedChannels = new();
+ private Task AcceptedChannel(object? sender, Event.Event_OpenChannelRequest e)
+ {
+ _acceptedChannels.TryAdd(e.temporary_channel_id.hash(), e);
+ return Task.CompletedTask;
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken)
+ {
+ _openChannelRequestEventHandler.AcceptedChannel -= AcceptedChannel;
+ }
}
\ No newline at end of file
diff --git a/BTCPayApp.Core/LSP/JIT/BTCPayJIT.cs b/BTCPayApp.Core/LSP/JIT/BTCPayJIT.cs
deleted file mode 100644
index a6cb50a..0000000
--- a/BTCPayApp.Core/LSP/JIT/BTCPayJIT.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using BTCPayApp.Core.Attempt2;
-using BTCPayServer.Lightning;
-
-namespace BTCPayApp.Core.LSP.JIT;
-
-public class BTCPayJIT: IJITService
-{
- public BTCPayJIT(BTCPayConnectionManager btcPayConnectionManager)
- {
-
- }
-
- public string ProviderName => "BTCPayServer";
-
- public async Task WrapInvoice(BOLT11PaymentRequest invoice)
- {
- throw new NotImplementedException();
- }
-}
-
-public interface IJITService
-{
- public string ProviderName { get; }
- public Task WrapInvoice(BOLT11PaymentRequest invoice);
-}
\ No newline at end of file
diff --git a/BTCPayApp.Core/LSP/JIT/Flow2Client.cs b/BTCPayApp.Core/LSP/JIT/Flow2Client.cs
new file mode 100644
index 0000000..c6ff046
--- /dev/null
+++ b/BTCPayApp.Core/LSP/JIT/Flow2Client.cs
@@ -0,0 +1,214 @@
+using System.Net;
+using System.Text.Json;
+using AngleSharp.Dom;
+using BTCPayApp.Core.Attempt2;
+using BTCPayApp.Core.Data;
+using BTCPayApp.Core.Helpers;
+using BTCPayApp.Core.LDK;
+using BTCPayServer.Lightning;
+using Microsoft.Extensions.Logging;
+using NBitcoin;
+using Newtonsoft.Json;
+using org.ldk.structs;
+using org.ldk.util;
+using JsonSerializer = System.Text.Json.JsonSerializer;
+using LightningPayment = BTCPayApp.CommonServer.Models.LightningPayment;
+
+namespace BTCPayApp.Core.LSP.JIT;
+
+
+///
+/// https://docs.voltage.cloud/flow/flow-2.0
+///
+public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandler
+
+{
+ private readonly HttpClient _httpClient;
+ private readonly Network _network;
+ private readonly LDKNode _node;
+ private readonly ChannelManager _channelManager;
+ private readonly ILogger _logger;
+
+ public static Uri? BaseAddress(Network network)
+ {
+ return network switch
+ {
+ not null when network == Network.Main => new Uri("https://lsp.voltageapi.com"),
+ not null when network == Network.TestNet => new Uri("https://testnet-lsp.voltageapi.com"),
+ // not null when network == Network.RegTest => new Uri("https://localhost:5001/jit-lsp"),
+ _ => null
+ };
+ }
+
+ public VoltageFlow2Jit(IHttpClientFactory httpClientFactory, Network network, LDKNode node,
+ ChannelManager channelManager, ILogger logger)
+ {
+ var httpClientInstance = httpClientFactory.CreateClient("VoltageFlow2JIT");
+ httpClientInstance.BaseAddress = BaseAddress(network);
+
+ _httpClient = httpClientInstance;
+ _network = network;
+ _node = node;
+ _channelManager = channelManager;
+ _logger = logger;
+ }
+
+ public async Task GetInfo(CancellationToken cancellationToken = default)
+ {
+ var path = "/api/v1/info";
+ var response = await _httpClient.GetAsync(path, cancellationToken);
+ response.EnsureSuccessStatusCode();
+ var content = await response.Content.ReadAsStringAsync(cancellationToken);
+ return JsonConvert.DeserializeObject(content);
+ }
+
+ public async Task GetFee(LightMoney amount, PubKey pubkey,
+ CancellationToken cancellationToken = default)
+ {
+ var path = "/api/v1/fee";
+ var request = new FlowFeeRequest(amount, pubkey);
+ var response = await _httpClient.PostAsync(path, new StringContent(JsonConvert.SerializeObject(request)),
+ cancellationToken);
+ response.EnsureSuccessStatusCode();
+ var content = await response.Content.ReadAsStringAsync(cancellationToken);
+ return JsonConvert.DeserializeObject(content);
+ }
+
+ public async Task GetProposal(BOLT11PaymentRequest bolt11PaymentRequest,
+ EndPoint? endPoint = null, string? feeId = null, CancellationToken cancellationToken = default)
+ {
+ var path = "/api/v1/proposal";
+ var request = new FlowProposalRequest()
+ {
+ Bolt11 = bolt11PaymentRequest.ToString(),
+ Host = endPoint?.Host(),
+ Port = endPoint?.Port(),
+ FeeId = feeId,
+ };
+ var response = await _httpClient
+ .PostAsync(path, new StringContent(JsonConvert.SerializeObject(request)), cancellationToken);
+ response.EnsureSuccessStatusCode();
+ var content = await response.Content.ReadAsStringAsync(cancellationToken);
+ var result = JsonConvert.DeserializeObject(content);
+
+ return BOLT11PaymentRequest.Parse(result!.WrappedBolt11, _network);
+ }
+
+ public string ProviderName => "Voltage";
+ public async Task CalculateInvoiceAmount(LightMoney expectedAmount)
+ {
+ try
+ {
+
+ var fee = await GetFee(expectedAmount, _node.NodeId);
+ return new JITFeeResponse(expectedAmount, expectedAmount + fee.Amount, fee.Amount, fee.Id, ProviderName);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Error while calculating invoice amount");
+ return null;
+ }
+ }
+
+ public const string LightningPaymentJITFeeKey = "JITFeeKey";
+ public const string LightningPaymentLSPKey = "LSP";
+ public async Task WrapInvoice(LightningPayment lightningPayment, JITFeeResponse? fee = null)
+ {
+
+ if (lightningPayment.PaymentRequests.Count > 1)
+ {
+ return false;
+ }
+ if(lightningPayment.AdditionalData?.ContainsKey(LightningPaymentLSPKey) is true)
+ return false;
+
+
+ fee??= await CalculateInvoiceAmount(new LightMoney(lightningPayment.Value));
+
+ if(fee is null)
+ return false;
+ var invoice = BOLT11PaymentRequest.Parse(lightningPayment.PaymentRequests[0], _network);
+
+
+ var proposal = await GetProposal(invoice,null, fee!.FeeIdentifier);
+ if(proposal.MinimumAmount != fee.AmountToRequestPayer || proposal.PaymentHash != invoice.PaymentHash)
+ return false;
+
+ lightningPayment.PaymentRequests.Insert(0, proposal.ToString());
+ lightningPayment.AdditionalData ??= new Dictionary();
+ lightningPayment.AdditionalData[LightningPaymentLSPKey] = JsonSerializer.SerializeToDocument(ProviderName);
+ lightningPayment.AdditionalData[LightningPaymentJITFeeKey] = JsonSerializer.SerializeToDocument(fee);
+ return true;
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ _node.ConfigUpdated += ConfigUpdated;
+ _ = Task.Run(async () =>
+ {
+
+ await ConfigUpdated(this, await _node.GetConfig());
+ }, cancellationToken);
+ }
+
+ private FlowInfoResponse? _info;
+
+ private async Task ConfigUpdated(object? sender, LightningConfig e)
+ {
+ if (e.JITLSP == ProviderName)
+ {
+ _info ??= await GetInfo();
+
+
+ var ni = _info.ToNodeInfo();
+ var configPeers = await _node.GetConfig();
+ var pubkey = new PubKey(_info.PubKey);
+ if (configPeers.Peers.TryGetValue(_info.PubKey, out var peer))
+ {
+ //check if the endpoint matches any of the info ones
+ if(!_info.ConnectionMethods.Any(a => a.ToEndpoint().ToEndpointString().Equals(peer.Endpoint, StringComparison.OrdinalIgnoreCase)))
+ {
+ peer = new PeerInfo {Endpoint = _info.ConnectionMethods.First().ToEndpoint().ToEndpointString(), Persistent = true, Trusted = true};
+ }else if (peer is {Persistent: true, Trusted: true})
+ return;
+ else
+ {
+ peer = peer with
+ {
+ Persistent = true,
+ Trusted = true
+ };
+ }
+ }
+ else
+ {
+
+ peer = new PeerInfo {Endpoint = _info.ConnectionMethods.First().ToEndpoint().ToEndpointString(), Persistent = true, Trusted = true};
+ }
+
+ _ = _node.Peer(pubkey, peer);
+ }
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken)
+ {
+ _node.ConfigUpdated -= ConfigUpdated;
+ }
+
+ public async Task Handle(Event.Event_ChannelPending @event)
+ {
+ var nodeId = new PubKey(@event.counterparty_node_id);
+ if(nodeId.ToString() == _info?.PubKey)
+ {
+ var channel = _channelManager
+ .list_channels_with_counterparty(@event.counterparty_node_id)
+ .FirstOrDefault(a => a.get_channel_id().eq(@event.channel_id));
+ if(channel is null)
+ return;
+ var channelConfig = channel.get_config();
+ channelConfig.set_accept_underpaying_htlcs(true);
+ _channelManager.update_channel_config(@event.counterparty_node_id, new[] {@event.channel_id}, channelConfig);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/BTCPayApp.Core/LSP/JIT/FlowFeeRequest.cs b/BTCPayApp.Core/LSP/JIT/FlowFeeRequest.cs
new file mode 100644
index 0000000..da738b8
--- /dev/null
+++ b/BTCPayApp.Core/LSP/JIT/FlowFeeRequest.cs
@@ -0,0 +1,21 @@
+using BTCPayServer.Lightning;
+using NBitcoin;
+using Newtonsoft.Json;
+
+namespace BTCPayApp.Core.LSP.JIT;
+
+public class FlowFeeRequest
+{
+ public FlowFeeRequest()
+ {
+ }
+
+ public FlowFeeRequest(LightMoney amount, PubKey pubkey)
+ {
+ Amount = amount.MilliSatoshi;
+ PubKey = pubkey.ToHex();
+ }
+
+ [JsonProperty("amount_msat")] public long Amount { get; set; }
+ [JsonProperty("pubkey")] public string PubKey { get; set; }
+}
\ No newline at end of file
diff --git a/BTCPayApp.Core/LSP/JIT/FlowFeeResponse.cs b/BTCPayApp.Core/LSP/JIT/FlowFeeResponse.cs
new file mode 100644
index 0000000..868c69e
--- /dev/null
+++ b/BTCPayApp.Core/LSP/JIT/FlowFeeResponse.cs
@@ -0,0 +1,9 @@
+using Newtonsoft.Json;
+
+namespace BTCPayApp.Core.LSP.JIT;
+
+public class FlowFeeResponse
+{
+ [JsonProperty("amount_msat")] public long Amount { get; set; }
+ [JsonProperty("id")] public required string Id { get; set; }
+}
\ No newline at end of file
diff --git a/BTCPayApp.Core/LSP/JIT/FlowInfoResponse.cs b/BTCPayApp.Core/LSP/JIT/FlowInfoResponse.cs
new file mode 100644
index 0000000..866f58a
--- /dev/null
+++ b/BTCPayApp.Core/LSP/JIT/FlowInfoResponse.cs
@@ -0,0 +1,31 @@
+using System.Net;
+using BTCPayApp.Core.Helpers;
+using BTCPayServer.Lightning;
+using NBitcoin;
+using Newtonsoft.Json;
+
+namespace BTCPayApp.Core.LSP.JIT;
+
+public class FlowInfoResponse
+{
+ [JsonProperty("connection_methods")] public ConnectionMethod[] ConnectionMethods { get; set; }
+ [JsonProperty("pubkey")] public required string PubKey { get; set; }
+
+ public NodeInfo[] ToNodeInfo()
+ {
+ var pubkey = new PubKey(PubKey);
+ return ConnectionMethods.Select(method => new NodeInfo(pubkey, method.Address, method.Port)).ToArray();
+ }
+
+ public class ConnectionMethod
+ {
+ [JsonProperty("address")] public string Address { get; set; }
+ [JsonProperty("port")] public int Port { get; set; }
+ [JsonProperty("type")] public string Type { get; set; }
+
+ public EndPoint? ToEndpoint()
+ {
+ return EndPointParser.TryParse($"{Address}:{Port}", 9735, out var endpoint) ? endpoint : null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/BTCPayApp.Core/LSP/JIT/FlowProposalRequest.cs b/BTCPayApp.Core/LSP/JIT/FlowProposalRequest.cs
new file mode 100644
index 0000000..c6ce79f
--- /dev/null
+++ b/BTCPayApp.Core/LSP/JIT/FlowProposalRequest.cs
@@ -0,0 +1,18 @@
+using Newtonsoft.Json;
+
+namespace BTCPayApp.Core.LSP.JIT;
+
+public class FlowProposalRequest
+{
+ [JsonProperty("bolt11")] public required string Bolt11 { get; set; }
+
+ [JsonProperty("host", NullValueHandling = NullValueHandling.Ignore)]
+ public string? Host { get; set; }
+
+
+ [JsonProperty("port", NullValueHandling = NullValueHandling.Ignore)]
+ public int? Port { get; set; }
+
+ [JsonProperty("fee_id", NullValueHandling = NullValueHandling.Ignore)]
+ public string? FeeId { get; set; }
+}
\ No newline at end of file
diff --git a/BTCPayApp.Core/LSP/JIT/FlowProposalResponse.cs b/BTCPayApp.Core/LSP/JIT/FlowProposalResponse.cs
new file mode 100644
index 0000000..abce8e9
--- /dev/null
+++ b/BTCPayApp.Core/LSP/JIT/FlowProposalResponse.cs
@@ -0,0 +1,8 @@
+using Newtonsoft.Json;
+
+namespace BTCPayApp.Core.LSP.JIT;
+
+public class FlowProposalResponse
+{
+ [JsonProperty("jit_bolt11")] public required string WrappedBolt11 { get; set; }
+}
\ No newline at end of file
diff --git a/BTCPayApp.Core/LSP/JIT/IJITService.cs b/BTCPayApp.Core/LSP/JIT/IJITService.cs
new file mode 100644
index 0000000..51620d5
--- /dev/null
+++ b/BTCPayApp.Core/LSP/JIT/IJITService.cs
@@ -0,0 +1,66 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using BTCPayServer.Lightning;
+using LightningPayment = BTCPayApp.CommonServer.Models.LightningPayment;
+
+namespace BTCPayApp.Core.LSP.JIT;
+
+public interface IJITService
+{
+ public string ProviderName { get; }
+ public Task CalculateInvoiceAmount(LightMoney expectedAmount);
+ public Task WrapInvoice(LightningPayment lightningPayment, JITFeeResponse? feeReponse);
+}
+
+public class LightMoneyJsonConverter : JsonConverter
+{
+ public override LightMoney? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return reader.TokenType switch
+ {
+ JsonTokenType.String => LightMoney.Parse(reader.GetString()),
+ JsonTokenType.Null => null,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, LightMoney value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToString());
+ }
+}
+
+public record JITFeeResponse
+{
+ public JITFeeResponse(LightMoney AmountToRequestPayer, LightMoney AmountToGenerateOurInvoice, LightMoney LSPFee,
+ string FeeIdentifier, string LSP)
+ {
+ this.AmountToRequestPayer = AmountToRequestPayer;
+ this.AmountToGenerateOurInvoice = AmountToGenerateOurInvoice;
+ this.LSPFee = LSPFee;
+ this.FeeIdentifier = FeeIdentifier;
+ this.LSP = LSP;
+ }
+
+ [JsonConverter(typeof(LightMoneyJsonConverter))]
+ public LightMoney AmountToRequestPayer { get; init; }
+
+ [JsonConverter(typeof(LightMoneyJsonConverter))]
+ public LightMoney AmountToGenerateOurInvoice { get; init; }
+
+ [JsonConverter(typeof(LightMoneyJsonConverter))]
+ public LightMoney LSPFee { get; init; }
+
+ public string FeeIdentifier { get; init; }
+
+ public string LSP { get; set; }
+
+ public void Deconstruct(out LightMoney AmountToRequestPayer, out LightMoney AmountToGenerateOurInvoice,
+ out LightMoney LSPFee, out string FeeIdentifier)
+ {
+ AmountToRequestPayer = this.AmountToRequestPayer;
+ AmountToGenerateOurInvoice = this.AmountToGenerateOurInvoice;
+ LSPFee = this.LSPFee;
+ FeeIdentifier = this.FeeIdentifier;
+ }
+}
\ No newline at end of file
diff --git a/BTCPayApp.UI/Pages/Lightning/LN.razor b/BTCPayApp.UI/Pages/Lightning/LN.razor
index ebc958f..8f31c0f 100644
--- a/BTCPayApp.UI/Pages/Lightning/LN.razor
+++ b/BTCPayApp.UI/Pages/Lightning/LN.razor
@@ -108,45 +108,45 @@ else
@if (_channels?.Any() is true)
{
-
-
-
- Channel ID |
- User Channel ID |
- Counterparty |
- Short Channel ID |
- Confirmations |
- Confirmations Required |
- Funding Transaction Hash |
- Usable |
- Ready |
- Balance |
- Inbound |
- Outbound |
- State |
-
-
-
- @foreach (var channel in _channels)
- {
+
+
- @channel.id |
- @channel.userId |
- @channel.counterparty |
- @channel.shortChannelId |
- @channel.confirmations |
- @channel.confirmationsRequired |
- @channel.fundingTransactionHash |
- @channel.usable |
- @channel.ready |
- @channel.Balance |
- @channel.Inbound |
- @channel.Outbound |
- @channel.State |
+ Channel ID |
+ User Channel ID |
+ Counterparty |
+ Short Channel ID |
+ Confirmations |
+ Confirmations Required |
+ Funding Transaction Hash |
+ Usable |
+ Ready |
+ Balance |
+ Inbound |
+ Outbound |
+ State |
- }
-
-
+
+
+ @foreach (var channel in _channels)
+ {
+
+ @channel.id |
+ @channel.userId |
+ @channel.counterparty |
+ @channel.shortChannelId |
+ @channel.confirmations |
+ @channel.confirmationsRequired |
+ @channel.fundingTransactionHash |
+ @channel.usable |
+ @channel.ready |
+ @channel.Balance |
+ @channel.Inbound |
+ @channel.Outbound |
+ @channel.State |
+
+ }
+
+
}
else
@@ -155,7 +155,8 @@ else
}
@if (_peers?.Any() is true)
{
- Open channel to
+
+ Open channel to
-
+
@if (channelResponse is not null)
{
@channelResponse
}
-
+
}
@@ -177,45 +178,45 @@ else
@if (_payments?.Any() is true)
{
-
-
-
- |
- Payment Hash |
- Inbound |
- Id |
- Preimage |
- Secret |
- Timestamp |
- Value |
- Status |
- Invoices |
-
-
-
- @foreach (var payment in _payments)
- {
+
+
-
- @if (payment.Status == LightningPaymentStatus.Pending)
- {
-
- }
-
- |
- @payment.PaymentHash |
- @payment.Inbound |
- @payment.PaymentId |
- @payment.Preimage |
- @payment.Secret |
- @payment.Timestamp |
- @payment.Value |
- @payment.Status |
- @string.Join('\n', payment.PaymentRequests) |
+ |
+ Payment Hash |
+ Inbound |
+ Id |
+ Preimage |
+ Secret |
+ Timestamp |
+ Value |
+ Status |
+ Invoices |
- }
-
-
+
+
+ @foreach (var payment in _payments)
+ {
+
+
+ @if (payment.Status == LightningPaymentStatus.Pending)
+ {
+
+ }
+
+ |
+ @payment.PaymentHash |
+ @payment.Inbound |
+ @payment.PaymentId |
+ @payment.Preimage |
+ @payment.Secret |
+ @payment.Timestamp |
+ @payment.Value |
+ @payment.Status |
+ @string.Join('\n', payment.PaymentRequests) |
+
+ }
+
+
}
else
@@ -231,18 +232,18 @@ else
-
-
-
-
-
+
+
+
+
+
@if (paymentResponse is not null)
{
@paymentResponse
}
-
+
}
@@ -338,15 +339,15 @@ else
}
}
- private async void UpdatePeer(string toString, PeerInfo? value)
+ private async void UpdatePeer(string nodeId, PeerInfo? value)
{
try
{
Loading = true;
await InvokeAsync(StateHasChanged);
await _semaphore.WaitAsync();
-
- await Node.Peer(toString, value);
+ var peer = new PubKey(nodeId);
+ await Node.Peer(peer, value);
}
finally
{
@@ -378,27 +379,27 @@ else
private string? selectedPeer { get; set; }
private decimal? channelOpenAmount { get; set; }
private string? channelResponse { get; set; }
+
private async void OpenChannel()
{
- if(Loading || channelOpenAmount is null || selectedPeer is null)
+ if (Loading || channelOpenAmount is null || selectedPeer is null)
return;
try
{
Loading = true;
await InvokeAsync(StateHasChanged);
await _semaphore.WaitAsync();
- var result = await Node.OpenChannel(Money.Coins(channelOpenAmount.Value), new PubKey(selectedPeer) );
+ var result = await Node.OpenChannel(Money.Coins(channelOpenAmount.Value), new PubKey(selectedPeer));
if (result is Result_ChannelIdAPIErrorZ.Result_ChannelIdAPIErrorZ_OK ok)
{
channelResponse = $"Channel creation started with id {Convert.ToHexString(ok.res.get_a())}";
channelOpenAmount = null;
selectedPeer = null;
}
- else if(result is Result_ChannelIdAPIErrorZ.Result_ChannelIdAPIErrorZ_Err err)
+ else if (result is Result_ChannelIdAPIErrorZ.Result_ChannelIdAPIErrorZ_Err err)
{
channelResponse = $"Error: {err.err.GetError()}";
}
-
}
finally
{
@@ -411,9 +412,10 @@ else
private decimal? paymentRequestAmt;
private string? paymentRequestSend;
private string? paymentResponse;
+
private async void ReceivePayment()
{
- if(Loading || paymentRequestAmt is null)
+ if (Loading || paymentRequestAmt is null)
return;
try
{
@@ -421,11 +423,10 @@ else
await InvokeAsync(StateHasChanged);
await _semaphore.WaitAsync();
var hash = new uint256(Hashes.SHA256(RandomUtils.GetBytes(32)));
- var result = await Node.PaymentsManager.RequestPayment(LightMoney.Satoshis(paymentRequestAmt??0), TimeSpan.FromDays(1), hash);
+ var result = await Node.PaymentsManager.RequestPayment(LightMoney.Satoshis(paymentRequestAmt ?? 0), TimeSpan.FromDays(1), hash);
- paymentResponse = $"Payment request created with invs {string.Join(',',result.PaymentRequests)}";
+ paymentResponse = $"Payment request created with invs {string.Join(',', result.PaymentRequests)}";
paymentRequestAmt = null;
-
}
catch (Exception e)
{
@@ -438,20 +439,20 @@ else
_semaphore.Release();
}
}
+
public async void SendPayment()
{
- if(Loading || paymentRequestSend is null)
+ if (Loading || paymentRequestSend is null)
return;
try
{
Loading = true;
await InvokeAsync(StateHasChanged);
await _semaphore.WaitAsync();
- var invoice = BOLT11PaymentRequest.Parse(paymentRequestSend, Node.Network );
-
+ var invoice = BOLT11PaymentRequest.Parse(paymentRequestSend, Node.Network);
- var result = await Node.PaymentsManager.PayInvoice(invoice, paymentRequestAmt is null? null: LightMoney.Satoshis((long)paymentRequestAmt.Value));
+ var result = await Node.PaymentsManager.PayInvoice(invoice, paymentRequestAmt is null ? null : LightMoney.Satoshis((long) paymentRequestAmt.Value));
paymentResponse = $"Payment {result.PaymentId} sent with status {result.Status}";
paymentRequestAmt = null;
paymentRequestSend = null;
@@ -467,9 +468,10 @@ else
_semaphore.Release();
}
}
+
public async void Cancel(string paymentId, bool inb)
{
- if(Loading)
+ if (Loading)
return;
try
{
@@ -486,4 +488,4 @@ else
}
}
-}
+}
\ No newline at end of file
diff --git a/BTCPayApp.UI/Pages/Lightning/SetupPage.razor b/BTCPayApp.UI/Pages/Lightning/SetupPage.razor
index 9153dab..922759a 100644
--- a/BTCPayApp.UI/Pages/Lightning/SetupPage.razor
+++ b/BTCPayApp.UI/Pages/Lightning/SetupPage.razor
@@ -45,6 +45,17 @@
Color: @_config.Color
+
+
+
+
+
}
@if (!string.IsNullOrEmpty(_config?.LightningDerivationPath))
@@ -85,7 +96,7 @@
private string? ConfiguredConnectionString;
private string ConnectionString => $"type=app;group={OnChainWalletManager.WalletConfig?.Derivations[WalletDerivation.LightningScripts].Identifier}".ToLower();
-
+ private string[] JITOptions;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
@@ -93,6 +104,7 @@
if (LightningNodeManager?.Node is not null)
{
_config = await LightningNodeManager.Node.GetConfig();
+ JITOptions = await LightningNodeManager.Node.GetJITLSPs();
}
var acc = AccountManager.GetAccount();
if (acc?.CurrentStoreId != null)
@@ -154,4 +166,13 @@
Logger.LogError(ex, "Error configuring LN wallet");
}
}
+
+ private async Task OnSelectLSP(ChangeEventArgs obj)
+ {
+
+ _config = await LightningNodeManager.Node.GetConfig();
+ _config.JITLSP = obj.Value?.ToString();
+ await LightningNodeManager.Node.UpdateConfig(_config);
+ }
+
}