public SendViewModel(Global global, SelectCoinsViewModel selectCoinsViewModel) { Global = global; SelectCoinsViewModel = selectCoinsViewModel; AmountText = "0.0"; AllSelectedAmount = Money.Zero; EstimatedBtcFee = Money.Zero; this.WhenAnyValue(x => x.AmountText) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(amount => { // Correct amount if (IsMax) { SetAmountIfMax(); } else { 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; } } }); _outputAmount = this.WhenAnyValue(x => x.AmountText, (amountText) => { Money.TryParse(amountText.TrimStart('~', ' '), out Money outputAmount); return(outputAmount); }).ToProperty(this, x => x.OutputAmount); this.WhenAnyValue(x => x.IsMax) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(isMax => { if (isMax) { SetAmountIfMax(); } }); Observable.FromEventPattern(SelectCoinsViewModel, nameof(SelectCoinsViewModel.SelectionChanged)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => SetFees()); _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; FeeRate = new FeeRate((decimal)50); //50 sat/vByte placeholder til loads SetFees(); Observable .FromEventPattern <AllFeeEstimate>(Global.FeeProviders, nameof(Global.FeeProviders.AllFeeEstimateChanged)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { SetFeeTargetLimits(); if (FeeTarget < MinimumFeeTarget) // Should never happen. { FeeTarget = MinimumFeeTarget; } else if (FeeTarget > MaximumFeeTarget) { FeeTarget = MaximumFeeTarget; } SetFees(); }) .DisposeWith(Disposables); this.WhenAnyValue(x => x.FeeTarget) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { SetFees(); }); _destinationUrl = this.WhenAnyValue(x => x.DestinationString, ParseDestinationString) .ToProperty(this, nameof(Url)); var isTransactionOkToSign = this.WhenAnyValue( x => x.Label, x => x.Address, x => x.OutputAmount, x => x.SelectCoinsViewModel.SelectedAmount, x => x.EstimatedBtcFee, (label, address, outputAmount, selectedAmount, feeAmount) => { return(label.NotNullAndNotEmpty() && address is not null && outputAmount > Money.Zero && outputAmount + feeAmount <= selectedAmount); }); _isTransactionOkToSign = isTransactionOkToSign .ToProperty(this, x => x.IsTransactionOkToSign); SendTransactionCommand = ReactiveCommand.CreateFromTask <string, bool>(SendTransaction, isTransactionOkToSign); }
public SendViewModel(Global global, SelectCoinsViewModel selectCoinsViewModel) { Global = global; SelectCoinsViewModel = selectCoinsViewModel; AmountText = "0.0"; AllSelectedAmount = Money.Zero; EstimatedBtcFee = Money.Zero; this.WhenAnyValue(x => x.AmountText) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(amount => { // Correct amount if (IsMax) { SetAmountIfMax(); } else { 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.IsMax) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(isMax => { if (isMax) { SetAmountIfMax(); } }); Observable.FromEventPattern(SelectCoinsViewModel, nameof(SelectCoinsViewModel.SelectionChanged)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => SetFees()); _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; FeeRate = new FeeRate((decimal)50); //50 sat/vByte placeholder til loads SetFees(); Observable .FromEventPattern <AllFeeEstimate>(Global.FeeProviders, nameof(Global.FeeProviders.AllFeeEstimateChanged)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { SetFeeTargetLimits(); if (FeeTarget < MinimumFeeTarget) // Should never happen. { FeeTarget = MinimumFeeTarget; } else if (FeeTarget > MaximumFeeTarget) { FeeTarget = MaximumFeeTarget; } SetFees(); }) .DisposeWith(Disposables); this.WhenAnyValue(x => x.FeeTarget) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { SetFees(); }); var canPromptPassword = this.WhenAnyValue(x => x.Label, x => x.Address, x => x.IsBusy, (label, addr, isBusy) => { BitcoinAddress address; try { address = BitcoinAddress.Create(addr.Trim(), Global.Network); } catch (FormatException) { // SetWarningMessage("Invalid address."); return(false); } return(!isBusy && label.Length > 0 && address is BitcoinAddress); }); //_promptViewModel = new PasswordPromptViewModel("SEND"); //_promptViewModel.ValidatePasswordCommand.Subscribe(async validPassword => //{ // if (validPassword != null) // { // await ViewStackService.PopModal(); // await BuildTransaction(validPassword); // await ViewStackService.PushPage(new SentViewModel()); // } //}); //PromptCommand = ReactiveCommand.CreateFromObservable(() => //{ // ViewStackService.PushModal(_promptViewModel).Subscribe(); // return Observable.Return(Unit.Default); //}, canPromptPassword); }
public SendViewModel(Global global, ChaincaseWalletManager walletManager, Config config, UiConfig uiConfig, SelectCoinsViewModel selectCoinsViewModel, FeeProviders feeProviders) { _global = global; _walletManager = walletManager; _config = config; _uiConfig = uiConfig; _feeProviders = feeProviders; SelectCoinsViewModel = selectCoinsViewModel; AmountText = "0.0"; AllSelectedAmount = Money.Zero; EstimatedBtcFee = Money.Zero; if (_global.IsInitialized) { OnAppInitialized(this, new AppInitializedEventArgs(_global)); } else { _global.Initialized += OnAppInitialized; } _outputAmount = this.WhenAnyValue(x => x.AmountText, (amountText) => { Money.TryParse(amountText.TrimStart('~', ' '), out Money outputAmount); return(outputAmount ?? OutputAmount); }).ToProperty(this, x => x.OutputAmount); this.WhenAnyValue(x => x.IsMax) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(isMax => { if (isMax) { SetAmountIfMax(); } }); Observable.FromEventPattern(SelectCoinsViewModel, nameof(SelectCoinsViewModel.SelectionChanged)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => ApplyFees()); _minMaxFeeTargetsEqual = this.WhenAnyValue(x => x.MinimumFeeTarget, x => x.MaximumFeeTarget, (x, y) => x == y) .ToProperty(this, x => x.MinMaxFeeTargetsEqual, scheduler: RxApp.MainThreadScheduler); SetFeeTargetLimits(); FeeTarget = _uiConfig.FeeTarget; FeeRate = new FeeRate((decimal)50); //50 sat/vByte placeholder til loads this.WhenAnyValue(x => x.FeeTarget) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { ApplyFees(); }); _destinationUrl = this.WhenAnyValue(x => x.DestinationString, ParseDestinationString) .ToProperty(this, nameof(DestinationUrl)); var isTransactionOkToSign = this.WhenAnyValue( x => x.Label, x => x.DestinationUrl, x => x.OutputAmount, x => x.SelectCoinsViewModel.SelectedAmount, x => x.EstimatedBtcFee, (label, address, outputAmount, selectedAmount, feeAmount) => { return(label.NotNullAndNotEmpty() && DestinationUrl.Address is not null && outputAmount > Money.Zero && outputAmount + feeAmount <= selectedAmount); }); _isTransactionOkToSign = isTransactionOkToSign .ToProperty(this, x => x.IsTransactionOkToSign); SendTransactionCommand = ReactiveCommand.CreateFromTask <string, bool>(SendTransaction, isTransactionOkToSign); }
public CoinJoinViewModel(ChaincaseWalletManager walletManager, Config config, INotificationManager notificationManager, SelectCoinsViewModel selectCoinsViewModel) { _walletManager = walletManager; _config = config; _notificationManager = notificationManager; CoinList = selectCoinsViewModel; if (Disposables != null) { throw new Exception("Wallet opened before it was closed."); } Disposables = new CompositeDisposable(); // Infer coordinator fee var registrableRound = _walletManager.CurrentWallet?.ChaumianClient?.State?.GetRegistrableRoundOrDefault(); CoordinatorFeePercent = registrableRound?.State?.CoordinatorFeePercent.ToString() ?? "0.003"; // Select most advanced coin join round ClientRound mostAdvancedRound = _walletManager.CurrentWallet?.ChaumianClient?.State?.GetMostAdvancedRoundOrDefault(); if (mostAdvancedRound != default) { RoundPhaseState = new RoundPhaseState(mostAdvancedRound.State.Phase, _walletManager.CurrentWallet.ChaumianClient?.State.IsInErrorState ?? false); RoundTimesout = mostAdvancedRound.State.Phase == RoundPhase.InputRegistration ? mostAdvancedRound.State.InputRegistrationTimesout : DateTimeOffset.UtcNow; PeersRegistered = mostAdvancedRound.State.RegisteredPeerCount; PeersQueued = mostAdvancedRound.State.QueuedPeerCount; PeersNeeded = mostAdvancedRound.State.RequiredPeerCount; RequiredBTC = mostAdvancedRound.State.CalculateRequiredAmount(); } else { RoundPhaseState = new RoundPhaseState(RoundPhase.InputRegistration, false); RoundTimesout = DateTimeOffset.UtcNow; PeersRegistered = 0; PeersQueued = 0; PeersNeeded = 100; RequiredBTC = Money.Parse("0.01"); } // Set time left in round this.WhenAnyValue(x => x.RoundTimesout) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { TimeLeftTillRoundTimeout = TimeUntilOffset(RoundTimesout); }); Task.Run(async() => { while (_walletManager.CurrentWallet?.ChaumianClient == null) { await Task.Delay(50).ConfigureAwait(false); } // Update view model state on chaumian client state updates Observable.FromEventPattern(_walletManager.CurrentWallet.ChaumianClient, nameof(_walletManager.CurrentWallet.ChaumianClient.CoinQueued)) .Merge(Observable.FromEventPattern(_walletManager.CurrentWallet.ChaumianClient, nameof(_walletManager.CurrentWallet.ChaumianClient.OnDequeue))) .Merge(Observable.FromEventPattern(_walletManager.CurrentWallet.ChaumianClient, nameof(_walletManager.CurrentWallet.ChaumianClient.StateUpdated))) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => UpdateStates()) .DisposeWith(Disposables); // Remove notification on unconfirming status in coin join round Observable.FromEventPattern(_walletManager.CurrentWallet.ChaumianClient, nameof(_walletManager.CurrentWallet.ChaumianClient.OnDequeue)) .Subscribe(pattern => { var e = (DequeueResult)pattern.EventArgs; try { foreach (var success in e.Successful.Where(x => x.Value.Any())) { DequeueReason reason = success.Key; if (reason == DequeueReason.UserRequested) { _notificationManager.RemoveAllPendingNotifications(); } } } catch (Exception ex) { Logger.LogWarning(ex); } }) .DisposeWith(Disposables); }); // Update timeout label Observable.Interval(TimeSpan.FromSeconds(1)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { TimeLeftTillRoundTimeout = TimeUntilOffset(RoundTimesout); }).DisposeWith(Disposables); }