This commit is contained in:
Kukks
2023-11-02 16:08:10 +01:00
parent 74c6931e8c
commit ba68159a3a
17 changed files with 960 additions and 58 deletions

View File

@@ -2,12 +2,17 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Breez.Sdk;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Models;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Plugins.Breez;
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Route("plugins/{storeId}/Breez")]
@@ -15,26 +20,201 @@ public class BreezController : Controller
{
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly BreezService _breezService;
private readonly BTCPayWalletProvider _btcWalletProvider;
public BreezController(BTCPayNetworkProvider btcPayNetworkProvider, BreezService breezService)
public BreezController(BTCPayNetworkProvider btcPayNetworkProvider,
BreezService breezService,
BTCPayWalletProvider btcWalletProvider)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_breezService = breezService;
_btcWalletProvider = btcWalletProvider;
}
[HttpGet("")]
[HttpGet("")]
public async Task<IActionResult> Index(string storeId)
{
return View(await _breezService.Get(storeId));
var client = _breezService.GetClient(storeId);
return RedirectToAction(client is null ? nameof(Configure) : nameof(Info), new {storeId});
}
[HttpGet("swapin")]
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")]
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("sweep")]
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")]
public async Task<IActionResult> Sweep(string storeId, string address, uint satPerByte)
{
var client = _breezService.GetClient(storeId);
if (client is null)
{
return RedirectToAction(nameof(Configure), new {storeId});
}
if (address.Equals("store", StringComparison.InvariantCultureIgnoreCase))
{
var store = ControllerContext.HttpContext.GetStoreData()
.GetDerivationSchemeSettings(_btcPayNetworkProvider, "BTC");
var res = await _btcWalletProvider.GetWallet(storeId)
.ReserveAddressAsync(storeId, store.AccountDerivation, "Breez");
address = res.Address.ToString();
}
try
{
var response = client.Sdk.Sweep(new SweepRequest(address, satPerByte));
TempData[WellKnownTempData.SuccessMessage] = $"sweep successful: {response.txid}";
}
catch (Exception e)
{
TempData[WellKnownTempData.ErrorMessage] = $"error with sweep: {e.Message}";
}
return View((object) storeId);
}
[HttpGet("swapin/create")]
public async Task<IActionResult> SwapInCreate(string storeId)
{
var client = _breezService.GetClient(storeId);
if (client is null)
{
return RedirectToAction(nameof(Configure), new {storeId});
}
client.Sdk.ReceiveOnchain(new ReceiveOnchainRequest());
TempData[WellKnownTempData.SuccessMessage] = "Swapin created successfully";
return RedirectToAction(nameof(SwapIn), new {storeId});
}
[HttpGet("swapout")]
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")]
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});
}
if (address.Equals("store", StringComparison.InvariantCultureIgnoreCase))
{
var store = ControllerContext.HttpContext.GetStoreData()
.GetDerivationSchemeSettings(_btcPayNetworkProvider, "BTC");
var res = await _btcWalletProvider.GetWallet(storeId)
.ReserveAddressAsync(storeId, store.AccountDerivation, "Breez");
address = res.Address.ToString();
}
try
{
var result = client.Sdk.SendOnchain(new SendOnchainRequest(amount, address, feesHash, satPerByte));
TempData[WellKnownTempData.SuccessMessage] = $"swap out created: {result.reverseSwapInfo.id}";
}
catch (Exception e)
{
TempData[WellKnownTempData.ErrorMessage] = $"Couldnt create swap out: {e.Message}";
}
return RedirectToAction("SwapOut", new {storeId});
}
[HttpGet("swapin/{address}/refund")]
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")]
public async Task<IActionResult> SwapInRefund(string storeId, string address, string refundAddress, uint satPerByte)
{
var client = _breezService.GetClient(storeId);
if (client is null)
{
return RedirectToAction(nameof(Configure), new {storeId});
}
try
{
var resp = client.Sdk.Refund(new RefundRequest(address, refundAddress, satPerByte));
TempData[WellKnownTempData.SuccessMessage] = $"Refund tx: {resp.refundTxId}";
}
catch (Exception e)
{
TempData[WellKnownTempData.ErrorMessage] = $"Couldnt refund: {e.Message}";
}
return RedirectToAction(nameof(SwapIn), new {storeId});
}
[HttpGet("configure")]
public async Task<IActionResult> Configure(string storeId)
{
return View(await _breezService.Get(storeId));
}
[HttpPost("")]
public async Task<IActionResult> Index(string storeId, string command, BreezSettings settings)
public async Task<IActionResult> Configure(string storeId, string command, BreezSettings settings)
{
if (command == "clear")
{
await _breezService.Set(storeId, null);
TempData[WellKnownTempData.SuccessMessage] = "Settings cleared successfully";
return RedirectToAction(nameof(Index), new {storeId});
return RedirectToAction(nameof(Configure), new {storeId});
}
if (command == "save")
@@ -48,11 +228,33 @@ public class BreezController : Controller
TempData[WellKnownTempData.ErrorMessage] = $"Couldnt use provided settings: {e.Message}";
return View(settings);
}
TempData[WellKnownTempData.SuccessMessage] = "Settings saved successfully";
return RedirectToAction(nameof(Index), new {storeId});
return RedirectToAction(nameof(Configure), new {storeId});
}
return NotFound();
}
[Route("payments")]
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(PaymentTypeFilter.ALL, null, null, null,
(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;
}

View File

@@ -20,13 +20,13 @@ public class BreezLightningConnectionStringHandler : ILightningConnectionStringH
return null;
}
if (!kv.TryGetValue("store", out var storeId))
if (!kv.TryGetValue("key", out var key))
{
error = $"The key 'store' is mandatory for breez connection strings";
error = $"The key 'key' is mandatory for breez connection strings";
return null;
}
error = null;
return _breezService.GetClient(storeId);
return _breezService.GetClientByPaymentKey(key);
}
}

View File

@@ -27,6 +27,10 @@ namespace BTCPayServer.Plugins.Breez
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("Breez/LNPaymentMethodSetupTabhead", "ln-payment-method-setup-tabhead"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("Breez/LNPaymentMethodSetupTab", "ln-payment-method-setup-tab"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("Breez/BreezNodeInfo",
"dashboard"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("Breez/BreezPaymentsTable",
"dashboard"));
base.Execute(applicationBuilder);
}
}

View File

@@ -6,6 +6,9 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Stores;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -106,8 +109,18 @@ public class BreezService:IHostedService
await _storeRepository.UpdateSetting(storeId, "Breez", settings!);
if (settings is null)
{
_settings.Remove(storeId);
_settings.Remove(storeId, out var oldSettings );
var data = await _storeRepository.FindStore(storeId);
var existing = data?.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>().FirstOrDefault(method =>
method.CryptoCode == "BTC" && method.PaymentId.PaymentType == LightningPaymentType.Instance);
var isBreez = existing?.GetExternalLightningUrl() == $"type=breez;key={oldSettings.PaymentKey}";
if (isBreez)
{
data.SetSupportedPaymentMethod(new PaymentMethodId("BTC", LightningPaymentType.Instance), null );
await _storeRepository.UpdateStore(data);
}
}
else if(result is not null )
{
@@ -122,9 +135,18 @@ public class BreezService:IHostedService
_clients.Values.ToList().ForEach(c => c.Dispose());
}
public BreezLightningClient? GetClient(string storeId)
public BreezLightningClient? GetClient(string? storeId)
{
if(storeId is null)
return null;
_clients.TryGetValue(storeId, out var client);
return client;
}
public BreezLightningClient? GetClientByPaymentKey(string? paymentKey)
{
if(paymentKey is null)
return null;
var match = _settings.FirstOrDefault(pair => pair.Value.PaymentKey == paymentKey).Key;
return GetClient(match);
}
}

View File

@@ -1,4 +1,6 @@
#nullable enable
using System;
namespace BTCPayServer.Plugins.Breez;
public class BreezSettings
@@ -6,4 +8,6 @@ public class BreezSettings
public string InviteCode { get; set; }
public string Mnemonic { get; set; }
public string ApiKey { get; set; }
public string PaymentKey { get; set; } = Guid.NewGuid().ToString();
}

View File

@@ -4,18 +4,25 @@
@model BTCPayServer.Plugins.Breez.BreezSettings?
@inject BreezService BreezService
@{
ViewData.SetActivePage("Breez", "Breez", "Breez");
ViewData.SetActivePage("Breez", "Configure", "Configure");
var storeId = Context.GetCurrentStoreId();
}
<form method="post" asp-action="Configure" asp-controller="Breez" 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 name="command" type="submit" value="save" class="btn btn-primary">Save</button>
@if (await BreezService.Get(storeId) is not null)
{
<button name="command" type="submit" value="clear" class="btn btn-danger">Clear</button>
}
</div>
</div>
<partial name="_StatusMessage"/>
<h2 class="mb-4">@ViewData["Title"]</h2>
<div class="row">
<div class="col-md-10">
<form method="post">
<div class="form-group">
<label asp-for="Mnemonic" class="form-label">Mnemonic</label>
<input asp-for="Mnemonic" class="form-control"/>
@@ -34,20 +41,8 @@
<span asp-validation-for="InviteCode" class="text-danger"></span>
</div>
<button name="command" type="submit" value="save" class="btn btn-primary">Submit</button>
@if (await BreezService.Get(storeId) is not null)
{
<button name="command" type="submit" value="clear" class="btn btn-danger">Clear</button>
var client = BreezService.GetClient(storeId);
if (client is not null)
{
<pre>
@Json.Serialize(client.Sdk.NodeInfo())
</pre>
}
}
</form>
<input type="hidden" asp-for="PaymentKey"/>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,15 @@
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Models.StoreViewModels
@using BTCPayServer.Security
@model object
@{
var storeId = Model switch
{
string s => s,
StoreDashboardViewModel dashboardModel => dashboardModel.StoreId,
_ => Context.GetImplicitStoreId()
};
ViewData.SetActivePage("Breez", "Create Swapin Refund", "Info");
}
<partial name="Breez/BreezNodeInfo" model="@storeId"/>

View File

@@ -0,0 +1,27 @@
@using BTCPayServer.Components
@using BTCPayServer
@using BTCPayServer.Abstractions.Extensions
@model BTCPayServer.Plugins.Breez.PaymentsViewModel
@{
var storeId = Context.GetCurrentStoreId();
ViewData.SetActivePage("Breez", "Payments", "Payments");
}
<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">Send</button>
<button type="submit" class="btn btn-primary">Receive</button>
</div>
</div>
<partial name="Breez/BreezPaymentsTable" model="Model.Payments"/>
<vc:pager view-model="Model"></vc:pager>
</div>
</div>

View File

@@ -0,0 +1,193 @@
@using Breez.Sdk
@using BTCPayServer
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Components.QRCode
@using BTCPayServer.Components.TruncateCenter
@using BTCPayServer.Models.StoreViewModels
@using BTCPayServer.Payments
@using BTCPayServer.Plugins.Breez
@using BTCPayServer.Security
@using NBitcoin
@inject BreezService BreezService
@inject BTCPayNetworkProvider BtcPayNetworkProvider
@{
ViewData.SetActivePage("Breez", "Swap In", "SwapIn");
string storeId = Model switch
{
string s => s,
StoreDashboardViewModel dashboardModel => dashboardModel.StoreId,
_ => Context.GetImplicitStoreId()
};
var sdk = BreezService.GetClient(storeId)?.Sdk;
if (sdk is null)
return;
var inProgressSwap = sdk.InProgressSwap();
var refundables = sdk.ListRefundables();
var deriv = Context.GetStoreData().GetDerivationSchemeSettings(BtcPayNetworkProvider, "BTC");
var ni = sdk.NodeInfo();
var f = sdk.RecommendedFees();
}
<datalist id="fees">
<option value="@f.fastestFee">Fastest fee</option>
<option value="@f.halfHourFee">Half hour fee</option>
<option value="@f.hourFee">Hour fee</option>
<option value="@f.economyFee">Economic fee</option>
<option value="@f.minimumFee">Minimum fee</option>
</datalist>
<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">
@if (inProgressSwap is null)
{
<a class="btn btn-primary" asp-action="SwapInCreate" asp-controller="Breez" asp-route-storeId="@storeId">Create</a>
}
</div>
</div>
@if (inProgressSwap is not null)
{
<div class="payment-box">
<div class="qr-container" data-clipboard="@inProgressSwap.bitcoinAddress">
<vc:qr-code data="@inProgressSwap.bitcoinAddress"/>
</div>
<div class="input-group mt-3">
<div class="form-floating">
<vc:truncate-center text="@inProgressSwap.bitcoinAddress" padding="15" elastic="true" classes="form-control-plaintext" id="Address"/>
<label for="Address">Address</label>
</div>
<span class="">Please send an amount between @Money.Satoshis(inProgressSwap.minAllowedDeposit).ToDecimal(MoneyUnit.BTC) BTC and @Money.Satoshis(inProgressSwap.maxAllowedDeposit).ToDecimal(MoneyUnit.BTC) </span>
@if (deriv is not null)
{
<a class="btn btn-primary" asp-controller="UIWallets" asp-action="WalletSend" asp-route-walletId="@Model.WalletId" asp-route-defaultDestination="@inProgressSwap.bitcoinAddress">Send using BTCPay Wallet</a>
}
@{
var onChainSats = ni.onchainBalanceMsat / 1000;
if (inProgressSwap.minAllowedDeposit <= (long) onChainSats)
{
<div>
<form asp-action="Sweep" asp-route-storeId="@storeId">
<input type="hidden" value="@inProgressSwap.bitcoinAddress" name="address"/>
<button type="submit"> Sweep onchain funds to swap in</button>
</form>
</div>
}
}
</div>
@if (inProgressSwap.unconfirmedSats + inProgressSwap.confirmedSats + inProgressSwap.paidSats > 0)
{
<div class="card truncate-center-id">
<span class="text-nowrap">@inProgressSwap.unconfirmedSats sats unconfirmed</span>
<span class="text-nowrap">@inProgressSwap.confirmedSats sats confirmed</span>
<span class="text-nowrap">@inProgressSwap.paidSats sats paid</span>
</div>
}
@if (inProgressSwap.unconfirmedTxIds.Any())
{
<div class="card truncate-center-id">
<span class="text-nowrap">Unconfirmed transactions</span>
@foreach (var txId in inProgressSwap.unconfirmedTxIds)
{
<vc:truncate-center text="@txId" link="@BitcoinPaymentType.Instance.GetTransactionLink(BtcPayNetworkProvider.BTC, txId)" classes="truncate-center-id"/>
}
</div>
}
@if (inProgressSwap.confirmedTxIds.Any())
{
<div class="card truncate-center-id">
<span class="text-nowrap">Confirmed transactions</span>
@foreach (var txId in inProgressSwap.confirmedTxIds)
{
<vc:truncate-center text="@txId" link="@BitcoinPaymentType.Instance.GetTransactionLink(BtcPayNetworkProvider.BTC, txId)" classes="truncate-center-id"/>
}
</div>
}
</div>
}
@if (refundables?.Any() is true)
{
<table class="table">
<thead>
<tr>
<th>Status</th>
<th>Created</th>
<th>Deposit Address</th>
<th>Payment</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var refund in refundables)
{
<tr>
<td>@refund.status</td>
<td>@DateTimeOffset.FromUnixTimeSeconds(refund.createdAt)</td>
<td>@refund.bitcoinAddress</td>
<td>
@if (refund.unconfirmedSats + refund.confirmedSats + refund.paidSats > 0)
{
<div class="card truncate-center-id">
<span class="text-nowrap">@refund.unconfirmedSats sats unconfirmed</span>
<span class="text-nowrap">@refund.confirmedSats sats confirmed</span>
<span class="text-nowrap">@refund.paidSats sats paid</span>
</div>
}
@if (refund.unconfirmedTxIds.Any())
{
<div class="card truncate-center-id">
<span class="text-nowrap">Unconfirmed transactions</span>
@foreach (var txId in refund.unconfirmedTxIds)
{
<vc:truncate-center text="@txId" link="@BitcoinPaymentType.Instance.GetTransactionLink(BtcPayNetworkProvider.BTC, txId)" classes="truncate-center-id"/>
}
</div>
}
@if (refund.confirmedTxIds.Any())
{
<div class="card truncate-center-id">
<span class="text-nowrap">Confirmed transactions</span>
@foreach (var txId in refund.confirmedTxIds)
{
<vc:truncate-center text="@txId" link="@BitcoinPaymentType.Instance.GetTransactionLink(BtcPayNetworkProvider.BTC, txId)" classes="truncate-center-id"/>
}
</div>
}
@if (refund.refundTxIds.Any())
{
<div class="card truncate-center-id">
<span class="text-nowrap">Refund transactions</span>
@foreach (var txId in refund.refundTxIds)
{
<vc:truncate-center text="@txId" link="@BitcoinPaymentType.Instance.GetTransactionLink(BtcPayNetworkProvider.BTC, txId)" classes="truncate-center-id"/>
}
</div>
}
</td>
<td>
<a class="btn btn-link" asp-controller="Breez" asp-action="SwapInRefund" asp-route-storeId="@storeId" asp-route-address="@refund.bitcoinAddress">Start Refund</a>
</td>
</tr>
}
</tbody>
</table>
}
</div>
</div>

View File

@@ -0,0 +1,52 @@
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Plugins.Breez
@using BTCPayServer.Security
@using Microsoft.AspNetCore.Routing
@model string
@inject BreezService BreezService
@{
var storeId = Context.GetImplicitStoreId();
var address = Context.GetRouteValue("address").ToString();
ViewData.SetActivePage("Breez", "Create Swapin Refund", "SwapIn");
var sdk = BreezService.GetClient(storeId)?.Sdk;
var f = sdk.RecommendedFees();
}
<datalist id="fees">
<option value="@f.fastestFee">Fastest fee</option>
<option value="@f.halfHourFee">Half hour fee</option>
<option value="@f.hourFee">Hour fee</option>
<option value="@f.economyFee">Economic fee</option>
<option value="@f.minimumFee">Minimum fee</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="All" class="text-danger"></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" data-required>Fees</label>
<input type="number" min="@f.minimumFee" list="satPerByte" id="satPerByte" name="satPerByte" class="form-control" required/>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,103 @@
@using Breez.Sdk
@using BTCPayServer
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Models.StoreViewModels
@using BTCPayServer.Plugins.Breez
@using BTCPayServer.Security
@inject BreezService BreezService
@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();
}
var sdk = BreezService.GetClient(storeId)?.Sdk;
if (sdk is null)
return;
var inProgressSwaps = sdk.InProgressReverseSwaps();
var deriv = Context.GetStoreData().GetDerivationSchemeSettings(BtcPayNetworkProvider, "BTC");
var f = sdk.RecommendedFees();
var swapOutRec = sdk.FetchReverseSwapFees(new ReverseSwapFeesRequest());
}
<datalist id="fees">
<option value="@f.fastestFee">Fastest fee</option>
<option value="@f.halfHourFee">Half hour fee</option>
<option value="@f.hourFee">Hour fee</option>
<option value="@f.economyFee">Economic fee</option>
<option value="@f.minimumFee">Minimum fee</option>
</datalist>
<datalist list="addresses">
@if (deriv is not null)
{
<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">
<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="address" class="form-label" data-required>Address</label>
<input type="text" id="address" name="address" class="form-control" required/>
</div>
<div class="form-group">
<label for="satPerByte" class="form-label" data-required>Refund address</label>
<input type="number" min="@f.minimumFee" list="satPerByte" id="satPerByte" name="satPerByte" class="form-control" required/>
</div>
<div class="form-group">
<label for="amount" class="form-label" data-required>Amount (sats)</label>
<input type="number" min="@swapOutRec.min" max="@swapOutRec.max" id="amount" name="amount" class="form-control" required/>
</div>
<input type="hidden" name="feesHash" value="@swapOutRec.feesHash"/>
@if (inProgressSwaps?.Any() is true)
{
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@foreach (var swap in inProgressSwaps)
{
<tr>
<td>@swap.id</td>
<td>@swap.status</td>
</tr>
}
</tbody>
</table>
}
</div>
</div>
</form>

View File

@@ -0,0 +1,67 @@
@using BTCPayServer
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Plugins.Breez
@using BTCPayServer.Security
@model string
@inject BreezService BreezService
@inject BTCPayNetworkProvider BtcPayNetworkProvider
@{
var storeId = Context.GetImplicitStoreId();
Layout = "_Layout";
ViewData.SetActivePage("Breez", "Sweep", "Sweep");
var sdk = BreezService.GetClient(storeId)?.Sdk;
var f = sdk.RecommendedFees();
var info = sdk.NodeInfo();
var deriv = Context.GetStoreData().GetDerivationSchemeSettings(BtcPayNetworkProvider, "BTC");
}
<datalist list="addresses">
@if (deriv is not null)
{
<option value="store"> Store wallet</option>
}
</datalist>
<datalist id="fees">
<option value="@f.fastestFee">Fastest fee</option>
<option value="@f.halfHourFee">Half hour fee</option>
<option value="@f.hourFee">Hour fee</option>
<option value="@f.economyFee">Economic fee</option>
<option value="@f.minimumFee">Minimum fee</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>
</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="address" class="form-label" data-required>address</label>
<input type="text" list="addresses" id="address" name="address" class="form-control" required/>
</div>
<div class="form-group">
<label for="satPerByte" class="form-label" data-required>Fees</label>
<input type="number" min="@f.minimumFee" list="satPerByte" id="satPerByte" name="satPerByte" class="form-control" required/>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</div>
</div>
</form>
@section PageFootContent {
<partial name="_ValidationScriptsPartial"/>
}

View File

@@ -0,0 +1,5 @@
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["NavPartialName"] = "_Nav";
}
@RenderBody()

View File

@@ -0,0 +1,33 @@
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Models.StoreViewModels
@using BTCPayServer.Plugins.Breez
@using BTCPayServer.Security
@inject BreezService BreezService
@{
var storeId = Model switch
{
string s => s,
StoreDashboardViewModel dashboardModel => dashboardModel.StoreId,
_ => Context.GetImplicitStoreId()
};
var sdk = BreezService.GetClient(storeId)?.Sdk;
}
<div class="sticky-header-setup"></div>
<div class="sticky-header mb-l">
<h2 class="mt-1 mb-2 mb-lg-4">Breez</h2>
<nav id="SectionNav">
<div class="nav">
@if (sdk is not null)
{
<a asp-action="Info" asp-route-storeId="@storeId" class="nav-link @ViewData.IsActivePage("Breez", null, "Info")">Info</a>
<a asp-action="Payments" asp-route-storeId="@storeId" class="nav-link @ViewData.IsActivePage("Breez", null, "Payments")">Payments</a>
<a asp-action="SwapIn" asp-route-storeId="@storeId" class="nav-link @ViewData.IsActivePage("Breez", null, "SwapIn")">Swap Ins</a>
<a asp-action="SwapOut" asp-route-storeId="@storeId" class="nav-link @ViewData.IsActivePage("Breez", null, "SwapOut")">Swap Outs</a>
}
<a asp-action="Configure" asp-route-storeId="@storeId" class="nav-link @ViewData.IsActivePage("Breez", null, "Configure")">Configuration</a>
</div>
</nav>
</div>

View File

@@ -0,0 +1,99 @@
@using BTCPayServer.Lightning
@using BTCPayServer.Models.StoreViewModels
@using BTCPayServer.Plugins.Breez
@using BTCPayServer.Security
@inject BreezService BreezService
@{
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;
var nodeState = sdk.NodeInfo();
var lspInfo = sdk.LspInfo();
}
<div class="widget store-numbers" id="Breez-Info" style="grid-column-start: 1; grid-column-end: 4;">
@if (Model is StoreDashboardViewModel)
{
<header>
<h4>Breez Node</h4>
<a asp-action="Info" asp-controller="Breez" asp-route-storeId="@storeId">
Manage
</a>
</header>
}
<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">@nodeState.id</h6>
</div>
@if (lspInfo is not null)
{
<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">@lspInfo.name connected</h6>
</div>
}
<div class="store-number">
<header>
<h6>On-Chain Balance</h6>
@if (Model is StoreDashboardViewModel && nodeState.onchainBalanceMsat > 0)
{
<div>
<a asp-action="Sweep" asp-controller="Breez" asp-route-storeId="@storeId">Sweep</a>
</div>
}
</header>
<div class="balance d-flex align-items-baseline gap-1">
<h3 class="d-inline-block me-1" data-balance="@LightMoney.MilliSatoshis(nodeState.onchainBalanceMsat)" data-sensitive>@LightMoney.MilliSatoshis(nodeState.onchainBalanceMsat)</h3>
<span class="text-secondary fw-semibold currency">BTC</span>
</div>
</div>
<div class="store-number">
<header>
<h6>Lightning Balance</h6>
@if (Model is StoreDashboardViewModel)
{
<div>
<a asp-action="SwapIn" asp-controller="Breez" asp-route-storeId="@storeId">Swap in</a>
@if (nodeState.channelsBalanceMsat > 0)
{
<span> - </span>
<a asp-action="SwapOut" asp-controller="Breez" asp-route-storeId="@storeId">Swap out</a>
}
</div>
}
</header>
<div class="balance d-flex align-items-baseline gap-1">
<h3 class="d-inline-block me-1" data-balance="@LightMoney.MilliSatoshis(nodeState.channelsBalanceMsat)" data-sensitive>@LightMoney.MilliSatoshis(nodeState.channelsBalanceMsat)</h3>
<span class="text-secondary fw-semibold currency">BTC</span>
</div>
</div>
<div class="store-number">
<header>
<h6>Lightning insight</h6>
</header>
<div class="balance d-flex align-items-baseline gap-1">
<h3 class="d-inline-block me-1" data-balance="@LightMoney.MilliSatoshis(nodeState.maxReceivableMsat)" data-sensitive>@LightMoney.MilliSatoshis(nodeState.maxReceivableMsat)</h3>
<span class="text-secondary fw-semibold currency">BTC receivable</span>
</div>
<div class="balance d-flex align-items-baseline gap-1">
<h3 class="d-inline-block me-1" data-balance="@LightMoney.MilliSatoshis(nodeState.maxPayableMsat)" data-sensitive>@LightMoney.MilliSatoshis(nodeState.maxPayableMsat)</h3>
<span class="text-secondary fw-semibold currency">BTC spendable</span>
</div>
</div>
</div>

View File

@@ -0,0 +1,90 @@
@using Breez.Sdk
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Lightning
@using BTCPayServer.Models.StoreViewModels
@using BTCPayServer.Plugins.Breez
@using BTCPayServer.Security
@model object
@inject BreezService BreezService
@{
var data = Model as List<Payment>;
var storeId = Context.GetImplicitStoreId();
if (data is null)
{
var sdk = BreezService.GetClient(storeId)?.Sdk;
if (sdk is null)
return;
data = sdk.ListPayments(new ListPaymentsRequest(PaymentTypeFilter.ALL, null, null, null, 0, 10));
}
var isDashboard = Model is StoreDashboardViewModel;
}
<div class="@(isDashboard ? "widget store-wallet-balance" : "")" style="@(isDashboard ? "grid-column-start: 4;grid-column-end: 13;" : "")">
@if (isDashboard)
{
<header>
<h3>Breez Payments</h3>
@if (data.Any())
{
<a asp-controller="Breez" 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>
<span class="text-break">@payment.id</span>
</td>
<td>
<span class="text-break">@DateTimeOffset.FromUnixTimeSeconds(payment.paymentTime).ToTimeAgo()</span>
</td>
<td>
<span class="text-break">>@payment.paymentType.ToString().ToLowerInvariant().Replace("_", " ")</span>
</td>
<td>
<span class="text-break">>@LightMoney.MilliSatoshis(payment.amountMsat).ToDecimal(LightMoneyUnit.BTC) BTC</span>
</td>
<td>
<span class="text-break">>@LightMoney.MilliSatoshis(payment.feeMsat).ToDecimal(LightMoneyUnit.BTC) BTC</span>
</td>
<td>
<span class="text-break">@payment.status.ToString().ToLowerInvariant()</span>
</td>
<td>
<span class="text-break">@payment.description</span>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>

View File

@@ -18,7 +18,7 @@
}
else
{
<a asp-action="Index" asp-controller="Breez" asp-route-storeId="@storeId">Breez needs to be configured beforehand.</a>
<a asp-action="Configure" asp-controller="Breez" asp-route-storeId="@storeId">Breez needs to be configured beforehand.</a>
}
</div>
@@ -26,22 +26,11 @@
const typePrefix = 'type=breez;store=@Model.StoreId';
const triggerEl = document.getElementById('LightningNodeType-Breez')
const connStringEl = document.getElementById('ConnectionString')
const connString = connStringEl.value;
const isBreez = connString.startsWith(typePrefix);
const setWallet = accessKey => connStringEl.value = accessKey.length
? `${typePrefix};`
: ''
if (isBreez) {
if (apiToken) {
walletEl.value = apiToken
} else if (walletId) {
const optionEl = document.querySelector(`option[data-wallet-id="${walletId}"]`)
if (optionEl) {
const accessKey = optionEl.getAttribute('value')
walletEl.value = accessKey
setWallet(accessKey)
}
}
// deactivate currently active tab and activate Breez tab
const activeEl = document.querySelector('input[name="LightningNodeType"]:checked')
@@ -52,6 +41,7 @@
triggerEl.setAttribute('checked', 'checked')
triggerEl.setAttribute('aria-selected', 'true')
document.getElementById('BreezSetup').classList.add('active', 'show')
}
}
@@ -62,12 +52,13 @@
tabTrigger.show()
}
delegate('change', '#BreezWallet', e => {
const { value } = e.target
setWallet(value)
const tabTrigger = new bootstrap.Tab(triggerEl)
triggerEl.checked = true
tabTrigger.show()
delegate('change', 'input[name="LightningNodeType"]', e => {
const activeEl = document.querySelector('input[name="LightningNodeType"]:checked')
if (activeEl.id === "LightningNodeType-Breez"){
connStringEl.value =typePrefix;
}
})
})
</script>