From b123a314f26bcf312242e97a1739b9da7eada416 Mon Sep 17 00:00:00 2001
From: Kukks
Date: Fri, 21 Jun 2024 08:24:18 +0200
Subject: [PATCH 1/3] jit wip
subm
---
BTCPayApp.Core/Attempt2/LDKNode.cs | 18 +-
BTCPayApp.Core/LDK/LDKExtensions.cs | 4 +
BTCPayApp.Core/LDK/PaymentsManager.cs | 58 +++-
BTCPayApp.Core/LSP/JIT/BTCPayJIT.cs | 14 +-
BTCPayApp.Core/LSP/JIT/Flow2Client.cs | 275 ++++++++++++++++++
BTCPayApp.Core/LSP/JIT/FlowFeeRequest.cs | 21 ++
BTCPayApp.Core/LSP/JIT/FlowFeeResponse.cs | 9 +
BTCPayApp.Core/LSP/JIT/FlowInfoResponse.cs | 31 ++
BTCPayApp.Core/LSP/JIT/FlowProposalRequest.cs | 18 ++
.../LSP/JIT/FlowProposalResponse.cs | 8 +
BTCPayApp.Core/LSP/JIT/IJITService.cs | 11 +
11 files changed, 449 insertions(+), 18 deletions(-)
create mode 100644 BTCPayApp.Core/LSP/JIT/Flow2Client.cs
create mode 100644 BTCPayApp.Core/LSP/JIT/FlowFeeRequest.cs
create mode 100644 BTCPayApp.Core/LSP/JIT/FlowFeeResponse.cs
create mode 100644 BTCPayApp.Core/LSP/JIT/FlowInfoResponse.cs
create mode 100644 BTCPayApp.Core/LSP/JIT/FlowProposalRequest.cs
create mode 100644 BTCPayApp.Core/LSP/JIT/FlowProposalResponse.cs
create mode 100644 BTCPayApp.Core/LSP/JIT/IJITService.cs
diff --git a/BTCPayApp.Core/Attempt2/LDKNode.cs b/BTCPayApp.Core/Attempt2/LDKNode.cs
index 885b8c1..6059d52 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
@@ -187,6 +200,7 @@ private async Task UpdateConfig(LightningConfig config)
await _started.Task;
await _configProvider.Set(LightningConfig.Key, config);
_config = config;
+
ConfigUpdated?.Invoke(this, config);
}
@@ -366,9 +380,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/LDK/LDKExtensions.cs b/BTCPayApp.Core/LDK/LDKExtensions.cs
index 66b6ed9..06d880f 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;
@@ -300,6 +301,9 @@ public static IServiceCollection AddLDK(this IServiceCollection services)
ProbabilisticScoringFeeParameters.with_default()));
services.AddScoped(provider => provider.GetRequiredService().as_Router());
+
+ services.AddScoped();
+
return services;
}
diff --git a/BTCPayApp.Core/LDK/PaymentsManager.cs b/BTCPayApp.Core/LDK/PaymentsManager.cs
index c4fd5a6..f7689ef 100644
--- a/BTCPayApp.Core/LDK/PaymentsManager.cs
+++ b/BTCPayApp.Core/LDK/PaymentsManager.cs
@@ -1,10 +1,14 @@
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;
@@ -16,26 +20,31 @@ public class PaymentsManager :
ILDKEventHandler,
ILDKEventHandler
{
+ public const string LightningPaymentDescriptionKey = "DescriptionHash";
+ public const string LightningPaymentExpiryKey = "Expiry";
+
private readonly IDbContextFactory _dbContextFactory;
private readonly ChannelManager _channelManager;
private readonly Logger _logger;
private readonly NodeSigner _nodeSigner;
private readonly Network _network;
+ private readonly LDKNode _ldkNode;
public PaymentsManager(
IDbContextFactory dbContextFactory,
ChannelManager channelManager,
Logger logger,
NodeSigner nodeSigner,
- Network network
- )
+ Network network,
+ LDKNode ldkNode)
{
_dbContextFactory = dbContextFactory;
_channelManager = channelManager;
_logger = logger;
_nodeSigner = nodeSigner;
_network = network;
+ _ldkNode = ldkNode;
}
public async Task> List(
@@ -47,6 +56,24 @@ 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);
@@ -58,9 +85,8 @@ public async Task RequestPayment(LightMoney amount, TimeSpan e
// _network.GetLdkCurrency(), amt, description, epoch, (int) Math.Ceiling(expiry.TotalSeconds),
// paymentHash, Option_u16Z.none());
-
var descHashBytes = Sha256.from_bytes(descriptionHash.ToBytes());
-
+ var lsp = await _ldkNode.GetJITLSPService();
var result = await Task.Run(() =>
org.ldk.util.UtilMethods.create_invoice_from_channelmanager_with_description_hash_and_duration_since_epoch(
_channelManager, _nodeSigner, _logger,
@@ -71,6 +97,11 @@ public async Task RequestPayment(LightMoney amount, TimeSpan e
throw new Exception(err.err.to_str());
}
+ var keyMaterial = _nodeSigner.get_inbound_payment_key_material();
+ var expandedKey = ExpandedKey.of(keyMaterial);
+ _channelManager.create_inbound_payment_for_hash()
+ UtilMethods.create_invoice_from_channelmanager()
+
var invoice = ((Result_Bolt11InvoiceSignOrCreationErrorZ.Result_Bolt11InvoiceSignOrCreationErrorZ_OK) result)
.res;
var preimageResult = _channelManager.get_payment_preimage(invoice.payment_hash(), invoice.payment_secret());
@@ -95,13 +126,24 @@ public async Task RequestPayment(LightMoney amount, TimeSpan e
Preimage = Convert.ToHexString(preimage!).ToLower(),
Status = LightningPaymentStatus.Pending,
Timestamp = DateTimeOffset.FromUnixTimeSeconds(epoch),
- PaymentRequests = [bolt11]
+ PaymentRequests = [bolt11],
+ AdditionalData = new Dictionary()
+ {
+ [LightningPaymentDescriptionKey] = JsonSerializer.SerializeToDocument(descriptionHash.ToString()),
+ [LightningPaymentExpiryKey] = JsonSerializer.SerializeToDocument(invoice.expires_at())
+ }
};
+ if (lsp is null)
+ {
+
+ }
+
+ lsp?.WrapInvoice(lp);
await Payment(lp);
return lp;
}
-
+
public async Task PayInvoice(BOLT11PaymentRequest paymentRequest,
LightMoney? explicitAmount = null)
{
@@ -269,10 +311,12 @@ public async Task Handle(Event.Event_PaymentClaimable eventPaymentClaimable)
payment.PaymentHash == Convert.ToHexString(eventPaymentClaimable.payment_hash).ToLower() &&
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 not null && preimage is not null && accept.Value == eventPaymentClaimable.amount_msat)
_channelManager.claim_funds(preimage);
else
diff --git a/BTCPayApp.Core/LSP/JIT/BTCPayJIT.cs b/BTCPayApp.Core/LSP/JIT/BTCPayJIT.cs
index a6cb50a..39898b5 100644
--- a/BTCPayApp.Core/LSP/JIT/BTCPayJIT.cs
+++ b/BTCPayApp.Core/LSP/JIT/BTCPayJIT.cs
@@ -3,23 +3,19 @@
namespace BTCPayApp.Core.LSP.JIT;
-public class BTCPayJIT: IJITService
+using System.Diagnostics;
+using BTCPayServer.Lightning;
+
+public class BTCPayJIT : IJITService
{
public BTCPayJIT(BTCPayConnectionManager btcPayConnectionManager)
{
-
}
public string ProviderName => "BTCPayServer";
- public async Task WrapInvoice(BOLT11PaymentRequest invoice)
+ public async Task WrapInvoice(LightningPayment lightningPayment)
{
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..642e71e
--- /dev/null
+++ b/BTCPayApp.Core/LSP/JIT/Flow2Client.cs
@@ -0,0 +1,275 @@
+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 NBitcoin;
+using Newtonsoft.Json;
+using org.ldk.structs;
+using org.ldk.util;
+using LightningPayment = BTCPayApp.CommonServer.Models.LightningPayment;
+
+namespace BTCPayApp.Core.LSP.JIT;
+
+public class FlowInvoiceDetails()
+{
+
+ public string FeeId { get; set; }
+ public long FeeAmount { get; set; }
+
+}
+
+///
+/// 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;
+
+ 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)
+ {
+ var httpClientInstance = httpClientFactory.CreateClient("VoltageFlow2JIT");
+ httpClientInstance.BaseAddress = BaseAddress(network);
+
+ _httpClient = httpClientInstance;
+ _network = network;
+ _node = node;
+ _channelManager = channelManager;
+ }
+
+ public VoltageFlow2Jit(HttpClient httpClient, Network network)
+ {
+ if (httpClient.BaseAddress == null)
+ throw new ArgumentException(
+ "HttpClient must have a base address, use Flow2Client.BaseAddress to get a predefined URI",
+ nameof(httpClient));
+
+ _httpClient = httpClient;
+ _network = network;
+ }
+
+ 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<(LightMoney invoiceAmount, LightMoney fee)> CalculateInvoiceAmount(LightMoney expectedAmount)
+ {
+ var fee = await GetFee(expectedAmount, _node.NodeId);
+ return (LightMoney.MilliSatoshis(expectedAmount.MilliSatoshi-fee.Amount), LightMoney.MilliSatoshis(fee.Amount));
+ }
+
+ public async Task WrapInvoice(LightningPayment lightningPayment)
+ {
+
+ if (lightningPayment.PaymentRequests.Count > 1)
+ {
+ return;
+ }
+ if(lightningPayment.AdditionalData?.TryGetValue("flowlsp", out var lsp) is true && lsp.RootElement.Deserialize() is { } invoiceDetails)
+ return;
+
+ var lm = new LightMoney(lightningPayment.Value);
+ var fee = await GetFee(lm, _node.NodeId);
+
+
+ if (lm < fee.Amount)
+ throw new InvalidOperationException("Invoice amount is too low to use Voltage LSP");
+
+
+ return await GetProposal(invoice, null, fee.Id);
+ }
+
+ 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);
+ }
+ }
+
+ private bool VerifyInvoice(BOLT11PaymentRequest ourInvoice,
+ BOLT11PaymentRequest lspInvoice,
+ LightMoney fee)
+ {
+ if(ourInvoice.PaymentHash != lspInvoice.PaymentHash)
+ return false;
+
+ var expected_lsp_invoice_amt = our_invoice_amt + lsp_fee_msats;
+
+ if (Bolt11Invoice.from_str(ourInvoice.ToString()) is Result_Bolt11InvoiceParseOrSemanticErrorZ.Result_Bolt11InvoiceParseOrSemanticErrorZ_OK
+ ourInvoiceResult &&
+ Bolt11Invoice.from_str(lspInvoice.ToString()) is Result_Bolt11InvoiceParseOrSemanticErrorZ.Result_Bolt11InvoiceParseOrSemanticErrorZ_OK lspInvoiceResult)
+ {
+ ourInvoiceResult.res.
+
+ }
+
+ return false;
+
+ if lsp_invoice.network() != our_invoice.network() {
+ return Some(format!(
+ "Received invoice on wrong network: {} != {}",
+ lsp_invoice.network(),
+ our_invoice.network()
+ ));
+ }
+
+ if lsp_invoice.payment_hash() != our_invoice.payment_hash() {
+ return Some(format!(
+ "Received invoice with wrong payment hash: {} != {}",
+ lsp_invoice.payment_hash(),
+ our_invoice.payment_hash()
+ ));
+ }
+
+ let invoice_pubkey = lsp_invoice.recover_payee_pub_key();
+ if invoice_pubkey != self.pubkey {
+ return Some(format!(
+ "Received invoice from wrong node: {invoice_pubkey} != {}",
+ self.pubkey
+ ));
+ }
+
+ if lsp_invoice.amount_milli_satoshis().is_none() {
+ return Some("Invoice amount is missing".to_string());
+ }
+
+ if our_invoice.amount_milli_satoshis().is_none() {
+ return Some("Invoice amount is missing".to_string());
+ }
+
+ let lsp_invoice_amt = lsp_invoice.amount_milli_satoshis().expect("just checked");
+ let our_invoice_amt = our_invoice.amount_milli_satoshis().expect("just checked");
+
+ let expected_lsp_invoice_amt = our_invoice_amt + lsp_fee_msats;
+
+ // verify invoice within 10 sats of our target
+ if lsp_invoice_amt.abs_diff(expected_lsp_invoice_amt) > 10_000 {
+ return Some(format!(
+ "Received invoice with wrong amount: {lsp_invoice_amt} when amount was {expected_lsp_invoice_amt}",
+ ));
+ }
+
+ None
+ }
+
+}
\ 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..dafe715
--- /dev/null
+++ b/BTCPayApp.Core/LSP/JIT/IJITService.cs
@@ -0,0 +1,11 @@
+using BTCPayServer.Lightning;
+using LightningPayment = BTCPayApp.CommonServer.Models.LightningPayment;
+
+namespace BTCPayApp.Core.LSP.JIT;
+
+public interface IJITService
+{
+ public string ProviderName { get; }
+ public Task<(LightMoney invoiceAmount, LightMoney fee)> CalculateInvoiceAmount(LightMoney expectedAmount);
+ public Task WrapInvoice(LightningPayment lightningPayment);
+}
\ No newline at end of file
From e6dffe02fb45bedf20dc37274cb530acdb897e6d Mon Sep 17 00:00:00 2001
From: Kukks
Date: Wed, 26 Jun 2024 13:29:52 +0200
Subject: [PATCH 2/3] crying my way to success
---
BTCPayApp.Core/Data/AppDbContext.cs | 12 +-
BTCPayApp.Core/LDK/LDKExtensions.cs | 1 +
.../LDK/LDKOpenChannelRequestEventHandler.cs | 5 +
BTCPayApp.Core/LDK/PaymentsManager.cs | 189 +++++++++++-------
BTCPayApp.Core/LSP/JIT/Flow2Client.cs | 122 ++++-------
BTCPayApp.Core/LSP/JIT/IJITService.cs | 61 +++++-
6 files changed, 226 insertions(+), 164 deletions(-)
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 06d880f..2ae72c8 100644
--- a/BTCPayApp.Core/LDK/LDKExtensions.cs
+++ b/BTCPayApp.Core/LDK/LDKExtensions.cs
@@ -214,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());
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/PaymentsManager.cs b/BTCPayApp.Core/LDK/PaymentsManager.cs
index f7689ef..168e47d 100644
--- a/BTCPayApp.Core/LDK/PaymentsManager.cs
+++ b/BTCPayApp.Core/LDK/PaymentsManager.cs
@@ -1,4 +1,5 @@
-using System.Security.Cryptography;
+using System.Collections.Concurrent;
+using System.Security.Cryptography;
using System.Text.Json;
using BTCPayApp.Core.Attempt2;
using BTCPayApp.Core.Data;
@@ -15,6 +16,7 @@
namespace BTCPayApp.Core.LDK;
public class PaymentsManager :
+ IScopedHostedService,
ILDKEventHandler,
ILDKEventHandler,
ILDKEventHandler,
@@ -26,6 +28,7 @@ public class PaymentsManager :
private readonly IDbContextFactory _dbContextFactory;
private readonly ChannelManager _channelManager;
+ private readonly LDKOpenChannelRequestEventHandler _openChannelRequestEventHandler;
private readonly Logger _logger;
private readonly NodeSigner _nodeSigner;
private readonly Network _network;
@@ -34,6 +37,7 @@ public class PaymentsManager :
public PaymentsManager(
IDbContextFactory dbContextFactory,
ChannelManager channelManager,
+ LDKOpenChannelRequestEventHandler openChannelRequestEventHandler,
Logger logger,
NodeSigner nodeSigner,
Network network,
@@ -41,6 +45,7 @@ public PaymentsManager(
{
_dbContextFactory = dbContextFactory;
_channelManager = channelManager;
+ _openChannelRequestEventHandler = openChannelRequestEventHandler;
_logger = logger;
_nodeSigner = nodeSigner;
_network = network;
@@ -56,89 +61,102 @@ 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(),
-
-
- }
+ // 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 lsp = await _ldkNode.GetJITLSPService();
- 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 keyMaterial = _nodeSigner.get_inbound_payment_key_material();
- var expandedKey = ExpandedKey.of(keyMaterial);
- _channelManager.create_inbound_payment_for_hash()
- UtilMethods.create_invoice_from_channelmanager()
-
- 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()
+
+ 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(invoice.expires_at())
+ [LightningPaymentExpiryKey] = JsonSerializer.SerializeToDocument(originalInvoice.expires_at())
}
};
- if (lsp is null)
+
+ 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;
+ }
}
-
- lsp?.WrapInvoice(lp);
+
await Payment(lp);
return lp;
@@ -311,16 +329,32 @@ public async Task Handle(Event.Event_PaymentClaimable eventPaymentClaimable)
payment.PaymentHash == Convert.ToHexString(eventPaymentClaimable.payment_hash).ToLower() &&
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 && accept.Value == eventPaymentClaimable.amount_msat)
- _channelManager.claim_funds(preimage);
- else
+
+ 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))
+ {
+
+ }
+
+
+ _channelManager.fail_htlc_backwards(eventPaymentClaimable.payment_hash);
}
public async Task Handle(Event.Event_PaymentClaimed eventPaymentClaimed)
@@ -343,4 +377,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/Flow2Client.cs b/BTCPayApp.Core/LSP/JIT/Flow2Client.cs
index 642e71e..f75d9d8 100644
--- a/BTCPayApp.Core/LSP/JIT/Flow2Client.cs
+++ b/BTCPayApp.Core/LSP/JIT/Flow2Client.cs
@@ -6,21 +6,16 @@
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;
-public class FlowInvoiceDetails()
-{
-
- public string FeeId { get; set; }
- public long FeeAmount { get; set; }
-
-}
///
/// https://docs.voltage.cloud/flow/flow-2.0
@@ -32,6 +27,7 @@ public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandl
private readonly Network _network;
private readonly LDKNode _node;
private readonly ChannelManager _channelManager;
+ private readonly ILogger _logger;
public static Uri? BaseAddress(Network network)
{
@@ -45,7 +41,7 @@ public class VoltageFlow2Jit : IJITService, IScopedHostedService, ILDKEventHandl
}
public VoltageFlow2Jit(IHttpClientFactory httpClientFactory, Network network, LDKNode node,
- ChannelManager channelManager)
+ ChannelManager channelManager, ILogger logger)
{
var httpClientInstance = httpClientFactory.CreateClient("VoltageFlow2JIT");
httpClientInstance.BaseAddress = BaseAddress(network);
@@ -54,6 +50,7 @@ public VoltageFlow2Jit(IHttpClientFactory httpClientFactory, Network network, LD
_network = network;
_node = node;
_channelManager = channelManager;
+ _logger = logger;
}
public VoltageFlow2Jit(HttpClient httpClient, Network network)
@@ -109,31 +106,50 @@ public async Task GetProposal(BOLT11PaymentRequest bolt11P
}
public string ProviderName => "Voltage";
- public async Task<(LightMoney invoiceAmount, LightMoney fee)> CalculateInvoiceAmount(LightMoney expectedAmount)
+ public async Task CalculateInvoiceAmount(LightMoney expectedAmount)
{
- var fee = await GetFee(expectedAmount, _node.NodeId);
- return (LightMoney.MilliSatoshis(expectedAmount.MilliSatoshi-fee.Amount), LightMoney.MilliSatoshis(fee.Amount));
+ 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 async Task WrapInvoice(LightningPayment lightningPayment)
+ 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;
+ return false;
}
- if(lightningPayment.AdditionalData?.TryGetValue("flowlsp", out var lsp) is true && lsp.RootElement.Deserialize() is { } invoiceDetails)
- return;
+ if(lightningPayment.AdditionalData?.ContainsKey(LightningPaymentLSPKey) is true)
+ return false;
- var lm = new LightMoney(lightningPayment.Value);
- var fee = await GetFee(lm, _node.NodeId);
+ fee??= await CalculateInvoiceAmount(new LightMoney(lightningPayment.Value));
+
+ if(fee is null)
+ return false;
+ var invoice = BOLT11PaymentRequest.Parse(lightningPayment.PaymentRequests[0], _network);
- if (lm < fee.Amount)
- throw new InvalidOperationException("Invoice amount is too low to use Voltage LSP");
+ var proposal = await GetProposal(invoice,null, fee!.FeeIdentifier);
+ if(proposal.MinimumAmount != fee.AmountToRequestPayer || proposal.PaymentHash != invoice.PaymentHash)
+ return false;
- return await GetProposal(invoice, null, fee.Id);
+ 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)
@@ -205,71 +221,5 @@ public async Task Handle(Event.Event_ChannelPending @event)
_channelManager.update_channel_config(@event.counterparty_node_id, new[] {@event.channel_id}, channelConfig);
}
}
-
- private bool VerifyInvoice(BOLT11PaymentRequest ourInvoice,
- BOLT11PaymentRequest lspInvoice,
- LightMoney fee)
- {
- if(ourInvoice.PaymentHash != lspInvoice.PaymentHash)
- return false;
-
- var expected_lsp_invoice_amt = our_invoice_amt + lsp_fee_msats;
-
- if (Bolt11Invoice.from_str(ourInvoice.ToString()) is Result_Bolt11InvoiceParseOrSemanticErrorZ.Result_Bolt11InvoiceParseOrSemanticErrorZ_OK
- ourInvoiceResult &&
- Bolt11Invoice.from_str(lspInvoice.ToString()) is Result_Bolt11InvoiceParseOrSemanticErrorZ.Result_Bolt11InvoiceParseOrSemanticErrorZ_OK lspInvoiceResult)
- {
- ourInvoiceResult.res.
-
- }
-
- return false;
-
- if lsp_invoice.network() != our_invoice.network() {
- return Some(format!(
- "Received invoice on wrong network: {} != {}",
- lsp_invoice.network(),
- our_invoice.network()
- ));
- }
-
- if lsp_invoice.payment_hash() != our_invoice.payment_hash() {
- return Some(format!(
- "Received invoice with wrong payment hash: {} != {}",
- lsp_invoice.payment_hash(),
- our_invoice.payment_hash()
- ));
- }
-
- let invoice_pubkey = lsp_invoice.recover_payee_pub_key();
- if invoice_pubkey != self.pubkey {
- return Some(format!(
- "Received invoice from wrong node: {invoice_pubkey} != {}",
- self.pubkey
- ));
- }
-
- if lsp_invoice.amount_milli_satoshis().is_none() {
- return Some("Invoice amount is missing".to_string());
- }
-
- if our_invoice.amount_milli_satoshis().is_none() {
- return Some("Invoice amount is missing".to_string());
- }
-
- let lsp_invoice_amt = lsp_invoice.amount_milli_satoshis().expect("just checked");
- let our_invoice_amt = our_invoice.amount_milli_satoshis().expect("just checked");
-
- let expected_lsp_invoice_amt = our_invoice_amt + lsp_fee_msats;
-
- // verify invoice within 10 sats of our target
- if lsp_invoice_amt.abs_diff(expected_lsp_invoice_amt) > 10_000 {
- return Some(format!(
- "Received invoice with wrong amount: {lsp_invoice_amt} when amount was {expected_lsp_invoice_amt}",
- ));
- }
-
- None
- }
}
\ No newline at end of file
diff --git a/BTCPayApp.Core/LSP/JIT/IJITService.cs b/BTCPayApp.Core/LSP/JIT/IJITService.cs
index dafe715..51620d5 100644
--- a/BTCPayApp.Core/LSP/JIT/IJITService.cs
+++ b/BTCPayApp.Core/LSP/JIT/IJITService.cs
@@ -1,4 +1,6 @@
-using BTCPayServer.Lightning;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using BTCPayServer.Lightning;
using LightningPayment = BTCPayApp.CommonServer.Models.LightningPayment;
namespace BTCPayApp.Core.LSP.JIT;
@@ -6,6 +8,59 @@ namespace BTCPayApp.Core.LSP.JIT;
public interface IJITService
{
public string ProviderName { get; }
- public Task<(LightMoney invoiceAmount, LightMoney fee)> CalculateInvoiceAmount(LightMoney expectedAmount);
- public Task WrapInvoice(LightningPayment lightningPayment);
+ 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
From 733f5bdb0c2fc4198d6218f04fc3ddab6733145c Mon Sep 17 00:00:00 2001
From: Kukks
Date: Wed, 26 Jun 2024 14:20:20 +0200
Subject: [PATCH 3/3] theoretically functional
---
BTCPayApp.Core/Attempt2/LDKNode.cs | 8 +-
BTCPayApp.Core/LDK/LDKExtensions.cs | 4 +-
BTCPayApp.Core/LDK/LDKPeerHandler.cs | 4 +-
BTCPayApp.Core/LDK/PaymentsManager.cs | 18 +-
BTCPayApp.Core/LSP/JIT/BTCPayJIT.cs | 21 --
BTCPayApp.Core/LSP/JIT/Flow2Client.cs | 11 -
BTCPayApp.UI/Pages/Lightning/LN.razor | 202 ++++++++++---------
BTCPayApp.UI/Pages/Lightning/SetupPage.razor | 23 ++-
8 files changed, 151 insertions(+), 140 deletions(-)
delete mode 100644 BTCPayApp.Core/LSP/JIT/BTCPayJIT.cs
diff --git a/BTCPayApp.Core/Attempt2/LDKNode.cs b/BTCPayApp.Core/Attempt2/LDKNode.cs
index 6059d52..ce14083 100644
--- a/BTCPayApp.Core/Attempt2/LDKNode.cs
+++ b/BTCPayApp.Core/Attempt2/LDKNode.cs
@@ -194,8 +194,12 @@ 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);
diff --git a/BTCPayApp.Core/LDK/LDKExtensions.cs b/BTCPayApp.Core/LDK/LDKExtensions.cs
index 2ae72c8..f2275d5 100644
--- a/BTCPayApp.Core/LDK/LDKExtensions.cs
+++ b/BTCPayApp.Core/LDK/LDKExtensions.cs
@@ -303,7 +303,9 @@ public static IServiceCollection AddLDK(this IServiceCollection services)
services.AddScoped(provider => provider.GetRequiredService().as_Router());
- services.AddScoped();
+ services.AddScoped();
+ services.AddScoped(provider => provider.GetRequiredService());
+ services.AddScoped(provider => provider.GetRequiredService());
return services;
}
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 168e47d..3a8cc45 100644
--- a/BTCPayApp.Core/LDK/PaymentsManager.cs
+++ b/BTCPayApp.Core/LDK/PaymentsManager.cs
@@ -348,9 +348,23 @@ public async Task Handle(Event.Event_PaymentClaimable eventPaymentClaimable)
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))
+ 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);
}
diff --git a/BTCPayApp.Core/LSP/JIT/BTCPayJIT.cs b/BTCPayApp.Core/LSP/JIT/BTCPayJIT.cs
deleted file mode 100644
index 39898b5..0000000
--- a/BTCPayApp.Core/LSP/JIT/BTCPayJIT.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using BTCPayApp.Core.Attempt2;
-using BTCPayServer.Lightning;
-
-namespace BTCPayApp.Core.LSP.JIT;
-
-using System.Diagnostics;
-using BTCPayServer.Lightning;
-
-public class BTCPayJIT : IJITService
-{
- public BTCPayJIT(BTCPayConnectionManager btcPayConnectionManager)
- {
- }
-
- public string ProviderName => "BTCPayServer";
-
- public async Task WrapInvoice(LightningPayment lightningPayment)
- {
- throw new NotImplementedException();
- }
-}
\ No newline at end of file
diff --git a/BTCPayApp.Core/LSP/JIT/Flow2Client.cs b/BTCPayApp.Core/LSP/JIT/Flow2Client.cs
index f75d9d8..c6ff046 100644
--- a/BTCPayApp.Core/LSP/JIT/Flow2Client.cs
+++ b/BTCPayApp.Core/LSP/JIT/Flow2Client.cs
@@ -53,17 +53,6 @@ public VoltageFlow2Jit(IHttpClientFactory httpClientFactory, Network network, LD
_logger = logger;
}
- public VoltageFlow2Jit(HttpClient httpClient, Network network)
- {
- if (httpClient.BaseAddress == null)
- throw new ArgumentException(
- "HttpClient must have a base address, use Flow2Client.BaseAddress to get a predefined URI",
- nameof(httpClient));
-
- _httpClient = httpClient;
- _network = network;
- }
-
public async Task GetInfo(CancellationToken cancellationToken = default)
{
var path = "/api/v1/info";
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);
+ }
+
}