lthn.io/app/Mod/Chain/Services/DaemonRpc.php
Claude b4e4766e01
feat: HealthCheckable interface on DaemonRpc and WalletRpc
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>
2026-04-04 11:43:01 +01:00

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'),
];
}
}