Start creating PSBT
MODIFY
WalletWasabi.Gui/Controls/WalletExplorer/CoinJoinTabView.xaml
WalletWasabi.Gui/Controls/WalletExplorer/CoinJoinTabView.xaml
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel IsVisible="{Binding IsWatchOnly}" Grid.Column="0" Spacing="8">
- <StackPanel VerticalAlignment="Center">
- <TextBlock Text="Hardware and watch-only wallets cannot coinjoin :(" />
+ <StackPanel IsVisible="{Binding IsHardwareWallet}" VerticalAlignment="Center">
+ <TextBlock Text="Hardware wallets cannot coinjoin :(" />
</StackPanel>
</StackPanel>
<StackPanel IsVisible="{Binding !IsWatchOnly}" Grid.Column="0" Spacing="10">

MODIFY
WalletWasabi.Gui/Controls/WalletExplorer/SendTabView.xaml
WalletWasabi.Gui/Controls/WalletExplorer/SendTabView.xaml
<Grid></Grid>
</DockPanel>
</StackPanel>
+ <StackPanel IsVisible="{Binding IsHardwareWallet}" Spacing="10" Orientation="Horizontal">
+ <DockPanel VerticalAlignment="Top" LastChildFill="True">
+ <Button Command="{Binding BuildTransactionCommand}" DockPanel.Dock="Right">
+ <StackPanel Orientation="Horizontal">
+ <controls:Spinner Height="20" Width="20" IsVisible="{Binding IsBusy}" Margin="0 0 8 0" />
+ <TextBlock Text="{Binding BuildTransactionButtonText}" />
+ </StackPanel>
+ </Button>
+ <Grid></Grid>
+ </DockPanel>
+ </StackPanel>
<TextBlock Text="{Binding WarningMessage}" Classes="warningMessage" />
<TextBlock Text="{Binding SuccessMessage}" Classes="validationMessage" Foreground="Green" />
<Grid ColumnDefinitions="60,100,100,600,100,Auto">

MODIFY
WalletWasabi.Gui/Controls/WalletExplorer/SendTabViewModel.cs
WalletWasabi.Gui/Controls/WalletExplorer/SendTabViewModel.cs
@@ -17,6 +17,7 @@ using WalletWasabi.Gui.Tabs.WalletManager;
using WalletWasabi.Gui.ViewModels;
using WalletWasabi.Gui.ViewModels.Validation;
using WalletWasabi.Helpers;
+using WalletWasabi.Hwi;
using WalletWasabi.Models;
using WalletWasabi.Services;
@@ -49,9 +50,11 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
private string _amountWaterMarkText;
private string _amountToolTip;
private bool _isBusy;
+ private bool _isHardwareBusy;
private string _warningMessage;
private string _successMessage;
private const string BuildTransactionButtonTextString = "Send Transaction";
+ private const string WaitingForHardwareWalletButtonTextString = "Waiting for Hardware Wallet...";
private const string BuildingTransactionButtonTextString = "Sending Transaction...";
private int _caretIndex;
private ObservableCollection<SuggestionViewModel> _suggestions;
@@ -136,16 +139,14 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
SetFeesAndTexts();
});
- this.WhenAnyValue(x => x.IsBusy).Subscribe(busy =>
+ (this).WhenAnyValue(x => x.IsBusy).Subscribe(_ =>
{
- if (busy)
- {
- BuildTransactionButtonText = BuildingTransactionButtonTextString;
- }
- else
+ SetSendText();
+ });
+
+ (this).WhenAnyValue(x => x.IsHardwareBusy).Subscribe(_ =>
{
- BuildTransactionButtonText = BuildTransactionButtonTextString;
- }
+ SetSendText();
});
this.WhenAnyValue(x => x.Password).Subscribe(x =>
@@ -252,7 +253,22 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
return;
}
- var result = await Task.Run(() => Global.WalletService.BuildTransaction(Password, new[] { operation }, FeeTarget, allowUnconfirmed: true, allowedInputs: selectedCoinReferences));
+ BuildTransactionResult result = await Task.Run(() => Global.WalletService.BuildTransaction(Password, new[] { operation }, FeeTarget, allowUnconfirmed: true, allowedInputs: selectedCoinReferences));
+
+ MainWindowViewModel.Instance.StatusBar.SetStatusAndDoUpdateActions("Signing transaction...");
+
+ if (IsHardwareWallet && !result.Signed) // If hardware but still has a privkey then it's password, then meh.
+ {
+ try
+ {
+ IsHardwareBusy = true;
+ await HwiProcessManager.SignTxAsync(KeyManager.HardwareWalletInfo, result.Psbt);
+ }
+ finally
+ {
+ IsHardwareBusy = false;
+ }
+ }
MainWindowViewModel.Instance.StatusBar.SetStatusAndDoUpdateActions("Broadcasting transaction...");
@@ -284,6 +300,22 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
(isMax, amount, address, busy) => (isMax.Value || !string.IsNullOrWhiteSpace(amount.Value)) && !string.IsNullOrWhiteSpace(Address) && !IsBusy));
}
+ private void SetSendText()
+ {
+ if (IsHardwareBusy)
+ {
+ BuildTransactionButtonText = WaitingForHardwareWalletButtonTextString;
+ }
+ else if (IsBusy)
+ {
+ BuildTransactionButtonText = BuildingTransactionButtonTextString;
+ }
+ else
+ {
+ BuildTransactionButtonText = BuildTransactionButtonTextString;
+ }
+ }
+
private void SetAmountWatermarkAndToolTip(Money amount)
{
if (amount == Money.Zero)
@@ -616,6 +648,12 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
set => this.RaiseAndSetIfChanged(ref _isBusy, value);
}
+ public bool IsHardwareBusy
+ {
+ get => _isHardwareBusy;
+ set => this.RaiseAndSetIfChanged(ref _isHardwareBusy, value);
+ }
+
public string BuildTransactionButtonText
{
get => _buildTransactionButtonText;

MODIFY
WalletWasabi.Gui/Controls/WalletExplorer/WalletActionViewModel.cs
WalletWasabi.Gui/Controls/WalletExplorer/WalletActionViewModel.cs
@@ -15,6 +15,7 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
public WalletService WalletService => Wallet.WalletService;
public KeyManager KeyManager => WalletService.KeyManager;
public bool IsWatchOnly => KeyManager.IsWatchOnly;
+ public bool IsHardwareWallet => KeyManager.IsHardwareWallet;
public WalletActionViewModel(string title, WalletViewModel walletViewModel)
: base(title)

MODIFY
WalletWasabi.Gui/Global.cs
WalletWasabi.Gui/Global.cs
@@ -287,7 +287,7 @@ namespace WalletWasabi.Gui
try
{
- await HwiProcessManager.EnsureHwiInstalledAsync(DataDir);
+ await HwiProcessManager.EnsureHwiInstalledAsync(DataDir, Network);
}
catch (Exception ex)
{

MODIFY
WalletWasabi.Gui/Tabs/WalletManager/LoadWalletViewModel.cs
WalletWasabi.Gui/Tabs/WalletManager/LoadWalletViewModel.cs
@@ -296,13 +296,13 @@ namespace WalletWasabi.Gui.Tabs.WalletManager
}
var walletName = SelectedWallet;
+ HardwareWalletInfo selectedHwi = null;
if (isHardwareWallet)
{
var lastEnumerationClone = LastHardwareWalletEnumeration.ToList();
if (lastEnumerationClone.Any())
{
var trimmedSelectedWallet = SelectedWallet.Trim();
- HardwareWalletInfo selectedHwi;
if (Enum.TryParse(typeof(HardwareWalletType), trimmedSelectedWallet, ignoreCase: true, out object t))
{
var type = (HardwareWalletType)t;
@@ -316,11 +316,13 @@ namespace WalletWasabi.Gui.Tabs.WalletManager
ExtPubKey extPubKey = await HwiProcessManager.GetXpubAsync(selectedHwi);
var walletFiles = new DirectoryInfo(Global.WalletsDir);
- var walletBackupFiles = new DirectoryInfo(Global.WalletsDir);
+ var walletBackupFiles = new DirectoryInfo(Global.WalletBackupsDir);
walletName = null;
- foreach (FileInfo walletFile in walletFiles.EnumerateFiles()
+ var walletFileNames = walletFiles.EnumerateFiles()
.Concat(walletBackupFiles.EnumerateFiles())
- .OrderByDescending(x => x.LastAccessTimeUtc))
+ .OrderByDescending(x => x.LastAccessTimeUtc)
+ .ToList();
+ foreach (FileInfo walletFile in walletFileNames)
{
var km = KeyManager.FromFile(walletFile.FullName);
if (km.ExtPubKey == extPubKey) // We already had it.
@@ -334,7 +336,7 @@ namespace WalletWasabi.Gui.Tabs.WalletManager
{
Logger.LogInfo<LoadWalletViewModel>("Hardware wallet wasn't used previously on this computer. Creating new wallet file.");
- walletName = $"{selectedHwi.Type}-{selectedHwi.Fingerprint}";
+ walletName = Utils.GetNextHardwareWalletName(selectedHwi);
var path = Global.GetWalletFullPath(walletName);
KeyManager.CreateNewWatchOnly(extPubKey, path);
}
@@ -357,6 +359,7 @@ namespace WalletWasabi.Gui.Tabs.WalletManager
}
KeyManager keyManager = Global.LoadKeyManager(walletFullPath, walletBackupFullPath);
+ keyManager.HardwareWalletInfo = selectedHwi;
if (!requirePassword && keyManager.PasswordVerified == false)
{

MODIFY
WalletWasabi.Gui/Utils.cs
WalletWasabi.Gui/Utils.cs
using System;
using System.IO;
using System.Threading.Tasks;
+using WalletWasabi.Hwi.Models;
namespace WalletWasabi.Gui
{
@@ -20,6 +21,19 @@ namespace WalletWasabi.Gui
throw new NotSupportedException("This is impossible.");
}
+ public static string GetNextHardwareWalletName(HardwareWalletInfo hwi)
+ {
+ for (int i = 0; i < int.MaxValue; i++)
+ {
+ if (!File.Exists(Path.Combine(Global.WalletsDir, $"{hwi.Type}-{hwi.Fingerprint}-{i}.json")))
+ {
+ return $"{hwi.Type}-{hwi.Fingerprint}-{i}";
+ }
+ }
+
+ throw new NotSupportedException("This is impossible.");
+ }
+
public static void PostLogException(this Dispatcher dispatcher, Func<Task> action, DispatcherPriority priority = DispatcherPriority.Normal)
{
dispatcher.Post(async () =>

MODIFY
WalletWasabi/KeyManagement/KeyManager.cs
WalletWasabi/KeyManagement/KeyManager.cs
@@ -9,6 +9,7 @@ using System.Text;
using System.Security;
using System;
using WalletWasabi.Models;
+using WalletWasabi.Hwi.Models;
namespace WalletWasabi.KeyManagement
{
@@ -57,6 +58,8 @@ namespace WalletWasabi.KeyManagement
private object ToFileLock { get; }
public bool IsWatchOnly { get; }
+ public bool IsHardwareWallet => HardwareWalletInfo != null;
+ public HardwareWalletInfo HardwareWalletInfo { get; set; }
[JsonConstructor]
public KeyManager(BitcoinEncryptedSecretNoEC encryptedSecret, byte[] chainCode, ExtPubKey extPubKey, bool? passwordVerified, BlockchainState blockchainState, string filePath = null)
@@ -77,6 +80,7 @@ namespace WalletWasabi.KeyManagement
PasswordVerified = passwordVerified;
BlockchainState = blockchainState ?? new BlockchainState();
+ HardwareWalletInfo = null;
SetFilePath(filePath);
ToFileLock = new object();
@@ -93,6 +97,7 @@ namespace WalletWasabi.KeyManagement
ScriptHdPubkeyMapLock = new object();
BlockchainState = new BlockchainState();
BlockchainStateLock = new object();
+ HardwareWalletInfo = null;
if (password is null)
{

MODIFY
WalletWasabi/Services/WalletService.cs
WalletWasabi/Services/WalletService.cs
@@ -24,6 +24,7 @@ using System.Diagnostics;
using NBitcoin.BitcoinCore;
using System.Net.Sockets;
using NBitcoin.Protocol.Behaviors;
+using NBitcoin.BIP174;
namespace WalletWasabi.Services
{
@@ -1195,36 +1196,47 @@ namespace WalletWasabi.Services
Logger.LogInfo<WalletService>("Selecting coins...");
IEnumerable<SmartCoin> coinsToSpend = SelectCoinsToSpend(allowedSmartCoinInputs, totalOutgoingAmount);
+ // 9. Build the transaction
+ Logger.LogInfo<WalletService>("Signing transaction...");
+ TransactionBuilder builder = Network.CreateTransactionBuilder();
+ // It must be watch only, too, because if we have the key and also hardware wallet, we don't care we can sign.
+ bool sign = !(KeyManager.IsWatchOnly && KeyManager.IsHardwareWallet);
+ if (sign)
+ {
// 8. Get signing keys
IEnumerable<ExtKey> signingKeys = KeyManager.GetSecrets(password, coinsToSpend.Select(x => x.ScriptPubKey).ToArray());
- // 9. Build the transaction
- Logger.LogInfo<WalletService>("Signing transaction...");
- var builder = Network.CreateTransactionBuilder();
builder = builder
.AddCoins(coinsToSpend.Select(x => x.GetCoin()))
.AddKeys(signingKeys.ToArray());
+ }
+ else
+ {
+ builder = builder
+ .AddCoins(coinsToSpend.Select(x => x.GetCoin()));
+ }
foreach ((Script scriptPubKey, Money amount, string label) output in realToSend)
{
builder = builder.Send(output.scriptPubKey, output.amount);
}
- var tx = builder
+ Transaction tx = builder
.SetChange(changeScriptPubKey)
.SendFees(fee)
- .BuildTransaction(true);
+ .BuildTransaction(sign);
+ if (sign)
+ {
TransactionPolicyError[] checkResults = builder.Check(tx, fee);
if (checkResults.Length > 0)
{
throw new InvalidTxException(tx, checkResults);
}
+ }
List<SmartCoin> spentCoins = Coins.Where(x => tx.Inputs.Any(y => y.PrevOut.Hash == x.TransactionId && y.PrevOut.N == x.Index)).ToList();
- TxoRef[] spentOutputs = spentCoins.Select(x => new TxoRef(x.TransactionId, x.Index)).ToArray();
-
var outerWalletOutputs = new List<SmartCoin>();
var innerWalletOutputs = new List<SmartCoin>();
for (var i = 0U; i < tx.Outputs.Count; i++)
@@ -1245,9 +1257,19 @@ namespace WalletWasabi.Services
}
}
+ PSBT psbt = PSBT.FromTransaction(tx);
+ uint masterFP = BitConverter.ToUInt32(KeyManager.ExtPubKey.Fingerprint);
+ foreach (var coin in spentCoins.Concat(innerWalletOutputs).Concat(outerWalletOutputs).ToHashSet())
+ {
+ if (coin.HdPubKey != null)
+ {
+ psbt.AddKeyPath(coin.HdPubKey.PubKey, Tuple.Create(masterFP, coin.HdPubKey.NonHardenedKeyPath));
+ }
+ }
+
Logger.LogInfo<WalletService>($"Transaction is successfully built: {tx.GetHash()}.");
- return new BuildTransactionResult(new SmartTransaction(tx, Height.Unknown), spendsUnconfirmed, fee, feePc, outerWalletOutputs, innerWalletOutputs, spentCoins);
+ return new BuildTransactionResult(new SmartTransaction(tx, Height.Unknown), psbt, spendsUnconfirmed, sign, fee, feePc, outerWalletOutputs, innerWalletOutputs, spentCoins);
}
private IEnumerable<SmartCoin> SelectCoinsToSpend(IEnumerable<SmartCoin> unspentCoins, Money totalOutAmount)

Start creating PSBT