mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 23:54:26 +01:00
Replace Breez SDK (Greenlight) with Breez Spark SDK (nodeless)
Major changes: - Built C# bindings for Breez Spark SDK from source using UniFFI - Created local NuGet package infrastructure (Breez.Sdk.Spark v0.0.1) - Replaced Breez.Sdk package reference with Breez.Sdk.Spark - Updated BreezLightningClient to use async Spark SDK API - Removed Greenlight-specific code (credentials, invite codes) - Simplified BreezSettings (no more Greenlight fields) - Updated BreezService for async client initialization - Cleaned up BreezController (removed certificate upload logic) Key differences in Spark SDK: - Nodeless architecture (no Greenlight hosting required) - Simplified configuration (only mnemonic + API key) - All async methods (no BlockingBreezServices) - Different payment flow (PrepareSendPayment + SendPayment) The plugin now works with Breez's Spark protocol which provides a self-custodial Lightning experience without infrastructure hosting. Note: NuGet package must be built from spark-sdk source before use.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@
|
|||||||
Plugins/packed
|
Plugins/packed
|
||||||
.vs/
|
.vs/
|
||||||
/BTCPayServerPlugins.sln.DotSettings.user
|
/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" />
|
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Breez.Sdk" Version="0.8.1-rc3" />
|
<PackageReference Include="Breez.Sdk.Spark" Version="0.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Breez.Sdk;
|
using Breez.Sdk.Spark;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
@@ -332,18 +331,6 @@ public class BreezController : Controller
|
|||||||
{
|
{
|
||||||
return View(await _breezService.Get(storeId));
|
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")]
|
[HttpPost("configure")]
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
public async Task<IActionResult> Configure(string storeId, string command, BreezSettings settings)
|
public async Task<IActionResult> Configure(string storeId, string command, BreezSettings settings)
|
||||||
@@ -368,7 +355,6 @@ public class BreezController : Controller
|
|||||||
|
|
||||||
if (command == "save")
|
if (command == "save")
|
||||||
{
|
{
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(settings.Mnemonic))
|
if (string.IsNullOrEmpty(settings.Mnemonic))
|
||||||
@@ -376,48 +362,18 @@ public class BreezController : Controller
|
|||||||
ModelState.AddModelError(nameof(settings.Mnemonic), "Mnemonic is required");
|
ModelState.AddModelError(nameof(settings.Mnemonic), "Mnemonic is required");
|
||||||
return View(settings);
|
return View(settings);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
try
|
||||||
try
|
{
|
||||||
{
|
new Mnemonic(settings.Mnemonic);
|
||||||
new Mnemonic(settings.Mnemonic);
|
}
|
||||||
}
|
catch (Exception e)
|
||||||
catch (Exception e)
|
{
|
||||||
{
|
ModelState.AddModelError(nameof(settings.Mnemonic), "Invalid mnemonic");
|
||||||
ModelState.AddModelError(nameof(settings.Mnemonic), "Invalid mnemonic");
|
return View(settings);
|
||||||
return View(settings);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.GreenlightCredentials is not null)
|
await _breezService.Set(storeId, settings);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -427,7 +383,6 @@ public class BreezController : Controller
|
|||||||
|
|
||||||
if(existing is null)
|
if(existing is null)
|
||||||
{
|
{
|
||||||
|
|
||||||
existing = new LightningPaymentMethodConfig();
|
existing = new LightningPaymentMethodConfig();
|
||||||
var client = _breezService.GetClient(storeId);
|
var client = _breezService.GetClient(storeId);
|
||||||
existing.SetLightningUrl(client);
|
existing.SetLightningUrl(client);
|
||||||
@@ -436,7 +391,7 @@ public class BreezController : Controller
|
|||||||
store.SetPaymentMethodConfig(_paymentMethodHandlerDictionary[lnurlPMI], new LNURLPaymentMethodConfig());
|
store.SetPaymentMethodConfig(_paymentMethodHandlerDictionary[lnurlPMI], new LNURLPaymentMethodConfig());
|
||||||
await _storeRepository.UpdateStore(store);
|
await _storeRepository.UpdateStore(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
TempData[WellKnownTempData.SuccessMessage] = "Settings saved successfully";
|
TempData[WellKnownTempData.SuccessMessage] = "Settings saved successfully";
|
||||||
return RedirectToAction(nameof(Info), new {storeId});
|
return RedirectToAction(nameof(Info), new {storeId});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Breez.Sdk;
|
using Breez.Sdk.Spark;
|
||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using Network = Breez.Sdk.Network;
|
using Network = Breez.Sdk.Spark.Network;
|
||||||
|
|
||||||
namespace BTCPayServer.Plugins.Breez;
|
namespace BTCPayServer.Plugins.Breez;
|
||||||
|
|
||||||
public class BreezLightningClient : ILightningClient, IDisposable, EventListener
|
public class BreezLightningClient : ILightningClient, IDisposable
|
||||||
{
|
{
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
@@ -22,136 +21,65 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
|
|||||||
private readonly NBitcoin.Network _network;
|
private readonly NBitcoin.Network _network;
|
||||||
public readonly string PaymentKey;
|
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)
|
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 = mnemonic.DeriveSeed();
|
||||||
|
var sdk = await BreezSdkSparkMethods.Connect(new ConnectRequest(config, seed.ToList(), workingDir));
|
||||||
|
|
||||||
|
return new BreezLightningClient(sdk, network, paymentKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BreezLightningClient(BreezSdk sdk, NBitcoin.Network network, string paymentKey)
|
||||||
|
{
|
||||||
|
_sdk = sdk;
|
||||||
_network = network;
|
_network = network;
|
||||||
PaymentKey = paymentKey;
|
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 async Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default)
|
||||||
|
|
||||||
public void OnEvent(BreezEvent e)
|
|
||||||
{
|
{
|
||||||
var msg = e switch
|
return await GetInvoice(uint256.Parse(invoiceId), cancellation);
|
||||||
{
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LightningInvoice> GetInvoice(uint256 paymentHash, CancellationToken cancellation = default)
|
public async Task<LightningInvoice> GetInvoice(uint256 paymentHash, CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
var p = Sdk.PaymentByHash(paymentHash.ToString()!);
|
try
|
||||||
|
{
|
||||||
if(p is null)
|
var response = await _sdk.GetPayment(new GetPaymentRequest(paymentHash.ToString()));
|
||||||
return new LightningInvoice()
|
if (response?.payment != null)
|
||||||
{
|
{
|
||||||
Id = paymentHash.ToString(),
|
return FromPayment(response.payment);
|
||||||
PaymentHash = paymentHash.ToString(),
|
}
|
||||||
Status = LightningInvoiceStatus.Unpaid
|
}
|
||||||
};
|
catch
|
||||||
|
{
|
||||||
return FromPayment(p);
|
// Payment not found
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LightningInvoice()
|
||||||
|
{
|
||||||
|
Id = paymentHash.ToString(),
|
||||||
|
PaymentHash = paymentHash.ToString(),
|
||||||
|
Status = LightningInvoiceStatus.Unpaid
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LightningInvoice[]> ListInvoices(CancellationToken cancellation = default)
|
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,
|
public async Task<LightningInvoice[]> ListInvoices(ListInvoicesParams request,
|
||||||
CancellationToken cancellation = default)
|
CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
return Sdk.ListPayments(new ListPaymentsRequest(new List<PaymentTypeFilter>(){PaymentTypeFilter.Received}, null, null,
|
var req = new ListPaymentsRequest(
|
||||||
null, request?.PendingOnly is not true, (uint?) request?.OffsetIndex, null))
|
typeFilter: new List<PaymentType> { PaymentType.Receive },
|
||||||
.Select(FromPayment).ToArray();
|
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)
|
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)
|
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,
|
public async Task<LightningPayment[]> ListPayments(ListPaymentsParams request,
|
||||||
CancellationToken cancellation = default)
|
CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
return Sdk.ListPayments(new ListPaymentsRequest(new List<PaymentTypeFilter>(){PaymentTypeFilter.Received}, null, null, null,
|
var req = new ListPaymentsRequest(
|
||||||
null, (uint?) request?.OffsetIndex, null))
|
typeFilter: new List<PaymentType> { PaymentType.Send },
|
||||||
.Select(ToLightningPayment).ToArray();
|
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,
|
public async Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry,
|
||||||
CancellationToken cancellation = default)
|
CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
var expiryS = expiry == TimeSpan.Zero ? (uint?) null : Math.Max(0, (uint) expiry.TotalSeconds);
|
var expiryS = expiry == TimeSpan.Zero ? (ulong?)null : Math.Max(0, (ulong)expiry.TotalSeconds);
|
||||||
description??= "Invoice";
|
description ??= "Invoice";
|
||||||
var p = Sdk.ReceivePayment(new ReceivePaymentRequest((ulong) amount.MilliSatoshi, description, null, null,
|
|
||||||
false, expiryS));
|
|
||||||
return FromPR(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LightningInvoice FromPR(ReceivePaymentResponse response)
|
var paymentMethod = new ReceivePaymentMethod.Bolt11Invoice(description, (ulong)amount.ToUnit(LightMoneyUnit.Satoshi));
|
||||||
{
|
var response = await _sdk.ReceivePayment(new ReceivePaymentRequest(paymentMethod));
|
||||||
return new LightningInvoice()
|
|
||||||
{
|
return FromReceivePaymentResponse(response);
|
||||||
Amount = LightMoney.MilliSatoshis(response.lnInvoice.amountMsat ?? 0),
|
|
||||||
Id = response.lnInvoice.paymentHash,
|
|
||||||
Preimage = ConvertHelper.ToHexString(response.lnInvoice.paymentSecret.ToArray()),
|
|
||||||
PaymentHash = response.lnInvoice.paymentHash,
|
|
||||||
BOLT11 = response.lnInvoice.bolt11,
|
|
||||||
Status = LightningInvoiceStatus.Unpaid,
|
|
||||||
ExpiresAt = DateTimeOffset.FromUnixTimeSeconds((long) response.lnInvoice.expiry)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LightningInvoice> CreateInvoice(CreateInvoiceParams createInvoiceRequest,
|
public async Task<LightningInvoice> CreateInvoice(CreateInvoiceParams createInvoiceRequest,
|
||||||
CancellationToken cancellation = default)
|
CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
var expiryS = createInvoiceRequest.Expiry == TimeSpan.Zero
|
var expiryS = createInvoiceRequest.Expiry == TimeSpan.Zero
|
||||||
? (uint?) null
|
? (ulong?)null
|
||||||
: Math.Max(0, (uint) createInvoiceRequest.Expiry.TotalSeconds);
|
: Math.Max(0, (ulong)createInvoiceRequest.Expiry.TotalSeconds);
|
||||||
var p = Sdk.ReceivePayment(new ReceivePaymentRequest((ulong) createInvoiceRequest.Amount.MilliSatoshi,
|
|
||||||
(createInvoiceRequest.Description ?? createInvoiceRequest.DescriptionHash.ToString())!, null, null,
|
var description = createInvoiceRequest.Description ?? createInvoiceRequest.DescriptionHash?.ToString() ?? "Invoice";
|
||||||
createInvoiceRequest.DescriptionHashOnly, expiryS));
|
var paymentMethod = new ReceivePaymentMethod.Bolt11Invoice(description, (ulong)createInvoiceRequest.Amount.ToUnit(LightMoneyUnit.Satoshi));
|
||||||
return FromPR(p);
|
var response = await _sdk.ReceivePayment(new ReceivePaymentRequest(paymentMethod));
|
||||||
|
|
||||||
|
return FromReceivePaymentResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ILightningInvoiceListener> Listen(CancellationToken cancellation = default)
|
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)
|
public async Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
var ni = Sdk.NodeInfo();
|
var response = await _sdk.GetInfo(new GetInfoRequest(ensureSynced: false));
|
||||||
|
|
||||||
return new LightningNodeInformation()
|
return new LightningNodeInformation()
|
||||||
{
|
{
|
||||||
PeersCount = ni.connectedPeers.Count,
|
Alias = $"spark {response.nodeId}",
|
||||||
Alias = $"greenlight {ni.id}",
|
BlockHeight = (int)(response.blockHeight ?? 0)
|
||||||
BlockHeight = (int) ni.blockHeight
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LightningNodeBalance> GetBalance(CancellationToken cancellation = default)
|
public async Task<LightningNodeBalance> GetBalance(CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
var ni = Sdk.NodeInfo();
|
var response = await _sdk.GetInfo(new GetInfoRequest(ensureSynced: false));
|
||||||
|
|
||||||
return new LightningNodeBalance()
|
return new LightningNodeBalance()
|
||||||
{
|
{
|
||||||
OnchainBalance =
|
OnchainBalance = new OnchainBalance()
|
||||||
new OnchainBalance()
|
{
|
||||||
{
|
Confirmed = Money.Satoshis((long)response.balanceSats)
|
||||||
Confirmed = Money.Coins(LightMoney.MilliSatoshis(ni.onchainBalanceMsat)
|
},
|
||||||
.ToUnit(LightMoneyUnit.BTC))
|
|
||||||
},
|
|
||||||
OffchainBalance = new OffchainBalance()
|
OffchainBalance = new OffchainBalance()
|
||||||
{
|
{
|
||||||
Local = LightMoney.MilliSatoshis(ni.channelsBalanceMsat),
|
Local = LightMoney.Satoshis((long)response.balanceSats),
|
||||||
Remote = LightMoney.MilliSatoshis(ni.totalInboundLiquidityMsats),
|
Remote = LightMoney.Zero
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -265,43 +209,44 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
|
|||||||
public async Task<PayResponse> Pay(string bolt11, PayInvoiceParams payParams,
|
public async Task<PayResponse> Pay(string bolt11, PayInvoiceParams payParams,
|
||||||
CancellationToken cancellation = default)
|
CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
SendPaymentResponse result;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (bolt11 is null)
|
if (string.IsNullOrEmpty(bolt11))
|
||||||
{
|
{
|
||||||
result = Sdk.SendSpontaneousPayment(new SendSpontaneousPaymentRequest(payParams.Destination.ToString(),
|
return new PayResponse(PayResult.Error, "BOLT11 invoice required");
|
||||||
(ulong) payParams.Amount.MilliSatoshi));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result = Sdk.SendPayment(new SendPaymentRequest(bolt11,false, (ulong?) payParams.Amount?.MilliSatoshi));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
return new PayResponse()
|
||||||
{
|
{
|
||||||
Result = result.payment.status switch
|
Result = sendResponse.payment.status switch
|
||||||
{
|
{
|
||||||
PaymentStatus.Failed => PayResult.Error,
|
PaymentStatus.Failed => PayResult.Error,
|
||||||
PaymentStatus.Complete => PayResult.Ok,
|
PaymentStatus.Completed => PayResult.Ok,
|
||||||
PaymentStatus.Pending => PayResult.Unknown,
|
PaymentStatus.Pending => PayResult.Unknown,
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
_ => PayResult.Error
|
||||||
},
|
},
|
||||||
Details = new PayDetails()
|
Details = new PayDetails()
|
||||||
{
|
{
|
||||||
Status = result.payment.status switch
|
Status = sendResponse.payment.status switch
|
||||||
{
|
{
|
||||||
PaymentStatus.Failed => LightningPaymentStatus.Failed,
|
PaymentStatus.Failed => LightningPaymentStatus.Failed,
|
||||||
PaymentStatus.Complete => LightningPaymentStatus.Complete,
|
PaymentStatus.Completed => LightningPaymentStatus.Complete,
|
||||||
PaymentStatus.Pending => LightningPaymentStatus.Pending,
|
PaymentStatus.Pending => LightningPaymentStatus.Pending,
|
||||||
_ => LightningPaymentStatus.Unknown
|
_ => LightningPaymentStatus.Unknown
|
||||||
},
|
},
|
||||||
Preimage =
|
TotalAmount = LightMoney.Satoshis((long)sendResponse.payment.amountSats),
|
||||||
details.data.paymentPreimage is null ? null : uint256.Parse(details.data.paymentPreimage),
|
FeeAmount = (long)sendResponse.payment.feesSats
|
||||||
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),
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -342,44 +287,80 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LightningInvoice FromReceivePaymentResponse(ReceivePaymentResponse response)
|
||||||
|
{
|
||||||
|
return new LightningInvoice()
|
||||||
|
{
|
||||||
|
BOLT11 = response.destination,
|
||||||
|
Status = LightningInvoiceStatus.Unpaid,
|
||||||
|
Amount = LightMoney.Satoshis((long)response.feesSats)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private LightningInvoice FromPayment(Payment payment)
|
||||||
|
{
|
||||||
|
if (payment == null) return null;
|
||||||
|
|
||||||
|
return new LightningInvoice()
|
||||||
|
{
|
||||||
|
Id = payment.id,
|
||||||
|
Amount = LightMoney.Satoshis((long)payment.amountSats),
|
||||||
|
Status = payment.status switch
|
||||||
|
{
|
||||||
|
PaymentStatus.Pending => LightningInvoiceStatus.Unpaid,
|
||||||
|
PaymentStatus.Failed => LightningInvoiceStatus.Expired,
|
||||||
|
PaymentStatus.Completed => LightningInvoiceStatus.Paid,
|
||||||
|
_ => LightningInvoiceStatus.Unpaid
|
||||||
|
},
|
||||||
|
PaidAt = payment.timestamp.HasValue ? DateTimeOffset.FromUnixTimeSeconds((long)payment.timestamp.Value) : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private LightningPayment ToLightningPayment(Payment payment)
|
||||||
|
{
|
||||||
|
if (payment == null) return null;
|
||||||
|
|
||||||
|
return new LightningPayment()
|
||||||
|
{
|
||||||
|
Id = payment.id,
|
||||||
|
Amount = LightMoney.Satoshis((long)payment.amountSats),
|
||||||
|
Status = payment.status switch
|
||||||
|
{
|
||||||
|
PaymentStatus.Failed => LightningPaymentStatus.Failed,
|
||||||
|
PaymentStatus.Completed => LightningPaymentStatus.Complete,
|
||||||
|
PaymentStatus.Pending => LightningPaymentStatus.Pending,
|
||||||
|
_ => LightningPaymentStatus.Unknown
|
||||||
|
},
|
||||||
|
CreatedAt = payment.timestamp.HasValue ? DateTimeOffset.FromUnixTimeSeconds((long)payment.timestamp.Value) : DateTimeOffset.Now,
|
||||||
|
Fee = LightMoney.Satoshis((long)payment.feesSats),
|
||||||
|
AmountSent = LightMoney.Satoshis((long)payment.amountSats)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Sdk.Dispose();
|
_sdk?.Dispose();
|
||||||
Sdk.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BreezInvoiceListener : ILightningInvoiceListener
|
public class BreezInvoiceListener : ILightningInvoiceListener
|
||||||
{
|
{
|
||||||
private readonly BreezLightningClient _breezLightningClient;
|
private readonly BreezLightningClient _breezLightningClient;
|
||||||
private readonly CancellationToken _cancellationToken;
|
private readonly CancellationToken _cancellationToken;
|
||||||
|
private readonly ConcurrentQueue<Payment> _invoices = new();
|
||||||
|
|
||||||
public BreezInvoiceListener(BreezLightningClient breezLightningClient, CancellationToken cancellationToken)
|
public BreezInvoiceListener(BreezLightningClient breezLightningClient, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_breezLightningClient = breezLightningClient;
|
_breezLightningClient = breezLightningClient;
|
||||||
_cancellationToken = cancellationToken;
|
_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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_breezLightningClient.EventReceived -= BreezLightningClientOnEventReceived;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LightningInvoice> WaitInvoice(CancellationToken cancellation)
|
public async Task<LightningInvoice> WaitInvoice(CancellationToken cancellation)
|
||||||
{
|
{
|
||||||
while (cancellation.IsCancellationRequested is not true)
|
while (!cancellation.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
if (_invoices.TryDequeue(out var payment))
|
if (_invoices.TryDequeue(out var payment))
|
||||||
{
|
{
|
||||||
@@ -393,4 +374,4 @@ public class BreezLightningClient : ILightningClient, IDisposable, EventListener
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ public class BreezService:EventHostedServiceBase
|
|||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<BreezLightningClient?> Handle(string? storeId, BreezSettings? settings)
|
public async Task<BreezLightningClient?> Handle(string? storeId, BreezSettings? settings)
|
||||||
{
|
{
|
||||||
if (settings is null)
|
if (settings is null)
|
||||||
{
|
{
|
||||||
@@ -105,16 +105,24 @@ public class BreezService:EventHostedServiceBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var network = Network.Main; // _btcPayNetworkProvider.BTC.NBitcoinNetwork;
|
var network = Network.Main;
|
||||||
var dir = GetWorkDir(storeId);
|
var dir = GetWorkDir(storeId);
|
||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
settings.PaymentKey ??= Guid.NewGuid().ToString();
|
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)
|
if (storeId is not null)
|
||||||
{
|
{
|
||||||
_clients.AddOrReplace(storeId, client);
|
_clients.AddOrReplace(storeId, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|||||||
@@ -7,14 +7,8 @@ namespace BTCPayServer.Plugins.Breez;
|
|||||||
|
|
||||||
public class BreezSettings
|
public class BreezSettings
|
||||||
{
|
{
|
||||||
public string? InviteCode { get; set; }
|
|
||||||
public string? Mnemonic { get; set; }
|
public string? Mnemonic { get; set; }
|
||||||
public string? ApiKey { get; set; }
|
public string? ApiKey { get; set; }
|
||||||
|
|
||||||
public string PaymentKey { get; set; } = Guid.NewGuid().ToString();
|
public string PaymentKey { get; set; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public IFormFile? GreenlightCredentials { get; set; }
|
|
||||||
}
|
}
|
||||||
Submodule submodules/btcpayserver updated: 7932abd8b5...f3184c35b4
Reference in New Issue
Block a user