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; }); Depth = BlockChain.Depth(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; }
/// <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<Mutex<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(); var accountsTask = AccountsAsync(); var networkResp = await networkTask; var activeBlockChain = BlockChainIdentity.FromNetworkBits(networkResp.ActiveNetwork); var txSetTask = GetTransactionsAsync(Wallet.MinRecentTransactions, Wallet.NumRecentBlocks(activeBlockChain)); var txSet = await txSetTask; var rpcAccounts = await accountsTask; var lastAccountBlockHeight = rpcAccounts.CurrentBlockHeight; var lastAccountBlockHash = new Blake256Hash(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, }); Func<AccountsResponse.Types.Account, AccountProperties> createProperties = 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, }; // This assumes that all but the last account listed in the RPC response are // BIP0032 accounts, with the same account number as their List index. var bip0032Accounts = rpcAccounts.Accounts.Take(rpcAccounts.Accounts.Count - 1).Select(createProperties).ToList(); var importedAccount = createProperties(rpcAccounts.Accounts.Last()); var chainTip = new BlockIdentity(lastAccountBlockHash, lastAccountBlockHeight); var wallet = new Wallet(activeBlockChain, txSet, bip0032Accounts, importedAccount, chainTip); wallet.ChangesProcessed += walletEventHandler; var walletMutex = new Mutex<Wallet>(wallet); var syncTask = Task.Run(async () => { var client = new WalletService.WalletServiceClient(_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; } using (var walletGuard = await walletMutex.LockAsync()) { var w = walletGuard.Instance; if (completedTask == accountChangesTask) { var accountProperties = accountsStream.ResponseStream.Current; var account = new Account(accountProperties.AccountNumber); w.UpdateAccountProperties(account, accountProperties.AccountName, accountProperties.ExternalKeyCount, accountProperties.InternalKeyCount, accountProperties.ImportedKeyCount); accountChangesTask = accountsStream.ResponseStream.MoveNext(); } else if (completedTask == txChangesTask) { var changes = notifications.Buffer.Receive(); w.ApplyTransactionChanges(changes); txChangesTask = notifications.Buffer.OutputAvailableAsync(); } } } await notificationsTask; }); return Tuple.Create(walletMutex, syncTask); } }
private void OnWalletChangesProcessed(object sender, Wallet.ChangesProcessedEventArgs e) { var wallet = (Wallet)sender; var currentHeight = e.NewChainTip?.Height ?? SyncedBlockHeight; // TODO: The OverviewViewModel should probably connect to this event. This could be // done after the wallet is synced. var overviewViewModel = ViewModelLocator.OverviewViewModel as OverviewViewModel; if (overviewViewModel != null) { var movedTxViewModels = overviewViewModel.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 movedTx in movedTxViewModels) { var txvm = movedTx.Item1; var location = movedTx.Item2; txvm.Location = location; txvm.Depth = BlockChain.Depth(currentHeight, location); } App.Current.Dispatcher.Invoke(() => { foreach (var txvm in newTxViewModels) { overviewViewModel.RecentTransactions.Insert(0, txvm); } }); } // TODO: same.. in fact these tx viewmodels should be reused so changes don't need to be recalculated. // It would be a good idea for this synchronzier viewmodel to manage these and hand them out to other // viewmodels for sorting and organization. var transactionHistoryViewModel = ViewModelLocator.TransactionHistoryViewModel as TransactionHistoryViewModel; if (transactionHistoryViewModel != null) { foreach (var tx in transactionHistoryViewModel.Transactions) { var txvm = tx.Transaction; BlockIdentity newLocation; if (e.MovedTransactions.TryGetValue(txvm.TxHash, out newLocation)) { txvm.Location = newLocation; } txvm.Depth = BlockChain.Depth(currentHeight, txvm.Location); } transactionHistoryViewModel.AppendNewTransactions(wallet, e.AddedTransactions); } foreach (var modifiedAccount in e.ModifiedAccountProperties) { var accountNumber = checked((int)modifiedAccount.Key.AccountNumber); var accountProperties = modifiedAccount.Value; if (accountNumber < Accounts.Count) { Accounts[accountNumber].AccountProperties = accountProperties; } } // TODO: this would be better if all new accounts were a field in the event message. var newAccounts = e.ModifiedAccountProperties. Where(kvp => kvp.Key.AccountNumber >= Accounts.Count). OrderBy(kvp => kvp.Key.AccountNumber); foreach (var modifiedAccount in newAccounts) { var accountNumber = checked((int)modifiedAccount.Key.AccountNumber); var accountProperties = modifiedAccount.Value; // TODO: This is very inefficient because it recalculates balances of every account, for each new account. var accountBalance = Wallet.CalculateBalances(2)[accountNumber]; var accountViewModel = new AccountViewModel(modifiedAccount.Key, accountProperties, accountBalance); App.Current.Dispatcher.Invoke(() => Accounts.Add(accountViewModel)); } if (e.NewChainTip != null) { SyncedBlockHeight = e.NewChainTip.Value.Height; } if (e.AddedTransactions.Count != 0 || e.RemovedTransactions.Count != 0 || e.NewChainTip != null) { RaisePropertyChanged(nameof(TotalBalance)); var balances = Wallet.CalculateBalances(2); // TODO: don't hardcode confs for (var i = 0; i < balances.Length; i++) { Accounts[i].Balances = balances[i]; } } }
private static IEnumerable<HistoryItem> EnumerateAccountTransactions(Wallet wallet, Account account) { Amount runningBalance = 0; // RecentTransactions currently includes every transaction. // This will change in a future release, but for now don't bother using RPC to fetch old transactions. // Iterate through them, oldest first. foreach (var block in wallet.RecentTransactions.MinedTransactions) { var minedAccountTxs = block.Transactions. Select(tx => AccountTransaction.Create(account, tx)). Where(atx => atx.HasValue). Select(atx => atx.Value); foreach (var accountTx in minedAccountTxs) { var txvm = new TransactionViewModel(wallet, accountTx.Transaction, block.Identity); runningBalance += accountTx.DebitCredit; yield return new HistoryItem(txvm, accountTx.Debit, accountTx.Credit, runningBalance); } } var unminedAccountTxs = wallet.RecentTransactions.UnminedTransactions. Select(tx => AccountTransaction.Create(account, tx.Value)). Where(atx => atx.HasValue). Select(atx => atx.Value). OrderBy(atx => atx.Transaction.SeenTime); foreach (var accountTx in unminedAccountTxs) { var txvm = new TransactionViewModel(wallet, accountTx.Transaction, BlockIdentity.Unmined); runningBalance += accountTx.DebitCredit; yield return new HistoryItem(txvm, accountTx.Debit, accountTx.Credit, runningBalance); } }
public void AppendNewTransactions(Wallet wallet, List<Tuple<WalletTransaction, BlockIdentity>> txs) { var account = SelectedAccount.Account; var totalDebits = DebitSum; var totalCredits = CreditSum; var runningBalance = totalDebits + totalCredits; foreach (var tx in txs) { var accountTxOption = AccountTransaction.Create(account, tx.Item1); if (accountTxOption == null) continue; var accountTx = accountTxOption.Value; var txvm = new TransactionViewModel(wallet, accountTx.Transaction, tx.Item2); totalDebits += accountTx.Debit; totalCredits += accountTx.Credit; runningBalance += accountTx.DebitCredit; var histItem = new HistoryItem(txvm, accountTx.Debit, accountTx.Credit, runningBalance); App.Current.Dispatcher.Invoke(() => Transactions.Add(histItem)); } DebitSum = totalDebits; CreditSum = totalCredits; }
private void OnSyncedWallet(Wallet wallet) { var accountBalances = wallet.CalculateBalances(2); // TODO: configurable confirmations var accountViewModels = wallet.EnumerateAccounts() .Zip(accountBalances, (a, bals) => new AccountViewModel(a.Item1, a.Item2, bals)) .ToList(); var txSet = wallet.RecentTransactions; var recentTx = txSet.UnminedTransactions .Select(x => new TransactionViewModel(wallet, x.Value, BlockIdentity.Unmined)) .Concat(txSet.MinedTransactions.ReverseList().SelectMany(b => b.Transactions.Select(tx => new TransactionViewModel(wallet, tx, b.Identity)))) .Take(10); var overviewViewModel = (OverviewViewModel)SingletonViewModelLocator.Resolve("Overview"); App.Current.Dispatcher.Invoke(() => { foreach (var vm in accountViewModels) Accounts.Add(vm); foreach (var tx in recentTx) overviewViewModel.RecentTransactions.Add(tx); }); TotalBalance = wallet.TotalBalance; TransactionCount = txSet.TransactionCount(); SyncedBlockHeight = wallet.ChainTip.Height; SelectedAccount = accountViewModels[0]; RaisePropertyChanged(nameof(TotalBalance)); RaisePropertyChanged(nameof(AccountNames)); overviewViewModel.AccountsCount = accountViewModels.Count(); var shell = (ShellViewModel)ViewModelLocator.ShellViewModel; shell.StartupWizardVisible = false; }