Allow only single run of `PrivacySuggestionsFlyoutViewModel.BuildPrivacySuggestionsAsync`
MODIFY
WalletWasabi.Fluent.Desktop/Program.cs
WalletWasabi.Fluent.Desktop/Program.cs
@@ -93,7 +93,8 @@ public class Program
Logger.LogSoftwareStarted("Wasabi GUI");
AppBuilder
- .Configure(() => new App(async () => await Global.InitializeNoWalletAsync(terminateService), runGuiInBackground)).UseReactiveUI()
+ .Configure(() => new App(async () => await Global.InitializeNoWalletAsync(terminateService), runGuiInBackground))
+ .UseReactiveUI()
.SetupAppBuilder()
.AfterSetup(_ =>
{
@@ -199,14 +200,14 @@ public class Program
private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
- Logger.LogDebug(e.Exception);
+ // Logger.LogDebug(e.Exception);
}
private static void CurrentDomain_UnhandledException(object? sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject is Exception ex)
{
- Logger.LogWarning(ex);
+ //Logger.LogWarning(ex);
}
}

MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/Send/ChangeAvoidanceSuggestionViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/Send/ChangeAvoidanceSuggestionViewModel.cs
@@ -54,16 +54,16 @@ public partial class ChangeAvoidanceSuggestionViewModel : SuggestionViewModel
decimal usdExchangeRate,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
- var selections = ChangelessTransactionCoinSelector.GetAllStrategyResultsAsync(
+ IAsyncEnumerable<IEnumerable<SmartCoin>> selectionsTask = ChangelessTransactionCoinSelector.GetAllStrategyResultsAsync(
coinsToUse,
transactionInfo.FeeRate,
new TxOut(transactionInfo.Amount, destination),
maxInputCount,
- cancellationToken).ConfigureAwait(false);
+ cancellationToken);
HashSet<Money> foundSolutionsByAmount = new();
- await foreach (var selection in selections)
+ await foreach (IEnumerable<SmartCoin> selection in selectionsTask.ConfigureAwait(false))
{
if (selection.Any())
{
@@ -86,7 +86,7 @@ public partial class ChangeAvoidanceSuggestionViewModel : SuggestionViewModel
if (transaction is not null)
{
- var destinationAmount = transaction.CalculateDestinationAmount();
+ Money destinationAmount = transaction.CalculateDestinationAmount();
// If BnB solutions become the same transaction somehow, do not show the same suggestion twice.
if (!foundSolutionsByAmount.Contains(destinationAmount))

MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/Send/PrivacySuggestionsFlyoutViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/Send/PrivacySuggestionsFlyoutViewModel.cs
using NBitcoin;
+using Nito.AsyncEx;
using ReactiveUI;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -6,9 +7,9 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using WalletWasabi.Blockchain.Analysis.Clustering;
using WalletWasabi.Blockchain.TransactionBuilding;
using WalletWasabi.Fluent.Helpers;
+using WalletWasabi.Logging;
using WalletWasabi.Wallets;
namespace WalletWasabi.Fluent.ViewModels.Wallets.Send;
@@ -20,6 +21,12 @@ public partial class PrivacySuggestionsFlyoutViewModel : ViewModelBase
[AutoNotify] private bool _isOpen;
private CancellationTokenSource? _suggestionCancellationTokenSource;
+ /// <remarks>Guards use of <see cref="_suggestionCancellationTokenSource"/>.</remarks>
+ private readonly object _lock = new();
+
+ /// <summary>Allow at most one suggestion generation run.</summary>
+ private readonly AsyncLock _asyncLock = new();
+
public PrivacySuggestionsFlyoutViewModel()
{
Suggestions = new ObservableCollection<SuggestionViewModel>();
@@ -36,14 +43,24 @@ public partial class PrivacySuggestionsFlyoutViewModel : ViewModelBase
public ObservableCollection<SuggestionViewModel> Suggestions { get; }
+ /// <remarks>Method supports being called multiple times. In that case the last call cancels the previous one.</remarks>
public async Task BuildPrivacySuggestionsAsync(Wallet wallet, TransactionInfo info, BitcoinAddress destination, BuildTransactionResult transaction, bool isFixedAmount, CancellationToken cancellationToken)
+ {
+ using CancellationTokenSource singleRunCts = new();
+
+ lock (_lock)
{
_suggestionCancellationTokenSource?.Cancel();
- _suggestionCancellationTokenSource?.Dispose();
+ _suggestionCancellationTokenSource = singleRunCts;
+ }
- _suggestionCancellationTokenSource = new(TimeSpan.FromSeconds(15));
- using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_suggestionCancellationTokenSource.Token, cancellationToken);
+ using CancellationTokenSource timeoutCts = new(TimeSpan.FromSeconds(15));
+ using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, singleRunCts.Token, cancellationToken);
+ using (await _asyncLock.LockAsync(CancellationToken.None))
+ {
+ try
+ {
Suggestions.Clear();
SelectedSuggestion = null;
@@ -77,4 +94,17 @@ public partial class PrivacySuggestionsFlyoutViewModel : ViewModelBase
Suggestions.Remove(loadingRing);
}
+ catch (OperationCanceledException)
+ {
+ Logger.LogTrace("Operation was cancelled.");
+ }
+ finally
+ {
+ lock (_lock)
+ {
+ _suggestionCancellationTokenSource = null;
+ }
+ }
+ }
+ }
}

MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/Send/TransactionPreviewViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/Send/TransactionPreviewViewModel.cs
@@ -482,7 +482,7 @@ public partial class TransactionPreviewViewModel : RoutableViewModel
private async Task<bool> AuthorizeAsync(TransactionAuthorizationInfo transactionAuthorizationInfo)
{
if (!_wallet.KeyManager.IsHardwareWallet &&
- string.IsNullOrEmpty(_wallet.Kitchen.SaltSoup())) // Do not show auth dialog when password is empty
+ string.IsNullOrEmpty(_wallet.Kitchen.SaltSoup())) // Do not show authentication dialog when password is empty
{
return true;
}

Allow only single run of `PrivacySuggestionsFlyoutViewModel.BuildPrivacySuggestionsAsync`