Transaction Preview Undo feature
MODIFY
WalletWasabi.Fluent/Icons/Icons.axaml
WalletWasabi.Fluent/Icons/Icons.axaml
<StreamGeometry x:Key="pause_regular">M17.75,7 C19.5449254,7 21,8.45507456 21,10.25 L21,37.75 C21,39.5449254 19.5449254,41 17.75,41 L12.25,41 C10.4550746,41 9,39.5449254 9,37.75 L9,10.25 C9,8.45507456 10.4550746,7 12.25,7 L17.75,7 Z M35.75,7 C37.5449254,7 39,8.45507456 39,10.25 L39,37.75 C39,39.5449254 37.5449254,41 35.75,41 L30.25,41 C28.4550746,41 27,39.5449254 27,37.75 L27,10.25 C27,8.45507456 28.4550746,7 30.25,7 L35.75,7 Z M17.75,9.5 L12.25,9.5 C11.8357864,9.5 11.5,9.83578644 11.5,10.25 L11.5,37.75 C11.5,38.1642136 11.8357864,38.5 12.25,38.5 L17.75,38.5 C18.1642136,38.5 18.5,38.1642136 18.5,37.75 L18.5,10.25 C18.5,9.83578644 18.1642136,9.5 17.75,9.5 Z M35.75,9.5 L30.25,9.5 C29.8357864,9.5 29.5,9.83578644 29.5,10.25 L29.5,37.75 C29.5,38.1642136 29.8357864,38.5 30.25,38.5 L35.75,38.5 C36.1642136,38.5 36.5,38.1642136 36.5,37.75 L36.5,10.25 C36.5,9.83578644 36.1642136,9.5 35.75,9.5 Z</StreamGeometry>
<StreamGeometry x:Key="stop_regular">M19.25 4.5C19.3881 4.5 19.5 4.61193 19.5 4.75V19.25C19.5 19.3881 19.3881 19.5 19.25 19.5H4.75C4.61193 19.5 4.5 19.3881 4.5 19.25V4.75C4.5 4.61193 4.61193 4.5 4.75 4.5H19.25ZM4.75 3C3.7835 3 3 3.7835 3 4.75V19.25C3 20.2165 3.7835 21 4.75 21H19.25C20.2165 21 21 20.2165 21 19.25V4.75C21 3.7835 20.2165 3 19.25 3H4.75Z</StreamGeometry>
+ <StreamGeometry x:Key="undo_icon_regular">M212.333 224.333H12c-6.627 0-12-5.373-12-12V12C0 5.373 5.373 0 12 0h48c6.627 0 12 5.373 12 12v78.112C117.773 39.279 184.26 7.47 258.175 8.007c136.906.994 246.448 111.623 246.157 248.532C504.041 393.258 393.12 504 256.333 504c-64.089 0-122.496-24.313-166.51-64.215c-5.099-4.622-5.334-12.554-.467-17.42l33.967-33.967c4.474-4.474 11.662-4.717 16.401-.525C170.76 415.336 211.58 432 256.333 432c97.268 0 176-78.716 176-176c0-97.267-78.716-176-176-176c-58.496 0-110.28 28.476-142.274 72.333h98.274c6.627 0 12 5.373 12 12v48c0 6.627-5.373 12-12 12z</StreamGeometry>
<!-- Wallet icons -->
<StreamGeometry x:Key="default_wallet_icon">M32.25 26C31.5596 26 31 26.5596 31 27.25C31 27.9404 31.5596 28.5 32.25 28.5H36.75C37.4404 28.5 38 27.9404 38 27.25C38 26.5596 37.4404 26 36.75 26H32.25ZM6 10V35.75C6 39.2018 8.79822 42 12.25 42H36.75C40.2018 42 43 39.2018 43 35.75V18.25C43 15.2263 40.8528 12.7041 38 12.125V10.25C38 7.90279 36.0972 6 33.75 6H10.25C7.98671 6 6.13664 7.76916 6.00723 10H6ZM35.5 10.25V12H10.25C9.2835 12 8.5 11.2165 8.5 10.25C8.5 9.2835 9.2835 8.5 10.25 8.5H33.75C34.7165 8.5 35.5 9.2835 35.5 10.25ZM8.5 14.5H36.75C38.8211 14.5 40.5 16.1789 40.5 18.25V35.75C40.5 37.8211 38.8211 39.5 36.75 39.5H12.25C10.1789 39.5 8.5 37.8211 8.5 35.75V14.5Z</StreamGeometry>

MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/Send/TransactionPreviewViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/Send/TransactionPreviewViewModel.cs
@@ -26,18 +26,21 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send;
[NavigationMetaData(Title = "Transaction Preview")]
public partial class TransactionPreviewViewModel : RoutableViewModel
{
+ private readonly Stack<(BuildTransactionResult, TransactionInfo)> _undoHistory;
private readonly bool _isFixedAmount;
private readonly Wallet _wallet;
- private readonly TransactionInfo _info;
private readonly BitcoinAddress _destination;
private BuildTransactionResult? _transaction;
+ private TransactionInfo _info;
private CancellationTokenSource _cancellationTokenSource;
[AutoNotify] private string _nextButtonText;
[AutoNotify] private bool _adjustFeeAvailable;
[AutoNotify] private TransactionSummaryViewModel? _displayedTransactionSummary;
+ [AutoNotify] private bool _canUndo;
public TransactionPreviewViewModel(Wallet wallet, TransactionInfo info, BitcoinAddress destination, bool isFixedAmount)
{
+ _undoHistory = new();
_wallet = wallet;
_info = info;
_destination = destination;
@@ -77,8 +80,10 @@ public partial class TransactionPreviewViewModel : RoutableViewModel
if (x is ChangeAvoidanceSuggestionViewModel ca)
{
+ AddToUndoHistory();
_info.ChangelessCoins = ca.TransactionResult.SpentCoins;
- UpdateTransaction(CurrentTransactionSummary, ca.TransactionResult);
+ _info.Amount = ca.TransactionResult.CalculateDestinationAmount();
+ UpdateTransaction(CurrentTransactionSummary, ca.TransactionResult, false);
await PrivacySuggestions.BuildPrivacySuggestionsAsync(_wallet, _info, _destination, ca.TransactionResult, _isFixedAmount, _cancellationTokenSource.Token);
}
@@ -128,6 +133,17 @@ public partial class TransactionPreviewViewModel : RoutableViewModel
await OnAdjustFeeAsync();
}
});
+
+ UndoCommand = ReactiveCommand.CreateFromTask(async () =>
+ {
+ if (_undoHistory.TryPop(out var previous))
+ {
+ _info = previous.Item2;
+ UpdateTransaction(CurrentTransactionSummary, previous.Item1, false);
+ await PrivacySuggestions.BuildPrivacySuggestionsAsync(_wallet, _info, _destination, previous.Item1, _isFixedAmount, _cancellationTokenSource.Token);
+ CanUndo = _undoHistory.Any();
+ }
+ });
}
public TransactionSummaryViewModel CurrentTransactionSummary { get; }
@@ -142,6 +158,8 @@ public partial class TransactionPreviewViewModel : RoutableViewModel
public ICommand AdjustFeeCommand { get; }
+ public ICommand UndoCommand { get; }
+
private async Task ShowAdvancedDialogAsync()
{
var result = await NavigateDialogAsync(new AdvancedSendOptionsViewModel(_info), NavigationTarget.CompactDialogScreen);
@@ -165,20 +183,27 @@ public partial class TransactionPreviewViewModel : RoutableViewModel
}
}
- private void UpdateTransaction(TransactionSummaryViewModel summary, BuildTransactionResult transaction)
+ private void UpdateTransaction(TransactionSummaryViewModel summary, BuildTransactionResult transaction, bool addToUndoHistory = true)
{
if (!summary.IsPreview)
{
+ if (addToUndoHistory && _transaction is { })
+ {
+ AddToUndoHistory();
+ }
+
_transaction = transaction;
}
- summary.UpdateTransaction(transaction);
+ summary.UpdateTransaction(transaction, _info);
DisplayedTransactionSummary = summary;
}
private async Task OnAdjustFeeAsync()
{
+ AddToUndoHistory();
+
var feeRateDialogResult = await NavigateDialogAsync(new SendFeeViewModel(_wallet, _info, false));
if (feeRateDialogResult.Kind == DialogResultKind.Normal && feeRateDialogResult.Result is { } newFeeRate &&
@@ -186,17 +211,23 @@ public partial class TransactionPreviewViewModel : RoutableViewModel
{
_info.FeeRate = feeRateDialogResult.Result;
- await BuildAndUpdateAsync(BuildTransactionReason.FeeChanged);
+ await BuildAndUpdateAsync(BuildTransactionReason.FeeChanged, false);
}
+ else
+ {
+ _undoHistory.Pop();
+ }
+
+ CanUndo = _undoHistory.Any();
}
- private async Task BuildAndUpdateAsync(BuildTransactionReason reason)
+ private async Task BuildAndUpdateAsync(BuildTransactionReason reason, bool addToUndoHistory = true)
{
var newTransaction = await BuildTransactionAsync(reason);
if (newTransaction is { })
{
- UpdateTransaction(CurrentTransactionSummary, newTransaction);
+ UpdateTransaction(CurrentTransactionSummary, newTransaction, addToUndoHistory);
await PrivacySuggestions.BuildPrivacySuggestionsAsync(_wallet, _info, _destination, newTransaction, _isFixedAmount, _cancellationTokenSource.Token);
}
@@ -209,9 +240,10 @@ public partial class TransactionPreviewViewModel : RoutableViewModel
if (selectPocketsDialog.Kind == DialogResultKind.Normal && selectPocketsDialog.Result is { })
{
+ AddToUndoHistory();
_info.Coins = selectPocketsDialog.Result;
_info.ChangelessCoins = Enumerable.Empty<SmartCoin>(); // Clear ChangelessCoins on pocket change, so we calculate the suggestions with the new pocket.
- await BuildAndUpdateAsync(BuildTransactionReason.PocketChanged);
+ await BuildAndUpdateAsync(BuildTransactionReason.PocketChanged, false);
}
}
@@ -509,4 +541,27 @@ public partial class TransactionPreviewViewModel : RoutableViewModel
return transaction;
}
+
+ private void AddToUndoHistory()
+ {
+ _undoHistory.Push((_transaction!, CloneTransactionInfo()));
+ CanUndo = true;
+ }
+
+ private TransactionInfo CloneTransactionInfo()
+ {
+ return new TransactionInfo(_wallet.KeyManager.MinAnonScoreTarget)
+ {
+ Amount = _info.Amount,
+ ChangelessCoins = _info.ChangelessCoins,
+ Coins = _info.Coins,
+ ConfirmationTimeSpan = _info.ConfirmationTimeSpan,
+ FeeRate = _info.FeeRate,
+ IsCustomFeeUsed = _info.IsCustomFeeUsed,
+ MaximumPossibleFeeRate = _info.MaximumPossibleFeeRate,
+ PayJoinClient = _info.PayJoinClient,
+ SubtractFee = _info.SubtractFee,
+ UserLabels = _info.UserLabels
+ };
+ }
}

MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/Send/TransactionSummaryViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/Send/TransactionSummaryViewModel.cs
@@ -13,7 +13,6 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send;
public partial class TransactionSummaryViewModel : ViewModelBase
{
private readonly Wallet _wallet;
- private readonly TransactionInfo _info;
private readonly BitcoinAddress _address;
private BuildTransactionResult? _transaction;
[AutoNotify] private string _amountText = "";
@@ -28,7 +27,6 @@ public partial class TransactionSummaryViewModel : ViewModelBase
{
Parent = parent;
_wallet = wallet;
- _info = info;
_address = address;
IsPreview = isPreview;
@@ -36,6 +34,7 @@ public partial class TransactionSummaryViewModel : ViewModelBase
.Subscribe(_ => MaxPrivacy = !TransactionHasPockets && !TransactionHasChange);
AddressText = _address.ToString();
+
PayJoinUrl = info.PayJoinClient?.PaymentUrl.AbsoluteUri;
IsPayJoin = PayJoinUrl is not null;
}
@@ -50,11 +49,11 @@ public partial class TransactionSummaryViewModel : ViewModelBase
public bool IsPayJoin { get; }
- public void UpdateTransaction(BuildTransactionResult transactionResult)
+ public void UpdateTransaction(BuildTransactionResult transactionResult, TransactionInfo info)
{
_transaction = transactionResult;
- ConfirmationTimeText = $"Approximately {TextHelpers.TimeSpanToFriendlyString(_info.ConfirmationTimeSpan)} ";
+ ConfirmationTimeText = $"Approximately {TextHelpers.TimeSpanToFriendlyString(info.ConfirmationTimeSpan)} ";
var destinationAmount = _transaction.CalculateDestinationAmount().ToDecimal(MoneyUnit.BTC);
var btcAmountText = $"{destinationAmount} BTC ";
@@ -72,8 +71,8 @@ public partial class TransactionSummaryViewModel : ViewModelBase
TransactionHasChange =
_transaction.InnerWalletOutputs.Any(x => x.ScriptPubKey != _address.ScriptPubKey);
- TransactionHasPockets = !_info.IsPrivate;
+ TransactionHasPockets = !info.IsPrivate;
- IsCustomFeeUsed = _info.IsCustomFeeUsed;
+ IsCustomFeeUsed = info.IsCustomFeeUsed;
}
}

MODIFY
WalletWasabi.Fluent/Views/Wallets/Send/TransactionPreviewView.axaml
WalletWasabi.Fluent/Views/Wallets/Send/TransactionPreviewView.axaml
ToolTip.Tip="Your privacy is being protected with payjoin."
IsVisible="{Binding CurrentTransactionSummary.IsPayJoin}" />
+ <Button Command="{Binding UndoCommand}" Classes="plain" Padding="10 0 10 10"
+ ToolTip.Tip="Undo last modification" IsVisible="{Binding CanUndo}">
+ <Viewbox Height="20">
+ <PathIcon Data="{StaticResource undo_icon_regular}" Foreground="{DynamicResource SystemAccentColor}" />
+ </Viewbox>
+ </Button>
+
<Border Padding="10 0 10 10" Cursor="Hand">
<Border.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.Or}">

Transaction Preview Undo feature