Paymetheus.Decred.Wallet.Wallet.ApplyTransactionChanges C# (CSharp) Method

ApplyTransactionChanges() public method

public ApplyTransactionChanges ( Paymetheus.Decred.Wallet.WalletChanges changes ) : void
changes Paymetheus.Decred.Wallet.WalletChanges
return void
        public void ApplyTransactionChanges(WalletChanges changes)
        {
            if (changes == null)
                throw new ArgumentNullException(nameof(changes));

            // A reorganize cannot be handled if the number of removed blocks exceeds the
            // minimum number saved in memory.
            if (changes.DetachedBlocks.Count >= NumRecentBlocks(ActiveChain))
                throw new BlockChainConsistencyException("Reorganize too deep");

            var newChainTip = changes.AttachedBlocks.LastOrDefault();
            if (ChainTip.Height >= newChainTip?.Height)
            {
                var msg = $"New chain tip {newChainTip.Hash} (height {newChainTip.Height}) neither extends nor replaces " +
                    $"the current chain (currently synced to hash {ChainTip.Hash}, height {ChainTip.Height})";
                throw new BlockChainConsistencyException(msg);
            }

            if (changes.NewUnminedTransactions.Any(tx => !changes.AllUnminedHashes.Contains(tx.Hash)))
                throw new BlockChainConsistencyException("New unmined transactions contains tx with hash not found in all unmined transaction hash set");

            var eventArgs = new ChangesProcessedEventArgs();

            var reorgedBlocks = RecentTransactions.MinedTransactions
                .ReverseList()
                .TakeWhile(b => changes.DetachedBlocks.Contains(b.Hash))
                .ToList();
            var numReorgedBlocks = reorgedBlocks.Count;
            foreach (var reorgedTx in reorgedBlocks.SelectMany(b => b.Transactions))
            {
                if (BlockChain.IsCoinbase(reorgedTx.Transaction) || !changes.AllUnminedHashes.Contains(reorgedTx.Hash))
                {
                    RemoveTransactionFromTotals(reorgedTx, eventArgs.ModifiedAccountProperties);
                }
                else
                {
                    RecentTransactions.UnminedTransactions[reorgedTx.Hash] = reorgedTx;
                    eventArgs.MovedTransactions.Add(reorgedTx.Hash, BlockIdentity.Unmined);
                }
            }
            var numRemoved = RecentTransactions.MinedTransactions.RemoveAll(block => changes.DetachedBlocks.Contains(block.Hash));
            if (numRemoved != numReorgedBlocks)
            {
                throw new BlockChainConsistencyException("Number of blocks removed exceeds those for which transactions were removed");
            }

            foreach (var block in changes.AttachedBlocks.Where(b => b.Transactions.Count > 0))
            {
                RecentTransactions.MinedTransactions.Add(block);

                foreach (var tx in block.Transactions)
                {
                    if (RecentTransactions.UnminedTransactions.ContainsKey(tx.Hash))
                    {
                        RecentTransactions.UnminedTransactions.Remove(tx.Hash);
                        eventArgs.MovedTransactions[tx.Hash] = block.Identity;
                    }
                    else if (!eventArgs.MovedTransactions.ContainsKey(tx.Hash))
                    {
                        AddTransactionToTotals(tx, eventArgs.ModifiedAccountProperties);
                        eventArgs.AddedTransactions.Add(Tuple.Create(tx, block.Identity));
                    }
                }
            }

            // TODO: What about new transactions which were not added in a newly processed
            // block (e.g. importing an address and rescanning for outputs)?

            foreach (var tx in changes.NewUnminedTransactions.Where(tx => !RecentTransactions.UnminedTransactions.ContainsKey(tx.Hash)))
            {
                RecentTransactions.UnminedTransactions[tx.Hash] = tx;
                AddTransactionToTotals(tx, eventArgs.ModifiedAccountProperties);

                // TODO: When reorgs are handled, this will need to check whether the transaction
                // being added to the unmined collection was previously in a block.
                eventArgs.AddedTransactions.Add(Tuple.Create(tx, BlockIdentity.Unmined));
            }

            var removedUnmined = RecentTransactions.UnminedTransactions
                .Where(kvp => !changes.AllUnminedHashes.Contains(kvp.Key))
                .ToList(); // Collect to list so UnminedTransactions can be modified below.
            foreach (var unmined in removedUnmined)
            {
                // Transactions that were mined rather than being removed from the unmined
                // set due to a conflict have already been removed.
                RecentTransactions.UnminedTransactions.Remove(unmined.Key);
                RemoveTransactionFromTotals(unmined.Value, eventArgs.ModifiedAccountProperties);
                eventArgs.RemovedTransactions.Add(unmined.Value);
            }

            if (newChainTip != null)
            {
                ChainTip = newChainTip.Identity;
                eventArgs.NewChainTip = newChainTip.Identity;
            }

            OnChangesProcessed(eventArgs);
        }

Usage Example

Example #1
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<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 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);
            }
        }