PHP WebShell
Текущая директория: /var/www/bitcardoApp/models/crypto
Просмотр файла: tron_deposit_scanner.php
<?php
// models/crypto/tron_deposit_scanner.php
namespace Models\Crypto;
class TronDepositScanner
{
private \mysqli $db;
private string $apiBase;
private string $apiKey;
private string $usdtContract;
// Minimum confirmations per coin
private array $minConf = [
'TRX' => 20,
'USDT-TRC20' => 20,
];
public function __construct(\mysqli $conn)
{
$this->db = $conn;
$config = include __DIR__ . '/../../config/tron_config.php';
$this->apiBase = rtrim($config['network'], '/');
$this->apiKey = $config['api_key'];
$this->usdtContract = $config['usdt_contract']; // TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
}
/**
* Scan all active TRX wallets (one per TRON address).
* For each address:
* - scan TRX deposits
* - scan USDT-TRC20 deposits
*/
public function scanAllUserWallets(): void
{
$sql = "SELECT wallet_id, user_id, wallet_add
FROM user_wallets
WHERE coin = 'TRX' AND wallet_status = 'active'";
$res = $this->db->query($sql);
if (!$res) {
echo "DB error fetching TRX wallets: " . $this->db->error . PHP_EOL;
return;
}
if ($res->num_rows === 0) {
echo "No TRX user wallets found to scan." . PHP_EOL;
return;
}
echo "Scanning {$res->num_rows} TRX user wallet(s)..." . PHP_EOL;
while ($row = $res->fetch_assoc()) {
$userId = (int)$row['user_id'];
$address = $row['wallet_add'];
echo " → Address {$address} (user_id={$userId})" . PHP_EOL;
$this->scanTrxDepositsForAddress($userId, $address);
$this->scanUsdtDepositsForAddress($userId, $address);
}
$res->free();
echo "Scan finished." . PHP_EOL;
}
/**
* Helper: find the wallet_id for a specific coin on a given address for a user.
*/
private function getWalletIdForCoin(int $userId, string $address, string $coin): ?int
{
$sql = "SELECT wallet_id
FROM user_wallets
WHERE user_id = ? AND wallet_add = ? AND coin = ? AND wallet_status = 'active'
LIMIT 1";
$stmt = $this->db->prepare($sql);
if (!$stmt) {
echo "DB error (getWalletIdForCoin): " . $this->db->error . PHP_EOL;
return null;
}
$stmt->bind_param("iss", $userId, $address, $coin);
$stmt->execute();
$res = $stmt->get_result();
$row = $res->fetch_assoc();
$stmt->close();
return $row ? (int)$row['wallet_id'] : null;
}
/**
* Scan inbound TRX transfers for a single address.
*/
private function scanTrxDepositsForAddress(int $userId, string $address): void
{
$url = $this->apiBase
. "/v1/accounts/{$address}/transactions?only_to=true&limit=50&order_by=block_timestamp,desc";
$data = $this->getJson($url);
if (!$data || empty($data['data'])) {
return;
}
foreach ($data['data'] as $tx) {
$txid = $tx['txID'] ?? null;
if (!$txid) {
continue;
}
$ret0 = $tx['ret'][0]['contractRet'] ?? '';
if ($ret0 !== 'SUCCESS') {
continue;
}
$amountSun = $tx['raw_data']['contract'][0]['parameter']['value']['amount'] ?? null;
if ($amountSun === null) {
continue;
}
$blockTs = $tx['block_timestamp'] ?? null; // ms
$confirmations = $this->estimateConfirmations($blockTs);
$amountRaw = (int)$amountSun;
$amountDec = $amountRaw / 1000000; // TRX has 6 decimals
$this->processDeposit(
network: 'TRON',
coin: 'TRX',
userId: $userId,
walletAddress: $address,
txid: $txid,
blockNum: null,
confirmations: $confirmations,
amountRaw: $amountRaw,
amountDec: $amountDec,
txRaw: $tx
);
}
}
/**
* Scan inbound USDT-TRC20 transfers for a single address.
*/
private function scanUsdtDepositsForAddress(int $userId, string $address): void
{
$url = $this->apiBase
. "/v1/accounts/{$address}/transactions/trc20?only_to=true&limit=50&order_by=block_timestamp,desc";
$data = $this->getJson($url);
if (!$data || empty($data['data'])) {
return;
}
foreach ($data['data'] as $tx) {
$txid = $tx['transaction_id'] ?? null;
if (!$txid) {
continue;
}
$contractAddress = $tx['token_info']['address'] ?? '';
if (!$contractAddress || strcasecmp($contractAddress, $this->usdtContract) !== 0) {
continue; // Not USDT-TRC20
}
$valueRaw = $tx['value'] ?? null;
if ($valueRaw === null) {
continue;
}
$blockTs = $tx['block_timestamp'] ?? null; // ms
$confirmations = $this->estimateConfirmations($blockTs);
$amountRaw = (int)$valueRaw;
$amountDec = $amountRaw / 1000000; // 6 decimals
$this->processDeposit(
network: 'TRON',
coin: 'USDT-TRC20',
userId: $userId,
walletAddress: $address,
txid: $txid,
blockNum: null,
confirmations: $confirmations,
amountRaw: $amountRaw,
amountDec: $amountDec,
txRaw: $tx
);
}
}
/**
* Insert/update crypto_deposit_log AND, once confirmations >= threshold,
* credit user_wallets and insert into transactions.
*
* NOTE: We now ALWAYS resolve wallet_id inside here using (user_id, address, coin).
*/
private function processDeposit(
string $network,
string $coin,
int $userId,
string $walletAddress,
string $txid,
?int $blockNum,
int $confirmations,
int $amountRaw,
float $amountDec,
array $txRaw
): void {
$min = $this->minConf[$coin] ?? 20;
try {
$this->db->begin_transaction();
// 0) Resolve wallet_id based on coin
$walletId = $this->getWalletIdForCoin($userId, $walletAddress, $coin);
if (!$walletId) {
// no matching wallet row, don't log or credit
echo " No wallet row for {$coin} (user_id={$userId}, address={$walletAddress}), skipping.\n";
$this->db->commit();
return;
}
// 1) Check if tx already logged
$stmt = $this->db->prepare("
SELECT id, credited, confirmations
FROM crypto_deposit_log
WHERE network = ? AND coin = ? AND txid = ?
LIMIT 1
");
if (!$stmt) {
throw new \Exception("Prepare failed (check log): " . $this->db->error);
}
$stmt->bind_param("sss", $network, $coin, $txid);
$stmt->execute();
$res = $stmt->get_result();
$existing = $res->fetch_assoc();
$stmt->close();
if ($existing) {
if ((int)$existing['credited'] === 1) {
// Already credited, nothing to do
$this->db->commit();
return;
}
// Update confirmations if they increased
if ($confirmations > (int)$existing['confirmations']) {
$stmtU = $this->db->prepare("
UPDATE crypto_deposit_log
SET confirmations = ?, updated_at = NOW()
WHERE id = ?
");
if ($stmtU) {
$logId = (int)$existing['id'];
$stmtU->bind_param("ii", $confirmations, $logId);
$stmtU->execute();
$stmtU->close();
}
}
} else {
// Insert new log row with credited = 0
$stmtI = $this->db->prepare("
INSERT INTO crypto_deposit_log
(network, coin, user_id, wallet_id, wallet_add, txid, block_num,
amount_raw, amount_dec, confirmations, credited, created_at, updated_at)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NOW(), NOW())
");
if (!$stmtI) {
throw new \Exception("Prepare failed (insert log): " . $this->db->error);
}
// Types: s s i i s s i i d i => "ssiissiidi"
$blockNumInt = $blockNum ?? 0;
$stmtI->bind_param(
"ssiissiidi",
$network, // s
$coin, // s
$userId, // i
$walletId, // i
$walletAddress, // s
$txid, // s
$blockNumInt, // i
$amountRaw, // i
$amountDec, // d
$confirmations // i
);
$stmtI->execute();
$stmtI->close();
echo " Logged new {$coin} deposit tx {$txid} (confirmations={$confirmations}, wallet_id={$walletId})" . PHP_EOL;
}
// 2) If confirmations below threshold, don't credit yet
if ($confirmations < $min) {
$this->db->commit();
return;
}
// 3) Credit user wallet balance
$stmtW = $this->db->prepare("
SELECT balance
FROM user_wallets
WHERE wallet_id = ?
FOR UPDATE
");
if (!$stmtW) {
throw new \Exception("Prepare failed (select wallet): " . $this->db->error);
}
$stmtW->bind_param("i", $walletId);
$stmtW->execute();
$resW = $stmtW->get_result();
$wallet = $resW->fetch_assoc();
$stmtW->close();
if (!$wallet) {
throw new \Exception("Wallet not found (ID {$walletId})");
}
$currentBal = (float)$wallet['balance'];
$newBal = $currentBal + $amountDec;
$stmtU2 = $this->db->prepare("
UPDATE user_wallets
SET balance = ?, updated_at = NOW()
WHERE wallet_id = ?
");
if (!$stmtU2) {
throw new \Exception("Prepare failed (update wallet): " . $this->db->error);
}
$stmtU2->bind_param("di", $newBal, $walletId);
$stmtU2->execute();
$stmtU2->close();
// 4) Insert into transactions
$senderAddress = 'external';
$receiverAddress = $walletAddress;
$provider = 'tron';
$metaJson = json_encode($txRaw, JSON_UNESCAPED_SLASHES);
$stmtT = $this->db->prepare("
INSERT INTO transactions
(coin, user_id, wallet_id, sender_address, receiver_address, amount,
type, txid, confirmation, status, applied, provider, provider_meta, created_at, updated_at)
VALUES
(?, ?, ?, ?, ?, ?, 'deposit', ?, ?, 'completed', 1, ?, ?, NOW(), NOW())
");
if (!$stmtT) {
throw new \Exception("Prepare failed (insert transaction): " . $this->db->error);
}
// Types: s i i s s d s i s s => "siissdsiss"
$stmtT->bind_param(
"siissdsiss",
$coin,
$userId,
$walletId,
$senderAddress,
$receiverAddress,
$amountDec,
$txid,
$confirmations,
$provider,
$metaJson
);
$stmtT->execute();
$stmtT->close();
// 5) Mark crypto_deposit_log as credited
$stmtL = $this->db->prepare("
UPDATE crypto_deposit_log
SET credited = 1, confirmations = ?, updated_at = NOW()
WHERE network = ? AND coin = ? AND txid = ?
");
if (!$stmtL) {
throw new \Exception("Prepare failed (update log credited): " . $this->db->error);
}
$stmtL->bind_param("isss", $confirmations, $network, $coin, $txid);
$stmtL->execute();
$stmtL->close();
$this->db->commit();
echo " Credited {$amountDec} {$coin} to user {$userId} wallet_id={$walletId} from tx {$txid}" . PHP_EOL;
} catch (\Throwable $e) {
$this->db->rollback();
echo " Error processing {$coin} deposit {$txid}: " . $e->getMessage() . PHP_EOL;
}
}
/**
* Estimate confirmations using timestamp (Tron ~3 sec/block).
*/
private function estimateConfirmations(?int $blockTsMs): int
{
if ($blockTsMs === null || $blockTsMs <= 0) {
return 0;
}
$nowMs = (int)round(microtime(true) * 1000);
$ageSec = max(0, ($nowMs - $blockTsMs) / 1000);
$conf = (int)floor($ageSec / 3); // ~3s per block
return ($conf > 1000) ? 1000 : $conf;
}
/**
* Simple TronGrid GET helper.
*/
private function getJson(string $url): ?array
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["TRON-PRO-API-KEY: " . $this->apiKey],
CURLOPT_TIMEOUT => 20,
]);
$resp = curl_exec($ch);
if ($resp === false) {
curl_close($ch);
return null;
}
curl_close($ch);
$decoded = json_decode($resp, true);
return is_array($decoded) ? $decoded : null;
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!