Implement countdown
MODIFY
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs
@@ -64,6 +64,7 @@ namespace WalletWasabi.Backend.Controllers
Phase = round.Phase,
SchnorrPubKeys = round.MixingLevels.SchnorrPubKeys,
Denomination = round.MixingLevels.GetBaseDenomination(),
+ InputRegistrationTimesout = round.InputRegistrationTimesout,
RegisteredPeerCount = round.CountAlices(syncLock: false),
RequiredPeerCount = round.AnonymitySet,
MaximumInputCountPerPeer = 7, // Constant for now. If we want to do something with it later, we'll put it to the config file.

MODIFY
WalletWasabi.Gui/Controls/WalletExplorer/CoinJoinTabView.xaml
WalletWasabi.Gui/Controls/WalletExplorer/CoinJoinTabView.xaml
<converters:PrivacyLevelValueConverter x:Key="PrivacyLevelValueConverter" />
<converters:LurkingWifeModeStringConverter x:Key="LurkingWifeModeStringConverter" />
<converters:InverseBooleanConverter x:Key="InverseBooleanConverter" />
+ <converters:TimeSpanStringConverter x:Key="TimeSpanStringConverter" />
</UserControl.Resources>
<Grid Margin="20">
<Grid.RowDefinitions>
<TextBlock VerticalAlignment="Center" Foreground="YellowGreen" Text="{Binding Phase, Converter={StaticResource PhaseStringConverter}}" />
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal" Spacing="4">
- <TextBlock Text="Number of Registered Peers:" />
+ <TextBlock Text="Registration Ends:" />
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" Spacing="4">
- <TextBlock Foreground="YellowGreen" Text="{Binding PeersRegistered}" />
- <TextBlock Text="/" />
- <TextBlock Text="{Binding PeersNeeded}" />
- <ProgressBar Minimum="0" Maximum="{Binding PeersNeeded}" Value="{Binding PeersRegistered}" />
+ <TextBlock Foreground="YellowGreen" Text="{Binding TimeLeftTillRoundTimeout, Converter={StaticResource TimeSpanStringConverter}}" />
</StackPanel>
<StackPanel Grid.Row="2" Grid.Column="0" Orientation="Horizontal" Spacing="4">
- <TextBlock Text="Number of Successful Rounds:" />
+ <TextBlock Text="Number of Registered Peers:" />
</StackPanel>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal" Spacing="4">
- <TextBlock Foreground="YellowGreen" Text="{Binding SuccessfulRoundCount}" />
+ <TextBlock Foreground="YellowGreen" Text="{Binding PeersRegistered}" />
+ <TextBlock Text="/" />
+ <TextBlock Text="{Binding PeersNeeded}" />
+ <ProgressBar Minimum="0" Maximum="{Binding PeersNeeded}" Value="{Binding PeersRegistered}" />
</StackPanel>
</Grid>
</StackPanel>

MODIFY
WalletWasabi.Gui/Controls/WalletExplorer/CoinJoinTabViewModel.cs
WalletWasabi.Gui/Controls/WalletExplorer/CoinJoinTabViewModel.cs
@@ -24,8 +24,9 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
private CompositeDisposable Disposables { get; set; }
private long _roundId;
- private int _successfulRoundCount;
private CcjRoundPhase _phase;
+ private DateTimeOffset _roundTimesout;
+ private TimeSpan _timeLeftTillRoundTimeout;
private Money _requiredBTC;
private string _coordinatorFeePercent;
private int _peersRegistered;
@@ -47,6 +48,7 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
: base("CoinJoin", walletViewModel)
{
Password = "";
+ TimeLeftTillRoundTimeout = TimeSpan.Zero;
CoinsList = new CoinListViewModel(Global, CoinListContainerType.CoinJoinTabViewModel);
@@ -120,6 +122,23 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
{
CoinJoinUntilAnonimitySet = 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.
+ });
+
+ this.WhenAnyValue(x => x.TimeLeftTillRoundTimeout)
+ .Throttle(TimeSpan.FromSeconds(1))
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .Subscribe(x =>
+ {
+ var next = TimeLeftTillRoundTimeout - TimeSpan.FromSeconds(1);
+ TimeLeftTillRoundTimeout = next > TimeSpan.Zero ? next : TimeSpan.Zero; // Make sure cannot be less than zero.
+ });
}
public override void OnOpen()
@@ -151,16 +170,16 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
if (mostAdvancedRound != default)
{
RoundId = mostAdvancedRound.State.RoundId;
- SuccessfulRoundCount = mostAdvancedRound.State.SuccessfulRoundCount;
Phase = mostAdvancedRound.State.Phase;
+ RoundTimesout = mostAdvancedRound.State.InputRegistrationTimesout;
PeersRegistered = mostAdvancedRound.State.RegisteredPeerCount;
PeersNeeded = mostAdvancedRound.State.RequiredPeerCount;
}
else
{
RoundId = -1;
- SuccessfulRoundCount = -1;
Phase = CcjRoundPhase.InputRegistration;
+ RoundTimesout = DateTimeOffset.UtcNow;
PeersRegistered = 0;
PeersNeeded = 100;
}
@@ -279,12 +298,13 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
if (mostAdvancedRound != default)
{
RoundId = mostAdvancedRound.State.RoundId;
- SuccessfulRoundCount = mostAdvancedRound.State.SuccessfulRoundCount;
if (!Global.ChaumianClient.State.IsInErrorState)
{
Phase = mostAdvancedRound.State.Phase;
+ RoundTimesout = mostAdvancedRound.State.InputRegistrationTimesout;
}
this.RaisePropertyChanged(nameof(Phase));
+ this.RaisePropertyChanged(nameof(RoundTimesout));
PeersRegistered = mostAdvancedRound.State.RegisteredPeerCount;
PeersNeeded = mostAdvancedRound.State.RequiredPeerCount;
}
@@ -377,18 +397,24 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
set => this.RaiseAndSetIfChanged(ref _roundId, value);
}
- public int SuccessfulRoundCount
- {
- get => _successfulRoundCount;
- set => this.RaiseAndSetIfChanged(ref _successfulRoundCount, value);
- }
-
public CcjRoundPhase Phase
{
get => _phase;
set => this.RaiseAndSetIfChanged(ref _phase, value);
}
+ public DateTimeOffset RoundTimesout
+ {
+ get => _roundTimesout;
+ set => this.RaiseAndSetIfChanged(ref _roundTimesout, value);
+ }
+
+ public TimeSpan TimeLeftTillRoundTimeout
+ {
+ get => _timeLeftTillRoundTimeout;
+ set => this.RaiseAndSetIfChanged(ref _timeLeftTillRoundTimeout, value);
+ }
+
public Money RequiredBTC
{
get => _requiredBTC;

ADD
WalletWasabi.Gui/Converters/TimeSpanStringConverter.cs
+using Avalonia.Data.Converters;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+
+namespace WalletWasabi.Gui.Converters
+{
+ public class TimeSpanStringConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is TimeSpan ts)
+ {
+ var builder = new StringBuilder();
+ if (ts.Days != 0)
+ {
+ if (ts.Days == 1)
+ {
+ builder.Append($"{ts.Days} day ");
+ }
+ else
+ {
+ builder.Append($"{ts.Days} days ");
+ }
+ }
+ if (ts.Hours != 0)
+ {
+ if (ts.Hours == 1)
+ {
+ builder.Append($"{ts.Hours} hour ");
+ }
+ else
+ {
+ builder.Append($"{ts.Hours} hours ");
+ }
+ }
+ if (ts.Minutes != 0)
+ {
+ if (ts.Minutes == 1)
+ {
+ builder.Append($"{ts.Minutes} minute ");
+ }
+ else
+ {
+ builder.Append($"{ts.Minutes} minutes ");
+ }
+ }
+ if (ts.Seconds != 0)
+ {
+ if (ts.Seconds == 1)
+ {
+ builder.Append($"{ts.Seconds} second");
+ }
+ else
+ {
+ builder.Append($"{ts.Seconds} seconds");
+ }
+ }
+ return builder.ToString();
+ }
+
+ throw new InvalidOperationException();
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}

MODIFY
WalletWasabi/Models/ChaumianCoinJoin/CcjRound.cs
WalletWasabi/Models/ChaumianCoinJoin/CcjRound.cs
@@ -141,6 +141,7 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
public TimeSpan AliceRegistrationTimeout => ConnectionConfirmationTimeout;
public TimeSpan InputRegistrationTimeout { get; }
+ public DateTimeOffset InputRegistrationTimesout { get; set; }
public TimeSpan ConnectionConfirmationTimeout { get; }
@@ -166,6 +167,7 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
CoordinatorFeePercent = (decimal)config.CoordinatorFeePercent;
AnonymitySet = (int)config.AnonymitySet;
InputRegistrationTimeout = TimeSpan.FromSeconds((long)config.InputRegistrationTimeout);
+ SetInputRegistrationTimesout();
ConnectionConfirmationTimeout = TimeSpan.FromSeconds((long)config.ConnectionConfirmationTimeout);
OutputRegistrationTimeout = TimeSpan.FromSeconds((long)config.OutputRegistrationTimeout);
SigningTimeout = TimeSpan.FromSeconds((long)config.SigningTimeout);
@@ -206,6 +208,11 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
}
}
+ private void SetInputRegistrationTimesout()
+ {
+ InputRegistrationTimesout = DateTimeOffset.UtcNow + InputRegistrationTimeout;
+ }
+
public static ConcurrentDictionary<(long roundId, CcjRoundPhase phase), DateTimeOffset> PhaseTimeoutLog { get; } = new ConcurrentDictionary<(long roundId, CcjRoundPhase phase), DateTimeOffset>();
public async Task ExecuteNextPhaseAsync(CcjRoundPhase expectedPhase, Money feePerInputs = null, Money feePerOutputs = null)
@@ -428,7 +435,10 @@ namespace WalletWasabi.Models.ChaumianCoinJoin
switch (expectedPhase)
{
case CcjRoundPhase.InputRegistration:
+ {
+ SetInputRegistrationTimesout(); // Update it, it's going to be slightly more accurate.
timeout = InputRegistrationTimeout;
+ }
break;
case CcjRoundPhase.ConnectionConfirmation:

MODIFY
WalletWasabi/Services/CcjClient.cs
WalletWasabi/Services/CcjClient.cs
@@ -5,6 +5,7 @@ using Nito.AsyncEx;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -195,6 +196,7 @@ namespace WalletWasabi.Services
private async Task TryProcessStatusAsync(IEnumerable<CcjRunningRoundState> states)
{
states = states ?? Enumerable.Empty<CcjRunningRoundState>();
+
if (Interlocked.Read(ref _statusProcessing) == 1) // It's ok to wait for status processing next time.
{
return;

Implement countdown