public async Task HandledDepositAsync(SignalCircleTransfer transfer) { transfer.AddToActivityAsJsonTag("circle-transfer"); _logger.LogInformation("Request to handle transfer from Circle: {transferJson}", JsonConvert.SerializeObject(transfer)); if (transfer.PaymentInfo.Source.Type != "blockchain" && transfer.PaymentInfo.Source.Type != "card") { _logger.LogInformation("Skip withdrawal transfer {type}", transfer.PaymentInfo.Source.Type); return; } transfer.BrokerId.AddToActivityAsTag("brokerId"); transfer.WalletId.AddToActivityAsTag("walletId"); transfer.ClientId.AddToActivityAsTag("clientId"); var circleAsset = _circleAssetMapper.CircleAssetToAsset(transfer.BrokerId, transfer.PaymentInfo.Amount.Currency); string asset = null; if (transfer.PaymentInfo.Source.Type == "card") { asset = circleAsset?.AssetSymbol; } else if (transfer.PaymentInfo.Source.Type == "blockchain") { asset = circleAsset?.AssetTokenSymbol; } if (string.IsNullOrEmpty(asset)) { _logger.LogError("Unknown circle asset {asset}", transfer.PaymentInfo.Amount.Currency); return; } var accuracy = _assetsDictionary.GetAssetById(new AssetIdentity { Symbol = asset, BrokerId = transfer.BrokerId }).Accuracy; var amount = (decimal)double.Parse(transfer.PaymentInfo.Amount.Amount); var feeAmount = (decimal)double.Parse(string.IsNullOrEmpty(transfer.PaymentInfo?.Fees?.Amount) ? "0.0" : transfer.PaymentInfo.Fees.Amount); var roundedAmount = Math.Round(amount - feeAmount, accuracy, MidpointRounding.ToNegativeInfinity); var isBlockchain = transfer.PaymentInfo.Source.Type == "blockchain"; var circleBlockchain = _circleBlockchainMapper.BlockchainToCircleBlockchain(transfer.BrokerId, transfer.PaymentInfo.Source.Chain); try { await using var ctx = DatabaseContext.Create(_dbContextOptionsBuilder); var existingEntity = await ctx.Deposits.Where(e => e.TransactionId == transfer.PaymentInfo.Id).FirstOrDefaultAsync(); if (existingEntity == null) { _logger.LogInformation("Got unknown deposit from circle, adding entry"); existingEntity = new DepositEntity { BrokerId = transfer.BrokerId, WalletId = transfer.WalletId, ClientId = transfer.ClientId, TransactionId = transfer.PaymentInfo.Id, Amount = (decimal)roundedAmount, AssetSymbol = asset, Comment = isBlockchain ? $"Circle blockchain {transfer.PaymentInfo.Source.Chain} deposit [{asset}]" : $"Circle card deposit [{asset}:{transfer.WalletId}, card: {transfer.PaymentInfo.Source.Id}]", Integration = isBlockchain ? "CircleBlockchain" : "CircleCard", Txid = isBlockchain ? transfer.PaymentInfo.TransactionHash : transfer.PaymentInfo.Id, Status = ConvertExternalStatus(transfer.PaymentInfo.Status), EventDate = DateTime.UtcNow, UpdateDate = DateTime.UtcNow, FeeAmount = feeAmount, FeeAssetSymbol = string.IsNullOrEmpty(transfer.PaymentInfo?.Fees?.Currency) ? transfer.PaymentInfo.Amount.Currency : transfer.PaymentInfo.Fees.Currency, Network = circleBlockchain != null ? circleBlockchain.Blockchain : transfer.PaymentInfo.Source.Chain, MatchingEngineId = "Deposit:" + Guid.NewGuid(), }; } else { existingEntity.Amount = roundedAmount; existingEntity.UpdateDate = DateTime.UtcNow; existingEntity.FeeAmount = feeAmount; existingEntity.FeeAssetSymbol = string.IsNullOrEmpty(transfer.PaymentInfo?.Fees?.Currency) ? transfer.PaymentInfo.Amount.Currency : transfer.PaymentInfo.Fees.Currency; existingEntity.Status = ConvertExternalStatus(transfer.PaymentInfo.Status); } await ctx.UpsertAsync(existingEntity); await _depositPublisher.PublishAsync(new Deposit(existingEntity)); } catch (Exception) { _logger.LogError("Unable to update deposit entry"); throw; } _logger.LogInformation("Deposit request from Circle {transferIdString} is handled", transfer.PaymentInfo.Id); }
public async Task <CryptoWithdrawalResponse> CryptoWithdrawalAsync(CryptoWithdrawalRequest request) { _logger.LogDebug("Receive CryptoWithdrawalRequest: {jsonText}", JsonConvert.SerializeObject(request)); request.WalletId.AddToActivityAsTag("walletId"); request.ClientId.AddToActivityAsTag("clientId"); request.BrokerId.AddToActivityAsTag("brokerId"); request.ToAddress.AddToActivityAsTag("blockchain-address"); try { var feeAmount = 0m; string feeAsset = null; string comment = null; string integration = null; string destinationWallet = null; string destinationClient = null; var requestId = request.RequestId ?? Guid.NewGuid().ToString("N"); var transactionId = OperationIdGenerator.GenerateOperationId(requestId, request.WalletId); var addressInfoResponse = await _walletService.GetUserByAddressAsync(new Blockchain.Wallets.Grpc.Models.UserWallets.GetUserByAddressRequest { Addresses = new Blockchain.Wallets.Grpc.Models.UserWallets.GetUserByAddressRequest.AddressAndTag[] { new Blockchain.Wallets.Grpc.Models.UserWallets.GetUserByAddressRequest.AddressAndTag { Address = request.ToAddress, Tag = string.IsNullOrEmpty(request.ToTag) ? null : request.ToTag, } }, }); var assetIdentity = new AssetIdentity { BrokerId = request.BrokerId, Symbol = request.AssetSymbol }; var paymentSettings = _assetPaymentSettingsClient.GetAssetById(assetIdentity); var isInternal = addressInfoResponse.Users != null && addressInfoResponse.Users.Any(); if (isInternal) { var existingAddress = addressInfoResponse.Users.First(); var clientIdResponse = await _clientWalletService.SearchClientsAsync(new SearchWalletsRequest() { SearchText = existingAddress.WalletId, Take = 1 }); destinationClient = clientIdResponse.Clients.FirstOrDefault()?.ClientId; destinationWallet = existingAddress.WalletId; comment = $"Internal withdrawal [{request.AssetSymbol}:{request.Amount}:{request.WalletId}]"; integration = "Internal"; if (destinationClient == request.ClientId) { return(new CryptoWithdrawalResponse() { Error = new BitgoErrorType() { Code = BitgoErrorType.ErrorCode.AddressIsNotValid, Message = "Cannot send fund to yourself" } }); } } else { if (paymentSettings?.Fireblocks?.IsEnabledWithdrawal == true) { comment = $"Fireblocks withdrawal [{request.AssetSymbol}:{request.Amount}:{request.WalletId}]"; integration = "Fireblocks"; } else if (paymentSettings?.Circle?.IsEnabledBlockchainWithdrawal == true) { var isGuidValid = Guid.TryParse(request.RequestId, out _); if (!isGuidValid) { return(new CryptoWithdrawalResponse() { Error = new BitgoErrorType() { Message = $"RequestId should be Guid {request.RequestId}", Code = BitgoErrorType.ErrorCode.InvalidRequestId } }); } var circleAsset = _circleAssetMapper.AssetToCircleTokenAsset(request.BrokerId, request.AssetSymbol); if (circleAsset == null) { _logger.LogInformation( $"[CryptoWithdrawalRequest] Cannot found circle coin association for asset {request.AssetSymbol}, broker {request.BrokerId}"); return(new CryptoWithdrawalResponse() { Error = new BitgoErrorType() { Message = $"Cannot found circle coin association for asset {request.AssetSymbol}, broker {request.BrokerId}", Code = BitgoErrorType.ErrorCode.AssetIsNotFoundInCircle } }); } var circleBlockchain = _circleBlockchainMapper.BlockchainToCircleBlockchain(request.BrokerId, request.Blockchain); if (circleBlockchain == null) { _logger.LogInformation( $"[CryptoWithdrawalRequest] Cannot found circle blockchain association for blockchain {request.Blockchain}, broker {request.BrokerId}"); return(new CryptoWithdrawalResponse() { Error = new BitgoErrorType() { Message = $"Cannot found circle blockchain association for blockchain {request.Blockchain}, broker {request.BrokerId}", Code = BitgoErrorType.ErrorCode.BlockchainIsNotFoundInCircle } }); } comment = $"Circle withdrawal [{request.AssetSymbol}:{request.Amount}:{request.WalletId}]"; integration = "Circle"; transactionId = requestId; } var fees = _assetFeesClient.GetAssetFees(request.BrokerId, request.AssetSymbol, OperationType.Withdrawal); if (fees != null) { feeAsset = fees.AssetId; if (feeAsset == request.AssetSymbol) { feeAmount = fees.FeeSizeType == FeeSizeType.Absolute ? (decimal)fees.FeeSize : (decimal)fees.FeeSize / 100 * request.Amount; } else { feeAmount = (decimal)fees.FeeSize; } } } await using var ctx = DatabaseContext.Create(_dbContextOptionsBuilder); var withdrawalEntity = new WithdrawalEntity() { BrokerId = request.BrokerId, ClientId = request.ClientId, WalletId = request.WalletId, TransactionId = transactionId, Amount = request.Amount, AssetSymbol = request.AssetSymbol, Comment = comment, Integration = integration, Status = WithdrawalStatus.New, EventDate = DateTime.UtcNow, ToAddress = request.ToAddress, ToTag = request.ToTag, ClientIp = request.ClientIp, ClientLang = request.ClientLang, FeeAmount = feeAmount, FeeAssetSymbol = feeAsset, IsInternal = isInternal, DestinationWalletId = destinationWallet, DestinationClientId = destinationClient, Blockchain = request.Blockchain, Signature = request.Signature, SignatureIssuedAt = request.SignatureIssuedAtUnixTime, }; try { await ctx.AddAsync(withdrawalEntity); await ctx.SaveChangesAsync(); } catch (Exception) { var existingWithdrawal = await ctx.Withdrawals.Where(t => t.TransactionId == transactionId).FirstAsync(); return(new CryptoWithdrawalResponse() { OperationId = existingWithdrawal.Id.ToString() }); } return(new CryptoWithdrawalResponse { OperationId = withdrawalEntity.Id.ToString() }); } catch (Exception ex) { ex.FailActivity(); _logger.LogError(ex, "Cannot handle CryptoWithdrawalRequest {jsonText}", JsonConvert.SerializeObject(request)); return(new CryptoWithdrawalResponse() { Error = new BitgoErrorType() { Code = BitgoErrorType.ErrorCode.InternalError, Message = ex.Message } }); } }