"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.metadata = void 0;
const db_1 = require("@b/db");
const sequelize_1 = require("sequelize");
const wallet_1 = require("@b/api/(ext)/ecosystem/utils/wallet");
const error_1 = require("@b/utils/error");
const tokens_1 = require("@b/api/(ext)/ecosystem/utils/tokens");
const query_1 = require("@b/utils/query");
const index_post_1 = require("@b/api/finance/transfer/index.post");
const withdrawalQueue_1 = __importDefault(require("../utils/withdrawalQueue"));
const safe_imports_1 = require("@b/utils/safe-imports");
const console_1 = require("@b/utils/console");
// Utility function to count decimal places
function countDecimals(value) {
    var _a;
    if (Number.isInteger(value))
        return 0;
    const str = value.toString();
    if (str.includes("e-")) {
        // Handle scientific notation
        const [_, exponent] = str.split("e-");
        return parseInt(exponent, 10);
    }
    const parts = str.split(".");
    return ((_a = parts[1]) === null || _a === void 0 ? void 0 : _a.length) || 0;
}
exports.metadata = {
    summary: "Withdraws funds to an external address",
    description: "Processes a withdrawal from the user's wallet to an external address.",
    operationId: "withdrawFunds",
    tags: ["Wallet", "Withdrawal"],
    requiresAuth: true,
    logModule: "ECO_WITHDRAW",
    logTitle: "Withdraw funds to external address",
    requestBody: {
        required: true,
        content: {
            "application/json": {
                schema: {
                    type: "object",
                    properties: {
                        currency: {
                            type: "string",
                            description: "Currency to withdraw",
                        },
                        chain: {
                            type: "string",
                            description: "Withdraw method ID",
                        },
                        amount: {
                            type: "number",
                            description: "Amount to withdraw",
                        },
                        toAddress: {
                            type: "string",
                            description: "Withdraw toAddress",
                        },
                    },
                    required: ["currency", "chain", "amount", "toAddress"],
                },
            },
        },
    },
    responses: {
        200: {
            description: "Withdrawal processed successfully",
            content: {
                "application/json": {
                    schema: {
                        type: "object",
                        properties: {
                            message: {
                                type: "string",
                                description: "Success message indicating the withdrawal has been processed.",
                            },
                        },
                    },
                },
            },
        },
        401: query_1.unauthorizedResponse,
        404: (0, query_1.notFoundMetadataResponse)("Wallet"),
        500: query_1.serverErrorResponse,
    },
};
// Input sanitization helper
function sanitizeInput(input, maxLength = 255) {
    if (!input || typeof input !== 'string')
        return '';
    // Remove null bytes, control characters, and excessive whitespace
    let sanitized = input
        .replace(/\0/g, '') // null bytes
        // eslint-disable-next-line no-control-regex
        .replace(/[\x00-\x1F\x7F]/g, '') // control characters
        .trim();
    // Limit length to prevent buffer overflow attacks
    if (sanitized.length > maxLength) {
        sanitized = sanitized.substring(0, maxLength);
    }
    return sanitized;
}
// Validate address format (basic alphanumeric check before chain-specific validation)
function validateAddressFormat(address) {
    if (!address || typeof address !== 'string')
        return false;
    // Check for suspicious patterns
    const suspiciousPatterns = [
        /[<>'"]/, // HTML/XSS characters
        /\$\{.*\}/, // Template injection
        /\.\.\//, // Path traversal
        /__proto__|constructor|prototype/i, // Prototype pollution
        /javascript:/i, // XSS
        /data:/i, // Data URI XSS
        /on\w+\s*=/i, // Event handler XSS
        /script|iframe|object|embed/i, // HTML injection
        /union.*select/i, // SQL injection
        /exec\(|eval\(|system\(/i, // Command injection
    ];
    for (const pattern of suspiciousPatterns) {
        if (pattern.test(address)) {
            console_1.logger.error("SECURITY", `Suspicious pattern detected in address: ${pattern}`);
            return false;
        }
    }
    // Must contain only valid blockchain address characters
    // Most blockchain addresses use base58, hex, or bech32 (alphanumeric)
    const validAddressPattern = /^[a-zA-Z0-9]+$/;
    return validAddressPattern.test(address);
}
exports.default = async (data) => {
    const { body, user, ctx } = data;
    if (!(user === null || user === void 0 ? void 0 : user.id)) {
        throw (0, error_1.createError)({ statusCode: 401, message: "Unauthorized" });
    }
    try {
        const { currency: rawCurrency, chain: rawChain, amount, toAddress: rawToAddress } = body;
        ctx === null || ctx === void 0 ? void 0 : ctx.step("Sanitizing input parameters");
        // Sanitize all string inputs
        const currency = sanitizeInput(rawCurrency, 10);
        const chain = sanitizeInput(rawChain, 20);
        const toAddress = sanitizeInput(rawToAddress, 200);
        ctx === null || ctx === void 0 ? void 0 : ctx.step("Validating withdrawal request");
        // Validate inputs
        if (!currency || !chain || !toAddress) {
            ctx === null || ctx === void 0 ? void 0 : ctx.fail("Missing required parameters");
            throw (0, error_1.createError)({
                statusCode: 400,
                message: "Missing required parameters",
            });
        }
        // Basic address format validation before chain-specific validation
        if (!validateAddressFormat(toAddress)) {
            ctx === null || ctx === void 0 ? void 0 : ctx.fail("Invalid address format detected");
            throw (0, error_1.createError)({
                statusCode: 400,
                message: "Invalid address format. Address contains invalid characters or suspicious patterns.",
            });
        }
        console_1.logger.info("ECO_WITHDRAW", `Starting withdrawal: userId=${user.id}, currency=${currency}, chain=${chain}, amount=${amount}, toAddress=${toAddress === null || toAddress === void 0 ? void 0 : toAddress.substring(0, 10)}...`);
        if (!chain) {
            console_1.logger.error("ECO_WITHDRAW", `Chain parameter missing`);
            ctx === null || ctx === void 0 ? void 0 : ctx.fail("Chain parameter is required");
            throw (0, error_1.createError)({
                statusCode: 400,
                message: "Chain parameter is required",
            });
        }
        ctx === null || ctx === void 0 ? void 0 : ctx.step("Validating withdrawal amount");
        // Validate amount is a valid positive number
        const parsedAmount = parseFloat(amount);
        if (isNaN(parsedAmount) || parsedAmount <= 0 || !isFinite(parsedAmount)) {
            ctx === null || ctx === void 0 ? void 0 : ctx.fail("Invalid amount provided");
            throw (0, error_1.createError)({
                statusCode: 400,
                message: "Invalid amount. Must be a positive number.",
            });
        }
        const finalAmount = Math.abs(parsedAmount);
        ctx === null || ctx === void 0 ? void 0 : ctx.step("Checking if address is internal");
        // Check if the toAddress belongs to an internal user
        console_1.logger.debug("ECO_WITHDRAW", `Checking if address is internal...`);
        const recipientWallet = await findWalletByAddress(toAddress);
        if (recipientWallet) {
            // Process as internal transfer
            ctx === null || ctx === void 0 ? void 0 : ctx.step("Processing as internal transfer");
            console_1.logger.info("ECO_WITHDRAW", `Address is internal, processing as transfer`);
            const result = await (0, index_post_1.processInternalTransfer)(user.id, recipientWallet.userId, currency, chain, finalAmount);
            ctx === null || ctx === void 0 ? void 0 : ctx.success(`Internal transfer of ${finalAmount} ${currency} completed`);
            return result;
        }
        else {
            // Proceed with the regular withdrawal process
            ctx === null || ctx === void 0 ? void 0 : ctx.step("Processing as external withdrawal");
            console_1.logger.info("ECO_WITHDRAW", `Address is external, processing withdrawal`);
            const result = await storeWithdrawal(user.id, currency, chain, finalAmount, toAddress, ctx);
            ctx === null || ctx === void 0 ? void 0 : ctx.success(`Withdrawal of ${finalAmount} ${currency} to ${chain} submitted`);
            return result;
        }
    }
    catch (error) {
        console_1.logger.error("ECO_WITHDRAW", `Error in withdrawal`, error);
        if (error.message === "INSUFFICIENT_FUNDS") {
            console_1.logger.debug("ECO_WITHDRAW", "Insufficient funds error");
            ctx === null || ctx === void 0 ? void 0 : ctx.fail("Insufficient funds");
            throw (0, error_1.createError)({ statusCode: 400, message: "Insufficient funds" });
        }
        ctx === null || ctx === void 0 ? void 0 : ctx.fail(`Withdrawal failed: ${error.message}`);
        throw (0, error_1.createError)({
            statusCode: 500,
            message: `Failed to withdraw: ${error}`,
        });
    }
};
const storeWithdrawal = async (userId, currency, chain, amount, toAddress, ctx) => {
    var _a, _b, _c, _d, _e, _f, _g;
    console_1.logger.debug("ECO_WITHDRAW", `Starting storeWithdrawal: userId=${userId}, currency=${currency}, chain=${chain}, amount=${amount}, toAddress=${toAddress === null || toAddress === void 0 ? void 0 : toAddress.substring(0, 10)}...`);
    if (!chain || typeof chain !== "string") {
        console_1.logger.error("ECO_WITHDRAW", `Invalid chain parameter: ${chain}`);
        throw (0, error_1.createError)({ statusCode: 400, message: "Invalid or missing chain parameter" });
    }
    ctx === null || ctx === void 0 ? void 0 : ctx.step("Validating withdrawal address");
    // Validate address for non-BTC, LTC, DOGE, and DASH chains
    if (!["BTC", "LTC", "DOGE", "DASH"].includes(chain)) {
        (0, wallet_1.validateAddress)(toAddress, chain);
    }
    ctx === null || ctx === void 0 ? void 0 : ctx.step("Retrieving user wallet");
    // Find the user's wallet (ECO type only - COPY_TRADING wallets cannot withdraw)
    console_1.logger.debug("ECO_WITHDRAW", `Looking for wallet: userId=${userId}, currency=${currency}, type=ECO`);
    const userWallet = await db_1.models.wallet.findOne({
        where: { userId, currency, type: "ECO" },
    });
    if (!userWallet) {
        console_1.logger.error("ECO_WITHDRAW", `Wallet not found for user ${userId}, currency ${currency}`);
        throw (0, error_1.createError)({ statusCode: 404, message: "User wallet not found" });
    }
    // Additional security check: ensure wallet type is ECO
    if (userWallet.type !== "ECO") {
        console_1.logger.error("ECO_WITHDRAW", `Attempted withdrawal from non-ECO wallet: type=${userWallet.type}`);
        throw (0, error_1.createError)({ statusCode: 403, message: "Withdrawals are only allowed from ECO wallets" });
    }
    console_1.logger.debug("ECO_WITHDRAW", `Found wallet: walletId=${userWallet.id}, balance=${userWallet.balance}`);
    ctx === null || ctx === void 0 ? void 0 : ctx.step("Fetching token configuration");
    // Fetch token settings (like withdrawal fees)
    console_1.logger.debug("ECO_WITHDRAW", `Fetching token settings for ${currency} on ${chain}`);
    const token = await (0, tokens_1.getEcosystemToken)(chain, currency);
    if (!token) {
        console_1.logger.error("ECO_WITHDRAW", `Token not found for ${currency} on ${chain}`);
        throw (0, error_1.createError)({ statusCode: 404, message: "Token not found" });
    }
    console_1.logger.debug("ECO_WITHDRAW", `Token found: currency=${token.currency}, decimals=${token.decimals}, precision=${token.precision}`);
    ctx === null || ctx === void 0 ? void 0 : ctx.step("Validating amount precision");
    // Validate decimal precision based on token configuration
    const maxPrecision = (_b = (_a = token.precision) !== null && _a !== void 0 ? _a : token.decimals) !== null && _b !== void 0 ? _b : 8;
    const actualDecimals = countDecimals(amount);
    if (actualDecimals > maxPrecision) {
        throw (0, error_1.createError)({
            statusCode: 400,
            message: `Amount has too many decimal places for ${currency} on ${chain}. Max allowed is ${maxPrecision} decimal places. Your amount has ${actualDecimals} decimal places.`
        });
    }
    ctx === null || ctx === void 0 ? void 0 : ctx.step("Calculating withdrawal fees");
    // Calculate the withdrawal fee based on token settings
    let withdrawalFee = 0;
    if (token.fee) {
        const tokenFee = JSON.parse(token.fee);
        const currencyWithdrawalFee = (_c = tokenFee.percentage) !== null && _c !== void 0 ? _c : 0;
        const minimumWithdrawalFee = (_d = tokenFee.min) !== null && _d !== void 0 ? _d : 0;
        withdrawalFee = calculateWithdrawalFee(amount, currencyWithdrawalFee, minimumWithdrawalFee);
    }
    // Initialize fees and logic for different chains
    let activationFee = 0;
    let estimatedFee = 0;
    // For NATIVE EVM tokens, estimate gas fee before withdrawal
    const evmChains = ["ETH", "BSC", "POLYGON", "FTM", "OPTIMISM", "ARBITRUM", "BASE", "CELO", "RSK", "AVAX"];
    const isNativeEVM = evmChains.includes(chain) && token.contractType === "NATIVE";
    if (isNativeEVM) {
        ctx === null || ctx === void 0 ? void 0 : ctx.step(`Estimating gas fees for ${chain}`);
        console_1.logger.debug("ECO_WITHDRAW", `Estimating gas for NATIVE ${currency} on ${chain}`);
        try {
            const { initializeProvider } = require("@b/api/(ext)/ecosystem/utils/provider");
            const provider = await initializeProvider(chain);
            // Estimate gas for a simple native transfer
            const gasPrice = await provider.getFeeData();
            const gasLimit = 21000; // Standard gas limit for ETH/EVM native transfers
            // Calculate estimated fee: gasLimit * gasPrice
            const { ethers } = require("ethers");
            const gasCost = BigInt(gasLimit) * (gasPrice.gasPrice || gasPrice.maxFeePerGas || BigInt(0));
            estimatedFee = parseFloat(ethers.formatUnits(gasCost, token.decimals));
            console_1.logger.debug("ECO_WITHDRAW", `Estimated gas fee: ${estimatedFee} ${currency}, gasLimit=${gasLimit}, gasPrice=${(_e = gasPrice.gasPrice) === null || _e === void 0 ? void 0 : _e.toString()}`);
        }
        catch (error) {
            console_1.logger.error("ECO_WITHDRAW", `Failed to estimate gas: ${error.message}`);
            // Use a fallback estimate based on chain
            const fallbackGasFees = {
                ETH: 0.001,
                BSC: 0.0003,
                POLYGON: 0.01,
                ARBITRUM: 0.0001,
                OPTIMISM: 0.0001,
                BASE: 0.0001,
                AVAX: 0.001,
            };
            estimatedFee = fallbackGasFees[chain] || 0.001;
            console_1.logger.debug("ECO_WITHDRAW", `Using fallback gas estimate: ${estimatedFee} ${currency}`);
        }
    }
    // Validate UTXO-based withdrawals BEFORE deducting balance
    // Note: We cannot accurately estimate UTXO network fees until transaction building
    // because fees depend on number of UTXOs, current fee rate, and change output
    if (["BTC", "LTC", "DOGE", "DASH"].includes(chain)) {
        ctx === null || ctx === void 0 ? void 0 : ctx.step(`Validating UTXO withdrawal for ${chain}`);
        console_1.logger.debug("ECO_WITHDRAW", `Pre-validating UTXO withdrawal for ${chain}`);
        const { calculateMinimumWithdrawal } = require("@b/api/(ext)/ecosystem/utils/utxo");
        try {
            const validationResult = await calculateMinimumWithdrawal(userWallet.id, chain, amount);
            if (!validationResult.isEconomical) {
                console_1.logger.error("ECO_WITHDRAW", `Withdrawal not economical: ${validationResult.reason}`);
                throw (0, error_1.createError)({ statusCode: 400, message: validationResult.reason });
            }
            console_1.logger.debug("ECO_WITHDRAW", `UTXO validation passed: withdrawal requires ${validationResult.utxoCount} UTXOs`);
            // Note: estimatedFee remains 0 for UTXO chains
            // Actual network fee will be calculated and deducted during transaction processing
        }
        catch (error) {
            console_1.logger.error("ECO_WITHDRAW", `UTXO validation error: ${error.message}`);
            throw error;
        }
    }
    else if (chain === "TRON") {
        ctx === null || ctx === void 0 ? void 0 : ctx.step("Estimating TRON transaction fees");
        // Handle Tron-specific logic
        const TronService = await (0, safe_imports_1.getTronService)();
        if (!TronService) {
            throw (0, error_1.createError)({ statusCode: 503, message: "Tron service not available" });
        }
        const tronService = await TronService.getInstance();
        // Check if the recipient address is activated
        const isActivated = await tronService.isAddressActivated(toAddress);
        if (!isActivated) {
            activationFee = 1; // 1 TRX required to activate a new account
        }
        // Get wallet data (private key, etc.)
        const walletData = await (0, wallet_1.getWalletData)(userWallet.id, chain);
        if (!walletData) {
            throw (0, error_1.createError)({ statusCode: 404, message: "Wallet data not found" });
        }
        // Fetch the user's Tron address
        const addresses = typeof userWallet.address === "string"
            ? JSON.parse(userWallet.address)
            : userWallet.address;
        const fromAddress = addresses[chain].address;
        // Convert amount to Sun (TRX's smaller unit)
        const amountSun = Math.round(amount * 1e6);
        // Estimate the transaction fee
        const estimatedFeeSun = await tronService.estimateTransactionFee(fromAddress, toAddress, amountSun);
        estimatedFee = estimatedFeeSun / 1e6; // Convert Sun to TRX
    }
    else if (chain === "XMR") {
        ctx === null || ctx === void 0 ? void 0 : ctx.step("Estimating Monero transaction fees");
        // Handle Monero-specific logic
        const MoneroService = await (0, safe_imports_1.getMoneroService)();
        if (!MoneroService) {
            throw (0, error_1.createError)({ statusCode: 503, message: "Monero service not available" });
        }
        const moneroService = await MoneroService.getInstance();
        estimatedFee = await moneroService.estimateMoneroFee();
    }
    ctx === null || ctx === void 0 ? void 0 : ctx.step("Calculating total fees and amounts");
    // Calculate the total fee for the transaction
    // For NATIVE EVM tokens, gas is paid FROM the withdrawal amount (not added on top)
    // For other chains, gas/network fees are added on top of the withdrawal amount
    const totalFee = isNativeEVM
        ? withdrawalFee + activationFee // Gas is paid from withdrawal, don't add it
        : withdrawalFee + activationFee + estimatedFee; // Other chains: add network fee
    // Calculate the total amount to deduct from the wallet (including fees)
    // Use parseFloat with toFixed to prevent floating-point precision errors
    const precision = (_g = (_f = token.precision) !== null && _f !== void 0 ? _f : token.decimals) !== null && _g !== void 0 ? _g : 8;
    const totalAmount = parseFloat((amount + totalFee).toFixed(precision));
    console_1.logger.debug("ECO_WITHDRAW", `Fee calculation: withdrawAmount=${amount}, withdrawalFee=${withdrawalFee}, activationFee=${activationFee}, estimatedNetworkFee=${estimatedFee}, totalFee=${totalFee}, totalToDeduct=${totalAmount}`);
    ctx === null || ctx === void 0 ? void 0 : ctx.step("Verifying balance and creating withdrawal transaction");
    // Use database transaction with row-level locking to prevent race conditions
    // This ensures that when multiple withdrawals happen simultaneously for the same wallet,
    // each one gets exclusive access to the wallet balance
    let transaction;
    await db_1.sequelize.transaction({
        isolationLevel: sequelize_1.Transaction.ISOLATION_LEVELS.READ_COMMITTED
    }, async (dbTransaction) => {
        // Lock the wallet row using FOR UPDATE
        // This prevents other concurrent withdrawals from reading the balance until we're done
        const lockedWallet = await db_1.models.wallet.findOne({
            where: { id: userWallet.id },
            lock: sequelize_1.Transaction.LOCK.UPDATE,
            transaction: dbTransaction,
        });
        if (!lockedWallet) {
            throw (0, error_1.createError)({ statusCode: 404, message: "Wallet not found" });
        }
        // For chain-specific withdrawals, check wallet_data balance (what's available on this chain)
        // instead of wallet.balance (which is total across all chains)
        const lockedWalletData = await db_1.models.walletData.findOne({
            where: {
                walletId: userWallet.id,
                chain: chain,
            },
            lock: sequelize_1.Transaction.LOCK.UPDATE,
            transaction: dbTransaction,
        });
        let availableBalance = lockedWalletData
            ? parseFloat(lockedWalletData.balance)
            : lockedWallet.balance;
        // For PERMIT tokens, check private ledger and subtract admin fees
        if (token.contractType === "PERMIT" && lockedWalletData) {
            const privateLedger = await db_1.models.ecosystemPrivateLedger.findOne({
                where: {
                    walletId: userWallet.id,
                    index: lockedWalletData.index,
                    currency: currency,
                    chain: chain,
                },
                transaction: dbTransaction,
            });
            if (privateLedger && privateLedger.offchainDifference) {
                const offchainDiff = parseFloat(privateLedger.offchainDifference) || 0;
                // If offchainDifference is positive, it means admin fees were collected
                // Subtract from available balance
                availableBalance = availableBalance - offchainDiff;
                console_1.logger.debug("ECO_WITHDRAW", `PERMIT token - adjusted for private ledger: walletDataBalance=${lockedWalletData.balance}, offchainDifference=${offchainDiff}, adjustedAvailableBalance=${availableBalance}`);
            }
        }
        console_1.logger.debug("ECO_WITHDRAW", `Balance check: walletBalance=${lockedWallet.balance}, walletDataBalance=${lockedWalletData === null || lockedWalletData === void 0 ? void 0 : lockedWalletData.balance}, contractType=${token.contractType}, availableBalance=${availableBalance}, totalRequired=${totalAmount}`);
        // Check balance with the chain-specific available balance
        if (availableBalance < totalAmount) {
            console_1.logger.error("ECO_WITHDRAW", `Insufficient funds: available ${availableBalance} < required ${totalAmount}`);
            throw (0, error_1.createError)({ statusCode: 400, message: "Insufficient funds" });
        }
        // Deduct the total amount from the user's wallet balance
        const walletResult = await (0, wallet_1.decrementWalletBalance)(lockedWallet, chain, totalAmount, dbTransaction);
        // Get the transaction record
        transaction = await db_1.models.transaction.findByPk(walletResult.transactionId, { transaction: dbTransaction });
        // Refresh userWallet.balance for response
        userWallet.balance = lockedWallet.balance - totalAmount;
    });
    ctx === null || ctx === void 0 ? void 0 : ctx.step("Adding transaction to withdrawal queue");
    // Add the transaction to the withdrawal queue
    const withdrawalQueue = withdrawalQueue_1.default.getInstance();
    withdrawalQueue.addTransaction(transaction.id);
    // Return updated wallet balance for immediate UI update
    return {
        transaction: transaction.get({ plain: true }),
        balance: userWallet.balance,
        method: chain,
        currency,
        message: "Withdrawal request submitted successfully",
        walletUpdate: {
            currency,
            balance: userWallet.balance,
            type: "ECO"
        }
    };
};
const calculateWithdrawalFee = (amount, currencyWithdrawalFee, minimumWithdrawalFee) => {
    const calculatedFee = (amount * currencyWithdrawalFee) / 100;
    return Math.max(calculatedFee, minimumWithdrawalFee);
};
async function findWalletByAddress(toAddress) {
    const wallets = await db_1.models.wallet.findAll({
        where: {
            type: "ECO",
        },
    });
    for (const wallet of wallets) {
        const addresses = (0, index_post_1.parseAddresses)(wallet.address);
        for (const chain in addresses) {
            if (addresses[chain].address === toAddress) {
                return wallet;
            }
        }
    }
    return null;
}
