Implement advanced recovery options dialog
ADD
WalletWasabi.Fluent/ViewModels/Dialogs/AdvancedRecoveryOptionsViewModel.cs
+using ReactiveUI;
+using System.Reactive.Linq;
+using System.Windows.Input;
+using NBitcoin;
+using WalletWasabi.Blockchain.Keys;
+using WalletWasabi.Gui.Validation;
+using WalletWasabi.Models;
+using WalletWasabi.Userfacing;
+
+namespace WalletWasabi.Fluent.ViewModels.Dialogs
+{
+ public class AdvancedRecoveryOptionsViewModel : DialogViewModelBase<(string? keypath, string? gaplimit)>
+ {
+ private string? _accountKeyPath;
+ private string? _minGapLimit;
+
+ public AdvancedRecoveryOptionsViewModel()
+ {
+ this.ValidateProperty(x => x.AccountKeyPath, ValidateAccountKeyPath);
+ this.ValidateProperty(x => x.MinGapLimit, ValidateMinGapLimit);
+
+ var continueCommandCanExecute = this.WhenAnyValue(
+ x => x.AccountKeyPath,
+ x => x.MinGapLimit,
+ (keyPath, minGapLimit) =>
+ {
+ // This will fire validations before return canExecute value.
+ this.RaisePropertyChanged(nameof(AccountKeyPath));
+ this.RaisePropertyChanged(nameof(MinGapLimit));
+
+ return string.IsNullOrEmpty(keyPath) && string.IsNullOrEmpty(minGapLimit) ||
+ !string.IsNullOrEmpty(keyPath) && !string.IsNullOrEmpty(minGapLimit) && !Validations.Any;
+ })
+ .ObserveOn(RxApp.MainThreadScheduler);
+
+ ContinueCommand = ReactiveCommand.Create(() => Close((AccountKeyPath, MinGapLimit)), continueCommandCanExecute);
+ CancelCommand = ReactiveCommand.Create(() => Close());
+ }
+
+ protected override void OnDialogClosed()
+ {
+ AccountKeyPath = "";
+ MinGapLimit = "";
+ }
+
+ private void ValidateMinGapLimit(IValidationErrors errors)
+ {
+ if (string.IsNullOrWhiteSpace(MinGapLimit))
+ {
+ return;
+ }
+
+ if (!int.TryParse(MinGapLimit, out var minGapLimit) || minGapLimit < KeyManager.AbsoluteMinGapLimit ||
+ minGapLimit > KeyManager.MaxGapLimit)
+ {
+ errors.Add(ErrorSeverity.Error,
+ $"Must be a number between {KeyManager.AbsoluteMinGapLimit} and {KeyManager.MaxGapLimit}.");
+ }
+ }
+
+ private void ValidateAccountKeyPath(IValidationErrors errors)
+ {
+ if (string.IsNullOrWhiteSpace(AccountKeyPath))
+ {
+ return;
+ }
+
+ if (KeyPath.TryParse(AccountKeyPath, out var keyPath) && keyPath is { })
+ {
+ var accountKeyPath = keyPath.GetAccountKeyPath();
+ if (keyPath.Length != accountKeyPath.Length ||
+ accountKeyPath.Length != KeyManager.DefaultAccountKeyPath.Length)
+ {
+ errors.Add(ErrorSeverity.Error, "Path is not a compatible account derivation path.");
+ }
+ }
+ else
+ {
+ errors.Add(ErrorSeverity.Error, "Path is not a valid derivation path.");
+ }
+ }
+
+ public string? AccountKeyPath
+ {
+ get => _accountKeyPath;
+ set => this.RaiseAndSetIfChanged(ref _accountKeyPath, value);
+ }
+
+ public string? MinGapLimit
+ {
+ get => _minGapLimit;
+ set => this.RaiseAndSetIfChanged(ref _minGapLimit, value);
+ }
+
+ public ICommand ContinueCommand { get; }
+
+ public ICommand CancelCommand { get; }
+ }
+}
\ No newline at end of file

MODIFY
WalletWasabi.Fluent/ViewModels/RecoverWalletPageViewModel.cs
WalletWasabi.Fluent/ViewModels/RecoverWalletPageViewModel.cs
@@ -2,16 +2,20 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reactive.Linq;
+using System.Reactive.Threading.Tasks;
+using System.Windows.Input;
using DynamicData;
using DynamicData.Binding;
using NBitcoin;
using ReactiveUI;
+using WalletWasabi.Fluent.ViewModels.Dialogs;
using WalletWasabi.Gui.Validation;
+using WalletWasabi.Gui.ViewModels;
using WalletWasabi.Models;
namespace WalletWasabi.Fluent.ViewModels
{
- public class RecoveryPageViewModel : NavBarItemViewModel
+ public class RecoveryPageViewModel : RoutableViewModel
{
private readonly ObservableAsPropertyHelper<Mnemonic?> _currentMnemonic;
private IEnumerable<string>? _suggestions;
@@ -33,8 +37,25 @@ namespace WalletWasabi.Fluent.ViewModels
.Subscribe(x => this.RaisePropertyChanged(nameof(Mnemonics)));
this.ValidateProperty(x => x.Mnemonics, ValidateMnemonics);
+
+
+ AdvancedOptionsInteraction = new Interaction<object, (string?, string?)>();
+ AdvancedOptionsInteraction.RegisterHandler(
+ async interaction => interaction.SetOutput(await new AdvancedRecoveryOptionsViewModel().ShowDialogAsync()));
+
+
+
+ TestDialogCommand = ReactiveCommand.CreateFromTask(
+ async () =>
+ {
+ var result = await AdvancedOptionsInteraction.Handle("").ToTask();
+
+ });
}
+ public ICommand TestDialogCommand { get; }
+ private Interaction<object, (string?, string?)> AdvancedOptionsInteraction { get; }
+
public ObservableCollection<string> Mnemonics { get; } = new ObservableCollection<string>();
public IEnumerable<string>? Suggestions
@@ -74,6 +95,5 @@ namespace WalletWasabi.Fluent.ViewModels
private Mnemonic? CurrentMnemonics => _currentMnemonic.Value;
- public override string IconName => "settings_regular";
}
}
\ No newline at end of file

MODIFY
WalletWasabi.Fluent/ViewModels/RoutableViewModel.cs
WalletWasabi.Fluent/ViewModels/RoutableViewModel.cs
@@ -11,6 +11,7 @@ namespace WalletWasabi.Fluent.ViewModels
HostScreen = screen;
}
+
public string UrlPathSegment { get; } = Guid.NewGuid().ToString().Substring(0, 5);
public IScreen HostScreen { get; }

ADD
WalletWasabi.Fluent/Views/Dialogs/AdvancedRecoveryOptionsView.axaml.cs
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using WalletWasabi.Fluent.ViewModels.Dialogs;
+
+namespace WalletWasabi.Fluent.Views.Dialogs
+{
+ public class AdvancedRecoveryOptionsView : ReactiveUserControl<AdvancedRecoveryOptionsViewModel>
+ {
+ public AdvancedRecoveryOptionsView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
\ No newline at end of file

MODIFY
WalletWasabi.Fluent/Views/RecoveryPageView.axaml
WalletWasabi.Fluent/Views/RecoveryPageView.axaml
xmlns:behaviors="clr-namespace:WalletWasabi.Fluent.Behaviors"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="vm:RecoveryPageViewModel"
+ x:CompileBindings="True"
x:Class="WalletWasabi.Fluent.Views.RecoveryPageView">
<Panel Margin="25 0 25 20">
- <Button Classes="invisible" Margin="0 35 0 0" Command="{Binding GoBackCommand}" DockPanel.Dock="Left"
- HorizontalAlignment="Left" VerticalAlignment="Top">
- <PathIcon Data="{DynamicResource arrow_left_regular}" />
- </Button>
+ <!-- <Button Classes="invisible" Margin="0 35 0 0" Command="{Binding GoBackCommand}" DockPanel.Dock="Left" -->
+ <!-- HorizontalAlignment="Left" VerticalAlignment="Top"> -->
+ <!-- <PathIcon Data="{DynamicResource arrow_left_regular}" /> -->
+ <!-- </Button> -->
<c:ContentArea Title="Enter recovery words" Caption="Type in your 12 recovery words and click continue"
Margin="44 0">
<StackPanel>
</c:TagsBox>
</StackPanel>
</c:ContentArea>
- <Button Content="Cancel" Command="{Binding CancelCommand}" Classes="invisible" HorizontalAlignment="Left"
- VerticalAlignment="Bottom" />
- <Button Content="Continue" Command="{Binding FinishCommand}" Classes="action" HorizontalAlignment="Right"
+ <Button Content="Cancel" Command="{Binding TestDialogCommand}" Classes="action" HorizontalAlignment="Left"
VerticalAlignment="Bottom" />
+ <!-- <Button Content="Cancel" Command="{Binding CancelCommand}" Classes="invisible" HorizontalAlignment="Left" -->
+ <!-- VerticalAlignment="Bottom" /> -->
+ <!-- <Button Content="Continue" Command="{Binding FinishCommand}" Classes="action" HorizontalAlignment="Right" -->
+ <!-- VerticalAlignment="Bottom" /> -->
</Panel>
</UserControl>
\ No newline at end of file

MODIFY
WalletWasabi.Fluent/WalletWasabi.Fluent.csproj
WalletWasabi.Fluent/WalletWasabi.Fluent.csproj
<SubType>Designer</SubType>
</AvaloniaResource>
<AvaloniaResource Include="Assets\**" />
+ <Compile Update="Views\Dialogs\AdvancedRecoveryOptionsView.axaml.cs">
+ <DependentUpon>AdvancedRecoveryOptionsView.axaml</DependentUpon>
+ <SubType>Code</SubType>
+ </Compile>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.999-cibuild0011763-beta" />

MODIFY
WalletWasabi.Gui/Tabs/WalletManager/RecoverWallets/RecoverWalletViewModel.cs
WalletWasabi.Gui/Tabs/WalletManager/RecoverWallets/RecoverWalletViewModel.cs
@@ -210,6 +210,11 @@ namespace WalletWasabi.Gui.Tabs.WalletManager.RecoverWallets
private void ValidateMinGapLimit(IValidationErrors errors)
{
+ if (string.IsNullOrWhiteSpace(MinGapLimit))
+ {
+ return;
+ }
+
if (!int.TryParse(MinGapLimit, out int minGapLimit) || minGapLimit < KeyManager.AbsoluteMinGapLimit || minGapLimit > KeyManager.MaxGapLimit)
{
errors.Add(ErrorSeverity.Error, $"Must be a number between {KeyManager.AbsoluteMinGapLimit} and {KeyManager.MaxGapLimit}.");
@@ -220,9 +225,10 @@ namespace WalletWasabi.Gui.Tabs.WalletManager.RecoverWallets
{
if (string.IsNullOrWhiteSpace(AccountKeyPath))
{
- errors.Add(ErrorSeverity.Error, "Path is not valid.");
+ return;
}
- else if (KeyPath.TryParse(AccountKeyPath, out var keyPath))
+
+ if (KeyPath.TryParse(AccountKeyPath, out var keyPath))
{
var accountKeyPath = keyPath.GetAccountKeyPath();
if (keyPath.Length != accountKeyPath.Length || accountKeyPath.Length != KeyManager.DefaultAccountKeyPath.Length)

Implement advanced recovery options dialog