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); }
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(); }
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."); }
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)); }
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); }
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); }
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(); }
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); }); }
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 { })