private async Task OnNext(Wallet wallet, TransactionBroadcaster broadcaster, BuildTransactionResult transaction) { var transactionAuthorizationInfo = new TransactionAuthorizationInfo(transaction); var authResult = await AuthorizeAsync(wallet, transactionAuthorizationInfo); if (authResult) { IsBusy = true; try { var finalTransaction = await GetFinalTransactionAsync(transactionAuthorizationInfo.Transaction, _info); await SendTransaction(wallet, broadcaster, finalTransaction); Navigate().To(new SendSuccessViewModel()); } catch (Exception ex) { await ShowErrorAsync("Transaction", ex.ToUserFriendlyString(), "Wasabi was unable to send your transaction."); } IsBusy = false; } }
private async Task SendTransaction(Wallet wallet, TransactionBroadcaster broadcaster, SmartTransaction transaction) { // Dequeue any coin-joining coins. await wallet.ChaumianClient.DequeueAllCoinsFromMixAsync(DequeueReason.TransactionBuilding); await broadcaster.SendTransactionAsync(transaction); }
public BroadcastTransactionViewModel( BitcoinStore store, Network network, TransactionBroadcaster broadcaster, SmartTransaction transaction) { Title = "Broadcast Transaction"; var nullMoney = new Money(-1L); var nullOutput = new TxOut(nullMoney, Script.Empty); var psbt = PSBT.FromTransaction(transaction.Transaction, network); TxOut GetOutput(OutPoint outpoint) => store.TransactionStore.TryGetTransaction(outpoint.Hash, out var prevTxn) ? prevTxn.Transaction.Outputs[outpoint.N] : nullOutput; var inputAddressAmount = psbt.Inputs .Select(x => x.PrevOut) .Select(GetOutput) .ToArray(); var outputAddressAmount = psbt.Outputs .Select(x => x.GetCoin().TxOut) .ToArray(); var psbtTxn = psbt.GetOriginalTransaction(); _transactionId = psbtTxn.GetHash().ToString(); _inputCount = inputAddressAmount.Length; _inputCountString = $" input{TextHelpers.AddSIfPlural(_inputCount)} and "; _outputCount = outputAddressAmount.Length; _outputCountString = $" output{TextHelpers.AddSIfPlural(_outputCount)}."; _totalInputValue = inputAddressAmount.Any(x => x.Value == nullMoney) ? null : inputAddressAmount.Select(x => x.Value).Sum(); _totalOutputValue = outputAddressAmount.Any(x => x.Value == nullMoney) ? null : outputAddressAmount.Select(x => x.Value).Sum(); _networkFee = TotalInputValue is null || TotalOutputValue is null ? null : TotalInputValue - TotalOutputValue; EnableCancel = true; EnableBack = false; this.WhenAnyValue(x => x.IsBusy) .Subscribe(x => EnableCancel = !x); var nextCommandCanExecute = this.WhenAnyValue(x => x.IsBusy) .Select(x => !x); NextCommand = ReactiveCommand.CreateFromTask( async() => await OnNext(broadcaster, transaction), nextCommandCanExecute); EnableAutoBusyOn(NextCommand); }
public Global(string dataDir, string torLogsFile, Config config, UiConfig uiConfig, WalletManager walletManager) { using (BenchmarkLogger.Measure()) { StoppingCts = new CancellationTokenSource(); DataDir = dataDir; Config = config; UiConfig = uiConfig; TorSettings = new TorSettings(DataDir, torLogsFile, distributionFolderPath: EnvironmentHelpers.GetFullBaseDirectory()); HostedServices = new HostedServices(); WalletManager = walletManager; WalletManager.OnDequeue += WalletManager_OnDequeue; WalletManager.WalletRelevantTransactionProcessed += WalletManager_WalletRelevantTransactionProcessed; var networkWorkFolderPath = Path.Combine(DataDir, "BitcoinStore", Network.ToString()); var transactionStore = new AllTransactionStore(networkWorkFolderPath, Network); var indexStore = new IndexStore(Path.Combine(networkWorkFolderPath, "IndexStore"), Network, new SmartHeaderChain()); var mempoolService = new MempoolService(); var blocks = new FileSystemBlockRepository(Path.Combine(networkWorkFolderPath, "Blocks"), Network); BitcoinStore = new BitcoinStore(indexStore, transactionStore, mempoolService, blocks); HttpClientFactory = Config.UseTor ? new HttpClientFactory(Config.TorSocks5EndPoint, backendUriGetter: () => Config.GetCurrentBackendUri()) : new HttpClientFactory(torEndPoint: null, backendUriGetter: () => Config.GetFallbackBackendUri()); Synchronizer = new WasabiSynchronizer(Network, BitcoinStore, HttpClientFactory); LegalChecker = new(DataDir); TransactionBroadcaster = new TransactionBroadcaster(Network, BitcoinStore, HttpClientFactory, WalletManager); } }
public TransactionPreviewViewModel(Wallet wallet, TransactionInfo info, TransactionBroadcaster broadcaster, BuildTransactionResult transaction) { EnableCancel = true; EnableBack = true; var destinationAmount = transaction.CalculateDestinationAmount().ToDecimal(MoneyUnit.BTC); var btcAmountText = $"{destinationAmount} bitcoins "; var fiatAmountText = destinationAmount.GenerateFiatText(wallet.Synchronizer.UsdExchangeRate, "USD"); AmountText = $"{btcAmountText}{fiatAmountText}"; Labels = info.Labels.Labels.ToArray(); AddressText = info.Address.ToString(); ConfirmationTimeText = $"Approximately {TextHelpers.TimeSpanToFriendlyString(info.ConfirmationTimeSpan)} "; var fee = transaction.Fee; var btcFeeText = $"{fee.ToDecimal(MoneyUnit.Satoshi)} satoshis "; var fiatFeeText = fee.ToDecimal(MoneyUnit.BTC).GenerateFiatText(wallet.Synchronizer.UsdExchangeRate, "USD"); FeeText = $"{btcFeeText}{fiatFeeText}"; NextCommand = ReactiveCommand.CreateFromTask(async() => await OnNext(wallet, broadcaster, transaction)); }
public TransactionPreviewViewModel(Wallet wallet, TransactionInfo info, TransactionBroadcaster broadcaster, BuildTransactionResult transaction) { _wallet = wallet; _info = info; EnableCancel = true; EnableBack = true; _confirmationTimeText = ""; var destinationAmount = transaction.CalculateDestinationAmount().ToDecimal(MoneyUnit.BTC); var btcAmountText = $"{destinationAmount} bitcoins "; var fiatAmountText = destinationAmount.GenerateFiatText(wallet.Synchronizer.UsdExchangeRate, "USD"); AmountText = $"{btcAmountText}{fiatAmountText}"; AddressText = info.Address.ToString(); var fee = transaction.Fee; var btcFeeText = $"{fee.ToDecimal(MoneyUnit.Satoshi)} satoshis "; var fiatFeeText = fee.ToDecimal(MoneyUnit.BTC).GenerateFiatText(wallet.Synchronizer.UsdExchangeRate, "USD"); FeeText = $"{btcFeeText}{fiatFeeText}"; PayJoinUrl = info.PayJoinClient?.PaymentUrl.AbsoluteUri; IsPayJoin = PayJoinUrl is { }; NextCommand = ReactiveCommand.CreateFromTask(async() => await OnNext(wallet, broadcaster, transaction)); }
public OptimisePrivacyViewModel(Wallet wallet, TransactionInfo transactionInfo, TransactionBroadcaster broadcaster, BuildTransactionResult requestedTransaction) { _wallet = wallet; _requestedTransaction = requestedTransaction; _transactionInfo = transactionInfo; this.WhenAnyValue(x => x.SelectedPrivacySuggestion) .Where(x => x is { })
public SendViewModel(WalletViewModel walletVm, TransactionBroadcaster broadcaster) : base(NavigationMode.Normal) { _to = ""; _owner = walletVm; _transactionInfo = new TransactionInfo(); _labels = new ObservableCollection <string>(); _lastXAxisCurrentValue = _xAxisCurrentValue; ExchangeRate = walletVm.Wallet.Synchronizer.UsdExchangeRate; PriorLabels = new(); this.ValidateProperty(x => x.To, ValidateToField); this.ValidateProperty(x => x.AmountBtc, ValidateAmount); this.WhenAnyValue(x => x.To) .Subscribe(ParseToField); this.WhenAnyValue(x => x.AmountBtc) .Subscribe(x => _transactionInfo.Amount = new Money(x, MoneyUnit.BTC)); this.WhenAnyValue(x => x.XAxisCurrentValue) .Subscribe(x => { if (x > 0) { _transactionInfo.FeeRate = new FeeRate(GetYAxisValueFromXAxisCurrentValue(x)); SetXAxisCurrentValueIndex(x); } }); this.WhenAnyValue(x => x.XAxisCurrentValueIndex) .Subscribe(SetXAxisCurrentValue); Labels.ToObservableChangeSet().Subscribe(x => _transactionInfo.Labels = new SmartLabel(_labels.ToArray())); EnableCancel = true; EnableBack = true; PasteCommand = ReactiveCommand.CreateFromTask(async() => await OnPaste()); var nextCommandCanExecute = this.WhenAnyValue(x => x.Labels, x => x.AmountBtc, x => x.To, x => x.XAxisCurrentValue).Select(_ => Unit.Default) .Merge(Observable.FromEventPattern(Labels, nameof(Labels.CollectionChanged)).Select(_ => Unit.Default)) .Select(_ => { var allFilled = !string.IsNullOrEmpty(To) && AmountBtc > 0 && Labels.Any(); var hasError = Validations.Any; return(allFilled && !hasError); }); NextCommand = ReactiveCommand.CreateFromTask(async() => await OnNext(broadcaster), nextCommandCanExecute); EnableAutoBusyOn(NextCommand); }
public WalletManagerViewModel(WalletManager walletManager, UiConfig uiConfig, BitcoinStore bitcoinStore, LegalChecker legalChecker, TransactionBroadcaster broadcaster) { WalletManager = walletManager; BitcoinStore = bitcoinStore; LegalChecker = legalChecker; _walletDictionary = new Dictionary <Wallet, WalletViewModelBase>(); _walletActionsDictionary = new Dictionary <WalletViewModelBase, List <NavBarItemViewModel> >(); _actions = new ObservableCollection <NavBarItemViewModel>(); _wallets = new ObservableCollection <WalletViewModelBase>(); _loggedInAndSelectedAlwaysFirst = true; _transactionBroadcaster = broadcaster;
private async Task OnNext(Wallet wallet, TransactionBroadcaster broadcaster, BuildTransactionResult transaction) { var transactionAuthorizationInfo = new TransactionAuthorizationInfo(transaction); var authResult = await AuthorizeAsync(wallet, transactionAuthorizationInfo); if (authResult) { await SendTransaction(wallet, broadcaster, transactionAuthorizationInfo.Transaction); Navigate().To(new SendSuccessViewModel()); } }
private async Task OnNext(TransactionBroadcaster broadcaster, SmartTransaction transaction) { try { await broadcaster.SendTransactionAsync(transaction); Navigate().To(new SuccessBroadcastTransactionViewModel()); } catch (Exception ex) { Logger.LogError(ex); await ShowErrorAsync(Title, ex.ToUserFriendlyString(), "It was not possible to broadcast the transaction."); } }
public TransactionPreviewViewModel(Wallet wallet, TransactionInfo info, TransactionBroadcaster broadcaster, BuildTransactionResult transaction) { var destinationAmount = transaction.CalculateDestinationAmount().ToDecimal(MoneyUnit.BTC); var fee = transaction.Fee; BtcAmountText = $"{destinationAmount} bitcoins "; FiatAmountText = $"(≈{(destinationAmount * wallet.Synchronizer.UsdExchangeRate).FormattedFiat()} USD) "; Labels = info.Labels.Labels.ToArray(); AddressText = info.Address.ToString(); ConfirmationTimeText = $"Approximately {TextHelpers.TimeSpanToFriendlyString(info.ConfirmationTimeSpan)} "; BtcFeeText = $"{fee.ToDecimal(MoneyUnit.Satoshi)} satoshis "; FiatFeeText = $"(≈{(fee.ToDecimal(MoneyUnit.BTC) * wallet.Synchronizer.UsdExchangeRate).FormattedFiat()} USD)"; NextCommand = ReactiveCommand.CreateFromTask(async() => { var transactionAuthorizationInfo = new TransactionAuthorizationInfo(transaction); var authDialog = AuthorizationHelpers.GetAuthorizationDialog(wallet, transactionAuthorizationInfo); var authDialogResult = await NavigateDialog(authDialog, authDialog.DefaultTarget); if (authDialogResult.Result) { IsBusy = true; // Dequeue any coin-joining coins. await wallet.ChaumianClient.DequeueAllCoinsFromMixAsync(DequeueReason.TransactionBuilding); await broadcaster.SendTransactionAsync(transactionAuthorizationInfo.Transaction); Navigate().Clear(); IsBusy = false; } else if (authDialogResult.Kind == DialogResultKind.Normal) { await ShowErrorAsync("Authorization", "The Authorization has failed, please try again.", ""); } }); }
public async Task InitializeWalletServiceAsync(KeyManager keyManager) { using (_cancelWalletServiceInitialization = new CancellationTokenSource()) { var token = _cancelWalletServiceInitialization.Token; while (!Initialized) { await Task.Delay(100, token); } if (Config.UseTor) { ChaumianClient = new CoinJoinClient(Synchronizer, Network, keyManager, () => Config.GetCurrentBackendUri(), Config.TorSocks5EndPoint); } else { ChaumianClient = new CoinJoinClient(Synchronizer, Network, keyManager, Config.GetFallbackBackendUri(), null); } try { keyManager.CorrectBlockHeights(BitcoinStore.HashChain); // Block heights are wrong sometimes. It's a hack. We have to retroactively fix existing wallets, but also we have to figure out where we ruin the block heights. } catch (Exception ex) // Whatever this is not critical, but let's log it. { Logger.LogWarning(ex); } WalletService = new WalletService(BitcoinStore, keyManager, Synchronizer, ChaumianClient, Nodes, DataDir, Config.ServiceConfiguration, FeeProviders, BitcoinCoreNode?.RpcClient); ChaumianClient.Start(); Logger.LogInfo("Start Chaumian CoinJoin service..."); Logger.LogInfo($"Starting {nameof(WalletService)}..."); await WalletService.InitializeAsync(token); Logger.LogInfo($"{nameof(WalletService)} started."); token.ThrowIfCancellationRequested(); WalletService.TransactionProcessor.CoinReceived += CoinReceived; TransactionBroadcaster.AddWalletService(WalletService); } _cancelWalletServiceInitialization = null; // Must make it null explicitly, because dispose won't make it null. }
private async Task OnNext(TransactionBroadcaster broadcaster) { var transactionInfo = _transactionInfo; var wallet = _owner.Wallet; var targetAnonymitySet = wallet.ServiceConfiguration.GetMixUntilAnonymitySetValue(); var mixedCoins = wallet.Coins.Where(x => x.HdPubKey.AnonymitySet >= targetAnonymitySet).ToList(); var totalMixedCoinsAmount = Money.FromUnit(mixedCoins.Sum(coin => coin.Amount), MoneyUnit.Satoshi); if (transactionInfo.Amount <= totalMixedCoinsAmount) { try { try { var txRes = await Task.Run(() => TransactionHelpers.BuildTransaction(wallet, transactionInfo.Address, transactionInfo.Amount, transactionInfo.Labels, transactionInfo.FeeRate, mixedCoins, subtractFee: false)); Navigate().To(new OptimisePrivacyViewModel(wallet, transactionInfo, broadcaster, txRes)); return; } catch (InsufficientBalanceException) { var txRes = await Task.Run(() => TransactionHelpers.BuildTransaction(wallet, transactionInfo.Address, totalMixedCoinsAmount, transactionInfo.Labels, transactionInfo.FeeRate, mixedCoins, subtractFee: true)); var dialog = new InsufficientBalanceDialogViewModel(BalanceType.Private, txRes, wallet.Synchronizer.UsdExchangeRate); var result = await NavigateDialog(dialog, NavigationTarget.DialogScreen); if (result.Result) { Navigate().To(new OptimisePrivacyViewModel(wallet, transactionInfo, broadcaster, txRes)); return; } } } catch (Exception ex) { Logger.LogError(ex); await ShowErrorAsync("Transaction Building", ex.ToUserFriendlyString(), "Wasabi was unable to create your transaction."); return; } } Navigate().To(new PrivacyControlViewModel(wallet, transactionInfo, broadcaster)); }
public Global(string dataDir, Config config, UiConfig uiConfig, WalletManager walletManager) { using (BenchmarkLogger.Measure()) { DataDir = dataDir; Config = config; UiConfig = uiConfig; TorSettings = new TorSettings(DataDir, distributionFolderPath: EnvironmentHelpers.GetFullBaseDirectory(), Config.TerminateTorOnExit, Environment.ProcessId); HostedServices = new HostedServices(); WalletManager = walletManager; var networkWorkFolderPath = Path.Combine(DataDir, "BitcoinStore", Network.ToString()); AllTransactionStore = new AllTransactionStore(networkWorkFolderPath, Network); SmartHeaderChain smartHeaderChain = new(maxChainSize : 20_000); IndexStore = new IndexStore(Path.Combine(networkWorkFolderPath, "IndexStore"), Network, smartHeaderChain); var mempoolService = new MempoolService(); var blocks = new FileSystemBlockRepository(Path.Combine(networkWorkFolderPath, "Blocks"), Network); BitcoinStore = new BitcoinStore(IndexStore, AllTransactionStore, mempoolService, blocks); if (Config.UseTor) { BackendHttpClientFactory = new HttpClientFactory(TorSettings.SocksEndpoint, backendUriGetter: () => Config.GetCurrentBackendUri()); ExternalHttpClientFactory = new HttpClientFactory(TorSettings.SocksEndpoint, backendUriGetter: null); } else { BackendHttpClientFactory = new HttpClientFactory(torEndPoint: null, backendUriGetter: () => Config.GetFallbackBackendUri()); ExternalHttpClientFactory = new HttpClientFactory(torEndPoint: null, backendUriGetter: null); } Synchronizer = new WasabiSynchronizer(BitcoinStore, BackendHttpClientFactory); LegalChecker = new(DataDir); TransactionBroadcaster = new TransactionBroadcaster(Network, BitcoinStore, BackendHttpClientFactory, WalletManager); RoundStateUpdaterCircuit = new PersonCircuit(); Cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 1_000, ExpirationScanFrequency = TimeSpan.FromSeconds(30) });
public SendViewModel(WalletViewModel walletVm, TransactionBroadcaster broadcaster, Config config, UiConfig uiConfig, HttpClientFactory httpClientFactory) : base(NavigationMode.Normal) { _to = ""; _owner = walletVm; _transactionInfo = new TransactionInfo(); _labels = new ObservableCollection <string>(); _lastXAxisCurrentValue = _xAxisCurrentValue; SelectionMode = NavBarItemSelectionMode.Button; 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) { _feeRate = new FeeRate(GetYAxisValueFromXAxisCurrentValue(x)); SetXAxisCurrentValueIndex(x); } }); this.WhenAnyValue(x => x.XAxisCurrentValueIndex) .Subscribe(SetXAxisCurrentValue); this.WhenAnyValue(x => x.PayJoinEndPoint) .Subscribe(endPoint => { if (endPoint is { }) { _transactionInfo.PayJoinClient = GetPayjoinClient(endPoint, config, httpClientFactory); IsPayJoin = true; }
public void OnAppInitialized(object sender, AppInitializedEventArgs args) { _transactionBroadcaster = args.TransactionBroadcaster; Observable .FromEventPattern <AllFeeEstimate>(_feeProviders, nameof(_feeProviders.AllFeeEstimateChanged)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => { SetFeeTargetLimits(); if (FeeTarget < MinimumFeeTarget) // Should never happen. { FeeTarget = MinimumFeeTarget; } else if (FeeTarget > MaximumFeeTarget) { FeeTarget = MaximumFeeTarget; } }) .DisposeWith(Disposables); }
private async Task OnNext(Wallet wallet, TransactionInfo transactionInfo, TransactionBroadcaster broadcaster, IObservableList <PocketViewModel> selectedList) { var coins = selectedList.Items.SelectMany(x => x.Coins).ToArray(); try { try { var transactionResult = await Task.Run(() => TransactionHelpers.BuildTransaction(_wallet, transactionInfo.Address, transactionInfo.Amount, transactionInfo.Labels, transactionInfo.FeeRate, coins, subtractFee: false)); Navigate().To(new OptimisePrivacyViewModel(wallet, transactionInfo, broadcaster, transactionResult)); } catch (InsufficientBalanceException) { var transactionResult = await Task.Run(() => TransactionHelpers.BuildTransaction(_wallet, transactionInfo.Address, transactionInfo.Amount, transactionInfo.Labels, transactionInfo.FeeRate, coins, subtractFee: true)); var dialog = new InsufficientBalanceDialogViewModel(BalanceType.Pocket, transactionResult, wallet.Synchronizer.UsdExchangeRate); var result = await NavigateDialog(dialog, NavigationTarget.DialogScreen); if (result.Result) { Navigate().To(new OptimisePrivacyViewModel(wallet, transactionInfo, broadcaster, transactionResult)); } else { Navigate().BackTo <SendViewModel>(); } } } catch (Exception ex) { Logger.LogError(ex); await ShowErrorAsync("Transaction Building", ex.ToUserFriendlyString(), "Wasabi was unable to create your transaction."); Navigate().BackTo <SendViewModel>(); } }
private async Task OnNext(Wallet wallet, TransactionBroadcaster broadcaster, BuildTransactionResult transaction) { var transactionAuthorizationInfo = new TransactionAuthorizationInfo(transaction); var authDialog = AuthorizationHelpers.GetAuthorizationDialog(wallet, transactionAuthorizationInfo); var authDialogResult = await NavigateDialog(authDialog, authDialog.DefaultTarget); if (authDialogResult.Result) { IsBusy = true; // Dequeue any coin-joining coins. await wallet.ChaumianClient.DequeueAllCoinsFromMixAsync(DequeueReason.TransactionBuilding); await broadcaster.SendTransactionAsync(transactionAuthorizationInfo.Transaction); Navigate().Clear(); IsBusy = false; } else if (authDialogResult.Kind == DialogResultKind.Normal) { await ShowErrorAsync("Authorization", "The Authorization has failed, please try again.", ""); } }
public Global(string dataDir, Config config, UiConfig uiConfig, WalletManager walletManager) { using (BenchmarkLogger.Measure()) { DataDir = dataDir; Config = config; UiConfig = uiConfig; TorSettings = new TorSettings(DataDir, distributionFolderPath: EnvironmentHelpers.GetFullBaseDirectory(), Config.TerminateTorOnExit, Environment.ProcessId); HostedServices = new HostedServices(); WalletManager = walletManager; var networkWorkFolderPath = Path.Combine(DataDir, "BitcoinStore", Network.ToString()); var transactionStore = new AllTransactionStore(networkWorkFolderPath, Network); var indexStore = new IndexStore(Path.Combine(networkWorkFolderPath, "IndexStore"), Network, new SmartHeaderChain()); var mempoolService = new MempoolService(); var blocks = new FileSystemBlockRepository(Path.Combine(networkWorkFolderPath, "Blocks"), Network); BitcoinStore = new BitcoinStore(indexStore, transactionStore, mempoolService, blocks); if (Config.UseTor) { BackendHttpClientFactory = new HttpClientFactory(TorSettings.SocksEndpoint, backendUriGetter: () => Config.GetCurrentBackendUri()); ExternalHttpClientFactory = new HttpClientFactory(TorSettings.SocksEndpoint, backendUriGetter: null); } else { BackendHttpClientFactory = new HttpClientFactory(torEndPoint: null, backendUriGetter: () => Config.GetFallbackBackendUri()); ExternalHttpClientFactory = new HttpClientFactory(torEndPoint: null, backendUriGetter: null); } Synchronizer = new WasabiSynchronizer(BitcoinStore, BackendHttpClientFactory); LegalChecker = new(DataDir); TransactionBroadcaster = new TransactionBroadcaster(Network, BitcoinStore, BackendHttpClientFactory, WalletManager); } }
public async Task InitializeNoWalletAsync(CancellationToken cancellationToken = default) { AddressManager = null; Logger.LogDebug($"{nameof(Global)}.InitializeNoWalletAsync(): Waiting for a lock"); try { InitializationStarted = true; Logger.LogDebug($"{nameof(Global)}.InitializeNoWalletAsync(): Got lock"); MemoryCache Cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 1_000, ExpirationScanFrequency = TimeSpan.FromSeconds(30) }); var bstoreInitTask = _bitcoinStore.InitializeAsync(); var addressManagerFolderPath = Path.Combine(DataDir, "AddressManager"); AddressManagerFilePath = Path.Combine(addressManagerFolderPath, $"AddressManager{Network}.dat"); var addrManTask = InitializeAddressManagerBehaviorAsync(); var blocksFolderPath = Path.Combine(DataDir, $"Blocks{Network}"); var userAgent = Constants.UserAgents.RandomElement(); var connectionParameters = new NodeConnectionParameters { UserAgent = userAgent }; #region TorProcessInitialization if (_config.UseTor) { await _torManager.StartAsync(ensureRunning : false, DataDir); Logger.LogInfo($"{nameof(_torManager)} is initialized."); } Logger.LogInfo($"{nameof(Global)}.InitializeNoWalletAsync():{nameof(_torManager)} is initialized."); #endregion TorProcessInitialization #region BitcoinStoreInitialization await bstoreInitTask.ConfigureAwait(false); // Make sure that the height of the wallets will not be better than the current height of the filters. _walletManager.SetMaxBestHeight(_bitcoinStore.IndexStore.SmartHeaderChain.TipHeight); #endregion BitcoinStoreInitialization #region MempoolInitialization connectionParameters.TemplateBehaviors.Add(_bitcoinStore.CreateUntrustedP2pBehavior()); #endregion MempoolInitialization #region AddressManagerInitialization AddressManagerBehavior addressManagerBehavior = await addrManTask.ConfigureAwait(false); connectionParameters.TemplateBehaviors.Add(addressManagerBehavior); #endregion AddressManagerInitialization #region P2PInitialization Node regTestMempoolServingNode = null; if (Network == Network.RegTest) { Nodes = new NodesGroup(Network, requirements: Constants.NodeRequirements); try { EndPoint bitcoinCoreEndpoint = _config.GetBitcoinP2pEndPoint(); Node node = await Node.ConnectAsync(NBitcoin.Network.RegTest, bitcoinCoreEndpoint).ConfigureAwait(false); Nodes.ConnectedNodes.Add(node); regTestMempoolServingNode = await Node.ConnectAsync(NBitcoin.Network.RegTest, bitcoinCoreEndpoint).ConfigureAwait(false); regTestMempoolServingNode.Behaviors.Add(_bitcoinStore.CreateUntrustedP2pBehavior()); } catch (SocketException ex) { Logger.LogError(ex); } } else { if (_config.UseTor) { // onlyForOnionHosts: false - Connect to clearnet IPs through Tor, too. connectionParameters.TemplateBehaviors.Add(new SocksSettingsBehavior(_config.TorSocks5EndPoint, onlyForOnionHosts: false, networkCredential: null, streamIsolation: true)); // allowOnlyTorEndpoints: true - Connect only to onions and don't connect to clearnet IPs at all. // This of course makes the first setting unneccessary, but it's better if that's around, in case someone wants to tinker here. connectionParameters.EndpointConnector = new DefaultEndpointConnector(allowOnlyTorEndpoints: Network == Network.Main); await AddKnownBitcoinFullNodeAsHiddenServiceAsync(AddressManager).ConfigureAwait(false); } Nodes = new NodesGroup(Network, connectionParameters, requirements: Constants.NodeRequirements); Nodes.MaximumNodeConnection = 12; regTestMempoolServingNode = null; } Nodes.Connect(); Logger.LogInfo($"{nameof(Global)}.InitializeNoWalletAsync(): Start connecting to nodes..."); if (regTestMempoolServingNode != null) { regTestMempoolServingNode.VersionHandshake(); Logger.LogInfo($"{nameof(Global)}.InitializeNoWalletAsync(): Start connecting to mempool serving regtest node..."); } #endregion P2PInitialization #region SynchronizerInitialization var requestInterval = TimeSpan.FromSeconds(30); if (Network == Network.RegTest) { requestInterval = TimeSpan.FromSeconds(5); } int maxFiltSyncCount = Network == Network.Main ? 1000 : 10000; // On testnet, filters are empty, so it's faster to query them together _synchronizer.Start(requestInterval, TimeSpan.FromMinutes(5), maxFiltSyncCount); Logger.LogInfo($"{nameof(Global)}.InitializeNoWalletAsync(): Start synchronizing filters..."); #endregion SynchronizerInitialization TransactionBroadcaster = new TransactionBroadcaster(Network, _bitcoinStore, _synchronizer, Nodes, _walletManager, null); // CoinJoinProcessor maintains an event handler to process joins. // We need this reference. _coinJoinProcessor = new CoinJoinProcessor(_synchronizer, _walletManager, null); #region Blocks provider var blockProvider = new CachedBlockProvider( new SmartBlockProvider( new P2pBlockProvider(Nodes, null, _synchronizer, _config.ServiceConfiguration, Network), Cache), new FileSystemBlockRepository(blocksFolderPath, Network)); #endregion Blocks provider _walletManager.RegisterServices(_bitcoinStore, _synchronizer, Nodes, _config.ServiceConfiguration, _feeProviders, blockProvider); Initialized(this, new AppInitializedEventArgs(this)); IsInitialized = true; }
public PrivacyControlViewModel(Wallet wallet, TransactionInfo transactionInfo, TransactionBroadcaster broadcaster) { _wallet = wallet; _pocketSource = new SourceList <PocketViewModel>(); _pocketSource.Connect() .Bind(out _pockets) .Subscribe(); var selected = _pocketSource.Connect() .AutoRefresh() .Filter(x => x.IsSelected); var selectedList = selected.AsObservableList(); selected.Sum(x => x.TotalBtc) .Subscribe(x => { StillNeeded = transactionInfo.Amount.ToDecimal(MoneyUnit.BTC) - x; EnoughSelected = StillNeeded <= 0; }); StillNeeded = transactionInfo.Amount.ToDecimal(MoneyUnit.BTC); NextCommand = ReactiveCommand.CreateFromTask( async() => { var coins = selectedList.Items.SelectMany(x => x.Coins).ToArray(); try { try { var transactionResult = await Task.Run(() => TransactionHelpers.BuildTransaction(_wallet, transactionInfo.Address, transactionInfo.Amount, transactionInfo.Labels, transactionInfo.FeeRate, coins, subtractFee: false)); Navigate().To(new TransactionPreviewViewModel(wallet, transactionInfo, broadcaster, transactionResult)); } catch (InsufficientBalanceException) { var dialog = new InsufficientBalanceDialogViewModel(BalanceType.Pocket); var result = await NavigateDialog(dialog, NavigationTarget.DialogScreen); if (result.Result) { var transactionResult = await Task.Run(() => TransactionHelpers.BuildTransaction(_wallet, transactionInfo.Address, transactionInfo.Amount, transactionInfo.Labels, transactionInfo.FeeRate, coins, subtractFee: true)); Navigate().To(new TransactionPreviewViewModel(wallet, transactionInfo, broadcaster, transactionResult)); } else { Navigate().BackTo <SendViewModel>(); } } } catch (Exception ex) { Logger.LogError(ex); await ShowErrorAsync("Transaction Building", ex.ToUserFriendlyString(), "Wasabi was unable to create your transaction."); Navigate().BackTo <SendViewModel>(); } }, this.WhenAnyValue(x => x.EnoughSelected)); EnableAutoBusyOn(NextCommand); }
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 { })
public AppInitializedEventArgs(Global global) { Nodes = global.Nodes; TransactionBroadcaster = global.TransactionBroadcaster; }
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); EnableCancel = true; EnableBack = true; NextCommand = ReactiveCommand.CreateFromTask( async() => await OnNext(wallet, transactionInfo, broadcaster, selectedList), this.WhenAnyValue(x => x.EnoughSelected)); EnableAutoBusyOn(NextCommand); }
public SendViewModel(WalletViewModel walletVm, TransactionBroadcaster broadcaster) { _to = ""; _owner = walletVm; _transactionInfo = new TransactionInfo(); _labels = new ObservableCollection <string>(); ExchangeRate = walletVm.Wallet.Synchronizer.UsdExchangeRate; PriorLabels = new(); this.ValidateProperty(x => x.To, ValidateToField); this.ValidateProperty(x => x.AmountBtc, ValidateAmount); this.WhenAnyValue(x => x.To) .Subscribe(ParseToField); this.WhenAnyValue(x => x.AmountBtc) .Subscribe(x => _transactionInfo.Amount = new Money(x, MoneyUnit.BTC)); this.WhenAnyValue(x => x.XAxisCurrentValue) .Subscribe(x => { if (x > 0) { _transactionInfo.FeeRate = new FeeRate(GetYAxisValueFromXAxisCurrentValue(x)); } }); Labels.ToObservableChangeSet().Subscribe(x => { _transactionInfo.Labels = new SmartLabel(_labels.ToArray()); }); PasteCommand = ReactiveCommand.CreateFromTask(async() => { var text = await Application.Current.Clipboard.GetTextAsync(); _parsingUrl = true; if (!TryParseUrl(text)) { To = text; // todo validation errors. } _parsingUrl = false; }); NextCommand = ReactiveCommand.Create(() => { var transactionInfo = _transactionInfo; var wallet = _owner.Wallet; var targetAnonymitySet = wallet.ServiceConfiguration.GetMixUntilAnonymitySetValue(); var mixedCoins = wallet.Coins.Where(x => x.HdPubKey.AnonymitySet >= targetAnonymitySet).ToList(); if (mixedCoins.Any()) { var intent = new PaymentIntent( destination: transactionInfo.Address, amount: transactionInfo.Amount, subtractFee: false, label: transactionInfo.Labels); try { var txRes = wallet.BuildTransaction( wallet.Kitchen.SaltSoup(), intent, FeeStrategy.CreateFromFeeRate(transactionInfo.FeeRate), allowUnconfirmed: true, mixedCoins.Select(x => x.OutPoint)); // Private coins are enough. Navigate().To(new OptimisePrivacyViewModel(wallet, transactionInfo, broadcaster, txRes)); return; } catch (NotEnoughFundsException) { // Do Nothing } } Navigate().To(new PrivacyControlViewModel()); }, this.WhenAnyValue(x => x.Labels.Count).Any()); }
public BroadcastTransactionViewModel( BitcoinStore store, Network network, TransactionBroadcaster broadcaster, SmartTransaction transaction) { Title = "Broadcast Transaction"; var nullMoney = new Money(-1L); var nullOutput = new TxOut(nullMoney, Script.Empty); var psbt = PSBT.FromTransaction(transaction.Transaction, network); TxOut GetOutput(OutPoint outpoint) => store.TransactionStore.TryGetTransaction(outpoint.Hash, out var prevTxn) ? prevTxn.Transaction.Outputs[outpoint.N] : nullOutput; var inputAddressAmount = psbt.Inputs .Select(x => x.PrevOut) .Select(GetOutput) .ToArray(); var outputAddressAmount = psbt.Outputs .Select(x => x.GetCoin().TxOut) .ToArray(); var psbtTxn = psbt.GetOriginalTransaction(); _transactionId = psbtTxn.GetHash().ToString(); _inputCount = inputAddressAmount.Length; _inputCountString = $" input{(_inputCount > 1 ? 's' : "")} and "; _outputCount = outputAddressAmount.Length; _outputCountString = $" output{(_outputCount > 1 ? 's' : "")}."; _totalInputValue = inputAddressAmount.Any(x => x.Value == nullMoney) ? null : inputAddressAmount.Select(x => x.Value).Sum(); _totalOutputValue = outputAddressAmount.Any(x => x.Value == nullMoney) ? null : outputAddressAmount.Select(x => x.Value).Sum(); _networkFee = TotalInputValue is null || TotalOutputValue is null ? null : TotalInputValue - TotalOutputValue; var nextCommandCanExecute = this.WhenAnyValue(x => x.IsBusy) .Select(x => !x); NextCommand = ReactiveCommand.CreateFromTask( async() => { try { await broadcaster.SendTransactionAsync(transaction); Navigate().To(new SuccessBroadcastTransactionViewModel()); } catch (Exception ex) { Logger.LogError(ex); await ShowErrorAsync(ex.ToUserFriendlyString(), "It was not possible to broadcast the transaction."); } }, nextCommandCanExecute); EnableAutoBusyOn(NextCommand); }
public SendViewModel(WalletViewModel walletVm, TransactionBroadcaster broadcaster) : base(NavigationMode.Normal) { _to = ""; _owner = walletVm; _transactionInfo = new TransactionInfo(); _labels = new ObservableCollection <string>(); _lastXAxisCurrentValue = _xAxisCurrentValue; ExchangeRate = walletVm.Wallet.Synchronizer.UsdExchangeRate; PriorLabels = new(); this.ValidateProperty(x => x.To, ValidateToField); this.ValidateProperty(x => x.AmountBtc, ValidateAmount); this.WhenAnyValue(x => x.To) .Subscribe(ParseToField); this.WhenAnyValue(x => x.AmountBtc) .Subscribe(x => _transactionInfo.Amount = new Money(x, MoneyUnit.BTC)); this.WhenAnyValue(x => x.XAxisCurrentValue) .Subscribe(x => { if (x > 0) { _transactionInfo.FeeRate = new FeeRate(GetYAxisValueFromXAxisCurrentValue(x)); SetXAxisCurrentValueIndex(x); } }); this.WhenAnyValue(x => x.XAxisCurrentValueIndex) .Subscribe(SetXAxisCurrentValue); Labels.ToObservableChangeSet().Subscribe(x => { _transactionInfo.Labels = new SmartLabel(_labels.ToArray()); }); PasteCommand = ReactiveCommand.CreateFromTask(async() => { var text = await Application.Current.Clipboard.GetTextAsync(); _parsingUrl = true; if (!TryParseUrl(text)) { To = text; // todo validation errors. } _parsingUrl = false; }); var nextCommandCanExecute = this.WhenAnyValue(x => x.Labels, x => x.AmountBtc, x => x.To, x => x.XAxisCurrentValue).Select(_ => Unit.Default) .Merge(Observable.FromEventPattern(Labels, nameof(Labels.CollectionChanged)).Select(_ => Unit.Default)) .Select(_ => { var allFilled = !string.IsNullOrEmpty(To) && AmountBtc > 0 && Labels.Any(); var hasError = Validations.Any; return(allFilled && !hasError); }); NextCommand = ReactiveCommand.CreateFromTask(async() => { var transactionInfo = _transactionInfo; var wallet = _owner.Wallet; var targetAnonymitySet = wallet.ServiceConfiguration.GetMixUntilAnonymitySetValue(); var mixedCoins = wallet.Coins.Where(x => x.HdPubKey.AnonymitySet >= targetAnonymitySet).ToList(); var totalMixedCoinsAmount = Money.FromUnit(mixedCoins.Sum(coin => coin.Amount), MoneyUnit.Satoshi); if (transactionInfo.Amount <= totalMixedCoinsAmount) { try { try { var txRes = await Task.Run(() => TransactionHelpers.BuildTransaction(wallet, transactionInfo.Address, transactionInfo.Amount, transactionInfo.Labels, transactionInfo.FeeRate, mixedCoins, subtractFee: false)); Navigate().To(new OptimisePrivacyViewModel(wallet, transactionInfo, broadcaster, txRes)); return; } catch (InsufficientBalanceException) { var dialog = new InsufficientBalanceDialogViewModel(BalanceType.Private); var result = await NavigateDialog(dialog, NavigationTarget.DialogScreen); if (result.Result) { var txRes = await Task.Run(() => TransactionHelpers.BuildTransaction(wallet, transactionInfo.Address, totalMixedCoinsAmount, transactionInfo.Labels, transactionInfo.FeeRate, mixedCoins, subtractFee: true)); Navigate().To(new OptimisePrivacyViewModel(wallet, transactionInfo, broadcaster, txRes)); return; } } } catch (Exception ex) { Logger.LogError(ex); await ShowErrorAsync("Transaction Building", ex.ToUserFriendlyString(), "Wasabi was unable to create your transaction."); return; } } Navigate().To(new PrivacyControlViewModel(wallet, transactionInfo, broadcaster)); }, nextCommandCanExecute); EnableAutoBusyOn(NextCommand); }
public TransactionPreviewViewModel(Wallet wallet, TransactionInfo info, TransactionBroadcaster broadcaster, BuildTransactionResult transaction) { var destinationAmount = transaction.CalculateDestinationAmount().ToDecimal(MoneyUnit.BTC); var fee = transaction.Fee; var labels = ""; if (info.Labels.Count() == 1) { labels = info.Labels.First() + " "; } else if (info.Labels.Count() > 1) { labels = string.Join(", ", info.Labels.Take(info.Labels.Count() - 1)); labels += $" and {info.Labels.Last()} "; } BtcAmountText = $"{destinationAmount} bitcoins "; FiatAmountText = $"(≈{(destinationAmount * wallet.Synchronizer.UsdExchangeRate).FormattedFiat()} USD) "; LabelsText = labels; AddressText = info.Address.ToString(); ConfirmationTimeText = "~20 minutes "; BtcFeeText = $"{fee.ToDecimal(MoneyUnit.Satoshi)} satoshis "; FiatFeeText = $"(≈{(fee.ToDecimal(MoneyUnit.BTC) * wallet.Synchronizer.UsdExchangeRate).FormattedFiat()} USD)"; PercentFeeText = $"{transaction.FeePercentOfSent:F2}%"; NextCommand = ReactiveCommand.CreateFromTask(async() => { var dialogResult = await NavigateDialog(new EnterPasswordDialogViewModel(""), NavigationTarget.DialogScreen); if (dialogResult.Kind == DialogResultKind.Normal) { IsBusy = true; var passwordValid = await Task.Run( () => PasswordHelper.TryPassword( wallet.KeyManager, dialogResult.Result, out string?compatibilityPasswordUsed)); if (passwordValid) { // Dequeue any coin-joining coins. await wallet.ChaumianClient.DequeueAllCoinsFromMixAsync(DequeueReason.TransactionBuilding); var signedTransaction = transaction.Transaction; // If it's a hardware wallet and still has a private key then it's password. if (wallet.KeyManager.IsHardwareWallet && !transaction.Signed) { try { var client = new HwiClient(wallet.Network); using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3)); PSBT?signedPsbt = null; try { signedPsbt = await client.SignTxAsync( wallet.KeyManager.MasterFingerprint !.Value, transaction.Psbt, cts.Token); } catch (HwiException ex) when(ex.ErrorCode is not HwiErrorCode.ActionCanceled) { await PinPadViewModel.UnlockAsync(); signedPsbt = await client.SignTxAsync( wallet.KeyManager.MasterFingerprint !.Value, transaction.Psbt, cts.Token); } signedTransaction = signedPsbt.ExtractSmartTransaction(transaction.Transaction); } catch (Exception _) { // probably throw something here? } } await broadcaster.SendTransactionAsync(signedTransaction); Navigate().Clear(); IsBusy = false; } else { IsBusy = false; await ShowErrorAsync("Password was incorrect.", "Please try again.", ""); } } }); }
public 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)); }