Write wiped Trezor enumerate test.
MODIFY
WalletWasabi.Tests/HwiTests/DeviceConnectedTests/UserWorkflowTests.cs
WalletWasabi.Tests/HwiTests/DeviceConnectedTests/UserWorkflowTests.cs
using NBitcoin;
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Hwi2;
+using WalletWasabi.Hwi2.Exceptions;
+using WalletWasabi.Hwi2.Models;
using Xunit;
namespace WalletWasabi.Tests.HwiTests.DeviceConnectedTests
{
public class UserWorkflowTests
{
+ #region SharedVariables
+
+ // Bottleneck: user action on device.
+ public TimeSpan ReasonableRequestTimeout { get; } = TimeSpan.FromMinutes(1);
+
+ #endregion SharedVariables
+
[Fact]
public async Task TrezorTTestsAsync()
{
- // USER: Connect a wiped Trezor T (hwi.exe enumerate, hwi.exe wipe --fingerprint FINGERPRINT)
- var hwiClient = new HwiClient(Network.Main);
+ // USER: Connect a Trezor T
+ var client = new HwiClient(Network.Main);
+ using (var cts = new CancellationTokenSource(ReasonableRequestTimeout))
+ {
+ var enumerate = await client.EnumerateAsync(cts.Token);
+
+ Assert.Single(enumerate);
+ HwiEnumerateEntry entry = enumerate.Single();
+ Assert.Equal(HardwareWalletVendors.Trezor, entry.Type);
+ Assert.NotNull(entry.Path);
+ Assert.NotEmpty(entry.Path);
+ Assert.False(entry.NeedsPassphraseSent);
+ Assert.False(entry.NeedsPinSent);
+ Assert.NotNull(entry.Error);
+ Assert.NotEmpty(entry.Error);
+ Assert.Equal(HwiErrorCode.NotInitialized, entry.Code);
+ Assert.Null(entry.Fingerprint);
+ }
}
}
}

MODIFY
WalletWasabi.Tests/HwiTests/NoDeviceConnectedTests/HwiDefaultResponseTests.cs
WalletWasabi.Tests/HwiTests/NoDeviceConnectedTests/HwiDefaultResponseTests.cs
@@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Hwi2;
using WalletWasabi.Hwi2.Exceptions;
+using WalletWasabi.Hwi2.Models;
using Xunit;
namespace WalletWasabi.Tests.HwiTests.NoDeviceConnectedTests
@@ -65,7 +66,7 @@ namespace WalletWasabi.Tests.HwiTests.NoDeviceConnectedTests
{
using (var cts = new CancellationTokenSource(ReasonableRequestTimeout))
{
- IEnumerable<string> enumerate = await client.EnumerateAsync(cts.Token);
+ IEnumerable<HwiEnumerateEntry> enumerate = await client.EnumerateAsync(cts.Token);
Assert.Empty(enumerate);
}
}

RENAME
WalletWasabi.Tests/HwiTests/IMockHwiProcessBridge.cs
WalletWasabi.Tests/HwiTests/NoDeviceConnectedTests/IMockHwiProcessBridge.cs
@@ -6,22 +6,22 @@ using System.Threading.Tasks;
using WalletWasabi.Hwi2.Models;
using WalletWasabi.Interfaces;
-namespace WalletWasabi.Tests.HwiTests
+namespace WalletWasabi.Tests.HwiTests.NoDeviceConnectedTests
{
public class IMockHwiProcessBridge : IProcessBridge
{
- public AllHardwareWallets Type { get; }
+ public HardwareWalletModels Model { get; }
- public IMockHwiProcessBridge(AllHardwareWallets type)
+ public IMockHwiProcessBridge(HardwareWalletModels model)
{
- Type = type;
+ Model = model;
}
public Task<(string response, int exitCode)> SendCommandAsync(string arguments, CancellationToken cancel)
{
if (arguments == "enumerate")
{
- if (Type == AllHardwareWallets.TrezorT)
+ if (Model == HardwareWalletModels.TrezorT)
{
var response = "[{\"type\": \"trezor\", \"path\": \"webusb: 001:4\", \"needs_pin_sent\": false, \"needs_passphrase_sent\": false, \"error\": \"Not initialized\"}]";
var code = 0;

MODIFY
WalletWasabi.Tests/HwiTests/NoDeviceConnectedTests/MockedDeviceTests.cs
WalletWasabi.Tests/HwiTests/NoDeviceConnectedTests/MockedDeviceTests.cs
using NBitcoin;
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Hwi2;
+using WalletWasabi.Hwi2.Exceptions;
using WalletWasabi.Hwi2.Models;
using Xunit;
@@ -23,11 +25,20 @@ namespace WalletWasabi.Tests.HwiTests.NoDeviceConnectedTests
[Fact]
public async Task CanEnumerateTestsAsync()
{
- var client = new HwiClient(Network.Main, new IMockHwiProcessBridge(AllHardwareWallets.TrezorT));
+ var client = new HwiClient(Network.Main, new IMockHwiProcessBridge(HardwareWalletModels.TrezorT));
using (var cts = new CancellationTokenSource(ReasonableRequestTimeout))
{
- IEnumerable<string> enumerate = await client.EnumerateAsync(cts.Token);
+ IEnumerable<HwiEnumerateEntry> enumerate = await client.EnumerateAsync(cts.Token);
Assert.Single(enumerate);
+ HwiEnumerateEntry entry = enumerate.Single();
+ Assert.Equal(HardwareWalletVendors.Trezor, entry.Type);
+ Assert.Equal("webusb: 001:4", entry.Path);
+ Assert.False(entry.NeedsPassphraseSent);
+ Assert.False(entry.NeedsPinSent);
+ Assert.NotNull(entry.Error);
+ Assert.NotEmpty(entry.Error);
+ Assert.Equal(HwiErrorCode.NotInitialized, entry.Code);
+ Assert.Null(entry.Fingerprint);
}
}

MODIFY
WalletWasabi/Hwi2/Exceptions/HwiErrorCode.cs
WalletWasabi/Hwi2/Exceptions/HwiErrorCode.cs
@@ -21,6 +21,7 @@ namespace WalletWasabi.Hwi2.Exceptions
UnknownError = -13,
ActionCanceled = -14,
DeviceBusy = -15,
- NeedToBeRoot = -16
+ NeedToBeRoot = -16,
+ NotInitialized = -99
}
}

MODIFY
WalletWasabi/Hwi2/HwiClient.cs
WalletWasabi/Hwi2/HwiClient.cs
@@ -13,6 +13,7 @@ using System.Threading.Tasks;
using WalletWasabi.Helpers;
using WalletWasabi.Hwi2.Exceptions;
using WalletWasabi.Hwi2.Models;
+using WalletWasabi.Hwi2.Parsers;
using WalletWasabi.Hwi2.ProcessBridge;
using WalletWasabi.Interfaces;
@@ -41,7 +42,7 @@ namespace WalletWasabi.Hwi2
private async Task<string> SendCommandAsync(IEnumerable<HwiOptions> options, HwiCommands? command, CancellationToken cancel)
{
- string arguments = GetArguments(options, command);
+ string arguments = HwiParser.ToArgumentString(Network, options, command);
try
{
@@ -74,19 +75,8 @@ namespace WalletWasabi.Hwi2
{
string responseString = await SendCommandAsync(options: new[] { HwiOptions.Version }, command: null, cancel).ConfigureAwait(false);
- // Example output: hwi 1.0.0
- if (TryParseVersion(responseString, "hwi", out Version v1))
- {
- return v1;
- }
-
- // Example output: hwi.exe 1.0.0
- if (TryParseVersion(responseString, "hwi.exe", out Version v2))
- {
- return v2;
- }
-
- throw new FormatException($"Cannot parse version from HWI's response. Response: {responseString}.");
+ var version = HwiParser.ParseVersion(responseString);
+ return version;
}
public async Task<string> GetHelpAsync(CancellationToken cancel)
@@ -96,19 +86,12 @@ namespace WalletWasabi.Hwi2
return responseString;
}
- public async Task<IEnumerable<string>> EnumerateAsync(CancellationToken cancel)
+ public async Task<IEnumerable<HwiEnumerateEntry>> EnumerateAsync(CancellationToken cancel)
{
string responseString = await SendCommandAsync(options: null, command: HwiCommands.Enumerate, cancel).ConfigureAwait(false);
- var jarr = JArray.Parse(responseString);
+ IEnumerable<HwiEnumerateEntry> response = HwiParser.ParseHwiEnumerateResponse(responseString);
- var hwis = new List<string>();
- foreach (JObject json in jarr)
- {
- string jsonString = json.ToString();
- hwis.Add(jsonString);
- }
-
- return hwis;
+ return response;
}
public async Task<string> SetupAsync(CancellationToken cancel)
@@ -127,91 +110,12 @@ namespace WalletWasabi.Hwi2
#region Helpers
- public static bool TryParseVersion(string hwiResponse, string substringFrom, out Version version)
- {
- int startIndex = hwiResponse.IndexOf(substringFrom) + substringFrom.Length;
- var versionString = hwiResponse.Substring(startIndex).Trim();
- version = null;
- if (Version.TryParse(versionString, out Version v))
- {
- version = v;
- return true;
- }
-
- return false;
- }
-
private static void ThrowIfError(string responseString)
{
- if (JsonHelpers.TryParseJToken(responseString, out JToken responseJToken))
- {
- if (responseJToken is JArray)
- {
- return;
- }
-
- var errToken = responseJToken["error"];
- var codeToken = responseJToken["code"];
- string err = "";
- HwiErrorCode? code = null;
- if (errToken != null)
- {
- err = Guard.Correct(errToken.Value<string>());
- }
- if (codeToken != null)
- {
- var codeInt = codeToken.Value<int>();
- if (Enum.IsDefined(typeof(HwiErrorCode), codeInt))
+ if (HwiParser.TryParseErrors(responseString, out HwiException error))
{
- code = (HwiErrorCode)codeInt;
- }
+ throw error;
}
-
- if (code.HasValue)
- {
- throw new HwiException(code.Value, err);
- }
- else if (err != "")
- {
- throw new HwiException(HwiErrorCode.UnknownError, err);
- }
- }
- else
- {
- var subString = "error:";
- if (responseString.Contains(subString, StringComparison.OrdinalIgnoreCase))
- {
- int startIndex = responseString.IndexOf(subString, StringComparison.OrdinalIgnoreCase) + subString.Length;
- var err = responseString.Substring(startIndex);
- throw new HwiException(HwiErrorCode.UnknownError, err);
- }
- }
- }
-
- private string GetArguments(IEnumerable<HwiOptions> options, HwiCommands? command)
- {
- options = options ?? Enumerable.Empty<HwiOptions>();
- var fullOptions = new List<HwiOptions>(options);
-
- if (Network != Network.Main)
- {
- fullOptions.Add(HwiOptions.TestNet);
- }
-
- var optionsString = string.Join(" --", fullOptions.Select(x => x.ToString().ToLowerInvariant()));
- optionsString = string.IsNullOrWhiteSpace(optionsString) ? "" : $"--{optionsString}";
- var argumentBuilder = new StringBuilder(optionsString);
- if (command != null)
- {
- if (argumentBuilder.Length != 0)
- {
- argumentBuilder.Append(' ');
- }
- argumentBuilder.Append(command.ToString().ToLowerInvariant());
- }
-
- var arguments = argumentBuilder.ToString().Trim();
- return arguments;
}
#endregion Helpers

RENAME
WalletWasabi/Hwi2/Models/AllHardwareWallets.cs
WalletWasabi/Hwi2/Models/HardwareWalletModels.cs
@@ -4,7 +4,7 @@ using System.Text;
namespace WalletWasabi.Hwi2.Models
{
- public enum AllHardwareWallets
+ public enum HardwareWalletModels
{
TrezorOne,
TrezorT,

ADD
WalletWasabi/Hwi2/Models/HardwareWalletVendors.cs
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace WalletWasabi.Hwi2.Models
+{
+ public enum HardwareWalletVendors
+ {
+ Trezor,
+ Coldcard,
+ Ledger,
+ KeepKey,
+ DigitalBitBox,
+ }
+}

ADD
WalletWasabi/Hwi2/Models/HwiEnumerateEntry.cs
+using NBitcoin;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using WalletWasabi.Hwi2.Exceptions;
+
+namespace WalletWasabi.Hwi2.Models
+{
+ public class HwiEnumerateEntry
+ {
+ public HardwareWalletVendors? Type { get; }
+ public string Path { get; }
+ public string SerialNumber { get; }
+ public HDFingerprint? Fingerprint { get; }
+ public bool? NeedsPinSent { get; }
+ public bool? NeedsPassphraseSent { get; }
+ public string Error { get; }
+ public HwiErrorCode? Code { get; }
+
+ public HwiEnumerateEntry(
+ HardwareWalletVendors? type,
+ string path,
+ string serialNumber,
+ HDFingerprint? fingerprint,
+ bool? needsPinSent,
+ bool? needsPassphraseSent,
+ string error,
+ HwiErrorCode? code)
+ {
+ Type = type;
+ Path = path;
+ SerialNumber = serialNumber;
+ Fingerprint = fingerprint;
+ NeedsPinSent = needsPinSent;
+ NeedsPassphraseSent = needsPassphraseSent;
+ Error = error;
+ Code = code;
+ }
+ }
+}

ADD
WalletWasabi/Hwi2/Parsers/HwiParser.cs
+using NBitcoin;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using WalletWasabi.Helpers;
+using WalletWasabi.Hwi2.Exceptions;
+using WalletWasabi.Hwi2.Models;
+
+namespace WalletWasabi.Hwi2.Parsers
+{
+ public static class HwiParser
+ {
+ public static bool TryParseErrors(string text, out HwiException error)
+ {
+ error = null;
+ if (JsonHelpers.TryParseJToken(text, out JToken token) && TryParseError(token, out HwiException e))
+ {
+ error = e;
+ }
+ else
+ {
+ var subString = "error:";
+ if (text.Contains(subString, StringComparison.OrdinalIgnoreCase))
+ {
+ int startIndex = text.IndexOf(subString, StringComparison.OrdinalIgnoreCase) + subString.Length;
+ var err = text.Substring(startIndex);
+ error = new HwiException(HwiErrorCode.UnknownError, err);
+ }
+ }
+
+ return error != null;
+ }
+
+ public static bool TryParseError(JToken token, out HwiException error)
+ {
+ error = null;
+ if (token is JArray)
+ {
+ return false;
+ }
+
+ var errToken = token["error"];
+ var codeToken = token["code"];
+
+ string err = "";
+ if (errToken != null)
+ {
+ err = Guard.Correct(errToken.Value<string>());
+ }
+
+ HwiErrorCode? code = null;
+ if (TryParseErrorCode(codeToken, out HwiErrorCode c))
+ {
+ code = c;
+ }
+ // HWI bug: it does not give error code.
+ // https://github.com/bitcoin-core/HWI/issues/216
+ else if (err == "Not initialized")
+ {
+ code = HwiErrorCode.NotInitialized;
+ }
+
+ if (code.HasValue)
+ {
+ error = new HwiException(code.Value, err);
+ }
+ else if (err != "")
+ {
+ error = new HwiException(HwiErrorCode.UnknownError, err);
+ }
+
+ return error != null;
+ }
+
+ public static bool TryParseErrorCode(JToken codeToken, out HwiErrorCode code)
+ {
+ code = default;
+
+ if (codeToken is null)
+ {
+ return false;
+ }
+
+ try
+ {
+ var codeInt = codeToken.Value<int>();
+ if (Enum.IsDefined(typeof(HwiErrorCode), codeInt))
+ {
+ code = (HwiErrorCode)codeInt;
+ return true;
+ }
+ }
+ catch
+ {
+ return false;
+ }
+
+ return false;
+ }
+
+ public static bool TryParseHardwareWalletVendor(JToken token, out HardwareWalletVendors vendor)
+ {
+ vendor = default;
+
+ if (token is null)
+ {
+ return false;
+ }
+
+ try
+ {
+ var typeString = token.Value<string>();
+ if (Enum.TryParse(typeString, ignoreCase: true, out HardwareWalletVendors t))
+ {
+ vendor = t;
+ return true;
+ }
+ }
+ catch
+ {
+ return false;
+ }
+
+ return false;
+ }
+
+ public static IEnumerable<HwiEnumerateEntry> ParseHwiEnumerateResponse(string responseString)
+ {
+ var jarr = JArray.Parse(responseString);
+
+ var response = new List<HwiEnumerateEntry>();
+ foreach (JObject json in jarr)
+ {
+ var hwiEntry = ParseHwiEnumerateEntry(json);
+ response.Add(hwiEntry);
+ }
+
+ return response;
+ }
+
+ public static HwiEnumerateEntry ParseHwiEnumerateEntry(JObject json)
+ {
+ JToken typeToken = json["type"];
+ var pathString = json["path"]?.ToString()?.Trim() ?? null;
+ var serialNumberString = json["serial_number"]?.ToString()?.Trim() ?? null;
+ var fingerprintString = json["fingerprint"]?.ToString()?.Trim() ?? null;
+ var needsPinSentString = json["needs_pin_sent"]?.ToString()?.Trim() ?? null;
+ var needsPassphraseSentString = json["needs_passphrase_sent"]?.ToString()?.Trim() ?? null;
+
+ HDFingerprint? fingerprint = null;
+ if (fingerprintString != null)
+ {
+ if (HDFingerprint.TryParse(fingerprintString, out HDFingerprint fp))
+ {
+ fingerprint = fp;
+ }
+ else
+ {
+ throw new FormatException($"Could not parse fingerprint: {fingerprintString}");
+ }
+ }
+
+ bool? needsPinSent = null;
+ if (needsPinSentString != null)
+ {
+ needsPinSent = bool.Parse(needsPinSentString);
+ }
+
+ bool? needsPassphraseSent = null;
+ if (needsPassphraseSentString != null)
+ {
+ needsPassphraseSent = bool.Parse(needsPassphraseSentString);
+ }
+
+ HwiErrorCode? code = null;
+ string errorString = null;
+ if (TryParseError(json, out HwiException err))
+ {
+ code = err.ErrorCode;
+ errorString = err.Message;
+ }
+
+ HardwareWalletVendors? type = null;
+ if (TryParseHardwareWalletVendor(typeToken, out HardwareWalletVendors t))
+ {
+ type = t;
+ }
+
+ return new HwiEnumerateEntry(
+ type: type,
+ path: pathString,
+ serialNumber: serialNumberString,
+ fingerprint: fingerprint,
+ needsPinSent: needsPinSent,
+ needsPassphraseSent: needsPassphraseSent,
+ error: errorString,
+ code: code);
+ }
+
+ public static bool TryParseVersion(string hwiResponse, string substringFrom, out Version version)
+ {
+ int startIndex = hwiResponse.IndexOf(substringFrom) + substringFrom.Length;
+ var versionString = hwiResponse.Substring(startIndex).Trim();
+ version = null;
+ if (Version.TryParse(versionString, out Version v))
+ {
+ version = v;
+ return true;
+ }
+
+ return false;
+ }
+
+ public static bool TryParseVersion(string hwiResponse, out Version version)
+ {
+ version = null;
+ // Example output: hwi 1.0.0
+ if (TryParseVersion(hwiResponse, "hwi", out Version v1))
+ {
+ version = v1;
+ }
+
+ // Example output: hwi.exe 1.0.0
+ if (TryParseVersion(hwiResponse, "hwi.exe", out Version v2))
+ {
+ version = v2;
+ }
+
+ return version != null;
+ }
+
+ public static Version ParseVersion(string hwiResponse)
+ {
+ if (TryParseVersion(hwiResponse, out Version version))
+ {
+ return version;
+ }
+
+ throw new FormatException($"Cannot parse version from HWI's response. Response: {hwiResponse}.");
+ }
+
+ public static string ToArgumentString(Network network, IEnumerable<HwiOptions> options, HwiCommands? command)
+ {
+ options = options ?? Enumerable.Empty<HwiOptions>();
+ var fullOptions = new List<HwiOptions>(options);
+
+ if (network != Network.Main)
+ {
+ fullOptions.Add(HwiOptions.TestNet);
+ }
+
+ var optionsString = string.Join(" --", fullOptions.Select(x => x.ToString().ToLowerInvariant()));
+ optionsString = string.IsNullOrWhiteSpace(optionsString) ? "" : $"--{optionsString}";
+ var argumentBuilder = new StringBuilder(optionsString);
+ if (command != null)
+ {
+ if (argumentBuilder.Length != 0)
+ {
+ argumentBuilder.Append(' ');
+ }
+ argumentBuilder.Append(command.ToString().ToLowerInvariant());
+ }
+
+ var arguments = argumentBuilder.ToString().Trim();
+ return arguments;
+ }
+ }
+}

Write wiped Trezor enumerate test.