private async Task OnNext(Wallet wallet, TransactionInfo transactionInfo, TransactionBroadcaster broadcaster, IObservableList <PocketViewModel> selectedList) { var coins = selectedList.Items.SelectMany(x => x.Coins).ToArray(); try { try { var transactionResult = await Task.Run(() => TransactionHelpers.BuildTransaction(_wallet, transactionInfo.Address, transactionInfo.Amount, transactionInfo.Labels, transactionInfo.FeeRate, coins, subtractFee: false)); Navigate().To(new OptimisePrivacyViewModel(wallet, transactionInfo, broadcaster, transactionResult)); } catch (InsufficientBalanceException) { var transactionResult = await Task.Run(() => TransactionHelpers.BuildTransaction(_wallet, transactionInfo.Address, transactionInfo.Amount, transactionInfo.Labels, transactionInfo.FeeRate, coins, subtractFee: true)); var dialog = new InsufficientBalanceDialogViewModel(BalanceType.Pocket, transactionResult, wallet.Synchronizer.UsdExchangeRate); var result = await NavigateDialog(dialog, NavigationTarget.DialogScreen); if (result.Result) { Navigate().To(new OptimisePrivacyViewModel(wallet, transactionInfo, broadcaster, transactionResult)); } else { Navigate().BackTo <SendViewModel>(); } } } catch (Exception ex) { Logger.LogError(ex); await ShowErrorAsync("Transaction Building", ex.ToUserFriendlyString(), "Wasabi was unable to create your transaction."); Navigate().BackTo <SendViewModel>(); } }
public PrivacyControlViewModel(Wallet wallet, TransactionInfo transactionInfo, bool isSilent) { _wallet = wallet; _transactionInfo = transactionInfo; _isSilent = isSilent; _pocketSource = new SourceList <PocketViewModel>(); _pocketSource.Connect() .Bind(out _pockets) .Subscribe(); var selected = _pocketSource.Connect() .AutoRefresh() .Filter(x => x.IsSelected); _selectedPockets = selected.AsObservableList(); selected.Sum(x => x.TotalBtc) .Subscribe(x => { IsWarningOpen = _selectedPockets.Count > 1 && _selectedPockets.Items.Any(x => x.Labels == CoinPocketHelper.PrivateFundsText); StillNeeded = transactionInfo.Amount.ToDecimal(MoneyUnit.BTC) - x; EnoughSelected = StillNeeded <= 0; }); StillNeeded = transactionInfo.Amount.ToDecimal(MoneyUnit.BTC); SetupCancel(enableCancel: false, enableCancelOnEscape: true, enableCancelOnPressed: false); EnableBack = true; NextCommand = ReactiveCommand.Create(Complete, this.WhenAnyValue(x => x.EnoughSelected)); EnableAutoBusyOn(NextCommand); }
private async Task <SmartTransaction> GetFinalTransactionAsync(SmartTransaction transaction, TransactionInfo transactionInfo) { if (transactionInfo.PayJoinClient is { })
public SendViewModel(WalletViewModel walletVm, TransactionBroadcaster broadcaster) : base(NavigationMode.Normal) { _to = ""; _owner = walletVm; _transactionInfo = new TransactionInfo(); _labels = new ObservableCollection <string>(); _lastXAxisCurrentValue = _xAxisCurrentValue; ExchangeRate = walletVm.Wallet.Synchronizer.UsdExchangeRate; PriorLabels = new(); this.ValidateProperty(x => x.To, ValidateToField); this.ValidateProperty(x => x.AmountBtc, ValidateAmount); this.WhenAnyValue(x => x.To) .Subscribe(ParseToField); this.WhenAnyValue(x => x.AmountBtc) .Subscribe(x => _transactionInfo.Amount = new Money(x, MoneyUnit.BTC)); this.WhenAnyValue(x => x.XAxisCurrentValue) .Subscribe(x => { if (x > 0) { _transactionInfo.FeeRate = new FeeRate(GetYAxisValueFromXAxisCurrentValue(x)); SetXAxisCurrentValueIndex(x); } }); this.WhenAnyValue(x => x.XAxisCurrentValueIndex) .Subscribe(SetXAxisCurrentValue); Labels.ToObservableChangeSet().Subscribe(x => { _transactionInfo.Labels = new SmartLabel(_labels.ToArray()); }); PasteCommand = ReactiveCommand.CreateFromTask(async() => { var text = await Application.Current.Clipboard.GetTextAsync(); _parsingUrl = true; if (!TryParseUrl(text)) { To = text; // todo validation errors. } _parsingUrl = false; }); var nextCommandCanExecute = this.WhenAnyValue(x => x.Labels, x => x.AmountBtc, x => x.To, x => x.XAxisCurrentValue).Select(_ => Unit.Default) .Merge(Observable.FromEventPattern(Labels, nameof(Labels.CollectionChanged)).Select(_ => Unit.Default)) .Select(_ => { var allFilled = !string.IsNullOrEmpty(To) && AmountBtc > 0 && Labels.Any(); var hasError = Validations.Any; return(allFilled && !hasError); }); NextCommand = ReactiveCommand.CreateFromTask(async() => { var transactionInfo = _transactionInfo; var wallet = _owner.Wallet; 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); if (transactionInfo.Amount <= totalMixedCoinsAmount) { try { try { var txRes = await Task.Run(() => TransactionHelpers.BuildTransaction(wallet, transactionInfo.Address, transactionInfo.Amount, transactionInfo.Labels, transactionInfo.FeeRate, mixedCoins, subtractFee: false)); Navigate().To(new OptimisePrivacyViewModel(wallet, transactionInfo, broadcaster, txRes)); return; } catch (InsufficientBalanceException) { var dialog = new InsufficientBalanceDialogViewModel(BalanceType.Private); var result = await NavigateDialog(dialog, NavigationTarget.DialogScreen); if (result.Result) { var txRes = await Task.Run(() => TransactionHelpers.BuildTransaction(wallet, transactionInfo.Address, totalMixedCoinsAmount, transactionInfo.Labels, transactionInfo.FeeRate, mixedCoins, subtractFee: true)); Navigate().To(new OptimisePrivacyViewModel(wallet, transactionInfo, broadcaster, txRes)); return; } } } catch (Exception ex) { Logger.LogError(ex); await ShowErrorAsync("Transaction Building", ex.ToUserFriendlyString(), "Wasabi was unable to create your transaction."); return; } } Navigate().To(new PrivacyControlViewModel(wallet, transactionInfo, broadcaster)); }, nextCommandCanExecute); EnableAutoBusyOn(NextCommand); }
public TransactionPreviewViewModel(Wallet wallet, TransactionInfo info, TransactionBroadcaster broadcaster, BuildTransactionResult transaction) { var destinationAmount = transaction.CalculateDestinationAmount().ToDecimal(MoneyUnit.BTC); var fee = transaction.Fee; var labels = ""; if (info.Labels.Count() == 1) { labels = info.Labels.First() + " "; } else if (info.Labels.Count() > 1) { labels = string.Join(", ", info.Labels.Take(info.Labels.Count() - 1)); labels += $" and {info.Labels.Last()} "; } BtcAmountText = $"{destinationAmount} bitcoins "; FiatAmountText = $"(≈{(destinationAmount * wallet.Synchronizer.UsdExchangeRate).FormattedFiat()} USD) "; LabelsText = labels; AddressText = info.Address.ToString(); ConfirmationTimeText = "~20 minutes "; BtcFeeText = $"{fee.ToDecimal(MoneyUnit.Satoshi)} satoshis "; FiatFeeText = $"(≈{(fee.ToDecimal(MoneyUnit.BTC) * wallet.Synchronizer.UsdExchangeRate).FormattedFiat()} USD)"; PercentFeeText = $"{transaction.FeePercentOfSent:F2}%"; NextCommand = ReactiveCommand.CreateFromTask(async() => { var dialogResult = await NavigateDialog(new EnterPasswordDialogViewModel(""), NavigationTarget.DialogScreen); if (dialogResult.Kind == DialogResultKind.Normal) { IsBusy = true; var passwordValid = await Task.Run( () => PasswordHelper.TryPassword( wallet.KeyManager, dialogResult.Result, out string?compatibilityPasswordUsed)); if (passwordValid) { // Dequeue any coin-joining coins. await wallet.ChaumianClient.DequeueAllCoinsFromMixAsync(DequeueReason.TransactionBuilding); var signedTransaction = transaction.Transaction; // If it's a hardware wallet and still has a private key then it's password. if (wallet.KeyManager.IsHardwareWallet && !transaction.Signed) { try { var client = new HwiClient(wallet.Network); using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3)); PSBT?signedPsbt = null; try { signedPsbt = await client.SignTxAsync( wallet.KeyManager.MasterFingerprint !.Value, transaction.Psbt, cts.Token); } catch (HwiException ex) when(ex.ErrorCode is not HwiErrorCode.ActionCanceled) { await PinPadViewModel.UnlockAsync(); signedPsbt = await client.SignTxAsync( wallet.KeyManager.MasterFingerprint !.Value, transaction.Psbt, cts.Token); } signedTransaction = signedPsbt.ExtractSmartTransaction(transaction.Transaction); } catch (Exception _) { // probably throw something here? } } await broadcaster.SendTransactionAsync(signedTransaction); Navigate().Clear(); IsBusy = false; } else { IsBusy = false; await ShowErrorAsync("Password was incorrect.", "Please try again.", ""); } } }); }
public SendViewModel(WalletViewModel walletVm, TransactionBroadcaster broadcaster) { _to = ""; _owner = walletVm; _transactionInfo = new TransactionInfo(); _labels = new ObservableCollection <string>(); ExchangeRate = walletVm.Wallet.Synchronizer.UsdExchangeRate; PriorLabels = new(); this.ValidateProperty(x => x.To, ValidateToField); this.ValidateProperty(x => x.AmountBtc, ValidateAmount); this.WhenAnyValue(x => x.To) .Subscribe(ParseToField); this.WhenAnyValue(x => x.AmountBtc) .Subscribe(x => _transactionInfo.Amount = new Money(x, MoneyUnit.BTC)); this.WhenAnyValue(x => x.XAxisCurrentValue) .Subscribe(x => { if (x > 0) { _transactionInfo.FeeRate = new FeeRate(GetYAxisValueFromXAxisCurrentValue(x)); } }); Labels.ToObservableChangeSet().Subscribe(x => { _transactionInfo.Labels = new SmartLabel(_labels.ToArray()); }); PasteCommand = ReactiveCommand.CreateFromTask(async() => { var text = await Application.Current.Clipboard.GetTextAsync(); _parsingUrl = true; if (!TryParseUrl(text)) { To = text; // todo validation errors. } _parsingUrl = false; }); NextCommand = ReactiveCommand.Create(() => { var transactionInfo = _transactionInfo; var wallet = _owner.Wallet; var targetAnonymitySet = wallet.ServiceConfiguration.GetMixUntilAnonymitySetValue(); var mixedCoins = wallet.Coins.Where(x => x.HdPubKey.AnonymitySet >= targetAnonymitySet).ToList(); if (mixedCoins.Any()) { var intent = new PaymentIntent( destination: transactionInfo.Address, amount: transactionInfo.Amount, subtractFee: false, label: transactionInfo.Labels); try { var txRes = wallet.BuildTransaction( wallet.Kitchen.SaltSoup(), intent, FeeStrategy.CreateFromFeeRate(transactionInfo.FeeRate), allowUnconfirmed: true, mixedCoins.Select(x => x.OutPoint)); // Private coins are enough. Navigate().To(new OptimisePrivacyViewModel(wallet, transactionInfo, broadcaster, txRes)); return; } catch (NotEnoughFundsException) { // Do Nothing } } Navigate().To(new PrivacyControlViewModel()); }, this.WhenAnyValue(x => x.Labels.Count).Any()); }
public PrivacyControlViewModel(Wallet wallet, TransactionInfo transactionInfo, TransactionBroadcaster broadcaster) { _wallet = wallet; _pocketSource = new SourceList <PocketViewModel>(); _pocketSource.Connect() .Bind(out _pockets) .Subscribe(); var selected = _pocketSource.Connect() .AutoRefresh() .Filter(x => x.IsSelected); var selectedList = selected.AsObservableList(); selected.Sum(x => x.TotalBtc) .Subscribe(x => { StillNeeded = transactionInfo.Amount.ToDecimal(MoneyUnit.BTC) - x; EnoughSelected = StillNeeded <= 0; }); StillNeeded = transactionInfo.Amount.ToDecimal(MoneyUnit.BTC); NextCommand = ReactiveCommand.CreateFromTask( async() => { var coins = selectedList.Items.SelectMany(x => x.Coins).ToArray(); try { try { var transactionResult = await Task.Run(() => TransactionHelpers.BuildTransaction(_wallet, transactionInfo.Address, transactionInfo.Amount, transactionInfo.Labels, transactionInfo.FeeRate, coins, subtractFee: false)); Navigate().To(new TransactionPreviewViewModel(wallet, transactionInfo, broadcaster, transactionResult)); } catch (InsufficientBalanceException) { var dialog = new InsufficientBalanceDialogViewModel(BalanceType.Pocket); var result = await NavigateDialog(dialog, NavigationTarget.DialogScreen); if (result.Result) { var transactionResult = await Task.Run(() => TransactionHelpers.BuildTransaction(_wallet, transactionInfo.Address, transactionInfo.Amount, transactionInfo.Labels, transactionInfo.FeeRate, coins, subtractFee: true)); Navigate().To(new TransactionPreviewViewModel(wallet, transactionInfo, broadcaster, transactionResult)); } else { Navigate().BackTo <SendViewModel>(); } } } catch (Exception ex) { Logger.LogError(ex); await ShowErrorAsync("Transaction Building", ex.ToUserFriendlyString(), "Wasabi was unable to create your transaction."); Navigate().BackTo <SendViewModel>(); } }, this.WhenAnyValue(x => x.EnoughSelected)); EnableAutoBusyOn(NextCommand); }