diff --git a/Framework/NethereumWorkflow/BlockTimeFinder.cs b/Framework/NethereumWorkflow/BlockTimeFinder.cs deleted file mode 100644 index 829870b..0000000 --- a/Framework/NethereumWorkflow/BlockTimeFinder.cs +++ /dev/null @@ -1,217 +0,0 @@ -using Logging; - -namespace NethereumWorkflow -{ - public class BlockTimeFinder - { - private readonly BlockCache cache; - private readonly IWeb3Blocks web3; - private readonly ILog log; - - public BlockTimeFinder(IWeb3Blocks web3, ILog log) - { - this.web3 = web3; - this.log = log; - - cache = new BlockCache(web3); - } - - public ulong? GetHighestBlockNumberBefore(DateTime moment) - { - cache.Initialize(); - if (moment <= cache.Genesis.Utc) return null; - if (moment >= cache.Current.Utc) return cache.Current.BlockNumber; - - return Search(cache.Genesis, cache.Current, moment, HighestBeforeSelector); - } - - public ulong? GetLowestBlockNumberAfter(DateTime moment) - { - cache.Initialize(); - if (moment >= cache.Current.Utc) return null; - if (moment <= cache.Genesis.Utc) return cache.Genesis.BlockNumber; - - return Search(cache.Genesis, cache.Current, moment, LowestAfterSelector); - } - - private ulong Search(BlockTimeEntry lower, BlockTimeEntry upper, DateTime target, Func isWhatIwant) - { - var middle = GetMiddle(lower, upper); - if (middle.BlockNumber == lower.BlockNumber) - { - if (isWhatIwant(target, upper)) return upper.BlockNumber; - } - - if (isWhatIwant(target, middle)) - { - return middle.BlockNumber; - } - - if (middle.Utc > target) - { - return Search(lower, middle, target, isWhatIwant); - } - else - { - return Search(middle, upper, target, isWhatIwant); - } - } - - private BlockTimeEntry GetMiddle(BlockTimeEntry lower, BlockTimeEntry upper) - { - ulong range = upper.BlockNumber - lower.BlockNumber; - ulong number = lower.BlockNumber + (range / 2); - return GetBlock(number); - } - - private bool HighestBeforeSelector(DateTime target, BlockTimeEntry entry) - { - var next = GetBlock(entry.BlockNumber + 1); - return - entry.Utc < target && - next.Utc > target; - } - - private bool LowestAfterSelector(DateTime target, BlockTimeEntry entry) - { - var previous = GetBlock(entry.BlockNumber - 1); - return - entry.Utc > target && - previous.Utc < target; - } - - private BlockTimeEntry GetBlock(ulong number) - { - if (number < cache.Genesis.BlockNumber) throw new Exception("Can't fetch block before genesis."); - if (number > cache.Current.BlockNumber) throw new Exception("Can't fetch block after current."); - - var dateTime = web3.GetTimestampForBlock(number); - if (dateTime == null) throw new Exception("Failed to get dateTime for block that should exist."); - return cache.Add(number, dateTime.Value); - } - } - - public class BlockCache - { - private const int MaxEntries = 1024; - private readonly Dictionary entries = new Dictionary(); - private readonly IWeb3Blocks web3; - - public BlockTimeEntry Genesis { get; private set; } = null!; - public BlockTimeEntry Current { get; private set; } = null!; - - public BlockCache(IWeb3Blocks web3) - { - this.web3 = web3; - } - - public void Initialize() - { - AddCurrentBlock(); - LookForGenesisBlock(); - - if (Current.BlockNumber == Genesis.BlockNumber) - { - throw new Exception("Unsupported condition: Current block is genesis block."); - } - } - - public BlockTimeEntry Add(ulong number, DateTime dateTime) - { - return Add(new BlockTimeEntry(number, dateTime)); - } - - public BlockTimeEntry Add(BlockTimeEntry entry) - { - if (!entries.ContainsKey(entry.BlockNumber)) - { - if (entries.Count > MaxEntries) - { - entries.Clear(); - Initialize(); - } - entries.Add(entry.BlockNumber, entry); - } - - return entries[entry.BlockNumber]; - } - - public BlockTimeEntry? Get(ulong number) - { - if (!entries.TryGetValue(number, out BlockTimeEntry? value)) return null; - return value; - } - - private void LookForGenesisBlock() - { - if (Genesis != null) return; - - var blockTime = web3.GetTimestampForBlock(0); - if (blockTime != null) - { - AddGenesisBlock(0, blockTime.Value); - return; - } - - LookForGenesisBlock(0, Current); - } - - private void LookForGenesisBlock(ulong lower, BlockTimeEntry upper) - { - if (Genesis != null) return; - - var range = upper.BlockNumber - lower; - if (range == 1) - { - var lowTime = web3.GetTimestampForBlock(lower); - if (lowTime != null) - { - AddGenesisBlock(lower, lowTime.Value); - } - else - { - AddGenesisBlock(upper); - } - return; - } - - var current = lower + (range / 2); - - var blockTime = web3.GetTimestampForBlock(current); - if (blockTime != null) - { - var newUpper = Add(current, blockTime.Value); - LookForGenesisBlock(lower, newUpper); - } - else - { - LookForGenesisBlock(current, upper); - } - } - - private void AddCurrentBlock() - { - var currentBlockNumber = web3.GetCurrentBlockNumber(); - var blockTime = web3.GetTimestampForBlock(currentBlockNumber); - if (blockTime == null) throw new Exception("Unable to get dateTime for current block."); - AddCurrentBlock(currentBlockNumber, blockTime.Value); - } - - private void AddCurrentBlock(ulong currentBlockNumber, DateTime dateTime) - { - Current = new BlockTimeEntry(currentBlockNumber, dateTime); - Add(Current); - } - - private void AddGenesisBlock(ulong number, DateTime dateTime) - { - AddGenesisBlock(new BlockTimeEntry(number, dateTime)); - } - - private void AddGenesisBlock(BlockTimeEntry entry) - { - Genesis = entry; - Add(Genesis); - } - } -} diff --git a/Framework/NethereumWorkflow/BlockUtils/BlockCache.cs b/Framework/NethereumWorkflow/BlockUtils/BlockCache.cs new file mode 100644 index 0000000..3fdf803 --- /dev/null +++ b/Framework/NethereumWorkflow/BlockUtils/BlockCache.cs @@ -0,0 +1,39 @@ +namespace NethereumWorkflow.BlockUtils +{ + public class BlockCache + { + public delegate void CacheClearedEvent(); + + private const int MaxEntries = 1024; + private readonly Dictionary entries = new Dictionary(); + + public event CacheClearedEvent? OnCacheCleared; + + public BlockTimeEntry Add(ulong number, DateTime dateTime) + { + return Add(new BlockTimeEntry(number, dateTime)); + } + + public BlockTimeEntry Add(BlockTimeEntry entry) + { + if (!entries.ContainsKey(entry.BlockNumber)) + { + if (entries.Count > MaxEntries) + { + entries.Clear(); + var e = OnCacheCleared; + if (e != null) e(); + } + entries.Add(entry.BlockNumber, entry); + } + + return entries[entry.BlockNumber]; + } + + public BlockTimeEntry? Get(ulong number) + { + if (!entries.TryGetValue(number, out BlockTimeEntry? value)) return null; + return value; + } + } +} diff --git a/Framework/NethereumWorkflow/BlockTimeEntry.cs b/Framework/NethereumWorkflow/BlockUtils/BlockTimeEntry.cs similarity index 90% rename from Framework/NethereumWorkflow/BlockTimeEntry.cs rename to Framework/NethereumWorkflow/BlockUtils/BlockTimeEntry.cs index 921f5b5..8846b93 100644 --- a/Framework/NethereumWorkflow/BlockTimeEntry.cs +++ b/Framework/NethereumWorkflow/BlockUtils/BlockTimeEntry.cs @@ -1,4 +1,4 @@ -namespace NethereumWorkflow +namespace NethereumWorkflow.BlockUtils { public class BlockTimeEntry { diff --git a/Framework/NethereumWorkflow/BlockUtils/BlockTimeFinder.cs b/Framework/NethereumWorkflow/BlockUtils/BlockTimeFinder.cs new file mode 100644 index 0000000..851a8bf --- /dev/null +++ b/Framework/NethereumWorkflow/BlockUtils/BlockTimeFinder.cs @@ -0,0 +1,95 @@ +using Logging; + +namespace NethereumWorkflow.BlockUtils +{ + public class BlockTimeFinder + { + private readonly BlockCache cache; + private readonly BlockchainBounds bounds; + private readonly IWeb3Blocks web3; + private readonly ILog log; + + public BlockTimeFinder(BlockCache cache, IWeb3Blocks web3, ILog log) + { + this.web3 = web3; + this.log = log; + + this.cache = cache; + bounds = new BlockchainBounds(cache, web3); + } + + public ulong? GetHighestBlockNumberBefore(DateTime moment) + { + bounds.Initialize(); + if (moment <= bounds.Genesis.Utc) return null; + if (moment >= bounds.Current.Utc) return bounds.Current.BlockNumber; + + return Search(bounds.Genesis, bounds.Current, moment, HighestBeforeSelector); + } + + public ulong? GetLowestBlockNumberAfter(DateTime moment) + { + bounds.Initialize(); + if (moment >= bounds.Current.Utc) return null; + if (moment <= bounds.Genesis.Utc) return bounds.Genesis.BlockNumber; + + return Search(bounds.Genesis, bounds.Current, moment, LowestAfterSelector); + } + + private ulong Search(BlockTimeEntry lower, BlockTimeEntry upper, DateTime target, Func isWhatIwant) + { + var middle = GetMiddle(lower, upper); + if (middle.BlockNumber == lower.BlockNumber) + { + if (isWhatIwant(target, upper)) return upper.BlockNumber; + } + + if (isWhatIwant(target, middle)) + { + return middle.BlockNumber; + } + + if (middle.Utc > target) + { + return Search(lower, middle, target, isWhatIwant); + } + else + { + return Search(middle, upper, target, isWhatIwant); + } + } + + private BlockTimeEntry GetMiddle(BlockTimeEntry lower, BlockTimeEntry upper) + { + ulong range = upper.BlockNumber - lower.BlockNumber; + ulong number = lower.BlockNumber + range / 2; + return GetBlock(number); + } + + private bool HighestBeforeSelector(DateTime target, BlockTimeEntry entry) + { + var next = GetBlock(entry.BlockNumber + 1); + return + entry.Utc < target && + next.Utc > target; + } + + private bool LowestAfterSelector(DateTime target, BlockTimeEntry entry) + { + var previous = GetBlock(entry.BlockNumber - 1); + return + entry.Utc > target && + previous.Utc < target; + } + + private BlockTimeEntry GetBlock(ulong number) + { + if (number < bounds.Genesis.BlockNumber) throw new Exception("Can't fetch block before genesis."); + if (number > bounds.Current.BlockNumber) throw new Exception("Can't fetch block after current."); + + var dateTime = web3.GetTimestampForBlock(number); + if (dateTime == null) throw new Exception("Failed to get dateTime for block that should exist."); + return cache.Add(number, dateTime.Value); + } + } +} diff --git a/Framework/NethereumWorkflow/BlockUtils/BlockchainBounds.cs b/Framework/NethereumWorkflow/BlockUtils/BlockchainBounds.cs new file mode 100644 index 0000000..92841b3 --- /dev/null +++ b/Framework/NethereumWorkflow/BlockUtils/BlockchainBounds.cs @@ -0,0 +1,106 @@ +namespace NethereumWorkflow.BlockUtils +{ + public class BlockchainBounds + { + private readonly BlockCache cache; + private readonly IWeb3Blocks web3; + + public BlockTimeEntry Genesis { get; private set; } = null!; + public BlockTimeEntry Current { get; private set; } = null!; + + public BlockchainBounds(BlockCache cache, IWeb3Blocks web3) + { + this.cache = cache; + this.web3 = web3; + + cache.OnCacheCleared += Initialize; + } + + public void Initialize() + { + AddCurrentBlock(); + LookForGenesisBlock(); + + if (Current.BlockNumber == Genesis.BlockNumber) + { + throw new Exception("Unsupported condition: Current block is genesis block."); + } + } + + private void LookForGenesisBlock() + { + if (Genesis != null) + { + cache.Add(Genesis); + return; + } + + var blockTime = web3.GetTimestampForBlock(0); + if (blockTime != null) + { + AddGenesisBlock(0, blockTime.Value); + return; + } + + LookForGenesisBlock(0, Current); + } + + private void LookForGenesisBlock(ulong lower, BlockTimeEntry upper) + { + if (Genesis != null) return; + + var range = upper.BlockNumber - lower; + if (range == 1) + { + var lowTime = web3.GetTimestampForBlock(lower); + if (lowTime != null) + { + AddGenesisBlock(lower, lowTime.Value); + } + else + { + AddGenesisBlock(upper); + } + return; + } + + var current = lower + range / 2; + + var blockTime = web3.GetTimestampForBlock(current); + if (blockTime != null) + { + var newUpper = cache.Add(current, blockTime.Value); + LookForGenesisBlock(lower, newUpper); + } + else + { + LookForGenesisBlock(current, upper); + } + } + + private void AddCurrentBlock() + { + var currentBlockNumber = web3.GetCurrentBlockNumber(); + var blockTime = web3.GetTimestampForBlock(currentBlockNumber); + if (blockTime == null) throw new Exception("Unable to get dateTime for current block."); + AddCurrentBlock(currentBlockNumber, blockTime.Value); + } + + private void AddCurrentBlock(ulong currentBlockNumber, DateTime dateTime) + { + Current = new BlockTimeEntry(currentBlockNumber, dateTime); + cache.Add(Current); + } + + private void AddGenesisBlock(ulong number, DateTime dateTime) + { + AddGenesisBlock(new BlockTimeEntry(number, dateTime)); + } + + private void AddGenesisBlock(BlockTimeEntry entry) + { + Genesis = entry; + cache.Add(Genesis); + } + } +} diff --git a/Framework/NethereumWorkflow/NethereumInteraction.cs b/Framework/NethereumWorkflow/NethereumInteraction.cs index 2f58874..3240d9b 100644 --- a/Framework/NethereumWorkflow/NethereumInteraction.cs +++ b/Framework/NethereumWorkflow/NethereumInteraction.cs @@ -3,12 +3,16 @@ using Nethereum.ABI.FunctionEncoding.Attributes; using Nethereum.Contracts; using Nethereum.RPC.Eth.DTOs; using Nethereum.Web3; +using NethereumWorkflow.BlockUtils; using Utils; namespace NethereumWorkflow { public class NethereumInteraction { + // BlockCache is a static instance: It stays alive for the duration of the application runtime. + private readonly static BlockCache blockCache = new BlockCache(); + private readonly ILog log; private readonly Web3 web3; @@ -88,12 +92,23 @@ namespace NethereumWorkflow public List> GetEvents(string address, TimeRange timeRange) where TEvent : IEventDTO, new() { var wrapper = new Web3Wrapper(web3, log); - var blockTimeFinder = new BlockTimeFinder(wrapper, log); + var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log); var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From); var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To); - return GetEvents(address, fromBlock, toBlock); + if (!fromBlock.HasValue) + { + log.Error("Failed to find lowest block for time range: " + timeRange); + throw new Exception("Failed"); + } + if (!toBlock.HasValue) + { + log.Error("Failed to find highest block for time range: " + timeRange); + throw new Exception("Failed"); + } + + return GetEvents(address, fromBlock.Value, toBlock.Value); } public List> GetEvents(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new() diff --git a/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs b/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs index 9403981..5653b45 100644 --- a/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs +++ b/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs @@ -1,6 +1,7 @@ using Logging; using Moq; using NethereumWorkflow; +using NethereumWorkflow.BlockUtils; using NUnit.Framework; namespace FrameworkTests.NethereumWorkflow @@ -39,7 +40,7 @@ namespace FrameworkTests.NethereumWorkflow return null; }); - finder = new BlockTimeFinder(web3.Object, log.Object); + finder = new BlockTimeFinder(new BlockCache(), web3.Object, log.Object); } [Test]