190 lines
5.7 KiB
PHP
Executable File
190 lines
5.7 KiB
PHP
Executable File
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace app\service;
|
||
|
||
/**
|
||
* NAC 量子浏览器 API 服务层
|
||
* 所有对后端 9551 端口的调用均在此类完成
|
||
* 前端模板不直接调用任何外部接口
|
||
*/
|
||
class ExplorerApi
|
||
{
|
||
// 后端 API 地址(服务端调用,不暴露给前端)
|
||
private static string $apiBase = 'http://127.0.0.1:9551';
|
||
private static int $timeout = 5;
|
||
|
||
/**
|
||
* 通用 GET 请求
|
||
*/
|
||
private static function get(string $path, array $params = []): ?array
|
||
{
|
||
$url = self::$apiBase . $path;
|
||
if (!empty($params)) {
|
||
$url .= '?' . http_build_query($params);
|
||
}
|
||
|
||
$ch = curl_init();
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_URL => $url,
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_TIMEOUT => self::$timeout,
|
||
CURLOPT_CONNECTTIMEOUT => 3,
|
||
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
||
CURLOPT_FOLLOWLOCATION => false,
|
||
]);
|
||
$body = curl_exec($ch);
|
||
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
if ($body === false || $status < 200 || $status >= 300) {
|
||
return null;
|
||
}
|
||
$json = json_decode($body, true);
|
||
return is_array($json) ? $json : null;
|
||
}
|
||
|
||
/**
|
||
* 获取网络统计数据
|
||
* 从 /health + /api/v1/blocks/latest 组合(/api/v1/network/stats 不存在)
|
||
*/
|
||
public static function getStats(): array
|
||
{
|
||
$health = self::get('/health');
|
||
$latest = self::get('/api/v1/blocks/latest');
|
||
|
||
$h = $health['data'] ?? [];
|
||
$b = $latest['data'] ?? [];
|
||
|
||
return [
|
||
'currentBlock' => $b['number'] ?? ($h['block']['height'] ?? 0),
|
||
'cbppConsensus' => $h['cbpp_consensus'] ?? ($b['cbppConsensus'] ?? 'unknown'),
|
||
'csnpNetwork' => $h['csnp_network'] ?? 'unknown',
|
||
'constitutionLayer' => $h['constitution_layer'] ?? ($b['constitutionLayer'] ?? false),
|
||
'fluidBlockMode' => $h['fluid_block_mode'] ?? ($b['isFluid'] ?? false),
|
||
'nvmVersion' => $h['nvm_version'] ?? 'N/A',
|
||
'chainId' => 20260131,
|
||
'network' => 'mainnet',
|
||
'nodeCount' => 1,
|
||
'totalTransactions' => 0,
|
||
'totalAddresses' => 0,
|
||
'totalAssets' => 0,
|
||
'tps' => 0,
|
||
'avgBlockTime' => 3.0,
|
||
'latestValidator' => $b['validator'] ?? 'N/A',
|
||
'latestHash' => $b['hash'] ?? '',
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 获取区块列表
|
||
* API 返回格式: {data: {blocks: [], total: N}}
|
||
* 注意:blocks 中的 txCount 字段来自 transactionCount
|
||
*/
|
||
public static function getBlocks(int $limit = 20, int $page = 1): array
|
||
{
|
||
$res = self::get('/api/v1/blocks', ['limit' => $limit, 'page' => $page]);
|
||
if ($res && isset($res['data']['blocks'])) {
|
||
// 统一字段名:transactionCount → txCount
|
||
$blocks = array_map(function ($b) {
|
||
$b['txCount'] = $b['transactionCount'] ?? $b['txCount'] ?? 0;
|
||
return $b;
|
||
}, $res['data']['blocks']);
|
||
return [
|
||
'blocks' => $blocks,
|
||
'total' => $res['data']['total'] ?? count($blocks),
|
||
'page' => $page,
|
||
'limit' => $limit,
|
||
];
|
||
}
|
||
return ['blocks' => [], 'total' => 0, 'page' => 1, 'limit' => $limit];
|
||
}
|
||
|
||
/**
|
||
* 获取单个区块详情
|
||
*/
|
||
public static function getBlock(int $number): ?array
|
||
{
|
||
$res = self::get('/api/v1/blocks/' . $number);
|
||
return $res['data'] ?? null;
|
||
}
|
||
|
||
/**
|
||
* 获取最新区块
|
||
*/
|
||
public static function getLatestBlock(): ?array
|
||
{
|
||
$res = self::get('/api/v1/blocks/latest');
|
||
return $res['data'] ?? null;
|
||
}
|
||
|
||
/**
|
||
* 搜索(区块号/交易哈希/地址)
|
||
*/
|
||
public static function search(string $query): ?array
|
||
{
|
||
return self::get('/api/v1/search', ['q' => $query]);
|
||
}
|
||
|
||
/**
|
||
* 获取地址信息
|
||
*/
|
||
public static function getAddress(string $address): ?array
|
||
{
|
||
$res = self::get('/api/v1/addresses/' . urlencode($address));
|
||
return $res['data'] ?? null;
|
||
}
|
||
|
||
/**
|
||
* 健康检查
|
||
*/
|
||
public static function health(): ?array
|
||
{
|
||
$res = self::get('/health');
|
||
return $res['data'] ?? null;
|
||
}
|
||
|
||
// ===== 工具函数 =====
|
||
|
||
/**
|
||
* 格式化时间戳
|
||
*/
|
||
public static function formatTime(int $ts): string
|
||
{
|
||
return date('Y-m-d H:i:s', $ts);
|
||
}
|
||
|
||
/**
|
||
* 多久前
|
||
*/
|
||
public static function timeAgo(int $ts): string
|
||
{
|
||
$diff = time() - $ts;
|
||
if ($diff < 60) return $diff . ' 秒前';
|
||
if ($diff < 3600) return floor($diff / 60) . ' 分钟前';
|
||
if ($diff < 86400) return floor($diff / 3600) . ' 小时前';
|
||
return floor($diff / 86400) . ' 天前';
|
||
}
|
||
|
||
/**
|
||
* 截断哈希显示
|
||
*/
|
||
public static function shortHash(string $hash, int $front = 10, int $back = 8): string
|
||
{
|
||
if (strlen($hash) <= $front + $back + 3) return $hash;
|
||
return substr($hash, 0, $front) . '...' . substr($hash, -$back);
|
||
}
|
||
|
||
/**
|
||
* 状态徽章样式
|
||
*/
|
||
public static function statusBadge(string $status): string
|
||
{
|
||
return match ($status) {
|
||
'active', 'connected', 'ok' => 'badge bg-success',
|
||
'unknown' => 'badge bg-secondary',
|
||
default => 'badge bg-danger',
|
||
};
|
||
}
|
||
}
|