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) {
- - - - - - - - - - - - - - - - - - - - @foreach (var channel in _channels) - { +
Channel IDUser Channel IDCounterpartyShort Channel IDConfirmationsConfirmations RequiredFunding Transaction HashUsableReadyBalanceInboundOutboundState
+ - - - - - - - - - - - - - + + + + + + + + + + + + + - } - -
@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.StateChannel IDUser Channel IDCounterpartyShort Channel IDConfirmationsConfirmations RequiredFunding Transaction HashUsableReadyBalanceInboundOutboundState
+ + + @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) {
- - - - - - - - - - - - - - - - - @foreach (var payment in _payments) - { +
Payment HashInboundIdPreimageSecretTimestampValueStatusInvoices
+ - - - - - - - - - - + + + + + + + + + + - } - -
- @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 HashInboundIdPreimageSecretTimestampValueStatusInvoices
+ + + @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); + } + }