diff --git a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Configure.cshtml b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Configure.cshtml index 7f0523c..138f1fb 100644 --- a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Configure.cshtml +++ b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Configure.cshtml @@ -4,7 +4,7 @@ @model BTCPayServer.Plugins.BreezSpark.BreezSparkSettings? @inject BreezSparkService BreezService @{ - ViewData.SetActivePage("Breez", "Configure", "Configure"); + ViewData.SetActivePage("BreezSpark", "Configure", "Configure"); var storeId = Context.GetCurrentStoreId(); var active = (await BreezService.Get(storeId)) is not null; } diff --git a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Info.cshtml b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Info.cshtml index 8fbce12..32d8c25 100644 --- a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Info.cshtml +++ b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Info.cshtml @@ -15,7 +15,7 @@ @inject TransactionLinkProviders TransactionLinkProviders @inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary @{ - ViewData.SetActivePage("Breez", "Info", "Info"); + ViewData.SetActivePage("BreezSpark", "Info", "Info"); string storeId = Model switch { string s => s, @@ -29,7 +29,7 @@
-

Breez Lightning Node Information

+

BreezSpark Lightning Node Information

@try { @@ -41,7 +41,7 @@

- Breez Lightning Node is connected and operational. + BreezSpark Lightning Node is connected and operational. This is a simplified view for SDK v0.4.1 compatibility.

@@ -57,7 +57,7 @@
Bitcoin
Type
-
Breez SDK v0.4.1
+
Breez Spark SDK v0.4.1
@@ -80,12 +80,12 @@
@@ -98,7 +98,7 @@ {
Node Information Unavailable
-

Unable to fetch Breez node information: @ex.Message

+

Unable to fetch BreezSpark node information: @ex.Message

This may be due to SDK v0.4.1 compatibility changes.

} diff --git a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Logs.cshtml b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Logs.cshtml index 3c637c1..d5d5f6a 100644 --- a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Logs.cshtml +++ b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Logs.cshtml @@ -3,7 +3,7 @@ @{ var storeId = Context.GetCurrentStoreId(); - ViewData.SetActivePage("Breez", "Logs", "Logs"); + ViewData.SetActivePage("BreezSpark", "Logs", "Logs"); }
diff --git a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Payments.cshtml b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Payments.cshtml index c4a6d63..ab46003 100644 --- a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Payments.cshtml +++ b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Payments.cshtml @@ -9,7 +9,7 @@ @{ var storeId = Context.GetCurrentStoreId(); - ViewData.SetActivePage("Breez", "Payments", "Payments"); + ViewData.SetActivePage("BreezSpark", "Payments", "Payments"); TempData.TryGetValue("bolt11", out var bolt11); } @@ -43,7 +43,7 @@
} - +
\ No newline at end of file diff --git a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Receive.cshtml b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Receive.cshtml index ca19652..0ae7b21 100644 --- a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Receive.cshtml +++ b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Receive.cshtml @@ -5,7 +5,7 @@ @inject BreezSparkService BreezService @{ - ViewData.SetActivePage("Breez", "Receive", "Receive"); + ViewData.SetActivePage("BreezSpark", "Receive", "Receive"); var storeId = Model switch { string s => s, diff --git a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Send.cshtml b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Send.cshtml index 315a5d2..2f6a366 100644 --- a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Send.cshtml +++ b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Send.cshtml @@ -6,7 +6,7 @@ @inject BreezSparkService BreezService @{ - ViewData.SetActivePage("Breez", "Send", "Send"); + ViewData.SetActivePage("BreezSpark", "Send", "Send"); var storeId = Model switch { string s => s, diff --git a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapIn.cshtml b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapIn.cshtml index 8c8bbac..c1a1ee6 100644 --- a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapIn.cshtml +++ b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapIn.cshtml @@ -16,7 +16,7 @@ @inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary @inject BTCPayNetworkProvider BTCPayNetworkProvider @{ - ViewData.SetActivePage("Breez", "Swap In", "SwapIn"); + ViewData.SetActivePage("BreezSpark", "Swap In", "SwapIn"); var pmi = PaymentMethodId.Parse("BTC-OnChain"); string storeId = Model switch { diff --git a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapInRefund.cshtml b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapInRefund.cshtml index eafd0de..ee71627 100644 --- a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapInRefund.cshtml +++ b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapInRefund.cshtml @@ -11,7 +11,7 @@ @{ var storeId = Context.GetImplicitStoreId(); var address = Context.GetRouteValue("address").ToString(); - ViewData.SetActivePage("Breez", "Create Swapin Refund", "SwapIn"); + ViewData.SetActivePage("BreezSpark", "Create Swapin Refund", "SwapIn"); @* TODO: Fix for v2.2.1 - derivation settings check needed *@ var sdk = BreezService.GetClient(storeId)?.Sdk; diff --git a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapOut.cshtml b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapOut.cshtml index be48c79..3e1b1d7 100644 --- a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapOut.cshtml +++ b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapOut.cshtml @@ -10,7 +10,7 @@ @* @inject BTCPayNetworkProvider BTCPayNetworkProvider *@ @{ Layout = "_Layout"; - ViewData.SetActivePage("Breez", "Swap Out", "SwapOut"); + ViewData.SetActivePage("BreezSpark", "Swap Out", "SwapOut"); string storeId = null; if (Model is string s) { diff --git a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Sweep.cshtml b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Sweep.cshtml index dbed979..2e54424 100644 --- a/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Sweep.cshtml +++ b/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Sweep.cshtml @@ -9,7 +9,7 @@ @* @inject BTCPayNetworkProvider BTCPayNetworkProvider *@ @{ Layout = "_Layout"; - ViewData.SetActivePage("Breez", "Sweep", "Sweep"); + ViewData.SetActivePage("BreezSpark", "Sweep", "Sweep"); string storeId = null; if (Model is string s) { diff --git a/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezNodeInfo.cshtml b/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezNodeInfo.cshtml index e4e80ab..9ed0d5d 100644 --- a/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezNodeInfo.cshtml +++ b/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezNodeInfo.cshtml @@ -33,20 +33,20 @@ } } -
+
@if (Model is StoreDashboardViewModel) {
-

Breez Node

+

BreezSpark Node

Manage @@ -80,10 +80,10 @@ else {
-

Breez node information not available

+

BreezSpark node information not available

@if (string.IsNullOrEmpty(storeId)) { - Configure Breez + Configure BreezSpark }
} diff --git a/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezSparkNav.cshtml b/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezSparkNav.cshtml index dd59361..1694b3d 100644 --- a/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezSparkNav.cshtml +++ b/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezSparkNav.cshtml @@ -15,14 +15,14 @@ StoreDashboardViewModel dashboardModel => dashboardModel.StoreId, _ => Context.GetImplicitStoreId() }; - var active = @ViewData.IsActivePage("Breez"); + var active = @ViewData.IsActivePage("BreezSpark"); var client = string.IsNullOrEmpty(active) ? null : BreezService.GetClient(storeId); var sdk = client?.Sdk; } @if (!string.IsNullOrEmpty(storeId)) { @@ -42,18 +42,18 @@ { @if (client.Events.Any()) { } } diff --git a/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezSparkPaymentsTable.cshtml b/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezSparkPaymentsTable.cshtml new file mode 100644 index 0000000..9eb7f50 --- /dev/null +++ b/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezSparkPaymentsTable.cshtml @@ -0,0 +1,94 @@ +@using Breez.Sdk.Spark +@using BTCPayServer.Lightning +@using BTCPayServer.Models.StoreViewModels +@using BTCPayServer.Plugins.BreezSpark +@using BTCPayServer.Security +@model List +@{ + var data = Model ?? new List(); + var storeId = Context.GetImplicitStoreId(); + if (data is null) + { + if (string.IsNullOrEmpty(storeId)) + return; + } + + var isDashboard = false; +} +@if (isDashboard) +{ + +} +
+ @if (isDashboard) + { +
+

BreezSpark Payments

+ @if (data.Any()) + { + View All + } +
+ } + @if (!data.Any()) + { +

+ There are no recent payments. +

+ } + else + { +
+ + + + + + + + + + + + + + @foreach (var payment in data) + { + + + + + + + + + + + } + +
IdTimestampTypeAmountFeeStatusDescription
+ + @payment.Id + + + @DateTimeOffset.FromUnixTimeSeconds((long)payment.Timestamp).ToTimeAgo() ?? "Unknown" + + @(payment.PaymentType == PaymentType.Receive ? "receive" : "send") + + @payment.Amount.ToDecimal(LightMoneyUnit.BTC) BTC + + @payment.Fee.ToDecimal(LightMoneyUnit.BTC) BTC + + @payment.Status.ToString().ToLowerInvariant() + + @payment.Description +
+
+ } +
diff --git a/btcpayserver b/btcpayserver new file mode 160000 index 0000000..9886b67 --- /dev/null +++ b/btcpayserver @@ -0,0 +1 @@ +Subproject commit 9886b672e0987f92f0b582b55945e18f1b1423eb diff --git a/dist/BTCPayServer.Plugins.Breez/1.1.0.0/BTCPayServer.Plugins.Breez.btcpay b/dist/BTCPayServer.Plugins.Breez/1.1.0.0/BTCPayServer.Plugins.Breez.btcpay new file mode 100644 index 0000000..1f1d9e8 Binary files /dev/null and b/dist/BTCPayServer.Plugins.Breez/1.1.0.0/BTCPayServer.Plugins.Breez.btcpay differ diff --git a/dist/BTCPayServer.Plugins.Breez/1.1.0.0/BTCPayServer.Plugins.Breez.btcpay.json b/dist/BTCPayServer.Plugins.Breez/1.1.0.0/BTCPayServer.Plugins.Breez.btcpay.json new file mode 100644 index 0000000..7413f94 --- /dev/null +++ b/dist/BTCPayServer.Plugins.Breez/1.1.0.0/BTCPayServer.Plugins.Breez.btcpay.json @@ -0,0 +1 @@ +{"Identifier":"BTCPayServer.Plugins.Breez","Name":"Breez Spark Lightning Plugin","Version":"1.1.0.0","Description":"Nodeless Lightning payments powered by Breez Spark SDK","SystemPlugin":false,"Dependencies":[{"Identifier":"BTCPayServer","Condition":"\u003E=2.2.0"}]} \ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.Breez/1.1.0.0/SHA256SUMS b/dist/BTCPayServer.Plugins.Breez/1.1.0.0/SHA256SUMS new file mode 100644 index 0000000..d949820 --- /dev/null +++ b/dist/BTCPayServer.Plugins.Breez/1.1.0.0/SHA256SUMS @@ -0,0 +1,2 @@ +30a4ce17e09c22d168db00ba618f9c76d015a248fa88793f5d333768fe346d95 BTCPayServer.Plugins.Breez.btcpay.json +a8062f16f45ba6d5e9de9cfdfdb34eb7e542d4f4d908aa77f7cdc596035c13ed BTCPayServer.Plugins.Breez.btcpay diff --git a/dist/BTCPayServer.Plugins.Breez/1.1.0.0/SHA256SUMS.asc b/dist/BTCPayServer.Plugins.Breez/1.1.0.0/SHA256SUMS.asc new file mode 100644 index 0000000..e69de29 diff --git a/dist/BTCPayServer.Plugins.BreezSpark.json b/dist/BTCPayServer.Plugins.BreezSpark.json new file mode 100644 index 0000000..e79ffce --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark.json @@ -0,0 +1,15 @@ +{ + "Identifier": "BTCPayServer.Plugins.BreezSpark", + "Name": "BreezSpark Lightning Plugin", + "Version": "1.1.0", + "Description": "Nodeless Lightning payments powered by Breez Spark SDK", + "Author": "Aljaz Ceru", + "AuthorLink": "https://github.com/aljazceru", + "SystemPlugin": false, + "Dependencies": [ + { + "Identifier": "BTCPayServer", + "Condition": ">=2.2.0" + } + ] +} \ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark.v2.btcpay b/dist/BTCPayServer.Plugins.BreezSpark.v2.btcpay new file mode 100644 index 0000000..6e90734 Binary files /dev/null and b/dist/BTCPayServer.Plugins.BreezSpark.v2.btcpay differ diff --git a/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.csproj b/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.csproj new file mode 100644 index 0000000..1e756e6 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.csproj @@ -0,0 +1,47 @@ + + + + net8.0 + latest + enable + true + $(NoWarn);CS1591 + + + + + BreezSpark Lightning Plugin + Nodeless Lightning payments powered by Breez Spark SDK + 1.1.0 + Aljaz Ceru + Aljaz Ceru + BTCPayServer.Plugins.BreezSpark + BTCPayServer.Plugins.BreezSpark +true + + + + true + false + true + + + + + + StaticWebAssetsEnabled=false + false + runtime;native;build;buildTransitive;contentFiles + + + + + + + + + + + + + diff --git a/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.deps.json b/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.deps.json new file mode 100644 index 0000000..3b924af --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.deps.json @@ -0,0 +1,73 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v8.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v8.0": { + "BTCPayServer.Plugins.BreezSpark/1.1.0": { + "dependencies": { + "Breez.Sdk.Spark": "0.4.1" + }, + "runtime": { + "BTCPayServer.Plugins.BreezSpark.dll": {} + } + }, + "Breez.Sdk.Spark/0.4.1": { + "runtime": { + "lib/net8.0/Breez.Sdk.Spark.dll": { + "assemblyVersion": "0.4.1.0", + "fileVersion": "0.4.1.0" + } + }, + "runtimeTargets": { + "runtimes/linux-arm64/native/libbreez_sdk_spark_bindings.so": { + "rid": "linux-arm64", + "assetType": "native", + "fileVersion": "0.0.0.0" + }, + "runtimes/linux-x64/native/libbreez_sdk_spark_bindings.so": { + "rid": "linux-x64", + "assetType": "native", + "fileVersion": "0.0.0.0" + }, + "runtimes/osx-arm64/native/libbreez_sdk_spark_bindings.dylib": { + "rid": "osx-arm64", + "assetType": "native", + "fileVersion": "0.0.0.0" + }, + "runtimes/osx-x64/native/libbreez_sdk_spark_bindings.dylib": { + "rid": "osx-x64", + "assetType": "native", + "fileVersion": "0.0.0.0" + }, + "runtimes/win-x64/native/breez_sdk_spark_bindings.dll": { + "rid": "win-x64", + "assetType": "native", + "fileVersion": "0.0.0.0" + }, + "runtimes/win-x86/native/breez_sdk_spark_bindings.dll": { + "rid": "win-x86", + "assetType": "native", + "fileVersion": "0.0.0.0" + } + } + } + } + }, + "libraries": { + "BTCPayServer.Plugins.BreezSpark/1.1.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Breez.Sdk.Spark/0.4.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-/5iC7V3PK0q5og4h5qnSf5xepfvcXSEeV5WJDIAlOGd7WUtMZdNQ0n6yDcgd1Rv5qcxPQsDGQN3IVQZbP0UE1w==", + "path": "breez.sdk.spark/0.4.1", + "hashPath": "breez.sdk.spark.0.4.1.nupkg.sha512" + } + } +} \ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.dll b/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.dll new file mode 100644 index 0000000..79beade Binary files /dev/null and b/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.dll differ diff --git a/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.pdb b/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.pdb new file mode 100644 index 0000000..89f1b9b Binary files /dev/null and b/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.pdb differ diff --git a/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.staticwebassets.endpoints.json b/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.staticwebassets.endpoints.json new file mode 100644 index 0000000..21da96b --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.staticwebassets.endpoints.json @@ -0,0 +1 @@ +{"Version":1,"ManifestType":"Publish","Endpoints":[]} \ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.xml b/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.xml new file mode 100644 index 0000000..25320e3 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.xml @@ -0,0 +1,8 @@ + + + + BTCPayServer.Plugins.BreezSpark + + + + diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Breez.Sdk.Spark.dll b/dist/BTCPayServer.Plugins.BreezSpark/Breez.Sdk.Spark.dll new file mode 100755 index 0000000..7db16f5 Binary files /dev/null and b/dist/BTCPayServer.Plugins.BreezSpark/Breez.Sdk.Spark.dll differ diff --git a/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkController.cs b/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkController.cs new file mode 100644 index 0000000..56dfbc6 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkController.cs @@ -0,0 +1,537 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Text.Json; +using System.Threading.Tasks; +using Breez.Sdk.Spark; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Client; +using BTCPayServer.Data; +using BTCPayServer.Lightning; +using BTCPayServer.Models; +using BTCPayServer.Payments; +using BTCPayServer.Payments.Lightning; +using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Stores; +using BTCPayServer.Services.Wallets; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NBitcoin; +using NBitcoin.DataEncoders; + +namespace BTCPayServer.Plugins.BreezSpark; + +[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] +[Route("plugins/{storeId}/BreezSpark")] +public class BreezSparkController : Controller +{ + private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary; + private readonly BTCPayNetworkProvider _btcPayNetworkProvider; + private readonly BreezSparkService _breezService; + private readonly BTCPayWalletProvider _btcWalletProvider; + private readonly StoreRepository _storeRepository; + + public BreezSparkController( + PaymentMethodHandlerDictionary paymentMethodHandlerDictionary, + BTCPayNetworkProvider btcPayNetworkProvider, + BreezSparkService breezService, + BTCPayWalletProvider btcWalletProvider, StoreRepository storeRepository) + { + _paymentMethodHandlerDictionary = paymentMethodHandlerDictionary; + _btcPayNetworkProvider = btcPayNetworkProvider; + _breezService = breezService; + _btcWalletProvider = btcWalletProvider; + _storeRepository = storeRepository; + } + + + [HttpGet("")] + public async Task Index(string storeId) + { + var client = _breezService.GetClient(storeId); + return RedirectToAction(client is null ? nameof(Configure) : nameof(Info), new {storeId}); + } + + [HttpGet("swapin")] + [Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task SwapIn(string storeId) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + + return View((object) storeId); + } + + [HttpGet("info")] + [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task Info(string storeId) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + + return View((object) storeId); + } + [HttpGet("logs")] + [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task Logs(string storeId) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + + return View( client.Events); + } + + [HttpPost("sweep")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task Sweep(string storeId) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + + try + { + // In Spark SDK v0.4.1, check for any unclaimed deposits + var request = new ListUnclaimedDepositsRequest(); + var response = await client.Sdk.ListUnclaimedDeposits(request); + + if (response.deposits.Any()) + { + TempData[WellKnownTempData.SuccessMessage] = $"Found {response.deposits.Count} unclaimed deposits"; + } + else + { + TempData[WellKnownTempData.SuccessMessage] = "No pending deposits to claim"; + } + } + catch (Exception e) + { + TempData[WellKnownTempData.ErrorMessage] = $"error claiming deposits: {e.Message}"; + } + + return View((object) storeId); + } + + [HttpGet("send")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task Send(string storeId) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + + return View((object) storeId); + } + [Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [HttpGet("receive")] + [Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task Receive(string storeId) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + + return View((object) storeId); + } + + [HttpPost("receive")] + [Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task Receive(string storeId, long? amount, string description) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + + try + { + description ??= "BTCPay Server Invoice"; + + var paymentMethod = new ReceivePaymentMethod.Bolt11Invoice( + description: description, + amountSats: amount != null ? (ulong)amount.Value : null + ); + + var request = new ReceivePaymentRequest(paymentMethod: paymentMethod); + var response = await client.Sdk.ReceivePayment(request: request); + + TempData["bolt11"] = response.paymentRequest; + TempData[WellKnownTempData.SuccessMessage] = "Invoice created successfully!"; + + return RedirectToAction(nameof(Payments), new {storeId}); + } + catch (Exception ex) + { + TempData[WellKnownTempData.ErrorMessage] = $"Error creating invoice: {ex.Message}"; + return View((object) storeId); + } + } + + [HttpPost("prepare-send")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task PrepareSend(string storeId, string address, long? amount) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + + try + { + if (string.IsNullOrWhiteSpace(address)) + { + TempData[WellKnownTempData.ErrorMessage] = "Payment destination is required"; + return RedirectToAction(nameof(Send), new {storeId}); + } + + BigInteger? amountSats = null; + if (amount > 0) + { + amountSats = new BigInteger(amount.Value); + } + + var prepareRequest = new PrepareSendPaymentRequest( + paymentRequest: address, + amount: amountSats + ); + + var prepareResponse = await client.Sdk.PrepareSendPayment(prepareRequest); + + if (prepareResponse.paymentMethod is SendPaymentMethod.Bolt11Invoice bolt11Method) + { + var totalFee = bolt11Method.lightningFeeSats + (bolt11Method.sparkTransferFeeSats ?? 0); + var viewModel = new + { + Destination = address, + Amount = amountSats ?? 0, + Fee = totalFee, + PrepareResponseJson = JsonSerializer.Serialize(prepareResponse) + }; + ViewData["PaymentDetails"] = viewModel; + } + else if (prepareResponse.paymentMethod is SendPaymentMethod.BitcoinAddress bitcoinMethod) + { + var fees = bitcoinMethod.feeQuote; + var mediumFee = fees.speedMedium.userFeeSat + fees.speedMedium.l1BroadcastFeeSat; + var viewModel = new + { + Destination = address, + Amount = amountSats ?? 0, + Fee = mediumFee, + PrepareResponseJson = JsonSerializer.Serialize(prepareResponse) + }; + ViewData["PaymentDetails"] = viewModel; + } + else if (prepareResponse.paymentMethod is SendPaymentMethod.SparkAddress sparkMethod) + { + var viewModel = new + { + Destination = address, + Amount = amountSats ?? 0, + Fee = sparkMethod.fee, + PrepareResponseJson = JsonSerializer.Serialize(prepareResponse) + }; + ViewData["PaymentDetails"] = viewModel; + } + } + catch (Exception ex) + { + TempData[WellKnownTempData.ErrorMessage] = $"Error preparing payment: {ex.Message}"; + } + + return View(nameof(Send), storeId); + } + + [HttpPost("confirm-send")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task ConfirmSend(string storeId, string paymentRequest, long amount, string prepareResponseJson) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + + try + { + var prepareResponse = JsonSerializer.Deserialize(prepareResponseJson); + if (prepareResponse == null) + { + throw new InvalidOperationException("Invalid payment preparation data"); + } + + SendPaymentOptions? options = prepareResponse.paymentMethod switch + { + SendPaymentMethod.Bolt11Invoice => new SendPaymentOptions.Bolt11Invoice( + preferSpark: false, + completionTimeoutSecs: 60 + ), + SendPaymentMethod.BitcoinAddress => new SendPaymentOptions.BitcoinAddress( + confirmationSpeed: OnchainConfirmationSpeed.Medium + ), + SendPaymentMethod.SparkAddress => null, + _ => throw new NotSupportedException("Unsupported payment method") + }; + + var sendRequest = new SendPaymentRequest( + prepareResponse: prepareResponse, + options: options + ); + + var sendResponse = await client.Sdk.SendPayment(sendRequest); + + TempData[WellKnownTempData.SuccessMessage] = "Payment sent successfully!"; + return RedirectToAction(nameof(Payments), new {storeId}); + } + catch (Exception ex) + { + TempData[WellKnownTempData.ErrorMessage] = $"Error sending payment: {ex.Message}"; + return RedirectToAction(nameof(Send), new {storeId}); + } + } + + + [HttpGet("swapout")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task SwapOut(string storeId) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + + return View((object) storeId); + } + + [HttpPost("swapout")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task SwapOut(string storeId, string address, ulong amount, uint satPerByte, + string feesHash) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + + try + { + // Use current SDK pattern for onchain payments + var prepareRequest = new PrepareSendPaymentRequest( + paymentRequest: address, + amount: new BigInteger(amount) + ); + + var prepareResponse = await client.Sdk.PrepareSendPayment(prepareRequest); + + if (prepareResponse.paymentMethod is SendPaymentMethod.BitcoinAddress bitcoinMethod) + { + var options = new SendPaymentOptions.BitcoinAddress( + confirmationSpeed: OnchainConfirmationSpeed.Medium + ); + + var sendRequest = new SendPaymentRequest( + prepareResponse: prepareResponse, + options: options + ); + + var sendResponse = await client.Sdk.SendPayment(sendRequest); + + TempData[WellKnownTempData.SuccessMessage] = "Onchain payment initiated successfully!"; + } + else + { + TempData[WellKnownTempData.ErrorMessage] = "Invalid payment method for onchain swap"; + } + } + catch (Exception ex) + { + TempData[WellKnownTempData.ErrorMessage] = $"Error processing swap-out: {ex.Message}"; + } + + return RedirectToAction(nameof(SwapOut), new {storeId}); + } + + [HttpGet("swapin/{address}/refund")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task SwapInRefund(string storeId, string address) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + + return View((object) storeId); + } + + [HttpPost("swapin/{address}/refund")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task SwapInRefund(string storeId, string txid, uint vout, string refundAddress, uint? satPerByte = null) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + + try + { + // Parse the txid:vout format from depositId if needed + var fee = new Fee.Rate((ulong)(satPerByte ?? 5m)); + var request = new RefundDepositRequest( + txid: txid, + vout: vout, + destinationAddress: refundAddress, + fee: fee + ); + + var resp = await client.Sdk.RefundDeposit(request); + TempData[WellKnownTempData.SuccessMessage] = $"Refund successful: {resp.txId}"; + } + catch (Exception e) + { + TempData[WellKnownTempData.ErrorMessage] = $"Couldnt refund: {e.Message}"; + } + + return RedirectToAction(nameof(SwapIn), new {storeId}); + } + + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [HttpGet("configure")] + public async Task Configure(string storeId) + { + return View(await _breezService.Get(storeId)); + } + [HttpPost("configure")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task Configure(string storeId, string command, BreezSparkSettings settings) + { + var store = HttpContext.GetStoreData(); + if (store == null) + { + return NotFound(); + } + var pmi = new PaymentMethodId("BTC-LN"); + // In v2.2.1, payment methods are handled differently + // TODO: Implement proper v2.2.1 payment method handling + object? existing = null; + + if (command == "clear") + { + await _breezService.Set(storeId, null); + TempData[WellKnownTempData.SuccessMessage] = "Settings cleared successfully"; + var client = _breezService.GetClient(storeId); + // In v2.2.1, payment methods are handled differently + // TODO: Implement proper v2.2.1 payment method handling + return RedirectToAction(nameof(Configure), new {storeId}); + } + + if (command == "save") + { + try + { + if (string.IsNullOrEmpty(settings.Mnemonic)) + { + ModelState.AddModelError(nameof(settings.Mnemonic), "Mnemonic is required"); + return View(settings); + } + + try + { + new Mnemonic(settings.Mnemonic); + } + catch (Exception e) + { + ModelState.AddModelError(nameof(settings.Mnemonic), "Invalid mnemonic"); + return View(settings); + } + + await _breezService.Set(storeId, settings); + } + catch (Exception e) + { + TempData[WellKnownTempData.ErrorMessage] = $"Couldnt use provided settings: {e.Message}"; + return View(settings); + } + + // In v2.2.1, payment methods are handled differently + // TODO: Implement proper v2.2.1 payment method handling + // This will require a complete rewrite of the payment method system + + TempData[WellKnownTempData.SuccessMessage] = "Settings saved successfully"; + return RedirectToAction(nameof(Info), new {storeId}); + } + + return NotFound(); + } + + [Route("payments")] + [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task Payments(string storeId, PaymentsViewModel viewModel) + { + var client = _breezService.GetClient(storeId); + if (client is null) + { + return RedirectToAction(nameof(Configure), new {storeId}); + } + + viewModel ??= new PaymentsViewModel(); + var req = new ListPaymentsRequest( + typeFilter: null, + statusFilter: null, + assetFilter: new AssetFilter.Bitcoin(), + fromTimestamp: null, + toTimestamp: null, + offset: viewModel.Skip != null ? (uint?)viewModel.Skip : null, + limit: viewModel.Count != null ? (uint?)viewModel.Count : null, + sortAscending: false + ); + var response = await client.Sdk.ListPayments(req); + viewModel.Payments = response.payments.Select(client.NormalizePayment).ToList(); + + return View(viewModel); + } +} + +public class PaymentsViewModel : BasePagingViewModel +{ + public List Payments { get; set; } = new(); + public override int CurrentPageCount => Payments.Count; +} + +// Helper class for swap information display in views +public class SwapInfo +{ + public string? bitcoinAddress { get; set; } + public ulong minAllowedDeposit { get; set; } + public ulong maxAllowedDeposit { get; set; } + public string? status { get; set; } +} + +// Helper class for swap limits display in views +public class SwapLimits +{ + public ulong min { get; set; } + public ulong max { get; set; } +} diff --git a/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkLightningClient.cs b/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkLightningClient.cs new file mode 100644 index 0000000..446a28a --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkLightningClient.cs @@ -0,0 +1,952 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Breez.Sdk.Spark; +using BTCPayServer.Lightning; +using NBitcoin; +using Network = Breez.Sdk.Spark.Network; + +namespace BTCPayServer.Plugins.BreezSpark; + +public class EventLogEntry +{ + public DateTimeOffset timestamp { get; set; } + public string log { get; set; } = string.Empty; +} + +public class BreezSparkLightningClient : ILightningClient, IDisposable +{ + public override string ToString() + { + return $"type=breezspark;key={PaymentKey}"; + } + + private readonly NBitcoin.Network _network; + public readonly string PaymentKey; + + public ConcurrentQueue Events { get; set; } = new ConcurrentQueue(); + private readonly ConcurrentQueue _paymentNotifications = new(); + private readonly ConcurrentDictionary _seenCompletedPayments = new(); + private readonly ConcurrentDictionary _seenPaymentHashes = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _invoicesByHash = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _invoicesByBolt11 = new(StringComparer.OrdinalIgnoreCase); + + private void DebugLog(string message) + { + // Debug logging disabled for release build + } + + private void DebugLogObject(string label, object obj) + { + // Debug logging disabled for release build + } + + private BreezSdk _sdk; + + public static async Task Create(string apiKey, string workingDir, NBitcoin.Network network, + Mnemonic mnemonic, string paymentKey) + { + apiKey ??= "99010c6f84541bf582899db6728f6098ba98ca95ea569f4c63f2c2c9205ace57"; + + var config = BreezSdkSparkMethods.DefaultConfig( + network == NBitcoin.Network.Main ? Network.Mainnet : + network == NBitcoin.Network.RegTest ? Network.Regtest : Network.Mainnet + ) with + { + apiKey = apiKey + }; + + var seed = new Seed.Mnemonic(mnemonic: mnemonic.ToString(), passphrase: null); + var sdk = await BreezSdkSparkMethods.Connect(new ConnectRequest(config, seed, workingDir)); + + return new BreezSparkLightningClient(sdk, network, paymentKey); + } + + private BreezSparkLightningClient(BreezSdk sdk, NBitcoin.Network network, string paymentKey) + { + _sdk = sdk; + _network = network; + PaymentKey = paymentKey; + + // Start monitoring payment events + _ = Task.Run(MonitorPaymentEvents); + } + + public BreezSdk Sdk => _sdk; + + public async Task GetInvoice(string invoiceId, CancellationToken cancellation = default) + { + var invoice = await GetInvoiceInternal(invoiceId, cancellation); + if (invoice is not null) + { + return invoice; + } + + return new LightningInvoice() + { + Id = invoiceId, + PaymentHash = invoiceId, + Status = LightningInvoiceStatus.Unpaid + }; + } + + public async Task GetInvoice(uint256 paymentHash, CancellationToken cancellation = default) + { + return await GetInvoice(paymentHash.ToString(), cancellation); + } + + public async Task ListInvoices(CancellationToken cancellation = default) + { + return await ListInvoices((ListInvoicesParams?)null, cancellation); + } + + public async Task ListInvoices(ListInvoicesParams request, + CancellationToken cancellation = default) + { + var req = new ListPaymentsRequest( + typeFilter: new List { PaymentType.Receive }, + statusFilter: request?.PendingOnly == true ? new List { PaymentStatus.Pending } : null, + assetFilter: new AssetFilter.Bitcoin(), + fromTimestamp: null, + toTimestamp: null, + offset: request?.OffsetIndex != null ? (uint?)request.OffsetIndex : null, + limit: null, + sortAscending: false + ); + + var response = await _sdk.ListPayments(req); + return response.payments.Select(FromPayment).Where(p => p != null).ToArray(); + } + + public async Task GetPayment(string paymentHash, CancellationToken cancellation = default) + { + var payment = await FindPayment(paymentHash, cancellation); + return payment is not null ? ToLightningPayment(payment) : null; + } + + public async Task ListPayments(CancellationToken cancellation = default) + { + return await ListPayments((ListPaymentsParams?)null, cancellation); + } + + public async Task ListPayments(ListPaymentsParams request, + CancellationToken cancellation = default) + { + var req = new ListPaymentsRequest( + typeFilter: new List { PaymentType.Send }, + statusFilter: null, + assetFilter: new AssetFilter.Bitcoin(), + fromTimestamp: null, + toTimestamp: null, + offset: request?.OffsetIndex != null ? (uint?)request.OffsetIndex : null, + limit: null, + sortAscending: false + ); + + var response = await _sdk.ListPayments(req); + return response.payments.Select(ToLightningPayment).Where(p => p != null).ToArray(); + } + + public async Task CreateInvoice(LightMoney amount, string description, TimeSpan expiry, + CancellationToken cancellation = default) + { + var descriptionToUse = description ?? "Invoice"; + var amountSats = (ulong)amount.ToUnit(LightMoneyUnit.Satoshi); + var paymentMethod = new ReceivePaymentMethod.Bolt11Invoice(descriptionToUse, amountSats); + var response = await _sdk.ReceivePayment(new ReceivePaymentRequest(paymentMethod)); + DebugLogObject("ReceivePaymentResponse(CreateInvoice)", response); + return FromReceivePaymentResponse(response, amount); + } + + public async Task CreateInvoice(CreateInvoiceParams createInvoiceRequest, + CancellationToken cancellation = default) + { + var description = createInvoiceRequest.Description ?? createInvoiceRequest.DescriptionHash?.ToString() ?? "Invoice"; + var amountSats = (ulong)createInvoiceRequest.Amount.ToUnit(LightMoneyUnit.Satoshi); + var paymentMethod = new ReceivePaymentMethod.Bolt11Invoice(description, amountSats); + var response = await _sdk.ReceivePayment(new ReceivePaymentRequest(paymentMethod)); + DebugLogObject("ReceivePaymentResponse(CreateInvoiceParams)", response); + return FromReceivePaymentResponse(response, createInvoiceRequest.Amount); + } + + public async Task Listen(CancellationToken cancellation = default) + { + return new BreezSparkInvoiceListener(this, cancellation); + } + + public async Task GetInfo(CancellationToken cancellation = default) + { + try + { + var response = await _sdk.GetInfo(new GetInfoRequest(ensureSynced: false)); + + return new LightningNodeInformation() + { + Alias = "BreezSpark (nodeless)", + BlockHeight = 0, // Spark SDK doesn't expose block height + Version = "0.4.1" // SDK version hardcoded since property not found + }; + } + catch + { + return new LightningNodeInformation() + { + Alias = "BreezSpark (nodeless)", + BlockHeight = 0 + }; + } + } + + public async Task GetBalance(CancellationToken cancellation = default) + { + try + { + var response = await _sdk.GetInfo(new GetInfoRequest(ensureSynced: false)); + + return new LightningNodeBalance() + { + OnchainBalance = new OnchainBalance() + { + Confirmed = Money.Satoshis((long)response.balanceSats) + }, + OffchainBalance = new OffchainBalance() + { + Local = LightMoney.Satoshis((long)response.balanceSats), + Remote = LightMoney.Zero + } + }; + } + catch + { + return new LightningNodeBalance() + { + OnchainBalance = new OnchainBalance() + { + Confirmed = Money.Zero + }, + OffchainBalance = new OffchainBalance() + { + Local = LightMoney.Zero, + Remote = LightMoney.Zero + } + }; + } + } + + public async Task Pay(PayInvoiceParams payParams, CancellationToken cancellation = default) + { + return await Pay(null, payParams, cancellation); + } + + public async Task Pay(string bolt11, PayInvoiceParams payParams, + CancellationToken cancellation = default) + { + try + { + if (string.IsNullOrEmpty(bolt11)) + { + return new PayResponse(PayResult.Error, "BOLT11 invoice required"); + } + + BigInteger? amountSats = null; + if (payParams.Amount > 0) + { + amountSats = new BigInteger(payParams.Amount); + } + + var prepareRequest = new PrepareSendPaymentRequest( + paymentRequest: bolt11, + amount: amountSats + ); + var prepareResponse = await _sdk.PrepareSendPayment(prepareRequest); + + if (prepareResponse.paymentMethod is SendPaymentMethod.Bolt11Invoice bolt11Method) + { + var options = new SendPaymentOptions.Bolt11Invoice( + preferSpark: false, + completionTimeoutSecs: 60 + ); + + var sendRequest = new SendPaymentRequest( + prepareResponse: prepareResponse, + options: options + ); + var sendResponse = await _sdk.SendPayment(sendRequest); + + return new PayResponse() + { + Result = sendResponse.payment.status switch + { + PaymentStatus.Failed => PayResult.Error, + PaymentStatus.Completed => PayResult.Ok, + PaymentStatus.Pending => PayResult.Unknown, + _ => PayResult.Error + }, + Details = new PayDetails() + { + Status = sendResponse.payment.status switch + { + PaymentStatus.Failed => LightningPaymentStatus.Failed, + PaymentStatus.Completed => LightningPaymentStatus.Complete, + PaymentStatus.Pending => LightningPaymentStatus.Pending, + _ => LightningPaymentStatus.Unknown + }, + TotalAmount = LightMoney.Satoshis((long)(sendResponse.payment.amount / 1000)), + FeeAmount = (long)(bolt11Method.lightningFeeSats + (bolt11Method.sparkTransferFeeSats ?? 0)) + } + }; + } + else + { + return new PayResponse(PayResult.Error, "Invalid payment method"); + } + } + catch (Exception e) + { + return new PayResponse(PayResult.Error, e.Message); + } + } + + public async Task Pay(string bolt11, CancellationToken cancellation = default) + { + return await Pay(bolt11, null, cancellation); + } + + public async Task OpenChannel(OpenChannelRequest openChannelRequest, + CancellationToken cancellation = default) + { + throw new NotImplementedException(); + } + + public async Task GetDepositAddress(CancellationToken cancellation = default) + { + throw new NotImplementedException(); + } + + public async Task ConnectTo(NodeInfo nodeInfo, CancellationToken cancellation = default) + { + throw new NotImplementedException(); + } + + public async Task CancelInvoice(string invoiceId, CancellationToken cancellation = default) + { + throw new NotImplementedException(); + } + + public async Task ListChannels(CancellationToken cancellation = default) + { + throw new NotImplementedException(); + } + + private LightningInvoice FromReceivePaymentResponse(ReceivePaymentResponse response, LightMoney requestedAmount) + { + string? paymentHash = null; + try + { + if (BOLT11PaymentRequest.TryParse(response.paymentRequest, out var pr, _network)) + { + paymentHash = pr.PaymentHash?.ToString(); + } + } + catch + { + // Ignore parse errors and fall back to raw request + } + + DebugLogObject("FromReceivePaymentResponse", response); + RecordInvoiceAmount(response.paymentRequest, paymentHash, requestedAmount); + + return new LightningInvoice() + { + Id = paymentHash ?? response.paymentRequest, + PaymentHash = paymentHash, + BOLT11 = response.paymentRequest, + Status = LightningInvoiceStatus.Unpaid, + Amount = requestedAmount + }; + } + + private LightningInvoice FromPayment(Payment payment) + { + if (payment == null) return null; + + string paymentHash = ExtractPaymentHash(payment); + string bolt11 = null; + LightMoney? boltAmount = null; + LightMoney? recordedAmount = null; + + if (payment.details is PaymentDetails.Lightning lightningDetails) + { + bolt11 = lightningDetails.invoice; + if (!string.IsNullOrEmpty(lightningDetails.invoice) && + BOLT11PaymentRequest.TryParse(lightningDetails.invoice, out var pr, _network)) + { + boltAmount = pr.MinimumAmount; + } + + var rec = LookupInvoice(lightningDetails.invoice, paymentHash); + recordedAmount = rec?.Amount; + } + + // Reject if hash is missing or not one we issued + if (string.IsNullOrEmpty(paymentHash)) + { + DebugLog($"FromPayment: missing payment hash for payment.id={payment.id}"); + return null; + } + + var record = LookupInvoice(null, paymentHash); + if (record is null || record.PaymentHash != paymentHash) + { + DebugLog($"FromPayment: unknown payment hash={paymentHash} payment.id={payment.id}"); + return null; + } + + recordedAmount ??= record.Amount; + + // Always use the invoice amount (BOLT11 truth). Never fall back to what Breez reports. + var resolvedAmount = recordedAmount ?? boltAmount; + if (boltAmount is not null && recordedAmount is not null && boltAmount != recordedAmount) + { + DebugLog($"FromPayment: bolt amount {boltAmount.ToUnit(LightMoneyUnit.Satoshi)} != recorded {recordedAmount.ToUnit(LightMoneyUnit.Satoshi)} for hash={paymentHash}"); + } + + var invoiceId = paymentHash; + if (resolvedAmount is null) + { + DebugLog($"FromPayment: missing amount for hash={paymentHash} bolt11={Shorten(bolt11)}"); + return null; + } + + DebugLog($"FromPayment: returning invoice id={invoiceId} hash={paymentHash} bolt11={Shorten(bolt11)} boltSat={boltAmount?.ToUnit(LightMoneyUnit.Satoshi)} recSat={recordedAmount?.ToUnit(LightMoneyUnit.Satoshi)} raw_msat={payment.amount} fee_msat={payment.fees} chosenSat={resolvedAmount.ToUnit(LightMoneyUnit.Satoshi)}"); + + return new LightningInvoice() + { + Id = invoiceId, + PaymentHash = paymentHash ?? invoiceId, + BOLT11 = bolt11 ?? payment.id, + Amount = resolvedAmount, + AmountReceived = resolvedAmount, + Status = payment.status switch + { + PaymentStatus.Pending => LightningInvoiceStatus.Unpaid, + PaymentStatus.Failed => LightningInvoiceStatus.Expired, + PaymentStatus.Completed => LightningInvoiceStatus.Paid, + _ => LightningInvoiceStatus.Unpaid + }, + PaidAt = DateTimeOffset.FromUnixTimeSeconds((long)payment.timestamp) + }; + } + + private LightningPayment ToLightningPayment(Payment payment) + { + if (payment == null) return null; + + string paymentHash = ExtractPaymentHash(payment); + string preimage = null; + string bolt11 = null; + LightMoney? boltAmount = null; + LightMoney? recordedAmount = null; + var feeAmount = GetFeeFromPayment(payment); + + if (payment.details is PaymentDetails.Lightning lightningDetails) + { + preimage = lightningDetails.preimage; + bolt11 = lightningDetails.invoice; + if (!string.IsNullOrEmpty(lightningDetails.invoice) && + BOLT11PaymentRequest.TryParse(lightningDetails.invoice, out var pr, _network)) + { + boltAmount = pr.MinimumAmount; + } + + var rec = LookupInvoice(lightningDetails.invoice, paymentHash); + recordedAmount = rec?.Amount; + } + + if (string.IsNullOrEmpty(paymentHash)) + { + DebugLog($"ToLightningPayment: missing payment hash for payment.id={payment.id}"); + return null; + } + + var record = LookupInvoice(null, paymentHash); + if (record is null || record.PaymentHash != paymentHash) + { + DebugLog($"ToLightningPayment: unknown payment hash={paymentHash} payment.id={payment.id}"); + return null; + } + + recordedAmount ??= record.Amount; + + var resolvedAmount = recordedAmount ?? boltAmount; + if (boltAmount is not null && recordedAmount is not null && boltAmount != recordedAmount) + { + DebugLog($"ToLightningPayment: bolt amount {boltAmount.ToUnit(LightMoneyUnit.Satoshi)} != recorded {recordedAmount.ToUnit(LightMoneyUnit.Satoshi)} for hash={paymentHash}"); + } + + var paymentId = paymentHash; + if (resolvedAmount is null) + { + DebugLog($"ToLightningPayment: missing amount for hash={paymentHash} bolt11={Shorten(bolt11)}"); + return null; + } + + DebugLog($"ToLightningPayment: returning payment id={paymentId} hash={paymentHash} bolt11={Shorten(bolt11)} boltSat={boltAmount?.ToUnit(LightMoneyUnit.Satoshi)} recSat={recordedAmount?.ToUnit(LightMoneyUnit.Satoshi)} raw_msat={payment.amount} fee_msat={payment.fees} chosenSat={resolvedAmount.ToUnit(LightMoneyUnit.Satoshi)}"); + + return new LightningPayment() + { + Id = paymentId, + PaymentHash = paymentHash ?? paymentId, + Preimage = preimage, + BOLT11 = bolt11, + Amount = resolvedAmount, + Status = payment.status switch + { + PaymentStatus.Failed => LightningPaymentStatus.Failed, + PaymentStatus.Completed => LightningPaymentStatus.Complete, + PaymentStatus.Pending => LightningPaymentStatus.Pending, + _ => LightningPaymentStatus.Unknown + }, + CreatedAt = DateTimeOffset.FromUnixTimeSeconds((long)payment.timestamp), + Fee = feeAmount, + AmountSent = resolvedAmount + }; + } + + private void RecordInvoiceAmount(string bolt11, string? paymentHash, LightMoney requestedAmount) + { + // Prefer the amount encoded in the BOLT11 (ground truth), fall back to the requested amount. + LightMoney amount = requestedAmount; + try + { + if (BOLT11PaymentRequest.TryParse(bolt11, out var pr, _network)) + { + if (pr.MinimumAmount is not null) + amount = pr.MinimumAmount; + if (string.IsNullOrEmpty(paymentHash) && pr.PaymentHash is not null) + paymentHash = pr.PaymentHash.ToString(); + } + } + catch { } + + if (string.IsNullOrEmpty(paymentHash)) + return; + + var record = new InvoiceRecord + { + PaymentHash = paymentHash, + Bolt11 = bolt11, + Amount = amount + }; + + _invoicesByHash[paymentHash] = record; + _invoicesByBolt11[bolt11] = record; + } + + private InvoiceRecord? LookupInvoice(string? bolt11, string? paymentHash) + { + if (!string.IsNullOrEmpty(paymentHash) && _invoicesByHash.TryGetValue(paymentHash, out var recByHash)) + { + DebugLog($"LookupInvoice: hit by hash={paymentHash} amount_sat={recByHash.Amount.ToUnit(LightMoneyUnit.Satoshi)} bolt11={Shorten(recByHash.Bolt11)}"); + return recByHash; + } + + if (!string.IsNullOrEmpty(bolt11) && _invoicesByBolt11.TryGetValue(bolt11, out var recByBolt)) + { + DebugLog($"LookupInvoice: hit by bolt11={Shorten(bolt11)} amount_sat={recByBolt.Amount.ToUnit(LightMoneyUnit.Satoshi)}"); + return recByBolt; + } + + DebugLog($"LookupInvoice: miss for hash={paymentHash} bolt11={Shorten(bolt11)}"); + return null; + } + + private bool IsKnownPayment(Payment payment) + { + var paymentHash = ExtractPaymentHash(payment); + if (string.IsNullOrEmpty(paymentHash)) + return false; + + return LookupInvoice(null, paymentHash) is not null; + } + + private LightMoney InferAmountFromPayment(Payment payment) + { + var rawAmount = payment.amount; + + if (rawAmount == 0) + { + return LightMoney.Zero; + } + + // Breez SDK surfaces amounts in millisats for lightning payments; fall back to sats otherwise. + if (rawAmount % 1000 == 0) + { + return LightMoney.Satoshis((long)(rawAmount / 1000)); + } + + return LightMoney.Satoshis((long)rawAmount); + } + + private string? ExtractPaymentHash(Payment payment) + { + if (payment?.details is not PaymentDetails.Lightning ln) + return null; + + if (!string.IsNullOrEmpty(ln.paymentHash)) + return ln.paymentHash; + + if (!string.IsNullOrEmpty(ln.invoice) && + BOLT11PaymentRequest.TryParse(ln.invoice, out var pr, _network) && + pr.PaymentHash is not null) + { + return pr.PaymentHash.ToString(); + } + + return null; + } + + private LightMoney GetFeeFromPayment(Payment payment) + { + return payment.fees % 1000 == 0 + ? LightMoney.Satoshis((long)(payment.fees / 1000)) + : LightMoney.Satoshis((long)payment.fees); + } + + private bool TryMarkPaymentSeen(Payment payment) + { + var paymentHash = ExtractPaymentHash(payment); + var seenByHash = !string.IsNullOrEmpty(paymentHash) && _seenPaymentHashes.ContainsKey(paymentHash); + var seenById = _seenCompletedPayments.ContainsKey(payment.id); + if (seenByHash || seenById) + { + DebugLog($"TryMarkPaymentSeen: already seen payment.id={payment.id} hash={paymentHash}"); + return false; + } + + _seenCompletedPayments.TryAdd(payment.id, true); + if (!string.IsNullOrEmpty(paymentHash)) + { + _seenPaymentHashes.TryAdd(paymentHash, true); + } + + return true; + } + + public NormalizedPayment NormalizePayment(Payment payment) + { + if (payment == null) throw new ArgumentNullException(nameof(payment)); + + string paymentHash = null; + string bolt11 = null; + string description = null; + LightMoney? boltAmount = null; + LightMoney? recordedAmount = null; + var feeAmount = GetFeeFromPayment(payment); + + if (payment.details is PaymentDetails.Lightning lightningDetails) + { + paymentHash = ExtractPaymentHash(payment); + bolt11 = lightningDetails.invoice; + description = lightningDetails.description; + if (!string.IsNullOrEmpty(lightningDetails.invoice) && + BOLT11PaymentRequest.TryParse(lightningDetails.invoice, out var pr, _network)) + { + boltAmount = pr.MinimumAmount; + } + + var rec = LookupInvoice(lightningDetails.invoice, lightningDetails.paymentHash); + recordedAmount = rec?.Amount; + } + + if (string.IsNullOrEmpty(paymentHash)) + { + DebugLog($"NormalizePayment: missing payment hash for payment.id={payment.id}"); + return null; + } + + var record = LookupInvoice(null, paymentHash); + if (record is null || record.PaymentHash != paymentHash) + { + DebugLog($"NormalizePayment: unknown payment hash={paymentHash} payment.id={payment.id}"); + return null; + } + + recordedAmount ??= record.Amount; + var amount = recordedAmount ?? boltAmount; + if (boltAmount is not null && recordedAmount is not null && boltAmount != recordedAmount) + { + DebugLog($"NormalizePayment: bolt amount {boltAmount.ToUnit(LightMoneyUnit.Satoshi)} != recorded {recordedAmount.ToUnit(LightMoneyUnit.Satoshi)} for hash={paymentHash}"); + } + + if (amount is null) + { + // If we can't prove the amount from the BOLT11 or stored record, reject the payment. + DebugLog($"NormalizePayment: missing amount for hash={paymentHash} bolt11={Shorten(bolt11)}"); + return null; + } + var fee = feeAmount; + + return new NormalizedPayment + { + Id = paymentHash ?? bolt11 ?? payment.id, + PaymentType = payment.paymentType, + Status = payment.status, + Timestamp = payment.timestamp, + Amount = amount, + Fee = fee, + Description = description ?? bolt11 + }; + } + + public void Dispose() + { + _sdk?.Dispose(); + } + + public class BreezSparkInvoiceListener : ILightningInvoiceListener + { + private readonly BreezSparkLightningClient _breezLightningClient; + private readonly CancellationToken _cancellationToken; + private readonly ConcurrentQueue _invoices = new(); + + public BreezSparkInvoiceListener(BreezSparkLightningClient breezLightningClient, CancellationToken cancellationToken) + { + _breezLightningClient = breezLightningClient; + _cancellationToken = cancellationToken; + } + + public void Dispose() + { + } + + public async Task WaitInvoice(CancellationToken cancellation) + { + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, cancellation); + + while (!linkedCts.Token.IsCancellationRequested) + { + // Check the client's payment notification queue + if (_breezLightningClient._paymentNotifications.TryDequeue(out var payment)) + { + var invoice = _breezLightningClient.FromPayment(payment); + if (invoice is not null) + { + _breezLightningClient.DebugLog($"WaitInvoice: dequeued payment.id={payment.id} hash={invoice.PaymentHash} bolt11={_breezLightningClient.Shorten(invoice.BOLT11)} status={payment.status} raw_msat={payment.amount} fee_msat={payment.fees}"); + // Force amount to the recorded invoice amount (BOLT11 truth) before handing to BTCPay + var rec = _breezLightningClient.LookupInvoice(invoice.BOLT11, invoice.PaymentHash); + if (rec is not null) + { + invoice.Amount = rec.Amount; + invoice.AmountReceived = rec.Amount; + _breezLightningClient.DebugLog($"WaitInvoice: normalized invoice amount to recorded {rec.Amount.ToUnit(LightMoneyUnit.Satoshi)} sats for hash={invoice.PaymentHash}"); + } + return invoice; + } + } + + // Also check the local queue for backwards compatibility + if (_invoices.TryDequeue(out var payment2)) + { + var invoice = _breezLightningClient.FromPayment(payment2); + if (invoice is not null) + { + _breezLightningClient.DebugLog($"WaitInvoice(local): dequeued payment.id={payment2.id} hash={invoice.PaymentHash} bolt11={_breezLightningClient.Shorten(invoice.BOLT11)} status={payment2.status} raw_msat={payment2.amount} fee_msat={payment2.fees}"); + var rec = _breezLightningClient.LookupInvoice(invoice.BOLT11, invoice.PaymentHash); + if (rec is not null) + { + invoice.Amount = rec.Amount; + invoice.AmountReceived = rec.Amount; + _breezLightningClient.DebugLog($"WaitInvoice: normalized (local queue) invoice amount to recorded {rec.Amount.ToUnit(LightMoneyUnit.Satoshi)} sats for hash={invoice.PaymentHash}"); + } + return invoice; + } + } + + await Task.Delay(1000, linkedCts.Token); // Check every second + } + + linkedCts.Token.ThrowIfCancellationRequested(); + return null; + } + } + + private async Task MonitorPaymentEvents() + { + try + { + while (true) + { + try + { + // Get all payments and check for new paid ones + var payments = await _sdk.ListPayments(new ListPaymentsRequest( + typeFilter: new List { PaymentType.Receive } + )); + + foreach (var payment in payments.payments) + { + // If payment is complete, add it to the notification queue + if (payment.status == PaymentStatus.Completed && + TryMarkPaymentSeen(payment) && + IsKnownPayment(payment)) + { + DebugLogObject("MonitorPaymentEvents:payment", payment); + LogCompletedPayment(payment); + _paymentNotifications.Enqueue(payment); + } + } + + await Task.Delay(5000); // Poll every 5 seconds + } + catch (Exception ex) + { + // Log error but continue monitoring + Console.WriteLine($"Error monitoring BreezSpark payments: {ex.Message}"); + await Task.Delay(10000); // Wait longer on error + } + } + } + catch (Exception ex) + { + Console.WriteLine($"BreezSpark payment monitoring stopped: {ex.Message}"); + } + } + + public void AddPaymentNotification(Payment payment) + { + if (TryMarkPaymentSeen(payment) && + IsKnownPayment(payment)) + { + DebugLogObject("AddPaymentNotification:payment", payment); + LogCompletedPayment(payment); + _paymentNotifications.Enqueue(payment); + } + } + + public async Task<(LightningInvoice Invoice, long FeeSats)> CreateInvoiceWithFee(CreateInvoiceParams createInvoiceRequest, CancellationToken cancellation = default) + { + var description = createInvoiceRequest.Description ?? createInvoiceRequest.DescriptionHash?.ToString() ?? "Invoice"; + var amountSats = (ulong)createInvoiceRequest.Amount.ToUnit(LightMoneyUnit.Satoshi); + var paymentMethod = new ReceivePaymentMethod.Bolt11Invoice(description, amountSats); + var response = await _sdk.ReceivePayment(new ReceivePaymentRequest(paymentMethod)); + var feeSats = (long)response.fee; + var invoice = FromReceivePaymentResponse(response, createInvoiceRequest.Amount); + return (invoice, feeSats); + } + + private async Task GetInvoiceInternal(string identifier, CancellationToken cancellation) + { + var payment = await FindPayment(identifier, cancellation); + if (payment is null) + return null; + + // Deduplicate completed payments so LightningListener doesn't try to add the same payment twice. + if (payment.status == PaymentStatus.Completed && !TryMarkPaymentSeen(payment)) + { + return null; + } + + return FromPayment(payment); + } + + private async Task FindPayment(string identifier, CancellationToken cancellation) + { + try + { + var byId = await _sdk.GetPayment(new GetPaymentRequest(identifier)); + DebugLogObject("FindPayment:GetPayment", byId); + if (byId?.payment != null && IsKnownPayment(byId.payment)) + { + return byId.payment; + } + } + catch + { + // Ignore and fallback to listing payments + } + + try + { + var list = await _sdk.ListPayments(new ListPaymentsRequest( + typeFilter: new List { PaymentType.Receive }, + assetFilter: new AssetFilter.Bitcoin() + )); + DebugLogObject("FindPayment:ListPayments", list); + + return list.payments.FirstOrDefault(p => + { + if (p.details is PaymentDetails.Lightning lightning) + { + if (!IsKnownPayment(p)) + return false; + + return lightning.paymentHash == identifier || + lightning.invoice == identifier; + } + + return p.id == identifier; + }); + } + catch + { + return null; + } + } + + private void LogCompletedPayment(Payment payment) + { + try + { + string paymentHash = ExtractPaymentHash(payment); + string bolt11 = null; + LightMoney? boltAmount = null; + if (payment.details is PaymentDetails.Lightning ln) + { + bolt11 = ln.invoice; + if (!string.IsNullOrEmpty(bolt11) && + BOLT11PaymentRequest.TryParse(bolt11, out var pr, _network)) + { + boltAmount = pr.MinimumAmount; + } + } + + var record = LookupInvoice(bolt11, paymentHash); + var recAmount = record?.Amount.ToUnit(LightMoneyUnit.Satoshi); + var rawAmount = payment.amount; + var fee = payment.fees; + var grossSat = InferAmountFromPayment(payment).ToUnit(LightMoneyUnit.Satoshi) + + GetFeeFromPayment(payment).ToUnit(LightMoneyUnit.Satoshi); + var boltSat = boltAmount?.ToUnit(LightMoneyUnit.Satoshi); + } + catch + { + // best-effort logging + } + } + + private string Shorten(string? s, int head = 6, int tail = 6) + { + if (string.IsNullOrEmpty(s)) + return string.Empty; + if (s.Length <= head + tail + 3) + return s; + return $"{s.Substring(0, head)}...{s.Substring(s.Length - tail)}"; + } +} + +public class NormalizedPayment +{ + public string Id { get; set; } = string.Empty; + public PaymentType PaymentType { get; set; } + public PaymentStatus Status { get; set; } + public ulong Timestamp { get; set; } + public LightMoney Amount { get; set; } = LightMoney.Zero; + public LightMoney Fee { get; set; } = LightMoney.Zero; + public string? Description { get; set; } +} diff --git a/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkLightningConnectionStringHandler.cs b/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkLightningConnectionStringHandler.cs new file mode 100644 index 0000000..ab17607 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkLightningConnectionStringHandler.cs @@ -0,0 +1,33 @@ +using BTCPayServer.Lightning; +using NBitcoin; + +namespace BTCPayServer.Plugins.BreezSpark; + +public class BreezSparkLightningConnectionStringHandler : ILightningConnectionStringHandler +{ + private readonly BreezSparkService _breezService; + + public BreezSparkLightningConnectionStringHandler(BreezSparkService breezService) + { + _breezService = breezService; + } + public ILightningClient? Create(string connectionString, Network network, out string? error) + { + var kv = LightningConnectionStringHelper.ExtractValues(connectionString, out var type); + if (type != "breezspark") + { + error = null; + return null; + } + + + if (!kv.TryGetValue("key", out var key)) + { + error = $"The key 'key' is mandatory for breezspark connection strings"; + return null; + } + + error = null; + return _breezService.GetClientByPaymentKey(key); + } +} diff --git a/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkPaymentMethodHandler.cs b/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkPaymentMethodHandler.cs new file mode 100644 index 0000000..35ecd32 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkPaymentMethodHandler.cs @@ -0,0 +1,162 @@ +#nullable enable +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using BTCPayServer.Configuration; +using BTCPayServer.Data; +using BTCPayServer.Lightning; +using BTCPayServer.Payments; +using BTCPayServer.Payments.Lightning; +using BTCPayServer.Services; +using BTCPayServer.Services.Invoices; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using NBitcoin; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Plugins.BreezSpark +{ + public class BreezSparkPaymentMethodConfig : LightningPaymentMethodConfig + { + public string PaymentKey { get; set; } = string.Empty; + public string StoreId { get; set; } = string.Empty; + } + + public class BreezSparkPaymentMethodHandler : IPaymentMethodHandler, ILightningPaymentHandler + { + private readonly BreezSparkService _breezService; + private readonly PaymentMethodId _paymentMethodId; + private readonly BTCPayNetwork _network; + private readonly LightningClientFactoryService _lightningClientFactory; + private readonly IOptions _lightningNetworkOptions; + public JsonSerializer Serializer { get; } + + public BreezSparkPaymentMethodHandler( + BreezSparkService breezService, + BTCPayNetwork network, + LightningClientFactoryService lightningClientFactory, + IOptions lightningNetworkOptions) + { + _breezService = breezService; + _network = network; + _lightningClientFactory = lightningClientFactory; + _lightningNetworkOptions = lightningNetworkOptions; + _paymentMethodId = PaymentMethodId.Parse("BTC-BreezSpark"); + Serializer = BlobSerializer.CreateSerializer(network.NBitcoinNetwork).Serializer; + } + + public PaymentMethodId PaymentMethodId => _paymentMethodId; + + public BTCPayNetwork Network => _network; + + public Task BeforeFetchingRates(PaymentMethodContext context) + { + context.Prompt.Currency = _network.CryptoCode; + context.Prompt.PaymentMethodFee = 0m; + context.Prompt.Divisibility = 11; + context.Prompt.RateDivisibility = 8; + return Task.CompletedTask; + } + + public async Task ConfigurePrompt(PaymentMethodContext context) + { + if (context.InvoiceEntity.Type == InvoiceType.TopUp) + { + throw new PaymentMethodUnavailableException("BreezSpark Lightning Network payment method is not available for top-up invoices"); + } + + var paymentPrompt = context.Prompt; + var storeBlob = context.StoreBlob; + var store = context.Store; + + // Parse BreezSpark-specific config + var breezConfig = ParsePaymentMethodConfig(context.PaymentMethodConfig); + if (breezConfig == null || string.IsNullOrEmpty(breezConfig.PaymentKey)) + { + throw new PaymentMethodUnavailableException("BreezSpark payment key is not configured"); + } + + // Get BreezSpark client + var breezClient = _breezService.GetClient(breezConfig.StoreId); + if (breezClient == null) + { + throw new PaymentMethodUnavailableException("BreezSpark client is not available for this store"); + } + + var invoice = context.InvoiceEntity; + decimal due = paymentPrompt.Calculate().Due; + var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow; + if (expiry < TimeSpan.Zero) + expiry = TimeSpan.FromSeconds(1); + + LightningInvoice lightningInvoice; + string description = storeBlob.LightningDescriptionTemplate; + description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase) + .Replace("{ItemDescription}", invoice.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase) + .Replace("{OrderId}", invoice.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase); + + try + { + var request = new CreateInvoiceParams( + new LightMoney(due, LightMoneyUnit.BTC), + description, + expiry); + request.PrivateRouteHints = storeBlob.LightningPrivateRouteHints; + + lightningInvoice = await breezClient.CreateInvoice(request, CancellationToken.None); + } + catch (Exception ex) + { + throw new PaymentMethodUnavailableException($"Impossible to create BreezSpark lightning invoice ({ex.Message})", ex); + } + + paymentPrompt.Destination = lightningInvoice.BOLT11; + var details = new LigthningPaymentPromptDetails + { + PaymentHash = lightningInvoice.GetPaymentHash(_network.NBitcoinNetwork), + Preimage = string.IsNullOrEmpty(lightningInvoice.Preimage) ? null : uint256.Parse(lightningInvoice.Preimage), + InvoiceId = lightningInvoice.Id, + NodeInfo = "BreezSpark Lightning Wallet" + }; + paymentPrompt.Details = JObject.FromObject(details, Serializer); + } + + public BreezSparkPaymentMethodConfig ParsePaymentMethodConfig(JToken config) + { + return config.ToObject(Serializer) ?? new BreezSparkPaymentMethodConfig(); + } + + object IPaymentMethodHandler.ParsePaymentMethodConfig(JToken config) + { + return ParsePaymentMethodConfig(config); + } + + public Task CreateLightningClient(LightningPaymentMethodConfig config) + { + var breezConfig = config as BreezSparkPaymentMethodConfig; + if (breezConfig == null || string.IsNullOrEmpty(breezConfig.StoreId)) + return Task.FromResult(null); + + return Task.FromResult(_breezService.GetClient(breezConfig.StoreId)); + } + + public object ParsePaymentPromptDetails(JToken details) + { + return details.ToObject(Serializer); + } + + public LightningPaymentData ParsePaymentDetails(JToken details) + { + return details.ToObject(Serializer); + } + + object IPaymentMethodHandler.ParsePaymentDetails(JToken details) + { + return ParsePaymentDetails(details); + } + } +} diff --git a/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkPlugin.cs b/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkPlugin.cs new file mode 100644 index 0000000..6a7129e --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkPlugin.cs @@ -0,0 +1,58 @@ +#nullable enable +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; +using BTCPayServer.Configuration; +using BTCPayServer.Lightning; +using BTCPayServer.Payments; +using BTCPayServer.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using NBitcoin; +using System; + +namespace BTCPayServer.Plugins.BreezSpark +{ + public class BreezSparkPlugin : BaseBTCPayServerPlugin + { + public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = + { + new() { Identifier = nameof(BTCPayServer), Condition = ">=2.2.0" } + }; + + public override void Execute(IServiceCollection applicationBuilder) + { + applicationBuilder.AddSingleton(); + applicationBuilder.AddSingleton(provider => provider.GetRequiredService()); + applicationBuilder.AddSingleton(); + applicationBuilder.AddSingleton(provider => provider.GetRequiredService()); + + // Register the BreezSpark payment method handler + applicationBuilder.AddSingleton(provider => + { + var breezService = provider.GetRequiredService(); + var networkProvider = provider.GetRequiredService(); + var lightningClientFactory = provider.GetRequiredService(); + var lightningNetworkOptions = provider.GetRequiredService>(); + + return new BreezSparkPaymentMethodHandler( + breezService, + networkProvider.GetNetwork("BTC"), + lightningClientFactory, + lightningNetworkOptions); + }); + + // Add UI extensions for lightning setup tab (like Boltz does) + applicationBuilder.AddSingleton(new UIExtension("BreezSpark/LNPaymentMethodSetupTab", + "ln-payment-method-setup-tab")); + applicationBuilder.AddSingleton(new UIExtension("BreezSpark/LNPaymentMethodSetupTabhead", + "ln-payment-method-setup-tabhead")); + + // Surface BreezSpark navigation inside the store integrations nav, matching the plugin template pattern. + applicationBuilder.AddUIExtension("store-integrations-nav", "BreezSpark/BreezSparkNav"); + + base.Execute(applicationBuilder); + } + } +} diff --git a/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkService.cs b/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkService.cs new file mode 100644 index 0000000..541f1be --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkService.cs @@ -0,0 +1,181 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Configuration; +using BTCPayServer.Data; +using BTCPayServer.HostedServices; +using BTCPayServer.Payments; +using BTCPayServer.Payments.Lightning; +using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Stores; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NBitcoin; + +namespace BTCPayServer.Plugins.BreezSpark; + +public class BreezSparkService:EventHostedServiceBase +{ + private readonly StoreRepository _storeRepository; + private readonly IOptions _dataDirectories; + private readonly IServiceProvider _serviceProvider; + private PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary => _serviceProvider.GetRequiredService(); + private readonly ILogger _logger; + private Dictionary _settings = new(); + private Dictionary _clients = new(); + + public BreezSparkService( + EventAggregator eventAggregator, + StoreRepository storeRepository, + IOptions dataDirectories, + IServiceProvider serviceProvider, + ILogger logger) : base(eventAggregator, logger) + { + _storeRepository = storeRepository; + _dataDirectories = dataDirectories; + _serviceProvider = serviceProvider; + _logger = logger; + } + + protected override void SubscribeToEvents() + { + base.SubscribeToEvents(); + } + + protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) + { + await base.ProcessEvent(evt, cancellationToken); + } + + public string GetWorkDir(string storeId) + { + var dir = _dataDirectories.Value.DataDir; + return Path.Combine(dir, "Plugins", "BreezSpark",storeId); + } + + TaskCompletionSource tcs = new(); + public override async Task StartAsync(CancellationToken cancellationToken) + { + _settings = (await _storeRepository.GetSettingsAsync("BreezSpark")).Where(pair => pair.Value is not null).ToDictionary(pair => pair.Key, pair => pair.Value!); + foreach (var keyValuePair in _settings) + { + try + { + + await Handle(keyValuePair.Key, keyValuePair.Value); + } + catch + { + } + } + tcs.TrySetResult(); + await base.StartAsync(cancellationToken); + } + + public async Task Get(string storeId) + { + await tcs.Task; + _settings.TryGetValue(storeId, out var settings); + + return settings; + } + + public async Task Handle(string? storeId, BreezSparkSettings? settings) + { + if (settings is null) + { + if (storeId is not null && _clients.Remove(storeId, out var client)) + { + client.Dispose(); + } + } + else + { + try + { + var network = Network.Main; + var dir = GetWorkDir(storeId); + Directory.CreateDirectory(dir); + settings.PaymentKey ??= Guid.NewGuid().ToString(); + + var client = await BreezSparkLightningClient.Create( + settings.ApiKey, + dir, + network, + new Mnemonic(settings.Mnemonic), + settings.PaymentKey + ); + + if (storeId is not null) + { + _clients.AddOrReplace(storeId, client); + } + + return client; + } + catch (Exception e) + { + _logger.LogError(e, "Could not create BreezSpark client"); + throw; + } + } + + return null; + } + + public async Task Set(string storeId, BreezSparkSettings? settings) + { + + var result = await Handle(storeId, settings); + await _storeRepository.UpdateSetting(storeId, "BreezSpark", settings!); + if (settings is null) + { + _settings.Remove(storeId, out var oldSettings ); + var data = await _storeRepository.FindStore(storeId); + if (data != null) + { + var pmi = new PaymentMethodId("BTC-LN"); + // In v2.2.1, the payment methods are handled differently + // We'll skip this for now as it needs to be refactored completely + // TODO: Implement proper v2.2.1 payment method handling + } + Directory.Delete(GetWorkDir(storeId), true); + + } + else if(result is not null ) + { + _settings.AddOrReplace(storeId, settings); + } + + + } + + public new async Task StopAsync(CancellationToken cancellationToken) + { + _clients.Values.ToList().ForEach(c => c.Dispose()); + await base.StopAsync(cancellationToken); + } + + public BreezSparkLightningClient? GetClient(string? storeId) + { + + tcs.Task.GetAwaiter().GetResult(); + if(storeId is null) + return null; + _clients.TryGetValue(storeId, out var client); + return client; + } + public BreezSparkLightningClient? GetClientByPaymentKey(string? paymentKey) + { + tcs.Task.GetAwaiter().GetResult(); + if(paymentKey is null) + return null; + var match = _settings.FirstOrDefault(pair => pair.Value.PaymentKey == paymentKey).Key; + return GetClient(match); + } +} \ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkSettings.cs b/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkSettings.cs new file mode 100644 index 0000000..b5ce980 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/BreezSparkSettings.cs @@ -0,0 +1,14 @@ +#nullable enable +using System; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; + +namespace BTCPayServer.Plugins.BreezSpark; + +public class BreezSparkSettings +{ + public string? Mnemonic { get; set; } + public string? ApiKey { get; set; } + + public string PaymentKey { get; set; } = Guid.NewGuid().ToString(); +} diff --git a/dist/BTCPayServer.Plugins.BreezSpark/InvoiceRecord.cs b/dist/BTCPayServer.Plugins.BreezSpark/InvoiceRecord.cs new file mode 100644 index 0000000..d5abd9c --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/InvoiceRecord.cs @@ -0,0 +1,10 @@ +using BTCPayServer.Lightning; + +namespace BTCPayServer.Plugins.BreezSpark; + +public class InvoiceRecord +{ + public string PaymentHash { get; set; } = string.Empty; + public string Bolt11 { get; set; } = string.Empty; + public LightMoney Amount { get; set; } = LightMoney.Zero; +} diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Configure.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Configure.cshtml new file mode 100644 index 0000000..7f0523c --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Configure.cshtml @@ -0,0 +1,44 @@ +@using BTCPayServer +@using BTCPayServer.Abstractions.Extensions +@using BTCPayServer.Plugins.BreezSpark +@model BTCPayServer.Plugins.BreezSpark.BreezSparkSettings? +@inject BreezSparkService BreezService +@{ + ViewData.SetActivePage("Breez", "Configure", "Configure"); + var storeId = Context.GetCurrentStoreId(); + var active = (await BreezService.Get(storeId)) is not null; +} +
+
+
+
+

+ @ViewData["Title"] +

+
+ + @if (active) + { + + } +
+
+ +
+ + + + A Bitcoin 12-word mnemonic seed phrase.BACK THIS UP SAFELY! GENERATE IT RANDOMLY! SERVER ADMINS HAVE ACCESS TO THIS! + +
+
+ + + + Optional. Leave blank to use the default Breez API key. +
+ + +
+
+
diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Info.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Info.cshtml new file mode 100644 index 0000000..8fbce12 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Info.cshtml @@ -0,0 +1,106 @@ +@using Breez.Sdk.Spark +@using BTCPayServer +@using BTCPayServer.Client +@using BTCPayServer.Components.QRCode +@using BTCPayServer.Components.TruncateCenter +@using BTCPayServer.Models.StoreViewModels +@using BTCPayServer.Payments +@using BTCPayServer.Plugins.BreezSpark +@using BTCPayServer.Security +@using BTCPayServer.Services +@using BTCPayServer.Services.Invoices +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using NBitcoin +@inject BreezSparkService BreezService +@inject TransactionLinkProviders TransactionLinkProviders +@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary +@{ + ViewData.SetActivePage("Breez", "Info", "Info"); + string storeId = Model switch + { + string s => s, + StoreDashboardViewModel dashboardModel => dashboardModel.StoreId, + _ => Context.GetImplicitStoreId() + }; + var sdk = BreezService.GetClient(storeId)?.Sdk; + if (sdk is null) + return; +} + +
+
+

Breez Lightning Node Information

+ + @try + { + if (sdk != null) + { +
+
+
Node Status
+
+
+

+ Breez Lightning Node is connected and operational. + This is a simplified view for SDK v0.4.1 compatibility. +

+ +
+
+
+
Status
+
+ Connected +
+ +
Network
+
Bitcoin
+ +
Type
+
Breez SDK v0.4.1
+
+
+
+
+
Service
+
Lightning Network
+ +
Integration
+
BTCPay Server
+
+
+
+
+
+ +
+
+
Quick Actions
+
+ +
+ } + } + catch (Exception ex) + { +
+
Node Information Unavailable
+

Unable to fetch Breez node information: @ex.Message

+

This may be due to SDK v0.4.1 compatibility changes.

+
+ } +
+
\ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Logs.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Logs.cshtml new file mode 100644 index 0000000..3c637c1 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Logs.cshtml @@ -0,0 +1,46 @@ +@using BTCPayServer +@model System.Collections.Concurrent.ConcurrentQueue<(DateTimeOffset timestamp, string log)> +@{ + var storeId = Context.GetCurrentStoreId(); + + ViewData.SetActivePage("Breez", "Logs", "Logs"); +} + +
+
+ @if (!Model.Any()) + { +

+ There are no recent logs. +

+ } + else + { +
+ + + + + + + + + @foreach (var log in Model) + { + + + + + + + } + +
TimestampLog
+ @log.timestamp.ToTimeAgo() + + @log.log +
+
+ } +
+
\ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Payments.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Payments.cshtml new file mode 100644 index 0000000..c4a6d63 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Payments.cshtml @@ -0,0 +1,49 @@ +@using BTCPayServer.Components +@using BTCPayServer +@using BTCPayServer.Abstractions.Extensions +@using BTCPayServer.Client +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using BTCPayServer.Components.QRCode +@using BTCPayServer.Components.TruncateCenter +@model BTCPayServer.Plugins.BreezSpark.PaymentsViewModel +@{ + var storeId = Context.GetCurrentStoreId(); + + ViewData.SetActivePage("Breez", "Payments", "Payments"); + TempData.TryGetValue("bolt11", out var bolt11); +} + +
+
+
+

+ @ViewData["Title"] +

+
+ + Send + Receive +
+
+ + @if (bolt11 is string bolt11s) + { +
+
+ +
+
+
+ + +
+ + +
+
+ } + + + +
+
\ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Receive.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Receive.cshtml new file mode 100644 index 0000000..ca19652 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Receive.cshtml @@ -0,0 +1,64 @@ +@using BTCPayServer.Lightning +@using BTCPayServer.Models.StoreViewModels +@using BTCPayServer.Plugins.BreezSpark +@using BTCPayServer.Security +@inject BreezSparkService 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 nodeInfo = new Breez.Sdk.Spark.GetInfoRequest(ensureSynced: false); + Breez.Sdk.Spark.GetInfoResponse? infoResponse = null; + try + { + infoResponse = await sdk.GetInfo(nodeInfo); + } + catch (Exception ex) + { + + return; + } + + var max = LightMoney.Satoshis((long)(infoResponse?.balanceSats ?? 0)); + +} + + +
+
+
+
+

+ @ViewData["Title"] +

+
+ + +
+
+
+
+ + + Maximum receivable: @max.ToUnit(LightMoneyUnit.Satoshi) sats +
+
+ + +
+ +
+
+
\ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Send.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Send.cshtml new file mode 100644 index 0000000..315a5d2 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Send.cshtml @@ -0,0 +1,100 @@ +@using System.Numerics +@using BTCPayServer.Lightning +@using BTCPayServer.Models.StoreViewModels +@using BTCPayServer.Plugins.BreezSpark +@using BTCPayServer.Security +@inject BreezSparkService 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 nodeInfo = new Breez.Sdk.Spark.GetInfoRequest(ensureSynced: false); + Breez.Sdk.Spark.GetInfoResponse? infoResponse = null; + try + { + infoResponse = await sdk.GetInfo(nodeInfo); + } + catch (Exception ex) + { + + return; + } + + var max = LightMoney.Satoshis((long)(infoResponse?.balanceSats ?? 0)).ToUnit(LightMoneyUnit.Satoshi); + +} + + +@if (ViewData["PaymentDetails"] is PaymentDetailsViewModel paymentDetails) +{ +
+

Payment Details

+
+
+

Destination: @paymentDetails.Destination

+

Amount: @paymentDetails.Amount sats

+
+
+

Fee: @paymentDetails.Fee sats

+

Total: @(paymentDetails.Amount + paymentDetails.Fee) sats

+
+
+
+ + + + + Cancel +
+
+} +else +{ +
+
+
+
+

+ @ViewData["Title"] +

+
+ +
+
+
+
+ + + Enter a Lightning bolt11 invoice or a Bitcoin address +
+
+ + + Maximum payable: @max sats +
+
+
+
+} + +@functions { + public class PaymentDetailsViewModel + { + public string Destination { get; set; } + public long Amount { get; set; } + public long Fee { get; set; } + public string PrepareResponseJson { get; set; } + } +} \ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapIn.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapIn.cshtml new file mode 100644 index 0000000..8c8bbac --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapIn.cshtml @@ -0,0 +1,212 @@ +@using Breez.Sdk.Spark +@using BTCPayServer +@using BTCPayServer.Client +@using BTCPayServer.Components.QRCode +@using BTCPayServer.Components.TruncateCenter +@using BTCPayServer.Models.StoreViewModels +@using BTCPayServer.Payments +@using BTCPayServer.Plugins.BreezSpark +@using BTCPayServer.Security +@using BTCPayServer.Services +@using BTCPayServer.Services.Invoices +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using NBitcoin +@inject BreezSparkService BreezService +@inject TransactionLinkProviders TransactionLinkProviders +@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary +@inject BTCPayNetworkProvider BTCPayNetworkProvider +@{ + ViewData.SetActivePage("Breez", "Swap In", "SwapIn"); + var pmi = PaymentMethodId.Parse("BTC-OnChain"); + string storeId = Model switch + { + string s => s, + StoreDashboardViewModel dashboardModel => dashboardModel.StoreId, + _ => Context.GetImplicitStoreId() + }; + var sdk = BreezService.GetClient(storeId)?.Sdk; + if (sdk is null) + return; + + // Get current receive payment swap info (if any) + SwapInfo? currentSwap = null; + try + { + // Simplified logic - skip complex payment lookup for now + Payment? pendingReceive = null; + + // Skip pending receive logic for SDK v0.4.1 compatibility + + // If no pending swap, create a new one + if (currentSwap == null) + { + var request = new ReceivePaymentRequest( + paymentMethod: new ReceivePaymentMethod.BitcoinAddress() + ); + var response = await sdk.ReceivePayment(request: request); + + // Create swap info from response + currentSwap = new SwapInfo + { + bitcoinAddress = response.paymentRequest, + minAllowedDeposit = 1000, // Default minimum + maxAllowedDeposit = 16777215, // Default maximum (~0.16 BTC) + status = "Created" + }; + } + } + catch (Exception ex) + { + + return; + } + + // Get refundable deposits using the new SDK pattern + // TODO: Fix for v2.2.1 - DepositInfo structure needs to be updated + // List? refundables = null; + // try + // { + // var request = new ListUnclaimedDepositsRequest(); + // var response = await sdk.ListUnclaimedDeposits(request); + // refundables = response.deposits.ToList(); + // } + // catch (Exception ex) + // { + // + // } + + @* TODO: Fix for v2.2.1 - derivation settings check needed *@ + + // NodeState API has changed in SDK v0.4.1 - using GetInfo instead + var nodeInfo = new Breez.Sdk.Spark.GetInfoRequest(ensureSynced: false); + Breez.Sdk.Spark.GetInfoResponse? ni = null; + try + { + ni = await sdk.GetInfo(nodeInfo); + } + catch (Exception ex) + { + + } + + // Use standard fee rates since RecommendedFees API may have changed + var hasFeeRates = true; // Always show fee options + + // Get explorer URL for transaction links + var network = BTCPayNetworkProvider.GetNetwork("BTC"); + var explorerUrl = network.BlockExplorerLink?.ToString() ?? "#"; +} + +@if (hasFeeRates) +{ + + + + + + + +} + +
+
+ + @if (currentSwap != null) + { +
+
+ +
+
+
+ + +
+
+ Please send an amount between
@Money.Satoshis(currentSwap.minAllowedDeposit).ToDecimal(MoneyUnit.BTC) and @Money.Satoshis(currentSwap.maxAllowedDeposit).ToDecimal(MoneyUnit.BTC)BTC
+ @* TODO: Fix for v2.2.1 - derivation settings check needed *@ + @* + @if (derivationSettings != null) + { + + } + *@ +
+
+
+ } + + @* + @if (refundables?.Any() is true) + { +
+ + + + + + + + + + + + @foreach (var deposit in refundables) + { + + + + + + + + } + +
Deposit TxAmountSwap TxRefundableActions
+ @{ + var txLink = TransactionLinkProviders.GetTransactionLink(deposit.txId, "BTC"); + } + + + + @deposit.amount + @if (!string.IsNullOrEmpty(deposit.swapTxId)) + { + var swapTxLink = TransactionLinkProviders.GetTransactionLink(deposit.swapTxId, "BTC"); + + + + } + else + { + N/A + } + + @if (deposit.refundable) + { + Yes + } + else + { + No + } + +
+ +
+
+
+ } + *@ +
+
\ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapInRefund.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapInRefund.cshtml new file mode 100644 index 0000000..eafd0de --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapInRefund.cshtml @@ -0,0 +1,73 @@ +@using BTCPayServer +@using BTCPayServer.Plugins.BreezSpark +@using BTCPayServer.Security +@using BTCPayServer.Services.Invoices +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using Microsoft.AspNetCore.Routing +@model string +@inject BreezSparkService BreezService +@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary +@* @inject BTCPayNetworkProvider BTCPayNetworkProvider *@ +@{ + var storeId = Context.GetImplicitStoreId(); + var address = Context.GetRouteValue("address").ToString(); + ViewData.SetActivePage("Breez", "Create Swapin Refund", "SwapIn"); + + @* TODO: Fix for v2.2.1 - derivation settings check needed *@ + var sdk = BreezService.GetClient(storeId)?.Sdk; + // Use standard fee rates since RecommendedFees API may have changed + var standardFees = new { fastestFee = 10, halfHourFee = 5, hourFee = 3, economyFee = 2, minimumFee = 1 }; +} + + + + + + + + + +@* + @if (derivationSettings is not null) + { + + } +*@ + + +
+
+
+
+

+ @ViewData["Title"] + + + +

+
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + Choose from preset fee rates or enter custom value +
+ +
+
+
\ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapOut.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapOut.cshtml new file mode 100644 index 0000000..be48c79 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/SwapOut.cshtml @@ -0,0 +1,83 @@ +@using Breez.Sdk.Spark +@using BTCPayServer +@using BTCPayServer.Models.StoreViewModels +@using BTCPayServer.Plugins.BreezSpark +@using BTCPayServer.Security +@using BTCPayServer.Services.Invoices +@using Microsoft.AspNetCore.Mvc.TagHelpers +@inject BreezSparkService BreezService +@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary +@* @inject BTCPayNetworkProvider BTCPayNetworkProvider *@ +@{ + Layout = "_Layout"; + ViewData.SetActivePage("Breez", "Swap Out", "SwapOut"); + string storeId = null; + if (Model is string s) + { + storeId = s; + } + else if (Model is StoreDashboardViewModel dashboardModel) + { + storeId = dashboardModel.StoreId; + } + else + { + storeId = Context.GetImplicitStoreId(); + } + + // Simplified fee structure for SDK v0.4.1 compatibility + var fastFee = 10; + var slowFee = 5; + var minFee = 1; +} + + + + + + + + + + + @{ + @* TODO: Fix derivation settings for v2.2.1 - GetDerivationSchemeSettings method signature changed *@ + var hasStoreWallet = false; + if (hasStoreWallet) + { + + } + } + + +
+
+
+
+

+ @ViewData["Title"] +

+
+ +
+
+
+
+ + +
+
+ + + Choose from preset fee rates or enter custom value +
+
+ + +

Minimum: 1000 sats, Maximum: 10000000 sats

+
+ + +
+
+
\ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Sweep.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Sweep.cshtml new file mode 100644 index 0000000..dbed979 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/Sweep.cshtml @@ -0,0 +1,79 @@ +@using BTCPayServer +@using BTCPayServer.Models.StoreViewModels +@using BTCPayServer.Plugins.BreezSpark +@using BTCPayServer.Security +@using BTCPayServer.Services.Invoices +@using Microsoft.AspNetCore.Mvc.TagHelpers +@inject BreezSparkService BreezService +@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary +@* @inject BTCPayNetworkProvider BTCPayNetworkProvider *@ +@{ + Layout = "_Layout"; + ViewData.SetActivePage("Breez", "Sweep", "Sweep"); + string storeId = null; + if (Model is string s) + { + storeId = s; + } + else if (Model is StoreDashboardViewModel dashboardModel) + { + storeId = dashboardModel.StoreId; + } + else + { + storeId = Context.GetImplicitStoreId(); + } + var sdk = BreezService.GetClient(storeId)?.Sdk; + if (sdk is null) + return; + + @* TODO: Fix for v2.2.1 - derivation settings check needed *@ + // Use standard fee rates since RecommendedFees API may have changed + var standardFees = new { fastestFee = 10, halfHourFee = 5, hourFee = 3, economyFee = 2, minimumFee = 1 }; +} + + + + + + + + + +@* + @if (derivationSettings is not null) + { + + } +*@ + + +
+
+
+
+

+ @ViewData["Title"] + + + +

+
+ +
+
+
+
+ + + Address to sweep funds to +
+
+ + + Choose from preset fee rates or enter custom value +
+ +
+
+
\ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/_Nav.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/BreezSpark/_Nav.cshtml new file mode 100644 index 0000000..e69de29 diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezNodeInfo.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezNodeInfo.cshtml new file mode 100644 index 0000000..e4e80ab --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezNodeInfo.cshtml @@ -0,0 +1,90 @@ +@using BTCPayServer.Lightning +@using BTCPayServer.Models.StoreViewModels +@using BTCPayServer.Plugins.BreezSpark +@using BTCPayServer.Security +@using BTCPayServer.Client +@inject BreezSparkService BreezService +@{ + string storeId = null; + if (Model is string s) + { + storeId = s; + } + else if (Model is StoreDashboardViewModel dashboardModel) + { + storeId = dashboardModel.StoreId; + } + else + { + storeId = Context.GetImplicitStoreId(); + } + + // In SDK v0.4.1, NodeState API has changed and async calls can't be made from partial views + // This widget needs to be refactored to receive data from controller + bool isConfigured = false; + try + { + var client = BreezService.GetClient(storeId); + isConfigured = client?.Sdk != null; + } + catch + { + // Handle any errors gracefully + } +} + + +
+ @if (Model is StoreDashboardViewModel) + { +
+

Breez Node

+ + Manage + + +
+ } + + @if (isConfigured) + { +
+ +
Breez Spark (nodeless)
+
+ +
+
+
Lightning Balance
+ @if (Model is StoreDashboardViewModel) + { +
+ Swap In + Send +
+ } +
+
+ Dashboard widget updated for SDK v0.4.1 +
+
+ } + else + { +
+

Breez node information not available

+ @if (string.IsNullOrEmpty(storeId)) + { + Configure Breez + } +
+ } +
\ No newline at end of file diff --git a/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezPaymentsTable.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezPaymentsTable.cshtml similarity index 100% rename from BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezPaymentsTable.cshtml rename to dist/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezPaymentsTable.cshtml diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezSparkNav.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezSparkNav.cshtml new file mode 100644 index 0000000..dd59361 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/BreezSparkNav.cshtml @@ -0,0 +1,60 @@ +@using Breez.Sdk +@using BTCPayServer.Abstractions.Contracts +@using BTCPayServer.Abstractions.Extensions +@using BTCPayServer.Client +@using BTCPayServer.Models.StoreViewModels +@using BTCPayServer.Plugins.BreezSpark +@using BTCPayServer.Security +@using Microsoft.AspNetCore.Mvc.TagHelpers +@inject IScopeProvider ScopeProvider +@inject BreezSparkService BreezService +@{ + var storeId = Model switch + { + string s => s, + StoreDashboardViewModel dashboardModel => dashboardModel.StoreId, + _ => Context.GetImplicitStoreId() + }; + var active = @ViewData.IsActivePage("Breez"); + var client = string.IsNullOrEmpty(active) ? null : BreezService.GetClient(storeId); + var sdk = client?.Sdk; +} +@if (!string.IsNullOrEmpty(storeId)) +{ + + + @if (sdk is not null) + { + + + + @if (client.Events.Any()) + { + + } + } +} \ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/LNPaymentMethodSetupTab.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/LNPaymentMethodSetupTab.cshtml new file mode 100644 index 0000000..c65d51f --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/LNPaymentMethodSetupTab.cshtml @@ -0,0 +1,90 @@ +@model BTCPayServer.Models.StoreViewModels.LightningNodeViewModel +@inject BreezSparkService BreezService +@{ + var storeId = Model.StoreId; + if (Model.CryptoCode != "BTC") + { + return; + } + + // Try to get existing Breez client to extract the payment key + var breezClient = BreezService.GetClient(Model.StoreId); + string paymentKey = ""; + + if (breezClient != null) + { + // Extract payment key from the existing client connection string + var connStr = breezClient.ToString(); + if (connStr.Contains("key=")) + { + var keyStart = connStr.IndexOf("key=") + 4; + var keyEnd = connStr.IndexOf(";", keyStart); + if (keyEnd == -1) keyEnd = connStr.Length; + paymentKey = connStr.Substring(keyStart, keyEnd - keyStart); + } + } +} + + + + + +
+

You can use Breez to accept lightning payments without running a traditional Lightning node.

+

Breez is a mobile-first Lightning Network client that provides a non-custodial solution for Lightning payments.

+
\ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/LNPaymentMethodSetupTabhead.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/LNPaymentMethodSetupTabhead.cshtml new file mode 100644 index 0000000..375a4f7 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/Views/Shared/BreezSpark/LNPaymentMethodSetupTabhead.cshtml @@ -0,0 +1,38 @@ +@inject BreezSparkService BreezService; +@using BTCPayServer.Client +@using BTCPayServer.Plugins.BreezSpark +@using Microsoft.AspNetCore.Mvc.TagHelpers +@model BTCPayServer.Models.StoreViewModels.LightningNodeViewModel + +@if (Model.CryptoCode != "BTC") +{ + return; +} + +@{ + var breezClient = BreezService.GetClient(Model.StoreId); +} + +@if (breezClient == null) +{ + + + +} +else +{ + + + +} \ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/Views/_ViewImports.cshtml b/dist/BTCPayServer.Plugins.BreezSpark/Views/_ViewImports.cshtml new file mode 100644 index 0000000..ee5ced0 --- /dev/null +++ b/dist/BTCPayServer.Plugins.BreezSpark/Views/_ViewImports.cshtml @@ -0,0 +1,9 @@ +@using BTCPayServer.Abstractions.Extensions +@using BTCPayServer.Plugins.BreezSpark +@using Breez.Sdk.Spark +@inject BTCPayServer.Abstractions.Services.Safe Safe +@addTagHelper *, BTCPayServer.Abstractions +@addTagHelper *, BTCPayServer.TagHelpers +@addTagHelper *, BTCPayServer.Views.TagHelpers +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, BTCPayServer \ No newline at end of file diff --git a/dist/BTCPayServer.Plugins.BreezSpark/runtimes/linux-arm64/native/libbreez_sdk_spark_bindings.so b/dist/BTCPayServer.Plugins.BreezSpark/runtimes/linux-arm64/native/libbreez_sdk_spark_bindings.so new file mode 100755 index 0000000..4cb9b94 Binary files /dev/null and b/dist/BTCPayServer.Plugins.BreezSpark/runtimes/linux-arm64/native/libbreez_sdk_spark_bindings.so differ diff --git a/dist/BTCPayServer.Plugins.BreezSpark/runtimes/linux-x64/native/libbreez_sdk_spark_bindings.so b/dist/BTCPayServer.Plugins.BreezSpark/runtimes/linux-x64/native/libbreez_sdk_spark_bindings.so new file mode 100755 index 0000000..33401a5 Binary files /dev/null and b/dist/BTCPayServer.Plugins.BreezSpark/runtimes/linux-x64/native/libbreez_sdk_spark_bindings.so differ diff --git a/dist/BTCPayServer.Plugins.BreezSpark/runtimes/osx-arm64/native/libbreez_sdk_spark_bindings.dylib b/dist/BTCPayServer.Plugins.BreezSpark/runtimes/osx-arm64/native/libbreez_sdk_spark_bindings.dylib new file mode 100755 index 0000000..00b9053 Binary files /dev/null and b/dist/BTCPayServer.Plugins.BreezSpark/runtimes/osx-arm64/native/libbreez_sdk_spark_bindings.dylib differ diff --git a/dist/BTCPayServer.Plugins.BreezSpark/runtimes/osx-x64/native/libbreez_sdk_spark_bindings.dylib b/dist/BTCPayServer.Plugins.BreezSpark/runtimes/osx-x64/native/libbreez_sdk_spark_bindings.dylib new file mode 100755 index 0000000..dbe3dff Binary files /dev/null and b/dist/BTCPayServer.Plugins.BreezSpark/runtimes/osx-x64/native/libbreez_sdk_spark_bindings.dylib differ diff --git a/dist/BTCPayServer.Plugins.BreezSpark/runtimes/win-x64/native/breez_sdk_spark_bindings.dll b/dist/BTCPayServer.Plugins.BreezSpark/runtimes/win-x64/native/breez_sdk_spark_bindings.dll new file mode 100755 index 0000000..f18d5f3 Binary files /dev/null and b/dist/BTCPayServer.Plugins.BreezSpark/runtimes/win-x64/native/breez_sdk_spark_bindings.dll differ diff --git a/dist/BTCPayServer.Plugins.BreezSpark/runtimes/win-x86/native/breez_sdk_spark_bindings.dll b/dist/BTCPayServer.Plugins.BreezSpark/runtimes/win-x86/native/breez_sdk_spark_bindings.dll new file mode 100755 index 0000000..1dd2e31 Binary files /dev/null and b/dist/BTCPayServer.Plugins.BreezSpark/runtimes/win-x86/native/breez_sdk_spark_bindings.dll differ diff --git a/published/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.deps.json b/published/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.deps.json new file mode 100644 index 0000000..3b924af --- /dev/null +++ b/published/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.deps.json @@ -0,0 +1,73 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v8.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v8.0": { + "BTCPayServer.Plugins.BreezSpark/1.1.0": { + "dependencies": { + "Breez.Sdk.Spark": "0.4.1" + }, + "runtime": { + "BTCPayServer.Plugins.BreezSpark.dll": {} + } + }, + "Breez.Sdk.Spark/0.4.1": { + "runtime": { + "lib/net8.0/Breez.Sdk.Spark.dll": { + "assemblyVersion": "0.4.1.0", + "fileVersion": "0.4.1.0" + } + }, + "runtimeTargets": { + "runtimes/linux-arm64/native/libbreez_sdk_spark_bindings.so": { + "rid": "linux-arm64", + "assetType": "native", + "fileVersion": "0.0.0.0" + }, + "runtimes/linux-x64/native/libbreez_sdk_spark_bindings.so": { + "rid": "linux-x64", + "assetType": "native", + "fileVersion": "0.0.0.0" + }, + "runtimes/osx-arm64/native/libbreez_sdk_spark_bindings.dylib": { + "rid": "osx-arm64", + "assetType": "native", + "fileVersion": "0.0.0.0" + }, + "runtimes/osx-x64/native/libbreez_sdk_spark_bindings.dylib": { + "rid": "osx-x64", + "assetType": "native", + "fileVersion": "0.0.0.0" + }, + "runtimes/win-x64/native/breez_sdk_spark_bindings.dll": { + "rid": "win-x64", + "assetType": "native", + "fileVersion": "0.0.0.0" + }, + "runtimes/win-x86/native/breez_sdk_spark_bindings.dll": { + "rid": "win-x86", + "assetType": "native", + "fileVersion": "0.0.0.0" + } + } + } + } + }, + "libraries": { + "BTCPayServer.Plugins.BreezSpark/1.1.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Breez.Sdk.Spark/0.4.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-/5iC7V3PK0q5og4h5qnSf5xepfvcXSEeV5WJDIAlOGd7WUtMZdNQ0n6yDcgd1Rv5qcxPQsDGQN3IVQZbP0UE1w==", + "path": "breez.sdk.spark/0.4.1", + "hashPath": "breez.sdk.spark.0.4.1.nupkg.sha512" + } + } +} \ No newline at end of file diff --git a/published/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.dll b/published/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.dll new file mode 100644 index 0000000..79beade Binary files /dev/null and b/published/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.dll differ diff --git a/published/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.pdb b/published/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.pdb new file mode 100644 index 0000000..89f1b9b Binary files /dev/null and b/published/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.pdb differ diff --git a/published/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.staticwebassets.endpoints.json b/published/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.staticwebassets.endpoints.json new file mode 100644 index 0000000..21da96b --- /dev/null +++ b/published/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.staticwebassets.endpoints.json @@ -0,0 +1 @@ +{"Version":1,"ManifestType":"Publish","Endpoints":[]} \ No newline at end of file diff --git a/published/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.xml b/published/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.xml new file mode 100644 index 0000000..25320e3 --- /dev/null +++ b/published/BTCPayServer.Plugins.BreezSpark/BTCPayServer.Plugins.BreezSpark.xml @@ -0,0 +1,8 @@ + + + + BTCPayServer.Plugins.BreezSpark + + + + diff --git a/published/BTCPayServer.Plugins.BreezSpark/Breez.Sdk.Spark.dll b/published/BTCPayServer.Plugins.BreezSpark/Breez.Sdk.Spark.dll new file mode 100755 index 0000000..7db16f5 Binary files /dev/null and b/published/BTCPayServer.Plugins.BreezSpark/Breez.Sdk.Spark.dll differ diff --git a/published/BTCPayServer.Plugins.BreezSpark/runtimes/linux-arm64/native/libbreez_sdk_spark_bindings.so b/published/BTCPayServer.Plugins.BreezSpark/runtimes/linux-arm64/native/libbreez_sdk_spark_bindings.so new file mode 100755 index 0000000..4cb9b94 Binary files /dev/null and b/published/BTCPayServer.Plugins.BreezSpark/runtimes/linux-arm64/native/libbreez_sdk_spark_bindings.so differ diff --git a/published/BTCPayServer.Plugins.BreezSpark/runtimes/linux-x64/native/libbreez_sdk_spark_bindings.so b/published/BTCPayServer.Plugins.BreezSpark/runtimes/linux-x64/native/libbreez_sdk_spark_bindings.so new file mode 100755 index 0000000..33401a5 Binary files /dev/null and b/published/BTCPayServer.Plugins.BreezSpark/runtimes/linux-x64/native/libbreez_sdk_spark_bindings.so differ diff --git a/published/BTCPayServer.Plugins.BreezSpark/runtimes/osx-arm64/native/libbreez_sdk_spark_bindings.dylib b/published/BTCPayServer.Plugins.BreezSpark/runtimes/osx-arm64/native/libbreez_sdk_spark_bindings.dylib new file mode 100755 index 0000000..00b9053 Binary files /dev/null and b/published/BTCPayServer.Plugins.BreezSpark/runtimes/osx-arm64/native/libbreez_sdk_spark_bindings.dylib differ diff --git a/published/BTCPayServer.Plugins.BreezSpark/runtimes/osx-x64/native/libbreez_sdk_spark_bindings.dylib b/published/BTCPayServer.Plugins.BreezSpark/runtimes/osx-x64/native/libbreez_sdk_spark_bindings.dylib new file mode 100755 index 0000000..dbe3dff Binary files /dev/null and b/published/BTCPayServer.Plugins.BreezSpark/runtimes/osx-x64/native/libbreez_sdk_spark_bindings.dylib differ diff --git a/published/BTCPayServer.Plugins.BreezSpark/runtimes/win-x64/native/breez_sdk_spark_bindings.dll b/published/BTCPayServer.Plugins.BreezSpark/runtimes/win-x64/native/breez_sdk_spark_bindings.dll new file mode 100755 index 0000000..f18d5f3 Binary files /dev/null and b/published/BTCPayServer.Plugins.BreezSpark/runtimes/win-x64/native/breez_sdk_spark_bindings.dll differ diff --git a/published/BTCPayServer.Plugins.BreezSpark/runtimes/win-x86/native/breez_sdk_spark_bindings.dll b/published/BTCPayServer.Plugins.BreezSpark/runtimes/win-x86/native/breez_sdk_spark_bindings.dll new file mode 100755 index 0000000..1dd2e31 Binary files /dev/null and b/published/BTCPayServer.Plugins.BreezSpark/runtimes/win-x86/native/breez_sdk_spark_bindings.dll differ