PHP WebShell

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

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

<?php
// cron/tron_withdraw_worker.php
declare(strict_types=1);

require_once __DIR__ . '/../config/db_config.php';

function log_line(string $msg): void {
  $file = __DIR__ . '/../storage/logs/tron_withdraw_worker.log';
  @file_put_contents($file, '[' . date('Y-m-d H:i:s') . '] ' . $msg . PHP_EOL, FILE_APPEND);
}

function tron_cfg(): array {
  $cfgFile = __DIR__ . '/../config/tron_config.php';
  $cfg = file_exists($cfgFile) ? require $cfgFile : [];
  return is_array($cfg) ? $cfg : [];
}

function json_try_decode(?string $s): array {
  if (!$s) return [];
  $d = json_decode($s, true);
  return is_array($d) ? $d : [];
}

function json_encode_safe(array $a): string {
  $j = json_encode($a, JSON_UNESCAPED_SLASHES);
  return $j !== false ? $j : '{}';
}

function canon_coin(string $coin): string {
  $c = strtoupper(trim($coin));
  if ($c === 'USDT-TRC20' || $c === 'USDT_TRC20' || $c === 'USDTTRC20') return 'USDT';
  return $c;
}

/**
 * Call node cron/tron_broadcast.js.
 * MUST return JSON.
 */
function tron_node_broadcast(string $nodeBin, array $payload): array {
  $script = __DIR__ . '/tron_broadcast.js';
  $json = json_encode_safe($payload);

  $descriptors = [
    0 => ['pipe', 'r'],
    1 => ['pipe', 'w'],
    2 => ['pipe', 'w'],
  ];

  $cmd = escapeshellarg($nodeBin) . ' ' . escapeshellarg($script);
  $proc = proc_open($cmd, $descriptors, $pipes);
  if (!is_resource($proc)) {
    return ['ok'=>false,'error'=>'Failed to start node process','exit'=>-1,'node_stdout'=>'','node_stderr'=>''];
  }

  fwrite($pipes[0], $json);
  fclose($pipes[0]);

  $out = stream_get_contents($pipes[1]) ?: '';
  fclose($pipes[1]);

  $err = stream_get_contents($pipes[2]) ?: '';
  fclose($pipes[2]);

  $code = proc_close($proc);

  $decoded = null;
  if (trim($out) !== '') $decoded = json_decode($out, true);

  if (!is_array($decoded)) {
    return [
      'ok'=>false,
      'error'=>'Invalid node output (not JSON)',
      'exit'=>$code,
      'node_stdout'=>$out,
      'node_stderr'=>$err,
    ];
  }

  $decoded['exit'] = $code;
  $decoded['node_stderr'] = $err;
  return $decoded;
}

function update_provider_meta(mysqli $conn, int $transId, array $meta): void {
  $pm = json_encode_safe($meta);
  $stmt = $conn->prepare("UPDATE transactions SET provider_meta = ?, updated_at = NOW() WHERE trans_id = ? LIMIT 1");
  if ($stmt) {
    $stmt->bind_param('si', $pm, $transId);
    $stmt->execute();
    $stmt->close();
  } else {
    log_line("trans_id={$transId} provider_meta update failed: " . $conn->error);
  }
}

/**
 * Insert a reversal ledger row.
 */
function insert_reversal_tx(
  mysqli $conn,
  string $coin,
  int $userId,
  string $walletId,
  string $userAddr,
  float $amount,
  int $origTransId,
  string $reason
): int {
  $provider = 'system';
  $status   = 'success';

  $note = json_encode_safe([
    'reversal_for_trans_id' => $origTransId,
    'reason' => $reason,
  ]);

  $senderAddress   = 'SYSTEM';
  $receiverAddress = $userAddr;

  $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
      (?, ?, ?, ?, ?, ?, 'reversal', ?, 0, ?, ?, NULL, NOW(), NOW())
  ");
  if (!$stmt) return 0;

  $stmt->bind_param('siissdsss', $coin, $userId, $walletId, $senderAddress, $receiverAddress, $amount, $status, $note, $provider);
  $stmt->execute();
  $id = (int)$conn->insert_id;
  $stmt->close();
  return $id;
}

/**
 * Reverse balances and write reversal tx rows.
 */
function reverse_with_ledger(mysqli $conn, array $txRow, string $reason): void {
  $transId  = (int)$txRow['trans_id'];
  $coin     = strtoupper(trim((string)$txRow['coin']));
  $userId   = (int)$txRow['user_id'];
  $walletId = (string)$txRow['wallet_id'];

  $meta = json_try_decode($txRow['provider_meta'] ?? '');
  $note = json_try_decode($txRow['note'] ?? '');

  if (!empty($meta['reversed'])) {
    log_line("trans_id={$transId} reversal skipped (already reversed)");
    return;
  }

  $userAddr = (string)($txRow['sender_address'] ?? '');
  if ($userAddr === '') $userAddr = (string)($meta['logical_from'] ?? '');

  $totalDebitCoin = 0.0;
  if (isset($meta['total_debit_coin'])) $totalDebitCoin = (float)$meta['total_debit_coin'];
  if ($totalDebitCoin <= 0 && isset($note['total_debit_coin'])) $totalDebitCoin = (float)$note['total_debit_coin'];

  $trxFeeWalletId = (string)($meta['trx_fee_wallet_id'] ?? '');
  $networkFeeTrx  = 0.0;
  if (isset($meta['network_fee_trx'])) $networkFeeTrx = (float)$meta['network_fee_trx'];
  if ($networkFeeTrx <= 0 && isset($note['network_fee_trx'])) $networkFeeTrx = (float)$note['network_fee_trx'];

  $reversalIds = [];
  $errors = [];

  $conn->begin_transaction();
  try {
    if ($walletId !== '' && $totalDebitCoin > 0) {
      $stmt = $conn->prepare("UPDATE user_wallets SET balance = balance + ? WHERE wallet_id = ? LIMIT 1");
      if ($stmt) {
        $stmt->bind_param('ds', $totalDebitCoin, $walletId);
        $stmt->execute();
        $stmt->close();
      } else {
        $errors[] = 'Balance reversal failed: ' . $conn->error;
      }

      $rid = insert_reversal_tx($conn, $coin, $userId, $walletId, $userAddr, $totalDebitCoin, $transId, $reason);
      if ($rid > 0) $reversalIds[] = $rid;
      else $errors[] = 'Reversal insert failed: ' . $conn->error;
    }

    if ($trxFeeWalletId !== '' && $networkFeeTrx > 0) {
      $stmt = $conn->prepare("UPDATE user_wallets SET balance = balance + ? WHERE wallet_id = ? LIMIT 1");
      if ($stmt) {
        $stmt->bind_param('ds', $networkFeeTrx, $trxFeeWalletId);
        $stmt->execute();
        $stmt->close();
      } else {
        $errors[] = 'TRX fee balance reversal failed: ' . $conn->error;
      }

      $rid2 = insert_reversal_tx($conn, 'TRX', $userId, $trxFeeWalletId, $userAddr, $networkFeeTrx, $transId, 'TRX fee reversal: ' . $reason);
      if ($rid2 > 0) $reversalIds[] = $rid2;
      else $errors[] = 'TRX fee reversal insert failed: ' . $conn->error;
    }

    $meta['reversed'] = true;
    $meta['reversal_reason'] = $reason;
    $meta['reversal_trans_ids'] = $reversalIds;
    if ($errors) $meta['reversal_errors'] = $errors;

    update_provider_meta($conn, $transId, $meta);

    $conn->commit();
    log_line("trans_id={$transId} reversed ids=" . json_encode($reversalIds) . " errors=" . json_encode($errors));
  } catch (Throwable $e) {
    $conn->rollback();
    log_line("trans_id={$transId} REVERSAL_FATAL: " . $e->getMessage());

    $meta['reversed'] = false;
    $meta['reversal_reason'] = $reason;
    $meta['reversal_errors'] = ['REVERSAL_FATAL: ' . $e->getMessage()];
    update_provider_meta($conn, $transId, $meta);
  }
}

try {
  log_line('Worker start');

  $cfg = tron_cfg();
  $network = (string)($cfg['network'] ?? '');
  $apiKey  = (string)($cfg['api_key'] ?? '');
  $usdtContract = (string)($cfg['usdt_contract'] ?? '');
  $nodeBin = (string)($cfg['node_bin'] ?? 'node');

  if ($network === '') { log_line('FATAL: tron_config missing network'); exit(1); }

  // Platform TRX hot wallet private key + address
  $stmt = $conn->prepare("SELECT wallet_add, encrypted_phrase FROM cwallet WHERE UPPER(coin)='TRX' LIMIT 1");
  if (!$stmt) { log_line('FATAL: DB error selecting cwallet TRX: ' . $conn->error); exit(1); }
  $stmt->execute();
  $cw = $stmt->get_result()->fetch_assoc();
  $stmt->close();
  if (!$cw || empty($cw['encrypted_phrase']) || empty($cw['wallet_add'])) {
    log_line('FATAL: cwallet TRX wallet_add/encrypted_phrase missing');
    exit(1);
  }

  $hotAddr = (string)$cw['wallet_add'];
  $privKey = trim((string)$cw['encrypted_phrase']);
  $privKey = preg_replace('/^0x/i', '', $privKey);

  $sql = "SELECT trans_id, coin, user_id, wallet_id, sender_address, receiver_address, amount, status, note, provider_meta
          FROM transactions
          WHERE provider='tron'
            AND type='send'
            AND status IN ('pending','processing')
            AND (txid IS NULL OR txid='')
          ORDER BY trans_id ASC
          LIMIT 10";
  $res = $conn->query($sql);
  if (!$res) { log_line('DB query failed: ' . $conn->error); exit(1); }

  while ($row = $res->fetch_assoc()) {
    $transId = (int)$row['trans_id'];
    $coinDb  = strtoupper(trim((string)$row['coin']));
    $coin    = canon_coin($coinDb); // TRX or USDT
    $to      = trim((string)$row['receiver_address']);
    $amount  = (float)$row['amount'];

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

    $meta = json_try_decode($row['provider_meta'] ?? '');
    $meta['worker'] = $meta['worker'] ?? ['attempts' => 0, 'last_error' => null];
    $meta['worker']['attempts'] = (int)($meta['worker']['attempts'] ?? 0) + 1;

    $payload = [
      'network'       => $network,
      'api_key'       => $apiKey,
      'usdt_contract' => $usdtContract,
      'private_key'   => $privKey,
      'to'            => $to,
      'coin'          => $coin,
      'amount_coin'   => (string)$amount,
      'fee_limit_sun' => 15000000,
    ];

    // For audit
    $meta['broadcast_from'] = $meta['broadcast_from'] ?? $hotAddr;
    $meta['logical_from']   = $meta['logical_from'] ?? (string)$row['sender_address'];

    log_line("trans_id={$transId} broadcast coin={$coin} amount={$amount} to={$to}");

    $resp = tron_node_broadcast($nodeBin, $payload);
    $meta['provider_last'] = $resp;

    if (!($resp['ok'] ?? false)) {
      $err = (string)($resp['error'] ?? 'Broadcast failed');
      $meta['worker']['last_error'] = $err;
      update_provider_meta($conn, $transId, $meta);

      log_line("trans_id={$transId} FAIL: {$err}");

      reverse_with_ledger($conn, $row, $err);

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

    $txid = (string)($resp['txid'] ?? '');
    if ($txid === '') {
      $err = 'Broadcast ok but missing txid';
      $meta['worker']['last_error'] = $err;
      update_provider_meta($conn, $transId, $meta);

      reverse_with_ledger($conn, $row, $err);

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

    $meta['worker']['last_error'] = null;
    update_provider_meta($conn, $transId, $meta);

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

    log_line("trans_id={$transId} OK txid={$txid}");
  }

  log_line('Worker end');
} catch (Throwable $e) {
  log_line('FATAL: ' . $e->getMessage());
  exit(1);
}

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


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