Add advanced options on Recover Wallet tab
MODIFY
WalletWasabi.Gui/Tabs/WalletManager/RecoverWalletView.xaml
WalletWasabi.Gui/Tabs/WalletManager/RecoverWalletView.xaml
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
+ <StackPanel Spacing="16">
+ <CheckBox IsChecked="{Binding ShowAdvancedOptions,Mode=TwoWay}">
+ <TextBlock Text="Show advanced options"/>
+ </CheckBox>
+ <StackPanel IsVisible="{Binding ShowAdvancedOptions}">
+ <controls:ExtendedTextBox Text="{Binding AccountKeyPath}" Watermark="Account Key Path" UseFloatingWatermark="True"/>
+ <TextBlock Text="Note that, Wasabi can only monitor native SegWit (bech32) addresses."/>
+ </StackPanel>
+ <StackPanel IsVisible="{Binding ShowAdvancedOptions}">
+ <controls:ExtendedTextBox Text="{Binding MinGapLimit}" Watermark="Minimum Gap Limit" UseFloatingWatermark="True"/>
+ </StackPanel>
+ </StackPanel>
</StackPanel>
</Grid>
</controls:GroupBox>

MODIFY
WalletWasabi.Gui/Tabs/WalletManager/RecoverWalletViewModel.cs
WalletWasabi.Gui/Tabs/WalletManager/RecoverWalletViewModel.cs
@@ -22,6 +22,9 @@ namespace WalletWasabi.Gui.Tabs.WalletManager
private string _mnemonicWords;
private string _walletName;
private string _validationMessage;
+ private bool _showAdvancedOptions;
+ private string _accountKeyPath;
+ private int _minGapLimit;
private ObservableCollection<SuggestionViewModel> _suggestions;
public RecoverWalletViewModel(WalletManagerViewModel owner) : base("Recover Wallet")
@@ -46,14 +49,31 @@ namespace WalletWasabi.Gui.Tabs.WalletManager
}
else if (string.IsNullOrWhiteSpace(MnemonicWords))
{
- ValidationMessage = $"Recovery Words were not supplied.";
+ ValidationMessage = "Recovery Words were not supplied.";
+ }
+ else if (string.IsNullOrWhiteSpace(AccountKeyPath))
+ {
+ ValidationMessage = "The account key path is not valid.";
+ }
+ else if (MinGapLimit < KeyManager.AbsoluteMinGapLimit - 1)
+ {
+ ValidationMessage = $"Min Gap Limit cannot be smaller than {KeyManager.AbsoluteMinGapLimit - 1}.";
+ }
+ else if (MinGapLimit > KeyManager.AbsoluteMaxGapLimit)
+ {
+ ValidationMessage = $"Min Gap Limit cannot be larger than {KeyManager.AbsoluteMaxGapLimit}.";
+ }
+ else if (!TryParseKeyPath(AccountKeyPath))
+ {
+ ValidationMessage = "The account key path is not a valid derivation path.";
}
else
{
+ KeyPath keyPath = KeyPath.Parse(AccountKeyPath);
try
{
var mnemonic = new Mnemonic(MnemonicWords);
- KeyManager.Recover(mnemonic, Password, walletFilePath);
+ KeyManager.Recover(mnemonic, Password, walletFilePath, keyPath, MinGapLimit + 1);
owner.SelectLoadWallet();
}
@@ -132,6 +152,24 @@ namespace WalletWasabi.Gui.Tabs.WalletManager
set => this.RaiseAndSetIfChanged(ref _caretIndex, value);
}
+ public bool ShowAdvancedOptions
+ {
+ get => _showAdvancedOptions;
+ set => this.RaiseAndSetIfChanged(ref _showAdvancedOptions, value);
+ }
+
+ public string AccountKeyPath
+ {
+ get => _accountKeyPath;
+ set => this.RaiseAndSetIfChanged(ref _accountKeyPath, value);
+ }
+
+ public int MinGapLimit
+ {
+ get => _minGapLimit;
+ set => this.RaiseAndSetIfChanged(ref _minGapLimit, value);
+ }
+
public ReactiveCommand<Unit, Unit> RecoverCommand { get; }
public void OnTermsClicked()
@@ -157,6 +195,9 @@ namespace WalletWasabi.Gui.Tabs.WalletManager
MnemonicWords = "";
WalletName = Utils.GetNextWalletName();
ValidationMessage = null;
+ ShowAdvancedOptions = false;
+ AccountKeyPath = $"m/{KeyManager.DefaultAccountKeyPath}";
+ MinGapLimit = KeyManager.AbsoluteMinGapLimit - 1;
}
private void UpdateSuggestions(string words)
@@ -203,6 +244,19 @@ namespace WalletWasabi.Gui.Tabs.WalletManager
Suggestions.Clear();
}
+ private bool TryParseKeyPath(string keyPath)
+ {
+ try
+ {
+ KeyPath.Parse(keyPath);
+ return true;
+ }
+ catch (FormatException)
+ {
+ return false;
+ }
+ }
+
private static IEnumerable<string> EnglishWords { get; } = Wordlist.English.GetWords();
}
}

MODIFY
WalletWasabi.Tests/KeyManagementTests.cs
WalletWasabi.Tests/KeyManagementTests.cs
@@ -72,10 +72,15 @@ namespace WalletWasabi.Tests
Assert.Equal(manager.EncryptedSecret, sameManager.EncryptedSecret);
Assert.Equal(manager.ExtPubKey, sameManager.ExtPubKey);
- var differentManager = KeyManager.Recover(mnemonic, "differentPassword");
+ var differentManager = KeyManager.Recover(mnemonic, "differentPassword", null, KeyPath.Parse("m/999'/999'/999'"), 55);
Assert.NotEqual(manager.ChainCode, differentManager.ChainCode);
Assert.NotEqual(manager.EncryptedSecret, differentManager.EncryptedSecret);
Assert.NotEqual(manager.ExtPubKey, differentManager.ExtPubKey);
+
+ differentManager.AssertCleanKeysIndexed();
+ var newKey = differentManager.GenerateNewKey("some-label", KeyState.Clean, true, false);
+ Assert.Equal(newKey.Index, differentManager.MinGapLimit);
+ Assert.Equal("999'/999'/999'/1/55", newKey.FullKeyPath.ToString());
}
[Fact]

MODIFY
WalletWasabi/KeyManagement/KeyManager.cs
WalletWasabi/KeyManagement/KeyManager.cs
@@ -6,11 +6,13 @@ using System.IO;
using System.Linq;
using System.Security;
using System.Text;
+using NBitcoin.JsonConverters;
using WalletWasabi.Helpers;
using WalletWasabi.Hwi.Models;
using WalletWasabi.JsonConverters;
using WalletWasabi.Logging;
using WalletWasabi.Models;
+using HDFingerprintJsonConverter = WalletWasabi.JsonConverters.HDFingerprintJsonConverter;
namespace WalletWasabi.KeyManagement
{
@@ -47,6 +49,10 @@ namespace WalletWasabi.KeyManagement
[JsonProperty(Order = 8)]
private List<HdPubKey> HdPubKeys { get; }
+ [JsonProperty(Order = 9)]
+ [JsonConverter(typeof(KeyPathJsonConverter))]
+ public KeyPath AccountKeyPath { get; private set; }
+
private object HdPubKeysLock { get; }
private List<byte[]> HdPubKeyScriptBytes { get; }
@@ -60,7 +66,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 AccountKeyPath = new KeyPath("m/84'/0'/0'");
+ public static readonly KeyPath DefaultAccountKeyPath = new KeyPath("m/84'/0'/0'");
public string FilePath { get; private set; }
private object ToFileLock { get; }
@@ -69,10 +75,11 @@ namespace WalletWasabi.KeyManagement
public bool IsHardwareWallet => EncryptedSecret is null && MasterFingerprint != null;
public HardwareWalletInfo HardwareWalletInfo { get; set; }
- private const int AbsoluteMinGapLimit = 21;
+ public const int AbsoluteMinGapLimit = 21;
+ public const int AbsoluteMaxGapLimit = 1_000_000;
[JsonConstructor]
- public KeyManager(BitcoinEncryptedSecretNoEC encryptedSecret, byte[] chainCode, HDFingerprint? masterFingerprint, ExtPubKey extPubKey, bool? passwordVerified, int? minGapLimit, BlockchainState blockchainState, string filePath = null)
+ public KeyManager(BitcoinEncryptedSecretNoEC encryptedSecret, byte[] chainCode, HDFingerprint? masterFingerprint, ExtPubKey extPubKey, bool? passwordVerified, int? minGapLimit, BlockchainState blockchainState, string filePath = null, KeyPath accountKeyPath = null)
{
HdPubKeys = new List<HdPubKey>();
HdPubKeyScriptBytes = new List<byte[]>();
@@ -88,17 +95,18 @@ namespace WalletWasabi.KeyManagement
ExtPubKey = Guard.NotNull(nameof(extPubKey), extPubKey);
PasswordVerified = passwordVerified;
- SetMinGaplimit(minGapLimit);
+ SetMinGapLimit(minGapLimit);
BlockchainState = blockchainState ?? new BlockchainState();
HardwareWalletInfo = null;
+ AccountKeyPath = accountKeyPath ?? DefaultAccountKeyPath;
SetFilePath(filePath);
ToFileLock = new object();
ToFile();
}
- public KeyManager(BitcoinEncryptedSecretNoEC encryptedSecret, byte[] chainCode, string password, int minGapLimit = AbsoluteMinGapLimit, string filePath = null)
+ public KeyManager(BitcoinEncryptedSecretNoEC encryptedSecret, byte[] chainCode, string password, int minGapLimit = AbsoluteMinGapLimit, string filePath = null, KeyPath accountKeyPath = null)
{
HdPubKeys = new List<HdPubKey>();
HdPubKeyScriptBytes = new List<byte[]>();
@@ -115,13 +123,14 @@ namespace WalletWasabi.KeyManagement
password = "";
}
- SetMinGaplimit(minGapLimit);
+ SetMinGapLimit(minGapLimit);
EncryptedSecret = Guard.NotNull(nameof(encryptedSecret), encryptedSecret);
ChainCode = Guard.NotNull(nameof(chainCode), chainCode);
var extKey = new ExtKey(encryptedSecret.GetKey(password), chainCode);
MasterFingerprint = extKey.Neuter().PubKey.GetHDFingerPrint();
+ AccountKeyPath = accountKeyPath ?? DefaultAccountKeyPath;
ExtPubKey = extKey.Derive(AccountKeyPath).Neuter();
SetFilePath(filePath);
@@ -141,8 +150,9 @@ namespace WalletWasabi.KeyManagement
var encryptedSecret = extKey.PrivateKey.GetEncryptedBitcoinSecret(password, Network.Main);
HDFingerprint masterFingerprint = extKey.Neuter().PubKey.GetHDFingerPrint();
- ExtPubKey extPubKey = extKey.Derive(AccountKeyPath).Neuter();
- return new KeyManager(encryptedSecret, extKey.ChainCode, masterFingerprint, extPubKey, false, AbsoluteMinGapLimit, new BlockchainState(), filePath);
+ KeyPath keyPath = DefaultAccountKeyPath;
+ ExtPubKey extPubKey = extKey.Derive(keyPath).Neuter();
+ return new KeyManager(encryptedSecret, extKey.ChainCode, masterFingerprint, extPubKey, false, AbsoluteMinGapLimit, new BlockchainState(), filePath, keyPath);
}
public static KeyManager CreateNewWatchOnly(ExtPubKey extPubKey, string filePath = null)
@@ -155,7 +165,7 @@ namespace WalletWasabi.KeyManagement
return new KeyManager(null, null, masterFingerpring, extPubKey, null, AbsoluteMinGapLimit, new BlockchainState(), filePath);
}
- public static KeyManager Recover(Mnemonic mnemonic, string password, string filePath = null)
+ public static KeyManager Recover(Mnemonic mnemonic, string password, string filePath = null, KeyPath accountKeyPath = null, int minGapLimit = AbsoluteMinGapLimit)
{
Guard.NotNull(nameof(mnemonic), mnemonic);
if (password is null)
@@ -166,12 +176,14 @@ namespace WalletWasabi.KeyManagement
ExtKey extKey = mnemonic.DeriveExtKey(password);
var encryptedSecret = extKey.PrivateKey.GetEncryptedBitcoinSecret(password, Network.Main);
- HDFingerprint masterFingerpring = extKey.Neuter().PubKey.GetHDFingerPrint();
- ExtPubKey extPubKey = extKey.Derive(AccountKeyPath).Neuter();
- return new KeyManager(encryptedSecret, extKey.ChainCode, masterFingerpring, extPubKey, true, AbsoluteMinGapLimit, new BlockchainState(), filePath);
+ HDFingerprint masterFingerprint = extKey.Neuter().PubKey.GetHDFingerPrint();
+
+ KeyPath keyPath = accountKeyPath ?? DefaultAccountKeyPath;
+ ExtPubKey extPubKey = extKey.Derive(keyPath).Neuter();
+ return new KeyManager(encryptedSecret, extKey.ChainCode, masterFingerprint, extPubKey, true, minGapLimit, new BlockchainState(), filePath, keyPath);
}
- public void SetMinGaplimit(int? minGapLimit)
+ public void SetMinGapLimit(int? minGapLimit)
{
if (minGapLimit is int val)
{

Add advanced options on Recover Wallet tab