Preliminary View and ViewModel
MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/CoinJoinSettingsViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/CoinJoinSettingsViewModel.cs
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using NBitcoin;
+using ReactiveUI;
+using WalletWasabi.Blockchain.Keys;
+using WalletWasabi.Fluent.Validation;
+using WalletWasabi.Fluent.ViewModels.CoinJoinProfiles;
using WalletWasabi.Fluent.ViewModels.Navigation;
+using WalletWasabi.Models;
+using WalletWasabi.Wallets;
namespace WalletWasabi.Fluent.ViewModels.Wallets;
-public class CoinJoinSettingsViewModel : RoutableViewModel
+public partial class CoinJoinSettingsViewModel : RoutableViewModel
{
- public CoinJoinSettingsViewModel()
+ [AutoNotify] private bool _preferPsbtWorkflow;
+ [AutoNotify] private bool _autoCoinJoin;
+ [AutoNotify] private string _plebStopThreshold;
+ [AutoNotify] private string? _selectedCoinjoinProfileName;
+ [AutoNotify] private bool _isCoinjoinProfileSelected;
+
+ private Wallet _wallet;
+
+ public CoinJoinSettingsViewModel(WalletViewModelBase walletViewModelBase)
+ {
+ _wallet = walletViewModelBase.Wallet;
+ Title = $"{_wallet.WalletName} - Coinjoin Settings";
+ _preferPsbtWorkflow = _wallet.KeyManager.PreferPsbtWorkflow;
+ _autoCoinJoin = _wallet.KeyManager.AutoCoinJoin;
+ IsHardwareWallet = _wallet.KeyManager.IsHardwareWallet;
+ IsWatchOnly = _wallet.KeyManager.IsWatchOnly;
+ _plebStopThreshold = _wallet.KeyManager.PlebStopThreshold?.ToString() ?? KeyManager.DefaultPlebStopThreshold.ToString();
+
+ SetupCancel(enableCancel: false, enableCancelOnEscape: true, enableCancelOnPressed: true);
+
+ NextCommand = CancelCommand;
+
+ VerifyRecoveryWordsCommand = ReactiveCommand.Create(() => Navigate().To(new VerifyRecoveryWordsViewModel(_wallet)));
+
+ this.WhenAnyValue(x => x.PreferPsbtWorkflow)
+ .Skip(1)
+ .Subscribe(value =>
+ {
+ _wallet.KeyManager.PreferPsbtWorkflow = value;
+ _wallet.KeyManager.ToFile();
+ walletViewModelBase.RaisePropertyChanged(nameof(walletViewModelBase.PreferPsbtWorkflow));
+ });
+
+ SetAutoCoinJoin = ReactiveCommand.CreateFromTask(async () =>
+ {
+ if (_wallet.KeyManager.IsCoinjoinProfileSelected)
+ {
+ AutoCoinJoin = !AutoCoinJoin;
+ }
+ else
{
+ await NavigateDialogAsync(new CoinJoinProfilesViewModel(_wallet.KeyManager, false), NavigationTarget.DialogScreen);
}
- public string Text => "Greetings";
+ if (_wallet.KeyManager.IsCoinjoinProfileSelected)
+ {
+ _wallet.KeyManager.AutoCoinJoin = AutoCoinJoin;
+ _wallet.KeyManager.ToFile();
+ }
+ else
+ {
+ AutoCoinJoin = false;
+ }
+ });
+
+ SelectCoinjoinProfileCommand = ReactiveCommand.CreateFromTask(SelectCoinjoinProfileAsync);
+
+ this.ValidateProperty(x => x.PlebStopThreshold, ValidatePlebStopThreshold);
- public override string Title
+ this.WhenAnyValue(x => x.PlebStopThreshold)
+ .Skip(1)
+ .Throttle(TimeSpan.FromMilliseconds(1000))
+ .ObserveOn(RxApp.TaskpoolScheduler)
+ .Subscribe(x =>
{
- get => "Coinjoin Settings";
- protected set { }
+ if (Money.TryParse(x, out Money result) && result != _wallet.KeyManager.PlebStopThreshold)
+ {
+ _wallet.KeyManager.PlebStopThreshold = result;
+ _wallet.KeyManager.ToFile();
+ }
+ });
+ }
+
+ public bool IsHardwareWallet { get; }
+
+ public bool IsWatchOnly { get; }
+
+ public override sealed string Title { get; protected set; }
+
+ public ICommand SetAutoCoinJoin { get; }
+
+ public ICommand SelectCoinjoinProfileCommand { get; }
+
+ public ICommand VerifyRecoveryWordsCommand { get; }
+
+ protected override void OnNavigatedTo(bool isInHistory, CompositeDisposable disposables)
+ {
+ base.OnNavigatedTo(isInHistory, disposables);
+ PlebStopThreshold = _wallet.KeyManager.PlebStopThreshold.ToString();
+
+ IsCoinjoinProfileSelected = _wallet.KeyManager.IsCoinjoinProfileSelected;
+ SelectedCoinjoinProfileName =
+ (_wallet.KeyManager.IsCoinjoinProfileSelected, CoinJoinProfilesViewModel.IdentifySelectedProfile(_wallet.KeyManager)) switch
+ {
+ (true, CoinJoinProfileViewModelBase x) => x.Title,
+ (false, _) => "None",
+ _ => "Unknown"
+ };
+ }
+
+ private async Task SelectCoinjoinProfileAsync()
+ {
+ await NavigateDialogAsync(new CoinJoinProfilesViewModel(_wallet.KeyManager, false), NavigationTarget.DialogScreen);
+ AutoCoinJoin = _wallet.KeyManager.AutoCoinJoin;
+ }
+
+ private void ValidatePlebStopThreshold(IValidationErrors errors) =>
+ ValidatePlebStopThreshold(errors, PlebStopThreshold);
+
+ private static void ValidatePlebStopThreshold(IValidationErrors errors, string plebStopThreshold)
+ {
+ if (string.IsNullOrWhiteSpace(plebStopThreshold) || string.IsNullOrEmpty(plebStopThreshold))
+ {
+ return;
+ }
+
+ if (plebStopThreshold.Contains(',', StringComparison.InvariantCultureIgnoreCase))
+ {
+ errors.Add(ErrorSeverity.Error, "Use decimal point instead of comma.");
+ }
+ else if (!decimal.TryParse(plebStopThreshold, out _))
+ {
+ errors.Add(ErrorSeverity.Error, "Invalid coinjoin threshold.");
+ }
}
}

MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/WalletViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/WalletViewModel.cs
@@ -159,7 +159,7 @@ public partial class WalletViewModel : WalletViewModelBase
WalletCoinsCommand = ReactiveCommand.Create(() => Navigate(NavigationTarget.DialogScreen).To(new WalletCoinsViewModel(this, balanceChanged)));
- CoinJoinSettingsCommand = ReactiveCommand.Create(() => Navigate(NavigationTarget.DialogScreen).To(new CoinJoinSettingsViewModel()));
+ CoinJoinSettingsCommand = ReactiveCommand.Create(() => Navigate(NavigationTarget.DialogScreen).To(new CoinJoinSettingsViewModel(this)));
CoinJoinStateViewModel = new CoinJoinStateViewModel(this, balanceChanged);
}

MODIFY
WalletWasabi.Fluent/Views/Wallets/CoinJoinSettingsView.axaml
WalletWasabi.Fluent/Views/Wallets/CoinJoinSettingsView.axaml
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:wallets="clr-namespace:WalletWasabi.Fluent.ViewModels.Wallets"
x:DataType="wallets:CoinJoinSettingsViewModel"
- x:CompileBindings="True"
- x:Class="WalletWasabi.Fluent.Views.Wallets.CoinJoinSettingsView">
+ x:Class="WalletWasabi.Fluent.Views.Wallets.CoinJoinSettingsView"
+ x:CompileBindings="True">
<c:ContentArea Title="{Binding Title}"
EnableNext="True" NextContent="Done"
EnableCancel="{Binding EnableCancel}"
EnableBack="{Binding EnableBack}">
+ <StackPanel Spacing="20" Margin="0 30">
- <TextBlock>Greetings</TextBlock>
+ <StackPanel Classes="settingsLayout">
+ <DockPanel>
+ <TextBlock Text="Automatically start coinjoin" />
+ <ToggleSwitch IsChecked="{Binding AutoCoinJoin, Mode=OneWay}" Command="{Binding SetAutoCoinJoin}" />
+ </DockPanel>
+
+ <StackPanel Spacing="10"
+ ToolTip.Tip="Coinjoin will not automatically start if the non-private wallet balance is less than this."
+ IsVisible="{Binding !IsWatchOnly}">
+ <TextBlock Text="Auto-start coinjoin threshold" />
+ <c:CurrencyEntryBox Text="{Binding PlebStopThreshold}" CurrencyCode="BTC" />
+ </StackPanel>
+
+ <DockPanel>
+ <DockPanel.IsVisible>
+ <MultiBinding Converter="{x:Static BoolConverters.And}">
+ <Binding Path="IsCoinjoinProfileSelected" />
+ <Binding Path="IsWatchOnly" Converter="{x:Static BoolConverters.Not}" />
+ </MultiBinding>
+ </DockPanel.IsVisible>
+
+ <TextBlock Text="Coinjoin strategy:" VerticalAlignment="Center" DockPanel.Dock="Left" Margin="0" />
+ <Button Content="Change" Command="{Binding SelectCoinjoinProfileCommand}" DockPanel.Dock="Right" />
+ <TextBlock Text="{Binding SelectedCoinjoinProfileName}" VerticalAlignment="Center" MinWidth="120" Margin="10 0 10 0" />
+ </DockPanel>
+ </StackPanel>
+
+ </StackPanel>
</c:ContentArea>
</UserControl>

Preliminary View and ViewModel