PHP WebShell

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

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

<?php
//../../models/crypto/send_crypto_processor.php
declare(strict_types=1);

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

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

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();
}

function is_usdt_trc20(string $coin): bool {
  $c = strtoupper(trim($coin));
  return ($c === 'USDT-TRC20' || $c === 'USDT_TRC20' || $c === 'USDTTRC20' || $c === 'USDT');
}
function is_tron_family(string $coin): bool {
  $c = strtoupper(trim($coin));
  return ($c === 'TRX' || is_usdt_trc20($c));
}
function platform_fee_coin_key(string $coin): string {
  $c = strtoupper(trim($coin));
  return is_usdt_trc20($c) ? 'USDT' : $c;
}
function coin_decimals(string $coin): int {
  $coin = strtoupper(trim($coin));
  if (is_usdt_trc20($coin)) return 6;
  return match ($coin) {
    'BTC'=>8,'ETH'=>18,'SOL'=>9,'USDT'=>6,'TRX'=>6, default=>8
  };
}
function looks_like_cwallet_insufficient(?string $respBody, int $httpCode): bool {
  if ($httpCode === 429) return false;
  if (!$respBody) return false;
  $t = strtolower($respBody);
  return (str_contains($t, 'insufficient') || str_contains($t, 'not enough') || str_contains($t, 'spendable') || str_contains($t, 'balance'));
}

/* User wallet coin condition (fix wallet_not_found) */
function user_wallet_coin_conditions(string $coin): array {
  $c = strtoupper(trim($coin));
  if (is_usdt_trc20($c)) {
    return ['sql' => "UPPER(coin) IN ('USDT','USDT-TRC20','USDT_TRC20','USDTTRC20')", 'bind' => []];
  }
  return ['sql' => "UPPER(coin) = ?", 'bind' => [$c]];
}

function cwallet_get(mysqli $conn, string $coinUpper): ?array {
  $stmt = $conn->prepare("SELECT cwallet_id, coin, wallet_add_id, wallet_add, wallet_balance, encrypted_phrase FROM cwallet WHERE UPPER(coin)=UPPER(?) LIMIT 1");
  if (!$stmt) return null;
  $stmt->bind_param('s', $coinUpper);
  $stmt->execute();
  $row = $stmt->get_result()->fetch_assoc();
  $stmt->close();
  return $row ?: null;
}
function cwallet_debit(mysqli $conn, int $cwalletId, float $amount): void {
  $stmt = $conn->prepare("UPDATE cwallet SET wallet_balance = wallet_balance - ? WHERE cwallet_id = ? LIMIT 1");
  if (!$stmt) throw new Exception('DB error (debit cwallet)');
  $stmt->bind_param('di', $amount, $cwalletId);
  $stmt->execute();
  $stmt->close();
}
function cwallet_credit(mysqli $conn, int $cwalletId, float $amount): void {
  $stmt = $conn->prepare("UPDATE cwallet SET wallet_balance = wallet_balance + ? WHERE cwallet_id = ? LIMIT 1");
  if (!$stmt) return;
  $stmt->bind_param('di', $amount, $cwalletId);
  $stmt->execute();
  $stmt->close();
}

/* 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 (USER ledger) with USDT-TRC20 fallback */
$cond = user_wallet_coin_conditions($coin);

if (empty($cond['bind'])) {
  $sql = "SELECT wallet_id, wallet_add, balance, coin FROM user_wallets WHERE user_id = ? AND {$cond['sql']} LIMIT 1";
  $stmt = $conn->prepare($sql);
  if (!$stmt) fail_redirect('DB error (sender wallet)');
  $stmt->bind_param('i', $userId);
} else {
  $sql = "SELECT wallet_id, wallet_add, balance, coin FROM user_wallets WHERE user_id = ? AND {$cond['sql']} LIMIT 1";
  $stmt = $conn->prepare($sql);
  if (!$stmt) fail_redirect('DB error (sender wallet)');
  $coinBind = $cond['bind'][0];
  $stmt->bind_param('is', $userId, $coinBind);
}

$stmt->execute();
$sender = $stmt->get_result()->fetch_assoc();
$stmt->close();
if (!$sender) fail_redirect('Wallet not found');

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

/* Quote required */
$quote = null;
if ($quoteId && !empty($_SESSION['send_quote'][$quoteId])) {
  $q = $_SESSION['send_quote'][$quoteId];
  if ((int)$q['user_id'] === $userId
      && strtoupper((string)$q['coin']) === $coin
      && strtolower((string)$q['recipient']) === strtolower($toAddr)
      && (int)$q['expires_at'] >= time()) {
    $quote = $q;
  }
}
if (!$quote) fail_redirect('Missing or expired quote. Please try again.');

$decimals         = (int)($quote['decimals'] ?? coin_decimals($coin));
$isInternal       = (bool)($quote['is_internal'] ?? false);
$provider         = (string)($quote['provider'] ?? ($isInternal ? 'internal' : (is_tron_family($coin) ? 'tron' : 'bitgo')));

$amountCoin       = (float)$quote['amount_coin'];
$platformFeeCoin  = (float)($quote['platform_fee_coin'] ?? 0.0);
$networkFeeCoin   = (float)($quote['network_fee_coin'] ?? 0.0);
$totalDebitCoin   = (float)($quote['total_debit_coin'] ?? 0.0);

$networkFeeTrx    = (float)($quote['network_fee_trx'] ?? 0.0);
$amountBase       = $quote['amount_base'] ?? null;
$networkFeeBase   = (string)($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 {

  /* 1) Debit USER ledger wallet */
  $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();

  /* 2) Internal transfer */
  if ($isInternal) {
    if (is_usdt_trc20($coin)) {
      $stmt = $conn->prepare("
        SELECT user_id, wallet_id
          FROM user_wallets
         WHERE UPPER(wallet_add) = UPPER(?)
           AND UPPER(coin) IN ('USDT','USDT-TRC20','USDT_TRC20','USDTTRC20')
         LIMIT 1
      ");
      if (!$stmt) throw new Exception('DB error (find receiver)');
      $stmt->bind_param('s', $toAddr);
    } else {
      $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, $userCoinStored);
    }

    $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'] ?? 0), 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);

    $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();

    $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();
  }

  /* 3) External TRON family: keep existing behavior (TRON worker handles broadcast) */
  if (is_tron_family($coin)) {

    $cwTrx = cwallet_get($conn, 'TRX');
    if (!$cwTrx || empty($cwTrx['wallet_add'])) throw new Exception("Central TRX wallet address not found");
    $broadcastFrom = (string)$cwTrx['wallet_add'];

    // Funding from CWALLET
    if ($coin === 'TRX') {
      $debitTrx = (float)$amountCoin + (float)$networkFeeCoin;
      if ($debitTrx > 0) cwallet_debit($conn, (int)$cwTrx['cwallet_id'], $debitTrx);
    } else {
      $cwUsdt = cwallet_get($conn, 'USDT') ?: cwallet_get($conn, 'USDT-TRC20');
      if (!$cwUsdt || empty($cwUsdt['wallet_add'])) throw new Exception("Central USDT wallet address not found");

      if (strcasecmp((string)$cwUsdt['wallet_add'], $broadcastFrom) !== 0) {
        throw new Exception("USDT cwallet address must match TRX cwallet address for TRC20 gas spending");
      }

      $platTrxBal = isset($cwTrx['wallet_balance']) ? (float)$cwTrx['wallet_balance'] : 0.0;
      if ($networkFeeTrx > 0 && $platTrxBal < $networkFeeTrx) {
        throw new Exception("Platform TRX wallet cannot cover USDT-TRC20 gas fee");
      }

      if ($amountCoin > 0) cwallet_debit($conn, (int)$cwUsdt['cwallet_id'], (float)$amountCoin);
      if ($networkFeeTrx > 0) cwallet_debit($conn, (int)$cwTrx['cwallet_id'], (float)$networkFeeTrx);
    }

    $provider = 'tron';
    $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'] ?? 0), 6), 'coin' => (string)$platformFeeCoin],
      'network_fee'       => ['usd' => round((float)($quote['network_fee_usd'] ?? 0), 6), 'coin' => (string)$networkFeeCoin],
      'network_fee_trx'   => ($coin === 'TRX') ? '0' : (string)$networkFeeTrx,
      'total_debit_coin'  => (string)$totalDebitCoin,
      'provider'          => 'tron',
      'gas_from_cwallet'  => ($coin === 'TRX') ? 0 : 1,
    ], JSON_UNESCAPED_SLASHES);

    $providerMeta = json_encode([
      'broadcast_from'     => $broadcastFrom,
      'to'                 => $toAddr,
      'coin'               => $coin,
      'amount_coin'        => (string)$amountCoin,
      'total_debit_coin'   => (string)$totalDebitCoin,
      'network_fee_trx'    => ($coin === 'TRX') ? '0' : (string)$networkFeeTrx,
      'gas_from_cwallet'   => ($coin === 'TRX') ? 0 : 1,
      'refunded'           => false,
      'refund_reason'      => null,
      'worker'             => ['attempts' => 0, 'last_error' => null],
    ], 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, ?, ?, ?, NOW(), NOW())
    ");
    if (!$stmt) throw new Exception('DB error (insert tron pending tx)');

    // sender_address must show USER wallet address, not platform broadcast
    $stmt->bind_param('siissdssss', $coin, $userId, $senderWalletId, $senderAddr, $toAddr, $amountCoin, $status, $note, $provider, $providerMeta);
    $stmt->execute();
    $transId = (int)$conn->insert_id;
    $stmt->close();

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

  /* 4) External BitGo (BTC/SOL): QUEUE ONLY. Worker will broadcast and use cwallet.encrypted_phrase */

  $coinKey = platform_fee_coin_key($coin); // BTC or SOL
  $cw = cwallet_get($conn, $coinKey);
  if (!$cw || empty($cw['wallet_add_id'])) throw new Exception("Central wallet not found for {$coinKey}");

  if ($amountBase === null) throw new Exception('Missing amount base units');

  $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'] ?? 0), 6), 'coin' => (string)$platformFeeCoin],
    'network_fee'       => ['usd' => round((float)($quote['network_fee_usd'] ?? 0), 6), 'coin' => (string)$networkFeeCoin],
    'total_fee_coin'    => (string)round($platformFeeCoin + $networkFeeCoin, $decimals),
    'total_debit_coin'  => (string)$totalDebitCoin,
    'provider'          => 'bitgo',
  ], JSON_UNESCAPED_SLASHES);

  // Worker inputs (NO passphrase stored here)
  $providerMeta = json_encode([
    'coin_key'         => strtolower($coinKey),                 // 'btc' | 'sol'
    'wallet_id'        => (string)$cw['wallet_add_id'],         // BitGo wallet id
    'to_address'       => $toAddr,
    'amount_base'      => (string)(int)$amountBase,
    'max_fee_base'     => (string)(int)$networkFeeBase,         // for BTC maxFee
    'fee_rate'         => ($feeRate !== null && $feeRate !== '') ? (string)(int)$feeRate : null,

    // platform funding info (worker may debit/revert)
    'cwallet_id'       => (int)$cw['cwallet_id'],
    'cwallet_debit'    => (string)((float)$amountCoin + (float)$networkFeeCoin),

    // refund context (worker refunds on hard failure only)
    'sender_wallet_id' => $senderWalletId,
    'total_debit_coin' => (string)$totalDebitCoin,

    'attempts'         => 0,
    'last_error'       => null,
  ], 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, ?, ?, ?, NOW(), NOW())
  ");
  if (!$stmt) throw new Exception('DB error (insert bitgo pending tx)');

  // sender_address must show USER wallet address
  $stmt->bind_param('siissdssss', $coin, $userId, $senderWalletId, $senderAddr, $toAddr, $amountCoin, $status, $note, $provider, $providerMeta);
  $stmt->execute();
  $transId = (int)$conn->insert_id;
  $stmt->close();

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

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

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


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