early work for privacy suggestions flyout.
MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/Send/TransactionPreviewViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/Send/TransactionPreviewViewModel.cs
using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
@@ -22,6 +24,171 @@ using WalletWasabi.Wallets;
namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
{
+ public class SuggestionViewModel : ViewModelBase
+ {
+ public string Suggestion { get; set; }
+
+ public BuildTransactionResult TransactionResult { get; protected set; }
+ }
+
+ public partial class ChangeAvoidanceSuggestionViewModel : SuggestionViewModel
+ {
+ [AutoNotify] private string _amount;
+ [AutoNotify] private string _amountFiat;
+ [AutoNotify] private List<PrivacySuggestionBenefit> _benefits;
+ [AutoNotify] private PrivacyOptimisationLevel _optimisationLevel;
+ [AutoNotify] private bool _optimisationLevelGood;
+
+ public ChangeAvoidanceSuggestionViewModel(decimal originalAmount,
+ BuildTransactionResult transactionResult,
+ PrivacyOptimisationLevel optimisationLevel,
+ decimal fiatExchangeRate,
+ params PrivacySuggestionBenefit[] benefits)
+ {
+ TransactionResult = transactionResult;
+ _optimisationLevel = optimisationLevel;
+ _benefits = benefits.ToList();
+
+ decimal total = transactionResult.CalculateDestinationAmount().ToDecimal(MoneyUnit.BTC);
+
+ var fiatTotal = total * fiatExchangeRate;
+
+ _amountFiat = total.GenerateFiatText(fiatExchangeRate, "USD");
+ _optimisationLevelGood = optimisationLevel == PrivacyOptimisationLevel.Better;
+
+ if (_optimisationLevelGood)
+ {
+ var fiatOriginal = originalAmount * fiatExchangeRate;
+ var fiatDifference = fiatTotal - fiatOriginal;
+
+ var difference = (fiatDifference > 0
+ ? $"{fiatDifference.GenerateFiatText("USD")} More"
+ : $"{Math.Abs(fiatDifference).GenerateFiatText("USD")} Less")
+ .Replace("(", "").Replace(")", "");
+
+ _benefits.Add(new(false, difference));
+ }
+ else
+ {
+ // This is just to pad the control.
+ _benefits.Add(new(false, " "));
+ }
+
+ _amount = $"{total}";
+ }
+
+
+
+ private static IEnumerable<ChangeAvoidanceSuggestionViewModel> NormalizeSuggestions(
+ IEnumerable<ChangeAvoidanceSuggestionViewModel> suggestions, ChangeAvoidanceSuggestionViewModel defaultSuggestion)
+ {
+ var normalized = suggestions
+ .OrderBy(x => x.TransactionResult.CalculateDestinationAmount())
+ .ToList();
+
+ if (normalized.Count == 3)
+ {
+ var index = normalized.IndexOf(defaultSuggestion);
+
+ switch (index)
+ {
+ case 1:
+ break;
+
+ case 0:
+ normalized = normalized.Take(2).ToList();
+ break;
+
+ case 2:
+ normalized = normalized.Skip(1).ToList();
+ break;
+ }
+ }
+
+ return normalized;
+ }
+
+ public static async Task<(ChangeAvoidanceSuggestionViewModel preSelected, IEnumerable<ChangeAvoidanceSuggestionViewModel> items)> GenerateSuggestions(
+ TransactionInfo transactionInfo, Wallet wallet, BuildTransactionResult requestedTransaction)
+ {
+ var intent = new PaymentIntent(
+ transactionInfo.Address,
+ MoneyRequest.CreateAllRemaining(subtractFee: true),
+ transactionInfo.UserLabels);
+
+ ChangeAvoidanceSuggestionViewModel? smallerSuggestion = null;
+
+ if (requestedTransaction.SpentCoins.Count() > 1)
+ {
+ var smallerTransaction = await Task.Run(() => wallet.BuildTransaction(
+ wallet.Kitchen.SaltSoup(),
+ intent,
+ FeeStrategy.CreateFromFeeRate(transactionInfo.FeeRate),
+ allowUnconfirmed: true,
+ requestedTransaction
+ .SpentCoins
+ .OrderBy(x => x.Amount)
+ .Skip(1)
+ .Select(x => x.OutPoint)));
+
+ smallerSuggestion = new ChangeAvoidanceSuggestionViewModel(
+ transactionInfo.Amount.ToDecimal(MoneyUnit.BTC), smallerTransaction,
+ PrivacyOptimisationLevel.Better, wallet.Synchronizer.UsdExchangeRate,
+ new PrivacySuggestionBenefit(true, "Improved Privacy"));
+ }
+
+ var defaultSelection = new ChangeAvoidanceSuggestionViewModel(
+ transactionInfo.Amount.ToDecimal(MoneyUnit.BTC), requestedTransaction,
+ PrivacyOptimisationLevel.Standard, wallet.Synchronizer.UsdExchangeRate,
+ new PrivacySuggestionBenefit(false, "As Requested"));
+
+ var largerTransaction = await Task.Run(() => wallet.BuildTransaction(
+ wallet.Kitchen.SaltSoup(),
+ intent,
+ FeeStrategy.CreateFromFeeRate(transactionInfo.FeeRate),
+ true,
+ requestedTransaction.SpentCoins.Select(x => x.OutPoint)));
+
+ var largerSuggestion = new ChangeAvoidanceSuggestionViewModel(
+ transactionInfo.Amount.ToDecimal(MoneyUnit.BTC), largerTransaction,
+ PrivacyOptimisationLevel.Better, wallet.Synchronizer.UsdExchangeRate,
+ new PrivacySuggestionBenefit(true, "Improved Privacy"));
+
+ // There are several scenarios, both the alternate suggestions are <, or >, or 1 < and 1 >.
+ // We sort them and add the suggestions accordingly.
+ var suggestions = new List<ChangeAvoidanceSuggestionViewModel> {defaultSelection, largerSuggestion};
+
+ if (smallerSuggestion is { })
+ {
+ suggestions.Add(smallerSuggestion);
+ }
+
+ var results = new List<ChangeAvoidanceSuggestionViewModel>();
+
+ foreach (var suggestion in NormalizeSuggestions(suggestions, defaultSelection))
+ {
+ results.Add(suggestion);
+ }
+
+ return (defaultSelection, results);
+ }
+ }
+
+ public partial class PrivacySuggestionsFlyoutViewModel : ViewModelBase
+ {
+ [AutoNotify] private SuggestionViewModel? _previewSuggestion;
+ [AutoNotify] private SuggestionViewModel? _selectedSuggestion;
+ [AutoNotify] private bool _isOpen;
+
+ public PrivacySuggestionsFlyoutViewModel()
+ {
+ Suggestions = new ObservableCollection<SuggestionViewModel>();
+ }
+
+ public ObservableCollection<SuggestionViewModel> Suggestions { get; }
+
+ }
+
[NavigationMetaData(Title = "Transaction Preview")]
public partial class TransactionPreviewViewModel : RoutableViewModel
{
@@ -39,7 +206,6 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
[AutoNotify] private bool _adjustFeeAvailable;
[AutoNotify] private bool _maxPrivacy;
[AutoNotify] private bool _issuesTooltip;
- [AutoNotify] private OptimisePrivacyViewModel? _changeAvoidance;
public TransactionPreviewViewModel(Wallet wallet, TransactionInfo info)
{
@@ -47,6 +213,8 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
_labels = SmartLabel.Empty;
_info = info;
+ PrivacySuggestions = new PrivacySuggestionsFlyoutViewModel();
+
SetupCancel(enableCancel: true, enableCancelOnEscape: true, enableCancelOnPressed: false);
EnableBack = true;
@@ -83,6 +251,8 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
ChangePocketsCommand = ReactiveCommand.CreateFromTask(OnChangePocketsAsync);
}
+ public PrivacySuggestionsFlyoutViewModel PrivacySuggestions { get; }
+
public bool PreferPsbtWorkflow => _wallet.KeyManager.PreferPsbtWorkflow;
public string AddressText { get; }
@@ -305,9 +475,17 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
if (TransactionHasChange)
{
- ChangeAvoidance = new OptimisePrivacyViewModel(_wallet, _info, _transaction!);
+ RxApp.MainThreadScheduler.Schedule(async () =>
+ {
+ var (selected, suggestions) = await ChangeAvoidanceSuggestionViewModel.GenerateSuggestions(_info, _wallet, _transaction);
- ChangeAvoidance.OnNavigatedTo(false);
+ foreach (var suggestion in suggestions)
+ {
+ PrivacySuggestions.Suggestions.Add(suggestion);
+ }
+
+ PrivacySuggestions.SelectedSuggestion = selected;
+ });
}
TransactionHasPockets = !_info.IsPrivatePocketUsed;

MODIFY
WalletWasabi.Fluent/Views/Wallets/Send/PrivacySuggestionControl.axaml
WalletWasabi.Fluent/Views/Wallets/Send/PrivacySuggestionControl.axaml
xmlns:vm="using:WalletWasabi.Fluent.ViewModels.Wallets.Send"
xmlns:models="clr-namespace:WalletWasabi.Fluent.Models"
mc:Ignorable="d"
- x:DataType="vm:PrivacySuggestionControlViewModel"
+ x:DataType="vm:ChangeAvoidanceSuggestionViewModel"
x:CompileBindings="True"
ClipToBounds="False"
x:Class="WalletWasabi.Fluent.Views.Wallets.Send.PrivacySuggestionControl">
<Border Name="PART_DecorationBorder" />
<Border Name="PART_MainContentBorder">
<StackPanel Spacing="20">
- <Panel MinHeight="150" MinWidth="150">
+ <Panel MinHeight="80" MinWidth="100">
<StackPanel Spacing="10" VerticalAlignment="Top">
<Panel>
- <Ellipse Stroke="{DynamicResource TextForegroundColor}" StrokeThickness="4" Height="115" Width="115" />
- <Ellipse Stroke="{DynamicResource TextForegroundColor}" StrokeThickness="3" StrokeDashArray="2.0, 1.5"
- Height="100" Width="100" />
- <PathIcon Data="{StaticResource btc_logo}" Height="50" Width="50"
+ <PathIcon Data="{StaticResource btc_logo}" Height="25" Width="25"
Foreground="{DynamicResource TextForegroundColor}" />
- <Viewbox IsVisible="{Binding OptimisationLevelGood}" Height="48" Width="48" Margin="0 0 10 2"
+ <Viewbox IsVisible="{Binding OptimisationLevelGood}" Height="24" Width="24" Margin="0 0 10 2"
HorizontalAlignment="Right" VerticalAlignment="Bottom">
<Panel>
<Path

MODIFY
WalletWasabi.Fluent/Views/Wallets/Send/TransactionPreviewView.axaml
WalletWasabi.Fluent/Views/Wallets/Send/TransactionPreviewView.axaml
</Interaction.Behaviors>
<FlyoutBase.AttachedFlyout>
<Flyout Placement="Bottom" ShowMode="TransientWithDismissOnPointerMoveAway">
+ <Panel DataContext="{Binding PrivacySuggestions}">
<DockPanel>
- <TextBlock Text="Some optional suggestions to improve your privacy even more" DockPanel.Dock="Top" />
- <send:OptimisePrivacyView DataContext="{Binding ChangeAvoidance}" />
+ <TextBlock Text="Suggestions to improve your privacy!" DockPanel.Dock="Top" />
+
+ <ListBox DockPanel.Dock="Top" Background="Transparent" VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ Items="{Binding Suggestions}" SelectedItem="{Binding SelectedSuggestion}">
+ <ListBox.Styles>
+ <Style Selector="ListBoxItem">
+ <Setter Property="Cursor" Value="Hand" />
+ </Style>
+ <Style Selector="ListBoxItem /template/ ContentPresenter#PART_ContentPresenter">
+ <Setter Property="Background" Value="Transparent" />
+ </Style>
+ <Style Selector="ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
+ <Setter Property="Background" Value="Transparent" />
+ </Style>
+ <Style Selector="ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
+ <Setter Property="Background" Value="Transparent" />
+ </Style>
+
+ <Style Selector="ListBoxItem Border#PART_MainContentBorder">
+ <Setter Property="Padding" Value="30,45,30,40" />
+ <Setter Property="CornerRadius" Value="4" />
+ <Setter Property="Margin" Value="5" />
+ </Style>
+
+ <Style Selector="ListBoxItem Border#PART_DecorationBorder">
+ <Setter Property="Padding" Value="15 30 15 30" />
+ <Setter Property="CornerRadius" Value="4" />
+ <Setter Property="Margin" Value="14" />
+
+ <Setter Property="BorderBrush" Value="#14FFFFFF" />
+ <Setter Property="BorderThickness" Value="1" />
+ <Setter Property="Background" Value="{StaticResource OptimizePrivacyOptionSelectedColor}" />
+ <Setter Property="BoxShadow" Value="{StaticResource OptimizePrivacyOptionBoxShadow1}" />
+
+ <Setter Property="Opacity" Value="0" />
+
+ <Setter Property="Transitions">
+ <Transitions>
+ <DoubleTransition Property="Opacity"
+ Easing="{StaticResource FluentEasing}"
+ Duration="0:0:0.25" />
+ </Transitions>
+ </Setter>
+ </Style>
+
+ <Style Selector="ListBoxItem:selected Border#PART_DecorationBorder">
+ <Setter Property="Opacity" Value="1" />
+ </Style>
+ </ListBox.Styles>
+ <ListBox.ItemsPanel>
+ <ItemsPanelTemplate>
+ <WrapPanel Orientation="Horizontal" />
+ </ItemsPanelTemplate>
+ </ListBox.ItemsPanel>
+ <ListBox.DataTemplates>
+ <DataTemplate DataType="vm:ChangeAvoidanceSuggestionViewModel">
+ <send:PrivacySuggestionControl />
+ </DataTemplate>
+ </ListBox.DataTemplates>
+ </ListBox>
</DockPanel>
+ </Panel>
+
</Flyout>
</FlyoutBase.AttachedFlyout>
<Panel.IsVisible>

early work for privacy suggestions flyout.