PHP WebShell
Текущая директория: /var/www/bitcardoApp/models/crypto
Просмотр файла: tron_sweeper.php
<?php
// models/crypto/tron_sweeper.php
namespace Models\Crypto;
use Elliptic\EC;
class TronSweeper
{
/** @var \mysqli */
private $db;
private string $baseUrl;
private string $apiKey;
private string $usdtContract;
private ?array $primaryTrx = null;
private ?array $primaryUsdt = null;
// Sweep thresholds
private const MIN_TRX_SWEEP = 3.0; // sweep if TRX > 3
private const TRX_GAS_BUFFER = 2.0; // leave 2 TRX for gas
private const MIN_USDT_SWEEP = 1.0; // sweep USDT if >= 1
public function __construct(\mysqli $conn)
{
$this->db = $conn;
$config = require __DIR__ . '/../../config/tron_config.php';
$this->baseUrl = rtrim($config['network'], '/'); // e.g. https://api.trongrid.io
$this->apiKey = $config['api_key'] ?? '';
$this->usdtContract = $config['usdt_contract'] ?? 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';
// load primary TRX & USDT-TRC20 hot wallets from cwallet
$this->primaryTrx = $this->loadPrimaryWallet('TRX');
$this->primaryUsdt = $this->loadPrimaryWallet('USDT-TRC20');
}
private function loadPrimaryWallet(string $coin): ?array
{
$sql = "SELECT cwallet_id, coin, wallet_add
FROM cwallet
WHERE coin = ?
LIMIT 1";
$stmt = $this->db->prepare($sql);
if (!$stmt) {
throw new \RuntimeException(
"DB error preparing loadPrimaryWallet: " . $this->db->error
);
}
$stmt->bind_param("s", $coin);
$stmt->execute();
$res = $stmt->get_result();
$row = $res->fetch_assoc();
$stmt->close();
return $row ?: null;
}
/**
* Simple TronGrid GET helper.
*/
private function httpGetJson(string $path): array
{
$url = $this->baseUrl . $path;
$ch = curl_init($url);
$headers = [];
if ($this->apiKey) {
$headers[] = 'TRON-PRO-API-KEY: ' . $this->apiKey;
}
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 20,
]);
$body = curl_exec($ch);
if ($body === false) {
$err = curl_error($ch);
curl_close($ch);
throw new \RuntimeException("HTTP error calling TronGrid GET: {$err}");
}
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($body, true);
if (!is_array($data)) {
throw new \RuntimeException(
"Invalid JSON from TronGrid GET ({$code}): {$body}"
);
}
return $data;
}
/**
* Simple TronGrid POST helper.
*/
private function httpPostJson(string $path, array $payload): array
{
$url = $this->baseUrl . $path;
$ch = curl_init($url);
$headers = ['Content-Type: application/json'];
if ($this->apiKey) {
$headers[] = 'TRON-PRO-API-KEY: ' . $this->apiKey;
}
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 20,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
if ($body === false) {
$err = curl_error($ch);
curl_close($ch);
throw new \RuntimeException("HTTP error calling TronGrid POST: {$err}");
}
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($body, true);
if (!is_array($data)) {
throw new \RuntimeException(
"Invalid JSON from TronGrid POST ({$code}): {$body}"
);
}
return $data;
}
/**
* Get on-chain TRX + USDT balance for a TRON address (Base58)
* using TronGrid /v1/accounts/{address}.
*/
private function getOnchainBalances(string $address): array
{
$data = $this->httpGetJson('/v1/accounts/' . $address);
if (empty($data['data']) || empty($data['data'][0])) {
return [
'trx_sun' => 0,
'trx' => 0.0,
'usdt_sun' => 0,
'usdt' => 0.0,
];
}
$acc = $data['data'][0];
// TRX in SUN
$trxSun = (int)($acc['balance'] ?? 0);
$trx = $trxSun / 1_000_000;
// USDT TRC20
$usdtSun = 0;
$trc20 = $acc['trc20'] ?? [];
foreach ($trc20 as $entry) {
if (isset($entry[$this->usdtContract])) {
$usdtSun = (int)$entry[$this->usdtContract];
break;
}
}
$usdt = $usdtSun / 1_000_000;
return [
'trx_sun' => $trxSun,
'trx' => $trx,
'usdt_sun' => $usdtSun,
'usdt' => $usdt,
];
}
/**
* Fetch private key from wallet_keys table for a given user + address.
*/
private function getPrivateKeyForAddress(int $userId, string $address): ?string
{
$sql = "SELECT private_key
FROM wallet_keys
WHERE user_id = ? AND wallet_add = ?
LIMIT 1";
$stmt = $this->db->prepare($sql);
if (!$stmt) {
throw new \RuntimeException(
"DB error preparing getPrivateKeyForAddress: " . $this->db->error
);
}
$stmt->bind_param("is", $userId, $address);
$stmt->execute();
$res = $stmt->get_result();
$row = $res->fetch_assoc();
$stmt->close();
return $row['private_key'] ?? null;
}
/**
* Log a sweep attempt into crypto_sweep_log.
*/
private function logSweep(
string $coin,
int $userId,
int $fromWalletId,
string $fromAddr,
int $toCwalletId,
string $toAddr,
float $amountDec,
int $amountRaw,
?string $txid,
string $status,
?string $reason = null
): void {
$sql = "INSERT INTO crypto_sweep_log
(network, coin, user_id, from_wallet_id, from_address,
to_cwallet_id, to_address,
amount_dec, amount_raw, txid, status, fail_reason)
VALUES (
'TRON', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)";
$stmt = $this->db->prepare($sql);
if (!$stmt) {
throw new \RuntimeException(
"DB error preparing logSweep: " . $this->db->error
);
}
$amountDecStr = number_format($amountDec, 6, '.', '');
$amountRawStr = (string)$amountRaw;
$txidStr = $txid ?? '';
$reasonStr = $reason ?? '';
$stmt->bind_param(
"siissssssss",
$coin,
$userId,
$fromWalletId,
$fromAddr,
$toCwalletId,
$toAddr,
$amountDecStr,
$amountRawStr,
$txidStr,
$status,
$reasonStr
);
$stmt->execute();
$stmt->close();
}
/**
* Sign a TRON transaction using ECDSA secp256k1.
*
* Signature format: r(32 bytes) || s(32 bytes) || v(1 byte),
* where v = 27 + recoveryParam. (TRC120-style)
*/
private function signTransaction(array $tx, string $privateKeyHex): array
{
if (empty($tx['raw_data_hex'])) {
throw new \RuntimeException('Missing raw_data_hex in transaction');
}
$rawHex = $tx['raw_data_hex'];
// Tron signs SHA-256(raw_data bytes)
$hashBin = hash('sha256', hex2bin($rawHex), true);
$hashHex = bin2hex($hashBin);
$ec = new EC('secp256k1');
$key = $ec->keyFromPrivate($privateKeyHex, 'hex');
$sig = $key->sign($hashHex, 'hex', ['canonical' => true]);
// r, s -> 32-byte hex each
$rHex = str_pad($sig->r->toString(16), 64, '0', STR_PAD_LEFT);
$sHex = str_pad($sig->s->toString(16), 64, '0', STR_PAD_LEFT);
// v = 27 + recId
$recId = $sig->recoveryParam; // 0 or 1
$v = 27 + $recId;
$vHex = str_pad(dechex($v), 2, '0', STR_PAD_LEFT);
$signatureHex = $rHex . $sHex . $vHex;
$tx['signature'] = [$signatureHex];
return $tx;
}
/**
* Send TRX from a user wallet to the central TRX wallet.
*
* Flow:
* 1) /wallet/createtransaction (unsigned)
* 2) locally sign raw_data_hex
* 3) /wallet/broadcasttransaction (signed tx)
*
* @return string|null txid on success, null on failure
*/
private function sendTrx(
string $fromAddress,
string $privateKeyHex,
string $toAddress,
int $amountSun
): ?string {
try {
// 1) Create unsigned transaction
$payload = [
'to_address' => $toAddress,
'owner_address' => $fromAddress,
'amount' => $amountSun,
'visible' => true, // Base58 addresses
];
$tx = $this->httpPostJson('/wallet/createtransaction', $payload);
if (!empty($tx['Error']) || !empty($tx['code'])) {
$msg = $tx['Error'] ?? ($tx['message'] ?? $tx['code'] ?? 'unknown');
echo " ! createtransaction error: {$msg}\n";
return null;
}
if (empty($tx['raw_data_hex'])) {
echo " ! createtransaction missing raw_data_hex\n";
return null;
}
// 2) Sign locally
$signedTx = $this->signTransaction($tx, $privateKeyHex);
// 3) Broadcast signed transaction
$resp = $this->httpPostJson('/wallet/broadcasttransaction', $signedTx);
if (!(isset($resp['result']) && $resp['result'] === true)) {
$msg = $resp['message'] ?? $resp['code'] ?? 'unknown';
echo " ! broadcasttransaction error: {$msg}\n";
return null;
}
if (!empty($resp['txid'])) {
return $resp['txid'];
}
if (!empty($tx['txID'])) {
return $tx['txID'];
}
echo " ! broadcasttransaction success but no txid in response\n";
return null;
} catch (\Throwable $e) {
echo " ! sendTrx exception: " . $e->getMessage() . "\n";
return null;
}
}
/**
* Placeholder: USDT-TRC20 sweep (not yet implemented).
* We still log as 'pending', no on-chain move yet.
*/
private function sendUsdt(
string $fromAddress,
string $privateKeyHex,
string $toAddress,
int $amountSun
): ?string {
echo " (USDT sweep not implemented yet - leaving as pending)\n";
return null;
}
/**
* Main sweeper:
* - loops all TRX user wallets
* - checks on-chain TRX & USDT balances via /v1/accounts/{address}
* - decides what to sweep
* - logs into crypto_sweep_log
*
* User balances in user_wallets are NOT touched here.
*/
public function run(): void
{
if (!$this->primaryTrx || !$this->primaryUsdt) {
echo "Missing primary TRX/USDT-TRC20 rows in cwallet table.\n";
echo "Make sure you have cwallet.coin = 'TRX' and 'USDT-TRC20'.\n";
return;
}
$sql = "SELECT wallet_id, user_id, wallet_add, balance
FROM user_wallets
WHERE coin = 'TRX' AND type = 'crypto' AND wallet_status = 'active'";
$res = $this->db->query($sql);
if (!$res) {
throw new \RuntimeException(
"DB error selecting TRX wallets: " . $this->db->error
);
}
echo "Sweeper starting...\n";
while ($row = $res->fetch_assoc()) {
$walletId = (int)$row['wallet_id'];
$userId = (int)$row['user_id'];
$address = $row['wallet_add'];
echo " → Checking {$address} (wallet_id={$walletId}, user_id={$userId})\n";
try {
$bal = $this->getOnchainBalances($address);
} catch (\Throwable $e) {
$this->logSweep(
'TRX',
$userId,
$walletId,
$address,
(int)$this->primaryTrx['cwallet_id'],
$this->primaryTrx['wallet_add'],
0.0,
0,
null,
'failed',
'balance_fetch: ' . $e->getMessage()
);
echo " ! Failed to fetch balances: " . $e->getMessage() . "\n";
continue;
}
$onchainTrx = $bal['trx'];
$onchainSun = $bal['trx_sun'];
$onchainUsdt = $bal['usdt'];
$usdtSun = $bal['usdt_sun'];
echo " On-chain TRX = {$onchainTrx}, USDT = {$onchainUsdt}\n";
$privKey = $this->getPrivateKeyForAddress($userId, $address);
if (!$privKey) {
echo " ! No private key in wallet_keys for this address, skip.\n";
continue;
}
// ---- TRX SWEEP ----
if ($onchainTrx > self::MIN_TRX_SWEEP) {
$sweepTrxAmount = max(0.0, $onchainTrx - self::TRX_GAS_BUFFER);
if ($sweepTrxAmount > 0) {
$sweepTrxSun = (int)round($sweepTrxAmount * 1_000_000);
$txid = $this->sendTrx(
$address,
$privKey,
$this->primaryTrx['wallet_add'],
$sweepTrxSun
);
$status = $txid ? 'broadcast' : 'failed';
$reason = $txid ? null : 'broadcast_failed';
$this->logSweep(
'TRX',
$userId,
$walletId,
$address,
(int)$this->primaryTrx['cwallet_id'],
$this->primaryTrx['wallet_add'],
$sweepTrxAmount,
$sweepTrxSun,
$txid,
$status,
$reason
);
if ($txid) {
echo " TRX sweep broadcasted: {$sweepTrxAmount} → {$this->primaryTrx['wallet_add']} (txid={$txid})\n";
} else {
echo " ! TRX sweep broadcast failed\n";
}
}
}
// ---- USDT SWEEP (plan + log only) ----
if ($onchainUsdt >= self::MIN_USDT_SWEEP) {
if ($onchainTrx <= self::TRX_GAS_BUFFER) {
echo " USDT present but not enough TRX for gas → skip for now.\n";
$this->logSweep(
'USDT-TRC20',
$userId,
$walletId,
$address,
(int)$this->primaryUsdt['cwallet_id'],
$this->primaryUsdt['wallet_add'],
$onchainUsdt,
$usdtSun,
null,
'failed',
'insufficient_gas_trx'
);
} else {
$sweepUsdtAmount = $onchainUsdt;
$sweepUsdtSun = $usdtSun;
$txid = $this->sendUsdt(
$address,
$privKey,
$this->primaryUsdt['wallet_add'],
$sweepUsdtSun
);
$status = $txid ? 'broadcast' : 'pending';
$reason = $txid ? null : 'not_broadcast_yet';
$this->logSweep(
'USDT-TRC20',
$userId,
$walletId,
$address,
(int)$this->primaryUsdt['cwallet_id'],
$this->primaryUsdt['wallet_add'],
$sweepUsdtAmount,
$sweepUsdtSun,
$txid,
$status,
$reason
);
echo " Planned USDT sweep {$sweepUsdtAmount} → {$this->primaryUsdt['wallet_add']} (txid=" . ($txid ?? 'PENDING') . ")\n";
}
}
}
$res->free();
echo "Sweeper finished.\n";
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!