Store master fingerprint
MODIFY
WalletWasabi.Gui/Tabs/WalletManager/LoadWalletViewModel.cs
WalletWasabi.Gui/Tabs/WalletManager/LoadWalletViewModel.cs
@@ -329,41 +329,15 @@ namespace WalletWasabi.Gui.Tabs.WalletManager
return null;
}
- ExtPubKey extPubKey = await HwiProcessManager.GetXpubAsync(selectedWallet.HardwareWalletInfo);
-
- var walletFiles = new DirectoryInfo(Global.WalletsDir);
- var walletBackupFiles = new DirectoryInfo(Global.WalletBackupsDir);
-
- // Start searching for the real wallet name.
- walletName = null;
-
- List<FileInfo> walletFileNames = new List<FileInfo>();
-
- if (walletFiles.Exists) walletFileNames.AddRange(walletFiles.EnumerateFiles());
- if (walletBackupFiles.Exists) walletFileNames.AddRange(walletFiles.EnumerateFiles());
-
- walletFileNames = walletFileNames.OrderByDescending(x => x.LastAccessTimeUtc).ToList();
-
- foreach (FileInfo walletFile in walletFileNames)
+ if (!TryFindWalletByMasterFingerprint(selectedWallet.HardwareWalletInfo.MasterFingerprint, out walletName))
{
- if (walletFile?.Extension?.Equals(".json", StringComparison.OrdinalIgnoreCase) is true)
- {
- var epk = KeyManager.ExtPubKeyFromFile(walletFile.FullName);
- if (epk == extPubKey) // We already had it.
- {
- walletName = walletFile.Name;
- break;
- }
- }
- }
+ ExtPubKey extPubKey = await HwiProcessManager.GetXpubAsync(selectedWallet.HardwareWalletInfo);
- if (walletName == null)
- {
Logger.LogInfo<LoadWalletViewModel>("Hardware wallet wasn't used previously on this computer. Creating new wallet file.");
walletName = Utils.GetNextHardwareWalletName(selectedWallet.HardwareWalletInfo);
var path = Global.GetWalletFullPath(walletName);
- KeyManager.CreateNewWatchOnly(extPubKey, path);
+ KeyManager.CreateNewHardwareWalletWatchOnly(selectedWallet.HardwareWalletInfo.MasterFingerprint, extPubKey, path);
}
}
@@ -416,6 +390,37 @@ namespace WalletWasabi.Gui.Tabs.WalletManager
}
}
+ private static bool TryFindWalletByMasterFingerprint(HDFingerprint masterFingerprint, out string walletName)
+ {
+ // Start searching for the real wallet name.
+ walletName = null;
+
+ var walletFiles = new DirectoryInfo(Global.WalletsDir);
+ var walletBackupFiles = new DirectoryInfo(Global.WalletBackupsDir);
+
+ List<FileInfo> walletFileNames = new List<FileInfo>();
+
+ if (walletFiles.Exists) walletFileNames.AddRange(walletFiles.EnumerateFiles());
+ if (walletBackupFiles.Exists) walletFileNames.AddRange(walletFiles.EnumerateFiles());
+
+ walletFileNames = walletFileNames.OrderByDescending(x => x.LastAccessTimeUtc).ToList();
+
+ foreach (FileInfo walletFile in walletFileNames)
+ {
+ if (walletFile?.Extension?.Equals(".json", StringComparison.OrdinalIgnoreCase) is true
+ && KeyManager.TryGetMasterFingerprintFromFile(walletFile.FullName, out HDFingerprint fp))
+ {
+ if (fp == masterFingerprint) // We already had it.
+ {
+ walletName = walletFile.Name;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
public async Task LoadWalletAsync()
{
try

MODIFY
WalletWasabi.Gui/Tabs/WalletManager/WalletManagerViewModel.cs
WalletWasabi.Gui/Tabs/WalletManager/WalletManagerViewModel.cs
@@ -97,10 +97,11 @@ namespace WalletWasabi.Gui.Tabs.WalletManager
{
await RefreshHardwareWalletListAsync();
HardwareWalletRefreshCancel?.Dispose();
+ HardwareWalletRefreshCancel = null;
});
}
- private CancellationTokenSource HardwareWalletRefreshCancel { get; }
+ private CancellationTokenSource HardwareWalletRefreshCancel { get; set; }
private bool HwTabAutomaticallySelectedOnce { get; set; } = false;
private async Task RefreshHardwareWalletListAsync()

MODIFY
WalletWasabi.Tests/KeyManagementTests.cs
WalletWasabi.Tests/KeyManagementTests.cs
@@ -35,7 +35,7 @@ namespace WalletWasabi.Tests
Assert.NotNull(manager3.EncryptedSecret);
Assert.NotNull(manager3.ExtPubKey);
- var sameManager = new KeyManager(manager.EncryptedSecret, manager.ChainCode, manager.ExtPubKey, true, new BlockchainState());
+ var sameManager = new KeyManager(manager.EncryptedSecret, manager.ChainCode, manager.MasterFingerprint, manager.ExtPubKey, true, new BlockchainState());
var sameManager2 = new KeyManager(manager.EncryptedSecret, manager.ChainCode, password);
Logger.TurnOff();
Assert.Throws<SecurityException>(() => new KeyManager(manager.EncryptedSecret, manager.ChainCode, "differentPassword"));

MODIFY
WalletWasabi/KeyManagement/KeyManager.cs
WalletWasabi/KeyManagement/KeyManager.cs
@@ -26,18 +26,22 @@ namespace WalletWasabi.KeyManagement
public byte[] ChainCode { get; }
[JsonProperty(Order = 3)]
+ [JsonConverter(typeof(HDFingerprintJsonConverter))]
+ public HDFingerprint? MasterFingerprint { get; private set; }
+
+ [JsonProperty(Order = 4)]
[JsonConverter(typeof(ExtPubKeyJsonConverter))]
public ExtPubKey ExtPubKey { get; }
- [JsonProperty(Order = 4)]
+ [JsonProperty(Order = 5)]
public bool? PasswordVerified { get; private set; }
- [JsonProperty(Order = 5)]
+ [JsonProperty(Order = 6)]
private BlockchainState BlockchainState { get; }
private object BlockchainStateLock { get; }
- [JsonProperty(Order = 6)]
+ [JsonProperty(Order = 7)]
private List<HdPubKey> HdPubKeys { get; }
private object HdPubKeysLock { get; }
@@ -63,7 +67,7 @@ namespace WalletWasabi.KeyManagement
public HardwareWalletInfo HardwareWalletInfo { get; set; }
[JsonConstructor]
- public KeyManager(BitcoinEncryptedSecretNoEC encryptedSecret, byte[] chainCode, ExtPubKey extPubKey, bool? passwordVerified, BlockchainState blockchainState, string filePath = null)
+ public KeyManager(BitcoinEncryptedSecretNoEC encryptedSecret, byte[] chainCode, HDFingerprint? masterFingerprint, ExtPubKey extPubKey, bool? passwordVerified, BlockchainState blockchainState, string filePath = null)
{
HdPubKeys = new List<HdPubKey>();
HdPubKeyScriptBytes = new List<byte[]>();
@@ -76,6 +80,7 @@ namespace WalletWasabi.KeyManagement
EncryptedSecret = encryptedSecret;
IsWatchOnly = EncryptedSecret is null;
ChainCode = chainCode;
+ MasterFingerprint = masterFingerprint;
ExtPubKey = Guard.NotNull(nameof(extPubKey), extPubKey);
PasswordVerified = passwordVerified;
@@ -110,6 +115,7 @@ namespace WalletWasabi.KeyManagement
ChainCode = Guard.NotNull(nameof(chainCode), chainCode);
var extKey = new ExtKey(encryptedSecret.GetKey(password), chainCode);
+ MasterFingerprint = extKey.Neuter().PubKey.GetHDFingerPrint();
ExtPubKey = extKey.Derive(AccountKeyPath).Neuter();
SetFilePath(filePath);
@@ -128,12 +134,19 @@ namespace WalletWasabi.KeyManagement
ExtKey extKey = mnemonic.DeriveExtKey(password);
var encryptedSecret = extKey.PrivateKey.GetEncryptedBitcoinSecret(password, Network.Main);
- return new KeyManager(encryptedSecret, extKey.ChainCode, extKey.Derive(AccountKeyPath).Neuter(), false, new BlockchainState(), filePath);
+ HDFingerprint masterFingerprint = extKey.Neuter().PubKey.GetHDFingerPrint();
+ ExtPubKey extPubKey = extKey.Derive(AccountKeyPath).Neuter();
+ return new KeyManager(encryptedSecret, extKey.ChainCode, masterFingerprint, extPubKey, false, new BlockchainState(), filePath);
}
public static KeyManager CreateNewWatchOnly(ExtPubKey extPubKey, string filePath = null)
{
- return new KeyManager(null, null, extPubKey, null, new BlockchainState(), filePath);
+ return new KeyManager(null, null, null, extPubKey, null, new BlockchainState(), filePath);
+ }
+
+ public static KeyManager CreateNewHardwareWalletWatchOnly(HDFingerprint masterFingerpring, ExtPubKey extPubKey, string filePath = null)
+ {
+ return new KeyManager(null, null, masterFingerpring, extPubKey, null, new BlockchainState(), filePath);
}
public static KeyManager Recover(Mnemonic mnemonic, string password, string filePath = null)
@@ -147,7 +160,9 @@ namespace WalletWasabi.KeyManagement
ExtKey extKey = mnemonic.DeriveExtKey(password);
var encryptedSecret = extKey.PrivateKey.GetEncryptedBitcoinSecret(password, Network.Main);
- return new KeyManager(encryptedSecret, extKey.ChainCode, extKey.Derive(AccountKeyPath).Neuter(), true, new BlockchainState(), filePath);
+ HDFingerprint masterFingerpring = extKey.Neuter().PubKey.GetHDFingerPrint();
+ ExtPubKey extPubKey = extKey.Derive(AccountKeyPath).Neuter();
+ return new KeyManager(encryptedSecret, extKey.ChainCode, masterFingerpring, extPubKey, true, new BlockchainState(), filePath);
}
public void SetFilePath(string filePath)
@@ -231,6 +246,7 @@ namespace WalletWasabi.KeyManagement
string jsonString = File.ReadAllText(safestFile, Encoding.UTF8);
var km = JsonConvert.DeserializeObject<KeyManager>(jsonString);
+
km.SetFilePath(filePath);
lock (km.HdPubKeyScriptBytesLock)
{
@@ -244,11 +260,19 @@ namespace WalletWasabi.KeyManagement
km.ScriptHdPubkeyMap.Add(key.P2wpkhScript, key);
}
}
+
+ // Backwards compatibility:
+ if (km.PasswordVerified is null)
+ {
+ km.PasswordVerified = true;
+ }
+
return km;
}
- public static ExtPubKey ExtPubKeyFromFile(string filePath)
+ public static bool TryGetExtPubKeyFromFile(string filePath, out ExtPubKey extPubKey)
{
+ extPubKey = default;
filePath = Guard.NotNullOrEmptyOrWhitespace(nameof(filePath), filePath);
if (!File.Exists(filePath))
@@ -263,7 +287,7 @@ namespace WalletWasabi.KeyManagement
if (string.IsNullOrEmpty(extpubkeyline))
{
- throw new InvalidOperationException($"Could not find line ExtPubKey in file: {filePath}");
+ return false;
}
var parts = extpubkeyline.Split("\"ExtPubKey\": \"");
@@ -274,8 +298,41 @@ namespace WalletWasabi.KeyManagement
var xpub = parts[1].TrimEnd(',', '"');
- var epk = NBitcoinHelpers.BetterParseExtPubKey(xpub);
- return epk;
+ extPubKey = NBitcoinHelpers.BetterParseExtPubKey(xpub);
+ return true;
+ }
+
+ public static bool TryGetMasterFingerprintFromFile(string filePath, out HDFingerprint masterFingerprint)
+ {
+ masterFingerprint = default;
+ filePath = Guard.NotNullOrEmptyOrWhitespace(nameof(filePath), filePath);
+
+ if (!File.Exists(filePath))
+ {
+ throw new FileNotFoundException($"Wallet file not found at: `{filePath}`.");
+ }
+
+ // Example text to handle: "ExtPubKey": "03BF8271268000000013B9013C881FE456DDF524764F6322F611B03CF6".
+ var masterfpline = File.ReadLines(filePath) // Enumerated read.
+ .Take(10) // Limit reads to x lines.
+ .FirstOrDefault(line => line.Contains("\"MasterFingerprint\": \"", StringComparison.InvariantCulture));
+
+ if (string.IsNullOrEmpty(masterfpline))
+ {
+ return false;
+ }
+
+ var parts = masterfpline.Split("\"MasterFingerprint\": \"");
+ if (parts.Length != 2)
+ {
+ throw new FormatException($"Could not split line: {masterfpline}");
+ }
+
+ var hex = parts[1].TrimEnd(',', '"');
+
+ var mfp = new HDFingerprint(ByteHelpers.FromHex(hex));
+ masterFingerprint = mfp;
+ return true;
}
public HdPubKey GenerateNewKey(string label, KeyState keyState, bool isInternal, bool toFile = true)
@@ -436,7 +493,15 @@ namespace WalletWasabi.KeyManagement
try
{
Key secret = EncryptedSecret.GetKey(password);
- return new ExtKey(secret, ChainCode);
+ var extkey = new ExtKey(secret, ChainCode);
+
+ // Backwards compatibility:
+ if (MasterFingerprint is null)
+ {
+ MasterFingerprint = secret.PubKey.GetHDFingerPrint();
+ }
+
+ return extkey;
}
catch (SecurityException ex)
{

MODIFY
WalletWasabi/Services/WalletService.cs
WalletWasabi/Services/WalletService.cs
@@ -1259,22 +1259,6 @@ namespace WalletWasabi.Services
}
PSBT psbt = builder.BuildPSBT(sign);
- HDFingerprint masterFP;
- if (KeyManager.IsHardwareWallet && !string.IsNullOrWhiteSpace(KeyManager.HardwareWalletInfo.Fingerprint)) // Prefer HWI's fingerprint at first, then fall back to NBitcoin's.
- {
- try
- {
- masterFP = new HDFingerprint(ByteHelpers.FromHex(KeyManager.HardwareWalletInfo.Fingerprint));
- }
- catch
- {
- masterFP = KeyManager.ExtPubKey.PubKey.GetHDFingerPrint();
- }
- }
- else
- {
- masterFP = KeyManager.ExtPubKey.PubKey.GetHDFingerPrint();
- }
HashSet<SmartCoin> allTxCoins = spentCoins.Concat(innerWalletOutputs).Concat(outerWalletOutputs).ToHashSet();
foreach (var coin in allTxCoins)
{
@@ -1296,7 +1280,11 @@ namespace WalletWasabi.Services
{
index = (int)coin.Index;
}
- psbt.AddKeyPath(masterFP, coin.HdPubKey.PubKey, coin.HdPubKey.FullKeyPath, coin.ScriptPubKey);
+
+ if (KeyManager.MasterFingerprint.HasValue)
+ {
+ psbt.AddKeyPath(KeyManager.MasterFingerprint.Value, coin.HdPubKey.PubKey, coin.HdPubKey.FullKeyPath, coin.ScriptPubKey);
+ }
}
}

Store master fingerprint