public WalletInfoViewModel(Wallet wallet) : base(wallet.WalletName) { Global = Locator.Current.GetService <Global>(); Wallet = wallet; this.ValidateProperty(x => x.Password, ValidatePassword); ClearSensitiveData(true); ToggleSensitiveKeysCommand = ReactiveCommand.Create(() => { if (ShowSensitiveKeys) { ClearSensitiveData(true); } else { var secret = PasswordHelper.GetMasterExtKey(Wallet.KeyManager, Password, out string isCompatibilityPasswordUsed); Password = ""; if (isCompatibilityPasswordUsed is { }) { NotificationHelpers.Warning(PasswordHelper.CompatibilityPasswordWarnMessage); } string master = secret.GetWif(Global.Network).ToWif(); string account = secret.Derive(Wallet.KeyManager.AccountKeyPath).GetWif(Global.Network).ToWif(); string masterZ = secret.ToZPrv(Global.Network); string accountZ = secret.Derive(Wallet.KeyManager.AccountKeyPath).ToZPrv(Global.Network); SetSensitiveData(master, account, masterZ, accountZ); }
private async Task DoDequeueAsync(IEnumerable <SmartCoin> coins) { IsDequeueBusy = true; try { if (!coins.Any()) { NotificationHelpers.Warning("No coins are selected.", ""); return; } try { await Global.ChaumianClient.DequeueCoinsFromMixAsync(coins.ToArray(), DequeueReason.UserRequested); } catch (Exception ex) { Logger.LogWarning(ex); } Password = string.Empty; } finally { IsDequeueBusy = false; } }
public WalletInfoViewModel(WalletViewModel walletViewModel) : base(walletViewModel.Name, walletViewModel) { ClearSensitiveData(true); ToggleSensitiveKeysCommand = ReactiveCommand.Create(() => { try { if (ShowSensitiveKeys) { ClearSensitiveData(true); } else { var secret = PasswordHelper.GetMasterExtKey(KeyManager, Password, out string isCompatibilityPasswordUsed); Password = ""; if (isCompatibilityPasswordUsed != null) { NotificationHelpers.Warning(PasswordHelper.CompatibilityPasswordWarnMessage); } string master = secret.GetWif(Global.Network).ToWif(); string account = secret.Derive(KeyManager.AccountKeyPath).GetWif(Global.Network).ToWif(); string masterZ = secret.ToZPrv(Global.Network); string accountZ = secret.Derive(KeyManager.AccountKeyPath).ToZPrv(Global.Network); SetSensitiveData(master, account, masterZ, accountZ); } } catch (Exception ex) { NotificationHelpers.Error(ex.ToTypeMessageString()); } }); }
protected override async Task BuildTransaction(string password, PaymentIntent payments, FeeStrategy feeStrategy, bool allowUnconfirmed = false, IEnumerable <OutPoint> allowedInputs = null) { BuildTransactionResult result = await Task.Run(() => Wallet.BuildTransaction(Password, payments, feeStrategy, allowUnconfirmed: true, allowedInputs: allowedInputs, GetPayjoinClient())); MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.SigningTransaction); SmartTransaction signedTransaction = result.Transaction; if (Wallet.KeyManager.IsHardwareWallet && !result.Signed) // If hardware but still has a privkey then it's password, then meh. { try { IsHardwareBusy = true; MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.AcquiringSignatureFromHardwareWallet); var client = new HwiClient(Global.Network); using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3)); PSBT signedPsbt = null; try { try { signedPsbt = await client.SignTxAsync(Wallet.KeyManager.MasterFingerprint.Value, result.Psbt, cts.Token); } catch (PSBTException ex) when(ex.Message.Contains("NullFail")) { NotificationHelpers.Warning("Fall back to Unverified Inputs Mode, trying to sign again."); // Ledger Nano S hackfix https://github.com/MetacoSA/NBitcoin/pull/888 var noinputtx = result.Psbt.Clone(); foreach (var input in noinputtx.Inputs) { input.NonWitnessUtxo = null; } signedPsbt = await client.SignTxAsync(Wallet.KeyManager.MasterFingerprint.Value, noinputtx, cts.Token); } } catch (HwiException) { await PinPadViewModel.UnlockAsync(); signedPsbt = await client.SignTxAsync(Wallet.KeyManager.MasterFingerprint.Value, result.Psbt, cts.Token); } signedTransaction = signedPsbt.ExtractSmartTransaction(result.Transaction); } finally { MainWindowViewModel.Instance.StatusBar.TryRemoveStatus(StatusType.AcquiringSignatureFromHardwareWallet); IsHardwareBusy = false; } } MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.BroadcastingTransaction); await Task.Run(async() => await Global.TransactionBroadcaster.SendTransactionAsync(signedTransaction)); ResetUi(); }
public ReceiveTabViewModel(Wallet wallet) : base("Receive") { Global = Locator.Current.GetService <Global>(); Wallet = wallet; LabelSuggestion = new SuggestLabelViewModel(Wallet); _addresses = new ObservableCollection <AddressViewModel>(); LabelSuggestion.Label = ""; InitializeAddresses(); GenerateCommand = ReactiveCommand.Create(() => { var label = new SmartLabel(LabelSuggestion.Label); LabelSuggestion.Label = label; if (label.IsEmpty) { NotificationHelpers.Warning("Observers are required."); return; } AvaloniaThreadingExtensions.PostLogException(Dispatcher.UIThread, () => { var newKey = Wallet.KeyManager.GetNextReceiveKey(label, out bool minGapLimitIncreased); if (minGapLimitIncreased) { int minGapLimit = Wallet.KeyManager.MinGapLimit.Value; int prevMinGapLimit = minGapLimit - 1; NotificationHelpers.Warning($"{nameof(KeyManager.MinGapLimit)} increased from {prevMinGapLimit} to {minGapLimit}."); } var newAddress = new AddressViewModel(newKey, Wallet.KeyManager, this); Addresses.Insert(0, newAddress); SelectedAddress = newAddress; LabelSuggestion.Label = ""; }); }); this.WhenAnyValue(x => x.SelectedAddress) .Subscribe(async address => { if (Global.UiConfig.Autocopy is false || address is null) { return; } await address.TryCopyToClipboardAsync(); }); Observable .Merge(GenerateCommand.ThrownExceptions) .ObserveOn(RxApp.TaskpoolScheduler) .Subscribe(ex => { Logger.LogError(ex); NotificationHelpers.Error(ex.ToUserFriendlyString()); }); }
public ReceiveTabViewModel(WalletViewModel walletViewModel) : base("Receive", walletViewModel) { _labelSuggestion = new SuggestLabelViewModel(Global); _addresses = new ObservableCollection <AddressViewModel>(); _labelSuggestion.Label = ""; InitializeAddresses(); GenerateCommand = ReactiveCommand.Create(() => { var label = new SmartLabel(_labelSuggestion.Label); _labelSuggestion.Label = label; if (label.IsEmpty) { NotificationHelpers.Warning("Label is required."); return; } Dispatcher.UIThread.PostLogException(() => { HdPubKey newKey = Global.WalletService.GetReceiveKey(_labelSuggestion.Label, Addresses.Select(x => x.Model).Take(7)); // Never touch the first 7 keys. AddressViewModel found = Addresses.FirstOrDefault(x => x.Model == newKey); if (found != default) { Addresses.Remove(found); } var newAddress = new AddressViewModel(newKey, Global); Addresses.Insert(0, newAddress); SelectedAddress = newAddress; _labelSuggestion.Label = ""; }); }); this.WhenAnyValue(x => x.SelectedAddress).Subscribe(async address => { if (Global.UiConfig?.Autocopy is false || address is null) { return; } await address.TryCopyToClipboardAsync(); }); var isCoinListItemSelected = this.WhenAnyValue(x => x.SelectedAddress).Select(coin => coin is { });
private async Task DoEnqueueAsync(IEnumerable <SmartCoin> coins) { IsEnqueueBusy = true; try { if (!coins.Any()) { NotificationHelpers.Warning("No coins are selected.", ""); return; } try { PasswordHelper.GetMasterExtKey(KeyManager, Password, out string compatiblityPassword); // If the password is not correct we throw. if (compatiblityPassword != null) { Password = compatiblityPassword; NotificationHelpers.Warning(PasswordHelper.CompatibilityPasswordWarnMessage); } await Global.ChaumianClient.QueueCoinsToMixAsync(Password, coins.ToArray()); } catch (SecurityException ex) { NotificationHelpers.Error(ex.Message, ""); } catch (Exception ex) { Logger.LogWarning(ex); var builder = new StringBuilder(ex.ToTypeMessageString()); if (ex is AggregateException aggex) { foreach (var iex in aggex.InnerExceptions) { builder.Append(Environment.NewLine + iex.ToTypeMessageString()); } } NotificationHelpers.Error(builder.ToString()); } Password = string.Empty; } finally { IsEnqueueBusy = false; } }
public ReceiveTabViewModel(WalletViewModel walletViewModel) : base("Receive", walletViewModel) { _labelSuggestion = new SuggestLabelViewModel(Global); _addresses = new ObservableCollection <AddressViewModel>(); _labelSuggestion.Label = ""; InitializeAddresses(); GenerateCommand = ReactiveCommand.Create(() => { var label = new SmartLabel(_labelSuggestion.Label); _labelSuggestion.Label = label; if (label.IsEmpty) { NotificationHelpers.Warning("Label is required."); return; } AvaloniaThreadingExtensions.PostLogException(Dispatcher.UIThread, () => { var newKey = KeyManager.GetNextReceiveKey(label, out bool minGapLimitIncreased); if (minGapLimitIncreased) { int minGapLimit = KeyManager.MinGapLimit.Value; int prevMinGapLimit = minGapLimit - 1; NotificationHelpers.Warning($"{nameof(KeyManager.MinGapLimit)} increased from {prevMinGapLimit} to {minGapLimit}."); } var newAddress = new AddressViewModel(newKey, Global); Addresses.Insert(0, newAddress); SelectedAddress = newAddress; _labelSuggestion.Label = ""; }); }); this.WhenAnyValue(x => x.SelectedAddress).Subscribe(async address => { if (Global.UiConfig?.Autocopy is false || address is null) { return; } await address.TryCopyToClipboardAsync(); }); var isCoinListItemSelected = this.WhenAnyValue(x => x.SelectedAddress).Select(coin => coin is { });
public WalletInfoViewModel(Wallet wallet) : base(wallet.WalletName) { Global = Locator.Current.GetService <Global>(); Wallet = wallet; this.ValidateProperty(x => x.Password, ValidatePassword); ClearSensitiveData(true); ToggleSensitiveKeysCommand = ReactiveCommand.Create(() => { if (ShowSensitiveKeys) { ClearSensitiveData(true); } else { var secret = PasswordHelper.GetMasterExtKey(Wallet.KeyManager, Password, out string isCompatibilityPasswordUsed); Password = ""; if (isCompatibilityPasswordUsed != null) { NotificationHelpers.Warning(PasswordHelper.CompatibilityPasswordWarnMessage); } string master = secret.GetWif(Global.Network).ToWif(); string account = secret.Derive(Wallet.KeyManager.AccountKeyPath).GetWif(Global.Network).ToWif(); string masterZ = secret.ToZPrv(Global.Network); string accountZ = secret.Derive(Wallet.KeyManager.AccountKeyPath).ToZPrv(Global.Network); SetSensitiveData(master, account, masterZ, accountZ); } }); ToggleSensitiveKeysCommand.ThrownExceptions .ObserveOn(RxApp.TaskpoolScheduler) .Subscribe(ex => { NotificationHelpers.Error(ex.ToUserFriendlyString()); Logger.LogError(ex); }); }
protected SendControlViewModel(Wallet wallet, string title) : base(title) { Global = Locator.Current.GetService <Global>(); Wallet = wallet; LabelSuggestion = new SuggestLabelViewModel(); _buildTransactionButtonText = DoButtonText; this.ValidateProperty(x => x.Address, ValidateAddress); this.ValidateProperty(x => x.CustomChangeAddress, ValidateCustomChangeAddress); this.ValidateProperty(x => x.Password, ValidatePassword); this.ValidateProperty(x => x.UserFeeText, ValidateUserFeeText); ResetUi(); CoinList = new CoinListViewModel(Wallet, Global.Config, Global.UiConfig, displayCommonOwnershipWarning: true); Observable.FromEventPattern(CoinList, nameof(CoinList.SelectionChanged)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => SetFeesAndTexts()); _minMaxFeeTargetsEqual = this.WhenAnyValue(x => x.MinimumFeeTarget, x => x.MaximumFeeTarget, (x, y) => x == y) .ToProperty(this, x => x.MinMaxFeeTargetsEqual, scheduler: RxApp.MainThreadScheduler); SetFeeTargetLimits(); FeeTarget = Global.UiConfig.FeeTarget; FeeDisplayFormat = (FeeDisplayFormat)(Enum.ToObject(typeof(FeeDisplayFormat), Global.UiConfig.FeeDisplayFormat) ?? FeeDisplayFormat.SatoshiPerByte); SetFeesAndTexts(); this.WhenAnyValue(x => x.AmountText) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(x => { if (Money.TryParse(x.TrimStart('~', ' '), out Money amountBtc)) { SetAmountWatermark(amountBtc); } else { SetAmountWatermark(Money.Zero); } SetFees(); }); AmountKeyUpCommand = ReactiveCommand.Create((KeyEventArgs key) => { if (IsMax) { SetFeesAndTexts(); } else if (BitcoinInput.TryCorrectAmount(AmountText, out var betterAmount)) { AmountText = betterAmount; } }); this.WhenAnyValue(x => x.IsBusy, x => x.IsHardwareBusy) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => BuildTransactionButtonText = IsHardwareBusy ? WaitingForHardwareWalletButtonTextString : IsBusy ? DoingButtonText : DoButtonText); Observable .Merge(this.WhenAnyValue(x => x.FeeTarget).Select(_ => true)) .Merge(this.WhenAnyValue(x => x.IsEstimateAvailable).Select(_ => true)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { IsSliderFeeUsed = IsEstimateAvailable; SetFeesAndTexts(); }); this.WhenAnyValue(x => x.IsSliderFeeUsed) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(enabled => FeeControlOpacity = enabled ? 1 : 0.5); // Give the control the disabled feeling. Real Disable it not a solution as we have to detect if the slider is moved. MaxCommand = ReactiveCommand.Create(() => IsMax = !IsMax, outputScheduler: RxApp.MainThreadScheduler); this.WhenAnyValue(x => x.IsMax) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { if (IsMax) { SetFeesAndTexts(); LabelToolTip = "Spending whole coins does not generate change, thus labeling is unnecessary."; } else { AmountText = "0.0"; LabelToolTip = "Who can link this transaction to you? E.g.: \"Max, BitPay\""; } }); // Triggering the detection of same address values. this.WhenAnyValue(x => x.Address) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => this.RaisePropertyChanged(nameof(CustomChangeAddress))); this.WhenAnyValue(x => x.CustomChangeAddress) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => this.RaisePropertyChanged(nameof(Address))); this.WhenAnyValue(x => x.IsCustomChangeAddressVisible) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { this.RaisePropertyChanged(nameof(Address)); this.RaisePropertyChanged(nameof(CustomChangeAddress)); }); FeeRateCommand = ReactiveCommand.Create(ChangeFeeRateDisplay, outputScheduler: RxApp.MainThreadScheduler); OnAddressPasteCommand = ReactiveCommand.Create((BitcoinUrlBuilder url) => OnAddressPaste(url)); BuildTransactionCommand = ReactiveCommand.CreateFromTask(async() => { try { IsBusy = true; MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.BuildingTransaction); var label = new SmartLabel(LabelSuggestion.Label); LabelSuggestion.Label = label; if (!IsMax && label.IsEmpty) { NotificationHelpers.Warning("Label is required.", ""); return; } var selectedCoinViewModels = CoinList.Coins.Where(cvm => cvm.IsSelected); var selectedCoinReferences = selectedCoinViewModels.Select(cvm => cvm.Model.OutPoint).ToList(); if (!selectedCoinReferences.Any()) { NotificationHelpers.Warning("No coins are selected to spend.", ""); return; } BitcoinAddress address; try { address = BitcoinAddress.Create(Address, Global.Network); } catch (FormatException) { NotificationHelpers.Warning("Invalid address.", ""); return; } var requests = new List <DestinationRequest>(); if (IsCustomChangeAddressVisible && !string.IsNullOrWhiteSpace(CustomChangeAddress)) { try { var customChangeAddress = BitcoinAddress.Create(CustomChangeAddress, Global.Network); if (customChangeAddress == address) { NotificationHelpers.Warning("The active address and the change address cannot be the same.", ""); return; } requests.Add(new DestinationRequest(customChangeAddress, MoneyRequest.CreateChange(subtractFee: true), label)); } catch (FormatException) { NotificationHelpers.Warning("Invalid custom change address.", ""); return; } } MoneyRequest moneyRequest; if (IsMax) { moneyRequest = MoneyRequest.CreateAllRemaining(subtractFee: true); } else { if (!Money.TryParse(AmountText, out Money amount) || amount == Money.Zero) { NotificationHelpers.Warning("Invalid amount."); return; } if (amount == selectedCoinViewModels.Sum(x => x.Amount)) { NotificationHelpers.Warning("Looks like you want to spend whole coins. Try Max button instead.", ""); return; } moneyRequest = MoneyRequest.Create(amount, subtractFee: false); } if (FeeRate is null || FeeRate.SatoshiPerByte < 1) { NotificationHelpers.Warning("Invalid fee rate.", ""); return; } var feeStrategy = FeeStrategy.CreateFromFeeRate(FeeRate); var activeDestinationRequest = new DestinationRequest(address, moneyRequest, label); requests.Add(activeDestinationRequest); var intent = new PaymentIntent(requests); try { MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.DequeuingSelectedCoins); OutPoint[] toDequeue = selectedCoinViewModels.Where(x => x.CoinJoinInProgress).Select(x => x.Model.OutPoint).ToArray(); if (toDequeue is { } && toDequeue.Any()) { await Wallet.ChaumianClient.DequeueCoinsFromMixAsync(toDequeue, DequeueReason.TransactionBuilding); } } catch { NotificationHelpers.Error("Cannot spend mixing coins.", ""); return; } finally { MainWindowViewModel.Instance.StatusBar.TryRemoveStatus(StatusType.DequeuingSelectedCoins); } if (!Wallet.KeyManager.IsWatchOnly) { try { PasswordHelper.GetMasterExtKey(Wallet.KeyManager, Password, out var compatiblityPasswordUsed); // We could use TryPassword but we need the exception. if (compatiblityPasswordUsed is { }) { Password = compatiblityPasswordUsed; // Overwrite the password for BuildTransaction function. NotificationHelpers.Warning(PasswordHelper.CompatibilityPasswordWarnMessage); } } catch (SecurityException ex) { NotificationHelpers.Error(ex.Message, ""); return; } catch (Exception ex) { Logger.LogError(ex); NotificationHelpers.Error(ex.ToUserFriendlyString()); return; } }
protected SendControlViewModel(Wallet wallet, string title) : base(title) { Global = Locator.Current.GetService <Global>(); Wallet = wallet; LabelSuggestion = new SuggestLabelViewModel(); BuildTransactionButtonText = DoButtonText; this.ValidateProperty(x => x.Address, ValidateAddress); this.ValidateProperty(x => x.CustomChangeAddress, ValidateCustomChangeAddress); this.ValidateProperty(x => x.Password, ValidatePassword); this.ValidateProperty(x => x.UserFeeText, ValidateUserFeeText); ResetUi(); SetAmountWatermark(Money.Zero); CoinList = new CoinListViewModel(Wallet, displayCommonOwnershipWarning: true); Observable.FromEventPattern(CoinList, nameof(CoinList.SelectionChanged)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => SetFeesAndTexts()); _minMaxFeeTargetsEqual = this.WhenAnyValue(x => x.MinimumFeeTarget, x => x.MaximumFeeTarget, (x, y) => x == y) .ToProperty(this, x => x.MinMaxFeeTargetsEqual, scheduler: RxApp.MainThreadScheduler); SetFeeTargetLimits(); FeeTarget = Global.UiConfig.FeeTarget; FeeDisplayFormat = (FeeDisplayFormat)(Enum.ToObject(typeof(FeeDisplayFormat), Global.UiConfig.FeeDisplayFormat) ?? FeeDisplayFormat.SatoshiPerByte); SetFeesAndTexts(); this.WhenAnyValue(x => x.AmountText) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(x => { if (Money.TryParse(x.TrimStart('~', ' '), out Money amountBtc)) { SetAmountWatermark(amountBtc); } else { SetAmountWatermark(Money.Zero); } SetFees(); }); AmountKeyUpCommand = ReactiveCommand.Create((KeyEventArgs key) => { var amount = AmountText; if (IsMax) { SetFeesAndTexts(); } else { // Correct amount Regex digitsOnly = new Regex(@"[^\d,.]"); string betterAmount = digitsOnly.Replace(amount, ""); // Make it digits , and . only. betterAmount = betterAmount.Replace(',', '.'); int countBetterAmount = betterAmount.Count(x => x == '.'); if (countBetterAmount > 1) // Do not enable typing two dots. { var index = betterAmount.IndexOf('.', betterAmount.IndexOf('.') + 1); if (index > 0) { betterAmount = betterAmount.Substring(0, index); } } var dotIndex = betterAmount.IndexOf('.'); if (dotIndex != -1 && betterAmount.Length - dotIndex > 8) // Enable max 8 decimals. { betterAmount = betterAmount.Substring(0, dotIndex + 1 + 8); } if (betterAmount != amount) { AmountText = betterAmount; } } }); this.WhenAnyValue(x => x.IsBusy, x => x.IsHardwareBusy) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => BuildTransactionButtonText = IsHardwareBusy ? WaitingForHardwareWalletButtonTextString : IsBusy ? DoingButtonText : DoButtonText); this.WhenAnyValue(x => x.FeeTarget) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { IsSliderFeeUsed = true; SetFeesAndTexts(); }); this.WhenAnyValue(x => x.IsSliderFeeUsed) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(enabled => FeeControlOpacity = enabled ? 1 : 0.5); // Give the control the disabled feeling. Real Disable it not a solution as we have to detect if the slider is moved. MaxCommand = ReactiveCommand.Create(() => IsMax = !IsMax, outputScheduler: RxApp.MainThreadScheduler); this.WhenAnyValue(x => x.IsMax) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { if (IsMax) { SetFeesAndTexts(); LabelToolTip = "Spending whole coins does not generate change, thus labeling is unnecessary."; } else { AmountText = "0.0"; LabelToolTip = "Who can link this transaction to you? E.g.: \"Max, BitPay\""; } }); // Triggering the detection of same address values. this.WhenAnyValue(x => x.Address) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => this.RaisePropertyChanged(nameof(CustomChangeAddress))); this.WhenAnyValue(x => x.CustomChangeAddress) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => this.RaisePropertyChanged(nameof(Address))); this.WhenAnyValue(x => x.IsCustomChangeAddressVisible) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { this.RaisePropertyChanged(nameof(Address)); this.RaisePropertyChanged(nameof(CustomChangeAddress)); }); FeeRateCommand = ReactiveCommand.Create(ChangeFeeRateDisplay, outputScheduler: RxApp.MainThreadScheduler); OnAddressPasteCommand = ReactiveCommand.Create((BitcoinUrlBuilder url) => OnAddressPaste(url)); BuildTransactionCommand = ReactiveCommand.CreateFromTask(async() => { try { IsBusy = true; MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.BuildingTransaction); var label = new SmartLabel(LabelSuggestion.Label); LabelSuggestion.Label = label; if (!IsMax && label.IsEmpty) { NotificationHelpers.Warning("Observers are required.", ""); return; } var selectedCoinViewModels = CoinList.Coins.Where(cvm => cvm.IsSelected); var selectedCoinReferences = selectedCoinViewModels.Select(cvm => cvm.Model.OutPoint).ToList(); if (!selectedCoinReferences.Any()) { NotificationHelpers.Warning("No coins are selected to spend.", ""); return; } BitcoinAddress address; try { address = BitcoinAddress.Create(Address, Global.Network); } catch (FormatException) { NotificationHelpers.Warning("Invalid address.", ""); return; } var requests = new List <DestinationRequest>(); if (IsCustomChangeAddressVisible && !string.IsNullOrWhiteSpace(CustomChangeAddress)) { try { var customChangeAddress = BitcoinAddress.Create(CustomChangeAddress, Global.Network); if (customChangeAddress == address) { NotificationHelpers.Warning("The active address and the change address cannot be the same.", ""); return; } requests.Add(new DestinationRequest(customChangeAddress, MoneyRequest.CreateChange(subtractFee: true), label)); } catch (FormatException) { NotificationHelpers.Warning("Invalid custom change address.", ""); return; } } MoneyRequest moneyRequest; if (IsMax) { moneyRequest = MoneyRequest.CreateAllRemaining(subtractFee: true); } else { if (!Money.TryParse(AmountText, out Money amount) || amount == Money.Zero) { NotificationHelpers.Warning("Invalid amount."); return; } if (amount == selectedCoinViewModels.Sum(x => x.Amount)) { NotificationHelpers.Warning("Looks like you want to spend whole coins. Try Max button instead.", ""); return; } moneyRequest = MoneyRequest.Create(amount, subtractFee: false); } if (FeeRate is null || FeeRate.SatoshiPerByte < 1) { NotificationHelpers.Warning("Invalid fee rate.", ""); return; } var feeStrategy = FeeStrategy.CreateFromFeeRate(FeeRate); var activeDestinationRequest = new DestinationRequest(address, moneyRequest, label); requests.Add(activeDestinationRequest); var intent = new PaymentIntent(requests); try { MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.DequeuingSelectedCoins); OutPoint[] toDequeue = selectedCoinViewModels.Where(x => x.CoinJoinInProgress).Select(x => x.Model.OutPoint).ToArray(); if (toDequeue != null && toDequeue.Any()) { await Wallet.ChaumianClient.DequeueCoinsFromMixAsync(toDequeue, DequeueReason.TransactionBuilding); } } catch { NotificationHelpers.Error("Cannot spend mixing coins.", ""); return; } finally { MainWindowViewModel.Instance.StatusBar.TryRemoveStatus(StatusType.DequeuingSelectedCoins); } if (!Wallet.KeyManager.IsWatchOnly) { try { PasswordHelper.GetMasterExtKey(Wallet.KeyManager, Password, out string compatiblityPasswordUsed); // We could use TryPassword but we need the exception. if (compatiblityPasswordUsed != null) { Password = compatiblityPasswordUsed; // Overwrite the password for BuildTransaction function. NotificationHelpers.Warning(PasswordHelper.CompatibilityPasswordWarnMessage); } } catch (SecurityException ex) { NotificationHelpers.Error(ex.Message, ""); return; } catch (Exception ex) { Logger.LogError(ex); NotificationHelpers.Error(ex.ToUserFriendlyString()); return; } } await BuildTransaction(Password, intent, feeStrategy, allowUnconfirmed: true, allowedInputs: selectedCoinReferences); } catch (InsufficientBalanceException ex) { Money needed = ex.Minimum - ex.Actual; NotificationHelpers.Error($"Not enough coins selected. You need an estimated {needed.ToString(false, true)} BTC more to make this transaction.", ""); } catch (HttpRequestException ex) { NotificationHelpers.Error(ex.ToUserFriendlyString()); Logger.LogError(ex); } catch (Exception ex) { NotificationHelpers.Error(ex.ToUserFriendlyString(), sender: Wallet); Logger.LogError(ex); } finally { MainWindowViewModel.Instance.StatusBar.TryRemoveStatus(StatusType.BuildingTransaction, StatusType.SigningTransaction, StatusType.BroadcastingTransaction); IsBusy = false; } }, this.WhenAny(x => x.IsMax, x => x.AmountText, x => x.Address, x => x.IsBusy, (isMax, amount, address, busy) => (isMax.Value || !string.IsNullOrWhiteSpace(amount.Value)) && !string.IsNullOrWhiteSpace(Address) && !IsBusy) .ObserveOn(RxApp.MainThreadScheduler)); UserFeeTextKeyUpCommand = ReactiveCommand.Create((KeyEventArgs key) => { IsSliderFeeUsed = !IsCustomFee; SetFeesAndTexts(); }); FeeSliderClickedCommand = ReactiveCommand.Create((PointerPressedEventArgs mouse) => IsSliderFeeUsed = true); HighLightFeeSliderCommand = ReactiveCommand.Create((bool entered) => { if (IsSliderFeeUsed) { return; } FeeControlOpacity = entered ? 0.8 : 0.5; }); Observable .Merge(MaxCommand.ThrownExceptions) .Merge(FeeRateCommand.ThrownExceptions) .Merge(OnAddressPasteCommand.ThrownExceptions) .Merge(BuildTransactionCommand.ThrownExceptions) .Merge(UserFeeTextKeyUpCommand.ThrownExceptions) .Merge(FeeSliderClickedCommand.ThrownExceptions) .Merge(HighLightFeeSliderCommand.ThrownExceptions) .Merge(AmountKeyUpCommand.ThrownExceptions) .ObserveOn(RxApp.TaskpoolScheduler) .Subscribe(ex => { NotificationHelpers.Error(ex.ToUserFriendlyString()); Logger.LogError(ex); }); }