dialog based transaction building.
MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/Send/OptimisePrivacyViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/Send/OptimisePrivacyViewModel.cs
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
+using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq;
@@ -11,13 +12,14 @@ using ReactiveUI;
using WalletWasabi.Blockchain.TransactionBuilding;
using WalletWasabi.Fluent.Helpers;
using WalletWasabi.Fluent.Models;
+using WalletWasabi.Fluent.ViewModels.Dialogs.Base;
using WalletWasabi.Fluent.ViewModels.Navigation;
using WalletWasabi.Wallets;
namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
{
[NavigationMetaData(Title = "Optimise your privacy")]
- public partial class OptimisePrivacyViewModel : RoutableViewModel
+ public partial class OptimisePrivacyViewModel : DialogViewModelBase<BuildTransactionResult>
{
private readonly TransactionInfo _transactionInfo;
private readonly Wallet _wallet;
@@ -57,8 +59,7 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
_transactionInfo.UserDidntRequestOptimisation =
_requestedTransaction == SelectedPrivacySuggestion!.TransactionResult;
- Navigate().To(new TransactionPreviewViewModel(_wallet, transactionInfo,
- SelectedPrivacySuggestion!.TransactionResult));
+ Close(DialogResultKind.Normal, SelectedPrivacySuggestion!.TransactionResult);
}
protected override void OnNavigatedTo(bool inHistory, CompositeDisposable disposables)
@@ -67,13 +68,6 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
if (!inHistory)
{
- if (_transactionInfo.UserDidntRequestOptimisation)
- {
- Navigate().To(new TransactionPreviewViewModel(_wallet, _transactionInfo, _requestedTransaction));
-
- return;
- }
-
RxApp.MainThreadScheduler.Schedule(async () =>
{
IsBusy = true;

MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/Send/PrivacyControlViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/Send/PrivacyControlViewModel.cs
using System;
using System.Collections.ObjectModel;
using System.Linq;
+using System.Reactive;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using DynamicData;
using DynamicData.Aggregation;
using NBitcoin;
using ReactiveUI;
-using WalletWasabi.Exceptions;
using WalletWasabi.Fluent.Helpers;
-using WalletWasabi.Fluent.Models;
-using WalletWasabi.Fluent.ViewModels.Dialogs;
-using WalletWasabi.Fluent.ViewModels.Navigation;
+using WalletWasabi.Fluent.ViewModels.Dialogs.Base;
using WalletWasabi.Logging;
using WalletWasabi.Wallets;
namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
{
[NavigationMetaData(Title = "Privacy Control")]
- public partial class PrivacyControlViewModel : RoutableViewModel
+ public partial class PrivacyControlViewModel : DialogViewModelBase<Unit>
{
private readonly Wallet _wallet;
+ private readonly TransactionInfo _transactionInfo;
private readonly SourceList<PocketViewModel> _pocketSource;
private readonly ReadOnlyObservableCollection<PocketViewModel> _pockets;
- private bool _silentNavigation;
+ private readonly IObservableList<PocketViewModel> _selectedPockets;
[AutoNotify] private decimal _stillNeeded;
[AutoNotify] private bool _enoughSelected;
@@ -33,6 +32,7 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
public PrivacyControlViewModel(Wallet wallet, TransactionInfo transactionInfo)
{
+ _transactionInfo = transactionInfo;
_wallet = wallet;
_pocketSource = new SourceList<PocketViewModel>();
@@ -45,12 +45,12 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
.AutoRefresh()
.Filter(x => x.IsSelected);
- var selectedList = selected.AsObservableList();
+ _selectedPockets = selected.AsObservableList();
selected.Sum(x => x.TotalBtc)
.Subscribe(x =>
{
- IsWarningOpen = selectedList.Count > 1 && selectedList.Items.Any(x => x.Labels == CoinPocketHelper.PrivateFundsText);
+ IsWarningOpen = _selectedPockets.Count > 1 && _selectedPockets.Items.Any(x => x.Labels == CoinPocketHelper.PrivateFundsText);
StillNeeded = transactionInfo.Amount.ToDecimal(MoneyUnit.BTC) - x;
EnoughSelected = StillNeeded <= 0;
@@ -61,19 +61,27 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
SetupCancel(enableCancel: false, enableCancelOnEscape: true, enableCancelOnPressed: false);
EnableBack = true;
- NextCommand = ReactiveCommand.CreateFromTask(
- async () => await OnNextAsync(transactionInfo, selectedList),
+ NextCommand = ReactiveCommand.Create(Complete,
this.WhenAnyValue(x => x.EnoughSelected));
EnableAutoBusyOn(NextCommand);
}
+ private void Complete()
+ {
+ _transactionInfo.Coins = _selectedPockets.Items.SelectMany(x => x.Coins).ToArray();
+
+ Close();
+ }
+
public ReadOnlyObservableCollection<PocketViewModel> Pockets => _pockets;
private async Task OnNextAsync(TransactionInfo transactionInfo,
IObservableList<PocketViewModel> selectedList)
{
- transactionInfo.Coins = selectedList.Items.SelectMany(x => x.Coins).ToArray();
+ Complete();
+
+ return;
try
{
@@ -81,11 +89,11 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
if (transactionInfo.PayJoinClient is { })
{
- await BuildTransactionAsPayJoinAsync(transactionInfo);
+ //await BuildTransactionAsPayJoinAsync(transactionInfo);
}
else
{
- await BuildTransactionAsNormalAsync(transactionInfo);
+ //await BuildTransactionAsNormalAsync(transactionInfo);
}
}
catch (Exception ex)
@@ -100,7 +108,7 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
}
}
- private async Task BuildTransactionAsNormalAsync(TransactionInfo transactionInfo)
+ /*private async Task BuildTransactionAsNormalAsync(TransactionInfo transactionInfo)
{
try
{
@@ -138,7 +146,7 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
Navigate().BackTo<SendViewModel>();
}
- }
+ }*/
protected override void OnNavigatedTo(bool isInHistory, CompositeDisposable disposables)
{
@@ -164,19 +172,8 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
if (_pocketSource.Count == 1)
{
_pocketSource.Items.First().IsSelected = true;
- _silentNavigation = true;
- if (isInHistory)
- {
- Navigate().Back();
- }
- else
- {
- if (NextCommand is {} cmd && cmd.CanExecute(default))
- {
- cmd.Execute(default);
- }
- }
+ Complete();
}
}
}

MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/Send/SendFeeViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/Send/SendFeeViewModel.cs
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Reactive.Concurrency;
+using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
-using System.Threading.Tasks;
using NBitcoin;
using ReactiveUI;
using WalletWasabi.Blockchain.Analysis.FeesEstimation;
-using WalletWasabi.Exceptions;
-using WalletWasabi.Fluent.Helpers;
-using WalletWasabi.Fluent.Models;
-using WalletWasabi.Fluent.ViewModels.Dialogs;
-using WalletWasabi.Fluent.ViewModels.Navigation;
-using WalletWasabi.Logging;
+using WalletWasabi.Fluent.ViewModels.Dialogs.Base;
using WalletWasabi.Wallets;
namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
@@ -25,7 +18,7 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
NavBarPosition = NavBarPosition.None,
Searchable = false,
NavigationTarget = NavigationTarget.DialogScreen)]
- public partial class SendFeeViewModel : RoutableViewModel
+ public partial class SendFeeViewModel : DialogViewModelBase<Unit>
{
private readonly Wallet _wallet;
private readonly TransactionInfo _transactionInfo;
@@ -46,56 +39,25 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
SetupCancel(false, true, false);
EnableBack = true;
- NextCommand = ReactiveCommand.CreateFromTask(async () =>
+ NextCommand = ReactiveCommand.Create( () =>
{
_lastConfirmationTarget = FeeChart.CurrentConfirmationTarget;
_transactionInfo.ConfirmationTimeSpan = CalculateConfirmationTime(_lastConfirmationTarget);
- await OnNextAsync();
+ Complete();
});
}
public FeeChartViewModel FeeChart { get; }
- private async Task OnNextAsync()
+
+ private void Complete()
{
var transactionInfo = _transactionInfo;
- var targetAnonymitySet = _wallet.ServiceConfiguration.GetMixUntilAnonymitySetValue();
- var mixedCoins = _wallet.Coins.Where(x => x.HdPubKey.AnonymitySet >= targetAnonymitySet).ToList();
- var totalMixedCoinsAmount = Money.FromUnit(mixedCoins.Sum(coin => coin.Amount), MoneyUnit.Satoshi);
- transactionInfo.Coins = mixedCoins;
transactionInfo.FeeRate = new FeeRate(FeeChart.GetSatoshiPerByte(FeeChart.CurrentConfirmationTarget));
- if (transactionInfo.FeeRate == _entryFeeRate)
- {
- Navigate().Back();
- return;
- }
-
- if (transactionInfo.Amount > totalMixedCoinsAmount)
- {
- Navigate().To(new PrivacyControlViewModel(_wallet, transactionInfo), _isSilent ? NavigationMode.Skip : NavigationMode.Normal);
- return;
- }
-
- try
- {
- if (_transactionInfo.PayJoinClient is { })
- {
- await BuildTransactionAsPayJoinAsync(transactionInfo);
- }
- else
- {
- await BuildTransactionAsNormalAsync(transactionInfo, totalMixedCoinsAmount);
- }
- }
- catch (Exception ex)
- {
- Logger.LogError(ex);
- await ShowErrorAsync("Transaction Building", ex.ToUserFriendlyString(),
- "Wasabi was unable to create your transaction.");
- }
+ Close();
}
protected override void OnNavigatedTo(bool isInHistory, CompositeDisposable disposables)
@@ -129,59 +91,12 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
}
if (_isSilent)
- {
- RxApp.MainThreadScheduler.Schedule(async () =>
{
// TODO implement algorithm to intelligently select fees.
_lastConfirmationTarget = 1;
_transactionInfo.ConfirmationTimeSpan = CalculateConfirmationTime(_lastConfirmationTarget);
- await OnNextAsync();
- });
- }
- }
-
- private async Task BuildTransactionAsNormalAsync(TransactionInfo transactionInfo, Money totalMixedCoinsAmount)
- {
- try
- {
- var txRes = await Task.Run(() => TransactionHelpers.BuildTransaction(_wallet, transactionInfo));
- Navigate().To(new OptimisePrivacyViewModel(_wallet, transactionInfo, txRes), _isSilent ? NavigationMode.Skip : NavigationMode.Normal);
- }
- catch (InsufficientBalanceException)
- {
- var txRes = await Task.Run(() => TransactionHelpers.BuildTransaction(_wallet, transactionInfo.Address,
- totalMixedCoinsAmount, transactionInfo.Labels, transactionInfo.FeeRate!, transactionInfo.Coins,
- subtractFee: true));
- var dialog = new InsufficientBalanceDialogViewModel(BalanceType.Private, txRes,
- _wallet.Synchronizer.UsdExchangeRate);
- var result = await NavigateDialogAsync(dialog, NavigationTarget.DialogScreen);
-
- if (result.Result)
- {
- Navigate().To(new OptimisePrivacyViewModel(_wallet, transactionInfo, txRes), _isSilent ? NavigationMode.Skip : NavigationMode.Normal);
- }
- else
- {
- Navigate().To(new PrivacyControlViewModel(_wallet, transactionInfo), _isSilent ? NavigationMode.Skip : NavigationMode.Normal);
- }
- }
- }
-
- private async Task BuildTransactionAsPayJoinAsync(TransactionInfo transactionInfo)
- {
- try
- {
- // Do not add the PayJoin client yet, it will be added before broadcasting.
- var txRes = await Task.Run(() => TransactionHelpers.BuildTransaction(_wallet, transactionInfo));
- Navigate().To(new TransactionPreviewViewModel(_wallet, transactionInfo, txRes), _isSilent ? NavigationMode.Skip : NavigationMode.Normal);
- }
- catch (InsufficientBalanceException)
- {
- await ShowErrorAsync("Transaction Building",
- "There are not enough private funds to cover the transaction fee",
- "Wasabi was unable to create your transaction.");
- Navigate().To(new PrivacyControlViewModel(_wallet, transactionInfo), _isSilent ? NavigationMode.Skip : NavigationMode.Normal);
+ Complete();
}
}

MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/Send/SendViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/Send/SendViewModel.cs
@@ -118,7 +118,7 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
NextCommand = ReactiveCommand.Create(() =>
{
- Navigate().To(new SendFeeViewModel(_wallet, _transactionInfo, true));
+ Navigate().To(new TransactionPreviewViewModel(wallet, _transactionInfo));
}, nextCommandCanExecute);
EnableAutoBusyOn(NextCommand);

MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/Send/TransactionPreviewViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/Send/TransactionPreviewViewModel.cs
using System;
+using System.Linq;
+using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using System.Windows.Input;
@@ -8,8 +10,10 @@ using WalletWasabi.Blockchain.Analysis.Clustering;
using WalletWasabi.Blockchain.TransactionBuilding;
using WalletWasabi.Blockchain.Transactions;
using WalletWasabi.CoinJoin.Client.Clients.Queuing;
+using WalletWasabi.Exceptions;
using WalletWasabi.Fluent.Helpers;
using WalletWasabi.Fluent.Models;
+using WalletWasabi.Fluent.ViewModels.Dialogs;
using WalletWasabi.Fluent.ViewModels.Dialogs.Base;
using WalletWasabi.Fluent.ViewModels.Navigation;
using WalletWasabi.Fluent.ViewModels.TransactionBroadcasting;
@@ -23,12 +27,15 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
{
private readonly Wallet _wallet;
private readonly TransactionInfo _info;
+ private BuildTransactionResult? _transaction;
[AutoNotify] private string _confirmationTimeText;
[AutoNotify] private string _nextButtonText;
[AutoNotify] private SmartLabel _labels;
+ [AutoNotify] private string _amountText;
+ [AutoNotify] private string _feeText;
- public TransactionPreviewViewModel(Wallet wallet, TransactionInfo info, BuildTransactionResult transaction)
+ public TransactionPreviewViewModel(Wallet wallet, TransactionInfo info)
{
_wallet = wallet;
_labels = SmartLabel.Empty;
@@ -37,27 +44,17 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
EnableBack = true;
_confirmationTimeText = "";
- var destinationAmount = transaction.CalculateDestinationAmount().ToDecimal(MoneyUnit.BTC);
- var btcAmountText = $"{destinationAmount} bitcoins ";
- var fiatAmountText = destinationAmount.GenerateFiatText(_wallet.Synchronizer.UsdExchangeRate, "USD");
- AmountText = $"{btcAmountText}{fiatAmountText}";
-
AddressText = info.Address.ToString();
- var fee = transaction.Fee;
- var btcFeeText = $"{fee.ToDecimal(MoneyUnit.Satoshi)} sats ";
- var fiatFeeText = fee.ToDecimal(MoneyUnit.BTC).GenerateFiatText(_wallet.Synchronizer.UsdExchangeRate, "USD");
- FeeText = $"{btcFeeText}{fiatFeeText}";
-
PayJoinUrl = info.PayJoinClient?.PaymentUrl.AbsoluteUri;
IsPayJoin = PayJoinUrl is not null;
if (PreferPsbtWorkflow)
{
- SkipCommand = ReactiveCommand.CreateFromTask(async () => await OnConfirmAsync(transaction));
+ SkipCommand = ReactiveCommand.CreateFromTask(async () => await OnConfirmAsync(_transaction));
NextCommand = ReactiveCommand.CreateFromTask(async () =>
{
- var saved = await TransactionHelpers.ExportTransactionToBinaryAsync(transaction);
+ var saved = await TransactionHelpers.ExportTransactionToBinaryAsync(_transaction);
if (saved)
{
@@ -68,36 +65,154 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Send
}
else
{
- NextCommand = ReactiveCommand.CreateFromTask(async () => await OnConfirmAsync(transaction));
+ NextCommand = ReactiveCommand.CreateFromTask(async () => await OnConfirmAsync(_transaction));
_nextButtonText = "Confirm";
}
- AdjustFeeCommand = ReactiveCommand.Create(() =>
+ AdjustFeeCommand = ReactiveCommand.CreateFromTask(async () =>
{
- Navigate().To(new SendFeeViewModel(wallet, info, false));
+ await NavigateDialogAsync(new SendFeeViewModel(wallet, info, false));
+
+ // TODO update the preview.
+
+ this.RaisePropertyChanged();
});
}
public bool PreferPsbtWorkflow => _wallet.KeyManager.PreferPsbtWorkflow;
- public string AmountText { get; }
-
public string AddressText { get; }
- public string FeeText { get; }
-
public string? PayJoinUrl { get; }
public bool IsPayJoin { get; }
public ICommand AdjustFeeCommand { get; }
+ private async Task<BuildTransactionResult?> BuildTransactionAsync()
+ {
+ var transactionInfo = _info;
+ var targetAnonymitySet = _wallet.ServiceConfiguration.GetMixUntilAnonymitySetValue();
+ var mixedCoins = _wallet.Coins.Where(x => x.HdPubKey.AnonymitySet >= targetAnonymitySet).ToList();
+ var totalMixedCoinsAmount = Money.FromUnit(mixedCoins.Sum(coin => coin.Amount), MoneyUnit.Satoshi);
+ transactionInfo.Coins = mixedCoins;
+
+ await NavigateDialogAsync(new SendFeeViewModel(_wallet, transactionInfo, true));
+
+ if (transactionInfo.Amount > totalMixedCoinsAmount)
+ {
+ await NavigateDialogAsync(new PrivacyControlViewModel(_wallet, transactionInfo));
+ }
+
+ try
+ {
+ if (transactionInfo.PayJoinClient is { })
+ {
+ return await BuildTransactionAsPayJoinAsync(transactionInfo);
+ }
+ else
+ {
+ return await BuildTransactionAsNormalAsync(transactionInfo, totalMixedCoinsAmount);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex);
+
+
+ await ShowErrorAsync("Transaction Building", ex.ToUserFriendlyString(),
+ "Wasabi was unable to create your transaction.");
+
+ return null;
+ }
+ }
+
protected override void OnNavigatedTo(bool isInHistory, CompositeDisposable disposables)
{
base.OnNavigatedTo(isInHistory, disposables);
ConfirmationTimeText = $"Approximately {TextHelpers.TimeSpanToFriendlyString(_info.ConfirmationTimeSpan)} ";
Labels = _info.Labels;
+
+ if (!isInHistory)
+ {
+ RxApp.MainThreadScheduler.Schedule(async () =>
+ {
+ var transaction = await BuildTransactionAsync();
+
+ _transaction = transaction;
+
+ if (transaction is { })
+ {
+ var destinationAmount = transaction.CalculateDestinationAmount().ToDecimal(MoneyUnit.BTC);
+ var btcAmountText = $"{destinationAmount} bitcoins ";
+ var fiatAmountText =
+ destinationAmount.GenerateFiatText(_wallet.Synchronizer.UsdExchangeRate, "USD");
+ AmountText = $"{btcAmountText}{fiatAmountText}";
+
+ var fee = transaction.Fee;
+ var btcFeeText = $"{fee.ToDecimal(MoneyUnit.Satoshi)} sats ";
+ var fiatFeeText = fee.ToDecimal(MoneyUnit.BTC)
+ .GenerateFiatText(_wallet.Synchronizer.UsdExchangeRate, "USD");
+ FeeText = $"{btcFeeText}{fiatFeeText}";
+ }
+ });
+ }
+ }
+
+ private async Task<BuildTransactionResult> BuildTransactionAsNormalAsync(TransactionInfo transactionInfo, Money totalMixedCoinsAmount)
+ {
+ try
+ {
+ var txRes = await Task.Run(() => TransactionHelpers.BuildTransaction(_wallet, transactionInfo));
+
+ return txRes;
+ }
+ catch (InsufficientBalanceException)
+ {
+ return null;
+
+ // @soosr - need help here ;)
+ /*
+ var txRes = await Task.Run(() => TransactionHelpers.BuildTransaction(_wallet, transactionInfo.Address,
+ totalMixedCoinsAmount, transactionInfo.Labels, transactionInfo.FeeRate!, transactionInfo.Coins,
+ subtractFee: true));
+
+ var dialog = new InsufficientBalanceDialogViewModel(BalanceType.Private, txRes,
+ _wallet.Synchronizer.UsdExchangeRate);
+
+ var result = await NavigateDialogAsync(dialog, NavigationTarget.DialogScreen);
+
+ if (result.Result)
+ {
+
+ await NavigateDialogAsync(new OptimisePrivacyViewModel(_wallet, transactionInfo, txRes));
+ }
+ else
+ {
+ await NavigateDialogAsync(new PrivacyControlViewModel(_wallet, transactionInfo));
+ }*/
+ }
+ }
+
+ private async Task<BuildTransactionResult> BuildTransactionAsPayJoinAsync(TransactionInfo transactionInfo)
+ {
+ try
+ {
+ // Do not add the PayJoin client yet, it will be added before broadcasting.
+ var txRes = await Task.Run(() => TransactionHelpers.BuildTransaction(_wallet, transactionInfo));
+
+ return txRes;
+ }
+ catch (InsufficientBalanceException)
+ {
+ await ShowErrorAsync("Transaction Building",
+ "There are not enough private funds to cover the transaction fee",
+ "Wasabi was unable to create your transaction.");
+ //Navigate().To(new PrivacyControlViewModel(_wallet, transactionInfo), _isSilent ? NavigationMode.Skip : NavigationMode.Normal);
+
+ return null;
+ }
}
private async Task OnConfirmAsync(BuildTransactionResult transaction)

dialog based transaction building.