mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-16 23:24:25 +01:00
Merge pull request #1 from aljazceru/claude/replace-breez-plugin-spark-sdk-011CV63ezSWMkG3iXjQYx6AH
Claude/replace breez plugin spark sdk
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@
|
||||
Plugins/packed
|
||||
.vs/
|
||||
/BTCPayServerPlugins.sln.DotSettings.user
|
||||
local-packages/*.nupkg
|
||||
|
||||
7
NuGet.Config
Normal file
7
NuGet.Config
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
<add key="local" value="./local-packages" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@@ -34,7 +34,7 @@
|
||||
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Breez.Sdk" Version="0.8.1-rc3" />
|
||||
<PackageReference Include="Breez.Sdk.Spark" Version="0.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -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<IActionResult> Sweep(string storeId, string address, uint satPerByte)
|
||||
public async Task<IActionResult> 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<IActionResult> SwapInRefund(string storeId, string address, string refundAddress, uint satPerByte)
|
||||
public async Task<IActionResult> 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<byte[]> 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<IActionResult> 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});
|
||||
}
|
||||
|
||||
@@ -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<BreezLightningClient> 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<BreezEvent> EventReceived;
|
||||
|
||||
public void OnEvent(BreezEvent e)
|
||||
public async Task<LightningInvoice> 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<LightningInvoice> 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<LightningInvoice> 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<LightningInvoice[]> ListInvoices(CancellationToken cancellation = default)
|
||||
@@ -162,14 +90,32 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
|
||||
public async Task<LightningInvoice[]> ListInvoices(ListInvoicesParams request,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
return Sdk.ListPayments(new ListPaymentsRequest(new List<PaymentTypeFilter>(){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> { PaymentType.Receive },
|
||||
statusFilter: request?.PendingOnly == true ? new List<PaymentStatus> { 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<LightningPayment> 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<LightningPayment[]> ListPayments(CancellationToken cancellation = default)
|
||||
@@ -180,46 +126,45 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
|
||||
public async Task<LightningPayment[]> ListPayments(ListPaymentsParams request,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
return Sdk.ListPayments(new ListPaymentsRequest(new List<PaymentTypeFilter>(){PaymentTypeFilter.Received}, null, null, null,
|
||||
null, (uint?) request?.OffsetIndex, null))
|
||||
.Select(ToLightningPayment).ToArray();
|
||||
}
|
||||
var req = new ListPaymentsRequest(
|
||||
typeFilter: new List<PaymentType> { 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<LightningInvoice> 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<LightningInvoice> 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<ILightningInvoiceListener> Listen(CancellationToken cancellation = default)
|
||||
@@ -229,30 +174,29 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
|
||||
|
||||
public async Task<LightningNodeInformation> 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<LightningNodeBalance> 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<PayResponse> 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<Payment> _invoices = new();
|
||||
|
||||
public BreezInvoiceListener(BreezLightningClient breezLightningClient, CancellationToken cancellationToken)
|
||||
{
|
||||
_breezLightningClient = breezLightningClient;
|
||||
_cancellationToken = cancellationToken;
|
||||
|
||||
breezLightningClient.EventReceived += BreezLightningClientOnEventReceived;
|
||||
}
|
||||
|
||||
private readonly ConcurrentQueue<Payment> _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<LightningInvoice> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ public class BreezService:EventHostedServiceBase
|
||||
return settings;
|
||||
}
|
||||
|
||||
public async Task<BreezLightningClient?> Handle(string? storeId, BreezSettings? settings)
|
||||
public async Task<BreezLightningClient?> 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)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user