Skip to content

Commit 009cfc5

Browse files
authored
[GEN-1766] Swap view fixes and improvements (#436)
* increased regtest max routing amount for local env * Letting EstimateRouteFee error pass through so we can display it in the frontend * Move MaxRoutingFeesPercent to decimal * added decimal step to _maxMinerFeesPercent * Moved MaxServiceFees to percentage * Fix estimation not working when EstimateRouteFee returns null * misc fixes * Refactor confirmation modal * removed console log * reversed order of null check and function call * removed unecessary redefinition of ComponentCancellationToken * Throw if EnvironmentHelpers.GetOrDefault fails to cast * Better indentation * Improve Argument null exception with error message * check also lnd envs to fetch node * moved repeated html for error handling to a function
1 parent 18ac2ee commit 009cfc5

File tree

8 files changed

+134
-57
lines changed

8 files changed

+134
-57
lines changed

docker/loop/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ services:
1919
- shared_data:/shared
2020
command:
2121
- "daemon"
22-
- "--maxamt=5000000"
22+
- "--maxamt=160000000"
2323
- "--batcher_cutoff_time=1m"
2424
- "--lnd.host=alice:10009"
2525
- "--lnd.macaroonpath=/shared/lnd/alice/admin.macaroon"

src/Data/Repositories/NodeRepository.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,10 @@ public async Task<List<Node>> GetAllLoopdConfigured(string? userId = null)
152152
.Include(x => x.ChannelOperationRequestsAsDestination)
153153
.Include(x => x.ChannelOperationRequestsAsSource)
154154
.Where(node => node.Endpoint != null)
155-
.Where(node => !string.IsNullOrEmpty(node.LoopdEndpoint) && !string.IsNullOrEmpty(node.LoopdMacaroon));
155+
.Where(node => !string.IsNullOrEmpty(node.LoopdEndpoint) &&
156+
!string.IsNullOrEmpty(node.LoopdMacaroon) &&
157+
!string.IsNullOrEmpty(node.Endpoint) &&
158+
!string.IsNullOrEmpty(node.ChannelAdminMacaroon));
156159

157160
if (!string.IsNullOrEmpty(userId)) {
158161
query = query.Where(node => node.Users.Any(user => user.Id == userId));

src/Helpers/CustomExceptions.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
13
namespace NodeGuard.Helpers;
24

35
public class NoUTXOsAvailableException: Exception {}
@@ -37,4 +39,16 @@ public NotEnoughBalanceInWalletException(string? message = null): base(message)
3739
public class BumpingException : Exception
3840
{
3941
public BumpingException(string? message = null): base(message) {}
42+
}
43+
44+
public class CustomArgumentNullException : ArgumentNullException
45+
{
46+
public static void ThrowIfNull([NotNull] object? obj, string paramName, string message, params object[] args)
47+
{
48+
if (obj == null)
49+
{
50+
string formattedMessage = string.Format(message, args);
51+
throw new ArgumentNullException(paramName, formattedMessage);
52+
}
53+
}
4054
}

src/Helpers/EnvironmentHelpers.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace NodeGuard.Helpers
2+
{
3+
public static class EnvironmentHelpers
4+
{
5+
/// <summary>
6+
/// Gets the value of an environment variable and converts it to the specified type.
7+
/// If the environment variable is not set or is empty, returns the provided default value.
8+
/// </summary>
9+
/// <typeparam name="T">The type to convert the environment variable value to.</typeparam>
10+
/// <param name="variableName">The name of the environment variable.</param>
11+
/// <param name="defaultValue">The default value to return if the environment variable is not set or is empty.</param>
12+
/// <returns>The value of the environment variable converted to type T, or the default value.</returns>
13+
/// <exception cref="InvalidCastException">Thrown if the environment variable value cannot be converted to type T.</exception>
14+
/// <exception cref="FormatException">Thrown if the environment variable value is not in a format recognized by type T.</exception>
15+
/// <exception cref="OverflowException">Thrown if the environment variable value represents a number less than MinValue or greater than MaxValue of type T. (If applicable)</exception>
16+
public static T GetOrDefault<T>(string variableName, T defaultValue)
17+
{
18+
var value = Environment.GetEnvironmentVariable(variableName);
19+
if (string.IsNullOrEmpty(value))
20+
return defaultValue;
21+
22+
return (T)Convert.ChangeType(value, typeof(T));
23+
}
24+
}
25+
}

src/Services/LightningRouterService.cs

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Grpc.Net.Client;
44
using Microsoft.Extensions.Logging.Abstractions;
55
using NodeGuard.Data.Models;
6+
using NodeGuard.Helpers;
67
using Routerrpc;
78

89
namespace NodeGuard.Services;
@@ -66,25 +67,17 @@ public Router.RouterClient GetRouterClient(string? endpoint)
6667

6768
public async Task<RouteFeeResponse?> EstimateRouteFee(Node node, RouteFeeRequest routeFeeRequest, Router.RouterClient? client = null)
6869
{
69-
RouteFeeResponse? routeFeeResponse = null;
70-
try
71-
{
72-
client ??= GetRouterClient(node.Endpoint);
73-
routeFeeResponse = await client.EstimateRouteFeeAsync(routeFeeRequest,
74-
new Metadata
75-
{
76-
{
77-
"macaroon", node.ChannelAdminMacaroon
78-
}
79-
});
80-
}
81-
catch (Exception e)
82-
{
83-
_logger.LogError(e, "Error while estimating route fee for node {NodeId}", node.Id);
84-
return null;
85-
}
70+
CustomArgumentNullException.ThrowIfNull(node.ChannelAdminMacaroon, nameof(node.ChannelAdminMacaroon), "LND Macaroon for {NodeName} is not well configured", node.Name);
71+
72+
client ??= GetRouterClient(node.Endpoint);
8673

87-
return routeFeeResponse;
74+
return await client.EstimateRouteFeeAsync(routeFeeRequest,
75+
new Metadata
76+
{
77+
{
78+
"macaroon", node.ChannelAdminMacaroon
79+
}
80+
});
8881
}
8982
}
9083

src/Services/LoopService.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,20 +147,19 @@ public async Task<SwapResponse> CreateSwapOutAsync(Node node, SwapOutRequest req
147147
Dest = request.Address,
148148
MaxMinerFee = request.MaxMinerFees ?? 0,
149149
MaxPrepayAmt = request.PrepayAmtSat ?? 0,
150-
MaxSwapFee = request.MaxServiceFees ?? 0,
151-
MaxPrepayRoutingFee = request.MaxRoutingFeesPercent != null ? calcFee(request.PrepayAmtSat ?? 0, maxRoutingFeeBaseSats, request.MaxRoutingFeesPercent.Value * 10000) : getMaxRoutingFee(request.PrepayAmtSat ?? 0),
152-
MaxSwapRoutingFee = request.MaxRoutingFeesPercent != null ? calcFee(request.Amount, maxRoutingFeeBaseSats, request.MaxRoutingFeesPercent.Value * 10000) : getMaxRoutingFee(request.Amount),
150+
MaxPrepayRoutingFee = request.MaxRoutingFeesPercent != null ? calcFee(request.PrepayAmtSat ?? 0, maxRoutingFeeBaseSats, (long)(request.MaxRoutingFeesPercent.Value * 10000)) : getMaxRoutingFee(request.PrepayAmtSat ?? 0),
151+
MaxSwapRoutingFee = request.MaxRoutingFeesPercent != null ? calcFee(request.Amount, maxRoutingFeeBaseSats, (long)(request.MaxRoutingFeesPercent.Value * 10000)) : getMaxRoutingFee(request.Amount),
153152
SweepConfTarget = request.SweepConfTarget,
154153
HtlcConfirmations = 3,
155154
SwapPublicationDeadline = (ulong)DateTimeOffset.UtcNow.AddMinutes(request.SwapPublicationDeadlineMinutes).ToUnixTimeSeconds(),
156155
Label = $"Loop Out {request.Amount} sats on date {DateTime.UtcNow} to {request.Address} via NodeGuard",
157156
Initiator = "NodeGuard",
158157
};
159158

160-
if (request.MaxServiceFees.HasValue)
159+
if (request.MaxServiceFeesPercent.HasValue)
161160
{
162-
loopReq.MaxSwapFee = (long)request.MaxServiceFees.Value;
163-
_logger.LogDebug("Max fees set to {MaxFees}", request.MaxServiceFees.Value);
161+
loopReq.MaxSwapFee = (long)(request.Amount * request.MaxServiceFeesPercent.Value / 100);
162+
_logger.LogDebug("Max fees set to {MaxFees}", loopReq.MaxSwapFee);
164163
}
165164

166165
if (request.ChannelsOut != null && request.ChannelsOut.Length > 0)

src/Services/SwapsService.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ public class SwapOutRequest
77
{
88
public long Amount { get; set; }
99
public string? Address { get; set; }
10-
public long? MaxServiceFees { get; set; }
10+
public decimal? MaxServiceFeesPercent { get; set; }
1111
public long? MaxMinerFees { get; set; }
1212
public ulong[]? ChannelsOut { get; set; }
13-
public int? MaxRoutingFeesPercent { get; set; }
13+
public decimal? MaxRoutingFeesPercent { get; set; }
1414
public long? PrepayAmtSat { get; set; }
1515
public int SwapPublicationDeadlineMinutes { get; set; } = 60;
1616
public int SweepConfTarget { get; set; } = 400;
@@ -100,9 +100,8 @@ private async Task<SwapOutQuoteResponse> GetLoopQuoteAsync(Node node, SwapOutQuo
100100
};
101101

102102
var lnResponse = await _lightningService.EstimateRouteFee(node.PubKey, request.Amount, null, 30);
103-
ArgumentNullException.ThrowIfNull(lnResponse, nameof(lnResponse));
104103

105-
if (lnResponse.FailureReason != Lnrpc.PaymentFailureReason.FailureReasonNone)
104+
if (lnResponse == null || lnResponse.FailureReason != Lnrpc.PaymentFailureReason.FailureReasonNone)
106105
{
107106
quote.CouldEstimateRoutingFees = false;
108107
quote.OffChainFees = 0;

src/Shared/NewSwapModal.razor

Lines changed: 71 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,12 @@
6666
</Tooltip>
6767
</FieldLabel>
6868
<div class="d-flex">
69-
<NumericPicker TValue="int"
69+
<NumericPicker TValue="decimal"
7070
@bind-Value="_maxRoutingFeesPercent"
7171
CurrencySymbolPlacement="CurrencySymbolPlacement.Suffix"
7272
CurrencySymbol="%"
73+
Decimals="2"
74+
Step="0.1m"
7375
Min="0"
7476
Max="100">
7577
</NumericPicker>
@@ -122,6 +124,8 @@
122124
@bind-Value="_maxMinerFeesPercent"
123125
CurrencySymbolPlacement="CurrencySymbolPlacement.Suffix"
124126
CurrencySymbol="%"
127+
Decimals="2"
128+
Step="0.1m"
125129
Min="0"
126130
Max="100">
127131
</NumericPicker>
@@ -132,21 +136,26 @@
132136
}
133137
@Math.Round(amount, 8) BTC (~@Math.Round(PriceConversionService.BtcToUsdConversion(amount, _btcPrice), 2) USD)
134138
</FieldHelp>
139+
<FieldHelp Style="color: red !important;">Max miner fee is calculated automatically, only change the value if you want to override the default</FieldHelp>
135140
</Field>
136141
<Field>
137142
<FieldLabel>Max Service Fees</FieldLabel>
138143
<div class="d-flex">
139144
<NumericPicker TValue="decimal"
140-
@bind-Value="_maxServiceFeesBtc"
145+
@bind-Value="_maxServiceFeesPercent"
141146
CurrencySymbolPlacement="CurrencySymbolPlacement.Suffix"
142-
CurrencySymbol=" BTC"
143-
Decimals="8"
144-
Step="0.00001000m"
145-
Min="0.00001000m">
147+
CurrencySymbol="%"
148+
Decimals="2"
149+
Step="0.1m"
150+
Min="0"
151+
Max="100">
146152
</NumericPicker>
147153
</div>
148154
<FieldHelp>
149-
$@Math.Round(PriceConversionService.BtcToUsdConversion(_maxServiceFeesBtc, _btcPrice), 2)
155+
@{
156+
var amount = _maxServiceFeesPercent * _amountBtc / 100;
157+
}
158+
@Math.Round(amount, 8) BTC (~@Math.Round(PriceConversionService.BtcToUsdConversion(amount, _btcPrice), 2) USD)
150159
</FieldHelp>
151160
</Field>
152161
<Field>
@@ -205,7 +214,7 @@
205214
private SwapDirection _selectedDirection = SwapDirection.Out;
206215
private SwapProvider _selectedProvider = SwapProvider.Loop;
207216
private decimal _amountBtc = 0.0025m;
208-
private decimal _maxServiceFeesBtc = 0.00001m;
217+
private decimal _maxServiceFeesPercent = 0.1m;
209218
private List<Wallet> _availableWallets = [];
210219
private Wallet? _selectedWallet;
211220
private string? _amountInWalletBtc;
@@ -217,12 +226,9 @@
217226
private bool _showAdvancedOptions = false;
218227
private int _swapPublicationDeadlineOffsetMinutes = Constants.IS_DEV_ENVIRONMENT ? 1 : 30;
219228
private int _maxMinerFeesPercent = 0;
220-
private int _maxRoutingFeesPercent = 1;
229+
private decimal _maxRoutingFeesPercent = 0.5m;
221230
private int _sweepConfirmationTarget = Constants.IS_DEV_ENVIRONMENT ? 250 : 400;
222231

223-
private CancellationTokenSource? _cancellationTokenSource;
224-
protected CancellationToken ComponentCancellationToken => (_cancellationTokenSource ??= new()).Token;
225-
226232
private string FormatBtcWithUsd(decimal btcAmount)
227233
{
228234
var usdAmount = Math.Round(PriceConversionService.BtcToUsdConversion(btcAmount, _btcPrice), 2);
@@ -264,7 +270,7 @@
264270
var swapRequest = new SwapOutRequest
265271
{
266272
Amount = amount,
267-
MaxServiceFees = NBitcoin.Money.FromUnit(_maxServiceFeesBtc, NBitcoin.MoneyUnit.BTC),
273+
MaxServiceFeesPercent = _maxServiceFeesPercent,
268274
PrepayAmtSat = _confirmationModal.GetData<long>("PrepayAmtSat"),
269275
Address = btcAddress.Address.ToString(),
270276
SwapPublicationDeadlineMinutes = _swapPublicationDeadlineOffsetMinutes,
@@ -316,6 +322,7 @@
316322
}
317323
catch (RpcException ex)
318324
{
325+
Logger.LogError(ex, "Error creating swap");
319326
ToastService.ShowError(ex.Status.Detail);
320327
}
321328
catch (Exception ex)
@@ -334,8 +341,35 @@
334341

335342
private async Task ShowConfirmationModal()
336343
{
344+
var HTLC_SWEEP_FEE_SAT_ON_QUOTE_ERROR = _amountBtc * EnvironmentHelpers.GetOrDefault("HTLC_SWEEP_FEE_SAT_ON_QUOTE_ERROR", 0.015m);
345+
var PREPAY_AMT_SAT_ON_QUOTE_ERROR = _amountBtc * EnvironmentHelpers.GetOrDefault("PREPAY_AMT_SAT_ON_QUOTE_ERROR", 0.005m);
346+
347+
var middleChunkMessage = $"<strong>- Provider:</strong> {_selectedProvider}<br>" +
348+
$"<strong>- Direction:</strong> {_selectedDirection}<br>" +
349+
$"<strong>- Amount:</strong> {FormatBtcWithUsd(_amountBtc)}<br>" +
350+
$"<strong>- Origin Node:</strong> {_selectedNode?.Name}<br>" +
351+
$"<strong>- Destination Wallet:</strong> {_selectedWallet?.Name}<br><br>";
352+
337353
ArgumentNullException.ThrowIfNull(_selectedWallet, nameof(_selectedWallet));
338354
ArgumentNullException.ThrowIfNull(_selectedNode, nameof(_selectedNode));
355+
356+
var showErrorModal = async (string errorMessage) => {
357+
_confirmationModalBody = "Quote was not successful because of the following error: <br><br>" +
358+
$"<span style='color: red;'>{errorMessage}</span>" + "<br><br>" +
359+
"The swap will be created with the following details:<br><br>" +
360+
middleChunkMessage +
361+
"<span style='color: red'>The fees could not be estimated, the swap might not go through.</span><br><br>" +
362+
"<br>Please confirm that you want to proceed.";
363+
364+
var data = new Dictionary<string, object>
365+
{
366+
{ "HtlcSweepFeeSat", HTLC_SWEEP_FEE_SAT_ON_QUOTE_ERROR },
367+
{ "PrepayAmtSat", PREPAY_AMT_SAT_ON_QUOTE_ERROR }
368+
};
369+
370+
await _confirmationModal.ShowModal(data);
371+
};
372+
339373
try
340374
{
341375
var request = new SwapOutQuoteRequest{
@@ -348,24 +382,34 @@
348382
var offChainFee = new Money(quote.OffChainFees);
349383
var onChainFee = new Money(quote.OnChainFees);
350384
var totalFees = new Money(quote.ServiceFees + quote.OffChainFees + quote.OnChainFees);
351-
385+
386+
var selectedServiceFee = _maxServiceFeesPercent * _amountBtc / 100;
387+
var serviceFeeChunkMessage = $"<li><strong>Service Fee:</strong> {FormatBtcWithUsd(serviceFee.ToUnit(MoneyUnit.BTC))}</li>";
388+
if (serviceFee.ToUnit(MoneyUnit.BTC) > selectedServiceFee) {
389+
serviceFeeChunkMessage += $"<li style='list-style-type: none;'><span style='color: orange;'> (You selected: {FormatBtcWithUsd(selectedServiceFee)}, it might not go through)</span></li>";
390+
}
391+
392+
var selectedRoutingFee = _maxRoutingFeesPercent * _amountBtc / 100;
393+
var routingFeeChunkMessage = $"<li><strong>Off-Chain Fee:</strong> {FormatBtcWithUsd(offChainFee.ToUnit(MoneyUnit.BTC))}</li>";
394+
if (offChainFee.ToUnit(MoneyUnit.BTC) > selectedRoutingFee) {
395+
routingFeeChunkMessage += $"<li style='list-style-type: none;'><span style='color: orange;'> (You selected: {FormatBtcWithUsd(selectedRoutingFee)}, it might not go through)</span></li>";
396+
}
397+
352398
_confirmationModalBody = $"You are about to create a new swap with the following details:<br><br>" +
353-
$"<strong>- Provider:</strong> {_selectedProvider}<br>" +
354-
$"<strong>- Direction:</strong> {_selectedDirection}<br>" +
355-
$"<strong>- Amount:</strong> {FormatBtcWithUsd(_amountBtc)}<br>" +
399+
middleChunkMessage +
356400
$"<strong>- Estimated Fees:</strong><br>" +
357-
$"&nbsp;&nbsp;&nbsp;&nbsp;<strong>• Service Fee:</strong> {FormatBtcWithUsd(serviceFee.ToUnit(MoneyUnit.BTC))}<br>" +
358-
$"&nbsp;&nbsp;&nbsp;&nbsp;<strong>• Off-Chain Fee:</strong> {FormatBtcWithUsd(offChainFee.ToUnit(MoneyUnit.BTC))}<br>" +
359-
$"&nbsp;&nbsp;&nbsp;&nbsp;<strong>• On-Chain Fee:</strong> {FormatBtcWithUsd(onChainFee.ToUnit(MoneyUnit.BTC))}<br>" +
360-
$"&nbsp;&nbsp;&nbsp;&nbsp;<strong>• Total:</strong> {FormatBtcWithUsd(totalFees.ToUnit(MoneyUnit.BTC))}<br>" +
361-
$"<strong>- Origin Node:</strong> {_selectedNode?.Name}<br>" +
362-
$"<strong>- Destination Wallet:</strong> {_selectedWallet?.Name}<br><br>" +
401+
"<ul>"+
402+
serviceFeeChunkMessage +
403+
routingFeeChunkMessage +
404+
$"<li><strong>On-Chain Fee:</strong> {FormatBtcWithUsd(onChainFee.ToUnit(MoneyUnit.BTC))}</li>" +
405+
$"<li><strong>Total:</strong> {FormatBtcWithUsd(totalFees.ToUnit(MoneyUnit.BTC))}</li>" +
406+
"<ul>"+
363407
"<br>" +
364-
$"<strong>- Estimated total cost:</strong> {FormatBtcWithUsd((Money.FromUnit(_amountBtc, MoneyUnit.BTC) + totalFees).ToUnit(MoneyUnit.BTC))}<br>";
408+
$"<strong>- Estimated total cost:</strong> {FormatBtcWithUsd(totalFees.ToUnit(MoneyUnit.BTC))} (Percentage of total swap: {Math.Round(totalFees.ToUnit(MoneyUnit.BTC) / _amountBtc * 100, 2)}%)<br>";
365409

366410
if (!quote.CouldEstimateRoutingFees)
367411
{
368-
_confirmationModalBody += "<br>" + $"<span style='color: orange;'><strong>Warning:</strong> Routing fees could not be estimated. The actual fees might be higher than the estimated ones.</span><br><br>";
412+
_confirmationModalBody += "<br>" + $"<span style='color: orange;'><strong>Warning:</strong> Routing fees could not be estimated. The actual fees will be higher than 0.</span><br><br>";
369413
}
370414

371415
_confirmationModalBody += $"<br>Please confirm that you want to proceed.";
@@ -379,12 +423,12 @@
379423
catch (RpcException ex)
380424
{
381425
Logger.LogError(ex, "Error getting swap quote");
382-
ToastService.ShowError(ex.Status.Detail);
426+
await showErrorModal(ex.Status.Detail);
383427
}
384428
catch (Exception ex)
385429
{
386430
Logger.LogError(ex, "Error getting swap quote");
387-
ToastService.ShowError("Failed to get swap quote, please try again later");
431+
await showErrorModal(ex.Message);
388432
}
389433
}
390434

0 commit comments

Comments
 (0)