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);
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!