fix: redirect /login to hanzo.id in production

Instead of showing a login page with multiple identity providers,
redirect directly to hanzo.id for a cleaner authentication flow.
This commit is contained in:
2026-02-04 23:09:42 -08:00
parent 7f1ba0733f
commit 58870c4d2a
17 changed files with 1948 additions and 147 deletions
+101 -6
View File
@@ -9,9 +9,9 @@ This codebase supports white-labeling for different brands:
- **Hanzo Web3** (default): web3.hanzo.ai - Enterprise blockchain infrastructure
- **Lux Cloud**: lux.cloud - Lux Network ecosystem infrastructure
- **Zoo Labs**: web3.zoo.ngo - Decentralized AI infrastructure
- **Bootnode**: bootnode.dev - Standalone deployment (legacy)
- **Bootnode**: bootno.de - Standalone deployment
Brand is auto-detected from domain or via `NEXT_PUBLIC_BRAND` environment variable.
Brand is auto-detected from domain or via `NEXT_PUBLIC_BRAND` environment variable (set at build time for Next.js).
## Current State Analysis
@@ -169,6 +169,96 @@ WS /v1/ws/{chain}
{"jsonrpc":"2.0","method":"eth_subscribe","params":["logs",{"address":"0x..."}],"id":3}
```
#### Native ZAP Protocol
ZAP (Zero-Copy App Proto) is a high-performance Cap'n Proto-based binary RPC protocol.
Bootnode implements **native ZAP** - no gateway, direct Cap'n Proto RPC over TCP.
**Connection**
```
AI Agent → zap://api.bootnode.dev:9999 (direct Cap'n Proto RPC)
```
**Schema Files**
- `bootnode/zap/bootnode.zap` - Clean whitespace-significant syntax (new format)
- `bootnode/zap/bootnode.capnp` - Compiled Cap'n Proto schema (for pycapnp)
**Python Client Example**
```python
from hanzo_zap import Client
async with Client.connect("zap://api.bootnode.dev:9999") as client:
# Initialize connection
server_info = await client.init({"name": "my-agent", "version": "1.0"})
# List tools
tools = await client.list_tools()
# Call a tool
result = await client.call_tool("rpc_call", {
"chain": "ethereum",
"method": "eth_blockNumber"
})
```
**REST Endpoints (Discovery)**
```
GET /v1/zap/connect # Connection info & examples
GET /v1/zap/info # Server capabilities
GET /v1/zap/tools # Available tools
GET /v1/zap/resources # Available resources
GET /v1/zap/schema # .zap schema (whitespace-significant)
GET /v1/zap/schema.capnp # Compiled .capnp schema
GET /v1/zap/health # Health check
```
**Code Generation**
```bash
# Get schema and compile
curl -H "X-API-Key: $KEY" https://api.bootnode.dev/v1/zap/schema > bootnode.zap
zapc compile bootnode.zap --out=bootnode.capnp
# Generate client code
zapc generate bootnode.capnp --lang python --out ./gen/
```
**ZAP Tools (MCP-compatible)**
| Tool | Description |
|------|-------------|
| rpc_call | Execute JSON-RPC on blockchain |
| get_token_balances | Get ERC-20 token balances for address |
| get_token_metadata | Get token metadata (name, symbol, decimals) |
| get_nfts_owned | Get NFTs owned by address |
| get_nft_metadata | Get NFT metadata and attributes |
| create_smart_wallet | Create ERC-4337 smart wallet |
| get_smart_wallet | Get smart wallet details |
| create_webhook | Create webhook for blockchain events |
| list_webhooks | List configured webhooks |
| delete_webhook | Delete webhook by ID |
| estimate_gas | Get current gas prices |
**ZAP Resources**
| URI | Description |
|-----|-------------|
| bootnode://chains | List of all supported blockchain networks |
| bootnode://usage | API usage for current billing period |
| bootnode://config | Current API configuration and limits |
**Server Implementation**
- Location: `api/bootnode/zap/server.py`
- Class: `BootnodeZapImpl(bootnode_capnp.Bootnode.Server)`
- Startup: Automatic in `main.py` lifespan handler
- Default port: 9999 (configurable via `ZAP_PORT` env var)
**Environment Variables**
```env
ZAP_ENABLED=true # Enable native ZAP server
ZAP_HOST=0.0.0.0 # Bind address
ZAP_PORT=9999 # Listen port
```
See: https://github.com/hanzo-ai/zap
#### Smart Wallets (ERC-4337)
```
POST /v1/wallets/create
@@ -314,7 +404,7 @@ All systems tested and operational:
| Wallets API | ✅ | Smart wallet creation working |
| Webhooks API | ✅ | Webhook creation working |
| Auth/Keys API | ✅ | API key management working |
| ZAP Protocol | ✅ | Integration active |
| ZAP Protocol | ✅ | Native ZAP server (zap://:9999) |
| Infra: Clusters | ✅ | K8s cluster management working |
| Infra: Volumes | ✅ | Persistent volume management working |
| Infra: Snapshots | ✅ | Snapshot creation working |
@@ -424,6 +514,7 @@ pnpm test --prefix web
- pydantic >= 2.0
- httpx
- websockets
- pycapnp >= 2.0 (native ZAP server)
### Frontend (TypeScript)
- next >= 15.0
@@ -623,11 +714,15 @@ docker compose --profile bundler up -d
```
### Key Files
- `api/bootnode/config.py` - Application settings (datastore, redis MQ)
- `api/bootnode/main.py` - FastAPI application entry
- `api/bootnode/config.py` - Application settings (datastore, redis MQ, ZAP)
- `api/bootnode/main.py` - FastAPI application entry (ZAP server startup)
- `api/bootnode/zap/bootnode.zap` - ZAP schema (whitespace-significant syntax)
- `api/bootnode/zap/bootnode.capnp` - Compiled Cap'n Proto schema
- `api/bootnode/zap/server.py` - Native ZAP server implementation
- `api/bootnode/api/zap.py` - ZAP REST endpoints (discovery)
- `api/bootnode/core/datastore/client.py` - Hanzo Datastore client
- `api/bootnode/workers/webhook.py` - Webhook delivery worker (arq/Redis-based)
- `api/pyproject.toml` - Python dependencies (arq, aiochclient)
- `api/pyproject.toml` - Python dependencies (arq, aiochclient, pycapnp)
- `infra/compose.yml` - Full Docker Compose stack (VictoriaMetrics, VMAgent)
- `infra/config/chains.yaml` - Multi-chain configuration
- `infra/monitoring/scrape.yml` - VMAgent scrape config
+5
View File
@@ -70,3 +70,8 @@ HANZO_IAM_CLIENT_SECRET=
# Hanzo HSM (Hardware Security - optional)
HANZO_HSM_ENABLED=false
# ZAP Server (native Cap'n Proto RPC)
ZAP_ENABLED=true
ZAP_HOST=0.0.0.0
ZAP_PORT=9999
+255 -101
View File
@@ -1,11 +1,20 @@
"""ZAP Protocol Integration for Bootnode Platform.
"""ZAP Protocol REST Endpoints.
ZAP (Zero-configuration Auto-configuring Protocol) integration for seamless
developer experience and automated blockchain infrastructure management.
ZAP (Zero-Copy App Proto) is a Cap'n Proto-based binary RPC protocol.
Bootnode implements native ZAP - connect directly via `zap://host:9999`.
This module provides REST endpoints for:
1. ZAP connection info and schema export
2. Health checks for ZAP discovery
3. Tool/resource manifests for documentation
"""
from typing import Any, Dict, Optional
from fastapi import APIRouter, HTTPException
from __future__ import annotations
from typing import Any
import structlog
from fastapi import APIRouter
from pydantic import BaseModel
from bootnode.api.deps import ApiKeyDep
@@ -13,111 +22,256 @@ from bootnode.config import get_settings
router = APIRouter()
settings = get_settings()
logger = structlog.get_logger()
class ZapConfigRequest(BaseModel):
"""ZAP configuration request."""
protocol_version: str = "1.0"
features: list[str] = []
auto_configure: bool = True
# =============================================================================
# Models
# =============================================================================
class ZapConfigResponse(BaseModel):
"""ZAP configuration response."""
supported: bool
version: str
endpoints: Dict[str, str]
capabilities: list[str]
auto_config: Dict[str, Any]
class ZapConnectionInfo(BaseModel):
"""ZAP connection information."""
protocol: str = "zap"
host: str
port: int
url: str
transport: str = "tcp"
@router.get("/config", response_model=ZapConfigResponse)
async def get_zap_config(api_key: ApiKeyDep) -> ZapConfigResponse:
"""Get ZAP protocol configuration for Bootnode platform."""
return ZapConfigResponse(
supported=True,
version="1.0.0",
endpoints={
"rpc": "/v1/rpc/{chain}",
"tokens": "/v1/tokens/{chain}",
"nfts": "/v1/nfts/{chain}",
"bundler": "/v1/bundler/{chain}",
"gas": "/v1/gas/{chain}",
"webhooks": "/v1/webhooks",
},
capabilities=[
"multi_chain_rpc",
"token_metadata",
"nft_metadata",
"smart_wallets",
"gas_management",
"webhooks",
"rate_limiting",
"analytics",
],
auto_config={
"chains": ["ethereum", "polygon", "arbitrum", "optimism", "base"],
"default_rate_limit": 1000,
"supported_methods": ["eth_*", "net_*", "web3_*"],
"authentication": "api_key",
"cors_enabled": True,
}
)
class ZapServerInfo(BaseModel):
"""ZAP server capabilities."""
name: str = "bootnode"
version: str = "2.0.0"
capabilities: dict[str, bool]
tools_count: int
resources_count: int
@router.post("/auto-configure")
async def zap_auto_configure(
config: ZapConfigRequest,
api_key: ApiKeyDep
) -> Dict[str, Any]:
"""Auto-configure Bootnode for ZAP protocol integration."""
# ZAP auto-configuration logic
auto_config = {
"status": "configured",
"message": "Bootnode platform auto-configured for ZAP protocol",
"config_applied": {
"protocol_version": config.protocol_version,
"features_enabled": config.features or [
"multi_chain_rpc",
"token_api",
"nft_api",
"webhooks"
],
"auto_scaling": config.auto_configure,
},
"next_steps": [
"Use the provided endpoints for blockchain operations",
"Configure webhooks for real-time notifications",
"Monitor usage via the dashboard",
"Scale your infrastructure as needed"
]
}
return auto_config
# =============================================================================
# REST Endpoints
# =============================================================================
@router.get("/status")
async def zap_status(api_key: ApiKeyDep) -> Dict[str, Any]:
"""Get ZAP protocol integration status."""
@router.get("/connect")
async def get_zap_connection(_api_key: ApiKeyDep) -> dict[str, Any]:
"""Get ZAP connection information.
Connect to bootnode's native ZAP server for high-performance RPC:
```python
from hanzo_zap import Client
async with Client.connect("zap://api.bootnode.dev:9999") as client:
tools = await client.list_tools()
result = await client.call_tool("rpc_call", {
"chain": "ethereum",
"method": "eth_blockNumber"
})
```
"""
host = settings.zap_host if settings.zap_host != "0.0.0.0" else "api.bootnode.dev"
port = settings.zap_port
return {
"protocol": "ZAP",
"version": "1.0.0",
"status": "active",
"platform": "bootnode",
"integration": "native",
"features": {
"multi_chain": True,
"auto_config": True,
"real_time": True,
"scaling": True,
"monitoring": True,
"connection": ZapConnectionInfo(
host=host,
port=port,
url=f"zap://{host}:{port}",
).model_dump(),
"status": "available" if settings.zap_enabled else "disabled",
"example": {
"python": f'client = await Client.connect("zap://{host}:{port}")',
"rust": f'let client = zap::Client::connect("zap://{host}:{port}").await?;',
"go": f'client, err := zap.Connect("zap://{host}:{port}")',
},
"endpoints_active": 12,
"chains_supported": 10,
"uptime": "99.9%"
}
}
@router.get("/info")
async def get_zap_server_info(_api_key: ApiKeyDep) -> dict[str, Any]:
"""Get ZAP server information and capabilities."""
return {
"server": ZapServerInfo(
capabilities={
"tools": True,
"resources": True,
"prompts": False,
"logging": True,
},
tools_count=11,
resources_count=3,
).model_dump(),
"protocol": {
"name": "ZAP",
"version": "1.0.0",
"transport": "Cap'n Proto RPC over TCP",
"schema": "zap.capnp",
},
}
@router.get("/tools")
async def list_zap_tools(_api_key: ApiKeyDep) -> dict[str, Any]:
"""List available ZAP tools.
Tools are the primary way to interact with bootnode via ZAP.
Each tool has a name, description, and JSON Schema for arguments.
"""
tools = [
{
"name": "rpc_call",
"description": "Execute JSON-RPC on any supported blockchain",
"inputSchema": {
"type": "object",
"properties": {
"chain": {"type": "string", "description": "Chain name (ethereum, polygon, etc.)"},
"network": {"type": "string", "default": "mainnet"},
"method": {"type": "string", "description": "RPC method"},
"params": {"type": "array", "default": []},
},
"required": ["chain", "method"],
},
},
{
"name": "get_token_balances",
"description": "Get all ERC-20 token balances for an address",
"inputSchema": {
"type": "object",
"properties": {
"address": {"type": "string"},
"chain": {"type": "string"},
"network": {"type": "string", "default": "mainnet"},
},
"required": ["address", "chain"],
},
},
{
"name": "get_nfts_owned",
"description": "Get all NFTs owned by an address",
"inputSchema": {
"type": "object",
"properties": {
"address": {"type": "string"},
"chain": {"type": "string"},
"network": {"type": "string", "default": "mainnet"},
},
"required": ["address", "chain"],
},
},
{
"name": "create_smart_wallet",
"description": "Create an ERC-4337 smart wallet",
"inputSchema": {
"type": "object",
"properties": {
"owner": {"type": "string", "description": "Owner EOA address"},
"chain": {"type": "string"},
"network": {"type": "string", "default": "mainnet"},
},
"required": ["owner", "chain"],
},
},
{
"name": "estimate_gas",
"description": "Get current gas prices for a chain",
"inputSchema": {
"type": "object",
"properties": {
"chain": {"type": "string"},
},
"required": ["chain"],
},
},
]
return {
"tools": tools,
"total": len(tools),
"note": "Connect via ZAP for full tool list with all parameters",
}
@router.get("/resources")
async def list_zap_resources(_api_key: ApiKeyDep) -> dict[str, Any]:
"""List available ZAP resources."""
resources = [
{
"uri": "bootnode://chains",
"name": "Supported Chains",
"description": "List of all supported blockchain networks",
"mimeType": "application/json",
},
{
"uri": "bootnode://usage",
"name": "Usage Statistics",
"description": "API usage for current billing period",
"mimeType": "application/json",
},
{
"uri": "bootnode://config",
"name": "Configuration",
"description": "Current API configuration and limits",
"mimeType": "application/json",
},
]
return {
"resources": resources,
"total": len(resources),
}
@router.get("/health")
async def zap_health() -> dict[str, str]:
"""Health check for ZAP server."""
return {
"status": "ok" if settings.zap_enabled else "disabled",
"service": "bootnode-zap",
"port": str(settings.zap_port),
}
@router.get("/schema")
async def get_zap_schema(_api_key: ApiKeyDep) -> dict[str, Any]:
"""Get the bootnode ZAP schema.
The schema defines the full RPC interface for bootnode.
Returns the clean whitespace-significant .zap format.
Use `zapc compile bootnode.zap` to generate .capnp for code generation.
"""
from pathlib import Path
schema_path = Path(__file__).parent.parent / "zap" / "bootnode.zap"
schema_content = ""
if schema_path.exists():
schema_content = schema_path.read_text()
return {
"schema": schema_content,
"format": "zap",
"version": "2.0.0",
"interfaces": ["Bootnode"],
"compile": "zapc compile bootnode.zap --out=bootnode.capnp",
}
@router.get("/schema.capnp")
async def get_capnp_schema(_api_key: ApiKeyDep) -> dict[str, Any]:
"""Get the compiled Cap'n Proto schema.
Pre-compiled version for direct use with pycapnp, capnp-rust, etc.
"""
from pathlib import Path
schema_path = Path(__file__).parent.parent / "zap" / "bootnode.capnp"
schema_content = ""
if schema_path.exists():
schema_content = schema_path.read_text()
return {
"schema": schema_content,
"format": "capnp",
"version": "2.0.0",
}
+5
View File
@@ -115,6 +115,11 @@ class Settings(BaseSettings):
bundler_private_key: str = ""
bundler_beneficiary: str = ""
# ZAP Server (native Cap'n Proto RPC)
zap_enabled: bool = True
zap_host: str = "0.0.0.0"
zap_port: int = 9999
# Deployment Target
deploy_target: Literal["docker", "process", "kubernetes"] = "docker"
deploy_compose_file: str = "infra/compose.yml"
+26 -1
View File
@@ -15,6 +15,7 @@ from bootnode.core.datastore import datastore_client
from bootnode.core.kms import inject_secrets
from bootnode.db.session import engine, init_db
from bootnode.ws import router as ws_router
from bootnode.zap import start_zap_server, stop_zap_server
# Load secrets from Hanzo KMS before settings
inject_secrets()
@@ -24,7 +25,7 @@ settings = get_settings()
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
"""Application lifespan handler."""
# Startup
logger.info("Starting Bootnode API", env=settings.app_env)
@@ -38,10 +39,34 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
except Exception as e:
logger.warning("DataStore not available", error=str(e))
# Start native ZAP server (Cap'n Proto RPC over TCP)
zap_server = None
if settings.zap_enabled:
try:
zap_server = await start_zap_server(
host=settings.zap_host,
port=settings.zap_port,
)
logger.info(
"ZAP server started",
url=f"zap://{settings.zap_host}:{settings.zap_port}",
)
except Exception as e:
logger.warning("ZAP server failed to start", error=str(e))
yield
# Shutdown
logger.info("Shutting down Bootnode API")
# Stop ZAP server
if zap_server:
try:
await stop_zap_server()
logger.info("ZAP server stopped")
except Exception as e:
logger.warning("ZAP server stop error", error=str(e))
await engine.dispose()
await redis_client.close()
await datastore_client.close()
+1 -1
View File
@@ -363,4 +363,4 @@ async def poll_logs(
except Exception as e:
logger.error("logs poll error", error=str(e))
await asyncio.sleep(5)
await asyncio.sleep(5)
+11
View File
@@ -0,0 +1,11 @@
"""ZAP - Native Cap'n Proto RPC Server for Bootnode.
This module implements native ZAP (Zero-Copy App Proto) support,
allowing agents to connect directly via `zap://bootnode:9999`.
No gateway required - this IS the ZAP server.
"""
from bootnode.zap.server import ZapServer, start_zap_server, stop_zap_server
__all__ = ["ZapServer", "start_zap_server", "stop_zap_server"]
+294
View File
@@ -0,0 +1,294 @@
@0x910d483065cfe07c;
# Bootnode Cloud API - ZAP Schema
# High-performance blockchain infrastructure for AI agents
# Connect via: zap://api.bootnode.dev:9999
# =============================================================================
# Core Types
# =============================================================================
struct ChainConfig @0xb511f1e1d89fa8af {
name @0 :Text;
slug @1 :Text;
chainId @2 :UInt64;
network @3 :Text;
isTestnet @4 :Bool;
}
struct Timestamp @0xfc34fff2290e18c7 {
seconds @0 :Int64;
nanos @1 :UInt32;
}
struct Metadata @0x9bebd03db05e8bce {
entries @0 :List(Entry);
struct Entry @0xe7ec9d8ce9709d49 {
key @0 :Text;
value @1 :Text;
}
}
# =============================================================================
# RPC Types
# =============================================================================
struct RPCRequest @0xff9cffed18477c91 {
chain @0 :Text;
network @1 :Text = "mainnet";
method @2 :Text;
params @3 :List(Data);
}
struct RPCResponse @0xa12731311ba9cd5a {
result @0 :Data;
error @1 :Text;
latencyMs @2 :UInt32;
}
# =============================================================================
# Token Types
# =============================================================================
struct TokenBalance @0xcd970c1c5230d11f {
contractAddress @0 :Text;
symbol @1 :Text;
name @2 :Text;
decimals @3 :UInt32;
balance @4 :Text;
balanceFormatted @5 :Float64;
priceUsd @6 :Float64;
valueUsd @7 :Float64;
}
struct TokenMetadata @0x89fb0e8fbf28db1c {
contractAddress @0 :Text;
symbol @1 :Text;
name @2 :Text;
decimals @3 :UInt32;
totalSupply @4 :Text;
logoUri @5 :Text;
}
struct TokenBalanceList @0xe6890bce0c8b7517 {
balances @0 :List(TokenBalance);
chain @1 :Text;
network @2 :Text;
}
# =============================================================================
# NFT Types
# =============================================================================
struct NFTAttribute @0x9d67f54c60b654f2 {
traitType @0 :Text;
value @1 :Text;
displayType @2 :Text;
}
struct NFTMetadata @0x9a74c009ecc4956a {
contractAddress @0 :Text;
tokenId @1 :Text;
name @2 :Text;
description @3 :Text;
imageUri @4 :Text;
animationUri @5 :Text;
externalUrl @6 :Text;
attributes @7 :List(NFTAttribute);
}
struct NFTList @0xaa3e8f9cd480bcad {
nfts @0 :List(NFTMetadata);
chain @1 :Text;
network @2 :Text;
total @3 :UInt32;
}
# =============================================================================
# Smart Wallet Types (ERC-4337)
# =============================================================================
struct SmartWallet @0xb6c7bb97622c76a1 {
address @0 :Text;
owner @1 :Text;
chain @2 :Text;
network @3 :Text;
isDeployed @4 :Bool;
nonce @5 :UInt64;
entryPoint @6 :Text;
factory @7 :Text;
}
struct UserOperation @0xd04ead91cb7c6b36 {
sender @0 :Text;
nonce @1 :UInt64;
initCode @2 :Data;
callData @3 :Data;
callGasLimit @4 :UInt64;
verificationGasLimit @5 :UInt64;
preVerificationGas @6 :UInt64;
maxFeePerGas @7 :UInt64;
maxPriorityFeePerGas @8 :UInt64;
paymasterAndData @9 :Data;
signature @10 :Data;
}
struct UserOperationReceipt @0xea8c5e896f750df1 {
userOpHash @0 :Text;
transactionHash @1 :Text;
success @2 :Bool;
reason @3 :Text;
actualGasCost @4 :UInt64;
actualGasUsed @5 :UInt64;
}
# =============================================================================
# Webhook Types
# =============================================================================
enum WebhookEventType @0xbf71b28ebf7c0502 {
addressActivity @0;
tokenTransfer @1;
nftTransfer @2;
contractEvent @3;
newBlock @4;
pendingTransaction @5;
}
struct Webhook @0xd13fcfaa0d485f98 {
id @0 :Text;
url @1 :Text;
eventType @2 :WebhookEventType;
chain @3 :Text;
network @4 :Text;
active @5 :Bool;
filters @6 :WebhookFilters;
createdAt @7 :Timestamp;
}
struct WebhookFilters @0xd5fac19b2b2a1254 {
addresses @0 :List(Text);
topics @1 :List(Text);
contractAddresses @2 :List(Text);
}
struct WebhookList @0xaf94ec9f28010a65 {
webhooks @0 :List(Webhook);
total @1 :UInt32;
}
# =============================================================================
# Gas Types
# =============================================================================
struct GasEstimate @0xcf670c46f9f17833 {
chain @0 :Text;
network @1 :Text;
gasPrice @2 :Text;
maxFee @3 :Text;
maxPriorityFee @4 :Text;
baseFee @5 :Text;
estimatedTime @6 :UInt32;
}
# =============================================================================
# Server Info
# =============================================================================
struct ServerInfo @0x8f550a5bec9ab424 {
name @0 :Text;
version @1 :Text;
capabilities @2 :Capabilities;
struct Capabilities @0xee2961376a4ac07c {
tools @0 :Bool;
resources @1 :Bool;
prompts @2 :Bool;
logging @3 :Bool;
}
}
struct ClientInfo @0xc430302c4544aa0e {
name @0 :Text;
version @1 :Text;
}
# =============================================================================
# Tool Interface
# =============================================================================
struct Tool @0x90fdd8e6f5e64b1e {
name @0 :Text;
description @1 :Text;
schema @2 :Data;
annotations @3 :Metadata;
}
struct ToolList @0xa7a259e334721959 {
tools @0 :List(Tool);
}
struct ToolCall @0x92892f3229dafcf3 {
id @0 :Text;
name @1 :Text;
args @2 :Data;
metadata @3 :Metadata;
}
struct ToolResult @0x8e4b4994ce992ac8 {
id @0 :Text;
content @1 :Data;
error @2 :Text;
metadata @3 :Metadata;
}
# =============================================================================
# Resource Interface
# =============================================================================
struct Resource @0xdfa86c7ffc9628e4 {
uri @0 :Text;
name @1 :Text;
description @2 :Text;
mimeType @3 :Text;
annotations @4 :Metadata;
}
struct ResourceList @0x8fcb7d5ee1450b49 {
resources @0 :List(Resource);
}
struct ResourceContent @0xc5f4c7b76d45b83b {
uri @0 :Text;
mimeType @1 :Text;
content :union {
text @2 :Text;
blob @3 :Data;
}
}
# =============================================================================
# Main Bootnode Interface
# =============================================================================
interface Bootnode @0xadbe8d0aa6c04798 {
init @0 (client :ClientInfo) -> (server :ServerInfo);
listTools @1 () -> (tools :ToolList);
callTool @2 (call :ToolCall) -> (result :ToolResult);
listResources @3 () -> (resources :ResourceList);
readResource @4 (uri :Text) -> (content :ResourceContent);
rpcCall @5 (request :RPCRequest) -> (response :RPCResponse);
rpcBatch @6 (requests :List(RPCRequest)) -> (responses :List(RPCResponse));
getTokenBalances @7 (address :Text, chain :Text, network :Text) -> (balances :TokenBalanceList);
getTokenMetadata @8 (contract :Text, chain :Text, network :Text) -> (metadata :TokenMetadata);
getNFTsOwned @9 (address :Text, chain :Text, network :Text) -> (nfts :NFTList);
getNFTMetadata @10 (contract :Text, tokenId :Text, chain :Text, network :Text) -> (metadata :NFTMetadata);
createWallet @11 (owner :Text, chain :Text, network :Text) -> (wallet :SmartWallet);
getWallet @12 (address :Text, chain :Text, network :Text) -> (wallet :SmartWallet);
sendUserOperation @13 (op :UserOperation, chain :Text, network :Text) -> (hash :Text);
getUserOperationReceipt @14 (hash :Text, chain :Text, network :Text) -> (receipt :UserOperationReceipt);
createWebhook @15 (url :Text, eventType :WebhookEventType, chain :Text, network :Text, filters :WebhookFilters) -> (webhook :Webhook);
listWebhooks @16 () -> (webhooks :WebhookList);
deleteWebhook @17 (id :Text) -> (success :Bool);
estimateGas @18 (chain :Text, network :Text) -> (estimate :GasEstimate);
listChains @19 () -> (chains :List(ChainConfig));
getChain @20 (slug :Text) -> (chain :ChainConfig);
log @21 (level :LogLevel, message :Text, data :Data) -> ();
enum LogLevel @0xf191386de21db6f3 {
debug @0;
info @1;
warn @2;
error @3;
}
}
+291
View File
@@ -0,0 +1,291 @@
# Bootnode Cloud API - ZAP Schema
# High-performance blockchain infrastructure for AI agents
# Connect via: zap://api.bootnode.dev:9999
# =============================================================================
# Core Types
# =============================================================================
struct ChainConfig
name Text
slug Text
chainId UInt64
network Text
isTestnet Bool
struct Timestamp
seconds Int64
nanos UInt32
struct Metadata
entries List(Entry)
struct Entry
key Text
value Text
# =============================================================================
# RPC Types
# =============================================================================
struct RPCRequest
chain Text
network Text = "mainnet"
method Text
params List(Data)
struct RPCResponse
result Data
error Text
latencyMs UInt32
# =============================================================================
# Token Types
# =============================================================================
struct TokenBalance
contractAddress Text
symbol Text
name Text
decimals UInt32
balance Text
balanceFormatted Float64
priceUsd Float64
valueUsd Float64
struct TokenMetadata
contractAddress Text
symbol Text
name Text
decimals UInt32
totalSupply Text
logoUri Text
struct TokenBalanceList
balances List(TokenBalance)
chain Text
network Text
# =============================================================================
# NFT Types
# =============================================================================
struct NFTAttribute
traitType Text
value Text
displayType Text
struct NFTMetadata
contractAddress Text
tokenId Text
name Text
description Text
imageUri Text
animationUri Text
externalUrl Text
attributes List(NFTAttribute)
struct NFTList
nfts List(NFTMetadata)
chain Text
network Text
total UInt32
# =============================================================================
# Smart Wallet Types (ERC-4337)
# =============================================================================
struct SmartWallet
address Text
owner Text
chain Text
network Text
isDeployed Bool
nonce UInt64
entryPoint Text
factory Text
struct UserOperation
sender Text
nonce UInt64
initCode Data
callData Data
callGasLimit UInt64
verificationGasLimit UInt64
preVerificationGas UInt64
maxFeePerGas UInt64
maxPriorityFeePerGas UInt64
paymasterAndData Data
signature Data
struct UserOperationReceipt
userOpHash Text
transactionHash Text
success Bool
reason Text
actualGasCost UInt64
actualGasUsed UInt64
# =============================================================================
# Webhook Types
# =============================================================================
enum WebhookEventType
addressActivity
tokenTransfer
nftTransfer
contractEvent
newBlock
pendingTransaction
struct Webhook
id Text
url Text
eventType WebhookEventType
chain Text
network Text
active Bool
filters WebhookFilters
createdAt Timestamp
struct WebhookFilters
addresses List(Text)
topics List(Text)
contractAddresses List(Text)
struct WebhookList
webhooks List(Webhook)
total UInt32
# =============================================================================
# Gas Types
# =============================================================================
struct GasEstimate
chain Text
network Text
gasPrice Text
maxFee Text
maxPriorityFee Text
baseFee Text
estimatedTime UInt32
# =============================================================================
# Server Info
# =============================================================================
struct ServerInfo
name Text
version Text
capabilities Capabilities
struct Capabilities
tools Bool
resources Bool
prompts Bool
logging Bool
struct ClientInfo
name Text
version Text
# =============================================================================
# Tool Interface
# =============================================================================
struct Tool
name Text
description Text
schema Data
annotations Metadata
struct ToolList
tools List(Tool)
struct ToolCall
id Text
name Text
args Data
metadata Metadata
struct ToolResult
id Text
content Data
error Text
metadata Metadata
# =============================================================================
# Resource Interface
# =============================================================================
struct Resource
uri Text
name Text
description Text
mimeType Text
annotations Metadata
struct ResourceList
resources List(Resource)
struct ResourceContent
uri Text
mimeType Text
union content
text Text
blob Data
# =============================================================================
# Main Bootnode Interface
# =============================================================================
interface Bootnode
# Connection
init (client ClientInfo) -> (server ServerInfo)
# Tool operations (MCP-compatible)
listTools () -> (tools ToolList)
callTool (call ToolCall) -> (result ToolResult)
# Resource operations (MCP-compatible)
listResources () -> (resources ResourceList)
readResource (uri Text) -> (content ResourceContent)
# RPC - Direct blockchain access
rpcCall (request RPCRequest) -> (response RPCResponse)
rpcBatch (requests List(RPCRequest)) -> (responses List(RPCResponse))
# Tokens - ERC-20 APIs
getTokenBalances (address Text, chain Text, network Text) -> (balances TokenBalanceList)
getTokenMetadata (contract Text, chain Text, network Text) -> (metadata TokenMetadata)
# NFTs - ERC-721/1155 APIs
getNFTsOwned (address Text, chain Text, network Text) -> (nfts NFTList)
getNFTMetadata (contract Text, tokenId Text, chain Text, network Text) -> (metadata NFTMetadata)
# Smart Wallets - ERC-4337
createWallet (owner Text, chain Text, network Text) -> (wallet SmartWallet)
getWallet (address Text, chain Text, network Text) -> (wallet SmartWallet)
sendUserOperation (op UserOperation, chain Text, network Text) -> (hash Text)
getUserOperationReceipt (hash Text, chain Text, network Text) -> (receipt UserOperationReceipt)
# Webhooks
createWebhook (url Text, eventType WebhookEventType, chain Text, network Text, filters WebhookFilters) -> (webhook Webhook)
listWebhooks () -> (webhooks WebhookList)
deleteWebhook (id Text) -> (success Bool)
# Gas
estimateGas (chain Text, network Text) -> (estimate GasEstimate)
# Chains
listChains () -> (chains List(ChainConfig))
getChain (slug Text) -> (chain ChainConfig)
# Logging
log (level LogLevel, message Text, data Data) -> ()
enum LogLevel
debug
info
warn
error
+505
View File
@@ -0,0 +1,505 @@
"""Native ZAP Server Implementation.
Implements the Zap interface from zap.capnp for native Cap'n Proto RPC.
This allows AI agents to connect directly via zap://bootnode:9999.
"""
from __future__ import annotations
import asyncio
import json
import os
from pathlib import Path
from typing import Any
import capnp
import structlog
from bootnode.config import get_settings
logger = structlog.get_logger()
settings = get_settings()
# Load the Bootnode schema (compiled from bootnode.zap)
SCHEMA_PATH = Path(__file__).parent / "bootnode.capnp"
bootnode_capnp = capnp.load(str(SCHEMA_PATH))
class BootnodeZapImpl(bootnode_capnp.Bootnode.Server):
"""Native ZAP interface implementation for Bootnode cloud APIs.
Implements the full Zap interface with tools for:
- RPC calls to any supported blockchain
- Token balance/metadata queries
- NFT ownership/metadata queries
- Smart wallet creation/management
- Webhook management
- Gas estimation
"""
def __init__(self, api_key: str | None = None) -> None:
self.api_key = api_key
self._tools = self._build_tools()
self._resources = self._build_resources()
def _build_tools(self) -> list[dict[str, Any]]:
"""Build tool definitions for cloud APIs."""
return [
{
"name": "rpc_call",
"description": "Execute JSON-RPC on any supported blockchain",
"schema": json.dumps({
"type": "object",
"properties": {
"chain": {"type": "string", "description": "Chain name (ethereum, polygon, etc.)"},
"network": {"type": "string", "default": "mainnet"},
"method": {"type": "string", "description": "RPC method"},
"params": {"type": "array", "default": []},
},
"required": ["chain", "method"],
}).encode(),
},
{
"name": "get_token_balances",
"description": "Get all ERC-20 token balances for an address",
"schema": json.dumps({
"type": "object",
"properties": {
"address": {"type": "string"},
"chain": {"type": "string"},
"network": {"type": "string", "default": "mainnet"},
},
"required": ["address", "chain"],
}).encode(),
},
{
"name": "get_token_metadata",
"description": "Get ERC-20 token metadata (name, symbol, decimals)",
"schema": json.dumps({
"type": "object",
"properties": {
"contract": {"type": "string"},
"chain": {"type": "string"},
"network": {"type": "string", "default": "mainnet"},
},
"required": ["contract", "chain"],
}).encode(),
},
{
"name": "get_nfts_owned",
"description": "Get all NFTs owned by an address",
"schema": json.dumps({
"type": "object",
"properties": {
"address": {"type": "string"},
"chain": {"type": "string"},
"network": {"type": "string", "default": "mainnet"},
},
"required": ["address", "chain"],
}).encode(),
},
{
"name": "get_nft_metadata",
"description": "Get NFT metadata (name, description, image, attributes)",
"schema": json.dumps({
"type": "object",
"properties": {
"contract": {"type": "string"},
"token_id": {"type": "string"},
"chain": {"type": "string"},
"network": {"type": "string", "default": "mainnet"},
},
"required": ["contract", "token_id", "chain"],
}).encode(),
},
{
"name": "create_smart_wallet",
"description": "Create an ERC-4337 smart wallet",
"schema": json.dumps({
"type": "object",
"properties": {
"owner": {"type": "string", "description": "Owner EOA address"},
"chain": {"type": "string"},
"network": {"type": "string", "default": "mainnet"},
},
"required": ["owner", "chain"],
}).encode(),
},
{
"name": "get_smart_wallet",
"description": "Get smart wallet details",
"schema": json.dumps({
"type": "object",
"properties": {
"address": {"type": "string"},
"chain": {"type": "string"},
"network": {"type": "string", "default": "mainnet"},
},
"required": ["address", "chain"],
}).encode(),
},
{
"name": "create_webhook",
"description": "Create a webhook for blockchain events",
"schema": json.dumps({
"type": "object",
"properties": {
"url": {"type": "string"},
"event_type": {"type": "string", "enum": [
"ADDRESS_ACTIVITY",
"TOKEN_TRANSFER",
"NFT_TRANSFER",
"CONTRACT_EVENT",
]},
"chain": {"type": "string"},
"network": {"type": "string", "default": "mainnet"},
"filters": {"type": "object"},
},
"required": ["url", "event_type", "chain"],
}).encode(),
},
{
"name": "list_webhooks",
"description": "List all configured webhooks",
"schema": json.dumps({
"type": "object",
"properties": {},
}).encode(),
},
{
"name": "delete_webhook",
"description": "Delete a webhook by ID",
"schema": json.dumps({
"type": "object",
"properties": {
"id": {"type": "string"},
},
"required": ["id"],
}).encode(),
},
{
"name": "estimate_gas",
"description": "Get current gas prices for a chain",
"schema": json.dumps({
"type": "object",
"properties": {
"chain": {"type": "string"},
},
"required": ["chain"],
}).encode(),
},
]
def _build_resources(self) -> list[dict[str, Any]]:
"""Build resource definitions."""
return [
{
"uri": "bootnode://chains",
"name": "Supported Chains",
"description": "List of all supported blockchain networks",
"mimeType": "application/json",
},
{
"uri": "bootnode://usage",
"name": "Usage Statistics",
"description": "API usage for current billing period",
"mimeType": "application/json",
},
{
"uri": "bootnode://config",
"name": "Configuration",
"description": "Current API configuration and limits",
"mimeType": "application/json",
},
]
async def init(self, client, _context, **_kwargs):
"""Initialize connection and return server info."""
logger.info("ZAP client connected", client_name=client.name, client_version=client.version)
return bootnode_capnp.ServerInfo.new_message(
name="bootnode",
version="2.0.0",
capabilities=bootnode_capnp.ServerInfo.Capabilities.new_message(
tools=True,
resources=True,
prompts=False,
logging=True,
),
)
async def listTools(self, _context, **_kwargs):
"""List available tools."""
tools_msg = bootnode_capnp.ToolList.new_message()
tools_list = tools_msg.init("tools", len(self._tools))
for i, tool in enumerate(self._tools):
tools_list[i].name = tool["name"]
tools_list[i].description = tool["description"]
tools_list[i].schema = tool["schema"]
return tools_msg
async def callTool(self, call, _context, **_kwargs):
"""Execute a tool call."""
tool_name = call.name
tool_id = call.id
args = json.loads(call.args.decode()) if call.args else {}
logger.info("ZAP tool call", tool=tool_name, id=tool_id)
try:
result = await self._execute_tool(tool_name, args)
return bootnode_capnp.ToolResult.new_message(
id=tool_id,
content=json.dumps(result).encode(),
error="",
)
except Exception as e:
logger.error("Tool call failed", tool=tool_name, error=str(e))
return bootnode_capnp.ToolResult.new_message(
id=tool_id,
content=b"",
error=str(e),
)
async def _execute_tool(self, name: str, args: dict[str, Any]) -> Any:
"""Execute tool by name with args. Routes to bootnode services."""
# Import services lazily to avoid circular imports
from bootnode.core.chains import ChainRegistry, RPCClient
if name == "rpc_call":
chain = args["chain"]
network = args.get("network", "mainnet")
method = args["method"]
params = args.get("params", [])
if not ChainRegistry.is_supported(chain, network):
raise ValueError(f"Chain not supported: {chain}/{network}")
async with RPCClient(chain, network) as rpc:
result = await rpc.call(method, params)
return {"result": result, "chain": chain, "network": network, "method": method}
elif name == "get_token_balances":
# TODO: Implement via token service
return {"balances": [], "chain": args["chain"]}
elif name == "get_token_metadata":
# TODO: Implement via token service
return {"contract": args["contract"], "chain": args["chain"]}
elif name == "get_nfts_owned":
# TODO: Implement via NFT service
return {"nfts": [], "chain": args["chain"]}
elif name == "get_nft_metadata":
# TODO: Implement via NFT service
return {"contract": args["contract"], "token_id": args["token_id"]}
elif name == "create_smart_wallet":
# TODO: Implement via wallet service
return {"address": "0x...", "owner": args["owner"], "chain": args["chain"]}
elif name == "get_smart_wallet":
# TODO: Implement via wallet service
return {"address": args["address"], "chain": args["chain"]}
elif name == "create_webhook":
# TODO: Implement via webhook service
return {"id": "wh_...", "url": args["url"], "event_type": args["event_type"]}
elif name == "list_webhooks":
# TODO: Implement via webhook service
return {"webhooks": []}
elif name == "delete_webhook":
# TODO: Implement via webhook service
return {"success": True}
elif name == "estimate_gas":
chain = args["chain"]
network = args.get("network", "mainnet")
if not ChainRegistry.is_supported(chain, network):
raise ValueError(f"Chain not supported: {chain}")
async with RPCClient(chain, network) as rpc:
gas_price = await rpc.get_gas_price()
return {"chain": chain, "gas_price": hex(gas_price), "gas_price_gwei": gas_price / 1e9}
else:
raise ValueError(f"Unknown tool: {name}")
async def listResources(self, _context, **_kwargs):
"""List available resources."""
resources_msg = bootnode_capnp.ResourceList.new_message()
resources_list = resources_msg.init("resources", len(self._resources))
for i, resource in enumerate(self._resources):
resources_list[i].uri = resource["uri"]
resources_list[i].name = resource["name"]
resources_list[i].description = resource["description"]
resources_list[i].mimeType = resource["mimeType"]
return resources_msg
async def readResource(self, uri, _context, **_kwargs):
"""Read a resource by URI."""
from bootnode.core.chains import ChainRegistry
if uri == "bootnode://chains":
all_chains = ChainRegistry.get_all_chains()
chains_data = [
{
"name": chain.name,
"slug": chain.slug,
"type": chain.chain_type.value,
"networks": list(chain.networks.keys()),
}
for chain in all_chains.values()
]
content_msg = bootnode_capnp.ResourceContent.new_message(
uri=uri,
mimeType="application/json",
)
content_msg.content.text = json.dumps(chains_data)
return content_msg
elif uri == "bootnode://usage":
# TODO: Get actual usage from billing service
content_msg = bootnode_capnp.ResourceContent.new_message(
uri=uri,
mimeType="application/json",
)
content_msg.content.text = json.dumps({
"requests": 0,
"compute_units": 0,
"period": "current",
})
return content_msg
elif uri == "bootnode://config":
content_msg = bootnode_capnp.ResourceContent.new_message(
uri=uri,
mimeType="application/json",
)
content_msg.content.text = json.dumps({
"version": "2.0.0",
"rate_limit": 1000,
})
return content_msg
else:
raise ValueError(f"Unknown resource: {uri}")
async def subscribe(self, uri, _context, **_kwargs):
"""Subscribe to resource updates."""
# TODO: Implement resource subscription
raise NotImplementedError("Resource subscription not yet implemented")
async def listPrompts(self, _context, **_kwargs):
"""List available prompts (none for bootnode)."""
return bootnode_capnp.PromptList.new_message()
async def getPrompt(self, name, args, _context, **_kwargs):
"""Get prompt messages (not implemented)."""
raise NotImplementedError("Prompts not supported")
async def log(self, level, message, _data, _context, **_kwargs):
"""Handle log messages from clients."""
level_map = {0: "debug", 1: "info", 2: "warning", 3: "error"}
log_level = level_map.get(level, "info")
getattr(logger, log_level)("ZAP client log", message=message)
class ZapServer:
"""Native ZAP TCP server for bootnode."""
def __init__(
self,
host: str = "0.0.0.0",
port: int = 9999,
) -> None:
self.host = host
self.port = port
self._server: asyncio.Server | None = None
self._running = False
async def _handle_connection(self, stream: capnp.AsyncIoStream) -> None:
"""Handle a new ZAP client connection."""
logger.info("ZAP client connected", host=self.host, port=self.port)
try:
await capnp.TwoPartyServer(stream, bootstrap=BootnodeZapImpl()).on_disconnect()
except Exception as e:
logger.error("ZAP connection error", error=str(e))
finally:
logger.info("ZAP client disconnected")
async def start(self) -> None:
"""Start the ZAP server."""
if self._running:
return
self._server = await capnp.AsyncIoStream.create_server(
self._handle_connection,
self.host,
self.port,
)
self._running = True
logger.info("ZAP server started", host=self.host, port=self.port, url=f"zap://{self.host}:{self.port}")
async def serve_forever(self) -> None:
"""Run the server until cancelled."""
if not self._server:
await self.start()
async with self._server:
await self._server.serve_forever()
async def stop(self) -> None:
"""Stop the ZAP server."""
if self._server and self._running:
self._server.close()
await self._server.wait_closed()
self._running = False
logger.info("ZAP server stopped")
# Global server instance
_zap_server: ZapServer | None = None
async def start_zap_server(
host: str | None = None,
port: int | None = None,
) -> ZapServer:
"""Start the global ZAP server.
Args:
host: Bind address (default: 0.0.0.0)
port: Port number (default: 9999 or ZAP_PORT env var)
Returns:
Running ZapServer instance
"""
global _zap_server
if _zap_server is not None:
return _zap_server
_host = host or os.environ.get("ZAP_HOST", "0.0.0.0")
_port = port or int(os.environ.get("ZAP_PORT", "9999"))
_zap_server = ZapServer(host=_host, port=_port)
await _zap_server.start()
return _zap_server
async def stop_zap_server() -> None:
"""Stop the global ZAP server."""
global _zap_server
if _zap_server:
await _zap_server.stop()
_zap_server = None
+2
View File
@@ -47,6 +47,8 @@ dependencies = [
"kubernetes>=31.0.0",
"pydo>=0.8.0",
"docker>=7.0.0",
# ZAP Protocol (native Cap'n Proto RPC)
"pycapnp>=2.0.0",
]
Generated
+38
View File
@@ -398,6 +398,7 @@ dependencies = [
{ name = "httpx" },
{ name = "kubernetes" },
{ name = "orjson" },
{ name = "pycapnp" },
{ name = "pydantic", extra = ["email"] },
{ name = "pydantic-settings" },
{ name = "pydo" },
@@ -442,6 +443,7 @@ requires-dist = [
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.13.0" },
{ name = "orjson", specifier = ">=3.10.0" },
{ name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.0.0" },
{ name = "pycapnp", specifier = ">=2.0.0" },
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.0" },
{ name = "pydantic-settings", specifier = ">=2.6.0" },
{ name = "pydo", specifier = ">=0.8.0" },
@@ -1987,6 +1989,42 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" },
]
[[package]]
name = "pycapnp"
version = "2.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/7b/b2f356bc24220068beffc03e94062e8059a1383addb837303794398aec36/pycapnp-2.2.2.tar.gz", hash = "sha256:7f6c23c2283173a3cb6f1a5086dd0114779d508a7cd1b138d25a6357857d02b6", size = 730142, upload-time = "2026-01-21T01:22:13.73Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/76/f8f81d32ddf950e934ec144facbc112e5acbef31a63ba5be0c5f34a00fd5/pycapnp-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b86cb8ea5b8011b562c4e022325a826a62f91196ceb5aa33a766c0bea0b8fd3", size = 1605194, upload-time = "2026-01-21T01:20:29.604Z" },
{ url = "https://files.pythonhosted.org/packages/50/dd/a31be782d56a8648fef899f39aeeab867cf544a6b170871e3f4cbfc58af6/pycapnp-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2353531cfa669e3eeb99be9f993573341650276abec46676d687cc12b3e6b6d9", size = 1486613, upload-time = "2026-01-21T01:20:31.415Z" },
{ url = "https://files.pythonhosted.org/packages/aa/bf/8da830dda94eb7327c6508d6c26fbd964897d742f8c1c0ec48623f0c515b/pycapnp-2.2.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ee27bdc78c7ccd8eaa0fe31e09f0ec4ef31deda3f475fc9373bb4b0de8083053", size = 5186701, upload-time = "2026-01-21T01:20:32.836Z" },
{ url = "https://files.pythonhosted.org/packages/8d/a1/13d0baa2f337f4f6fe8c2142646ba437a26b9c433f5d7ce016a912bad052/pycapnp-2.2.2-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:a8ded808911d1d7a9a2197626c09eea6e269e74dc1276760789538b1efcf6cd5", size = 5239464, upload-time = "2026-01-21T01:20:34.793Z" },
{ url = "https://files.pythonhosted.org/packages/82/76/0451c64b5f0132e4b75a0afe8cec957c8bf8fa981264a7c0b264cb94663e/pycapnp-2.2.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:59e92e1db40041d82a95eab0bd8de2676ce50c6b97c1457e2edde4d134b6d046", size = 5542887, upload-time = "2026-01-21T01:20:36.463Z" },
{ url = "https://files.pythonhosted.org/packages/04/00/d025d68d9a5330d55cbe2d018091cacfef0835c3ad422eb6778c4525041f/pycapnp-2.2.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:ee1e9ac2f0b80fa892b922b60e36efc925d072ecf1204ba3e59d8d9ac7c3dc83", size = 5659696, upload-time = "2026-01-21T01:20:38.069Z" },
{ url = "https://files.pythonhosted.org/packages/58/b7/28f7c539a5f4cbaa12e55ec27d081d11473464230f2e801e4714606d3453/pycapnp-2.2.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:53273b385be78ed8ac997ff8697f2a4c760e93c190b509822a937de5531f4861", size = 5413827, upload-time = "2026-01-21T01:20:39.781Z" },
{ url = "https://files.pythonhosted.org/packages/3c/a7/83bc13d90675f0cee8a38d4ad8401bb2f8662c543b3a6622aeffb7b56b1e/pycapnp-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:812cbdd002bc542b63f969b85c6b9041dfdaf4185613635a6d4feea84c9092fa", size = 6046815, upload-time = "2026-01-21T01:20:42.172Z" },
{ url = "https://files.pythonhosted.org/packages/0d/8a/80f46baa1684bbcc4754ce22c5a44693a1209a64de6df2b256b85b8b8a97/pycapnp-2.2.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9c330218a44bd649b96f565dbf5326d183fdd20f9887bdedfeabd73f0366c2e1", size = 6367625, upload-time = "2026-01-21T01:20:44.004Z" },
{ url = "https://files.pythonhosted.org/packages/02/00/60e82eaf6b4e78d887157bf9f18234c852771cc575355e63d1114c4a5d79/pycapnp-2.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:796aa0ba18bcd4e6b2815471bbed059ad7ee8a815a30e81ac8a9aa030ec7818d", size = 6487265, upload-time = "2026-01-21T01:20:46.137Z" },
{ url = "https://files.pythonhosted.org/packages/57/6e/2dedd8f95dc22357c50a775ee2b8711b3d711f30344d244141e0e1962c3e/pycapnp-2.2.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:251a6abdd64b9b11d2a8e16fc365b922ef6ba6c968959b72a3a3d9d8ec8cc8d7", size = 6576699, upload-time = "2026-01-21T01:20:47.987Z" },
{ url = "https://files.pythonhosted.org/packages/2f/53/f7f69ed1d11ea30ea4f0f6d8319fbc18bc8781c480c118005e0a394492a7/pycapnp-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6aab811e0fcc27ae8bf5f04dedaa7e0af47e0d4db51d9c85ab0d2dad26a46bd7", size = 6344114, upload-time = "2026-01-21T01:20:50.367Z" },
{ url = "https://files.pythonhosted.org/packages/ab/78/ab78ee42797ff44c7e1fc0d1aa9396c6742cb05ff01a7cdf9c8f19e0defe/pycapnp-2.2.2-cp312-cp312-win32.whl", hash = "sha256:5061c85dd8f843b2656720ca6976d2a9b418845580c6f6d9602f7119fc2208d5", size = 1047207, upload-time = "2026-01-21T01:20:51.842Z" },
{ url = "https://files.pythonhosted.org/packages/4c/fb/6edf56d5144c476270fa8b2e6a660ef5a188fb0097193e342618fbcb0210/pycapnp-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:700eb8c77405222903af3fb5a371c0d766f86139c3d51f4bff41ccd6403b51f9", size = 1185178, upload-time = "2026-01-21T01:20:53.429Z" },
{ url = "https://files.pythonhosted.org/packages/ba/70/376c3f1be4ba453584bc96a9e6a7372486ce920cb9c0869c06066e77d626/pycapnp-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:90138fceca1e85ea3eaa0de6656e33c4bef1c8da3c191db0a5b5bacc969f7889", size = 1603886, upload-time = "2026-01-21T01:20:55.383Z" },
{ url = "https://files.pythonhosted.org/packages/b6/11/2728b563f3f25d826024136cd3aab39f5d1727195de5c90a2ba3d232e897/pycapnp-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd0549036bfddb003f8e371c3c4ed3f56ac847953eb57cdd371100bb52afa64e", size = 1485731, upload-time = "2026-01-21T01:20:57.587Z" },
{ url = "https://files.pythonhosted.org/packages/77/1b/ab9bb376e7314b92dde24843fcf3d6459f10e48d95eb54d25912f973f5d7/pycapnp-2.2.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7c5a8f6d96017a7bb7e202fa8920fcdad119deab0d761f9aca1e6a4755376cef", size = 5209046, upload-time = "2026-01-21T01:20:59.049Z" },
{ url = "https://files.pythonhosted.org/packages/5f/62/303548df0316740caad513e4b81b18b2db1990785f3f01c36fd19889e7d4/pycapnp-2.2.2-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:bba31a8ba8ec32a04c5a14a5e469df7e1f1b85e169f49f7c2edfdfb78ec5075f", size = 5235782, upload-time = "2026-01-21T01:21:00.676Z" },
{ url = "https://files.pythonhosted.org/packages/5d/ec/df320105d17e118207f01208af1e505d48981856c07fe10b04a8feab39d9/pycapnp-2.2.2-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2decdaeb517120e152f7d9ddede393a830087ac3f1024c5224f2ec58e1735abd", size = 5544647, upload-time = "2026-01-21T01:21:02.812Z" },
{ url = "https://files.pythonhosted.org/packages/03/47/bc3ea9b0d71cc3e0694993a5907ac56aab2c1ad803697be068fff55a0e1b/pycapnp-2.2.2-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:e879127ac9580005efcd51ec9ae903f1dd98954fb4ea88db9cf534e9a4afb379", size = 5596651, upload-time = "2026-01-21T01:21:04.504Z" },
{ url = "https://files.pythonhosted.org/packages/84/82/c500220d4eccca845a06f2f9d0747ad8043f8b893ef5278e2019cbc906cc/pycapnp-2.2.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7092c2393191221b8ce1c03ddc1343d1ff26d36129a831017d2371867e9c09bb", size = 5393131, upload-time = "2026-01-21T01:21:06.459Z" },
{ url = "https://files.pythonhosted.org/packages/ed/61/be35f0b8d81cf107a9d0329d180f1fd8f5d6d75e117fdf7fcad779443f17/pycapnp-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:682812bf9ba4a60309b7150763cd5abed9341a31375398f3d2f60fee26143e13", size = 6071761, upload-time = "2026-01-21T01:21:08.238Z" },
{ url = "https://files.pythonhosted.org/packages/54/c0/d09da26ebb1bf0a130650a940b8e32f41219241c8d5f9c0891f15343dd9c/pycapnp-2.2.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6f321dbf33767bc2fd77dd66265177284697089f9e45b6dee7fd462b2b5cf918", size = 6369216, upload-time = "2026-01-21T01:21:10.338Z" },
{ url = "https://files.pythonhosted.org/packages/3d/58/a651de950437d8ef9fa91c3cce84ecd068e05011d421a6cf288f930f331c/pycapnp-2.2.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1d443d38f1cbeaec5b20b420117885fd5de812c5687e3bc8456d2e2c5cba371e", size = 6488273, upload-time = "2026-01-21T01:21:12.127Z" },
{ url = "https://files.pythonhosted.org/packages/4b/28/254f511272fde9fea1220807d759bcbdb47ecdff64ac860499be9aeddeaa/pycapnp-2.2.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:20a31fedb7ef30ec53d5d2d951a7cec8f639c954557093a3ba3d11aba5d174b4", size = 6542078, upload-time = "2026-01-21T01:21:14.581Z" },
{ url = "https://files.pythonhosted.org/packages/f9/b3/eea39216b7b59cefd21e6783e38a4d78db7e58c4f8f1f45e8351057432b7/pycapnp-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fbefb388464501899233d0b7d27256edd22c3cebb5a312da5d9f246671e9455e", size = 6333374, upload-time = "2026-01-21T01:21:16.522Z" },
{ url = "https://files.pythonhosted.org/packages/be/08/42ab52a3e7e5381d8fd7ae227ae75f54454f9e339d214ebe92763bb78115/pycapnp-2.2.2-cp313-cp313-win32.whl", hash = "sha256:a25e3b3ef40d430309acc7c024aa62b1ee6b2e7a85b341a8b7a8a6f8e29d4133", size = 1046173, upload-time = "2026-01-21T01:21:18.074Z" },
{ url = "https://files.pythonhosted.org/packages/34/d4/fec2023c4709d3631fe24e09dae2badc23d613f1288b4a6b398302945984/pycapnp-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:db65c9ec9d69bac6d19092db43b5e1f3cb2c680810418fd1e02316f7ef157fc1", size = 1184414, upload-time = "2026-01-21T01:21:19.334Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
+12 -1
View File
@@ -26,7 +26,7 @@ const sections = [
{ name: "Introduction", href: "/docs/intro" },
{ name: "Authentication", href: "/docs/auth" },
{ name: "Making Requests", href: "/docs/requests" },
{ name: "Error Handling", href: "/docs/errors" },
{ name: "ZAP Protocol", href: "/docs/zap/quickstart" },
],
},
{
@@ -84,6 +84,17 @@ const sections = [
{ name: "Limits", href: "/docs/gas/limits" },
],
},
{
title: "ZAP Protocol",
description: "Native binary RPC for AI agents",
icon: Zap,
items: [
{ name: "Quickstart", href: "/docs/zap/quickstart" },
{ name: "Schema", href: "/docs/zap/quickstart#schema" },
{ name: "Tools", href: "/docs/zap/quickstart#available-tools" },
{ name: "Resources", href: "/docs/zap/quickstart#available-resources" },
],
},
]
const sdks = [
+1 -1
View File
@@ -236,10 +236,10 @@ console.log("Tokens:", tokens);`}</code>
<div className="grid gap-3 sm:grid-cols-2">
{[
{ title: "API Reference", href: "/docs/api", desc: "Full endpoint documentation" },
{ title: "ZAP Protocol", href: "/docs/zap/quickstart", desc: "Native binary RPC for AI agents" },
{ title: "Smart Wallets", href: "/docs/wallets/quickstart", desc: "Create ERC-4337 smart wallets" },
{ title: "Webhooks", href: "/docs/webhooks/quickstart", desc: "Real-time event notifications" },
{ title: "WebSockets", href: "/docs/websockets/quickstart", desc: "Subscribe to live blockchain data" },
{ title: "Account Abstraction", href: "/docs/aa/quickstart", desc: "ERC-4337 bundler and paymasters" },
{ title: "SDKs", href: "/docs/sdks", desc: "Client libraries for every language" },
].map((item) => (
<Link
+372
View File
@@ -0,0 +1,372 @@
import Link from "next/link"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { DocsLayout } from "@/components/docs-layout"
import { ArrowRight } from "lucide-react"
import { docsConfig } from "@/lib/docs-config"
export const metadata = {
title: "ZAP Protocol",
description: `Native ZAP (Zero-Copy App Proto) support for high-performance AI agent access to ${docsConfig.brandName}.`,
}
export default function ZapQuickstartPage() {
return (
<DocsLayout>
<div className="space-y-10">
{/* Header */}
<div>
<div className="flex items-center gap-3 mb-4">
<h1 className="text-4xl font-bold">ZAP Protocol</h1>
<Badge variant="secondary">Native</Badge>
</div>
<p className="text-lg text-muted-foreground">
ZAP (Zero-Copy App Proto) is a high-performance binary RPC protocol based on Cap'n Proto.
{docsConfig.brandName} implements native ZAP - direct Cap'n Proto RPC over TCP for AI agents.
</p>
</div>
{/* Connection */}
<section className="space-y-4">
<h2 className="text-2xl font-semibold">Connect</h2>
<p className="text-muted-foreground">
Connect directly to the native ZAP server:
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{docsConfig.zapUrl}</code>
</pre>
</section>
{/* Schema */}
<section className="space-y-4">
<h2 className="text-2xl font-semibold">Schema</h2>
<p className="text-muted-foreground">
ZAP uses a clean, whitespace-significant syntax. No ordinals, no braces, no semicolons.
Field order determines wire encoding.
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`# Structs - field order determines ordinals
struct User
name Text
email Text
age UInt32
# Adding fields is always backwards-compatible
struct User
name Text
email Text
age UInt32
phone Text # New field - safe to add
verified Bool # New field - safe to add
# Nested structs
struct Metadata
entries List(Entry)
struct Entry
key Text
value Text
# Enums - value order determines encoding
enum Status
pending
active
completed
# Unions
struct Content
union data
text Text
blob Data
json Text
# Interfaces with methods
interface Bootnode
init (client ClientInfo) -> (server ServerInfo)
listTools () -> (tools ToolList)
callTool (call ToolCall) -> (result ToolResult)
rpcCall (request RPCRequest) -> (response RPCResponse)`}</code>
</pre>
</section>
{/* Python Client */}
<section className="space-y-4">
<h2 className="text-2xl font-semibold">Python Client</h2>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`from hanzo_zap import Client
async with Client.connect("${docsConfig.zapUrl}") as client:
# Initialize connection
server_info = await client.init({
"name": "my-agent",
"version": "1.0.0"
})
print(f"Connected to {server_info.name} v{server_info.version}")
# List available tools
tools = await client.list_tools()
for tool in tools:
print(f" - {tool.name}: {tool.description}")
# Call a tool
result = await client.call_tool("rpc_call", {
"chain": "ethereum",
"method": "eth_blockNumber"
})
print(f"Block: {int(result['result'], 16)}")`}</code>
</pre>
</section>
{/* Rust Client */}
<section className="space-y-4">
<h2 className="text-2xl font-semibold">Rust Client</h2>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`use hanzo_zap::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::connect("${docsConfig.zapUrl}").await?;
// Initialize
let server = client.init("my-agent", "1.0.0").await?;
println!("Connected to {} v{}", server.name, server.version);
// List tools
let tools = client.list_tools().await?;
for tool in tools {
println!(" - {}: {}", tool.name, tool.description);
}
// Call tool
let result = client.call_tool("rpc_call", serde_json::json!({
"chain": "ethereum",
"method": "eth_blockNumber"
})).await?;
println!("Result: {:?}", result);
Ok(())
}`}</code>
</pre>
</section>
{/* Go Client */}
<section className="space-y-4">
<h2 className="text-2xl font-semibold">Go Client</h2>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`package main
import (
"context"
"fmt"
"log"
"github.com/hanzo-ai/zap-go"
)
func main() {
ctx := context.Background()
client, err := zap.Connect(ctx, "${docsConfig.zapUrl}")
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Initialize
server, err := client.Init(ctx, "my-agent", "1.0.0")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Connected to %s v%s\\n", server.Name, server.Version)
// List tools
tools, err := client.ListTools(ctx)
if err != nil {
log.Fatal(err)
}
for _, tool := range tools {
fmt.Printf(" - %s: %s\\n", tool.Name, tool.Description)
}
// Call tool
result, err := client.CallTool(ctx, "rpc_call", map[string]any{
"chain": "ethereum",
"method": "eth_blockNumber",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Result: %v\\n", result)
}`}</code>
</pre>
</section>
{/* Available Tools */}
<section className="space-y-4">
<h2 className="text-2xl font-semibold">Available Tools</h2>
<div className="overflow-x-auto border rounded-lg">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50">
<th className="text-left p-3 font-medium">Tool</th>
<th className="text-left p-3 font-medium">Description</th>
</tr>
</thead>
<tbody>
{[
{ name: "rpc_call", desc: "Execute JSON-RPC on any supported blockchain" },
{ name: "get_token_balances", desc: "Get ERC-20 token balances for an address" },
{ name: "get_token_metadata", desc: "Get token metadata (name, symbol, decimals)" },
{ name: "get_nfts_owned", desc: "Get NFTs owned by an address" },
{ name: "get_nft_metadata", desc: "Get NFT metadata and attributes" },
{ name: "create_smart_wallet", desc: "Create ERC-4337 smart wallet" },
{ name: "get_smart_wallet", desc: "Get smart wallet details" },
{ name: "create_webhook", desc: "Create webhook for blockchain events" },
{ name: "list_webhooks", desc: "List configured webhooks" },
{ name: "delete_webhook", desc: "Delete webhook by ID" },
{ name: "estimate_gas", desc: "Get current gas prices" },
].map((tool) => (
<tr key={tool.name} className="border-b last:border-0">
<td className="p-3 font-mono text-xs">{tool.name}</td>
<td className="p-3 text-muted-foreground">{tool.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
{/* Available Resources */}
<section className="space-y-4">
<h2 className="text-2xl font-semibold">Available Resources</h2>
<div className="overflow-x-auto border rounded-lg">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50">
<th className="text-left p-3 font-medium">URI</th>
<th className="text-left p-3 font-medium">Description</th>
</tr>
</thead>
<tbody>
{[
{ uri: "bootnode://chains", desc: "List of all supported blockchain networks" },
{ uri: "bootnode://usage", desc: "API usage for current billing period" },
{ uri: "bootnode://config", desc: "Current API configuration and limits" },
].map((resource) => (
<tr key={resource.uri} className="border-b last:border-0">
<td className="p-3 font-mono text-xs">{resource.uri}</td>
<td className="p-3 text-muted-foreground">{resource.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
{/* Code Generation */}
<section className="space-y-4">
<h2 className="text-2xl font-semibold">Code Generation</h2>
<p className="text-muted-foreground">
Download the schema and generate client code for your language:
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`# Download the .zap schema (new whitespace-significant format)
curl -H "X-API-Key: YOUR_API_KEY" \\
${docsConfig.apiUrl}/v1/zap/schema > bootnode.zap
# Compile to Cap'n Proto
zapc compile bootnode.zap --out=bootnode.capnp
# Generate Python client
zapc generate bootnode.capnp --lang python --out ./gen/
# Generate Rust client
zapc generate bootnode.capnp --lang rust --out ./gen/
# Generate Go client
zapc generate bootnode.capnp --lang go --out ./gen/
# Generate TypeScript client
zapc generate bootnode.capnp --lang typescript --out ./gen/`}</code>
</pre>
</section>
{/* REST Discovery */}
<section className="space-y-4">
<h2 className="text-2xl font-semibold">REST Discovery Endpoints</h2>
<p className="text-muted-foreground">
Use these REST endpoints for discovery and debugging:
</p>
<div className="grid gap-3">
{[
{ method: "GET", path: "/v1/zap/connect", desc: "Connection info & client examples" },
{ method: "GET", path: "/v1/zap/info", desc: "Server capabilities" },
{ method: "GET", path: "/v1/zap/tools", desc: "Available tools" },
{ method: "GET", path: "/v1/zap/resources", desc: "Available resources" },
{ method: "GET", path: "/v1/zap/schema", desc: ".zap schema (whitespace-significant)" },
{ method: "GET", path: "/v1/zap/schema.capnp", desc: "Compiled .capnp schema" },
{ method: "GET", path: "/v1/zap/health", desc: "Health check" },
].map((endpoint) => (
<div
key={endpoint.path}
className="flex items-center gap-3 p-3 border rounded-lg"
>
<Badge
variant="secondary"
className="font-mono"
>
{endpoint.method}
</Badge>
<div>
<code className="text-sm">{endpoint.path}</code>
<p className="text-xs text-muted-foreground">{endpoint.desc}</p>
</div>
</div>
))}
</div>
</section>
{/* MCP Compatibility */}
<section className="space-y-4">
<h2 className="text-2xl font-semibold">MCP Compatibility</h2>
<p className="text-muted-foreground">
ZAP implements a superset of the Model Context Protocol (MCP). All MCP clients can connect
to ZAP servers, and ZAP extends MCP with:
</p>
<ul className="list-disc list-inside text-muted-foreground space-y-1">
<li>Binary wire format (Cap'n Proto) for 10-100x faster serialization</li>
<li>Zero-copy reads for large payloads</li>
<li>Streaming responses for real-time data</li>
<li>Native TCP transport (no HTTP overhead)</li>
<li>Bidirectional communication</li>
</ul>
</section>
{/* Next Steps */}
<section className="space-y-4">
<h2 className="text-2xl font-semibold">Next Steps</h2>
<div className="grid gap-3 sm:grid-cols-2">
{[
{ title: "API Reference", href: "/docs/api", desc: "Full REST API documentation" },
{ title: "SDKs", href: "/docs/sdks", desc: "Client libraries for every language" },
{ title: "Smart Wallets", href: "/docs/wallets/quickstart", desc: "ERC-4337 account abstraction" },
{ title: "Webhooks", href: "/docs/webhooks/quickstart", desc: "Real-time event notifications" },
].map((item) => (
<Link
key={item.href}
href={item.href}
className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted transition-colors group"
>
<div>
<p className="font-medium">{item.title}</p>
<p className="text-sm text-muted-foreground">{item.desc}</p>
</div>
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
</Link>
))}
</div>
</section>
</div>
</DocsLayout>
)
}
+15 -36
View File
@@ -62,48 +62,27 @@ function LoginForm() {
const brand = useBrand()
// Production mode: Show IAM login options
// Production mode: Redirect directly to Hanzo ID
React.useEffect(() => {
if (isProduction && !authLoading && !user) {
const returnUrl = searchParams.get("returnUrl") || "/dashboard"
const callbackUrl = `${window.location.origin}/auth/callback`
const hanzoIdUrl = `https://hanzo.id/login?redirect_uri=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(returnUrl)}`
window.location.href = hanzoIdUrl
}
}, [isProduction, authLoading, user, searchParams])
// Show loading while redirecting in production
if (isProduction) {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-background to-muted/20 px-4">
<Link href="/" className="mb-8">
<BrandLogo size="large" />
</Link>
<Card className="w-full max-w-sm">
<CardHeader className="text-center">
<CardTitle className="text-xl">Sign in to {brand.name}</CardTitle>
<CardDescription>
Choose your identity provider to continue
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<HanzoLoginButton org="hanzo" />
<Button variant="outline" className="w-full" onClick={() => window.location.href = `${process.env.NEXT_PUBLIC_IAM_URL || "https://iam.hanzo.ai"}/oauth2/authorize?response_type=code&client_id=${process.env.NEXT_PUBLIC_IAM_CLIENT_ID || "bootnode-platform"}&redirect_uri=${encodeURIComponent(window.location.origin + "/auth/callback")}&scope=openid+profile+email&state=zoo`}>
Continue with Zoo ID
</Button>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-card px-2 text-muted-foreground">or</span>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<Button variant="outline" size="sm" onClick={() => window.location.href = `${process.env.NEXT_PUBLIC_IAM_URL || "https://iam.hanzo.ai"}/oauth2/authorize?response_type=code&client_id=${process.env.NEXT_PUBLIC_IAM_CLIENT_ID || "bootnode-platform"}&redirect_uri=${encodeURIComponent(window.location.origin + "/auth/callback")}&scope=openid+profile+email&state=lux`}>
Lux ID
</Button>
<Button variant="outline" size="sm" onClick={() => window.location.href = `${process.env.NEXT_PUBLIC_IAM_URL || "https://iam.hanzo.ai"}/oauth2/authorize?response_type=code&client_id=${process.env.NEXT_PUBLIC_IAM_CLIENT_ID || "bootnode-platform"}&redirect_uri=${encodeURIComponent(window.location.origin + "/auth/callback")}&scope=openid+profile+email&state=pars`}>
Pars ID
</Button>
</div>
</CardContent>
</Card>
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">Redirecting to Hanzo ID...</p>
</div>
</div>
)
}
+14
View File
@@ -16,6 +16,7 @@ export const docsConfig = {
apiUrl: process.env.NEXT_PUBLIC_API_URL || `https://api.${brand.domain}`,
dashboardUrl: process.env.NEXT_PUBLIC_DASHBOARD_URL || `https://${brand.domain}`,
wsUrl: process.env.NEXT_PUBLIC_WS_URL || `wss://ws.${brand.domain}`,
zapUrl: process.env.NEXT_PUBLIC_ZAP_URL || `zap://api.${brand.domain}:9999`,
docsUrl: process.env.NEXT_PUBLIC_DOCS_URL || `https://docs.${brand.domain}`,
statusUrl: brand.statusUrl,
@@ -76,3 +77,16 @@ client = Client(api_key="${docsConfig.apiKeyPrefix}YOUR_API_KEY")
result = client.rpc.${method}()
print(result)`
}
// Example ZAP client snippet
export function exampleZap(): string {
return `from hanzo_zap import Client
async with Client.connect("${docsConfig.zapUrl}") as client:
server = await client.init({"name": "my-agent", "version": "1.0"})
result = await client.call_tool("rpc_call", {
"chain": "ethereum",
"method": "eth_blockNumber"
})
print(f"Block: {int(result['result'], 16)}")`
}