예제 #1
0
        public async Task <CommandHandlingResult> Handle(AcceptCashoutCommand command, IEventPublisher publisher)
        {
            var blockchainConfiguration = _blockchainConfigurationProvider.GetConfiguration(command.BlockchainType);

            var recipientClientId = !_disableDirectCrossClientCashouts
                ? await _walletsClient.TryGetClientIdAsync(command.BlockchainType, command.ToAddress)
                : null;

            if (recipientClientId.HasValue)
            {
                return(StartCrossClientCashaout(command, publisher, recipientClientId.Value));
            }

            if (blockchainConfiguration.SupportCashoutAggregation)
            {
                return(await StartBatchedCashoutAsync(command, publisher, blockchainConfiguration.CashoutsAggregation));
            }

            return(StartRegularCashout(command, publisher));
        }
예제 #2
0
        public async Task <CommandHandlingResult> Handle(RetrieveClientCommand command, IEventPublisher publisher)
        {
            // TODO: Add client cache for the walletsClient

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

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

            _chaosKitty.Meow(command.OperationId);

            return(CommandHandlingResult.Ok());
        }
예제 #3
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());
            }
        }
        /// <summary>
        ///
        /// </summary>
        /// <param name="cashoutModel"></param>
        /// <returns>
        /// ValidationError - client error
        /// ArgumentValidationException - developer error
        /// </returns>
        public async Task <IReadOnlyCollection <ValidationError> > ValidateAsync(CashoutModel cashoutModel)
        {
            var errors = new List <ValidationError>(1);

            if (cashoutModel == null)
            {
                return(FieldNotValidResult("cashoutModel can't be null"));
            }

            if (string.IsNullOrEmpty(cashoutModel.AssetId))
            {
                return(FieldNotValidResult("cashoutModel.AssetId can't be null or empty"));
            }

            Asset asset;

            try
            {
                asset = await _assetsService.TryGetAssetAsync(cashoutModel.AssetId);
            }
            catch (Exception)
            {
                throw new ArgumentValidationException($"Asset with Id-{cashoutModel.AssetId} does not exists",
                                                      "assetId");
            }

            if (asset.IsDisabled)
            {
                errors.Add(ValidationError.Create(ValidationErrorType.None, $"Asset {asset.Id} is disabled"));
            }

            if (asset == null)
            {
                throw new ArgumentValidationException($"Asset with Id-{cashoutModel.AssetId} does not exists",
                                                      "assetId");
            }

            var isAddressValid = true;
            IBlockchainApiClient blockchainClient = null;

            if (asset.Id != LykkeConstants.SolarAssetId)
            {
                if (string.IsNullOrEmpty(asset.BlockchainIntegrationLayerId))
                {
                    throw new ArgumentValidationException(
                              $"Given asset Id-{cashoutModel.AssetId} is not a part of Blockchain Integration Layer",
                              "assetId");
                }

                blockchainClient = _blockchainApiClientProvider.Get(asset.BlockchainIntegrationLayerId);
            }

            if (string.IsNullOrEmpty(cashoutModel.DestinationAddress) ||
                !cashoutModel.DestinationAddress.IsValidPartitionOrRowKey() ||
                asset.Id != LykkeConstants.SolarAssetId && blockchainClient != null &&
                !await blockchainClient.IsAddressValidAsync(cashoutModel.DestinationAddress) ||
                asset.Id == LykkeConstants.SolarAssetId &&
                !SolarCoinValidation.ValidateAddress(cashoutModel.DestinationAddress)
                )
            {
                isAddressValid = false;
                errors.Add(ValidationError.Create(ValidationErrorType.AddressIsNotValid, "Address is not valid"));
            }

            if (isAddressValid)
            {
                if (asset.Id != LykkeConstants.SolarAssetId)
                {
                    var isBlocked = await _blackListService.IsBlockedWithoutAddressValidationAsync
                                    (
                        asset.BlockchainIntegrationLayerId,
                        cashoutModel.DestinationAddress
                                    );

                    if (isBlocked)
                    {
                        errors.Add(ValidationError.Create(ValidationErrorType.BlackListedAddress, "Address is in the black list"));
                    }
                }

                if (cashoutModel.Volume.HasValue && Math.Abs(cashoutModel.Volume.Value) < (decimal)asset.CashoutMinimalAmount)
                {
                    var minimalAmount = asset.CashoutMinimalAmount.GetFixedAsString(asset.Accuracy).TrimEnd('0');

                    errors.Add(ValidationError.Create(ValidationErrorType.LessThanMinCashout,
                                                      $"Please enter an amount greater than {minimalAmount}"));
                }

                if (asset.Id != LykkeConstants.SolarAssetId)
                {
                    var blockchainSettings = _blockchainSettingsProvider.Get(asset.BlockchainIntegrationLayerId);

                    if (cashoutModel.DestinationAddress == blockchainSettings.HotWalletAddress)
                    {
                        errors.Add(ValidationError.Create(ValidationErrorType.HotwalletTargetProhibited,
                                                          "Hot wallet as destitnation address prohibited"));
                    }

                    var isPublicExtensionRequired =
                        _blockchainWalletsCacheService.IsPublicExtensionRequired(asset.BlockchainIntegrationLayerId);
                    if (isPublicExtensionRequired)
                    {
                        var hotWalletParseResult = await _blockchainWalletsClient.ParseAddressAsync(
                            asset.BlockchainIntegrationLayerId,
                            blockchainSettings.HotWalletAddress);

                        var destAddressParseResult = await _blockchainWalletsClient.ParseAddressAsync(
                            asset.BlockchainIntegrationLayerId,
                            cashoutModel.DestinationAddress);

                        if (hotWalletParseResult.BaseAddress == destAddressParseResult.BaseAddress)
                        {
                            var existedClientIdAsDestination = await _blockchainWalletsClient.TryGetClientIdAsync(
                                asset.BlockchainIntegrationLayerId,
                                cashoutModel.DestinationAddress);

                            if (existedClientIdAsDestination == null)
                            {
                                errors.Add(ValidationError.Create(ValidationErrorType.DepositAddressNotFound,
                                                                  $"Deposit address {cashoutModel.DestinationAddress} not found"));
                            }
                        }

                        var forbiddenCharacterErrors = await ValidateForForbiddenCharsAsync(
                            destAddressParseResult.BaseAddress,
                            destAddressParseResult.AddressExtension,
                            asset.BlockchainIntegrationLayerId);

                        if (forbiddenCharacterErrors != null)
                        {
                            errors.AddRange(forbiddenCharacterErrors);
                        }

                        if (!string.IsNullOrEmpty(destAddressParseResult.BaseAddress))
                        {
                            if (!cashoutModel.DestinationAddress.Contains(destAddressParseResult.BaseAddress))
                            {
                                errors.Add(ValidationError.Create(ValidationErrorType.FieldIsNotValid,
                                                                  "Base Address should be part of destination address"));
                            }

                            // full address is already checked by integration,
                            // we don't need to validate it again,
                            // just ensure that base address is not black-listed
                            var isBlockedBase = await _blackListService.IsBlockedWithoutAddressValidationAsync(
                                asset.BlockchainIntegrationLayerId,
                                destAddressParseResult.BaseAddress);

                            if (isBlockedBase)
                            {
                                errors.Add(ValidationError.Create(ValidationErrorType.BlackListedAddress,
                                                                  "Base Address is in the black list"));
                            }
                        }
                    }
                }

                if (cashoutModel.ClientId.HasValue)
                {
                    var destinationClientId = await _blockchainWalletsClient.TryGetClientIdAsync
                                              (
                        asset.BlockchainIntegrationLayerId,
                        cashoutModel.DestinationAddress
                                              );

                    if (destinationClientId.HasValue && destinationClientId == cashoutModel.ClientId.Value)
                    {
                        var error = ValidationError.Create
                                    (
                            ValidationErrorType.CashoutToSelfAddress,
                            "Withdrawals to the deposit wallet owned by the customer himself prohibited"
                                    );

                        errors.Add(error);
                    }
                }
            }

            return(errors);
        }