mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-12 19:02:01 +01:00
Display fiat amount previews in Transaction Details page (#6610)
* Code cleanup * Preparing model to include data needed for fiat display * Displaying fiat amount and allowing switching between it and BTC * Restoring parts removed by vibe coding * Making ToFiatAmount method work for in wider variety of cases * Tweaks for display and negative values * Calculating amounts on serverside and simplifying * Fix warnings --------- Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
parent
71dbfd9f28
commit
9dcf8d3251
4 changed files with 199 additions and 109 deletions
|
@ -367,6 +367,19 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fetch helper that will be used to convert the value to fiat
|
||||||
|
FiatRate rate = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
rate = await FetchRate(walletId);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// keeping model simple
|
||||||
|
// vm.RateError = ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
if (psbtObject.IsAllFinalized())
|
if (psbtObject.IsAllFinalized())
|
||||||
{
|
{
|
||||||
vm.CanCalculateBalance = false;
|
vm.CanCalculateBalance = false;
|
||||||
|
@ -374,7 +387,7 @@ namespace BTCPayServer.Controllers
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var balanceChange = psbtObject.GetBalance(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath);
|
var balanceChange = psbtObject.GetBalance(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath);
|
||||||
vm.BalanceChange = ValueToString(balanceChange, network);
|
vm.BalanceChange = ValueToString(balanceChange, network, rate);
|
||||||
vm.CanCalculateBalance = true;
|
vm.CanCalculateBalance = true;
|
||||||
vm.Positive = balanceChange >= Money.Zero;
|
vm.Positive = balanceChange >= Money.Zero;
|
||||||
}
|
}
|
||||||
|
@ -390,7 +403,7 @@ namespace BTCPayServer.Controllers
|
||||||
var balanceChange2 = txOut?.Value ?? Money.Zero;
|
var balanceChange2 = txOut?.Value ?? Money.Zero;
|
||||||
if (mine)
|
if (mine)
|
||||||
balanceChange2 = -balanceChange2;
|
balanceChange2 = -balanceChange2;
|
||||||
inputVm.BalanceChange = ValueToString(balanceChange2, network);
|
inputVm.BalanceChange = ValueToString(balanceChange2, network, rate);
|
||||||
inputVm.Positive = balanceChange2 >= Money.Zero;
|
inputVm.Positive = balanceChange2 >= Money.Zero;
|
||||||
inputVm.Index = (int)input.Index;
|
inputVm.Index = (int)input.Index;
|
||||||
|
|
||||||
|
@ -412,7 +425,7 @@ namespace BTCPayServer.Controllers
|
||||||
var balanceChange2 = output.Value;
|
var balanceChange2 = output.Value;
|
||||||
if (!mine)
|
if (!mine)
|
||||||
balanceChange2 = -balanceChange2;
|
balanceChange2 = -balanceChange2;
|
||||||
dest.Balance = ValueToString(balanceChange2, network);
|
dest.Balance = ValueToString(balanceChange2, network, rate);
|
||||||
dest.Positive = balanceChange2 >= Money.Zero;
|
dest.Positive = balanceChange2 >= Money.Zero;
|
||||||
dest.Destination = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString() ?? output.ScriptPubKey.ToString();
|
dest.Destination = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString() ?? output.ScriptPubKey.ToString();
|
||||||
var address = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString();
|
var address = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString();
|
||||||
|
@ -426,7 +439,7 @@ namespace BTCPayServer.Controllers
|
||||||
vm.Destinations.Add(new WalletPSBTReadyViewModel.DestinationViewModel
|
vm.Destinations.Add(new WalletPSBTReadyViewModel.DestinationViewModel
|
||||||
{
|
{
|
||||||
Positive = false,
|
Positive = false,
|
||||||
Balance = ValueToString(-fee, network),
|
Balance = ValueToString(-fee, network, rate),
|
||||||
Destination = "Mining fees"
|
Destination = "Mining fees"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Bitcoin;
|
using BTCPayServer.Payments.Bitcoin;
|
||||||
using BTCPayServer.Payments.PayJoin;
|
using BTCPayServer.Payments.PayJoin;
|
||||||
using BTCPayServer.Payouts;
|
using BTCPayServer.Payouts;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Labels;
|
using BTCPayServer.Services.Labels;
|
||||||
|
@ -33,9 +34,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Routing;
|
|
||||||
using Microsoft.AspNetCore.WebUtilities;
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
@ -58,9 +57,9 @@ namespace BTCPayServer.Controllers
|
||||||
private WalletRepository WalletRepository { get; }
|
private WalletRepository WalletRepository { get; }
|
||||||
private BTCPayNetworkProvider NetworkProvider { get; }
|
private BTCPayNetworkProvider NetworkProvider { get; }
|
||||||
private ExplorerClientProvider ExplorerClientProvider { get; }
|
private ExplorerClientProvider ExplorerClientProvider { get; }
|
||||||
public IServiceProvider ServiceProvider { get; }
|
private IServiceProvider ServiceProvider { get; }
|
||||||
public RateFetcher RateFetcher { get; }
|
private RateFetcher RateFetcher { get; }
|
||||||
public IStringLocalizer StringLocalizer { get; }
|
private IStringLocalizer StringLocalizer { get; }
|
||||||
|
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
private readonly NBXplorerDashboard _dashboard;
|
private readonly NBXplorerDashboard _dashboard;
|
||||||
|
@ -81,33 +80,35 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
private readonly PendingTransactionService _pendingTransactionService;
|
private readonly PendingTransactionService _pendingTransactionService;
|
||||||
readonly CurrencyNameTable _currencyTable;
|
readonly CurrencyNameTable _currencyTable;
|
||||||
|
private readonly DisplayFormatter _displayFormatter;
|
||||||
|
|
||||||
public UIWalletsController(
|
public UIWalletsController(
|
||||||
PendingTransactionService pendingTransactionService,
|
PendingTransactionService pendingTransactionService,
|
||||||
StoreRepository repo,
|
StoreRepository repo,
|
||||||
WalletRepository walletRepository,
|
WalletRepository walletRepository,
|
||||||
CurrencyNameTable currencyTable,
|
CurrencyNameTable currencyTable,
|
||||||
BTCPayNetworkProvider networkProvider,
|
BTCPayNetworkProvider networkProvider,
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
NBXplorerDashboard dashboard,
|
NBXplorerDashboard dashboard,
|
||||||
WalletHistogramService walletHistogramService,
|
WalletHistogramService walletHistogramService,
|
||||||
RateFetcher rateProvider,
|
RateFetcher rateProvider,
|
||||||
IAuthorizationService authorizationService,
|
IAuthorizationService authorizationService,
|
||||||
ExplorerClientProvider explorerProvider,
|
ExplorerClientProvider explorerProvider,
|
||||||
IFeeProviderFactory feeRateProvider,
|
IFeeProviderFactory feeRateProvider,
|
||||||
BTCPayWalletProvider walletProvider,
|
BTCPayWalletProvider walletProvider,
|
||||||
WalletReceiveService walletReceiveService,
|
WalletReceiveService walletReceiveService,
|
||||||
SettingsRepository settingsRepository,
|
SettingsRepository settingsRepository,
|
||||||
DelayedTransactionBroadcaster broadcaster,
|
DelayedTransactionBroadcaster broadcaster,
|
||||||
PayjoinClient payjoinClient,
|
PayjoinClient payjoinClient,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
PullPaymentHostedService pullPaymentHostedService,
|
PullPaymentHostedService pullPaymentHostedService,
|
||||||
LabelService labelService,
|
LabelService labelService,
|
||||||
DefaultRulesCollection defaultRules,
|
DefaultRulesCollection defaultRules,
|
||||||
PaymentMethodHandlerDictionary handlers,
|
PaymentMethodHandlerDictionary handlers,
|
||||||
Dictionary<PaymentMethodId, ICheckoutModelExtension> paymentModelExtensions,
|
Dictionary<PaymentMethodId, ICheckoutModelExtension> paymentModelExtensions,
|
||||||
IStringLocalizer stringLocalizer,
|
IStringLocalizer stringLocalizer,
|
||||||
TransactionLinkProviders transactionLinkProviders)
|
TransactionLinkProviders transactionLinkProviders,
|
||||||
|
DisplayFormatter displayFormatter)
|
||||||
{
|
{
|
||||||
_pendingTransactionService = pendingTransactionService;
|
_pendingTransactionService = pendingTransactionService;
|
||||||
_currencyTable = currencyTable;
|
_currencyTable = currencyTable;
|
||||||
|
@ -134,6 +135,7 @@ namespace BTCPayServer.Controllers
|
||||||
ServiceProvider = serviceProvider;
|
ServiceProvider = serviceProvider;
|
||||||
_walletHistogramService = walletHistogramService;
|
_walletHistogramService = walletHistogramService;
|
||||||
StringLocalizer = stringLocalizer;
|
StringLocalizer = stringLocalizer;
|
||||||
|
_displayFormatter = displayFormatter;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{walletId}/pending/{transactionId}/cancel")]
|
[HttpGet("{walletId}/pending/{transactionId}/cancel")]
|
||||||
|
@ -513,10 +515,7 @@ namespace BTCPayServer.Controllers
|
||||||
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||||
if (network == null || network.ReadonlyWallet)
|
if (network == null || network.ReadonlyWallet)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
var storeData = store.GetStoreBlob();
|
|
||||||
var rateRules = store.GetStoreBlob().GetRateRules(_defaultRules);
|
|
||||||
rateRules.Spread = 0.0m;
|
|
||||||
var currencyPair = new Rating.CurrencyPair(walletId.CryptoCode, storeData.DefaultCurrency);
|
|
||||||
double.TryParse(defaultAmount, out var amount);
|
double.TryParse(defaultAmount, out var amount);
|
||||||
|
|
||||||
var model = new WalletSendModel
|
var model = new WalletSendModel
|
||||||
|
@ -587,31 +586,46 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
model.FeeSatoshiPerByte = recommendedFees[1].GetAwaiter().GetResult()?.FeeRate;
|
model.FeeSatoshiPerByte = recommendedFees[1].GetAwaiter().GetResult()?.FeeRate;
|
||||||
model.CryptoDivisibility = network.Divisibility;
|
model.CryptoDivisibility = network.Divisibility;
|
||||||
using (CancellationTokenSource cts = new CancellationTokenSource())
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
try
|
var r = await FetchRate(walletId);
|
||||||
{
|
|
||||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
model.Rate = r.Rate;
|
||||||
var result = await RateFetcher.FetchRate(currencyPair, rateRules, new StoreIdRateContext(walletId.StoreId), cts.Token)
|
model.FiatDivisibility = _currencyTable.GetNumberFormatInfo(r.Fiat, true)
|
||||||
.WithCancellation(cts.Token);
|
.CurrencyDecimalDigits;
|
||||||
if (result.BidAsk != null)
|
model.Fiat = r.Fiat;
|
||||||
{
|
|
||||||
model.Rate = result.BidAsk.Center;
|
|
||||||
model.FiatDivisibility = _currencyTable.GetNumberFormatInfo(currencyPair.Right, true)
|
|
||||||
.CurrencyDecimalDigits;
|
|
||||||
model.Fiat = currencyPair.Right;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
model.RateError =
|
|
||||||
$"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex) { model.RateError = ex.Message; }
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) { model.RateError = ex.Message; }
|
||||||
|
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record FiatRate(decimal Rate, string Fiat);
|
||||||
|
private async Task<FiatRate> FetchRate(WalletId walletId)
|
||||||
|
{
|
||||||
|
var store = await Repository.FindStore(walletId.StoreId);
|
||||||
|
if (store is null)
|
||||||
|
throw new Exception("Store not found");
|
||||||
|
var storeData = store.GetStoreBlob();
|
||||||
|
var rateRules = storeData.GetRateRules(_defaultRules);
|
||||||
|
rateRules.Spread = 0.0m;
|
||||||
|
var currencyPair = new CurrencyPair(walletId.CryptoCode, storeData.DefaultCurrency);
|
||||||
|
|
||||||
|
using CancellationTokenSource cts = new();
|
||||||
|
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||||
|
var result = await RateFetcher.FetchRate(currencyPair, rateRules, new StoreIdRateContext(store.Id), cts.Token)
|
||||||
|
.WithCancellation(cts.Token);
|
||||||
|
|
||||||
|
if (result.BidAsk == null)
|
||||||
|
{
|
||||||
|
throw new Exception(
|
||||||
|
$"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new (result.BidAsk.Center, currencyPair.Right);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<string?> GetSeed(WalletId walletId, BTCPayNetwork network)
|
private async Task<string?> GetSeed(WalletId walletId, BTCPayNetwork network)
|
||||||
{
|
{
|
||||||
return await CanUseHotWallet() &&
|
return await CanUseHotWallet() &&
|
||||||
|
@ -1214,10 +1228,13 @@ namespace BTCPayServer.Controllers
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ValueToString(Money v, BTCPayNetworkBase network)
|
private WalletPSBTReadyViewModel.StringAmounts ValueToString(Money v, BTCPayNetworkBase network,
|
||||||
{
|
FiatRate? rate) =>
|
||||||
return v.ToString() + " " + network.CryptoCode;
|
new(
|
||||||
}
|
CryptoAmount : _displayFormatter.Currency(v.ToDecimal(MoneyUnit.BTC), network.CryptoCode),
|
||||||
|
FiatAmount : rate is null ? null
|
||||||
|
: _displayFormatter.Currency(rate.Rate * v.ToDecimal(MoneyUnit.BTC), rate.Fiat)
|
||||||
|
);
|
||||||
|
|
||||||
[HttpGet("{walletId}/rescan")]
|
[HttpGet("{walletId}/rescan")]
|
||||||
public async Task<IActionResult> WalletRescan(
|
public async Task<IActionResult> WalletRescan(
|
||||||
|
|
|
@ -15,7 +15,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||||
{
|
{
|
||||||
public bool Positive { get; set; }
|
public bool Positive { get; set; }
|
||||||
public string Destination { get; set; }
|
public string Destination { get; set; }
|
||||||
public string Balance { get; set; }
|
public StringAmounts Balance { get; set; }
|
||||||
public IEnumerable<TransactionTagModel> Labels { get; set; } = new List<TransactionTagModel>();
|
public IEnumerable<TransactionTagModel> Labels { get; set; } = new List<TransactionTagModel>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||||
public int Index { get; set; }
|
public int Index { get; set; }
|
||||||
public string Error { get; set; }
|
public string Error { get; set; }
|
||||||
public bool Positive { get; set; }
|
public bool Positive { get; set; }
|
||||||
public string BalanceChange { get; set; }
|
public StringAmounts BalanceChange { get; set; }
|
||||||
public IEnumerable<TransactionTagModel> Labels { get; set; } = new List<TransactionTagModel>();
|
public IEnumerable<TransactionTagModel> Labels { get; set; } = new List<TransactionTagModel>();
|
||||||
}
|
}
|
||||||
public class AmountViewModel
|
public class AmountViewModel
|
||||||
|
@ -34,7 +34,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||||
}
|
}
|
||||||
public AmountViewModel ReplacementBalanceChange { get; set; }
|
public AmountViewModel ReplacementBalanceChange { get; set; }
|
||||||
public bool HasErrors => Inputs.Count == 0 || Inputs.Any(i => !string.IsNullOrEmpty(i.Error));
|
public bool HasErrors => Inputs.Count == 0 || Inputs.Any(i => !string.IsNullOrEmpty(i.Error));
|
||||||
public string BalanceChange { get; set; }
|
public StringAmounts BalanceChange { get; set; }
|
||||||
public bool CanCalculateBalance { get; set; }
|
public bool CanCalculateBalance { get; set; }
|
||||||
public bool Positive { get; set; }
|
public bool Positive { get; set; }
|
||||||
public List<DestinationViewModel> Destinations { get; set; } = new List<DestinationViewModel>();
|
public List<DestinationViewModel> Destinations { get; set; } = new List<DestinationViewModel>();
|
||||||
|
@ -50,5 +50,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||||
Inputs[(int)err.InputIndex].Error = err.Message;
|
Inputs[(int)err.InputIndex].Error = err.Message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record StringAmounts(string CryptoAmount, string FiatAmount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,13 @@
|
||||||
<p class="lead text-center text-secondary">
|
<p class="lead text-center text-secondary">
|
||||||
<span text-translate="true">This transaction will change your balance:</span>
|
<span text-translate="true">This transaction will change your balance:</span>
|
||||||
<br>
|
<br>
|
||||||
<span class="text-@(Model.Positive ? "success" : "danger")">@Model.BalanceChange</span>
|
<span id="balance-toggle" class="text-@(Model.Positive ? "success" : "danger") cursor-pointer">
|
||||||
|
@Model.BalanceChange.CryptoAmount
|
||||||
|
@if (Model.BalanceChange.FiatAmount != null)
|
||||||
|
{
|
||||||
|
<small style="text-decoration: underline dotted;">(@Model.BalanceChange.FiatAmount)</small>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,36 +31,41 @@
|
||||||
<tr>
|
<tr>
|
||||||
@if (input.Error != null)
|
@if (input.Error != null)
|
||||||
{
|
{
|
||||||
<td>
|
<td>@input.Index <span class="text-danger" title="@input.Error"><vc:icon symbol="warning"/></span></td>
|
||||||
@input.Index <span class="text-danger" title="@input.Error"><vc:icon symbol="warning"/></span>
|
|
||||||
</td>
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<td>@input.Index</td>
|
<td>@input.Index</td>
|
||||||
}<td>
|
}
|
||||||
|
<td>
|
||||||
@if (input.Labels.Any())
|
@if (input.Labels.Any())
|
||||||
{
|
{
|
||||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||||
@foreach (var label in input.Labels)
|
@foreach (var label in input.Labels)
|
||||||
{
|
{
|
||||||
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
|
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
|
||||||
<span>@label.Text</span>
|
<span>@label.Text</span>
|
||||||
@if (!string.IsNullOrEmpty(label.Link))
|
@if (!string.IsNullOrEmpty(label.Link))
|
||||||
{
|
{
|
||||||
<a class="transaction-label-info transaction-details-icon" href="@label.Link"
|
<a class="transaction-label-info transaction-details-icon" href="@label.Link"
|
||||||
target="_blank" rel="noreferrer noopener" title="@label.Tooltip" data-bs-html="true"
|
target="_blank" rel="noreferrer noopener" title="@label.Tooltip"
|
||||||
data-bs-toggle="tooltip" data-bs-custom-class="transaction-label-tooltip">
|
data-bs-html="true" data-bs-toggle="tooltip"
|
||||||
<vc:icon symbol="info" />
|
data-bs-custom-class="transaction-label-tooltip">
|
||||||
</a>
|
<vc:icon symbol="info" />
|
||||||
}
|
</a>
|
||||||
</div>
|
}
|
||||||
}
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="text-end text-@(input.Positive ? "success" : "danger")">
|
||||||
|
<span class="amount-toggle cursor-pointer"
|
||||||
|
data-btc="@input.BalanceChange.CryptoAmount"
|
||||||
|
data-fiat="@input.BalanceChange.FiatAmount">
|
||||||
|
@input.BalanceChange.CryptoAmount
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end text-@(input.Positive ? "success" : "danger")">@input.BalanceChange</td>
|
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -77,28 +88,34 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-break">@destination.Destination</td>
|
<td class="text-break">@destination.Destination</td>
|
||||||
<td>
|
<td>
|
||||||
@if (destination.Labels.Any())
|
@if (destination.Labels.Any())
|
||||||
{
|
{
|
||||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||||
@foreach (var label in destination.Labels)
|
@foreach (var label in destination.Labels)
|
||||||
{
|
{
|
||||||
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
|
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
|
||||||
<span>@label.Text</span>
|
<span>@label.Text</span>
|
||||||
@if (!string.IsNullOrEmpty(label.Link))
|
@if (!string.IsNullOrEmpty(label.Link))
|
||||||
{
|
{
|
||||||
<a class="transaction-label-info transaction-details-icon" href="@label.Link"
|
<a class="transaction-label-info transaction-details-icon" href="@label.Link"
|
||||||
target="_blank" rel="noreferrer noopener" title="@label.Tooltip" data-bs-html="true"
|
target="_blank" rel="noreferrer noopener" title="@label.Tooltip"
|
||||||
data-bs-toggle="tooltip" data-bs-custom-class="transaction-label-tooltip">
|
data-bs-html="true" data-bs-toggle="tooltip"
|
||||||
<vc:icon symbol="info" />
|
data-bs-custom-class="transaction-label-tooltip">
|
||||||
</a>
|
<vc:icon symbol="info"/>
|
||||||
}
|
</a>
|
||||||
</div>
|
}
|
||||||
}
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="text-end text-@(destination.Positive ? "success" : "danger")">
|
||||||
|
<span class="amount-toggle cursor-pointer"
|
||||||
|
data-btc="@destination.Balance.CryptoAmount"
|
||||||
|
data-fiat="@destination.Balance.FiatAmount">
|
||||||
|
@destination.Balance.CryptoAmount
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end text-@(destination.Positive ? "success" : "danger")">@destination.Balance</td>
|
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -112,3 +129,44 @@
|
||||||
<b>@Model.FeeRate</b>
|
<b>@Model.FeeRate</b>
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const balanceElement = document.getElementById("balance-toggle");
|
||||||
|
const amounts = document.querySelectorAll(".amount-toggle");
|
||||||
|
|
||||||
|
let showFiat = false;
|
||||||
|
|
||||||
|
// Toggle all amounts when clicking balance header
|
||||||
|
balanceElement.addEventListener("click", function () {
|
||||||
|
// Check if all elements have a non-empty data-fiat
|
||||||
|
const allHaveValidFiat = Array.from(amounts).every(el => {
|
||||||
|
const fiatAmount = el.getAttribute("data-fiat");
|
||||||
|
return fiatAmount !== null && fiatAmount.trim() !== "";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!allHaveValidFiat) return; // If any is missing or empty, do nothing
|
||||||
|
|
||||||
|
showFiat = !showFiat;
|
||||||
|
amounts.forEach(el => {
|
||||||
|
const btcAmount = el.getAttribute("data-btc");
|
||||||
|
const fiatAmount = el.getAttribute("data-fiat");
|
||||||
|
el.innerText = showFiat ? fiatAmount : btcAmount;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle individual amounts
|
||||||
|
amounts.forEach(el => {
|
||||||
|
el.addEventListener("click", function (event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const btcAmount = el.getAttribute("data-btc");
|
||||||
|
const fiatAmount = el.getAttribute("data-fiat");
|
||||||
|
if (fiatAmount !== null && fiatAmount.trim() !== "") {
|
||||||
|
el.innerText = el.innerText === btcAmount ? fiatAmount : btcAmount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue