Esempio n. 1
0
        /// <summary>
        /// Calculates the balance for specified address.
        /// </summary>
        /// <param name="address"></param>
        public QueryAddress AddressBalance(string address)
        {
            AddressComputedTable addressComputedTable = ComputeAddressBalance(address);

            List <MapMempoolAddressBag> mempoolAddressBag = MempoolBalance(address);

            return(new QueryAddress
            {
                Address = address,
                Balance = addressComputedTable.Available,
                TotalReceived = addressComputedTable.Received,
                TotalStake = addressComputedTable.Staked,
                TotalMine = addressComputedTable.Mined,
                TotalSent = addressComputedTable.Sent,
                TotalReceivedCount = addressComputedTable.CountReceived,
                TotalSentCount = addressComputedTable.CountSent,
                TotalStakeCount = addressComputedTable.CountStaked,
                TotalMineCount = addressComputedTable.CountMined,
                PendingSent = mempoolAddressBag.Sum(s => s.AmountInInputs),
                PendingReceived = mempoolAddressBag.Sum(s => s.AmountInOutputs)
            });
        }
Esempio n. 2
0
        /// <summary>
        /// Compute the balance and history of a given address.
        /// If the address already has history only the difference is computed.
        /// The difference is any new entries related to the given address from the last time it was computed.
        ///
        /// Edge cases that need special handling:
        /// - two inputs in the same transaction
        /// - to outputs in the same transaction
        /// - outputs and inputs in the same transaction
        ///
        /// Paging:
        /// We use a computed field called position that is incremented on each entry that is added to the list.
        /// The position is indexed but is only directly related to the given address
        /// When paging is requested we will fetch directly the required rows (no need to perform a table scan)
        ///
        /// Resource Access:
        /// concerns around computing tables
        ///    users call the method concurrently and compute the data simultaneously, this is mostly cpu wistful
        ///    as the tables are idempotent and the first call will compute and persist the computed data but second
        ///    will just fail to persist any existing entries, to apply this we use OCC (Optimistic Concurrency Control)
        ///    on the block height, if the version currently in disk is not the same as when the row was read
        ///    another process already calculated the latest additional entries
        /// </summary>
        private AddressComputedTable ComputeAddressBalance(string address)
        {
            //if (globalState.IndexModeCompleted == false)
            //{
            //   // do not compute tables if indexes have not run.
            //   throw new ApplicationException("node in syncing process");
            //}

            FilterDefinition <AddressComputedTable> addrFilter = Builders <AddressComputedTable> .Filter
                                                                 .Where(f => f.Address == address);

            AddressComputedTable addressComputedTable = mongoDb.AddressComputedTable.Find(addrFilter).FirstOrDefault();

            if (addressComputedTable == null)
            {
                addressComputedTable = new AddressComputedTable()
                {
                    Id = address, Address = address, ComputedBlockIndex = 0
                };
                mongoDb.AddressComputedTable.ReplaceOne(addrFilter, addressComputedTable, new ReplaceOptions {
                    IsUpsert = true
                });
            }

            SyncBlockInfo storeTip = globalState.StoreTip;

            if (storeTip == null)
            {
                return(addressComputedTable); // this can happen if node is in the middle of reorg
            }
            long currentHeight = addressComputedTable.ComputedBlockIndex;
            long tipHeight     = storeTip.BlockIndex;

            IQueryable <OutputTable> filterOutputs = mongoDb.OutputTable.AsQueryable()
                                                     .Where(t => t.Address == address)
                                                     .Where(b => b.BlockIndex > currentHeight && b.BlockIndex <= tipHeight);

            IQueryable <InputTable> filterInputs = mongoDb.InputTable.AsQueryable()
                                                   .Where(t => t.Address == address)
                                                   .Where(b => b.BlockIndex > currentHeight && b.BlockIndex <= tipHeight);

            long countReceived = 0, countSent = 0, countStaked = 0, countMined = 0;
            long received = 0, sent = 0, staked = 0, mined = 0;
            long maxHeight = 0;

            var history      = new Dictionary <string, AddressHistoryComputedTable>();
            var transcations = new Dictionary <string, MapAddressBag>();
            var utxoToAdd    = new Dictionary <string, AddressUtxoComputedTable>();
            var utxoToDelete = new Dictionary <string, Outpoint>();

            foreach (OutputTable item in filterOutputs)
            {
                if (item.BlockIndex > currentHeight && item.BlockIndex <= tipHeight)
                {
                    maxHeight = Math.Max(maxHeight, item.BlockIndex);

                    if (transcations.TryGetValue(item.Outpoint.TransactionId, out MapAddressBag current))
                    {
                        current.CoinBase  = item.CoinBase;
                        current.CoinStake = item.CoinStake;
                        current.Ouputs.Add(item);
                    }
                    else
                    {
                        var bag = new MapAddressBag {
                            BlockIndex = item.BlockIndex, CoinBase = item.CoinBase, CoinStake = item.CoinStake
                        };
                        bag.Ouputs.Add(item);
                        transcations.Add(item.Outpoint.TransactionId, bag);
                    }

                    // add to the utxo table
                    utxoToAdd.Add(item.Outpoint.ToString(), new AddressUtxoComputedTable {
                        Outpoint = item.Outpoint, BlockIndex = item.BlockIndex, Address = item.Address, CoinBase = item.CoinBase, CoinStake = item.CoinStake, ScriptHex = item.ScriptHex, Value = item.Value
                    });
                }
            }

            foreach (InputTable item in filterInputs)
            {
                if (item.BlockIndex > currentHeight && item.BlockIndex <= tipHeight)
                {
                    maxHeight = Math.Max(maxHeight, item.BlockIndex);

                    if (transcations.TryGetValue(item.TrxHash, out MapAddressBag current))
                    {
                        current.Inputs.Add(item);
                    }
                    else
                    {
                        var bag = new MapAddressBag {
                            BlockIndex = item.BlockIndex
                        };
                        bag.Inputs.Add(item);
                        transcations.Add(item.TrxHash, bag);
                    }

                    // remove from the utxo table
                    if (!utxoToAdd.Remove(item.Outpoint.ToString()))
                    {
                        // if not found in memory we need to delete form disk
                        utxoToDelete.Add(item.Outpoint.ToString(), item.Outpoint);
                    }
                }
            }

            if (transcations.Any())
            {
                foreach ((string key, MapAddressBag mapAddressBag) in transcations.OrderBy(o => o.Value.BlockIndex))
                {
                    var historyItem = new AddressHistoryComputedTable
                    {
                        Address       = addressComputedTable.Address,
                        TransactionId = key,
                        BlockIndex    = Convert.ToUInt32(mapAddressBag.BlockIndex),
                        Id            = $"{key}-{address}",
                    };

                    history.Add(key, historyItem);

                    foreach (OutputTable output in mapAddressBag.Ouputs)
                    {
                        historyItem.AmountInOutputs += output.Value;
                    }

                    foreach (InputTable output in mapAddressBag.Inputs)
                    {
                        historyItem.AmountInInputs += output.Value;
                    }

                    if (mapAddressBag.CoinBase)
                    {
                        countMined++;
                        mined += historyItem.AmountInOutputs;
                        historyItem.EntryType = "mine";
                    }
                    else if (mapAddressBag.CoinStake)
                    {
                        countStaked++;
                        staked += historyItem.AmountInOutputs - historyItem.AmountInInputs;
                        historyItem.EntryType = "stake";
                    }
                    else
                    {
                        received += historyItem.AmountInOutputs;
                        sent     += historyItem.AmountInInputs;

                        if (historyItem.AmountInOutputs > historyItem.AmountInInputs)
                        {
                            countReceived++;
                            historyItem.EntryType = "receive";
                        }
                        else
                        {
                            countSent++;
                            historyItem.EntryType = "send";
                        }
                    }
                }

                long totalCount = countSent + countReceived + countMined + countStaked;
                if (totalCount < history.Values.Count)
                {
                    throw new ApplicationException("Failed to compute history correctly");
                }

                // each entry is assigned an incremental id to improve efficiency of paging.
                long position = addressComputedTable.CountSent + addressComputedTable.CountReceived + addressComputedTable.CountStaked + addressComputedTable.CountMined;
                foreach (AddressHistoryComputedTable historyValue in history.Values.OrderBy(o => o.BlockIndex))
                {
                    historyValue.Position = ++position;
                }

                addressComputedTable.Received      += received;
                addressComputedTable.Staked        += staked;
                addressComputedTable.Mined         += mined;
                addressComputedTable.Sent          += sent;
                addressComputedTable.Available      = addressComputedTable.Received + addressComputedTable.Mined + addressComputedTable.Staked - addressComputedTable.Sent;
                addressComputedTable.CountReceived += countReceived;
                addressComputedTable.CountSent     += countSent;
                addressComputedTable.CountStaked   += countStaked;
                addressComputedTable.CountMined    += countMined;
                addressComputedTable.CountUtxo      = addressComputedTable.CountUtxo - utxoToDelete.Count + utxoToAdd.Count;

                addressComputedTable.ComputedBlockIndex = maxHeight; // the last block a trx was received to this address

                if (addressComputedTable.Available < 0)
                {
                    throw new ApplicationException("Failed to compute balance correctly");
                }

                try
                {
                    // only push to store if the same version of computed bloc index is present (meaning entry was not modified)
                    // block height must change if new trx are added so use it to apply OCC (Optimistic Concurrency Control)
                    // to determine if a newer entry was pushed to store.
                    FilterDefinition <AddressComputedTable> updateFilter = Builders <AddressComputedTable> .Filter
                                                                           .Where(f => f.Address == address && f.ComputedBlockIndex == currentHeight);

                    // update the computed address entry, this will throw if a newer version is in store
                    mongoDb.AddressComputedTable.ReplaceOne(updateFilter, addressComputedTable, new ReplaceOptions {
                        IsUpsert = true
                    });
                }
                catch (MongoWriteException nwe)
                {
                    if (nwe.WriteError.Category != ServerErrorCategory.DuplicateKey)
                    {
                        throw;
                    }

                    // address was already modified fetch the latest version
                    addressComputedTable = mongoDb.AddressComputedTable.Find(addrFilter).FirstOrDefault();

                    return(addressComputedTable);
                }

                var historyTask = Task.Run(() =>
                {
                    try
                    {
                        // if we managed to update the address we can safely insert history
                        mongoDb.AddressHistoryComputedTable.InsertMany(history.Values, new InsertManyOptions {
                            IsOrdered = false
                        });
                    }
                    catch (MongoBulkWriteException mbwex)
                    {
                        // in cases of reorgs trx are not deleted from the store,
                        // if a trx is already written and we attempt to write it again
                        // the write will fail and throw, so we ignore such errors.
                        // (IsOrdered = false will attempt all entries and only throw when done)
                        if (mbwex.WriteErrors.Any(e => e.Category != ServerErrorCategory.DuplicateKey))
                        {
                            throw;
                        }
                    }
                });

                Task.WaitAll(historyTask);
            }

            return(addressComputedTable);
        }
Esempio n. 3
0
        public QueryResult <QueryAddressItem> AddressHistory(string address, int?offset, int limit)
        {
            // make sure fields are computed
            AddressComputedTable addressComputedTable = ComputeAddressBalance(address);

            IQueryable <AddressHistoryComputedTable> filter = mongoDb.AddressHistoryComputedTable.AsQueryable()
                                                              .Where(t => t.Address == address);

            SyncBlockInfo storeTip = globalState.StoreTip;

            if (storeTip == null)
            {
                // this can happen if node is in the middle of reorg

                return(new QueryResult <QueryAddressItem>
                {
                    Items = Enumerable.Empty <QueryAddressItem>(),
                    Offset = 0,
                    Limit = limit,
                    Total = 0
                });
            }
            ;

            // This will first perform one db query.
            long total = addressComputedTable.CountSent + addressComputedTable.CountReceived + addressComputedTable.CountStaked + addressComputedTable.CountMined;

            // Filter by the position, in the order of first entry being 1 and then second entry being 2.
            filter = filter.OrderBy(s => s.Position);

            long startPosition = offset ?? total - limit;
            long endPosition   = (startPosition) + limit;

            // Get all items that is higher than start position and lower than end position.
            var list = filter.Where(w => w.Position > startPosition && w.Position <= endPosition).ToList();

            // Loop all transaction IDs and get the transaction object.
            IEnumerable <QueryAddressItem> transactions = list.Select(item => new QueryAddressItem
            {
                BlockIndex      = item.BlockIndex,
                Value           = item.AmountInOutputs - item.AmountInInputs,
                EntryType       = item.EntryType,
                TransactionHash = item.TransactionId,
                Confirmations   = storeTip.BlockIndex + 1 - item.BlockIndex
            });

            IEnumerable <QueryAddressItem> mempollTransactions = null;

            if (offset == total)
            {
                List <MapMempoolAddressBag> mempoolAddressBag = MempoolBalance(address);

                mempollTransactions = mempoolAddressBag.Select(item => new QueryAddressItem
                {
                    BlockIndex      = 0,
                    Value           = item.AmountInOutputs - item.AmountInInputs,
                    EntryType       = item.AmountInOutputs > item.AmountInInputs ? "receive" : "send",
                    TransactionHash = item.Mempool.TransactionId,
                    Confirmations   = 0
                });
            }

            List <QueryAddressItem> allTransactions = new();

            if (mempollTransactions != null)
            {
                allTransactions.AddRange(mempollTransactions);
            }

            allTransactions.AddRange(transactions);

            return(new QueryResult <QueryAddressItem>
            {
                Items = allTransactions,
                Offset = (int)startPosition,
                Limit = limit,
                Total = total
            });
        }