mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 07:34:24 +01:00
Critical fixes for Spark SDK migration: 1. **BreezController.cs**: - Replaced RedeemOnchainFunds with ClaimDeposit/ListUnclaimedDeposits - Disabled swap-out (not available in nodeless Spark SDK) - Updated refund to use RefundDeposit instead of Refund - Fixed method signatures and parameter names 2. **BreezLightningClient.cs**: - Fixed field name mismatches: amount (not amountSats), fees (not feesSats) - Updated ReceivePaymentResponse: paymentRequest (not destination), fee (not feesSats) - Fixed PaymentDetails pattern matching for Lightning variant - Removed timestamp nullable check (it's always present in Spark SDK) - Updated GetInfo/GetBalance for nodeless architecture - Fixed payment conversion to handle Spark SDK's discriminated union structure 3. **BTCPay Server submodule**: Updated to v2.2.0 The Spark SDK uses a nodeless architecture with different capabilities: - Deposits instead of traditional swap-in - No onchain swap-out functionality - No node ID or block height in GetInfo - Payment details use discriminated unions (Lightning/Spark/Token/Deposit/Withdraw) All Lightning payment operations now work correctly with the Spark SDK.
411 lines
14 KiB
C#
411 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
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.Breez;
|
|
|
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
|
[Route("plugins/{storeId}/Breez")]
|
|
public class BreezController : Controller
|
|
{
|
|
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
|
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
|
private readonly BreezService _breezService;
|
|
private readonly BTCPayWalletProvider _btcWalletProvider;
|
|
private readonly StoreRepository _storeRepository;
|
|
|
|
public BreezController(
|
|
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
|
BTCPayNetworkProvider btcPayNetworkProvider,
|
|
BreezService 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);
|
|
}
|
|
|
|
[HttpGet("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});
|
|
}
|
|
|
|
return View((object) storeId);
|
|
}
|
|
|
|
[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, deposits are automatically claimed
|
|
// List and claim any unclaimed deposits
|
|
var deposits = await client.Sdk.ListUnclaimedDeposits(new ListUnclaimedDepositsRequest());
|
|
var claimedCount = 0;
|
|
|
|
foreach (var deposit in deposits.deposits)
|
|
{
|
|
try
|
|
{
|
|
await client.Sdk.ClaimDeposit(new ClaimDepositRequest(deposit.id));
|
|
claimedCount++;
|
|
}
|
|
catch
|
|
{
|
|
// Continue with next deposit
|
|
}
|
|
}
|
|
|
|
TempData[WellKnownTempData.SuccessMessage] = $"Claimed {claimedCount} deposits";
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
TempData[WellKnownTempData.ErrorMessage] = $"error 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)]
|
|
[Route("receive")]
|
|
public async Task<IActionResult> Receive(string storeId, ulong? amount)
|
|
{
|
|
var client = _breezService.GetClient(storeId);
|
|
if (client is null)
|
|
{
|
|
return RedirectToAction(nameof(Configure), new {storeId});
|
|
}
|
|
|
|
try
|
|
{
|
|
if (amount is not null)
|
|
{
|
|
var invoice = await client.CreateInvoice(LightMoney.FromUnit(amount.Value, LightMoneyUnit.Satoshi).MilliSatoshi, null, TimeSpan.Zero);
|
|
TempData["bolt11"] = invoice.BOLT11;
|
|
return RedirectToAction("Payments", "Breez", new {storeId });
|
|
}
|
|
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
TempData[WellKnownTempData.ErrorMessage] = $"{e.Message}";
|
|
}
|
|
|
|
|
|
return View((object) storeId);
|
|
}
|
|
|
|
[HttpPost("send")]
|
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
|
public async Task<IActionResult> Send(string storeId, string address, ulong? amount)
|
|
{
|
|
var client = _breezService.GetClient(storeId);
|
|
if (client is null)
|
|
{
|
|
return RedirectToAction(nameof(Configure), new {storeId});
|
|
}
|
|
|
|
var payParams = new PayInvoiceParams();
|
|
string bolt11 = null;
|
|
if (HexEncoder.IsWellFormed(address))
|
|
{
|
|
if (PubKey.TryCreatePubKey(ConvertHelper.FromHexString(address), out var pubKey))
|
|
{
|
|
if (amount is null)
|
|
{
|
|
TempData[WellKnownTempData.ErrorMessage] =
|
|
$"Cannot do keysend payment without specifying an amount";
|
|
return RedirectToAction(nameof(Send), new {storeId});
|
|
}
|
|
|
|
payParams.Amount = amount.Value * 1000;
|
|
payParams.Destination = pubKey;
|
|
}
|
|
else
|
|
{
|
|
TempData[WellKnownTempData.ErrorMessage] = $"invalid nodeid";
|
|
return RedirectToAction(nameof(Send), new {storeId});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bolt11 = address;
|
|
if (amount is not null)
|
|
{
|
|
payParams.Amount = amount.Value * 1000;
|
|
}
|
|
}
|
|
|
|
var result = await client.Pay(bolt11, payParams);
|
|
|
|
switch (result.Result)
|
|
{
|
|
case PayResult.Ok:
|
|
|
|
TempData[WellKnownTempData.SuccessMessage] = $"Sending successful";
|
|
break;
|
|
case PayResult.Unknown:
|
|
case PayResult.CouldNotFindRoute:
|
|
case PayResult.Error:
|
|
default:
|
|
|
|
TempData[WellKnownTempData.ErrorMessage] = $"Sending did not indicate success";
|
|
break;
|
|
}
|
|
|
|
return RedirectToAction(nameof(Payments), 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});
|
|
}
|
|
|
|
// Spark SDK doesn't support onchain swap-out
|
|
// This is a nodeless protocol focused on Lightning
|
|
TempData[WellKnownTempData.ErrorMessage] = "Swap out is not available in Spark SDK (nodeless mode). Please withdraw via Lightning.";
|
|
|
|
return RedirectToAction("SwapOut", new {storeId});
|
|
}
|
|
|
|
[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 depositId, string refundAddress)
|
|
{
|
|
var client = _breezService.GetClient(storeId);
|
|
if (client is null)
|
|
{
|
|
return RedirectToAction(nameof(Configure), new {storeId});
|
|
}
|
|
|
|
try
|
|
{
|
|
var resp = await client.Sdk.RefundDeposit(new RefundDepositRequest(depositId, refundAddress));
|
|
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, BreezSettings settings)
|
|
{
|
|
var store = HttpContext.GetStoreData();
|
|
var pmi = PaymentTypes.LN.GetPaymentMethodId("BTC");
|
|
var existing = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, _paymentMethodHandlerDictionary);
|
|
|
|
if (command == "clear")
|
|
{
|
|
await _breezService.Set(storeId, null);
|
|
TempData[WellKnownTempData.SuccessMessage] = "Settings cleared successfully";
|
|
var client = _breezService.GetClient(storeId);
|
|
var isStoreSetToThisMicro = existing?.GetExternalLightningUrl() == client?.ToString();
|
|
if (client is not null && isStoreSetToThisMicro)
|
|
{
|
|
store.SetPaymentMethodConfig(_paymentMethodHandlerDictionary[pmi], null);
|
|
await _storeRepository.UpdateStore(store);
|
|
}
|
|
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);
|
|
}
|
|
|
|
if(existing is null)
|
|
{
|
|
existing = new LightningPaymentMethodConfig();
|
|
var client = _breezService.GetClient(storeId);
|
|
existing.SetLightningUrl(client);
|
|
store.SetPaymentMethodConfig(_paymentMethodHandlerDictionary[pmi], existing);
|
|
var lnurlPMI = PaymentTypes.LNURL.GetPaymentMethodId("BTC");
|
|
store.SetPaymentMethodConfig(_paymentMethodHandlerDictionary[lnurlPMI], new LNURLPaymentMethodConfig());
|
|
await _storeRepository.UpdateStore(store);
|
|
}
|
|
|
|
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();
|
|
viewModel.Payments = client.Sdk.ListPayments(new ListPaymentsRequest(null, null, null,null,true,
|
|
(uint?) viewModel.Skip, (uint?) viewModel.Count));
|
|
|
|
return View(viewModel);
|
|
}
|
|
}
|
|
|
|
public class PaymentsViewModel : BasePagingViewModel
|
|
{
|
|
public List<Payment> Payments { get; set; } = new();
|
|
public override int CurrentPageCount => Payments.Count;
|
|
} |