PHP WebShell

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

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

<?php
declare(strict_types=1);

@ini_set('display_errors', '0');
session_start();

require_once "../../config/db_config.php";
require_once "../../config/bitgo_config.php";

/* Absolute paths to result pages (no query params; use session) */
const SUCCESS_PATH = '../../user/crypto/send_crypto_success.php';
const FAILED_PATH  = '../../user/crypto/send_crypto_failed.php';

function fail_redirect(string $msg): void {
  $_SESSION['tx_failed'] = ['reason' => $msg];
  header("Location: " . FAILED_PATH);
  exit();
}

/* platform fee toggle */
function is_platform_fee_enabled(mysqli $conn, string $coin): bool {
  $key = 'platform_fee_' . strtolower($coin);
  $stmt = $conn->prepare("SELECT setting_value FROM site_settings WHERE setting_key = ? LIMIT 1");
  if (!$stmt) return true;
  $stmt->bind_param('s', $key);
  $stmt->execute();
  $stmt->bind_result($val);
  $found = $stmt->fetch();
  $stmt->close();
  if (!$found) return true;
  return (string)$val === '1';
}

/* tiny http + providers (same as estimator; omitted here for brevity if you already switched to quotes) */
/* We still keep fetch_usd_rate as ultimate fallback when a quote is missing (shouldn’t happen). */
function http_get_json(string $url, float $timeout = 1.5): ?array {
  $ch = curl_init($url);
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => $timeout,
    CURLOPT_CONNECTTIMEOUT => $timeout,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_SSL_VERIFYHOST => 2,
    CURLOPT_USERAGENT      => 'bitcardo/fx/1.0'
  ]);
  $res  = curl_exec($ch);
  $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  curl_close($ch);
  if ($code !== 200 || !$res) return null;
  $d = json_decode($res, true);
  return (json_last_error() === JSON_ERROR_NONE) ? $d : null;
}
function price_from_kraken(string $coin): ?float {
  $pair = match (strtoupper($coin)) {'BTC'=>'XBTUSD','ETH'=>'ETHUSD','SOL'=>'SOLUSD','USDT'=>'USDTUSD', default => strtoupper($coin).'USD',};
  $d = http_get_json("https://api.kraken.com/0/public/Ticker?pair={$pair}");
  if (!$d || !empty($d['error'])) return null;
  if (empty($d['result']) || !is_array($d['result'])) return null;
  $first = reset($d['result']);
  $price = is_array($first) && isset($first['c'][0]) ? (float)$first['c'][0] : null;
  return ($price && $price > 0) ? $price : null;
}
function price_from_binance(string $coin): ?float {
  $sym = strtoupper($coin);
  if ($sym === 'USDT') return 1.0;
  $pair = $sym . 'USDT';
  $d = http_get_json("https://api.binance.com/api/v3/ticker/price?symbol={$pair}");
  if (!$d || empty($d['price'])) return null;
  $p = (float)$d['price'];
  return $p > 0 ? $p : null;
}
function price_from_bitfinex(string $coin): ?float {
  $sym = strtoupper($coin);
  if ($sym === 'USDT') return 1.0;
  $pair = 't' . $sym . 'USD';
  $d = http_get_json("https://api-pub.bitfinex.com/v2/ticker/{$pair}");
  if (!$d || !is_array($d) || count($d) < 7) return null;
  $last = (float)$d[6];
  return $last > 0 ? $last : null;
}
function price_from_okx(string $coin): ?float {
  $sym = strtoupper($coin);
  $inst = $sym . '-USD';
  $d = http_get_json("https://www.okx.com/api/v5/market/ticker?instId={$inst}");
  if (!$d || ($d['code'] ?? '') !== '0' || empty($d['data'][0]['last'])) return null;
  $p = (float)$d['data'][0]['last'];
  return $p > 0 ? $p : null;
}
function price_from_gemini(string $coin): ?float {
  $sym = strtoupper($coin);
  if ($sym === 'USDT') return 1.0;
  $d = http_get_json("https://api.gemini.com/v1/pricefeed");
  if (!$d || !is_array($d)) return null;
  $pair = $sym . 'USD';
  foreach ($d as $row) {
    if (!empty($row['pair']) && $row['pair'] === $pair && !empty($row['price'])) {
      $p = (float)$row['price'];
      if ($p > 0) return $p;
    }
  }
  return null;
}
function price_from_coinpaprika(string $coin): ?float {
  $map = ['BTC'=>'btc-bitcoin','ETH'=>'eth-ethereum','SOL'=>'sol-solana','USDT'=>'usdt-tether'];
  $id = $map[strtoupper($coin)] ?? null;
  if (!$id) return null;
  $d = http_get_json("https://api.coinpaprika.com/v1/tickers/{$id}");
  $p = $d['quotes']['USD']['price'] ?? null;
  $p = $p !== null ? (float)$p : null;
  return ($p && $p > 0) ? $p : null;
}
function fetch_usd_rate(mysqli $conn, string $coin): ?float {
  $coin = strtoupper($coin);
  $cacheKey = "fx_usd_{$coin}";
  if (function_exists('apcu_fetch')) { $cached = apcu_fetch($cacheKey, $ok); if ($ok && is_numeric($cached) && $cached > 0) return (float)$cached; }
  foreach (['price_from_kraken','price_from_binance','price_from_bitfinex','price_from_okx','price_from_gemini','price_from_coinpaprika'] as $fn) {
    $p = $fn($coin); if ($p && $p > 0) { if (function_exists('apcu_store')) apcu_store($cacheKey, $p, 20); return $p; }
  }
  $stmt = $conn->prepare("SELECT sell_rate FROM coin_rates WHERE coin = ? LIMIT 1");
  if ($stmt) { $stmt->bind_param('s', $coin); $stmt->execute(); $stmt->bind_result($rateUsd); $rate = $stmt->fetch() ? (float)$rateUsd : null; $stmt->close(); if ($rate && $rate > 0) { if (function_exists('apcu_store')) apcu_store($cacheKey, $rate, 20); return $rate; } }
  return null;
}

function coin_decimals(string $coin): int {
  return match (strtoupper($coin)) {'BTC'=>8,'ETH'=>18,'SOL'=>9,'USDT'=>6, default=>8};
}

/* guards */
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') fail_redirect('Invalid request method');
if (empty($_SESSION['user_id'])) fail_redirect('Login required');
$userId = (int)$_SESSION['user_id'];

$coin    = strtoupper(trim($_POST['coin'] ?? ''));
$amountU = (float)($_POST['amount'] ?? 0);
$toAddr  = trim($_POST['recipient'] ?? '');
$quoteId = trim($_POST['quote_id'] ?? '');

if ($coin === '' || $amountU <= 0 || $toAddr === '') fail_redirect("Missing required fields");

/* sender wallet */
$stmt = $conn->prepare("SELECT wallet_id, wallet_add, balance FROM user_wallets WHERE user_id = ? AND UPPER(coin) = ? LIMIT 1");
if (!$stmt) fail_redirect('DB error (sender wallet)');
$stmt->bind_param('is', $userId, $coin);
$stmt->execute();
$sender = $stmt->get_result()->fetch_assoc();
$stmt->close();
if (!$sender) fail_redirect('Wallet not found');

$senderWalletId = (string)$sender['wallet_id'];
$senderAddr = $sender['wallet_add'];
$senderBal  = (float)$sender['balance'];

$decimals = coin_decimals($coin);

/* if a fresh quote exists, use it to get exact numbers + fee policy */
$quote = null;
if ($quoteId && !empty($_SESSION['send_quote'][$quoteId])) {
  $q = $_SESSION['send_quote'][$quoteId];
  // basic integrity checks
  if ((int)$q['user_id'] === $userId && strtoupper($q['coin']) === $coin && strtolower($q['recipient']) === strtolower($toAddr) && (int)$q['expires_at'] >= time()) {
    $quote = $q;
  }
}
if (!$quote) {
  // last resort (shouldn’t happen if UI uses quote flow)
  $usdRate = fetch_usd_rate($conn, $coin);
  if (!$usdRate || $usdRate <= 0) fail_redirect("Could not get USD rate for $coin");
  $amountCoin = round($amountU / $usdRate, $decimals);

  // internal?
  $stmt = $conn->prepare("SELECT user_id, wallet_id FROM user_wallets WHERE UPPER(wallet_add) = UPPER(?) AND UPPER(coin) = ? LIMIT 1");
  if (!$stmt) fail_redirect('DB error (find receiver)');
  $stmt->bind_param('ss', $toAddr, $coin);
  $stmt->execute();
  $recv = $stmt->get_result()->fetch_assoc();
  $stmt->close();
  $isInternal = (bool)$recv;

  $platformFeeCoin = 0.0; $networkFeeCoin = 0.0;
  if (!$isInternal && is_platform_fee_enabled($conn, $coin)) {
    // use withdraw_fees table (same logic as before)
    $stmt = $conn->prepare("
      SELECT percent_fee, flat_fee, max_fee FROM withdraw_fees
       WHERE coin = ? AND type='crypto'
         AND (threshold IS NULL OR ? <= threshold)
       ORDER BY threshold ASC LIMIT 1
    ");
    if ($stmt) {
      $stmt->bind_param('sd', $coin, $amountU);
      $stmt->execute();
      $stmt->bind_result($percent,$flat,$cap);
      $found=$stmt->fetch();
      $stmt->close();
      if (!$found) { $percent=0;$flat=0;$cap=null; }
      $platformFeeUsd = ($percent/100.0)*$amountU + (float)$flat;
      if ($cap !== null && $cap > 0) $platformFeeUsd = min($platformFeeUsd,(float)$cap);
      $platformFeeCoin = round($platformFeeUsd / $usdRate, $decimals);
    }
  }
  $totalDebitCoin = $isInternal ? $amountCoin : round($amountCoin + $platformFeeCoin, $decimals);
  if ($totalDebitCoin > $senderBal) fail_redirect('Total (amount + fees) exceeds your wallet balance');

  // proceed as a pure internal OR call BitGo directly (no pinning); omitted because UI should always use quote
}

/* With quote — authoritative totals */
if ($quote) {
  $amountCoin       = (float)$quote['amount_coin'];
  $isInternal       = (bool)$quote['is_internal'];
  $platformFeeCoin  = (float)$quote['platform_fee_coin'];
  $networkFeeCoin   = (float)$quote['network_fee_coin'];
  $totalDebitCoin   = (float)$quote['total_debit_coin'];
  $amountBase       = $quote['amount_base'];   // may be null for internal
  $networkFeeBase   = $quote['network_fee_base'] ?? '0';
  $feeRate          = $quote['fee_rate'] ?? null;

  if ($totalDebitCoin > $senderBal) fail_redirect('Total (amount + fees) exceeds your wallet balance');

  $conn->begin_transaction();
  try {
    // debit sender
    $stmt = $conn->prepare("UPDATE user_wallets SET balance = balance - ? WHERE wallet_id = ? LIMIT 1");
    if (!$stmt) throw new Exception('DB error (debit sender)');
    $stmt->bind_param('ds', $totalDebitCoin, $senderWalletId);
    $stmt->execute();
    $stmt->close();

    if ($isInternal) {
      // credit receiver
      $stmt = $conn->prepare("SELECT user_id, wallet_id FROM user_wallets WHERE UPPER(wallet_add) = UPPER(?) AND UPPER(coin) = ? LIMIT 1");
      if (!$stmt) throw new Exception('DB error (find receiver)');
      $stmt->bind_param('ss', $toAddr, $coin);
      $stmt->execute();
      $recv = $stmt->get_result()->fetch_assoc();
      $stmt->close();
      if (!$recv) throw new Exception('Recipient wallet disappeared');

      $recvWalletId = (string)$recv['wallet_id'];
      $recvUserId   = (int)$recv['user_id'];

      $stmt = $conn->prepare("UPDATE user_wallets SET balance = balance + ? WHERE wallet_id = ? LIMIT 1");
      if (!$stmt) throw new Exception('DB error (credit receiver)');
      $stmt->bind_param('ds', $amountCoin, $recvWalletId);
      $stmt->execute();
      $stmt->close();

      $provider = 'internal';
      $note = json_encode([
        'ui_amount_usd'   => round((float)$quote['amount_usd'], 2),
        'usd_per_coin'    => round((float)$quote['usd_rate'], 8),
        'amount_coin'     => (string)$amountCoin,
        'platform_fee'    => ['usd' => round((float)$quote['platform_fee_usd'], 6), 'coin' => (string)$platformFeeCoin],
        'network_fee'     => ['usd' => 0.0, 'coin' => '0'],
        'total_fee_coin'  => (string)round($platformFeeCoin, $decimals),
        'total_debit_coin'=> (string)$totalDebitCoin,
      ], JSON_UNESCAPED_SLASHES);

      // sender tx
      $stmt = $conn->prepare("
        INSERT INTO transactions
          (coin, user_id, wallet_id, sender_address, receiver_address, amount, type, status, confirmation, note, provider, created_at, updated_at)
        VALUES
          (?, ?, ?, ?, ?, ?, 'send', 'success', 0, ?, ?, NOW(), NOW())
      ");
      if (!$stmt) throw new Exception('DB error (insert sender tx)');
      $stmt->bind_param('siissdss', $coin, $userId, $senderWalletId, $senderAddr, $toAddr, $amountCoin, $note, $provider);
      $stmt->execute();
      $senderTransId = (int)$conn->insert_id;
      $stmt->close();

      // receiver tx
      $stmt = $conn->prepare("
        INSERT INTO transactions
          (coin, user_id, wallet_id, sender_address, receiver_address, amount, type, status, confirmation, note, provider, created_at, updated_at)
        VALUES
          (?, ?, ?, ?, ?, ?, 'receive', 'success', 0, ?, ?, NOW(), NOW())
      ");
      if (!$stmt) throw new Exception('DB error (insert receiver tx)');
      $stmt->bind_param('siissdss', $coin, $recvUserId, $recvWalletId, $senderAddr, $toAddr, $amountCoin, $note, $provider);
      $stmt->execute();
      $stmt->close();

      $conn->commit();
      $_SESSION['tx_success'] = ['tid' => $senderTransId];
      header("Location: " . SUCCESS_PATH);
      exit();
    }

    /* external — send with fee pinning */
    $stmt = $conn->prepare("SELECT wallet_add_id, encrypted_phrase FROM cwallet WHERE coin = ? LIMIT 1");
    if (!$stmt) throw new Exception('DB error (central wallet)');
    $stmt->bind_param('s', $coin);
    $stmt->execute();
    $cw = $stmt->get_result()->fetch_assoc();
    $stmt->close();
    if (!$cw) throw new Exception("Central wallet not found for {$coin}");

    $walletId   = $cw['wallet_add_id'];
    $passphrase = $cw['encrypted_phrase'];

    // pending tx
    $provider = 'bitgo';
    $status   = 'pending';
    $note = json_encode([
      'ui_amount_usd'   => round((float)$quote['amount_usd'], 2),
      'usd_per_coin'    => round((float)$quote['usd_rate'], 8),
      'amount_coin'     => (string)$amountCoin,
      'platform_fee'    => ['usd' => round((float)$quote['platform_fee_usd'], 6), 'coin' => (string)$platformFeeCoin],
      'network_fee'     => ['usd' => round((float)$quote['network_fee_usd'], 6), 'coin' => (string)$networkFeeCoin],
      'total_fee_coin'  => (string)round($platformFeeCoin + $networkFeeCoin, $decimals),
      'total_debit_coin'=> (string)$totalDebitCoin,
    ], JSON_UNESCAPED_SLASHES);

    $stmt = $conn->prepare("
      INSERT INTO transactions
        (coin, user_id, wallet_id, sender_address, receiver_address, amount, type, status, confirmation, note, provider, provider_meta, created_at, updated_at)
      VALUES
        (?, ?, ?, ?, ?, ?, 'send', ?, 0, ?, ?, NULL, NOW(), NOW())
    ");
    if (!$stmt) throw new Exception('DB error (insert pending tx)');
    $stmt->bind_param('siissdsss', $coin, $userId, $senderWalletId, $senderAddr, $toAddr, $amountCoin, $status, $note, $provider);
    $stmt->execute();
    $transId = (int)$conn->insert_id;
    $stmt->close();

    // prepare payload with fee pinning
    $payloadArr = [
      'address'          => $toAddr,
      'amount'           => (int)$quote['amount_base'],
      'walletPassphrase' => $passphrase,
      // Pinning:
      // If BitGo returns a higher fee than quoted, sendcoins will fail (we’ll refund & redirect).
      'maxFee'           => (int)$networkFeeBase,
    ];
    if (!empty($feeRate)) $payloadArr['feeRate'] = (int)$feeRate;

    $url = rtrim(BITGO_API_BASE_URL, '/') . '/' . strtolower($coin) . '/wallet/' . $walletId . '/sendcoins';
    $ch = curl_init($url);
    curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST           => true,
      CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . BITGO_ACCESS_TOKEN
      ],
      CURLOPT_POSTFIELDS     => json_encode($payloadArr),
      CURLOPT_TIMEOUT        => 25,
    ]);
    $resp = curl_exec($ch);
    $http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    // save provider_meta (raw BitGo response) regardless of success/fail
    $stmt = $conn->prepare("UPDATE transactions SET provider_meta = ?, updated_at = NOW() WHERE trans_id = ? LIMIT 1");
    if ($stmt) { $pm = $resp ?: ''; $stmt->bind_param('si', $pm, $transId); $stmt->execute(); $stmt->close(); }

    if ($http === 200 && $resp) {
      $d = json_decode($resp, true);
      $txid = $d['transfer']['txid'] ?? ($d['txid'] ?? null);

      if ($txid) {
        $stmt = $conn->prepare("UPDATE transactions SET txid = ?, updated_at = NOW() WHERE trans_id = ? LIMIT 1");
        if ($stmt) { $stmt->bind_param('si', $txid, $transId); $stmt->execute(); $stmt->close(); }
      }

      $conn->commit();
      $_SESSION['tx_success'] = ['tid' => $transId];
      header("Location: " . SUCCESS_PATH);
      exit();
    }

    // sendcoins failed — refund and mark failed
    $stmt = $conn->prepare("UPDATE user_wallets SET balance = balance + ? WHERE wallet_id = ? LIMIT 1");
    if ($stmt) { $stmt->bind_param('ds', $totalDebitCoin, $senderWalletId); $stmt->execute(); $stmt->close(); }

    $stmt = $conn->prepare("UPDATE transactions SET status = 'failed', updated_at = NOW() WHERE trans_id = ? LIMIT 1");
    if ($stmt) { $stmt->bind_param('i', $transId); $stmt->execute(); $stmt->close(); }

    $conn->commit();
    fail_redirect("Fees changed. Please review and try again.");

  } catch (Throwable $e) {
    $conn->rollback();
    fail_redirect($e->getMessage());
  }
}

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


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