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);
        }
Exemplo n.º 2
0
        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
                    }
                });
            }
        }