Knocks/BackEnd/Knoks.TokenContractTracking/TokenContractTracker.cs

223 lines
8.6 KiB
C#

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<TokenContractTracker> _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<TokenContractTracker> 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<EventLog<TransferEventDto>> resultEvents = await _tokenContractService.GetTransferEvents(_lastCheckedBlockNumber + 1);
List<TransferInfo> infos = new List<TransferInfo>();
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();
}
}
}