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