diff --git a/app/Mod/Chain/Boot.php b/app/Mod/Chain/Boot.php index 8542763..18f5e4b 100644 --- a/app/Mod/Chain/Boot.php +++ b/app/Mod/Chain/Boot.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Mod\Chain; +use Core\Events\ConsoleBooting; use Core\Events\FrameworkBooted; use Mod\Chain\Services\DaemonRpc; use Mod\Chain\Services\WalletRpc; @@ -12,6 +13,7 @@ class Boot { public static array $listens = [ FrameworkBooted::class => 'onFrameworkBooted', + ConsoleBooting::class => 'onConsole', ]; public function onFrameworkBooted(FrameworkBooted $event): void @@ -22,4 +24,10 @@ class Boot app('router')->aliasMiddleware('auth.api', \App\Http\Middleware\ApiTokenAuth::class); } + + public function onConsole(ConsoleBooting $event): void + { + $event->command(Commands\ChainStart::class); + $event->command(Commands\ChainStatus::class); + } } diff --git a/app/Mod/Chain/Commands/ChainStart.php b/app/Mod/Chain/Commands/ChainStart.php new file mode 100644 index 0000000..b980d99 --- /dev/null +++ b/app/Mod/Chain/Commands/ChainStart.php @@ -0,0 +1,163 @@ +option('wallet-only')) { + $this->startDaemon($binDir, $dataDir, $miningAddress, $miningThreads); + } + + if (! $this->option('daemon-only')) { + $this->waitForDaemon($config['daemon_rpc'] ?? 'http://127.0.0.1:46941/json_rpc'); + $this->startWallet($binDir, $walletDir, $walletFile); + } + + return self::SUCCESS; + } + + private function startDaemon(string $binDir, string $dataDir, string $miningAddress, int $threads): void + { + if ($this->isRunning('lethean-testnet-chain-node')) { + $this->info('Daemon already running.'); + + return; + } + + @unlink("{$dataDir}/lock.lck"); + + $binary = "{$binDir}/lethean-testnet-chain-node"; + if (! is_executable($binary)) { + $this->error("Daemon binary not found: {$binary}"); + + return; + } + + $args = [ + $binary, + "--data-dir {$dataDir}", + '--rpc-bind-ip 0.0.0.0 --rpc-bind-port 46941', + '--p2p-bind-port 46942 --api-bind-port 46943', + '--rpc-enable-admin-api --rpc-ignore-offline', + '--allow-local-ip --log-level 1 --disable-upnp', + ]; + + if ($miningAddress) { + $args[] = "--start-mining {$miningAddress} --mining-threads {$threads}"; + } + + $cmd = implode(' ', $args); + $logFile = "{$dataDir}/lethean-testnet-chain-node.log"; + + // Safe: all values from config, no user input + exec("nohup {$cmd} > {$logFile} 2>&1 &"); // @codeCoverageIgnore + + $this->info("Daemon started. Mining: {$threads} threads."); + } + + private function startWallet(string $binDir, string $walletDir, string $walletFile): void + { + if ($this->isRunning('lethean-wallet-cli')) { + $this->info('Wallet already running.'); + + return; + } + + $binary = "{$binDir}/lethean-testnet-wallet-cli"; + if (! is_executable($binary)) { + $binary = '/opt/lethean/bin/lethean-wallet-cli'; + } + + if (! is_executable($binary)) { + $this->error('Wallet binary not found.'); + + return; + } + + $walletPath = "{$walletDir}/{$walletFile}"; + if (! file_exists($walletPath)) { + $this->error("Wallet file not found: {$walletPath}"); + + return; + } + + $cmd = implode(' ', [ + $binary, + "--wallet-file {$walletPath}", + '--password ""', + '--daemon-address 127.0.0.1:46941', + '--rpc-bind-port 46944 --rpc-bind-ip 127.0.0.1', + ]); + + // Safe: all values from config, no user input + exec("nohup {$cmd} > {$walletDir}/wallet-rpc.log 2>&1 &"); // @codeCoverageIgnore + + $this->info('Wallet RPC started on port 46944.'); + } + + private function waitForDaemon(string $rpcUrl, int $timeout = 30): void + { + $this->info('Waiting for daemon RPC...'); + $endpoint = str_replace('/json_rpc', '', $rpcUrl) . '/json_rpc'; + $start = time(); + + while (time() - $start < $timeout) { + try { + $response = @file_get_contents($endpoint, false, stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n", + 'content' => '{"jsonrpc":"2.0","id":"0","method":"getinfo"}', + 'timeout' => 3, + ], + ])); + + if ($response) { + $this->info('Daemon ready.'); + + return; + } + } catch (\Throwable $e) { + // Not ready yet + } + + sleep(2); + } + + $this->warn("Daemon not responding after {$timeout}s."); + } + + private function isRunning(string $processName): bool + { + exec("pgrep -f " . escapeshellarg($processName), $output); + + return ! empty($output); + } +} diff --git a/app/Mod/Chain/Commands/ChainStatus.php b/app/Mod/Chain/Commands/ChainStatus.php new file mode 100644 index 0000000..80ebc42 --- /dev/null +++ b/app/Mod/Chain/Commands/ChainStatus.php @@ -0,0 +1,60 @@ +info('=== Chain Status ==='); + + // Daemon — safe: hardcoded process name, no user input + $daemonRunning = $this->isRunning('lethean-testnet-chain-node'); + $this->line('Daemon: ' . ($daemonRunning ? 'running' : 'stopped')); + + if ($daemonRunning) { + $info = $rpc->getInfo(); + if (! isset($info['_offline'])) { + $this->line(" Height: {$info['height']}"); + $this->line(" Aliases: {$info['alias_count']}"); + $this->line(" TX Pool: {$info['tx_pool_size']}"); + $this->line(' PoS: ' . (($info['pos_allowed'] ?? false) ? 'active' : 'inactive')); + $this->line(" Difficulty: {$info['pow_difficulty']}"); + } else { + $this->warn(' RPC unreachable'); + } + } + + // Wallet — safe: hardcoded process name + $walletRunning = $this->isRunning('lethean-wallet-cli'); + $this->line('Wallet: ' . ($walletRunning ? 'running' : 'stopped')); + + // LNS — safe: hardcoded process name + $lnsRunning = $this->isRunning('lns-new'); + $this->line('LNS: ' . ($lnsRunning ? 'running' : 'stopped')); + + return self::SUCCESS; + } + + private function isRunning(string $processName): bool + { + // Safe: escapeshellarg prevents injection, process names are hardcoded + exec("pgrep -f " . escapeshellarg($processName), $output); // @codeCoverageIgnore + + return ! empty($output); + } +} diff --git a/app/Mod/Chain/config.php b/app/Mod/Chain/config.php index 62e1d78..725a6d4 100644 --- a/app/Mod/Chain/config.php +++ b/app/Mod/Chain/config.php @@ -6,8 +6,16 @@ return [ 'daemon_rpc' => env('DAEMON_RPC', 'http://127.0.0.1:46941/json_rpc'), 'wallet_rpc' => env('WALLET_RPC', 'http://127.0.0.1:46944/json_rpc'), 'cache_ttl' => (int) env('CHAIN_CACHE_TTL', 10), - 'network' => env('CHAIN_NETWORK', 'testnet'), // testnet or mainnet + 'network' => env('CHAIN_NETWORK', 'testnet'), 'lns_url' => env('LNS_URL', 'http://127.0.0.1:5553'), 'api_token' => env('API_TOKEN', ''), - 'api_url' => env('API_URL', ''), // empty = same origin, set to https://api.lthn.io for production + 'api_url' => env('API_URL', 'https://api.lthn.io'), + + // Binary management (chain:start, chain:status) + 'bin_dir' => env('CHAIN_BIN_DIR', '/opt/lethean/testnet'), + 'data_dir' => env('CHAIN_DATA_DIR', '/opt/lethean/testnet-data'), + 'wallet_dir' => env('CHAIN_WALLET_DIR', '/opt/lethean/testnet-wallet'), + 'wallet_file' => env('CHAIN_WALLET_FILE', 'registrar.wallet'), + 'mining_address' => env('CHAIN_MINING_ADDRESS', 'iTHNHN11yXMeBphpFSuHnDaSJ15QxiSEJXNY59VKbxKq4ype4xAH1PZHd1EKTknkPK9hHTu2G2tBBZzvrcRFaYMF8vWTzFZjGY'), + 'mining_threads' => (int) env('CHAIN_MINING_THREADS', 12), ];