diff --git a/BTCPayServerPlugins.sln b/BTCPayServerPlugins.sln index 2639a6d..9b1b943 100644 --- a/BTCPayServerPlugins.sln +++ b/BTCPayServerPlugins.sln @@ -54,6 +54,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Dynami EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Bringin", "Plugins\BTCPayServer.Plugins.Bringin\BTCPayServer.Plugins.Bringin.csproj", "{D4AFEC95-64D4-4FC4-9AE4-B82F4C6D6E29}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.LDK", "Plugins\BTCPayServer.Plugins.LDK\BTCPayServer.Plugins.LDK.csproj", "{661DBF95-0F60-49C0-829A-C5997B44AF60}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -262,6 +264,14 @@ Global {5934F898-00B1-4781-BD18-04DF8685BC76}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU {5934F898-00B1-4781-BD18-04DF8685BC76}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU {5934F898-00B1-4781-BD18-04DF8685BC76}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU + {661DBF95-0F60-49C0-829A-C5997B44AF60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {661DBF95-0F60-49C0-829A-C5997B44AF60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {661DBF95-0F60-49C0-829A-C5997B44AF60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {661DBF95-0F60-49C0-829A-C5997B44AF60}.Release|Any CPU.Build.0 = Release|Any CPU + {661DBF95-0F60-49C0-829A-C5997B44AF60}.Altcoins-Debug|Any CPU.ActiveCfg = Debug|Any CPU + {661DBF95-0F60-49C0-829A-C5997B44AF60}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU + {661DBF95-0F60-49C0-829A-C5997B44AF60}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU + {661DBF95-0F60-49C0-829A-C5997B44AF60}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962} diff --git a/Plugins/BTCPayServer.Plugins.Breez/BreezController.cs b/Plugins/BTCPayServer.Plugins.Breez/BreezController.cs index 8acfcf5..cfeb60e 100644 --- a/Plugins/BTCPayServer.Plugins.Breez/BreezController.cs +++ b/Plugins/BTCPayServer.Plugins.Breez/BreezController.cs @@ -5,10 +5,13 @@ using System.Threading.Tasks; using Breez.Sdk; using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; +using BTCPayServer.Lightning; using BTCPayServer.Models; using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using NBitcoin; +using NBitcoin.DataEncoders; using NBXplorer.DerivationStrategy; namespace BTCPayServer.Plugins.Breez; @@ -108,8 +111,8 @@ public class BreezController : Controller return View((object) storeId); } - [HttpGet("swapin/create")] - public async Task SwapInCreate(string storeId) + [HttpGet("send")] + public async Task Send(string storeId) { var client = _breezService.GetClient(storeId); if (client is null) @@ -117,9 +120,85 @@ public class BreezController : Controller return RedirectToAction(nameof(Configure), new {storeId}); } - client.Sdk.ReceiveOnchain(new ReceiveOnchainRequest()); - TempData[WellKnownTempData.SuccessMessage] = "Swapin created successfully"; - return RedirectToAction(nameof(SwapIn), new {storeId}); + return View((object) storeId); + } + [Route("receive")] + public async Task Receive(string storeId, ulong? amount) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + if (amount is not null) + { + var invoice = await client.CreateInvoice(LightMoney.FromUnit(amount.Value, LightMoneyUnit.Satoshi).MilliSatoshi, null, TimeSpan.Zero); + TempData["bolt11"] = invoice.BOLT11; + return RedirectToAction("Payments", "Breez", new {storeId }); + } + + + return View((object) storeId); + } + + [HttpPost("send")] + public async Task Send(string storeId, string address, ulong? amount) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + + var payParams = new PayInvoiceParams(); + string bolt11 = null; + if (HexEncoder.IsWellFormed(address)) + { + if (PubKey.TryCreatePubKey(ConvertHelper.FromHexString(address), out var pubKey)) + { + if (amount is null) + { + TempData[WellKnownTempData.ErrorMessage] = + $"Cannot do keysend payment without specifying an amount"; + return RedirectToAction(nameof(Send), new {storeId}); + } + + payParams.Amount = amount.Value * 1000; + payParams.Destination = pubKey; + } + else + { + TempData[WellKnownTempData.ErrorMessage] = $"invalid nodeid"; + return RedirectToAction(nameof(Send), new {storeId}); + } + } + else + { + bolt11 = address; + if (amount is not null) + { + payParams.Amount = amount.Value * 1000; + } + } + + var result = await client.Pay(bolt11, payParams); + + switch (result.Result) + { + case PayResult.Ok: + + TempData[WellKnownTempData.SuccessMessage] = $"Sending successful"; + break; + case PayResult.Unknown: + case PayResult.CouldNotFindRoute: + case PayResult.Error: + default: + + TempData[WellKnownTempData.ErrorMessage] = $"Sending did not indicate success"; + break; + } + + return RedirectToAction(nameof(Payments), new {storeId}); } @@ -207,7 +286,7 @@ public class BreezController : Controller return View(await _breezService.Get(storeId)); } - [HttpPost("")] + [HttpPost("configure")] public async Task Configure(string storeId, string command, BreezSettings settings) { if (command == "clear") @@ -244,9 +323,10 @@ public class BreezController : Controller { return RedirectToAction(nameof(Configure), new {storeId}); } + viewModel ??= new PaymentsViewModel(); - viewModel.Payments = client.Sdk.ListPayments(new ListPaymentsRequest(PaymentTypeFilter.ALL, null, null, null, + viewModel.Payments = client.Sdk.ListPayments(new ListPaymentsRequest(PaymentTypeFilter.ALL, null, null, true, (uint?) viewModel.Skip, (uint?) viewModel.Count)); return View(viewModel); diff --git a/Plugins/BTCPayServer.Plugins.Breez/BreezLightningClient.cs b/Plugins/BTCPayServer.Plugins.Breez/BreezLightningClient.cs index 09bb7d5..45195ca 100644 --- a/Plugins/BTCPayServer.Plugins.Breez/BreezLightningClient.cs +++ b/Plugins/BTCPayServer.Plugins.Breez/BreezLightningClient.cs @@ -10,38 +10,47 @@ using Network = Breez.Sdk.Network; namespace BTCPayServer.Plugins.Breez; - -public class BreezLightningClient: ILightningClient, IDisposable, EventListener +public class BreezLightningClient : ILightningClient, IDisposable, EventListener { + public override string ToString() + { + return $"type=breez;key={PaymentKey}"; + } + private readonly NBitcoin.Network _network; + public readonly string PaymentKey; public BreezLightningClient(string inviteCode, string apiKey, string workingDir, NBitcoin.Network network, - string mnemonic) + string mnemonic, string paymentKey) { _network = network; + PaymentKey = paymentKey; var nodeConfig = new NodeConfig.Greenlight( new GreenlightNodeConfig(null, inviteCode) ); var config = BreezSdkMethods.DefaultConfig( - network ==NBitcoin.Network.Main ? EnvironmentType.PRODUCTION: EnvironmentType.STAGING, - apiKey, - nodeConfig - ) with { - workingDir= workingDir, - network = network == NBitcoin.Network.Main ? Network.BITCOIN : network == NBitcoin.Network.TestNet ? Network.TESTNET: network == NBitcoin.Network.RegTest? Network.REGTEST: Network.SIGNET - }; + network == NBitcoin.Network.Main ? EnvironmentType.PRODUCTION : EnvironmentType.STAGING, + apiKey, + nodeConfig + ) with + { + workingDir = workingDir, + network = network == NBitcoin.Network.Main ? Network.BITCOIN : + network == NBitcoin.Network.TestNet ? Network.TESTNET : + network == NBitcoin.Network.RegTest ? Network.REGTEST : Network.SIGNET + }; var seed = BreezSdkMethods.MnemonicToSeed(mnemonic); - Sdk = BreezSdkMethods.Connect(config, seed, this); - + Sdk = BreezSdkMethods.Connect(config, seed, this); } - public BlockingBreezServices Sdk { get; } + public BlockingBreezServices Sdk { get; } - public event EventHandler EventReceived; - public void OnEvent(BreezEvent e) - { - EventReceived?.Invoke(this, e); - } + public event EventHandler EventReceived; + + public void OnEvent(BreezEvent e) + { + EventReceived?.Invoke(this, e); + } public Task GetInvoice(string invoiceId, CancellationToken cancellation = default) { @@ -54,7 +63,7 @@ public class BreezLightningClient: ILightningClient, IDisposable, EventListener { return null; } - + return new LightningPayment() { Amount = LightMoney.MilliSatoshis(payment.amountMsat), @@ -69,21 +78,22 @@ public class BreezLightningClient: ILightningClient, IDisposable, EventListener PaymentStatus.PENDING => LightningPaymentStatus.Pending, _ => throw new ArgumentOutOfRangeException() }, - CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(payment.paymentTime), + CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(payment.paymentTime), Fee = LightMoney.MilliSatoshis(payment.feeMsat), AmountSent = LightMoney.MilliSatoshis(payment.amountMsat) }; - - } + private LightningInvoice FromPayment(Payment p) { + if (p?.details is not PaymentDetails.Ln lnPaymentDetails) { return null; } + var bolt11 = BOLT11PaymentRequest.Parse(lnPaymentDetails.data.bolt11, _network); - + return new LightningInvoice() { Amount = LightMoney.MilliSatoshis(p.amountMsat), @@ -97,7 +107,7 @@ public class BreezLightningClient: ILightningClient, IDisposable, EventListener PaymentStatus.COMPLETE => LightningInvoiceStatus.Paid, _ => LightningInvoiceStatus.Unpaid }, - PaidAt = DateTimeOffset.FromUnixTimeMilliseconds(p.paymentTime), + PaidAt = DateTimeOffset.FromUnixTimeSeconds(p.paymentTime), ExpiresAt = bolt11.ExpiryDate }; } @@ -105,18 +115,28 @@ public class BreezLightningClient: ILightningClient, IDisposable, EventListener public async Task GetInvoice(uint256 paymentHash, CancellationToken cancellation = default) { var p = Sdk.PaymentByHash(paymentHash.ToString()!); + + if(p is null) + return new LightningInvoice() + { + Id = paymentHash.ToString(), + PaymentHash = paymentHash.ToString(), + Status = LightningInvoiceStatus.Expired, + }; + return FromPayment(p); } public async Task ListInvoices(CancellationToken cancellation = default) { - return await ListInvoices(null, cancellation); } - public async Task ListInvoices(ListInvoicesParams request, CancellationToken cancellation = default) + public async Task ListInvoices(ListInvoicesParams request, + CancellationToken cancellation = default) { - return Sdk.ListPayments(new ListPaymentsRequest(PaymentTypeFilter.RECEIVED, null, null, request?.PendingOnly is not true, (uint?) request?.OffsetIndex, null)) + return Sdk.ListPayments(new ListPaymentsRequest(PaymentTypeFilter.RECEIVED, null, null, + request?.PendingOnly is not true, (uint?) request?.OffsetIndex, null)) .Select(FromPayment).ToArray(); } @@ -130,26 +150,48 @@ public class BreezLightningClient: ILightningClient, IDisposable, EventListener return await ListPayments(null, cancellation); } - public async Task ListPayments(ListPaymentsParams request, CancellationToken cancellation = default) + public async Task ListPayments(ListPaymentsParams request, + CancellationToken cancellation = default) { - return Sdk.ListPayments(new ListPaymentsRequest(PaymentTypeFilter.RECEIVED, null, null, null, (uint?) request?.OffsetIndex, null)) + return Sdk.ListPayments(new ListPaymentsRequest(PaymentTypeFilter.RECEIVED, null, null, null, + (uint?) request?.OffsetIndex, null)) .Select(ToLightningPayment).ToArray(); } - public async Task CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation = default) + public async Task CreateInvoice(LightMoney amount, string description, TimeSpan expiry, + CancellationToken cancellation = default) { - - var expiryS =expiry == TimeSpan.Zero? (uint?) null: Math.Max(0, (uint)expiry.TotalSeconds); - var p = Sdk.ReceivePayment(new ReceivePaymentRequest((ulong)amount.MilliSatoshi, description, null, null, false,expiryS )); - return await GetInvoice(p.lnInvoice.paymentHash, cancellation); + var expiryS = expiry == TimeSpan.Zero ? (uint?) null : Math.Max(0, (uint) expiry.TotalSeconds); + var p = Sdk.ReceivePayment(new ReceivePaymentRequest((ulong) amount.MilliSatoshi, description, null, null, + false, expiryS)); + return FromPR(p); } - public async Task CreateInvoice(CreateInvoiceParams createInvoiceRequest, CancellationToken cancellation = default) + public LightningInvoice FromPR(ReceivePaymentResponse response) { - var expiryS =createInvoiceRequest.Expiry == TimeSpan.Zero? (uint?) null: Math.Max(0, (uint)createInvoiceRequest.Expiry.TotalSeconds); - var p = Sdk.ReceivePayment(new ReceivePaymentRequest((ulong)createInvoiceRequest.Amount.MilliSatoshi, (createInvoiceRequest.Description??createInvoiceRequest.DescriptionHash.ToString())!, null, null, createInvoiceRequest.DescriptionHashOnly,expiryS )); - return await GetInvoice(p.lnInvoice.paymentHash, cancellation); + return new LightningInvoice() + { + Amount = LightMoney.MilliSatoshis(response.lnInvoice.amountMsat ?? 0), + Id = response.lnInvoice.paymentHash, + Preimage = ConvertHelper.ToHexString(response.lnInvoice.paymentSecret.ToArray()), + PaymentHash = response.lnInvoice.paymentHash, + BOLT11 = response.lnInvoice.bolt11, + Status = LightningInvoiceStatus.Unpaid, + ExpiresAt = DateTimeOffset.FromUnixTimeSeconds((long) response.lnInvoice.expiry) + }; + } + + public async Task CreateInvoice(CreateInvoiceParams createInvoiceRequest, + CancellationToken cancellation = default) + { + var expiryS = createInvoiceRequest.Expiry == TimeSpan.Zero + ? (uint?) null + : Math.Max(0, (uint) createInvoiceRequest.Expiry.TotalSeconds); + var p = Sdk.ReceivePayment(new ReceivePaymentRequest((ulong) createInvoiceRequest.Amount.MilliSatoshi, + (createInvoiceRequest.Description ?? createInvoiceRequest.DescriptionHash.ToString())!, null, null, + createInvoiceRequest.DescriptionHashOnly, expiryS)); + return FromPR(p); } public async Task Listen(CancellationToken cancellation = default) @@ -159,14 +201,16 @@ public class BreezLightningClient: ILightningClient, IDisposable, EventListener public async Task GetInfo(CancellationToken cancellation = default) { - var ni = Sdk.NodeInfo(); return new LightningNodeInformation() { PeersCount = ni.connectedPeers.Count, Alias = $"greenlight {ni.id}", - NodeInfoList = {new NodeInfo(new PubKey(ni.id), "blockstrean.com", 69)},//we have to fake this as btcpay currently requires this to enable the payment method - BlockHeight = (int)ni.blockHeight + NodeInfoList = + { + new NodeInfo(new PubKey(ni.id), "blockstrean.com", 69) + }, //we have to fake this as btcpay currently requires this to enable the payment method + BlockHeight = (int) ni.blockHeight }; } @@ -194,9 +238,53 @@ public class BreezLightningClient: ILightningClient, IDisposable, EventListener return await Pay(null, payParams, cancellation); } - public async Task Pay(string bolt11, PayInvoiceParams payParams, CancellationToken cancellation = default) + public async Task Pay(string bolt11, PayInvoiceParams payParams, + CancellationToken cancellation = default) { - throw new NotImplementedException(); + SendPaymentResponse result; + try + { + if (bolt11 is null) + { + result = Sdk.SendSpontaneousPayment(new SendSpontaneousPaymentRequest(payParams.Destination.ToString(), + (ulong) payParams.Amount.MilliSatoshi)); + } + else + { + result = Sdk.SendPayment(new SendPaymentRequest(bolt11, (ulong?) payParams.Amount?.MilliSatoshi)); + } + + var details = result.payment.details as PaymentDetails.Ln; + return new PayResponse() + { + Result = result.payment.status switch + { + PaymentStatus.FAILED => PayResult.Error, + PaymentStatus.COMPLETE => PayResult.Ok, + PaymentStatus.PENDING => PayResult.Unknown, + _ => throw new ArgumentOutOfRangeException() + }, + Details = new PayDetails() + { + Status = result.payment.status switch + { + PaymentStatus.FAILED => LightningPaymentStatus.Failed, + PaymentStatus.COMPLETE => LightningPaymentStatus.Complete, + PaymentStatus.PENDING => LightningPaymentStatus.Pending, + _ => LightningPaymentStatus.Unknown + }, + Preimage = + details.data.paymentPreimage is null ? null : uint256.Parse(details.data.paymentPreimage), + PaymentHash = details.data.paymentHash is null ? null : uint256.Parse(details.data.paymentHash), + FeeAmount = result.payment.feeMsat, + TotalAmount = LightMoney.MilliSatoshis(result.payment.amountMsat + result.payment.feeMsat), + } + }; + } + catch (Exception e) + { + return new PayResponse(PayResult.Error, e.Message); + } } public async Task Pay(string bolt11, CancellationToken cancellation = default) @@ -204,7 +292,8 @@ public class BreezLightningClient: ILightningClient, IDisposable, EventListener return await Pay(bolt11, null, cancellation); } - public async Task OpenChannel(OpenChannelRequest openChannelRequest, CancellationToken cancellation = default) + public async Task OpenChannel(OpenChannelRequest openChannelRequest, + CancellationToken cancellation = default) { throw new NotImplementedException(); } @@ -221,7 +310,6 @@ public class BreezLightningClient: ILightningClient, IDisposable, EventListener public async Task CancelInvoice(string invoiceId, CancellationToken cancellation = default) { - throw new NotImplementedException(); } @@ -235,8 +323,8 @@ public class BreezLightningClient: ILightningClient, IDisposable, EventListener Sdk.Dispose(); Sdk.Dispose(); } - - public class BreezInvoiceListener: ILightningInvoiceListener + + public class BreezInvoiceListener : ILightningInvoiceListener { private readonly BreezLightningClient _breezLightningClient; private readonly CancellationToken _cancellationToken; @@ -245,7 +333,7 @@ public class BreezLightningClient: ILightningClient, IDisposable, EventListener { _breezLightningClient = breezLightningClient; _cancellationToken = cancellationToken; - + breezLightningClient.EventReceived += BreezLightningClientOnEventReceived; } @@ -266,17 +354,18 @@ public class BreezLightningClient: ILightningClient, IDisposable, EventListener public async Task WaitInvoice(CancellationToken cancellation) { - while(cancellation.IsCancellationRequested is not true) + while (cancellation.IsCancellationRequested is not true) { if (_invoices.TryDequeue(out var task)) { return await task.WithCancellation(cancellation); } + await Task.Delay(100, cancellation); } + cancellation.ThrowIfCancellationRequested(); return null; } } -} - +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Breez/BreezService.cs b/Plugins/BTCPayServer.Plugins.Breez/BreezService.cs index bd0877e..89e16c5 100644 --- a/Plugins/BTCPayServer.Plugins.Breez/BreezService.cs +++ b/Plugins/BTCPayServer.Plugins.Breez/BreezService.cs @@ -7,17 +7,18 @@ using System.Threading; using System.Threading.Tasks; using BTCPayServer.Configuration; using BTCPayServer.Data; +using BTCPayServer.Events; +using BTCPayServer.HostedServices; using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; using BTCPayServer.Services.Stores; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NBitcoin; namespace BTCPayServer.Plugins.Breez; -public class BreezService:IHostedService +public class BreezService:EventHostedServiceBase { private readonly StoreRepository _storeRepository; private readonly IOptions _dataDirectories; @@ -26,10 +27,12 @@ public class BreezService:IHostedService private Dictionary _settings; private Dictionary _clients = new(); - public BreezService(StoreRepository storeRepository, + public BreezService( + EventAggregator eventAggregator, + StoreRepository storeRepository, IOptions dataDirectories, BTCPayNetworkProvider btcPayNetworkProvider, - ILogger logger) + ILogger logger) : base(eventAggregator, logger) { _storeRepository = storeRepository; _dataDirectories = dataDirectories; @@ -37,6 +40,22 @@ public class BreezService:IHostedService _logger = logger; } + protected override void SubscribeToEvents() + { + Subscribe(); + base.SubscribeToEvents(); + } + + protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) + { + if (evt is StoreRemovedEvent storeRemovedEvent) + { + await Handle(storeRemovedEvent.StoreId, null); + _settings.Remove(storeRemovedEvent.StoreId); + } + await base.ProcessEvent(evt, cancellationToken); + } + private string GetWorkDir() { var dir = _dataDirectories.Value.DataDir; @@ -44,7 +63,7 @@ public class BreezService:IHostedService } TaskCompletionSource tcs = new(); - public async Task StartAsync(CancellationToken cancellationToken) + public override async Task StartAsync(CancellationToken cancellationToken) { _settings = (await _storeRepository.GetSettingsAsync("Breez")).Where(pair => pair.Value is not null).ToDictionary(pair => pair.Key, pair => pair.Value!); foreach (var keyValuePair in _settings) @@ -59,6 +78,7 @@ public class BreezService:IHostedService } } tcs.TrySetResult(); + await base.StartAsync(cancellationToken); } public async Task Get(string storeId) @@ -85,7 +105,7 @@ public class BreezService:IHostedService var network = Network.Main; // _btcPayNetworkProvider.BTC.NBitcoinNetwork; Directory.CreateDirectory(GetWorkDir()); var client = new BreezLightningClient(settings.InviteCode, settings.ApiKey, GetWorkDir(), - network, settings.Mnemonic); + network, settings.Mnemonic, settings.PaymentKey); if (storeId is not null) { _clients.AddOrReplace(storeId, client); diff --git a/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/Payments.cshtml b/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/Payments.cshtml index 8d0081a..3e3a5b8 100644 --- a/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/Payments.cshtml +++ b/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/Payments.cshtml @@ -6,6 +6,7 @@ var storeId = Context.GetCurrentStoreId(); ViewData.SetActivePage("Breez", "Payments", "Payments"); + TempData.TryGetValue("bolt11", out var bolt11); }
@@ -16,11 +17,28 @@
- - + Send + Receive
+ @if (bolt11 is string bolt11s) + { +
+
+ +
+
+
+ + +
+ + +
+
+ } + diff --git a/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/Receive.cshtml b/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/Receive.cshtml new file mode 100644 index 0000000..c459eb6 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/Receive.cshtml @@ -0,0 +1,44 @@ +@using BTCPayServer.Models.StoreViewModels +@using BTCPayServer.Plugins.Breez +@using BTCPayServer.Security +@inject BreezService BreezService + +@{ + ViewData.SetActivePage("Breez", "Receive", "Receive"); + var storeId = Model switch + { + string s => s, + StoreDashboardViewModel dashboardModel => dashboardModel.StoreId, + _ => Context.GetImplicitStoreId() + }; + var sdk = BreezService.GetClient(storeId)?.Sdk; + if (sdk is null) + return; + + var nodeState = sdk.NodeInfo(); + var max = nodeState.maxReceivableMsat / 1000; + +} + + +
+
+
+
+

+ @ViewData["Title"] +

+
+ + +
+
+
+
+ + +
+ +
+
+
\ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/Send.cshtml b/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/Send.cshtml new file mode 100644 index 0000000..f979ad8 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/Send.cshtml @@ -0,0 +1,48 @@ +@using BTCPayServer.Models.StoreViewModels +@using BTCPayServer.Plugins.Breez +@using BTCPayServer.Security +@inject BreezService BreezService + +@{ + ViewData.SetActivePage("Breez", "Send", "Send"); + var storeId = Model switch + { + string s => s, + StoreDashboardViewModel dashboardModel => dashboardModel.StoreId, + _ => Context.GetImplicitStoreId() + }; + var sdk = BreezService.GetClient(storeId)?.Sdk; + if (sdk is null) + return; + + var nodeState = sdk.NodeInfo(); + var max = nodeState.maxPayableMsat / 1000; + +} + + +
+
+
+
+

+ @ViewData["Title"] +

+
+ + +
+
+
+
+ + +
+
+ + +
+ +
+
+
\ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/SwapIn.cshtml b/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/SwapIn.cshtml index b3331eb..7a2bc92 100644 --- a/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/SwapIn.cshtml +++ b/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/SwapIn.cshtml @@ -23,7 +23,15 @@ if (sdk is null) return; - var inProgressSwap = sdk.InProgressSwap(); + SwapInfo inProgressSwap = null; + try + { + inProgressSwap = sdk.InProgressSwap(); + inProgressSwap ??= sdk.ReceiveOnchain(new ReceiveOnchainRequest()); + } + catch (Exception e) + { + } var refundables = sdk.ListRefundables(); var deriv = Context.GetStoreData().GetDerivationSchemeSettings(BtcPayNetworkProvider, "BTC"); var ni = sdk.NodeInfo(); @@ -46,12 +54,6 @@

@ViewData["Title"]

-
- @if (inProgressSwap is null) - { - Create - } -
@if (inProgressSwap is not null) @@ -65,19 +67,20 @@ - Please send an amount between @Money.Satoshis(inProgressSwap.minAllowedDeposit).ToDecimal(MoneyUnit.BTC) BTC and @Money.Satoshis(inProgressSwap.maxAllowedDeposit).ToDecimal(MoneyUnit.BTC) + Please send an amount between
@Money.Satoshis(inProgressSwap.minAllowedDeposit).ToDecimal(MoneyUnit.BTC) and @Money.Satoshis(inProgressSwap.maxAllowedDeposit).ToDecimal(MoneyUnit.BTC)BTC
@if (deriv is not null) { - Send using BTCPay Wallet + var wallet = new WalletId(storeId, "BTC"); + Send using BTCPay Wallet } @{ var onChainSats = ni.onchainBalanceMsat / 1000; if (inProgressSwap.minAllowedDeposit <= (long) onChainSats) { -
+
- +
} diff --git a/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/SwapOut.cshtml b/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/SwapOut.cshtml index f12cffa..f09dcb8 100644 --- a/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/SwapOut.cshtml +++ b/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/SwapOut.cshtml @@ -27,7 +27,6 @@ var sdk = BreezService.GetClient(storeId)?.Sdk; if (sdk is null) return; - var inProgressSwaps = sdk.InProgressReverseSwaps(); var deriv = Context.GetStoreData().GetDerivationSchemeSettings(BtcPayNetworkProvider, "BTC"); var f = sdk.RecommendedFees(); @@ -63,11 +62,11 @@
- +
- - + +
diff --git a/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/_Nav.cshtml b/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/_Nav.cshtml index dc0edb8..3e7b8e3 100644 --- a/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/_Nav.cshtml +++ b/Plugins/BTCPayServer.Plugins.Breez/Views/Breez/_Nav.cshtml @@ -24,8 +24,8 @@ { Info Payments - Swap Ins - Swap Outs + Swap In + Swap Out } Configuration
diff --git a/Plugins/BTCPayServer.Plugins.Breez/Views/Shared/Breez/BreezNodeInfo.cshtml b/Plugins/BTCPayServer.Plugins.Breez/Views/Shared/Breez/BreezNodeInfo.cshtml index 0845cf3..a401a43 100644 --- a/Plugins/BTCPayServer.Plugins.Breez/Views/Shared/Breez/BreezNodeInfo.cshtml +++ b/Plugins/BTCPayServer.Plugins.Breez/Views/Shared/Breez/BreezNodeInfo.cshtml @@ -91,6 +91,10 @@

@LightMoney.MilliSatoshis(nodeState.maxReceivableMsat)

BTC receivable
+
+

@LightMoney.MilliSatoshis(nodeState.inboundLiquidityMsats)

+ BTC inbound liquidity +

@LightMoney.MilliSatoshis(nodeState.maxPayableMsat)

BTC spendable diff --git a/Plugins/BTCPayServer.Plugins.Breez/Views/Shared/Breez/LNPaymentMethodSetupTab.cshtml b/Plugins/BTCPayServer.Plugins.Breez/Views/Shared/Breez/LNPaymentMethodSetupTab.cshtml index 2dae76d..295526f 100644 --- a/Plugins/BTCPayServer.Plugins.Breez/Views/Shared/Breez/LNPaymentMethodSetupTab.cshtml +++ b/Plugins/BTCPayServer.Plugins.Breez/Views/Shared/Breez/LNPaymentMethodSetupTab.cshtml @@ -1,10 +1,9 @@ - -@inject BreezService BreezService; +@inject BreezService BreezService; @using BTCPayServer.Plugins.Breez @model BTCPayServer.Models.StoreViewModels.LightningNodeViewModel @{ var storeId = Model.StoreId; - if(Model.CryptoCode != "BTC") + if (Model.CryptoCode != "BTC") { return; } @@ -14,16 +13,16 @@
@if (client is not null) { - } else { Breez needs to be configured beforehand. }
- - - +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Breez/Views/_ViewImports.cshtml b/Plugins/BTCPayServer.Plugins.Breez/Views/_ViewImports.cshtml index 52e6837..cf06ff9 100644 --- a/Plugins/BTCPayServer.Plugins.Breez/Views/_ViewImports.cshtml +++ b/Plugins/BTCPayServer.Plugins.Breez/Views/_ViewImports.cshtml @@ -1,2 +1,9 @@ -@addTagHelper *, BTCPayServer.Abstractions +@using BTCPayServer.Abstractions.Extensions +@inject BTCPayServer.Abstractions.Services.Safe Safe +@addTagHelper *, BTCPayServer.Abstractions +@addTagHelper *, BTCPayServer.TagHelpers +@addTagHelper *, BTCPayServer.Views.TagHelpers @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, BTCPayServer \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.LDK/BTCPayServer.Plugins.LDK.csproj b/Plugins/BTCPayServer.Plugins.LDK/BTCPayServer.Plugins.LDK.csproj new file mode 100644 index 0000000..0f65511 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LDK/BTCPayServer.Plugins.LDK.csproj @@ -0,0 +1,42 @@ + + + + net6.0 + 10 + + + + + LDK + The way lightning's meant to be + 1.0.0 + + + + true + false + true + + + + + + StaticWebAssetsEnabled=false + false + runtime;native;build;buildTransitive;contentFiles + + + + + + + + + + + + + + + + diff --git a/Plugins/BTCPayServer.Plugins.LDK/Program.cs b/Plugins/BTCPayServer.Plugins.LDK/Program.cs new file mode 100644 index 0000000..24c775c --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LDK/Program.cs @@ -0,0 +1,232 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Runtime.Internal.Util; +using BTCPayServer; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Configuration; +using BTCPayServer.Services; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NBitcoin; +using NBXplorer; +using org.ldk.enums; +using org.ldk.structs; +using enums_Network = org.ldk.enums.Network; +using ILogger = Microsoft.Extensions.Logging.ILogger; +using Logger = org.ldk.structs.Logger; +using Network = NBitcoin.Network; +using OutPoint = org.ldk.structs.OutPoint; +using Path = System.IO.Path; + +public class LDKService : IHostedService, PersistInterface, BroadcasterInterfaceInterface, FeeEstimatorInterface, EventHandlerInterface, LoggerInterface, FilterInterface +{ + private readonly ILogger _logger; + private readonly IFeeProviderFactory _feeProviderFactory; + private readonly IOptions _dataDirectories; + private readonly BTCPayNetwork _network; + private readonly ExplorerClient _explorerClient; + private readonly string _workDir; + private readonly enums_Network _ldkNetwork; + private readonly Logger _ldklogger; + private readonly FeeEstimator _ldkfeeEstimator; + private readonly BroadcasterInterface _ldkbroadcaster; + private readonly Persist _ldkpersist; + private readonly Filter _ldkfilter; + private readonly NetworkGraph _ldkNetworkGraph; + private readonly ChainMonitor _ldkChainMonitor; + + public LDKService(BTCPayNetworkProvider btcPayNetworkProvider, + ExplorerClientProvider explorerClientProvider, + ILogger logger, + IFeeProviderFactory feeProviderFactory, + IOptions dataDirectories) + { + _logger = logger; + _feeProviderFactory = feeProviderFactory; + _dataDirectories = dataDirectories; + + _network = btcPayNetworkProvider.GetNetwork("BTC"); + _explorerClient = explorerClientProvider.GetExplorerClient(_network); + _workDir = GetWorkDir(); + Directory.CreateDirectory(_workDir); + + _ldkNetwork = GetLdkNetwork(_network.NBitcoinNetwork); + _ldklogger = Logger.new_impl(this); + _ldkfeeEstimator = FeeEstimator.new_impl(this); + _ldkbroadcaster = BroadcasterInterface.new_impl(this); + _ldkpersist = Persist.new_impl(this); + _ldkfilter = Filter.new_impl(this); + + _ldkNetworkGraph = NetworkGraph.of(_ldkNetwork, _ldklogger); + _ldkChainMonitor = ChainMonitor.of( Option_FilterZ.Option_FilterZ_Some.some(_ldkfilter), _ldkbroadcaster, _ldklogger, _ldkfeeEstimator, _ldkpersist); + } + + + private static enums_Network GetLdkNetwork(Network network) + { + enums_Network? ldkNetwork = null; + if (network.ChainName == ChainName.Mainnet) + ldkNetwork = org.ldk.enums.Network.LDKNetwork_Bitcoin; + else if (network.ChainName == ChainName.Testnet) + ldkNetwork = org.ldk.enums.Network.LDKNetwork_Testnet; + else if (network.ChainName == ChainName.Regtest) + ldkNetwork = org.ldk.enums.Network.LDKNetwork_Regtest; + + return ldkNetwork ?? throw new NotSupportedException(); + } + + + private string GetWorkDir() + { + var dir = _dataDirectories.Value.DataDir; + return Path.Combine(dir, "Plugins", "LDK"); + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public int get_est_sat_per_1000_weight(ConfirmationTarget confirmation_target) + { + var feeProvider = _feeProviderFactory.CreateFeeProvider(_network); + var targetBlocks = confirmation_target switch + { + ConfirmationTarget.LDKConfirmationTarget_OnChainSweep => 30, // High priority (10-50 blocks) + ConfirmationTarget + .LDKConfirmationTarget_MaxAllowedNonAnchorChannelRemoteFee => + 20, // Moderate to high priority (small multiple of high-priority estimate) + ConfirmationTarget + .LDKConfirmationTarget_MinAllowedAnchorChannelRemoteFee => + 12, // Moderate priority (long-term mempool minimum or medium-priority) + ConfirmationTarget + .LDKConfirmationTarget_MinAllowedNonAnchorChannelRemoteFee => + 12, // Moderate priority (medium-priority feerate) + ConfirmationTarget.LDKConfirmationTarget_AnchorChannelFee => 6, // Lower priority (can be bumped later) + ConfirmationTarget + .LDKConfirmationTarget_NonAnchorChannelFee => 20, // Moderate to high priority (high-priority feerate) + ConfirmationTarget.LDKConfirmationTarget_ChannelCloseMinimum => 144, // Within a day or so (144-250 blocks) + _ => throw new ArgumentOutOfRangeException(nameof(confirmation_target), confirmation_target, null) + }; + return (int) Math.Max(253, feeProvider.GetFeeRateAsync(targetBlocks).GetAwaiter().GetResult().FeePerK.Satoshi); + } + + public void log(Record record) + { + var level = record.get_level() switch + { + Level.LDKLevel_Trace => LogLevel.Trace, + Level.LDKLevel_Debug => LogLevel.Debug, + Level.LDKLevel_Info => LogLevel.Information, + Level.LDKLevel_Warn => LogLevel.Warning, + Level.LDKLevel_Error => LogLevel.Error, + Level.LDKLevel_Gossip => LogLevel.Trace, + }; + _logger.Log(level, $"[{record.get_module_path()}] {record.get_args()}"); + } + + public void broadcast_transactions(byte[][] txs) + { + foreach (var tx in txs) + { + var loadedTx = Transaction.Load(tx, _explorerClient.Network.NBitcoinNetwork); + + _explorerClient.Broadcast(loadedTx); + } + } + + + public ChannelMonitorUpdateStatus persist_new_channel(OutPoint channel_id, ChannelMonitor data, + MonitorUpdateId update_id) + { + var name = Convert.ToHexString(channel_id.write()); + File.WriteAllBytes(Path.Combine(_workDir, name), data.write()); + return ChannelMonitorUpdateStatus.LDKChannelMonitorUpdateStatus_Completed; + } + + public ChannelMonitorUpdateStatus update_persisted_channel(OutPoint channel_id, ChannelMonitorUpdate update, + ChannelMonitor data, MonitorUpdateId update_id) + { + var name = Convert.ToHexString(channel_id.write()); + File.WriteAllBytes(Path.Combine(_workDir, name), data.write()); + return ChannelMonitorUpdateStatus.LDKChannelMonitorUpdateStatus_Completed; + } + + public void handle_event(Event _event) + { + switch (_event) + { + case Event.Event_BumpTransaction eventBumpTransaction: + switch (eventBumpTransaction.bump_transaction) + { + case BumpTransactionEvent.BumpTransactionEvent_ChannelClose bumpTransactionEventChannelClose: + break; + case BumpTransactionEvent.BumpTransactionEvent_HTLCResolution bumpTransactionEventHtlcResolution: + break; + default: + throw new ArgumentOutOfRangeException(); + } + + break; + case Event.Event_ChannelClosed eventChannelClosed: + break; + case Event.Event_ChannelPending eventChannelPending: + break; + case Event.Event_ChannelReady eventChannelReady: + break; + case Event.Event_DiscardFunding eventDiscardFunding: + break; + case Event.Event_FundingGenerationReady eventFundingGenerationReady: + break; + case Event.Event_HTLCHandlingFailed eventHtlcHandlingFailed: + break; + case Event.Event_HTLCIntercepted eventHtlcIntercepted: + break; + case Event.Event_InvoiceRequestFailed eventInvoiceRequestFailed: + break; + case Event.Event_OpenChannelRequest eventOpenChannelRequest: + break; + case Event.Event_PaymentClaimable eventPaymentClaimable: + break; + case Event.Event_PaymentClaimed eventPaymentClaimed: + break; + case Event.Event_PaymentFailed eventPaymentFailed: + break; + case Event.Event_PaymentForwarded eventPaymentForwarded: + break; + case Event.Event_PaymentPathFailed eventPaymentPathFailed: + break; + case Event.Event_PaymentPathSuccessful eventPaymentPathSuccessful: + break; + case Event.Event_PaymentSent eventPaymentSent: + break; + case Event.Event_PendingHTLCsForwardable eventPendingHtlCsForwardable: + break; + case Event.Event_ProbeFailed eventProbeFailed: + break; + case Event.Event_ProbeSuccessful eventProbeSuccessful: + break; + case Event.Event_SpendableOutputs eventSpendableOutputs: + break; + default: + throw new ArgumentOutOfRangeException(nameof(_event)); + } + } + + public void register_tx(byte[] txid, byte[] script_pubkey) + { + throw new NotImplementedException(); + } + + public void register_output(WatchedOutput output) + { + throw new NotImplementedException(); + } +} \ No newline at end of file