diff --git a/lib/opengradient/contracts/teeRegistry.ts b/lib/opengradient/contracts/teeRegistry.ts index c5097fa7c2..7e8138c52a 100644 --- a/lib/opengradient/contracts/teeRegistry.ts +++ b/lib/opengradient/contracts/teeRegistry.ts @@ -1,55 +1,14 @@ import { ethers } from 'ethers'; import type { Address } from 'viem'; +import type { TEENodeWithStatus, TEEInfo, TEERegistryOverview, TEETypeInfo, TEETypeSummary } from 'lib/opengradient/teeRegistry'; +import { TEE_REGISTRY_ADDRESS } from 'lib/opengradient/teeRegistry'; + import TEERegistryAbi from './abi/TEERegistry.json'; import { ethDevnetProvider } from './providers'; -export const TEE_REGISTRY_ADDRESS = '0x4e72238852f3c918f4E4e57AeC9280dDB0c80248'; - const contract = new ethers.Contract(TEE_REGISTRY_ADDRESS, TEERegistryAbi, ethDevnetProvider); -export interface TEETypeInfo { - typeId: number; - name: string; - addedAt: bigint; -} - -export interface TEEInfo { - teeId: string; - owner: Address; - paymentAddress: Address; - endpoint: string; - publicKey: string; - tlsCertificate: string; - pcrHash: string; - teeType: number; - enabled: boolean; - registeredAt: bigint; - lastHeartbeatAt: bigint; -} - -export interface TEENodeWithStatus extends TEEInfo { - isActive: boolean; -} - -export interface TEETypeSummary { - typeId: number; - name: string; - totalNodes: number; - enabledNodes: number; - activeNodes: number; - approvedPCRs: number; - addedAt: bigint; -} - -export interface TEERegistryStats { - totalTypes: number; - totalNodes: number; - activeNodes: number; - enabledNodes: number; - approvedPCRs: number; -} - export const getTEETypes = async(): Promise> => { const [ typeIds, infos ] = await contract.getTEETypes(); @@ -109,11 +68,7 @@ export const getHeartbeatMaxAge = async(): Promise => { /** * Fetch full registry overview: types, nodes per type with status, and global stats. */ -export const getTEERegistryOverview = async(): Promise<{ - types: Array; - stats: TEERegistryStats; - nodesByType: Record>; -}> => { +export const getTEERegistryOverviewFromContract = async(): Promise => { // 1. Get all TEE types const types = await getTEETypes(); @@ -186,5 +141,3 @@ export const getTEERegistryOverview = async(): Promise<{ nodesByType, }; }; - -export const TEE_REGISTRY_QUERY_KEY = [ 'opengradient', 'teeRegistry' ]; diff --git a/lib/opengradient/teeRegistry.ts b/lib/opengradient/teeRegistry.ts new file mode 100644 index 0000000000..d7559f0c51 --- /dev/null +++ b/lib/opengradient/teeRegistry.ts @@ -0,0 +1,107 @@ +import type { Address } from 'viem'; + +export const TEE_REGISTRY_ADDRESS = '0x4e72238852f3c918f4E4e57AeC9280dDB0c80248'; + +export interface TEETypeInfo { + typeId: number; + name: string; + addedAt: bigint; +} + +export interface TEEInfo { + teeId: string; + owner: Address; + paymentAddress: Address; + endpoint: string; + publicKey: string; + tlsCertificate: string; + pcrHash: string; + teeType: number; + enabled: boolean; + registeredAt: bigint; + lastHeartbeatAt: bigint; +} + +export interface TEENodeWithStatus extends TEEInfo { + isActive: boolean; +} + +export interface TEETypeSummary { + typeId: number; + name: string; + totalNodes: number; + enabledNodes: number; + activeNodes: number; + approvedPCRs: number; + addedAt: bigint; +} + +export interface TEERegistryStats { + totalTypes: number; + totalNodes: number; + activeNodes: number; + enabledNodes: number; + approvedPCRs: number; +} + +export type TEERegistryOverview = { + types: Array; + stats: TEERegistryStats; + nodesByType: Record>; +}; + +export type SerializedTEERegistryOverview = { + types: Array & { addedAt: string }>; + stats: TEERegistryStats; + nodesByType: Record & { + registeredAt: string; + lastHeartbeatAt: string; + }>>; +}; + +export const serializeTEERegistryOverview = (overview: TEERegistryOverview): SerializedTEERegistryOverview => ({ + types: overview.types.map((type) => ({ + ...type, + addedAt: type.addedAt.toString(), + })), + stats: overview.stats, + nodesByType: Object.fromEntries( + Object.entries(overview.nodesByType).map(([ typeId, nodes ]) => [ + typeId, + nodes.map((node) => ({ + ...node, + registeredAt: node.registeredAt.toString(), + lastHeartbeatAt: node.lastHeartbeatAt.toString(), + })), + ]), + ), +}); + +export const parseTEERegistryOverview = (overview: SerializedTEERegistryOverview): TEERegistryOverview => ({ + types: overview.types.map((type) => ({ + ...type, + addedAt: BigInt(type.addedAt), + })), + stats: overview.stats, + nodesByType: Object.fromEntries( + Object.entries(overview.nodesByType).map(([ typeId, nodes ]) => [ + Number(typeId), + nodes.map((node) => ({ + ...node, + registeredAt: BigInt(node.registeredAt), + lastHeartbeatAt: BigInt(node.lastHeartbeatAt), + })), + ]), + ), +}); + +export const getTEERegistryOverview = async(): Promise => { + const response = await fetch('/api/opengradient/tee-registry'); + if (!response.ok) { + throw new Error(`Failed to load TEE registry overview: ${ response.status }`); + } + + return parseTEERegistryOverview(await response.json() as SerializedTEERegistryOverview); +}; + +export const TEE_REGISTRY_QUERY_KEY = [ 'opengradient', 'teeRegistry' ]; diff --git a/pages/api/opengradient/tee-registry.ts b/pages/api/opengradient/tee-registry.ts new file mode 100644 index 0000000000..3c7c180021 --- /dev/null +++ b/pages/api/opengradient/tee-registry.ts @@ -0,0 +1,15 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +import { getTEERegistryOverviewFromContract } from 'lib/opengradient/contracts/teeRegistry'; +import { serializeTEERegistryOverview } from 'lib/opengradient/teeRegistry'; + +export default async function handler(_req: NextApiRequest, res: NextApiResponse) { + try { + const overview = await getTEERegistryOverviewFromContract(); + res.status(200).json(serializeTEERegistryOverview(overview)); + } catch (error) { + res.status(500).json({ + error: error instanceof Error ? error.message : 'Failed to load TEE registry overview', + }); + } +} diff --git a/ui/home/HeroBanner.tsx b/ui/home/HeroBanner.tsx index a19b97ce17..71b9c8edb3 100644 --- a/ui/home/HeroBanner.tsx +++ b/ui/home/HeroBanner.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { route } from 'nextjs-routes'; import useApiQuery from 'lib/api/useApiQuery'; -import { getTEERegistryOverview, TEE_REGISTRY_QUERY_KEY } from 'lib/opengradient/contracts/teeRegistry'; +import { getTEERegistryOverview, TEE_REGISTRY_QUERY_KEY } from 'lib/opengradient/teeRegistry'; import { HOMEPAGE_STATS, HOMEPAGE_STATS_MICROSERVICE } from 'stubs/stats'; import { LinkBox, LinkOverlay } from 'toolkit/chakra/link'; import { Skeleton } from 'toolkit/chakra/skeleton'; @@ -319,7 +319,7 @@ const HeroBanner = () => { label="TEE Operators" iconName="nft_shield" loading={ teeRegistryQuery.isPlaceholderData } - value={ `${ teeStats.activeNodes }/${ teeStats.enabledNodes }` } + value={ teeStats.activeNodes.toLocaleString() } /> { const types = query.data?.types ?? PLACEHOLDER_TEE_TYPES; const nodes = React.useMemo(() => { const nodesByType = query.data?.nodesByType ?? {}; - return Object.values(nodesByType).flat().sort((a, b) => Number(b.lastHeartbeatAt - a.lastHeartbeatAt)); + return Object.values(nodesByType).flat() + .filter((node) => node.isActive) + .sort((a, b) => Number(b.lastHeartbeatAt - a.lastHeartbeatAt)); }, [ query.data?.nodesByType ]); - const primaryType = types[0] ?? PLACEHOLDER_TEE_TYPES[0]; - const visibleNodes = nodes.slice(0, 3); + const primaryType = types[0]; const isLoading = query.isPlaceholderData; return ( @@ -239,13 +240,13 @@ const TrustedExecution = () => { @@ -270,32 +271,23 @@ const TrustedExecution = () => { - { primaryType.name } + { primaryType?.name ?? 'Loading' } Active - { primaryType.activeNodes }/{ primaryType.totalNodes } + { primaryType?.activeNodes.toLocaleString() ?? '0' } Enabled - { primaryType.enabledNodes } + { primaryType?.enabledNodes.toLocaleString() ?? '0' } PCRs - { primaryType.approvedPCRs } + { primaryType?.approvedPCRs.toLocaleString() ?? '0' } - - 0 ? `${ Math.round((primaryType.activeNodes / primaryType.totalNodes) * 100) }%` : '0' } - minW={ primaryType.activeNodes > 0 ? '18px' : '0' } - bg={ colors.cyan } - borderRadius="2px" - /> - { - { visibleNodes.length > 0 ? visibleNodes.map((node) => ( + { nodes.length > 0 ? nodes.map((node) => ( )) : ( diff --git a/ui/opengradient/teeRegistry/TEENodeDetailDrawer.tsx b/ui/opengradient/teeRegistry/TEENodeDetailDrawer.tsx index 9c3c890fb6..54304b51ec 100644 --- a/ui/opengradient/teeRegistry/TEENodeDetailDrawer.tsx +++ b/ui/opengradient/teeRegistry/TEENodeDetailDrawer.tsx @@ -2,7 +2,7 @@ import { Box, Flex, Text, VStack } from '@chakra-ui/react'; import React from 'react'; import dayjs from 'lib/date/dayjs'; -import type { TEENodeWithStatus } from 'lib/opengradient/contracts/teeRegistry'; +import type { TEENodeWithStatus } from 'lib/opengradient/teeRegistry'; import { DrawerBackdrop, DrawerBody, DrawerCloseTrigger, DrawerContent, DrawerHeader, DrawerRoot, DrawerTitle } from 'toolkit/chakra/drawer'; import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; diff --git a/ui/opengradient/teeRegistry/TEENodesTable.tsx b/ui/opengradient/teeRegistry/TEENodesTable.tsx index b039cf44fb..1019804210 100644 --- a/ui/opengradient/teeRegistry/TEENodesTable.tsx +++ b/ui/opengradient/teeRegistry/TEENodesTable.tsx @@ -2,7 +2,7 @@ import { Box, Flex, Text } from '@chakra-ui/react'; import React from 'react'; import dayjs from 'lib/date/dayjs'; -import type { TEENodeWithStatus, TEETypeSummary } from 'lib/opengradient/contracts/teeRegistry'; +import type { TEENodeWithStatus, TEETypeSummary } from 'lib/opengradient/teeRegistry'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { TableBody, TableCell, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table'; import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand'; diff --git a/ui/opengradient/teeRegistry/TEETypeCard.tsx b/ui/opengradient/teeRegistry/TEETypeCard.tsx index eb81ace351..177ef2d528 100644 --- a/ui/opengradient/teeRegistry/TEETypeCard.tsx +++ b/ui/opengradient/teeRegistry/TEETypeCard.tsx @@ -1,7 +1,7 @@ import { Box, Flex, Grid, Text } from '@chakra-ui/react'; import React from 'react'; -import type { TEETypeSummary } from 'lib/opengradient/contracts/teeRegistry'; +import type { TEETypeSummary } from 'lib/opengradient/teeRegistry'; import { Skeleton } from 'toolkit/chakra/skeleton'; import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand'; @@ -46,8 +46,6 @@ const TEETypeCard = ({ type, isSelected, isLoading, onClick }: Props) => { onClick(type.typeId); }, [ onClick, type.typeId ]); - const activePct = type.totalNodes > 0 ? Math.round((type.activeNodes / type.totalNodes) * 100) : 0; - return ( { - + - - - 0 ? '18px' : '0' } - bg={ colors.cyan } - borderRadius="2px" - transition="width 0.2s ease" - /> - ); }; diff --git a/ui/opengradient/teeRegistry/placeholders.ts b/ui/opengradient/teeRegistry/placeholders.ts index cdc706079b..906dec2f1f 100644 --- a/ui/opengradient/teeRegistry/placeholders.ts +++ b/ui/opengradient/teeRegistry/placeholders.ts @@ -1,15 +1,11 @@ -import type { TEERegistryStats, TEETypeSummary } from 'lib/opengradient/contracts/teeRegistry'; +import type { TEERegistryStats, TEETypeSummary } from 'lib/opengradient/teeRegistry'; export const PLACEHOLDER_TEE_REGISTRY_STATS: TEERegistryStats = { - totalTypes: 3, - totalNodes: 12, - activeNodes: 8, - enabledNodes: 10, - approvedPCRs: 5, + totalTypes: 0, + totalNodes: 0, + activeNodes: 0, + enabledNodes: 0, + approvedPCRs: 0, }; -export const PLACEHOLDER_TEE_TYPES: Array = [ - { typeId: 0, name: 'LLM Inference', totalNodes: 5, enabledNodes: 4, activeNodes: 3, approvedPCRs: 2, addedAt: BigInt(0) }, - { typeId: 1, name: 'Agent Execution', totalNodes: 4, enabledNodes: 3, activeNodes: 3, approvedPCRs: 2, addedAt: BigInt(0) }, - { typeId: 2, name: 'Model Training', totalNodes: 3, enabledNodes: 3, activeNodes: 2, approvedPCRs: 1, addedAt: BigInt(0) }, -]; +export const PLACEHOLDER_TEE_TYPES: Array = []; diff --git a/ui/pages/opengradient/TEERegistry.tsx b/ui/pages/opengradient/TEERegistry.tsx index 9da00426d3..845b35c62e 100644 --- a/ui/pages/opengradient/TEERegistry.tsx +++ b/ui/pages/opengradient/TEERegistry.tsx @@ -4,8 +4,8 @@ import React from 'react'; import { route } from 'nextjs-routes'; -import { getTEERegistryOverview, TEE_REGISTRY_QUERY_KEY, TEE_REGISTRY_ADDRESS } from 'lib/opengradient/contracts/teeRegistry'; -import type { TEENodeWithStatus } from 'lib/opengradient/contracts/teeRegistry'; +import { getTEERegistryOverview, TEE_REGISTRY_QUERY_KEY, TEE_REGISTRY_ADDRESS } from 'lib/opengradient/teeRegistry'; +import type { TEENodeWithStatus } from 'lib/opengradient/teeRegistry'; import { Checkbox } from 'toolkit/chakra/checkbox'; import { Link } from 'toolkit/chakra/link'; import { Skeleton } from 'toolkit/chakra/skeleton'; @@ -120,18 +120,8 @@ const TEERegistry = () => { return Object.values(nodesByType).flat(); }, [ query.data?.nodesByType, selectedType ]); - const hasNonDisabledNodes = React.useMemo( - () => allNodes.some((node) => node.isActive || node.enabled), - [ allNodes ], - ); - - const [ showDisabled, setShowDisabled ] = React.useState(null); - - React.useEffect(() => { - setShowDisabled(null); - }, [ selectedType ]); - - const resolvedShowDisabled = showDisabled ?? !hasNonDisabledNodes; + const [ showDisabled, setShowDisabled ] = React.useState(false); + const resolvedShowDisabled = showDisabled; const filteredNodes = React.useMemo( () => resolvedShowDisabled ? allNodes : allNodes.filter((node) => node.isActive || node.enabled), @@ -144,7 +134,7 @@ const TEERegistry = () => { const tableSubtitle = resolvedShowDisabled ? 'Showing active, enabled, and disabled registry records.' : - 'Showing active and enabled registry records.'; + 'Showing active and enabled registry records only.'; const activeVisibleCount = React.useMemo( () => filteredNodes.filter((node) => node.isActive).length, @@ -152,11 +142,8 @@ const TEERegistry = () => { ); const handleToggleShowDisabled = React.useCallback(() => { - setShowDisabled((prev) => { - const current = prev ?? !hasNonDisabledNodes; - return !current; - }); - }, [ hasNonDisabledNodes ]); + setShowDisabled((prev) => !prev); + }, []); const handleTypeClick = React.useCallback((typeId: number) => { setSelectedType((prev) => prev === typeId ? null : typeId); @@ -344,7 +331,7 @@ const TEERegistry = () => { fontSize="11px" color={ text.muted } > - { filteredNodes.length } shown / { allNodes.length } total + { filteredNodes.length.toLocaleString() } { resolvedShowDisabled ? 'registry records' : 'active or enabled' } @@ -364,7 +351,7 @@ const TEERegistry = () => { letterSpacing="0.04em" color={ text.secondary } > - Show disabled + Show disabled records @@ -386,7 +373,10 @@ const TEERegistry = () => { borderColor={ panel.border } > - No TEE nodes match the current filter. + { resolvedShowDisabled ? + 'No TEE nodes match the current filter.' : + 'No active or enabled TEE nodes are currently registered. Enable disabled records to inspect historical entries.' + } ) }