PHP WebShell

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

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

<?php
/**
 * /var/www/bitcardoApp/cron/update_cwallet_balances.php
 *
 * MAINNET ONLY
 * - BTC: use BitGo wallet ID from cwallet.wallet_add_id (NOT address)
 * - SOL: address-based (Solana RPC) (unchanged unless you later want BitGo as well)
 * - TRX: address-based (TronGrid) (UNCHANGED logic)
 * - USDT-TRC20: address-based (TronGrid) (may 429 without TRON_API_KEY)
 */

ini_set('display_errors', '0');
error_reporting(E_ALL);
date_default_timezone_set('Africa/Lagos');

/* =========================================================
   CONFIG (keep everything here)
   ========================================================= */

$ROOT = realpath(__DIR__ . '/../'); // /var/www/bitcardoApp
if (!$ROOT) exit;

// Log file
$logDir  = $ROOT . '/storage/logs';
$logFile = $logDir . '/cwallet_balance_cron.log';

// ----- BitGo MAINNET (BTC uses this) -----
if (!defined('BITGO_API_BASE_URL')) {
    // Production base
    define('BITGO_API_BASE_URL', 'https://app.bitgo.com/api/v2');
}
if (!defined('BITGO_ACCESS_TOKEN')) {
    // Put your production BitGo token here
    define('BITGO_ACCESS_TOKEN', 'v2x684ccb535d69ea3fdcdf8657164bba3796f71d61f5ec0a0065bc202e637cce24'); // REQUIRED for BTC balance via wallet_add_id
}

// ----- Solana MAINNET -----
if (!defined('SOLANA_RPC_URL')) {
    define('SOLANA_RPC_URL', 'https://api.mainnet-beta.solana.com');
}

// ----- Tron MAINNET -----
if (!defined('TRON_HTTP_API')) {
    define('TRON_HTTP_API', 'https://api.trongrid.io');
}
if (!defined('TRON_API_KEY')) {
    define('TRON_API_KEY', '02e7b10b-32ed-47fb-a975-60c7d007d411'); // recommended to avoid 429 (USDT-TRC20)
}
if (!defined('USDT_TRC20_CONTRACT')) {
    define('USDT_TRC20_CONTRACT', 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t');
}

/* =========================================================
   DB include
   ========================================================= */

$db1 = $ROOT . '/config/db_config.php';
$db2 = $ROOT . '/backyard/config/db_config.php';

if (file_exists($db1)) require_once $db1;
elseif (file_exists($db2)) require_once $db2;
else exit;

if (!isset($conn) || !($conn instanceof mysqli)) exit;

/* =========================================================
   Lock (prevent overlap)
   ========================================================= */

$lockFile = sys_get_temp_dir() . '/bitcardo_cwallet_balance_cron.lock';
$lockFp = @fopen($lockFile, 'c+');
if (!$lockFp || !flock($lockFp, LOCK_EX | LOCK_NB)) exit;

/* =========================================================
   Helpers
   ========================================================= */

if (!is_dir($logDir)) @mkdir($logDir, 0775, true);

function log_line(string $msg): void {
    global $logFile;
    $ts = date('Y-m-d H:i:s');
    @file_put_contents($logFile, "[$ts] $msg\n", FILE_APPEND);
}

function col_exists(mysqli $conn, string $table, string $col): bool {
    $table = mysqli_real_escape_string($conn, $table);
    $col   = mysqli_real_escape_string($conn, $col);
    $res = mysqli_query($conn, "SHOW COLUMNS FROM `{$table}` LIKE '{$col}'");
    if (!$res) return false;
    $ok = (mysqli_num_rows($res) > 0);
    mysqli_free_result($res);
    return $ok;
}

function detect_balance_col(mysqli $conn): string {
    foreach (['wallet_balance','balance','available_balance','cwallet_balance'] as $c) {
        if (col_exists($conn, 'cwallet', $c)) return $c;
    }
    return 'wallet_balance';
}

function detect_address_col(mysqli $conn): ?string {
    if (col_exists($conn, 'cwallet', 'wallet_add')) return 'wallet_add';
    if (col_exists($conn, 'cwallet', 'address'))    return 'address';
    return null;
}

function curl_request(string $method, string $url, array $headers = [], ?string $body = null, int $timeout = 25): array {
    $ch = curl_init($url);
    $opts = [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_TIMEOUT        => $timeout,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_CUSTOMREQUEST  => $method,
    ];
    if ($body !== null) $opts[CURLOPT_POSTFIELDS] = $body;
    curl_setopt_array($ch, $opts);

    $raw  = curl_exec($ch);
    $cerr = curl_error($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return [
        'ok'   => ($raw !== false && $code >= 200 && $code < 300),
        'code' => $code,
        'cerr' => $cerr ?: null,
        'raw'  => ($raw === false ? null : $raw),
    ];
}

function curl_json_get(string $url, array $headers = [], int $timeout = 25): array {
    $r = curl_request('GET', $url, $headers, null, $timeout);
    if ($r['raw'] === null) return ['ok'=>false,'code'=>$r['code'],'err'=>($r['cerr'] ?? 'curl_error'),'data'=>null];
    $data = json_decode($r['raw'], true);
    if (!is_array($data)) return ['ok'=>false,'code'=>$r['code'],'err'=>'invalid_json','data'=>null];
    return ['ok'=>$r['ok'],'code'=>$r['code'],'err'=>($r['ok'] ? null : 'http_'.$r['code']),'data'=>$data];
}

function curl_json_post(string $url, array $payload, array $headers = [], int $timeout = 25): array {
    $headers = array_merge(['Content-Type: application/json','Accept: application/json'], $headers);
    $r = curl_request('POST', $url, $headers, json_encode($payload), $timeout);
    if ($r['raw'] === null) return ['ok'=>false,'code'=>$r['code'],'err'=>($r['cerr'] ?? 'curl_error'),'data'=>null];
    $data = json_decode($r['raw'], true);
    if (!is_array($data)) return ['ok'=>false,'code'=>$r['code'],'err'=>'invalid_json','data'=>null];
    return ['ok'=>$r['ok'],'code'=>$r['code'],'err'=>($r['ok'] ? null : 'http_'.$r['code']),'data'=>$data];
}

function retry(callable $fn, int $tries = 4, int $baseSleepMs = 600): array {
    $last = ['ok'=>false,'err'=>'retry_failed'];
    for ($i=1; $i<=$tries; $i++) {
        $last = $fn();
        if (!empty($last['ok'])) return $last;
        usleep((int)($baseSleepMs * $i) * 1000);
    }
    return $last;
}

/* =========================================================
   BTC via BitGo walletId (cwallet.wallet_add_id)
   ========================================================= */

function bitgo_enabled(): bool {
    return (BITGO_API_BASE_URL !== '' && BITGO_ACCESS_TOKEN !== '');
}

/**
 * BTC wallet total from BitGo wallet endpoint.
 * Returns BTC in BTC units.
 */
function fetch_bitgo_btc_wallet_balance(string $walletId): array {
    if (!bitgo_enabled()) return ['ok'=>false,'bal'=>0,'err'=>'bitgo_not_configured'];
    $walletId = trim($walletId);
    if ($walletId === '') return ['ok'=>false,'bal'=>0,'err'=>'missing_wallet_id'];

    $url = rtrim(BITGO_API_BASE_URL, '/') . "/btc/wallet/" . rawurlencode($walletId);
    $headers = [
        'Accept: application/json',
        'Authorization: Bearer ' . BITGO_ACCESS_TOKEN,
    ];

    $r = curl_json_get($url, $headers, 25);
    if (!$r['ok']) return ['ok'=>false,'bal'=>0,'err'=>"bitgo_http_{$r['code']}"];

    $data = $r['data'];

    // Prefer spendableBalance; fallback to balance
    $base = $data['spendableBalance'] ?? $data['balance'] ?? null;
    if ($base === null || !is_numeric($base)) return ['ok'=>false,'bal'=>0,'err'=>'bitgo_bad_balance'];

    $sats = (float)$base;
    $btc  = $sats / 1e8;

    return ['ok'=>true,'bal'=>$btc,'err'=>null];
}

/* =========================================================
   SOL via Solana RPC (address-based)
   ========================================================= */

function fetch_sol_balance_mainnet(string $address): array {
    if ($address === '') return ['ok'=>false,'bal'=>0,'err'=>'missing_address'];

    $payload = [
        'jsonrpc' => '2.0',
        'id'      => 1,
        'method'  => 'getBalance',
        'params'  => [$address],
    ];

    $r = curl_json_post(SOLANA_RPC_URL, $payload, [], 25);
    if (!$r['ok']) return ['ok'=>false,'bal'=>0,'err'=>"sol_http_{$r['code']}"];

    if (isset($r['data']['error'])) {
        $msg = is_array($r['data']['error']) ? ($r['data']['error']['message'] ?? 'rpc_error') : 'rpc_error';
        return ['ok'=>false,'bal'=>0,'err'=>"sol_rpc_{$msg}"];
    }

    $lamports = $r['data']['result']['value'] ?? null;
    if (!is_numeric($lamports)) return ['ok'=>false,'bal'=>0,'err'=>'sol_bad_response'];

    return ['ok'=>true,'bal'=>(((float)$lamports) / 1e9),'err'=>null];
}

/* =========================================================
   TRON (TRX + USDT-TRC20) (TRX logic unchanged)
   ========================================================= */

function tron_headers(): array {
    $h = ['Accept: application/json'];
    if (TRON_API_KEY !== '') $h[] = 'TRON-PRO-API-KEY: ' . TRON_API_KEY;
    return $h;
}

function tron_account_mainnet(string $address): array {
    if ($address === '') return ['ok'=>false,'acc'=>null,'err'=>'missing_address'];
    $base = rtrim(TRON_HTTP_API, '/');
    $url  = $base . "/v1/accounts/" . rawurlencode(trim($address));

    return retry(function() use ($url) {
        $r = curl_json_get($url, tron_headers(), 25);
        if (!$r['ok']) {
            return ['ok'=>false,'acc'=>null,'err'=>"tron_http_{$r['code']}"];
        }
        $data = $r['data']['data'] ?? [];
        if (empty($data[0]) || !is_array($data[0])) {
            return ['ok'=>true,'acc'=>['balance'=>0,'trc20'=>[]],'err'=>null];
        }
        return ['ok'=>true,'acc'=>$data[0],'err'=>null];
    }, 4, 700);
}

function fetch_trx_balance_mainnet(string $address): array {
    $r = tron_account_mainnet($address);
    if (!$r['ok']) return ['ok'=>false,'bal'=>0,'err'=>$r['err']];
    $sun = (float)($r['acc']['balance'] ?? 0);
    return ['ok'=>true,'bal'=>($sun / 1e6),'err'=>null];
}

function fetch_usdt_trc20_balance_mainnet(string $address): array {
    $r = tron_account_mainnet($address);
    if (!$r['ok']) return ['ok'=>false,'bal'=>0,'err'=>$r['err']];

    $acc = $r['acc'];
    $trc20 = $acc['trc20'] ?? [];
    $raw = '0';

    if (is_array($trc20)) {
        foreach ($trc20 as $row) {
            if (is_array($row) && isset($row[USDT_TRC20_CONTRACT])) {
                $raw = (string)$row[USDT_TRC20_CONTRACT];
                break;
            }
        }
    }

    if (!is_numeric($raw)) $raw = '0';
    return ['ok'=>true,'bal'=>(((float)$raw) / 1e6),'err'=>null];
}

/* =========================================================
   MAIN
   ========================================================= */

$balanceCol = detect_balance_col($conn);
$addrCol    = detect_address_col($conn);

// BTC wallet id column must exist for your request:
$btcWalletIdCol = col_exists($conn, 'cwallet', 'wallet_add_id') ? 'wallet_add_id' : null;

if (!$addrCol) {
    log_line("ERROR: cwallet has no address column (wallet_add/address).");
    exit;
}
if (!$btcWalletIdCol) {
    log_line("ERROR: cwallet.wallet_add_id not found. BTC requires wallet_add_id.");
    exit;
}

$res = mysqli_query($conn, "SELECT * FROM `cwallet` ORDER BY `cwallet_id` ASC");
if (!$res) {
    log_line("ERROR: cannot query cwallet: " . mysqli_error($conn));
    exit;
}

log_line("Run start. addrCol={$addrCol} balanceCol={$balanceCol} BTC_walletIdCol={$btcWalletIdCol} BITGO=" . (bitgo_enabled() ? 'enabled' : 'disabled') . " TRON_KEY=" . (TRON_API_KEY !== '' ? 'set' : 'not_set'));

$updated = 0;
$failed  = 0;

while ($w = mysqli_fetch_assoc($res)) {
    $cid  = (int)($w['cwallet_id'] ?? 0);
    $coin = strtoupper(trim((string)($w['coin'] ?? '')));

    $addr = trim((string)($w[$addrCol] ?? ''));
    $oldDb = isset($w[$balanceCol]) ? (string)$w[$balanceCol] : '';

    if ($cid <= 0 || $coin === '') continue;

    try {
        $result = ['ok'=>false,'bal'=>0,'err'=>'no_fetcher'];

        // ✅ BTC: use BitGo walletId from wallet_add_id
        if ($coin === 'BTC') {
            $walletId = trim((string)($w[$btcWalletIdCol] ?? ''));
            if ($walletId === '') {
                $failed++;
                log_line("FAIL cwallet_id={$cid} coin=BTC err=missing_wallet_add_id");
                continue;
            }
            $result = fetch_bitgo_btc_wallet_balance($walletId);
            if (!$result['ok']) {
                $failed++;
                log_line("FAIL cwallet_id={$cid} coin=BTC wallet_add_id={$walletId} err={$result['err']}");
                continue;
            }

            $newBal = (float)$result['bal'];
            $balStr = number_format($newBal, 12, '.', '');

            log_line("FETCH cwallet_id={$cid} coin=BTC wallet_add_id={$walletId} db_old={$oldDb} new={$balStr}");

        } else {
            // Other coins remain address-based
            if ($addr === '') {
                $failed++;
                log_line("FAIL cwallet_id={$cid} coin={$coin} err=missing_address");
                continue;
            }

            if ($coin === 'SOL') {
                $result = fetch_sol_balance_mainnet($addr);
            } elseif ($coin === 'TRX') {
                // ✅ TRX unchanged
                $result = fetch_trx_balance_mainnet($addr);
            } elseif (in_array($coin, ['USDT-TRC20','USDTTRC20','USDT_TRX'], true)) {
                $result = fetch_usdt_trc20_balance_mainnet($addr);
            } else {
                $result = ['ok'=>false,'bal'=>0,'err'=>"unsupported_coin_{$coin}"];
            }

            if (!$result['ok']) {
                $failed++;
                log_line("FAIL cwallet_id={$cid} coin={$coin} addr={$addr} err={$result['err']}");
                continue;
            }

            $newBal = (float)$result['bal'];
            $balStr = number_format($newBal, 12, '.', '');

            log_line("FETCH cwallet_id={$cid} coin={$coin} addr={$addr} db_old={$oldDb} new={$balStr}");
        }

        // Write to DB (string bind to preserve decimals)
        $stmt = mysqli_prepare($conn, "UPDATE `cwallet` SET `{$balanceCol}` = ? WHERE `cwallet_id` = ? LIMIT 1");
        if (!$stmt) {
            $failed++;
            log_line("FAIL cwallet_id={$cid} coin={$coin} err=prepare_failed " . mysqli_error($conn));
            continue;
        }

        mysqli_stmt_bind_param($stmt, 'si', $balStr, $cid);
        $ok = mysqli_stmt_execute($stmt);
        mysqli_stmt_close($stmt);

        if (!$ok) {
            $failed++;
            log_line("FAIL cwallet_id={$cid} coin={$coin} err=update_failed " . mysqli_error($conn));
            continue;
        }

        $updated++;
        log_line("OK   cwallet_id={$cid} coin={$coin} balance={$balStr}");

    } catch (\Throwable $e) {
        $failed++;
        log_line("EXC  cwallet_id={$cid} coin={$coin} " . $e->getMessage());
        continue;
    }
}

mysqli_free_result($res);

log_line("Run done. updated={$updated} failed={$failed}");

@flock($lockFp, LOCK_UN);
@fclose($lockFp);

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


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