Services implement healthCheck() returning {status, detail, stale?}.
Status page refactored to use healthCheck() instead of ad-hoc checks.
Statuses: healthy, degraded (stale data), unhealthy (unreachable).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
179 lines
4.7 KiB
PHP
179 lines
4.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Mod\Chain\Services;
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
/**
|
|
* Lethean daemon JSON-RPC client.
|
|
*
|
|
* $rpc = app(DaemonRpc::class);
|
|
* $info = $rpc->getInfo();
|
|
* $block = $rpc->getBlockByHeight(12345);
|
|
*/
|
|
class DaemonRpc implements \Mod\Chain\Contracts\ChainDaemon, \Mod\Chain\Contracts\HealthCheckable
|
|
{
|
|
private string $endpoint;
|
|
private int $cacheTtl;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->endpoint = config('chain.daemon_rpc', 'http://127.0.0.1:46941/json_rpc');
|
|
$this->cacheTtl = config('chain.cache_ttl', 10);
|
|
}
|
|
|
|
/**
|
|
* $info = $rpc->getInfo();
|
|
* echo $info['height'];
|
|
*/
|
|
public function getInfo(): array
|
|
{
|
|
return Cache::remember('chain.info', $this->cacheTtl, function () {
|
|
return $this->call('getinfo');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* $header = $rpc->getBlockByHeight(12345);
|
|
*/
|
|
public function getBlockByHeight(int $height): array
|
|
{
|
|
// Blocks are immutable — cache for 1 hour
|
|
return Cache::remember("chain.block.{$height}", 3600, function () use ($height) {
|
|
return $this->call('getblockheaderbyheight', ['height' => $height]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* $header = $rpc->getBlockByHash('abc123...');
|
|
*/
|
|
public function getBlockByHash(string $hash): array
|
|
{
|
|
return $this->call('getblockheaderbyhash', ['hash' => $hash]);
|
|
}
|
|
|
|
/**
|
|
* $header = $rpc->getLastBlockHeader();
|
|
*/
|
|
public function getLastBlockHeader(): array
|
|
{
|
|
return Cache::remember('chain.lastblock', $this->cacheTtl, function () {
|
|
return $this->call('getlastblockheader');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* $aliases = $rpc->getAllAliases();
|
|
*/
|
|
public function getAllAliases(): array
|
|
{
|
|
return Cache::remember('chain.aliases', 60, function () {
|
|
return $this->call('get_all_alias_details');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* $alias = $rpc->getAliasByName('charon');
|
|
*/
|
|
public function getAliasByName(string $name): ?array
|
|
{
|
|
$result = $this->call('get_alias_details', ['alias' => $name]);
|
|
|
|
if (($result['status'] ?? '') === 'NOT_FOUND' || empty($result['alias_details']['address'] ?? '')) {
|
|
return null;
|
|
}
|
|
|
|
return $result['alias_details'] ?? null;
|
|
}
|
|
|
|
/**
|
|
* $count = $rpc->getBlockCount();
|
|
*/
|
|
public function getBlockCount(): int
|
|
{
|
|
$result = $this->call('getblockcount');
|
|
|
|
return $result['count'] ?? 0;
|
|
}
|
|
|
|
/**
|
|
* $tx = $rpc->getTransaction('abc123...');
|
|
*/
|
|
public function getTransaction(string $hash): array
|
|
{
|
|
return $this->call('get_tx_details', ['tx_hash' => $hash]);
|
|
}
|
|
|
|
/**
|
|
* Raw JSON-RPC call.
|
|
*
|
|
* $result = $rpc->call('getinfo');
|
|
*/
|
|
public function call(string $method, array $params = []): array
|
|
{
|
|
$payload = [
|
|
'jsonrpc' => '2.0',
|
|
'id' => '0',
|
|
'method' => $method,
|
|
];
|
|
|
|
if ($params) {
|
|
$payload['params'] = $params;
|
|
}
|
|
|
|
try {
|
|
$response = Http::timeout(5)
|
|
->accept('application/json')
|
|
->post($this->endpoint, $payload);
|
|
|
|
if ($response->failed()) {
|
|
return ['error' => 'Daemon unreachable', '_offline' => true];
|
|
}
|
|
|
|
$data = $response->json();
|
|
$result = $data["result"] ?? $data["error"] ?? [];
|
|
|
|
// Only cache successful responses as stale fallback
|
|
if (isset($data['result'])) {
|
|
Cache::put("chain.stale.{$method}", $result, 3600);
|
|
}
|
|
|
|
return $result;
|
|
} catch (\Throwable $e) {
|
|
// Return stale cached data if daemon is down
|
|
$stale = Cache::get("chain.stale.{$method}");
|
|
if ($stale) {
|
|
$stale['_stale'] = true;
|
|
|
|
return $stale;
|
|
}
|
|
|
|
return ['error' => 'Daemon unreachable', '_offline' => true];
|
|
}
|
|
}
|
|
|
|
public function healthCheck(): array
|
|
{
|
|
$info = $this->getInfo();
|
|
|
|
if (isset($info['_offline'])) {
|
|
return ['status' => 'unhealthy', 'detail' => 'Unreachable'];
|
|
}
|
|
|
|
if (isset($info['_stale'])) {
|
|
return [
|
|
'status' => 'degraded',
|
|
'detail' => 'Stale data — height ' . number_format($info['height'] ?? 0),
|
|
'stale' => true,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'status' => 'healthy',
|
|
'detail' => 'Height ' . number_format($info['height'] ?? 0) . ' — ' . ($info['status'] ?? 'OK'),
|
|
];
|
|
}
|
|
}
|