private async Task ProcessCashoutsAsync() { _cancellationTokenSource = new CancellationTokenSource(); while (!_cancellationTokenSource.IsCancellationRequested) { _lastCursor = await _lastCursorRepository.GetAsync(_brokerAccountId); var assets = await _assetsService.GetAllAssetsAsync(false, _cancellationTokenSource.Token); var streamId = Guid.NewGuid().ToString(); try { var request = new WithdrawalUpdateSearchRequest { StreamId = streamId, BrokerAccountId = _brokerAccountId, Cursor = _lastCursor }; _log.Info("Getting updates...", context: new { StreamId = request.StreamId, BrokerAccountId = request.BrokerAccountId, Cursor = request.Cursor, }); var updates = _apiClient.Withdrawals.GetUpdates(request); while (await updates.ResponseStream.MoveNext(_cancellationTokenSource.Token).ConfigureAwait(false)) { WithdrawalUpdateArrayResponse update = updates.ResponseStream.Current; if (!update.Items.Any()) { _log.Warning("Empty collection of update items:", context: new { StreamId = request.StreamId, BrokerAccountId = request.BrokerAccountId, Cursor = request.Cursor, }); } foreach (var item in update.Items) { if (item.WithdrawalUpdateId <= _lastCursor) { continue; } if (string.IsNullOrWhiteSpace(item.Withdrawal.UserNativeId)) { _log.Warning("UserNativeId is empty", context: new { StreamId = request.StreamId, BrokerAccountId = request.BrokerAccountId, Cursor = request.Cursor, }); _log.Warning(message: "Withdrawal update body", context: new { Item = item.Withdrawal.ToJson(), StreamId = request.StreamId, BrokerAccountId = request.BrokerAccountId, Cursor = request.Cursor, }); continue; } _log.Info("Withdrawal update", context: new { Withdrawal = item.ToJson(), StreamId = request.StreamId, BrokerAccountId = request.BrokerAccountId, Cursor = request.Cursor, }); Asset asset = assets.FirstOrDefault(x => x.SiriusAssetId == item.Withdrawal.AssetId); if (asset == null) { _log.Warning( "Lykke asset not found", context: new { siriusAssetId = item.Withdrawal.AssetId, withdrawalId = item.Withdrawal.Id, StreamId = request.StreamId, BrokerAccountId = request.BrokerAccountId, Cursor = request.Cursor, }); continue; } await _withdrawalLogsRepository.AddAsync(item.Withdrawal.TransferContext.WithdrawalReferenceId, $"Withdrawal update (state: {item.Withdrawal.State.ToString()})", new { siriusWithdrawalId = item.Withdrawal.Id, clientId = item.Withdrawal.UserNativeId, walletId = item.Withdrawal.AccountReferenceId == item.Withdrawal.UserNativeId ? item.Withdrawal.UserNativeId : item.Withdrawal.AccountReferenceId, fees = item.Withdrawal.ActualFees.ToJson(), item.Withdrawal.State, TransactionHash = item.Withdrawal.TransactionInfo?.TransactionId }.ToJson() ); if (!Guid.TryParse(item.Withdrawal.TransferContext.WithdrawalReferenceId, out var operationId)) { operationId = Guid.Empty; } Guid?walletId = item.Withdrawal.AccountReferenceId == item.Withdrawal.UserNativeId ? null : Guid.Parse(item.Withdrawal.AccountReferenceId); switch (item.Withdrawal.State) { case WithdrawalState.Completed: _cqrsEngine.PublishEvent(new CashoutCompletedEvent { OperationId = operationId, ClientId = Guid.Parse(item.Withdrawal.UserNativeId), WalletId = walletId, AssetId = asset.Id, Amount = Convert.ToDecimal(item.Withdrawal.Amount.Value), Address = item.Withdrawal.DestinationDetails.Address, Tag = item.Withdrawal.DestinationDetails.Tag, TransactionHash = item.Withdrawal.TransactionInfo?.TransactionId, Timestamp = item.Withdrawal.UpdatedAt.ToDateTime().ToUniversalTime(), }, SiriusCashoutProcessorBoundedContext.Name); await _lastCursorRepository.AddAsync(_brokerAccountId, item.WithdrawalUpdateId); _lastCursor = item.WithdrawalUpdateId; break; case WithdrawalState.Failed: await _withdrawalLogsRepository.AddAsync( item.Withdrawal.TransferContext.WithdrawalReferenceId, "Withdrawal failed, finishing without Refund", null); await _lastCursorRepository.AddAsync(_brokerAccountId, item.WithdrawalUpdateId); _lastCursor = item.WithdrawalUpdateId; break; case WithdrawalState.Rejected: case WithdrawalState.Refunded: { await _withdrawalLogsRepository.AddAsync( item.Withdrawal.TransferContext.WithdrawalReferenceId, "Withdrawal failed, processing refund in ME", new { WithdrawalError = item.Withdrawal.Error?.ToJson() }.ToJson()); decimal amount = Convert.ToDecimal(item.Withdrawal.Amount.Value); var operation = await _operationsClient.Get(operationId); var operationContext = JsonConvert.DeserializeObject <OperationContext>(operation.ContextJson); decimal fee = operationContext.Fee.Type == "Absolute" ? operationContext.Fee.Size.TruncateDecimalPlaces(asset.Accuracy, true) : (amount * operationContext.Fee.Size).TruncateDecimalPlaces(asset.Accuracy, true); var refund = await _refundsRepository.GetAsync(item.Withdrawal.UserNativeId, item.Withdrawal.TransferContext.WithdrawalReferenceId) ?? await _refundsRepository.AddAsync( item.Withdrawal.TransferContext.WithdrawalReferenceId, item.Withdrawal.UserNativeId, walletId?.ToString() ?? item.Withdrawal.UserNativeId, operationContext.GlobalSettings.FeeSettings.TargetClients.Cashout, asset.Id, asset.SiriusAssetId, amount, fee); if (refund.FeeAmount > 0) { var feeReturnResult = await ReturnFeeAsync(refund); if (feeReturnResult != null && (feeReturnResult.Status == MeStatusCodes.Ok || feeReturnResult.Status == MeStatusCodes.Duplicate)) { _log.Info("Fee cashed out from fee wallet", new { OperationId = refund.FeeOperationId, StreamId = request.StreamId, BrokerAccountId = request.BrokerAccountId, Cursor = request.Cursor, }); await _withdrawalLogsRepository.AddAsync(refund.Id, "Fee cashed out from fee wallet", new { refund.FeeOperationId, refund.FeeClientId, refund.FeeAmount, refund.AssetId }.ToJson()); } else { _log.Info("Can't cashout fee from fee wallet", context: new { OperationId = refund.FeeOperationId, StreamId = request.StreamId, BrokerAccountId = request.BrokerAccountId, Cursor = request.Cursor, }); await _withdrawalLogsRepository.AddAsync(refund.Id, "Can't cashout fee from fee wallet", new { refund.FeeOperationId, refund.FeeClientId, refund.FeeAmount, refund.AssetId, error = feeReturnResult == null ? "response from ME is null" : $"{feeReturnResult.Status}: {feeReturnResult.Message}" }.ToJson()); } } var result = await RefundAsync(refund); if (result != null && (result.Status == MeStatusCodes.Ok || result.Status == MeStatusCodes.Duplicate)) { _log.Info("Refund processed", context: new { OperationId = refund.OperationId, StreamId = request.StreamId, BrokerAccountId = request.BrokerAccountId, Cursor = request.Cursor, }); await _refundsRepository.UpdateAsync(refund.ClientId, refund.Id, WithdrawalState.Completed.ToString()); await _withdrawalLogsRepository.AddAsync(refund.Id, "Refund processed in ME", new { result.Status, refund.OperationId }.ToJson()); _cqrsEngine.PublishEvent(new CashoutFailedEvent { OperationId = refund.Id, RefundId = refund.OperationId, Status = MeStatusCodes.Ok.ToString() }, SiriusCashoutProcessorBoundedContext.Name); await _lastCursorRepository.AddAsync(_brokerAccountId, item.WithdrawalUpdateId); _lastCursor = item.WithdrawalUpdateId; } else { _log.Info("Refund failed", context: new { OperationId = refund.OperationId, StreamId = request.StreamId, BrokerAccountId = request.BrokerAccountId, Cursor = request.Cursor, }); await _refundsRepository.UpdateAsync(refund.ClientId, refund.Id, WithdrawalState.Failed.ToString()); await _withdrawalLogsRepository.AddAsync(refund.Id, "Refund in ME failed", new { Error = result == null ? "response from ME is null" : $"{result.Status}: {result.Message}", OperationId = refund.Id }.ToJson()); _cqrsEngine.PublishEvent(new CashoutFailedEvent { OperationId = refund.Id, RefundId = refund.OperationId, Status = result?.Status.ToString(), Error = result == null ? "response from ME is null" : result.Message }, SiriusCashoutProcessorBoundedContext.Name); await _lastCursorRepository.AddAsync(_brokerAccountId, item.WithdrawalUpdateId); _lastCursor = item.WithdrawalUpdateId; } break; } } } } _log.Info("End of stream", context: new { request.StreamId }); } catch (RpcException ex) { if (ex.StatusCode == StatusCode.ResourceExhausted) { _log.Warning($"Rate limit has been reached. Waiting 1 minute...", ex, context: new { StreamId = streamId }); await Task.Delay(60000); } else { _log.Warning($"RpcException. {ex.Status}; {ex.StatusCode}", ex, context: new { StreamId = streamId }); } } catch (Exception ex) { _log.Error(ex); } await Task.Delay(5000); } }
private async Task ProcessDepositsAsync() { _cancellationTokenSource = new CancellationTokenSource(); while (!_cancellationTokenSource.IsCancellationRequested) { _lastCursor = await _lastCursorRepository.GetAsync(_brokerAccountId); var assets = await _assetsService.GetAllAssetsAsync(false, _cancellationTokenSource.Token); try { var request = new DepositUpdateSearchRequest { BrokerAccountId = _brokerAccountId, Cursor = _lastCursor }; request.State.Add(DepositState.Confirmed); _log.Info("Getting updates...", context: $"request: {request.ToJson()}"); var updates = _apiClient.Deposits.GetUpdates(request); while (await updates.ResponseStream.MoveNext(_cancellationTokenSource.Token).ConfigureAwait(false)) { DepositUpdateArrayResponse update = updates.ResponseStream.Current; foreach (var item in update.Items) { if (item.DepositUpdateId <= _lastCursor) { continue; } if (string.IsNullOrWhiteSpace(item.UserNativeId)) { _log.Warning("UserNativeId is empty"); continue; } _log.Info("Deposit detected", context: $"deposit: {item.ToJson()}"); Guid operationId = await _operationIdsRepository.GetOperationIdAsync(item.DepositId); string assetId = assets.FirstOrDefault(x => x.SiriusAssetId == item.AssetId)?.Id; if (string.IsNullOrEmpty(assetId)) { _log.Warning("Lykke asset not found", context: new { siriusAssetId = item.AssetId, depositId = item.DepositId }); continue; } var cashInResult = await _meClient.CashInOutAsync ( id : operationId.ToString(), clientId : item.ReferenceId ?? item.UserNativeId, assetId : assetId, amount : double.Parse(item.Amount.Value) ); if (cashInResult == null) { throw new InvalidOperationException("ME response is null, don't know what to do"); } switch (cashInResult.Status) { case MeStatusCodes.Ok: case MeStatusCodes.Duplicate: if (cashInResult.Status == MeStatusCodes.Duplicate) { _log.Info(message: "Deduplicated by the ME", context: new { operationId, depositId = item.DepositId }); } _log.Info("Deposit processed", context: new { cashInResult.TransactionId }); _cqrsEngine.PublishEvent(new CashinCompletedEvent { ClientId = item.UserNativeId, AssetId = assetId, Amount = decimal.Parse(item.Amount.Value), OperationId = operationId, TransactionHash = item.TransactionInfo.TransactionId, WalletId = item.ReferenceId == item.UserNativeId ? default(string) : item.ReferenceId, }, SiriusDepositsDetectorBoundedContext.Name); await _lastCursorRepository.AddAsync(_brokerAccountId, item.DepositUpdateId); _lastCursor = item.DepositUpdateId; break; case MeStatusCodes.Runtime: throw new Exception($"Cashin into the ME is failed. ME status: {cashInResult.Status}, ME message: {cashInResult.Message}"); default: _log.Warning($"Unexpected response from ME. Status: {cashInResult.Status}, ME message: {cashInResult.Message}", context: operationId.ToString()); break; } } } _log.Info("End of stream"); } catch (RpcException ex) { if (ex.StatusCode == StatusCode.ResourceExhausted) { _log.Warning($"Rate limit has been reached. Waiting 1 minute...", ex); await Task.Delay(60000); } else { _log.Warning($"RpcException. {ex.Status}; {ex.StatusCode}", ex); } } catch (Exception ex) { _log.Error(ex); } await Task.Delay(5000); } }