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());
            });
        }
Exemple #2
0
        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 { });
        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 { });
        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);
            });
        }