mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-09-30 20:46:32 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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"]
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)}")`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user