$p !== '')); if ($parts === []) { $bot->sendMessage( "Usage: /transfer [TRX|USDT|ETH|BTC] [amount]\n" ."Omit amount to transfer all. Chain is inferred from address.\n" ."Defaults: Tron→USDT, ETH→ETH, BTC→BTC.\n" .'To = per-chain TRANSFER_TO_ADDRESS* (env).' ); return; } $from = $parts[0]; $chain = $this->detectChain($from); if ($chain === null) { $bot->sendMessage('Unrecognized address. Supported: Tron (T…), ETH (0x…), BTC (1…/3…/bc1…).'); return; } $asset = $this->defaultAsset($chain); $amount = null; $allowed = $this->assetsForChain($chain); if (count($parts) === 2) { $second = strtoupper($parts[1]); if (in_array($second, $allowed, true)) { $asset = $second; } elseif ($this->isAmount($parts[1], match ($chain) { 'eth' => 18, 'btc' => 8, default => 6, })) { $amount = $parts[1]; } else { $bot->sendMessage('Second arg must be '.implode('|', $allowed).', or an amount.'); return; } } elseif (count($parts) >= 3) { $asset = strtoupper($parts[1]); if (! in_array($asset, $allowed, true)) { $bot->sendMessage('Asset must be '.implode(' or ', $allowed).' for '.$chain.'.'); return; } $decimals = match ($asset) { 'ETH' => 18, 'BTC' => 8, default => 6, }; if (! $this->isAmount($parts[2], $decimals)) { $bot->sendMessage('Invalid amount.'); return; } $amount = $parts[2]; } $label = $amount === null ? "ALL {$asset}" : "{$amount} {$asset}"; $bot->sendMessage("⏳ Transferring {$label} from {$from} ({$chain}) …"); $result = $this->transfers->handle($chain, $from, $amount, $asset, $this->operatorLabel($bot)); if (! ($result['ok'] ?? false)) { $bot->sendMessage('❌ '.($result['error'] ?? 'Transfer failed')); return; } $bot->sendMessage(implode("\n", [ '✅ Transfer submitted', 'Chain: '.$chain, 'From: '.$result['from'], 'To: '.$result['to'], 'Amount: '.$result['amount'].' '.$result['asset'], 'TxID: '.$result['txid'], ])); } private function detectChain(string $address): ?string { $address = trim($address); if (preg_match('/^T[1-9A-HJ-NP-Za-km-z]{33}$/', $address)) { return 'tron'; } if (preg_match('/^0x[0-9a-fA-F]{40}$/', $address)) { return 'eth'; } if (preg_match('/^(bc1|tb1)[a-z0-9]{8,87}$/i', $address)) { return 'btc'; } if (preg_match('/^[13][1-9A-HJ-NP-Za-km-z]{24,33}$/', $address)) { return 'btc'; } return null; } private function defaultAsset(string $chain): string { return match ($chain) { 'eth' => 'ETH', 'btc' => 'BTC', default => 'USDT', }; } /** @return list */ private function assetsForChain(string $chain): array { return match ($chain) { 'eth' => ['ETH', 'USDT'], 'btc' => ['BTC'], default => ['TRX', 'USDT'], }; } private function isAmount(string $value, int $decimals = 6): bool { return (bool) preg_match('/^\d+(\.\d{1,'.$decimals.'})?$/', $value); } private function operatorLabel(Nutgram $bot): string { $user = $bot->user(); $id = $user?->id ?? $bot->userId(); $username = $user?->username; if ($username) { return "telegram:{$id}@{$username}"; } $name = trim((string) ($user?->first_name ?? '')); return $name !== '' ? "telegram:{$id}({$name})" : "telegram:{$id}"; } }