コード例 #1
0
        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);
            }
コード例 #2
0
        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;
                }
            });
        }
コード例 #3
0
        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());
            });
        }
コード例 #4
0
        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);
            }
コード例 #5
0
        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();
            }
コード例 #6
0
        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));
            });
        }
コード例 #7
0
        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;
                        }
                    }
コード例 #8
0
        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>();
        }
コード例 #9
0
        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);
            });
        }
コード例 #10
0
        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));
        }
コード例 #11
0
ファイル: CoinViewModel.cs プロジェクト: carsenk/WalletWasabi
 public CoinViewModel(CoinListViewModel owner, SmartCoin model)
 {
     Model           = model;
     _owner          = owner;
     DisposablesLock = new object();
 }
コード例 #12
0
        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;
                }
            });
        }
コード例 #13
0
        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.
        }
コード例 #14
0
        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.
            });
        }
コード例 #15
0
		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;
				}
			});
		}
コード例 #16
0
        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));
        }
コード例 #17
0
        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);
        }
コード例 #18
0
        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--;
                }
            });
        }
コード例 #19
0
        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);
        }
コード例 #20
0
        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();
                    }
                }
            });
        }
コード例 #21
0
 public CoinViewModel(CoinListViewModel owner, SmartCoin model)
 {
     Model  = model;
     _owner = owner;
 }
コード例 #22
0
        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));
        }