Add ImportColdCard and Enumerate HW to the ConnectHwWalletVM
MODIFY
WalletWasabi.Gui/Tabs/WalletManager/HardwareWallets/ConnectHardwareWalletViewModel.cs
WalletWasabi.Gui/Tabs/WalletManager/HardwareWallets/ConnectHardwareWalletViewModel.cs
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
using AvalonStudio.Extensibility;
+using NBitcoin;
+using Newtonsoft.Json.Linq;
using ReactiveUI;
using Splat;
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Reactive;
+using System.Reactive.Linq;
+using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -19,10 +27,92 @@ using WalletWasabi.Logging;
namespace WalletWasabi.Gui.Tabs.WalletManager.HardwareWallets
{
- internal class ConnectHardwareWalletViewModel : LoadWalletViewModel
+ public class ConnectHardwareWalletViewModel : LoadWalletViewModel
{
- internal ConnectHardwareWalletViewModel(WalletManagerViewModel owner) : base(owner, LoadWalletType.Hardware)
+ public ConnectHardwareWalletViewModel(WalletManagerViewModel owner) : base(owner, LoadWalletType.Hardware)
{
+ Global = Locator.Current.GetService<Global>();
+ Owner = owner;
+
+ ImportColdcardCommand = ReactiveCommand.CreateFromTask(async () =>
+ {
+ var ofd = new OpenFileDialog
+ {
+ AllowMultiple = false,
+ Title = "Import Coldcard"
+ };
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ ofd.Directory = Path.Combine("/media", Environment.UserName);
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ ofd.Directory = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
+ }
+
+ var window = (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).MainWindow;
+ var selected = await ofd.ShowAsync(window, fallBack: true);
+ if (selected != null && selected.Any())
+ {
+ var path = selected.First();
+ var jsonString = await File.ReadAllTextAsync(path);
+ var json = JObject.Parse(jsonString);
+ var xpubString = json["ExtPubKey"].ToString();
+ var mfpString = json["MasterFingerprint"].ToString();
+
+ // https://github.com/zkSNACKs/WalletWasabi/pull/1663#issuecomment-508073066
+ // Coldcard 2.1.0 improperly implemented Wasabi skeleton fingerprint at first, so we must reverse byte order.
+ // The solution was to add a ColdCardFirmwareVersion json field from 2.1.1 and correct the one generated by 2.1.0.
+ var coldCardVersionString = json["ColdCardFirmwareVersion"]?.ToString();
+ var reverseByteOrder = false;
+ if (coldCardVersionString is null)
+ {
+ reverseByteOrder = true;
+ }
+ else
+ {
+ Version coldCardVersion = new Version(coldCardVersionString);
+
+ if (coldCardVersion == new Version("2.1.0")) // Should never happen though.
+ {
+ reverseByteOrder = true;
+ }
}
+
+ var bytes = ByteHelpers.FromHex(Guard.NotNullOrEmptyOrWhitespace(nameof(mfpString), mfpString, trim: true));
+ HDFingerprint mfp = reverseByteOrder ? new HDFingerprint(bytes.Reverse().ToArray()) : new HDFingerprint(bytes);
+
+ ExtPubKey extPubKey = NBitcoinHelpers.BetterParseExtPubKey(xpubString);
+
+ Logger.LogInfo("Creating a new wallet file.");
+ var walletName = Global.WalletDirectories.GetNextWalletName("Coldcard");
+ var walletFullPath = Global.WalletDirectories.GetWalletFilePaths(walletName).walletFilePath;
+ KeyManager.CreateNewHardwareWalletWatchOnly(mfp, extPubKey, walletFullPath);
+ owner.SelectLoadWallet();
+ }
+ });
+
+ EnumerateHardwareWalletsCommand = ReactiveCommand.CreateFromTask(async () => await EnumerateIfHardwareWalletsAsync());
+
+ Observable
+ .Merge(OpenBrowserCommand.ThrownExceptions)
+ .Merge(LoadCommand.ThrownExceptions)
+ .Merge(TestPasswordCommand.ThrownExceptions)
+ .Merge(OpenFolderCommand.ThrownExceptions)
+ .Merge(ImportColdcardCommand.ThrownExceptions)
+ .Merge(EnumerateHardwareWalletsCommand.ThrownExceptions)
+ .ObserveOn(RxApp.TaskpoolScheduler)
+ .Subscribe(ex =>
+ {
+ Logger.LogError(ex);
+ NotificationHelpers.Error(ex.ToUserFriendlyString());
+ });
+ }
+
+ public ReactiveCommand<Unit, Unit> ImportColdcardCommand { get; set; }
+ public ReactiveCommand<Unit, Unit> EnumerateHardwareWalletsCommand { get; set; }
+ private Global Global { get; }
+ private WalletManagerViewModel Owner { get; }
}
}

MODIFY
WalletWasabi.Gui/Tabs/WalletManager/LoadWallets/LoadWalletView.xaml
WalletWasabi.Gui/Tabs/WalletManager/LoadWallets/LoadWalletView.xaml
<TextBlock Text="{Binding LoadButtonText}" />
</StackPanel>
</Button>
- <Button IsVisible="{Binding IsHardwareWallet}" Content="Search Hardware Wallets" Command="{Binding EnumerateHardwareWalletsCommand}" DockPanel.Dock="Right" Margin="0 0 10 0" />
- <Button IsVisible="{Binding IsHardwareWallet}" Content="Import Coldcard" Command="{Binding ImportColdcardCommand}" DockPanel.Dock="Right" Margin="0 0 10 0" />
<Button IsVisible="{Binding IsDesktopWallet}" Command="{Binding OpenFolderCommand}" DockPanel.Dock="Right" Margin="0 0 10 0">
<StackPanel Orientation="Horizontal" Spacing="8">
<DrawingPresenter Width="28" Drawing="{DynamicResource LoadWallet_Folder}" />

MODIFY
WalletWasabi.Gui/Tabs/WalletManager/LoadWallets/LoadWalletViewModel.cs
WalletWasabi.Gui/Tabs/WalletManager/LoadWallets/LoadWalletViewModel.cs
@@ -37,7 +37,7 @@ using WalletWasabi.Models;
namespace WalletWasabi.Gui.Tabs.WalletManager.LoadWallets
{
- internal class LoadWalletViewModel : CategoryViewModel
+ public class LoadWalletViewModel : CategoryViewModel
{
private ObservableCollection<LoadWalletEntry> _wallets;
private string _password;
@@ -74,67 +74,6 @@ namespace WalletWasabi.Gui.Tabs.WalletManager.LoadWallets
LoadCommand = ReactiveCommand.CreateFromTask(LoadWalletAsync, this.WhenAnyValue(x => x.CanLoadWallet));
TestPasswordCommand = ReactiveCommand.CreateFromTask(LoadKeyManagerAsync, this.WhenAnyValue(x => x.CanTestPassword));
OpenFolderCommand = ReactiveCommand.Create(OpenWalletsFolder);
- ImportColdcardCommand = ReactiveCommand.CreateFromTask(async () =>
- {
- var ofd = new OpenFileDialog
- {
- AllowMultiple = false,
- Title = "Import Coldcard"
- };
-
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- {
- ofd.Directory = Path.Combine("/media", Environment.UserName);
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
- {
- ofd.Directory = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
- }
-
- var window = (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).MainWindow;
- var selected = await ofd.ShowAsync(window, fallBack: true);
- if (selected != null && selected.Any())
- {
- var path = selected.First();
- var jsonString = await File.ReadAllTextAsync(path);
- var json = JObject.Parse(jsonString);
- var xpubString = json["ExtPubKey"].ToString();
- var mfpString = json["MasterFingerprint"].ToString();
-
- // https://github.com/zkSNACKs/WalletWasabi/pull/1663#issuecomment-508073066
- // Coldcard 2.1.0 improperly implemented Wasabi skeleton fingerprint at first, so we must reverse byte order.
- // The solution was to add a ColdCardFirmwareVersion json field from 2.1.1 and correct the one generated by 2.1.0.
- var coldCardVersionString = json["ColdCardFirmwareVersion"]?.ToString();
- var reverseByteOrder = false;
- if (coldCardVersionString is null)
- {
- reverseByteOrder = true;
- }
- else
- {
- Version coldCardVersion = new Version(coldCardVersionString);
-
- if (coldCardVersion == new Version("2.1.0")) // Should never happen though.
- {
- reverseByteOrder = true;
- }
- }
-
- var bytes = ByteHelpers.FromHex(Guard.NotNullOrEmptyOrWhitespace(nameof(mfpString), mfpString, trim: true));
- HDFingerprint mfp = reverseByteOrder ? new HDFingerprint(bytes.Reverse().ToArray()) : new HDFingerprint(bytes);
-
- ExtPubKey extPubKey = NBitcoinHelpers.BetterParseExtPubKey(xpubString);
-
- Logger.LogInfo("Creating a new wallet file.");
- var walletName = Global.WalletManager.WalletDirectories.GetNextWalletName("Coldcard");
- var walletFullPath = Global.WalletManager.WalletDirectories.GetWalletFilePaths(walletName).walletFilePath;
- KeyManager.CreateNewHardwareWalletWatchOnly(mfp, extPubKey, walletFullPath);
- owner.SelectLoadWallet();
- }
- });
-
- EnumerateHardwareWalletsCommand = ReactiveCommand.CreateFromTask(async () => await EnumerateIfHardwareWalletsAsync());
-
OpenBrowserCommand = ReactiveCommand.CreateFromTask<string>(IoHelpers.OpenBrowserAsync);
Observable
@@ -142,8 +81,6 @@ namespace WalletWasabi.Gui.Tabs.WalletManager.LoadWallets
.Merge(LoadCommand.ThrownExceptions)
.Merge(TestPasswordCommand.ThrownExceptions)
.Merge(OpenFolderCommand.ThrownExceptions)
- .Merge(ImportColdcardCommand.ThrownExceptions)
- .Merge(EnumerateHardwareWalletsCommand.ThrownExceptions)
.ObserveOn(RxApp.TaskpoolScheduler)
.Subscribe(ex =>
{
@@ -243,8 +180,7 @@ namespace WalletWasabi.Gui.Tabs.WalletManager.LoadWallets
public ReactiveCommand<Unit, Unit> LoadCommand { get; }
public ReactiveCommand<Unit, KeyManager> TestPasswordCommand { get; }
- public ReactiveCommand<Unit, Unit> ImportColdcardCommand { get; set; }
- public ReactiveCommand<Unit, Unit> EnumerateHardwareWalletsCommand { get; set; }
+
public ReactiveCommand<string, Unit> OpenBrowserCommand { get; }
public ReactiveCommand<Unit, Unit> OpenFolderCommand { get; }
private WalletManagerViewModel Owner { get; }
@@ -606,7 +542,7 @@ namespace WalletWasabi.Gui.Tabs.WalletManager.LoadWallets
return false;
}
- private async Task EnumerateIfHardwareWalletsAsync()
+ protected async Task EnumerateIfHardwareWalletsAsync()
{
if (!IsHardwareWallet)
{

MODIFY
WalletWasabi.Gui/Tabs/WalletManager/WalletManagerViewModel.cs
WalletWasabi.Gui/Tabs/WalletManager/WalletManagerViewModel.cs
@@ -19,7 +19,7 @@ namespace WalletWasabi.Gui.Tabs.WalletManager
{
[Export]
[Shared]
- internal class WalletManagerViewModel : WasabiDocumentTabViewModel
+ public class WalletManagerViewModel : WasabiDocumentTabViewModel
{
private ObservableCollection<CategoryViewModel> _categories;
private CategoryViewModel _selectedCategory;

Add ImportColdCard and Enumerate HW to the ConnectHwWalletVM