private async void StartupWizard_WalletOpened(object sender, EventArgs args) { try { var syncingWallet = await App.Current.WalletRpcClient.Synchronize(_wallet_ChangesProcessed); _wallet = syncingWallet.Item1; OnSyncedWallet(); var syncTask = syncingWallet.Item2; await syncTask; } catch (ConnectTimeoutException) { MessageBox.Show("Unable to connect to wallet"); } catch (Exception ex) { var ae = ex as AggregateException; if (ae != null) { Exception inner; if (ae.TryUnwrap(out inner)) ex = inner; } HandleSyncFault(ex); } finally { if (_wallet != null) _wallet.ChangesProcessed -= _wallet_ChangesProcessed; StartupWizardVisible = true; } }
public TransactionViewModel(Wallet wallet, WalletTransaction transaction, BlockIdentity transactionLocation) { _transaction = transaction; _location = transactionLocation; var groupedOutputs = _transaction.NonChangeOutputs.Select(o => { var destination = wallet.OutputDestination(o); return new GroupedOutput(o.Amount, destination); }).Aggregate(new List<GroupedOutput>(), (items, next) => { var item = items.Find(a => a.Destination == next.Destination); if (item == null) items.Add(next); else item.Amount += next.Amount; return items; }); Confirmations = BlockChain.Confirmations(wallet.ChainTip.Height, transactionLocation); Inputs = _transaction.Inputs.Select(input => new Input(-input.Amount, wallet.AccountName(input.PreviousAccount))).ToArray(); Outputs = _transaction.Outputs.Select(output => new Output(output.Amount, wallet.OutputDestination(output))).ToArray(); GroupedOutputs = groupedOutputs; }
public AccountViewModel(ShellViewModel shell, Wallet wallet, Account account) : base(shell) { _wallet = wallet; _account = account; _accountState = _wallet.LookupAccountState(_account); UpdateAccountStateProperties(1, _accountState); // TODO: Don't hardcode confs RenameAccount = new DelegateCommand(RenameAcountAction); ImportKey = new DelegateCommand(ImportKeyAction); PopulateTransactionHistory(); FetchUnspentOutputs = new DelegateCommand(FetchUnspentOutputsAction); AddPendingOutput = new ButtonCommand("Add output", AddPendingOutputAction); RemovePendingOutput = new DelegateCommand<PendingOutput>(RemovePendingOutputAction); FinishCreateTransaction = new ButtonCommand("Send transaction", FinishCreateTransactionAction); AddPendingOutputAction(); RecalculateCreateTransaction(); RequestAddress = new ButtonCommand("Create single-use address", RequestAddressAction); }
/// <summary> /// Begins synchronization of the client with the remote wallet process. /// A delegate must be passed to be connected to the wallet's ChangesProcessed event to avoid /// a race where additional notifications are processed in the sync task before the caller /// can connect the event. The caller is responsible for disconnecting the delegate from the /// event handler when finished. /// </summary> /// <param name="walletEventHandler">Event handler for changes to wallet as new transactions are processed.</param> /// <returns>The synced Wallet and the Task that is keeping the wallet in sync.</returns> public async Task<Tuple<Wallet, Task>> Synchronize(EventHandler<Wallet.ChangesProcessedEventArgs> walletEventHandler) { if (walletEventHandler == null) throw new ArgumentNullException(nameof(walletEventHandler)); TransactionNotifications notifications; Task notificationsTask; // TODO: Initialization requests need timeouts. // Loop until synchronization did not race on a reorg. while (true) { // Begin receiving notifications for new and removed wallet transactions before // old transactions are downloaded. Any received notifications are saved to // a buffer and are processed after GetAllTransactionsAsync is awaited. notifications = new TransactionNotifications(_channel, _tokenSource.Token); notificationsTask = notifications.ListenAndBuffer(); var networkTask = NetworkAsync(); // Concurrently download transaction history and account properties. var txSetTask = GetTransactionsAsync(Wallet.MinRecentTransactions, Wallet.NumRecentBlocks); var accountsTask = AccountsAsync(); var networkResp = await networkTask; var activeBlockChain = BlockChainIdentity.FromNetworkBits(networkResp.ActiveNetwork); var txSet = await txSetTask; var rpcAccounts = await accountsTask; var lastAccountBlockHeight = rpcAccounts.CurrentBlockHeight; var lastAccountBlockHash = new Sha256Hash(rpcAccounts.CurrentBlockHash.ToByteArray()); var lastTxBlock = txSet.MinedTransactions.LastOrDefault(); if (lastTxBlock != null) { var lastTxBlockHeight = lastTxBlock.Height; var lastTxBlockHash = lastTxBlock.Hash; if (lastTxBlockHeight > lastAccountBlockHeight || (lastTxBlockHeight == lastAccountBlockHeight && !lastTxBlockHash.Equals(lastAccountBlockHash))) { _tokenSource.Cancel(); continue; } } // Read all received notifications thus far and determine if synchronization raced // on a chain reorganize. Try again if so. IList<WalletChanges> transactionNotifications; if (notifications.Buffer.TryReceiveAll(out transactionNotifications)) { if (transactionNotifications.Any(r => r.DetachedBlocks.Count != 0)) { _tokenSource.Cancel(); continue; } // Skip all attached block notifications that are in blocks lower than the // block accounts notification. If blocks exist at or past that height, // the first's hash should equal that from the accounts notification. // // This ensures that both notifications contain data that is valid at this // block. var remainingNotifications = transactionNotifications .SelectMany(r => r.AttachedBlocks) .SkipWhile(b => b.Height < lastAccountBlockHeight) .ToList(); if (remainingNotifications.Count != 0) { if (!remainingNotifications[0].Hash.Equals(lastAccountBlockHash)) { _tokenSource.Cancel(); continue; } } // TODO: Merge remaining notifications with the transaction set. // For now, be lazy and start the whole sync over. if (remainingNotifications.Count > 1) { _tokenSource.Cancel(); continue; } } var accounts = rpcAccounts.Accounts.ToDictionary( a => new Account(a.AccountNumber), a => new AccountProperties { AccountName = a.AccountName, TotalBalance = a.TotalBalance, // TODO: uncomment when added to protospec and implemented by wallet. //ImmatureCoinbaseReward = a.ImmatureBalance, ExternalKeyCount = a.ExternalKeyCount, InternalKeyCount = a.InternalKeyCount, ImportedKeyCount = a.ImportedKeyCount, }); var chainTip = new BlockIdentity(lastAccountBlockHash, lastAccountBlockHeight); var wallet = new Wallet(activeBlockChain, txSet, accounts, chainTip); wallet.ChangesProcessed += walletEventHandler; var syncTask = Task.Run(async () => { var client = WalletService.NewClient(_channel); var accountsStream = client.AccountNotifications(new AccountNotificationsRequest(), cancellationToken: _tokenSource.Token); var accountChangesTask = accountsStream.ResponseStream.MoveNext(); var txChangesTask = notifications.Buffer.OutputAvailableAsync(); while (true) { var completedTask = await Task.WhenAny(accountChangesTask, txChangesTask); if (!await completedTask) { break; } if (completedTask == accountChangesTask) { var accountProperties = accountsStream.ResponseStream.Current; var account = new Account(accountProperties.AccountNumber); wallet.UpdateAccountProperties(account, accountProperties.AccountName, accountProperties.ExternalKeyCount, accountProperties.InternalKeyCount, accountProperties.ImportedKeyCount); accountChangesTask = accountsStream.ResponseStream.MoveNext(); } else if (completedTask == txChangesTask) { var changes = notifications.Buffer.Receive(); wallet.ApplyTransactionChanges(changes); } } await notificationsTask; }); return Tuple.Create(wallet, syncTask); } }
private void _wallet_ChangesProcessed(object sender, Wallet.ChangesProcessedEventArgs e) { var currentHeight = e.NewChainTip?.Height ?? SyncedBlockHeight; var movedTxViewModels = _recentActivityViewModel.RecentTransactions .Where(txvm => e.MovedTransactions.ContainsKey(txvm.TxHash)) .Select(txvm => Tuple.Create(txvm, e.MovedTransactions[txvm.TxHash])); var newTxViewModels = e.AddedTransactions.Select(tx => new TransactionViewModel(_wallet, tx.Item1, tx.Item2)).ToList(); foreach (var kvp in e.ModifiedAccountProperties) { var account = kvp.Key; var state = kvp.Value; var recentAccountVM = RecentAccounts.FirstOrDefault(vm => vm.Account == account); if (recentAccountVM != null) { recentAccountVM.AccountName = state.AccountName; recentAccountVM.Balance = state.TotalBalance; } else { Application.Current.Dispatcher.Invoke(() => { RecentAccounts.Add(new RecentAccountViewModel(this, account, state)); }); } } RaisePropertyChanged(nameof(TotalBalance)); if (VisibleContent is AccountViewModel) { var accountViewModel = (AccountViewModel)VisibleContent; AccountProperties accountProperties; if (e.ModifiedAccountProperties.TryGetValue(accountViewModel.Account, out accountProperties)) { accountViewModel.UpdateAccountProperties(1, accountProperties); } } foreach (var movedTx in movedTxViewModels) { var txvm = movedTx.Item1; var location = movedTx.Item2; txvm.Location = location; txvm.Confirmations = BlockChain.Confirmations(currentHeight, location); } Application.Current.Dispatcher.Invoke(() => { foreach (var txvm in newTxViewModels) { _recentActivityViewModel.RecentTransactions.Insert(0, txvm); } }); if (e.NewChainTip != null) { SyncedBlockHeight = ((BlockIdentity)(e.NewChainTip)).Height; } }