PHP WebShell

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

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

<?php
/**
 * cron/update_online_rates.php
 *
 * Fetch real rates for BTC, ETH, SOL, USDT, NGN (base = USDT) every 3 minutes.
 * Store:
 *  - rate: real online rate (USDT per COIN; for NGN it's NGN per USDT)
 *  - buy_rate  = rate - (margin_percent% of rate)
 *  - sell_rate = rate + (margin_percent% of rate)
 *
 * IMPORTANT: margin_percent is ADMIN-CONTROLLED.
 * This script NEVER writes or updates margin_percent. It only READS it.
 */

declare(strict_types=1);
ini_set('display_errors', '0');
error_reporting(E_ALL);

require_once __DIR__ . "/../config/db_config.php"; // $conn (mysqli)

$COINS    = ['BTC','ETH','SOL','USDT','NGN'];
$LOG_FILE = __DIR__ . "/logs/update_online_rates.log";
@is_dir(dirname($LOG_FILE)) || @mkdir(dirname($LOG_FILE), 0775, true);

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

function curl_json(string $url): ?array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 6,
        CURLOPT_TIMEOUT        => 6,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
        CURLOPT_USERAGENT      => 'bitcardo-rates-cron/3.0',
    ]);
    $res  = curl_exec($ch);
    $err  = curl_error($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($err || $code < 200 || $code >= 300 || !$res) return null;
    $json = json_decode($res, true);
    return json_last_error() === JSON_ERROR_NONE ? $json : null;
}

/** Crypto: USDT per COIN (USDT base). USDT=1.0 */
function get_crypto_rate(string $coin): ?float {
    $coin = strtoupper($coin);
    if ($coin === 'USDT') return 1.0;

    // Primary: Binance
    $binance = ['BTC'=>'BTCUSDT','ETH'=>'ETHUSDT','SOL'=>'SOLUSDT'];
    if (isset($binance[$coin])) {
        $j = curl_json("https://api.binance.com/api/v3/ticker/price?symbol={$binance[$coin]}");
        if ($j && isset($j['price']) && is_numeric($j['price'])) return (float)$j['price'];
    }

    // Fallback: CoinGecko (USD ~= USDT)
    $cg = ['BTC'=>'bitcoin','ETH'=>'ethereum','SOL'=>'solana'];
    if (isset($cg[$coin])) {
        $j = curl_json("https://api.coingecko.com/api/v3/simple/price?ids={$cg[$coin]}&vs_currencies=usd");
        if ($j && isset($j[$cg[$coin]]['usd']) && is_numeric($j[$cg[$coin]]['usd'])) return (float)$j[$cg[$coin]]['usd'];
    }
    return null;
}

/** NGN per USDT (≈ NGN per USD) */
function get_ngn_per_usdt(): ?float {
    $j = curl_json("https://api.exchangerate.host/latest?base=USD&symbols=NGN");
    if ($j && isset($j['rates']['NGN']) && is_numeric($j['rates']['NGN'])) return (float)$j['rates']['NGN'];

    $j = curl_json("https://open.er-api.com/v6/latest/USD");
    if ($j && isset($j['rates']['NGN']) && is_numeric($j['rates']['NGN'])) return (float)$j['rates']['NGN'];

    return null;
}

/**
 * Read margin_percent for a coin without modifying it.
 * Priority: online_coin_rates.margin_percent -> coin_rates.margin_percent -> 0
 */
function read_margin_percent(mysqli $conn, string $coin): float {
    $coin = strtoupper($coin);

    // 1) online_coin_rates
    $sql1 = "SELECT margin_percent FROM online_coin_rates WHERE UPPER(coin)=?";
    $stmt = $conn->prepare($sql1);
    $stmt->bind_param("s", $coin);
    $stmt->execute();
    $row = $stmt->get_result()->fetch_assoc();
    $stmt->close();
    if ($row && $row['margin_percent'] !== null) {
        return max(0.0, (float)$row['margin_percent']);
    }

    // 2) coin_rates
    $sql2 = "SELECT margin_percent FROM coin_rates WHERE UPPER(coin)=?";
    $stmt = $conn->prepare($sql2);
    $stmt->bind_param("s", $coin);
    $stmt->execute();
    $row = $stmt->get_result()->fetch_assoc();
    $stmt->close();
    if ($row && $row['margin_percent'] !== null) {
        return max(0.0, (float)$row['margin_percent']);
    }

    // 3) default
    return 0.0;
}

/** Upsert without touching margin_percent */
function upsert_rate(mysqli $conn, string $coin, float $rate, float $buy_rate, float $sell_rate, string $source): bool {
    $sql = "
        INSERT INTO online_coin_rates (coin, rate, buy_rate, sell_rate, source, fetched_at)
        VALUES (?, ?, ?, ?, ?, NOW())
        ON DUPLICATE KEY UPDATE
            rate      = VALUES(rate),
            buy_rate  = VALUES(buy_rate),
            sell_rate = VALUES(sell_rate),
            source    = VALUES(source),
            fetched_at= VALUES(fetched_at)
    ";
    $stmt = $conn->prepare($sql);
    if (!$stmt) return false;
    $coin = strtoupper($coin);
    $stmt->bind_param("sddds", $coin, $rate, $buy_rate, $sell_rate, $source);
    $ok = $stmt->execute();
    $stmt->close();
    return $ok;
}

// Concurrency guard
$gotLock = false;
if ($r = $conn->query("SELECT GET_LOCK('update_online_rates_lock_v4', 10) AS l")) {
    $gotLock = ((int)$r->fetch_assoc()['l'] === 1);
    $r->close();
}
if (!$gotLock) {
    log_line("Another instance running; exiting.");
    exit;
}

// Main
try {
    $summary = [];

    foreach ($COINS as $coin) {
        $coin  = strtoupper($coin);
        $rate  = ($coin === 'NGN') ? get_ngn_per_usdt() : get_crypto_rate($coin);
        $src   = ($coin === 'NGN') ? 'fx:exchangeratehost|erapi' : 'crypto:binance|coingecko';

        if ($rate && $rate > 0) {
            $margin = read_margin_percent($conn, $coin);            // read-only
            $delta  = $rate * ($margin / 100.0);
            $buy    = $rate - $delta;                                // coin -> USDT
            $sell   = $rate + $delta;                                // USDT -> coin

            $ok = upsert_rate($conn, $coin, $rate, $buy, $sell, $src);
            $summary[] = "$coin=" . ($ok ? "OK" : "FAIL") . " rate:$rate margin:$margin";
        } else {
            $summary[] = "$coin=NO_DATA";
        }
    }

    log_line("Update complete: " . implode(', ', $summary));
} catch (Throwable $e) {
    log_line("ERROR: " . $e->getMessage());
} finally {
    @$conn->query("SELECT RELEASE_LOCK('update_online_rates_lock_v4')");
}

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


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