diff --git a/.gitignore b/.gitignore
index 209f4bb..0d9b821 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
Plugins/packed
.vs/
/BTCPayServerPlugins.sln.DotSettings.user
+local-packages/*.nupkg
diff --git a/NuGet.Config b/NuGet.Config
new file mode 100644
index 0000000..918046d
--- /dev/null
+++ b/NuGet.Config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/Plugins/BTCPayServer.Plugins.Breez/BTCPayServer.Plugins.Breez.csproj b/Plugins/BTCPayServer.Plugins.Breez/BTCPayServer.Plugins.Breez.csproj
index 5338fb1..ddfb00a 100644
--- a/Plugins/BTCPayServer.Plugins.Breez/BTCPayServer.Plugins.Breez.csproj
+++ b/Plugins/BTCPayServer.Plugins.Breez/BTCPayServer.Plugins.Breez.csproj
@@ -34,7 +34,7 @@
-
+
diff --git a/Plugins/BTCPayServer.Plugins.Breez/BreezController.cs b/Plugins/BTCPayServer.Plugins.Breez/BreezController.cs
index 3042109..b4c54a3 100644
--- a/Plugins/BTCPayServer.Plugins.Breez/BreezController.cs
+++ b/Plugins/BTCPayServer.Plugins.Breez/BreezController.cs
@@ -1,9 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.IO.Compression;
using System.Threading.Tasks;
-using Breez.Sdk;
+using Breez.Sdk.Spark;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Data;
@@ -105,7 +104,7 @@ public class BreezController : Controller
[HttpPost("sweep")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
- public async Task Sweep(string storeId, string address, uint satPerByte)
+ public async Task Sweep(string storeId)
{
var client = _breezService.GetClient(storeId);
if (client is null)
@@ -113,27 +112,33 @@ public class BreezController : Controller
return RedirectToAction(nameof(Configure), new {storeId});
}
- if (address.Equals("store", StringComparison.InvariantCultureIgnoreCase))
- {
- var store = ControllerContext.HttpContext.GetStoreData()
- .GetDerivationSchemeSettings(_paymentMethodHandlerDictionary, "BTC");
- var res = await _btcWalletProvider.GetWallet(storeId)
- .ReserveAddressAsync(storeId, store.AccountDerivation, "Breez");
- address = res.Address.ToString();
- }
-
try
{
- var response = client.Sdk.RedeemOnchainFunds(new RedeemOnchainFundsRequest(address, satPerByte));
+ // In Spark SDK, deposits are automatically claimed
+ // List and claim any unclaimed deposits
+ var deposits = await client.Sdk.ListUnclaimedDeposits(new ListUnclaimedDepositsRequest());
+ var claimedCount = 0;
- TempData[WellKnownTempData.SuccessMessage] = $"sweep successful: {response.txid}";
+ foreach (var deposit in deposits.deposits)
+ {
+ try
+ {
+ await client.Sdk.ClaimDeposit(new ClaimDepositRequest(deposit.id));
+ claimedCount++;
+ }
+ catch
+ {
+ // Continue with next deposit
+ }
+ }
+
+ TempData[WellKnownTempData.SuccessMessage] = $"Claimed {claimedCount} deposits";
}
catch (Exception e)
{
- TempData[WellKnownTempData.ErrorMessage] = $"error with sweep: {e.Message}";
+ TempData[WellKnownTempData.ErrorMessage] = $"error claiming deposits: {e.Message}";
}
-
return View((object) storeId);
}
@@ -264,28 +269,9 @@ public class BreezController : Controller
return RedirectToAction(nameof(Configure), new {storeId});
}
- if (address.Equals("store", StringComparison.InvariantCultureIgnoreCase))
- {
- var store = ControllerContext.HttpContext.GetStoreData()
- .GetDerivationSchemeSettings(_paymentMethodHandlerDictionary, "BTC");
- var res = await _btcWalletProvider.GetWallet(storeId)
- .ReserveAddressAsync(storeId, store.AccountDerivation, "Breez");
- address = res.Address.ToString();
- }
-
- try
- {
-
-
- var prep = client.Sdk.PrepareOnchainPayment(new PrepareOnchainPaymentRequest(amount, SwapAmountType.Send, satPerByte));
- var result = client.Sdk.PayOnchain(new PayOnchainRequest(address, prep));
- // var result = client.Sdk.SendSpontaneousPayment(new SendSpontaneousPaymentRequestew SendOnchainRequest(amount, address, feesHash, satPerByte));
- TempData[WellKnownTempData.SuccessMessage] = $"swap out created: {result.reverseSwapInfo.id}";
- }
- catch (Exception e)
- {
- TempData[WellKnownTempData.ErrorMessage] = $"Couldnt create swap out: {e.Message}";
- }
+ // Spark SDK doesn't support onchain swap-out
+ // This is a nodeless protocol focused on Lightning
+ TempData[WellKnownTempData.ErrorMessage] = "Swap out is not available in Spark SDK (nodeless mode). Please withdraw via Lightning.";
return RedirectToAction("SwapOut", new {storeId});
}
@@ -305,7 +291,7 @@ public class BreezController : Controller
[HttpPost("swapin/{address}/refund")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
- public async Task SwapInRefund(string storeId, string address, string refundAddress, uint satPerByte)
+ public async Task SwapInRefund(string storeId, string depositId, string refundAddress)
{
var client = _breezService.GetClient(storeId);
if (client is null)
@@ -315,8 +301,8 @@ public class BreezController : Controller
try
{
- var resp = client.Sdk.Refund(new RefundRequest(address, refundAddress, satPerByte));
- TempData[WellKnownTempData.SuccessMessage] = $"Refund tx: {resp.refundTxId}";
+ var resp = await client.Sdk.RefundDeposit(new RefundDepositRequest(depositId, refundAddress));
+ TempData[WellKnownTempData.SuccessMessage] = $"Refund successful: {resp.txId}";
}
catch (Exception e)
{
@@ -332,18 +318,6 @@ public class BreezController : Controller
{
return View(await _breezService.Get(storeId));
}
-
- private static async Task ReadAsByteArrayAsync( Stream source)
- {
- // Optimization
- if (source is MemoryStream memorySource)
- return memorySource.ToArray();
-
- using var memoryStream = new MemoryStream();
- await source.CopyToAsync(memoryStream);
- return memoryStream.ToArray();
- }
-
[HttpPost("configure")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task Configure(string storeId, string command, BreezSettings settings)
@@ -368,7 +342,6 @@ public class BreezController : Controller
if (command == "save")
{
-
try
{
if (string.IsNullOrEmpty(settings.Mnemonic))
@@ -376,48 +349,18 @@ public class BreezController : Controller
ModelState.AddModelError(nameof(settings.Mnemonic), "Mnemonic is required");
return View(settings);
}
- else
- {
- try
- {
- new Mnemonic(settings.Mnemonic);
- }
- catch (Exception e)
- {
- ModelState.AddModelError(nameof(settings.Mnemonic), "Invalid mnemonic");
- return View(settings);
- }
+
+ try
+ {
+ new Mnemonic(settings.Mnemonic);
+ }
+ catch (Exception e)
+ {
+ ModelState.AddModelError(nameof(settings.Mnemonic), "Invalid mnemonic");
+ return View(settings);
}
- if (settings.GreenlightCredentials is not null)
- {
- await using var stream = settings.GreenlightCredentials .OpenReadStream();
- using var archive = new ZipArchive(stream);
- var deviceClientArchiveEntry = archive.GetEntry("client.crt");
- var deviceKeyArchiveEntry = archive.GetEntry("client-key.pem");
- if(deviceClientArchiveEntry is null || deviceKeyArchiveEntry is null)
- {
- ModelState.AddModelError(nameof(settings.GreenlightCredentials), "Invalid zip file (does not have client.crt or client-key.pem)");
- return View(settings);
- }
- else
- {
- var deviceClient = await ReadAsByteArrayAsync(deviceClientArchiveEntry.Open());
- var deviceKey = await ReadAsByteArrayAsync(deviceKeyArchiveEntry.Open());
- var dir = _breezService.GetWorkDir(storeId);
- Directory.CreateDirectory(dir);
- await System.IO.File.WriteAllBytesAsync(Path.Combine(dir, "client.crt"), deviceClient);
- await System.IO.File.WriteAllBytesAsync(Path.Combine(dir, "client-key.pem"), deviceKey);
-
- await _breezService.Set(storeId, settings);
- }
-
- }
- else
- {
-
- await _breezService.Set(storeId, settings);
- }
+ await _breezService.Set(storeId, settings);
}
catch (Exception e)
{
@@ -427,7 +370,6 @@ public class BreezController : Controller
if(existing is null)
{
-
existing = new LightningPaymentMethodConfig();
var client = _breezService.GetClient(storeId);
existing.SetLightningUrl(client);
@@ -436,7 +378,7 @@ public class BreezController : Controller
store.SetPaymentMethodConfig(_paymentMethodHandlerDictionary[lnurlPMI], new LNURLPaymentMethodConfig());
await _storeRepository.UpdateStore(store);
}
-
+
TempData[WellKnownTempData.SuccessMessage] = "Settings saved successfully";
return RedirectToAction(nameof(Info), new {storeId});
}
diff --git a/Plugins/BTCPayServer.Plugins.Breez/BreezLightningClient.cs b/Plugins/BTCPayServer.Plugins.Breez/BreezLightningClient.cs
index b380444..b192101 100644
--- a/Plugins/BTCPayServer.Plugins.Breez/BreezLightningClient.cs
+++ b/Plugins/BTCPayServer.Plugins.Breez/BreezLightningClient.cs
@@ -1,18 +1,17 @@
-using System;
+using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Breez.Sdk;
+using Breez.Sdk.Spark;
using BTCPayServer.Lightning;
using NBitcoin;
-using Network = Breez.Sdk.Network;
+using Network = Breez.Sdk.Spark.Network;
namespace BTCPayServer.Plugins.Breez;
-public class BreezLightningClient : ILightningClient, IDisposable, EventListener
+public class BreezLightningClient : ILightningClient, IDisposable
{
public override string ToString()
{
@@ -22,136 +21,65 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
private readonly NBitcoin.Network _network;
public readonly string PaymentKey;
- public ConcurrentQueue<(DateTimeOffset timestamp, string log)> Events { get; set; } = new();
+ public ConcurrentQueue<(DateTimeOffset timestamp, string log)> Events { get; set} = new();
- public BreezLightningClient(string inviteCode, string apiKey, string workingDir, NBitcoin.Network network,
+ private BreezSdk _sdk;
+
+ public static async Task Create(string apiKey, string workingDir, NBitcoin.Network network,
Mnemonic mnemonic, string paymentKey)
{
- apiKey??= "99010c6f84541bf582899db6728f6098ba98ca95ea569f4c63f2c2c9205ace57";
+ apiKey ??= "99010c6f84541bf582899db6728f6098ba98ca95ea569f4c63f2c2c9205ace57";
+
+ var config = BreezSdkSparkMethods.DefaultConfig(
+ network == NBitcoin.Network.Main ? Network.Mainnet :
+ network == NBitcoin.Network.TestNet ? Network.Testnet :
+ network == NBitcoin.Network.RegTest ? Network.Regtest : Network.Signet
+ ) 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 BreezLightningClient(sdk, network, paymentKey);
+ }
+
+ private BreezLightningClient(BreezSdk sdk, NBitcoin.Network network, string paymentKey)
+ {
+ _sdk = sdk;
_network = network;
PaymentKey = paymentKey;
- GreenlightCredentials glCreds = null;
- if (File.Exists(Path.Combine(workingDir, "client.crt")) && File.Exists(Path.Combine(workingDir, "client-key.pem")))
- {
- var deviceCert = File.ReadAllBytes(Path.Combine(workingDir, "client.crt"));
- var deviceKey = File.ReadAllBytes(Path.Combine(workingDir, "client-key.pem"));
-
- glCreds = new GreenlightCredentials(deviceKey.ToList(), deviceCert.ToList());
- }
- var nodeConfig = new NodeConfig.Greenlight(
- new GreenlightNodeConfig(glCreds, inviteCode)
- );
- var config = BreezSdkMethods.DefaultConfig(
- network == NBitcoin.Network.Main ? EnvironmentType.Production : EnvironmentType.Staging,
- apiKey,
- nodeConfig
- ) with
- {
- workingDir = workingDir,
- network = network == NBitcoin.Network.Main ? Network.Bitcoin :
- network == NBitcoin.Network.TestNet ? Network.Testnet :
- network == NBitcoin.Network.RegTest ? Network.Regtest : Network.Signet
- };
- var seed = mnemonic.DeriveSeed();
- Sdk = BreezSdkMethods.Connect(new ConnectRequest(config, seed.ToList()), this);
}
- public BlockingBreezServices Sdk { get; }
+ public BreezSdk Sdk => _sdk;
- public event EventHandler EventReceived;
-
- public void OnEvent(BreezEvent e)
+ public async Task GetInvoice(string invoiceId, CancellationToken cancellation = default)
{
- var msg = e switch
- {
- BreezEvent.BackupFailed backupFailed => $"{e.GetType().Name}: {backupFailed.details.error}",
- BreezEvent.InvoicePaid invoicePaid => $"{e.GetType().Name}: {invoicePaid.details.paymentHash}",
- BreezEvent.PaymentFailed paymentFailed => $"{e.GetType().Name}: {paymentFailed.details.error} {paymentFailed.details.invoice?.paymentHash}",
- BreezEvent.PaymentSucceed paymentSucceed => $"{e.GetType().Name}: {paymentSucceed.details.id}",
- BreezEvent.SwapUpdated swapUpdated => $"{e.GetType().Name}: {swapUpdated.details.status} {ConvertHelper.ToHexString(swapUpdated.details.paymentHash.ToArray())} {swapUpdated.details.bitcoinAddress}",
- _ => e.GetType().Name
- };
-
- Events.Enqueue((DateTimeOffset.Now, msg));
- if(Events.Count > 100)
- Events.TryDequeue(out _);
- EventReceived?.Invoke(this, e);
- }
-
- public Task GetInvoice(string invoiceId, CancellationToken cancellation = default)
- {
- return GetInvoice(uint256.Parse(invoiceId), cancellation);
- }
-
- private LightningPayment ToLightningPayment(Payment payment)
- {
- if (payment?.details is not PaymentDetails.Ln lnPaymentDetails)
- {
- return null;
- }
-
- return new LightningPayment()
- {
- Amount = LightMoney.MilliSatoshis(payment.amountMsat),
- Id = lnPaymentDetails.data.paymentHash,
- Preimage = lnPaymentDetails.data.paymentPreimage,
- PaymentHash = lnPaymentDetails.data.paymentHash,
- BOLT11 = lnPaymentDetails.data.bolt11,
- Status = payment.status switch
- {
- PaymentStatus.Failed => LightningPaymentStatus.Failed,
- PaymentStatus.Complete => LightningPaymentStatus.Complete,
- PaymentStatus.Pending => LightningPaymentStatus.Pending,
- _ => throw new ArgumentOutOfRangeException()
- },
- CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(payment.paymentTime),
- Fee = LightMoney.MilliSatoshis(payment.feeMsat),
- AmountSent = LightMoney.MilliSatoshis(payment.amountMsat)
- };
- }
-
- private LightningInvoice FromPayment(Payment p)
- {
-
- if (p?.details is not PaymentDetails.Ln lnPaymentDetails)
- {
- return null;
- }
-
- var bolt11 = BOLT11PaymentRequest.Parse(lnPaymentDetails.data.bolt11, _network);
-
- return new LightningInvoice()
- {
- Amount = LightMoney.MilliSatoshis(p.amountMsat + p.feeMsat),
- Id = lnPaymentDetails.data.paymentHash,
- Preimage = lnPaymentDetails.data.paymentPreimage,
- PaymentHash = lnPaymentDetails.data.paymentHash,
- BOLT11 = lnPaymentDetails.data.bolt11,
- Status = p.status switch
- {
- PaymentStatus.Pending => LightningInvoiceStatus.Unpaid,
- PaymentStatus.Failed => LightningInvoiceStatus.Expired,
- PaymentStatus.Complete => LightningInvoiceStatus.Paid,
- _ => LightningInvoiceStatus.Unpaid
- },
- PaidAt = DateTimeOffset.FromUnixTimeSeconds(p.paymentTime),
- ExpiresAt = bolt11.ExpiryDate
- };
+ return await GetInvoice(uint256.Parse(invoiceId), cancellation);
}
public async Task GetInvoice(uint256 paymentHash, CancellationToken cancellation = default)
{
- var p = Sdk.PaymentByHash(paymentHash.ToString()!);
-
- if(p is null)
- return new LightningInvoice()
+ try
+ {
+ var response = await _sdk.GetPayment(new GetPaymentRequest(paymentHash.ToString()));
+ if (response?.payment != null)
{
- Id = paymentHash.ToString(),
- PaymentHash = paymentHash.ToString(),
- Status = LightningInvoiceStatus.Unpaid
- };
-
- return FromPayment(p);
+ return FromPayment(response.payment);
+ }
+ }
+ catch
+ {
+ // Payment not found
+ }
+
+ return new LightningInvoice()
+ {
+ Id = paymentHash.ToString(),
+ PaymentHash = paymentHash.ToString(),
+ Status = LightningInvoiceStatus.Unpaid
+ };
}
public async Task ListInvoices(CancellationToken cancellation = default)
@@ -162,14 +90,32 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
public async Task ListInvoices(ListInvoicesParams request,
CancellationToken cancellation = default)
{
- return Sdk.ListPayments(new ListPaymentsRequest(new List(){PaymentTypeFilter.Received}, null, null,
- null, request?.PendingOnly is not true, (uint?) request?.OffsetIndex, null))
- .Select(FromPayment).ToArray();
+ 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: (ulong?)request?.OffsetIndex,
+ 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)
{
- return ToLightningPayment(Sdk.PaymentByHash(paymentHash));
+ try
+ {
+ var response = await _sdk.GetPayment(new GetPaymentRequest(paymentHash));
+ return ToLightningPayment(response?.payment);
+ }
+ catch
+ {
+ return null;
+ }
}
public async Task ListPayments(CancellationToken cancellation = default)
@@ -180,46 +126,45 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
public async Task ListPayments(ListPaymentsParams request,
CancellationToken cancellation = default)
{
- return Sdk.ListPayments(new ListPaymentsRequest(new List(){PaymentTypeFilter.Received}, null, null, null,
- null, (uint?) request?.OffsetIndex, null))
- .Select(ToLightningPayment).ToArray();
- }
+ var req = new ListPaymentsRequest(
+ typeFilter: new List { PaymentType.Send },
+ statusFilter: null,
+ assetFilter: new AssetFilter.Bitcoin(),
+ fromTimestamp: null,
+ toTimestamp: null,
+ offset: (ulong?)request?.OffsetIndex,
+ 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 expiryS = expiry == TimeSpan.Zero ? (uint?) null : Math.Max(0, (uint) expiry.TotalSeconds);
- description??= "Invoice";
- var p = Sdk.ReceivePayment(new ReceivePaymentRequest((ulong) amount.MilliSatoshi, description, null, null,
- false, expiryS));
- return FromPR(p);
- }
+ var expiryS = expiry == TimeSpan.Zero ? (ulong?)null : Math.Max(0, (ulong)expiry.TotalSeconds);
+ description ??= "Invoice";
- public LightningInvoice FromPR(ReceivePaymentResponse response)
- {
- return new LightningInvoice()
- {
- Amount = LightMoney.MilliSatoshis(response.lnInvoice.amountMsat ?? 0),
- Id = response.lnInvoice.paymentHash,
- Preimage = ConvertHelper.ToHexString(response.lnInvoice.paymentSecret.ToArray()),
- PaymentHash = response.lnInvoice.paymentHash,
- BOLT11 = response.lnInvoice.bolt11,
- Status = LightningInvoiceStatus.Unpaid,
- ExpiresAt = DateTimeOffset.FromUnixTimeSeconds((long) response.lnInvoice.expiry)
- };
+ var paymentMethod = new ReceivePaymentMethod.Bolt11Invoice(description, (ulong)amount.ToUnit(LightMoneyUnit.Satoshi));
+ var response = await _sdk.ReceivePayment(new ReceivePaymentRequest(paymentMethod));
+
+ return FromReceivePaymentResponse(response);
}
public async Task CreateInvoice(CreateInvoiceParams createInvoiceRequest,
CancellationToken cancellation = default)
{
var expiryS = createInvoiceRequest.Expiry == TimeSpan.Zero
- ? (uint?) null
- : Math.Max(0, (uint) createInvoiceRequest.Expiry.TotalSeconds);
- var p = Sdk.ReceivePayment(new ReceivePaymentRequest((ulong) createInvoiceRequest.Amount.MilliSatoshi,
- (createInvoiceRequest.Description ?? createInvoiceRequest.DescriptionHash.ToString())!, null, null,
- createInvoiceRequest.DescriptionHashOnly, expiryS));
- return FromPR(p);
+ ? (ulong?)null
+ : Math.Max(0, (ulong)createInvoiceRequest.Expiry.TotalSeconds);
+
+ var description = createInvoiceRequest.Description ?? createInvoiceRequest.DescriptionHash?.ToString() ?? "Invoice";
+ var paymentMethod = new ReceivePaymentMethod.Bolt11Invoice(description, (ulong)createInvoiceRequest.Amount.ToUnit(LightMoneyUnit.Satoshi));
+ var response = await _sdk.ReceivePayment(new ReceivePaymentRequest(paymentMethod));
+
+ return FromReceivePaymentResponse(response);
}
public async Task Listen(CancellationToken cancellation = default)
@@ -229,30 +174,29 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
public async Task GetInfo(CancellationToken cancellation = default)
{
- var ni = Sdk.NodeInfo();
+ var response = await _sdk.GetInfo(new GetInfoRequest(ensureSynced: false));
+
return new LightningNodeInformation()
{
- PeersCount = ni.connectedPeers.Count,
- Alias = $"greenlight {ni.id}",
- BlockHeight = (int) ni.blockHeight
+ Alias = "Breez Spark (nodeless)",
+ BlockHeight = 0
};
}
public async Task GetBalance(CancellationToken cancellation = default)
{
- var ni = Sdk.NodeInfo();
+ var response = await _sdk.GetInfo(new GetInfoRequest(ensureSynced: false));
+
return new LightningNodeBalance()
{
- OnchainBalance =
- new OnchainBalance()
- {
- Confirmed = Money.Coins(LightMoney.MilliSatoshis(ni.onchainBalanceMsat)
- .ToUnit(LightMoneyUnit.BTC))
- },
+ OnchainBalance = new OnchainBalance()
+ {
+ Confirmed = Money.Zero
+ },
OffchainBalance = new OffchainBalance()
{
- Local = LightMoney.MilliSatoshis(ni.channelsBalanceMsat),
- Remote = LightMoney.MilliSatoshis(ni.totalInboundLiquidityMsats),
+ Local = LightMoney.Satoshis((long)response.balanceSats),
+ Remote = LightMoney.Zero
}
};
}
@@ -265,43 +209,44 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
public async Task Pay(string bolt11, PayInvoiceParams payParams,
CancellationToken cancellation = default)
{
- SendPaymentResponse result;
try
{
- if (bolt11 is null)
+ if (string.IsNullOrEmpty(bolt11))
{
- result = Sdk.SendSpontaneousPayment(new SendSpontaneousPaymentRequest(payParams.Destination.ToString(),
- (ulong) payParams.Amount.MilliSatoshi));
- }
- else
- {
- result = Sdk.SendPayment(new SendPaymentRequest(bolt11,false, (ulong?) payParams.Amount?.MilliSatoshi));
+ return new PayResponse(PayResult.Error, "BOLT11 invoice required");
}
- var details = result.payment.details as PaymentDetails.Ln;
+ var prepareRequest = new PrepareSendPaymentRequest(
+ new SendPaymentDestination.Bolt11Invoice(bolt11, null, false)
+ );
+ var prepareResponse = await _sdk.PrepareSendPayment(prepareRequest);
+
+ var sendRequest = new SendPaymentRequest(
+ prepareResponse,
+ new SendPaymentOptions.Bolt11Invoice(preferSpark: false, completionTimeoutSecs: 30)
+ );
+ var sendResponse = await _sdk.SendPayment(sendRequest);
+
return new PayResponse()
{
- Result = result.payment.status switch
+ Result = sendResponse.payment.status switch
{
PaymentStatus.Failed => PayResult.Error,
- PaymentStatus.Complete => PayResult.Ok,
+ PaymentStatus.Completed => PayResult.Ok,
PaymentStatus.Pending => PayResult.Unknown,
- _ => throw new ArgumentOutOfRangeException()
+ _ => PayResult.Error
},
Details = new PayDetails()
{
- Status = result.payment.status switch
+ Status = sendResponse.payment.status switch
{
PaymentStatus.Failed => LightningPaymentStatus.Failed,
- PaymentStatus.Complete => LightningPaymentStatus.Complete,
+ PaymentStatus.Completed => LightningPaymentStatus.Complete,
PaymentStatus.Pending => LightningPaymentStatus.Pending,
_ => LightningPaymentStatus.Unknown
},
- Preimage =
- details.data.paymentPreimage is null ? null : uint256.Parse(details.data.paymentPreimage),
- PaymentHash = details.data.paymentHash is null ? null : uint256.Parse(details.data.paymentHash),
- FeeAmount = result.payment.feeMsat,
- TotalAmount = LightMoney.MilliSatoshis(result.payment.amountMsat + result.payment.feeMsat),
+ TotalAmount = LightMoney.Satoshis((long)sendResponse.payment.amount),
+ FeeAmount = (long)sendResponse.payment.fees
}
};
}
@@ -342,44 +287,105 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
throw new NotImplementedException();
}
+ private LightningInvoice FromReceivePaymentResponse(ReceivePaymentResponse response)
+ {
+ return new LightningInvoice()
+ {
+ BOLT11 = response.paymentRequest,
+ Status = LightningInvoiceStatus.Unpaid,
+ Amount = LightMoney.Satoshis((long)response.fee)
+ };
+ }
+
+ private LightningInvoice FromPayment(Payment payment)
+ {
+ if (payment == null) return null;
+
+ string paymentHash = null;
+ string bolt11 = null;
+
+ if (payment.details is PaymentDetails.Lightning lightningDetails)
+ {
+ paymentHash = lightningDetails.paymentHash;
+ bolt11 = lightningDetails.invoice;
+ }
+
+ return new LightningInvoice()
+ {
+ Id = payment.id,
+ PaymentHash = paymentHash ?? payment.id,
+ BOLT11 = bolt11,
+ Amount = LightMoney.Satoshis((long)payment.amount),
+ 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 = null;
+ string preimage = null;
+ string bolt11 = null;
+
+ if (payment.details is PaymentDetails.Lightning lightningDetails)
+ {
+ paymentHash = lightningDetails.paymentHash;
+ preimage = lightningDetails.preimage;
+ bolt11 = lightningDetails.invoice;
+ }
+
+ return new LightningPayment()
+ {
+ Id = payment.id,
+ PaymentHash = paymentHash ?? payment.id,
+ Preimage = preimage,
+ BOLT11 = bolt11,
+ Amount = LightMoney.Satoshis((long)payment.amount),
+ 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 = LightMoney.Satoshis((long)payment.fees),
+ AmountSent = LightMoney.Satoshis((long)payment.amount)
+ };
+ }
+
public void Dispose()
{
- Sdk.Dispose();
- Sdk.Dispose();
+ _sdk?.Dispose();
}
public class BreezInvoiceListener : ILightningInvoiceListener
{
private readonly BreezLightningClient _breezLightningClient;
private readonly CancellationToken _cancellationToken;
+ private readonly ConcurrentQueue _invoices = new();
public BreezInvoiceListener(BreezLightningClient breezLightningClient, CancellationToken cancellationToken)
{
_breezLightningClient = breezLightningClient;
_cancellationToken = cancellationToken;
-
- breezLightningClient.EventReceived += BreezLightningClientOnEventReceived;
- }
-
- private readonly ConcurrentQueue _invoices = new();
-
- private void BreezLightningClientOnEventReceived(object sender, BreezEvent e)
- {
- if (e is BreezEvent.InvoicePaid pre && pre.details.payment is {})
- {
-
- _invoices.Enqueue(pre.details.payment);
- }
}
public void Dispose()
{
- _breezLightningClient.EventReceived -= BreezLightningClientOnEventReceived;
}
public async Task WaitInvoice(CancellationToken cancellation)
{
- while (cancellation.IsCancellationRequested is not true)
+ while (!cancellation.IsCancellationRequested)
{
if (_invoices.TryDequeue(out var payment))
{
@@ -393,4 +399,4 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
return null;
}
}
-}
\ No newline at end of file
+}
diff --git a/Plugins/BTCPayServer.Plugins.Breez/BreezService.cs b/Plugins/BTCPayServer.Plugins.Breez/BreezService.cs
index c89e139..83680c1 100644
--- a/Plugins/BTCPayServer.Plugins.Breez/BreezService.cs
+++ b/Plugins/BTCPayServer.Plugins.Breez/BreezService.cs
@@ -92,7 +92,7 @@ public class BreezService:EventHostedServiceBase
return settings;
}
- public async Task Handle(string? storeId, BreezSettings? settings)
+ public async Task Handle(string? storeId, BreezSettings? settings)
{
if (settings is null)
{
@@ -105,16 +105,24 @@ public class BreezService:EventHostedServiceBase
{
try
{
- var network = Network.Main; // _btcPayNetworkProvider.BTC.NBitcoinNetwork;
+ var network = Network.Main;
var dir = GetWorkDir(storeId);
Directory.CreateDirectory(dir);
settings.PaymentKey ??= Guid.NewGuid().ToString();
- var client = new BreezLightningClient(settings.InviteCode, settings.ApiKey, dir,
- network, new Mnemonic(settings.Mnemonic), settings.PaymentKey);
+
+ var client = await BreezLightningClient.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)
diff --git a/Plugins/BTCPayServer.Plugins.Breez/BreezSettings.cs b/Plugins/BTCPayServer.Plugins.Breez/BreezSettings.cs
index 737b4f9..3655478 100644
--- a/Plugins/BTCPayServer.Plugins.Breez/BreezSettings.cs
+++ b/Plugins/BTCPayServer.Plugins.Breez/BreezSettings.cs
@@ -7,14 +7,8 @@ namespace BTCPayServer.Plugins.Breez;
public class BreezSettings
{
- public string? InviteCode { get; set; }
public string? Mnemonic { get; set; }
public string? ApiKey { get; set; }
public string PaymentKey { get; set; } = Guid.NewGuid().ToString();
-
-
-
- [JsonIgnore]
- public IFormFile? GreenlightCredentials { get; set; }
}
\ No newline at end of file
diff --git a/Plugins/BTCPayServer.Plugins.Breez/README.md b/Plugins/BTCPayServer.Plugins.Breez/README.md
index c354859..f6d656c 100644
--- a/Plugins/BTCPayServer.Plugins.Breez/README.md
+++ b/Plugins/BTCPayServer.Plugins.Breez/README.md
@@ -1,32 +1,90 @@
-# Breez lightning support plugin
+# Breez Spark Lightning Plugin
## BETA RELEASE
-Allows you to enable lightning on your stores using [Breez SDK](https://breez.technology/sdk/), powered by [Blockstream Greenlight](https://blockstream.com/lightning/greenlight/).
+Allows you to enable lightning on your stores using [Breez Spark SDK](https://github.com/breez/spark-sdk), a **nodeless** Lightning protocol.
-Breez SDK and Greenlight enables you to have a non-custodial lightning experience, without hosting any of the infrastructure yourself.
+Breez Spark SDK enables you to have a self-custodial Lightning experience without hosting any infrastructure yourself or managing channels.
-Additionally, Breez SDK comes with built-in liquidity and channel automation, reducing the complexity of managing your lightning node.
+If you have used other wallets that use Breez SDK, you may be able to import the same mnemonic directly into BTCPay Server.
-If you have used any other wallet that uses Breez SDK, you can import it directly into BTCPay Server and continue using it in parallel.
+## What is Spark SDK?
+
+Spark SDK is Breez's "nodeless" Layer 2 protocol that provides Lightning functionality without running a full Lightning node. Unlike the previous Greenlight-based implementation which required Blockstream's hosting service, Spark operates through a decentralized network of operators.
+
+### Key Differences from Greenlight:
+
+| Feature | Spark (Nodeless) | Greenlight (Old) |
+|---------|------------------|------------------|
+| **Infrastructure** | No node hosting required | Hosted by Blockstream |
+| **Setup** | Mnemonic + API key only | Required certificates + invite code |
+| **Architecture** | Nodeless protocol | Full Lightning node |
+| **Channel Management** | Automatic via deposits | Manual swap-in/swap-out |
+| **Onchain Operations** | Deposit claims only | Full swap functionality |
## Usage
-1. Install the plugin from the BTCPay Server > Settings > Plugin > Available Plugins, and restart
+1. Install the plugin from BTCPay Server > Settings > Plugin > Available Plugins, and restart
2. In your store > Wallets > Lightning, Configure Breez
3. You will be on a page asking for:
- - Mnemonic: This is your 12 word seed phrase. YOU SHOULD GENERATE THIS FROM A SEGWIT OR LEGACY SEGWIT WALLET AND KEEP IT SAFE. If you have used Breez before, you can use the same seed phrase you used in Breez. You can also use the seed phrase from your BTCPay hot wallet. This SEED PHRASE will be stored on BTCPAY SERVER. IF YOU USE A SHARED BTCPAY SERVER, YOU ARE EXPOSING YOUR SEED PHRASE TO THE SERVER ADMINISTRATOR. Type it in 'word word word..." format.
- - Greenlight credentials: In the case of a new seed, you'll need to acquire certificates for issuing new nodes from Blockstream. You can get these for free at https://greenlight.blockstream.com. Select the entire `gl-certs.zip` file provided by Greenlight.
- - Invite Code: Alternatively, you may have an invite code which can be used instead of the Greenlight credentials.
+ - **Mnemonic**: This is your 12 word seed phrase. **GENERATE THIS FROM A SECURE WALLET AND KEEP IT SAFE**. This seed phrase will be stored on BTCPay Server. **IF YOU USE A SHARED BTCPAY SERVER, YOU ARE EXPOSING YOUR SEED PHRASE TO THE SERVER ADMINISTRATOR**. Type it in 'word word word...' format.
+ - **API Key** (optional): Breez API key. If not provided, a default key will be used.
4. Click Save
-5. Your new lightning node will be created.
-6. Your first lightning invoice will have a relatively high minimum amount limit. This is because Breez SDK requires a minimum amount to be able to open a channel. A note on Inbound Liquidity - Breez appears to provide a low inbound liquidity at first and as you use it, it grants more inbound liquidity gradaully which puts you at risk of receiving partial paid invoices. But if you want instant channel inbound liquidity, use the Swap In feature to bring in Bitcoin, then create a lightning invoice from an outside lightning wallet such as Breez or Phoenix wallet, and pay it via the Payments tab in Breez. This lubricates the channels and your inbound liquidity will increase by the amount of your paid outbound invoice. You pay the channel open fee, instead of your customers. The amount you set your inbound liquidity is up to you so make it high enough that you don't have to regularly increase channel size which costs you sats down the road. For example, if you receive ten payments of 50,000 sats a month (500,000 sats), to start with 1.5 to 2 million sats channel at the beginning.
-7. You can now use your lightning node to receive payments.
+5. Your Spark wallet will be initialized
+6. You can now use Lightning to receive and send payments
+## Important Notes
-NOTE: In the future, Blockstream Greenlight will offer a way to generate read-only access keys for your already issued node, so that you can use these instead of exposing your mnemonic phrase to BTCPay Server, allowing a lightweight, non-custodial lightning experience, even on shared BTCPay Server instances.
+### Security Warning
+⚠️ **Your mnemonic seed phrase is stored on the BTCPay Server**. On shared/hosted BTCPay instances, the server administrator can access your funds. Only use this plugin on BTCPay servers you fully trust or self-host.
-## Additional features
+### Liquidity Management
-* Swap-in: Send and convert onchain funds to your Breez lightning nodes.
-* Swap-out: Send and convert lightning funds to your onchain wallet.
+Spark SDK handles liquidity differently than traditional Lightning:
+
+- **Deposits**: Send Bitcoin to generated deposit addresses. These are automatically claimed and converted to Lightning liquidity.
+- **No Swap-Out**: Unlike Greenlight, Spark SDK does not support converting Lightning funds back to onchain. You can only withdraw via Lightning.
+- **Automatic Channel Management**: The Spark network handles all channel operations automatically.
+
+### First Invoice Considerations
+
+Your first Lightning invoices may have higher minimum amounts because the system needs to establish initial liquidity through deposits.
+
+## Additional Features
+
+### Available Operations:
+* **Claim Deposits**: Automatically claim onchain deposits and convert them to Lightning liquidity
+* **Refund Deposits**: Refund unclaimed deposits back to an onchain address
+* **Lightning Payments**: Send and receive Lightning payments (BOLT11)
+* **Payment History**: View all Lightning and deposit transactions
+
+### Not Available (compared to Greenlight):
+* ❌ Swap-Out (Lightning → Onchain conversion)
+* ❌ Manual channel management
+* ❌ Node ID / pubkey (this is a nodeless protocol)
+
+## Technical Details
+
+This plugin uses:
+- **Breez Spark SDK** C# bindings (built from source)
+- Async/await pattern throughout
+- Nodeless architecture - no Lightning node infrastructure required
+
+## Building From Source
+
+The Spark SDK C# bindings must be built from the [spark-sdk repository](https://github.com/breez/spark-sdk):
+
+```bash
+cd spark-sdk/crates/breez-sdk/bindings
+make build-release
+make bindings-csharp
+cd langs/csharp/src
+dotnet pack -c Release
+```
+
+## Support
+
+For issues with:
+- **The plugin**: Open an issue on this repository
+- **Spark SDK**: Visit [Breez Spark SDK](https://github.com/breez/spark-sdk)
+- **BTCPay Server**: Visit [BTCPay Server](https://github.com/btcpayserver/btcpayserver)