feat: chain:start and chain:status artisan commands
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>
This commit is contained in:
parent
c89bee8e51
commit
9286f84020
4 changed files with 241 additions and 2 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
163
app/Mod/Chain/Commands/ChainStart.php
Normal file
163
app/Mod/Chain/Commands/ChainStart.php
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
60
app/Mod/Chain/Commands/ChainStatus.php
Normal file
60
app/Mod/Chain/Commands/ChainStatus.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mod\Chain\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Mod\Chain\Services\DaemonRpc;
|
||||
|
||||
/**
|
||||
* Show chain daemon and wallet status.
|
||||
*
|
||||
* php artisan chain:status
|
||||
*/
|
||||
class ChainStatus extends Command
|
||||
{
|
||||
protected $signature = 'chain:status';
|
||||
|
||||
protected $description = 'Show testnet chain daemon and wallet status';
|
||||
|
||||
public function handle(DaemonRpc $rpc): int
|
||||
{
|
||||
$this->info('=== Chain Status ===');
|
||||
|
||||
// Daemon — safe: hardcoded process name, no user input
|
||||
$daemonRunning = $this->isRunning('lethean-testnet-chain-node');
|
||||
$this->line('Daemon: ' . ($daemonRunning ? '<fg=green>running</>' : '<fg=red>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 ? '<fg=green>running</>' : '<fg=red>stopped</>'));
|
||||
|
||||
// LNS — safe: hardcoded process name
|
||||
$lnsRunning = $this->isRunning('lns-new');
|
||||
$this->line('LNS: ' . ($lnsRunning ? '<fg=green>running</>' : '<fg=red>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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue