using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using BTCPayApp.CommonServer; using BTCPayServer.Client.Models; using BTCPayServer.Controllers; using BTCPayServer.Lightning; using Microsoft.AspNetCore.SignalR; using NBitcoin; using LightningPayment = BTCPayApp.CommonServer.LightningPayment; namespace BTCPayServer.App; public class BTCPayAppLightningConnectionStringHandler:ILightningConnectionStringHandler { private readonly IHubContext _hubContext; private readonly BTCPayAppState _appState; private readonly DefaultHubLifetimeManager _lifetimeManager; public BTCPayAppLightningConnectionStringHandler(IHubContext hubContext, BTCPayAppState appState) { _hubContext = hubContext; _appState = appState; } public ILightningClient Create(string connectionString, Network network, [UnscopedRef] out string error) { var kv = LightningConnectionStringHelper.ExtractValues(connectionString, out var type); if (type != "app") { error = null; return null; } if (!kv.TryGetValue("group", out var key)) { error = $"The key 'group' is mandatory for app connection strings"; return null; } if (!_appState.GroupToConnectionId.TryGetValue(key, out var connectionId)) { error = $"The group {key} is not connected"; return null; } error = null; return new BTCPayAppLightningClient(_hubContext, _appState, key, network ); } } public class BTCPayAppLightningClient:ILightningClient { private readonly IHubContext _hubContext; private readonly BTCPayAppState _appState; private readonly string _key; private readonly Network _network; public BTCPayAppLightningClient(IHubContext hubContext, BTCPayAppState appState, string key, Network network) { _hubContext = hubContext; _appState = appState; _key = key; _network = network; } public override string ToString() { return $"type=app;group={_key}"; } public IBTCPayAppHubClient HubClient => _appState.GroupToConnectionId.TryGetValue(_key, out var connId) ? _hubContext.Clients.Client(connId) : throw new InvalidOperationException("Connection not found"); public async Task GetInvoice(string invoiceId, CancellationToken cancellation = new CancellationToken()) { return await GetInvoice(uint256.Parse(invoiceId), cancellation); } public async Task GetInvoice(uint256 paymentHash, CancellationToken cancellation = new CancellationToken()) { var lp = await HubClient.GetLightningInvoice(paymentHash.ToString()); return ToLightningInvoice(lp, _network); } public async Task ListInvoices(CancellationToken cancellation = new CancellationToken()) { return await ListInvoices(new ListInvoicesParams(), cancellation); } public async Task ListInvoices(ListInvoicesParams request, CancellationToken cancellation = new CancellationToken()) { var invs = await HubClient.GetLightningInvoices(request); return invs.Select(i => ToLightningInvoice(i, _network)).ToArray(); } public async Task GetPayment(string paymentHash, CancellationToken cancellation = new CancellationToken()) { return ToLightningPayment(await HubClient.GetLightningPayment(paymentHash)); } private static Lightning.LightningPayment ToLightningPayment(LightningPayment lightningPayment) { return new Lightning.LightningPayment() { Id = lightningPayment.PaymentHash, Amount = LightMoney.MilliSatoshis(lightningPayment.Value), PaymentHash = lightningPayment.PaymentHash, Preimage = lightningPayment.Preimage, BOLT11 = lightningPayment.PaymentRequests.FirstOrDefault(), Status = lightningPayment.Status }; } public async Task ListPayments(CancellationToken cancellation = new CancellationToken()) { return await ListPayments(new ListPaymentsParams(), cancellation); } public async Task ListPayments(ListPaymentsParams request, CancellationToken cancellation = new CancellationToken()) { var invs = await HubClient.GetLightningPayments(request); return invs.Select(ToLightningPayment).ToArray(); } public async Task CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation = new CancellationToken()) { return await CreateInvoice(new CreateInvoiceParams(amount, description, expiry), cancellation); } public async Task CreateInvoice(CreateInvoiceParams createInvoiceRequest, CancellationToken cancellation = new CancellationToken()) { var lp = await HubClient.CreateInvoice(new CreateLightningInvoiceRequest(createInvoiceRequest.Amount, createInvoiceRequest.Description, createInvoiceRequest.Expiry) { DescriptionHashOnly = createInvoiceRequest.DescriptionHashOnly, PrivateRouteHints = createInvoiceRequest.PrivateRouteHints, }); return ToLightningInvoice(lp, _network); } private static LightningInvoice ToLightningInvoice(LightningPayment lightningPayment, Network _network) { var paymenRequest = BOLT11PaymentRequest.Parse(lightningPayment.PaymentRequests.First(), _network); return new LightningInvoice() { Id = lightningPayment.PaymentHash, Amount = LightMoney.MilliSatoshis(lightningPayment.Value), PaymentHash = lightningPayment.PaymentHash, Preimage = lightningPayment.Preimage, BOLT11 = lightningPayment.PaymentRequests.FirstOrDefault(), Status = lightningPayment.Status == LightningPaymentStatus.Complete? LightningInvoiceStatus.Paid: paymenRequest.ExpiryDate < DateTimeOffset.UtcNow? LightningInvoiceStatus.Expired: LightningInvoiceStatus.Unpaid }; } public async Task Listen(CancellationToken cancellation = new CancellationToken()) { return new Listener(_appState, _network, _key); } public class Listener:ILightningInvoiceListener { private readonly BTCPayAppState _btcPayAppState; private readonly Network _network; private readonly string _key; private readonly Channel _channel = Channel.CreateUnbounded(); private readonly CancellationTokenSource _cts; public Listener(BTCPayAppState btcPayAppState, Network network, string key) { _btcPayAppState = btcPayAppState; btcPayAppState.GroupRemoved += BtcPayAppStateOnGroupRemoved; _network = network; _key = key; _cts = new CancellationTokenSource(); _btcPayAppState.OnPaymentUpdate += BtcPayAppStateOnOnPaymentUpdate; } private void BtcPayAppStateOnGroupRemoved(object sender, string e) { if(e == _key) _channel.Writer.Complete(); } private void BtcPayAppStateOnOnPaymentUpdate(object sender, (string, LightningPayment) e) { if(e.Item1 != _key) return; _channel.Writer.TryWrite(e.Item2); } public void Dispose() { _cts?.Cancel(); _btcPayAppState.OnPaymentUpdate -= BtcPayAppStateOnOnPaymentUpdate; _btcPayAppState.GroupRemoved -= BtcPayAppStateOnGroupRemoved; _channel.Writer.TryComplete(); } public async Task WaitInvoice(CancellationToken cancellation) { return ToLightningInvoice(await _channel.Reader.ReadAsync( CancellationTokenSource.CreateLinkedTokenSource(cancellation, _cts.Token).Token), _network); } } public async Task GetInfo(CancellationToken cancellation = new CancellationToken()) { throw new NotSupportedException(); } public async Task GetBalance(CancellationToken cancellation = new CancellationToken()) { throw new NotImplementedException(); } public async Task Pay(PayInvoiceParams payParams, CancellationToken cancellation = new CancellationToken()) { return await Pay(null, payParams, cancellation); } public async Task Pay(string bolt11, PayInvoiceParams payParams, CancellationToken cancellation = new CancellationToken()) { return await HubClient.PayInvoice(bolt11, payParams.Amount?.MilliSatoshi); } public async Task Pay(string bolt11, CancellationToken cancellation = new CancellationToken()) { return await Pay(bolt11, new PayInvoiceParams(), cancellation); } public async Task OpenChannel(OpenChannelRequest openChannelRequest, CancellationToken cancellation = new CancellationToken()) { throw new NotImplementedException(); } public async Task GetDepositAddress(CancellationToken cancellation = new CancellationToken()) { throw new NotImplementedException(); } public async Task ConnectTo(NodeInfo nodeInfo, CancellationToken cancellation = new CancellationToken()) { throw new NotImplementedException(); } public async Task CancelInvoice(string invoiceId, CancellationToken cancellation = new CancellationToken()) { throw new NotImplementedException(); } public async Task ListChannels(CancellationToken cancellation = new CancellationToken()) { throw new NotImplementedException(); } }