diff --git a/BTCPayServer/Controllers/UIWalletsController.PSBT.cs b/BTCPayServer/Controllers/UIWalletsController.PSBT.cs index b09c91fbe..2e2950ba9 100644 --- a/BTCPayServer/Controllers/UIWalletsController.PSBT.cs +++ b/BTCPayServer/Controllers/UIWalletsController.PSBT.cs @@ -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()) { vm.CanCalculateBalance = false; @@ -374,7 +387,7 @@ namespace BTCPayServer.Controllers else { var balanceChange = psbtObject.GetBalance(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath); - vm.BalanceChange = ValueToString(balanceChange, network); + vm.BalanceChange = ValueToString(balanceChange, network, rate); vm.CanCalculateBalance = true; vm.Positive = balanceChange >= Money.Zero; } @@ -390,7 +403,7 @@ namespace BTCPayServer.Controllers var balanceChange2 = txOut?.Value ?? Money.Zero; if (mine) balanceChange2 = -balanceChange2; - inputVm.BalanceChange = ValueToString(balanceChange2, network); + inputVm.BalanceChange = ValueToString(balanceChange2, network, rate); inputVm.Positive = balanceChange2 >= Money.Zero; inputVm.Index = (int)input.Index; @@ -412,7 +425,7 @@ namespace BTCPayServer.Controllers var balanceChange2 = output.Value; if (!mine) balanceChange2 = -balanceChange2; - dest.Balance = ValueToString(balanceChange2, network); + dest.Balance = ValueToString(balanceChange2, network, rate); dest.Positive = balanceChange2 >= Money.Zero; dest.Destination = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString() ?? output.ScriptPubKey.ToString(); var address = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString(); @@ -426,7 +439,7 @@ namespace BTCPayServer.Controllers vm.Destinations.Add(new WalletPSBTReadyViewModel.DestinationViewModel { Positive = false, - Balance = ValueToString(-fee, network), + Balance = ValueToString(-fee, network, rate), Destination = "Mining fees" }); } diff --git a/BTCPayServer/Controllers/UIWalletsController.cs b/BTCPayServer/Controllers/UIWalletsController.cs index 4b5bc8984..2b1c31758 100644 --- a/BTCPayServer/Controllers/UIWalletsController.cs +++ b/BTCPayServer/Controllers/UIWalletsController.cs @@ -21,6 +21,7 @@ using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.PayJoin; using BTCPayServer.Payouts; +using BTCPayServer.Rating; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Labels; @@ -33,9 +34,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.WebUtilities; -using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using NBitcoin; @@ -58,9 +57,9 @@ namespace BTCPayServer.Controllers private WalletRepository WalletRepository { get; } private BTCPayNetworkProvider NetworkProvider { get; } private ExplorerClientProvider ExplorerClientProvider { get; } - public IServiceProvider ServiceProvider { get; } - public RateFetcher RateFetcher { get; } - public IStringLocalizer StringLocalizer { get; } + private IServiceProvider ServiceProvider { get; } + private RateFetcher RateFetcher { get; } + private IStringLocalizer StringLocalizer { get; } private readonly UserManager _userManager; private readonly NBXplorerDashboard _dashboard; @@ -81,33 +80,35 @@ namespace BTCPayServer.Controllers private readonly PendingTransactionService _pendingTransactionService; readonly CurrencyNameTable _currencyTable; + private readonly DisplayFormatter _displayFormatter; public UIWalletsController( PendingTransactionService pendingTransactionService, StoreRepository repo, - WalletRepository walletRepository, - CurrencyNameTable currencyTable, - BTCPayNetworkProvider networkProvider, - UserManager userManager, - NBXplorerDashboard dashboard, - WalletHistogramService walletHistogramService, - RateFetcher rateProvider, - IAuthorizationService authorizationService, - ExplorerClientProvider explorerProvider, - IFeeProviderFactory feeRateProvider, - BTCPayWalletProvider walletProvider, - WalletReceiveService walletReceiveService, - SettingsRepository settingsRepository, - DelayedTransactionBroadcaster broadcaster, - PayjoinClient payjoinClient, - IServiceProvider serviceProvider, - PullPaymentHostedService pullPaymentHostedService, - LabelService labelService, - DefaultRulesCollection defaultRules, - PaymentMethodHandlerDictionary handlers, - Dictionary paymentModelExtensions, - IStringLocalizer stringLocalizer, - TransactionLinkProviders transactionLinkProviders) + WalletRepository walletRepository, + CurrencyNameTable currencyTable, + BTCPayNetworkProvider networkProvider, + UserManager userManager, + NBXplorerDashboard dashboard, + WalletHistogramService walletHistogramService, + RateFetcher rateProvider, + IAuthorizationService authorizationService, + ExplorerClientProvider explorerProvider, + IFeeProviderFactory feeRateProvider, + BTCPayWalletProvider walletProvider, + WalletReceiveService walletReceiveService, + SettingsRepository settingsRepository, + DelayedTransactionBroadcaster broadcaster, + PayjoinClient payjoinClient, + IServiceProvider serviceProvider, + PullPaymentHostedService pullPaymentHostedService, + LabelService labelService, + DefaultRulesCollection defaultRules, + PaymentMethodHandlerDictionary handlers, + Dictionary paymentModelExtensions, + IStringLocalizer stringLocalizer, + TransactionLinkProviders transactionLinkProviders, + DisplayFormatter displayFormatter) { _pendingTransactionService = pendingTransactionService; _currencyTable = currencyTable; @@ -134,6 +135,7 @@ namespace BTCPayServer.Controllers ServiceProvider = serviceProvider; _walletHistogramService = walletHistogramService; StringLocalizer = stringLocalizer; + _displayFormatter = displayFormatter; } [HttpGet("{walletId}/pending/{transactionId}/cancel")] @@ -513,10 +515,7 @@ namespace BTCPayServer.Controllers var network = this.NetworkProvider.GetNetwork(walletId.CryptoCode); if (network == null || network.ReadonlyWallet) 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); var model = new WalletSendModel @@ -587,31 +586,46 @@ namespace BTCPayServer.Controllers model.FeeSatoshiPerByte = recommendedFees[1].GetAwaiter().GetResult()?.FeeRate; model.CryptoDivisibility = network.Divisibility; - using (CancellationTokenSource cts = new CancellationTokenSource()) + + try { - try - { - cts.CancelAfter(TimeSpan.FromSeconds(5)); - var result = await RateFetcher.FetchRate(currencyPair, rateRules, new StoreIdRateContext(walletId.StoreId), cts.Token) - .WithCancellation(cts.Token); - if (result.BidAsk != null) - { - 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().ToArray())})"; - } - } - catch (Exception ex) { model.RateError = ex.Message; } + var r = await FetchRate(walletId); + + model.Rate = r.Rate; + model.FiatDivisibility = _currencyTable.GetNumberFormatInfo(r.Fiat, true) + .CurrencyDecimalDigits; + model.Fiat = r.Fiat; } + catch (Exception ex) { model.RateError = ex.Message; } + return View(model); } + public record FiatRate(decimal Rate, string Fiat); + private async Task 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().ToArray())})"); + } + + return new (result.BidAsk.Center, currencyPair.Right); + } + private async Task GetSeed(WalletId walletId, BTCPayNetwork network) { return await CanUseHotWallet() && @@ -1214,10 +1228,13 @@ namespace BTCPayServer.Controllers }); } - private string ValueToString(Money v, BTCPayNetworkBase network) - { - return v.ToString() + " " + network.CryptoCode; - } + private WalletPSBTReadyViewModel.StringAmounts ValueToString(Money v, BTCPayNetworkBase network, + FiatRate? rate) => + 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")] public async Task WalletRescan( diff --git a/BTCPayServer/Models/WalletViewModels/WalletPSBTReadyViewModel.cs b/BTCPayServer/Models/WalletViewModels/WalletPSBTReadyViewModel.cs index 2c0e03552..0c9946034 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletPSBTReadyViewModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletPSBTReadyViewModel.cs @@ -15,7 +15,7 @@ namespace BTCPayServer.Models.WalletViewModels { public bool Positive { get; set; } public string Destination { get; set; } - public string Balance { get; set; } + public StringAmounts Balance { get; set; } public IEnumerable Labels { get; set; } = new List(); } @@ -24,7 +24,7 @@ namespace BTCPayServer.Models.WalletViewModels public int Index { get; set; } public string Error { get; set; } public bool Positive { get; set; } - public string BalanceChange { get; set; } + public StringAmounts BalanceChange { get; set; } public IEnumerable Labels { get; set; } = new List(); } public class AmountViewModel @@ -34,7 +34,7 @@ namespace BTCPayServer.Models.WalletViewModels } public AmountViewModel ReplacementBalanceChange { get; set; } 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 Positive { get; set; } public List Destinations { get; set; } = new List(); @@ -50,5 +50,7 @@ namespace BTCPayServer.Models.WalletViewModels Inputs[(int)err.InputIndex].Error = err.Message; } } + + public record StringAmounts(string CryptoAmount, string FiatAmount); } } diff --git a/BTCPayServer/Views/UIWallets/_PSBTInfo.cshtml b/BTCPayServer/Views/UIWallets/_PSBTInfo.cshtml index 6e272c8fd..1269d9ffe 100644 --- a/BTCPayServer/Views/UIWallets/_PSBTInfo.cshtml +++ b/BTCPayServer/Views/UIWallets/_PSBTInfo.cshtml @@ -5,7 +5,13 @@

This transaction will change your balance:
- @Model.BalanceChange + + @Model.BalanceChange.CryptoAmount + @if (Model.BalanceChange.FiatAmount != null) + { + (@Model.BalanceChange.FiatAmount) + } +

} @@ -25,36 +31,41 @@ @if (input.Error != null) { - - @input.Index - + @input.Index } else { @input.Index - } + } + @if (input.Labels.Any()) - { -
- @foreach (var label in input.Labels) - { -
- @label.Text - @if (!string.IsNullOrEmpty(label.Link)) - { - - - - } -
- } -
- } + { +
+ @foreach (var label in input.Labels) + { +
+ @label.Text + @if (!string.IsNullOrEmpty(label.Link)) + { + + + + } +
+ } +
+ } + + + + @input.BalanceChange.CryptoAmount + - @input.BalanceChange - } @@ -77,28 +88,34 @@ @destination.Destination - @if (destination.Labels.Any()) - { -
- @foreach (var label in destination.Labels) - { -
- @label.Text - @if (!string.IsNullOrEmpty(label.Link)) - { - - - - } -
- } -
- } + @if (destination.Labels.Any()) + { +
+ @foreach (var label in destination.Labels) + { +
+ @label.Text + @if (!string.IsNullOrEmpty(label.Link)) + { + + + + } +
+ } +
+ } + + + + @destination.Balance.CryptoAmount + - @destination.Balance - } @@ -112,3 +129,44 @@ @Model.FeeRate

} + + + +