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;
        }
Exemple #2
0
        /// <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;
        }