コード例 #1
0
        protected virtual bool TryTransfer(IDestination destination, decimal amount, out Transaction tx, out string error)
        {
            try
            {
                tx = Wallet.BuildTransaction(destination, amount);

                if (Wallet.TryBroadcast(tx, out var result))
                {
                    error = String.Empty;
                    return(true);
                }

                error = _("Error - Transaction transmission failed. {0}", _(result));
            }
            catch (NotEnoughFundsException)
            {
                error = _("Insufficient funds.");
            }
            catch (ArgumentOutOfRangeException e)
            {
                error = _("Invalid amount. {0}", _(e.Message));
            }
            catch (ArgumentException e)
            {
                error = _("Error - Transaction generation failed. {0}", _(e.Message));
            }

            tx = null;

            return(false);
        }
コード例 #2
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();
        }
コード例 #3
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.");
        }
コード例 #4
0
        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));
        }
コード例 #5
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);
        }
コード例 #6
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);
    }
コード例 #7
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();
        }
コード例 #8
0
        protected SendControlViewModel(Wallet wallet, string title)
            : base(title)
        {
            Global = Locator.Current.GetService <Global>();
            Wallet = wallet;

            LabelSuggestion            = new SuggestLabelViewModel();
            BuildTransactionButtonText = DoButtonText;

            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)));

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

            OnAddressPasteCommand = ReactiveCommand.Create((BitcoinUrlBuilder url) =>
            {
                SmartLabel label = url.Label;
                if (!label.IsEmpty)
                {
                    LabelSuggestion.Label = label;
                }

                if (url.Amount != null)
                {
                    AmountText = url.Amount.ToString(false, true);
                }
            });

            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 (Global.UiConfig.IsCustomChangeAddress && !IsMax && !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;
                        }
                    }

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

                    await DoAfterBuildTransaction(result);
                }
                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());
                    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);
            });
        }
コード例 #9
0
        public async Task BuildTransactionValidationsTestAsync()
        {
            (string password, RPCClient 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();

            using var wallet           = new Wallet(network, bitcoinStore, keyManager, synchronizer, nodes, workDir, serviceConfiguration, synchronizer);
            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 { })