A React-based web application for monitoring the health and status of the XMTP network. This status page displays real-time node information from the NodeRegistry smart contract and shows live health status via gRPC-Web health checks.
- Features
- Architecture
- Project Structure
- Getting Started
- Environment Configuration
- Node Metadata JSON Schema
- Smart Contract Integration
- Health Check System
- Network Status Logic
- Development Commands
- Key Components
- Network Status Banner: Shows overall network health (Operational, Degraded, Major Outage, Outage)
- Status Summary Cards: Displays canonical/community node counts and average latency
- Real-time Health Checks: gRPC-Web calls to MetadataApi.GetVersion
- NFT Metadata Display: Fetches and displays tokenURI JSON (avatar, operator info, location)
- Collapsible Node Sections: Separate sections for canonical and community nodes
- Search Functionality: Filter nodes by ID, operator, address, or version
- Responsive Design: Works on desktop and mobile
┌─────────────────────────────────────────────────────────────────┐
│ Frontend (React) │
├─────────────────────────────────────────────────────────────────┤
│ Components │ Hooks │ State │
│ - NetworkStatusBanner │ - useNodeRegistry │ - nodeStore │
│ - StatusSummaryCards │ - useNodeStatus │ (Zustand) │
│ - NodeTable │ - useNodeMetadata │ │
│ - NodeStatusBadge │ - useAllNodeStatuses │ │
│ - NodeDetailsDialog │ - useNetworkStatus │ │
├─────────────────────────────────────────────────────────────────┤
│ External Services │
├──────────────────────┬──────────────────────┬───────────────────┤
│ NodeRegistry │ Node gRPC Endpoints │ Metadata URIs │
│ (Settlement Chain) │ (Health Checks) │ (tokenURI JSON) │
│ - getAllNodes() │ - GetVersion │ - name, image │
│ - tokenURI() │ │ - operator info │
└──────────────────────┴──────────────────────┴───────────────────┘
node-registry-portal/
├── environments/ # Environment-specific JSON configs
│ ├── testnet.json # Testnet configuration (Base Sepolia)
│ └── mainnet.json # Mainnet configuration (Base Mainnet)
├── proto/ # Protocol buffer definitions
│ └── metadata_api/
│ └── metadata_api.proto # MetadataApi service definition
├── public/
│ └── xmtp-logo.svg # XMTP logo
├── src/
│ ├── abi/
│ │ └── NodeRegistry.abi.json # Contract ABI
│ ├── components/
│ │ ├── base/ # Reusable UI primitives
│ │ │ ├── Button.tsx
│ │ │ ├── Card.tsx
│ │ │ ├── Dialog.tsx
│ │ │ └── ...
│ │ ├── typography/ # Text components
│ │ ├── ui/
│ │ │ ├── nodes/ # Node-specific components
│ │ │ │ ├── NodeTable.tsx # Table view for nodes
│ │ │ │ ├── NodeStatusBadge.tsx # Online/offline indicator
│ │ │ │ ├── NodeDetailsDialog.tsx # Full node details modal
│ │ │ │ └── NodesPageContent.tsx # Main page content
│ │ │ └── status/ # Status page components
│ │ │ ├── NetworkStatusBanner.tsx # Main status banner
│ │ │ ├── StatusSummaryCards.tsx # Summary statistics
│ │ │ └── NetworkStatusIndicator.tsx
│ │ └── Layout.tsx # App layout with header/footer
│ ├── config/
│ │ └── wagmi.ts # Wagmi configuration
│ ├── constants/
│ │ └── chains.ts # Chain configuration
│ ├── hooks/
│ │ ├── contracts/
│ │ │ └── useNodeRegistry.ts # Contract read operations
│ │ ├── nodes/
│ │ │ ├── useNodeStatus.ts # Single node health check
│ │ │ ├── useAllNodeStatuses.ts # Batch status polling
│ │ │ ├── useNodeMetadata.ts # Metadata fetching with cache
│ │ │ └── useNetworkStatus.ts # Network status calculation
│ │ └── utils/
│ │ └── useFormatters.ts # Address formatting utilities
│ ├── lib/
│ │ └── grpc/
│ │ └── nodeHealthClient.ts # gRPC-Web health check client
│ ├── pages/
│ │ └── Nodes.tsx # Main nodes page
│ ├── store/
│ │ └── nodeStore.ts # Zustand state management
│ ├── types/
│ │ └── nodes.ts # TypeScript type definitions
│ ├── utils/
│ │ ├── cn.ts # className merge utility
│ │ ├── networkStatus.ts # Status calculation utilities
│ │ └── typeGuards.ts # Type guard utilities
│ ├── App.tsx # Main app component
│ ├── main.tsx # Entry point
│ └── index.css # Global styles
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
└── tailwind.config.js
- Node.js 18+
- Yarn or npm
# Install dependencies
yarn install
# Start development server (testnet)
yarn dev
# Or start with mainnet config
yarn dev:mainnetOpen http://localhost:5173 in your browser.
The application uses JSON configuration files in /environments/ directory.
{
"settlementChain": {
"chainId": 84532,
"name": "Base Sepolia",
"publicRpcUrl": "https://base-sepolia.g.alchemy.com/v2/YOUR_KEY",
"blockExplorerUrl": "https://sepolia.basescan.org",
"nodeRegistry": "0xFac49258e9F06f321d992EFe1Ea289308002a1E4"
}
}| Field | Description |
|---|---|
settlementChain.chainId |
Chain ID (84532 for Base Sepolia, 8453 for Base) |
settlementChain.publicRpcUrl |
RPC endpoint URL |
settlementChain.nodeRegistry |
NodeRegistry contract address |
settlementChain.blockExplorerUrl |
Block explorer for transaction links |
Each node NFT can have a tokenURI that returns JSON metadata. The contract's tokenURI(nodeId) function returns a URL like https://metadata.example.com/nodeId.
Note: The node display name is always derived from the contract as "XMTP Node #<nodeId>" and should not be included in metadata.
{
"description": "An XMTP Network Node operated by Example Labs",
"image": "https://example.com/node-avatar.png",
"external_url": "https://node-operator.example.com",
"operator_name": "Example Labs",
"region": "US-East",
"social": {
"twitter": "@example",
"discord": "example-discord",
"convos": "username_or_url"
}
}| Field | Required | Description |
|---|---|---|
description |
Yes | Description of the node |
image |
No | Image URL for avatar |
external_url |
No | Link to operator's website |
operator_name |
No | Operator display name |
region |
No | Geographic region (e.g., "US-East", "EU-West") |
social.twitter |
No | Twitter handle |
social.discord |
No | Discord server/handle |
social.convos |
No | Convos username or full URL |
The status page reads from the NodeRegistry contract on the settlement chain.
// Get all registered nodes
function getAllNodes() external view returns (NodeWithId[] memory);
// Get canonical node IDs
function getCanonicalNodes() external view returns (uint32[] memory);
// Get node owner (NFT owner)
function ownerOf(uint256 tokenId) external view returns (address);
// Get metadata URI
function tokenURI(uint256 tokenId) external view returns (string memory);struct Node {
address signer; // Address derived from signing public key
bool isCanonical; // Part of canonical network
bytes signingPublicKey; // Public key for verification
string httpAddress; // HTTP endpoint (e.g., "https://node.example.com:443")
}
struct NodeWithId {
uint32 nodeId; // Unique ID (100, 200, 300, ...)
Node node;
}- Portal fetches node's
httpAddressfrom contract - Makes gRPC-Web POST request to
{httpAddress}/xmtp.xmtpv4.metadata_api.MetadataApi/GetVersion - Parses binary protobuf response for version string
- Updates status: online (success), offline (timeout/error), unknown (CORS blocked)
curl -X POST {httpAddress}/xmtp.xmtpv4.metadata_api.MetadataApi/GetVersion \
-H "Content-Type: application/grpc-web" \
-H "Accept: application/grpc-web" \
--data-binary @<(head -c 5 /dev/zero)- gRPC-Web frame: 5 bytes header (1 flag + 4 length) + protobuf message
- Protobuf: field 1 (version string) encoded as
0x0a+ length + UTF-8 bytes - Example:
0x0a 0x05 0x31 0x2e 0x31 0x2e 0x30= "1.1.0"
Browser requests may be blocked by CORS if nodes don't send Access-Control-Allow-Origin headers. In this case, status shows as "Unknown" with tooltip explaining CORS limitation.
- Initial check on page load
- Auto-refresh every 30 seconds when page is visible
- Manual refresh button available
- Polling pauses when page is hidden (visibility API)
The network status is calculated based on the health of canonical nodes only.
| Status | Condition | Color |
|---|---|---|
| Operational | ≥80% canonical nodes online | Green |
| Degraded | 50-79% canonical nodes online | Yellow |
| Major Outage | 1-49% canonical nodes online | Orange |
| Outage | 0% canonical nodes online | Red |
| Command | Description |
|---|---|
yarn dev |
Start dev server (testnet) |
yarn dev:mainnet |
Start dev server (mainnet) |
yarn build |
Production build (testnet) |
yarn build:mainnet |
Production build (mainnet) |
yarn preview |
Preview production build |
yarn lint |
Run ESLint |
yarn lint:fix |
Fix ESLint issues |
yarn type-check |
TypeScript type checking |
yarn format |
Format code with Prettier |
yarn format:check |
Check code formatting |
yarn test |
Run tests in watch mode |
yarn test:run |
Run tests once |
yarn clean |
Clean build artifacts |
Main banner at top of page showing:
- Overall network status (Operational, Degraded, etc.)
- Color-coded background based on status
- Last updated timestamp with refresh button
Three summary cards showing:
- Canonical Nodes: X/Y Online
- Community Nodes: X/Y Online
- Average Latency: Xms
Table view displaying nodes with:
- Avatar and node name
- Node ID (with canonical badge)
- Status badge (online/offline)
- Location (with map pin icon)
- HTTP address
- Owner address
Visual indicator showing:
- Green dot + "Online" for healthy nodes
- Red dot + "Offline" for unreachable nodes
- Yellow dot + "Error" for error responses
- Gray dot + "Unknown" for CORS-blocked or unchecked
Tooltip shows version, latency, and last check time.
Modal showing full node details:
- Centered node name and ID
- Avatar with canonical/status badges and location
- Description (from metadata)
- Operator name
- HTTP Address, Owner, Signer (with copy buttons)
- Version, Latency, Last Checked
- Signing Public Key
- Social links (Website, Twitter, Discord, Convos)
interface NodeData {
nodeId: number;
signer: Address;
isCanonical: boolean;
signingPublicKey: `0x${string}`;
httpAddress: string;
owner: Address;
}interface NodeMetadata {
description: string;
image?: string;
external_url?: string;
operator_name?: string;
region?: string;
social?: {
twitter?: string;
discord?: string;
convos?: string;
};
}interface NodeHealthResult {
nodeId: number;
httpAddress: string;
status: 'online' | 'offline' | 'error' | 'unknown';
version?: string;
latencyMs?: number;
lastChecked: Date;
error?: string;
}interface NetworkStatusInfo {
status: 'operational' | 'degraded' | 'major-outage' | 'outage';
canonicalOnline: number;
canonicalTotal: number;
communityOnline: number;
communityTotal: number;
averageLatencyMs: number | null;
lastChecked: Date;
}Uses Zustand with persistence:
- statusCache (memory only): Map of nodeId → health status
- metadataCache (localStorage, 24h TTL): Map of nodeId → metadata
MIT