using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Knoks.Core.Data.Dao; using Knoks.Core.Data.Interfaces; using Knoks.Core.Entities; using Knoks.Core.Entities.Settings; using Knoks.Core.Logic.Interfaces; using Microsoft.Extensions.Logging; using Nethereum.Contracts; using Nethereum.Util; using Nethereum.Web3; using AccountTransactionType = Knoks.Core.Entities.Args.AccountTransactions.AccountTransactionType; namespace Knoks.TokenContractTracking { #region InnerClass internal class TransferInfo { public string From; public User FromUser; public Account FromAccount; public string To; public User ToUser; public Account ToAccount; public decimal AmountKnoks; public string TransactionHash; public DateTime EventDate; public ulong BlockNumber; } #endregion public class TokenContractTracker : IDisposable { private readonly ILogger _logger; private readonly TokenContractTrackerSettings _trackerSettings; private readonly EthereumConnectionSettings _ethSettings; private readonly string _trackerName = nameof(TokenContractTracker); CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); public bool IsInitialized { get; private set; } private Task _worker; private TokenContractService _tokenContractService; private ulong _lastCheckedBlockNumber = 0; private IAccountTransactionManager _accountTransactionManager; private IUserManager _userManager; private readonly IAccountDao _accountDao; public TokenContractTracker(TokenContractTrackerSettings trackerSettings, EthereumConnectionSettings ethSettings, ILogger logger, IAccountTransactionManager accountTransactionManager, IUserManager userManager, IAccountDao accountDao) { _accountTransactionManager = accountTransactionManager ?? throw new ArgumentNullException(nameof(accountTransactionManager)); _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); _accountDao = accountDao ?? throw new ArgumentNullException(nameof(accountDao)); _trackerSettings = trackerSettings ?? throw new ArgumentNullException(nameof(trackerSettings)); _ethSettings = ethSettings ?? throw new ArgumentNullException(nameof(ethSettings)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } private void LogInfo(string info) { _logger.LogInformation($"{DateTime.UtcNow:G} {info}"); } public async Task Init() { if (IsInitialized) throw new InvalidOperationException("Already initialized"); LogInfo($"Initializing service {_trackerName}.."); //load tickers dictionary var transaction = await _accountTransactionManager.GetLatestDepositEvent(); if (transaction != null) { if(!ulong.TryParse(transaction.Comment, out _lastCheckedBlockNumber)) throw new InvalidOperationException("Wrong Deposit transaction in database: Comment does not contan block number. AccountTransactionId = " + transaction.AccountTransactionId); } _tokenContractService = new TokenContractService(new Web3(_ethSettings.Web3TargetAddress), _ethSettings.KnokTokenContractAddress, _ethSettings.KnokTokenContractAbi); IsInitialized = true; } private void DebugInfo(string info, bool timestamp = false) { if (timestamp) _logger.LogDebug($"{DateTime.UtcNow:G} {info}"); else _logger.LogDebug($"{info}"); } public void Start() { //if (!IsInitialized) // throw new InvalidOperationException($"Service is not initialized"); if (_worker != null) throw new InvalidOperationException($"Service is already started ({_worker.Status})"); LogInfo($"Starting {_trackerName}.."); _worker = Task.Run(() => CheckContractChanges_DoWork(_cancellationTokenSource.Token), _cancellationTokenSource.Token); LogInfo($"{_trackerName} started."); } public void Stop() { if (_worker == null) throw new InvalidOperationException("Workers are not started"); LogInfo($"Signalling {_trackerName} to stop.."); _cancellationTokenSource.Cancel(); LogInfo($"{_trackerName} signalled to stop."); } private async Task CheckContractChanges_DoWork(CancellationToken ct) { if (ct.IsCancellationRequested) { LogInfo("Task was cancelled before it got started."); ct.ThrowIfCancellationRequested(); } DateTime cycleBeginTime; do { cycleBeginTime = DateTime.UtcNow; List> resultEvents = await _tokenContractService.GetTransferEvents(_lastCheckedBlockNumber + 1); List infos = new List(); foreach (var resultEvent in resultEvents) { infos.Add(new TransferInfo() { From = resultEvent.Event.From, To = resultEvent.Event.To, AmountKnoks = UnitConversion.Convert.FromWei(resultEvent.Event.Value), EventDate = DateTime.UtcNow, TransactionHash = resultEvent.Log.TransactionHash, BlockNumber = (ulong)resultEvent.Log.BlockNumber.Value }); } //TODO: change to batch processing inside single stored procedure (PASS datatable to it) foreach (var depositInfo in infos) { var userFromInfo = await _userManager.GetUserInfo(depositInfo.From); var userToInfo = await _userManager.GetUserInfo(depositInfo.To); if (userFromInfo.User == null || userToInfo.User == null) { //TODO: support single account transaction (use SetAccountBalance method) LogInfo($"Balance TRANSFER processing FAILURE: at least one of wallet addresses are not exist. From: {depositInfo.From}; To: {depositInfo.To}"); } else { await _accountTransactionManager.Transfer(userFromInfo.User, userFromInfo.Accounts.First(), userToInfo.User, userToInfo.Accounts.First(), depositInfo.AmountKnoks, null, depositInfo.TransactionHash, depositInfo.BlockNumber); } if (_lastCheckedBlockNumber < depositInfo.BlockNumber) _lastCheckedBlockNumber = depositInfo.BlockNumber; } } while (!ct.WaitHandle.WaitOne(GetRemainRefreshDataIntervalMs(cycleBeginTime, _trackerSettings.RefreshDataIntervalMs))); LogInfo("Task_DoWork exited."); } private int GetRemainRefreshDataIntervalMs(DateTime cycleBeginTime, int intervalMs) { var cycleMS = (int)(DateTime.UtcNow - cycleBeginTime).TotalMilliseconds; if (cycleMS < intervalMs) return intervalMs - cycleMS; return 0; } public async Task WaitForCompletion() { LogInfo("Waiting for service to stop.."); try { await Task.WhenAll(_worker); LogInfo($"{_trackerName} stopped. Really."); } catch (AggregateException e) { LogInfo("\nStopping failed: AggregateException thrown with the following inner exceptions:"); // Display information about each exception. foreach (var v in e.InnerExceptions) { if (v is TaskCanceledException) LogInfo(" TaskCanceledException: Task " + ((TaskCanceledException)v).Task.Id); else LogInfo(" Exception: " + v.GetType().Name); } } } public void Dispose() { _cancellationTokenSource?.Dispose(); _worker?.Dispose(); } } }