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