lthn.io/app/Mod/Chain/Services/DaemonRpc.php
Claude 1f29000c11
feat(chain): circuit breaker with stale cache fallback
- DaemonRpc: try/catch with stale cache (1h TTL) when daemon is down
- WalletRpc: try/catch with clear error message
- Health endpoint: status=offline/degraded/critical/low_funds/healthy
- Reports wallet_online, daemon_online, stale flags
- Reduced daemon timeout from 10s to 5s, wallet from 30s to 15s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 03:06:23 +01:00

152 lines
3.8 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
{
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
{
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'] ?? [];
// Store successful response as stale fallback (1 hour TTL)
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];
}
}
}