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