Implement GetXpubAsync
MODIFY
WalletWasabi.Tests/ExtPubKeyExplorerTests.cs
WalletWasabi.Tests/ExtPubKeyExplorerTests.cs
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using WalletWasabi.Backend.Models;
+using WalletWasabi.KeyManagement;
using WalletWasabi.Services;
using Xunit;
@@ -21,7 +22,7 @@ namespace WalletWasabi.Tests
var mnemonic = new Mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about");
ExtKey = mnemonic.DeriveExtKey();
- ExtPubKey = ExtKey.Derive(new KeyPath("m/84'/0'/0'/1/0")).Neuter();
+ ExtPubKey = ExtKey.Derive(new KeyPath($"{KeyManager.DefaultAccountKeyPath.ToString(true, "h")}/1/0")).Neuter();
}
[Fact]

MODIFY
WalletWasabi.Tests/HwiTests/DeviceConnectedTests/Katas.cs
WalletWasabi.Tests/HwiTests/DeviceConnectedTests/Katas.cs
@@ -54,11 +54,12 @@ namespace WalletWasabi.Tests.HwiTests.DeviceConnectedTests
await Assert.ThrowsAsync<HwiException>(async () => await client.BackupAsync(deviceType, devicePath, cts.Token));
// Trezor T doesn't support it.
- await Assert.ThrowsAsync<HwiException>(async () => await client.PromptPinAsync(deviceType, devicePath, cts.Token));
+ var ex = await Assert.ThrowsAsync<HwiException>(async () => await client.PromptPinAsync(deviceType, devicePath, cts.Token));
await Assert.ThrowsAsync<HwiException>(async () => await client.SendPinAsync(deviceType, devicePath, 1111, cts.Token));
+ ExtPubKey xpub = await client.GetXpubAsync(deviceType, devicePath, cts.Token);
+ Assert.NotNull(xpub);
- // ToDo: getxpub
// ToDo: displayaddress
// ToDo: signmessage
// ToDo: signtx

MODIFY
WalletWasabi.Tests/HwiTests/NoDeviceConnectedTests/IMockHwiProcessBridge.cs
WalletWasabi.Tests/HwiTests/NoDeviceConnectedTests/IMockHwiProcessBridge.cs
@@ -19,7 +19,7 @@ namespace WalletWasabi.Tests.HwiTests.NoDeviceConnectedTests
public Task<(string response, int exitCode)> SendCommandAsync(string arguments, CancellationToken cancel)
{
- if (arguments == "enumerate" || arguments == "--testnet enumerate")
+ if (CompareArguments(arguments, "enumerate"))
{
if (Model == HardwareWalletModels.TrezorT)
{
@@ -28,7 +28,7 @@ namespace WalletWasabi.Tests.HwiTests.NoDeviceConnectedTests
return Task.FromResult((response, code));
}
}
- else if (arguments == "--device-path \"webusb: 001:4\" --device-type \"trezor\" wipe" || arguments == "--testnet --device-path \"webusb: 001:4\" --device-type \"trezor\" wipe")
+ else if (CompareArguments(arguments, "--device-path \"webusb: 001:4\" --device-type \"trezor\" wipe"))
{
if (Model == HardwareWalletModels.TrezorT)
{
@@ -37,7 +37,7 @@ namespace WalletWasabi.Tests.HwiTests.NoDeviceConnectedTests
return Task.FromResult((response, code));
}
}
- else if (arguments == "--device-path \"webusb: 001:4\" --device-type \"trezor\" setup" || arguments == "--testnet --device-path \"webusb: 001:4\" --device-type \"trezor\" setup")
+ else if (CompareArguments(arguments, "--device-path \"webusb: 001:4\" --device-type \"trezor\" setup"))
{
if (Model == HardwareWalletModels.TrezorT)
{
@@ -46,7 +46,7 @@ namespace WalletWasabi.Tests.HwiTests.NoDeviceConnectedTests
return Task.FromResult((response, code));
}
}
- else if (arguments == "--device-path \"webusb: 001:4\" --device-type \"trezor\" --interactive setup" || arguments == "--testnet --device-path \"webusb: 001:4\" --device-type \"trezor\" --interactive setup")
+ else if (CompareArguments(arguments, "--device-path \"webusb: 001:4\" --device-type \"trezor\" --interactive setup"))
{
if (Model == HardwareWalletModels.TrezorT)
{
@@ -55,7 +55,7 @@ namespace WalletWasabi.Tests.HwiTests.NoDeviceConnectedTests
return Task.FromResult((response, code));
}
}
- else if (arguments == "--device-path \"webusb: 001:4\" --device-type \"trezor\" --interactive restore" || arguments == "--testnet --device-path \"webusb: 001:4\" --device-type \"trezor\" --interactive restore")
+ else if (CompareArguments(arguments, "--device-path \"webusb: 001:4\" --device-type \"trezor\" --interactive restore"))
{
if (Model == HardwareWalletModels.TrezorT)
{
@@ -64,7 +64,7 @@ namespace WalletWasabi.Tests.HwiTests.NoDeviceConnectedTests
return Task.FromResult((response, code));
}
}
- else if (arguments == "--device-path \"webusb: 001:4\" --device-type \"trezor\" --interactive backup" || arguments == "--testnet --device-path \"webusb: 001:4\" --device-type \"trezor\" --interactive backup")
+ else if (CompareArguments(arguments, "--device-path \"webusb: 001:4\" --device-type \"trezor\" --interactive backup"))
{
if (Model == HardwareWalletModels.TrezorT)
{
@@ -73,7 +73,7 @@ namespace WalletWasabi.Tests.HwiTests.NoDeviceConnectedTests
return Task.FromResult((response, code));
}
}
- else if (arguments == "--device-path \"webusb: 001:4\" --device-type \"trezor\" backup" || arguments == "--testnet --device-path \"webusb: 001:4\" --device-type \"trezor\" backup")
+ else if (CompareArguments(arguments, "--device-path \"webusb: 001:4\" --device-type \"trezor\" backup"))
{
if (Model == HardwareWalletModels.TrezorT)
{
@@ -82,7 +82,7 @@ namespace WalletWasabi.Tests.HwiTests.NoDeviceConnectedTests
return Task.FromResult((response, code));
}
}
- else if (arguments == "--device-path \"webusb: 001:4\" --device-type \"trezor\" promptpin" || arguments == "--testnet --device-path \"webusb: 001:4\" --device-type \"trezor\" promptpin")
+ else if (CompareArguments(arguments, "--device-path \"webusb: 001:4\" --device-type \"trezor\" promptpin"))
{
if (Model == HardwareWalletModels.TrezorT)
{
@@ -91,7 +91,7 @@ namespace WalletWasabi.Tests.HwiTests.NoDeviceConnectedTests
return Task.FromResult((response, code));
}
}
- else if (arguments.StartsWith("--device-path \"webusb: 001:4\" --device-type \"trezor\" sendpin", StringComparison.OrdinalIgnoreCase) || arguments.StartsWith("--testnet --device-path \"webusb: 001:4\" --device-type \"trezor\" sendpin", StringComparison.OrdinalIgnoreCase))
+ else if (CompareArguments(arguments, "--device-path \"webusb: 001:4\" --device-type \"trezor\" sendpin", true))
{
if (Model == HardwareWalletModels.TrezorT)
{
@@ -100,8 +100,39 @@ namespace WalletWasabi.Tests.HwiTests.NoDeviceConnectedTests
return Task.FromResult((response, code));
}
}
+ else if (CompareArguments(arguments, "--device-path \"webusb: 001:4\" --device-type \"trezor\" getxpub", true))
+ {
+ if (Model == HardwareWalletModels.TrezorT)
+ {
+ var response = "{\"xpub\": \"xpub6DHjDx4gzLV37gJWMxYJAqyKRGN46MT61RHVizdU62cbVUYu9L95cXKzX62yJ2hPbN11EeprS8sSn8kj47skQBrmycCMzFEYBQSntVKFQ5M\"}\r\n";
+ var code = 0;
+ return Task.FromResult((response, code));
+ }
+ }
throw new NotImplementedException($"Mocking is not implemented for '{arguments}'");
}
+
+ private static bool CompareArguments(string arguments, string desired, bool useStartWith = false)
+ {
+ var testnetDesired = $"--testnet {desired}";
+
+ if (useStartWith)
+ {
+ if (arguments.StartsWith(desired, StringComparison.OrdinalIgnoreCase) || arguments.StartsWith(testnetDesired, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+ else
+ {
+ if (arguments == desired || arguments == testnetDesired)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
}

MODIFY
WalletWasabi.Tests/HwiTests/NoDeviceConnectedTests/MockedDeviceTests.cs
WalletWasabi.Tests/HwiTests/NoDeviceConnectedTests/MockedDeviceTests.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using WalletWasabi.Helpers;
using WalletWasabi.Hwi2;
using WalletWasabi.Hwi2.Exceptions;
using WalletWasabi.Hwi2.Models;
@@ -51,11 +52,21 @@ namespace WalletWasabi.Tests.HwiTests.NoDeviceConnectedTests
// Trezor T doesn't support it.
var backup = await Assert.ThrowsAsync<HwiException>(async () => await client.BackupAsync(deviceType, devicePath, cts.Token));
+ Assert.Equal("The Trezor does not support creating a backup via software", backup.Message);
+ Assert.Equal(HwiErrorCode.UnavailableAction, backup.ErrorCode);
// Trezor T doesn't support it.
var promptpin = await Assert.ThrowsAsync<HwiException>(async () => await client.PromptPinAsync(deviceType, devicePath, cts.Token));
+ Assert.Equal("The PIN has already been sent to this device", promptpin.Message);
+ Assert.Equal(HwiErrorCode.DeviceAlreadyUnlocked, promptpin.ErrorCode);
var sendpin = await Assert.ThrowsAsync<HwiException>(async () => await client.SendPinAsync(deviceType, devicePath, 1111, cts.Token));
+ Assert.Equal("The PIN has already been sent to this device", sendpin.Message);
+ Assert.Equal(HwiErrorCode.DeviceAlreadyUnlocked, sendpin.ErrorCode);
+
+ ExtPubKey xpub = await client.GetXpubAsync(deviceType, devicePath, cts.Token);
+ var expecteXpub = NBitcoinHelpers.BetterParseExtPubKey("xpub6DHjDx4gzLV37gJWMxYJAqyKRGN46MT61RHVizdU62cbVUYu9L95cXKzX62yJ2hPbN11EeprS8sSn8kj47skQBrmycCMzFEYBQSntVKFQ5M");
+ Assert.Equal(expecteXpub, xpub);
}
}

MODIFY
WalletWasabi.Tests/KeyManagementTests.cs
WalletWasabi.Tests/KeyManagementTests.cs
@@ -147,7 +147,7 @@ namespace WalletWasabi.Tests
Assert.Equal(isInternal, generatedKey.IsInternal);
Assert.Equal(label, generatedKey.Label);
Assert.Equal(keyState, generatedKey.KeyState);
- Assert.StartsWith("84'/0'/0'", generatedKey.FullKeyPath.ToString());
+ Assert.StartsWith(KeyManager.DefaultAccountKeyPath.ToString(), generatedKey.FullKeyPath.ToString());
}
}

MODIFY
WalletWasabi/Extensions/NBitcoinExtensions.cs
WalletWasabi/Extensions/NBitcoinExtensions.cs
@@ -5,6 +5,7 @@ using NBitcoin.RPC;
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Helpers;
@@ -263,5 +264,24 @@ namespace NBitcoin
{
list.Sort((x, y) => x.Value.CompareTo(y.Value));
}
+
+ /// <param name="startWithM">The keypath will start with m/ or not.</param>
+ /// <param name="format">h or ', eg.: m/84h/0h/0 or m/84'/0'/0</param>
+ public static string ToString(this KeyPath me, bool startWithM, string format)
+ {
+ var toStringBuilder = new StringBuilder(me.ToString());
+
+ if (startWithM)
+ {
+ toStringBuilder.Insert(0, "m/");
+ }
+
+ if (format == "h")
+ {
+ toStringBuilder.Replace('\'', 'h');
+ }
+
+ return toStringBuilder.ToString();
+ }
}
}

MODIFY
WalletWasabi/Hwi2/HwiClient.cs
WalletWasabi/Hwi2/HwiClient.cs
@@ -16,6 +16,7 @@ using WalletWasabi.Hwi2.Models;
using WalletWasabi.Hwi2.Parsers;
using WalletWasabi.Hwi2.ProcessBridge;
using WalletWasabi.Interfaces;
+using WalletWasabi.KeyManagement;
namespace WalletWasabi.Hwi2
{
@@ -89,6 +90,19 @@ namespace WalletWasabi.Hwi2
cancel).ConfigureAwait(false);
}
+ public async Task<ExtPubKey> GetXpubAsync(HardwareWalletVendors deviceType, string devicePath, CancellationToken cancel)
+ {
+ var response = await SendCommandAsync(
+ options: new[] { HwiOption.DevicePath(devicePath), HwiOption.DeviceType(deviceType) },
+ command: HwiCommands.GetXpub,
+ commandArguments: KeyManager.DefaultAccountKeyPath.ToString(true, "h"),
+ cancel).ConfigureAwait(false);
+
+ var extPubKey = HwiParser.ParseExtPubKey(response);
+
+ return extPubKey;
+ }
+
public async Task WipeAsync(HardwareWalletVendors deviceType, string devicePath, CancellationToken cancel)
{
await SendCommandAsync(

MODIFY
WalletWasabi/Hwi2/Models/HwiCommands.cs
WalletWasabi/Hwi2/Models/HwiCommands.cs
@@ -10,7 +10,7 @@ namespace WalletWasabi.Hwi2.Models
GetMasterXpub,
SignTx,
GetXpub,
- SignBessage,
+ SignMessage,
GetKeypool,
DisplayAddress,
Setup,

MODIFY
WalletWasabi/Hwi2/Parsers/HwiParser.cs
WalletWasabi/Hwi2/Parsers/HwiParser.cs
@@ -146,6 +146,20 @@ namespace WalletWasabi.Hwi2.Parsers
return response;
}
+ public static ExtPubKey ParseExtPubKey(string json)
+ {
+ if (JsonHelpers.TryParseJToken(json, out JToken token))
+ {
+ var extPubKeyString = token["xpub"]?.ToString()?.Trim() ?? null;
+ var extPubKey = NBitcoinHelpers.BetterParseExtPubKey(extPubKeyString);
+ return extPubKey;
+ }
+ else
+ {
+ throw new FormatException($"Could not parse extpubkey: {json}");
+ }
+ }
+
public static HwiEnumerateEntry ParseHwiEnumerateEntry(JObject json)
{
JToken typeToken = json["type"];

MODIFY
WalletWasabi/KeyManagement/KeyManager.cs
WalletWasabi/KeyManagement/KeyManager.cs
@@ -65,7 +65,7 @@ namespace WalletWasabi.KeyManagement
// BIP84-ish derivation scheme
// m / purpose' / coin_type' / account' / change / address_index
// https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki
- public static readonly KeyPath DefaultAccountKeyPath = new KeyPath("m/84'/0'/0'");
+ public static readonly KeyPath DefaultAccountKeyPath = new KeyPath("m/84h/0h/0h");
public string FilePath { get; private set; }
private object ToFileLock { get; }

Implement GetXpubAsync