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