diff --git a/src/Blockcore.Indexer.Angor/Controllers/MempoolSpaceController.cs b/src/Blockcore.Indexer.Angor/Controllers/MempoolSpaceController.cs new file mode 100644 index 0000000..040635a --- /dev/null +++ b/src/Blockcore.Indexer.Angor/Controllers/MempoolSpaceController.cs @@ -0,0 +1,102 @@ +using Microsoft.AspNetCore.Mvc; +using Blockcore.Indexer.Core.Storage; +using Blockcore.Indexer.Core.Models; +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using Blockcore.Indexer.Core.Storage.Types; +using Blockcore.Indexer.Core.Handlers; + + + +namespace Blockcore.Indexer.Angor.Controllers +{ + [ApiController] + [Route("api/mempoolspace")] + public class MempoolSpaceController : Controller + { + private readonly IStorage storage; + private readonly StatsHandler statsHandler; + + private readonly JsonSerializerOptions serializeOption = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + WriteIndented = true + }; + + public MempoolSpaceController(IStorage storage, StatsHandler statsHandler) + { + this.storage = storage; + this.statsHandler = statsHandler; + } + + [HttpGet] + [Route("address/{address}")] + public IActionResult GetAddress([MinLength(4)][MaxLength(100)] string address) + { + AddressResponse addressResponse = storage.AddressResponseBalance(address); + return Ok(JsonSerializer.Serialize(addressResponse, serializeOption)); + } + + [HttpGet] + [Route("address/{address}/txs")] + public async Task GetAddressTransactions(string address) + { + var transactions = storage.AddressHistory(address, null, 50).Items.Select(t => t.TransactionHash).ToList(); + List txns = await storage.GetMempoolTransactionListAsync(transactions); + return Ok(JsonSerializer.Serialize(txns, serializeOption)); + } + + [HttpGet] + [Route("tx/{txid}/outspends")] + public async Task GetTransactionOutspends(string txid) + { + List responses = await storage.GetTransactionOutspendsAsync(txid); + return Ok(JsonSerializer.Serialize(responses, serializeOption)); + } + + [HttpGet] + [Route("fees/recommended")] + public IActionResult GetRecommendedFees() + { + RecommendedFees recommendedFees = new(); + var statsFees = statsHandler.GetFeeEstimation([1, 3, 6, 12, 48]); + statsFees.Wait(); + var Fees = statsFees.Result.Fees.Select(fee => ConvertToSatsPerVByte(fee.FeeRate)).ToList(); + recommendedFees.FastestFee = (int)Fees[0]; + recommendedFees.HalfHourFee = (int)Fees[1]; + recommendedFees.HourFee = (int)Fees[2]; + recommendedFees.EconomyFee = (int)Fees[3]; + recommendedFees.MinimumFee = (int)Fees[4]; + + return Ok(JsonSerializer.Serialize(recommendedFees, new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + })); + } + + private double ConvertToSatsPerVByte(double fee) + { + return fee / 1_000; + } + + [HttpGet] + [Route("tx/{txid}/hex")] + public IActionResult GetTransactionHex(string txid) + { + var txn = storage.GetRawTransaction(txid); + if (txn == null) + { + return NotFound(); + } + return Ok(txn); + } + + [HttpGet] + [Route("block-height/{height}")] + public IActionResult GetBlockHeight(int height) + { + return Ok(storage.BlockByIndex(height).BlockHash); + } + } +} \ No newline at end of file diff --git a/src/Blockcore.Indexer.Core/Models/MempoolSpaceModels.cs b/src/Blockcore.Indexer.Core/Models/MempoolSpaceModels.cs new file mode 100644 index 0000000..6134e51 --- /dev/null +++ b/src/Blockcore.Indexer.Core/Models/MempoolSpaceModels.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Net; +using System.Net.Http.Json; + + +namespace Blockcore.Indexer.Core.Models +{ + + public class AddressStats + { + public int FundedTxoCount { get; set; } + public long FundedTxoSum { get; set; } + public int SpentTxoCount { get; set; } + public long SpentTxoSum { get; set; } + public int TxCount { get; set; } + } + public class AddressResponse + { + public string Address { get; set; } + public AddressStats ChainStats { get; set; } + public AddressStats MempoolStats { get; set; } + } + + public class OutspentResponse{ + public bool spent { get; set; } + public string txid { get; set; } + public int vin { get; set; } + public UtxoStatus status { get; set; } + } + + public class AddressUtxo + { + public string Txid { get; set; } + public int Vout { get; set; } + public UtxoStatus Status { get; set; } + public long Value { get; set; } + } + + public class UtxoStatus + { + public bool Confirmed { get; set; } + public int BlockHeight { get; set; } + public string BlockHash { get; set; } + public long BlockTime { get; set; } + } + + public class RecommendedFees + { + public int FastestFee { get; set; } + public int HalfHourFee { get; set; } + public int HourFee { get; set; } + public int EconomyFee { get; set; } + public int MinimumFee { get; set; } + } + + public class Vin + { + public bool IsCoinbase { get; set; } + public PrevOut Prevout { get; set; } + public string Scriptsig { get; set; } + public string Asm { get; set; } + public long Sequence { get; set; } + public string Txid { get; set; } + public int Vout { get; set; } + public List Witness { get; set; } + public string InnserRedeemscriptAsm { get; set; } + public string InnerWitnessscriptAsm { get; set; } + } + public class PrevOut + { + public long Value { get; set; } + public string Scriptpubkey { get; set; } + public string ScriptpubkeyAddress { get; set; } + public string ScriptpubkeyAsm { get; set; } + public string ScriptpubkeyType { get; set; } + } + + public class MempoolTransaction + { + public string Txid { get; set; } + + public int Version { get; set; } + + public int Locktime { get; set; } + public int Size { get; set; } + public int Weight { get; set; } + public int Fee { get; set; } + public List Vin { get; set; } + public List Vout { get; set; } + public UtxoStatus Status { get; set; } + } + + public class Outspent + { + public bool Spent { get; set; } + } +} diff --git a/src/Blockcore.Indexer.Core/Storage/IStorage.cs b/src/Blockcore.Indexer.Core/Storage/IStorage.cs index cd85cb3..99d0297 100644 --- a/src/Blockcore.Indexer.Core/Storage/IStorage.cs +++ b/src/Blockcore.Indexer.Core/Storage/IStorage.cs @@ -15,11 +15,15 @@ public interface IStorage QueryAddress AddressBalance(string address); + AddressResponse AddressResponseBalance(string address); + Task> QuickBalancesLookupForAddressesWithHistoryCheckAsync( IEnumerable addresses, bool includePending = false); QueryResult AddressHistory(string address, int? offset, int limit); + Task> GetMempoolTransactionListAsync(List txids); + QueryResult GetMemoryTransactionsSlim(int offset, int limit); QueryResult GetMemoryTransactions(int offset, int limit); @@ -50,7 +54,7 @@ Task> QuickBalancesLookupForAddressesWithHistoryCheckA long TotalBalance(); - Task> GetUnspentTransactionsByAddressAsync(string address,long confirmations, int offset, int limit); + Task> GetUnspentTransactionsByAddressAsync(string address, long confirmations, int offset, int limit); Task DeleteBlockAsync(string blockHash); @@ -62,5 +66,9 @@ Task> QuickBalancesLookupForAddressesWithHistoryCheckA List GetPeerFromDate(DateTime date); Task InsertPeer(PeerDetails info); + + public Task GetOutputFromOutpointAsync(string txid, int index); + + public Task> GetTransactionOutspendsAsync(string txid); } } diff --git a/src/Blockcore.Indexer.Core/Storage/Mongo/MongoData.cs b/src/Blockcore.Indexer.Core/Storage/Mongo/MongoData.cs index 1924708..d83dc2f 100644 --- a/src/Blockcore.Indexer.Core/Storage/Mongo/MongoData.cs +++ b/src/Blockcore.Indexer.Core/Storage/Mongo/MongoData.cs @@ -203,6 +203,18 @@ public SyncBlockInfo BlockByIndex(long blockIndex) return block; } + public async Task BlockByIndexAsync(long blockIndex) + { + FilterDefinition filter = Builders.Filter.Eq(info => info.BlockIndex, blockIndex); + var blockCursor = await mongoDb.BlockTable.FindAsync(filter); + var block = await blockCursor.FirstOrDefaultAsync(); + SyncBlockInfo tip = globalState.StoreTip; + if (tip != null && block != null) + block.Confirmations = tip.BlockIndex + 1 - block.BlockIndex; + + return mongoBlockToStorageBlock.Map(block); + } + public SyncBlockInfo BlockByHash(string blockHash) { FilterDefinition filter = Builders.Filter.Eq(info => info.BlockHash, blockHash); @@ -310,13 +322,28 @@ public SyncRawTransaction TransactionGetByHash(string trxHash) return mongoDb.TransactionTable.Find(filter).ToList().Select(t => new SyncRawTransaction { TransactionHash = trxHash, RawTransaction = t.RawTransaction }).FirstOrDefault(); } + public async Task TransactionGetByHashAsync(string trxHash){ + FilterDefinition filter = Builders.Filter.Eq(info => info.TransactionId, trxHash); + + var TransactionTableCursor = await mongoDb.TransactionTable.FindAsync(filter); + var TransactionTable = await TransactionTableCursor.ToListAsync(); + + return TransactionTable.Select(t => new SyncRawTransaction { TransactionHash = trxHash, RawTransaction = t.RawTransaction }).FirstOrDefault(); + } + public InputTable GetTransactionInput(string transaction, int index) { FilterDefinition filter = Builders.Filter.Eq(addr => addr.Outpoint, new Outpoint { TransactionId = transaction, OutputIndex = index }); return mongoDb.InputTable.Find(filter).ToList().FirstOrDefault(); } + public async Task GetTransactionInputAsync(string transaction, int index){ + FilterDefinition filter = Builders.Filter.Eq(addr => addr.Outpoint, new Outpoint { TransactionId = transaction, OutputIndex = index }); + var cursor = await mongoDb.InputTable.FindAsync(filter); + var list = await cursor.ToListAsync(); + return list.FirstOrDefault(); + } public OutputTable GetTransactionOutput(string transaction, int index) { FilterDefinition filter = Builders.Filter.Eq(addr => addr.Outpoint, new Outpoint { TransactionId = transaction, OutputIndex = index }); @@ -324,6 +351,14 @@ public OutputTable GetTransactionOutput(string transaction, int index) return mongoDb.OutputTable.Find(filter).ToList().FirstOrDefault(); } + public async Task GetTransactionOutputAsync(string transaction, int index) + { + FilterDefinition filter = Builders.Filter.Eq(addr => addr.Outpoint, new Outpoint { TransactionId = transaction, OutputIndex = index }); + var cursor = await mongoDb.OutputTable.FindAsync(filter); + var list = await cursor.ToListAsync(); + return list.FirstOrDefault(); + } + public SyncTransactionInfo BlockTransactionGet(string transactionId) { FilterDefinition filter = Builders.Filter.Eq(info => info.TransactionId, transactionId); @@ -469,6 +504,92 @@ public SyncTransactionItems TransactionItemsGet(string transactionId, Transactio return ret; } + public async Task TransactionItemsGetAsync(string transactionId, Transaction transaction = null) + { + if (transaction == null) + { + // Try to find the trx in disk + SyncRawTransaction rawtrx = await TransactionGetByHashAsync(transactionId); + + if (rawtrx == null) + { + var client = clientFactory.Create(syncConnection); + + Client.Types.DecodedRawTransaction res = client.GetRawTransactionAsync(transactionId, 0).Result; + + if (res.Hex == null) + { + return null; + } + + transaction = syncConnection.Network.Consensus.ConsensusFactory.CreateTransaction(res.Hex); + transaction.PrecomputeHash(false, true); + } + else + { + transaction = syncConnection.Network.Consensus.ConsensusFactory.CreateTransaction(rawtrx.RawTransaction); + transaction.PrecomputeHash(false, true); + } + } + + bool hasWitness = transaction.HasWitness; + int witnessScaleFactor = syncConnection.Network.Consensus.Options?.WitnessScaleFactor ?? 4; + + int size = NBitcoin.BitcoinSerializableExtensions.GetSerializedSize(transaction, syncConnection.Network.Consensus.ConsensusFactory); + int virtualSize = hasWitness ? transaction.GetVirtualSize(witnessScaleFactor) : size; + int weight = virtualSize * witnessScaleFactor - (witnessScaleFactor - 1); + + var ret = new SyncTransactionItems + { + RBF = transaction.RBF, + LockTime = transaction.LockTime.ToString(), + Version = transaction.Version, + HasWitness = hasWitness, + Size = size, + VirtualSize = virtualSize, + Weight = weight, + IsCoinbase = transaction.IsCoinBase, + IsCoinstake = syncConnection.Network.Consensus.IsProofOfStake && transaction.IsCoinStake, + Inputs = transaction.Inputs.Select(v => new SyncTransactionItemInput + { + PreviousTransactionHash = v.PrevOut.Hash.ToString(), + PreviousIndex = (int)v.PrevOut.N, + WitScript = v.WitScript.ToScript().ToHex(), + ScriptSig = v.ScriptSig.ToHex(), + InputAddress = scriptInterpeter.GetSignerAddress(syncConnection.Network, v.ScriptSig), + SequenceLock = v.Sequence.ToString(), + }).ToList(), + Outputs = transaction.Outputs.Select((output, index) => new SyncTransactionItemOutput + { + Address = scriptInterpeter.InterpretScript(syncConnection.Network, output.ScriptPubKey)?.Addresses?.FirstOrDefault(), + Index = index, + Value = output.Value, + OutputType = scriptInterpeter.InterpretScript(syncConnection.Network, output.ScriptPubKey)?.ScriptType, // StandardScripts.GetTemplateFromScriptPubKey(output.ScriptPubKey)?.Type.ToString(), + ScriptPubKey = output.ScriptPubKey.ToHex() + }).ToList() + }; + + foreach (SyncTransactionItemInput input in ret.Inputs) + { + OutputTable outputTable = await GetTransactionOutputAsync(input.PreviousTransactionHash, input.PreviousIndex); + input.InputAddress = outputTable?.Address; + input.InputAmount = outputTable?.Value ?? 0; + } + + // try to fetch spent outputs + foreach (SyncTransactionItemOutput output in ret.Outputs) + { + output.SpentInTransaction = (await GetTransactionInputAsync(transactionId, output.Index))?.TrxHash; + } + + if (!ret.IsCoinbase && !ret.IsCoinstake) + { + // calcualte fee and feePk + ret.Fee = ret.Inputs.Sum(s => s.InputAmount) - ret.Outputs.Sum(s => s.Value); + } + + return ret; + } public QueryResult Richlist(int offset, int limit) { FilterDefinitionBuilder filterBuilder = Builders.Filter; @@ -646,6 +767,102 @@ public QueryResult AddressHistory(string address, int? offset, }; } + public async Task> GetMempoolTransactionListAsync(List txids) + { + FilterDefinition filter = Builders.Filter.In(info => info.TransactionId, txids); + var trxsCursor = await mongoDb.TransactionBlockTable.FindAsync(filter); + var trxs = await trxsCursor.ToListAsync(); + + SyncBlockInfo current = globalState.StoreTip;// GetLatestBlock(); + var blkTasks = trxs.Select(trx => BlockByIndexAsync(trx.BlockIndex)).ToList(); + var trxItemsTasks = trxs.Select(trx => TransactionItemsGetAsync(trx.TransactionId)).ToList(); + + SyncBlockInfo[] blks = await Task.WhenAll(blkTasks); + SyncTransactionItems[] transactionItemsList = await Task.WhenAll(trxItemsTasks); + List transactions = blks.Select((blk, index) => new SyncTransactionInfo + { + BlockIndex = trxs[index].BlockIndex, + BlockHash = blk.BlockHash, + Timestamp = blk.BlockTime, + TransactionHash = trxs[index].TransactionId, + TransactionIndex = trxs[index].TransactionIndex, + Confirmations = current.BlockIndex + 1 - trxs[index].BlockIndex + } + ).ToList(); + + var tasks = await Task.WhenAll(transactions.Select(async (transaction, index) => + { + var blk = blks[index]; + var outputsTasks = transactionItemsList[index].Inputs.Select(async input => await GetTransactionOutputAsync(input.PreviousTransactionHash, input.PreviousIndex)); + var outputs = await Task.WhenAll(outputsTasks); + + return new MempoolTransaction + { + Txid = transaction.TransactionHash, + Version = (int)transactionItemsList[index].Version, + Locktime = int.Parse(transactionItemsList[index].LockTime.Split(':').Last()), + Size = transactionItemsList[index].Size, + Weight = transactionItemsList[index].Weight, + Fee = (int)transactionItemsList[index].Fee, + Status = new() + { + Confirmed = transaction.Confirmations > 0, + BlockHeight = (int)transaction.BlockIndex, + BlockHash = transaction.BlockHash, + BlockTime = transaction.Timestamp + }, + Vin = transactionItemsList[index].Inputs.Select((input, inputIndex) => + { + OutputTable output = outputs[inputIndex]; + return new Vin() + { + IsCoinbase = input.InputCoinBase != null, + Prevout = new PrevOut() + { + Value = output.Value, + Scriptpubkey = output.ScriptHex, + ScriptpubkeyAddress = output.Address, + ScriptpubkeyAsm = null, + ScriptpubkeyType = null + }, + Scriptsig = input.ScriptSig, + Asm = null, + Sequence = long.Parse(input.SequenceLock), + Txid = input.PreviousTransactionHash, + Vout = input.PreviousIndex, + Witness = ComputeWitScript(input.WitScript), + InnserRedeemscriptAsm = null, + InnerWitnessscriptAsm = null + }; + }).ToList(), + Vout = transactionItemsList[index].Outputs.Select(output => new PrevOut() + { + Value = output.Value, + Scriptpubkey = output.ScriptPubKey, + ScriptpubkeyAddress = output.Address, + ScriptpubkeyAsm = null, + }).ToList(), + }; + } + )); + return tasks.ToList(); + } + + static List ComputeWitScript(string witScript) + { + List scripts = new(); + int index = 0; + while (index < witScript.Length) + { + string sizeHex = witScript.Substring(index, 2); + int size = int.Parse(sizeHex, System.Globalization.NumberStyles.HexNumber); + index += 2; + string script = witScript.Substring(index, size * 2); + scripts.Add(script); + } + + return scripts; + } /// /// Calculates the balance for specified address. /// @@ -673,6 +890,40 @@ public QueryAddress AddressBalance(string address) }; } + /// + /// Calculates the balance for specified address. + /// + /// + public AddressResponse AddressResponseBalance(string address) + { + AddressComputedTable addressComputedTable = ComputeAddressBalance(address); + List mempoolAddressBag = MempoolBalance(address); + + AddressResponse response = new() + { + Address = address, + ChainStats = new() + { + FundedTxoCount = (int)addressComputedTable.CountReceived, + FundedTxoSum = addressComputedTable.Received, + SpentTxoCount = (int)addressComputedTable.CountSent, + SpentTxoSum = addressComputedTable.Sent, + TxCount = (int)addressComputedTable.CountReceived + (int)addressComputedTable.CountSent + }, + MempoolStats = new() + { + FundedTxoCount = mempoolAddressBag.Count(s => s.AmountInOutputs > 0), + FundedTxoSum = mempoolAddressBag.Sum(s => s.AmountInOutputs), + SpentTxoCount = mempoolAddressBag.Count(s => s.AmountInInputs > 0), + SpentTxoSum = mempoolAddressBag.Sum(s => s.AmountInInputs), + TxCount = mempoolAddressBag.Count(s => s.AmountInOutputs > 0) + mempoolAddressBag.Count(s => s.AmountInInputs > 0) + } + }; + + return response; + } + + public async Task> QuickBalancesLookupForAddressesWithHistoryCheckAsync(IEnumerable addresses, bool includePending = false) { var outputTask = mongoDb.OutputTable.Distinct(_ => _.Address, _ => addresses.Contains(_.Address)) @@ -1223,5 +1474,61 @@ public async Task> GetUnspentTransactionsByAddressAsync(stri Limit = limit }; } + + public async Task> GetTransactionOutspendsAsync(string txid) + { + var outpoints = (await mongoDb.OutputTable.FindAsync(o => o.Outpoint.TransactionId == txid)).ToList(); + var OutspentResponses = (await Task.WhenAll(outpoints.Select(async op => await GetOutputSpendingStatusAsync(op.Outpoint)))).ToList(); + return OutspentResponses; + } + public async Task GetOutputSpendingStatusAsync(Outpoint outpoint) + { + // check if the output is spent in the mempool + var spentOutput = (await mongoDb.InputTable.FindAsync(i => i.Outpoint == outpoint)).FirstOrDefault(); + + OutspentResponse response = new() + { + spent = false, + txid = null, + vin = -1, + status = null + }; + + if (spentOutput != null){ + response.spent = true; + response.txid = spentOutput.TrxHash; + response.vin = 0; + var block = BlockByIndex(spentOutput.BlockIndex); + + response.status = new UtxoStatus(){ + Confirmed = true, + BlockHeight = (int)spentOutput.BlockIndex, + BlockHash = block.BlockHash, + BlockTime = block.BlockTime, + }; + } + + //check in mempool + var mempoolSpentOutput = (await mongoDb.Mempool.FindAsync(m => m.Inputs.Any(i => i.Outpoint == outpoint))).FirstOrDefault(); + if (mempoolSpentOutput != null){ + response.spent = true; + response.txid = mempoolSpentOutput.TransactionId; + response.vin = 0; + response.status = new UtxoStatus(){ + Confirmed = false, + BlockHeight = 0, + BlockHash = null, + BlockTime = 0, + }; + } + + return response; + } + + public async Task GetOutputFromOutpointAsync(string txid, int index) + { + var outpoint = new Outpoint { TransactionId = txid, OutputIndex = index }; + return (await mongoDb.OutputTable.FindAsync(o => o.Outpoint == outpoint)).FirstOrDefault(); + } } }