diff --git a/Framework/NethereumWorkflow/BlockTimeFinder.cs b/Framework/NethereumWorkflow/BlockTimeFinder.cs new file mode 100644 index 0000000..acc8b1c --- /dev/null +++ b/Framework/NethereumWorkflow/BlockTimeFinder.cs @@ -0,0 +1,205 @@ +using Nethereum.RPC.Eth.DTOs; +using Nethereum.Web3; +using Utils; + +namespace NethereumWorkflow +{ + public class BlockTimeFinder + { + private class BlockTimeEntry + { + public BlockTimeEntry(ulong blockNumber, DateTime utc) + { + BlockNumber = blockNumber; + Utc = utc; + } + + public ulong BlockNumber { get; } + public DateTime Utc { get; } + } + + private const ulong FetchRange = 6; + private const int MaxEntries = 1024; + private readonly Web3 web3; + private static readonly Dictionary entries = new Dictionary(); + + public BlockTimeFinder(Web3 web3) + { + this.web3 = web3; + } + + public ulong GetHighestBlockNumberBefore(DateTime moment) + { + AssertMomentIsInPast(moment); + Initialize(); + + var closestBefore = FindClosestBeforeEntry(moment); + var closestAfter = FindClosestAfterEntry(moment); + + if (closestBefore.Utc < moment && + closestAfter.Utc > moment && + closestBefore.BlockNumber + 1 == closestAfter.BlockNumber) + { + return closestBefore.BlockNumber; + } + + FetchBlocksAround(moment); + return GetHighestBlockNumberBefore(moment); + } + + public ulong GetLowestBlockNumberAfter(DateTime moment) + { + AssertMomentIsInPast(moment); + Initialize(); + + var closestBefore = FindClosestBeforeEntry(moment); + var closestAfter = FindClosestAfterEntry(moment); + + if (closestBefore.Utc < moment && + closestAfter.Utc > moment && + closestBefore.BlockNumber + 1 == closestAfter.BlockNumber) + { + return closestAfter.BlockNumber; + } + + FetchBlocksAround(moment); + return GetLowestBlockNumberAfter(moment); + } + + private void FetchBlocksAround(DateTime moment) + { + var timePerBlock = EstimateTimePerBlock(); + EnsureRecentBlockIfNecessary(moment, timePerBlock); + + var max = entries.Keys.Max(); + var latest = entries[max]; + var timeDifference = latest.Utc - moment; + double secondsDifference = Math.Abs(timeDifference.TotalSeconds); + double secondsPerBlock = timePerBlock.TotalSeconds; + + double numberOfBlocksDifference = secondsDifference / secondsPerBlock; + var blockDifference = Convert.ToUInt64(numberOfBlocksDifference); + + var fetchStart = (max - blockDifference) - (FetchRange / 2); + for (ulong i = 0; i < FetchRange; i++) + { + AddBlockNumber(fetchStart + i); + } + } + + private void EnsureRecentBlockIfNecessary(DateTime moment, TimeSpan timePerBlock) + { + var max = entries.Keys.Max(); + var latest = entries[max]; + var maxRetry = 10; + while (moment > latest.Utc) + { + var newBlock = AddCurrentBlock(); + if (newBlock.BlockNumber == latest.BlockNumber) + { + maxRetry--; + if (maxRetry == 0) throw new Exception("Unable to fetch recent block after 10x tries."); + Thread.Sleep(timePerBlock); + } + } + } + + private BlockTimeEntry AddBlockNumber(decimal blockNumber) + { + return AddBlockNumber(Convert.ToUInt64(blockNumber)); + } + + private BlockTimeEntry AddBlockNumber(ulong blockNumber) + { + if (entries.ContainsKey(blockNumber)) + { + return entries[blockNumber]; + } + + if (entries.Count > MaxEntries) + { + entries.Clear(); + Initialize(); + } + + var time = GetTimestampFromBlock(blockNumber); + var entry = new BlockTimeEntry(blockNumber, time); + entries.Add(blockNumber, entry); + return entry; + } + + private TimeSpan EstimateTimePerBlock() + { + var min = entries.Keys.Min(); + var max = entries.Keys.Max(); + var minTime = entries[min].Utc; + var maxTime = entries[max].Utc; + var elapsedTime = maxTime - minTime; + + double elapsedSeconds = elapsedTime.TotalSeconds; + double numberOfBlocks = max - min; + double secondsPerBlock = elapsedSeconds / numberOfBlocks; + + return TimeSpan.FromSeconds(secondsPerBlock); + } + + private void Initialize() + { + if (!entries.Any()) + { + AddCurrentBlock(); + AddBlockNumber(entries.Single().Key - 1); + } + } + + private static void AssertMomentIsInPast(DateTime moment) + { + if (moment > DateTime.UtcNow) throw new Exception("Moment must be UTC and must be in the past."); + } + + private BlockTimeEntry AddCurrentBlock() + { + var number = Time.Wait(web3.Eth.Blocks.GetBlockNumber.SendRequestAsync()); + var blockNumber = number.ToDecimal(); + return AddBlockNumber(blockNumber); + } + + private DateTime GetTimestampFromBlock(ulong blockNumber) + { + var block = Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(blockNumber))); + return DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(block.Timestamp.ToDecimal())).UtcDateTime; + } + + private BlockTimeEntry FindClosestBeforeEntry(DateTime moment) + { + var result = entries.Values.First(); + var highestTime = result.Utc; + + foreach (var entry in entries.Values) + { + if (entry.Utc > highestTime && entry.Utc < moment) + { + highestTime = entry.Utc; + result = entry; + } + } + return result; + } + + private BlockTimeEntry FindClosestAfterEntry(DateTime moment) + { + var result = entries.Values.First(); + var lowestTime = result.Utc; + + foreach (var entry in entries.Values) + { + if (entry.Utc < lowestTime && entry.Utc > moment) + { + lowestTime = entry.Utc; + result = entry; + } + } + return result; + } + } +} diff --git a/Framework/NethereumWorkflow/NethereumInteraction.cs b/Framework/NethereumWorkflow/NethereumInteraction.cs index 1efef81..1139175 100644 --- a/Framework/NethereumWorkflow/NethereumInteraction.cs +++ b/Framework/NethereumWorkflow/NethereumInteraction.cs @@ -1,4 +1,5 @@ using Logging; +using Nethereum.ABI.FunctionEncoding.Attributes; using Nethereum.Contracts; using Nethereum.RPC.Eth.DTOs; using Nethereum.Web3; @@ -77,5 +78,26 @@ namespace NethereumWorkflow return false; } } + + public List> GetEvent(string address, DateTime from, DateTime to) where TEvent : IEventDTO, new() + { + if (from >= to) throw new Exception("Time range is invalid."); + + var blockTimeFinder = new BlockTimeFinder(web3); + + var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(from); + var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(to); + + return GetEvent(address, fromBlock, toBlock); + } + + public List> GetEvent(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new() + { + var eventHandler = web3.Eth.GetEvent(address); + var from = new BlockParameter(fromBlockNumber); + var to = new BlockParameter(toBlockNumber); + var blockFilter = Time.Wait(eventHandler.CreateFilterBlockRangeAsync(from, to)); + return Time.Wait(eventHandler.GetAllChangesAsync(blockFilter)); + } } } diff --git a/ProjectPlugins/GethPlugin/GethNode.cs b/ProjectPlugins/GethPlugin/GethNode.cs index d2e2122..8c83d34 100644 --- a/ProjectPlugins/GethPlugin/GethNode.cs +++ b/ProjectPlugins/GethPlugin/GethNode.cs @@ -1,6 +1,7 @@ using Core; using KubernetesWorkflow.Types; using Logging; +using Nethereum.ABI.FunctionEncoding.Attributes; using Nethereum.Contracts; using NethereumWorkflow; @@ -20,6 +21,9 @@ namespace GethPlugin decimal? GetSyncedBlockNumber(); bool IsContractAvailable(string abi, string contractAddress); GethBootstrapNode GetBootstrapRecord(); + List> GetEvents(string address) where TEvent : IEventDTO, new(); + List> GetEvents(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new(); + List> GetEvents(string address, DateTime from, DateTime to) where TEvent : IEventDTO, new(); } public class DeploymentGethNode : BaseGethNode, IGethNode @@ -133,6 +137,19 @@ namespace GethPlugin return StartInteraction().IsContractAvailable(abi, contractAddress); } + public List> GetEvents(string address) where TEvent : IEventDTO, new() + { + StartInteraction().GetEvent(); + } + + public List> GetEvents(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new() + { + } + + public List> GetEvents(string address, DateTime from, DateTime to) where TEvent : IEventDTO, new() + { + } + protected abstract NethereumInteraction StartInteraction(); } }