private async Task <MeResponseModel> RefundAsync(IRefund refund)
        {
            var policy = Policy
                         .Handle <TaskCanceledException>(exception =>
            {
                _log.Warning($"Retry on TaskCanceledException", context: $"clientId = {refund.ClientId}, assetId = {refund.AssetId}, amount = {refund.Amount + refund.FeeAmount}");
                return(true);
            })
                         .OrResult <MeResponseModel>(r =>
            {
                _log.Warning($"Response from ME: {(r == null ? "null" : r.ToJson())}");
                _withdrawalLogsRepository.AddAsync(refund.Id, "Response from ME",
                                                   new { refund.ClientId, refund.OperationId, refund.AssetId, refund.Amount, refund.FeeAmount, meResponse = r == null ? "null" : r.ToJson() }.ToJson()).GetAwaiter().GetResult();
                return(r == null || r.Status == MeStatusCodes.Runtime);
            })
                         .WaitAndRetryAsync(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

            var result = await policy.ExecuteAsync(async() =>
            {
                await _withdrawalLogsRepository.AddAsync(refund.Id, "Send refund to ME",
                                                         new { refund.ClientId, refund.OperationId, refund.AssetId, refund.Amount }.ToJson()
                                                         );

                var res = await _meClient.CashInOutAsync(refund.OperationId,
                                                         refund.WalletId ?? refund.ClientId,
                                                         refund.AssetId,
                                                         Convert.ToDouble(refund.Amount + refund.FeeAmount)
                                                         );

                return(res);
            });

            return(result);
        }
Example #2
0
        private async Task TransferAsync(
            IReadOnlyList <BalanceEntity> balances)
        {
            for (var i = 0; i < balances.Count; i++)
            {
                var balance = balances[i];

                try
                {
                    var cashInId = await _migrationRepository.GetOrCreateCashInOutIdAsync(balance.ClientId, _options.TargetAssetId);

                    var cashOutId = await _migrationRepository.GetOrCreateCashInOutIdAsync(balance.ClientId, _options.SourceAssetId);

                    var cashInAmount  = ((double)balance.Balance * _options.Multiplier).TruncateDecimalPlaces((int)_options.TargetAssetAccuracy);
                    var cashOutAmount = (double)balance.Balance * -1;

                    var cashInResult = await _meClient.CashInOutAsync
                                       (
                        id : cashInId.ToString(),
                        clientId : balance.ClientId,
                        assetId : _options.TargetAssetId,
                        amount : cashInAmount
                                       );

                    if (cashInResult.Status != MeStatusCodes.Ok && cashInResult.Status != MeStatusCodes.Duplicate)
                    {
                        _log.Warning($"CashIn [{cashInAmount}] for client [{balance.ClientId}] completed with [{cashInResult.Status.ToString()}] status.");
                    }

                    var cashOutResult = await _meClient.CashInOutAsync
                                        (
                        id : cashOutId.ToString(),
                        clientId : balance.ClientId,
                        assetId : _options.SourceAssetId,
                        amount : cashOutAmount
                                        );

                    if (cashOutResult.Status != MeStatusCodes.Ok || cashOutResult.Status != MeStatusCodes.Duplicate)
                    {
                        _log.Warning($"CashOut [{cashOutAmount}] completed for client {balance.ClientId} with [{cashOutResult.Status.ToString()}] status.");
                    }

                    _log.Info($"Completed {i + 1} of {balances.Count} transfers.");
                }
                catch (Exception e)
                {
                    _log.Error(e, $"Failed to transfer balance [{balance.Balance}] for client [{balance.ClientId}].");
                }
            }
        }
        private async Task BurnAsync(
            IReadOnlyList <BalanceEntity> balances)
        {
            for (var i = 0; i < balances.Count; i++)
            {
                var balance = balances[i];

                try
                {
                    var burnAmount = ((double)balance.Balance).TruncateDecimalPlaces((int)_options.AssetAccuracy) * -1;

                    var burnResult = await _meClient.CashInOutAsync
                                     (
                        id : Guid.NewGuid().ToString(),
                        clientId : balance.ClientId,
                        assetId : _options.AssetId,
                        amount : burnAmount
                                     );

                    if (burnResult.Status != MeStatusCodes.Ok)
                    {
                        _log.Warning($"CashOut [{burnAmount}] for client [{balance.ClientId}] completed with [{burnResult.Status.ToString()}] status.");
                    }

                    _log.Info($"Burned {i + 1} of {balances.Count} balances.");
                }
                catch (Exception e)
                {
                    _log.Error(e, $"Failed to copy balance [{balance.Balance}] for client [{balance.ClientId}].");
                }
            }
        }
        public async Task <CommandHandlingResult> Handle(ProcessCashInCommand command, IEventPublisher eventPublisher)
        {
            var id          = command.CommandId;
            var asset       = command.Asset;
            var amount      = command.Amount;
            var transaction = command.Transaction;

            ChaosKitty.Meow();

            var responseModel = await _matchingEngineClient.CashInOutAsync(id, transaction.ClientId, asset.Id, amount.TruncateDecimalPlaces(asset.Accuracy));

            if (responseModel.Status != MeStatusCodes.Ok && responseModel.Status != MeStatusCodes.AlreadyProcessed && responseModel.Status != MeStatusCodes.Duplicate)
            {
                _log.WriteInfo(nameof(ProcessCashInCommand), command, responseModel.ToJson());
                throw new ProcessingException(responseModel.ToJson());
            }

            ChaosKitty.Meow();

            eventPublisher.PublishEvent(new TransactionProcessedEvent {
                ClientId = command.Transaction.ClientId, Asset = command.Asset, Amount = command.Amount
            });

            return(CommandHandlingResult.Ok());
        }
        public async Task HandleCashInOperation(IAsset asset, double amount, string clientId, string clientAddress, string hash)
        {
            var id = Guid.NewGuid().ToString("N");

            var pt = await _paymentTransactionsRepository.TryCreateAsync(PaymentTransaction.Create(hash,
                                                                                                   CashInPaymentSystem.Ethereum, clientId, amount,
                                                                                                   asset.DisplayId ?? asset.Id, status: PaymentStatus.Processing));

            if (pt == null)
            {
                await
                _log.WriteWarningAsync(nameof(EthereumEventsQueue), nameof(HandleCashInOperation), hash,
                                       "Transaction already handled");

                //return if was handled previously
                return;
            }

            var result = await _matchingEngineClient.CashInOutAsync(id, clientId, asset.Id, amount);

            if (result == null || result.Status != MeStatusCodes.Ok)
            {
                await
                _log.WriteWarningAsync(nameof(EthereumEventsQueue), nameof(HandleCashInOperation), "ME error",
                                       result.ToJson());
            }
            else
            {
                var walletCreds = await _walletCredentialsRepository.GetAsync(clientId);

                await _cashOperationsRepository.RegisterAsync(new CashInOutOperation
                {
                    Id             = id,
                    ClientId       = clientId,
                    Multisig       = walletCreds.MultiSig,
                    AssetId        = asset.Id,
                    Amount         = amount,
                    BlockChainHash = hash,
                    DateTime       = DateTime.UtcNow,
                    AddressTo      = clientAddress,
                    State          = TransactionStates.SettledOnchain
                });

                var clientAcc = await _clientAccountsRepository.GetByIdAsync(clientId);

                await _srvEmailsFacade.SendNoRefundDepositDoneMail(clientAcc.Email, amount, asset.Id);

                await _paymentTransactionsRepository.SetStatus(hash, PaymentStatus.NotifyProcessed);
            }
        }
Example #6
0
        public async Task <CommandHandlingResult> Handle(EnrollEthCashinToMatchingEngineCommand command, IEventPublisher eventPublisher)
        {
            try
            {
                var cashinId = command.CashinOperationId.ToString("N");
                var clientId = command.ClientId;
                var hash     = command.TransactionHash;
                var amount   = command.Amount;
                var asset    = await _assetsServiceWithCache.TryGetAssetAsync(command.AssetId);

                var createPendingActions = command.CreatePendingActions;
                var paymentTransaction   = PaymentTransaction.Create(hash,
                                                                     CashInPaymentSystem.Ethereum, clientId.ToString(), (double)amount,
                                                                     asset.DisplayId ?? asset.Id, status: PaymentStatus.Processing);

                var exists = await _paymentTransactionsRepository.CheckExistsAsync(paymentTransaction);

                if (exists)
                {
                    _log.Warning(command.TransactionHash ?? "Empty", $"Transaction already handled {hash}",
                                 context: command);

                    return(CommandHandlingResult.Ok());
                }

                if (createPendingActions && asset.IsTrusted)
                {
                    await _ethererumPendingActionsRepository.CreateAsync(clientId.ToString(), Guid.NewGuid().ToString());
                }

                ChaosKitty.Meow();

                MeResponseModel result = null;

                await ExecuteWithTimeoutHelper.ExecuteWithTimeoutAsync(async() =>
                {
                    result = await _matchingEngineClient.CashInOutAsync(cashinId, clientId.ToString(), asset.Id, (double)amount);
                }, 5 * 60 * 1000);     // 5 min in ms

                if (result == null ||
                    (result.Status != MeStatusCodes.Ok &&
                     result.Status != MeStatusCodes.Duplicate))
                {
                    _log.Warning(command.TransactionHash ?? "Empty", "ME error", context: result);

                    return(CommandHandlingResult.Fail(TimeSpan.FromMinutes(1)));
                }

                eventPublisher.PublishEvent(new EthCashinEnrolledToMatchingEngineEvent()
                {
                    TransactionHash = hash,
                });

                ChaosKitty.Meow();

                await _paymentTransactionsRepository.TryCreateAsync(paymentTransaction);

                return(CommandHandlingResult.Ok());
            }
            catch (Exception e)
            {
                _log.Error(nameof(EnrollEthCashinToMatchingEngineCommand), e, context: command);
                throw;
            }
        }
Example #7
0
        public async Task <CommandHandlingResult> Handle(EnrollToMatchingEngineCommand command, IEventPublisher publisher)
        {
            // First level deduplication just to reduce traffic to the ME
            if (await _deduplicationRepository.IsExistsAsync(command.CashinOperationId))
            {
                _log.Info("Deduplicated at first level", command);

                // Workflow should be continued

                publisher.PublishEvent(new CashinEnrolledToMatchingEngineEvent
                {
                    CashoutOperationId = command.CashoutOperationId
                });

                return(CommandHandlingResult.Ok());
            }

            var cashInResult = await _meClient.CashInOutAsync(
                command.CashinOperationId.ToString(),
                command.RecipientClientId.ToString(),
                command.AssetId,
                (double)command.Amount);

            _chaosKitty.Meow(command.CashoutOperationId);

            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("Deduplicated by the ME", command);
                }

                publisher.PublishEvent(new CashinEnrolledToMatchingEngineEvent
                {
                    CashoutOperationId = command.CashoutOperationId
                });

                _chaosKitty.Meow(command.CashinOperationId);

                await _deduplicationRepository.InsertOrReplaceAsync(command.CashinOperationId);

                _chaosKitty.Meow(command.CashinOperationId);

                return(CommandHandlingResult.Ok());

            case MeStatusCodes.Runtime:
                // Retry forever with the default delay + log the error outside.
                throw new Exception($"Cashin into the ME is failed. ME status: {cashInResult.Status}, ME message: {cashInResult.Message}");

            default:
                // Just abort cashout for further manual processing. ME call could not be retried anyway if responce was received.
                _log.Warning(
                    $"Unexpected response from ME. Status: {cashInResult.Status}, ME message: {cashInResult.Message}",
                    context: command);
                return(CommandHandlingResult.Ok());
            }
        }
Example #8
0
        public async Task <CommandHandlingResult> Handle(EnrollToMatchingEngineCommand command, IEventPublisher publisher)
        {
            var clientId = command.ClientId;

            var amountDecimal = (decimal)command.MatchingEngineOperationAmount;

            var scale  = amountDecimal.GetScale();
            var amount = command.MatchingEngineOperationAmount.TruncateDecimalPlaces(scale);

            if (clientId == null)
            {
                clientId = await _walletsClient.TryGetClientIdAsync(
                    command.BlockchainType,
                    command.DepositWalletAddress);
            }

            if (clientId == null)
            {
                throw new InvalidOperationException("Client ID for the blockchain deposit wallet address is not found");
            }

            // First level deduplication just to reduce traffic to the ME
            if (await _deduplicationRepository.IsExistsAsync(command.OperationId))
            {
                _log.Info(nameof(EnrollToMatchingEngineCommand), "Deduplicated at first level", command.OperationId);

                // Workflow should be continued

                publisher.PublishEvent(new CashinEnrolledToMatchingEngineEvent
                {
                    ClientId    = clientId.Value,
                    OperationId = command.OperationId
                });

                return(CommandHandlingResult.Ok());
            }

            var cashInResult = await _meClient.CashInOutAsync
                               (
                id : command.OperationId.ToString(),
                clientId : clientId.Value.ToString(),
                assetId : command.AssetId,
                amount : amount
                               );

            _chaosKitty.Meow(command.OperationId);

            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(nameof(EnrollToMatchingEngineCommand), "Deduplicated by the ME", command.OperationId);
                }

                publisher.PublishEvent(new CashinEnrolledToMatchingEngineEvent
                {
                    ClientId    = clientId.Value,
                    OperationId = command.OperationId
                });

                _chaosKitty.Meow(command.OperationId);

                await _deduplicationRepository.InsertOrReplaceAsync(command.OperationId);

                _chaosKitty.Meow(command.OperationId);

                return(CommandHandlingResult.Ok());

            case MeStatusCodes.Runtime:
                // Retry forever with the default delay + log the error outside.
                throw new Exception($"Cashin into the ME is failed. ME status: {cashInResult.Status}, ME message: {cashInResult.Message}");

            default:
                // Just abort cashin for futher manual processing. ME call could not be retried anyway if responce was received.
                _log.Error(nameof(EnrollToMatchingEngineCommand), null, $"Unexpected response from ME. Status: {cashInResult.Status}, ME message: {cashInResult.Message}", context: command.OperationId);
                return(CommandHandlingResult.Ok());
            }
        }
        public async Task ExecutePlanAsync(
            Guid planId)
        {
            _log.Info("Distribution plan execution started...", new
            {
                planId
            });

            var distributionPlan = await _distributionPlanRepository.TryGetAsync(planId);

            if (distributionPlan != null)
            {
                _log.Info($"Plan contains {distributionPlan.Amounts} amounts to distribute");

                foreach (var distributionAmount in distributionPlan.Amounts)
                {
                    var result = await _matchingEngineClient.CashInOutAsync
                                 (
                        id : distributionAmount.Id.ToString(),
                        clientId : distributionAmount.WalletId.ToString(),
                        assetId : _gasAssetId,
                        amount : ((double)distributionAmount.Value)
                                 );

                    // ReSharper disable once SwitchStatementMissingSomeCases
                    switch (result.Status)
                    {
                    case MeStatusCodes.Ok:
                        _log.Info
                        (
                            $"{distributionAmount.Value} of gas has been distributed to {distributionAmount.WalletId}.",
                            distributionAmount
                        );
                        break;

                    case MeStatusCodes.Duplicate:
                        _log.Info
                        (
                            $"Distribution of {distributionAmount.Value} gas to {distributionAmount.WalletId} has been deduplicated by ME.",
                            distributionAmount
                        );
                        break;

                    case MeStatusCodes.Runtime:
                        throw new Exception($"Distribution failed. ME status: {result.Status}. ME message: {result.Message}.");

                    default:
                        _log.Warning
                        (
                            $"Got unexpected response from ME. ME status: {result.Status}, ME message: {result.Message}.",
                            context: distributionAmount
                        );
                        break;
                    }
                }
            }
            else
            {
                throw new InvalidOperationException($"Distribution plan [{planId}] has not been found.");
            }
        }
        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);
            }
        }