refactoring first step
MODIFY
WalletWasabi.Fluent/ViewModels/AddWallet/HardwareWallet/ConnectHardwareWalletViewModel.cs
WalletWasabi.Fluent/ViewModels/AddWallet/HardwareWallet/ConnectHardwareWalletViewModel.cs
+using System;
+using System.Collections.Generic;
using System.Linq;
-using System.Reactive;
-using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Threading;
+using System.Threading.Tasks;
using NBitcoin;
using ReactiveUI;
using WalletWasabi.Fluent.ViewModels.Navigation;
+using WalletWasabi.Hwi.Exceptions;
+using WalletWasabi.Hwi.Models;
using WalletWasabi.Wallets;
namespace WalletWasabi.Fluent.ViewModels.AddWallet.HardwareWallet
{
public class ConnectHardwareWalletViewModel : RoutableViewModel
{
- private readonly bool _trySkipPage;
- private readonly HardwareDetectionState _detectionState;
+ private string _message;
- public ConnectHardwareWalletViewModel(string walletName, Network network, WalletManager walletManager, bool trySkipPage = true)
+ public ConnectHardwareWalletViewModel(string walletName, Network network, WalletManager walletManager)
{
- _detectionState = new HardwareDetectionState(walletName, walletManager, network);
+ WalletName = walletName;
+ HardwareWalletOperations = new HardwareWalletOperations(walletManager, network); // TODO: call dispose
+ HardwareWalletOperations.HardwareWalletsFound += OnHardwareWalletsFound;
- _trySkipPage = trySkipPage;
-
- if (trySkipPage)
- {
- IsBusy = true;
+ _message = "";
}
- NextCommand = ReactiveCommand.Create(
- () =>
+ public string WalletName { get; }
+
+ public string Message
{
- Navigate().To(new DetectHardwareWalletViewModel(_detectionState));
- });
+ get => _message;
+ set => this.RaiseAndSetIfChanged(ref _message, value);
}
- public ReactiveCommand<string, Unit> OpenBrowserCommand { get; }
+ public HardwareWalletOperations HardwareWalletOperations { get; }
- protected override void OnNavigatedTo(bool inStack, CompositeDisposable disposable)
+ private void OnHardwareWalletsFound(object? sender, HwiEnumerateEntry[] devices)
{
- base.OnNavigatedTo(inStack, disposable);
-
- if (!inStack && _trySkipPage)
- {
- RxApp.MainThreadScheduler.Schedule(
- async () =>
+ if (devices.Length > 1)
{
- await _detectionState.EnumerateHardwareWalletsAsync(CancellationToken.None);
+ Message = "Make sure you have only one hardware wallet connected to the PC.";
+ return;
+ }
- var deviceCount = _detectionState.Devices.Count();
+ var device = devices.First();
- if (deviceCount == 0)
+ if (!device.IsInitialized())
{
- // navigate to detecting page.
- Navigate().To(new DetectHardwareWalletViewModel(_detectionState));
+ Message = "Check your device and finish the initialization";
+ // TODO: execute init if possible
+ return;
}
- else if (deviceCount == 1)
+
+ if (device.Code is { } errorCode)
{
- // navigate to detected hw wallet page.
- _detectionState.SelectedDevice = _detectionState.Devices.First();
+ ShowErrorMessage(errorCode);
+ return;
+ }
- Navigate().To(new DetectedHardwareWalletViewModel(_detectionState));
+ HardwareWalletOperations.SelectedDevice = device;
+ Navigate().To(new DetectedHardwareWalletViewModel(HardwareWalletOperations, WalletName));
}
- else
+
+ private void ShowErrorMessage(HwiErrorCode errorCode)
{
- // Do nothing... stay on this page.
- }
+ // TODO: Show message that helps the user
- IsBusy = false;
- });
+ switch (errorCode)
+ {
+ case HwiErrorCode.NoDeviceType:
+ break;
+ case HwiErrorCode.MissingArguments:
+ break;
+ case HwiErrorCode.DeviceConnError:
+ break;
+ case HwiErrorCode.UnknownDeviceType:
+ break;
+ case HwiErrorCode.InvalidTx:
+ break;
+ case HwiErrorCode.NoPassword:
+ break;
+ case HwiErrorCode.BadArgument:
+ break;
+ case HwiErrorCode.NotImplemented:
+ break;
+ case HwiErrorCode.UnavailableAction:
+ break;
+ case HwiErrorCode.DeviceAlreadyInit:
+ break;
+ case HwiErrorCode.DeviceAlreadyUnlocked:
+ break;
+ case HwiErrorCode.DeviceNotReady:
+ break;
+ case HwiErrorCode.UnknownError:
+ break;
+ case HwiErrorCode.ActionCanceled:
+ break;
+ case HwiErrorCode.DeviceBusy:
+ break;
+ case HwiErrorCode.NeedToBeRoot:
+ break;
+ case HwiErrorCode.HelpText:
+ break;
+ case HwiErrorCode.DeviceNotInitialized:
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(errorCode), errorCode, null);
}
}
}

MODIFY
WalletWasabi.Fluent/ViewModels/AddWallet/HardwareWallet/DetectHardwareWalletViewModel.cs
WalletWasabi.Fluent/ViewModels/AddWallet/HardwareWallet/DetectHardwareWalletViewModel.cs
@@ -20,12 +20,12 @@ namespace WalletWasabi.Fluent.ViewModels.AddWallet.HardwareWallet
public class DetectHardwareWalletViewModel : RoutableViewModel
{
private HardwareWalletViewModel? _selectedHardwareWallet;
- private HardwareDetectionState _detectionState;
+ private HardwareWalletOperations _walletOperations;
- public DetectHardwareWalletViewModel(HardwareDetectionState detectionState)
+ public DetectHardwareWalletViewModel(HardwareWalletOperations walletOperations)
{
HardwareWallets = new ObservableCollection<HardwareWalletViewModel>();
- _detectionState = detectionState;
+ _walletOperations = walletOperations;
OpenBrowserCommand = ReactiveCommand.CreateFromTask<string>(IoHelpers.OpenBrowserAsync);
@@ -55,7 +55,7 @@ namespace WalletWasabi.Fluent.ViewModels.AddWallet.HardwareWallet
{
// TODO: Notify the user to check the device
- await _detectionState.Client.SetupAsync(x.HardwareWalletInfo.Model, x.HardwareWalletInfo.Path, interactiveMode, initCts.Token);
+ await _walletOperations.Client.SetupAsync(x.HardwareWalletInfo.Model, x.HardwareWalletInfo.Path, interactiveMode, initCts.Token);
// todo... check this doesnt trigger the other navigage to.
await ConnectSelectedHardwareWalletAsync();
@@ -111,9 +111,9 @@ namespace WalletWasabi.Fluent.ViewModels.AddWallet.HardwareWallet
IsBusy = true;
await StopDetectionAsync();
- _detectionState.SelectedDevice = SelectedHardwareWallet.HardwareWalletInfo;
+ _walletOperations.SelectedDevice = SelectedHardwareWallet.HardwareWalletInfo;
- Navigate().To(new DetectedHardwareWalletViewModel(_detectionState));
+ // Navigate().To(new DetectedHardwareWalletViewModel(_walletOperations));
}
catch (Exception ex)
{
@@ -158,13 +158,13 @@ namespace WalletWasabi.Fluent.ViewModels.AddWallet.HardwareWallet
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
var detectedHardwareWallets =
- (await _detectionState.Client.EnumerateAsync(timeoutCts.Token).ConfigureAwait(false))
+ (await _walletOperations.Client.EnumerateAsync(timeoutCts.Token).ConfigureAwait(false))
.Select(x => new HardwareWalletViewModel(x)).ToArray();
detectionCts.Token.ThrowIfCancellationRequested();
// The wallets that already exist in the software.
var alreadyExistingWalletsToRemove = detectedHardwareWallets.Where(
- wallet => _detectionState.WalletManager.GetWallets().Any(
+ wallet => _walletOperations.WalletManager.GetWallets().Any(
x => x.KeyManager.MasterFingerprint == wallet.HardwareWalletInfo.Fingerprint));
// The wallets that are not detectable since the last enumeration.

MODIFY
WalletWasabi.Fluent/ViewModels/AddWallet/HardwareWallet/DetectedHardwareWalletViewModel.cs
WalletWasabi.Fluent/ViewModels/AddWallet/HardwareWallet/DetectedHardwareWalletViewModel.cs
+using System;
using System.Threading.Tasks;
using System.Windows.Input;
using ReactiveUI;
using WalletWasabi.Extensions;
using WalletWasabi.Fluent.ViewModels.Navigation;
+using WalletWasabi.Hwi.Models;
+using WalletWasabi.Logging;
namespace WalletWasabi.Fluent.ViewModels.AddWallet.HardwareWallet
{
public class DetectedHardwareWalletViewModel : RoutableViewModel
{
- public DetectedHardwareWalletViewModel(HardwareDetectionState detectionState)
+
+ public DetectedHardwareWalletViewModel(HardwareWalletOperations walletOperations, string walletName)
{
+ WalletName = walletName;
var type = WalletType.Hardware;
- switch (detectionState.SelectedDevice!.Model)
+ switch (walletOperations.SelectedDevice!.Model)
{
- case Hwi.Models.HardwareWalletModels.Coldcard:
- case Hwi.Models.HardwareWalletModels.Coldcard_Simulator:
+ case HardwareWalletModels.Coldcard:
+ case HardwareWalletModels.Coldcard_Simulator:
type = WalletType.Coldcard;
break;
- case Hwi.Models.HardwareWalletModels.Ledger_Nano_S:
+ case HardwareWalletModels.Ledger_Nano_S:
type = WalletType.Ledger;
break;
- case Hwi.Models.HardwareWalletModels.Trezor_1:
- case Hwi.Models.HardwareWalletModels.Trezor_1_Simulator:
- case Hwi.Models.HardwareWalletModels.Trezor_T:
- case Hwi.Models.HardwareWalletModels.Trezor_T_Simulator:
+ case HardwareWalletModels.Trezor_1:
+ case HardwareWalletModels.Trezor_1_Simulator:
+ case HardwareWalletModels.Trezor_T:
+ case HardwareWalletModels.Trezor_T_Simulator:
type = WalletType.Trezor;
break;
}
Type = type;
- TypeName = detectionState.SelectedDevice.Model.FriendlyName();
+ TypeName = walletOperations.SelectedDevice.Model.FriendlyName();
NextCommand = ReactiveCommand.CreateFromTask(
async () =>
{
IsBusy = true;
- var newWallet = await Task.Run(
- async () =>
- {
- return await detectionState.GenerateWalletAsync();
- });
-
- detectionState.WalletManager.AddWallet(newWallet);
+ await Task.Run(async () => await walletOperations.GenerateWalletAsync(WalletName));
- Navigate().To(new AddedWalletPageViewModel(detectionState.WalletName, Type));
+ Navigate().To(new AddedWalletPageViewModel(WalletName, Type));
IsBusy = false;
});
@@ -55,10 +54,12 @@ namespace WalletWasabi.Fluent.ViewModels.AddWallet.HardwareWallet
NoCommand = ReactiveCommand.Create(
() =>
{
- Navigate().To(new ConnectHardwareWalletViewModel(detectionState.WalletName, detectionState.Network, detectionState.WalletManager, false));
+ Navigate().Back();
});
}
+ public string WalletName { get; }
+
public WalletType Type { get; }
public string TypeName { get; }

DELETE
WalletWasabi.Fluent/ViewModels/AddWallet/HardwareWallet/HardwareDetectionState.cs
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using NBitcoin;
-using WalletWasabi.Blockchain.Keys;
-using WalletWasabi.Hwi;
-using WalletWasabi.Hwi.Models;
-using WalletWasabi.Wallets;
-
-namespace WalletWasabi.Fluent.ViewModels.AddWallet.HardwareWallet
-{
- public class HardwareDetectionState
- {
- public HardwareDetectionState(string walletName, WalletManager walletManager, Network network)
- {
- WalletName = walletName;
- WalletManager = walletManager;
- Network = network;
- Client = new HwiClient(network);
-
- Devices = Enumerable.Empty<HwiEnumerateEntry>();
- }
-
- public IEnumerable<HwiEnumerateEntry> Devices { get; private set; }
-
- public HwiEnumerateEntry? SelectedDevice { get; set; }
-
- public WalletManager WalletManager { get; }
-
- public Network Network { get; }
-
- public HwiClient Client { get; }
-
- public string WalletName { get; }
-
- public async Task<KeyManager> GenerateWalletAsync()
- {
- var selectedDevice = SelectedDevice;
-
- if (selectedDevice?.Fingerprint is null)
- {
- throw new Exception("Cannot be null.");
- }
-
- var fingerPrint = (HDFingerprint) selectedDevice.Fingerprint;
- using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
- var extPubKey = await Client.GetXpubAsync(
- selectedDevice.Model,
- selectedDevice.Path,
- KeyManager.DefaultAccountKeyPath,
- cts.Token).ConfigureAwait(false);
- var path = WalletManager.WalletDirectories.GetWalletFilePaths(WalletName).walletFilePath;
-
- return KeyManager.CreateNewHardwareWalletWatchOnly(fingerPrint, extPubKey, path);
- }
-
- public async Task EnumerateHardwareWalletsAsync(CancellationToken token)
- {
- Devices = (await Client.EnumerateAsync(token).ConfigureAwait(false))
- .Where(
- wallet => WalletManager.GetWallets()
- .Any(x => x.KeyManager.MasterFingerprint == wallet.Fingerprint) == false);
- }
- }
-}

ADD
WalletWasabi.Fluent/ViewModels/AddWallet/HardwareWallet/HardwareWalletOperations.cs
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using NBitcoin;
+using ReactiveUI;
+using WalletWasabi.Blockchain.Keys;
+using WalletWasabi.Hwi;
+using WalletWasabi.Hwi.Models;
+using WalletWasabi.Logging;
+using WalletWasabi.Wallets;
+
+namespace WalletWasabi.Fluent.ViewModels.AddWallet.HardwareWallet
+{
+ public class HardwareWalletOperations : IDisposable
+ {
+ public event EventHandler<HwiEnumerateEntry[]>? HardwareWalletsFound;
+
+ public HardwareWalletOperations(WalletManager walletManager, Network network)
+ {
+ WalletManager = walletManager;
+ Network = network;
+ Client = new HwiClient(network);
+
+ DisposeCts = new CancellationTokenSource();
+
+ StartDetection();
+ }
+
+ public HwiEnumerateEntry? SelectedDevice { get; set; }
+
+ public WalletManager WalletManager { get; }
+
+ public Network Network { get; }
+
+ public HwiClient Client { get; }
+
+ private CancellationTokenSource? DetectionCts { get; set; }
+
+ private CancellationTokenSource DisposeCts { get; }
+
+ public Task? DetectionTask { get; set; }
+
+ private void OnHardwareWalletsFound(HwiEnumerateEntry[] wallet)
+ {
+ HardwareWalletsFound?.Invoke(this,wallet);
+ }
+
+ public async Task GenerateWalletAsync(string walletName)
+ {
+ var selectedDevice = SelectedDevice;
+
+ try
+ {
+ if (selectedDevice?.Fingerprint is null)
+ {
+ throw new Exception("Cannot be null.");
+ }
+
+ await StopDetectionAsync();
+
+ var fingerPrint = (HDFingerprint) selectedDevice.Fingerprint;
+ using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
+ var extPubKey = await Client.GetXpubAsync(
+ selectedDevice.Model,
+ selectedDevice.Path,
+ KeyManager.DefaultAccountKeyPath,
+ cts.Token).ConfigureAwait(false);
+ var path = WalletManager.WalletDirectories.GetWalletFilePaths(walletName).walletFilePath;
+
+ var km = KeyManager.CreateNewHardwareWalletWatchOnly(fingerPrint, extPubKey, path);
+ WalletManager.AddWallet(km);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex);
+ StartDetection();
+ }
+ }
+
+ private async Task StopDetectionAsync()
+ {
+ if (DetectionTask is { } task && DetectionCts is { } cts)
+ {
+ cts.Cancel();
+ await task;
+ }
+ }
+
+ private void StartDetection()
+ {
+ if (DisposeCts is { } cts)
+ {
+ DetectionCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
+ DetectionTask = HardwareWalletDetectionAsync(DetectionCts);
+ }
+ }
+
+ protected async Task HardwareWalletDetectionAsync(CancellationTokenSource detectionCts)
+ {
+ while (!detectionCts.IsCancellationRequested)
+ {
+ var sw = Stopwatch.StartNew();
+
+ try
+ {
+ using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
+
+ var detectedHardwareWallets =
+ (await Client.EnumerateAsync(timeoutCts.Token)
+ .ConfigureAwait(false))
+ .Where(wallet => WalletManager.GetWallets()
+ .Any(x => x.KeyManager.MasterFingerprint == wallet.Fingerprint) == false)
+ .ToArray();
+
+ detectionCts.Token.ThrowIfCancellationRequested();
+
+ if (detectedHardwareWallets.Length > 0)
+ {
+ OnHardwareWalletsFound(detectedHardwareWallets);
+ }
+ }
+ catch (Exception ex)
+ {
+ if (ex is not OperationCanceledException)
+ {
+ Logger.LogError(ex);
+ }
+ }
+
+ // Too fast enumeration causes the detected hardware wallets to be unable to provide the fingerprint.
+ // Wait at least 5 seconds between two enumerations.
+ sw.Stop();
+ if (sw.Elapsed.Milliseconds < 5000)
+ {
+ await Task.Delay(5000 - sw.Elapsed.Milliseconds).ConfigureAwait(false);
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ Task.Run(async () =>
+ {
+ await StopDetectionAsync();
+
+ DisposeCts.Dispose();
+ DetectionCts?.Dispose();
+ DetectionTask?.Dispose();
+ });
+ }
+ }
+}

MODIFY
WalletWasabi.Fluent/Views/AddWallet/HardwareWallet/ConnectHardwareWalletView.axaml
WalletWasabi.Fluent/Views/AddWallet/HardwareWallet/ConnectHardwareWalletView.axaml
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:c="clr-namespace:WalletWasabi.Fluent.Controls"
- xmlns:hardwareWallets="clr-namespace:WalletWasabi.Gui.Tabs.WalletManager.HardwareWallets;assembly=WalletWasabi.Gui"
+ xmlns:hardwareWallet="clr-namespace:WalletWasabi.Fluent.ViewModels.AddWallet.HardwareWallet"
mc:Ignorable="d"
d:DesignWidth="600" d:DesignHeight="500"
- x:DataType="hardwareWallets:ConnectHardwareWalletViewModel"
+ x:DataType="hardwareWallet:ConnectHardwareWalletViewModel"
x:CompileBindings="True"
x:Class="WalletWasabi.Fluent.Views.AddWallet.HardwareWallet.ConnectHardwareWalletView">
<c:ContentArea Title="Hardware Wallet"
- Caption="Ensure your device is not connected. Press continue when ready."
- IsBusy="{Binding IsBusy}"
+ Caption="Connect your hardware wallet to the PC"
EnableCancel="True" CancelContent="Cancel"
- EnableBack="True"
- EnableNext="True" NextContent="Continue" >
+ EnableBack="True">
<DockPanel>
- <Button DockPanel.Dock="Bottom" HorizontalAlignment="Center" Classes="h8 plain activeHyperLink" Foreground="{DynamicResource SystemAccentColor}" Content="More information and help with Hardware Wallets" />
+
+ <c:InfoMessage DockPanel.Dock="Bottom" Classes="h8" IsVisible="False">
+ <StackPanel>
+ <TextBlock Text="Nothing showing up?"/>
+ <StackPanel Orientation="Horizontal">
+ <Button Content="Click here" Classes="h8 plain activeHyperLink" Foreground="{DynamicResource SystemAccentColor}" />
+ <TextBlock Text=" for more help."/>
+ </StackPanel>
+ </StackPanel>
+ </c:InfoMessage>
+
+ <DockPanel>
+
+ <StackPanel VerticalAlignment="Top" DockPanel.Dock="Top" Margin="0 20 0 0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="40">
<Image Height="100" Source="/Assets/HardwareIcons/ledger.png" />
<Image Height="100" Source="/Assets/HardwareIcons/coldcard.png" />
<Image Height="100" Source="/Assets/HardwareIcons/trezor.png" />
<Image Height="100" Source="/Assets/HardwareIcons/generic.png" />
</StackPanel>
+ <TextBlock Text="{Binding Message}" HorizontalAlignment="Center" Margin="0 100 0 0"/>
+ </StackPanel>
+
+ <c:ProgressRing IsIndeterminate="True" VerticalAlignment="Center" Height="100" Width="100" />
+
</DockPanel>
+ </DockPanel>
+
</c:ContentArea>
</UserControl>

MODIFY
WalletWasabi.Fluent/Views/AddWallet/HardwareWallet/DetectHardwareWalletView.axaml
WalletWasabi.Fluent/Views/AddWallet/HardwareWallet/DetectHardwareWalletView.axaml
<DockPanel>
<Button DockPanel.Dock="Bottom" HorizontalAlignment="Center" Classes="h8 plain activeHyperLink" Foreground="{DynamicResource SystemAccentColor}" Content="More information and help with Hardware Wallets" />
- <DockPanel IsVisible="{Binding !WalletListVisible}">
+ <DockPanel>
<StackPanel VerticalAlignment="Top" Spacing="40" DockPanel.Dock="Top" Margin="0 20 0 0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="40">
<Image Height="100" Source="/Assets/HardwareIcons/ledger.png" />

refactoring first step