private void UpdateRequiredBtcLabel(ClientRound registrableRound) { if (Global.WalletService is null) { return; // Otherwise NullReferenceException at shutdown. } if (registrableRound == default) { if (RequiredBTC == default) { RequiredBTC = Money.Zero; } } else { var coins = Global.WalletService.Coins; var queued = coins.CoinJoinInProcess(); if (queued.Any()) { RequiredBTC = registrableRound.State.CalculateRequiredAmount(Global.ChaumianClient.State.GetAllQueuedCoinAmounts().ToArray()); } else { var available = coins.Confirmed().Available(); RequiredBTC = available.Any() ? registrableRound.State.CalculateRequiredAmount(available.Where(x => x.AnonymitySet < Global.Config.PrivacyLevelStrong).Select(x => x.Amount).ToArray()) : registrableRound.State.CalculateRequiredAmount(); } } }
private async Task TryProcessStatusAsync(IEnumerable <RoundStateResponseBase> states) { states ??= Enumerable.Empty <RoundStateResponseBase>(); if (Interlocked.Read(ref _statusProcessing) == 1) // It's ok to wait for status processing next time. { return; } try { Synchronizer.BlockRequests(); Interlocked.Exchange(ref _statusProcessing, 1); using (await MixLock.LockAsync().ConfigureAwait(false)) { // First, if there's delayed round registration update based on the state. if (DelayedRoundRegistration != null) { ClientRound roundRegistered = State.GetSingleOrDefaultRound(DelayedRoundRegistration.AliceClient.RoundId); roundRegistered.Registration = DelayedRoundRegistration; DelayedRoundRegistration = null; // Do not dispose. } await DequeueSpentCoinsFromMixNoLockAsync().ConfigureAwait(false); State.UpdateRoundsByStates(ExposedLinks, states.ToArray()); // If we do not have enough coin queued to register a round, then dequeue all. ClientRound registrableRound = State.GetRegistrableRoundOrDefault(); if (registrableRound is { }) { DequeueReason?reason = null; // If the coordinator increases fees, do not register. Let the users register manually again. if (CoordinatorFeepercentToCheck is { } && registrableRound.State.CoordinatorFeePercent > CoordinatorFeepercentToCheck)
private async Task TryConfirmConnectionAsync(ClientRound inputRegistrableRound) { try { var res = await inputRegistrableRound.Registration.AliceClient.PostConfirmationAsync().ConfigureAwait(false); if (res.activeOutputs.Any()) { inputRegistrableRound.Registration.ActiveOutputs = res.activeOutputs; } if (res.currentPhase > RoundPhase.InputRegistration) // Then the phase went to connection confirmation (probably). { inputRegistrableRound.Registration.SetPhaseCompleted(RoundPhase.ConnectionConfirmation); inputRegistrableRound.State.Phase = res.currentPhase; } } catch (Exception ex) { if (ex.Message.StartsWith("Not Found", StringComparison.Ordinal)) // Alice timed out. { State.ClearRoundRegistration(inputRegistrableRound.State.RoundId); } Logger.LogError(ex); } }
public async Task <IEnumerable <SmartCoin> > QueueCoinsToMixAsync(string password, params SmartCoin[] coins) { if (coins is null || !coins.Any() || IsQuitPending) { return(Enumerable.Empty <SmartCoin>()); } var successful = new List <SmartCoin>(); using (await MixLock.LockAsync().ConfigureAwait(false)) { await DequeueSpentCoinsFromMixNoLockAsync().ConfigureAwait(false); // Every time the user enqueues (intentionally writes in password) then the coordinator fee percent must be noted and dequeue later if changes. ClientRound latestRound = State.GetLatestRoundOrDefault(); CoordinatorFeepercentToCheck = latestRound?.State?.CoordinatorFeePercent; var except = new List <SmartCoin>(); foreach (SmartCoin coin in coins) { if (State.Contains(coin)) { successful.Add(coin); except.Add(coin); continue; } if (coin.Unavailable) { except.Add(coin); continue; } } var coinsExcept = coins.Except(except); var secPubs = KeyManager.GetSecretsAndPubKeyPairs(password, coinsExcept.Select(x => x.ScriptPubKey).ToArray()); Cook(password); foreach (SmartCoin coin in coinsExcept) { coin.Secret = secPubs.Single(x => x.pubKey.P2wpkhScript == coin.ScriptPubKey).secret; coin.CoinJoinInProgress = true; State.AddCoinToWaitingList(coin); successful.Add(coin); Logger.LogInfo($"Coin queued: {coin.Index}:{coin.TransactionId}."); } } foreach (var coin in successful) { CoinQueued?.Invoke(this, coin); } return(successful); }
public override void OnOpen() { base.OnOpen(); Disposables = Disposables is null ? new CompositeDisposable() : throw new NotSupportedException($"Cannot open {GetType().Name} before closing it."); TargetPrivacy = Global.Config.GetTargetPrivacy(); var registrableRound = Global.ChaumianClient.State.GetRegistrableRoundOrDefault(); UpdateRequiredBtcLabel(registrableRound); CoordinatorFeePercent = registrableRound?.State?.CoordinatorFeePercent.ToString() ?? "0.003"; Observable.FromEventPattern(Global.ChaumianClient, nameof(Global.ChaumianClient.CoinQueued)) .Merge(Observable.FromEventPattern(Global.ChaumianClient, nameof(Global.ChaumianClient.OnDequeue))) .Merge(Observable.FromEventPattern(Global.ChaumianClient, nameof(Global.ChaumianClient.StateUpdated))) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => UpdateStates()) .DisposeWith(Disposables); ClientRound mostAdvancedRound = Global.ChaumianClient?.State?.GetMostAdvancedRoundOrDefault(); if (mostAdvancedRound != default) { RoundId = mostAdvancedRound.State.RoundId; Phase = mostAdvancedRound.State.Phase; RoundTimesout = mostAdvancedRound.State.Phase == RoundPhase.InputRegistration ? mostAdvancedRound.State.InputRegistrationTimesout : DateTimeOffset.UtcNow; PeersRegistered = mostAdvancedRound.State.RegisteredPeerCount; PeersNeeded = mostAdvancedRound.State.RequiredPeerCount; } else { RoundId = -1; Phase = RoundPhase.InputRegistration; RoundTimesout = DateTimeOffset.UtcNow; PeersRegistered = 0; PeersNeeded = 100; } Global.UiConfig.WhenAnyValue(x => x.LurkingWifeMode).ObserveOn(RxApp.MainThreadScheduler).Subscribe(_ => { this.RaisePropertyChanged(nameof(AmountQueued)); this.RaisePropertyChanged(nameof(IsLurkingWifeMode)); }).DisposeWith(Disposables); Observable.Interval(TimeSpan.FromSeconds(1)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { TimeSpan left = RoundTimesout - DateTimeOffset.UtcNow; TimeLeftTillRoundTimeout = left > TimeSpan.Zero ? left : TimeSpan.Zero; // Make sure cannot be less than zero. }).DisposeWith(Disposables); }
private async Task RegisterOutputAsync(ClientRound ongoingRound) { IEnumerable <TxoRef> registeredInputs = ongoingRound.Registration.CoinsRegistered.Select(x => x.GetTxoRef()); var shuffledOutputs = ongoingRound.Registration.ActiveOutputs.ToList(); shuffledOutputs.Shuffle(); foreach (var activeOutput in shuffledOutputs) { using var bobClient = new BobClient(CcjHostUriAction, TorSocks5EndPoint); if (!await bobClient.PostOutputAsync(ongoingRound.RoundId, activeOutput).ConfigureAwait(false)) { Logger.LogWarning($"Round ({ongoingRound.State.RoundId}) Bobs did not have enough time to post outputs before timeout. If you see this message, contact nopara73, so he can optimize the phase timeout periods to the worst Internet/Tor connections, which may be yours."); break; } // Unblind our exposed links. foreach (TxoRef input in registeredInputs) { if (ExposedLinks.ContainsKey(input)) // Should never not contain, but oh well, let's not disrupt the round for this. { var found = ExposedLinks[input].FirstOrDefault(x => x.Key.GetP2wpkhAddress(Network) == activeOutput.Address); if (found != default) { found.IsBlinded = false; } else { // Should never happen, but oh well we can autocorrect it so why not. ExposedLinks[input] = ExposedLinks[input].Append(new HdPubKeyBlindedPair(KeyManager.GetKeyForScriptPubKey(activeOutput.Address.ScriptPubKey), false)); } } } } ongoingRound.Registration.SetPhaseCompleted(RoundPhase.OutputRegistration); Logger.LogInfo($"Round ({ongoingRound.State.RoundId}) Bob Posted outputs: {ongoingRound.Registration.ActiveOutputs.Count()}."); }
private async Task TryRegisterCoinsAsync(ClientRound inputRegistrableRound) { try { // Select the most suitable coins to regiter. List <TxoRef> registrableCoins = State.GetRegistrableCoins( inputRegistrableRound.State.MaximumInputCountPerPeer, inputRegistrableRound.State.Denomination, inputRegistrableRound.State.FeePerInputs, inputRegistrableRound.State.FeePerOutputs).ToList(); // If there are no suitable coins to register return. if (!registrableCoins.Any()) { return; } (HdPubKey change, IEnumerable <HdPubKey> actives)outputAddresses = GetOutputsToRegister(inputRegistrableRound.State.Denomination, inputRegistrableRound.State.SchnorrPubKeys.Count(), registrableCoins); SchnorrPubKey[] schnorrPubKeys = inputRegistrableRound.State.SchnorrPubKeys.ToArray(); List <Requester> requesters = new List <Requester>(); var blindedOutputScriptHashes = new List <uint256>(); var registeredAddresses = new List <BitcoinAddress>(); for (int i = 0; i < schnorrPubKeys.Length; i++) { if (outputAddresses.actives.Count() <= i) { break; } BitcoinAddress address = outputAddresses.actives.Select(x => x.GetP2wpkhAddress(Network)).ElementAt(i); SchnorrPubKey schnorrPubKey = schnorrPubKeys[i]; var outputScriptHash = new uint256(Hashes.SHA256(address.ScriptPubKey.ToBytes())); var requester = new Requester(); uint256 blindedOutputScriptHash = requester.BlindMessage(outputScriptHash, schnorrPubKey); requesters.Add(requester); blindedOutputScriptHashes.Add(blindedOutputScriptHash); registeredAddresses.Add(address); } byte[] blindedOutputScriptHashesByte = ByteHelpers.Combine(blindedOutputScriptHashes.Select(x => x.ToBytes())); uint256 blindedOutputScriptsHash = new uint256(Hashes.SHA256(blindedOutputScriptHashesByte)); var inputProofs = new List <InputProofModel>(); foreach (TxoRef coinReference in registrableCoins) { SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference); if (coin is null) { throw new NotSupportedException("This is impossible."); } coin.Secret ??= KeyManager.GetSecrets(SaltSoup(), coin.ScriptPubKey).Single(); var inputProof = new InputProofModel { Input = coin.GetTxoRef(), Proof = coin.Secret.PrivateKey.SignCompact(blindedOutputScriptsHash) }; inputProofs.Add(inputProof); } AliceClient aliceClient = null; try { aliceClient = await AliceClient.CreateNewAsync(inputRegistrableRound.RoundId, registeredAddresses, schnorrPubKeys, requesters, Network, outputAddresses.change.GetP2wpkhAddress(Network), blindedOutputScriptHashes, inputProofs, CcjHostUriAction, TorSocks5EndPoint).ConfigureAwait(false); } catch (HttpRequestException ex) when(ex.Message.Contains("Input is banned", StringComparison.InvariantCultureIgnoreCase)) { string[] parts = ex.Message.Split(new[] { "Input is banned from participation for ", " minutes: " }, StringSplitOptions.RemoveEmptyEntries); string minutesString = parts[1]; int minuteInt = int.Parse(minutesString); string bannedInputString = parts[2].TrimEnd('.'); string[] bannedInputStringParts = bannedInputString.Split(':', StringSplitOptions.RemoveEmptyEntries); TxoRef coinReference = new TxoRef(new uint256(bannedInputStringParts[1]), uint.Parse(bannedInputStringParts[0])); SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference); if (coin is null) { throw new NotSupportedException("This is impossible."); } coin.BannedUntilUtc = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(minuteInt); Logger.LogWarning(ex.Message.Split('\n')[1]); await DequeueCoinsFromMixNoLockAsync(coinReference, "Failed to register the coin with the coordinator.").ConfigureAwait(false); aliceClient?.Dispose(); return; } catch (HttpRequestException ex) when(ex.Message.Contains("Provided input is not unspent", StringComparison.InvariantCultureIgnoreCase)) { string[] parts = ex.Message.Split(new[] { "Provided input is not unspent: " }, StringSplitOptions.RemoveEmptyEntries); string spentInputString = parts[1].TrimEnd('.'); string[] bannedInputStringParts = spentInputString.Split(':', StringSplitOptions.RemoveEmptyEntries); TxoRef coinReference = new TxoRef(new uint256(bannedInputStringParts[1]), uint.Parse(bannedInputStringParts[0])); SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference); if (coin is null) { throw new NotSupportedException("This is impossible."); } coin.SpentAccordingToBackend = true; Logger.LogWarning(ex.Message.Split('\n')[1]); await DequeueCoinsFromMixNoLockAsync(coinReference, "Failed to register the coin with the coordinator. The coin is already spent.").ConfigureAwait(false); aliceClient?.Dispose(); return; } catch (HttpRequestException ex) when(ex.Message.Contains("No such running round in InputRegistration.", StringComparison.InvariantCultureIgnoreCase)) { Logger.LogInfo("Client tried to register a round that is not in InputRegistration anymore. Trying again later."); aliceClient?.Dispose(); return; } catch (HttpRequestException ex) when(ex.Message.Contains("too-long-mempool-chain", StringComparison.InvariantCultureIgnoreCase)) { Logger.LogInfo("Coordinator failed because too much unconfirmed parent transactions. Trying again later."); aliceClient?.Dispose(); return; } var coinsRegistered = new List <SmartCoin>(); foreach (TxoRef coinReference in registrableCoins) { var coin = State.GetSingleOrDefaultFromWaitingList(coinReference); if (coin is null) { throw new NotSupportedException("This is impossible."); } coinsRegistered.Add(coin); State.RemoveCoinFromWaitingList(coin); } var registration = new ClientRoundRegistration(aliceClient, coinsRegistered, outputAddresses.change.GetP2wpkhAddress(Network)); ClientRound roundRegistered = State.GetSingleOrDefaultRound(aliceClient.RoundId); if (roundRegistered is null) { // If our SatoshiClient does not yet know about the round, because of delay, then delay the round registration. DelayedRoundRegistration?.Dispose(); DelayedRoundRegistration = registration; } roundRegistered.Registration = registration; } catch (Exception ex) { Logger.LogError(ex); } }
private Dictionary <int, WitScript> SignCoinJoin(ClientRound ongoingRound, Transaction unsignedCoinJoin) { TxOut[] myOutputs = unsignedCoinJoin.Outputs .Where(x => x.ScriptPubKey == ongoingRound.Registration.ChangeAddress.ScriptPubKey || ongoingRound.Registration.ActiveOutputs.Select(y => y.Address.ScriptPubKey).Contains(x.ScriptPubKey)) .ToArray(); Money amountBack = myOutputs.Sum(y => y.Value); // Make sure change is counted. Money minAmountBack = ongoingRound.CoinsRegistered.Sum(x => x.Amount); // Start with input sum. // Do outputs.lenght + 1 in case the server estimated the network fees wrongly due to insufficient data in an edge case. Money networkFeesAfterOutputs = ongoingRound.State.FeePerOutputs * (ongoingRound.Registration.AliceClient.RegisteredAddresses.Length + 1); // Use registered addresses here, because network fees are decided at inputregistration. Money networkFeesAfterInputs = ongoingRound.State.FeePerInputs * ongoingRound.Registration.CoinsRegistered.Count(); Money networkFees = networkFeesAfterOutputs + networkFeesAfterInputs; minAmountBack -= networkFees; // Minus miner fee. IOrderedEnumerable <(Money value, int count)> indistinguishableOutputs = unsignedCoinJoin.GetIndistinguishableOutputs(includeSingle: false).OrderByDescending(x => x.count); foreach ((Money value, int count)denomPair in indistinguishableOutputs) { var mineCount = myOutputs.Count(x => x.Value == denomPair.value); Money denomination = denomPair.value; int anonset = Math.Min(110, denomPair.count); // https://github.com/zkSNACKs/WalletWasabi/issues/1379 Money expectedCoordinatorFee = denomination.Percentage(ongoingRound.State.CoordinatorFeePercent * anonset); for (int i = 0; i < mineCount; i++) { minAmountBack -= expectedCoordinatorFee; // Minus expected coordinator fee. } } // If there's no change output then coordinator protection may happened: bool gotChange = myOutputs.Select(x => x.ScriptPubKey).Contains(ongoingRound.Registration.ChangeAddress.ScriptPubKey); if (!gotChange) { Money minimumOutputAmount = Money.Coins(0.0001m); // If the change would be less than about $1 then add it to the coordinator. Money baseDenomination = indistinguishableOutputs.First().value; Money onePercentOfDenomination = baseDenomination.Percentage(1m); // If the change is less than about 1% of the newDenomination then add it to the coordinator fee. Money minimumChangeAmount = Math.Max(minimumOutputAmount, onePercentOfDenomination); minAmountBack -= minimumChangeAmount; // Minus coordinator protections (so it won't create bad coinjoins.) } if (amountBack < minAmountBack && !amountBack.Almost(minAmountBack, Money.Satoshis(1000))) // Just in case. Rounding error maybe? { Money diff = minAmountBack - amountBack; throw new NotSupportedException($"Coordinator did not add enough value to our outputs in the coinjoin. Missing: {diff.Satoshi} satoshis."); } var signedCoinJoin = unsignedCoinJoin.Clone(); signedCoinJoin.Sign(ongoingRound.CoinsRegistered.Select(x => x.Secret ??= KeyManager.GetSecrets(SaltSoup(), x.ScriptPubKey).Single()).ToArray(), ongoingRound.Registration.CoinsRegistered.Select(x => x.GetCoin()).ToArray()); // Old way of signing, which randomly fails! https://github.com/zkSNACKs/WalletWasabi/issues/716#issuecomment-435498906 // Must be fixed in NBitcoin. //var builder = Network.CreateTransactionBuilder(); //var signedCoinJoin = builder // .ContinueToBuild(unsignedCoinJoin) // .AddKeys(ongoingRound.Registration.CoinsRegistered.Select(x => x.Secret = x.Secret ?? KeyManager.GetSecrets(OnePiece, x.ScriptPubKey).Single()).ToArray()) // .AddCoins(ongoingRound.Registration.CoinsRegistered.Select(x => x.GetCoin())) // .BuildTransaction(true); var myDic = new Dictionary <int, WitScript>(); for (int i = 0; i < signedCoinJoin.Inputs.Count; i++) { var input = signedCoinJoin.Inputs[i]; if (ongoingRound.CoinsRegistered.Select(x => x.GetOutPoint()).Contains(input.PrevOut)) { myDic.Add(i, signedCoinJoin.Inputs[i].WitScript); } } return(myDic); }
private async Task TryProcessStatusAsync(IEnumerable <RoundStateResponse> states) { states ??= Enumerable.Empty <RoundStateResponse>(); if (Interlocked.Read(ref _statusProcessing) == 1) // It's ok to wait for status processing next time. { return; } try { Synchronizer.BlockRequests(); Interlocked.Exchange(ref _statusProcessing, 1); using (await MixLock.LockAsync().ConfigureAwait(false)) { // First, if there's delayed round registration update based on the state. if (DelayedRoundRegistration != null) { ClientRound roundRegistered = State.GetSingleOrDefaultRound(DelayedRoundRegistration.AliceClient.RoundId); roundRegistered.Registration = DelayedRoundRegistration; DelayedRoundRegistration = null; // Do not dispose. } await DequeueSpentCoinsFromMixNoLockAsync().ConfigureAwait(false); State.UpdateRoundsByStates(ExposedLinks, states.ToArray()); // If we do not have enough coin queued to register a round, then dequeue all. ClientRound registrableRound = State.GetRegistrableRoundOrDefault(); if (registrableRound != default) { // If the coordinator increases fees, do not register. Let the users register manually again. bool dequeueBecauseCoordinatorFeeChanged = false; if (CoordinatorFeepercentToCheck != default) { dequeueBecauseCoordinatorFeeChanged = registrableRound.State.CoordinatorFeePercent > CoordinatorFeepercentToCheck; } if (!registrableRound.State.HaveEnoughQueued(State.GetAllQueuedCoinAmounts().ToArray()) || dequeueBecauseCoordinatorFeeChanged) { await DequeueAllCoinsFromMixNoLockAsync("The total value of the registered coins is not enough or the coordinator's fee changed.").ConfigureAwait(false); } } } StateUpdated?.Invoke(this, null); int delaySeconds = new Random().Next(0, 7); // delay the response to defend timing attack privacy. if (Network == Network.RegTest) { delaySeconds = 0; } await Task.Delay(TimeSpan.FromSeconds(delaySeconds), Cancel.Token).ConfigureAwait(false); using (await MixLock.LockAsync().ConfigureAwait(false)) { foreach (long ongoingRoundId in State.GetActivelyMixingRounds()) { await TryProcessRoundStateAsync(ongoingRoundId).ConfigureAwait(false); } await DequeueSpentCoinsFromMixNoLockAsync().ConfigureAwait(false); ClientRound inputRegistrableRound = State.GetRegistrableRoundOrDefault(); if (inputRegistrableRound != null) { if (inputRegistrableRound.Registration is null) // If did not register already, check what can we register. { await TryRegisterCoinsAsync(inputRegistrableRound).ConfigureAwait(false); } else // We registered, let's confirm we're online. { await TryConfirmConnectionAsync(inputRegistrableRound).ConfigureAwait(false); } } } } catch (TaskCanceledException ex) { Logger.LogTrace(ex); } catch (Exception ex) { Logger.LogError(ex); } finally { Interlocked.Exchange(ref _statusProcessing, 0); Synchronizer.EnableRequests(); } }
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); }
public CoinJoinViewModel(CoinListViewModel coinList) : base(Locator.Current.GetService <IViewStackService>()) { Global = Locator.Current.GetService <Global>(); SetBalance(); notificationManager = Global.NotificationManager; if (Disposables != null) { throw new Exception("Wallet opened before it was closed."); } Disposables = new CompositeDisposable(); CoinList = coinList; Observable .FromEventPattern <SmartCoin>(CoinList, nameof(CoinList.DequeueCoinsPressed)) .Subscribe(async x => await DoDequeueAsync(x.EventArgs)); this.WhenAnyValue(x => x.RoundTimesout) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { TimeSpan left = RoundTimesout - DateTimeOffset.UtcNow; TimeLeftTillRoundTimeout = left > TimeSpan.Zero ? left : TimeSpan.Zero; // Make sure cannot be less than zero. }); AmountQueued = Money.Zero; var registrableRound = Global.Wallet.ChaumianClient.State.GetRegistrableRoundOrDefault(); CoordinatorFeePercent = registrableRound?.State?.CoordinatorFeePercent.ToString() ?? "0.003"; Observable.FromEventPattern(Global.Wallet.ChaumianClient, nameof(Global.Wallet.ChaumianClient.CoinQueued)) .Merge(Observable.FromEventPattern(Global.Wallet.ChaumianClient, nameof(Global.Wallet.ChaumianClient.OnDequeue))) .Merge(Observable.FromEventPattern(Global.Wallet.ChaumianClient, nameof(Global.Wallet.ChaumianClient.StateUpdated))) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => UpdateStates()) .DisposeWith(Disposables); Observable.FromEventPattern(Global.Wallet.ChaumianClient, nameof(Global.Wallet.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); ClientRound mostAdvancedRound = Global.Wallet.ChaumianClient?.State?.GetMostAdvancedRoundOrDefault(); if (mostAdvancedRound != default) { RoundPhaseState = new RoundPhaseState(mostAdvancedRound.State.Phase, Global.Wallet.ChaumianClient?.State.IsInErrorState ?? false); RoundTimesout = mostAdvancedRound.State.Phase == RoundPhase.InputRegistration ? mostAdvancedRound.State.InputRegistrationTimesout : DateTimeOffset.UtcNow; PeersRegistered = mostAdvancedRound.State.RegisteredPeerCount; PeersNeeded = mostAdvancedRound.State.RequiredPeerCount; } else { RoundPhaseState = new RoundPhaseState(RoundPhase.InputRegistration, false); RoundTimesout = DateTimeOffset.UtcNow; PeersRegistered = 0; PeersNeeded = 100; } Observable.Interval(TimeSpan.FromSeconds(1)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { TimeSpan left = RoundTimesout - DateTimeOffset.UtcNow; TimeLeftTillRoundTimeout = left > TimeSpan.Zero ? left : TimeSpan.Zero; // Make sure cannot be less than zero. }).DisposeWith(Disposables); CoinJoinCommand = ReactiveCommand.CreateFromTask <string, bool>(DoEnqueueAsync); ExitCoinJoinCommand = ReactiveCommand.CreateFromTask(ExitCoinJoinAsync); var canPromptPassword = this.WhenAnyValue( x => x.CoinList.SelectedAmount, x => x.RequiredBTC, (amnt, rBTC) => { return(!(rBTC is null) && !(amnt is null) && amnt >= rBTC); });