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