PHP WebShell

Текущая директория: /var/www/bitcardoApp/models/crypto

Просмотр файла: tron_deposit_scanner.php

<?php
// models/crypto/tron_deposit_scanner.php

namespace Models\Crypto;

class TronDepositScanner
{
    private \mysqli $db;
    private string $apiBase;
    private string $apiKey;
    private string $usdtContract;

    // Minimum confirmations per coin
    private array $minConf = [
        'TRX'        => 20,
        'USDT-TRC20' => 20,
    ];

    public function __construct(\mysqli $conn)
    {
        $this->db = $conn;

        $config = include __DIR__ . '/../../config/tron_config.php';
        $this->apiBase      = rtrim($config['network'], '/');
        $this->apiKey       = $config['api_key'];
        $this->usdtContract = $config['usdt_contract']; // TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
    }

    /**
     * Scan all active TRX wallets (one per TRON address).
     * For each address:
     *  - scan TRX deposits
     *  - scan USDT-TRC20 deposits
     */
    public function scanAllUserWallets(): void
    {
        $sql = "SELECT wallet_id, user_id, wallet_add
                FROM user_wallets
                WHERE coin = 'TRX' AND wallet_status = 'active'";

        $res = $this->db->query($sql);
        if (!$res) {
            echo "DB error fetching TRX wallets: " . $this->db->error . PHP_EOL;
            return;
        }

        if ($res->num_rows === 0) {
            echo "No TRX user wallets found to scan." . PHP_EOL;
            return;
        }

        echo "Scanning {$res->num_rows} TRX user wallet(s)..." . PHP_EOL;

        while ($row = $res->fetch_assoc()) {
            $userId  = (int)$row['user_id'];
            $address = $row['wallet_add'];

            echo "  → Address {$address} (user_id={$userId})" . PHP_EOL;

            $this->scanTrxDepositsForAddress($userId, $address);
            $this->scanUsdtDepositsForAddress($userId, $address);
        }

        $res->free();

        echo "Scan finished." . PHP_EOL;
    }

    /**
     * Helper: find the wallet_id for a specific coin on a given address for a user.
     */
    private function getWalletIdForCoin(int $userId, string $address, string $coin): ?int
    {
        $sql = "SELECT wallet_id 
                FROM user_wallets
                WHERE user_id = ? AND wallet_add = ? AND coin = ? AND wallet_status = 'active'
                LIMIT 1";
        $stmt = $this->db->prepare($sql);
        if (!$stmt) {
            echo "DB error (getWalletIdForCoin): " . $this->db->error . PHP_EOL;
            return null;
        }
        $stmt->bind_param("iss", $userId, $address, $coin);
        $stmt->execute();
        $res = $stmt->get_result();
        $row = $res->fetch_assoc();
        $stmt->close();

        return $row ? (int)$row['wallet_id'] : null;
    }

    /**
     * Scan inbound TRX transfers for a single address.
     */
    private function scanTrxDepositsForAddress(int $userId, string $address): void
    {
        $url = $this->apiBase
             . "/v1/accounts/{$address}/transactions?only_to=true&limit=50&order_by=block_timestamp,desc";

        $data = $this->getJson($url);
        if (!$data || empty($data['data'])) {
            return;
        }

        foreach ($data['data'] as $tx) {
            $txid = $tx['txID'] ?? null;
            if (!$txid) {
                continue;
            }

            $ret0 = $tx['ret'][0]['contractRet'] ?? '';
            if ($ret0 !== 'SUCCESS') {
                continue;
            }

            $amountSun = $tx['raw_data']['contract'][0]['parameter']['value']['amount'] ?? null;
            if ($amountSun === null) {
                continue;
            }

            $blockTs       = $tx['block_timestamp'] ?? null; // ms
            $confirmations = $this->estimateConfirmations($blockTs);

            $amountRaw = (int)$amountSun;
            $amountDec = $amountRaw / 1000000; // TRX has 6 decimals

            $this->processDeposit(
                network:       'TRON',
                coin:          'TRX',
                userId:        $userId,
                walletAddress: $address,
                txid:          $txid,
                blockNum:      null,
                confirmations: $confirmations,
                amountRaw:     $amountRaw,
                amountDec:     $amountDec,
                txRaw:         $tx
            );
        }
    }

    /**
     * Scan inbound USDT-TRC20 transfers for a single address.
     */
    private function scanUsdtDepositsForAddress(int $userId, string $address): void
    {
        $url = $this->apiBase
             . "/v1/accounts/{$address}/transactions/trc20?only_to=true&limit=50&order_by=block_timestamp,desc";

        $data = $this->getJson($url);
        if (!$data || empty($data['data'])) {
            return;
        }

        foreach ($data['data'] as $tx) {
            $txid = $tx['transaction_id'] ?? null;
            if (!$txid) {
                continue;
            }

            $contractAddress = $tx['token_info']['address'] ?? '';
            if (!$contractAddress || strcasecmp($contractAddress, $this->usdtContract) !== 0) {
                continue; // Not USDT-TRC20
            }

            $valueRaw = $tx['value'] ?? null;
            if ($valueRaw === null) {
                continue;
            }

            $blockTs       = $tx['block_timestamp'] ?? null; // ms
            $confirmations = $this->estimateConfirmations($blockTs);

            $amountRaw = (int)$valueRaw;
            $amountDec = $amountRaw / 1000000; // 6 decimals

            $this->processDeposit(
                network:       'TRON',
                coin:          'USDT-TRC20',
                userId:        $userId,
                walletAddress: $address,
                txid:          $txid,
                blockNum:      null,
                confirmations: $confirmations,
                amountRaw:     $amountRaw,
                amountDec:     $amountDec,
                txRaw:         $tx
            );
        }
    }

    /**
     * Insert/update crypto_deposit_log AND, once confirmations >= threshold,
     * credit user_wallets and insert into transactions.
     *
     * NOTE: We now ALWAYS resolve wallet_id inside here using (user_id, address, coin).
     */
    private function processDeposit(
        string $network,
        string $coin,
        int $userId,
        string $walletAddress,
        string $txid,
        ?int $blockNum,
        int $confirmations,
        int $amountRaw,
        float $amountDec,
        array $txRaw
    ): void {
        $min = $this->minConf[$coin] ?? 20;

        try {
            $this->db->begin_transaction();

            // 0) Resolve wallet_id based on coin
            $walletId = $this->getWalletIdForCoin($userId, $walletAddress, $coin);
            if (!$walletId) {
                // no matching wallet row, don't log or credit
                echo "    No wallet row for {$coin} (user_id={$userId}, address={$walletAddress}), skipping.\n";
                $this->db->commit();
                return;
            }

            // 1) Check if tx already logged
            $stmt = $this->db->prepare("
                SELECT id, credited, confirmations
                FROM crypto_deposit_log
                WHERE network = ? AND coin = ? AND txid = ?
                LIMIT 1
            ");
            if (!$stmt) {
                throw new \Exception("Prepare failed (check log): " . $this->db->error);
            }
            $stmt->bind_param("sss", $network, $coin, $txid);
            $stmt->execute();
            $res      = $stmt->get_result();
            $existing = $res->fetch_assoc();
            $stmt->close();

            if ($existing) {
                if ((int)$existing['credited'] === 1) {
                    // Already credited, nothing to do
                    $this->db->commit();
                    return;
                }

                // Update confirmations if they increased
                if ($confirmations > (int)$existing['confirmations']) {
                    $stmtU = $this->db->prepare("
                        UPDATE crypto_deposit_log
                        SET confirmations = ?, updated_at = NOW()
                        WHERE id = ?
                    ");
                    if ($stmtU) {
                        $logId = (int)$existing['id'];
                        $stmtU->bind_param("ii", $confirmations, $logId);
                        $stmtU->execute();
                        $stmtU->close();
                    }
                }
            } else {
                // Insert new log row with credited = 0
                $stmtI = $this->db->prepare("
                    INSERT INTO crypto_deposit_log
                        (network, coin, user_id, wallet_id, wallet_add, txid, block_num,
                         amount_raw, amount_dec, confirmations, credited, created_at, updated_at)
                    VALUES
                        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NOW(), NOW())
                ");
                if (!$stmtI) {
                    throw new \Exception("Prepare failed (insert log): " . $this->db->error);
                }
                // Types: s s i i s s i i d i  => "ssiissiidi"
                $blockNumInt = $blockNum ?? 0;
                $stmtI->bind_param(
                    "ssiissiidi",
                    $network,        // s
                    $coin,           // s
                    $userId,         // i
                    $walletId,       // i
                    $walletAddress,  // s
                    $txid,           // s
                    $blockNumInt,    // i
                    $amountRaw,      // i
                    $amountDec,      // d
                    $confirmations   // i
                );
                $stmtI->execute();
                $stmtI->close();

                echo "    Logged new {$coin} deposit tx {$txid} (confirmations={$confirmations}, wallet_id={$walletId})" . PHP_EOL;
            }

            // 2) If confirmations below threshold, don't credit yet
            if ($confirmations < $min) {
                $this->db->commit();
                return;
            }

            // 3) Credit user wallet balance
            $stmtW = $this->db->prepare("
                SELECT balance
                FROM user_wallets
                WHERE wallet_id = ?
                FOR UPDATE
            ");
            if (!$stmtW) {
                throw new \Exception("Prepare failed (select wallet): " . $this->db->error);
            }
            $stmtW->bind_param("i", $walletId);
            $stmtW->execute();
            $resW   = $stmtW->get_result();
            $wallet = $resW->fetch_assoc();
            $stmtW->close();

            if (!$wallet) {
                throw new \Exception("Wallet not found (ID {$walletId})");
            }

            $currentBal = (float)$wallet['balance'];
            $newBal     = $currentBal + $amountDec;

            $stmtU2 = $this->db->prepare("
                UPDATE user_wallets
                SET balance = ?, updated_at = NOW()
                WHERE wallet_id = ?
            ");
            if (!$stmtU2) {
                throw new \Exception("Prepare failed (update wallet): " . $this->db->error);
            }
            $stmtU2->bind_param("di", $newBal, $walletId);
            $stmtU2->execute();
            $stmtU2->close();

            // 4) Insert into transactions
            $senderAddress   = 'external';
            $receiverAddress = $walletAddress;
            $provider        = 'tron';
            $metaJson        = json_encode($txRaw, JSON_UNESCAPED_SLASHES);

            $stmtT = $this->db->prepare("
                INSERT INTO transactions
                    (coin, user_id, wallet_id, sender_address, receiver_address, amount,
                     type, txid, confirmation, status, applied, provider, provider_meta, created_at, updated_at)
                VALUES
                    (?, ?, ?, ?, ?, ?, 'deposit', ?, ?, 'completed', 1, ?, ?, NOW(), NOW())
            ");
            if (!$stmtT) {
                throw new \Exception("Prepare failed (insert transaction): " . $this->db->error);
            }
            // Types: s i i s s d s i s s  => "siissdsiss"
            $stmtT->bind_param(
                "siissdsiss",
                $coin,
                $userId,
                $walletId,
                $senderAddress,
                $receiverAddress,
                $amountDec,
                $txid,
                $confirmations,
                $provider,
                $metaJson
            );
            $stmtT->execute();
            $stmtT->close();

            // 5) Mark crypto_deposit_log as credited
            $stmtL = $this->db->prepare("
                UPDATE crypto_deposit_log
                SET credited = 1, confirmations = ?, updated_at = NOW()
                WHERE network = ? AND coin = ? AND txid = ?
            ");
            if (!$stmtL) {
                throw new \Exception("Prepare failed (update log credited): " . $this->db->error);
            }
            $stmtL->bind_param("isss", $confirmations, $network, $coin, $txid);
            $stmtL->execute();
            $stmtL->close();

            $this->db->commit();

            echo "    Credited {$amountDec} {$coin} to user {$userId} wallet_id={$walletId} from tx {$txid}" . PHP_EOL;

        } catch (\Throwable $e) {
            $this->db->rollback();
            echo "    Error processing {$coin} deposit {$txid}: " . $e->getMessage() . PHP_EOL;
        }
    }

    /**
     * Estimate confirmations using timestamp (Tron ~3 sec/block).
     */
    private function estimateConfirmations(?int $blockTsMs): int
    {
        if ($blockTsMs === null || $blockTsMs <= 0) {
            return 0;
        }
        $nowMs  = (int)round(microtime(true) * 1000);
        $ageSec = max(0, ($nowMs - $blockTsMs) / 1000);
        $conf   = (int)floor($ageSec / 3);   // ~3s per block

        return ($conf > 1000) ? 1000 : $conf;
    }

    /**
     * Simple TronGrid GET helper.
     */
    private function getJson(string $url): ?array
    {
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => ["TRON-PRO-API-KEY: " . $this->apiKey],
            CURLOPT_TIMEOUT        => 20,
        ]);

        $resp = curl_exec($ch);
        if ($resp === false) {
            curl_close($ch);
            return null;
        }
        curl_close($ch);

        $decoded = json_decode($resp, true);
        return is_array($decoded) ? $decoded : null;
    }
}

Выполнить команду


Для локальной разработки. Не используйте в интернете!