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);
            }
        }