public CoinJoinTabViewModel(WalletViewModel walletViewModel) : base("CoinJoin", walletViewModel) { Password = ""; var onCoinsSetModified = Observable.FromEventPattern(Global.WalletService.Coins, nameof(Global.WalletService.Coins.HashSetChanged)) .ObserveOn(RxApp.MainThreadScheduler); var globalCoins = Global.WalletService.Coins.CreateDerivedCollection(c => new CoinViewModel(c), null, (first, second) => second.Amount.CompareTo(first.Amount), signalReset: onCoinsSetModified, RxApp.MainThreadScheduler); globalCoins.ChangeTrackingEnabled = true; var available = globalCoins.CreateDerivedCollection(c => c, c => c.Confirmed && !c.SpentOrCoinJoinInProgress); var queued = globalCoins.CreateDerivedCollection(c => c, c => c.CoinJoinInProgress); var registrableRound = Global.ChaumianClient.State.GetRegistrableRoundOrDefault(); UpdateRequiredBtcLabel(registrableRound, available, queued); if (registrableRound != default) { CoordinatorFeePercent = registrableRound.State.CoordinatorFeePercent.ToString(); } else { CoordinatorFeePercent = "0.003"; } if (!(registrableRound?.State?.Denomination is null) && registrableRound.State.Denomination != Money.Zero) { AvailableCoinsList = new CoinListViewModel(available, RequiredBTC, PreSelectMaxAnonSetExcludingCondition); }
public SendTabViewModel(WalletViewModel walletViewModel) : base("Send", walletViewModel) { CoinList = new CoinListViewModel(Global.WalletService.Coins); BuildTransactionButtonText = BuildTransactionButtonTextString; this.WhenAnyValue(x => x.Amount).Subscribe(_ => { if (!_ignoreAmountChanges) { IsMax = false; } }); BuildTransactionCommand = ReactiveCommand.Create(async() => { IsBusy = true; try { await Task.Delay(5000); //CoinList.SelectedCoins //IsMax = use all selected coins. // backend stuff here. } catch { } finally { IsBusy = false; } }, this.WhenAny(x => x.IsMax, x => x.Amount, x => x.Address, x => x.IsBusy, (isMax, amount, address, busy) => (isMax.Value || !string.IsNullOrWhiteSpace(amount.Value) && !string.IsNullOrWhiteSpace(Address) && !IsBusy))); MaxCommand = ReactiveCommand.Create(() => { SetMax(); }); this.WhenAnyValue(x => x.IsBusy).Subscribe(busy => { if (busy) { BuildTransactionButtonText = BuildingTransactionButtonTextString; } else { BuildTransactionButtonText = BuildTransactionButtonTextString; } }); }
public CoinJoinTabViewModel(WalletViewModel walletViewModel) : base("CoinJoin", walletViewModel) { Password = ""; var onCoinsSetModified = Observable.FromEventPattern(Global.WalletService.Coins, nameof(Global.WalletService.Coins.HashSetChanged)) .ObserveOn(RxApp.MainThreadScheduler); var globalCoins = Global.WalletService.Coins.CreateDerivedCollection(c => new CoinViewModel(c), null, (first, second) => second.Amount.CompareTo(first.Amount), signalReset: onCoinsSetModified, RxApp.MainThreadScheduler); globalCoins.ChangeTrackingEnabled = true; var available = globalCoins.CreateDerivedCollection(c => c, c => c.Confirmed && !c.SpentOrCoinJoinInProcess); var queued = globalCoins.CreateDerivedCollection(c => c, c => c.CoinJoinInProgress); AvailableCoinsList = new CoinListViewModel(available); QueuedCoinsList = new CoinListViewModel(queued); EnqueueCommand = ReactiveCommand.Create(async() => { var selectedCoins = AvailableCoinsList.Coins.Where(c => c.IsSelected).ToList(); foreach (var coin in selectedCoins) { coin.IsSelected = false; } await Global.ChaumianClient.QueueCoinsToMixAsync(Password, selectedCoins.Select(c => c.Model).ToArray()); }); DequeueCommand = ReactiveCommand.Create(async() => { var selectedCoins = QueuedCoinsList.Coins.Where(c => c.IsSelected).ToList(); foreach (var coin in selectedCoins) { coin.IsSelected = false; } await Global.ChaumianClient.DequeueCoinsFromMixAsync(selectedCoins.Select(c => c.Model).ToArray()); }); }
public CoinJoinTabViewModel(WalletViewModel walletViewModel) : base("CoinJoin", walletViewModel) { Password = ""; var registrableRound = Global.ChaumianClient.State.GetRegistrableRoundOrDefault(); UpdateRequiredBtcLabel(registrableRound); if (registrableRound != default) { CoordinatorFeePercent = registrableRound.State.CoordinatorFeePercent.ToString(); } else { CoordinatorFeePercent = "0.003"; } if (!(registrableRound?.State?.Denomination is null) && registrableRound.State.Denomination != Money.Zero) { CoinsList = new CoinListViewModel(RequiredBTC, PreSelectMaxAnonSetExcludingCondition); }
public CoinJoinTabViewModel(WalletViewModel walletViewModel) : base("CoinJoin", walletViewModel) { Password = ""; TargetPrivacy = ShieldLevelHelper.GetTargetPrivacy(Global.Config.MixUntilAnonymitySet); var registrableRound = Global.ChaumianClient.State.GetRegistrableRoundOrDefault(); UpdateRequiredBtcLabel(registrableRound); if (registrableRound != default) { CoordinatorFeePercent = registrableRound.State.CoordinatorFeePercent.ToString(); } else { CoordinatorFeePercent = "0.003"; } if (!(registrableRound?.State?.Denomination is null) && registrableRound.State.Denomination != Money.Zero) { CoinsList = new CoinListViewModel(); }
public CoinViewModel(CoinListViewModel owner, SmartCoin model) { _model = model; this.WhenAnyValue(x => x.IsSelected).Subscribe(selected => { if (selected) { if (!owner.SelectedCoins.Contains(this)) { owner.SelectedCoins.Add(this); } } else { owner.SelectedCoins.Remove(this); } }); model.WhenAnyValue(x => x.Confirmed).ObserveOn(RxApp.MainThreadScheduler).Subscribe(confirmed => { this.RaisePropertyChanged(nameof(Confirmed)); }); }
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; } }
public SendTabViewModel(WalletViewModel walletViewModel) : base("Send", walletViewModel) { Label = ""; var onCoinsSetModified = Observable.FromEventPattern(Global.WalletService.Coins, nameof(Global.WalletService.Coins.HashSetChanged)) .ObserveOn(RxApp.MainThreadScheduler); var globalCoins = Global.WalletService.Coins.CreateDerivedCollection(c => new CoinViewModel(c), null, (first, second) => second.Amount.CompareTo(first.Amount), signalReset: onCoinsSetModified, RxApp.MainThreadScheduler); globalCoins.ChangeTrackingEnabled = true; var filteredCoins = globalCoins.CreateDerivedCollection(c => c, c => !c.SpentOrCoinJoinInProgress); CoinList = new CoinListViewModel(filteredCoins); BuildTransactionButtonText = BuildTransactionButtonTextString; ResetMax(); this.WhenAnyValue(x => x.Amount).Subscribe(amount => { if (!IgnoreAmountChanges) { IsMax = false; // 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) // Don't enable typing two dots. { var index = betterAmount.IndexOf('.', betterAmount.IndexOf('.') + 1); if (index > 0) { betterAmount = betterAmount.Substring(0, index); } } var dotIndex = betterAmount.IndexOf('.'); if (betterAmount.Length - dotIndex > 8) // Enable max 8 decimals. { betterAmount = betterAmount.Substring(0, dotIndex + 1 + 8); } if (betterAmount != amount) { Dispatcher.UIThread.Post(() => { Amount = betterAmount; }); } } }); BuildTransactionCommand = ReactiveCommand.Create(async() => { IsBusy = true; try { Password = Guard.Correct(Password); if (!IsMax && string.IsNullOrWhiteSpace(Label)) { SetWarningMessage("Label is required."); return; } var selectedCoins = CoinList.Coins.Where(cvm => cvm.IsSelected).Select(cvm => new TxoRef(cvm.Model.TransactionId, cvm.Model.Index)).ToList(); if (!selectedCoins.Any()) { SetWarningMessage("No coins are selected to spend."); return; } BitcoinAddress address; try { address = BitcoinAddress.Create(Address.Trim(), Global.Network); } catch (FormatException) { SetWarningMessage("Invalid address."); return; } var script = address.ScriptPubKey; var amount = Money.Zero; if (!IsMax) { amount = Money.Parse(Amount); if (amount == Money.Zero) { SetWarningMessage($"Invalid amount."); return; } } var label = Label.Trim(',', ' ').Trim(); var operation = new WalletService.Operation(script, amount, label); var result = await Task.Run(async() => await Global.WalletService.BuildTransactionAsync(Password, new[] { operation }, Fee, allowUnconfirmed: true, allowedInputs: selectedCoins)); await Task.Run(async() => await Global.WalletService.SendTransactionAsync(result.Transaction)); ResetMax(); Address = ""; Label = ""; Password = ""; SetSuccessMessage("Transaction is successfully sent!"); } catch (InsufficientBalanceException ex) { Money needed = ex.Minimum - ex.Actual; SetWarningMessage($"Not enough coins selected. You need an estimated {needed.ToString(false, true)} BTC more to make this transaction."); } catch (Exception ex) { SetWarningMessage(ex.ToTypeMessageString()); } finally { IsBusy = false; } }, this.WhenAny(x => x.IsMax, x => x.Amount, x => x.Address, x => x.IsBusy, (isMax, amount, address, busy) => ((isMax.Value || !string.IsNullOrWhiteSpace(amount.Value)) && !string.IsNullOrWhiteSpace(Address) && !IsBusy))); MaxCommand = ReactiveCommand.Create(() => { SetMax(); }); this.WhenAnyValue(x => x.IsBusy).Subscribe(busy => { if (busy) { BuildTransactionButtonText = BuildingTransactionButtonTextString; } else { BuildTransactionButtonText = BuildTransactionButtonTextString; } }); this.WhenAnyValue(x => x.Password).Subscribe(x => { if (x.NotNullAndNotEmpty()) { char lastChar = x.Last(); if (lastChar == '\r' || lastChar == '\n') // If the last character is cr or lf then act like it'd be a sign to do the job. { Password = x.TrimEnd('\r', '\n'); } } }); this.WhenAnyValue(x => x.Label).Subscribe(x => UpdateSuggestions(x)); this.WhenAnyValue(x => x.CaretIndex).Subscribe(_ => { if (Label == null) { return; } if (CaretIndex != Label.Length) { CaretIndex = Label.Length; } }); _suggestions = new ObservableCollection <SuggestionViewModel>(); }
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); }); }
public CoinJoinTabViewModel(Wallet wallet) : base("CoinJoin") { Global = Locator.Current.GetService <Global>(); Wallet = wallet; this.ValidateProperty(x => x.Password, ValidatePassword); Password = ""; TimeLeftTillRoundTimeout = TimeSpan.Zero; CoinsList = new CoinListViewModel(Wallet, canDequeueCoins: true); Observable .FromEventPattern <SmartCoin>(CoinsList, nameof(CoinsList.DequeueCoinsPressed)) .Subscribe(async x => await DoDequeueAsync(x.EventArgs)); AmountQueued = Money.Zero; // Global.ChaumianClient.State.SumAllQueuedCoinAmounts(); EnqueueCommand = ReactiveCommand.CreateFromTask(async() => await DoEnqueueAsync(CoinsList.Coins.Where(c => c.IsSelected).Select(c => c.Model))); DequeueCommand = ReactiveCommand.CreateFromTask(async() => await DoDequeueAsync(CoinsList.Coins.Where(c => c.IsSelected).Select(x => x.Model))); PrivacySomeCommand = ReactiveCommand.Create(() => TargetPrivacy = TargetPrivacy.Some); PrivacyFineCommand = ReactiveCommand.Create(() => TargetPrivacy = TargetPrivacy.Fine); PrivacyStrongCommand = ReactiveCommand.Create(() => TargetPrivacy = TargetPrivacy.Strong); TargetButtonCommand = ReactiveCommand.Create(() => { switch (TargetPrivacy) { case TargetPrivacy.None: TargetPrivacy = TargetPrivacy.Some; break; case TargetPrivacy.Some: TargetPrivacy = TargetPrivacy.Fine; break; case TargetPrivacy.Fine: TargetPrivacy = TargetPrivacy.Strong; break; case TargetPrivacy.Strong: TargetPrivacy = TargetPrivacy.Some; break; } Global.Config.MixUntilAnonymitySet = CoinJoinUntilAnonymitySet; Global.Config.ToFile(); }); this.WhenAnyValue(x => x.IsEnqueueBusy) .Select(x => x ? EnqueuingButtonTextString : EnqueueButtonTextString) .Subscribe(text => EnqueueButtonText = text); this.WhenAnyValue(x => x.IsDequeueBusy) .Select(x => x ? DequeuingButtonTextString : DequeueButtonTextString) .Subscribe(text => DequeueButtonText = text); this.WhenAnyValue(x => x.TargetPrivacy) .Subscribe(target => CoinJoinUntilAnonymitySet = Global.Config.GetTargetLevel(target)); this.WhenAnyValue(x => x.RoundTimesout) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(x => { TimeSpan left = x - DateTimeOffset.UtcNow; TimeLeftTillRoundTimeout = left > TimeSpan.Zero ? left : TimeSpan.Zero; // Make sure cannot be less than zero. }); Observable .Merge(EnqueueCommand.ThrownExceptions) .Merge(DequeueCommand.ThrownExceptions) .Merge(PrivacySomeCommand.ThrownExceptions) .Merge(PrivacyFineCommand.ThrownExceptions) .Merge(PrivacyStrongCommand.ThrownExceptions) .Merge(TargetButtonCommand.ThrownExceptions) .ObserveOn(RxApp.TaskpoolScheduler) .Subscribe(ex => Logger.LogError(ex)); }
public CoinViewModel(CoinListViewModel owner, SmartCoin model) { Model = model; _owner = owner; DisposablesLock = new object(); }
public SendTabViewModel(WalletViewModel walletViewModel) : base("Send", walletViewModel) { var onCoinsSetModified = Observable.FromEventPattern(Global.WalletService.Coins, nameof(Global.WalletService.Coins.HashSetChanged)) .ObserveOn(RxApp.MainThreadScheduler); var globalCoins = Global.WalletService.Coins.CreateDerivedCollection(c => new CoinViewModel(c), null, (first, second) => second.Amount.CompareTo(first.Amount), signalReset: onCoinsSetModified, RxApp.MainThreadScheduler); globalCoins.ChangeTrackingEnabled = true; var filteredCoins = globalCoins.CreateDerivedCollection(c => c, c => !c.SpentOrCoinJoinInProgress); CoinList = new CoinListViewModel(filteredCoins); BuildTransactionButtonText = BuildTransactionButtonTextString; ResetMax(); this.WhenAnyValue(x => x.Amount).Subscribe(amount => { if (!IgnoreAmountChanges) { IsMax = false; // Correct amount Regex digitsOnly = new Regex(@"[^\d,.]"); string betterAmount = digitsOnly.Replace(amount, ""); // Make it digits , and . only. betterAmount = betterAmount.Replace(',', '.'); if (1 < betterAmount.Count(x => x == '.')) // Don't enable typing two dots. { var index = betterAmount.IndexOf('.', betterAmount.IndexOf('.') + 1); if (index > 0) { betterAmount = betterAmount.Substring(0, index); } } var dotIndex = betterAmount.IndexOf('.'); if (betterAmount.Length - dotIndex > 8) // Enable max 8 decimals. { betterAmount = betterAmount.Substring(0, dotIndex + 1 + 8); } if (betterAmount != amount) { Dispatcher.UIThread.Post(() => { Amount = betterAmount; }); } } }); BuildTransactionCommand = ReactiveCommand.Create(async() => { IsBusy = true; try { if (string.IsNullOrWhiteSpace(Label)) { throw new InvalidOperationException("Label is required."); } var selectedCoins = CoinList.Coins.Where(cvm => cvm.IsSelected).Select(cvm => new TxoRef(cvm.Model.TransactionId, cvm.Model.Index)).ToList(); if (!selectedCoins.Any()) { throw new InvalidOperationException("No coins are selected to spend."); } var address = BitcoinAddress.Create(Address.Trim(), Global.Network); var script = address.ScriptPubKey; var amount = Money.Zero; if (!IsMax) { amount = Money.Parse(Amount); if (amount == Money.Zero) { throw new FormatException($"Invalid {nameof(Amount)}"); } } var operation = new WalletService.Operation(script, amount, Label); var result = await Task.Run(async() => await Global.WalletService.BuildTransactionAsync(Password, new[] { operation }, Fee, allowUnconfirmed: true, allowedInputs: selectedCoins)); await Task.Run(async() => await Global.WalletService.SendTransactionAsync(result.Transaction)); ResetMax(); Address = ""; Label = ""; Password = ""; SuccessMessage = "Transaction is successfully sent!"; WarningMessage = ""; } catch (Exception ex) { SuccessMessage = ""; WarningMessage = ex.ToTypeMessageString(); } finally { IsBusy = false; } }, this.WhenAny(x => x.IsMax, x => x.Amount, x => x.Address, x => x.IsBusy, (isMax, amount, address, busy) => ((isMax.Value || !string.IsNullOrWhiteSpace(amount.Value)) && !string.IsNullOrWhiteSpace(Address) && !IsBusy))); MaxCommand = ReactiveCommand.Create(() => { SetMax(); }); this.WhenAnyValue(x => x.IsBusy).Subscribe(busy => { if (busy) { BuildTransactionButtonText = BuildingTransactionButtonTextString; } else { BuildTransactionButtonText = BuildTransactionButtonTextString; } }); }
public CoinViewModel(CoinListViewModel owner, SmartCoin model) { Global = Locator.Current.GetService <Global>(); Model = model; Owner = owner; InCoinJoinContainer = owner.CoinListContainerType == CoinListContainerType.CoinJoinTabViewModel; RefreshSmartCoinStatus(); Disposables = new CompositeDisposable(); _coinJoinInProgress = Model .WhenAnyValue(x => x.CoinJoinInProgress) .ToProperty(this, x => x.CoinJoinInProgress, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); _unspent = Model .WhenAnyValue(x => x.Unspent) .ToProperty(this, x => x.Unspent, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); _confirmed = Model .WhenAnyValue(x => x.Confirmed) .ToProperty(this, x => x.Confirmed, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); _cluster = Model .WhenAnyValue(x => x.Clusters, x => x.Clusters.Labels) .Select(x => x.Item2.ToString()) .ToProperty(this, x => x.Clusters, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); _unavailable = Model .WhenAnyValue(x => x.Unavailable) .ToProperty(this, x => x.Unavailable, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); this.WhenAnyValue(x => x.Status) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => this.RaisePropertyChanged(nameof(ToolTip))); Observable .Merge(Model.WhenAnyValue(x => x.IsBanned, x => x.SpentAccordingToBackend, x => x.Confirmed, x => x.CoinJoinInProgress).Select(_ => Unit.Default)) .Merge(Observable.FromEventPattern(Global.ChaumianClient, nameof(Global.ChaumianClient.StateUpdated)).Select(_ => Unit.Default)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => RefreshSmartCoinStatus()) .DisposeWith(Disposables); Global.BitcoinStore.SmartHeaderChain .WhenAnyValue(x => x.TipHeight).Select(_ => Unit.Default) .Merge(Model.WhenAnyValue(x => x.Height).Select(_ => Unit.Default)) .Throttle(TimeSpan.FromSeconds(0.1)) // DO NOT TAKE THIS THROTTLE OUT, OTHERWISE SYNCING WITH COINS IN THE WALLET WILL STACKOVERFLOW! .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => this.RaisePropertyChanged(nameof(Confirmations))) .DisposeWith(Disposables); Global.UiConfig .WhenAnyValue(x => x.LurkingWifeMode) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { this.RaisePropertyChanged(nameof(AmountBtc)); this.RaisePropertyChanged(nameof(Clusters)); }).DisposeWith(Disposables); DequeueCoin = ReactiveCommand.Create(() => Owner.PressDequeue(Model), this.WhenAnyValue(x => x.CoinJoinInProgress)); _expandMenuCaption = this .WhenAnyValue(x => x.IsExpanded) .Select(x => (x ? "Hide " : "Show ") + "Details") .ObserveOn(RxApp.MainThreadScheduler) .ToProperty(this, x => x.ExpandMenuCaption) .DisposeWith(Disposables); ToggleDetails = ReactiveCommand.Create(() => IsExpanded = !IsExpanded); ToggleDetails.ThrownExceptions .ObserveOn(RxApp.TaskpoolScheduler) .Subscribe(ex => { NotificationHelpers.Error(ex.ToTypeMessageString()); Logging.Logger.LogWarning(ex); }); DequeueCoin.ThrownExceptions .ObserveOn(RxApp.TaskpoolScheduler) .Subscribe(ex => Logging.Logger.LogError(ex)); // Don't notify about it. Dequeue failure (and success) is notified by other mechanism. }
public CoinJoinTabViewModel(WalletViewModel walletViewModel) : base("CoinJoin", walletViewModel) { Password = ""; TimeLeftTillRoundTimeout = TimeSpan.Zero; CoinsList = new CoinListViewModel(Global, CoinListContainerType.CoinJoinTabViewModel); Observable.FromEventPattern(CoinsList, nameof(CoinsList.DequeueCoinsPressed)).Subscribe(_ => OnCoinsListDequeueCoinsPressedAsync()); AmountQueued = Money.Zero; // Global.ChaumianClient.State.SumAllQueuedCoinAmounts(); EnqueueCommand = ReactiveCommand.CreateFromTask(async() => await DoEnqueueAsync(CoinsList.Coins.Where(c => c.IsSelected))); DequeueCommand = ReactiveCommand.CreateFromTask(async() => await DoDequeueAsync(CoinsList.Coins.Where(c => c.IsSelected))); PrivacySomeCommand = ReactiveCommand.Create(() => TargetPrivacy = TargetPrivacy.Some); PrivacyFineCommand = ReactiveCommand.Create(() => TargetPrivacy = TargetPrivacy.Fine); PrivacyStrongCommand = ReactiveCommand.Create(() => TargetPrivacy = TargetPrivacy.Strong); TargetButtonCommand = ReactiveCommand.CreateFromTask(async() => { switch (TargetPrivacy) { case TargetPrivacy.None: TargetPrivacy = TargetPrivacy.Some; break; case TargetPrivacy.Some: TargetPrivacy = TargetPrivacy.Fine; break; case TargetPrivacy.Fine: TargetPrivacy = TargetPrivacy.Strong; break; case TargetPrivacy.Strong: TargetPrivacy = TargetPrivacy.Some; break; } Global.Config.MixUntilAnonymitySet = CoinJoinUntilAnonymitySet; await Global.Config.ToFileAsync(); }); this.WhenAnyValue(x => x.Password).Subscribe(async x => { try { if (x.NotNullAndNotEmpty()) { char lastChar = x.Last(); if (lastChar == '\r' || lastChar == '\n') // If the last character is cr or lf then act like it'd be a sign to do the job. { Password = x.TrimEnd('\r', '\n'); await DoEnqueueAsync(CoinsList.Coins.Where(c => c.IsSelected)); } } } catch (Exception ex) { Logger.LogTrace(ex); } }); this.WhenAnyValue(x => x.IsEnqueueBusy) .Select(x => x ? EnqueuingButtonTextString : EnqueueButtonTextString) .Subscribe(text => EnqueueButtonText = text); this.WhenAnyValue(x => x.IsDequeueBusy) .Select(x => x ? DequeuingButtonTextString : DequeueButtonTextString) .Subscribe(text => DequeueButtonText = text); this.WhenAnyValue(x => x.TargetPrivacy).Subscribe(target => { CoinJoinUntilAnonymitySet = Global.Config.GetTargetLevel(target); }); this.WhenAnyValue(x => x.RoundTimesout) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(x => { TimeSpan left = x - DateTimeOffset.UtcNow; TimeLeftTillRoundTimeout = left > TimeSpan.Zero ? left : TimeSpan.Zero; // Make sure cannot be less than zero. }); }
public SendTabViewModel(WalletViewModel walletViewModel) : base("Send", walletViewModel) { var onCoinsSetModified = Observable.FromEventPattern(Global.WalletService.Coins, nameof(Global.WalletService.Coins.HashSetChanged)) .ObserveOn(RxApp.MainThreadScheduler); var globalCoins = Global.WalletService.Coins.CreateDerivedCollection(c => new CoinViewModel(c), null, (first, second) => second.Amount.CompareTo(first.Amount), signalReset: onCoinsSetModified, RxApp.MainThreadScheduler); globalCoins.ChangeTrackingEnabled = true; var filteredCoins = globalCoins.CreateDerivedCollection(c => c, c => !c.SpentOrCoinJoinInProcess); CoinList = new CoinListViewModel(filteredCoins); BuildTransactionButtonText = BuildTransactionButtonTextString; this.WhenAnyValue(x => x.Amount).Subscribe(_ => { if (!_ignoreAmountChanges) { IsMax = false; } }); BuildTransactionCommand = ReactiveCommand.Create(async () => { IsBusy = true; try { if (string.IsNullOrWhiteSpace(Label)) { throw new InvalidOperationException("Label is required."); } var selectedCoins = CoinList.Coins.Where(cvm => cvm.IsSelected).Select(cvm => new TxoRef(cvm.Model.TransactionId, cvm.Model.Index)).ToList(); if (!selectedCoins.Any()) { throw new InvalidOperationException("No coins are selected to spend."); } var address = BitcoinAddress.Create(Address, Global.Network); var script = address.ScriptPubKey; var amount = IsMax ? Money.Zero : Money.Parse(Amount); var operation = new WalletService.Operation(script, amount, Label); var result = await Task.Run(async () => await Global.WalletService.BuildTransactionAsync(Password, new[] { operation }, Fee, allowUnconfirmed: true, allowedInputs: selectedCoins)); await Task.Run(async () => await Global.WalletService.SendTransactionAsync(result.Transaction)); SuccessMessage = "Transaction is successfully sent!"; WarningMessage = ""; } catch (Exception ex) { SuccessMessage = ""; WarningMessage = ex.ToTypeMessageString(); } finally { IsBusy = false; } }, this.WhenAny(x => x.IsMax, x => x.Amount, x => x.Address, x => x.IsBusy, (isMax, amount, address, busy) => ((isMax.Value || !string.IsNullOrWhiteSpace(amount.Value)) && !string.IsNullOrWhiteSpace(Address) && !IsBusy))); MaxCommand = ReactiveCommand.Create(() => { SetMax(); }); this.WhenAnyValue(x => x.IsBusy).Subscribe(busy => { if (busy) { BuildTransactionButtonText = BuildingTransactionButtonTextString; } else { BuildTransactionButtonText = BuildTransactionButtonTextString; } }); }
public CoinJoinTabViewModel(Wallet wallet) : base("CoinJoin") { Global = Locator.Current.GetService <Global>(); Wallet = wallet; this.ValidateProperty(x => x.Password, ValidatePassword); _password = ""; TimeLeftTillRoundTimeout = TimeSpan.Zero; CoinsList = new CoinListViewModel(Wallet, Global.Config, Global.UiConfig, canDequeueCoins: true); Observable .FromEventPattern <SmartCoin>(CoinsList, nameof(CoinsList.DequeueCoinsPressed)) .Subscribe(async x => await DoDequeueAsync(x.EventArgs)); _amountQueued = Money.Zero; EnqueueCommand = ReactiveCommand.CreateFromTask(async() => await DoEnqueueAsync(CoinsList.Coins.Where(c => c.IsSelected).Select(c => c.Model))); DequeueCommand = ReactiveCommand.CreateFromTask(async() => await DoDequeueAsync(CoinsList.Coins.Where(c => c.IsSelected).Select(x => x.Model))); PrivacySomeCommand = ReactiveCommand.Create(() => CoinJoinUntilAnonymitySet = MixUntilAnonymitySet.PrivacyLevelSome.ToString()); PrivacyFineCommand = ReactiveCommand.Create(() => CoinJoinUntilAnonymitySet = MixUntilAnonymitySet.PrivacyLevelFine.ToString()); PrivacyStrongCommand = ReactiveCommand.Create(() => CoinJoinUntilAnonymitySet = MixUntilAnonymitySet.PrivacyLevelStrong.ToString()); TargetButtonCommand = ReactiveCommand.Create(() => { switch (CoinJoinUntilAnonymitySet) { case nameof(MixUntilAnonymitySet.PrivacyLevelSome): CoinJoinUntilAnonymitySet = MixUntilAnonymitySet.PrivacyLevelFine.ToString(); break; case nameof(MixUntilAnonymitySet.PrivacyLevelFine): CoinJoinUntilAnonymitySet = MixUntilAnonymitySet.PrivacyLevelStrong.ToString(); break; case nameof(MixUntilAnonymitySet.PrivacyLevelStrong): CoinJoinUntilAnonymitySet = MixUntilAnonymitySet.PrivacyLevelSome.ToString(); break; } Global.Config.MixUntilAnonymitySet = CoinJoinUntilAnonymitySet; // Config.json can be different than Global.Config. Only change the MixUntilAnonymitySet in the file. var config = new Config(Global.Config.FilePath); config.LoadOrCreateDefaultFile(); config.MixUntilAnonymitySet = CoinJoinUntilAnonymitySet; config.ToFile(); }); this.WhenAnyValue(x => x.IsEnqueueBusy) .Select(x => x ? EnqueuingButtonTextString : EnqueueButtonTextString) .Subscribe(text => EnqueueButtonText = text); this.WhenAnyValue(x => x.IsDequeueBusy) .Select(x => x ? DequeuingButtonTextString : DequeueButtonTextString) .Subscribe(text => DequeueButtonText = text); this.WhenAnyValue(x => x.RoundTimesout) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(x => { TimeSpan left = x - DateTimeOffset.UtcNow; TimeLeftTillRoundTimeout = left > TimeSpan.Zero ? left : TimeSpan.Zero; // Make sure cannot be less than zero. }); Observable .Merge(EnqueueCommand.ThrownExceptions) .Merge(DequeueCommand.ThrownExceptions) .Merge(PrivacySomeCommand.ThrownExceptions) .Merge(PrivacyFineCommand.ThrownExceptions) .Merge(PrivacyStrongCommand.ThrownExceptions) .Merge(TargetButtonCommand.ThrownExceptions) .ObserveOn(RxApp.TaskpoolScheduler) .Subscribe(ex => Logger.LogError(ex)); }
public CoinJoinTabViewModel(WalletViewModel walletViewModel) : base("CoinJoin", walletViewModel) { Password = ""; TargetPrivacy = GetTargetPrivacy(Global.Config.MixUntilAnonymitySet); var registrableRound = Global.ChaumianClient.State.GetRegistrableRoundOrDefault(); UpdateRequiredBtcLabel(registrableRound); CoordinatorFeePercent = registrableRound?.State?.CoordinatorFeePercent.ToString() ?? "0.003"; CoinsList = new CoinListViewModel().DisposeWith(Disposables); AmountQueued = Money.Zero; // Global.ChaumianClient.State.SumAllQueuedCoinAmounts(); Global.ChaumianClient.CoinQueued += ChaumianClient_CoinQueued; Global.ChaumianClient.CoinDequeued += ChaumianClient_CoinDequeued; CcjClientRound mostAdvancedRound = Global.ChaumianClient?.State?.GetMostAdvancedRoundOrDefault(); if (mostAdvancedRound != default) { RoundId = mostAdvancedRound.State.RoundId; SuccessfulRoundCount = mostAdvancedRound.State.SuccessfulRoundCount; Phase = mostAdvancedRound.State.Phase; PeersRegistered = mostAdvancedRound.State.RegisteredPeerCount; PeersNeeded = mostAdvancedRound.State.RequiredPeerCount; } else { RoundId = -1; SuccessfulRoundCount = -1; Phase = CcjRoundPhase.InputRegistration; PeersRegistered = 0; PeersNeeded = 100; } Global.ChaumianClient.StateUpdated += ChaumianClient_StateUpdated; EnqueueCommand = ReactiveCommand.Create(async() => { await DoEnqueueAsync(CoinsList.Coins.Where(c => c.IsSelected)); }).DisposeWith(Disposables); DequeueCommand = ReactiveCommand.Create(async() => { await DoDequeueAsync(CoinsList.Coins.Where(c => c.IsSelected)); }).DisposeWith(Disposables); PrivacySomeCommand = ReactiveCommand.Create(() => { TargetPrivacy = TargetPrivacy.Some; }).DisposeWith(Disposables); PrivacyFineCommand = ReactiveCommand.Create(() => { TargetPrivacy = TargetPrivacy.Fine; }).DisposeWith(Disposables); PrivacyStrongCommand = ReactiveCommand.Create(() => { TargetPrivacy = TargetPrivacy.Strong; }).DisposeWith(Disposables); TargetButtonCommand = ReactiveCommand.Create(async() => { switch (TargetPrivacy) { case TargetPrivacy.None: TargetPrivacy = TargetPrivacy.Some; break; case TargetPrivacy.Some: TargetPrivacy = TargetPrivacy.Fine; break; case TargetPrivacy.Fine: TargetPrivacy = TargetPrivacy.Strong; break; case TargetPrivacy.Strong: TargetPrivacy = TargetPrivacy.Some; break; } Global.Config.MixUntilAnonymitySet = CoinJoinUntilAnonimitySet; await Global.Config.ToFileAsync(); }).DisposeWith(Disposables); this.WhenAnyValue(x => x.Password).Subscribe(async x => { try { if (x.NotNullAndNotEmpty()) { char lastChar = x.Last(); if (lastChar == '\r' || lastChar == '\n') // If the last character is cr or lf then act like it'd be a sign to do the job. { Password = x.TrimEnd('\r', '\n'); await DoEnqueueAsync(CoinsList.Coins.Where(c => c.IsSelected)); } } } catch (Exception ex) { Logger.LogTrace(ex); } }).DisposeWith(Disposables); this.WhenAnyValue(x => x.IsEnqueueBusy).Subscribe(busy => { if (busy) { EnqueueButtonText = EnqueuingButtonTextString; } else { EnqueueButtonText = EnqueueButtonTextString; } }).DisposeWith(Disposables); this.WhenAnyValue(x => x.IsDequeueBusy).Subscribe(busy => { if (busy) { DequeueButtonText = DequeuingButtonTextString; } else { DequeueButtonText = DequeueButtonTextString; } }).DisposeWith(Disposables); this.WhenAnyValue(x => x.TargetPrivacy).Subscribe(target => { CoinJoinUntilAnonimitySet = GetTargetLevel(target); }).DisposeWith(Disposables); }
public CoinJoinTabViewModel(WalletViewModel walletViewModel) : base("CoinJoin", walletViewModel) { Password = ""; var onCoinsSetModified = Observable.FromEventPattern(Global.WalletService.Coins, nameof(Global.WalletService.Coins.HashSetChanged)) .ObserveOn(RxApp.MainThreadScheduler); var globalCoins = Global.WalletService.Coins.CreateDerivedCollection(c => new CoinViewModel(c), null, (first, second) => second.Amount.CompareTo(first.Amount), signalReset: onCoinsSetModified, RxApp.MainThreadScheduler); globalCoins.ChangeTrackingEnabled = true; var available = globalCoins.CreateDerivedCollection(c => c, c => c.Confirmed && !c.SpentOrCoinJoinInProcess); var queued = globalCoins.CreateDerivedCollection(c => c, c => c.CoinJoinInProgress); AvailableCoinsList = new CoinListViewModel(available); QueuedCoinsList = new CoinListViewModel(queued); AmountQueued = QueuedCoinsList.Coins.Sum(x => x.Amount); QueuedCoinsList.Coins.CollectionChanged += Coins_CollectionChanged; var registrableRound = Global.ChaumianClient.State.GetRegistrableRoundOrDefault(); if (registrableRound != default) { CoordinatorFeePercent = registrableRound.State.CoordinatorFeePercent.ToString(); RequiredBTC = registrableRound.State.Denomination + registrableRound.State.Denomination.Percentange(0.3m) + registrableRound.State.FeePerInputs * registrableRound.State.MaximumInputCountPerPeer + registrableRound.State.FeePerOutputs * 2; } else { CoordinatorFeePercent = "0.3"; RequiredBTC = Money.Zero; } var mostAdvancedRound = Global.ChaumianClient.State.GetMostAdvancedRoundOrDefault(); if (mostAdvancedRound != default) { RoundId = mostAdvancedRound.State.RoundId; Phase = mostAdvancedRound.State.Phase; PeersRegistered = mostAdvancedRound.State.RegisteredPeerCount; PeersNeeded = mostAdvancedRound.State.RequiredPeerCount; } else { RoundId = -1; Phase = CcjRoundPhase.InputRegistration; PeersRegistered = 0; PeersNeeded = 100; } Global.ChaumianClient.StateUpdated += ChaumianClient_StateUpdated; EnqueueCommand = ReactiveCommand.Create(async() => { var increasePeersInAdvance = false; if (RequiredBTC - AmountQueued > Money.Zero) { increasePeersInAdvance = true; } var selectedCoins = AvailableCoinsList.Coins.Where(c => c.IsSelected).ToList(); if (!selectedCoins.Any()) { WarningMessageEnqueue = "No coins are selected to enqueue."; return; } WarningMessageEnqueue = string.Empty; try { await Global.ChaumianClient.QueueCoinsToMixAsync(Password, selectedCoins.Select(c => c.Model).ToArray()); } catch (Exception ex) { Logger.LogWarning <CoinJoinTabViewModel>(ex); WarningMessageEnqueue = ex.ToTypeMessageString(); if (ex is AggregateException aggex) { foreach (var iex in aggex.InnerExceptions) { WarningMessageEnqueue += Environment.NewLine + iex.ToTypeMessageString(); } } return; } WarningMessageEnqueue = string.Empty; foreach (var coin in selectedCoins) { coin.IsSelected = false; } if (RequiredBTC - AmountQueued <= Money.Zero && increasePeersInAdvance && PeersRegistered < PeersNeeded) // The status response will come a few seconds later, but here we can already guess the peers are increased. { PeersRegistered++; } }); DequeueCommand = ReactiveCommand.Create(async() => { var selectedCoins = QueuedCoinsList.Coins.Where(c => c.IsSelected).ToList(); foreach (var coin in selectedCoins) { coin.IsSelected = false; } try { await Global.ChaumianClient.DequeueCoinsFromMixAsync(selectedCoins.Select(c => c.Model).ToArray()); } catch (Exception ex) { Logger.LogWarning <CoinJoinTabViewModel>(ex); WarningMessageDequeue = ex.ToTypeMessageString(); if (ex is AggregateException aggex) { foreach (var iex in aggex.InnerExceptions) { WarningMessageDequeue += Environment.NewLine + iex.ToTypeMessageString(); } } return; } WarningMessageDequeue = string.Empty; if (RequiredBTC - AmountQueued > Money.Zero && PeersRegistered > 0) // The status response will come a few seconds later, but here we can already guess the peers are decreased. { PeersRegistered--; } }); }
public CoinViewModel(CoinListViewModel owner, SmartCoin model) { Model = model; _owner = owner; Disposables = new CompositeDisposable(); _coinJoinInProgress = Model .WhenAnyValue(x => x.CoinJoinInProgress) .ToProperty(this, x => x.CoinJoinInProgress, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); _unspent = Model .WhenAnyValue(x => x.Unspent) .ToProperty(this, x => x.Unspent, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); _confirmed = Model .WhenAnyValue(x => x.Confirmed) .ToProperty(this, x => x.Confirmed, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); _unavailable = Model .WhenAnyValue(x => x.Unavailable) .ToProperty(this, x => x.Unavailable, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); this.WhenAnyValue(x => x.Status) .Subscribe(_ => this.RaisePropertyChanged(nameof(ToolTip))); this.WhenAnyValue(x => x.Confirmed, x => x.CoinJoinInProgress, x => x.Confirmations) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => RefreshSmartCoinStatus()); this.WhenAnyValue(x => x.IsSelected) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => _owner.OnCoinIsSelectedChanged(this)); this.WhenAnyValue(x => x.Status) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => _owner.OnCoinStatusChanged()); this.WhenAnyValue(x => x.Unspent) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => _owner.OnCoinUnspentChanged(this)); Model.WhenAnyValue(x => x.IsBanned, x => x.SpentAccordingToBackend) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => RefreshSmartCoinStatus()) .DisposeWith(Disposables); Observable .FromEventPattern(Global.ChaumianClient, nameof(Global.ChaumianClient.StateUpdated)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => RefreshSmartCoinStatus()) .DisposeWith(Disposables); Global.BitcoinStore.HashChain .WhenAnyValue(x => x.TipHeight) .Throttle(TimeSpan.FromMilliseconds(100)) .Select(x => new Height(x)) .Merge(Model.WhenAnyValue(x => x.Height)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => this.RaisePropertyChanged(nameof(Confirmations))) .DisposeWith(Disposables); Global.UiConfig .WhenAnyValue(x => x.LurkingWifeMode) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { this.RaisePropertyChanged(nameof(AmountBtc)); this.RaisePropertyChanged(nameof(Clusters)); }).DisposeWith(Disposables); }
public CoinJoinTabViewModel(WalletViewModel walletViewModel) : base("CoinJoin", walletViewModel) { Password = ""; var onCoinsSetModified = Observable.FromEventPattern(Global.WalletService.Coins, nameof(Global.WalletService.Coins.HashSetChanged)) .ObserveOn(RxApp.MainThreadScheduler); var globalCoins = Global.WalletService.Coins.CreateDerivedCollection(c => new CoinViewModel(c), null, (first, second) => second.Amount.CompareTo(first.Amount), signalReset: onCoinsSetModified, RxApp.MainThreadScheduler); globalCoins.ChangeTrackingEnabled = true; var available = globalCoins.CreateDerivedCollection(c => c, c => c.Confirmed && !c.SpentOrCoinJoinInProgress); var queued = globalCoins.CreateDerivedCollection(c => c, c => c.CoinJoinInProgress); AvailableCoinsList = new CoinListViewModel(available); QueuedCoinsList = new CoinListViewModel(queued); AmountQueued = Global.ChaumianClient.State.SumAllQueuedCoinAmounts(); Global.ChaumianClient.CoinQueued += ChaumianClient_CoinQueued; Global.ChaumianClient.CoinDequeued += ChaumianClient_CoinDequeued; var registrableRound = Global.ChaumianClient.State.GetRegistrableRoundOrDefault(); if (registrableRound != default) { CoordinatorFeePercent = registrableRound.State.CoordinatorFeePercent.ToString(); RequiredBTC = registrableRound.State.CalculateRequiredAmount(Global.ChaumianClient.State.GetAllQueuedCoinAmounts().ToArray()); } else { CoordinatorFeePercent = "0.003"; RequiredBTC = Money.Zero; } var mostAdvancedRound = Global.ChaumianClient.State.GetMostAdvancedRoundOrDefault(); if (mostAdvancedRound != default) { RoundId = mostAdvancedRound.State.RoundId; Phase = mostAdvancedRound.State.Phase; PeersRegistered = mostAdvancedRound.State.RegisteredPeerCount; PeersNeeded = mostAdvancedRound.State.RequiredPeerCount; } else { RoundId = -1; Phase = CcjRoundPhase.InputRegistration; PeersRegistered = 0; PeersNeeded = 100; } Global.ChaumianClient.StateUpdated += ChaumianClient_StateUpdated; EnqueueCommand = ReactiveCommand.Create(async() => { await DoEnqueueAsync(); }); DequeueCommand = ReactiveCommand.Create(async() => { var selectedCoins = QueuedCoinsList.Coins.Where(c => c.IsSelected).ToList(); foreach (var coin in selectedCoins) { coin.IsSelected = false; } try { await Global.ChaumianClient.DequeueCoinsFromMixAsync(selectedCoins.Select(c => c.Model).ToArray()); } catch (Exception ex) { Logger.LogWarning <CoinJoinTabViewModel>(ex); WarningMessageDequeue = ex.ToTypeMessageString(); if (ex is AggregateException aggex) { foreach (var iex in aggex.InnerExceptions) { WarningMessageDequeue += Environment.NewLine + iex.ToTypeMessageString(); } } return; } WarningMessageDequeue = string.Empty; }); this.WhenAnyValue(x => x.Password).Subscribe(async x => { if (x.NotNullAndNotEmpty()) { char lastChar = x.Last(); if (lastChar == '\r' || lastChar == '\n') // If the last character is cr or lf then act like it'd be a sign to do the job. { Password = x.TrimEnd('\r', '\n'); await DoEnqueueAsync(); } } }); }
public CoinViewModel(CoinListViewModel owner, SmartCoin model) { Model = model; _owner = owner; }
public CoinViewModel(Wallet wallet, CoinListViewModel owner, SmartCoin model) { Global = Locator.Current.GetService <Global>(); Model = model; Wallet = wallet; Owner = owner; RefreshSmartCoinStatus(); Disposables = new CompositeDisposable(); _coinJoinInProgress = Model .WhenAnyValue(x => x.CoinJoinInProgress) .ToProperty(this, x => x.CoinJoinInProgress, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); _unspent = Model .WhenAnyValue(x => x.Unspent) .ToProperty(this, x => x.Unspent, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); _confirmed = Model .WhenAnyValue(x => x.Confirmed) .ToProperty(this, x => x.Confirmed, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); _cluster = Model .WhenAnyValue(x => x.Clusters, x => x.Clusters.Labels) .Select(x => x.Item2.ToString()) .ToProperty(this, x => x.Clusters, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); _unavailable = Model .WhenAnyValue(x => x.Unavailable) .ToProperty(this, x => x.Unavailable, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); this.WhenAnyValue(x => x.Status) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => this.RaisePropertyChanged(nameof(ToolTip))); Observable .Merge(Model.WhenAnyValue(x => x.IsBanned, x => x.SpentAccordingToBackend, x => x.Confirmed, x => x.CoinJoinInProgress).Select(_ => Unit.Default)) .Merge(Observable.FromEventPattern(Wallet.ChaumianClient, nameof(Wallet.ChaumianClient.StateUpdated)).Select(_ => Unit.Default)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => RefreshSmartCoinStatus()) .DisposeWith(Disposables); Global.BitcoinStore.SmartHeaderChain .WhenAnyValue(x => x.TipHeight).Select(_ => Unit.Default) .Merge(Model.WhenAnyValue(x => x.Height).Select(_ => Unit.Default)) .Throttle(TimeSpan.FromSeconds(0.1)) // DO NOT TAKE THIS THROTTLE OUT, OTHERWISE SYNCING WITH COINS IN THE WALLET WILL STACKOVERFLOW! .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => this.RaisePropertyChanged(nameof(Confirmations))) .DisposeWith(Disposables); Global.UiConfig .WhenAnyValue(x => x.LurkingWifeMode) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { this.RaisePropertyChanged(nameof(AmountBtc)); this.RaisePropertyChanged(nameof(Clusters)); }).DisposeWith(Disposables); DequeueCoin = ReactiveCommand.Create(() => Owner.PressDequeue(Model), this.WhenAnyValue(x => x.CoinJoinInProgress)); OpenCoinInfo = ReactiveCommand.Create(() => { var shell = IoC.Get <IShell>(); var coinInfo = shell.Documents?.OfType <CoinInfoTabViewModel>()?.FirstOrDefault(x => x.Coin?.Model == Model); if (coinInfo is null) { coinInfo = new CoinInfoTabViewModel(this); shell.AddDocument(coinInfo); } shell.Select(coinInfo); }); CopyClusters = ReactiveCommand.CreateFromTask(async() => await Application.Current.Clipboard.SetTextAsync(Clusters)); Observable .Merge(DequeueCoin.ThrownExceptions) // Don't notify about it. Dequeue failure (and success) is notified by other mechanism. .Merge(OpenCoinInfo.ThrownExceptions) .Merge(CopyClusters.ThrownExceptions) .ObserveOn(RxApp.TaskpoolScheduler) .Subscribe(ex => Logger.LogError(ex)); }