223 lines
8.6 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|