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.Create(
                () =>
            {
                var coins = selectedList.Items.SelectMany(x => x.Coins);

                var intent = new PaymentIntent(
                    transactionInfo.Address,
                    transactionInfo.Amount,
                    subtractFee: false,
                    transactionInfo.Labels);

                var transactionResult = _wallet.BuildTransaction(
                    _wallet.Kitchen.SaltSoup(),
                    intent,
                    FeeStrategy.CreateFromFeeRate(transactionInfo.FeeRate),
                    true,
                    coins.Select(x => x.OutPoint));

                Navigate().To(new TransactionPreviewViewModel(wallet, transactionInfo, broadcaster, transactionResult));
            },
                this.WhenAnyValue(x => x.EnoughSelected));
        }
Ejemplo n.º 2
0
        public static BuildTransactionResult BuildTransaction(Wallet wallet, BitcoinAddress address, Money amount, SmartLabel labels, FeeRate feeRate, IEnumerable <SmartCoin> coins, bool subtractFee)
        {
            var intent = new PaymentIntent(
                destination: address,
                amount: amount,
                subtractFee: subtractFee,
                label: labels);

            var txRes = wallet.BuildTransaction(
                wallet.Kitchen.SaltSoup(),
                intent,
                FeeStrategy.CreateFromFeeRate(feeRate),
                allowUnconfirmed: true,
                coins.Select(coin => coin.OutPoint));

            return(txRes);
        }
Ejemplo n.º 3
0
    public static BuildTransactionResult BuildChangelessTransaction(Wallet wallet, BitcoinAddress address, SmartLabel labels, FeeRate feeRate, IEnumerable <SmartCoin> coins, bool tryToSign = true)
    {
        var intent = new PaymentIntent(
            address,
            MoneyRequest.CreateAllRemaining(subtractFee: true),
            labels);

        var txRes = wallet.BuildTransaction(
            wallet.Kitchen.SaltSoup(),
            intent,
            FeeStrategy.CreateFromFeeRate(feeRate),
            allowUnconfirmed: true,
            coins.Select(coin => coin.OutPoint),
            tryToSign: tryToSign);

        return(txRes);
    }
Ejemplo n.º 4
0
        public string BuildTransaction(PaymentInfo[] payments, TxoRef[] coins, int feeTarget, string password = null)
        {
            Guard.NotNull(nameof(payments), payments);
            Guard.NotNull(nameof(coins), coins);
            Guard.InRangeAndNotNull(nameof(feeTarget), feeTarget, 2, Constants.SevenDaysConfirmationTarget);
            password = Guard.Correct(password);

            AssertWalletIsLoaded();
            var sync    = Global.Synchronizer;
            var payment = new PaymentIntent(payments.Select(p =>
                                                            new DestinationRequest(p.Sendto.ScriptPubKey, MoneyRequest.Create(p.Amount, p.SubtractFee), new SmartLabel(p.Label))));
            var feeStrategy = FeeStrategy.CreateFromConfirmationTarget(feeTarget);
            var result      = Global.WalletService.BuildTransaction(
                password,
                payment,
                feeStrategy,
                allowUnconfirmed: true,
                allowedInputs: coins);
            var smartTx = result.Transaction;

            return(smartTx.Transaction.ToHex());
        }
Ejemplo n.º 5
0
        public async Task <object> SendTransactionAsync(PaymentInfo[] payments, TxoRef[] coins, int feeTarget, string password = null)
        {
            Guard.NotNull(nameof(payments), payments);
            Guard.NotNull(nameof(coins), coins);
            Guard.InRangeAndNotNull(nameof(feeTarget), feeTarget, 2, Constants.SevenDaysConfirmationTarget);
            password = Guard.Correct(password);

            AssertWalletIsLoaded();
            var sync    = Global.Synchronizer;
            var payment = new PaymentIntent(payments.Select(p =>
                                                            new DestinationRequest(p.Sendto.ScriptPubKey, MoneyRequest.Create(p.Amount, p.SubtractFee), new SmartLabel(p.Label))));
            var feeStrategy = FeeStrategy.CreateFromConfirmationTarget(feeTarget);
            var result      = Global.WalletService.BuildTransaction(
                password,
                payment,
                feeStrategy,
                allowUnconfirmed: true,
                allowedInputs: coins);
            var smartTx = result.Transaction;

            // dequeue the coins we are going to spend
            var toDequeue = Global.WalletService.Coins
                            .Where(x => x.CoinJoinInProgress && coins.Contains(x.GetTxoRef()))
                            .ToArray();

            if (toDequeue.Any())
            {
                await Global.ChaumianClient.DequeueCoinsFromMixAsync(toDequeue, DequeueReason.TransactionBuilding).ConfigureAwait(false);
            }

            await Global.TransactionBroadcaster.SendTransactionAsync(smartTx).ConfigureAwait(false);

            return(new
            {
                txid = smartTx.Transaction.GetHash(),
                tx = smartTx.Transaction.ToHex()
            });
        }
Ejemplo n.º 6
0
        protected SendControlViewModel(Wallet wallet, string title)
            : base(title)
        {
            Global = Locator.Current.GetService <Global>();
            Wallet = wallet;

            LabelSuggestion             = new SuggestLabelViewModel();
            _buildTransactionButtonText = DoButtonText;

            this.ValidateProperty(x => x.Address, ValidateAddress);
            this.ValidateProperty(x => x.CustomChangeAddress, ValidateCustomChangeAddress);
            this.ValidateProperty(x => x.Password, ValidatePassword);
            this.ValidateProperty(x => x.UserFeeText, ValidateUserFeeText);

            ResetUi();

            CoinList = new CoinListViewModel(Wallet, Global.Config, Global.UiConfig, displayCommonOwnershipWarning: true);

            Observable.FromEventPattern(CoinList, nameof(CoinList.SelectionChanged))
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ => SetFeesAndTexts());

            _minMaxFeeTargetsEqual = this.WhenAnyValue(x => x.MinimumFeeTarget, x => x.MaximumFeeTarget, (x, y) => x == y)
                                     .ToProperty(this, x => x.MinMaxFeeTargetsEqual, scheduler: RxApp.MainThreadScheduler);

            SetFeeTargetLimits();
            FeeTarget        = Global.UiConfig.FeeTarget;
            FeeDisplayFormat = (FeeDisplayFormat)(Enum.ToObject(typeof(FeeDisplayFormat), Global.UiConfig.FeeDisplayFormat) ?? FeeDisplayFormat.SatoshiPerByte);
            SetFeesAndTexts();

            this.WhenAnyValue(x => x.AmountText)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(x =>
            {
                if (Money.TryParse(x.TrimStart('~', ' '), out Money amountBtc))
                {
                    SetAmountWatermark(amountBtc);
                }
                else
                {
                    SetAmountWatermark(Money.Zero);
                }

                SetFees();
            });

            AmountKeyUpCommand = ReactiveCommand.Create((KeyEventArgs key) =>
            {
                if (IsMax)
                {
                    SetFeesAndTexts();
                }
                else if (BitcoinInput.TryCorrectAmount(AmountText, out var betterAmount))
                {
                    AmountText = betterAmount;
                }
            });

            this.WhenAnyValue(x => x.IsBusy, x => x.IsHardwareBusy)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ => BuildTransactionButtonText = IsHardwareBusy
                                                ? WaitingForHardwareWalletButtonTextString
                                                : IsBusy
                                                        ? DoingButtonText
                                                        : DoButtonText);

            Observable
            .Merge(this.WhenAnyValue(x => x.FeeTarget).Select(_ => true))
            .Merge(this.WhenAnyValue(x => x.IsEstimateAvailable).Select(_ => true))
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ =>
            {
                IsSliderFeeUsed = IsEstimateAvailable;
                SetFeesAndTexts();
            });

            this.WhenAnyValue(x => x.IsSliderFeeUsed)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(enabled => FeeControlOpacity = enabled ? 1 : 0.5);                     // Give the control the disabled feeling. Real Disable it not a solution as we have to detect if the slider is moved.

            MaxCommand = ReactiveCommand.Create(() => IsMax = !IsMax, outputScheduler: RxApp.MainThreadScheduler);

            this.WhenAnyValue(x => x.IsMax)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ =>
            {
                if (IsMax)
                {
                    SetFeesAndTexts();

                    LabelToolTip = "Spending whole coins does not generate change, thus labeling is unnecessary.";
                }
                else
                {
                    AmountText = "0.0";

                    LabelToolTip = "Who can link this transaction to you? E.g.: \"Max, BitPay\"";
                }
            });

            // Triggering the detection of same address values.
            this.WhenAnyValue(x => x.Address)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ => this.RaisePropertyChanged(nameof(CustomChangeAddress)));

            this.WhenAnyValue(x => x.CustomChangeAddress)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ => this.RaisePropertyChanged(nameof(Address)));

            this.WhenAnyValue(x => x.IsCustomChangeAddressVisible)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ =>
            {
                this.RaisePropertyChanged(nameof(Address));
                this.RaisePropertyChanged(nameof(CustomChangeAddress));
            });

            FeeRateCommand = ReactiveCommand.Create(ChangeFeeRateDisplay, outputScheduler: RxApp.MainThreadScheduler);

            OnAddressPasteCommand = ReactiveCommand.Create((BitcoinUrlBuilder url) => OnAddressPaste(url));

            BuildTransactionCommand = ReactiveCommand.CreateFromTask(async() =>
            {
                try
                {
                    IsBusy = true;
                    MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.BuildingTransaction);

                    var label             = new SmartLabel(LabelSuggestion.Label);
                    LabelSuggestion.Label = label;
                    if (!IsMax && label.IsEmpty)
                    {
                        NotificationHelpers.Warning("Label is required.", "");
                        return;
                    }

                    var selectedCoinViewModels = CoinList.Coins.Where(cvm => cvm.IsSelected);
                    var selectedCoinReferences = selectedCoinViewModels.Select(cvm => cvm.Model.OutPoint).ToList();

                    if (!selectedCoinReferences.Any())
                    {
                        NotificationHelpers.Warning("No coins are selected to spend.", "");
                        return;
                    }

                    BitcoinAddress address;
                    try
                    {
                        address = BitcoinAddress.Create(Address, Global.Network);
                    }
                    catch (FormatException)
                    {
                        NotificationHelpers.Warning("Invalid address.", "");
                        return;
                    }

                    var requests = new List <DestinationRequest>();

                    if (IsCustomChangeAddressVisible && !string.IsNullOrWhiteSpace(CustomChangeAddress))
                    {
                        try
                        {
                            var customChangeAddress = BitcoinAddress.Create(CustomChangeAddress, Global.Network);

                            if (customChangeAddress == address)
                            {
                                NotificationHelpers.Warning("The active address and the change address cannot be the same.", "");
                                return;
                            }

                            requests.Add(new DestinationRequest(customChangeAddress, MoneyRequest.CreateChange(subtractFee: true), label));
                        }
                        catch (FormatException)
                        {
                            NotificationHelpers.Warning("Invalid custom change address.", "");
                            return;
                        }
                    }

                    MoneyRequest moneyRequest;
                    if (IsMax)
                    {
                        moneyRequest = MoneyRequest.CreateAllRemaining(subtractFee: true);
                    }
                    else
                    {
                        if (!Money.TryParse(AmountText, out Money amount) || amount == Money.Zero)
                        {
                            NotificationHelpers.Warning("Invalid amount.");
                            return;
                        }

                        if (amount == selectedCoinViewModels.Sum(x => x.Amount))
                        {
                            NotificationHelpers.Warning("Looks like you want to spend whole coins. Try Max button instead.", "");
                            return;
                        }
                        moneyRequest = MoneyRequest.Create(amount, subtractFee: false);
                    }

                    if (FeeRate is null || FeeRate.SatoshiPerByte < 1)
                    {
                        NotificationHelpers.Warning("Invalid fee rate.", "");
                        return;
                    }

                    var feeStrategy = FeeStrategy.CreateFromFeeRate(FeeRate);

                    var activeDestinationRequest = new DestinationRequest(address, moneyRequest, label);
                    requests.Add(activeDestinationRequest);

                    var intent = new PaymentIntent(requests);
                    try
                    {
                        MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.DequeuingSelectedCoins);
                        OutPoint[] toDequeue = selectedCoinViewModels.Where(x => x.CoinJoinInProgress).Select(x => x.Model.OutPoint).ToArray();
                        if (toDequeue is { } && toDequeue.Any())
                        {
                            await Wallet.ChaumianClient.DequeueCoinsFromMixAsync(toDequeue, DequeueReason.TransactionBuilding);
                        }
                    }
                    catch
                    {
                        NotificationHelpers.Error("Cannot spend mixing coins.", "");
                        return;
                    }
                    finally
                    {
                        MainWindowViewModel.Instance.StatusBar.TryRemoveStatus(StatusType.DequeuingSelectedCoins);
                    }

                    if (!Wallet.KeyManager.IsWatchOnly)
                    {
                        try
                        {
                            PasswordHelper.GetMasterExtKey(Wallet.KeyManager, Password, out var compatiblityPasswordUsed);                             // We could use TryPassword but we need the exception.
                            if (compatiblityPasswordUsed is { })
                            {
                                Password = compatiblityPasswordUsed;                                 // Overwrite the password for BuildTransaction function.
                                NotificationHelpers.Warning(PasswordHelper.CompatibilityPasswordWarnMessage);
                            }
                        }
                        catch (SecurityException ex)
                        {
                            NotificationHelpers.Error(ex.Message, "");
                            return;
                        }
                        catch (Exception ex)
                        {
                            Logger.LogError(ex);
                            NotificationHelpers.Error(ex.ToUserFriendlyString());
                            return;
                        }
                    }
Ejemplo n.º 7
0
        protected SendControlViewModel(Wallet wallet, string title)
            : base(title)
        {
            Global = Locator.Current.GetService <Global>();
            Wallet = wallet;

            LabelSuggestion            = new SuggestLabelViewModel();
            BuildTransactionButtonText = DoButtonText;

            this.ValidateProperty(x => x.Address, ValidateAddress);
            this.ValidateProperty(x => x.CustomChangeAddress, ValidateCustomChangeAddress);
            this.ValidateProperty(x => x.Password, ValidatePassword);
            this.ValidateProperty(x => x.UserFeeText, ValidateUserFeeText);

            ResetUi();
            SetAmountWatermark(Money.Zero);

            CoinList = new CoinListViewModel(Wallet, displayCommonOwnershipWarning: true);

            Observable.FromEventPattern(CoinList, nameof(CoinList.SelectionChanged))
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ => SetFeesAndTexts());

            _minMaxFeeTargetsEqual = this.WhenAnyValue(x => x.MinimumFeeTarget, x => x.MaximumFeeTarget, (x, y) => x == y)
                                     .ToProperty(this, x => x.MinMaxFeeTargetsEqual, scheduler: RxApp.MainThreadScheduler);

            SetFeeTargetLimits();
            FeeTarget        = Global.UiConfig.FeeTarget;
            FeeDisplayFormat = (FeeDisplayFormat)(Enum.ToObject(typeof(FeeDisplayFormat), Global.UiConfig.FeeDisplayFormat) ?? FeeDisplayFormat.SatoshiPerByte);
            SetFeesAndTexts();

            this.WhenAnyValue(x => x.AmountText)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(x =>
            {
                if (Money.TryParse(x.TrimStart('~', ' '), out Money amountBtc))
                {
                    SetAmountWatermark(amountBtc);
                }
                else
                {
                    SetAmountWatermark(Money.Zero);
                }

                SetFees();
            });

            AmountKeyUpCommand = ReactiveCommand.Create((KeyEventArgs key) =>
            {
                var amount = AmountText;
                if (IsMax)
                {
                    SetFeesAndTexts();
                }
                else
                {
                    // Correct amount
                    Regex digitsOnly    = new Regex(@"[^\d,.]");
                    string betterAmount = digitsOnly.Replace(amount, "");                     // Make it digits , and . only.

                    betterAmount          = betterAmount.Replace(',', '.');
                    int countBetterAmount = betterAmount.Count(x => x == '.');
                    if (countBetterAmount > 1)                     // Do not enable typing two dots.
                    {
                        var index = betterAmount.IndexOf('.', betterAmount.IndexOf('.') + 1);
                        if (index > 0)
                        {
                            betterAmount = betterAmount.Substring(0, index);
                        }
                    }
                    var dotIndex = betterAmount.IndexOf('.');
                    if (dotIndex != -1 && betterAmount.Length - dotIndex > 8)                     // Enable max 8 decimals.
                    {
                        betterAmount = betterAmount.Substring(0, dotIndex + 1 + 8);
                    }

                    if (betterAmount != amount)
                    {
                        AmountText = betterAmount;
                    }
                }
            });

            this.WhenAnyValue(x => x.IsBusy, x => x.IsHardwareBusy)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ => BuildTransactionButtonText = IsHardwareBusy
                                                ? WaitingForHardwareWalletButtonTextString
                                                : IsBusy
                                                        ? DoingButtonText
                                                        : DoButtonText);

            this.WhenAnyValue(x => x.FeeTarget)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ =>
            {
                IsSliderFeeUsed = true;
                SetFeesAndTexts();
            });

            this.WhenAnyValue(x => x.IsSliderFeeUsed)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(enabled => FeeControlOpacity = enabled ? 1 : 0.5);                     // Give the control the disabled feeling. Real Disable it not a solution as we have to detect if the slider is moved.

            MaxCommand = ReactiveCommand.Create(() => IsMax = !IsMax, outputScheduler: RxApp.MainThreadScheduler);

            this.WhenAnyValue(x => x.IsMax)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ =>
            {
                if (IsMax)
                {
                    SetFeesAndTexts();

                    LabelToolTip = "Spending whole coins does not generate change, thus labeling is unnecessary.";
                }
                else
                {
                    AmountText = "0.0";

                    LabelToolTip = "Who can link this transaction to you? E.g.: \"Max, BitPay\"";
                }
            });

            // Triggering the detection of same address values.
            this.WhenAnyValue(x => x.Address)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ => this.RaisePropertyChanged(nameof(CustomChangeAddress)));

            this.WhenAnyValue(x => x.CustomChangeAddress)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ => this.RaisePropertyChanged(nameof(Address)));

            this.WhenAnyValue(x => x.IsCustomChangeAddressVisible)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(_ =>
            {
                this.RaisePropertyChanged(nameof(Address));
                this.RaisePropertyChanged(nameof(CustomChangeAddress));
            });

            FeeRateCommand = ReactiveCommand.Create(ChangeFeeRateDisplay, outputScheduler: RxApp.MainThreadScheduler);

            OnAddressPasteCommand = ReactiveCommand.Create((BitcoinUrlBuilder url) => OnAddressPaste(url));

            BuildTransactionCommand = ReactiveCommand.CreateFromTask(async() =>
            {
                try
                {
                    IsBusy = true;
                    MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.BuildingTransaction);

                    var label             = new SmartLabel(LabelSuggestion.Label);
                    LabelSuggestion.Label = label;
                    if (!IsMax && label.IsEmpty)
                    {
                        NotificationHelpers.Warning("Observers are required.", "");
                        return;
                    }

                    var selectedCoinViewModels = CoinList.Coins.Where(cvm => cvm.IsSelected);
                    var selectedCoinReferences = selectedCoinViewModels.Select(cvm => cvm.Model.OutPoint).ToList();

                    if (!selectedCoinReferences.Any())
                    {
                        NotificationHelpers.Warning("No coins are selected to spend.", "");
                        return;
                    }

                    BitcoinAddress address;
                    try
                    {
                        address = BitcoinAddress.Create(Address, Global.Network);
                    }
                    catch (FormatException)
                    {
                        NotificationHelpers.Warning("Invalid address.", "");
                        return;
                    }

                    var requests = new List <DestinationRequest>();

                    if (IsCustomChangeAddressVisible && !string.IsNullOrWhiteSpace(CustomChangeAddress))
                    {
                        try
                        {
                            var customChangeAddress = BitcoinAddress.Create(CustomChangeAddress, Global.Network);

                            if (customChangeAddress == address)
                            {
                                NotificationHelpers.Warning("The active address and the change address cannot be the same.", "");
                                return;
                            }

                            requests.Add(new DestinationRequest(customChangeAddress, MoneyRequest.CreateChange(subtractFee: true), label));
                        }
                        catch (FormatException)
                        {
                            NotificationHelpers.Warning("Invalid custom change address.", "");
                            return;
                        }
                    }

                    MoneyRequest moneyRequest;
                    if (IsMax)
                    {
                        moneyRequest = MoneyRequest.CreateAllRemaining(subtractFee: true);
                    }
                    else
                    {
                        if (!Money.TryParse(AmountText, out Money amount) || amount == Money.Zero)
                        {
                            NotificationHelpers.Warning("Invalid amount.");
                            return;
                        }

                        if (amount == selectedCoinViewModels.Sum(x => x.Amount))
                        {
                            NotificationHelpers.Warning("Looks like you want to spend whole coins. Try Max button instead.", "");
                            return;
                        }
                        moneyRequest = MoneyRequest.Create(amount, subtractFee: false);
                    }

                    if (FeeRate is null || FeeRate.SatoshiPerByte < 1)
                    {
                        NotificationHelpers.Warning("Invalid fee rate.", "");
                        return;
                    }

                    var feeStrategy = FeeStrategy.CreateFromFeeRate(FeeRate);

                    var activeDestinationRequest = new DestinationRequest(address, moneyRequest, label);
                    requests.Add(activeDestinationRequest);

                    var intent = new PaymentIntent(requests);
                    try
                    {
                        MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.DequeuingSelectedCoins);
                        OutPoint[] toDequeue = selectedCoinViewModels.Where(x => x.CoinJoinInProgress).Select(x => x.Model.OutPoint).ToArray();
                        if (toDequeue != null && toDequeue.Any())
                        {
                            await Wallet.ChaumianClient.DequeueCoinsFromMixAsync(toDequeue, DequeueReason.TransactionBuilding);
                        }
                    }
                    catch
                    {
                        NotificationHelpers.Error("Cannot spend mixing coins.", "");
                        return;
                    }
                    finally
                    {
                        MainWindowViewModel.Instance.StatusBar.TryRemoveStatus(StatusType.DequeuingSelectedCoins);
                    }

                    if (!Wallet.KeyManager.IsWatchOnly)
                    {
                        try
                        {
                            PasswordHelper.GetMasterExtKey(Wallet.KeyManager, Password, out string compatiblityPasswordUsed);                             // We could use TryPassword but we need the exception.
                            if (compatiblityPasswordUsed != null)
                            {
                                Password = compatiblityPasswordUsed;                                 // Overwrite the password for BuildTransaction function.
                                NotificationHelpers.Warning(PasswordHelper.CompatibilityPasswordWarnMessage);
                            }
                        }
                        catch (SecurityException ex)
                        {
                            NotificationHelpers.Error(ex.Message, "");
                            return;
                        }
                        catch (Exception ex)
                        {
                            Logger.LogError(ex);
                            NotificationHelpers.Error(ex.ToUserFriendlyString());
                            return;
                        }
                    }

                    await BuildTransaction(Password, intent, feeStrategy, allowUnconfirmed: true, allowedInputs: selectedCoinReferences);
                }
                catch (InsufficientBalanceException ex)
                {
                    Money needed = ex.Minimum - ex.Actual;
                    NotificationHelpers.Error($"Not enough coins selected. You need an estimated {needed.ToString(false, true)} BTC more to make this transaction.", "");
                }
                catch (HttpRequestException ex)
                {
                    NotificationHelpers.Error(ex.ToUserFriendlyString());
                    Logger.LogError(ex);
                }
                catch (Exception ex)
                {
                    NotificationHelpers.Error(ex.ToUserFriendlyString(), sender: Wallet);
                    Logger.LogError(ex);
                }
                finally
                {
                    MainWindowViewModel.Instance.StatusBar.TryRemoveStatus(StatusType.BuildingTransaction, StatusType.SigningTransaction, StatusType.BroadcastingTransaction);
                    IsBusy = false;
                }
            },
                                                                     this.WhenAny(x => x.IsMax, x => x.AmountText, x => x.Address, x => x.IsBusy,
                                                                                  (isMax, amount, address, busy) => (isMax.Value || !string.IsNullOrWhiteSpace(amount.Value)) && !string.IsNullOrWhiteSpace(Address) && !IsBusy)
                                                                     .ObserveOn(RxApp.MainThreadScheduler));

            UserFeeTextKeyUpCommand = ReactiveCommand.Create((KeyEventArgs key) =>
            {
                IsSliderFeeUsed = !IsCustomFee;
                SetFeesAndTexts();
            });

            FeeSliderClickedCommand = ReactiveCommand.Create((PointerPressedEventArgs mouse) => IsSliderFeeUsed = true);

            HighLightFeeSliderCommand = ReactiveCommand.Create((bool entered) =>
            {
                if (IsSliderFeeUsed)
                {
                    return;
                }

                FeeControlOpacity = entered ? 0.8 : 0.5;
            });

            Observable
            .Merge(MaxCommand.ThrownExceptions)
            .Merge(FeeRateCommand.ThrownExceptions)
            .Merge(OnAddressPasteCommand.ThrownExceptions)
            .Merge(BuildTransactionCommand.ThrownExceptions)
            .Merge(UserFeeTextKeyUpCommand.ThrownExceptions)
            .Merge(FeeSliderClickedCommand.ThrownExceptions)
            .Merge(HighLightFeeSliderCommand.ThrownExceptions)
            .Merge(AmountKeyUpCommand.ThrownExceptions)
            .ObserveOn(RxApp.TaskpoolScheduler)
            .Subscribe(ex =>
            {
                NotificationHelpers.Error(ex.ToUserFriendlyString());
                Logger.LogError(ex);
            });
        }
Ejemplo n.º 8
0
        public async Task SendTestsAsync()
        {
            (string password, IRPCClient rpc, Network network, _, ServiceConfiguration serviceConfiguration, BitcoinStore bitcoinStore, Backend.Global global) = await Common.InitializeTestEnvironmentAsync(RegTestFixture, 1);

            bitcoinStore.IndexStore.NewFilter += Common.Wallet_NewFilterProcessed;
            // Create the services.
            // 1. Create connection service.
            var nodes = new NodesGroup(global.Config.Network, requirements: Constants.NodeRequirements);

            nodes.ConnectedNodes.Add(await RegTestFixture.BackendRegTestNode.CreateNewP2pNodeAsync());

            // 2. Create mempool service.

            Node node = await RegTestFixture.BackendRegTestNode.CreateNewP2pNodeAsync();

            node.Behaviors.Add(bitcoinStore.CreateUntrustedP2pBehavior());

            // 3. Create wasabi synchronizer service.
            var synchronizer = new WasabiSynchronizer(rpc.Network, bitcoinStore, new Uri(RegTestFixture.BackendEndPoint), null);

            // 4. Create key manager service.
            var keyManager = KeyManager.CreateNew(out _, password);

            // 5. Create wallet service.
            var workDir = Tests.Common.GetWorkDir();

            CachedBlockProvider blockProvider = new CachedBlockProvider(
                new P2pBlockProvider(nodes, null, synchronizer, serviceConfiguration, network),
                bitcoinStore.BlockRepository);

            var walletManager = new WalletManager(network, new WalletDirectories(workDir));

            walletManager.RegisterServices(bitcoinStore, synchronizer, nodes, serviceConfiguration, synchronizer, blockProvider);

            // Get some money, make it confirm.
            var key  = keyManager.GetNextReceiveKey("foo label", out _);
            var key2 = keyManager.GetNextReceiveKey("foo label", out _);
            var txId = await rpc.SendToAddressAsync(key.GetP2wpkhAddress(network), Money.Coins(1m));

            Assert.NotNull(txId);
            await rpc.GenerateAsync(1);

            var txId2 = await rpc.SendToAddressAsync(key2.GetP2wpkhAddress(network), Money.Coins(1m));

            Assert.NotNull(txId2);
            await rpc.GenerateAsync(1);

            try
            {
                Interlocked.Exchange(ref Common.FiltersProcessedByWalletCount, 0);
                nodes.Connect();                                                                              // Start connection service.
                node.VersionHandshake();                                                                      // Start mempool service.
                synchronizer.Start(requestInterval: TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(5), 10000); // Start wasabi synchronizer service.

                // Wait until the filter our previous transaction is present.
                var blockCount = await rpc.GetBlockCountAsync();

                await Common.WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), blockCount);

                var wallet = await walletManager.AddAndStartWalletAsync(keyManager);

                var broadcaster = new TransactionBroadcaster(network, bitcoinStore, synchronizer, nodes, walletManager, rpc);

                var waitCount = 0;
                while (wallet.Coins.Sum(x => x.Amount) == Money.Zero)
                {
                    await Task.Delay(1000);

                    waitCount++;
                    if (waitCount >= 21)
                    {
                        Logger.LogInfo("Funding transaction to the wallet did not arrive.");
                        return;                         // Very rarely this test fails. I have no clue why. Probably because all these RegTests are interconnected, anyway let's not bother the CI with it.
                    }
                }

                var scp  = new Key().ScriptPubKey;
                var res2 = wallet.BuildTransaction(password, new PaymentIntent(scp, Money.Coins(0.05m), label: "foo"), FeeStrategy.CreateFromConfirmationTarget(5), allowUnconfirmed: false);

                Assert.NotNull(res2.Transaction);
                Assert.Single(res2.OuterWalletOutputs);
                Assert.Equal(scp, res2.OuterWalletOutputs.Single().ScriptPubKey);
                Assert.Single(res2.InnerWalletOutputs);
                Assert.True(res2.Fee > Money.Satoshis(2 * 100));                 // since there is a sanity check of 2sat/vb in the server
                Assert.InRange(res2.FeePercentOfSent, 0, 1);
                Assert.Single(res2.SpentCoins);
                var spentCoin = Assert.Single(res2.SpentCoins);
                Assert.Contains(new[] { key.P2wpkhScript, key2.P2wpkhScript }, x => x == spentCoin.ScriptPubKey);
                Assert.Equal(Money.Coins(1m), res2.SpentCoins.Single().Amount);
                Assert.False(res2.SpendsUnconfirmed);

                await broadcaster.SendTransactionAsync(res2.Transaction);

                Assert.Contains(res2.InnerWalletOutputs.Single(), wallet.Coins);

                #region Basic

                Script receive      = keyManager.GetNextReceiveKey("Basic", out _).P2wpkhScript;
                Money  amountToSend = wallet.Coins.Where(x => !x.Unavailable).Sum(x => x.Amount) / 2;
                var    res          = wallet.BuildTransaction(password, new PaymentIntent(receive, amountToSend, label: "foo"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true);

                foreach (SmartCoin coin in res.SpentCoins)
                {
                    Assert.False(coin.CoinJoinInProgress);
                    Assert.True(coin.Confirmed);
                    Assert.Null(coin.SpenderTransactionId);
                    Assert.True(coin.Unspent);
                }

                Assert.Equal(2, res.InnerWalletOutputs.Count());
                Assert.Empty(res.OuterWalletOutputs);
                var activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive);
                var changeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey != receive);

                Assert.Equal(receive, activeOutput.ScriptPubKey);
                Assert.Equal(amountToSend, activeOutput.Amount);
                if (res.SpentCoins.Sum(x => x.Amount) - activeOutput.Amount == res.Fee)                 // this happens when change is too small
                {
                    Assert.Contains(res.Transaction.Transaction.Outputs, x => x.Value == activeOutput.Amount);
                    Logger.LogInfo($"Change Output: {changeOutput.Amount.ToString(false, true)} {changeOutput.ScriptPubKey.GetDestinationAddress(network)}");
                }
                Logger.LogInfo($"{nameof(res.Fee)}: {res.Fee}");
                Logger.LogInfo($"{nameof(res.FeePercentOfSent)}: {res.FeePercentOfSent} %");
                Logger.LogInfo($"{nameof(res.SpendsUnconfirmed)}: {res.SpendsUnconfirmed}");
                Logger.LogInfo($"Active Output: {activeOutput.Amount.ToString(false, true)} {activeOutput.ScriptPubKey.GetDestinationAddress(network)}");
                Logger.LogInfo($"TxId: {res.Transaction.GetHash()}");

                var foundReceive = false;
                Assert.InRange(res.Transaction.Transaction.Outputs.Count, 1, 2);
                foreach (var output in res.Transaction.Transaction.Outputs)
                {
                    if (output.ScriptPubKey == receive)
                    {
                        foundReceive = true;
                        Assert.Equal(amountToSend, output.Value);
                    }
                }
                Assert.True(foundReceive);

                await broadcaster.SendTransactionAsync(res.Transaction);

                #endregion Basic

                #region SubtractFeeFromAmount

                receive      = keyManager.GetNextReceiveKey("SubtractFeeFromAmount", out _).P2wpkhScript;
                amountToSend = wallet.Coins.Where(x => !x.Unavailable).Sum(x => x.Amount) / 3;
                res          = wallet.BuildTransaction(password, new PaymentIntent(receive, amountToSend, subtractFee: true, label: "foo"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true);

                Assert.Equal(2, res.InnerWalletOutputs.Count());
                Assert.Empty(res.OuterWalletOutputs);
                activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive);
                changeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey != receive);

                Assert.Equal(receive, activeOutput.ScriptPubKey);
                Assert.Equal(amountToSend - res.Fee, activeOutput.Amount);
                Assert.Contains(res.Transaction.Transaction.Outputs, x => x.Value == changeOutput.Amount);
                Logger.LogInfo($"{nameof(res.Fee)}: {res.Fee}");
                Logger.LogInfo($"{nameof(res.FeePercentOfSent)}: {res.FeePercentOfSent} %");
                Logger.LogInfo($"{nameof(res.SpendsUnconfirmed)}: {res.SpendsUnconfirmed}");
                Logger.LogInfo($"Active Output: {activeOutput.Amount.ToString(false, true)} {activeOutput.ScriptPubKey.GetDestinationAddress(network)}");
                Logger.LogInfo($"Change Output: {changeOutput.Amount.ToString(false, true)} {changeOutput.ScriptPubKey.GetDestinationAddress(network)}");
                Logger.LogInfo($"TxId: {res.Transaction.GetHash()}");

                foundReceive = false;
                Assert.InRange(res.Transaction.Transaction.Outputs.Count, 1, 2);
                foreach (var output in res.Transaction.Transaction.Outputs)
                {
                    if (output.ScriptPubKey == receive)
                    {
                        foundReceive = true;
                        Assert.Equal(amountToSend - res.Fee, output.Value);
                    }
                }
                Assert.True(foundReceive);

                #endregion SubtractFeeFromAmount

                #region LowFee

                res = wallet.BuildTransaction(password, new PaymentIntent(receive, amountToSend, label: "foo"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true);

                Assert.Equal(2, res.InnerWalletOutputs.Count());
                Assert.Empty(res.OuterWalletOutputs);
                activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive);
                changeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey != receive);

                Assert.Equal(receive, activeOutput.ScriptPubKey);
                Assert.Equal(amountToSend, activeOutput.Amount);
                Assert.Contains(res.Transaction.Transaction.Outputs, x => x.Value == changeOutput.Amount);
                Logger.LogInfo($"{nameof(res.Fee)}: {res.Fee}");
                Logger.LogInfo($"{nameof(res.FeePercentOfSent)}: {res.FeePercentOfSent} %");
                Logger.LogInfo($"{nameof(res.SpendsUnconfirmed)}: {res.SpendsUnconfirmed}");
                Logger.LogInfo($"Active Output: {activeOutput.Amount.ToString(false, true)} {activeOutput.ScriptPubKey.GetDestinationAddress(network)}");
                Logger.LogInfo($"Change Output: {changeOutput.Amount.ToString(false, true)} {changeOutput.ScriptPubKey.GetDestinationAddress(network)}");
                Logger.LogInfo($"TxId: {res.Transaction.GetHash()}");

                foundReceive = false;
                Assert.InRange(res.Transaction.Transaction.Outputs.Count, 1, 2);
                foreach (var output in res.Transaction.Transaction.Outputs)
                {
                    if (output.ScriptPubKey == receive)
                    {
                        foundReceive = true;
                        Assert.Equal(amountToSend, output.Value);
                    }
                }
                Assert.True(foundReceive);

                #endregion LowFee

                #region MediumFee

                res = wallet.BuildTransaction(password, new PaymentIntent(receive, amountToSend, label: "foo"), FeeStrategy.OneDayConfirmationTargetStrategy, allowUnconfirmed: true);

                Assert.Equal(2, res.InnerWalletOutputs.Count());
                Assert.Empty(res.OuterWalletOutputs);
                activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive);
                changeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey != receive);

                Assert.Equal(receive, activeOutput.ScriptPubKey);
                Assert.Equal(amountToSend, activeOutput.Amount);
                Assert.Contains(res.Transaction.Transaction.Outputs, x => x.Value == changeOutput.Amount);
                Logger.LogInfo($"{nameof(res.Fee)}: {res.Fee}");
                Logger.LogInfo($"{nameof(res.FeePercentOfSent)}: {res.FeePercentOfSent} %");
                Logger.LogInfo($"{nameof(res.SpendsUnconfirmed)}: {res.SpendsUnconfirmed}");
                Logger.LogInfo($"Active Output: {activeOutput.Amount.ToString(false, true)} {activeOutput.ScriptPubKey.GetDestinationAddress(network)}");
                Logger.LogInfo($"Change Output: {changeOutput.Amount.ToString(false, true)} {changeOutput.ScriptPubKey.GetDestinationAddress(network)}");
                Logger.LogInfo($"TxId: {res.Transaction.GetHash()}");

                foundReceive = false;
                Assert.InRange(res.Transaction.Transaction.Outputs.Count, 1, 2);
                foreach (var output in res.Transaction.Transaction.Outputs)
                {
                    if (output.ScriptPubKey == receive)
                    {
                        foundReceive = true;
                        Assert.Equal(amountToSend, output.Value);
                    }
                }
                Assert.True(foundReceive);

                #endregion MediumFee

                #region HighFee

                res = wallet.BuildTransaction(password, new PaymentIntent(receive, amountToSend, label: "foo"), FeeStrategy.TwentyMinutesConfirmationTargetStrategy, allowUnconfirmed: true);

                Assert.Equal(2, res.InnerWalletOutputs.Count());
                Assert.Empty(res.OuterWalletOutputs);
                activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive);
                changeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey != receive);

                Assert.Equal(receive, activeOutput.ScriptPubKey);
                Assert.Equal(amountToSend, activeOutput.Amount);
                Assert.Contains(res.Transaction.Transaction.Outputs, x => x.Value == changeOutput.Amount);
                Logger.LogInfo($"{nameof(res.Fee)}: {res.Fee}");
                Logger.LogInfo($"{nameof(res.FeePercentOfSent)}: {res.FeePercentOfSent} %");
                Logger.LogInfo($"{nameof(res.SpendsUnconfirmed)}: {res.SpendsUnconfirmed}");
                Logger.LogInfo($"Active Output: {activeOutput.Amount.ToString(false, true)} {activeOutput.ScriptPubKey.GetDestinationAddress(network)}");
                Logger.LogInfo($"Change Output: {changeOutput.Amount.ToString(false, true)} {changeOutput.ScriptPubKey.GetDestinationAddress(network)}");
                Logger.LogInfo($"TxId: {res.Transaction.GetHash()}");

                foundReceive = false;
                Assert.InRange(res.Transaction.Transaction.Outputs.Count, 1, 2);
                foreach (var output in res.Transaction.Transaction.Outputs)
                {
                    if (output.ScriptPubKey == receive)
                    {
                        foundReceive = true;
                        Assert.Equal(amountToSend, output.Value);
                    }
                }
                Assert.True(foundReceive);

                Assert.InRange(res.Fee, Money.Zero, res.Fee);
                Assert.InRange(res.Fee, res.Fee, res.Fee);

                await broadcaster.SendTransactionAsync(res.Transaction);

                #endregion HighFee

                #region MaxAmount

                receive = keyManager.GetNextReceiveKey("MaxAmount", out _).P2wpkhScript;

                res = wallet.BuildTransaction(password, new PaymentIntent(receive, MoneyRequest.CreateAllRemaining(), "foo"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true);

                Assert.Single(res.InnerWalletOutputs);
                Assert.Empty(res.OuterWalletOutputs);
                activeOutput = res.InnerWalletOutputs.Single();

                Assert.Equal(receive, activeOutput.ScriptPubKey);

                Assert.Single(res.Transaction.Transaction.Outputs);
                var maxBuiltTxOutput = res.Transaction.Transaction.Outputs.Single();
                Assert.Equal(receive, maxBuiltTxOutput.ScriptPubKey);
                Assert.Equal(wallet.Coins.Where(x => !x.Unavailable).Sum(x => x.Amount) - res.Fee, maxBuiltTxOutput.Value);

                await broadcaster.SendTransactionAsync(res.Transaction);

                #endregion MaxAmount

                #region InputSelection

                receive = keyManager.GetNextReceiveKey("InputSelection", out _).P2wpkhScript;

                var inputCountBefore = res.SpentCoins.Count();

                res = wallet.BuildTransaction(password, new PaymentIntent(receive, MoneyRequest.CreateAllRemaining(), "foo"), FeeStrategy.SevenDaysConfirmationTargetStrategy,
                                              allowUnconfirmed: true,
                                              allowedInputs: wallet.Coins.Where(x => !x.Unavailable).Select(x => x.OutPoint).Take(1));

                Assert.Single(res.InnerWalletOutputs);
                Assert.Empty(res.OuterWalletOutputs);
                activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive);

                Assert.True(inputCountBefore >= res.SpentCoins.Count());
                Assert.Equal(res.SpentCoins.Count(), res.Transaction.Transaction.Inputs.Count);

                Assert.Equal(receive, activeOutput.ScriptPubKey);
                Logger.LogInfo($"{nameof(res.Fee)}: {res.Fee}");
                Logger.LogInfo($"{nameof(res.FeePercentOfSent)}: {res.FeePercentOfSent} %");
                Logger.LogInfo($"{nameof(res.SpendsUnconfirmed)}: {res.SpendsUnconfirmed}");
                Logger.LogInfo($"Active Output: {activeOutput.Amount.ToString(false, true)} {activeOutput.ScriptPubKey.GetDestinationAddress(network)}");
                Logger.LogInfo($"TxId: {res.Transaction.GetHash()}");

                Assert.Single(res.Transaction.Transaction.Outputs);

                res = wallet.BuildTransaction(password, new PaymentIntent(receive, MoneyRequest.CreateAllRemaining(), "foo"), FeeStrategy.SevenDaysConfirmationTargetStrategy,
                                              allowUnconfirmed: true,
                                              allowedInputs: new[] { res.SpentCoins.Select(x => x.OutPoint).First() });

                Assert.Single(res.InnerWalletOutputs);
                Assert.Empty(res.OuterWalletOutputs);
                activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive);

                Assert.Single(res.Transaction.Transaction.Inputs);
                Assert.Single(res.Transaction.Transaction.Outputs);
                Assert.Single(res.SpentCoins);

                #endregion InputSelection

                #region Labeling

                Script receive2 = keyManager.GetNextReceiveKey("foo", out _).P2wpkhScript;
                res = wallet.BuildTransaction(password, new PaymentIntent(receive2, MoneyRequest.CreateAllRemaining(), "my label"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true);

                Assert.Single(res.InnerWalletOutputs);
                Assert.Equal("foo, my label", res.InnerWalletOutputs.Single().Label);

                amountToSend = wallet.Coins.Where(x => !x.Unavailable).Sum(x => x.Amount) / 3;
                res          = wallet.BuildTransaction(
                    password,
                    new PaymentIntent(
                        new DestinationRequest(new Key(), amountToSend, label: "outgoing"),
                        new DestinationRequest(new Key(), amountToSend, label: "outgoing2")),
                    FeeStrategy.SevenDaysConfirmationTargetStrategy,
                    allowUnconfirmed: true);

                Assert.Single(res.InnerWalletOutputs);
                Assert.Equal(2, res.OuterWalletOutputs.Count());
                IEnumerable <string> change = res.InnerWalletOutputs.Single().Label.Labels;
                Assert.Contains("outgoing", change);
                Assert.Contains("outgoing2", change);

                await broadcaster.SendTransactionAsync(res.Transaction);

                IEnumerable <SmartCoin> unconfirmedCoins      = wallet.Coins.Where(x => x.Height == Height.Mempool).ToArray();
                IEnumerable <string>    unconfirmedCoinLabels = unconfirmedCoins.SelectMany(x => x.Label.Labels).ToArray();
                Assert.Contains("outgoing", unconfirmedCoinLabels);
                Assert.Contains("outgoing2", unconfirmedCoinLabels);
                IEnumerable <string> allKeyLabels = keyManager.GetKeys().SelectMany(x => x.Label.Labels);
                Assert.Contains("outgoing", allKeyLabels);
                Assert.Contains("outgoing2", allKeyLabels);

                Interlocked.Exchange(ref Common.FiltersProcessedByWalletCount, 0);
                await rpc.GenerateAsync(1);

                await Common.WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), 1);

                var bestHeight = new Height(bitcoinStore.SmartHeaderChain.TipHeight);
                IEnumerable <string> confirmedCoinLabels = wallet.Coins.Where(x => x.Height == bestHeight).SelectMany(x => x.Label.Labels);
                Assert.Contains("outgoing", confirmedCoinLabels);
                Assert.Contains("outgoing2", confirmedCoinLabels);
                allKeyLabels = keyManager.GetKeys().SelectMany(x => x.Label.Labels);
                Assert.Contains("outgoing", allKeyLabels);
                Assert.Contains("outgoing2", allKeyLabels);

                #endregion Labeling

                #region AllowedInputsDisallowUnconfirmed

                inputCountBefore = res.SpentCoins.Count();

                receive = keyManager.GetNextReceiveKey("AllowedInputsDisallowUnconfirmed", out _).P2wpkhScript;

                var allowedInputs = wallet.Coins.Where(x => !x.Unavailable).Select(x => x.OutPoint).Take(1);
                var toSend        = new PaymentIntent(receive, MoneyRequest.CreateAllRemaining(), "fizz");

                // covers:
                // disallow unconfirmed with allowed inputs
                res = wallet.BuildTransaction(password, toSend, FeeStrategy.TwentyMinutesConfirmationTargetStrategy, false, allowedInputs: allowedInputs);

                activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive);
                Assert.Single(res.InnerWalletOutputs);
                Assert.Empty(res.OuterWalletOutputs);

                Assert.Equal(receive, activeOutput.ScriptPubKey);
                Logger.LogDebug($"{nameof(res.Fee)}: {res.Fee}");
                Logger.LogDebug($"{nameof(res.FeePercentOfSent)}: {res.FeePercentOfSent} %");
                Logger.LogDebug($"{nameof(res.SpendsUnconfirmed)}: {res.SpendsUnconfirmed}");
                Logger.LogDebug($"Active Output: {activeOutput.Amount.ToString(false, true)} {activeOutput.ScriptPubKey.GetDestinationAddress(network)}");
                Logger.LogDebug($"TxId: {res.Transaction.GetHash()}");

                Assert.True(inputCountBefore >= res.SpentCoins.Count());
                Assert.False(res.SpendsUnconfirmed);

                Assert.Single(res.Transaction.Transaction.Inputs);
                Assert.Single(res.Transaction.Transaction.Outputs);
                Assert.Single(res.SpentCoins);

                Assert.True(inputCountBefore >= res.SpentCoins.Count());
                Assert.Equal(res.SpentCoins.Count(), res.Transaction.Transaction.Inputs.Count);

                #endregion AllowedInputsDisallowUnconfirmed

                #region CustomChange

                // covers:
                // customchange
                // feePc > 1
                var k1 = new Key();
                var k2 = new Key();
                res = wallet.BuildTransaction(
                    password,
                    new PaymentIntent(
                        new DestinationRequest(k1, MoneyRequest.CreateChange()),
                        new DestinationRequest(k2, Money.Coins(0.0003m), label: "outgoing")),
                    FeeStrategy.TwentyMinutesConfirmationTargetStrategy);

                Assert.Contains(k1.ScriptPubKey, res.OuterWalletOutputs.Select(x => x.ScriptPubKey));
                Assert.Contains(k2.ScriptPubKey, res.OuterWalletOutputs.Select(x => x.ScriptPubKey));

                #endregion CustomChange

                #region FeePcHigh

                res = wallet.BuildTransaction(
                    password,
                    new PaymentIntent(new Key(), Money.Coins(0.0003m), label: "outgoing"),
                    FeeStrategy.TwentyMinutesConfirmationTargetStrategy);

                Assert.True(res.FeePercentOfSent > 1);

                var newChangeK = keyManager.GenerateNewKey("foo", KeyState.Clean, isInternal: true);
                res = wallet.BuildTransaction(
                    password,
                    new PaymentIntent(
                        new DestinationRequest(newChangeK.P2wpkhScript, MoneyRequest.CreateChange(), "boo"),
                        new DestinationRequest(new Key(), Money.Coins(0.0003m), label: "outgoing")),
                    FeeStrategy.TwentyMinutesConfirmationTargetStrategy);

                Assert.True(res.FeePercentOfSent > 1);
                Assert.Single(res.OuterWalletOutputs);
                Assert.Single(res.InnerWalletOutputs);
                SmartCoin changeRes = res.InnerWalletOutputs.Single();
                Assert.Equal(newChangeK.P2wpkhScript, changeRes.ScriptPubKey);
                Assert.Equal(newChangeK.Label, changeRes.Label);
                Assert.Equal(KeyState.Clean, newChangeK.KeyState);                 // Still clean, because the tx wasn't yet propagated.

                #endregion FeePcHigh
            }
            finally
            {
                bitcoinStore.IndexStore.NewFilter -= Common.Wallet_NewFilterProcessed;
                await walletManager.RemoveAndStopAllAsync(CancellationToken.None);

                // Dispose wasabi synchronizer service.
                if (synchronizer is { })
Ejemplo n.º 9
0
        protected override async Task BuildTransaction(string password, PaymentIntent payments, FeeStrategy feeStrategy, bool allowUnconfirmed = false, IEnumerable <OutPoint> allowedInputs = null)
        {
            BuildTransactionResult result = await Task.Run(() => Wallet.BuildTransaction(Password, payments, feeStrategy, allowUnconfirmed: true, allowedInputs: allowedInputs, GetPayjoinClient()));

            MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.SigningTransaction);
            SmartTransaction signedTransaction = result.Transaction;

            if (Wallet.KeyManager.IsHardwareWallet && !result.Signed)             // If hardware but still has a privkey then it's password, then meh.
            {
                try
                {
                    IsHardwareBusy = true;
                    MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.AcquiringSignatureFromHardwareWallet);
                    var client = new HwiClient(Global.Network);

                    using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
                    PSBT signedPsbt = null;
                    try
                    {
                        try
                        {
                            signedPsbt = await client.SignTxAsync(Wallet.KeyManager.MasterFingerprint.Value, result.Psbt, cts.Token);
                        }
                        catch (PSBTException ex) when(ex.Message.Contains("NullFail"))
                        {
                            NotificationHelpers.Warning("Fall back to Unverified Inputs Mode, trying to sign again.");

                            // Ledger Nano S hackfix https://github.com/MetacoSA/NBitcoin/pull/888

                            var noinputtx = result.Psbt.Clone();

                            foreach (var input in noinputtx.Inputs)
                            {
                                input.NonWitnessUtxo = null;
                            }

                            signedPsbt = await client.SignTxAsync(Wallet.KeyManager.MasterFingerprint.Value, noinputtx, cts.Token);
                        }
                    }
                    catch (HwiException)
                    {
                        await PinPadViewModel.UnlockAsync();

                        signedPsbt = await client.SignTxAsync(Wallet.KeyManager.MasterFingerprint.Value, result.Psbt, cts.Token);
                    }
                    signedTransaction = signedPsbt.ExtractSmartTransaction(result.Transaction);
                }
                finally
                {
                    MainWindowViewModel.Instance.StatusBar.TryRemoveStatus(StatusType.AcquiringSignatureFromHardwareWallet);
                    IsHardwareBusy = false;
                }
            }

            MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.BroadcastingTransaction);
            await Task.Run(async() => await Global.TransactionBroadcaster.SendTransactionAsync(signedTransaction));

            ResetUi();
        }
Ejemplo n.º 10
0
        protected override async Task BuildTransaction(string password, PaymentIntent payments, FeeStrategy feeStrategy, bool allowUnconfirmed = false, IEnumerable <OutPoint> allowedInputs = null)
        {
            BuildTransactionResult result = await Task.Run(() => Wallet.BuildTransaction(Password, payments, feeStrategy, allowUnconfirmed: true, allowedInputs: allowedInputs));

            var txviewer = new TransactionViewerViewModel();

            IoC.Get <IShell>().AddDocument(txviewer);
            IoC.Get <IShell>().Select(txviewer);

            txviewer.Update(result);
            ResetUi();
            NotificationHelpers.Success("Transaction was built.");
        }
Ejemplo n.º 11
0
        public async Task BuildTransactionValidationsTestAsync()
        {
            (string password, IRPCClient rpc, Network network, Coordinator coordinator, ServiceConfiguration serviceConfiguration, BitcoinStore bitcoinStore, Backend.Global global) = await Common.InitializeTestEnvironmentAsync(RegTestFixture, 1);

            // Create the services.
            // 1. Create connection service.
            var nodes = new NodesGroup(global.Config.Network, requirements: Constants.NodeRequirements);

            nodes.ConnectedNodes.Add(await RegTestFixture.BackendRegTestNode.CreateNewP2pNodeAsync());

            // 2. Create mempool service.

            Node node = await RegTestFixture.BackendRegTestNode.CreateNewP2pNodeAsync();

            node.Behaviors.Add(bitcoinStore.CreateUntrustedP2pBehavior());

            // 3. Create wasabi synchronizer service.
            var synchronizer = new WasabiSynchronizer(rpc.Network, bitcoinStore, new Uri(RegTestFixture.BackendEndPoint), null);

            // 4. Create key manager service.
            var keyManager = KeyManager.CreateNew(out _, password);

            // 5. Create wallet service.
            var workDir          = Common.GetWorkDir();
            var blocksFolderPath = Path.Combine(workDir, "Blocks", network.ToString());
            CachedBlockProvider blockProvider = new CachedBlockProvider(
                new P2pBlockProvider(nodes, null, synchronizer, serviceConfiguration, network),
                new FileSystemBlockRepository(blocksFolderPath, network));

            using var wallet           = Wallet.CreateAndRegisterServices(network, bitcoinStore, keyManager, synchronizer, nodes, workDir, serviceConfiguration, synchronizer, blockProvider);
            wallet.NewFilterProcessed += Common.Wallet_NewFilterProcessed;

            var scp = new Key().ScriptPubKey;

            var validIntent   = new PaymentIntent(scp, Money.Coins(1));
            var invalidIntent = new PaymentIntent(
                new DestinationRequest(scp, Money.Coins(10 * 1000 * 1000)),
                new DestinationRequest(scp, Money.Coins(12 * 1000 * 1000)));

            Assert.Throws <OverflowException>(() => new PaymentIntent(
                                                  new DestinationRequest(scp, Money.Satoshis(long.MaxValue)),
                                                  new DestinationRequest(scp, Money.Satoshis(long.MaxValue)),
                                                  new DestinationRequest(scp, Money.Satoshis(5))));

            Logger.TurnOff();
            Assert.Throws <ArgumentNullException>(() => wallet.BuildTransaction(null, null, FeeStrategy.CreateFromConfirmationTarget(4)));

            // toSend cannot have a null element
            Assert.Throws <ArgumentNullException>(() => wallet.BuildTransaction(null, new PaymentIntent(new[] { (DestinationRequest)null }), FeeStrategy.CreateFromConfirmationTarget(0)));

            // toSend cannot have a zero element
            Assert.Throws <ArgumentException>(() => wallet.BuildTransaction(null, new PaymentIntent(Array.Empty <DestinationRequest>()), FeeStrategy.SevenDaysConfirmationTargetStrategy));

            // feeTarget has to be in the range 0 to 1008
            Assert.Throws <ArgumentOutOfRangeException>(() => wallet.BuildTransaction(null, validIntent, FeeStrategy.CreateFromConfirmationTarget(-10)));
            Assert.Throws <ArgumentOutOfRangeException>(() => wallet.BuildTransaction(null, validIntent, FeeStrategy.CreateFromConfirmationTarget(2000)));

            // toSend amount sum has to be in range 0 to 2099999997690000
            Assert.Throws <ArgumentOutOfRangeException>(() => wallet.BuildTransaction(null, invalidIntent, FeeStrategy.TwentyMinutesConfirmationTargetStrategy));

            // toSend negative sum amount
            Assert.Throws <ArgumentOutOfRangeException>(() => wallet.BuildTransaction(null, new PaymentIntent(scp, Money.Satoshis(-10000)), FeeStrategy.TwentyMinutesConfirmationTargetStrategy));

            // toSend negative operation amount
            Assert.Throws <ArgumentOutOfRangeException>(() => wallet.BuildTransaction(
                                                            null,
                                                            new PaymentIntent(
                                                                new DestinationRequest(scp, Money.Satoshis(20000)),
                                                                new DestinationRequest(scp, Money.Satoshis(-10000))),
                                                            FeeStrategy.TwentyMinutesConfirmationTargetStrategy));

            // allowedInputs cannot be empty
            Assert.Throws <ArgumentException>(() => wallet.BuildTransaction(null, validIntent, FeeStrategy.TwentyMinutesConfirmationTargetStrategy, allowedInputs: Array.Empty <OutPoint>()));

            // "Only one element can contain the AllRemaining flag.
            Assert.Throws <ArgumentException>(() => wallet.BuildTransaction(
                                                  password,
                                                  new PaymentIntent(
                                                      new DestinationRequest(scp, MoneyRequest.CreateAllRemaining(), "zero"),
                                                      new DestinationRequest(scp, MoneyRequest.CreateAllRemaining(), "zero")),
                                                  FeeStrategy.SevenDaysConfirmationTargetStrategy,
                                                  false));

            // Get some money, make it confirm.
            var txId = await rpc.SendToAddressAsync(keyManager.GetNextReceiveKey("foo", out _).GetP2wpkhAddress(network), Money.Coins(1m));

            // Generate some coins
            await rpc.GenerateAsync(2);

            try
            {
                nodes.Connect();                                                                              // Start connection service.
                node.VersionHandshake();                                                                      // Start mempool service.
                synchronizer.Start(requestInterval: TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(5), 10000); // Start wasabi synchronizer service.

                // Wait until the filter our previous transaction is present.
                var blockCount = await rpc.GetBlockCountAsync();

                await Common.WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), blockCount);

                using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
                {
                    await wallet.StartAsync(cts.Token);                     // Initialize wallet service.
                }

                // subtract Fee from amount index with no enough money
                var operations = new PaymentIntent(
                    new DestinationRequest(scp, Money.Coins(1m), subtractFee: true),
                    new DestinationRequest(scp, Money.Coins(0.5m)));
                Assert.Throws <InsufficientBalanceException>(() => wallet.BuildTransaction(password, operations, FeeStrategy.TwentyMinutesConfirmationTargetStrategy, false));

                // No enough money (only one confirmed coin, no unconfirmed allowed)
                operations = new PaymentIntent(scp, Money.Coins(1.5m));
                Assert.Throws <InsufficientBalanceException>(() => wallet.BuildTransaction(null, operations, FeeStrategy.TwentyMinutesConfirmationTargetStrategy));

                // No enough money (only one confirmed coin, unconfirmed allowed)
                Assert.Throws <InsufficientBalanceException>(() => wallet.BuildTransaction(null, operations, FeeStrategy.TwentyMinutesConfirmationTargetStrategy, true));

                // Add new money with no confirmation
                var txId2 = await rpc.SendToAddressAsync(keyManager.GetNextReceiveKey("bar", out _).GetP2wpkhAddress(network), Money.Coins(2m));

                await Task.Delay(1000);                 // Wait tx to arrive and get processed.

                // Enough money (one confirmed coin and one unconfirmed coin, unconfirmed are NOT allowed)
                Assert.Throws <InsufficientBalanceException>(() => wallet.BuildTransaction(null, operations, FeeStrategy.TwentyMinutesConfirmationTargetStrategy, false));

                // Enough money (one unconfirmed coin, unconfirmed are allowed)
                var btx       = wallet.BuildTransaction(password, operations, FeeStrategy.TwentyMinutesConfirmationTargetStrategy, true);
                var spentCoin = Assert.Single(btx.SpentCoins);
                Assert.False(spentCoin.Confirmed);

                // Enough money (one confirmed coin and one unconfirmed coin, unconfirmed are allowed)
                operations = new PaymentIntent(scp, Money.Coins(2.5m));
                btx        = wallet.BuildTransaction(password, operations, FeeStrategy.TwentyMinutesConfirmationTargetStrategy, true);
                Assert.Equal(2, btx.SpentCoins.Count());
                Assert.Equal(1, btx.SpentCoins.Count(c => c.Confirmed));
                Assert.Equal(1, btx.SpentCoins.Count(c => !c.Confirmed));

                // Only one operation with AllRemainingFlag

                Assert.Throws <ArgumentException>(() => wallet.BuildTransaction(
                                                      null,
                                                      new PaymentIntent(
                                                          new DestinationRequest(scp, MoneyRequest.CreateAllRemaining()),
                                                          new DestinationRequest(scp, MoneyRequest.CreateAllRemaining())),
                                                      FeeStrategy.TwentyMinutesConfirmationTargetStrategy));

                Logger.TurnOn();

                operations = new PaymentIntent(scp, Money.Coins(0.5m));
                btx        = wallet.BuildTransaction(password, operations, FeeStrategy.TwentyMinutesConfirmationTargetStrategy);
            }
            finally
            {
                await wallet.StopAsync(CancellationToken.None);

                // Dispose wasabi synchronizer service.
                if (synchronizer is { })
Ejemplo n.º 12
0
        protected override async Task BuildTransaction(string password, PaymentIntent payments, FeeStrategy feeStrategy, bool allowUnconfirmed = false, IEnumerable <OutPoint> allowedInputs = null)
        {
            BuildTransactionResult result = await Task.Run(() => Wallet.BuildTransaction(Password, payments, feeStrategy, allowUnconfirmed: true, allowedInputs: allowedInputs, GetPayjoinClient()));

            MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.SigningTransaction);
            SmartTransaction signedTransaction = result.Transaction;

            if (Wallet.KeyManager.IsHardwareWallet && !result.Signed)             // If hardware but still has a privkey then it's password, then meh.
            {
                try
                {
                    IsHardwareBusy = true;
                    MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.AcquiringSignatureFromHardwareWallet);
                    var client = new HwiClient(Global.Network);

                    using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
                    PSBT signedPsbt = null;
                    try
                    {
                        signedPsbt = await client.SignTxAsync(Wallet.KeyManager.MasterFingerprint.Value, result.Psbt, cts.Token);
                    }
                    catch (HwiException)
                    {
                        await PinPadViewModel.UnlockAsync();

                        signedPsbt = await client.SignTxAsync(Wallet.KeyManager.MasterFingerprint.Value, result.Psbt, cts.Token);
                    }
                    signedTransaction = signedPsbt.ExtractSmartTransaction(result.Transaction);
                }
                finally
                {
                    MainWindowViewModel.Instance.StatusBar.TryRemoveStatus(StatusType.AcquiringSignatureFromHardwareWallet);
                    IsHardwareBusy = false;
                }
            }

            MainWindowViewModel.Instance.StatusBar.TryAddStatus(StatusType.BroadcastingTransaction);
            await Task.Run(async() => await Global.TransactionBroadcaster.SendTransactionAsync(signedTransaction));

            ResetUi();
        }
Ejemplo n.º 13
0
        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());
        }
Ejemplo n.º 14
0
        public async Task <bool> BuildTransaction(string password)
        {
            try
            {
                IsBusy   = true;
                password = Guard.Correct(password);
                Memo     = Memo.Trim(',', ' ').Trim();

                var selectedCoinViewModels = SendAmountViewModel.CoinList.CoinList.Where(cvm => cvm.IsSelected);
                var selectedCoinReferences = selectedCoinViewModels.Select(cvm => cvm.Model.OutPoint).ToList();

                if (!selectedCoinReferences.Any())
                {
                    //SetWarningMessage("No coins are selected to spend.");
                    return(false);
                }

                BitcoinAddress address;
                try
                {
                    address = BitcoinAddress.Create(Address.Trim(), Global.Network);
                }
                catch (FormatException)
                {
                    // SetWarningMessage("Invalid address.");
                    return(false);
                }

                var amount = Money.Zero;

                var requests = new List <DestinationRequest>();

                MoneyRequest moneyRequest;
                if (SendAmountViewModel.IsMax)
                {
                    moneyRequest = MoneyRequest.CreateAllRemaining(subtractFee: true);
                }
                else
                {
                    if (!Money.TryParse(SendAmountViewModel.AmountText, out amount) || amount == Money.Zero)
                    {
                        // SetWarningMessage($"Invalid amount.");
                        return(false);
                    }

                    if (amount == selectedCoinViewModels.Sum(x => x.Amount))
                    {
                        // NotificationHelpers.Warning("Looks like you want to spend whole coins. Try Max button instead.", "");
                        return(false);
                    }
                    moneyRequest = MoneyRequest.Create(amount, subtractFee: false);
                }

                if (SendAmountViewModel.FeeRate is null || SendAmountViewModel.FeeRate.SatoshiPerByte < 1)
                {
                    return(false);
                }

                var feeStrategy = FeeStrategy.CreateFromFeeRate(SendAmountViewModel.FeeRate);

                var smartLabel = new SmartLabel(Memo);
                var activeDestinationRequest = new DestinationRequest(address, moneyRequest, smartLabel);
                requests.Add(activeDestinationRequest);
                var intent = new PaymentIntent(requests);

                var result = await Task.Run(() => Global.Wallet.BuildTransaction(
                                                password,
                                                intent,
                                                feeStrategy,
                                                allowUnconfirmed: true,
                                                allowedInputs: selectedCoinReferences));

                SmartTransaction signedTransaction = result.Transaction;

                await Global.TransactionBroadcaster.SendTransactionAsync(signedTransaction); // put this on non-ui theread?

                return(true);
            }
            catch (InsufficientBalanceException ex)
            {
                Money needed = ex.Minimum - ex.Actual;
                Logger.LogDebug(ex);
                //SetWarningMessage($"Not enough coins selected. You need an estimated {needed.ToString(false, true)} BTC more to make this transaction.");
            }
            catch (Exception ex)
            {
                Logger.LogDebug(ex);
                //SetWarningMessage(ex.ToTypeMessageString());
            }
            finally
            {
                IsBusy = false;
            }
            return(false);
        }