CorePHP manages the testnet chain binaries via ConsoleBooting lifecycle. chain:start checks if daemon/wallet are running, starts them if not, waits for RPC readiness. chain:status shows daemon height, aliases, PoS status, wallet and LNS node state. Config-driven paths for binary locations, data dirs, mining address. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
163 lines
4.9 KiB
PHP
163 lines
4.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Mod\Chain\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
|
|
/**
|
|
* Start the testnet chain daemon and wallet if not already running.
|
|
*
|
|
* php artisan chain:start
|
|
* php artisan chain:start --daemon-only
|
|
* php artisan chain:start --wallet-only
|
|
*/
|
|
class ChainStart extends Command
|
|
{
|
|
protected $signature = 'chain:start
|
|
{--daemon-only : Only start the daemon}
|
|
{--wallet-only : Only start the wallet}';
|
|
|
|
protected $description = 'Start testnet chain daemon and wallet';
|
|
|
|
public function handle(): int
|
|
{
|
|
$config = config('chain');
|
|
$binDir = $config['bin_dir'] ?? '/opt/lethean/testnet';
|
|
$dataDir = $config['data_dir'] ?? '/opt/lethean/testnet-data';
|
|
$walletDir = $config['wallet_dir'] ?? '/opt/lethean/testnet-wallet';
|
|
$walletFile = $config['wallet_file'] ?? 'registrar.wallet';
|
|
$miningAddress = $config['mining_address'] ?? '';
|
|
$miningThreads = $config['mining_threads'] ?? 12;
|
|
|
|
if (! $this->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);
|
|
}
|
|
}
|