mirror of
https://github.com/aljazceru/btcpayserver-breez-nodeless-spark.git
synced 2025-12-17 01:14:19 +01:00
593 lines
22 KiB
C#
593 lines
22 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 Microsoft.Extensions.Logging;
|
|
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;
|
|
private readonly ILogger<BreezSparkController> _logger;
|
|
|
|
public BreezSparkController(
|
|
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
|
BTCPayNetworkProvider btcPayNetworkProvider,
|
|
BreezSparkService breezService,
|
|
BTCPayWalletProvider btcWalletProvider,
|
|
StoreRepository storeRepository,
|
|
ILogger<BreezSparkController> logger)
|
|
{
|
|
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
|
|
_btcPayNetworkProvider = btcPayNetworkProvider;
|
|
_breezService = breezService;
|
|
_btcWalletProvider = btcWalletProvider;
|
|
_storeRepository = storeRepository;
|
|
_logger = logger;
|
|
}
|
|
|
|
|
|
[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(Transactions), 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});
|
|
}
|
|
|
|
var amountSats = ResolveAmountSats(address, amount);
|
|
|
|
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 amt = amountSats ?? BigInteger.Zero;
|
|
ViewData["PaymentDetails"] = new PaymentDetailsDto(
|
|
Destination: address,
|
|
Amount: (long)amt,
|
|
Fee: (long)totalFee
|
|
);
|
|
}
|
|
else if (prepareResponse.paymentMethod is SendPaymentMethod.BitcoinAddress bitcoinMethod)
|
|
{
|
|
var fees = bitcoinMethod.feeQuote;
|
|
var mediumFee = fees.speedMedium.userFeeSat + fees.speedMedium.l1BroadcastFeeSat;
|
|
ViewData["PaymentDetails"] = new PaymentDetailsDto(
|
|
Destination: address,
|
|
Amount: (long)BigInteger.Abs(amountSats ?? BigInteger.Zero),
|
|
Fee: (long)mediumFee
|
|
);
|
|
}
|
|
else if (prepareResponse.paymentMethod is SendPaymentMethod.SparkAddress sparkMethod)
|
|
{
|
|
ViewData["PaymentDetails"] = new PaymentDetailsDto(
|
|
Destination: address,
|
|
Amount: (long)BigInteger.Abs(amountSats ?? BigInteger.Zero),
|
|
Fee: (long)sparkMethod.fee
|
|
);
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
var client = _breezService.GetClient(storeId);
|
|
if (client is null)
|
|
{
|
|
return RedirectToAction(nameof(Configure), new {storeId});
|
|
}
|
|
|
|
try
|
|
{
|
|
// Re-run preparation to avoid polymorphic JSON deserialization issues
|
|
var amountSats = ResolveAmountSats(paymentRequest, amount);
|
|
var prepareResponse = await client.Sdk.PrepareSendPayment(new PrepareSendPaymentRequest(
|
|
paymentRequest: paymentRequest,
|
|
amount: amountSats
|
|
));
|
|
|
|
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,
|
|
SendPaymentMethod.SparkInvoice => null,
|
|
_ => null
|
|
};
|
|
|
|
var sendRequest = new SendPaymentRequest(
|
|
prepareResponse: prepareResponse,
|
|
options: options
|
|
);
|
|
|
|
_logger.LogInformation("BreezSpark sending payment for store {StoreId} to {Destination}", storeId, paymentRequest);
|
|
var sendResponse = await client.Sdk.SendPayment(sendRequest);
|
|
_logger.LogInformation("BreezSpark send complete for store {StoreId}: payment id {PaymentId}, status {Status}",
|
|
storeId, sendResponse.payment?.id, sendResponse.payment?.status);
|
|
|
|
TempData[WellKnownTempData.SuccessMessage] = "Payment sent successfully!";
|
|
return RedirectToAction(nameof(Transactions), new {storeId});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
TempData[WellKnownTempData.ErrorMessage] = $"Error sending payment: {ex.Message}";
|
|
_logger.LogError(ex, "BreezSpark send failed for store {StoreId}", storeId);
|
|
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
|
|
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)
|
|
{
|
|
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("transactions")]
|
|
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
|
public async Task<IActionResult> Transactions(string storeId, PaymentsViewModel viewModel)
|
|
{
|
|
var client = _breezService.GetClient(storeId);
|
|
if (client is null)
|
|
{
|
|
return RedirectToAction(nameof(Configure), new {storeId});
|
|
}
|
|
|
|
viewModel ??= new PaymentsViewModel();
|
|
viewModel.Balance = await client.GetBalance();
|
|
var req = new ListPaymentsRequest(
|
|
typeFilter: null,
|
|
statusFilter: null,
|
|
assetFilter: new AssetFilter.Bitcoin(),
|
|
fromTimestamp: null,
|
|
toTimestamp: null,
|
|
offset: viewModel.Skip > 0 ? (uint?)viewModel.Skip : null,
|
|
limit: viewModel.Count > 0 ? (uint?)viewModel.Count : null,
|
|
sortAscending: false
|
|
);
|
|
var response = await client.Sdk.ListPayments(req);
|
|
var normalized = new List<NormalizedPayment>();
|
|
foreach (var p in response.payments.Where(p => p != null))
|
|
{
|
|
var norm = client.NormalizePayment(p);
|
|
if (norm is not null)
|
|
{
|
|
normalized.Add(norm);
|
|
continue;
|
|
}
|
|
|
|
// Fallback: show raw SDK payment even if we lack invoice context
|
|
long amountSat = 0;
|
|
if (p.details is PaymentDetails.Lightning l && !string.IsNullOrEmpty(l.invoice))
|
|
{
|
|
var nbitcoinNetwork = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>("BTC")?.NBitcoinNetwork ?? NBitcoin.Network.Main;
|
|
if (BOLT11PaymentRequest.TryParse(l.invoice, out var pr, nbitcoinNetwork) && pr?.MinimumAmount is not null)
|
|
{
|
|
amountSat = (long)pr.MinimumAmount.ToUnit(LightMoneyUnit.Satoshi);
|
|
}
|
|
}
|
|
|
|
long feeSat = 0;
|
|
if (p.fees != null)
|
|
{
|
|
feeSat = (long)(p.fees / 1000);
|
|
}
|
|
normalized.Add(new NormalizedPayment
|
|
{
|
|
Id = p.id ?? Guid.NewGuid().ToString("N"),
|
|
PaymentType = p.paymentType,
|
|
Status = p.status,
|
|
Timestamp = p.timestamp,
|
|
Amount = LightMoney.Satoshis(amountSat),
|
|
Fee = LightMoney.Satoshis(feeSat),
|
|
Description = p.details?.ToString() ?? "BreezSpark payment"
|
|
});
|
|
}
|
|
viewModel.Payments = normalized;
|
|
|
|
return View("Transactions", viewModel);
|
|
}
|
|
|
|
private BigInteger? ResolveAmountSats(string paymentRequest, long? amount)
|
|
{
|
|
if (amount.HasValue && amount.Value > 0)
|
|
{
|
|
return new BigInteger(amount.Value);
|
|
}
|
|
|
|
// Try to derive amount from bolt11 invoice if present
|
|
var nbitcoinNetwork = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>("BTC")?.NBitcoinNetwork ?? NBitcoin.Network.Main;
|
|
if (BOLT11PaymentRequest.TryParse(paymentRequest, out var pr, nbitcoinNetwork) && pr?.MinimumAmount is not null)
|
|
{
|
|
return new BigInteger((long)pr.MinimumAmount.ToUnit(LightMoneyUnit.Satoshi));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public class PaymentsViewModel : BasePagingViewModel
|
|
{
|
|
public List<NormalizedPayment> Payments { get; set; } = new();
|
|
public LightningNodeBalance? Balance { get; set; }
|
|
public override int CurrentPageCount => Payments.Count;
|
|
}
|
|
|
|
public record PaymentDetailsDto(string Destination, long Amount, long Fee);
|
|
|
|
// 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; }
|
|
}
|