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) 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.ModifiedAccountStates); } 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.ModifiedAccountStates); 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.ModifiedAccountStates); // 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.ModifiedAccountStates); eventArgs.RemovedTransactions.Add(unmined.Value); } if (newChainTip != null) { ChainTip = newChainTip.Identity; eventArgs.NewChainTip = newChainTip.Identity; } OnChangesProcessed(eventArgs); }
public async Task ListenAndBuffer() { const int sec = 1000; // Delays counted in ms try { var request = new TransactionNotificationsRequest(); using (var stream = _client.TransactionNotifications(request, cancellationToken: _cancelToken)) { var responses = stream.ResponseStream; while (!_cancelToken.IsCancellationRequested) { var respTask = responses.MoveNext(); // If no notifications are received in the next minute, use a ping // (with a 3s pong timeout) to check that connection is still active. while (respTask != await Task.WhenAny(respTask, Task.Delay(60 * sec))) { var pingCall = _client.PingAsync(new PingRequest()); var pingTask = pingCall.ResponseAsync; var finishedTask = await Task.WhenAny(respTask, pingTask, Task.Delay(3 * sec)); if (finishedTask == respTask) break; else if (finishedTask == pingTask) continue; else throw new TimeoutException("Timeout listening for transaction notification"); } // Check whether the stream was ended cleany by the server. Accessing this // result will throw a RpcException with the Unavailable status code if // the task instead ended due to losing the connection. if (!respTask.Result) break; // Marshal the notification and append to the buffer. var n = responses.Current; var detachedBlocks = n.DetachedBlocks.Select(hash => new Sha256Hash(hash.ToByteArray())).ToHashSet(); var attachedBlocks = n.AttachedBlocks.Select(MarshalBlock).ToList(); var unminedTransactions = n.UnminedTransactions.Select(MarshalWalletTransaction).ToList(); var unminedHashes = n.UnminedTransactionHashes.Select(hash => new Sha256Hash(hash.ToByteArray())).ToHashSet(); var changes = new WalletChanges(detachedBlocks, attachedBlocks, unminedTransactions, unminedHashes); _buffer.Post(changes); } } _buffer.Complete(); } catch (TaskCanceledException) { _buffer.Complete(); } catch (RpcException ex) when (ex.Status.StatusCode == StatusCode.Cancelled) { _buffer.Complete(); } catch (Exception ex) { var block = (IDataflowBlock)_buffer; block.Fault(ex); } }
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) { 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); }