mirror of
https://github.com/aljazceru/btcpayserver-breez-nodeless-spark.git
synced 2025-12-16 17:04:20 +01:00
cleanup
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,9 +1,7 @@
|
||||
**/bin/**/*
|
||||
**/obj
|
||||
**/.idea
|
||||
*//Plugins/packed
|
||||
.idea
|
||||
Plugins/packed
|
||||
.vs/
|
||||
/BTCPayServerPlugins.sln.DotSettings.user
|
||||
*/documentation/
|
||||
**/build.sh
|
||||
*/build-and-test.sh
|
||||
documentation/
|
||||
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"Identifier":"BTCPayServer.Plugins.Breez","Name":"Breez Spark Lightning Plugin","Version":"1.1.0.0","Description":"Nodeless Lightning payments powered by Breez Spark SDK","SystemPlugin":false,"Dependencies":[{"Identifier":"BTCPayServer","Condition":"\u003E=2.2.0"}]}
|
||||
@@ -1,2 +0,0 @@
|
||||
30a4ce17e09c22d168db00ba618f9c76d015a248fa88793f5d333768fe346d95 BTCPayServer.Plugins.Breez.btcpay.json
|
||||
a8062f16f45ba6d5e9de9cfdfdb34eb7e542d4f4d908aa77f7cdc596035c13ed BTCPayServer.Plugins.Breez.btcpay
|
||||
15
dist/BTCPayServer.Plugins.BreezSpark.json
vendored
15
dist/BTCPayServer.Plugins.BreezSpark.json
vendored
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"Identifier": "BTCPayServer.Plugins.BreezSpark",
|
||||
"Name": "BreezSpark Lightning Plugin",
|
||||
"Version": "1.1.0",
|
||||
"Description": "Nodeless Lightning payments powered by Breez Spark SDK",
|
||||
"Author": "Aljaz Ceru",
|
||||
"AuthorLink": "https://github.com/aljazceru",
|
||||
"SystemPlugin": false,
|
||||
"Dependencies": [
|
||||
{
|
||||
"Identifier": "BTCPayServer",
|
||||
"Condition": ">=2.2.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
dist/BTCPayServer.Plugins.BreezSpark.v3.btcpay
vendored
BIN
dist/BTCPayServer.Plugins.BreezSpark.v3.btcpay
vendored
Binary file not shown.
@@ -1,47 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Plugin specific properties -->
|
||||
<PropertyGroup>
|
||||
<Product>BreezSpark Lightning Plugin</Product>
|
||||
<Description>Nodeless Lightning payments powered by Breez Spark SDK</Description>
|
||||
<Version>1.1.0</Version>
|
||||
<Author>Aljaz Ceru</Author>
|
||||
<Company>Aljaz Ceru</Company>
|
||||
<AssemblyName>BTCPayServer.Plugins.BreezSpark</AssemblyName>
|
||||
<RootNamespace>BTCPayServer.Plugins.BreezSpark</RootNamespace>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
<!-- Plugin development properties -->
|
||||
<PropertyGroup>
|
||||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||
<PreserveCompilationContext>false</PreserveCompilationContext>
|
||||
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- This will make sure that referencing BTCPayServer doesn't put any artifact in the published directory -->
|
||||
<ItemDefinitionGroup>
|
||||
<ProjectReference>
|
||||
<Properties>StaticWebAssetsEnabled=false</Properties>
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime;native;build;buildTransitive;contentFiles</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\**" />
|
||||
<ProjectReference Include="../btcpayserver/BTCPayServer/BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Breez.Sdk.Spark" Version="0.4.1" />
|
||||
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v8.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v8.0": {
|
||||
"BTCPayServer.Plugins.BreezSpark/1.1.0": {
|
||||
"dependencies": {
|
||||
"Breez.Sdk.Spark": "0.4.1"
|
||||
},
|
||||
"runtime": {
|
||||
"BTCPayServer.Plugins.BreezSpark.dll": {}
|
||||
}
|
||||
},
|
||||
"Breez.Sdk.Spark/0.4.1": {
|
||||
"runtime": {
|
||||
"lib/net8.0/Breez.Sdk.Spark.dll": {
|
||||
"assemblyVersion": "0.4.1.0",
|
||||
"fileVersion": "0.4.1.0"
|
||||
}
|
||||
},
|
||||
"runtimeTargets": {
|
||||
"runtimes/linux-arm64/native/libbreez_sdk_spark_bindings.so": {
|
||||
"rid": "linux-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-x64/native/libbreez_sdk_spark_bindings.so": {
|
||||
"rid": "linux-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/osx-arm64/native/libbreez_sdk_spark_bindings.dylib": {
|
||||
"rid": "osx-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/osx-x64/native/libbreez_sdk_spark_bindings.dylib": {
|
||||
"rid": "osx-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-x64/native/breez_sdk_spark_bindings.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-x86/native/breez_sdk_spark_bindings.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"BTCPayServer.Plugins.BreezSpark/1.1.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Breez.Sdk.Spark/0.4.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-/5iC7V3PK0q5og4h5qnSf5xepfvcXSEeV5WJDIAlOGd7WUtMZdNQ0n6yDcgd1Rv5qcxPQsDGQN3IVQZbP0UE1w==",
|
||||
"path": "breez.sdk.spark/0.4.1",
|
||||
"hashPath": "breez.sdk.spark.0.4.1.nupkg.sha512"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"Version":1,"ManifestType":"Publish","Endpoints":[]}
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<doc>
|
||||
<assembly>
|
||||
<name>BTCPayServer.Plugins.BreezSpark</name>
|
||||
</assembly>
|
||||
<members>
|
||||
</members>
|
||||
</doc>
|
||||
Binary file not shown.
@@ -1,537 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Breez.Sdk.Spark;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Plugins.BreezSpark;
|
||||
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Route("plugins/{storeId}/BreezSpark")]
|
||||
public class BreezSparkController : Controller
|
||||
{
|
||||
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly BreezSparkService _breezService;
|
||||
private readonly BTCPayWalletProvider _btcWalletProvider;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public BreezSparkController(
|
||||
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
BreezSparkService breezService,
|
||||
BTCPayWalletProvider btcWalletProvider, StoreRepository storeRepository)
|
||||
{
|
||||
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_breezService = breezService;
|
||||
_btcWalletProvider = btcWalletProvider;
|
||||
_storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<IActionResult> Index(string storeId)
|
||||
{
|
||||
var client = _breezService.GetClient(storeId);
|
||||
return RedirectToAction(client is null ? nameof(Configure) : nameof(Info), new {storeId});
|
||||
}
|
||||
|
||||
[HttpGet("swapin")]
|
||||
[Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> SwapIn(string storeId)
|
||||
{
|
||||
var client = _breezService.GetClient(storeId);
|
||||
if (client is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
return View((object) storeId);
|
||||
}
|
||||
|
||||
[HttpGet("info")]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Info(string storeId)
|
||||
{
|
||||
var client = _breezService.GetClient(storeId);
|
||||
if (client is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
return View((object) storeId);
|
||||
}
|
||||
[HttpGet("logs")]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Logs(string storeId)
|
||||
{
|
||||
var client = _breezService.GetClient(storeId);
|
||||
if (client is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
return View( client.Events);
|
||||
}
|
||||
|
||||
[HttpPost("sweep")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Sweep(string storeId)
|
||||
{
|
||||
var client = _breezService.GetClient(storeId);
|
||||
if (client is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// In Spark SDK v0.4.1, check for any unclaimed deposits
|
||||
var request = new ListUnclaimedDepositsRequest();
|
||||
var response = await client.Sdk.ListUnclaimedDeposits(request);
|
||||
|
||||
if (response.deposits.Any())
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Found {response.deposits.Count} unclaimed deposits";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = "No pending deposits to claim";
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"error claiming deposits: {e.Message}";
|
||||
}
|
||||
|
||||
return View((object) storeId);
|
||||
}
|
||||
|
||||
[HttpGet("send")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Send(string storeId)
|
||||
{
|
||||
var client = _breezService.GetClient(storeId);
|
||||
if (client is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
return View((object) storeId);
|
||||
}
|
||||
[Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("receive")]
|
||||
[Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Receive(string storeId)
|
||||
{
|
||||
var client = _breezService.GetClient(storeId);
|
||||
if (client is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
return View((object) storeId);
|
||||
}
|
||||
|
||||
[HttpPost("receive")]
|
||||
[Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Receive(string storeId, long? amount, string description)
|
||||
{
|
||||
var client = _breezService.GetClient(storeId);
|
||||
if (client is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
description ??= "BTCPay Server Invoice";
|
||||
|
||||
var paymentMethod = new ReceivePaymentMethod.Bolt11Invoice(
|
||||
description: description,
|
||||
amountSats: amount != null ? (ulong)amount.Value : null
|
||||
);
|
||||
|
||||
var request = new ReceivePaymentRequest(paymentMethod: paymentMethod);
|
||||
var response = await client.Sdk.ReceivePayment(request: request);
|
||||
|
||||
TempData["bolt11"] = response.paymentRequest;
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Invoice created successfully!";
|
||||
|
||||
return RedirectToAction(nameof(Payments), new {storeId});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"Error creating invoice: {ex.Message}";
|
||||
return View((object) storeId);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("prepare-send")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> PrepareSend(string storeId, string address, long? amount)
|
||||
{
|
||||
var client = _breezService.GetClient(storeId);
|
||||
if (client is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(address))
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Payment destination is required";
|
||||
return RedirectToAction(nameof(Send), new {storeId});
|
||||
}
|
||||
|
||||
BigInteger? amountSats = null;
|
||||
if (amount > 0)
|
||||
{
|
||||
amountSats = new BigInteger(amount.Value);
|
||||
}
|
||||
|
||||
var prepareRequest = new PrepareSendPaymentRequest(
|
||||
paymentRequest: address,
|
||||
amount: amountSats
|
||||
);
|
||||
|
||||
var prepareResponse = await client.Sdk.PrepareSendPayment(prepareRequest);
|
||||
|
||||
if (prepareResponse.paymentMethod is SendPaymentMethod.Bolt11Invoice bolt11Method)
|
||||
{
|
||||
var totalFee = bolt11Method.lightningFeeSats + (bolt11Method.sparkTransferFeeSats ?? 0);
|
||||
var viewModel = new
|
||||
{
|
||||
Destination = address,
|
||||
Amount = amountSats ?? 0,
|
||||
Fee = totalFee,
|
||||
PrepareResponseJson = JsonSerializer.Serialize(prepareResponse)
|
||||
};
|
||||
ViewData["PaymentDetails"] = viewModel;
|
||||
}
|
||||
else if (prepareResponse.paymentMethod is SendPaymentMethod.BitcoinAddress bitcoinMethod)
|
||||
{
|
||||
var fees = bitcoinMethod.feeQuote;
|
||||
var mediumFee = fees.speedMedium.userFeeSat + fees.speedMedium.l1BroadcastFeeSat;
|
||||
var viewModel = new
|
||||
{
|
||||
Destination = address,
|
||||
Amount = amountSats ?? 0,
|
||||
Fee = mediumFee,
|
||||
PrepareResponseJson = JsonSerializer.Serialize(prepareResponse)
|
||||
};
|
||||
ViewData["PaymentDetails"] = viewModel;
|
||||
}
|
||||
else if (prepareResponse.paymentMethod is SendPaymentMethod.SparkAddress sparkMethod)
|
||||
{
|
||||
var viewModel = new
|
||||
{
|
||||
Destination = address,
|
||||
Amount = amountSats ?? 0,
|
||||
Fee = sparkMethod.fee,
|
||||
PrepareResponseJson = JsonSerializer.Serialize(prepareResponse)
|
||||
};
|
||||
ViewData["PaymentDetails"] = viewModel;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"Error preparing payment: {ex.Message}";
|
||||
}
|
||||
|
||||
return View(nameof(Send), storeId);
|
||||
}
|
||||
|
||||
[HttpPost("confirm-send")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ConfirmSend(string storeId, string paymentRequest, long amount, string prepareResponseJson)
|
||||
{
|
||||
var client = _breezService.GetClient(storeId);
|
||||
if (client is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var prepareResponse = JsonSerializer.Deserialize<PrepareSendPaymentResponse>(prepareResponseJson);
|
||||
if (prepareResponse == null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid payment preparation data");
|
||||
}
|
||||
|
||||
SendPaymentOptions? options = prepareResponse.paymentMethod switch
|
||||
{
|
||||
SendPaymentMethod.Bolt11Invoice => new SendPaymentOptions.Bolt11Invoice(
|
||||
preferSpark: false,
|
||||
completionTimeoutSecs: 60
|
||||
),
|
||||
SendPaymentMethod.BitcoinAddress => new SendPaymentOptions.BitcoinAddress(
|
||||
confirmationSpeed: OnchainConfirmationSpeed.Medium
|
||||
),
|
||||
SendPaymentMethod.SparkAddress => null,
|
||||
_ => throw new NotSupportedException("Unsupported payment method")
|
||||
};
|
||||
|
||||
var sendRequest = new SendPaymentRequest(
|
||||
prepareResponse: prepareResponse,
|
||||
options: options
|
||||
);
|
||||
|
||||
var sendResponse = await client.Sdk.SendPayment(sendRequest);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Payment sent successfully!";
|
||||
return RedirectToAction(nameof(Payments), new {storeId});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"Error sending payment: {ex.Message}";
|
||||
return RedirectToAction(nameof(Send), new {storeId});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("swapout")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> SwapOut(string storeId)
|
||||
{
|
||||
var client = _breezService.GetClient(storeId);
|
||||
if (client is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
return View((object) storeId);
|
||||
}
|
||||
|
||||
[HttpPost("swapout")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> SwapOut(string storeId, string address, ulong amount, uint satPerByte,
|
||||
string feesHash)
|
||||
{
|
||||
var client = _breezService.GetClient(storeId);
|
||||
if (client is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Use current SDK pattern for onchain payments
|
||||
var prepareRequest = new PrepareSendPaymentRequest(
|
||||
paymentRequest: address,
|
||||
amount: new BigInteger(amount)
|
||||
);
|
||||
|
||||
var prepareResponse = await client.Sdk.PrepareSendPayment(prepareRequest);
|
||||
|
||||
if (prepareResponse.paymentMethod is SendPaymentMethod.BitcoinAddress bitcoinMethod)
|
||||
{
|
||||
var options = new SendPaymentOptions.BitcoinAddress(
|
||||
confirmationSpeed: OnchainConfirmationSpeed.Medium
|
||||
);
|
||||
|
||||
var sendRequest = new SendPaymentRequest(
|
||||
prepareResponse: prepareResponse,
|
||||
options: options
|
||||
);
|
||||
|
||||
var sendResponse = await client.Sdk.SendPayment(sendRequest);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Onchain payment initiated successfully!";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Invalid payment method for onchain swap";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"Error processing swap-out: {ex.Message}";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(SwapOut), new {storeId});
|
||||
}
|
||||
|
||||
[HttpGet("swapin/{address}/refund")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> SwapInRefund(string storeId, string address)
|
||||
{
|
||||
var client = _breezService.GetClient(storeId);
|
||||
if (client is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
return View((object) storeId);
|
||||
}
|
||||
|
||||
[HttpPost("swapin/{address}/refund")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> SwapInRefund(string storeId, string txid, uint vout, string refundAddress, uint? satPerByte = null)
|
||||
{
|
||||
var client = _breezService.GetClient(storeId);
|
||||
if (client is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Parse the txid:vout format from depositId if needed
|
||||
var fee = new Fee.Rate((ulong)(satPerByte ?? 5m));
|
||||
var request = new RefundDepositRequest(
|
||||
txid: txid,
|
||||
vout: vout,
|
||||
destinationAddress: refundAddress,
|
||||
fee: fee
|
||||
);
|
||||
|
||||
var resp = await client.Sdk.RefundDeposit(request);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Refund successful: {resp.txId}";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"Couldnt refund: {e.Message}";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(SwapIn), new {storeId});
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("configure")]
|
||||
public async Task<IActionResult> Configure(string storeId)
|
||||
{
|
||||
return View(await _breezService.Get(storeId));
|
||||
}
|
||||
[HttpPost("configure")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Configure(string storeId, string command, BreezSparkSettings settings)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
var pmi = new PaymentMethodId("BTC-LN");
|
||||
// In v2.2.1, payment methods are handled differently
|
||||
// TODO: Implement proper v2.2.1 payment method handling
|
||||
object? existing = null;
|
||||
|
||||
if (command == "clear")
|
||||
{
|
||||
await _breezService.Set(storeId, null);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Settings cleared successfully";
|
||||
var client = _breezService.GetClient(storeId);
|
||||
// In v2.2.1, payment methods are handled differently
|
||||
// TODO: Implement proper v2.2.1 payment method handling
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
if (command == "save")
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(settings.Mnemonic))
|
||||
{
|
||||
ModelState.AddModelError(nameof(settings.Mnemonic), "Mnemonic is required");
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
new Mnemonic(settings.Mnemonic);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(nameof(settings.Mnemonic), "Invalid mnemonic");
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
await _breezService.Set(storeId, settings);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"Couldnt use provided settings: {e.Message}";
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
// In v2.2.1, payment methods are handled differently
|
||||
// TODO: Implement proper v2.2.1 payment method handling
|
||||
// This will require a complete rewrite of the payment method system
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Settings saved successfully";
|
||||
return RedirectToAction(nameof(Info), new {storeId});
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
[Route("payments")]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Payments(string storeId, PaymentsViewModel viewModel)
|
||||
{
|
||||
var client = _breezService.GetClient(storeId);
|
||||
if (client is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
viewModel ??= new PaymentsViewModel();
|
||||
var req = new ListPaymentsRequest(
|
||||
typeFilter: null,
|
||||
statusFilter: null,
|
||||
assetFilter: new AssetFilter.Bitcoin(),
|
||||
fromTimestamp: null,
|
||||
toTimestamp: null,
|
||||
offset: viewModel.Skip != null ? (uint?)viewModel.Skip : null,
|
||||
limit: viewModel.Count != null ? (uint?)viewModel.Count : null,
|
||||
sortAscending: false
|
||||
);
|
||||
var response = await client.Sdk.ListPayments(req);
|
||||
viewModel.Payments = response.payments.Select(client.NormalizePayment).ToList();
|
||||
|
||||
return View(viewModel);
|
||||
}
|
||||
}
|
||||
|
||||
public class PaymentsViewModel : BasePagingViewModel
|
||||
{
|
||||
public List<NormalizedPayment> Payments { get; set; } = new();
|
||||
public override int CurrentPageCount => Payments.Count;
|
||||
}
|
||||
|
||||
// Helper class for swap information display in views
|
||||
public class SwapInfo
|
||||
{
|
||||
public string? bitcoinAddress { get; set; }
|
||||
public ulong minAllowedDeposit { get; set; }
|
||||
public ulong maxAllowedDeposit { get; set; }
|
||||
public string? status { get; set; }
|
||||
}
|
||||
|
||||
// Helper class for swap limits display in views
|
||||
public class SwapLimits
|
||||
{
|
||||
public ulong min { get; set; }
|
||||
public ulong max { get; set; }
|
||||
}
|
||||
@@ -1,952 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Breez.Sdk.Spark;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin;
|
||||
using Network = Breez.Sdk.Spark.Network;
|
||||
|
||||
namespace BTCPayServer.Plugins.BreezSpark;
|
||||
|
||||
public class EventLogEntry
|
||||
{
|
||||
public DateTimeOffset timestamp { get; set; }
|
||||
public string log { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class BreezSparkLightningClient : ILightningClient, IDisposable
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return $"type=breezspark;key={PaymentKey}";
|
||||
}
|
||||
|
||||
private readonly NBitcoin.Network _network;
|
||||
public readonly string PaymentKey;
|
||||
|
||||
public ConcurrentQueue<EventLogEntry> Events { get; set; } = new ConcurrentQueue<EventLogEntry>();
|
||||
private readonly ConcurrentQueue<Payment> _paymentNotifications = new();
|
||||
private readonly ConcurrentDictionary<string, bool> _seenCompletedPayments = new();
|
||||
private readonly ConcurrentDictionary<string, bool> _seenPaymentHashes = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, InvoiceRecord> _invoicesByHash = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, InvoiceRecord> _invoicesByBolt11 = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private void DebugLog(string message)
|
||||
{
|
||||
// Debug logging disabled for release build
|
||||
}
|
||||
|
||||
private void DebugLogObject(string label, object obj)
|
||||
{
|
||||
// Debug logging disabled for release build
|
||||
}
|
||||
|
||||
private BreezSdk _sdk;
|
||||
|
||||
public static async Task<BreezSparkLightningClient> Create(string apiKey, string workingDir, NBitcoin.Network network,
|
||||
Mnemonic mnemonic, string paymentKey)
|
||||
{
|
||||
apiKey ??= "99010c6f84541bf582899db6728f6098ba98ca95ea569f4c63f2c2c9205ace57";
|
||||
|
||||
var config = BreezSdkSparkMethods.DefaultConfig(
|
||||
network == NBitcoin.Network.Main ? Network.Mainnet :
|
||||
network == NBitcoin.Network.RegTest ? Network.Regtest : Network.Mainnet
|
||||
) with
|
||||
{
|
||||
apiKey = apiKey
|
||||
};
|
||||
|
||||
var seed = new Seed.Mnemonic(mnemonic: mnemonic.ToString(), passphrase: null);
|
||||
var sdk = await BreezSdkSparkMethods.Connect(new ConnectRequest(config, seed, workingDir));
|
||||
|
||||
return new BreezSparkLightningClient(sdk, network, paymentKey);
|
||||
}
|
||||
|
||||
private BreezSparkLightningClient(BreezSdk sdk, NBitcoin.Network network, string paymentKey)
|
||||
{
|
||||
_sdk = sdk;
|
||||
_network = network;
|
||||
PaymentKey = paymentKey;
|
||||
|
||||
// Start monitoring payment events
|
||||
_ = Task.Run(MonitorPaymentEvents);
|
||||
}
|
||||
|
||||
public BreezSdk Sdk => _sdk;
|
||||
|
||||
public async Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default)
|
||||
{
|
||||
var invoice = await GetInvoiceInternal(invoiceId, cancellation);
|
||||
if (invoice is not null)
|
||||
{
|
||||
return invoice;
|
||||
}
|
||||
|
||||
return new LightningInvoice()
|
||||
{
|
||||
Id = invoiceId,
|
||||
PaymentHash = invoiceId,
|
||||
Status = LightningInvoiceStatus.Unpaid
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> GetInvoice(uint256 paymentHash, CancellationToken cancellation = default)
|
||||
{
|
||||
return await GetInvoice(paymentHash.ToString(), cancellation);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice[]> ListInvoices(CancellationToken cancellation = default)
|
||||
{
|
||||
return await ListInvoices((ListInvoicesParams?)null, cancellation);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice[]> ListInvoices(ListInvoicesParams request,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
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: request?.OffsetIndex != null ? (uint?)request.OffsetIndex : null,
|
||||
limit: null,
|
||||
sortAscending: false
|
||||
);
|
||||
|
||||
var response = await _sdk.ListPayments(req);
|
||||
return response.payments.Select(FromPayment).Where(p => p != null).ToArray();
|
||||
}
|
||||
|
||||
public async Task<LightningPayment> GetPayment(string paymentHash, CancellationToken cancellation = default)
|
||||
{
|
||||
var payment = await FindPayment(paymentHash, cancellation);
|
||||
return payment is not null ? ToLightningPayment(payment) : null;
|
||||
}
|
||||
|
||||
public async Task<LightningPayment[]> ListPayments(CancellationToken cancellation = default)
|
||||
{
|
||||
return await ListPayments((ListPaymentsParams?)null, cancellation);
|
||||
}
|
||||
|
||||
public async Task<LightningPayment[]> ListPayments(ListPaymentsParams request,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
var req = new ListPaymentsRequest(
|
||||
typeFilter: new List<PaymentType> { PaymentType.Send },
|
||||
statusFilter: null,
|
||||
assetFilter: new AssetFilter.Bitcoin(),
|
||||
fromTimestamp: null,
|
||||
toTimestamp: null,
|
||||
offset: request?.OffsetIndex != null ? (uint?)request.OffsetIndex : null,
|
||||
limit: null,
|
||||
sortAscending: false
|
||||
);
|
||||
|
||||
var response = await _sdk.ListPayments(req);
|
||||
return response.payments.Select(ToLightningPayment).Where(p => p != null).ToArray();
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
var descriptionToUse = description ?? "Invoice";
|
||||
var amountSats = (ulong)amount.ToUnit(LightMoneyUnit.Satoshi);
|
||||
var paymentMethod = new ReceivePaymentMethod.Bolt11Invoice(descriptionToUse, amountSats);
|
||||
var response = await _sdk.ReceivePayment(new ReceivePaymentRequest(paymentMethod));
|
||||
DebugLogObject("ReceivePaymentResponse(CreateInvoice)", response);
|
||||
return FromReceivePaymentResponse(response, amount);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> CreateInvoice(CreateInvoiceParams createInvoiceRequest,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
var description = createInvoiceRequest.Description ?? createInvoiceRequest.DescriptionHash?.ToString() ?? "Invoice";
|
||||
var amountSats = (ulong)createInvoiceRequest.Amount.ToUnit(LightMoneyUnit.Satoshi);
|
||||
var paymentMethod = new ReceivePaymentMethod.Bolt11Invoice(description, amountSats);
|
||||
var response = await _sdk.ReceivePayment(new ReceivePaymentRequest(paymentMethod));
|
||||
DebugLogObject("ReceivePaymentResponse(CreateInvoiceParams)", response);
|
||||
return FromReceivePaymentResponse(response, createInvoiceRequest.Amount);
|
||||
}
|
||||
|
||||
public async Task<ILightningInvoiceListener> Listen(CancellationToken cancellation = default)
|
||||
{
|
||||
return new BreezSparkInvoiceListener(this, cancellation);
|
||||
}
|
||||
|
||||
public async Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _sdk.GetInfo(new GetInfoRequest(ensureSynced: false));
|
||||
|
||||
return new LightningNodeInformation()
|
||||
{
|
||||
Alias = "BreezSpark (nodeless)",
|
||||
BlockHeight = 0, // Spark SDK doesn't expose block height
|
||||
Version = "0.4.1" // SDK version hardcoded since property not found
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new LightningNodeInformation()
|
||||
{
|
||||
Alias = "BreezSpark (nodeless)",
|
||||
BlockHeight = 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LightningNodeBalance> GetBalance(CancellationToken cancellation = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _sdk.GetInfo(new GetInfoRequest(ensureSynced: false));
|
||||
|
||||
return new LightningNodeBalance()
|
||||
{
|
||||
OnchainBalance = new OnchainBalance()
|
||||
{
|
||||
Confirmed = Money.Satoshis((long)response.balanceSats)
|
||||
},
|
||||
OffchainBalance = new OffchainBalance()
|
||||
{
|
||||
Local = LightMoney.Satoshis((long)response.balanceSats),
|
||||
Remote = LightMoney.Zero
|
||||
}
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new LightningNodeBalance()
|
||||
{
|
||||
OnchainBalance = new OnchainBalance()
|
||||
{
|
||||
Confirmed = Money.Zero
|
||||
},
|
||||
OffchainBalance = new OffchainBalance()
|
||||
{
|
||||
Local = LightMoney.Zero,
|
||||
Remote = LightMoney.Zero
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PayResponse> Pay(PayInvoiceParams payParams, CancellationToken cancellation = default)
|
||||
{
|
||||
return await Pay(null, payParams, cancellation);
|
||||
}
|
||||
|
||||
public async Task<PayResponse> Pay(string bolt11, PayInvoiceParams payParams,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(bolt11))
|
||||
{
|
||||
return new PayResponse(PayResult.Error, "BOLT11 invoice required");
|
||||
}
|
||||
|
||||
BigInteger? amountSats = null;
|
||||
if (payParams.Amount > 0)
|
||||
{
|
||||
amountSats = new BigInteger(payParams.Amount);
|
||||
}
|
||||
|
||||
var prepareRequest = new PrepareSendPaymentRequest(
|
||||
paymentRequest: bolt11,
|
||||
amount: amountSats
|
||||
);
|
||||
var prepareResponse = await _sdk.PrepareSendPayment(prepareRequest);
|
||||
|
||||
if (prepareResponse.paymentMethod is SendPaymentMethod.Bolt11Invoice bolt11Method)
|
||||
{
|
||||
var options = new SendPaymentOptions.Bolt11Invoice(
|
||||
preferSpark: false,
|
||||
completionTimeoutSecs: 60
|
||||
);
|
||||
|
||||
var sendRequest = new SendPaymentRequest(
|
||||
prepareResponse: prepareResponse,
|
||||
options: options
|
||||
);
|
||||
var sendResponse = await _sdk.SendPayment(sendRequest);
|
||||
|
||||
return new PayResponse()
|
||||
{
|
||||
Result = sendResponse.payment.status switch
|
||||
{
|
||||
PaymentStatus.Failed => PayResult.Error,
|
||||
PaymentStatus.Completed => PayResult.Ok,
|
||||
PaymentStatus.Pending => PayResult.Unknown,
|
||||
_ => PayResult.Error
|
||||
},
|
||||
Details = new PayDetails()
|
||||
{
|
||||
Status = sendResponse.payment.status switch
|
||||
{
|
||||
PaymentStatus.Failed => LightningPaymentStatus.Failed,
|
||||
PaymentStatus.Completed => LightningPaymentStatus.Complete,
|
||||
PaymentStatus.Pending => LightningPaymentStatus.Pending,
|
||||
_ => LightningPaymentStatus.Unknown
|
||||
},
|
||||
TotalAmount = LightMoney.Satoshis((long)(sendResponse.payment.amount / 1000)),
|
||||
FeeAmount = (long)(bolt11Method.lightningFeeSats + (bolt11Method.sparkTransferFeeSats ?? 0))
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new PayResponse(PayResult.Error, "Invalid payment method");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new PayResponse(PayResult.Error, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PayResponse> Pay(string bolt11, CancellationToken cancellation = default)
|
||||
{
|
||||
return await Pay(bolt11, null, cancellation);
|
||||
}
|
||||
|
||||
public async Task<OpenChannelResponse> OpenChannel(OpenChannelRequest openChannelRequest,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<BitcoinAddress> GetDepositAddress(CancellationToken cancellation = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<ConnectionResult> ConnectTo(NodeInfo nodeInfo, CancellationToken cancellation = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task CancelInvoice(string invoiceId, CancellationToken cancellation = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<LightningChannel[]> ListChannels(CancellationToken cancellation = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private LightningInvoice FromReceivePaymentResponse(ReceivePaymentResponse response, LightMoney requestedAmount)
|
||||
{
|
||||
string? paymentHash = null;
|
||||
try
|
||||
{
|
||||
if (BOLT11PaymentRequest.TryParse(response.paymentRequest, out var pr, _network))
|
||||
{
|
||||
paymentHash = pr.PaymentHash?.ToString();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore parse errors and fall back to raw request
|
||||
}
|
||||
|
||||
DebugLogObject("FromReceivePaymentResponse", response);
|
||||
RecordInvoiceAmount(response.paymentRequest, paymentHash, requestedAmount);
|
||||
|
||||
return new LightningInvoice()
|
||||
{
|
||||
Id = paymentHash ?? response.paymentRequest,
|
||||
PaymentHash = paymentHash,
|
||||
BOLT11 = response.paymentRequest,
|
||||
Status = LightningInvoiceStatus.Unpaid,
|
||||
Amount = requestedAmount
|
||||
};
|
||||
}
|
||||
|
||||
private LightningInvoice FromPayment(Payment payment)
|
||||
{
|
||||
if (payment == null) return null;
|
||||
|
||||
string paymentHash = ExtractPaymentHash(payment);
|
||||
string bolt11 = null;
|
||||
LightMoney? boltAmount = null;
|
||||
LightMoney? recordedAmount = null;
|
||||
|
||||
if (payment.details is PaymentDetails.Lightning lightningDetails)
|
||||
{
|
||||
bolt11 = lightningDetails.invoice;
|
||||
if (!string.IsNullOrEmpty(lightningDetails.invoice) &&
|
||||
BOLT11PaymentRequest.TryParse(lightningDetails.invoice, out var pr, _network))
|
||||
{
|
||||
boltAmount = pr.MinimumAmount;
|
||||
}
|
||||
|
||||
var rec = LookupInvoice(lightningDetails.invoice, paymentHash);
|
||||
recordedAmount = rec?.Amount;
|
||||
}
|
||||
|
||||
// Reject if hash is missing or not one we issued
|
||||
if (string.IsNullOrEmpty(paymentHash))
|
||||
{
|
||||
DebugLog($"FromPayment: missing payment hash for payment.id={payment.id}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var record = LookupInvoice(null, paymentHash);
|
||||
if (record is null || record.PaymentHash != paymentHash)
|
||||
{
|
||||
DebugLog($"FromPayment: unknown payment hash={paymentHash} payment.id={payment.id}");
|
||||
return null;
|
||||
}
|
||||
|
||||
recordedAmount ??= record.Amount;
|
||||
|
||||
// Always use the invoice amount (BOLT11 truth). Never fall back to what Breez reports.
|
||||
var resolvedAmount = recordedAmount ?? boltAmount;
|
||||
if (boltAmount is not null && recordedAmount is not null && boltAmount != recordedAmount)
|
||||
{
|
||||
DebugLog($"FromPayment: bolt amount {boltAmount.ToUnit(LightMoneyUnit.Satoshi)} != recorded {recordedAmount.ToUnit(LightMoneyUnit.Satoshi)} for hash={paymentHash}");
|
||||
}
|
||||
|
||||
var invoiceId = paymentHash;
|
||||
if (resolvedAmount is null)
|
||||
{
|
||||
DebugLog($"FromPayment: missing amount for hash={paymentHash} bolt11={Shorten(bolt11)}");
|
||||
return null;
|
||||
}
|
||||
|
||||
DebugLog($"FromPayment: returning invoice id={invoiceId} hash={paymentHash} bolt11={Shorten(bolt11)} boltSat={boltAmount?.ToUnit(LightMoneyUnit.Satoshi)} recSat={recordedAmount?.ToUnit(LightMoneyUnit.Satoshi)} raw_msat={payment.amount} fee_msat={payment.fees} chosenSat={resolvedAmount.ToUnit(LightMoneyUnit.Satoshi)}");
|
||||
|
||||
return new LightningInvoice()
|
||||
{
|
||||
Id = invoiceId,
|
||||
PaymentHash = paymentHash ?? invoiceId,
|
||||
BOLT11 = bolt11 ?? payment.id,
|
||||
Amount = resolvedAmount,
|
||||
AmountReceived = resolvedAmount,
|
||||
Status = payment.status switch
|
||||
{
|
||||
PaymentStatus.Pending => LightningInvoiceStatus.Unpaid,
|
||||
PaymentStatus.Failed => LightningInvoiceStatus.Expired,
|
||||
PaymentStatus.Completed => LightningInvoiceStatus.Paid,
|
||||
_ => LightningInvoiceStatus.Unpaid
|
||||
},
|
||||
PaidAt = DateTimeOffset.FromUnixTimeSeconds((long)payment.timestamp)
|
||||
};
|
||||
}
|
||||
|
||||
private LightningPayment ToLightningPayment(Payment payment)
|
||||
{
|
||||
if (payment == null) return null;
|
||||
|
||||
string paymentHash = ExtractPaymentHash(payment);
|
||||
string preimage = null;
|
||||
string bolt11 = null;
|
||||
LightMoney? boltAmount = null;
|
||||
LightMoney? recordedAmount = null;
|
||||
var feeAmount = GetFeeFromPayment(payment);
|
||||
|
||||
if (payment.details is PaymentDetails.Lightning lightningDetails)
|
||||
{
|
||||
preimage = lightningDetails.preimage;
|
||||
bolt11 = lightningDetails.invoice;
|
||||
if (!string.IsNullOrEmpty(lightningDetails.invoice) &&
|
||||
BOLT11PaymentRequest.TryParse(lightningDetails.invoice, out var pr, _network))
|
||||
{
|
||||
boltAmount = pr.MinimumAmount;
|
||||
}
|
||||
|
||||
var rec = LookupInvoice(lightningDetails.invoice, paymentHash);
|
||||
recordedAmount = rec?.Amount;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(paymentHash))
|
||||
{
|
||||
DebugLog($"ToLightningPayment: missing payment hash for payment.id={payment.id}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var record = LookupInvoice(null, paymentHash);
|
||||
if (record is null || record.PaymentHash != paymentHash)
|
||||
{
|
||||
DebugLog($"ToLightningPayment: unknown payment hash={paymentHash} payment.id={payment.id}");
|
||||
return null;
|
||||
}
|
||||
|
||||
recordedAmount ??= record.Amount;
|
||||
|
||||
var resolvedAmount = recordedAmount ?? boltAmount;
|
||||
if (boltAmount is not null && recordedAmount is not null && boltAmount != recordedAmount)
|
||||
{
|
||||
DebugLog($"ToLightningPayment: bolt amount {boltAmount.ToUnit(LightMoneyUnit.Satoshi)} != recorded {recordedAmount.ToUnit(LightMoneyUnit.Satoshi)} for hash={paymentHash}");
|
||||
}
|
||||
|
||||
var paymentId = paymentHash;
|
||||
if (resolvedAmount is null)
|
||||
{
|
||||
DebugLog($"ToLightningPayment: missing amount for hash={paymentHash} bolt11={Shorten(bolt11)}");
|
||||
return null;
|
||||
}
|
||||
|
||||
DebugLog($"ToLightningPayment: returning payment id={paymentId} hash={paymentHash} bolt11={Shorten(bolt11)} boltSat={boltAmount?.ToUnit(LightMoneyUnit.Satoshi)} recSat={recordedAmount?.ToUnit(LightMoneyUnit.Satoshi)} raw_msat={payment.amount} fee_msat={payment.fees} chosenSat={resolvedAmount.ToUnit(LightMoneyUnit.Satoshi)}");
|
||||
|
||||
return new LightningPayment()
|
||||
{
|
||||
Id = paymentId,
|
||||
PaymentHash = paymentHash ?? paymentId,
|
||||
Preimage = preimage,
|
||||
BOLT11 = bolt11,
|
||||
Amount = resolvedAmount,
|
||||
Status = payment.status switch
|
||||
{
|
||||
PaymentStatus.Failed => LightningPaymentStatus.Failed,
|
||||
PaymentStatus.Completed => LightningPaymentStatus.Complete,
|
||||
PaymentStatus.Pending => LightningPaymentStatus.Pending,
|
||||
_ => LightningPaymentStatus.Unknown
|
||||
},
|
||||
CreatedAt = DateTimeOffset.FromUnixTimeSeconds((long)payment.timestamp),
|
||||
Fee = feeAmount,
|
||||
AmountSent = resolvedAmount
|
||||
};
|
||||
}
|
||||
|
||||
private void RecordInvoiceAmount(string bolt11, string? paymentHash, LightMoney requestedAmount)
|
||||
{
|
||||
// Prefer the amount encoded in the BOLT11 (ground truth), fall back to the requested amount.
|
||||
LightMoney amount = requestedAmount;
|
||||
try
|
||||
{
|
||||
if (BOLT11PaymentRequest.TryParse(bolt11, out var pr, _network))
|
||||
{
|
||||
if (pr.MinimumAmount is not null)
|
||||
amount = pr.MinimumAmount;
|
||||
if (string.IsNullOrEmpty(paymentHash) && pr.PaymentHash is not null)
|
||||
paymentHash = pr.PaymentHash.ToString();
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (string.IsNullOrEmpty(paymentHash))
|
||||
return;
|
||||
|
||||
var record = new InvoiceRecord
|
||||
{
|
||||
PaymentHash = paymentHash,
|
||||
Bolt11 = bolt11,
|
||||
Amount = amount
|
||||
};
|
||||
|
||||
_invoicesByHash[paymentHash] = record;
|
||||
_invoicesByBolt11[bolt11] = record;
|
||||
}
|
||||
|
||||
private InvoiceRecord? LookupInvoice(string? bolt11, string? paymentHash)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(paymentHash) && _invoicesByHash.TryGetValue(paymentHash, out var recByHash))
|
||||
{
|
||||
DebugLog($"LookupInvoice: hit by hash={paymentHash} amount_sat={recByHash.Amount.ToUnit(LightMoneyUnit.Satoshi)} bolt11={Shorten(recByHash.Bolt11)}");
|
||||
return recByHash;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(bolt11) && _invoicesByBolt11.TryGetValue(bolt11, out var recByBolt))
|
||||
{
|
||||
DebugLog($"LookupInvoice: hit by bolt11={Shorten(bolt11)} amount_sat={recByBolt.Amount.ToUnit(LightMoneyUnit.Satoshi)}");
|
||||
return recByBolt;
|
||||
}
|
||||
|
||||
DebugLog($"LookupInvoice: miss for hash={paymentHash} bolt11={Shorten(bolt11)}");
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsKnownPayment(Payment payment)
|
||||
{
|
||||
var paymentHash = ExtractPaymentHash(payment);
|
||||
if (string.IsNullOrEmpty(paymentHash))
|
||||
return false;
|
||||
|
||||
return LookupInvoice(null, paymentHash) is not null;
|
||||
}
|
||||
|
||||
private LightMoney InferAmountFromPayment(Payment payment)
|
||||
{
|
||||
var rawAmount = payment.amount;
|
||||
|
||||
if (rawAmount == 0)
|
||||
{
|
||||
return LightMoney.Zero;
|
||||
}
|
||||
|
||||
// Breez SDK surfaces amounts in millisats for lightning payments; fall back to sats otherwise.
|
||||
if (rawAmount % 1000 == 0)
|
||||
{
|
||||
return LightMoney.Satoshis((long)(rawAmount / 1000));
|
||||
}
|
||||
|
||||
return LightMoney.Satoshis((long)rawAmount);
|
||||
}
|
||||
|
||||
private string? ExtractPaymentHash(Payment payment)
|
||||
{
|
||||
if (payment?.details is not PaymentDetails.Lightning ln)
|
||||
return null;
|
||||
|
||||
if (!string.IsNullOrEmpty(ln.paymentHash))
|
||||
return ln.paymentHash;
|
||||
|
||||
if (!string.IsNullOrEmpty(ln.invoice) &&
|
||||
BOLT11PaymentRequest.TryParse(ln.invoice, out var pr, _network) &&
|
||||
pr.PaymentHash is not null)
|
||||
{
|
||||
return pr.PaymentHash.ToString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private LightMoney GetFeeFromPayment(Payment payment)
|
||||
{
|
||||
return payment.fees % 1000 == 0
|
||||
? LightMoney.Satoshis((long)(payment.fees / 1000))
|
||||
: LightMoney.Satoshis((long)payment.fees);
|
||||
}
|
||||
|
||||
private bool TryMarkPaymentSeen(Payment payment)
|
||||
{
|
||||
var paymentHash = ExtractPaymentHash(payment);
|
||||
var seenByHash = !string.IsNullOrEmpty(paymentHash) && _seenPaymentHashes.ContainsKey(paymentHash);
|
||||
var seenById = _seenCompletedPayments.ContainsKey(payment.id);
|
||||
if (seenByHash || seenById)
|
||||
{
|
||||
DebugLog($"TryMarkPaymentSeen: already seen payment.id={payment.id} hash={paymentHash}");
|
||||
return false;
|
||||
}
|
||||
|
||||
_seenCompletedPayments.TryAdd(payment.id, true);
|
||||
if (!string.IsNullOrEmpty(paymentHash))
|
||||
{
|
||||
_seenPaymentHashes.TryAdd(paymentHash, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public NormalizedPayment NormalizePayment(Payment payment)
|
||||
{
|
||||
if (payment == null) throw new ArgumentNullException(nameof(payment));
|
||||
|
||||
string paymentHash = null;
|
||||
string bolt11 = null;
|
||||
string description = null;
|
||||
LightMoney? boltAmount = null;
|
||||
LightMoney? recordedAmount = null;
|
||||
var feeAmount = GetFeeFromPayment(payment);
|
||||
|
||||
if (payment.details is PaymentDetails.Lightning lightningDetails)
|
||||
{
|
||||
paymentHash = ExtractPaymentHash(payment);
|
||||
bolt11 = lightningDetails.invoice;
|
||||
description = lightningDetails.description;
|
||||
if (!string.IsNullOrEmpty(lightningDetails.invoice) &&
|
||||
BOLT11PaymentRequest.TryParse(lightningDetails.invoice, out var pr, _network))
|
||||
{
|
||||
boltAmount = pr.MinimumAmount;
|
||||
}
|
||||
|
||||
var rec = LookupInvoice(lightningDetails.invoice, lightningDetails.paymentHash);
|
||||
recordedAmount = rec?.Amount;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(paymentHash))
|
||||
{
|
||||
DebugLog($"NormalizePayment: missing payment hash for payment.id={payment.id}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var record = LookupInvoice(null, paymentHash);
|
||||
if (record is null || record.PaymentHash != paymentHash)
|
||||
{
|
||||
DebugLog($"NormalizePayment: unknown payment hash={paymentHash} payment.id={payment.id}");
|
||||
return null;
|
||||
}
|
||||
|
||||
recordedAmount ??= record.Amount;
|
||||
var amount = recordedAmount ?? boltAmount;
|
||||
if (boltAmount is not null && recordedAmount is not null && boltAmount != recordedAmount)
|
||||
{
|
||||
DebugLog($"NormalizePayment: bolt amount {boltAmount.ToUnit(LightMoneyUnit.Satoshi)} != recorded {recordedAmount.ToUnit(LightMoneyUnit.Satoshi)} for hash={paymentHash}");
|
||||
}
|
||||
|
||||
if (amount is null)
|
||||
{
|
||||
// If we can't prove the amount from the BOLT11 or stored record, reject the payment.
|
||||
DebugLog($"NormalizePayment: missing amount for hash={paymentHash} bolt11={Shorten(bolt11)}");
|
||||
return null;
|
||||
}
|
||||
var fee = feeAmount;
|
||||
|
||||
return new NormalizedPayment
|
||||
{
|
||||
Id = paymentHash ?? bolt11 ?? payment.id,
|
||||
PaymentType = payment.paymentType,
|
||||
Status = payment.status,
|
||||
Timestamp = payment.timestamp,
|
||||
Amount = amount,
|
||||
Fee = fee,
|
||||
Description = description ?? bolt11
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_sdk?.Dispose();
|
||||
}
|
||||
|
||||
public class BreezSparkInvoiceListener : ILightningInvoiceListener
|
||||
{
|
||||
private readonly BreezSparkLightningClient _breezLightningClient;
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
private readonly ConcurrentQueue<Payment> _invoices = new();
|
||||
|
||||
public BreezSparkInvoiceListener(BreezSparkLightningClient breezLightningClient, CancellationToken cancellationToken)
|
||||
{
|
||||
_breezLightningClient = breezLightningClient;
|
||||
_cancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> WaitInvoice(CancellationToken cancellation)
|
||||
{
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, cancellation);
|
||||
|
||||
while (!linkedCts.Token.IsCancellationRequested)
|
||||
{
|
||||
// Check the client's payment notification queue
|
||||
if (_breezLightningClient._paymentNotifications.TryDequeue(out var payment))
|
||||
{
|
||||
var invoice = _breezLightningClient.FromPayment(payment);
|
||||
if (invoice is not null)
|
||||
{
|
||||
_breezLightningClient.DebugLog($"WaitInvoice: dequeued payment.id={payment.id} hash={invoice.PaymentHash} bolt11={_breezLightningClient.Shorten(invoice.BOLT11)} status={payment.status} raw_msat={payment.amount} fee_msat={payment.fees}");
|
||||
// Force amount to the recorded invoice amount (BOLT11 truth) before handing to BTCPay
|
||||
var rec = _breezLightningClient.LookupInvoice(invoice.BOLT11, invoice.PaymentHash);
|
||||
if (rec is not null)
|
||||
{
|
||||
invoice.Amount = rec.Amount;
|
||||
invoice.AmountReceived = rec.Amount;
|
||||
_breezLightningClient.DebugLog($"WaitInvoice: normalized invoice amount to recorded {rec.Amount.ToUnit(LightMoneyUnit.Satoshi)} sats for hash={invoice.PaymentHash}");
|
||||
}
|
||||
return invoice;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check the local queue for backwards compatibility
|
||||
if (_invoices.TryDequeue(out var payment2))
|
||||
{
|
||||
var invoice = _breezLightningClient.FromPayment(payment2);
|
||||
if (invoice is not null)
|
||||
{
|
||||
_breezLightningClient.DebugLog($"WaitInvoice(local): dequeued payment.id={payment2.id} hash={invoice.PaymentHash} bolt11={_breezLightningClient.Shorten(invoice.BOLT11)} status={payment2.status} raw_msat={payment2.amount} fee_msat={payment2.fees}");
|
||||
var rec = _breezLightningClient.LookupInvoice(invoice.BOLT11, invoice.PaymentHash);
|
||||
if (rec is not null)
|
||||
{
|
||||
invoice.Amount = rec.Amount;
|
||||
invoice.AmountReceived = rec.Amount;
|
||||
_breezLightningClient.DebugLog($"WaitInvoice: normalized (local queue) invoice amount to recorded {rec.Amount.ToUnit(LightMoneyUnit.Satoshi)} sats for hash={invoice.PaymentHash}");
|
||||
}
|
||||
return invoice;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(1000, linkedCts.Token); // Check every second
|
||||
}
|
||||
|
||||
linkedCts.Token.ThrowIfCancellationRequested();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MonitorPaymentEvents()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get all payments and check for new paid ones
|
||||
var payments = await _sdk.ListPayments(new ListPaymentsRequest(
|
||||
typeFilter: new List<PaymentType> { PaymentType.Receive }
|
||||
));
|
||||
|
||||
foreach (var payment in payments.payments)
|
||||
{
|
||||
// If payment is complete, add it to the notification queue
|
||||
if (payment.status == PaymentStatus.Completed &&
|
||||
TryMarkPaymentSeen(payment) &&
|
||||
IsKnownPayment(payment))
|
||||
{
|
||||
DebugLogObject("MonitorPaymentEvents:payment", payment);
|
||||
LogCompletedPayment(payment);
|
||||
_paymentNotifications.Enqueue(payment);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(5000); // Poll every 5 seconds
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log error but continue monitoring
|
||||
Console.WriteLine($"Error monitoring BreezSpark payments: {ex.Message}");
|
||||
await Task.Delay(10000); // Wait longer on error
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"BreezSpark payment monitoring stopped: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void AddPaymentNotification(Payment payment)
|
||||
{
|
||||
if (TryMarkPaymentSeen(payment) &&
|
||||
IsKnownPayment(payment))
|
||||
{
|
||||
DebugLogObject("AddPaymentNotification:payment", payment);
|
||||
LogCompletedPayment(payment);
|
||||
_paymentNotifications.Enqueue(payment);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(LightningInvoice Invoice, long FeeSats)> CreateInvoiceWithFee(CreateInvoiceParams createInvoiceRequest, CancellationToken cancellation = default)
|
||||
{
|
||||
var description = createInvoiceRequest.Description ?? createInvoiceRequest.DescriptionHash?.ToString() ?? "Invoice";
|
||||
var amountSats = (ulong)createInvoiceRequest.Amount.ToUnit(LightMoneyUnit.Satoshi);
|
||||
var paymentMethod = new ReceivePaymentMethod.Bolt11Invoice(description, amountSats);
|
||||
var response = await _sdk.ReceivePayment(new ReceivePaymentRequest(paymentMethod));
|
||||
var feeSats = (long)response.fee;
|
||||
var invoice = FromReceivePaymentResponse(response, createInvoiceRequest.Amount);
|
||||
return (invoice, feeSats);
|
||||
}
|
||||
|
||||
private async Task<LightningInvoice?> GetInvoiceInternal(string identifier, CancellationToken cancellation)
|
||||
{
|
||||
var payment = await FindPayment(identifier, cancellation);
|
||||
if (payment is null)
|
||||
return null;
|
||||
|
||||
// Deduplicate completed payments so LightningListener doesn't try to add the same payment twice.
|
||||
if (payment.status == PaymentStatus.Completed && !TryMarkPaymentSeen(payment))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return FromPayment(payment);
|
||||
}
|
||||
|
||||
private async Task<Payment?> FindPayment(string identifier, CancellationToken cancellation)
|
||||
{
|
||||
try
|
||||
{
|
||||
var byId = await _sdk.GetPayment(new GetPaymentRequest(identifier));
|
||||
DebugLogObject("FindPayment:GetPayment", byId);
|
||||
if (byId?.payment != null && IsKnownPayment(byId.payment))
|
||||
{
|
||||
return byId.payment;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore and fallback to listing payments
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var list = await _sdk.ListPayments(new ListPaymentsRequest(
|
||||
typeFilter: new List<PaymentType> { PaymentType.Receive },
|
||||
assetFilter: new AssetFilter.Bitcoin()
|
||||
));
|
||||
DebugLogObject("FindPayment:ListPayments", list);
|
||||
|
||||
return list.payments.FirstOrDefault(p =>
|
||||
{
|
||||
if (p.details is PaymentDetails.Lightning lightning)
|
||||
{
|
||||
if (!IsKnownPayment(p))
|
||||
return false;
|
||||
|
||||
return lightning.paymentHash == identifier ||
|
||||
lightning.invoice == identifier;
|
||||
}
|
||||
|
||||
return p.id == identifier;
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void LogCompletedPayment(Payment payment)
|
||||
{
|
||||
try
|
||||
{
|
||||
string paymentHash = ExtractPaymentHash(payment);
|
||||
string bolt11 = null;
|
||||
LightMoney? boltAmount = null;
|
||||
if (payment.details is PaymentDetails.Lightning ln)
|
||||
{
|
||||
bolt11 = ln.invoice;
|
||||
if (!string.IsNullOrEmpty(bolt11) &&
|
||||
BOLT11PaymentRequest.TryParse(bolt11, out var pr, _network))
|
||||
{
|
||||
boltAmount = pr.MinimumAmount;
|
||||
}
|
||||
}
|
||||
|
||||
var record = LookupInvoice(bolt11, paymentHash);
|
||||
var recAmount = record?.Amount.ToUnit(LightMoneyUnit.Satoshi);
|
||||
var rawAmount = payment.amount;
|
||||
var fee = payment.fees;
|
||||
var grossSat = InferAmountFromPayment(payment).ToUnit(LightMoneyUnit.Satoshi) +
|
||||
GetFeeFromPayment(payment).ToUnit(LightMoneyUnit.Satoshi);
|
||||
var boltSat = boltAmount?.ToUnit(LightMoneyUnit.Satoshi);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best-effort logging
|
||||
}
|
||||
}
|
||||
|
||||
private string Shorten(string? s, int head = 6, int tail = 6)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s))
|
||||
return string.Empty;
|
||||
if (s.Length <= head + tail + 3)
|
||||
return s;
|
||||
return $"{s.Substring(0, head)}...{s.Substring(s.Length - tail)}";
|
||||
}
|
||||
}
|
||||
|
||||
public class NormalizedPayment
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public PaymentType PaymentType { get; set; }
|
||||
public PaymentStatus Status { get; set; }
|
||||
public ulong Timestamp { get; set; }
|
||||
public LightMoney Amount { get; set; } = LightMoney.Zero;
|
||||
public LightMoney Fee { get; set; } = LightMoney.Zero;
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Plugins.BreezSpark;
|
||||
|
||||
public class BreezSparkLightningConnectionStringHandler : ILightningConnectionStringHandler
|
||||
{
|
||||
private readonly BreezSparkService _breezService;
|
||||
|
||||
public BreezSparkLightningConnectionStringHandler(BreezSparkService breezService)
|
||||
{
|
||||
_breezService = breezService;
|
||||
}
|
||||
public ILightningClient? Create(string connectionString, Network network, out string? error)
|
||||
{
|
||||
var kv = LightningConnectionStringHelper.ExtractValues(connectionString, out var type);
|
||||
if (type != "breezspark")
|
||||
{
|
||||
error = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if (!kv.TryGetValue("key", out var key))
|
||||
{
|
||||
error = $"The key 'key' is mandatory for breezspark connection strings";
|
||||
return null;
|
||||
}
|
||||
|
||||
error = null;
|
||||
return _breezService.GetClientByPaymentKey(key);
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Plugins.BreezSpark
|
||||
{
|
||||
public class BreezSparkPaymentMethodConfig : LightningPaymentMethodConfig
|
||||
{
|
||||
public string PaymentKey { get; set; } = string.Empty;
|
||||
public string StoreId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class BreezSparkPaymentMethodHandler : IPaymentMethodHandler, ILightningPaymentHandler
|
||||
{
|
||||
private readonly BreezSparkService _breezService;
|
||||
private readonly PaymentMethodId _paymentMethodId;
|
||||
private readonly BTCPayNetwork _network;
|
||||
private readonly LightningClientFactoryService _lightningClientFactory;
|
||||
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
|
||||
public JsonSerializer Serializer { get; }
|
||||
|
||||
public BreezSparkPaymentMethodHandler(
|
||||
BreezSparkService breezService,
|
||||
BTCPayNetwork network,
|
||||
LightningClientFactoryService lightningClientFactory,
|
||||
IOptions<LightningNetworkOptions> lightningNetworkOptions)
|
||||
{
|
||||
_breezService = breezService;
|
||||
_network = network;
|
||||
_lightningClientFactory = lightningClientFactory;
|
||||
_lightningNetworkOptions = lightningNetworkOptions;
|
||||
_paymentMethodId = PaymentMethodId.Parse("BTC-BreezSpark");
|
||||
Serializer = BlobSerializer.CreateSerializer(network.NBitcoinNetwork).Serializer;
|
||||
}
|
||||
|
||||
public PaymentMethodId PaymentMethodId => _paymentMethodId;
|
||||
|
||||
public BTCPayNetwork Network => _network;
|
||||
|
||||
public Task BeforeFetchingRates(PaymentMethodContext context)
|
||||
{
|
||||
context.Prompt.Currency = _network.CryptoCode;
|
||||
context.Prompt.PaymentMethodFee = 0m;
|
||||
context.Prompt.Divisibility = 11;
|
||||
context.Prompt.RateDivisibility = 8;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task ConfigurePrompt(PaymentMethodContext context)
|
||||
{
|
||||
if (context.InvoiceEntity.Type == InvoiceType.TopUp)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException("BreezSpark Lightning Network payment method is not available for top-up invoices");
|
||||
}
|
||||
|
||||
var paymentPrompt = context.Prompt;
|
||||
var storeBlob = context.StoreBlob;
|
||||
var store = context.Store;
|
||||
|
||||
// Parse BreezSpark-specific config
|
||||
var breezConfig = ParsePaymentMethodConfig(context.PaymentMethodConfig);
|
||||
if (breezConfig == null || string.IsNullOrEmpty(breezConfig.PaymentKey))
|
||||
{
|
||||
throw new PaymentMethodUnavailableException("BreezSpark payment key is not configured");
|
||||
}
|
||||
|
||||
// Get BreezSpark client
|
||||
var breezClient = _breezService.GetClient(breezConfig.StoreId);
|
||||
if (breezClient == null)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException("BreezSpark client is not available for this store");
|
||||
}
|
||||
|
||||
var invoice = context.InvoiceEntity;
|
||||
decimal due = paymentPrompt.Calculate().Due;
|
||||
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
||||
if (expiry < TimeSpan.Zero)
|
||||
expiry = TimeSpan.FromSeconds(1);
|
||||
|
||||
LightningInvoice lightningInvoice;
|
||||
string description = storeBlob.LightningDescriptionTemplate;
|
||||
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{ItemDescription}", invoice.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{OrderId}", invoice.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
try
|
||||
{
|
||||
var request = new CreateInvoiceParams(
|
||||
new LightMoney(due, LightMoneyUnit.BTC),
|
||||
description,
|
||||
expiry);
|
||||
request.PrivateRouteHints = storeBlob.LightningPrivateRouteHints;
|
||||
|
||||
lightningInvoice = await breezClient.CreateInvoice(request, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"Impossible to create BreezSpark lightning invoice ({ex.Message})", ex);
|
||||
}
|
||||
|
||||
paymentPrompt.Destination = lightningInvoice.BOLT11;
|
||||
var details = new LigthningPaymentPromptDetails
|
||||
{
|
||||
PaymentHash = lightningInvoice.GetPaymentHash(_network.NBitcoinNetwork),
|
||||
Preimage = string.IsNullOrEmpty(lightningInvoice.Preimage) ? null : uint256.Parse(lightningInvoice.Preimage),
|
||||
InvoiceId = lightningInvoice.Id,
|
||||
NodeInfo = "BreezSpark Lightning Wallet"
|
||||
};
|
||||
paymentPrompt.Details = JObject.FromObject(details, Serializer);
|
||||
}
|
||||
|
||||
public BreezSparkPaymentMethodConfig ParsePaymentMethodConfig(JToken config)
|
||||
{
|
||||
return config.ToObject<BreezSparkPaymentMethodConfig>(Serializer) ?? new BreezSparkPaymentMethodConfig();
|
||||
}
|
||||
|
||||
object IPaymentMethodHandler.ParsePaymentMethodConfig(JToken config)
|
||||
{
|
||||
return ParsePaymentMethodConfig(config);
|
||||
}
|
||||
|
||||
public Task<ILightningClient?> CreateLightningClient(LightningPaymentMethodConfig config)
|
||||
{
|
||||
var breezConfig = config as BreezSparkPaymentMethodConfig;
|
||||
if (breezConfig == null || string.IsNullOrEmpty(breezConfig.StoreId))
|
||||
return Task.FromResult<ILightningClient?>(null);
|
||||
|
||||
return Task.FromResult<ILightningClient?>(_breezService.GetClient(breezConfig.StoreId));
|
||||
}
|
||||
|
||||
public object ParsePaymentPromptDetails(JToken details)
|
||||
{
|
||||
return details.ToObject<LigthningPaymentPromptDetails>(Serializer);
|
||||
}
|
||||
|
||||
public LightningPaymentData ParsePaymentDetails(JToken details)
|
||||
{
|
||||
return details.ToObject<LightningPaymentData>(Serializer);
|
||||
}
|
||||
|
||||
object IPaymentMethodHandler.ParsePaymentDetails(JToken details)
|
||||
{
|
||||
return ParsePaymentDetails(details);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
#nullable enable
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Plugins.BreezSpark
|
||||
{
|
||||
public class BreezSparkPlugin : BaseBTCPayServerPlugin
|
||||
{
|
||||
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
|
||||
{
|
||||
new() { Identifier = nameof(BTCPayServer), Condition = ">=2.2.0" }
|
||||
};
|
||||
|
||||
public override void Execute(IServiceCollection applicationBuilder)
|
||||
{
|
||||
applicationBuilder.AddSingleton<BreezSparkService>();
|
||||
applicationBuilder.AddSingleton<IHostedService>(provider => provider.GetRequiredService<BreezSparkService>());
|
||||
applicationBuilder.AddSingleton<BreezSparkLightningConnectionStringHandler>();
|
||||
applicationBuilder.AddSingleton<ILightningConnectionStringHandler>(provider => provider.GetRequiredService<BreezSparkLightningConnectionStringHandler>());
|
||||
|
||||
// Register the BreezSpark payment method handler
|
||||
applicationBuilder.AddSingleton<IPaymentMethodHandler>(provider =>
|
||||
{
|
||||
var breezService = provider.GetRequiredService<BreezSparkService>();
|
||||
var networkProvider = provider.GetRequiredService<BTCPayNetworkProvider>();
|
||||
var lightningClientFactory = provider.GetRequiredService<LightningClientFactoryService>();
|
||||
var lightningNetworkOptions = provider.GetRequiredService<IOptions<Configuration.LightningNetworkOptions>>();
|
||||
|
||||
return new BreezSparkPaymentMethodHandler(
|
||||
breezService,
|
||||
networkProvider.GetNetwork<BTCPayNetwork>("BTC"),
|
||||
lightningClientFactory,
|
||||
lightningNetworkOptions);
|
||||
});
|
||||
|
||||
// Add UI extensions for lightning setup tab (like Boltz does)
|
||||
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("BreezSpark/LNPaymentMethodSetupTab",
|
||||
"ln-payment-method-setup-tab"));
|
||||
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("BreezSpark/LNPaymentMethodSetupTabhead",
|
||||
"ln-payment-method-setup-tabhead"));
|
||||
|
||||
// Surface BreezSpark navigation inside the store integrations nav, matching the plugin template pattern.
|
||||
applicationBuilder.AddUIExtension("store-integrations-nav", "BreezSpark/BreezSparkNav");
|
||||
|
||||
base.Execute(applicationBuilder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Plugins.BreezSpark;
|
||||
|
||||
public class BreezSparkService:EventHostedServiceBase
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly IOptions<DataDirectories> _dataDirectories;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary => _serviceProvider.GetRequiredService<PaymentMethodHandlerDictionary>();
|
||||
private readonly ILogger _logger;
|
||||
private Dictionary<string, BreezSparkSettings> _settings = new();
|
||||
private Dictionary<string, BreezSparkLightningClient> _clients = new();
|
||||
|
||||
public BreezSparkService(
|
||||
EventAggregator eventAggregator,
|
||||
StoreRepository storeRepository,
|
||||
IOptions<DataDirectories> dataDirectories,
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<BreezSparkService> logger) : base(eventAggregator, logger)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_dataDirectories = dataDirectories;
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
base.SubscribeToEvents();
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
await base.ProcessEvent(evt, cancellationToken);
|
||||
}
|
||||
|
||||
public string GetWorkDir(string storeId)
|
||||
{
|
||||
var dir = _dataDirectories.Value.DataDir;
|
||||
return Path.Combine(dir, "Plugins", "BreezSpark",storeId);
|
||||
}
|
||||
|
||||
TaskCompletionSource tcs = new();
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_settings = (await _storeRepository.GetSettingsAsync<BreezSparkSettings>("BreezSpark")).Where(pair => pair.Value is not null).ToDictionary(pair => pair.Key, pair => pair.Value!);
|
||||
foreach (var keyValuePair in _settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
await Handle(keyValuePair.Key, keyValuePair.Value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
tcs.TrySetResult();
|
||||
await base.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<BreezSparkSettings?> Get(string storeId)
|
||||
{
|
||||
await tcs.Task;
|
||||
_settings.TryGetValue(storeId, out var settings);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
public async Task<BreezSparkLightningClient?> Handle(string? storeId, BreezSparkSettings? settings)
|
||||
{
|
||||
if (settings is null)
|
||||
{
|
||||
if (storeId is not null && _clients.Remove(storeId, out var client))
|
||||
{
|
||||
client.Dispose();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var network = Network.Main;
|
||||
var dir = GetWorkDir(storeId);
|
||||
Directory.CreateDirectory(dir);
|
||||
settings.PaymentKey ??= Guid.NewGuid().ToString();
|
||||
|
||||
var client = await BreezSparkLightningClient.Create(
|
||||
settings.ApiKey,
|
||||
dir,
|
||||
network,
|
||||
new Mnemonic(settings.Mnemonic),
|
||||
settings.PaymentKey
|
||||
);
|
||||
|
||||
if (storeId is not null)
|
||||
{
|
||||
_clients.AddOrReplace(storeId, client);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Could not create BreezSpark client");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task Set(string storeId, BreezSparkSettings? settings)
|
||||
{
|
||||
|
||||
var result = await Handle(storeId, settings);
|
||||
await _storeRepository.UpdateSetting(storeId, "BreezSpark", settings!);
|
||||
if (settings is null)
|
||||
{
|
||||
_settings.Remove(storeId, out var oldSettings );
|
||||
var data = await _storeRepository.FindStore(storeId);
|
||||
if (data != null)
|
||||
{
|
||||
var pmi = new PaymentMethodId("BTC-LN");
|
||||
// In v2.2.1, the payment methods are handled differently
|
||||
// We'll skip this for now as it needs to be refactored completely
|
||||
// TODO: Implement proper v2.2.1 payment method handling
|
||||
}
|
||||
Directory.Delete(GetWorkDir(storeId), true);
|
||||
|
||||
}
|
||||
else if(result is not null )
|
||||
{
|
||||
_settings.AddOrReplace(storeId, settings);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public new async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_clients.Values.ToList().ForEach(c => c.Dispose());
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public BreezSparkLightningClient? GetClient(string? storeId)
|
||||
{
|
||||
|
||||
tcs.Task.GetAwaiter().GetResult();
|
||||
if(storeId is null)
|
||||
return null;
|
||||
_clients.TryGetValue(storeId, out var client);
|
||||
return client;
|
||||
}
|
||||
public BreezSparkLightningClient? GetClientByPaymentKey(string? paymentKey)
|
||||
{
|
||||
tcs.Task.GetAwaiter().GetResult();
|
||||
if(paymentKey is null)
|
||||
return null;
|
||||
var match = _settings.FirstOrDefault(pair => pair.Value.PaymentKey == paymentKey).Key;
|
||||
return GetClient(match);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.BreezSpark;
|
||||
|
||||
public class BreezSparkSettings
|
||||
{
|
||||
public string? Mnemonic { get; set; }
|
||||
public string? ApiKey { get; set; }
|
||||
|
||||
public string PaymentKey { get; set; } = Guid.NewGuid().ToString();
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Plugins.BreezSpark;
|
||||
|
||||
public class InvoiceRecord
|
||||
{
|
||||
public string PaymentHash { get; set; } = string.Empty;
|
||||
public string Bolt11 { get; set; } = string.Empty;
|
||||
public LightMoney Amount { get; set; } = LightMoney.Zero;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
@using BTCPayServer
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Plugins.BreezSpark
|
||||
@model BTCPayServer.Plugins.BreezSpark.BreezSparkSettings?
|
||||
@inject BreezSparkService BreezService
|
||||
@{
|
||||
ViewData.SetActivePage("Breez", "Configure", "Configure");
|
||||
var storeId = Context.GetCurrentStoreId();
|
||||
var active = (await BreezService.Get(storeId)) is not null;
|
||||
}
|
||||
<form method="post" asp-action="Configure" asp-controller="BreezSpark" asp-route-storeId="@storeId" enctype="multipart/form-data">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 class="mb-0">
|
||||
<span>@ViewData["Title"]</span>
|
||||
</h3>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<button name="command" type="submit" value="save" class="btn btn-primary">Save</button>
|
||||
@if (active)
|
||||
{
|
||||
<button name="command" type="submit" value="clear" class="btn btn-danger">Clear</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Mnemonic" class="form-label">Mnemonic</label>
|
||||
<input value="@Model?.Mnemonic" asp-for="Mnemonic" class="form-control" type="password" disabled="@active"/>
|
||||
<span asp-validation-for="Mnemonic" class="text-danger"></span>
|
||||
<span class="text-muted">A Bitcoin 12-word mnemonic seed phrase.<strong>BACK THIS UP SAFELY! GENERATE IT RANDOMLY! SERVER ADMINS HAVE ACCESS TO THIS!</strong></span>
|
||||
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ApiKey" class="form-label">Breez API Key</label>
|
||||
<input asp-for="ApiKey" class="form-control" placeholder="Defaults to public key if left empty"/>
|
||||
<span asp-validation-for="ApiKey" class="text-danger"></span>
|
||||
<span class="text-muted">Optional. Leave blank to use the default Breez API key.</span>
|
||||
</div>
|
||||
|
||||
<input type="hidden" asp-for="PaymentKey"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,106 +0,0 @@
|
||||
@using Breez.Sdk.Spark
|
||||
@using BTCPayServer
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Components.QRCode
|
||||
@using BTCPayServer.Components.TruncateCenter
|
||||
@using BTCPayServer.Models.StoreViewModels
|
||||
@using BTCPayServer.Payments
|
||||
@using BTCPayServer.Plugins.BreezSpark
|
||||
@using BTCPayServer.Security
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using NBitcoin
|
||||
@inject BreezSparkService BreezService
|
||||
@inject TransactionLinkProviders TransactionLinkProviders
|
||||
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||
@{
|
||||
ViewData.SetActivePage("Breez", "Info", "Info");
|
||||
string storeId = Model switch
|
||||
{
|
||||
string s => s,
|
||||
StoreDashboardViewModel dashboardModel => dashboardModel.StoreId,
|
||||
_ => Context.GetImplicitStoreId()
|
||||
};
|
||||
var sdk = BreezService.GetClient(storeId)?.Sdk;
|
||||
if (sdk is null)
|
||||
return;
|
||||
}
|
||||
|
||||
<div class="row mb-4 mt-4">
|
||||
<div class="col-12">
|
||||
<h3>Breez Lightning Node Information</h3>
|
||||
|
||||
@try
|
||||
{
|
||||
if (sdk != null)
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Node Status</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
Breez Lightning Node is connected and operational.
|
||||
This is a simplified view for SDK v0.4.1 compatibility.
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-6">Status</dt>
|
||||
<dd class="col-sm-6">
|
||||
<span class="badge bg-success">Connected</span>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-6">Network</dt>
|
||||
<dd class="col-sm-6">Bitcoin</dd>
|
||||
|
||||
<dt class="col-sm-6">Type</dt>
|
||||
<dd class="col-sm-6">Breez SDK v0.4.1</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-6">Service</dt>
|
||||
<dd class="col-sm-6">Lightning Network</dd>
|
||||
|
||||
<dt class="col-sm-6">Integration</dt>
|
||||
<dd class="col-sm-6">BTCPay Server</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Quick Actions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<a href="@Url.Action("SwapIn", "Breez", new { storeId = storeId })" class="btn btn-primary w-100 mb-2">
|
||||
<i class="bi bi-arrow-down-circle"></i> Swap In
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<a href="@Url.Action("SwapOut", "Breez", new { storeId = storeId })" class="btn btn-success w-100 mb-2">
|
||||
<i class="bi bi-arrow-up-circle"></i> Swap Out
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<h5>Node Information Unavailable</h5>
|
||||
<p>Unable to fetch Breez node information: @ex.Message</p>
|
||||
<p class="mb-0">This may be due to SDK v0.4.1 compatibility changes.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,46 +0,0 @@
|
||||
@using BTCPayServer
|
||||
@model System.Collections.Concurrent.ConcurrentQueue<(DateTimeOffset timestamp, string log)>
|
||||
@{
|
||||
var storeId = Context.GetCurrentStoreId();
|
||||
|
||||
ViewData.SetActivePage("Breez", "Logs", "Logs");
|
||||
}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<p class="text-secondary mt-3 mb-0">
|
||||
There are no recent logs.
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th >Timestamp</th>
|
||||
<th >Log</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var log in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<span >@log.timestamp.ToTimeAgo()</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span class="text-break">@log.log</span>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,49 +0,0 @@
|
||||
@using BTCPayServer.Components
|
||||
@using BTCPayServer
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Client
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Components.QRCode
|
||||
@using BTCPayServer.Components.TruncateCenter
|
||||
@model BTCPayServer.Plugins.BreezSpark.PaymentsViewModel
|
||||
@{
|
||||
var storeId = Context.GetCurrentStoreId();
|
||||
|
||||
ViewData.SetActivePage("Breez", "Payments", "Payments");
|
||||
TempData.TryGetValue("bolt11", out var bolt11);
|
||||
}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 class="mb-0">
|
||||
<span>@ViewData["Title"]</span>
|
||||
</h3>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
|
||||
<a permission="@Policies.CanModifyStoreSettings" asp-action="Send" asp-controller="BreezSpark" asp-route-storeId="@storeId" type="submit" class="btn btn-primary">Send</a>
|
||||
<a permission="@Policies.CanCreateInvoice" asp-action="Receive" asp-controller="BreezSpark" asp-route-storeId="@storeId" type="submit" class="btn btn-primary">Receive</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (bolt11 is string bolt11s)
|
||||
{
|
||||
<div class="payment-box">
|
||||
<div class="qr-container" data-clipboard="@bolt11s">
|
||||
<vc:qr-code data="@bolt11s"/>
|
||||
</div>
|
||||
<div class="input-group mt-3">
|
||||
<div class="form-floating">
|
||||
<vc:truncate-center text="@bolt11s" padding="15" elastic="true" classes="form-control-plaintext" id="Address"/>
|
||||
<label for="Address">BOLT11 Invoice</label>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<partial name="Breez/BreezPaymentsTable" model="Model.Payments"/>
|
||||
<vc:pager view-model="Model"></vc:pager>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,64 +0,0 @@
|
||||
@using BTCPayServer.Lightning
|
||||
@using BTCPayServer.Models.StoreViewModels
|
||||
@using BTCPayServer.Plugins.BreezSpark
|
||||
@using BTCPayServer.Security
|
||||
@inject BreezSparkService BreezService
|
||||
|
||||
@{
|
||||
ViewData.SetActivePage("Breez", "Receive", "Receive");
|
||||
var storeId = Model switch
|
||||
{
|
||||
string s => s,
|
||||
StoreDashboardViewModel dashboardModel => dashboardModel.StoreId,
|
||||
_ => Context.GetImplicitStoreId()
|
||||
};
|
||||
var sdk = BreezService.GetClient(storeId)?.Sdk;
|
||||
if (sdk is null)
|
||||
return;
|
||||
|
||||
var nodeInfo = new Breez.Sdk.Spark.GetInfoRequest(ensureSynced: false);
|
||||
Breez.Sdk.Spark.GetInfoResponse? infoResponse = null;
|
||||
try
|
||||
{
|
||||
infoResponse = await sdk.GetInfo(nodeInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h5 class="alert-heading">Error</h5>
|
||||
<p class="mb-0">Unable to fetch node information: @ex.Message</p>
|
||||
</div>
|
||||
return;
|
||||
}
|
||||
|
||||
var max = LightMoney.Satoshis((long)(infoResponse?.balanceSats ?? 0));
|
||||
|
||||
}
|
||||
|
||||
|
||||
<form method="post" asp-action="Receive" asp-route-storeId="@storeId">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 class="mb-0">
|
||||
<span>@ViewData["Title"]</span>
|
||||
</h3>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
|
||||
<button type="submit" class="btn btn-primary">Create Invoice</button>
|
||||
</div>
|
||||
</div>
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label for="amount" class="form-label">Amount (sats)</label>
|
||||
<input type="number" id="amount" min="1" max="@max.ToUnit(LightMoneyUnit.Satoshi)" name="amount" class="form-control" placeholder="Optional: leave empty for amountless invoice"/>
|
||||
<small class="form-text text-muted">Maximum receivable: @max.ToUnit(LightMoneyUnit.Satoshi) sats</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<input type="text" id="description" name="description" class="form-control" placeholder="Payment description"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,100 +0,0 @@
|
||||
@using System.Numerics
|
||||
@using BTCPayServer.Lightning
|
||||
@using BTCPayServer.Models.StoreViewModels
|
||||
@using BTCPayServer.Plugins.BreezSpark
|
||||
@using BTCPayServer.Security
|
||||
@inject BreezSparkService BreezService
|
||||
|
||||
@{
|
||||
ViewData.SetActivePage("Breez", "Send", "Send");
|
||||
var storeId = Model switch
|
||||
{
|
||||
string s => s,
|
||||
StoreDashboardViewModel dashboardModel => dashboardModel.StoreId,
|
||||
_ => Context.GetImplicitStoreId()
|
||||
};
|
||||
var sdk = BreezService.GetClient(storeId)?.Sdk;
|
||||
if (sdk is null)
|
||||
return;
|
||||
|
||||
var nodeInfo = new Breez.Sdk.Spark.GetInfoRequest(ensureSynced: false);
|
||||
Breez.Sdk.Spark.GetInfoResponse? infoResponse = null;
|
||||
try
|
||||
{
|
||||
infoResponse = await sdk.GetInfo(nodeInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h5 class="alert-heading">Error</h5>
|
||||
<p class="mb-0">Unable to fetch node information: @ex.Message</p>
|
||||
</div>
|
||||
return;
|
||||
}
|
||||
|
||||
var max = LightMoney.Satoshis((long)(infoResponse?.balanceSats ?? 0)).ToUnit(LightMoneyUnit.Satoshi);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@if (ViewData["PaymentDetails"] is PaymentDetailsViewModel paymentDetails)
|
||||
{
|
||||
<div class="payment-details mb-4">
|
||||
<h4>Payment Details</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Destination:</strong> @paymentDetails.Destination</p>
|
||||
<p><strong>Amount:</strong> @paymentDetails.Amount sats</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Fee:</strong> @paymentDetails.Fee sats</p>
|
||||
<p><strong>Total:</strong> @(paymentDetails.Amount + paymentDetails.Fee) sats</p>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" asp-action="ConfirmSend" asp-route-storeId="@storeId">
|
||||
<input type="hidden" name="paymentRequest" value="@paymentDetails.Destination" />
|
||||
<input type="hidden" name="amount" value="@paymentDetails.Amount" />
|
||||
<input type="hidden" name="prepareResponse" value="@paymentDetails.PrepareResponseJson" />
|
||||
<button type="submit" class="btn btn-success">Confirm and Send</button>
|
||||
<a href="javascript:history.back()" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" asp-action="PrepareSend" asp-route-storeId="@storeId">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 class="mb-0">
|
||||
<span>@ViewData["Title"]</span>
|
||||
</h3>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<button type="submit" class="btn btn-primary">Prepare Payment</button>
|
||||
</div>
|
||||
</div>
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label for="address" class="form-label" data-required>Bolt11 or Bitcoin Address</label>
|
||||
<input type="text" id="address" name="address" class="form-control" required placeholder="lnbc... or bc1..."/>
|
||||
<small class="form-text text-muted">Enter a Lightning bolt11 invoice or a Bitcoin address</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="amount" class="form-label">Amount (sats)</label>
|
||||
<input type="number" id="amount" name="amount" min="1" max="@max" class="form-control" placeholder="Amount in satoshis"/>
|
||||
<small class="form-text text-muted">Maximum payable: @max sats</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
@functions {
|
||||
public class PaymentDetailsViewModel
|
||||
{
|
||||
public string Destination { get; set; }
|
||||
public long Amount { get; set; }
|
||||
public long Fee { get; set; }
|
||||
public string PrepareResponseJson { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
@using Breez.Sdk.Spark
|
||||
@using BTCPayServer
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Components.QRCode
|
||||
@using BTCPayServer.Components.TruncateCenter
|
||||
@using BTCPayServer.Models.StoreViewModels
|
||||
@using BTCPayServer.Payments
|
||||
@using BTCPayServer.Plugins.BreezSpark
|
||||
@using BTCPayServer.Security
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using NBitcoin
|
||||
@inject BreezSparkService BreezService
|
||||
@inject TransactionLinkProviders TransactionLinkProviders
|
||||
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||
@inject BTCPayNetworkProvider BTCPayNetworkProvider
|
||||
@{
|
||||
ViewData.SetActivePage("Breez", "Swap In", "SwapIn");
|
||||
var pmi = PaymentMethodId.Parse("BTC-OnChain");
|
||||
string storeId = Model switch
|
||||
{
|
||||
string s => s,
|
||||
StoreDashboardViewModel dashboardModel => dashboardModel.StoreId,
|
||||
_ => Context.GetImplicitStoreId()
|
||||
};
|
||||
var sdk = BreezService.GetClient(storeId)?.Sdk;
|
||||
if (sdk is null)
|
||||
return;
|
||||
|
||||
// Get current receive payment swap info (if any)
|
||||
SwapInfo? currentSwap = null;
|
||||
try
|
||||
{
|
||||
// Simplified logic - skip complex payment lookup for now
|
||||
Payment? pendingReceive = null;
|
||||
|
||||
// Skip pending receive logic for SDK v0.4.1 compatibility
|
||||
|
||||
// If no pending swap, create a new one
|
||||
if (currentSwap == null)
|
||||
{
|
||||
var request = new ReceivePaymentRequest(
|
||||
paymentMethod: new ReceivePaymentMethod.BitcoinAddress()
|
||||
);
|
||||
var response = await sdk.ReceivePayment(request: request);
|
||||
|
||||
// Create swap info from response
|
||||
currentSwap = new SwapInfo
|
||||
{
|
||||
bitcoinAddress = response.paymentRequest,
|
||||
minAllowedDeposit = 1000, // Default minimum
|
||||
maxAllowedDeposit = 16777215, // Default maximum (~0.16 BTC)
|
||||
status = "Created"
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h5 class="alert-heading">Error</h5>
|
||||
<p class="mb-0">Failed to initialize swap-in: @ex.Message</p>
|
||||
</div>
|
||||
return;
|
||||
}
|
||||
|
||||
// Get refundable deposits using the new SDK pattern
|
||||
// TODO: Fix for v2.2.1 - DepositInfo structure needs to be updated
|
||||
// List<DepositInfo>? refundables = null;
|
||||
// try
|
||||
// {
|
||||
// var request = new ListUnclaimedDepositsRequest();
|
||||
// var response = await sdk.ListUnclaimedDeposits(request);
|
||||
// refundables = response.deposits.ToList();
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// <div class="alert alert-warning" role="alert">
|
||||
// <small>Unable to fetch refundable swaps: @ex.Message</small>
|
||||
// </div>
|
||||
// }
|
||||
|
||||
@* TODO: Fix for v2.2.1 - derivation settings check needed *@
|
||||
|
||||
// NodeState API has changed in SDK v0.4.1 - using GetInfo instead
|
||||
var nodeInfo = new Breez.Sdk.Spark.GetInfoRequest(ensureSynced: false);
|
||||
Breez.Sdk.Spark.GetInfoResponse? ni = null;
|
||||
try
|
||||
{
|
||||
ni = await sdk.GetInfo(nodeInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<small>Unable to fetch node info: @ex.Message</small>
|
||||
</div>
|
||||
}
|
||||
|
||||
// Use standard fee rates since RecommendedFees API may have changed
|
||||
var hasFeeRates = true; // Always show fee options
|
||||
|
||||
// Get explorer URL for transaction links
|
||||
var network = BTCPayNetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
|
||||
var explorerUrl = network.BlockExplorerLink?.ToString() ?? "#";
|
||||
}
|
||||
|
||||
@if (hasFeeRates)
|
||||
{
|
||||
<datalist id="fees">
|
||||
<!-- Using standard fee rates as fallback since SDK properties may have different names -->
|
||||
<option value="10">Fastest fee (10 sat/vB)</option>
|
||||
<option value="5">Hour fee (5 sat/vB)</option>
|
||||
<option value="2">Economic fee (2 sat/vB)</option>
|
||||
<option value="1">Minimum fee (1 sat/vB)</option>
|
||||
</datalist>
|
||||
}
|
||||
|
||||
<div class="row mb-4 mt-4">
|
||||
<div class="col-12">
|
||||
|
||||
@if (currentSwap != null)
|
||||
{
|
||||
<div class="payment-box">
|
||||
<div class="qr-container" data-clipboard="@currentSwap.bitcoinAddress">
|
||||
<vc:qr-code data="@currentSwap.bitcoinAddress"/>
|
||||
</div>
|
||||
<div class="input-group mt-3">
|
||||
<div class="form-floating">
|
||||
<vc:truncate-center text="@currentSwap.bitcoinAddress" padding="15" elastic="true" classes="form-control-plaintext" id="Address"/>
|
||||
<label for="Address">Address</label>
|
||||
</div>
|
||||
<div class="w-100">
|
||||
<span class="text-muted">Please send an amount between <br/> @Money.Satoshis(currentSwap.minAllowedDeposit).ToDecimal(MoneyUnit.BTC) and @Money.Satoshis(currentSwap.maxAllowedDeposit).ToDecimal(MoneyUnit.BTC)BTC </span>
|
||||
@* TODO: Fix for v2.2.1 - derivation settings check needed *@
|
||||
@*
|
||||
@if (derivationSettings != null)
|
||||
{
|
||||
<button type="button" class="btn btn-primary" id="copy-address" data-clipboard="@currentSwap.bitcoinAddress">
|
||||
<span class="d-none d-md-inline">Copy Address</span>
|
||||
<span class="d-md-none">Copy</span>
|
||||
</button>
|
||||
}
|
||||
*@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@*
|
||||
@if (refundables?.Any() is true)
|
||||
{
|
||||
<div class="table-responsive mt-4">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Deposit Tx</th>
|
||||
<th>Amount</th>
|
||||
<th>Swap Tx</th>
|
||||
<th>Refundable</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var deposit in refundables)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
@{
|
||||
var txLink = TransactionLinkProviders.GetTransactionLink(deposit.txId, "BTC");
|
||||
}
|
||||
<a href="@txLink" target="_blank" rel="noreferrer noopener">
|
||||
<vc:truncate-center text="@deposit.txId" padding="10" elastic="true" />
|
||||
</a>
|
||||
</td>
|
||||
<td>@deposit.amount</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(deposit.swapTxId))
|
||||
{
|
||||
var swapTxLink = TransactionLinkProviders.GetTransactionLink(deposit.swapTxId, "BTC");
|
||||
<a href="@swapTxLink" target="_blank" rel="noreferrer noopener">
|
||||
<vc:truncate-center text="@deposit.swapTxId" padding="10" elastic="true" />
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>N/A</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (deposit.refundable)
|
||||
{
|
||||
<span class="text-success">Yes</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">No</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<form asp-action="SwapInRefund" asp-route-storeId="@storeId" method="get">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary">Refund</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
*@
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,73 +0,0 @@
|
||||
@using BTCPayServer
|
||||
@using BTCPayServer.Plugins.BreezSpark
|
||||
@using BTCPayServer.Security
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using Microsoft.AspNetCore.Routing
|
||||
@model string
|
||||
@inject BreezSparkService BreezService
|
||||
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||
@* @inject BTCPayNetworkProvider BTCPayNetworkProvider *@
|
||||
@{
|
||||
var storeId = Context.GetImplicitStoreId();
|
||||
var address = Context.GetRouteValue("address").ToString();
|
||||
ViewData.SetActivePage("Breez", "Create Swapin Refund", "SwapIn");
|
||||
|
||||
@* TODO: Fix for v2.2.1 - derivation settings check needed *@
|
||||
var sdk = BreezService.GetClient(storeId)?.Sdk;
|
||||
// Use standard fee rates since RecommendedFees API may have changed
|
||||
var standardFees = new { fastestFee = 10, halfHourFee = 5, hourFee = 3, economyFee = 2, minimumFee = 1 };
|
||||
}
|
||||
|
||||
<datalist id="fees">
|
||||
<option value="@standardFees.fastestFee">Fastest fee (@standardFees.fastestFee sat/vB)</option>
|
||||
<option value="@standardFees.halfHourFee">Half hour fee (@standardFees.halfHourFee sat/vB)</option>
|
||||
<option value="@standardFees.hourFee">Hour fee (@standardFees.hourFee sat/vB)</option>
|
||||
<option value="@standardFees.economyFee">Economic fee (@standardFees.economyFee sat/vB)</option>
|
||||
<option value="@standardFees.minimumFee">Minimum fee (@standardFees.minimumFee sat/vB)</option>
|
||||
</datalist>
|
||||
<datalist list="addresses">
|
||||
@*
|
||||
@if (derivationSettings is not null)
|
||||
{
|
||||
<option value="store"> Store wallet</option>
|
||||
}
|
||||
*@
|
||||
</datalist>
|
||||
|
||||
<form method="post" asp-action="SwapInRefund" asp-route-storeId="@storeId" asp-route-address="@address">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 class="mb-0">
|
||||
<span>@ViewData["Title"]</span>
|
||||
<a href="https://docs.btcpayserver.org/Forms" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info"/>
|
||||
</a>
|
||||
</h3>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label for="txid" class="form-label" data-required>Transaction ID</label>
|
||||
<input type="text" id="txid" name="txid" class="form-control" required/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vout" class="form-label" data-required>Output Index</label>
|
||||
<input type="number" id="vout" name="vout" class="form-control" required/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="refundAddress" class="form-label" data-required>Refund Address</label>
|
||||
<input type="text" id="refundAddress" name="refundAddress" class="form-control" required/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="satPerByte" class="form-label">Fee Rate (sat/vB)</label>
|
||||
<input type="number" id="satPerByte" name="satPerByte" list="fees" class="form-control" value="@standardFees.halfHourFee"/>
|
||||
<small class="form-text text-muted">Choose from preset fee rates or enter custom value</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Process Refund</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,83 +0,0 @@
|
||||
@using Breez.Sdk.Spark
|
||||
@using BTCPayServer
|
||||
@using BTCPayServer.Models.StoreViewModels
|
||||
@using BTCPayServer.Plugins.BreezSpark
|
||||
@using BTCPayServer.Security
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@inject BreezSparkService BreezService
|
||||
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||
@* @inject BTCPayNetworkProvider BTCPayNetworkProvider *@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
ViewData.SetActivePage("Breez", "Swap Out", "SwapOut");
|
||||
string storeId = null;
|
||||
if (Model is string s)
|
||||
{
|
||||
storeId = s;
|
||||
}
|
||||
else if (Model is StoreDashboardViewModel dashboardModel)
|
||||
{
|
||||
storeId = dashboardModel.StoreId;
|
||||
}
|
||||
else
|
||||
{
|
||||
storeId = Context.GetImplicitStoreId();
|
||||
}
|
||||
|
||||
// Simplified fee structure for SDK v0.4.1 compatibility
|
||||
var fastFee = 10;
|
||||
var slowFee = 5;
|
||||
var minFee = 1;
|
||||
}
|
||||
|
||||
<datalist id="fees">
|
||||
<option value="@fastFee">Fastest fee (@fastFee sat/vB)</option>
|
||||
<option value="@slowFee">Half hour fee (@slowFee sat/vB)</option>
|
||||
<option value="@slowFee">Hour fee (@slowFee sat/vB)</option>
|
||||
<option value="@slowFee">Economic fee (@slowFee sat/vB)</option>
|
||||
<option value="@minFee">Minimum fee (@minFee sat/vB)</option>
|
||||
</datalist>
|
||||
|
||||
<datalist list="addresses">
|
||||
@{
|
||||
@* TODO: Fix derivation settings for v2.2.1 - GetDerivationSchemeSettings method signature changed *@
|
||||
var hasStoreWallet = false;
|
||||
if (hasStoreWallet)
|
||||
{
|
||||
<option value="store"> Store wallet</option>
|
||||
}
|
||||
}
|
||||
</datalist>
|
||||
|
||||
<form method="post" asp-action="SwapOut" asp-route-storeId="@storeId">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 class="mb-0">
|
||||
<span>@ViewData["Title"]</span>
|
||||
</h3>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label for="address" class="form-label" data-required>Address</label>
|
||||
<input type="text" id="address" list="addresses" name="address" class="form-control" required/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="satPerByte" class="form-label" data-required>Feerate (sat/vB)</label>
|
||||
<input type="number" min="@(minFee)" list="fees" id="satPerByte" name="satPerByte" class="form-control" required/>
|
||||
<small class="form-text text-muted">Choose from preset fee rates or enter custom value</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="amount" class="form-label" data-required>Amount (sats)</label>
|
||||
<input type="number" min="1000" max="10000000" id="amount" name="amount" class="form-control" required/>
|
||||
<p class="text-muted">Minimum: 1000 sats, Maximum: 10000000 sats</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Initiate Swap-Out</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,79 +0,0 @@
|
||||
@using BTCPayServer
|
||||
@using BTCPayServer.Models.StoreViewModels
|
||||
@using BTCPayServer.Plugins.BreezSpark
|
||||
@using BTCPayServer.Security
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@inject BreezSparkService BreezService
|
||||
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||
@* @inject BTCPayNetworkProvider BTCPayNetworkProvider *@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
ViewData.SetActivePage("Breez", "Sweep", "Sweep");
|
||||
string storeId = null;
|
||||
if (Model is string s)
|
||||
{
|
||||
storeId = s;
|
||||
}
|
||||
else if (Model is StoreDashboardViewModel dashboardModel)
|
||||
{
|
||||
storeId = dashboardModel.StoreId;
|
||||
}
|
||||
else
|
||||
{
|
||||
storeId = Context.GetImplicitStoreId();
|
||||
}
|
||||
var sdk = BreezService.GetClient(storeId)?.Sdk;
|
||||
if (sdk is null)
|
||||
return;
|
||||
|
||||
@* TODO: Fix for v2.2.1 - derivation settings check needed *@
|
||||
// Use standard fee rates since RecommendedFees API may have changed
|
||||
var standardFees = new { fastestFee = 10, halfHourFee = 5, hourFee = 3, economyFee = 2, minimumFee = 1 };
|
||||
}
|
||||
|
||||
<datalist id="fees">
|
||||
<option value="@standardFees.fastestFee">Fastest fee (@standardFees.fastestFee sat/vB)</option>
|
||||
<option value="@standardFees.halfHourFee">Half hour fee (@standardFees.halfHourFee sat/vB)</option>
|
||||
<option value="@standardFees.hourFee">Hour fee (@standardFees.hourFee sat/vB)</option>
|
||||
<option value="@standardFees.economyFee">Economic fee (@standardFees.economyFee sat/vB)</option>
|
||||
<option value="@standardFees.minimumFee">Minimum fee (@standardFees.minimumFee sat/vB)</option>
|
||||
</datalist>
|
||||
<datalist list="addresses">
|
||||
@*
|
||||
@if (derivationSettings is not null)
|
||||
{
|
||||
<option value="store"> Store wallet</option>
|
||||
}
|
||||
*@
|
||||
</datalist>
|
||||
|
||||
<form method="post" asp-action="Sweep" asp-route-storeId="@storeId">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 class="mb-0">
|
||||
<span>@ViewData["Title"]</span>
|
||||
<a href="https://docs.btcpayserver.org/Forms" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info"/>
|
||||
</a>
|
||||
</h3>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<button type="submit" class="btn btn-primary">Sweep Funds</button>
|
||||
</div>
|
||||
</div>
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label for="address" class="form-label" data-required>Destination Address</label>
|
||||
<input type="text" id="address" list="addresses" name="address" class="form-control" required/>
|
||||
<small class="form-text text-muted">Address to sweep funds to</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="satPerByte" class="form-label">Fee Rate (sat/vB)</label>
|
||||
<input type="number" id="satPerByte" name="satPerByte" list="fees" class="form-control" value="@standardFees.halfHourFee"/>
|
||||
<small class="form-text text-muted">Choose from preset fee rates or enter custom value</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Sweep Funds</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,90 +0,0 @@
|
||||
@using BTCPayServer.Lightning
|
||||
@using BTCPayServer.Models.StoreViewModels
|
||||
@using BTCPayServer.Plugins.BreezSpark
|
||||
@using BTCPayServer.Security
|
||||
@using BTCPayServer.Client
|
||||
@inject BreezSparkService BreezService
|
||||
@{
|
||||
string storeId = null;
|
||||
if (Model is string s)
|
||||
{
|
||||
storeId = s;
|
||||
}
|
||||
else if (Model is StoreDashboardViewModel dashboardModel)
|
||||
{
|
||||
storeId = dashboardModel.StoreId;
|
||||
}
|
||||
else
|
||||
{
|
||||
storeId = Context.GetImplicitStoreId();
|
||||
}
|
||||
|
||||
// In SDK v0.4.1, NodeState API has changed and async calls can't be made from partial views
|
||||
// This widget needs to be refactored to receive data from controller
|
||||
bool isConfigured = false;
|
||||
try
|
||||
{
|
||||
var client = BreezService.GetClient(storeId);
|
||||
isConfigured = client?.Sdk != null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Handle any errors gracefully
|
||||
}
|
||||
}
|
||||
<style>
|
||||
#Breez-Info{
|
||||
order: -3;
|
||||
}
|
||||
@@media (min-width: 1200px) {
|
||||
#Breez-Info{
|
||||
grid-column-start: 1; grid-column-end: 6; order: -3;
|
||||
}}
|
||||
</style>
|
||||
|
||||
<div class="widget store-numbers" id="Breez-Info" style="">
|
||||
@if (Model is StoreDashboardViewModel)
|
||||
{
|
||||
<header>
|
||||
<h4>Breez Node</h4>
|
||||
<a asp-action="Info" asp-controller="BreezSpark" asp-route-storeId="@storeId">
|
||||
Manage
|
||||
</a>
|
||||
|
||||
</header>
|
||||
}
|
||||
|
||||
@if (isConfigured)
|
||||
{
|
||||
<div class="d-flex w-100 align-items-center justify-content-start gap-3">
|
||||
<span class="btcpay-status btcpay-status--enabled"></span>
|
||||
<h6 class="text-truncate">Breez Spark (nodeless)</h6>
|
||||
</div>
|
||||
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>Lightning Balance</h6>
|
||||
@if (Model is StoreDashboardViewModel)
|
||||
{
|
||||
<div permission="@Policies.CanModifyStoreSettings">
|
||||
<a asp-action="SwapIn" asp-controller="BreezSpark" asp-route-storeId="@storeId">Swap In</a>
|
||||
<a asp-action="Send" asp-controller="BreezSpark" asp-route-storeId="@storeId">Send</a>
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
<div class="balance d-flex align-items-baseline gap-1">
|
||||
<span class="text-muted">Dashboard widget updated for SDK v0.4.1</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center p-3">
|
||||
<p class="text-muted">Breez node information not available</p>
|
||||
@if (string.IsNullOrEmpty(storeId))
|
||||
{
|
||||
<a asp-action="Configure" asp-controller="BreezSpark" asp-route-storeId="@storeId">Configure Breez</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -1,94 +0,0 @@
|
||||
@using Breez.Sdk.Spark
|
||||
@using BTCPayServer.Lightning
|
||||
@using BTCPayServer.Models.StoreViewModels
|
||||
@using BTCPayServer.Plugins.BreezSpark
|
||||
@using BTCPayServer.Security
|
||||
@model List<NormalizedPayment>
|
||||
@{
|
||||
var data = Model ?? new List<NormalizedPayment>();
|
||||
var storeId = Context.GetImplicitStoreId();
|
||||
if (data is null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(storeId))
|
||||
return;
|
||||
}
|
||||
|
||||
var isDashboard = false;
|
||||
}
|
||||
@if (isDashboard)
|
||||
{
|
||||
<style>
|
||||
@@media (min-width: 1200px) {
|
||||
#breez-payments{
|
||||
grid-column-start: 6; grid-column-end: 13;
|
||||
order: -3;
|
||||
}}
|
||||
</style>
|
||||
}
|
||||
<div id="breez-payments" class="@(isDashboard ? "widget store-wallet-balance" : "")">
|
||||
@if (isDashboard)
|
||||
{
|
||||
<header>
|
||||
<h3>Breez Payments</h3>
|
||||
@if (data.Any())
|
||||
{
|
||||
<a asp-controller="BreezSpark" asp-action="Payments" asp-route-storeId="@storeId">View All</a>
|
||||
}
|
||||
</header>
|
||||
}
|
||||
@if (!data.Any())
|
||||
{
|
||||
<p class="text-secondary mt-3 mb-0">
|
||||
There are no recent payments.
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-125px">Id</th>
|
||||
<th class="w-125px">Timestamp</th>
|
||||
<th class="w-125px">Type</th>
|
||||
<th class="w-125px">Amount</th>
|
||||
<th class="text-nowrap">Fee</th>
|
||||
<th class="text-nowrap">Status</th>
|
||||
<th class="text-nowrap">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in data)
|
||||
{
|
||||
<tr>
|
||||
<td class="smMaxWidth text-truncate">
|
||||
|
||||
<span >@payment.Id</span>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<span >@DateTimeOffset.FromUnixTimeSeconds((long)payment.Timestamp).ToTimeAgo() ?? "Unknown"</span>
|
||||
</td>
|
||||
<td>
|
||||
<span >@(payment.PaymentType == PaymentType.Receive ? "receive" : "send")</span>
|
||||
</td>
|
||||
<td>
|
||||
<span >@payment.Amount.ToDecimal(LightMoneyUnit.BTC) BTC</span>
|
||||
</td>
|
||||
<td>
|
||||
<span >@payment.Fee.ToDecimal(LightMoneyUnit.BTC) BTC</span>
|
||||
</td>
|
||||
<td>
|
||||
<span >@payment.Status.ToString().ToLowerInvariant()</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-break">@payment.Description</span>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -1,60 +0,0 @@
|
||||
@using Breez.Sdk
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Models.StoreViewModels
|
||||
@using BTCPayServer.Plugins.BreezSpark
|
||||
@using BTCPayServer.Security
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@inject IScopeProvider ScopeProvider
|
||||
@inject BreezSparkService BreezService
|
||||
@{
|
||||
var storeId = Model switch
|
||||
{
|
||||
string s => s,
|
||||
StoreDashboardViewModel dashboardModel => dashboardModel.StoreId,
|
||||
_ => Context.GetImplicitStoreId()
|
||||
};
|
||||
var active = @ViewData.IsActivePage("Breez");
|
||||
var client = string.IsNullOrEmpty(active) ? null : BreezService.GetClient(storeId);
|
||||
var sdk = client?.Sdk;
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(storeId))
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a permission="@Policies.CanViewStoreSettings" asp-controller="BreezSpark" asp-action="Index" asp-route-storeId="@storeId" class="nav-link @active" id="Nav-Breez">
|
||||
|
||||
<svg style="width: 15px; margin-left: 3px; margin-right: 7px;" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="favicon-64-2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group" transform="translate(3.000000, 2.000000)" fill-rule="nonzero">
|
||||
<path d="M35.4723926,39.8214286 C35.4723926,37.9541367 33.9340883,36.4285714 32.0236136,36.4285714 L10.3260796,36.4285714 C8.42801059,36.4285714 6.87730061,37.9419322 6.87730061,39.8214286 C6.87730061,41.6887205 8.41560491,43.2142857 10.3260796,43.2142857 L32.0236136,43.2142857 C33.9216827,43.2020812 35.4723926,41.6887205 35.4723926,39.8214286 Z" id="Shape" fill="currentColor"></path>
|
||||
<path d="M49.7395693,25.9068425 C51.3370507,23.7180544 52.2855553,21.0346249 52.2855553,18.1162407 C52.2855553,10.7460841 46.257559,4.77328937 38.819286,4.77328937 L36.9721981,4.77328937 L36.9721981,3.32646331 C36.9721981,1.48392415 35.462079,0 33.614991,0 C31.7554228,0 30.257784,1.49629019 30.257784,3.32646331 L30.257784,4.77328937 L26.8381753,4.77328937 L26.8381753,4.7856554 L24.6790793,4.7856554 C22.8819126,4.7856554 21.4341951,6.23248145 21.4341951,8.0008244 C21.4341951,9.78153339 22.894393,11.2159934 24.6790793,11.2159934 L26.8381753,11.2159934 L26.8381753,11.2283594 L38.7693647,11.2283594 C42.5009815,11.2283594 45.5212199,14.2209398 45.5212199,17.9183842 L45.5212199,17.9307502 C45.5212199,21.5292663 42.6507454,24.5342127 39.0439319,24.6084089 C38.3824747,24.6207749 36.3856229,24.6084089 36.3856229,24.6084089 C36.3731426,24.6084089 36.3481819,24.6084089 36.3357016,24.6084089 L36.310741,24.6084089 C34.3263695,24.657873 32.7413684,26.3520198 32.9535339,28.3800495 C33.1407387,30.1483924 34.7382202,31.4468261 36.5353868,31.4468261 L43.3496435,31.4468261 C44.0485417,31.4468261 44.7599201,31.4962902 45.4338576,31.6694147 C49.0905925,32.6215993 51.7988227,35.9109646 51.7988227,39.8433636 C51.7988227,44.5053586 47.9798437,48.2893652 43.2747616,48.2893652 L22.7071881,48.2893652 L18.8382878,48.2893652 C17.0161605,48.2893652 15.3937184,49.6125309 15.2189939,51.4056059 C15.0193087,53.4583677 16.6542311,55.1896125 18.6885239,55.1896125 L30.257784,55.1896125 L30.257784,56.6735367 C30.257784,58.5160758 31.7679031,60 33.614991,60 C35.4745593,60 36.9721981,58.5037098 36.9721981,56.6735367 L36.9721981,55.1896125 L39.8925939,55.1896125 C39.8925939,55.1896125 39.8925939,55.1896125 39.9050742,55.1896125 L43.5742894,55.1896125 C52.0983505,55.1896125 58.9999695,48.3388293 58.9999695,39.9051937 C59.0124498,33.6479802 55.1934708,28.2563891 49.7395693,25.9068425 Z" id="Shape" fill="currentColor"></path>
|
||||
<path d="M22.4417178,28.0357143 C22.4417178,26.1684224 20.8948734,24.6428571 18.9737925,24.6428571 L3.46792526,24.6428571 C1.55931891,24.6428571 0,26.1562179 0,28.0357143 C0,29.9030062 1.54684436,31.4285714 3.46792526,31.4285714 L18.9737925,31.4285714 C20.8948734,31.4285714 22.4417178,29.9030062 22.4417178,28.0357143 Z" id="Shape" fill="currentColor"></path>
|
||||
<path d="M15.7746737,21.4285714 L31.280541,21.4285714 C33.1891473,21.4285714 34.7484663,19.9152107 34.7484663,18.0357143 C34.7484663,16.1684224 33.2016219,14.6428571 31.280541,14.6428571 L15.7746737,14.6428571 C13.8660674,14.6428571 12.3067485,16.1562179 12.3067485,18.0357143 C12.3067485,19.9030062 13.8535928,21.4285714 15.7746737,21.4285714 Z" id="Shape" fill="currentColor"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<span>Breez</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@if (sdk is not null)
|
||||
{
|
||||
<li class="nav-item nav-item-sub">
|
||||
|
||||
<a permission="@Policies.CanViewStoreSettings" asp-action="Info" asp-route-storeId="@storeId" class="nav-link @ViewData.IsActivePage("Breez", null, "Info")">Info</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a permission="@Policies.CanViewStoreSettings" asp-action="Payments" asp-route-storeId="@storeId" class="nav-link @ViewData.IsActivePage("Breez", null, "Payments")">Payments</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a permission="@Policies.CanModifyStoreSettings" asp-action="Configure" asp-route-storeId="@storeId" class="nav-link @ViewData.IsActivePage("Breez", null, "Configure")">Configuration</a>
|
||||
</li>
|
||||
@if (client.Events.Any())
|
||||
{
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a permission="@Policies.CanViewStoreSettings" asp-action="Logs" asp-route-storeId="@storeId" class="nav-link @ViewData.IsActivePage("Breez", null, "Logs")">Logs</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
@model BTCPayServer.Models.StoreViewModels.LightningNodeViewModel
|
||||
@inject BreezSparkService BreezService
|
||||
@{
|
||||
var storeId = Model.StoreId;
|
||||
if (Model.CryptoCode != "BTC")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to get existing Breez client to extract the payment key
|
||||
var breezClient = BreezService.GetClient(Model.StoreId);
|
||||
string paymentKey = "";
|
||||
|
||||
if (breezClient != null)
|
||||
{
|
||||
// Extract payment key from the existing client connection string
|
||||
var connStr = breezClient.ToString();
|
||||
if (connStr.Contains("key="))
|
||||
{
|
||||
var keyStart = connStr.IndexOf("key=") + 4;
|
||||
var keyEnd = connStr.IndexOf(";", keyStart);
|
||||
if (keyEnd == -1) keyEnd = connStr.Length;
|
||||
paymentKey = connStr.Substring(keyStart, keyEnd - keyStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const customNodeAccordian = document.getElementById("CustomNodeSupport");
|
||||
const template = document.getElementById("breez");
|
||||
customNodeAccordian.appendChild(template.content.cloneNode(true));
|
||||
|
||||
// Auto-fill the connection string when Breez is selected
|
||||
const breezRadio = document.getElementById("LightningNodeType-Breez");
|
||||
const connStringEl = document.getElementById('ConnectionString');
|
||||
|
||||
if (breezRadio && connStringEl) {
|
||||
breezRadio.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
const storeId = '@Model.StoreId';
|
||||
const paymentKey = '@Html.Raw(paymentKey)';
|
||||
const connString = `type=breez;key=${paymentKey};storeId=${storeId}`;
|
||||
connStringEl.value = connString;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template id="breez">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="CustomBreezHeader">
|
||||
<button type="button" class="accordion-button collapsed" data-bs-toggle="collapse" data-bs-target="#CustomBreezContent" aria-controls="CustomBreezContent" aria-expanded="false">
|
||||
<span><strong>Breez</strong> non-custodial Lightning wallet</span>
|
||||
<vc:icon symbol="caret-down"/>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="CustomBreezContent" class="accordion-collapse collapse" aria-labelledby="CustomBreezHeader" data-bs-parent="#CustomNodeSupport">
|
||||
<div class="accordion-body">
|
||||
@if (!string.IsNullOrEmpty(paymentKey))
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
<strong>Connection string will be auto-filled:</strong>
|
||||
<br/>
|
||||
<code>type=breez;key=@paymentKey;storeId=@Model.StoreId</code>
|
||||
</div>
|
||||
<p class="my-2">
|
||||
The connection string above will be automatically filled when you select Breez.
|
||||
Connects to your <a href="https://breez.technology" target="_blank" rel="noreferrer noopener">Breez</a> mobile wallet.
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<strong>Breez not configured</strong>
|
||||
<br/>
|
||||
Please <a href="/Breez/Index?storeId=@Model.StoreId">configure Breez first</a> to set up your payment key.
|
||||
</div>
|
||||
<p class="my-2">Connects to a <a href="https://breez.technology" target="_blank" rel="noreferrer noopener">Breez</a> mobile wallet, using the Breez SDK to handle Lightning payments through swaps.</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div id="BreezSetup" class="pt-3 tab-pane fade" role="tabpanel" aria-labelledby="LightningNodeType-Breez">
|
||||
<p>You can use Breez to accept lightning payments without running a traditional Lightning node.</p>
|
||||
<p>Breez is a mobile-first Lightning Network client that provides a non-custodial solution for Lightning payments.</p>
|
||||
</div>
|
||||
@@ -1,38 +0,0 @@
|
||||
@inject BreezSparkService BreezService;
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Plugins.BreezSpark
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model BTCPayServer.Models.StoreViewModels.LightningNodeViewModel
|
||||
|
||||
@if (Model.CryptoCode != "BTC")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@{
|
||||
var breezClient = BreezService.GetClient(Model.StoreId);
|
||||
}
|
||||
|
||||
@if (breezClient == null)
|
||||
{
|
||||
<a asp-action="Index" asp-controller="BreezSpark" permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.StoreId" type="radio" role="tab" aria-controls="BreezSetup" aria-selected="false" name="LightningNodeType">
|
||||
<label for="LightningNodeType-Breez">Configure Breez</label>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input value="Custom" type="radio" id="LightningNodeType-Breez" data-bs-toggle="pill" data-bs-target="#BreezSetup" role="tab" aria-controls="BreezSetup" aria-selected="false" name="LightningNodeType">
|
||||
<label for="LightningNodeType-Breez">Use Breez</label>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const typePrefix = '@breezClient.ToString()';
|
||||
const connStringEl = document.getElementById('ConnectionString')
|
||||
delegate('change', 'input[name="LightningNodeType"]', e => {
|
||||
const activeEl = document.querySelector('input[name="LightningNodeType"]:checked')
|
||||
if (activeEl.id === "LightningNodeType-Breez"){
|
||||
connStringEl.value = typePrefix;
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Plugins.BreezSpark
|
||||
@using Breez.Sdk.Spark
|
||||
@inject BTCPayServer.Abstractions.Services.Safe Safe
|
||||
@addTagHelper *, BTCPayServer.Abstractions
|
||||
@addTagHelper *, BTCPayServer.TagHelpers
|
||||
@addTagHelper *, BTCPayServer.Views.TagHelpers
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@addTagHelper *, BTCPayServer
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v8.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v8.0": {
|
||||
"BTCPayServer.Plugins.BreezSpark/1.1.0": {
|
||||
"dependencies": {
|
||||
"Breez.Sdk.Spark": "0.4.1"
|
||||
},
|
||||
"runtime": {
|
||||
"BTCPayServer.Plugins.BreezSpark.dll": {}
|
||||
}
|
||||
},
|
||||
"Breez.Sdk.Spark/0.4.1": {
|
||||
"runtime": {
|
||||
"lib/net8.0/Breez.Sdk.Spark.dll": {
|
||||
"assemblyVersion": "0.4.1.0",
|
||||
"fileVersion": "0.4.1.0"
|
||||
}
|
||||
},
|
||||
"runtimeTargets": {
|
||||
"runtimes/linux-arm64/native/libbreez_sdk_spark_bindings.so": {
|
||||
"rid": "linux-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-x64/native/libbreez_sdk_spark_bindings.so": {
|
||||
"rid": "linux-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/osx-arm64/native/libbreez_sdk_spark_bindings.dylib": {
|
||||
"rid": "osx-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/osx-x64/native/libbreez_sdk_spark_bindings.dylib": {
|
||||
"rid": "osx-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-x64/native/breez_sdk_spark_bindings.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-x86/native/breez_sdk_spark_bindings.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"BTCPayServer.Plugins.BreezSpark/1.1.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Breez.Sdk.Spark/0.4.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-/5iC7V3PK0q5og4h5qnSf5xepfvcXSEeV5WJDIAlOGd7WUtMZdNQ0n6yDcgd1Rv5qcxPQsDGQN3IVQZbP0UE1w==",
|
||||
"path": "breez.sdk.spark/0.4.1",
|
||||
"hashPath": "breez.sdk.spark.0.4.1.nupkg.sha512"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"Version":1,"ManifestType":"Publish","Endpoints":[]}
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<doc>
|
||||
<assembly>
|
||||
<name>BTCPayServer.Plugins.BreezSpark</name>
|
||||
</assembly>
|
||||
<members>
|
||||
</members>
|
||||
</doc>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user