mirror of
https://github.com/aljazceru/btcpayserver-breez-nodeless-spark.git
synced 2025-12-17 09:24:19 +01:00
538 lines
19 KiB
C#
538 lines
19 KiB
C#
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.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);
|
|
}
|
|
|
|
[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, BreezSettings 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; }
|
|
}
|