/// <summary> /// Initializes a new instance of the <see cref="RewardProcessor"/> class. /// </summary> /// <param name="pollPeriod">The poll period.</param> /// <param name="cancelToken">The cancel token.</param> public RewardProcessor(int pollPeriod, CancellationToken cancelToken) { _pollPeriod = pollPeriod; _cancelToken = cancelToken; ExchangeDataContextFactory = new ExchangeDataContextFactory(); CreateRewardTypeWeightedList(); }
public async Task <UserBalanceItemModel> GetBalance(string userId, int currencyId) { try { var currentUser = new Guid(userId); using (var context = ExchangeDataContextFactory.CreateReadOnlyContext()) { var balance = await context.Balance .AsNoTracking() .Where(x => x.UserId == currentUser && x.CurrencyId == currencyId) .Select(x => new UserBalanceItemModel { CurrencyId = x.CurrencyId, HeldForTrades = (decimal?)x.HeldForTrades ?? 0, PendingWithdraw = (decimal?)x.PendingWithdraw ?? 0, Name = x.Currency.Name, Symbol = x.Currency.Symbol, Status = x.Currency.Status, ListingStatus = x.Currency.ListingStatus, StatusMessage = x.Currency.StatusMessage, Total = (decimal?)x.Total ?? 0, Unconfirmed = (decimal?)x.Unconfirmed ?? 0, IsFavorite = (bool?)x.IsFavorite ?? false, CurrencyType = x.Currency.Type, BaseAddress = x.Currency.BaseAddress }).FirstOrDefaultNoLockAsync().ConfigureAwait(false); return(balance); } } catch (Exception) { return(new UserBalanceItemModel()); } }
private async Task ProcessInternal(DateTime startOfMonthToProcess) { Log.Message(LogLevel.Info, $"[Process] - CEFS processing round {startOfMonthToProcess.Date} started."); List <string> transactionIds = new List <string>(); Dictionary <int, decimal> tradeHistoryFeeByCurrencyTotals = new Dictionary <int, decimal>(); Dictionary <int, decimal> paytopiaPaymentFeeByCurrencyTotals = new Dictionary <int, decimal>(); Dictionary <int, decimal> tradeHistoryPortionsOfCurrency = new Dictionary <int, decimal>(); Dictionary <int, decimal> paytopiaPaymentPortionsOfCurrency = new Dictionary <int, decimal>(); using (var context = ExchangeDataContextFactory.CreateContext()) { // get users and balances for each user var userIdsAndBalances = await GetUsersAndTheirBalanacesForCEFPayments(startOfMonthToProcess, context); // if we have some users, do the CEFS processing routine. if (userIdsAndBalances.Any()) { await CalculateFeeTotalsAndPortions(startOfMonthToProcess, tradeHistoryFeeByCurrencyTotals, paytopiaPaymentFeeByCurrencyTotals, tradeHistoryPortionsOfCurrency, paytopiaPaymentPortionsOfCurrency); TransferSumsToSystemStagingUser(transactionIds, tradeHistoryFeeByCurrencyTotals, paytopiaPaymentFeeByCurrencyTotals, context); List <int> Currencies = GetDistinctCurrencyIds(tradeHistoryPortionsOfCurrency, paytopiaPaymentPortionsOfCurrency); List <TransferHistory> transfers = CreateTransferHistoriesForUsersToReceiveCEFSPayments(tradeHistoryPortionsOfCurrency, paytopiaPaymentPortionsOfCurrency, userIdsAndBalances); Log.Message(LogLevel.Info, "Committing CEFS calculations to database."); await SaveTransfersAndAuditAllUserBalances(userIdsAndBalances, transfers, Currencies, context); await PerformWalletTransactionsAndUpdateDepositsWithIds(transactionIds, context); } } Log.Message(LogLevel.Info, "CEFS Processing round complete."); }
private async Task <List <ApiTransactionResult> > GetWithdrawals(ApiUserTransactionsRequest request) { var count = Math.Min(request.Count ?? 100, 1000); var cacheResult = await CacheService.GetOrSetHybridAsync(CacheKey.ApiUserTransactions(request.UserId.ToString(), TransactionType.Withdraw), TimeSpan.FromSeconds(60), async() => { using (var context = ExchangeDataContextFactory.CreateReadOnlyContext()) { return(await context.Withdraw .AsNoTracking() .Where(x => x.UserId == request.UserId) .OrderByDescending(x => x.Id) .Take(1000) .Select(x => new ApiTransactionResult { Amount = x.Amount, Confirmations = x.Confirmations, Currency = x.Currency.Symbol, Fee = 0, Id = x.Id, Status = x.Status.ToString(), Timestamp = x.TimeStamp, TxId = x.Txid, Type = "Withdraw", Address = x.Address }).ToListNoLockAsync().ConfigureAwait(false)); } }).ConfigureAwait(false); return(cacheResult); }
public async Task <IWriterResult> DelistCurrency(string adminUserId, UpdateListingStatusModel model) { model.ListingStatus = CurrencyListingStatus.Delisted; var writerResult = await UpdateListingStatus(adminUserId, model); if (!writerResult.Success) { return(writerResult); } using (var context = ExchangeDataContextFactory.CreateContext()) { // Checks for closing trade pairs only as it's expected to be in 'delisting' before it's delisted. var tradePairs = await context.TradePair.Where(t => t.Status == TradePairStatus.Closing && (t.CurrencyId1 == model.CurrencyId || t.CurrencyId2 == model.CurrencyId)).ToListNoLockAsync(); foreach (var tradePair in tradePairs) { tradePair.Status = TradePairStatus.Closed; } using (var adminContext = DataContextFactory.CreateContext()) { adminContext.LogActivity(adminUserId, $"Delisted Currency: {model.Name}"); await adminContext.SaveChangesAsync().ConfigureAwait(false); } await context.SaveChangesAsync().ConfigureAwait(false); await CacheService.InvalidateAsync(CacheKey.Currencies(), CacheKey.CurrencyInfo(), CacheKey.CurrencyDataTable(), CacheKey.CurrencySummary(model.CurrencyId)).ConfigureAwait(false); } writerResult.Message = "Successfully delisted currency."; return(writerResult); }
public ReferralProcessor(CancellationToken cancelToken) : base(cancelToken) { #if DEBUG _pollPeriod = 1; #endif _cancelToken = cancelToken; DataContextFactory = new DataContextFactory(); ExchangeDataContextFactory = new ExchangeDataContextFactory(); }
public async Task <UserBalanceModel> GetBalances(string userId, bool calculateEstimate) { try { var model = new UserBalanceModel(); var currentUser = new Guid(userId); using (var context = ExchangeDataContextFactory.CreateReadOnlyContext()) { var query = from currency in context.Currency from balance in context.Balance.Where(b => b.UserId == currentUser && b.CurrencyId == currency.Id).DefaultIfEmpty() // from address in context.Address.Where(a => a.UserId == currentUser && a.CurrencyId == currency.Id).DefaultIfEmpty() where currency.IsEnabled orderby currency.Name select new UserBalanceItemModel { //Address = address.AddressHash, CurrencyId = currency.Id, HeldForTrades = (decimal?)balance.HeldForTrades ?? 0, PendingWithdraw = (decimal?)balance.PendingWithdraw ?? 0, Name = currency.Name, Symbol = currency.Symbol, Status = currency.Status, ListingStatus = currency.ListingStatus, StatusMessage = currency.StatusMessage, Total = (decimal?)balance.Total ?? 0, Unconfirmed = (decimal?)balance.Unconfirmed ?? 0, IsFavorite = (bool?)balance.IsFavorite ?? false, CurrencyType = currency.Type, BaseAddress = currency.BaseAddress }; var balances = await query.ToListNoLockAsync().ConfigureAwait(false); model.Balances = balances.DistinctBy(x => x.CurrencyId).ToList(); } if (calculateEstimate) { await model.Balances.ForEachAsync(async b => b.EstimatedBTC = await BalanceEstimationService.GetEstimatedBTC(b.Total, b.CurrencyId)); model.BTCEstimate = model.Balances.Sum(x => x.EstimatedBTC); model.BTCEstimateAlt = model.Balances.Where(x => x.CurrencyId != Constant.BITCOIN_ID).Sum(x => x.EstimatedBTC); var verificationData = await UserVerificationReader.GetVerificationStatus(userId).ConfigureAwait(false); model.HasWithdrawLimit = verificationData.Limit > 0; model.WithdrawLimit = verificationData.Limit; model.WithdrawTotal = verificationData.Current; } return(model); } catch (Exception) { return(new UserBalanceModel()); } }
public NzdtProcessor(CancellationToken cancelToken) : base(cancelToken) { _cancelToken = cancelToken; ExchangeDataContextFactory = new ExchangeDataContextFactory(); _nzdtAssetWalletPort = int.Parse(ConfigurationManager.AppSettings["NzdtAssetWalletPort"]); _nzdtAssetWalletIp = ConfigurationManager.AppSettings["NzdtAssetWalletIp"]; _nzdtAssetWalletUserName = ConfigurationManager.AppSettings["NzdtAssetWalletUserName"]; _nzdtAssetWalletPassword = ConfigurationManager.AppSettings["NzdtAssetWalletPassword"]; if (string.IsNullOrEmpty(_nzdtAssetWalletIp) || string.IsNullOrEmpty(_nzdtAssetWalletUserName) || string.IsNullOrEmpty(_nzdtAssetWalletPassword)) { throw new ArgumentNullException("Nzdt Asset Wallet app.config settings missing"); } }
public async Task <DataTablesResponse> GetAddresses(DataTablesModel model) { using (var context = ExchangeDataContextFactory.CreateReadOnlyContext()) { var query = context.Address .AsNoTracking() .Select(x => new { Id = x.Id, UserName = x.User.UserName, x.AddressHash }).Distinct(); return(await query.GetDataTableResultNoLockAsync(model).ConfigureAwait(false)); } }
public async Task <IWriterResult> AddUserToTransaction(string adminUserId, UpdateNzdtTransactionModel model) { int transactionId; using (var context = ExchangeDataContextFactory.CreateContext()) { var transaction = await context.NzdtTransaction.FirstOrDefaultNoLockAsync(x => x.Id == model.TransactionId).ConfigureAwait(false); if (transaction == null) { return(new WriterResult(false, "Transaction not Found")); } else if (transaction.TransactionStatus != Enums.NzdtTransactionStatus.ErrorUserNotFound) { return(new WriterResult(false, "Transaction not in ErrorUserNotFound Status")); } var user = await context.Users.FirstOrDefaultNoLockAsync(x => x.UserName == model.UserName).ConfigureAwait(false); if (user == null) { return(new WriterResult(false, "User not Found")); } else if (user.VerificationLevel != Enums.VerificationLevel.Level2 && user.VerificationLevel != Enums.VerificationLevel.Level3) { return(new WriterResult(false, "User Verification level is not Level2 or Level3")); } transactionId = transaction.Id; transaction.User = user; transaction.TransactionStatus = Enums.NzdtTransactionStatus.ReadyForProcessing; await context.SaveChangesAsync().ConfigureAwait(false); } using (var context = HubDataContextFactory.CreateContext()) { var logMessage = $"[NZDT Import] Added User To Transaction with Id {transactionId}"; context.LogActivity(adminUserId, logMessage); await context.SaveChangesAsync().ConfigureAwait(false); } return(new WriterResult(true, "User Added to Transaction")); }
public async Task <UpdateWithdrawalTxModel> GetWithdrawalToUpdate(int id) { using (var context = ExchangeDataContextFactory.CreateReadOnlyContext()) { return(await context.Withdraw .AsNoTracking() .Where(x => x.Status == Enums.WithdrawStatus.Processing) .Select(x => new UpdateWithdrawalTxModel { Id = x.Id, TxId = x.Txid, Status = x.Status, Address = x.Address, Amount = x.Amount, RetryCount = x.RetryCount }).FirstOrDefaultNoLockAsync(x => x.Id == id).ConfigureAwait(false)); } }
public async Task <IWriterResult> UpdateCurrency(string adminUserId, UpdateCurrencyModel model) { try { using (var context = ExchangeDataContextFactory.CreateContext()) { var currency = await context.Currency.Where(c => c.Id == model.Id).FirstOrDefaultNoLockAsync().ConfigureAwait(false); if (currency == null) { return(new WriterResult(false, "Currency not found")); } currency.PoolFee = model.PoolFee; currency.TradeFee = model.TradeFee; currency.WithdrawFee = model.WithdrawFee; currency.WithdrawFeeType = model.WithdrawFeeType; currency.MinWithdraw = model.WithdrawMin; currency.MaxWithdraw = model.WithdrawMax; currency.MinTip = model.TipMin; currency.MinBaseTrade = model.MinBaseTrade; currency.MinConfirmations = model.MinConfirmations; currency.Status = model.Status; currency.StatusMessage = model.StatusMessage; currency.ListingStatus = model.ListingStatus; using (var adminContext = DataContextFactory.CreateContext()) { adminContext.LogActivity(adminUserId, $"Updated Currency: {currency.Symbol}"); } await context.SaveChangesAsync().ConfigureAwait(false); await CacheService.InvalidateAsync(CacheKey.Currencies(), CacheKey.CurrencyInfo(), CacheKey.CurrencyDataTable(), CacheKey.CurrencySummary(model.Id)).ConfigureAwait(false); return(new WriterResult(true, "Succesfully updated currency settings.")); } } catch (Exception) { return(null); } }
public async Task <VerificationStatusModel> GetVerificationStatus(string userId) { var currentUser = new Guid(userId); var lastTime = DateTime.UtcNow.AddHours(-24); using (var context = ExchangeDataContextFactory.CreateReadOnlyContext()) { var user = await context.Users.FirstOrDefaultNoLockAsync(x => x.Id == currentUser); if (user == null) { return(null); } var current = 0m; if (user.VerificationLevel != VerificationLevel.Legacy) { var currentWithdrawDetails = await context.Withdraw .Where(x => x.UserId == currentUser && x.Status != Enums.WithdrawStatus.Canceled && x.TimeStamp > lastTime) .ToListNoLockAsync(); if (currentWithdrawDetails.Any()) { current = currentWithdrawDetails.Sum(x => x.EstimatedPrice); } var currentTransferDetails = await context.Transfer .Where(x => x.UserId == currentUser && x.Timestamp > lastTime && (x.TransferType == TransferType.User || x.TransferType == TransferType.Tip)) .ToListNoLockAsync(); if (currentTransferDetails.Any()) { current += currentTransferDetails.Sum(x => x.EstimatedPrice); } } return(new VerificationStatusModel { Level = user.VerificationLevel, Limit = VerificationLimit(user.VerificationLevel), Current = current }); } }
public async Task <DataTablesResponse> GetTransfers(DataTablesModel model) { using (var context = ExchangeDataContextFactory.CreateReadOnlyContext()) { var query = context.Transfer .AsNoTracking() .Select(x => new { Id = x.Id, Sender = x.User.UserName, Receiver = x.ToUser.UserName, Amount = x.Amount, Type = x.TransferType, Timestamp = x.Timestamp }); return(await query.GetDataTableResultNoLockAsync(model).ConfigureAwait(false)); } }
public async Task <DataTablesResponse> GetIncompleteWithdrawals(DataTablesModel model) { var newest = DateTime.Now.AddHours(-2.0); List <int> pendingApprovalIds = null; using (var approvalContext = DataContextFactory.CreateReadOnlyContext()) { var approvals = await approvalContext.ApprovalQueue .Where(a => a.Type == ApprovalQueueType.WithdrawalReprocessing && a.Status == ApprovalQueueStatus.Pending) .Select(a => a) .ToListNoLockAsync().ConfigureAwait(false); pendingApprovalIds = approvals.Select(a => { int id; bool success = int.TryParse(JsonConvert.DeserializeObject <ReprocessingApprovalDataModel>(a.Data).WithdrawalId, out id); return(new { success, id }); }) .Where(x => x.success) .Select(x => x.id).ToList(); } using (var context = ExchangeDataContextFactory.CreateReadOnlyContext()) { var query = context.Withdraw .AsNoTracking() .Where(w => w.Status == Enums.WithdrawStatus.Processing && w.Confirmed <= newest && !pendingApprovalIds.Contains(w.Id)) .Select(x => new { Id = x.Id, UserName = x.User.UserName, Currency = x.Currency.Symbol, Amount = x.Amount, Address = x.Address, Confirmed = x.Confirmed, RetryCount = x.RetryCount }); var result = await query.GetDataTableResultNoLockAsync(model).ConfigureAwait(false); return(result); } }
public async Task <DataTablesResponse> GetDeposits(DataTablesModel model) { using (var context = ExchangeDataContextFactory.CreateReadOnlyContext()) { var query = context.Deposit .AsNoTracking() .Select(x => new { Id = x.Id, UserName = x.User.UserName, Amount = x.Amount, Status = x.Status, Type = x.Type.ToString(), TxId = x.Txid, Conf = x.Confirmations, Timestamp = x.TimeStamp }); return(await query.GetDataTableResultNoLockAsync(model).ConfigureAwait(false)); } }
public async Task <DataTablesResponse> GetWithdrawals(DataTablesModel model) { using (var context = ExchangeDataContextFactory.CreateReadOnlyContext()) { var query = context.Withdraw .AsNoTracking() .Select(x => new { Id = x.Id, UserName = x.User.UserName, Amount = x.Amount, Status = x.Status, Confirmed = x.Confirmed, TxId = x.Txid, Address = x.Address, Conf = x.Confirmations, Timestamp = x.TimeStamp, Init = x.IsApi ? "API" : "UI" }); return(await query.GetDataTableResultNoLockAsync(model).ConfigureAwait(false)); } }
private async Task <IWriterResult> UpdateListingStatus(string adminUserId, UpdateListingStatusModel model) { try { using (var context = ExchangeDataContextFactory.CreateContext()) { var currency = await context.Currency.Where(c => c.Id == model.CurrencyId).FirstOrDefaultNoLockAsync().ConfigureAwait(false); if (currency == null) { return(new WriterResult(false, "Currency not found")); } var oldStatus = currency.ListingStatus; currency.StatusMessage = model.StatusMessage; currency.ListingStatus = model.ListingStatus; currency.Settings.DelistOn = model.DelistOn; using (var adminContext = DataContextFactory.CreateContext()) { adminContext.LogActivity(adminUserId, $"Updated Currency listing status from : {oldStatus} to: {model.ListingStatus}"); await adminContext.SaveChangesAsync().ConfigureAwait(false); } await context.SaveChangesAsync().ConfigureAwait(false); await CacheService.InvalidateAsync(CacheKey.Currencies(), CacheKey.CurrencyInfo(), CacheKey.CurrencyDataTable(), CacheKey.CurrencySummary(model.CurrencyId)).ConfigureAwait(false); return(new WriterResult(true, "Succesfully updated listing status.")); } } catch (Exception) { return(null); } }
public async Task <DataTablesResponse> GetWalletTransactions(WalletTxRequestModel model, DataTablesModel tableModel) { var cacheResult = await CacheService.GetOrSetMemoryAsync(CacheKey.WalletTransactions(model.Currency), TimeSpan.FromMinutes(10), async() => { int currencyId = -1; using (var context = ExchangeDataContextFactory.CreateReadOnlyContext()) { Entity.Currency selectedCurrency = await context.Currency.FirstOrDefaultNoLockAsync(c => c.Symbol.Equals(model.Currency)); if (selectedCurrency != null) { currencyId = selectedCurrency.Id; } } if (currencyId == -1) { return(null); } List <WalletTransaction> transactions = new List <WalletTransaction>(); using (var service = new AdmintopiaServiceClient()) transactions = await service.GetWalletTransactionsSinceAsync(AdmintopiaService.TransactionDataType.Withdraw, currencyId, WalletTimeoutMinutes, model.BlockLength); return(transactions.Select(x => new { Type = x.Type, Amount = x.Amount, Txid = x.Txid, Address = x.Address })); }); return(cacheResult.GetDataTableResult(tableModel, true)); }
public async Task ProcessExchanges() { using (var context = ExchangeDataContextFactory.CreateContext()) { Log.Message(LogLevel.Info, "[ProcessExchanges] - Creating tradepair map..."); var tradePairs = await context.TradePair .Where(x => x.Status == TradePairStatus.OK || x.Status == TradePairStatus.Paused) .Select(t => new { TradePairId = t.Id, Symbol = t.Currency1.Symbol, BaseSymbol = t.Currency2.Symbol }).ToListNoLockAsync(); if (tradePairs.IsNullOrEmpty()) { return; } var tradePairMap = tradePairs.ToDictionary(k => string.Format("{0}_{1}", k.Symbol, k.BaseSymbol), v => v.TradePairId); Log.Message(LogLevel.Info, "[ProcessExchanges] - Tradepair map created."); Log.Message(LogLevel.Info, "[ProcessExchanges] - Processing exchanges..."); foreach (var exchange in _exchanges) { if (!_isEnabled) { break; } try { Log.Message(LogLevel.Info, "[ProcessExchanges] - Processing exchange. Exchange: {0}", exchange.Name); var existingData = await context.IntegrationMarketData.Where(x => x.IntegrationExchangeId == exchange.Id).ToListNoLockAsync(); var marketData = await exchange.GetMarketData(tradePairMap); if (!marketData.IsNullOrEmpty()) { Log.Message(LogLevel.Info, "[ProcessExchanges] - Market data found, Updating database"); foreach (var data in marketData) { if (!_isEnabled) { break; } if (tradePairMap.ContainsKey(data.TradePair)) { var existing = existingData.FirstOrDefault(x => x.TradePairId == tradePairMap[data.TradePair]); if (existing == null) { existing = new IntegrationMarketData { IntegrationExchangeId = exchange.Id, TradePairId = tradePairMap[data.TradePair] }; context.IntegrationMarketData.Add(existing); } existing.Timestamp = DateTime.UtcNow; existing.Ask = data.Ask; existing.Bid = data.Bid; existing.Last = data.Last; existing.Volume = data.Volume; existing.BaseVolume = data.BaseVolume; existing.MarketUrl = data.MarketUrl; } } await context.SaveChangesAsync(); Log.Message(LogLevel.Info, "[ProcessExchanges] - Market data updated."); } var expiredData = existingData.Where(x => x.Timestamp.AddMinutes(_expirePeriod) < DateTime.UtcNow); if (expiredData.Any()) { Log.Message(LogLevel.Info, "[ProcessExchanges] - Expired data found, Deleteing..."); foreach (var expired in expiredData) { if (!_isEnabled) { break; } context.IntegrationMarketData.Remove(expired); } await context.SaveChangesAsync(); Log.Message(LogLevel.Info, "[ProcessExchanges] - Expired data deleted."); } } catch (Exception ex) { Log.Exception("[ProcessExchanges] - An exception occured processing exchange, Exchange: {0}", ex, exchange.Name); } } Log.Message(LogLevel.Info, "[ProcessExchanges] - Processing exchanges complete."); } }
/// <summary> /// Processes the lotto items. /// </summary> private async Task ProcessLotto() { try { var payouts = new List <PrizePayout>(); using (var context = ExchangeDataContextFactory.CreateContext()) { Log.Message(LogLevel.Info, "[ProcessLotto] - Processing LottoItems..."); var dueLottoDraws = await context.LottoItem.Where(x => x.Status != LottoItemStatus.Disabled && DateTime.UtcNow >= x.NextDraw).ToListNoLockAsync(); if (dueLottoDraws.IsNullOrEmpty()) { // no draws ready Log.Message(LogLevel.Info, "[ProcessLotto] - No lotto draws are due, Waiting..."); return; } Log.Message(LogLevel.Info, "[ProcessLotto] - {0} lotto draws found.", dueLottoDraws.Count()); foreach (var lottoDraw in dueLottoDraws) { Log.Message(LogLevel.Info, "[ProcessLotto] - Processing LottoItem, Id: {0}, Name: {1}, Currency: {2}", lottoDraw.Id, lottoDraw.Name, lottoDraw.CurrencyId); // 1. sum up total tickets, subtract site fee var tickets = await context.LottoTicket.Where(x => !x.IsArchived && x.LottoItemId == lottoDraw.Id && x.DrawId == lottoDraw.CurrentDrawId).ToListNoLockAsync(); if (!tickets.IsNullOrEmpty()) { Log.Message(LogLevel.Info, "[ProcessLotto] - {0} tickets found for draw.", tickets.Count()); // Archive all the tickets foreach (var ticket in tickets) { ticket.IsArchived = true; } // Calculate prize pool var totalAmount = lottoDraw.Rate * tickets.Count(); var siteFee = (totalAmount / 100m) * lottoDraw.Fee; var prizePool = totalAmount - siteFee; var prizePoolFraction = prizePool / 100m; var prizeWeights = LottoHelpers.GetPrizeWeights(lottoDraw.Prizes); var winningTicketIds = GetWinningLottoItemIds(tickets.Select(x => x.Id), lottoDraw.Prizes); Log.Message(LogLevel.Info, "[ProcessLotto] - LottoItem draw info, Total: {0}, PrizePool: {1}, Fee: {2}", totalAmount, prizePool, siteFee); // Calculate user prizes var drawTime = lottoDraw.NextDraw; for (int i = 0; i < prizeWeights.Count; i++) { var prizeWeight = prizeWeights[i]; var winningTicket = tickets.FirstOrDefault(x => x.Id == winningTicketIds[i]); var amount = Math.Round(prizePoolFraction * prizeWeight, 8); payouts.Add(new PrizePayout { UserId = winningTicket.UserId, CurrencyId = lottoDraw.CurrencyId, Amount = amount }); context.LottoHistory.Add(new LottoHistory { Amount = amount, Percent = prizeWeight, LottoItemId = lottoDraw.Id, Position = (i + 1), Timestamp = drawTime, UserId = winningTicket.UserId, LottoTicketId = winningTicket.Id, LottoDrawId = lottoDraw.CurrentDrawId, TotalAmount = prizePool }); Log.Message(LogLevel.Info, "[ProcessLotto] - User payout info, UserId: {0}, Position: {1}, Weight: {2}, Amount: {3}", winningTicket.UserId, (i + 1), prizeWeight, amount); } } else { Log.Message(LogLevel.Info, "[ProcessLotto] - No tickets found for draw."); } // 5. update LottoItem lottoDraw.Status = LottoItemStatus.Finished; if (lottoDraw.LottoType == LottoType.Recurring || (lottoDraw.LottoType == LottoType.RecurringExpire && lottoDraw.Expires > DateTime.UtcNow)) { lottoDraw.Status = LottoItemStatus.Active; lottoDraw.CurrentDrawId = lottoDraw.CurrentDrawId + 1; lottoDraw.NextDraw = lottoDraw.NextDraw.AddHours(lottoDraw.Hours); Log.Message(LogLevel.Info, "[ProcessLotto] - Set next draw date for recurring lotto, NextDraw: {0}", lottoDraw.NextDraw); } Log.Message(LogLevel.Info, "[ProcessLotto] - Processing LottoItem complete."); } // commit the transaction Log.Message(LogLevel.Debug, "[ProcessLotto] - Comitting database transaction..."); await context.SaveChangesAsync(); Log.Message(LogLevel.Debug, "[ProcessLotto] - Comitted database transaction."); } // Send Winnings await PayoutUsers(payouts); Log.Message(LogLevel.Info, "[ProcessLotto] - Processing LottoItems complete."); } catch (Exception ex) { Log.Exception("[ProcessLotto] - An exception occured processing LottoItem.", ex); } }
/// <summary> /// Initializes a new instance of the <see cref="LottoProcessor"/> class. /// </summary> /// <param name="cancelToken">The cancel token.</param> public LottoProcessor(CancellationToken cancelToken) { _cancelToken = cancelToken; ExchangeDataContextFactory = new ExchangeDataContextFactory(); }
private async Task ProcessPaytopiaItems() { try { var now = DateTime.UtcNow; var beginingOfWeek = now.StartOfWeek(DayOfWeek.Monday); var endOfWeek = beginingOfWeek.AddDays(7); var featuredItems = new List <FeaturedInfo>(); using (var context = DataContextFactory.CreateContext()) { featuredItems = await context.PaytopiaPayments .Where(x => x.Begins >= beginingOfWeek && x.Ends <= endOfWeek && (x.PaytopiaItem.Type == PaytopiaItemType.FeaturedCurrency || x.PaytopiaItem.Type == PaytopiaItemType.FeaturedPool)) .Select(x => new FeaturedInfo { Id = x.ReferenceId, Type = x.PaytopiaItem.Type, ExpireTime = x.Ends }).ToListNoLockAsync(); } if (!featuredItems.Any()) { return; } // update currencies using (var context = ExchangeDataContextFactory.CreateContext()) { var currencyIds = featuredItems.Where(x => x.Type == PaytopiaItemType.FeaturedCurrency).Select(x => x.Id).ToList(); var currencies = await context.Currency.Where(x => currencyIds.Contains(x.Id)).ToListNoLockAsync(); foreach (var currency in currencies) { var featureExpireTime = featuredItems.First(x => x.Type == PaytopiaItemType.FeaturedCurrency && x.Id == currency.Id).ExpireTime; if (featureExpireTime != currency.FeaturedExpires) { currency.FeaturedExpires = featureExpireTime; } } await context.SaveChangesAsync(); } // update pools using (var context = PoolDataContextFactory.CreateContext()) { var poolIds = featuredItems.Where(x => x.Type == PaytopiaItemType.FeaturedPool).Select(x => x.Id).ToList(); var pools = await context.Pool.Where(x => poolIds.Contains(x.Id)).ToListNoLockAsync(); foreach (var pool in pools) { var featureExpireTime = featuredItems.First(x => x.Type == PaytopiaItemType.FeaturedPool && x.Id == pool.Id).ExpireTime; if (featureExpireTime != pool.FeaturedExpires) { pool.FeaturedExpires = featureExpireTime; } } await context.SaveChangesAsync(); } } catch (Exception ex) { Log.Exception("[ProcessPaytopiaItems] - An exception occurred processing Paytopia items.", ex); } }
public async Task CalculateFeeTotalsAndPortions(DateTime startOfMonthToProcess, Dictionary <int, decimal> tradeHistoryFeeByCurrencyTotals, Dictionary <int, decimal> paytopiaPaymentFeeByCurrencyTotals, Dictionary <int, decimal> tradeHistoryPortionsOfCurrency, Dictionary <int, decimal> paytopiaPaymentPortionsOfCurrency) { Log.Message(LogLevel.Info, "CalculateFeeTotalsAndPortions entered."); List <string> auditMessages = new List <string>(); auditMessages.Add($"CEFS Process. Calculating total fees and portion sizes for CEFS holders."); // get trade histories from the start of last month to the start of this month // (i.e. 1 Oct (incl) - 1 Nov (excl)). var startOfFollowingMonth = startOfMonthToProcess.AddMonths(1).Date; using (var context = ExchangeDataContextFactory.CreateReadOnlyContext()) { var tradeHistoryFeesByCurrency = await(from h in context.TradeHistory join tp in context.TradePair on h.TradePairId equals tp.Id join c in context.Currency on tp.CurrencyId2 equals c.Id where h.Timestamp < startOfFollowingMonth && h.Timestamp >= startOfMonthToProcess group h.Fee by c.Id into g select new { CurrencyId = g.Key, TotalFees = g.Sum() * 2 }).ToListAsync(); // multiplied by two to account for both sides of the trade foreach (var history in tradeHistoryFeesByCurrency) { var tradeHistoryPortionSize = (history.TotalFees * FOUR_POINT_FIVE_PERCENT) / TOTAL_PORTIONS; tradeHistoryPortionsOfCurrency.Add(history.CurrencyId, tradeHistoryPortionSize); tradeHistoryFeeByCurrencyTotals.Add(history.CurrencyId, history.TotalFees); // add auditing to be able to check if there is anything wrong after deployment. auditMessages.Add($"Trade History Total for {history.CurrencyId}: {history.TotalFees}"); auditMessages.Add($"Trade History portion size for {history.CurrencyId}: {tradeHistoryPortionSize}"); } } using (var hubContext = DataContextFactory.CreateContext()) { var paytopiaPaymentsByCurrency = await(from p in hubContext.PaytopiaPayments join pi in hubContext.PaytopiaItems on p.PaytopiaItemId equals pi.Id where p.Timestamp < startOfFollowingMonth && p.Timestamp >= startOfMonthToProcess && p.Status == PaytopiaPaymentStatus.Complete && pi.CurrencyId == 2 group pi.Price by pi.CurrencyId into g select new { CurrencyId = g.Key, TotalPayments = g.Sum() }).ToListAsync(); var paytopiaRefundsByCurrency = await(from p in hubContext.PaytopiaPayments join pi in hubContext.PaytopiaItems on p.PaytopiaItemId equals pi.Id where p.Timestamp < startOfFollowingMonth && p.Timestamp >= startOfMonthToProcess && p.Status == PaytopiaPaymentStatus.Refunded && pi.CurrencyId == 2 group pi.Price by pi.CurrencyId into g select new { CurrencyId = g.Key, TotalRefunds = g.Sum() }).ToListAsync(); foreach (var payment in paytopiaPaymentsByCurrency) { var refund = paytopiaRefundsByCurrency.FirstOrDefault(x => x.CurrencyId == payment.CurrencyId); decimal total = 0; if (refund == null) { total = payment.TotalPayments; } else { total = payment.TotalPayments - refund.TotalRefunds; } var portionSize = (total * FOUR_POINT_FIVE_PERCENT) / TOTAL_PORTIONS; paytopiaPaymentPortionsOfCurrency.Add(payment.CurrencyId, portionSize); paytopiaPaymentFeeByCurrencyTotals.Add(payment.CurrencyId, total); auditMessages.Add($"Paytopia Payments Total for {payment.CurrencyId}: {payment.TotalPayments}"); if (refund != null) { auditMessages.Add($"Paytopia Payment refunds total for {payment.CurrencyId}: {refund.TotalRefunds}"); } auditMessages.Add($"Paytopia Payment portion size for {payment.CurrencyId}: {portionSize}"); } auditMessages.Add($"CEFS Process. Calculation complete."); foreach (var message in auditMessages) { hubContext.LogActivity(Constant.SYSTEM_USER_CEFS.ToString(), message); } await hubContext.SaveChangesAsync(); } Log.Message(LogLevel.Info, "CalculateFeeTotalsAndPortions exited."); }
public CEFSProcessor(CancellationToken cancelToken) : base(cancelToken) { _token = cancelToken; DataContextFactory = new DataContextFactory(); ExchangeDataContextFactory = new ExchangeDataContextFactory(); }
private async Task ProcessTransactions() { Log.Message(LogLevel.Info, "[ProcessNZDT] - Processing NZDT Transactions..."); List <NzdtTransaction> nzdtTransactions; using (var context = ExchangeDataContextFactory.CreateContext()) { nzdtTransactions = await context.NzdtTransaction .Where(x => x.TransactionStatus == NzdtTransactionStatus.ReadyForProcessing) .Where(x => DbFunctions.AddHours(x.CreatedOn, 1) <= DateTime.UtcNow) .ToListNoLockAsync(); Log.Message(LogLevel.Info, $"[ProcessNZDT] - {nzdtTransactions.Count()} transactions found, processing..."); foreach (var transaction in nzdtTransactions) { transaction.TransactionStatus = NzdtTransactionStatus.Processed; } await context.SaveChangesAsync(); } var wallet = new WalletConnector(_nzdtAssetWalletIp, _nzdtAssetWalletPort, _nzdtAssetWalletUserName, _nzdtAssetWalletPassword, 30000); foreach (var transaction in nzdtTransactions) { try { var sendResult = await wallet.SendToAddressAsync(Constant.NzdtBaseExchangeAddress, transaction.Amount); using (var context = ExchangeDataContextFactory.CreateContext()) { var deposit = new Deposit { Txid = string.IsNullOrEmpty(sendResult?.Txid) ? $"{transaction.Id}" : sendResult.Txid, Amount = transaction.Amount, TimeStamp = DateTime.UtcNow, CurrencyId = Constant.NZDT_ID, Status = DepositStatus.Confirmed, Confirmations = 20, UserId = transaction.UserId.Value, Type = DepositType.Normal }; var tx = await context.NzdtTransaction.FirstNoLockAsync(x => x.Id == transaction.Id); tx.Deposit = deposit; await context.SaveChangesAsync(); await context.AuditUserBalance(transaction.UserId.Value, Constant.NZDT_ID); await context.SaveChangesAsync(); } } catch (Exception ex) { Log.Exception($"[ProcessNZDT] Insert Deposit failed for transaction {transaction.Id}", ex); } } Log.Message(LogLevel.Info, $"[ProcessNZDT] - Processing NZDT Transactions complete."); }
public async Task <ApiSubmitUserWithdrawResponse> SubmitUserWithdraw(ApiSubmitUserWithdrawRequest request) { var currency = request.CurrencyId.HasValue ? await CurrencyReader.GetCurrency(request.CurrencyId.Value).ConfigureAwait(false) : await CurrencyReader.GetCurrency(request.Currency).ConfigureAwait(false); if (currency == null) { return new ApiSubmitUserWithdrawResponse { Success = false, Error = "Currency not found." } } ; if (currency.Status == CurrencyStatus.Maintenance || currency.Status == CurrencyStatus.Offline || currency.Status == CurrencyStatus.NoConnections) { return new ApiSubmitUserWithdrawResponse { Success = false, Error = $"Currency is currently not available for withdraw, Status: {currency.Status}" } } ; if (request.Amount < currency.WithdrawMin) { return new ApiSubmitUserWithdrawResponse { Success = false, Error = $"Withdraw amount is below the minimum, Minimum: {currency.WithdrawMin:F8} {currency.Symbol}" } } ; if (request.Amount > currency.WithdrawMax) { return new ApiSubmitUserWithdrawResponse { Success = false, Error = $"Withdraw amount is above the maximum, Maximum: {currency.WithdrawMax:F8} {currency.Symbol}" } } ; var balance = await UserBalanceReader.GetBalance(request.UserId.ToString(), currency.CurrencyId).ConfigureAwait(false); if (balance == null || request.Amount > balance.Available) { return new ApiSubmitUserWithdrawResponse { Success = false, Error = "Insufficient funds." } } ; var user = await UserReader.GetUserById(request.UserId.ToString()).ConfigureAwait(false); if (user == null || !user.IsApiWithdrawEnabled) { return new ApiSubmitUserWithdrawResponse { Success = false, Error = "Your API withdraw setting is currently disabled." } } ; var address = request.Address; if (currency.AddressType != AddressType.Standard) { address = $"{request.Address}:{request.PaymentId ?? string.Empty}"; } if (currency.Type == CurrencyType.Fiat) { address = address.TrimEnd(':'); } if (!user.IsApiUnsafeWithdrawEnabled) { using (var context = ExchangeDataContextFactory.CreateReadOnlyContext()) { var validAddress = await context.AddressBook .AsNoTracking() .FirstOrDefaultAsync(x => x.UserId == request.UserId && x.CurrencyId == currency.CurrencyId && x.Address == address && x.IsEnabled).ConfigureAwait(false); if (validAddress == null) { return new ApiSubmitUserWithdrawResponse { Success = false, Error = $"Address does not exist in your secure Withdraw Address Book" } } ; } } else { if (currency.CurrencyId == Constant.NZDT_ID && !UserVerificationReader.IsVerified(user.VerificationLevel)) { return(new ApiSubmitUserWithdrawResponse { Success = false, Error = $"Id verification required for NZDT services." }); } if (!await DepositService.ValidateAddress(currency.CurrencyId, address)) { return new ApiSubmitUserWithdrawResponse { Success = false, Error = $"Invalid {currency.Symbol} address." } } ; } var response = await TradeService.CreateWithdraw(request.UserId.ToString(), new CreateWithdrawModel { Address = address, Amount = Math.Max(0, request.Amount), CurrencyId = balance.CurrencyId, TwoFactorToken = string.Empty, Type = WithdrawType.Normal }, true).ConfigureAwait(false); if (response.IsError) { return new ApiSubmitUserWithdrawResponse { Success = false, Error = response.Error } } ; return(new ApiSubmitUserWithdrawResponse { Success = true, Data = response.WithdrawId }); }
public async Task <IServiceResult <NzdtUploadResultModel> > ValidateAndUpload(string adminUserId, Stream inputStream) { var processResult = await ProcessCsv(inputStream); if (!processResult.Success || !processResult.HasResult) { return(new ServiceResult <NzdtUploadResultModel>(false, processResult.Message)); } var csvTransactionItems = processResult.Result; var firstDate = csvTransactionItems.Min(x => x.Date); var resultModel = new NzdtUploadResultModel { TotalCount = csvTransactionItems.Count }; List <NzdtTransaction> transactionsToUpload = new List <NzdtTransaction>(); long firstUploadedTransactionUniqueId = 0; long lastUploadedTransactionUniqueId = 0; using (var context = ExchangeDataContextFactory.CreateContext()) { var transactions = await context.NzdtTransaction .Where(t => t.Date >= firstDate) .ToListNoLockAsync().ConfigureAwait(false); var addressProjections = await context.Address .Where(a => a.CurrencyId == Constant.NZDT_ID) .Select(a => new AddressProjection { UserId = a.UserId, UserName = a.User.UserName, VerificationLevel = a.User.VerificationLevel, AddressHash = a.AddressHash, }).ToListNoLockAsync().ConfigureAwait(false); foreach (var item in csvTransactionItems) { var matchingTransaction = transactions.FirstOrDefault(x => x.UniqueId == item.UniqueId); var matchingAddress = addressProjections.FirstOrDefault(x => item.Memo.ToUpper().Contains(x.Address)); if (matchingTransaction != null) { //Transaction Exists resultModel.ExistingCount++; continue; } if (matchingAddress != null) { var isVerificationValid = (matchingAddress.VerificationLevel == VerificationLevel.Level2 || matchingAddress.VerificationLevel == VerificationLevel.Level3); if (isVerificationValid) { transactionsToUpload.Add(new NzdtTransaction { TransactionStatus = NzdtTransactionStatus.ReadyForProcessing, UserId = matchingAddress.UserId, Date = item.Date, UniqueId = item.UniqueId, TranType = item.TranType, ChequeNumber = item.ChequeNumber, Payee = item.Payee, Memo = item.Memo, Amount = item.Amount, CreatedOn = DateTime.UtcNow }); resultModel.ReadyForProcessingCount++; } else { transactionsToUpload.Add(new NzdtTransaction { TransactionStatus = NzdtTransactionStatus.ErrorUserNotVerified, UserId = matchingAddress.UserId, Date = item.Date, UniqueId = item.UniqueId, TranType = item.TranType, ChequeNumber = item.ChequeNumber, Payee = item.Payee, Memo = item.Memo, Amount = item.Amount, CreatedOn = DateTime.UtcNow }); resultModel.ErroredCount++; } } else { transactionsToUpload.Add(new NzdtTransaction { TransactionStatus = NzdtTransactionStatus.ErrorUserNotFound, Date = item.Date, UniqueId = item.UniqueId, TranType = item.TranType, ChequeNumber = item.ChequeNumber, Payee = item.Payee, Memo = item.Memo, Amount = item.Amount, CreatedOn = DateTime.UtcNow }); resultModel.ErroredCount++; } } if (resultModel.TotalCount != (resultModel.ExistingCount + resultModel.ReadyForProcessingCount + resultModel.ErroredCount)) { return(new ServiceResult <NzdtUploadResultModel>(false, "Error. Processed Transaction Count was out from Total Count")); } if (transactionsToUpload.Any()) { firstUploadedTransactionUniqueId = transactionsToUpload.First().UniqueId; lastUploadedTransactionUniqueId = transactionsToUpload.Last().UniqueId; } context.NzdtTransaction.AddRange(transactionsToUpload); await context.SaveChangesAsync().ConfigureAwait(false); } using (var context = HubDataContextFactory.CreateContext()) { var logMessage = $"[NZDT Import] Imported {resultModel.ReadyForProcessingCount}. "; if (resultModel.ReadyForProcessingCount > 0) { logMessage += $"UniqueIds between {firstUploadedTransactionUniqueId} and {lastUploadedTransactionUniqueId}"; } context.LogActivity(adminUserId, logMessage); await context.SaveChangesAsync().ConfigureAwait(false); } return(new ServiceResult <NzdtUploadResultModel>(true, resultModel)); }
public async Task <PaytopiaPaymentModel> GetPayment(int id) { PaytopiaPaymentModel item; string currencySymbol; dynamic refCurrency; Entity.Pool refPool; using (var context = DataContextFactory.CreateReadOnlyContext()) { item = await context.PaytopiaPayments .AsNoTracking() .Where(x => x.Id == id) .Select(payment => new PaytopiaPaymentModel { Id = payment.Id, Type = payment.PaytopiaItem.Type, CurrencyId = payment.PaytopiaItem.CurrencyId, Amount = payment.Amount, Status = payment.Status, UserName = payment.User.UserName, IsAnonymous = payment.IsAnonymous, Begins = payment.Begins, Ends = payment.Ends, Timestamp = payment.Timestamp, TransferId = payment.TransferId, RefundId = payment.RefundId, ReferenceCode = payment.ReferenceCode, ReferenceId = payment.ReferenceId, RefundReason = payment.RefundReason, RequestData = payment.RequestData, }).FirstOrDefaultNoLockAsync().ConfigureAwait(false); } using (var exchangeContext = ExchangeDataContextFactory.CreateReadOnlyContext()) { currencySymbol = await exchangeContext.Currency.Where(c => c.Id == item.CurrencyId).Select(c => c.Symbol).FirstOrDefaultNoLockAsync(); refCurrency = await exchangeContext.Currency.Where(c => c.Id == item.ReferenceId).Select(c => new { Name = c.Name, AlgoType = c.Info.AlgoType, Symbol = c.Symbol }).FirstOrDefaultNoLockAsync(); } using (var poolContext = PoolDataContextFactory.CreateContext()) { refPool = await poolContext.Pool.Where(p => p.IsEnabled && p.Id == item.ReferenceId).FirstOrDefaultNoLockAsync(); } item.Symbol = currencySymbol; if (item.ReferenceId > 0) { if (item.Type == PaytopiaItemType.FeaturedCurrency || item.Type == PaytopiaItemType.LottoSlot || item.Type == PaytopiaItemType.RewardSlot || item.Type == PaytopiaItemType.TipSlot) { if (refCurrency != null) { item.ReferenceName = refCurrency.Name; item.ReferenceAlgo = refCurrency.AlgoType; item.ReferenceSymbol = refCurrency.Symbol; } } else if (item.Type == PaytopiaItemType.FeaturedPool || item.Type == PaytopiaItemType.PoolListing) { if (refPool != null) { item.ReferenceName = refPool.Name; item.ReferenceAlgo = refPool.AlgoType; item.ReferenceSymbol = refPool.Symbol; } } } return(item); }
public async Task <IWriterResult> BeginDelistingCurrency(string adminUserId, UpdateListingStatusModel model) { model.ListingStatus = CurrencyListingStatus.Delisting; if (model.DelistOn == null) { return(new WriterResult(false, "Delist date cannot be null")); } var writerResult = await UpdateListingStatus(adminUserId, model); if (!writerResult.Success) { return(writerResult); } using (var context = ExchangeDataContextFactory.CreateContext()) { // Checks for open trade pairs only to avoid accidentally relisting delisted currencies. var tradePairs = await context.TradePair.Where(t => (t.Status == TradePairStatus.OK || t.Status == TradePairStatus.Paused) && (t.CurrencyId1 == model.CurrencyId || t.CurrencyId2 == model.CurrencyId)).ToListNoLockAsync(); foreach (var tradePair in tradePairs) { tradePair.Status = TradePairStatus.Closing; } using (var adminContext = DataContextFactory.CreateContext()) { adminContext.LogActivity(adminUserId, "Closing TradePair due to delisting"); await adminContext.SaveChangesAsync().ConfigureAwait(false); } await context.SaveChangesAsync().ConfigureAwait(false); await CacheService.InvalidateAsync(CacheKey.AllTradePairs(), CacheKey.TradePairs()).ConfigureAwait(false); } writerResult.Message = "Successfully started the delisting process."; try { var email = new EmailMessageModel { EmailType = EmailTemplateType.CoinDelisting, BodyParameters = new object[0], SystemIdentifier = model.Symbol }; using (var context = ExchangeDataContextFactory.CreateReadOnlyContext()) { var balances = await context.Balance.Include(b => b.User).Where(b => b.Currency.Name == model.Name && b.Total > 0).OrderBy(b => b.User.Email).ToListNoLockAsync(); if (!balances.Any()) { writerResult.Message = "Successfully started the delisting process but no non zero balances found so no emails were sent."; return(writerResult); } var personalisations = new List <IEmailPersonalisation>(balances.Select(balance => new EmailPersonalisation { Tos = new List <string> { balance.User.Email }, Substitutions = new Dictionary <string, string> { { "-name-", balance.User.UserName }, { "-coinName-", model.Name }, { "-coinSymbol-", model.Symbol }, { "-delistDate-", model.DelistOn.Value.ToString("R") }, { "-delistReason-", model.StatusMessage }, { "-balance-", balance.Total.ToString(CultureInfo.InvariantCulture) } } }) .ToList()); var hasSentSuccessfully = await EmailService.SendEmails(email, personalisations); if (!hasSentSuccessfully) { writerResult.Success = false; writerResult.Message = "Successfully started the delisting process but emails failed to send."; return(writerResult); } } using (var adminContext = DataContextFactory.CreateContext()) { adminContext.LogActivity(adminUserId, $"Started Currency Delisting Process for: {model.Name}"); await adminContext.SaveChangesAsync().ConfigureAwait(false); } } catch (Exception) { return(new WriterResult(false, "Sending emails failed")); } return(writerResult); }