initial implementation of verify seed words.
ADD
WalletWasabi.Fluent/ViewModels/Wallets/VerifyRecoveryPhraseViewModel.cs
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using DynamicData;
+using DynamicData.Binding;
+using NBitcoin;
+using ReactiveUI;
+using WalletWasabi.Fluent.Validation;
+using WalletWasabi.Fluent.ViewModels.Dialogs.Authorization;
+using WalletWasabi.Fluent.ViewModels.Navigation;
+using WalletWasabi.Logging;
+using WalletWasabi.Models;
+using WalletWasabi.Wallets;
+
+namespace WalletWasabi.Fluent.ViewModels.Wallets;
+
+[NavigationMetaData(Title = "Recovery Words Verification")]
+public partial class VerifyRecoveryPhraseViewModel : RoutableViewModel
+{
+ [AutoNotify] private IEnumerable<string>? _suggestions;
+ [AutoNotify] private Mnemonic? _currentMnemonics;
+ private readonly Wallet _wallet;
+
+ public VerifyRecoveryPhraseViewModel(Wallet wallet)
+ {
+ _suggestions = new Mnemonic(Wordlist.English, WordCount.Twelve).WordList.GetWords();
+
+ _wallet = wallet;
+
+ Mnemonics.ToObservableChangeSet().ToCollection()
+ .Select(x =>
+ x.Count is 12 or 15 or 18 or 21 or 24
+ ? new Mnemonic(GetTagsAsConcatString().ToLowerInvariant())
+ : default)
+ .Subscribe(x =>
+ {
+ CurrentMnemonics = x;
+ this.RaisePropertyChanged(nameof(Mnemonics));
+ });
+
+ this.ValidateProperty(x => x.Mnemonics, ValidateMnemonics);
+
+ EnableBack = true;
+
+ NextCommandCanExecute =
+ this.WhenAnyValue(x => x.CurrentMnemonics)
+ .Select(_ => IsMnemonicsValid);
+
+ NextCommand = ReactiveCommand.CreateFromTask(
+ async () => await OnNextAsync(),
+ NextCommandCanExecute);
+ }
+
+ private async Task OnNextAsync()
+ {
+ var dialogResult = await NavigateDialogAsync(
+ new PasswordAuthDialogViewModel(_wallet)
+ , NavigationTarget.CompactDialogScreen);
+
+ if (dialogResult.Result)
+ {
+ IsBusy = true;
+
+ try
+ {
+ if (_currentMnemonics is { })
+ {
+ var saltSoup = _wallet.Kitchen.SaltSoup();
+
+ var extKey = _currentMnemonics.DeriveExtKey(saltSoup);
+ var masterFingerprint = extKey.Neuter().PubKey.GetHDFingerPrint();
+
+ if (_wallet.KeyManager.MasterFingerprint == masterFingerprint)
+ {
+ Navigate().To(new SuccessViewModel("Your Seed Recovery words have been verified as correct."),
+ NavigationMode.Clear);
+ }
+ else
+ {
+ await ShowErrorAsync("Error", "Your recovery phrase was incorrect.",
+ "You may try again, but if you are unable to verify your recovery phrase correctly. Your MUST move your funds to a new wallet as soon as possible.");
+
+ Mnemonics.Clear();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // TODO navigate to error dialog.
+ Logger.LogError(ex);
+ }
+
+ IsBusy = false;
+ }
+ }
+
+ public bool IsMnemonicsValid => CurrentMnemonics is { IsValidChecksum: true };
+
+ public IObservable<bool> NextCommandCanExecute { get; }
+
+ public ObservableCollection<string> Mnemonics { get; } = new();
+
+ private void ValidateMnemonics(IValidationErrors errors)
+ {
+ if (IsMnemonicsValid)
+ {
+ return;
+ }
+
+ if (!Mnemonics.Any())
+ {
+ return;
+ }
+
+ errors.Add(ErrorSeverity.Error, "Recovery words are not valid.");
+ }
+
+ private string GetTagsAsConcatString()
+ {
+ return string.Join(' ', Mnemonics);
+ }
+
+ protected override void OnNavigatedTo(bool isInHistory, CompositeDisposable disposables)
+ {
+ base.OnNavigatedTo(isInHistory, disposables);
+
+ SetupCancel(enableCancel: true, enableCancelOnEscape: true, enableCancelOnPressed: true);
+ }
+}
\ No newline at end of file

MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/WalletSettingsViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/WalletSettingsViewModel.cs
-using System.Reactive.Linq;
+using System.Reactive.Linq;using System.Windows.Input;
using ReactiveUI;
using WalletWasabi.Fluent.ViewModels.Navigation;
@@ -22,6 +22,11 @@ public partial class WalletSettingsViewModel : RoutableViewModel
NextCommand = CancelCommand;
+ VerifyRecoveryPhraseCommand = ReactiveCommand.Create(() =>
+ {
+ Navigate().To(new VerifyRecoveryPhraseViewModel(wallet));
+ });
+
this.WhenAnyValue(x => x.PreferPsbtWorkflow)
.Skip(1)
.Subscribe(value =>
@@ -46,4 +51,6 @@ public partial class WalletSettingsViewModel : RoutableViewModel
public bool IsWatchOnly { get; }
public sealed override string Title { get; protected set; }
+
+ public ICommand VerifyRecoveryPhraseCommand { get; }
}

ADD
WalletWasabi.Fluent/Views/Wallets/VerifyRecoveryPhraseView.axaml
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:controls="clr-namespace:WalletWasabi.Fluent.Controls"
+ xmlns:i="using:Avalonia.Xaml.Interactivity"
+ xmlns:behaviors="clr-namespace:WalletWasabi.Fluent.Behaviors"
+ xmlns:vm="clr-namespace:WalletWasabi.Fluent.ViewModels.Wallets"
+ x:CompileBindings="True"
+ x:DataType="vm:VerifyRecoveryPhraseViewModel"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ x:Class="WalletWasabi.Fluent.Views.Wallets.VerifyRecoveryPhraseView">
+ <controls:ContentArea Title="{Binding Title}"
+ Caption="Please enter your wallet recovery words in the correct order"
+ NextContent="Verify" EnableNext="True" EnableCancel="True">
+ <DockPanel VerticalAlignment="Top">
+ <PathIcon Name="IconCheckmark"
+ Margin="15 0 0 0"
+ Classes="checkMark"
+ VerticalAlignment="Center"
+ DockPanel.Dock="Right"
+ Opacity="{Binding NextCommandCanExecute^}" />
+ <controls:TagsBox x:Name="SeedTagsBox"
+ DockPanel.Dock="Left"
+ ItemCountLimit="24"
+ SuggestionsAreCaseSensitive="False"
+ RestrictInputToSuggestions="True"
+ Items="{Binding Mnemonics}"
+ Suggestions="{Binding Suggestions}"
+ AllowDuplication="True"
+ EnableCounter="True"
+ EnableDelete="False">
+ <i:Interaction.Behaviors>
+ <behaviors:FocusOnAttachedBehavior />
+ </i:Interaction.Behaviors>
+ </controls:TagsBox>
+ </DockPanel>
+ </controls:ContentArea>
+</UserControl>
\ No newline at end of file

ADD
WalletWasabi.Fluent/Views/Wallets/VerifyRecoveryPhraseView.axaml.cs
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace WalletWasabi.Fluent.Views.Wallets;
+
+public class VerifyRecoveryPhraseView : UserControl
+{
+ public VerifyRecoveryPhraseView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
\ No newline at end of file

MODIFY
WalletWasabi.Fluent/Views/Wallets/WalletSettingsView.axaml
WalletWasabi.Fluent/Views/Wallets/WalletSettingsView.axaml
EnableNext="True" NextContent="Done"
EnableCancel="{Binding EnableCancel}"
EnableBack="{Binding EnableBack}">
+ <StackPanel Spacing="20">
<StackPanel Classes="settingsLayout">
<DockPanel IsVisible="{Binding IsHardwareWallet}">
<TextBlock Text="PSBT workflow" />
<ToggleSwitch IsChecked="{Binding AutoCoinJoin}" />
</DockPanel>
</StackPanel>
+ <StackPanel Spacing="20">
+ <TextBlock Classes="h6" Text="Have you checked you wallet recovery seed words?" />
+ <c:InfoMessage>
+ <DockPanel>
+ <Button Margin="50 0" Command="{Binding VerifyRecoveryPhraseCommand}" Content="Verify Recovery Seed Words" DockPanel.Dock="Right" />
+ <TextBlock Text="It is recommended that you verify your wallet recovery seed words every 6 months." />
+ </DockPanel>
+ </c:InfoMessage>
+ </StackPanel>
+ </StackPanel>
</c:ContentArea>
</UserControl>
\ No newline at end of file

initial implementation of verify seed words.