/// <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) }); }
/// <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); }
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 }); }