feat: comprehensive bootnode platform updates

- Add brand assets and logos
- Add KMS integration for secure key management
- Add infrastructure configs (cloud providers, monitoring)
- Add indexer service with documentation
- Update K8s deployments and ingress
- Enhance dashboard pages (analytics, assets, infrastructure)
- Update auth and API components
- Add .gitignore for embedded repos
This commit is contained in:
2026-02-03 12:48:01 -08:00
parent 38f6b31d44
commit e6ffd5bc87
102 changed files with 9788 additions and 474 deletions
+58
View File
@@ -0,0 +1,58 @@
# Dependencies
node_modules/
__pycache__/
*.pyc
.pnpm-store/
.uv/
# Environment files (NEVER commit real secrets)
.env
.env.local
.env.development
.env.production
.env*.local
*.env
!.env.example
!api/.env.example
!infra/.env.example
!bootnode/.env.example
# Build outputs
.next/
out/
dist/
build/
*.egg-info/
# IDE
.idea/
.vscode/
*.swp
*.swo
.DS_Store
# Logs
*.log
logs/
# Test coverage
coverage/
.coverage
htmlcov/
.pytest_cache/
# Kubernetes secrets (use kubectl create secret)
**/secrets.local.yaml
**/secrets.prod.yaml
# Temporary files
tmp/
temp/
*.tmp
# Embedded git repositories (separate projects)
bootnode/
bootnode-admin/
# Playwright MCP screenshots
.playwright-mcp/
Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

+21
View File
@@ -49,3 +49,24 @@ IPFS_GATEWAY=https://ipfs.io/ipfs/
# Webhooks
WEBHOOK_TIMEOUT=30
WEBHOOK_MAX_RETRIES=5
# Pricing/Market Data
MARKETS_API_URL=https://markets.lux.network
COINGECKO_API_KEY=your-coingecko-api-key
# Hanzo KMS (Secret Management)
HANZO_KMS_URL=https://kms.hanzo.ai
HANZO_KMS_ORG=hanzo
HANZO_KMS_PROJECT=bootnode
HANZO_KMS_ENV=development
HANZO_KMS_CLIENT_ID=
HANZO_KMS_CLIENT_SECRET=
# Hanzo IAM (Authentication)
HANZO_IAM_URL=https://hanzo.id
HANZO_IAM_ORG=HANZO
HANZO_IAM_CLIENT_ID=
HANZO_IAM_CLIENT_SECRET=
# Hanzo HSM (Hardware Security - optional)
HANZO_HSM_ENABLED=false
+1
View File
@@ -17,6 +17,7 @@ COPY pyproject.toml README.md ./
COPY bootnode/ bootnode/
# Install dependencies
# Hanzo SDKs are optional - in K8s, the KMS operator injects secrets as env vars
RUN uv pip install --system -e .
# Run the application
File diff suppressed because it is too large Load Diff
+158
View File
@@ -0,0 +1,158 @@
"""
Hanzo KMS Client for Bootnode
Fetches secrets from Hanzo KMS at startup using the official SDK.
Supports multiple auth methods: universal auth, K8s auth, AWS IAM.
Usage:
from bootnode.core.kms import get_secret, get_secrets, inject_secrets
# Get single secret
db_url = get_secret("DATABASE_URL")
# Get all secrets for environment
secrets = get_secrets()
# Inject all secrets at startup
inject_secrets()
Environment Variables:
HANZO_KMS_URL: KMS API URL (default: https://kms.hanzo.ai)
HANZO_KMS_CLIENT_ID: Universal auth client ID
HANZO_KMS_CLIENT_SECRET: Universal auth client secret
HANZO_KMS_PROJECT: Project ID or slug
HANZO_KMS_ENV: Environment (development, staging, production)
In Kubernetes, secrets are synced via the KMS Operator (KMSSecret CRD).
The operator automatically injects secrets as environment variables.
"""
import os
import logging
from typing import Optional
logger = logging.getLogger(__name__)
# KMS Configuration
KMS_URL = os.getenv("HANZO_KMS_URL", "https://kms.hanzo.ai")
KMS_ORG = os.getenv("HANZO_KMS_ORG", "hanzo")
KMS_CLIENT_ID = os.getenv("HANZO_KMS_CLIENT_ID", "")
KMS_CLIENT_SECRET = os.getenv("HANZO_KMS_CLIENT_SECRET", "")
KMS_PROJECT = os.getenv("HANZO_KMS_PROJECT", "bootnode")
KMS_ENV = os.getenv("HANZO_KMS_ENV", "production")
# Singleton client
_client = None
def _get_client():
"""Get or create KMS client singleton."""
global _client
if _client is not None:
return _client
# Skip if no credentials (K8s operator handles secrets)
if not KMS_CLIENT_ID or not KMS_CLIENT_SECRET:
logger.debug("No KMS credentials, secrets managed by K8s operator or env vars")
return None
try:
from hanzo_kms import (
KMSClient,
ClientSettings,
AuthenticationOptions,
UniversalAuthMethod,
)
_client = KMSClient(ClientSettings(
site_url=KMS_URL,
organization=KMS_ORG,
auth=AuthenticationOptions(
universal_auth=UniversalAuthMethod(
client_id=KMS_CLIENT_ID,
client_secret=KMS_CLIENT_SECRET,
)
)
))
logger.info("KMS client initialized", extra={"url": KMS_URL, "org": KMS_ORG, "project": KMS_PROJECT})
return _client
except ImportError:
logger.warning("hanzo-kms not installed, using environment variables only")
return None
except Exception as e:
logger.error(f"Failed to initialize KMS client: {e}")
return None
def get_secret(key: str, default: Optional[str] = None) -> Optional[str]:
"""Get a secret from KMS or environment.
Args:
key: Secret name
default: Default value if not found
Returns:
Secret value or default
"""
# First check environment (may be set by K8s operator)
env_value = os.getenv(key)
if env_value is not None:
return env_value
# Try KMS client
client = _get_client()
if client:
try:
return client.get_value(
project_id=KMS_PROJECT,
environment=KMS_ENV,
secret_name=key,
default=default,
)
except Exception as e:
logger.warning(f"Failed to get secret {key} from KMS: {e}")
return default
def get_secrets() -> dict[str, str]:
"""Get all secrets from KMS.
Returns:
Dictionary of secret key -> value
"""
client = _get_client()
if not client:
return {}
try:
secrets = client.list_secrets(
project_id=KMS_PROJECT,
environment=KMS_ENV,
)
return {s.secret_key: s.secret_value for s in secrets}
except Exception as e:
logger.error(f"Failed to list secrets from KMS: {e}")
return {}
def inject_secrets() -> None:
"""Inject KMS secrets into environment at startup.
Secrets already in environment (e.g., from K8s operator) are not overwritten.
"""
client = _get_client()
if not client:
logger.debug("KMS client not available, skipping secret injection")
return
try:
count = client.inject_env(
project_id=KMS_PROJECT,
environment=KMS_ENV,
overwrite=False, # Don't overwrite existing env vars
)
logger.info(f"Injected {count} secrets from KMS")
except Exception as e:
logger.error(f"Failed to inject secrets from KMS: {e}")
+4
View File
@@ -12,9 +12,13 @@ from bootnode.api import router as api_router
from bootnode.config import get_settings
from bootnode.core.cache import redis_client
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
# Load secrets from Hanzo KMS before settings
inject_secrets()
logger = structlog.get_logger()
settings = get_settings()
+8
View File
@@ -46,8 +46,10 @@ dependencies = [
# Infrastructure Management
"kubernetes>=31.0.0",
"pydo>=0.8.0",
"docker>=7.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.3.0",
@@ -58,6 +60,12 @@ dev = [
"mypy>=1.13.0",
"pre-commit>=4.0.0",
]
# Hanzo SDKs - optional, graceful degradation without them
# In K8s, the KMS operator injects secrets as env vars
hanzo = [
"hanzo-kms>=1.0.0",
"hanzo-iam>=1.0.0",
]
[project.scripts]
bootnode = "bootnode.cli:main"
Generated
+65
View File
@@ -391,9 +391,12 @@ dependencies = [
{ name = "asyncpg" },
{ name = "bcrypt" },
{ name = "cachetools" },
{ name = "docker" },
{ name = "eth-abi" },
{ name = "eth-account" },
{ name = "fastapi" },
{ name = "hanzo-iam" },
{ name = "hanzo-kms" },
{ name = "httpx" },
{ name = "kubernetes" },
{ name = "orjson" },
@@ -431,9 +434,12 @@ requires-dist = [
{ name = "asyncpg", specifier = ">=0.30.0" },
{ name = "bcrypt", specifier = ">=4.0.0" },
{ name = "cachetools", specifier = ">=5.5.0" },
{ name = "docker", specifier = ">=7.0.0" },
{ name = "eth-abi", specifier = ">=5.0.0" },
{ name = "eth-account", specifier = ">=0.13.0" },
{ name = "fastapi", specifier = ">=0.115.0" },
{ name = "hanzo-iam", editable = "../../hanzo/python-sdk/pkg/hanzo-iam" },
{ name = "hanzo-kms", editable = "../../hanzo/python-sdk/pkg/hanzo-kms" },
{ name = "httpx", specifier = ">=0.28.0" },
{ name = "httpx", marker = "extra == 'dev'", specifier = ">=0.28.0" },
{ name = "kubernetes", specifier = ">=31.0.0" },
@@ -934,6 +940,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
]
[[package]]
name = "docker"
version = "7.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pywin32", marker = "sys_platform == 'win32'" },
{ name = "requests" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" },
]
[[package]]
name = "durationpy"
version = "0.10"
@@ -1251,6 +1271,51 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "hanzo-iam"
version = "1.0.0"
source = { editable = "../../hanzo/python-sdk/pkg/hanzo-iam" }
dependencies = [
{ name = "httpx" },
{ name = "pydantic" },
{ name = "pyjwt" },
]
[package.metadata]
requires-dist = [
{ name = "fastapi", marker = "extra == 'fastapi'", specifier = ">=0.100.0" },
{ name = "hanzo-kms", marker = "extra == 'kms'", specifier = ">=1.0.0" },
{ name = "httpx", specifier = ">=0.25.0" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10.0" },
{ name = "pydantic", specifier = ">=2.0.0" },
{ name = "pyjwt", specifier = ">=2.8.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5.0" },
]
provides-extras = ["fastapi", "kms", "dev"]
[[package]]
name = "hanzo-kms"
version = "1.0.0"
source = { editable = "../../hanzo/python-sdk/pkg/hanzo-kms" }
dependencies = [
{ name = "httpx" },
{ name = "pydantic" },
]
[package.metadata]
requires-dist = [
{ name = "httpx", specifier = ">=0.25.0" },
{ name = "httpx", extras = ["http2"], marker = "extra == 'async'", specifier = ">=0.25.0" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10.0" },
{ name = "pydantic", specifier = ">=2.0.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5.0" },
]
provides-extras = ["async", "dev"]
[[package]]
name = "hexbytes"
version = "1.3.1"
+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffffff"/>
<stop offset="1" stop-color="#d8d8d8"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feOffset dx="2" dy="3" result="off"/>
<feGaussianBlur in="off" stdDeviation="2" result="blur"/>
<feColorMatrix in="blur" type="matrix"
values="0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0.35 0" result="shadow"/>
<feMerge>
<feMergeNode in="shadow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<rect width="200" height="200" fill="#372929"/>
<path d="M 27 99 L 48 137 L 92 137 L 100 124 L 108 137 L 151 137 L 172 101 L 151 62 L 108 62 L 100 75 L 92 62 L 49 62 Z M 148 133 L 166 103 L 131 102 L 113 132 Z M 51 133 L 86 132 L 69 102 L 33 103 Z M 91 101 L 100 114 L 109 100 L 99 85 Z M 102 80 L 113 100 L 102 120 L 109 130 L 127 99 L 109 69 Z M 73 101 L 91 130 L 97 118 L 86 99 L 97 79 L 90 69 Z M 131 98 L 166 97 L 148 66 L 113 67 Z M 68 98 L 86 67 L 51 66 L 34 97 Z" fill="#000000" opacity="0.18" transform="translate(3 3)" />
<path d="M 27 99 L 48 137 L 92 137 L 100 124 L 108 137 L 151 137 L 172 101 L 151 62 L 108 62 L 100 75 L 92 62 L 49 62 Z M 148 133 L 166 103 L 131 102 L 113 132 Z M 51 133 L 86 132 L 69 102 L 33 103 Z M 91 101 L 100 114 L 109 100 L 99 85 Z M 102 80 L 113 100 L 102 120 L 109 130 L 127 99 L 109 69 Z M 73 101 L 91 130 L 97 118 L 86 99 L 97 79 L 90 69 Z M 131 98 L 166 97 L 148 66 L 113 67 Z M 68 98 L 86 67 L 51 66 L 34 97 Z" fill="url(#g)" fill-rule="evenodd" filter="url(#shadow)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Submodule
+1
Submodule bootnode added at 0b7dd189f9
+1
Submodule bootnode-admin added at 69975fea7b
+98
View File
@@ -0,0 +1,98 @@
# Bootnode Brand Guidelines
Following [Hanzo ecosystem branding standards](../hanzo/brand/README.md).
## Logo Usage
### Primary Logo
- Use `bootnode-logo.svg` for main branding
- Minimum size: 32px height
- Clear space: 50% of logo height on all sides
### Logo Variants
- **Main**: Full color with shadow and gradient
- **White**: For dark backgrounds
- **Mono**: Single color for monochrome uses
- **Icon**: Symbol only for compact spaces
### Don'ts
- Don't stretch or distort the logo
- Don't change colors outside approved palette
- Don't add effects or modifications
- Don't use on busy backgrounds without proper contrast
## Color Palette
```scss
// Primary brand colors
$bootnode-primary: #372929; // Logo background
$bootnode-white: #ffffff; // Logo highlight
$bootnode-gray: #d8d8d8; // Logo gradient
$bootnode-surface: #f5f5f5; // UI surfaces
$bootnode-text: #1a1a1a; // Primary text
$bootnode-muted: #6b7280; // Secondary text
```
## Typography
- **Primary**: Geist Sans (modern, clean)
- **Monospace**: Geist Mono (code, technical content)
- **Weights**: 400 (normal), 500 (medium), 600 (semibold), 700 (bold)
## Voice & Tone
- **Professional** yet approachable
- **Technical** but accessible
- **Innovative** and forward-thinking
- **Reliable** and trustworthy
- **Developer-focused** language
## Messaging
### Tagline
"Blockchain Infrastructure for Developers"
### Key Messages
- Complete blockchain development platform
- Multi-chain support out of the box
- Enterprise-grade security and reliability
- Developer-first experience
- Unified Hanzo ecosystem integration
## Applications
### Web Applications
- Use primary logo in header/navigation
- Apply color palette consistently
- Follow typography hierarchy
- Maintain proper spacing and alignment
### Documentation
- Use mono logo for technical contexts
- Include proper attribution
- Link back to main platform
- Follow established information architecture
### Admin Interfaces
- Emphasize security and professionalism
- Use Hanzo IAM integration badges
- Clear organizational hierarchy
- Consistent with main platform design
## Integration Standards
### Hanzo Ecosystem
- Maintain visual consistency with other Hanzo properties
- Use unified authentication (hanzo.id, zoo.id, lux.id, pars.id)
- Follow shared component library patterns
- Respect cross-product user journeys
### External Integrations
- Maintain brand integrity in third-party contexts
- Provide clear attribution and links
- Use appropriate logo variants for different platforms
- Follow platform-specific guidelines while preserving brand essence
---
For questions or brand asset requests, contact the Hanzo brand team.
+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffffff"/>
<stop offset="1" stop-color="#d8d8d8"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feOffset dx="2" dy="3" result="off"/>
<feGaussianBlur in="off" stdDeviation="2" result="blur"/>
<feColorMatrix in="blur" type="matrix"
values="0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0.35 0" result="shadow"/>
<feMerge>
<feMergeNode in="shadow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<rect width="200" height="200" fill="#372929"/>
<path d="M 27 99 L 48 137 L 92 137 L 100 124 L 108 137 L 151 137 L 172 101 L 151 62 L 108 62 L 100 75 L 92 62 L 49 62 Z M 148 133 L 166 103 L 131 102 L 113 132 Z M 51 133 L 86 132 L 69 102 L 33 103 Z M 91 101 L 100 114 L 109 100 L 99 85 Z M 102 80 L 113 100 L 102 120 L 109 130 L 127 99 L 109 69 Z M 73 101 L 91 130 L 97 118 L 86 99 L 97 79 L 90 69 Z M 131 98 L 166 97 L 148 66 L 113 67 Z M 68 98 L 86 67 L 51 66 L 34 97 Z" fill="#000000" opacity="0.18" transform="translate(3 3)" />
<path d="M 27 99 L 48 137 L 92 137 L 100 124 L 108 137 L 151 137 L 172 101 L 151 62 L 108 62 L 100 75 L 92 62 L 49 62 Z M 148 133 L 166 103 L 131 102 L 113 132 Z M 51 133 L 86 132 L 69 102 L 33 103 Z M 91 101 L 100 114 L 109 100 L 99 85 Z M 102 80 L 113 100 L 102 120 L 109 130 L 127 99 L 109 69 Z M 73 101 L 91 130 L 97 118 L 86 99 L 97 79 L 90 69 Z M 131 98 L 166 97 L 148 66 L 113 67 Z M 68 98 L 86 67 L 51 66 L 34 97 Z" fill="url(#g)" fill-rule="evenodd" filter="url(#shadow)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+85
View File
@@ -0,0 +1,85 @@
// Bootnode Logo Components
// React components for consistent logo usage across the platform
import { bootnode } from './config'
interface LogoProps {
variant?: 'main' | 'white' | 'mono' | 'icon'
size?: number | string
className?: string
}
export function BootnodeLogo({
variant = 'main',
size = 32,
className = ''
}: LogoProps) {
const logoPath = bootnode.logo[variant]
return (
<img
src={logoPath}
alt="Bootnode"
width={size}
height={size}
className={className}
/>
)
}
export function BootnodeIcon({
size = 24,
className = ''
}: Omit<LogoProps, 'variant'>) {
return (
<BootnodeLogo
variant="icon"
size={size}
className={className}
/>
)
}
// Logo with text for headers
export function BootnodeBrand({
variant = 'main',
showTagline = false,
className = ''
}: LogoProps & { showTagline?: boolean }) {
return (
<div className={`flex items-center space-x-3 ${className}`}>
<BootnodeLogo variant={variant} size={32} />
<div>
<h1 className="text-xl font-bold text-bootnode-text">
{bootnode.name}
</h1>
{showTagline && (
<p className="text-sm text-bootnode-muted">
{bootnode.tagline}
</p>
)}
</div>
</div>
)
}
// Organization badge component
export function OrgBadge({ org }: { org: keyof typeof bootnode.orgs }) {
const orgConfig = bootnode.orgs[org]
return (
<span
className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium text-white"
style={{ backgroundColor: orgConfig.color }}
>
{orgConfig.name}
</span>
)
}
export default {
BootnodeLogo,
BootnodeIcon,
BootnodeBrand,
OrgBadge,
}
+93
View File
@@ -0,0 +1,93 @@
// Bootnode Brand Configuration
// Following Hanzo ecosystem standards
export const bootnode = {
name: 'Bootnode',
tagline: 'Blockchain Infrastructure for Developers',
description: 'The complete blockchain development platform. Multi-chain RPC, Token APIs, NFT APIs, Smart Wallets, Webhooks, and more.',
// Colors aligned with logo
colors: {
primary: '#372929', // From logo background
secondary: '#ffffff', // From logo gradient
accent: '#d8d8d8', // From logo gradient
surface: '#f5f5f5', // Light surface
text: '#1a1a1a', // Dark text
textMuted: '#6b7280', // Muted text
},
// Typography
typography: {
fontFamily: 'Geist Sans, system-ui, sans-serif',
monoFamily: 'Geist Mono, Consolas, monospace',
},
// Logo variants
logo: {
main: '/logo/bootnode-logo.svg',
white: '/logo/bootnode-logo-white.svg',
mono: '/logo/bootnode-logo-mono.svg',
icon: '/logo/bootnode-icon.svg',
},
// URLs & Links
urls: {
app: 'https://bootnode.dev',
admin: 'https://admin.bootnode.dev',
docs: 'https://docs.bootnode.dev',
api: 'https://api.bootnode.dev',
github: 'https://github.com/bootnode',
},
// Organizations
orgs: {
hanzo: {
name: 'Hanzo',
domain: 'hanzo.id',
color: '#0066ff',
},
zoo: {
name: 'Zoo Labs',
domain: 'zoo.id',
color: '#00cc66',
},
lux: {
name: 'Lux Network',
domain: 'lux.id',
color: '#8b5cf6',
},
pars: {
name: 'Pars',
domain: 'pars.id',
color: '#f59e0b',
},
},
// Supported networks
networks: [
'Ethereum', 'Polygon', 'Arbitrum', 'Optimism',
'Base', 'Avalanche', 'BNB Chain', 'Lux',
'Bitcoin', 'Solana'
],
// Platform features
features: [
'Multi-chain RPC Proxy',
'Token & NFT APIs',
'Smart Wallets (ERC-4337)',
'Gas Management',
'Webhook System',
'Real-time Analytics',
'Developer Dashboard',
'Admin Management',
],
// Social media
social: {
twitter: '@bootnode_dev',
discord: 'https://discord.gg/bootnode',
telegram: 'https://t.me/bootnode',
},
}
export default bootnode
+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffffff"/>
<stop offset="1" stop-color="#d8d8d8"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feOffset dx="2" dy="3" result="off"/>
<feGaussianBlur in="off" stdDeviation="2" result="blur"/>
<feColorMatrix in="blur" type="matrix"
values="0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0.35 0" result="shadow"/>
<feMerge>
<feMergeNode in="shadow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<rect width="200" height="200" fill="#372929"/>
<path d="M 27 99 L 48 137 L 92 137 L 100 124 L 108 137 L 151 137 L 172 101 L 151 62 L 108 62 L 100 75 L 92 62 L 49 62 Z M 148 133 L 166 103 L 131 102 L 113 132 Z M 51 133 L 86 132 L 69 102 L 33 103 Z M 91 101 L 100 114 L 109 100 L 99 85 Z M 102 80 L 113 100 L 102 120 L 109 130 L 127 99 L 109 69 Z M 73 101 L 91 130 L 97 118 L 86 99 L 97 79 L 90 69 Z M 131 98 L 166 97 L 148 66 L 113 67 Z M 68 98 L 86 67 L 51 66 L 34 97 Z" fill="#000000" opacity="0.18" transform="translate(3 3)" />
<path d="M 27 99 L 48 137 L 92 137 L 100 124 L 108 137 L 151 137 L 172 101 L 151 62 L 108 62 L 100 75 L 92 62 L 49 62 Z M 148 133 L 166 103 L 131 102 L 113 132 Z M 51 133 L 86 132 L 69 102 L 33 103 Z M 91 101 L 100 114 L 109 100 L 99 85 Z M 102 80 L 113 100 L 102 120 L 109 130 L 127 99 L 109 69 Z M 73 101 L 91 130 L 97 118 L 86 99 L 97 79 L 90 69 Z M 131 98 L 166 97 L 148 66 L 113 67 Z M 68 98 L 86 67 L 51 66 L 34 97 Z" fill="url(#g)" fill-rule="evenodd" filter="url(#shadow)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+11
View File
@@ -82,6 +82,13 @@ services:
API_KEY_SALT: dev-api-key-salt
# Multi-tenant off for local dev
ENABLE_MULTI_TENANT: "false"
# Hanzo KMS (optional for local dev - uses env vars directly)
HANZO_KMS_URL: ${HANZO_KMS_URL:-https://kms.hanzo.ai}
HANZO_KMS_ORG: ${HANZO_KMS_ORG:-hanzo}
HANZO_KMS_PROJECT: ${HANZO_KMS_PROJECT:-bootnode}
HANZO_KMS_ENV: ${HANZO_KMS_ENV:-development}
HANZO_KMS_CLIENT_ID: ${HANZO_KMS_CLIENT_ID:-}
HANZO_KMS_CLIENT_SECRET: ${HANZO_KMS_CLIENT_SECRET:-}
# Public RPC endpoints
ETH_MAINNET_RPC: https://eth.llamarpc.com
ETH_SEPOLIA_RPC: https://ethereum-sepolia-rpc.publicnode.com
@@ -106,6 +113,10 @@ services:
retries: 3
volumes:
- ./api:/app:cached
# Mount Docker socket for node deployment
- /var/run/docker.sock:/var/run/docker.sock
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped
web:
+139
View File
@@ -0,0 +1,139 @@
# Production Docker Compose - Real Infrastructure
# Usage: docker-compose -f docker-compose.prod.yml up -d
version: '3.8'
services:
# PostgreSQL Database - Production Ready
postgres:
image: postgres:16-alpine
container_name: bootnode-postgres-prod
environment:
POSTGRES_USER: bootnode
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: bootnode
ports:
- "5432:5432"
volumes:
- bootnode-postgres-prod:/var/lib/postgresql/data
- ./infra/sql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U bootnode"]
interval: 30s
timeout: 10s
retries: 3
# Redis Cache & MQ - Production Ready
redis:
image: redis:7-alpine
container_name: bootnode-redis-prod
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
ports:
- "6379:6379"
volumes:
- bootnode-redis-prod:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "--pass", "${REDIS_PASSWORD}", "ping"]
interval: 30s
timeout: 10s
retries: 3
# ClickHouse Analytics - Production Ready
clickhouse:
image: clickhouse/clickhouse-server:24-alpine
container_name: bootnode-clickhouse-prod
environment:
CLICKHOUSE_DB: bootnode
CLICKHOUSE_USER: bootnode
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
ports:
- "8123:8123"
- "9000:9000"
volumes:
- bootnode-clickhouse-prod:/var/lib/clickhouse
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8123/ping"]
interval: 30s
timeout: 10s
retries: 3
# Bootnode API - Production
bootnode-api:
build:
context: ./api
dockerfile: Dockerfile
container_name: bootnode-api-prod
env_file:
- .env.production
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 15s
retries: 5
deploy:
resources:
limits:
memory: 2G
cpus: '1.0'
# Web Frontend - Production
bootnode-web:
build:
context: ./web
dockerfile: Dockerfile
container_name: bootnode-web-prod
environment:
- NODE_ENV=production
- NEXT_PUBLIC_API_URL=${API_BASE_URL:-http://localhost:8000}
ports:
- "3000:3000"
depends_on:
- bootnode-api
restart: unless-stopped
deploy:
resources:
limits:
memory: 1G
cpus: '0.5'
# Webhook Worker - Background Jobs
bootnode-worker:
build:
context: ./api
dockerfile: Dockerfile
container_name: bootnode-worker-prod
env_file:
- .env.production
command: ["python", "-m", "arq", "bootnode.workers.webhook.WorkerSettings"]
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
deploy:
resources:
limits:
memory: 1G
cpus: '0.5'
volumes:
bootnode-postgres-prod:
bootnode-redis-prod:
bootnode-clickhouse-prod:
networks:
default:
name: bootnode-prod
+126
View File
@@ -0,0 +1,126 @@
#!/bin/bash
# Health Check Script - Check all Bootnode services
# Usage: ./health-check.sh
set -e
echo "🏥 Bootnode Health Check"
echo "======================="
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to check service
check_service() {
local name="$1"
local url="$2"
local expected="$3"
echo -n "Checking $name... "
if curl -f -s "$url" | grep -q "$expected" 2>/dev/null; then
echo -e "${GREEN}✅ OK${NC}"
return 0
else
echo -e "${RED}❌ FAILED${NC}"
return 1
fi
}
# Function to check port
check_port() {
local name="$1"
local host="$2"
local port="$3"
echo -n "Checking $name port... "
if nc -z "$host" "$port" 2>/dev/null; then
echo -e "${GREEN}✅ OK${NC}"
return 0
else
echo -e "${RED}❌ FAILED${NC}"
return 1
fi
}
# Track failures
FAILURES=0
echo "📡 API Services"
echo "---------------"
# API Health
if ! check_service "Bootnode API" "http://localhost:8100/health" "healthy"; then
FAILURES=$((FAILURES + 1))
fi
# API Chains
if ! check_service "Chains API" "http://localhost:8100/v1/chains" "ethereum"; then
FAILURES=$((FAILURES + 1))
fi
# Frontend
if ! check_service "Web Frontend" "http://localhost:3001" "Bootnode"; then
FAILURES=$((FAILURES + 1))
fi
echo ""
echo "🗄️ Infrastructure"
echo "------------------"
# PostgreSQL
if ! check_port "PostgreSQL" "localhost" "5434"; then
FAILURES=$((FAILURES + 1))
fi
# Redis
if ! check_port "Redis" "localhost" "6383"; then
FAILURES=$((FAILURES + 1))
fi
# ClickHouse
if ! check_port "ClickHouse" "localhost" "8125"; then
FAILURES=$((FAILURES + 1))
fi
echo ""
echo "🐳 Docker Containers"
echo "-------------------"
# Check Docker containers
if command -v docker >/dev/null 2>&1; then
docker ps --format "table {{.Names}}\t{{.Status}}" | grep nodes-
else
echo -e "${YELLOW}⚠️ Docker not available${NC}"
fi
echo ""
echo "📊 Summary"
echo "----------"
if [ $FAILURES -eq 0 ]; then
echo -e "${GREEN}🎉 All services are healthy!${NC}"
echo ""
echo "📍 Service URLs:"
echo " • API: http://localhost:8100"
echo " • Web: http://localhost:3001"
echo " • Docs: http://localhost:8100/docs"
echo ""
echo "🧪 Quick Tests:"
echo " curl http://localhost:8100/health"
echo " curl http://localhost:8100/v1/chains"
exit 0
else
echo -e "${RED}❌ $FAILURES service(s) failed health check${NC}"
echo ""
echo "🔧 Troubleshooting:"
echo " 1. Check if Docker is running: docker ps"
echo " 2. Restart services: docker-compose restart"
echo " 3. Check logs: docker-compose logs -f"
echo " 4. Verify environment: cat api/.env"
exit 1
fi
+41
View File
@@ -0,0 +1,41 @@
# Bootnode Indexer - Multi-chain blockchain indexer
# Based on github.com/luxfi/indexer
FROM golang:1.22-alpine AS builder
WORKDIR /build
# Install dependencies
RUN apk add --no-cache git make gcc musl-dev
# Copy indexer source (forked from lux/indexer)
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Build the indexer
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /indexer ./cmd/multichain
# Final image
FROM alpine:3.19
RUN apk add --no-cache ca-certificates curl
WORKDIR /app
COPY --from=builder /indexer /app/indexer
COPY config/chains.yaml /app/config/chains.yaml
# Create non-root user
RUN adduser -D -u 1000 indexer
RUN chown -R indexer:indexer /app
USER indexer
EXPOSE 5000 9090
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD curl -f http://localhost:5000/health || exit 1
ENTRYPOINT ["/app/indexer"]
CMD ["--config", "/app/config/chains.yaml"]
+160
View File
@@ -0,0 +1,160 @@
# Bootnode Indexer
Multi-chain blockchain indexer for the Bootnode platform. Forked from [luxfi/indexer](https://github.com/luxfi/indexer).
## Features
- **100+ Chain Support**: Indexes data from Ethereum, Solana, Bitcoin, and 97+ other chains
- **High Performance**: Optimized for high-throughput indexing with parallel processing
- **DataStore Integration**: Writes indexed data to ClickHouse for fast analytics queries
- **Protocol Indexing**: Supports 50+ DeFi, NFT, and bridge protocols
- **Real-time Events**: Publishes events to RabbitMQ for webhook delivery
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ Bootnode Indexer │
├─────────────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ EVM │ │ Solana │ │ Bitcoin │ │ Lux │ │
│ │ Indexer │ │ Indexer │ │ Indexer │ │ Native │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └─────────────┴─────────────┴─────────────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ Manager │ │
│ └──────┬──────┘ │
├───────────────────────────┼─────────────────────────────────────┤
│ Storage Layer │ │
│ ┌────────────┐ ┌────────┴───────┐ ┌───────────────┐ │
│ │ PostgreSQL │ │ DataStore │ │ RabbitMQ │ │
│ │ (Metadata) │ │ (ClickHouse) │ │ (Events) │ │
│ └────────────┘ └────────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## Supported Chains
### EVM Chains
- Ethereum (mainnet, sepolia, holesky)
- Arbitrum (one, nova, sepolia)
- Base (mainnet, sepolia)
- Optimism (mainnet, sepolia)
- Polygon (PoS, zkEVM)
- zkSync Era
- Scroll
- Linea
- BNB Chain (mainnet, opBNB)
- Avalanche C-Chain
- Gnosis
- Celo
- Blast
- Mantle
- Mode
- Zora
- And 50+ more...
### Non-EVM Chains
- Solana (mainnet, devnet)
- Bitcoin (mainnet, testnet)
- Aptos
- Starknet
- Flow
- Tron
- TON
### Lux Native
- C-Chain (EVM)
- P-Chain (Platform/Staking)
- X-Chain (Assets/DAG)
## Configuration
Configuration is done via `config/chains.yaml`:
```yaml
ethereum:
mainnet:
chain_id: 1
name: "Ethereum Mainnet"
type: evm
rpc: "${ETH_MAINNET_RPC}"
enabled: true
protocols:
- uniswap_v3
- aave_v3
- seaport
```
## Environment Variables
```bash
# Database
DATABASE_URL=postgres://user:pass@localhost:5432/bootnode
DATASTORE_URL=clickhouse://user:pass@localhost:8123/bootnode
# Message Queue
RABBITMQ_URL=amqp://user:pass@localhost:5672/
# Cache
REDIS_URL=redis://localhost:6379/1
# Settings
CONFIG_PATH=/app/config/chains.yaml
LOG_LEVEL=info
MAX_CHAINS=100
BATCH_SIZE=1000
```
## API Endpoints
| Endpoint | Description |
|----------|-------------|
| `GET /health` | Health check |
| `GET /stats` | Aggregate indexing stats |
| `GET /chains` | Per-chain stats |
| `GET /chain/{id}` | Individual chain stats |
| `GET /metrics` | Prometheus metrics |
## Running Locally
```bash
# With Go
go run ./cmd/multichain --config config/chains.yaml
# With Docker
docker build -t bootnode/indexer .
docker run -p 5000:5000 \
-e DATABASE_URL=... \
-e DATASTORE_URL=... \
bootnode/indexer
```
## Development
```bash
# Install dependencies
go mod download
# Run tests
go test ./...
# Build
go build -o indexer ./cmd/multichain
# Run
./indexer --config config/chains.yaml
```
## Adding New Chains
1. Add chain config to `config/chains.yaml`
2. If new chain type, create adapter in `multichain/`
3. Implement `ChainIndexer` interface
4. Add schema migration if needed
## License
BSD-3-Clause
+73
View File
@@ -0,0 +1,73 @@
# Bootnode Infrastructure Environment Variables
# Copy to .env and configure for your deployment
# =============================================================================
# APPLICATION
# =============================================================================
APP_ENV=development
LOG_LEVEL=info
# =============================================================================
# DATABASE PASSWORDS
# =============================================================================
POSTGRES_PASSWORD=bootnode_secret
CLICKHOUSE_PASSWORD=bootnode_secret
# =============================================================================
# SECURITY
# =============================================================================
JWT_SECRET=change_me_in_production_use_openssl_rand_hex_32
API_KEY_SALT=change_me_in_production_use_openssl_rand_hex_32
# =============================================================================
# CHAIN RPC ENDPOINTS
# =============================================================================
# Configure your own RPC endpoints for best performance
# Free public RPCs are included as defaults but may be rate limited
# Ethereum
ETH_MAINNET_RPC=https://eth.llamarpc.com
ETH_SEPOLIA_RPC=https://ethereum-sepolia-rpc.publicnode.com
# Layer 2s
BASE_MAINNET_RPC=https://mainnet.base.org
ARB_MAINNET_RPC=https://arb1.arbitrum.io/rpc
POLYGON_MAINNET_RPC=https://polygon-rpc.com
OP_MAINNET_RPC=https://mainnet.optimism.io
# Solana
SOL_MAINNET_RPC=https://api.mainnet-beta.solana.com
SOL_DEVNET_RPC=https://api.devnet.solana.com
# Lux Network
LUX_MAINNET_RPC=https://api.lux.network/ext/bc/C/rpc
LUX_TESTNET_RPC=https://api.lux-test.network/ext/bc/C/rpc
# =============================================================================
# BLOCKCHAIN NODES (Optional - use "docker compose --profile nodes up -d")
# =============================================================================
# When running local nodes, override the RPC endpoints to point to them:
# ETH_MAINNET_RPC=http://ethereum-node:8545
# SOL_MAINNET_RPC=http://solana-node:8899
# Bitcoin Node
BTC_RPC_USER=bootnode
BTC_RPC_PASSWORD=bootnode_btc_secret
# BTC_MAINNET_RPC=http://bitcoin-node:8332
# =============================================================================
# ERC-4337 BUNDLER (Optional)
# =============================================================================
# Private key for bundler to submit UserOperations
# BUNDLER_PRIVATE_KEY=0x...
# =============================================================================
# MONITORING (Optional)
# =============================================================================
GRAFANA_USER=admin
GRAFANA_PASSWORD=bootnode
# =============================================================================
# API URL (for web dashboard)
# =============================================================================
API_URL=http://localhost:8000
+205
View File
@@ -0,0 +1,205 @@
# Bootnode Infrastructure
Docker Compose and Kubernetes configurations for running the Bootnode blockchain development platform.
## Quick Start
### Prerequisites
- Docker & Docker Compose v2
- 8GB+ RAM recommended
- Configure RPC endpoints in `.env`
### Local Development
```bash
# Copy environment template
cp .env.example .env
# Edit .env with your RPC endpoints and secrets
vim .env
# Start core services
docker compose up -d
# View logs
docker compose logs -f
# Check health
curl http://localhost:8000/health
```
### Services
| Service | Port | Description |
|---------|------|-------------|
| API | 8000 | Bootnode FastAPI backend |
| Web | 3001 | Next.js dashboard |
| Indexer | 5000 | Multi-chain blockchain indexer |
| PostgreSQL | 5432 | Operational database |
| Redis | 6379 | Cache & rate limiting |
| DataStore | 8123 | ClickHouse analytics |
| RabbitMQ | 5672, 15672 | Message queue |
### Profiles
```bash
# Core services only
docker compose up -d
# With monitoring (Prometheus + Grafana)
docker compose --profile monitoring up -d
# With ERC-4337 bundler
docker compose --profile bundler up -d
# Everything
docker compose --profile monitoring --profile bundler up -d
```
### Scaling
```bash
# Scale API horizontally
docker compose up -d --scale api=3
```
## Kubernetes
Deploy to Kubernetes clusters on any cloud provider.
### Prerequisites
- kubectl configured
- Helm 3 installed
- Cluster with 3+ nodes (8GB+ RAM each)
### Deploy
```bash
# Create namespace and secrets
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/secrets.yaml
# Deploy services
kubectl apply -f k8s/
# Check status
kubectl -n bootnode get pods
```
### Cloud-Specific Deployment
```bash
# AWS EKS
./cloud/deploy.sh aws
# Google Cloud GKE
./cloud/deploy.sh gcp
# Azure AKS
./cloud/deploy.sh azure
# DigitalOcean DOKS
./cloud/deploy.sh digitalocean
```
## Configuration
### Environment Variables
See `.env.example` for all configuration options.
Key variables:
- `DATABASE_URL` - PostgreSQL connection
- `REDIS_URL` - Redis connection
- `DATASTORE_URL` - ClickHouse connection
- `*_RPC` - Chain RPC endpoints
### Chain Configuration
Edit `config/chains.yaml` to enable/disable chains and configure protocols.
```yaml
ethereum:
mainnet:
chain_id: 1
enabled: true
protocols:
- uniswap_v3
- aave_v3
```
## Monitoring
### Prometheus
Access at http://localhost:9091
Metrics available:
- API request latency
- RPC call counts
- Indexer block height
- Queue depth
### Grafana
Access at http://localhost:3000
Default credentials: admin / bootnode
Pre-configured dashboards:
- API Overview
- Chain Status
- Indexer Progress
## Troubleshooting
### Service won't start
```bash
# Check logs
docker compose logs <service>
# Verify dependencies
docker compose ps
```
### Database connection issues
```bash
# Ensure postgres is healthy
docker compose exec postgres pg_isready
# Reset database
docker compose down -v
docker compose up -d
```
### High memory usage
```bash
# Limit indexer chains
MAX_CHAINS=10 docker compose up -d indexer
```
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ Load Balancer │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ API │ │ API │ │ Web │ │ Indexer │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │ │
│ └────────────┴────────────┴────────────┘ │
│ │ │
│ ┌────────────────────────┴────────────────────────┐ │
│ │ Data Layer │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │PostgreSQL│ │ Redis │ │ DataStore │ │ │
│ │ └──────────┘ └──────────┘ └──────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
+109
View File
@@ -0,0 +1,109 @@
# AWS EKS Cluster Configuration
# Deploy with: eksctl create cluster -f eks-cluster.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: bootnode-cluster
region: us-east-1
version: "1.29"
tags:
environment: production
project: bootnode
iam:
withOIDC: true
serviceAccounts:
- metadata:
name: bootnode-api
namespace: bootnode
attachPolicyARNs:
- arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
- metadata:
name: external-secrets
namespace: kube-system
attachPolicyARNs:
- arn:aws:iam::aws:policy/SecretsManagerReadWrite
vpc:
cidr: 10.0.0.0/16
nat:
gateway: HighlyAvailable
clusterEndpoints:
publicAccess: true
privateAccess: true
addons:
- name: vpc-cni
version: latest
- name: coredns
version: latest
- name: kube-proxy
version: latest
- name: aws-ebs-csi-driver
version: latest
serviceAccountRoleARN: arn:aws:iam::${AWS_ACCOUNT_ID}:role/AmazonEKS_EBS_CSI_DriverRole
managedNodeGroups:
# API Nodes - General purpose
- name: api-nodes
instanceType: m6i.xlarge
desiredCapacity: 3
minSize: 2
maxSize: 10
volumeSize: 100
volumeType: gp3
labels:
role: api
nodegroup-type: api
tags:
nodegroup-role: api
iam:
withAddonPolicies:
ebs: true
cloudWatch: true
# Indexer Nodes - Memory optimized
- name: indexer-nodes
instanceType: r6i.2xlarge
desiredCapacity: 2
minSize: 1
maxSize: 5
volumeSize: 500
volumeType: gp3
labels:
role: indexer
nodegroup-type: indexer
tags:
nodegroup-role: indexer
taints:
- key: dedicated
value: indexer
effect: NoSchedule
# Database Nodes - Storage optimized
- name: db-nodes
instanceType: i3.xlarge
desiredCapacity: 3
minSize: 3
maxSize: 5
volumeSize: 200
volumeType: gp3
labels:
role: database
nodegroup-type: database
tags:
nodegroup-role: database
taints:
- key: dedicated
value: database
effect: NoSchedule
cloudWatch:
clusterLogging:
enableTypes:
- api
- audit
- authenticator
- controllerManager
- scheduler
+142
View File
@@ -0,0 +1,142 @@
// Azure AKS Cluster Configuration
// Deploy with: az deployment group create --template-file aks-cluster.bicep
@description('The name of the AKS cluster')
param clusterName string = 'bootnode-cluster'
@description('The location for the cluster')
param location string = resourceGroup().location
@description('The Kubernetes version')
param kubernetesVersion string = '1.29.0'
@description('The VM size for API nodes')
param apiNodeVmSize string = 'Standard_D4s_v3'
@description('The VM size for indexer nodes')
param indexerNodeVmSize string = 'Standard_E8s_v3'
@description('The VM size for database nodes')
param dbNodeVmSize string = 'Standard_L8s_v2'
resource aksCluster 'Microsoft.ContainerService/managedClusters@2024-01-01' = {
name: clusterName
location: location
identity: {
type: 'SystemAssigned'
}
properties: {
kubernetesVersion: kubernetesVersion
dnsPrefix: '${clusterName}-dns'
enableRBAC: true
networkProfile: {
networkPlugin: 'azure'
networkPolicy: 'calico'
loadBalancerSku: 'standard'
serviceCidr: '10.0.0.0/16'
dnsServiceIP: '10.0.0.10'
}
addonProfiles: {
azurePolicy: {
enabled: true
}
omsagent: {
enabled: true
config: {
logAnalyticsWorkspaceResourceID: logAnalyticsWorkspace.id
}
}
}
agentPoolProfiles: [
{
name: 'system'
count: 2
vmSize: 'Standard_D2s_v3'
osType: 'Linux'
mode: 'System'
enableAutoScaling: true
minCount: 2
maxCount: 4
osDiskSizeGB: 100
osDiskType: 'Managed'
type: 'VirtualMachineScaleSets'
nodeTaints: [
'CriticalAddonsOnly=true:NoSchedule'
]
}
{
name: 'api'
count: 3
vmSize: apiNodeVmSize
osType: 'Linux'
mode: 'User'
enableAutoScaling: true
minCount: 2
maxCount: 10
osDiskSizeGB: 100
osDiskType: 'Managed'
type: 'VirtualMachineScaleSets'
nodeLabels: {
role: 'api'
'nodegroup-type': 'api'
}
}
{
name: 'indexer'
count: 2
vmSize: indexerNodeVmSize
osType: 'Linux'
mode: 'User'
enableAutoScaling: true
minCount: 1
maxCount: 5
osDiskSizeGB: 500
osDiskType: 'Managed'
type: 'VirtualMachineScaleSets'
nodeLabels: {
role: 'indexer'
'nodegroup-type': 'indexer'
}
nodeTaints: [
'dedicated=indexer:NoSchedule'
]
}
{
name: 'database'
count: 3
vmSize: dbNodeVmSize
osType: 'Linux'
mode: 'User'
enableAutoScaling: false
osDiskSizeGB: 200
osDiskType: 'Managed'
type: 'VirtualMachineScaleSets'
nodeLabels: {
role: 'database'
'nodegroup-type': 'database'
}
nodeTaints: [
'dedicated=database:NoSchedule'
]
}
]
}
}
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
name: '${clusterName}-logs'
location: location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 30
}
}
output clusterName string = aksCluster.name
output controlPlaneFQDN string = aksCluster.properties.fqdn
output kubeletIdentityObjectId string = aksCluster.properties.identityProfile.kubeletidentity.objectId
+278
View File
@@ -0,0 +1,278 @@
#!/bin/bash
# Bootnode Cloud Deployment Script
# Usage: ./deploy.sh <provider> <environment>
# Example: ./deploy.sh aws production
set -euo pipefail
PROVIDER="${1:-docker}"
ENVIRONMENT="${2:-development}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
echo "=== Bootnode Deployment ==="
echo "Provider: $PROVIDER"
echo "Environment: $ENVIRONMENT"
echo ""
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Check prerequisites
check_prerequisites() {
case $PROVIDER in
aws)
command -v aws >/dev/null 2>&1 || { log_error "AWS CLI required"; exit 1; }
command -v eksctl >/dev/null 2>&1 || { log_error "eksctl required"; exit 1; }
;;
gcp)
command -v gcloud >/dev/null 2>&1 || { log_error "gcloud CLI required"; exit 1; }
;;
azure)
command -v az >/dev/null 2>&1 || { log_error "Azure CLI required"; exit 1; }
;;
digitalocean)
command -v doctl >/dev/null 2>&1 || { log_error "doctl required"; exit 1; }
;;
docker)
command -v docker >/dev/null 2>&1 || { log_error "Docker required"; exit 1; }
;;
esac
command -v kubectl >/dev/null 2>&1 || { log_error "kubectl required"; exit 1; }
}
# Build Docker images
build_images() {
log_info "Building Docker images..."
cd "$ROOT_DIR"
# Build API
docker build -t bootnode/api:latest -f api/Dockerfile api/
# Build Web
docker build -t bootnode/web:latest -f web/Dockerfile web/
# Build Indexer (if exists)
if [ -d "indexer" ]; then
docker build -t bootnode/indexer:latest -f indexer/Dockerfile indexer/
fi
log_info "Images built successfully"
}
# Deploy to Docker Compose (local/development)
deploy_docker() {
log_info "Deploying with Docker Compose..."
cd "$ROOT_DIR/infra"
if [ "$ENVIRONMENT" = "production" ]; then
docker compose --profile monitoring up -d
else
docker compose up -d
fi
log_info "Waiting for services to be healthy..."
sleep 10
docker compose ps
log_info "Deployment complete!"
echo ""
echo "Services available at:"
echo " - API: http://localhost:8000"
echo " - Web: http://localhost:3001"
echo " - Docs: http://localhost:8000/docs"
}
# Deploy to AWS EKS
deploy_aws() {
log_info "Deploying to AWS EKS..."
# Create cluster if it doesn't exist
if ! eksctl get cluster --name bootnode-cluster 2>/dev/null; then
log_info "Creating EKS cluster..."
eksctl create cluster -f "$SCRIPT_DIR/aws/eks-cluster.yaml"
fi
# Update kubeconfig
aws eks update-kubeconfig --name bootnode-cluster
# Push images to ECR
log_info "Pushing images to ECR..."
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
AWS_REGION=${AWS_REGION:-us-east-1}
ECR_REGISTRY="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $ECR_REGISTRY
for image in api web indexer; do
aws ecr describe-repositories --repository-names bootnode/$image 2>/dev/null || \
aws ecr create-repository --repository-name bootnode/$image
docker tag bootnode/$image:latest $ECR_REGISTRY/bootnode/$image:latest
docker push $ECR_REGISTRY/bootnode/$image:latest
done
# Deploy to Kubernetes
deploy_kubernetes
}
# Deploy to GCP GKE
deploy_gcp() {
log_info "Deploying to GCP GKE..."
PROJECT_ID=$(gcloud config get-value project)
REGION=${GCP_REGION:-us-central1}
# Create cluster if it doesn't exist
if ! gcloud container clusters describe bootnode-cluster --region $REGION 2>/dev/null; then
log_info "Creating GKE cluster..."
gcloud container clusters create bootnode-cluster \
--region $REGION \
--num-nodes 3 \
--machine-type e2-standard-4 \
--enable-autoscaling --min-nodes 2 --max-nodes 10 \
--workload-pool=$PROJECT_ID.svc.id.goog
fi
# Get credentials
gcloud container clusters get-credentials bootnode-cluster --region $REGION
# Push images to GCR
log_info "Pushing images to GCR..."
for image in api web indexer; do
docker tag bootnode/$image:latest gcr.io/$PROJECT_ID/bootnode-$image:latest
docker push gcr.io/$PROJECT_ID/bootnode-$image:latest
done
# Deploy to Kubernetes
deploy_kubernetes
}
# Deploy to Azure AKS
deploy_azure() {
log_info "Deploying to Azure AKS..."
RESOURCE_GROUP=${AZURE_RESOURCE_GROUP:-bootnode-rg}
LOCATION=${AZURE_LOCATION:-eastus}
# Create resource group
az group create --name $RESOURCE_GROUP --location $LOCATION 2>/dev/null || true
# Deploy cluster
az deployment group create \
--resource-group $RESOURCE_GROUP \
--template-file "$SCRIPT_DIR/azure/aks-cluster.bicep"
# Get credentials
az aks get-credentials --resource-group $RESOURCE_GROUP --name bootnode-cluster
# Push images to ACR
log_info "Pushing images to ACR..."
ACR_NAME="${RESOURCE_GROUP}acr"
az acr create --resource-group $RESOURCE_GROUP --name $ACR_NAME --sku Standard 2>/dev/null || true
az acr login --name $ACR_NAME
for image in api web indexer; do
docker tag bootnode/$image:latest $ACR_NAME.azurecr.io/bootnode/$image:latest
docker push $ACR_NAME.azurecr.io/bootnode/$image:latest
done
# Deploy to Kubernetes
deploy_kubernetes
}
# Deploy to DigitalOcean DOKS
deploy_digitalocean() {
log_info "Deploying to DigitalOcean Kubernetes..."
# Create cluster if it doesn't exist
if ! doctl kubernetes cluster get bootnode-cluster 2>/dev/null; then
log_info "Creating DOKS cluster..."
doctl kubernetes cluster create bootnode-cluster \
--region nyc1 \
--version 1.29.1-do.0 \
--node-pool "name=api-pool;size=s-4vcpu-8gb;count=3;auto-scale=true;min-nodes=2;max-nodes=10"
fi
# Get credentials
doctl kubernetes cluster kubeconfig save bootnode-cluster
# Push images to DO Container Registry
log_info "Pushing images to DO Container Registry..."
doctl registry login
REGISTRY=$(doctl registry get --format RegistryName --no-header)
for image in api web indexer; do
docker tag bootnode/$image:latest registry.digitalocean.com/$REGISTRY/bootnode-$image:latest
docker push registry.digitalocean.com/$REGISTRY/bootnode-$image:latest
done
# Deploy to Kubernetes
deploy_kubernetes
}
# Deploy Kubernetes resources
deploy_kubernetes() {
log_info "Deploying Kubernetes resources..."
K8S_DIR="$ROOT_DIR/infra/k8s"
# Create namespace
kubectl apply -f "$K8S_DIR/namespace.yaml"
# Create secrets (you should use external secrets manager in production)
kubectl apply -f "$K8S_DIR/secrets.yaml"
# Deploy services
kubectl apply -f "$K8S_DIR/api-deployment.yaml"
kubectl apply -f "$K8S_DIR/indexer-deployment.yaml"
kubectl apply -f "$K8S_DIR/ingress.yaml"
# Wait for deployments
log_info "Waiting for deployments to be ready..."
kubectl rollout status deployment/bootnode-api -n bootnode --timeout=300s
kubectl rollout status deployment/bootnode-indexer -n bootnode --timeout=300s
log_info "Deployment complete!"
kubectl get pods -n bootnode
kubectl get svc -n bootnode
}
# Main execution
check_prerequisites
# Build images for cloud deployments
if [ "$PROVIDER" != "docker" ]; then
build_images
fi
# Deploy based on provider
case $PROVIDER in
docker)
deploy_docker
;;
aws)
deploy_aws
;;
gcp)
deploy_gcp
;;
azure)
deploy_azure
;;
digitalocean)
deploy_digitalocean
;;
*)
log_error "Unknown provider: $PROVIDER"
echo "Supported providers: docker, aws, gcp, azure, digitalocean"
exit 1
;;
esac
@@ -0,0 +1,64 @@
# DigitalOcean Kubernetes (DOKS) Cluster Configuration
# Deploy with: doctl kubernetes cluster create --config doks-cluster.yaml
name: bootnode-cluster
region: nyc1
version: 1.29.1-do.0
auto_upgrade: true
surge_upgrade: true
ha: true
vpc_uuid: ${VPC_UUID} # Create VPC first
node_pools:
# System/API Node Pool
- name: api-pool
size: s-4vcpu-8gb
count: 3
auto_scale: true
min_nodes: 2
max_nodes: 10
labels:
role: api
nodegroup-type: api
tags:
- bootnode
- api
# Indexer Node Pool (Memory Optimized)
- name: indexer-pool
size: m-8vcpu-64gb
count: 2
auto_scale: true
min_nodes: 1
max_nodes: 5
labels:
role: indexer
nodegroup-type: indexer
taints:
- key: dedicated
value: indexer
effect: NoSchedule
tags:
- bootnode
- indexer
# Database Node Pool (Storage Optimized)
- name: db-pool
size: so1_5-8vcpu-64gb # Storage optimized
count: 3
auto_scale: false
labels:
role: database
nodegroup-type: database
taints:
- key: dedicated
value: database
effect: NoSchedule
tags:
- bootnode
- database
maintenance_policy:
day: sunday
start_time: "04:00"
+161
View File
@@ -0,0 +1,161 @@
# GCP GKE Cluster Configuration
# Deploy with: gcloud container clusters create --config gke-cluster.yaml
# Or use Terraform with google_container_cluster resource
apiVersion: container.cnrm.cloud.google.com/v1beta1
kind: ContainerCluster
metadata:
name: bootnode-cluster
namespace: bootnode-infra
spec:
location: us-central1
releaseChannel:
channel: REGULAR
# Network config
networkRef:
name: bootnode-vpc
subnetworkRef:
name: bootnode-subnet
ipAllocationPolicy:
clusterSecondaryRangeName: pods
servicesSecondaryRangeName: services
# Cluster features
workloadIdentityConfig:
workloadPool: ${PROJECT_ID}.svc.id.goog
addonsConfig:
horizontalPodAutoscaling:
disabled: false
httpLoadBalancing:
disabled: false
gcePersistentDiskCsiDriverConfig:
enabled: true
# Logging and monitoring
loggingConfig:
enableComponents:
- SYSTEM_COMPONENTS
- WORKLOADS
monitoringConfig:
enableComponents:
- SYSTEM_COMPONENTS
managedPrometheus:
enabled: true
# Security
masterAuth:
clientCertificateConfig:
issueClientCertificate: false
privateClusterConfig:
enablePrivateNodes: true
enablePrivateEndpoint: false
masterIpv4CidrBlock: 172.16.0.0/28
---
# API Node Pool
apiVersion: container.cnrm.cloud.google.com/v1beta1
kind: ContainerNodePool
metadata:
name: bootnode-api-pool
namespace: bootnode-infra
spec:
clusterRef:
name: bootnode-cluster
location: us-central1
initialNodeCount: 1
autoscaling:
minNodeCount: 2
maxNodeCount: 10
nodeConfig:
machineType: e2-standard-4
diskSizeGb: 100
diskType: pd-ssd
imageType: COS_CONTAINERD
labels:
role: api
nodegroup-type: api
oauthScopes:
- https://www.googleapis.com/auth/cloud-platform
workloadMetadataConfig:
mode: GKE_METADATA
management:
autoRepair: true
autoUpgrade: true
---
# Indexer Node Pool
apiVersion: container.cnrm.cloud.google.com/v1beta1
kind: ContainerNodePool
metadata:
name: bootnode-indexer-pool
namespace: bootnode-infra
spec:
clusterRef:
name: bootnode-cluster
location: us-central1
initialNodeCount: 1
autoscaling:
minNodeCount: 1
maxNodeCount: 5
nodeConfig:
machineType: n2-highmem-8
diskSizeGb: 500
diskType: pd-ssd
imageType: COS_CONTAINERD
labels:
role: indexer
nodegroup-type: indexer
taints:
- key: dedicated
value: indexer
effect: NO_SCHEDULE
oauthScopes:
- https://www.googleapis.com/auth/cloud-platform
management:
autoRepair: true
autoUpgrade: true
---
# Database Node Pool
apiVersion: container.cnrm.cloud.google.com/v1beta1
kind: ContainerNodePool
metadata:
name: bootnode-db-pool
namespace: bootnode-infra
spec:
clusterRef:
name: bootnode-cluster
location: us-central1
initialNodeCount: 3
autoscaling:
minNodeCount: 3
maxNodeCount: 5
nodeConfig:
machineType: n2-standard-8
diskSizeGb: 200
diskType: pd-ssd
localSsdCount: 2
imageType: COS_CONTAINERD
labels:
role: database
nodegroup-type: database
taints:
- key: dedicated
value: database
effect: NO_SCHEDULE
oauthScopes:
- https://www.googleapis.com/auth/cloud-platform
management:
autoRepair: true
autoUpgrade: true
+712
View File
@@ -0,0 +1,712 @@
# Bootnode Multi-Chain Configuration
# Supports 100+ chains - Enable/disable as needed
# =============================================================================
# ETHEREUM & EVM LAYER 1s
# =============================================================================
ethereum:
mainnet:
chain_id: 1
name: "Ethereum Mainnet"
type: evm
rpc: "${ETH_MAINNET_RPC:-https://eth.llamarpc.com}"
ws: "${ETH_MAINNET_WS:-wss://eth.llamarpc.com}"
enabled: true
features:
- mev_protection
- archive_nodes
- debug_apis
- beacon_api
protocols:
- seaport
- uniswap_v2
- uniswap_v3
- aave_v3
- compound_v3
- lido
sepolia:
chain_id: 11155111
name: "Sepolia Testnet"
type: evm
rpc: "${ETH_SEPOLIA_RPC:-https://ethereum-sepolia-rpc.publicnode.com}"
enabled: true
testnet: true
holesky:
chain_id: 17000
name: "Holesky Testnet"
type: evm
rpc: "${ETH_HOLESKY_RPC:-https://ethereum-holesky-rpc.publicnode.com}"
enabled: true
testnet: true
bnb:
mainnet:
chain_id: 56
name: "BNB Smart Chain"
type: evm
rpc: "${BNB_MAINNET_RPC:-https://bsc-dataseed.binance.org}"
enabled: true
protocols:
- pancakeswap_v2
- pancakeswap_v3
- venus
testnet:
chain_id: 97
name: "BNB Testnet"
type: evm
rpc: "${BNB_TESTNET_RPC:-https://data-seed-prebsc-1-s1.binance.org:8545}"
enabled: true
testnet: true
avalanche:
mainnet:
chain_id: 43114
name: "Avalanche C-Chain"
type: evm
rpc: "${AVAX_MAINNET_RPC:-https://api.avax.network/ext/bc/C/rpc}"
enabled: true
protocols:
- trader_joe
- pangolin
- benqi
fuji:
chain_id: 43113
name: "Avalanche Fuji"
type: evm
rpc: "${AVAX_FUJI_RPC:-https://api.avax-test.network/ext/bc/C/rpc}"
enabled: true
testnet: true
gnosis:
mainnet:
chain_id: 100
name: "Gnosis Chain"
type: evm
rpc: "${GNOSIS_MAINNET_RPC:-https://rpc.gnosischain.com}"
enabled: true
chiado:
chain_id: 10200
name: "Gnosis Chiado"
type: evm
rpc: "${GNOSIS_CHIADO_RPC:-https://rpc.chiadochain.net}"
enabled: true
testnet: true
celo:
mainnet:
chain_id: 42220
name: "Celo"
type: evm
rpc: "${CELO_MAINNET_RPC:-https://forno.celo.org}"
enabled: true
alfajores:
chain_id: 44787
name: "Celo Alfajores"
type: evm
rpc: "${CELO_ALFAJORES_RPC:-https://alfajores-forno.celo-testnet.org}"
enabled: true
testnet: true
rootstock:
mainnet:
chain_id: 30
name: "Rootstock"
type: evm
rpc: "${RSK_MAINNET_RPC:-https://public-node.rsk.co}"
enabled: true
testnet:
chain_id: 31
name: "Rootstock Testnet"
type: evm
rpc: "${RSK_TESTNET_RPC:-https://public-node.testnet.rsk.co}"
enabled: true
testnet: true
# =============================================================================
# LAYER 2 - OPTIMISTIC ROLLUPS
# =============================================================================
arbitrum:
mainnet:
chain_id: 42161
name: "Arbitrum One"
type: evm
rpc: "${ARB_MAINNET_RPC:-https://arb1.arbitrum.io/rpc}"
enabled: true
protocols:
- uniswap_v3
- gmx
- camelot
sepolia:
chain_id: 421614
name: "Arbitrum Sepolia"
type: evm
rpc: "${ARB_SEPOLIA_RPC:-https://sepolia-rollup.arbitrum.io/rpc}"
enabled: true
testnet: true
arbitrum_nova:
mainnet:
chain_id: 42170
name: "Arbitrum Nova"
type: evm
rpc: "${ARB_NOVA_RPC:-https://nova.arbitrum.io/rpc}"
enabled: true
optimism:
mainnet:
chain_id: 10
name: "OP Mainnet"
type: evm
rpc: "${OP_MAINNET_RPC:-https://mainnet.optimism.io}"
enabled: true
features:
- flashblocks
protocols:
- uniswap_v3
- velodrome
- synthetix
sepolia:
chain_id: 11155420
name: "OP Sepolia"
type: evm
rpc: "${OP_SEPOLIA_RPC:-https://sepolia.optimism.io}"
enabled: true
testnet: true
base:
mainnet:
chain_id: 8453
name: "Base"
type: evm
rpc: "${BASE_MAINNET_RPC:-https://mainnet.base.org}"
enabled: true
features:
- flashblocks
protocols:
- uniswap_v3
- aerodrome
sepolia:
chain_id: 84532
name: "Base Sepolia"
type: evm
rpc: "${BASE_SEPOLIA_RPC:-https://sepolia.base.org}"
enabled: true
testnet: true
blast:
mainnet:
chain_id: 81457
name: "Blast"
type: evm
rpc: "${BLAST_MAINNET_RPC:-https://rpc.blast.io}"
enabled: true
protocols:
- thruster
sepolia:
chain_id: 168587773
name: "Blast Sepolia"
type: evm
rpc: "${BLAST_SEPOLIA_RPC:-https://sepolia.blast.io}"
enabled: true
testnet: true
mantle:
mainnet:
chain_id: 5000
name: "Mantle"
type: evm
rpc: "${MANTLE_MAINNET_RPC:-https://rpc.mantle.xyz}"
enabled: true
sepolia:
chain_id: 5003
name: "Mantle Sepolia"
type: evm
rpc: "${MANTLE_SEPOLIA_RPC:-https://rpc.sepolia.mantle.xyz}"
enabled: true
testnet: true
metis:
mainnet:
chain_id: 1088
name: "Metis Andromeda"
type: evm
rpc: "${METIS_MAINNET_RPC:-https://andromeda.metis.io}"
enabled: true
mode:
mainnet:
chain_id: 34443
name: "Mode"
type: evm
rpc: "${MODE_MAINNET_RPC:-https://mainnet.mode.network}"
enabled: true
zora:
mainnet:
chain_id: 7777777
name: "Zora"
type: evm
rpc: "${ZORA_MAINNET_RPC:-https://rpc.zora.energy}"
enabled: true
# =============================================================================
# LAYER 2 - ZK ROLLUPS
# =============================================================================
polygon_zkevm:
mainnet:
chain_id: 1101
name: "Polygon zkEVM"
type: evm
rpc: "${POLYGON_ZKEVM_RPC:-https://zkevm-rpc.com}"
enabled: true
cardona:
chain_id: 2442
name: "Polygon zkEVM Cardona"
type: evm
rpc: "${POLYGON_ZKEVM_CARDONA_RPC:-https://rpc.cardona.zkevm-rpc.com}"
enabled: true
testnet: true
zksync:
mainnet:
chain_id: 324
name: "zkSync Era"
type: evm
rpc: "${ZKSYNC_MAINNET_RPC:-https://mainnet.era.zksync.io}"
enabled: true
protocols:
- syncswap
sepolia:
chain_id: 300
name: "zkSync Sepolia"
type: evm
rpc: "${ZKSYNC_SEPOLIA_RPC:-https://sepolia.era.zksync.dev}"
enabled: true
testnet: true
scroll:
mainnet:
chain_id: 534352
name: "Scroll"
type: evm
rpc: "${SCROLL_MAINNET_RPC:-https://rpc.scroll.io}"
enabled: true
sepolia:
chain_id: 534351
name: "Scroll Sepolia"
type: evm
rpc: "${SCROLL_SEPOLIA_RPC:-https://sepolia-rpc.scroll.io}"
enabled: true
testnet: true
linea:
mainnet:
chain_id: 59144
name: "Linea"
type: evm
rpc: "${LINEA_MAINNET_RPC:-https://rpc.linea.build}"
enabled: true
sepolia:
chain_id: 59141
name: "Linea Sepolia"
type: evm
rpc: "${LINEA_SEPOLIA_RPC:-https://rpc.sepolia.linea.build}"
enabled: true
testnet: true
starknet:
mainnet:
chain_id: 0
name: "Starknet"
type: starknet
rpc: "${STARKNET_MAINNET_RPC:-https://starknet-mainnet.public.blastapi.io}"
enabled: true
sepolia:
chain_id: 0
name: "Starknet Sepolia"
type: starknet
rpc: "${STARKNET_SEPOLIA_RPC:-https://starknet-sepolia.public.blastapi.io}"
enabled: true
testnet: true
# =============================================================================
# LAYER 2 - OTHER
# =============================================================================
polygon:
mainnet:
chain_id: 137
name: "Polygon PoS"
type: evm
rpc: "${POLYGON_MAINNET_RPC:-https://polygon-rpc.com}"
enabled: true
protocols:
- quickswap
- uniswap_v3
- aave_v3
amoy:
chain_id: 80002
name: "Polygon Amoy"
type: evm
rpc: "${POLYGON_AMOY_RPC:-https://rpc-amoy.polygon.technology}"
enabled: true
testnet: true
opbnb:
mainnet:
chain_id: 204
name: "opBNB"
type: evm
rpc: "${OPBNB_MAINNET_RPC:-https://opbnb-mainnet-rpc.bnbchain.org}"
enabled: true
testnet:
chain_id: 5611
name: "opBNB Testnet"
type: evm
rpc: "${OPBNB_TESTNET_RPC:-https://opbnb-testnet-rpc.bnbchain.org}"
enabled: true
testnet: true
# =============================================================================
# ALTERNATIVE LAYER 1s
# =============================================================================
solana:
mainnet:
chain_id: 0
name: "Solana"
type: solana
rpc: "${SOL_MAINNET_RPC:-https://api.mainnet-beta.solana.com}"
enabled: true
protocols:
- raydium
- orca
- marinade
- metaplex
features:
- das_api
devnet:
chain_id: 0
name: "Solana Devnet"
type: solana
rpc: "${SOL_DEVNET_RPC:-https://api.devnet.solana.com}"
enabled: true
testnet: true
aptos:
mainnet:
chain_id: 0
name: "Aptos"
type: move
rpc: "${APTOS_MAINNET_RPC:-https://fullnode.mainnet.aptoslabs.com/v1}"
enabled: true
testnet:
chain_id: 0
name: "Aptos Testnet"
type: move
rpc: "${APTOS_TESTNET_RPC:-https://fullnode.testnet.aptoslabs.com/v1}"
enabled: true
testnet: true
flow:
mainnet:
chain_id: 0
name: "Flow"
type: flow
rpc: "${FLOW_MAINNET_RPC:-https://rest-mainnet.onflow.org}"
enabled: true
testnet:
chain_id: 0
name: "Flow Testnet"
type: flow
rpc: "${FLOW_TESTNET_RPC:-https://rest-testnet.onflow.org}"
enabled: true
testnet: true
berachain:
mainnet:
chain_id: 80094
name: "Berachain"
type: evm
rpc: "${BERA_MAINNET_RPC:-https://rpc.berachain.com}"
enabled: true
bartio:
chain_id: 80084
name: "Berachain Bartio"
type: evm
rpc: "${BERA_BARTIO_RPC:-https://bartio.rpc.berachain.com}"
enabled: true
testnet: true
sei:
mainnet:
chain_id: 1329
name: "Sei"
type: evm
rpc: "${SEI_MAINNET_RPC:-https://evm-rpc.sei-apis.com}"
enabled: true
atlantic:
chain_id: 1328
name: "Sei Atlantic"
type: evm
rpc: "${SEI_ATLANTIC_RPC:-https://evm-rpc-testnet.sei-apis.com}"
enabled: true
testnet: true
sonic:
mainnet:
chain_id: 146
name: "Sonic"
type: evm
rpc: "${SONIC_MAINNET_RPC:-https://rpc.soniclabs.com}"
enabled: true
testnet:
chain_id: 57054
name: "Sonic Testnet"
type: evm
rpc: "${SONIC_TESTNET_RPC:-https://rpc.testnet.soniclabs.com}"
enabled: true
testnet: true
monad:
devnet:
chain_id: 0
name: "Monad Devnet"
type: evm
rpc: "${MONAD_DEVNET_RPC:-}"
enabled: false
testnet: true
tron:
mainnet:
chain_id: 0
name: "Tron"
type: tron
rpc: "${TRON_MAINNET_RPC:-https://api.trongrid.io}"
enabled: true
shasta:
chain_id: 0
name: "Tron Shasta"
type: tron
rpc: "${TRON_SHASTA_RPC:-https://api.shasta.trongrid.io}"
enabled: true
testnet: true
# =============================================================================
# BITCOIN
# =============================================================================
bitcoin:
mainnet:
chain_id: 0
name: "Bitcoin"
type: bitcoin
rpc: "${BTC_MAINNET_RPC:-}"
enabled: true
protocols:
- ordinals
- runes
- brc20
testnet:
chain_id: 0
name: "Bitcoin Testnet"
type: bitcoin
rpc: "${BTC_TESTNET_RPC:-}"
enabled: true
testnet: true
# =============================================================================
# EMERGING NETWORKS
# =============================================================================
astar:
mainnet:
chain_id: 592
name: "Astar"
type: evm
rpc: "${ASTAR_MAINNET_RPC:-https://evm.astar.network}"
enabled: true
zetachain:
mainnet:
chain_id: 7000
name: "ZetaChain"
type: evm
rpc: "${ZETA_MAINNET_RPC:-https://zetachain-mainnet-archive.allthatnode.com}"
enabled: true
ink:
mainnet:
chain_id: 57073
name: "Ink"
type: evm
rpc: "${INK_MAINNET_RPC:-https://rpc-gel.inkonchain.com}"
enabled: true
unichain:
mainnet:
chain_id: 130
name: "Unichain"
type: evm
rpc: "${UNICHAIN_MAINNET_RPC:-https://rpc.unichain.org}"
enabled: true
features:
- flashblocks
world_chain:
mainnet:
chain_id: 480
name: "World Chain"
type: evm
rpc: "${WORLDCHAIN_MAINNET_RPC:-https://worldchain-mainnet.g.alchemy.com/public}"
enabled: true
abstract:
mainnet:
chain_id: 2741
name: "Abstract"
type: evm
rpc: "${ABSTRACT_MAINNET_RPC:-https://api.mainnet.abs.xyz}"
enabled: true
soneium:
mainnet:
chain_id: 1868
name: "Soneium"
type: evm
rpc: "${SONEIUM_MAINNET_RPC:-https://rpc.soneium.org}"
enabled: true
crossfi:
mainnet:
chain_id: 4158
name: "CrossFi"
type: evm
rpc: "${CROSSFI_MAINNET_RPC:-https://rpc.mainnet.ms}"
enabled: true
apechain:
mainnet:
chain_id: 33139
name: "ApeChain"
type: evm
rpc: "${APECHAIN_MAINNET_RPC:-https://rpc.apechain.com}"
enabled: true
lens:
mainnet:
chain_id: 37111
name: "Lens"
type: evm
rpc: "${LENS_MAINNET_RPC:-https://rpc.lens.dev}"
enabled: true
lumia:
mainnet:
chain_id: 994873017
name: "Lumia"
type: evm
rpc: "${LUMIA_MAINNET_RPC:-https://rpc.lumia.org}"
enabled: true
hyperliquid:
mainnet:
chain_id: 0
name: "Hyperliquid"
type: evm
rpc: "${HYPERLIQUID_RPC:-https://api.hyperliquid.xyz/evm}"
enabled: true
ronin:
mainnet:
chain_id: 2020
name: "Ronin"
type: evm
rpc: "${RONIN_MAINNET_RPC:-https://api.roninchain.com/rpc}"
enabled: true
moonbeam:
mainnet:
chain_id: 1284
name: "Moonbeam"
type: evm
rpc: "${MOONBEAM_MAINNET_RPC:-https://rpc.api.moonbeam.network}"
enabled: true
bob:
mainnet:
chain_id: 60808
name: "BOB"
type: evm
rpc: "${BOB_MAINNET_RPC:-https://rpc.gobob.xyz}"
enabled: true
frax:
mainnet:
chain_id: 252
name: "Frax"
type: evm
rpc: "${FRAX_MAINNET_RPC:-https://rpc.frax.com}"
enabled: true
degen:
mainnet:
chain_id: 666666666
name: "Degen"
type: evm
rpc: "${DEGEN_MAINNET_RPC:-https://rpc.degen.tips}"
enabled: true
megaeth:
testnet:
chain_id: 0
name: "MegaETH Testnet"
type: evm
rpc: "${MEGAETH_TESTNET_RPC:-}"
enabled: false
testnet: true
# =============================================================================
# LUX NETWORK (Native Support)
# =============================================================================
lux:
cchain:
chain_id: 96369
name: "Lux C-Chain"
type: evm
rpc: "${LUX_MAINNET_RPC:-https://api.lux.network/ext/bc/C/rpc}"
enabled: true
features:
- native
- archive_nodes
protocols:
- luxfi_dex
pchain:
chain_id: 0
name: "Lux P-Chain"
type: lux_platform
rpc: "${LUX_PCHAIN_RPC:-https://api.lux.network/ext/bc/P}"
enabled: true
features:
- staking
- validators
xchain:
chain_id: 0
name: "Lux X-Chain"
type: lux_dag
rpc: "${LUX_XCHAIN_RPC:-https://api.lux.network/ext/bc/X}"
enabled: true
features:
- utxo
- assets
testnet:
chain_id: 96370
name: "Lux Testnet"
type: evm
rpc: "${LUX_TESTNET_RPC:-https://api.lux-test.network/ext/bc/C/rpc}"
enabled: true
testnet: true
+143
View File
@@ -0,0 +1,143 @@
name: hanzo-apps
region: nyc
features:
- buildpack-stack=ubuntu-22
# Managed Databases (shared by all apps)
databases:
- engine: PG
name: postgres
num_nodes: 1
size: db-s-1vcpu-1gb
version: "16"
- engine: REDIS
name: redis
num_nodes: 1
size: db-s-1vcpu-1gb
version: "7"
# Domains
domains:
- domain: console.hanzo.ai
type: PRIMARY
- domain: chat.hanzo.ai
type: ALIAS
- domain: commerce.hanzo.ai
type: ALIAS
services:
# ==========================================================================
# Console (LLM Development Environment)
# ==========================================================================
- name: console
image:
registry_type: GHCR
registry: hanzoai
repository: console
tag: latest
instance_count: 1
instance_size_slug: professional-xs
http_port: 3000
routes:
- path: /
preserve_path_prefix: true
envs:
- key: NODE_ENV
value: production
- key: DATABASE_URL
scope: RUN_TIME
value: ${postgres.DATABASE_URL}
- key: REDIS_HOST
scope: RUN_TIME
value: ${redis.HOSTNAME}
- key: REDIS_PORT
value: "25061"
- key: REDIS_AUTH
scope: RUN_TIME
value: ${redis.PASSWORD}
- key: NEXTAUTH_URL
value: https://console.hanzo.ai
- key: NEXTAUTH_SECRET
scope: RUN_TIME
type: SECRET
value: e21ae21c91df0875b5dc54ba98c3a2731a69c4284f0113ab93f1cb2919128abc
- key: SALT
value: "295f20f7c8375c04bc2c8383106f1cf855e5f9a7755144bd23e8c9530185c89b"
- key: ENCRYPTION_KEY
value: "295f20f7c8375c04bc2c8383106f1cf855e5f9a7755144bd23e8c9530185c89b"
- key: HOSTNAME
value: "0.0.0.0"
- key: PORT
value: "3000"
- key: HANZO_AUTO_CLICKHOUSE_MIGRATION_DISABLED
value: "true"
- key: NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
value: US
health_check:
http_path: /api/public/health
initial_delay_seconds: 90
period_seconds: 30
timeout_seconds: 10
failure_threshold: 5
# ==========================================================================
# Chat (AI Chat Interface)
# ==========================================================================
- name: chat
image:
registry_type: GHCR
registry: hanzoai
repository: chat
tag: latest
instance_count: 1
instance_size_slug: professional-xs
http_port: 3000
routes:
- path: /
preserve_path_prefix: true
envs:
- key: NODE_ENV
value: production
- key: DATABASE_URL
scope: RUN_TIME
value: ${postgres.DATABASE_URL}
- key: REDIS_URL
scope: RUN_TIME
value: rediss://${redis.USERNAME}:${redis.PASSWORD}@${redis.HOSTNAME}:${redis.PORT}
- key: NEXTAUTH_URL
value: https://chat.hanzo.ai
- key: NEXTAUTH_SECRET
scope: RUN_TIME
type: SECRET
value: e21ae21c91df0875b5dc54ba98c3a2731a69c4284f0113ab93f1cb2919128abc
health_check:
http_path: /
initial_delay_seconds: 60
period_seconds: 15
# ==========================================================================
# Commerce (Multi-tenant Commerce API)
# ==========================================================================
- name: commerce
image:
registry_type: GHCR
registry: hanzoai
repository: commerce
tag: latest
instance_count: 1
instance_size_slug: professional-xs
http_port: 8000
routes:
- path: /
preserve_path_prefix: true
envs:
- key: DATABASE_URL
scope: RUN_TIME
value: ${postgres.DATABASE_URL}
- key: REDIS_URL
scope: RUN_TIME
value: rediss://${redis.USERNAME}:${redis.PASSWORD}@${redis.HOSTNAME}:${redis.PORT}
health_check:
http_path: /health
initial_delay_seconds: 30
period_seconds: 10
@@ -0,0 +1,190 @@
version: '3.8'
networks:
hanzo-network:
driver: overlay
attachable: true
platform-network:
external: true
volumes:
postgres_data:
redis_data:
traefik_certs:
services:
traefik:
image: traefik:v2.11
deploy:
mode: global
placement:
constraints:
- node.role == manager
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
delay: 5s
command:
- --api.dashboard=true
- --api.insecure=true
- --providers.docker=true
- --providers.docker.swarmMode=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=hanzo_hanzo-network
- --providers.file.directory=/etc/platform/traefik/dynamic
- --providers.file.watch=true
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --[email protected]
- --certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_certs:/etc/traefik
- /etc/platform/traefik:/etc/platform/traefik:ro
networks:
- hanzo-network
- platform-network
postgres:
image: postgres:16-alpine
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- node.role == manager
restart_policy:
condition: on-failure
environment:
POSTGRES_USER: hanzo
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
POSTGRES_DB: platform
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U hanzo"]
interval: 5s
timeout: 5s
retries: 5
networks:
- hanzo-network
redis:
image: redis:7-alpine
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- node.role == manager
restart_policy:
condition: on-failure
command: redis-server --requirepass ${REDIS_PASSWORD:-changeme} --appendonly yes
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-changeme}", "ping"]
interval: 5s
timeout: 5s
retries: 5
networks:
- hanzo-network
# ==========================================================================
# Platform - Production (Scalable)
# ==========================================================================
platform:
image: hanzoai/platform:latest
deploy:
mode: replicated
replicas: 2
update_config:
parallelism: 1
delay: 10s
order: start-first
failure_action: rollback
rollback_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
labels:
- "traefik.enable=true"
- "traefik.http.routers.platform.rule=Host(`platform.hanzo.ai`)"
- "traefik.http.routers.platform.tls.certresolver=letsencrypt"
- "traefik.http.services.platform.loadbalancer.server.port=3000"
environment:
NODE_ENV: production
DATABASE_URL: postgresql://hanzo:${POSTGRES_PASSWORD:-changeme}@postgres:5432/platform
REDIS_URL: redis://:${REDIS_PASSWORD:-changeme}@redis:6379
REDIS_HOST: redis
REDIS_PORT: "6379"
REDIS_PASSWORD: ${REDIS_PASSWORD:-changeme}
NEXTAUTH_URL: https://platform.hanzo.ai
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-changeme}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /etc/platform:/etc/platform
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 90s
networks:
- hanzo-network
# ==========================================================================
# Platform - Staging
# ==========================================================================
platform-staging:
image: hanzoai/platform:latest
deploy:
mode: replicated
replicas: 1
update_config:
parallelism: 1
delay: 10s
order: start-first
failure_action: rollback
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
labels:
- "traefik.enable=true"
- "traefik.http.routers.platform-staging.rule=Host(`staging.platform.hanzo.ai`)"
- "traefik.http.routers.platform-staging.tls.certresolver=letsencrypt"
- "traefik.http.services.platform-staging.loadbalancer.server.port=3000"
environment:
NODE_ENV: production
DATABASE_URL: postgresql://hanzo:${POSTGRES_PASSWORD:-changeme}@postgres:5432/platform_staging
REDIS_URL: redis://:${REDIS_PASSWORD:-changeme}@redis:6379/1
REDIS_HOST: redis
REDIS_PORT: "6379"
REDIS_PASSWORD: ${REDIS_PASSWORD:-changeme}
REDIS_DB: "1"
NEXTAUTH_URL: https://staging.platform.hanzo.ai
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-changeme}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /etc/platform:/etc/platform
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 90s
networks:
- hanzo-network
+286
View File
@@ -0,0 +1,286 @@
name: hanzo-platform
region: nyc
features:
- buildpack-stack=ubuntu-22
# Managed Databases
databases:
- engine: PG
name: postgres
num_nodes: 1
size: db-s-1vcpu-1gb
version: "16"
- engine: REDIS
name: redis
num_nodes: 1
size: db-s-1vcpu-1gb
version: "7"
# Domain routing
domains:
- domain: platform.hanzo.ai
type: PRIMARY
- domain: console.hanzo.ai
type: ALIAS
- domain: iam.hanzo.ai
type: ALIAS
- domain: analytics.hanzo.ai
type: ALIAS
- domain: chat.hanzo.ai
type: ALIAS
- domain: api.hanzo.ai
type: ALIAS
- domain: commerce.hanzo.ai
type: ALIAS
ingress:
rules:
- component:
name: platform
match:
path:
prefix: /
cors:
allow_origins:
- prefix: https://
- component:
name: console
match:
path:
prefix: /
- component:
name: iam
match:
path:
prefix: /
- component:
name: analytics
match:
path:
prefix: /
- component:
name: chat
match:
path:
prefix: /
- component:
name: router
match:
path:
prefix: /
- component:
name: commerce
match:
path:
prefix: /
services:
# ==========================================================================
# Platform (PaaS UI)
# ==========================================================================
- name: platform
image:
registry_type: GHCR
registry: hanzoai
repository: platform
tag: latest
instance_count: 1
instance_size_slug: professional-xs
http_port: 3000
envs:
- key: NODE_ENV
value: production
- key: DATABASE_URL
scope: RUN_TIME
value: ${postgres.DATABASE_URL}
- key: REDIS_URL
scope: RUN_TIME
value: ${redis.DATABASE_URL}
- key: NEXTAUTH_URL
value: https://platform.hanzo.ai
- key: NEXTAUTH_SECRET
scope: RUN_TIME
type: SECRET
value: EV[1:changeme]
health_check:
http_path: /
initial_delay_seconds: 30
period_seconds: 10
# ==========================================================================
# Console (LLM Development Environment)
# ==========================================================================
- name: console
image:
registry_type: GHCR
registry: hanzoai
repository: console
tag: latest
instance_count: 1
instance_size_slug: professional-xs
http_port: 3000
envs:
- key: NODE_ENV
value: production
- key: DATABASE_URL
scope: RUN_TIME
value: ${postgres.DATABASE_URL}?schema=console
- key: REDIS_HOST
scope: RUN_TIME
value: ${redis.HOSTNAME}
- key: REDIS_PORT
scope: RUN_TIME
value: ${redis.PORT}
- key: REDIS_AUTH
scope: RUN_TIME
value: ${redis.PASSWORD}
- key: NEXTAUTH_URL
value: https://console.hanzo.ai
- key: NEXTAUTH_SECRET
scope: RUN_TIME
type: SECRET
value: EV[1:changeme]
- key: HOSTNAME
value: "0.0.0.0"
health_check:
http_path: /api/public/health
initial_delay_seconds: 60
period_seconds: 30
# ==========================================================================
# IAM (Identity & Access Management)
# ==========================================================================
- name: iam
image:
registry_type: GHCR
registry: hanzoai
repository: iam
tag: latest
instance_count: 1
instance_size_slug: professional-xs
http_port: 8000
envs:
- key: runmode
value: prod
- key: driverName
value: postgres
- key: dataSourceName
scope: RUN_TIME
value: ${postgres.DATABASE_URL}
- key: redisEndpoint
scope: RUN_TIME
value: ${redis.HOSTNAME}:${redis.PORT}
- key: appname
value: Hanzo IAM
health_check:
http_path: /api/health
initial_delay_seconds: 30
period_seconds: 10
# ==========================================================================
# Analytics (PostHog-based)
# ==========================================================================
- name: analytics
image:
registry_type: GHCR
registry: hanzoai
repository: analytics
tag: latest
instance_count: 1
instance_size_slug: professional-xs
http_port: 8000
envs:
- key: DATABASE_URL
scope: RUN_TIME
value: ${postgres.DATABASE_URL}?schema=analytics
- key: REDIS_URL
scope: RUN_TIME
value: ${redis.DATABASE_URL}
- key: SECRET_KEY
scope: RUN_TIME
type: SECRET
value: EV[1:changeme]
- key: SITE_URL
value: https://analytics.hanzo.ai
health_check:
http_path: /_health
initial_delay_seconds: 60
period_seconds: 30
# ==========================================================================
# Chat (AI Chat Interface)
# ==========================================================================
- name: chat
image:
registry_type: GHCR
registry: hanzoai
repository: chat
tag: latest
instance_count: 1
instance_size_slug: professional-xs
http_port: 3000
envs:
- key: NODE_ENV
value: production
- key: DATABASE_URL
scope: RUN_TIME
value: ${postgres.DATABASE_URL}?schema=chat
- key: REDIS_URL
scope: RUN_TIME
value: ${redis.DATABASE_URL}
health_check:
http_path: /
initial_delay_seconds: 30
period_seconds: 10
# ==========================================================================
# Router (LiteLLM API Gateway)
# ==========================================================================
- name: router
image:
registry_type: GHCR
registry: hanzoai
repository: router
tag: latest
instance_count: 1
instance_size_slug: professional-xs
http_port: 4000
envs:
- key: DATABASE_URL
scope: RUN_TIME
value: ${postgres.DATABASE_URL}?schema=router
- key: REDIS_URL
scope: RUN_TIME
value: ${redis.DATABASE_URL}
- key: LITELLM_MASTER_KEY
scope: RUN_TIME
type: SECRET
value: EV[1:changeme]
health_check:
http_path: /health
initial_delay_seconds: 30
period_seconds: 10
# ==========================================================================
# Commerce (Multi-tenant Commerce API)
# ==========================================================================
- name: commerce
image:
registry_type: GHCR
registry: hanzoai
repository: commerce
tag: latest
instance_count: 1
instance_size_slug: professional-xs
http_port: 8000
envs:
- key: DATABASE_URL
scope: RUN_TIME
value: ${postgres.DATABASE_URL}?schema=commerce
- key: REDIS_URL
scope: RUN_TIME
value: ${redis.DATABASE_URL}
health_check:
http_path: /health
initial_delay_seconds: 30
period_seconds: 10
+44
View File
@@ -0,0 +1,44 @@
name: hanzo-platform
region: nyc
features:
- buildpack-stack=ubuntu-22
databases:
- engine: PG
name: db
num_nodes: 1
size: db-s-1vcpu-2gb
version: "16"
domains:
- domain: platform.hanzo.ai
type: PRIMARY
services:
- name: platform
image:
registry_type: DOCKER_HUB
registry: hanzoai
repository: platform
tag: latest
instance_count: 2
instance_size_slug: professional-xs
http_port: 3000
envs:
- key: NODE_ENV
value: production
- key: DATABASE_URL
scope: RUN_TIME
value: ${db.DATABASE_URL}
- key: NEXTAUTH_URL
value: https://platform.hanzo.ai
- key: NEXTAUTH_SECRET
scope: RUN_TIME
type: SECRET
value: e21ae21c91df0875b5dc54ba98c3a2731a69c4284f0113ab93f1cb2919128abc
- key: HOSTNAME
value: "0.0.0.0"
health_check:
http_path: /
initial_delay_seconds: 30
period_seconds: 10
+347
View File
@@ -0,0 +1,347 @@
-- Bootnode DataStore Schema (ClickHouse)
-- High-performance analytics for blockchain data
-- =============================================================================
-- BLOCKS TABLE (Partitioned by chain_id and month)
-- =============================================================================
CREATE TABLE IF NOT EXISTS blocks (
chain_id UInt64,
network String,
block_number UInt64,
block_hash String,
parent_hash String,
timestamp DateTime64(3),
miner String,
gas_used UInt64,
gas_limit UInt64,
base_fee_per_gas Nullable(UInt64),
transaction_count UInt32,
size UInt32,
extra_data String,
created_at DateTime64(3) DEFAULT now64(3)
)
ENGINE = ReplacingMergeTree(created_at)
PARTITION BY (chain_id, toYYYYMM(timestamp))
ORDER BY (chain_id, block_number)
SETTINGS index_granularity = 8192;
-- =============================================================================
-- TRANSACTIONS TABLE
-- =============================================================================
CREATE TABLE IF NOT EXISTS transactions (
chain_id UInt64,
network String,
tx_hash String,
block_number UInt64,
block_hash String,
tx_index UInt32,
from_address String,
to_address Nullable(String),
value UInt256,
gas UInt64,
gas_price UInt64,
max_fee_per_gas Nullable(UInt64),
max_priority_fee_per_gas Nullable(UInt64),
input String,
nonce UInt64,
tx_type UInt8,
status UInt8, -- 0: failed, 1: success
gas_used UInt64,
effective_gas_price UInt64,
timestamp DateTime64(3),
created_at DateTime64(3) DEFAULT now64(3)
)
ENGINE = ReplacingMergeTree(created_at)
PARTITION BY (chain_id, toYYYYMM(timestamp))
ORDER BY (chain_id, block_number, tx_index)
SETTINGS index_granularity = 8192;
-- =============================================================================
-- LOGS (Events) TABLE
-- =============================================================================
CREATE TABLE IF NOT EXISTS logs (
chain_id UInt64,
network String,
block_number UInt64,
block_hash String,
tx_hash String,
tx_index UInt32,
log_index UInt32,
address String,
topic0 String,
topic1 Nullable(String),
topic2 Nullable(String),
topic3 Nullable(String),
data String,
removed UInt8,
timestamp DateTime64(3),
created_at DateTime64(3) DEFAULT now64(3)
)
ENGINE = ReplacingMergeTree(created_at)
PARTITION BY (chain_id, toYYYYMM(timestamp))
ORDER BY (chain_id, block_number, tx_index, log_index)
SETTINGS index_granularity = 8192;
-- =============================================================================
-- TOKEN TRANSFERS (ERC-20)
-- =============================================================================
CREATE TABLE IF NOT EXISTS token_transfers (
chain_id UInt64,
network String,
block_number UInt64,
tx_hash String,
log_index UInt32,
token_address String,
from_address String,
to_address String,
value UInt256,
token_decimals UInt8,
token_symbol String,
timestamp DateTime64(3),
created_at DateTime64(3) DEFAULT now64(3)
)
ENGINE = ReplacingMergeTree(created_at)
PARTITION BY (chain_id, toYYYYMM(timestamp))
ORDER BY (chain_id, block_number, tx_hash, log_index)
SETTINGS index_granularity = 8192;
-- =============================================================================
-- NFT TRANSFERS (ERC-721 & ERC-1155)
-- =============================================================================
CREATE TABLE IF NOT EXISTS nft_transfers (
chain_id UInt64,
network String,
block_number UInt64,
tx_hash String,
log_index UInt32,
contract_address String,
token_standard Enum8('ERC721' = 1, 'ERC1155' = 2),
from_address String,
to_address String,
token_id UInt256,
amount UInt256, -- Always 1 for ERC-721
operator Nullable(String), -- For ERC-1155
timestamp DateTime64(3),
created_at DateTime64(3) DEFAULT now64(3)
)
ENGINE = ReplacingMergeTree(created_at)
PARTITION BY (chain_id, toYYYYMM(timestamp))
ORDER BY (chain_id, block_number, tx_hash, log_index)
SETTINGS index_granularity = 8192;
-- =============================================================================
-- TOKEN BALANCES (Materialized View)
-- =============================================================================
CREATE TABLE IF NOT EXISTS token_balances (
chain_id UInt64,
network String,
address String,
token_address String,
balance UInt256,
last_block UInt64,
updated_at DateTime64(3) DEFAULT now64(3)
)
ENGINE = ReplacingMergeTree(updated_at)
ORDER BY (chain_id, address, token_address)
SETTINGS index_granularity = 8192;
-- =============================================================================
-- NFT OWNERSHIP
-- =============================================================================
CREATE TABLE IF NOT EXISTS nft_ownership (
chain_id UInt64,
network String,
contract_address String,
token_id UInt256,
owner_address String,
amount UInt256, -- Always 1 for ERC-721
last_block UInt64,
updated_at DateTime64(3) DEFAULT now64(3)
)
ENGINE = ReplacingMergeTree(updated_at)
ORDER BY (chain_id, contract_address, token_id)
SETTINGS index_granularity = 8192;
-- =============================================================================
-- INTERNAL TRANSACTIONS (Traces)
-- =============================================================================
CREATE TABLE IF NOT EXISTS internal_transactions (
chain_id UInt64,
network String,
block_number UInt64,
tx_hash String,
trace_address Array(UInt32),
trace_type Enum8('call' = 1, 'create' = 2, 'suicide' = 3, 'reward' = 4),
from_address String,
to_address Nullable(String),
value UInt256,
gas UInt64,
gas_used UInt64,
input String,
output Nullable(String),
error Nullable(String),
timestamp DateTime64(3),
created_at DateTime64(3) DEFAULT now64(3)
)
ENGINE = ReplacingMergeTree(created_at)
PARTITION BY (chain_id, toYYYYMM(timestamp))
ORDER BY (chain_id, block_number, tx_hash, trace_address)
SETTINGS index_granularity = 8192;
-- =============================================================================
-- ADDRESS ACTIVITY (Aggregated)
-- =============================================================================
CREATE TABLE IF NOT EXISTS address_activity (
chain_id UInt64,
network String,
address String,
date Date,
tx_count UInt32,
tx_sent UInt32,
tx_received UInt32,
eth_sent UInt256,
eth_received UInt256,
token_transfers_out UInt32,
token_transfers_in UInt32,
nft_transfers_out UInt32,
nft_transfers_in UInt32,
gas_spent UInt256,
first_tx_block UInt64,
last_tx_block UInt64,
created_at DateTime64(3) DEFAULT now64(3)
)
ENGINE = SummingMergeTree()
PARTITION BY (chain_id, toYYYYMM(date))
ORDER BY (chain_id, address, date)
SETTINGS index_granularity = 8192;
-- =============================================================================
-- PROTOCOL EVENTS (DeFi, NFT Marketplaces, etc.)
-- =============================================================================
CREATE TABLE IF NOT EXISTS protocol_events (
chain_id UInt64,
network String,
block_number UInt64,
tx_hash String,
log_index UInt32,
protocol String, -- e.g., 'uniswap_v3', 'aave_v3', 'seaport'
event_type String, -- e.g., 'swap', 'deposit', 'sale'
contract_address String,
user_address String,
data String, -- JSON encoded event data
timestamp DateTime64(3),
created_at DateTime64(3) DEFAULT now64(3)
)
ENGINE = ReplacingMergeTree(created_at)
PARTITION BY (chain_id, toYYYYMM(timestamp))
ORDER BY (chain_id, protocol, block_number, tx_hash, log_index)
SETTINGS index_granularity = 8192;
-- =============================================================================
-- WEBHOOK DELIVERIES LOG
-- =============================================================================
CREATE TABLE IF NOT EXISTS webhook_deliveries (
id UUID DEFAULT generateUUIDv4(),
webhook_id UUID,
project_id UUID,
chain_id UInt64,
network String,
event_type String,
payload String,
status Enum8('pending' = 0, 'success' = 1, 'failed' = 2),
status_code UInt16,
response_time_ms UInt32,
attempt UInt8,
error Nullable(String),
created_at DateTime64(3) DEFAULT now64(3),
delivered_at Nullable(DateTime64(3))
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)
ORDER BY (webhook_id, created_at)
TTL created_at + INTERVAL 30 DAY
SETTINGS index_granularity = 8192;
-- =============================================================================
-- API USAGE METRICS
-- =============================================================================
CREATE TABLE IF NOT EXISTS api_usage (
project_id UUID,
api_key_id UUID,
chain_id UInt64,
network String,
endpoint String,
method String,
compute_units UInt32,
response_time_ms UInt32,
status_code UInt16,
ip_address String,
user_agent String,
timestamp DateTime64(3) DEFAULT now64(3)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (project_id, timestamp)
TTL timestamp + INTERVAL 90 DAY
SETTINGS index_granularity = 8192;
-- =============================================================================
-- MATERIALIZED VIEWS FOR REAL-TIME AGGREGATIONS
-- =============================================================================
-- Hourly API usage per project
CREATE MATERIALIZED VIEW IF NOT EXISTS api_usage_hourly
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (project_id, chain_id, hour)
AS SELECT
project_id,
chain_id,
toStartOfHour(timestamp) as hour,
count() as request_count,
sum(compute_units) as total_compute_units,
avg(response_time_ms) as avg_response_time_ms,
countIf(status_code >= 400) as error_count
FROM api_usage
GROUP BY project_id, chain_id, hour;
-- Daily transaction counts per chain
CREATE MATERIALIZED VIEW IF NOT EXISTS chain_daily_stats
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(date)
ORDER BY (chain_id, date)
AS SELECT
chain_id,
toDate(timestamp) as date,
count() as tx_count,
uniq(from_address) as unique_senders,
uniq(to_address) as unique_receivers,
sum(value) as total_value,
sum(gas_used) as total_gas_used
FROM transactions
GROUP BY chain_id, date;
-- =============================================================================
-- INDEXES
-- =============================================================================
-- Bloom filter for address lookups
ALTER TABLE transactions ADD INDEX idx_from_address from_address TYPE bloom_filter GRANULARITY 4;
ALTER TABLE transactions ADD INDEX idx_to_address to_address TYPE bloom_filter GRANULARITY 4;
ALTER TABLE logs ADD INDEX idx_address address TYPE bloom_filter GRANULARITY 4;
ALTER TABLE logs ADD INDEX idx_topic0 topic0 TYPE bloom_filter GRANULARITY 4;
ALTER TABLE token_transfers ADD INDEX idx_from from_address TYPE bloom_filter GRANULARITY 4;
ALTER TABLE token_transfers ADD INDEX idx_to to_address TYPE bloom_filter GRANULARITY 4;
ALTER TABLE token_transfers ADD INDEX idx_token token_address TYPE bloom_filter GRANULARITY 4;
+260
View File
@@ -0,0 +1,260 @@
-- Bootnode PostgreSQL Schema
-- Operational data: projects, API keys, webhooks, smart wallets
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- =============================================================================
-- PROJECTS
-- =============================================================================
CREATE TABLE IF NOT EXISTS projects (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
owner_id UUID NOT NULL,
description TEXT,
settings JSONB DEFAULT '{}',
tier VARCHAR(50) DEFAULT 'free',
monthly_cu_limit BIGINT DEFAULT 100000000, -- 100M compute units
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_projects_owner ON projects(owner_id);
-- =============================================================================
-- API KEYS
-- =============================================================================
CREATE TABLE IF NOT EXISTS api_keys (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
key_hash VARCHAR(255) NOT NULL UNIQUE,
key_prefix VARCHAR(10) NOT NULL, -- "bn_" + first 4 chars
permissions JSONB DEFAULT '["read", "write"]',
allowed_origins TEXT[],
allowed_ips INET[],
rate_limit_per_second INT DEFAULT 100,
is_active BOOLEAN DEFAULT TRUE,
last_used_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_api_keys_project ON api_keys(project_id);
CREATE INDEX idx_api_keys_prefix ON api_keys(key_prefix);
-- =============================================================================
-- WEBHOOKS
-- =============================================================================
CREATE TABLE IF NOT EXISTS webhooks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
url TEXT NOT NULL,
secret VARCHAR(255) NOT NULL, -- HMAC signing secret
chain_id BIGINT NOT NULL,
network VARCHAR(50) NOT NULL,
event_type VARCHAR(50) NOT NULL, -- ADDRESS_ACTIVITY, NFT_ACTIVITY, TOKEN_TRANSFER, MINED_TRANSACTION
filters JSONB DEFAULT '{}',
is_active BOOLEAN DEFAULT TRUE,
failure_count INT DEFAULT 0,
last_triggered_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_webhooks_project ON webhooks(project_id);
CREATE INDEX idx_webhooks_chain ON webhooks(chain_id, network);
CREATE INDEX idx_webhooks_event ON webhooks(event_type);
CREATE INDEX idx_webhooks_active ON webhooks(is_active) WHERE is_active = TRUE;
-- =============================================================================
-- SMART WALLETS (ERC-4337)
-- =============================================================================
CREATE TABLE IF NOT EXISTS smart_wallets (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
chain_id BIGINT NOT NULL,
network VARCHAR(50) NOT NULL,
address VARCHAR(42) NOT NULL,
owner_address VARCHAR(42) NOT NULL,
factory_address VARCHAR(42) NOT NULL,
implementation VARCHAR(50) DEFAULT 'simple_account', -- simple_account, kernel, safe
salt VARCHAR(66),
is_deployed BOOLEAN DEFAULT FALSE,
deploy_tx_hash VARCHAR(66),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(chain_id, network, address)
);
CREATE INDEX idx_smart_wallets_project ON smart_wallets(project_id);
CREATE INDEX idx_smart_wallets_owner ON smart_wallets(owner_address);
CREATE INDEX idx_smart_wallets_chain ON smart_wallets(chain_id, network);
-- =============================================================================
-- GAS POLICIES (Paymaster)
-- =============================================================================
CREATE TABLE IF NOT EXISTS gas_policies (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
chain_id BIGINT NOT NULL,
network VARCHAR(50) NOT NULL,
policy_type VARCHAR(50) DEFAULT 'sponsorship', -- sponsorship, token_paymaster, verifying
rules JSONB NOT NULL DEFAULT '{}',
max_gas_per_tx BIGINT DEFAULT 1000000,
daily_limit_usd DECIMAL(20, 6) DEFAULT 100.00,
monthly_limit_usd DECIMAL(20, 6) DEFAULT 1000.00,
current_daily_spend_usd DECIMAL(20, 6) DEFAULT 0,
current_monthly_spend_usd DECIMAL(20, 6) DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_gas_policies_project ON gas_policies(project_id);
CREATE INDEX idx_gas_policies_chain ON gas_policies(chain_id, network);
-- =============================================================================
-- USER OPERATIONS (ERC-4337)
-- =============================================================================
CREATE TABLE IF NOT EXISTS user_operations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
project_id UUID REFERENCES projects(id),
chain_id BIGINT NOT NULL,
network VARCHAR(50) NOT NULL,
user_op_hash VARCHAR(66) NOT NULL UNIQUE,
sender VARCHAR(42) NOT NULL,
nonce BIGINT NOT NULL,
init_code TEXT,
call_data TEXT NOT NULL,
call_gas_limit BIGINT NOT NULL,
verification_gas_limit BIGINT NOT NULL,
pre_verification_gas BIGINT NOT NULL,
max_fee_per_gas BIGINT NOT NULL,
max_priority_fee_per_gas BIGINT NOT NULL,
paymaster_and_data TEXT,
signature TEXT NOT NULL,
status VARCHAR(50) DEFAULT 'pending', -- pending, submitted, included, failed
tx_hash VARCHAR(66),
block_number BIGINT,
actual_gas_cost BIGINT,
actual_gas_used BIGINT,
error TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_user_ops_project ON user_operations(project_id);
CREATE INDEX idx_user_ops_sender ON user_operations(sender);
CREATE INDEX idx_user_ops_status ON user_operations(status);
CREATE INDEX idx_user_ops_chain ON user_operations(chain_id, network);
-- =============================================================================
-- TOKEN METADATA CACHE
-- =============================================================================
CREATE TABLE IF NOT EXISTS token_metadata (
chain_id BIGINT NOT NULL,
network VARCHAR(50) NOT NULL,
address VARCHAR(42) NOT NULL,
name VARCHAR(255),
symbol VARCHAR(50),
decimals SMALLINT,
total_supply NUMERIC(78, 0),
logo_uri TEXT,
website TEXT,
description TEXT,
is_verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (chain_id, network, address)
);
-- =============================================================================
-- NFT COLLECTION METADATA
-- =============================================================================
CREATE TABLE IF NOT EXISTS nft_collections (
chain_id BIGINT NOT NULL,
network VARCHAR(50) NOT NULL,
address VARCHAR(42) NOT NULL,
name VARCHAR(255),
symbol VARCHAR(50),
token_standard VARCHAR(20), -- ERC721, ERC1155
total_supply BIGINT,
description TEXT,
image_uri TEXT,
external_url TEXT,
floor_price NUMERIC(78, 0),
is_verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (chain_id, network, address)
);
-- =============================================================================
-- CHAIN CONFIGURATION
-- =============================================================================
CREATE TABLE IF NOT EXISTS chains (
chain_id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
category VARCHAR(50) NOT NULL, -- ethereum-l1, layer-2, alt-l1, bitcoin, other
chain_type VARCHAR(50) NOT NULL, -- evm, svm, move, bitcoin, starknet, other
native_currency_name VARCHAR(50),
native_currency_symbol VARCHAR(20),
native_currency_decimals SMALLINT DEFAULT 18,
block_explorer_url TEXT,
is_testnet BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
rpc_endpoints JSONB DEFAULT '[]',
features JSONB DEFAULT '[]',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS chain_networks (
id SERIAL PRIMARY KEY,
chain_id BIGINT REFERENCES chains(chain_id),
network_id VARCHAR(50) NOT NULL,
name VARCHAR(255) NOT NULL,
is_testnet BOOLEAN DEFAULT FALSE,
rpc_url TEXT,
ws_url TEXT,
is_active BOOLEAN DEFAULT TRUE,
UNIQUE(chain_id, network_id)
);
-- =============================================================================
-- FUNCTIONS
-- =============================================================================
-- Auto-update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Apply trigger to all tables
CREATE TRIGGER update_projects_updated_at BEFORE UPDATE ON projects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_api_keys_updated_at BEFORE UPDATE ON api_keys FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_webhooks_updated_at BEFORE UPDATE ON webhooks FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_smart_wallets_updated_at BEFORE UPDATE ON smart_wallets FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_gas_policies_updated_at BEFORE UPDATE ON gas_policies FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_user_operations_updated_at BEFORE UPDATE ON user_operations FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+27 -1
View File
@@ -29,7 +29,7 @@ spec:
serviceAccountName: bootnode-api
containers:
- name: api
image: ghcr.io/hanzoai/bootnode:api-latest
image: hanzoai/bootnode:api-latest
imagePullPolicy: Always
ports:
- name: http
@@ -63,6 +63,32 @@ spec:
secretKeyRef:
name: bootnode-secrets
key: api-key-salt
# Hanzo KMS Configuration
- name: HANZO_KMS_URL
value: "https://kms.hanzo.ai"
- name: HANZO_KMS_ORG
value: "hanzo"
- name: HANZO_KMS_PROJECT
value: "bootnode"
- name: HANZO_KMS_ENV
value: "production"
- name: HANZO_KMS_CLIENT_ID
valueFrom:
secretKeyRef:
name: hanzo-kms-secrets
key: client-id
optional: true
- name: HANZO_KMS_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: hanzo-kms-secrets
key: client-secret
optional: true
# Hanzo IAM Configuration
- name: HANZO_IAM_URL
value: "https://hanzo.id"
- name: HANZO_IAM_ORG
value: "HANZO"
resources:
requests:
cpu: "500m"
+97
View File
@@ -0,0 +1,97 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloud
namespace: hanzo
labels:
app: cloud
spec:
replicas: 2
selector:
matchLabels:
app: cloud
template:
metadata:
labels:
app: cloud
spec:
containers:
- name: cloud
image: registry.digitalocean.com/hanzo/cloud:latest
imagePullPolicy: Always
ports:
- containerPort: 14000
env:
- name: RUNNING_IN_DOCKER
value: "true"
- name: httpport
value: "14000"
- name: driverName
value: "postgres"
- name: dataSourceName
valueFrom:
secretKeyRef:
name: cloud-secrets
key: database-url
- name: casdoorEndpoint
value: "https://hanzo.id"
- name: clientId
valueFrom:
secretKeyRef:
name: cloud-secrets
key: oauth-client-id
- name: clientSecret
valueFrom:
secretKeyRef:
name: cloud-secrets
key: oauth-client-secret
- name: casdoorOrganization
value: "hanzo"
- name: casdoorApplication
value: "app-cloud"
- name: redirectPath
value: "/callback"
- name: publicDomain
value: "https://cloud.hanzo.ai"
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /api/health
port: 14000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/health
port: 14000
initialDelaySeconds: 5
periodSeconds: 5
imagePullSecrets:
- name: registry-hanzo
---
apiVersion: v1
kind: Service
metadata:
name: cloud
namespace: hanzo
labels:
app: cloud
spec:
selector:
app: cloud
ports:
- protocol: TCP
port: 80
targetPort: 14000
type: ClusterIP
# Note: cloud-secrets must be created separately with real OAuth credentials:
# kubectl create secret generic cloud-secrets -n hanzo \
# --from-literal=oauth-client-id="hanzo-cloud-client-id" \
# --from-literal=oauth-client-secret="<real-secret>" \
# --from-literal=database-url="postgres://hanzo:<password>@postgres:5432/hanzo_cloud?sslmode=disable"
+28
View File
@@ -0,0 +1,28 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cloud
namespace: hanzo
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
nginx.ingress.kubernetes.io/proxy-send-timeout: "120"
spec:
ingressClassName: nginx
rules:
- host: cloud.hanzo.ai
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: cloud
port:
number: 80
tls:
- hosts:
- cloud.hanzo.ai
secretName: cloud-tls
+108
View File
@@ -0,0 +1,108 @@
# External Secrets Operator configuration for Hanzo KMS
# This syncs secrets from Hanzo KMS to K8s without storing credentials in code
#
# Prerequisites:
# 1. Install External Secrets Operator: helm install external-secrets external-secrets/external-secrets -n external-secrets --create-namespace
# 2. Configure ClusterSecretStore with Hanzo KMS credentials
#
# Hanzo KMS Integration:
# - KMS URL: https://kms.hanzo.ai (or self-hosted)
# - Project: bootnode
# - Environment: production
---
# ClusterSecretStore - connects to Hanzo KMS
# Uses service token stored in hanzo-kms-credentials secret
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: hanzo-kms
spec:
provider:
# Hanzo KMS is Infisical-compatible
# Use the Infisical provider with Hanzo KMS endpoint
webhook:
url: "https://kms.hanzo.ai/api/v3/secrets/raw"
headers:
Authorization:
name: hanzo-kms-credentials
namespace: external-secrets
key: service-token
result:
jsonPath: "$.secrets"
secrets:
- name: env
secretRef:
name: hanzo-kms-credentials
namespace: external-secrets
key: environment
- name: project
secretRef:
name: hanzo-kms-credentials
namespace: external-secrets
key: project-id
---
# ExternalSecret for Bootnode API
# Syncs secrets from Hanzo KMS to bootnode-secrets
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: bootnode-secrets
namespace: bootnode
spec:
refreshInterval: 1h
secretStoreRef:
name: hanzo-kms
kind: ClusterSecretStore
target:
name: bootnode-secrets
creationPolicy: Owner
data:
# Database credentials
- secretKey: database-url
remoteRef:
key: DATABASE_URL
- secretKey: database-url-go
remoteRef:
key: DATABASE_URL_GO
# Redis
- secretKey: redis-url
remoteRef:
key: REDIS_URL
# Datastore (ClickHouse)
- secretKey: datastore-url
remoteRef:
key: DATASTORE_URL
# JWT & API Keys
- secretKey: jwt-secret
remoteRef:
key: JWT_SECRET
- secretKey: api-key-salt
remoteRef:
key: API_KEY_SALT
# Optional: Bundler private key
- secretKey: bundler-private-key
remoteRef:
key: BUNDLER_PRIVATE_KEY
---
# Setup instructions:
#
# 1. Create service token in Hanzo KMS:
# - Go to https://kms.hanzo.ai
# - Create project "bootnode"
# - Add secrets: DATABASE_URL, REDIS_URL, JWT_SECRET, etc.
# - Create service token with read access
#
# 2. Create K8s secret for KMS credentials:
# kubectl create secret generic hanzo-kms-credentials \
# -n external-secrets \
# --from-literal=service-token='st.xxx.xxx' \
# --from-literal=environment='production' \
# --from-literal=project-id='xxx-xxx-xxx'
#
# 3. Apply this file:
# kubectl apply -f external-secrets.yaml
#
# 4. Verify sync:
# kubectl get externalsecret -n bootnode
# kubectl get secret bootnode-secrets -n bootnode -o yaml
+1 -1
View File
@@ -6,7 +6,7 @@ metadata:
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/ssl-redirect: "false"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
nginx.ingress.kubernetes.io/proxy-send-timeout: "60"
+30 -28
View File
@@ -1,38 +1,40 @@
# Bootnode Secrets - Uses shared Hanzo infrastructure
# Services in hanzo namespace: postgres, datastore, redis-master
# Bootnode Secrets Template
#
# IMPORTANT: Do NOT commit actual secrets to this file!
#
# Options for secret management:
#
# 1. Hanzo KMS (Recommended for production):
# - Use external-secrets.yaml with External Secrets Operator
# - Secrets sync automatically from https://kms.hanzo.ai
#
# 2. kubectl create secret (for development/testing):
# kubectl create secret generic bootnode-secrets \
# --namespace bootnode \
# --from-literal=database-url='postgresql+asyncpg://user:pass@host:5432/db' \
# --from-literal=redis-url='redis://host:6379/0' \
# --from-literal=datastore-url='clickhouse://user:pass@host:8123/db' \
# --from-literal=jwt-secret="$(openssl rand -hex 32)" \
# --from-literal=api-key-salt="$(openssl rand -hex 16)"
#
# 3. Sealed Secrets (for GitOps):
# kubeseal --format yaml < secret.yaml > sealed-secret.yaml
---
apiVersion: v1
kind: Secret
metadata:
name: bootnode-secrets
namespace: bootnode
annotations:
# Indicate this is a template - actual values from KMS
kms.hanzo.ai/project: "bootnode"
kms.hanzo.ai/environment: "production"
type: Opaque
stringData:
# Shared PostgreSQL (hanzo namespace)
database-url: "postgresql+asyncpg://hanzo:${POSTGRES_PASSWORD}@postgres.hanzo.svc.cluster.local:5432/hanzo"
database-url-go: "postgres://hanzo:${POSTGRES_PASSWORD}@postgres.hanzo.svc.cluster.local:5432/hanzo"
# Shared Datastore/ClickHouse (hanzo namespace)
datastore-url: "clickhouse://hanzo:${DATASTORE_PASSWORD}@datastore.hanzo.svc.cluster.local:8123/hanzo"
# Shared Redis (hanzo namespace)
redis-url: "redis://redis-master.hanzo.svc.cluster.local:6379/0"
# JWT & API Keys
jwt-secret: "${JWT_SECRET}"
api-key-salt: "${API_KEY_SALT}"
# Bundler (ERC-4337) - optional
bundler-private-key: ""
# RPC Endpoints
eth-mainnet-rpc: "https://eth.llamarpc.com"
eth-sepolia-rpc: "https://ethereum-sepolia-rpc.publicnode.com"
base-mainnet-rpc: "https://mainnet.base.org"
polygon-mainnet-rpc: "https://polygon-rpc.com"
arbitrum-mainnet-rpc: "https://arb1.arbitrum.io/rpc"
optimism-mainnet-rpc: "https://mainnet.optimism.io"
sol-mainnet-rpc: "https://api.mainnet-beta.solana.com"
# stringData intentionally empty - use KMS or kubectl create secret
stringData: {}
---
# Chain configuration (non-sensitive)
apiVersion: v1
kind: ConfigMap
metadata:
+8 -2
View File
@@ -24,7 +24,7 @@ spec:
spec:
containers:
- name: web
image: ghcr.io/hanzoai/bootnode:web-latest
image: hanzoai/bootnode:web-latest
imagePullPolicy: Always
ports:
- name: http
@@ -33,6 +33,8 @@ spec:
env:
- name: NODE_ENV
value: "production"
- name: NEXT_PUBLIC_BRAND
value: "hanzo"
- name: NEXT_PUBLIC_API_URL
value: "https://api.web3.hanzo.ai"
- name: NEXT_PUBLIC_WS_URL
@@ -40,7 +42,11 @@ spec:
- name: NEXT_PUBLIC_IAM_URL
value: "https://iam.hanzo.ai"
- name: NEXT_PUBLIC_IAM_CLIENT_ID
value: "bootnode-platform"
value: "hanzo-web3"
- name: NEXT_PUBLIC_IAM_DOMAIN
value: "hanzo.id"
- name: NEXT_PUBLIC_AUTH_MODE
value: "iam"
resources:
requests:
cpu: "100m"
+23
View File
@@ -0,0 +1,23 @@
# Grafana Datasources for Bootnode Monitoring
apiVersion: 1
datasources:
# VictoriaMetrics as primary metrics source
- name: VictoriaMetrics
type: prometheus
access: proxy
url: http://victoriametrics:8428
isDefault: true
editable: false
# ClickHouse/DataStore for analytics
- name: DataStore
type: grafana-clickhouse-datasource
access: proxy
url: http://datastore:8123
jsonData:
defaultDatabase: bootnode
username: bootnode
secureJsonData:
password: ${CLICKHOUSE_PASSWORD}
editable: false
+32
View File
@@ -0,0 +1,32 @@
# Prometheus Configuration for Bootnode
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
# Bootnode API metrics
- job_name: 'bootnode-api'
static_configs:
- targets: ['api:8000']
metrics_path: /metrics
# Indexer metrics
- job_name: 'bootnode-indexer'
static_configs:
- targets: ['indexer:9090']
metrics_path: /metrics
# Redis metrics (via redis-exporter if deployed)
- job_name: 'redis'
static_configs:
- targets: ['redis:6379']
# ClickHouse/DataStore metrics
- job_name: 'datastore'
static_configs:
- targets: ['datastore:9000']
# Prometheus self-monitoring
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
+35
View File
@@ -0,0 +1,35 @@
# VMAgent Scrape Configuration for Bootnode
# Compatible with Prometheus scrape_configs format
global:
scrape_interval: 15s
scrape_timeout: 10s
scrape_configs:
# Bootnode API metrics
- job_name: 'bootnode-api'
static_configs:
- targets: ['api:8000']
metrics_path: /metrics
# Indexer metrics (github.com/luxfi/metric compatible)
- job_name: 'bootnode-indexer'
static_configs:
- targets: ['indexer:9090']
metrics_path: /metrics
# DataStore (ClickHouse) metrics
- job_name: 'datastore'
static_configs:
- targets: ['datastore:9363']
metrics_path: /metrics
# Redis metrics (if redis-exporter is deployed)
- job_name: 'redis'
static_configs:
- targets: ['redis:6379']
# VictoriaMetrics self-monitoring
- job_name: 'victoriametrics'
static_configs:
- targets: ['victoriametrics:8428']
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="M 13.5 49.5 L 24 68.5 L 46 68.5 L 50 62 L 54 68.5 L 75.5 68.5 L 86 50.5 L 75.5 31 L 54 31 L 50 37.5 L 46 31 L 24.5 31 Z M 74 66.5 L 83 51.5 L 65.5 51 L 56.5 66 Z M 25.5 66.5 L 43 66 L 34.5 51 L 16.5 51.5 Z M 45.5 50.5 L 50 57 L 54.5 50 L 49.5 42.5 Z M 51 40 L 56.5 50 L 51 60 L 54.5 65 L 63.5 49.5 L 54.5 34.5 Z M 36.5 50.5 L 45.5 65 L 48.5 59 L 43 49.5 L 48.5 39.5 L 45 34.5 Z M 65.5 49 L 83 48.5 L 74 33 L 56.5 33.5 Z M 34 49 L 43 33.5 L 25.5 33 L 17 48.5 Z" fill="currentColor" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 583 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<path d="M 27 99 L 48 137 L 92 137 L 100 124 L 108 137 L 151 137 L 172 101 L 151 62 L 108 62 L 100 75 L 92 62 L 49 62 Z M 148 133 L 166 103 L 131 102 L 113 132 Z M 51 133 L 86 132 L 69 102 L 33 103 Z M 91 101 L 100 114 L 109 100 L 99 85 Z M 102 80 L 113 100 L 102 120 L 109 130 L 127 99 L 109 69 Z M 73 101 L 91 130 L 97 118 L 86 99 L 97 79 L 90 69 Z M 131 98 L 166 97 L 148 66 L 113 67 Z M 68 98 L 86 67 L 51 66 L 34 97 Z" fill="#000000" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 532 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<path d="M 27 99 L 48 137 L 92 137 L 100 124 L 108 137 L 151 137 L 172 101 L 151 62 L 108 62 L 100 75 L 92 62 L 49 62 Z M 148 133 L 166 103 L 131 102 L 113 132 Z M 51 133 L 86 132 L 69 102 L 33 103 Z M 91 101 L 100 114 L 109 100 L 99 85 Z M 102 80 L 113 100 L 102 120 L 109 130 L 127 99 L 109 69 Z M 73 101 L 91 130 L 97 118 L 86 99 L 97 79 L 90 69 Z M 131 98 L 166 97 L 148 66 L 113 67 Z M 68 98 L 86 67 L 51 66 L 34 97 Z" fill="#ffffff" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 532 B

+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffffff"/>
<stop offset="1" stop-color="#d8d8d8"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feOffset dx="2" dy="3" result="off"/>
<feGaussianBlur in="off" stdDeviation="2" result="blur"/>
<feColorMatrix in="blur" type="matrix"
values="0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0.35 0" result="shadow"/>
<feMerge>
<feMergeNode in="shadow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<rect width="200" height="200" fill="#372929"/>
<path d="M 27 99 L 48 137 L 92 137 L 100 124 L 108 137 L 151 137 L 172 101 L 151 62 L 108 62 L 100 75 L 92 62 L 49 62 Z M 148 133 L 166 103 L 131 102 L 113 132 Z M 51 133 L 86 132 L 69 102 L 33 103 Z M 91 101 L 100 114 L 109 100 L 99 85 Z M 102 80 L 113 100 L 102 120 L 109 130 L 127 99 L 109 69 Z M 73 101 L 91 130 L 97 118 L 86 99 L 97 79 L 90 69 Z M 131 98 L 166 97 L 148 66 L 113 67 Z M 68 98 L 86 67 L 51 66 L 34 97 Z" fill="#000000" opacity="0.18" transform="translate(3 3)" />
<path d="M 27 99 L 48 137 L 92 137 L 100 124 L 108 137 L 151 137 L 172 101 L 151 62 L 108 62 L 100 75 L 92 62 L 49 62 Z M 148 133 L 166 103 L 131 102 L 113 132 Z M 51 133 L 86 132 L 69 102 L 33 103 Z M 91 101 L 100 114 L 109 100 L 99 85 Z M 102 80 L 113 100 L 102 120 L 109 130 L 127 99 L 109 69 Z M 73 101 L 91 130 L 97 118 L 86 99 L 97 79 L 90 69 Z M 131 98 L 166 97 L 148 66 L 113 67 Z M 68 98 L 86 67 L 51 66 L 34 97 Z" fill="url(#g)" fill-rule="evenodd" filter="url(#shadow)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffffff"/>
<stop offset="1" stop-color="#d8d8d8"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feOffset dx="2" dy="3" result="off"/>
<feGaussianBlur in="off" stdDeviation="2" result="blur"/>
<feColorMatrix in="blur" type="matrix"
values="0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0.35 0" result="shadow"/>
<feMerge>
<feMergeNode in="shadow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<rect width="200" height="200" fill="#372929"/>
<path d="M 27 99 L 48 137 L 92 137 L 100 124 L 108 137 L 151 137 L 172 101 L 151 62 L 108 62 L 100 75 L 92 62 L 49 62 Z M 148 133 L 166 103 L 131 102 L 113 132 Z M 51 133 L 86 132 L 69 102 L 33 103 Z M 91 101 L 100 114 L 109 100 L 99 85 Z M 102 80 L 113 100 L 102 120 L 109 130 L 127 99 L 109 69 Z M 73 101 L 91 130 L 97 118 L 86 99 L 97 79 L 90 69 Z M 131 98 L 166 97 L 148 66 L 113 67 Z M 68 98 L 86 67 L 51 66 L 34 97 Z" fill="#000000" opacity="0.18" transform="translate(3 3)" />
<path d="M 27 99 L 48 137 L 92 137 L 100 124 L 108 137 L 151 137 L 172 101 L 151 62 L 108 62 L 100 75 L 92 62 L 49 62 Z M 148 133 L 166 103 L 131 102 L 113 132 Z M 51 133 L 86 132 L 69 102 L 33 103 Z M 91 101 L 100 114 L 109 100 L 99 85 Z M 102 80 L 113 100 L 102 120 L 109 130 L 127 99 L 109 69 Z M 73 101 L 91 130 L 97 118 L 86 99 L 97 79 L 90 69 Z M 131 98 L 166 97 L 148 66 L 113 67 Z M 68 98 L 86 67 L 51 66 L 34 97 Z" fill="url(#g)" fill-rule="evenodd" filter="url(#shadow)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Executable
+182
View File
@@ -0,0 +1,182 @@
#!/bin/bash
# Bootnode Platform - One-Command Local Development Setup
# This script starts all services required for local development
set -e
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() {
echo -e "${GREEN}[BOOTNODE]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
# Check if Docker is running
if ! docker info > /dev/null 2>&1; then
warn "Docker is not running. Please start Docker and try again."
exit 1
fi
# Check if required ports are available
check_port() {
if lsof -Pi :$1 -sTCP:LISTEN -t >/dev/null ; then
warn "Port $1 is already in use. Please free it and try again."
if [ "$1" = "3001" ]; then
info "To kill existing Next.js process: pkill -f 'next dev'"
fi
exit 1
fi
}
log "🚀 Starting Bootnode Platform for Local Development"
log "================================================="
# Check required ports
info "Checking port availability..."
# check_port 3001 # Web interface - might be running already
check_port 8100 # API - Docker will handle
check_port 5434 # PostgreSQL - Docker will handle
check_port 6383 # Redis - Docker will handle
# Start backend infrastructure with Docker Compose
log "🐳 Starting backend infrastructure (PostgreSQL, Redis, ClickHouse, API)..."
cd "$(dirname "$0")"
docker compose up -d
# Wait for services to be ready
log "⏳ Waiting for services to be ready..."
sleep 10
# Check if API is healthy
api_ready=false
for i in {1..30}; do
if curl -s http://localhost:8100/health > /dev/null; then
api_ready=true
break
fi
sleep 2
echo -n "."
done
if [ "$api_ready" = false ]; then
warn "❌ API failed to start within 60 seconds"
docker logs nodes-api --tail 20
exit 1
fi
log "✅ Backend services are ready!"
# Check if web development server is already running
if curl -s http://localhost:3001 > /dev/null 2>&1; then
log "✅ Web interface already running at http://localhost:3001"
else
# Start web development server
log "🌐 Starting web development server..."
cd web
# Install dependencies if needed
if [ ! -d "node_modules" ]; then
log "📦 Installing web dependencies..."
npm install
fi
# Start in background
nohup npm run dev > /tmp/bootnode-web.log 2>&1 &
web_pid=$!
echo $web_pid > /tmp/bootnode-web.pid
# Wait for web server to be ready
log "⏳ Waiting for web server to be ready..."
web_ready=false
for i in {1..30}; do
if curl -s http://localhost:3001 > /dev/null; then
web_ready=true
break
fi
sleep 2
echo -n "."
done
if [ "$web_ready" = false ]; then
warn "❌ Web server failed to start within 60 seconds"
tail /tmp/bootnode-web.log
exit 1
fi
log "✅ Web development server started!"
cd ..
fi
# Show status
log "🎉 Bootnode Platform is now running!"
log "================================================="
log "📊 Service URLs:"
log " 🌐 Web Dashboard: http://localhost:3001"
log " 🔌 API: http://localhost:8100"
log " 📖 API Documentation: http://localhost:8100/docs"
log " 📚 ReDoc: http://localhost:8100/redoc"
log ""
log "🗄️ Database Connections:"
log " 📊 PostgreSQL: localhost:5434"
log " 🔴 Redis: localhost:6383"
log " 📈 ClickHouse: localhost:8125"
log ""
log "🔍 Key Features Available:"
log " ✅ Multi-chain RPC (Ethereum, Polygon, Arbitrum, etc.)"
log " ✅ Token & NFT APIs"
log " ✅ Smart Wallets (ERC-4337)"
log " ✅ Webhooks & Events"
log " ✅ Gas Management"
log " ✅ Analytics Dashboard"
log " ✅ API Key Management"
log ""
log "📋 Next Steps:"
log " 1. Visit http://localhost:3001/dashboard"
log " 2. Create an API key"
log " 3. Test RPC endpoints with your key"
log " 4. Explore the documentation at http://localhost:8100/docs"
log ""
log "🛑 To stop all services:"
log " docker compose down"
log " kill \$(cat /tmp/bootnode-web.pid 2>/dev/null) 2>/dev/null || true"
log ""
log "🎯 For production deployment, see FINAL-PRODUCTION-SUMMARY.md"
log "================================================="
# Create a simple stop script
cat > stop.sh << 'EOF'
#!/bin/bash
echo "🛑 Stopping Bootnode Platform..."
docker compose down
if [ -f /tmp/bootnode-web.pid ]; then
kill $(cat /tmp/bootnode-web.pid) 2>/dev/null || true
rm -f /tmp/bootnode-web.pid
fi
rm -f /tmp/bootnode-web.log
echo "✅ All services stopped"
EOF
chmod +x stop.sh
log "💾 Created stop.sh script for easy shutdown"
# Run a quick health check
log "🔍 Running quick health check..."
if ./test-e2e.sh 2>/dev/null | grep -q "✅ API Health Check"; then
log "✅ Health check passed - Platform is ready for development!"
else
warn "⚠️ Health check had issues, but basic services are running"
fi
log "🚀 Happy coding! The Bootnode platform is ready for development."
Executable
+102
View File
@@ -0,0 +1,102 @@
#!/bin/bash
# Bootnode - Easy Start Script
# Starts everything needed for local development
set -e
echo "🚀 Starting Bootnode Development Stack..."
# Check if Docker is running
if ! docker info >/dev/null 2>&1; then
echo "❌ Docker is not running. Please start Docker first."
exit 1
fi
# Create .env files if they don't exist
if [ ! -f ".env.development" ]; then
echo "❌ .env.development not found. Please copy from .env.development.example"
exit 1
fi
if [ ! -f "api/.env" ]; then
echo "📋 Copying API environment file..."
cp api/.env.example api/.env
fi
if [ ! -f "web/.env.local" ]; then
echo "📋 Copying web environment file..."
echo "NEXT_PUBLIC_API_URL=http://localhost:8100" > web/.env.local
echo "NEXT_PUBLIC_API_KEY=bn_dev_test_key_12345" >> web/.env.local
fi
echo "🐳 Starting infrastructure services..."
docker-compose up -d
# Wait for services to be healthy
echo "⏳ Waiting for services to be ready..."
sleep 10
# Check if API is healthy
for i in {1..30}; do
if curl -f http://localhost:8100/health >/dev/null 2>&1; then
echo "✅ API is healthy"
break
fi
echo "⏳ Waiting for API to be ready... ($i/30)"
sleep 2
done
# Start web development server in background
if [ "$1" != "--api-only" ]; then
echo "🌐 Starting web development server..."
cd web
if [ ! -d "node_modules" ]; then
echo "📦 Installing web dependencies..."
npm install
fi
npm run dev &
WEB_PID=$!
cd ..
echo "✅ Web server started (PID: $WEB_PID)"
fi
echo ""
echo "🎉 Bootnode is ready!"
echo ""
echo "📍 Services:"
echo " • API: http://localhost:8100"
echo " • Web: http://localhost:3001"
echo " • Docs: http://localhost:8100/docs"
echo ""
echo "📊 Infrastructure:"
echo " • PostgreSQL: localhost:5434"
echo " • Redis: localhost:6383"
echo " • ClickHouse: localhost:8125"
echo ""
echo "🛠 Commands:"
echo " • View logs: docker-compose logs -f"
echo " • Stop all: docker-compose down"
echo " • Restart: ./start.sh"
echo ""
# Keep script running if web server was started
if [ "$1" != "--api-only" ]; then
echo "💡 Press Ctrl+C to stop all services"
# Function to cleanup when script exits
cleanup() {
echo ""
echo "🛑 Stopping services..."
if [ ! -z "$WEB_PID" ]; then
kill $WEB_PID 2>/dev/null || true
fi
docker-compose down
echo "✅ All services stopped"
}
trap cleanup EXIT
# Wait for interrupt
wait $WEB_PID
fi
Executable
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
echo "🛑 Stopping Bootnode Platform..."
docker compose down
if [ -f /tmp/bootnode-web.pid ]; then
kill $(cat /tmp/bootnode-web.pid) 2>/dev/null || true
rm -f /tmp/bootnode-web.pid
fi
rm -f /tmp/bootnode-web.log
echo "✅ All services stopped"
+1 -1
View File
@@ -40,7 +40,7 @@ function CallbackContent() {
}
// Exchange code for token via our API
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8100"
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
const response = await fetch(`${apiUrl}/v1/auth/oauth/callback`, {
method: "POST",
headers: {
+79 -58
View File
@@ -5,6 +5,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { NodeMap } from "@/components/d3/node-map"
import { GlobeView } from "@/components/d3/globe-view"
import { DonutChart } from "@/components/d3/donut-chart"
import {
Activity,
@@ -57,10 +58,10 @@ export default function AnalyticsPage() {
// Fetch real data from multiple endpoints
const [nodesRes, chainsRes, keysRes, webhooksRes] = await Promise.all([
fetch("http://localhost:8100/v1/nodes/", { headers }).catch(() => null),
fetch("http://localhost:8100/v1/chains", { headers }).catch(() => null),
fetch("http://localhost:8100/v1/auth/keys", { headers }).catch(() => null),
fetch("http://localhost:8100/v1/webhooks", { headers }).catch(() => null),
fetch("http://localhost:8000/v1/nodes/", { headers }).catch(() => null),
fetch("http://localhost:8000/v1/chains", { headers }).catch(() => null),
fetch("http://localhost:8000/v1/auth/keys", { headers }).catch(() => null),
fetch("http://localhost:8000/v1/webhooks", { headers }).catch(() => null),
])
const nodes = nodesRes?.ok ? await nodesRes.json() : []
@@ -68,73 +69,91 @@ export default function AnalyticsPage() {
const keys = keysRes?.ok ? await keysRes.json() : []
const webhooks = webhooksRes?.ok ? await webhooksRes.json() : []
// Build chain breakdown from chains data
const chainColors = [
"#3b82f6", // blue
"#8b5cf6", // violet
"#10b981", // emerald
"#f59e0b", // amber
"#ef4444", // red
"#ec4899", // pink
"#06b6d4", // cyan
"#84cc16", // lime
]
// Build chain breakdown from actual deployed nodes (not fake data)
const chainColors: Record<string, string> = {
ethereum: "#627EEA",
polygon: "#8247E5",
arbitrum: "#28A0F0",
optimism: "#FF0420",
base: "#0052FF",
avalanche: "#E84142",
bsc: "#F0B90B",
lux: "#9333EA",
bitcoin: "#F7931A",
solana: "#14F195",
}
const chainBreakdown = Array.isArray(chains)
? chains.slice(0, 5).map((chain: any, idx: number) => ({
label: chain.name || chain.chain || "Unknown",
value: Math.floor(Math.random() * 40) + 10, // Placeholder - would come from usage API
color: chainColors[idx % chainColors.length]
}))
: []
// Add "Others" if there are more chains
if (chains.length > 5) {
chainBreakdown.push({
label: "Others",
value: chains.length - 5,
color: "#6b7280"
// Count nodes by chain for real data
const chainCounts: Record<string, number> = {}
if (Array.isArray(nodes)) {
nodes.forEach((node: any) => {
const chain = node.chain || "unknown"
chainCounts[chain] = (chainCounts[chain] || 0) + 1
})
}
const chainBreakdown = Object.entries(chainCounts).map(([chain, count]) => ({
label: chain.charAt(0).toUpperCase() + chain.slice(1),
value: count,
color: chainColors[chain] || "#6b7280"
}))
// Build node locations from nodes data
// Example node locations - in production would come from node metadata
const regions: Record<string, [number, number]> = {
"us-east": [-74.006, 40.7128],
"us-west": [-122.4194, 37.7749],
"eu-west": [-0.1276, 51.5074],
"eu-central": [13.405, 52.52],
"asia-east": [139.6917, 35.6895],
"asia-south": [77.1025, 28.7041],
"default": [0, 20]
// For local Docker nodes, detect user's location via IP geolocation
const getNodeLocation = async (): Promise<[number, number]> => {
try {
// Use free IP geolocation API to get user's location
const geoRes = await fetch("https://ipapi.co/json/", {
signal: AbortSignal.timeout(3000)
})
if (geoRes.ok) {
const geo = await geoRes.json()
if (geo.longitude && geo.latitude) {
return [geo.longitude, geo.latitude]
}
}
} catch {
// Fallback if geolocation fails
}
// Default to San Francisco
return [-122.4194, 37.7749]
}
const defaultLocation = await getNodeLocation()
const nodeLocations = Array.isArray(nodes)
? nodes.map((node: any, idx: number) => {
const region = node.region || "default"
const coords = regions[region] || regions.default
// Add small random offset to prevent overlap
const offset = [(Math.random() - 0.5) * 10, (Math.random() - 0.5) * 10]
// For local Docker nodes (provider === "docker"), use local coordinates
// Add small offset per node to prevent exact overlap
const offset: [number, number] = [
(idx % 3 - 1) * 2,
Math.floor(idx / 3) * 2
]
const coords: [number, number] = node.region
? (node.region === "us-west" ? [-122.4194, 37.7749] :
node.region === "us-east" ? [-74.006, 40.7128] :
node.region === "eu-west" ? [-0.1276, 51.5074] :
node.region === "eu-central" ? [13.405, 52.52] :
node.region === "asia-east" ? [139.6917, 35.6895] :
defaultLocation)
: defaultLocation
return {
id: node.id || `node-${idx}`,
name: node.name || `Node ${idx + 1}`,
point: [coords[0] + offset[0], coords[1] + offset[1]] as [number, number],
count: 1,
status: (node.status === "running" ? "running" : node.status === "syncing" ? "syncing" : "stopped") as "running" | "stopped" | "syncing" | "error",
status: (node.status === "running" ? "running" :
node.status === "syncing" || node.status === "starting" ? "syncing" :
node.status === "error" ? "error" : "stopped") as "running" | "stopped" | "syncing" | "error",
chain: node.chain || node.network
}
})
: []
// Common RPC methods with placeholder counts
const methodBreakdown = [
{ method: "eth_call", count: 0, percentage: 25 },
{ method: "eth_getBalance", count: 0, percentage: 20 },
{ method: "eth_getLogs", count: 0, percentage: 18 },
{ method: "eth_getTransactionReceipt", count: 0, percentage: 15 },
{ method: "eth_blockNumber", count: 0, percentage: 12 },
{ method: "Others", count: 0, percentage: 10 },
]
// Only show method breakdown when we have real usage data from DataStore
// Empty array means "no data" which shows the placeholder message
const methodBreakdown: { method: string; count: number; percentage: number }[] = []
setData({
totalRequests: 0, // Would come from usage tracking API
@@ -316,12 +335,14 @@ export default function AnalyticsPage() {
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : data?.nodeLocations && data.nodeLocations.length > 0 ? (
<NodeMap
data={data.nodeLocations}
width={800}
height={400}
className="mx-auto"
/>
<div className="flex justify-center">
<GlobeView
data={data.nodeLocations}
width={450}
height={450}
className="mx-auto"
/>
</div>
) : (
<div className="h-[300px] flex flex-col items-center justify-center text-muted-foreground">
<Server className="h-12 w-12 mb-4 opacity-50" />
+3 -3
View File
@@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
try {
// Forward request to the Bootnode API backend
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8100'}/v1/auth/api-keys/keys`, {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'}/v1/auth/api-keys/keys`, {
headers: {
'Authorization': request.headers.get('authorization') || '',
'Content-Type': 'application/json',
@@ -32,7 +32,7 @@ export async function POST(request: NextRequest) {
const body = await request.json()
// Forward request to the Bootnode API backend
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8100'}/v1/auth/api-keys/keys`, {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'}/v1/auth/api-keys/keys`, {
method: 'POST',
headers: {
'Authorization': request.headers.get('authorization') || '',
@@ -71,7 +71,7 @@ export async function DELETE(request: NextRequest) {
}
// Forward request to the Bootnode API backend
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8100'}/v1/auth/api-keys/keys/${keyId}`, {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'}/v1/auth/api-keys/keys/${keyId}`, {
method: 'DELETE',
headers: {
'Authorization': request.headers.get('authorization') || '',
+189
View File
@@ -0,0 +1,189 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Image, Search, ExternalLink, Loader2 } from "lucide-react"
interface NFT {
contract: string
tokenId: string
name?: string
image?: string
chain: string
}
export default function NFTsPage() {
const [address, setAddress] = useState("")
const [chain, setChain] = useState("ethereum")
const [nfts, setNfts] = useState<NFT[]>([])
const [loading, setLoading] = useState(false)
const [searched, setSearched] = useState(false)
function getAuthHeaders(): Record<string, string> {
if (typeof window === "undefined") return {}
const token = localStorage.getItem("bootnode_token")
const apiKey = localStorage.getItem("bootnode_api_key")
if (token) return { "Authorization": `Bearer ${token}` }
if (apiKey) return { "X-API-Key": apiKey }
return {}
}
async function searchNFTs() {
if (!address.trim()) return
setLoading(true)
setSearched(true)
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/v1/nfts/${chain}/owned/${address}`,
{ headers: getAuthHeaders() }
)
if (res.ok) {
const data = await res.json()
setNfts(Array.isArray(data) ? data : data.nfts || [])
} else {
setNfts([])
}
} catch (e) {
console.error("Failed to fetch NFTs:", e)
setNfts([])
} finally {
setLoading(false)
}
}
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-bold">NFTs</h1>
<p className="text-muted-foreground">
Browse NFT collections and ownership
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Search NFTs</CardTitle>
<CardDescription>
Enter a wallet address to view owned NFTs
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-4">
<select
value={chain}
onChange={(e) => setChain(e.target.value)}
className="px-3 py-2 border rounded-md bg-background"
>
<option value="ethereum">Ethereum</option>
<option value="polygon">Polygon</option>
<option value="arbitrum">Arbitrum</option>
<option value="optimism">Optimism</option>
<option value="base">Base</option>
</select>
<Input
placeholder="Enter wallet address (0x...)"
value={address}
onChange={(e) => setAddress(e.target.value)}
className="flex-1"
/>
<Button onClick={searchNFTs} disabled={loading || !address.trim()}>
{loading ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : <Search className="h-4 w-4 mr-2" />}
Search
</Button>
</div>
</CardContent>
</Card>
{loading ? (
<div className="flex items-center justify-center p-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : searched ? (
nfts.length === 0 ? (
<Card>
<CardContent className="text-center p-12 text-muted-foreground">
<Image className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p className="font-medium">No NFTs found</p>
<p className="text-sm">This address has no NFTs on {chain}</p>
</CardContent>
</Card>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{nfts.map((nft, idx) => (
<Card key={`${nft.contract}-${nft.tokenId}-${idx}`} className="overflow-hidden">
<div className="aspect-square bg-muted relative">
{nft.image ? (
<img
src={nft.image}
alt={nft.name || `NFT #${nft.tokenId}`}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<Image className="h-12 w-12 text-muted-foreground" />
</div>
)}
</div>
<CardContent className="p-4">
<h3 className="font-medium truncate">{nft.name || `Token #${nft.tokenId}`}</h3>
<p className="text-sm text-muted-foreground truncate font-mono">
{nft.contract.slice(0, 6)}...{nft.contract.slice(-4)}
</p>
<div className="flex items-center justify-between mt-2">
<Badge variant="outline">{chain}</Badge>
<Button variant="ghost" size="icon" asChild>
<a
href={`https://opensea.io/assets/${chain}/${nft.contract}/${nft.tokenId}`}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="h-4 w-4" />
</a>
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)
) : (
<Card>
<CardContent className="text-center p-12 text-muted-foreground">
<Image className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p className="font-medium">Search for NFTs</p>
<p className="text-sm">Enter a wallet address above to browse NFT holdings</p>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle>NFT API Endpoints</CardTitle>
<CardDescription>Available endpoints for NFT data</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{[
{ method: "GET", path: "/v1/nfts/{chain}/owned/{address}", description: "Get owned NFTs" },
{ method: "GET", path: "/v1/nfts/{chain}/collection/{contract}", description: "Get collection info" },
{ method: "GET", path: "/v1/nfts/{chain}/metadata/{contract}/{tokenId}", description: "Get NFT metadata" },
{ method: "POST", path: "/v1/nfts/{chain}/refresh/{contract}/{tokenId}", description: "Refresh metadata" },
].map((endpoint) => (
<div key={endpoint.path} className="flex items-center gap-3 p-3 border rounded-lg">
<Badge variant="default" className="font-mono text-xs w-12 justify-center">
{endpoint.method}
</Badge>
<code className="text-sm font-mono flex-1">{endpoint.path}</code>
<span className="text-sm text-muted-foreground">{endpoint.description}</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)
}
+4 -2
View File
@@ -53,8 +53,10 @@ export default function AssetsPage() {
<p className="text-sm text-muted-foreground mb-4">
Browse NFT collections, ownership, and metadata
</p>
<Button variant="outline" className="w-full">
Browse NFTs
<Button variant="outline" className="w-full" asChild>
<Link href="/dashboard/assets/nfts">
Browse NFTs
</Link>
</Button>
</CardContent>
</Card>
+1 -1
View File
@@ -38,7 +38,7 @@ export default function TokensPage() {
setToken(null)
try {
const res = await fetch(`http://localhost:8100/v1/tokens/${chain}/metadata/${contract}`, {
const res = await fetch(`http://localhost:8000/v1/tokens/${chain}/metadata/${contract}`, {
headers: getAuthHeaders()
})
if (!res.ok) {
@@ -46,7 +46,7 @@ export default function ClustersPage() {
async function fetchClusters() {
try {
const res = await fetch("http://localhost:8100/v1/infra/clusters", {
const res = await fetch("http://localhost:8000/v1/infra/clusters", {
headers: getAuthHeaders()
})
if (res.ok) {
@@ -70,7 +70,7 @@ export default function ClustersPage() {
if (!name.trim()) return
setCreating(true)
try {
const res = await fetch("http://localhost:8100/v1/infra/clusters", {
const res = await fetch("http://localhost:8000/v1/infra/clusters", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -99,7 +99,7 @@ export default function ClustersPage() {
async function downloadKubeconfig(clusterId: string) {
try {
const res = await fetch(`http://localhost:8100/v1/infra/clusters/${clusterId}/kubeconfig`, {
const res = await fetch(`http://localhost:8000/v1/infra/clusters/${clusterId}/kubeconfig`, {
headers: getAuthHeaders()
})
if (res.ok) {
@@ -0,0 +1,365 @@
"use client"
import { useState, useEffect } from "react"
import { useParams, useRouter } from "next/navigation"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import {
Server, ArrowLeft, Play, Square, Trash2, RefreshCcw, Copy, Check,
Activity, HardDrive, Cpu, MemoryStick, Network, Loader2, Terminal
} from "lucide-react"
interface NodeDetails {
id: string
name: string
chain: string
network: string
provider: string
status: string
endpoint: string | null
ws_endpoint?: string | null
beacon_api?: string | null
validator_api?: string | null
execution_client?: string
consensus_client?: string
mev_enabled?: boolean
containers?: string[]
created_at: string
metrics?: {
current_block?: number
highest_block?: number
sync_progress?: number
syncing?: boolean
peer_count?: number
chain_id?: number
cpu_usage?: number
memory_usage?: number
disk_usage?: number
network_rx?: number
network_tx?: number
}
error?: string
}
export default function NodeDetailPage() {
const params = useParams()
const router = useRouter()
const nodeId = params.id as string
const [node, setNode] = useState<NodeDetails | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [copied, setCopied] = useState<string | null>(null)
const [rpcInput, setRpcInput] = useState('{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}')
const [rpcResult, setRpcResult] = useState<string | null>(null)
const [rpcLoading, setRpcLoading] = useState(false)
function getAuthHeaders(): Record<string, string> {
if (typeof window === "undefined") return {}
const token = localStorage.getItem("bootnode_token")
const apiKey = localStorage.getItem("bootnode_api_key")
if (token) return { "Authorization": `Bearer ${token}` }
if (apiKey) return { "X-API-Key": apiKey }
return {}
}
async function fetchNode() {
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/v1/nodes/${nodeId}`,
{ headers: getAuthHeaders() }
)
if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`)
const data = await res.json()
setNode(data)
} catch (e) {
setError(String(e))
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchNode()
const interval = setInterval(fetchNode, 5000)
return () => clearInterval(interval)
}, [nodeId])
async function handleAction(action: "start" | "stop" | "delete") {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
try {
if (action === "delete") {
if (!confirm("Are you sure you want to delete this node?")) return
await fetch(`${apiUrl}/v1/nodes/${nodeId}`, {
method: "DELETE",
headers: getAuthHeaders()
})
router.push("/dashboard/infrastructure/nodes")
} else {
const res = await fetch(`${apiUrl}/v1/nodes/${nodeId}/${action}`, {
method: "POST",
headers: getAuthHeaders()
})
if (res.ok) fetchNode()
}
} catch (e) {
console.error(`Failed to ${action}:`, e)
}
}
async function sendRpcRequest() {
if (!node?.endpoint) return
setRpcLoading(true)
try {
const payload = JSON.parse(rpcInput)
const res = await fetch(node.endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
})
const data = await res.json()
setRpcResult(JSON.stringify(data, null, 2))
} catch (e) {
setRpcResult(`Error: ${e}`)
} finally {
setRpcLoading(false)
}
}
function copyToClipboard(text: string, key: string) {
navigator.clipboard.writeText(text)
setCopied(key)
setTimeout(() => setCopied(null), 2000)
}
if (loading) {
return (
<div className="flex items-center justify-center h-96">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
)
}
if (error || !node) {
return (
<div className="space-y-4">
<Button variant="ghost" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" /> Back
</Button>
<Card className="border-destructive">
<CardContent className="p-6">
<p className="text-destructive">{error || "Node not found"}</p>
</CardContent>
</Card>
</div>
)
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<Server className="h-6 w-6" />
{node.name}
</h1>
<div className="flex items-center gap-2 mt-1">
<Badge variant={node.status === "running" ? "default" : "secondary"}>
{node.status}
</Badge>
<Badge variant="outline">{node.chain}</Badge>
<Badge variant="outline">{node.network}</Badge>
{node.execution_client && <Badge variant="secondary">{node.execution_client}</Badge>}
{node.consensus_client && <Badge variant="secondary">{node.consensus_client}</Badge>}
{node.mev_enabled && <Badge className="bg-purple-500">MEV</Badge>}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={fetchNode}>
<RefreshCcw className="h-4 w-4 mr-2" /> Refresh
</Button>
{node.status === "running" ? (
<Button variant="outline" size="sm" onClick={() => handleAction("stop")}>
<Square className="h-4 w-4 mr-2" /> Stop
</Button>
) : (
<Button variant="outline" size="sm" onClick={() => handleAction("start")}>
<Play className="h-4 w-4 mr-2" /> Start
</Button>
)}
<Button variant="destructive" size="sm" onClick={() => handleAction("delete")}>
<Trash2 className="h-4 w-4 mr-2" /> Delete
</Button>
</div>
</div>
{/* Endpoints */}
<Card>
<CardHeader>
<CardTitle>Endpoints</CardTitle>
<CardDescription>Connection URLs for your node</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{node.endpoint && (
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
<div>
<p className="text-sm font-medium">RPC Endpoint</p>
<code className="text-sm text-muted-foreground">{node.endpoint}</code>
</div>
<Button variant="ghost" size="icon" onClick={() => copyToClipboard(node.endpoint!, "rpc")}>
{copied === "rpc" ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
)}
{node.ws_endpoint && (
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
<div>
<p className="text-sm font-medium">WebSocket Endpoint</p>
<code className="text-sm text-muted-foreground">{node.ws_endpoint}</code>
</div>
<Button variant="ghost" size="icon" onClick={() => copyToClipboard(node.ws_endpoint!, "ws")}>
{copied === "ws" ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
)}
{node.beacon_api && (
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
<div>
<p className="text-sm font-medium">Beacon API</p>
<code className="text-sm text-muted-foreground">{node.beacon_api}</code>
</div>
<Button variant="ghost" size="icon" onClick={() => copyToClipboard(node.beacon_api!, "beacon")}>
{copied === "beacon" ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
)}
</CardContent>
</Card>
{/* Metrics */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Activity className="h-4 w-4" /> Sync Status
</CardTitle>
</CardHeader>
<CardContent>
{node.metrics?.syncing ? (
<>
<p className="text-2xl font-bold">{node.metrics.sync_progress?.toFixed(1)}%</p>
<p className="text-xs text-muted-foreground">
Block {node.metrics.current_block?.toLocaleString()} / {node.metrics.highest_block?.toLocaleString()}
</p>
</>
) : node.metrics?.current_block ? (
<>
<p className="text-2xl font-bold text-green-500">Synced</p>
<p className="text-xs text-muted-foreground">Block {node.metrics.current_block?.toLocaleString()}</p>
</>
) : (
<p className="text-2xl font-bold text-muted-foreground">-</p>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Network className="h-4 w-4" /> Peers
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">{node.metrics?.peer_count ?? "-"}</p>
<p className="text-xs text-muted-foreground">Connected peers</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Cpu className="h-4 w-4" /> CPU
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">{node.metrics?.cpu_usage?.toFixed(1) ?? "-"}%</p>
<p className="text-xs text-muted-foreground">Usage</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<MemoryStick className="h-4 w-4" /> Memory
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">{node.metrics?.memory_usage?.toFixed(1) ?? "-"} GB</p>
<p className="text-xs text-muted-foreground">Used</p>
</CardContent>
</Card>
</div>
{/* RPC Console */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Terminal className="h-5 w-5" /> RPC Console
</CardTitle>
<CardDescription>Send JSON-RPC requests to your node</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="text-sm font-medium">Request</label>
<textarea
value={rpcInput}
onChange={(e) => setRpcInput(e.target.value)}
className="w-full h-24 mt-1 p-3 font-mono text-sm border rounded-lg bg-muted"
placeholder='{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
/>
</div>
<Button onClick={sendRpcRequest} disabled={rpcLoading || !node.endpoint}>
{rpcLoading ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : null}
Send Request
</Button>
{rpcResult && (
<div>
<label className="text-sm font-medium">Response</label>
<pre className="mt-1 p-3 font-mono text-sm bg-muted rounded-lg overflow-auto max-h-48">
{rpcResult}
</pre>
</div>
)}
</CardContent>
</Card>
{/* Containers */}
{node.containers && node.containers.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Containers</CardTitle>
<CardDescription>Docker containers for this node</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
{node.containers.map((cid) => (
<div key={cid} className="flex items-center gap-2 p-2 bg-muted rounded font-mono text-sm">
<Server className="h-4 w-4" />
{cid}
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
)
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -29,7 +29,7 @@ export default function InfrastructurePage() {
useEffect(() => {
async function fetchStats() {
try {
const res = await fetch("http://localhost:8100/v1/infra/stats", {
const res = await fetch("http://localhost:8000/v1/infra/stats", {
headers: getAuthHeaders()
})
if (res.ok) {
@@ -64,9 +64,9 @@ export default function StoragePage() {
try {
const headers = getAuthHeaders()
const [volRes, snapRes, cloneRes] = await Promise.all([
fetch("http://localhost:8100/v1/infra/volumes", { headers }),
fetch("http://localhost:8100/v1/infra/snapshots", { headers }),
fetch("http://localhost:8100/v1/infra/clone", { headers }),
fetch("http://localhost:8000/v1/infra/volumes", { headers }),
fetch("http://localhost:8000/v1/infra/snapshots", { headers }),
fetch("http://localhost:8000/v1/infra/clone", { headers }),
])
if (volRes.ok) setVolumes(await volRes.json())
@@ -87,7 +87,7 @@ export default function StoragePage() {
if (!cloneName.trim() || !sourceId) return
setCloning(true)
try {
const res = await fetch("http://localhost:8100/v1/infra/clone", {
const res = await fetch("http://localhost:8000/v1/infra/clone", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -43,7 +43,7 @@ export default function VolumesPage() {
async function fetchVolumes() {
try {
const res = await fetch("http://localhost:8100/v1/infra/volumes", {
const res = await fetch("http://localhost:8000/v1/infra/volumes", {
headers: getAuthHeaders()
})
if (res.ok) {
@@ -67,7 +67,7 @@ export default function VolumesPage() {
if (!name.trim()) return
setCreating(true)
try {
const res = await fetch("http://localhost:8100/v1/infra/volumes", {
const res = await fetch("http://localhost:8000/v1/infra/volumes", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -95,7 +95,7 @@ export default function VolumesPage() {
async function createSnapshot(volumeId: string, volumeName: string) {
try {
const res = await fetch("http://localhost:8100/v1/infra/snapshots", {
const res = await fetch("http://localhost:8000/v1/infra/snapshots", {
method: "POST",
headers: {
"Content-Type": "application/json",
+511
View File
@@ -0,0 +1,511 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import {
TrendingUp, TrendingDown, Server, Coins, Search, ArrowUpRight,
Loader2, Filter, ChevronRight, Zap, Shield, Globe, Star, Lock
} from "lucide-react"
import Link from "next/link"
interface StakingData {
apy: number
apy_change_7d: number
staking_ratio: number
staked_tokens: number
staked_tokens_change_7d: number
tvl: number
tvl_change_7d: number
validator_fee: number
min_stake: number
unbonding_days: number
}
interface ScoreBreakdown {
market_score: number
staking_score: number
security_score: number
adoption_score: number
tech_score: number
}
interface MarketAsset {
chain: string
name: string
symbol: string
image: string
price: number
price_change_24h: number
price_change_7d: number
market_cap: number
market_cap_rank: number
volume_24h: number
circulating_supply: number
ath: number
ath_change_percentage: number
apy: number
staking_ratio: number
staked_tokens: number
tvl: number
validator_fee: number
score: number
score_breakdown: ScoreBreakdown
supported: boolean
has_validator: boolean
has_mev: boolean
}
export default function MarketsPage() {
const [assets, setAssets] = useState<MarketAsset[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState("")
const [filter, setFilter] = useState<"all" | "high_yield" | "top_mcap" | "supported">("all")
const [sortBy, setSortBy] = useState<"score" | "apy" | "tvl" | "market_cap">("score")
function getAuthHeaders(): Record<string, string> {
if (typeof window === "undefined") return {}
const token = localStorage.getItem("bootnode_token")
const apiKey = localStorage.getItem("bootnode_api_key")
if (token) return { "Authorization": `Bearer ${token}` }
if (apiKey) return { "X-API-Key": apiKey }
return {}
}
async function fetchMarketData() {
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/v1/nodes/staking`,
{ headers: getAuthHeaders() }
)
if (res.ok) {
const data = await res.json()
setAssets(data.chains || [])
}
} catch (e) {
console.error("Failed to fetch market data:", e)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchMarketData()
// Refresh every 5 minutes
const interval = setInterval(fetchMarketData, 5 * 60 * 1000)
return () => clearInterval(interval)
}, [])
const filteredAssets = assets
.filter(asset => {
const matchesSearch = asset.chain.toLowerCase().includes(search.toLowerCase()) ||
asset.name.toLowerCase().includes(search.toLowerCase()) ||
asset.symbol.toLowerCase().includes(search.toLowerCase())
if (!matchesSearch) return false
switch (filter) {
case "high_yield":
return asset.apy >= 10
case "top_mcap":
return asset.market_cap >= 1e9
case "supported":
return asset.supported
default:
return true
}
})
.sort((a, b) => {
switch (sortBy) {
case "apy": return (b.apy || 0) - (a.apy || 0)
case "tvl": return (b.tvl || 0) - (a.tvl || 0)
case "market_cap": return (b.market_cap || 0) - (a.market_cap || 0)
default: return (b.score || 0) - (a.score || 0)
}
})
const totalTVL = assets.reduce((acc, c) => acc + (c.tvl || 0), 0)
const totalMarketCap = assets.reduce((acc, c) => acc + (c.market_cap || 0), 0)
const avgAPY = assets.length > 0
? assets.filter(c => c.apy > 0).reduce((acc, c) => acc + c.apy, 0) / assets.filter(c => c.apy > 0).length
: 0
const formatValue = (value: number, decimals = 2) => {
if (value >= 1e12) return `$${(value / 1e12).toFixed(decimals)}T`
if (value >= 1e9) return `$${(value / 1e9).toFixed(decimals)}B`
if (value >= 1e6) return `$${(value / 1e6).toFixed(decimals)}M`
if (value >= 1e3) return `$${(value / 1e3).toFixed(decimals)}K`
return `$${value.toFixed(decimals)}`
}
const formatTokens = (value: number) => {
if (value >= 1e9) return `${(value / 1e9).toFixed(2)}B`
if (value >= 1e6) return `${(value / 1e6).toFixed(2)}M`
if (value >= 1e3) return `${(value / 1e3).toFixed(2)}K`
return value.toFixed(2)
}
const getScoreColor = (score: number) => {
if (score >= 80) return "text-green-500"
if (score >= 60) return "text-yellow-500"
if (score >= 40) return "text-orange-500"
return "text-red-500"
}
return (
<div className="space-y-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Markets & Staking</h1>
<p className="text-muted-foreground">
Real-time staking rewards, TVL, and market data for {assets.length}+ networks
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={fetchMarketData}>
<Loader2 className={`mr-2 h-4 w-4 ${loading ? "animate-spin" : ""}`} />
Refresh
</Button>
<Button asChild>
<Link href="/dashboard/infrastructure/nodes">
<Server className="mr-2 h-4 w-4" />
Deploy Node
</Link>
</Button>
</div>
</div>
{/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-5">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Lock className="h-4 w-4" /> Total Value Locked
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{loading ? "..." : formatValue(totalTVL)}</div>
<p className="text-xs text-muted-foreground">Staked across networks</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Globe className="h-4 w-4" /> Total Market Cap
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{loading ? "..." : formatValue(totalMarketCap)}</div>
<p className="text-xs text-muted-foreground">Combined value</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<TrendingUp className="h-4 w-4" /> Average APY
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-500">
{loading ? "..." : `${avgAPY.toFixed(2)}%`}
</div>
<p className="text-xs text-muted-foreground">Mean reward rate</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Zap className="h-4 w-4" /> High Yield
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{loading ? "..." : assets.filter(c => c.apy >= 10).length}
</div>
<p className="text-xs text-muted-foreground">10%+ APY chains</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Shield className="h-4 w-4" /> Supported
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{loading ? "..." : assets.filter(c => c.supported).length}
</div>
<p className="text-xs text-muted-foreground">Ready to deploy</p>
</CardContent>
</Card>
</div>
{/* Top Opportunities */}
<Card className="bg-gradient-to-br from-primary/5 to-primary/10 border-primary/20">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Star className="h-5 w-5 text-yellow-500" />
Top Rated Staking Networks
</CardTitle>
<CardDescription>Based on market position, yield, security, and adoption</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-5">
{assets
.filter(c => c.supported && c.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map((asset, idx) => (
<Link
key={asset.chain}
href={`/dashboard/infrastructure/nodes?chain=${asset.chain}`}
className="flex flex-col items-center p-4 rounded-lg bg-background hover:bg-muted transition-colors border relative"
>
<Badge className="absolute -top-2 -right-2 bg-yellow-500">#{idx + 1}</Badge>
{asset.image ? (
<img src={asset.image} alt={asset.name} className="w-10 h-10 rounded-full mb-2" />
) : (
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center mb-2">
<Coins className="h-5 w-5" />
</div>
)}
<span className="font-medium text-sm">{asset.name}</span>
<span className={`text-lg font-bold ${getScoreColor(asset.score)}`}>{asset.score.toFixed(0)}</span>
<span className="text-xs text-muted-foreground">Score</span>
<div className="flex gap-2 mt-2 text-xs">
<span className="text-green-500">{asset.apy}% APY</span>
</div>
</Link>
))}
</div>
</CardContent>
</Card>
{/* Search, Filter, and Sort */}
<div className="flex flex-wrap gap-4 items-center">
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search by name, symbol, or chain..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10"
/>
</div>
<div className="flex gap-2">
<Button
variant={filter === "all" ? "default" : "outline"}
size="sm"
onClick={() => setFilter("all")}
>
All
</Button>
<Button
variant={filter === "supported" ? "default" : "outline"}
size="sm"
onClick={() => setFilter("supported")}
>
<Server className="mr-1 h-3 w-3" />
Supported
</Button>
<Button
variant={filter === "high_yield" ? "default" : "outline"}
size="sm"
onClick={() => setFilter("high_yield")}
>
<TrendingUp className="mr-1 h-3 w-3" />
10%+ APY
</Button>
<Button
variant={filter === "top_mcap" ? "default" : "outline"}
size="sm"
onClick={() => setFilter("top_mcap")}
>
<Coins className="mr-1 h-3 w-3" />
$1B+ TVL
</Button>
</div>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="px-3 py-2 border rounded-md bg-background text-sm"
>
<option value="score">Sort by Score</option>
<option value="apy">Sort by APY</option>
<option value="tvl">Sort by TVL</option>
<option value="market_cap">Sort by Market Cap</option>
</select>
</div>
{/* Assets Table */}
<Card>
<CardHeader>
<CardTitle>Staking Networks</CardTitle>
<CardDescription>
{filteredAssets.length} networks • Live data from CoinGecko
</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center p-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b text-left text-xs">
<th className="py-3 px-2 font-medium text-muted-foreground">#</th>
<th className="py-3 px-2 font-medium text-muted-foreground">Asset</th>
<th className="py-3 px-2 font-medium text-muted-foreground text-right">Price</th>
<th className="py-3 px-2 font-medium text-muted-foreground text-right">7d</th>
<th className="py-3 px-2 font-medium text-muted-foreground text-right">APY</th>
<th className="py-3 px-2 font-medium text-muted-foreground text-right">Fee</th>
<th className="py-3 px-2 font-medium text-muted-foreground text-right">Staked</th>
<th className="py-3 px-2 font-medium text-muted-foreground text-right">TVL</th>
<th className="py-3 px-2 font-medium text-muted-foreground text-center">Score</th>
<th className="py-3 px-2 font-medium text-muted-foreground"></th>
</tr>
</thead>
<tbody>
{filteredAssets.map((asset, idx) => (
<tr
key={asset.chain}
className="border-b hover:bg-muted/50 transition-colors"
>
<td className="py-3 px-2 text-muted-foreground text-sm">{idx + 1}</td>
<td className="py-3 px-2">
<div className="flex items-center gap-3">
{asset.image ? (
<img src={asset.image} alt={asset.name} className="w-8 h-8 rounded-full" />
) : (
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center">
<Coins className="h-4 w-4" />
</div>
)}
<div>
<p className="font-medium text-sm">{asset.name}</p>
<p className="text-xs text-muted-foreground">{asset.symbol}</p>
</div>
</div>
</td>
<td className="py-3 px-2 text-right font-mono text-sm">
${asset.price?.toLocaleString(undefined, { maximumFractionDigits: 4 }) || "-"}
</td>
<td className="py-3 px-2 text-right text-sm">
{asset.price_change_7d !== undefined && asset.price_change_7d !== 0 && (
<span className={asset.price_change_7d >= 0 ? "text-green-500" : "text-red-500"}>
{asset.price_change_7d >= 0 ? "+" : ""}{asset.price_change_7d.toFixed(1)}%
</span>
)}
</td>
<td className="py-3 px-2 text-right">
<span className={`font-bold text-sm ${asset.apy >= 10 ? "text-green-500" : asset.apy >= 5 ? "text-yellow-500" : ""}`}>
{asset.apy > 0 ? `${asset.apy.toFixed(2)}%` : "-"}
</span>
</td>
<td className="py-3 px-2 text-right text-sm text-muted-foreground">
{asset.validator_fee > 0 ? `${asset.validator_fee}%` : "-"}
</td>
<td className="py-3 px-2 text-right text-sm">
<div>
<p>{asset.staked_tokens > 0 ? formatTokens(asset.staked_tokens) : "-"}</p>
<p className="text-xs text-muted-foreground">
{asset.staking_ratio > 0 ? `${asset.staking_ratio.toFixed(1)}%` : ""}
</p>
</div>
</td>
<td className="py-3 px-2 text-right font-mono text-sm">
{asset.tvl > 0 ? formatValue(asset.tvl, 1) : "-"}
</td>
<td className="py-3 px-2 text-center">
<div className={`font-bold text-lg ${getScoreColor(asset.score || 0)}`}>
{asset.score > 0 ? asset.score.toFixed(0) : "-"}
</div>
</td>
<td className="py-3 px-2">
<div className="flex items-center gap-1">
{asset.has_validator && <Badge variant="secondary" className="text-xs">Val</Badge>}
{asset.has_mev && <Badge className="text-xs bg-purple-500">MEV</Badge>}
{asset.supported && (
<Button variant="ghost" size="sm" asChild>
<Link href={`/dashboard/infrastructure/nodes?chain=${asset.chain}`}>
<ChevronRight className="h-4 w-4" />
</Link>
</Button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
{/* Score Explanation */}
<Card>
<CardHeader>
<CardTitle>How We Score Networks</CardTitle>
<CardDescription>Our scoring algorithm evaluates networks across 5 dimensions</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-5">
<div className="p-4 border rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Globe className="h-4 w-4" />
<span className="font-medium">Market (25pts)</span>
</div>
<p className="text-sm text-muted-foreground">
Market cap ranking and liquidity depth
</p>
</div>
<div className="p-4 border rounded-lg">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="h-4 w-4" />
<span className="font-medium">Staking (25pts)</span>
</div>
<p className="text-sm text-muted-foreground">
APY attractiveness and network participation
</p>
</div>
<div className="p-4 border rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Shield className="h-4 w-4" />
<span className="font-medium">Security (20pts)</span>
</div>
<p className="text-sm text-muted-foreground">
Network maturity and economic security
</p>
</div>
<div className="p-4 border rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Coins className="h-4 w-4" />
<span className="font-medium">Adoption (15pts)</span>
</div>
<p className="text-sm text-muted-foreground">
Trading volume and usage metrics
</p>
</div>
<div className="p-4 border rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Zap className="h-4 w-4" />
<span className="font-medium">Tech (15pts)</span>
</div>
<p className="text-sm text-muted-foreground">
Price strength vs ATH and momentum
</p>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
+2 -2
View File
@@ -36,8 +36,8 @@ export default function OrganizationPage() {
const headers = getAuthHeaders()
const [teamRes, keysRes] = await Promise.all([
fetch("http://localhost:8100/v1/team", { headers }).catch(() => null),
fetch("http://localhost:8100/v1/auth/keys", { headers }).catch(() => null),
fetch("http://localhost:8000/v1/team", { headers }).catch(() => null),
fetch("http://localhost:8000/v1/auth/keys", { headers }).catch(() => null),
])
const teamData = teamRes?.ok ? await teamRes.json() : { members: [], total: 0 }
@@ -80,7 +80,7 @@ export default function OrganizationSettingsPage() {
try {
// In production, this would call the API
// await fetch("http://localhost:8100/v1/organization/settings", {
// await fetch("http://localhost:8000/v1/organization/settings", {
// method: "PATCH",
// headers: { ...getAuthHeaders(), "Content-Type": "application/json" },
// body: JSON.stringify({ name: formData.name })
+5 -5
View File
@@ -67,11 +67,11 @@ export default function DashboardPage() {
// Fetch multiple endpoints in parallel
const [teamRes, keysRes, webhooksRes, chainsRes, nodesRes] = await Promise.all([
fetch("http://localhost:8100/v1/team", { headers }).catch(() => null),
fetch("http://localhost:8100/v1/auth/keys", { headers }).catch(() => null),
fetch("http://localhost:8100/v1/webhooks", { headers }).catch(() => null),
fetch("http://localhost:8100/v1/chains", { headers }).catch(() => null),
fetch("http://localhost:8100/v1/nodes/", { headers }).catch(() => null),
fetch("http://localhost:8000/v1/team", { headers }).catch(() => null),
fetch("http://localhost:8000/v1/auth/keys", { headers }).catch(() => null),
fetch("http://localhost:8000/v1/webhooks", { headers }).catch(() => null),
fetch("http://localhost:8000/v1/chains", { headers }).catch(() => null),
fetch("http://localhost:8000/v1/nodes/", { headers }).catch(() => null),
])
// Parse responses
+1 -1
View File
@@ -38,7 +38,7 @@ export default function SettingsPage() {
setProjectName(localStorage.getItem("bootnode_project_name") || "")
setProjectDescription(localStorage.getItem("bootnode_project_description") || "")
setApiKey(localStorage.getItem("bootnode_api_key") || "")
setApiUrl(process.env.NEXT_PUBLIC_API_URL || "http://localhost:8100")
setApiUrl(process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000")
}, [])
function handleSaveProject() {
+422
View File
@@ -0,0 +1,422 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import {
Wallet, Key, Shield, Server, Coins, Lock, Unlock, ExternalLink,
Loader2, ChevronRight, Zap, AlertTriangle, Check, Copy, RefreshCcw
} from "lucide-react"
import Link from "next/link"
interface Validator {
id: string
chain: string
address: string
stake: number
rewards: number
status: "active" | "pending" | "inactive"
uptime: number
commission: number
}
interface WalletState {
connected: boolean
address: string | null
chain: string | null
balance: number
kmsConfigured: boolean
}
export default function StakingPage() {
const [wallet, setWallet] = useState<WalletState>({
connected: false,
address: null,
chain: null,
balance: 0,
kmsConfigured: false,
})
const [validators, setValidators] = useState<Validator[]>([])
const [loading, setLoading] = useState(false)
const [copied, setCopied] = useState(false)
function getAuthHeaders(): Record<string, string> {
if (typeof window === "undefined") return {}
const token = localStorage.getItem("bootnode_token")
const apiKey = localStorage.getItem("bootnode_api_key")
if (token) return { "Authorization": `Bearer ${token}` }
if (apiKey) return { "X-API-Key": apiKey }
return {}
}
async function connectWallet(type: "metamask" | "ledger" | "kms") {
setLoading(true)
try {
if (type === "metamask") {
// MetaMask/EIP-1193 connection
if (typeof window !== "undefined" && (window as any).ethereum) {
const accounts = await (window as any).ethereum.request({
method: "eth_requestAccounts",
})
if (accounts.length > 0) {
const chainId = await (window as any).ethereum.request({
method: "eth_chainId",
})
const balance = await (window as any).ethereum.request({
method: "eth_getBalance",
params: [accounts[0], "latest"],
})
setWallet({
connected: true,
address: accounts[0],
chain: parseInt(chainId, 16) === 1 ? "ethereum" : `chain-${parseInt(chainId, 16)}`,
balance: parseInt(balance, 16) / 1e18,
kmsConfigured: false,
})
}
}
} else if (type === "ledger") {
// Ledger connection would use @ledgerhq/hw-transport-webusb
alert("Ledger connection requires hardware wallet. Please connect your Ledger device.")
} else if (type === "kms") {
// Check if KMS (Hanzo KSM) is configured
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/v1/keys/status`,
{ headers: getAuthHeaders() }
)
if (res.ok) {
const data = await res.json()
setWallet({
connected: true,
address: data.default_address || "KMS Managed",
chain: data.default_chain || "multi-chain",
balance: data.total_balance || 0,
kmsConfigured: true,
})
} else {
alert("KMS not configured. Please configure Hanzo KSM first.")
}
}
} catch (e) {
console.error("Failed to connect wallet:", e)
} finally {
setLoading(false)
}
}
function disconnectWallet() {
setWallet({
connected: false,
address: null,
chain: null,
balance: 0,
kmsConfigured: false,
})
}
function copyAddress() {
if (wallet.address) {
navigator.clipboard.writeText(wallet.address)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
}
return (
<div className="space-y-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Staking Dashboard</h1>
<p className="text-muted-foreground">
Manage validators, stake tokens, and earn rewards
</p>
</div>
<Button asChild variant="outline">
<Link href="/dashboard/markets">
<Coins className="mr-2 h-4 w-4" />
View Markets
</Link>
</Button>
</div>
{/* Wallet Connection */}
<Card className={wallet.connected ? "border-green-500/50 bg-green-500/5" : ""}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Wallet className="h-5 w-5" />
{wallet.connected ? "Wallet Connected" : "Connect Wallet"}
</CardTitle>
<CardDescription>
{wallet.connected
? "Your wallet is connected and ready for staking"
: "Connect a wallet to start staking across multiple chains"}
</CardDescription>
</CardHeader>
<CardContent>
{wallet.connected ? (
<div className="space-y-4">
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center">
{wallet.kmsConfigured ? <Key className="h-5 w-5" /> : <Wallet className="h-5 w-5" />}
</div>
<div>
<p className="font-mono text-sm">
{wallet.address?.slice(0, 8)}...{wallet.address?.slice(-6)}
</p>
<p className="text-xs text-muted-foreground">
{wallet.chain} {wallet.kmsConfigured && "• KMS Secured"}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" onClick={copyAddress}>
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
<Button variant="outline" size="sm" onClick={disconnectWallet}>
Disconnect
</Button>
</div>
</div>
<div className="grid gap-4 md:grid-cols-3">
<div className="p-4 bg-muted rounded-lg">
<p className="text-sm text-muted-foreground">Balance</p>
<p className="text-2xl font-bold">{wallet.balance.toFixed(4)} ETH</p>
</div>
<div className="p-4 bg-muted rounded-lg">
<p className="text-sm text-muted-foreground">Staked</p>
<p className="text-2xl font-bold">0 ETH</p>
</div>
<div className="p-4 bg-muted rounded-lg">
<p className="text-sm text-muted-foreground">Rewards</p>
<p className="text-2xl font-bold text-green-500">0 ETH</p>
</div>
</div>
</div>
) : (
<div className="grid gap-4 md:grid-cols-3">
<Button
variant="outline"
className="h-24 flex-col gap-2"
onClick={() => connectWallet("metamask")}
disabled={loading}
>
<Wallet className="h-8 w-8" />
<span>MetaMask / Browser</span>
</Button>
<Button
variant="outline"
className="h-24 flex-col gap-2"
onClick={() => connectWallet("ledger")}
disabled={loading}
>
<Shield className="h-8 w-8" />
<span>Ledger Hardware</span>
</Button>
<Button
variant="outline"
className="h-24 flex-col gap-2 border-primary/50 bg-primary/5"
onClick={() => connectWallet("kms")}
disabled={loading}
>
<Key className="h-8 w-8" />
<span>Hanzo KMS</span>
<Badge variant="secondary" className="text-xs">Recommended</Badge>
</Button>
</div>
)}
</CardContent>
</Card>
{/* KMS Security Notice */}
<Card className="border-blue-500/50 bg-blue-500/5">
<CardContent className="p-4">
<div className="flex items-start gap-3">
<Shield className="h-5 w-5 text-blue-500 mt-0.5" />
<div>
<p className="font-medium">Enterprise Key Management</p>
<p className="text-sm text-muted-foreground">
For production deployments, use Hanzo KMS (Key Management System) with HSM backing.
Supports multi-sig, role-based access, and hardware security modules for maximum security.
</p>
<div className="flex gap-2 mt-2">
<Button variant="outline" size="sm" asChild>
<Link href="/dashboard/settings/keys">
<Key className="mr-2 h-4 w-4" />
Configure KMS
</Link>
</Button>
<Button variant="ghost" size="sm" asChild>
<a href="https://docs.hanzo.ai/kms" target="_blank" rel="noopener noreferrer">
Documentation <ExternalLink className="ml-1 h-3 w-3" />
</a>
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Active Validators */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Your Validators</CardTitle>
<CardDescription>Active validators across all chains</CardDescription>
</div>
<Button asChild>
<Link href="/dashboard/infrastructure/nodes">
<Server className="mr-2 h-4 w-4" />
Deploy Validator
</Link>
</Button>
</div>
</CardHeader>
<CardContent>
{validators.length === 0 ? (
<div className="text-center p-12 text-muted-foreground">
<Server className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p className="font-medium">No validators yet</p>
<p className="text-sm">Deploy a staking node to start earning rewards</p>
<Button className="mt-4" asChild>
<Link href="/dashboard/infrastructure/nodes">
Get Started
</Link>
</Button>
</div>
) : (
<div className="space-y-4">
{validators.map((v) => (
<div key={v.id} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-4">
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center">
<Server className="h-5 w-5" />
</div>
<div>
<p className="font-medium">{v.chain} Validator</p>
<p className="text-xs text-muted-foreground font-mono">{v.address}</p>
</div>
</div>
<div className="flex items-center gap-6">
<div className="text-right">
<p className="font-medium">{v.stake} staked</p>
<p className="text-xs text-green-500">+{v.rewards} earned</p>
</div>
<Badge variant={v.status === "active" ? "default" : "secondary"}>
{v.status}
</Badge>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Quick Actions */}
<div className="grid gap-6 md:grid-cols-3">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Zap className="h-5 w-5 text-yellow-500" />
Quick Stake
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
One-click staking deployment with optimized settings for maximum yield.
</p>
<Button className="w-full" disabled={!wallet.connected}>
{wallet.connected ? "Stake Now" : "Connect Wallet First"}
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<RefreshCcw className="h-5 w-5 text-blue-500" />
Auto-Compound
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
Automatically reinvest staking rewards for compound growth.
</p>
<Button variant="outline" className="w-full" disabled={!wallet.connected}>
Configure
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Lock className="h-5 w-5 text-purple-500" />
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
Stake while keeping liquidity with stETH, rETH, and more derivatives.
</p>
<Button variant="outline" className="w-full">
Coming Soon
</Button>
</CardContent>
</Card>
</div>
{/* Security Best Practices */}
<Card>
<CardHeader>
<CardTitle>Security Best Practices</CardTitle>
<CardDescription>Protect your staking infrastructure</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2">
<div className="flex items-start gap-3">
<Check className="h-5 w-5 text-green-500 mt-0.5" />
<div>
<p className="font-medium">Use Hardware Security Modules (HSM)</p>
<p className="text-sm text-muted-foreground">
Store validator keys in tamper-resistant hardware
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Check className="h-5 w-5 text-green-500 mt-0.5" />
<div>
<p className="font-medium">Enable Multi-Signature</p>
<p className="text-sm text-muted-foreground">
Require multiple approvals for sensitive operations
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Check className="h-5 w-5 text-green-500 mt-0.5" />
<div>
<p className="font-medium">Monitor Slashing Conditions</p>
<p className="text-sm text-muted-foreground">
Set up alerts for double-signing and downtime risks
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Check className="h-5 w-5 text-green-500 mt-0.5" />
<div>
<p className="font-medium">Use Kubernetes Operators</p>
<p className="text-sm text-muted-foreground">
Deploy with production-grade orchestration
</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
+1 -1
View File
@@ -35,7 +35,7 @@ export default function UsagePage() {
<CardContent>
<Badge variant="default">Connected</Badge>
<p className="text-xs text-muted-foreground mt-1">
http://localhost:8100
http://localhost:8000
</p>
</CardContent>
</Card>
+3 -2
View File
@@ -3,10 +3,11 @@ import { Badge } from "@/components/ui/badge"
import { Card, CardContent } 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: "Account Abstraction",
description: "Build with ERC-4337 account abstraction on Bootnode.",
description: `Build with ERC-4337 account abstraction on ${docsConfig.brandName}.`,
}
export default function AAQuickstartPage() {
@@ -74,7 +75,7 @@ export default function AAQuickstartPage() {
automatically when the first UserOperation is sent.
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`const BASE_URL = "https://api.bootnode.dev/v1";
<code>{`const BASE_URL = "${docsConfig.apiUrl}/v1";
const API_KEY = process.env.BOOTNODE_API_KEY!;
// Create a smart account
+26 -25
View File
@@ -1,10 +1,11 @@
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { DocsLayout } from "@/components/docs-layout"
import { docsConfig } from "@/lib/docs-config"
export const metadata = {
title: "API Reference",
description: "Complete API reference for all Bootnode endpoints.",
description: `Complete API reference for all ${docsConfig.brandName} endpoints.`,
}
function Endpoint({
@@ -94,7 +95,7 @@ export default function ApiReferencePage() {
<div>
<h1 className="text-4xl font-bold mb-4">API Reference</h1>
<p className="text-lg text-muted-foreground">
Complete reference for all Bootnode REST and JSON-RPC endpoints.
Complete reference for all {docsConfig.brandName} REST and JSON-RPC endpoints.
</p>
</div>
@@ -108,13 +109,13 @@ export default function ApiReferencePage() {
<div className="border rounded-lg p-4">
<p className="font-medium text-sm mb-1">API Key Header</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`X-API-Key: bn_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6`}</code>
<code>{`X-API-Key: ${docsConfig.apiKeyPrefix}live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6`}</code>
</pre>
</div>
<div className="border rounded-lg p-4">
<p className="font-medium text-sm mb-1">Bearer Token</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`Authorization: Bearer bn_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6`}</code>
<code>{`Authorization: Bearer ${docsConfig.apiKeyPrefix}live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6`}</code>
</pre>
</div>
</div>
@@ -124,7 +125,7 @@ export default function ApiReferencePage() {
<section className="space-y-4">
<h2 className="text-2xl font-semibold">Base URL</h2>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`https://api.bootnode.dev/v1`}</code>
<code>{`${docsConfig.apiUrl}/v1`}</code>
</pre>
</section>
@@ -180,7 +181,7 @@ X-RateLimit-Reset: 1706400000`}</code>
{ name: "chain", type: "string", description: "Chain identifier (ethereum, polygon, solana, base, arbitrum, etc.)" },
{ name: "network", type: "string", description: "Network name (mainnet, sepolia, devnet, etc.)" },
]}
request={`curl -X POST https://api.bootnode.dev/v1/rpc/ethereum/mainnet \\
request={`curl -X POST ${docsConfig.apiUrl}/v1/rpc/ethereum/mainnet \\
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
@@ -211,7 +212,7 @@ X-RateLimit-Reset: 1706400000`}</code>
{ name: "page", type: "integer", description: "Page number for pagination (default: 1)", required: false },
{ name: "limit", type: "integer", description: "Results per page, max 100 (default: 50)", required: false },
]}
request={`curl https://api.bootnode.dev/v1/tokens/ethereum/balances/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 \\
request={`curl ${docsConfig.apiUrl}/v1/tokens/ethereum/balances/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 \\
-H "X-API-Key: YOUR_API_KEY"`}
response={`{
"address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
@@ -250,7 +251,7 @@ X-RateLimit-Reset: 1706400000`}</code>
{ name: "chain", type: "string", description: "Chain identifier" },
{ name: "contract", type: "string", description: "Token contract address (0x-prefixed hex)" },
]}
request={`curl https://api.bootnode.dev/v1/tokens/ethereum/metadata/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \\
request={`curl ${docsConfig.apiUrl}/v1/tokens/ethereum/metadata/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \\
-H "X-API-Key: YOUR_API_KEY"`}
response={`{
"contract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
@@ -281,7 +282,7 @@ X-RateLimit-Reset: 1706400000`}</code>
{ name: "contract", type: "string", description: "NFT contract address" },
{ name: "token_id", type: "string", description: "Token ID" },
]}
request={`curl https://api.bootnode.dev/v1/nfts/ethereum/metadata/0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D/1234 \\
request={`curl ${docsConfig.apiUrl}/v1/nfts/ethereum/metadata/0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D/1234 \\
-H "X-API-Key: YOUR_API_KEY"`}
response={`{
"contract": "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D",
@@ -308,7 +309,7 @@ X-RateLimit-Reset: 1706400000`}</code>
{ name: "chain", type: "string", description: "Chain identifier" },
{ name: "contract", type: "string", description: "NFT contract address" },
]}
request={`curl https://api.bootnode.dev/v1/nfts/ethereum/collection/0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D \\
request={`curl ${docsConfig.apiUrl}/v1/nfts/ethereum/collection/0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D \\
-H "X-API-Key: YOUR_API_KEY"`}
response={`{
"contract": "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D",
@@ -335,7 +336,7 @@ X-RateLimit-Reset: 1706400000`}</code>
{ name: "page", type: "integer", description: "Page number (default: 1)", required: false },
{ name: "limit", type: "integer", description: "Results per page, max 100 (default: 50)", required: false },
]}
request={`curl https://api.bootnode.dev/v1/nfts/ethereum/owned/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 \\
request={`curl ${docsConfig.apiUrl}/v1/nfts/ethereum/owned/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 \\
-H "X-API-Key: YOUR_API_KEY"`}
response={`{
"address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
@@ -370,7 +371,7 @@ X-RateLimit-Reset: 1706400000`}</code>
{ name: "chain", type: "string", description: "Chain to deploy on (ethereum, base, polygon, etc.)" },
{ name: "salt", type: "string", description: "Optional salt for deterministic address generation", required: false },
]}
request={`curl -X POST https://api.bootnode.dev/v1/wallets/create \\
request={`curl -X POST ${docsConfig.apiUrl}/v1/wallets/create \\
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
@@ -393,7 +394,7 @@ X-RateLimit-Reset: 1706400000`}</code>
params={[
{ name: "address", type: "string", description: "Smart wallet address" },
]}
request={`curl https://api.bootnode.dev/v1/wallets/0x7A0b3e4C5F1234567890abcdef1234567890ABCD \\
request={`curl ${docsConfig.apiUrl}/v1/wallets/0x7A0b3e4C5F1234567890abcdef1234567890ABCD \\
-H "X-API-Key: YOUR_API_KEY"`}
response={`{
"address": "0x7A0b3e4C5F1234567890abcdef1234567890ABCD",
@@ -416,7 +417,7 @@ X-RateLimit-Reset: 1706400000`}</code>
{ name: "limit", type: "integer", description: "Results per page, max 100 (default: 25)", required: false },
{ name: "chain", type: "string", description: "Filter by chain", required: false },
]}
request={`curl https://api.bootnode.dev/v1/wallets?chain=ethereum&limit=10 \\
request={`curl ${docsConfig.apiUrl}/v1/wallets?chain=ethereum&limit=10 \\
-H "X-API-Key: YOUR_API_KEY"`}
response={`{
"wallets": [
@@ -442,14 +443,14 @@ X-RateLimit-Reset: 1706400000`}</code>
<Endpoint
method="POST"
path="/v1/webhooks"
description="Create a new webhook subscription. Bootnode will POST events to your URL as they occur on-chain."
description={`Create a new webhook subscription. ${docsConfig.brandName} will POST events to your URL as they occur on-chain.`}
params={[
{ name: "url", type: "string", description: "HTTPS endpoint to receive webhook events" },
{ name: "chain", type: "string", description: "Chain to monitor" },
{ name: "event_type", type: "string", description: "Event type: address_activity, token_transfer, nft_transfer, mined_transaction, log" },
{ name: "config", type: "object", description: "Event-specific filter configuration", required: false },
]}
request={`curl -X POST https://api.bootnode.dev/v1/webhooks \\
request={`curl -X POST ${docsConfig.apiUrl}/v1/webhooks \\
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
@@ -482,7 +483,7 @@ X-RateLimit-Reset: 1706400000`}</code>
{ name: "limit", type: "integer", description: "Results per page, max 100 (default: 25)", required: false },
{ name: "status", type: "string", description: "Filter by status: active, paused, disabled", required: false },
]}
request={`curl https://api.bootnode.dev/v1/webhooks \\
request={`curl ${docsConfig.apiUrl}/v1/webhooks \\
-H "X-API-Key: YOUR_API_KEY"`}
response={`{
"webhooks": [
@@ -509,7 +510,7 @@ X-RateLimit-Reset: 1706400000`}</code>
{ name: "status", type: "string", description: "Set status: active, paused", required: false },
{ name: "config", type: "object", description: "Updated filter configuration", required: false },
]}
request={`curl -X PATCH https://api.bootnode.dev/v1/webhooks/wh_a1b2c3d4e5f6 \\
request={`curl -X PATCH ${docsConfig.apiUrl}/v1/webhooks/wh_a1b2c3d4e5f6 \\
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
@@ -531,7 +532,7 @@ X-RateLimit-Reset: 1706400000`}</code>
params={[
{ name: "id", type: "string", description: "Webhook ID" },
]}
request={`curl -X DELETE https://api.bootnode.dev/v1/webhooks/wh_a1b2c3d4e5f6 \\
request={`curl -X DELETE ${docsConfig.apiUrl}/v1/webhooks/wh_a1b2c3d4e5f6 \\
-H "X-API-Key: YOUR_API_KEY"`}
response={`{
"id": "wh_a1b2c3d4e5f6",
@@ -552,7 +553,7 @@ X-RateLimit-Reset: 1706400000`}</code>
params={[
{ name: "chain", type: "string", description: "Chain identifier (ethereum, polygon, base, etc.)" },
]}
request={`curl https://api.bootnode.dev/v1/gas/ethereum/prices \\
request={`curl ${docsConfig.apiUrl}/v1/gas/ethereum/prices \\
-H "X-API-Key: YOUR_API_KEY"`}
response={`{
"chain": "ethereum",
@@ -581,13 +582,13 @@ X-RateLimit-Reset: 1706400000`}</code>
<Endpoint
method="POST"
path="/v1/gas/policies"
description="Create a gas sponsorship policy. Policies define rules for when Bootnode will pay gas fees on behalf of your users."
description={`Create a gas sponsorship policy. Policies define rules for when ${docsConfig.brandName} will pay gas fees on behalf of your users.`}
params={[
{ name: "name", type: "string", description: "Human-readable policy name" },
{ name: "chain", type: "string", description: "Chain this policy applies to" },
{ name: "rules", type: "object", description: "Sponsorship rules (max_gas_usd, allowed_contracts, rate_limit, etc.)" },
]}
request={`curl -X POST https://api.bootnode.dev/v1/gas/policies \\
request={`curl -X POST ${docsConfig.apiUrl}/v1/gas/policies \\
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
@@ -627,7 +628,7 @@ X-RateLimit-Reset: 1706400000`}</code>
{ name: "policy_id", type: "string", description: "Gas policy ID" },
{ name: "user_operation", type: "object", description: "The UserOperation to sponsor" },
]}
request={`curl -X POST https://api.bootnode.dev/v1/gas/sponsor \\
request={`curl -X POST ${docsConfig.apiUrl}/v1/gas/sponsor \\
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
@@ -666,7 +667,7 @@ X-RateLimit-Reset: 1706400000`}</code>
{ name: "chain", type: "string", description: "Chain identifier" },
{ name: "network", type: "string", description: "Network name (mainnet, sepolia, etc.)" },
]}
request={`curl -X POST https://api.bootnode.dev/v1/bundler/ethereum/mainnet \\
request={`curl -X POST ${docsConfig.apiUrl}/v1/bundler/ethereum/mainnet \\
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
@@ -709,7 +710,7 @@ X-RateLimit-Reset: 1706400000`}</code>
{ name: "chain", type: "string", description: "Chain identifier" },
{ name: "network", type: "string", description: "Network name (mainnet, sepolia, etc.)" },
]}
request={`wscat -c "wss://api.bootnode.dev/v1/ws/ethereum/mainnet?apiKey=YOUR_API_KEY"
request={`wscat -c "${docsConfig.wsUrl}/v1/ws/ethereum/mainnet?apiKey=YOUR_API_KEY"
> {"jsonrpc":"2.0","id":1,"method":"eth_subscribe","params":["newHeads"]}`}
response={`{"jsonrpc":"2.0","id":1,"result":"0x1a2b3c4d"}
+23 -22
View File
@@ -3,10 +3,11 @@ import { Badge } from "@/components/ui/badge"
import { Card, CardContent } 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: "Authentication",
description: "Learn how to authenticate with the Bootnode API.",
description: `Learn how to authenticate with the ${docsConfig.brandName} API.`,
}
export default function AuthQuickstartPage() {
@@ -17,7 +18,7 @@ export default function AuthQuickstartPage() {
<div>
<h1 className="text-4xl font-bold mb-4">Authentication</h1>
<p className="text-lg text-muted-foreground">
Every request to the Bootnode API must be authenticated. This guide covers
Every request to the {docsConfig.brandName} API must be authenticated. This guide covers
how to get your API key, use it in requests, handle rate limits, and manage
errors.
</p>
@@ -30,9 +31,9 @@ export default function AuthQuickstartPage() {
<h2 className="text-2xl font-semibold">Get Your API Key</h2>
</div>
<p className="text-muted-foreground">
Sign in to your Bootnode dashboard at{" "}
<Link href="https://dashboard.bootnode.dev" className="text-primary underline underline-offset-4">
dashboard.bootnode.dev
Sign in to your {docsConfig.brandName} dashboard at{" "}
<Link href={docsConfig.dashboardUrl} className="text-primary underline underline-offset-4">
{docsConfig.dashboardUrl.replace("https://", "")}
</Link>{" "}
and navigate to <strong>Settings &rarr; API Keys</strong>.
</p>
@@ -50,12 +51,12 @@ export default function AuthQuickstartPage() {
</thead>
<tbody>
<tr className="border-b">
<td className="p-3 font-mono text-xs">bn_live_</td>
<td className="p-3 font-mono text-xs">${docsConfig.apiKeyPrefix}live_</td>
<td className="p-3">Production</td>
<td className="p-3 text-muted-foreground">Live mainnet requests</td>
</tr>
<tr>
<td className="p-3 font-mono text-xs">bn_test_</td>
<td className="p-3 font-mono text-xs">${docsConfig.apiKeyPrefix}test_</td>
<td className="p-3">Testnet</td>
<td className="p-3 text-muted-foreground">Development and testing (testnets only)</td>
</tr>
@@ -75,7 +76,7 @@ export default function AuthQuickstartPage() {
</Card>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`# .env.local (Next.js, Vite, etc.)
BOOTNODE_API_KEY=bn_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
BOOTNODE_API_KEY=${docsConfig.apiKeyPrefix}live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
# Never commit this file to version control.
# Add .env.local to your .gitignore.`}</code>
@@ -94,11 +95,11 @@ BOOTNODE_API_KEY=bn_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`# curl
curl https://api.bootnode.dev/v1/gas/ethereum/prices \\
-H "X-API-Key: bn_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
curl ${docsConfig.apiUrl}/v1/gas/ethereum/prices \\
-H "X-API-Key: ${docsConfig.apiKeyPrefix}live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
# TypeScript / fetch
const res = await fetch("https://api.bootnode.dev/v1/gas/ethereum/prices", {
const res = await fetch("${docsConfig.apiUrl}/v1/gas/ethereum/prices", {
headers: {
"X-API-Key": process.env.BOOTNODE_API_KEY!,
},
@@ -107,13 +108,13 @@ const res = await fetch("https://api.bootnode.dev/v1/gas/ethereum/prices", {
# Python / requests
import requests
res = requests.get(
"https://api.bootnode.dev/v1/gas/ethereum/prices",
headers={"X-API-Key": "bn_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"},
"${docsConfig.apiUrl}/v1/gas/ethereum/prices",
headers={"X-API-Key": "${docsConfig.apiKeyPrefix}live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"},
)
# Go / net/http
req, _ := http.NewRequest("GET", "https://api.bootnode.dev/v1/gas/ethereum/prices", nil)
req.Header.Set("X-API-Key", "bn_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6")`}</code>
req, _ := http.NewRequest("GET", "${docsConfig.apiUrl}/v1/gas/ethereum/prices", nil)
req.Header.Set("X-API-Key", "${docsConfig.apiKeyPrefix}live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6")`}</code>
</pre>
</section>
@@ -128,11 +129,11 @@ req.Header.Set("X-API-Key", "bn_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6")`}</code>
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`# curl
curl https://api.bootnode.dev/v1/gas/ethereum/prices \\
-H "Authorization: Bearer bn_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
curl ${docsConfig.apiUrl}/v1/gas/ethereum/prices \\
-H "Authorization: Bearer ${docsConfig.apiKeyPrefix}live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
# TypeScript / fetch
const res = await fetch("https://api.bootnode.dev/v1/gas/ethereum/prices", {
const res = await fetch("${docsConfig.apiUrl}/v1/gas/ethereum/prices", {
headers: {
Authorization: \`Bearer \${process.env.BOOTNODE_API_KEY}\`,
},
@@ -141,8 +142,8 @@ const res = await fetch("https://api.bootnode.dev/v1/gas/ethereum/prices", {
# Python / requests
import requests
res = requests.get(
"https://api.bootnode.dev/v1/gas/ethereum/prices",
headers={"Authorization": "Bearer bn_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"},
"${docsConfig.apiUrl}/v1/gas/ethereum/prices",
headers={"Authorization": "Bearer ${docsConfig.apiKeyPrefix}live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"},
)`}</code>
</pre>
</section>
@@ -212,7 +213,7 @@ Retry-After: 1 # Seconds to wait (only on 429)`}</code>
<h2 className="text-2xl font-semibold">Error Handling</h2>
</div>
<p className="text-muted-foreground">
Bootnode returns structured error responses. Always check the HTTP status code
{docsConfig.brandName} returns structured error responses. Always check the HTTP status code
and handle errors gracefully with retries for transient failures.
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
@@ -225,7 +226,7 @@ async function bootnodeRequest(
const baseDelay = 1000; // 1 second
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(\`https://api.bootnode.dev/v1\${path}\`, {
const res = await fetch(\`${docsConfig.apiUrl}/v1\${path}\`, {
...options,
headers: {
"X-API-Key": process.env.BOOTNODE_API_KEY!,
+6 -5
View File
@@ -1,10 +1,11 @@
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { DocsLayout } from "@/components/docs-layout"
import { docsConfig } from "@/lib/docs-config"
export const metadata = {
title: "Changelog",
description: "Bootnode platform changelog and release notes.",
description: `${docsConfig.brandName} platform changelog and release notes.`,
}
export default function ChangelogPage() {
@@ -15,7 +16,7 @@ export default function ChangelogPage() {
<div>
<h1 className="text-4xl font-bold mb-4">Changelog</h1>
<p className="text-lg text-muted-foreground">
All notable changes to the Bootnode platform, API, and SDKs.
All notable changes to the {docsConfig.brandName} platform, API, and SDKs.
</p>
</div>
@@ -33,7 +34,7 @@ export default function ChangelogPage() {
</CardHeader>
<CardContent className="space-y-6">
<p className="text-muted-foreground">
General availability of the Bootnode platform. This is a complete rewrite
General availability of the {docsConfig.brandName} platform. This is a complete rewrite
with a unified API, new product lines, and multi-chain support from day one.
</p>
@@ -116,7 +117,7 @@ export default function ChangelogPage() {
<div>
<h3 className="font-semibold mb-3">Platform</h3>
<ul className="list-disc list-inside text-muted-foreground space-y-1.5">
<li>New dashboard at dashboard.bootnode.dev with usage analytics, API key management, and webhook monitoring</li>
<li>New dashboard at {docsConfig.dashboardUrl.replace("https://", "")} with usage analytics, API key management, and webhook monitoring</li>
<li>Unified authentication via X-API-Key or Bearer token</li>
<li>Scoped API keys for least-privilege access</li>
<li>Rate limiting with per-plan compute unit budgets</li>
@@ -138,7 +139,7 @@ export default function ChangelogPage() {
</CardHeader>
<CardContent className="space-y-6">
<p className="text-muted-foreground">
Beta launch of the Bootnode platform with core RPC proxy and Token API functionality.
Beta launch of the {docsConfig.brandName} platform with core RPC proxy and Token API functionality.
</p>
<div>
+11 -10
View File
@@ -3,10 +3,11 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { DocsLayout } from "@/components/docs-layout"
import { ArrowRight } from "lucide-react"
import { docsConfig } from "@/lib/docs-config"
export const metadata = {
title: "Examples",
description: "Common Bootnode API usage examples with full code snippets.",
description: `Common ${docsConfig.brandName} API usage examples with full code snippets.`,
}
export default function ExamplesPage() {
@@ -45,7 +46,7 @@ async function getEthBalance(
address: string
): Promise<{ wei: string; eth: string }> {
const res = await fetch(
\`https://api.bootnode.dev/v1/rpc/\${chain}/mainnet\`,
\`${docsConfig.apiUrl}/v1/rpc/\${chain}/mainnet\`,
{
method: "POST",
headers: {
@@ -96,7 +97,7 @@ console.log(\`Balance: \${balance.eth} ETH (\${balance.wei} wei)\`);
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`async function getTokenBalances(chain: string, address: string) {
const res = await fetch(
\`https://api.bootnode.dev/v1/tokens/\${chain}/balances/\${address}\`,
\`${docsConfig.apiUrl}/v1/tokens/\${chain}/balances/\${address}\`,
{
headers: { "X-API-Key": process.env.BOOTNODE_API_KEY! },
}
@@ -142,7 +143,7 @@ for (const token of data.tokens) {
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`// 1. Create the webhook subscription
async function monitorWallet(address: string) {
const res = await fetch("https://api.bootnode.dev/v1/webhooks", {
const res = await fetch("${docsConfig.apiUrl}/v1/webhooks", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -219,7 +220,7 @@ await monitorWallet("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");`}</code>
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`async function createWallet(ownerAddress: string, chain: string) {
const res = await fetch("https://api.bootnode.dev/v1/wallets/create", {
const res = await fetch("${docsConfig.apiUrl}/v1/wallets/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -273,7 +274,7 @@ console.log("Chain:", wallet.chain); // "base"
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`import { encodeFunctionData, parseAbi } from "viem";
const BASE_URL = "https://api.bootnode.dev/v1";
const BASE_URL = "${docsConfig.apiUrl}/v1";
const API_KEY = process.env.BOOTNODE_API_KEY!;
const ENTRY_POINT = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789";
@@ -383,11 +384,11 @@ console.log("UserOp hash:", opHash);`}</code>
<code>{`const API_KEY = process.env.BOOTNODE_API_KEY!;
const ws = new WebSocket(
\`wss://api.bootnode.dev/v1/ws/ethereum/mainnet?apiKey=\${API_KEY}\`
\`${docsConfig.wsUrl}/v1/ws/ethereum/mainnet?apiKey=\${API_KEY}\`
);
ws.onopen = () => {
console.log("Connected to Bootnode WebSocket");
console.log("Connected to ${docsConfig.brandName} WebSocket");
// Subscribe to new block headers
ws.send(JSON.stringify({
@@ -452,7 +453,7 @@ ws.onerror = (err) => console.error("Error:", err);
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`const API_KEY = process.env.BOOTNODE_API_KEY!;
const BASE = "https://api.bootnode.dev/v1";
const BASE = "${docsConfig.apiUrl}/v1";
// Get collection info
async function getCollection(chain: string, contract: string) {
@@ -516,7 +517,7 @@ for (const trait of nft.traits) {
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`async function getGasPrices(chain: string) {
const res = await fetch(
\`https://api.bootnode.dev/v1/gas/\${chain}/prices\`,
\`${docsConfig.apiUrl}/v1/gas/\${chain}/prices\`,
{ headers: { "X-API-Key": process.env.BOOTNODE_API_KEY! } }
);
return res.json();
+3 -2
View File
@@ -15,6 +15,7 @@ import {
FileCode,
ArrowRight,
} from "lucide-react"
import { docsConfig } from "@/lib/docs-config"
const sections = [
{
@@ -103,7 +104,7 @@ export default function DocsPage() {
<h1 className="text-4xl font-bold">Documentation</h1>
</div>
<p className="text-xl text-muted-foreground max-w-2xl">
Everything you need to build blockchain applications with Bootnode.
Everything you need to build blockchain applications with {docsConfig.brandName}.
Multi-chain RPC, indexed data, webhooks, smart wallets, and more.
</p>
</div>
@@ -212,7 +213,7 @@ export default function DocsPage() {
<CardHeader>
<CardTitle>API Reference</CardTitle>
<CardDescription>
Complete reference for all Bootnode APIs
Complete reference for all {docsConfig.brandName} APIs
</CardDescription>
</CardHeader>
<CardContent>
+7 -6
View File
@@ -3,10 +3,11 @@ 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: "Quickstart",
description: "Get started with Bootnode in under 5 minutes.",
description: `Get started with ${docsConfig.brandName} in under 5 minutes.`,
}
export default function QuickstartPage() {
@@ -28,9 +29,9 @@ export default function QuickstartPage() {
<h2 className="text-2xl font-semibold">Sign Up and Get Your API Key</h2>
</div>
<p className="text-muted-foreground">
Create a free Bootnode account at{" "}
<Link href="https://dashboard.bootnode.dev" className="text-primary underline underline-offset-4">
dashboard.bootnode.dev
Create a free {docsConfig.brandName} account at{" "}
<Link href={docsConfig.dashboardUrl} className="text-primary underline underline-offset-4">
{docsConfig.dashboardUrl.replace("https://", "")}
</Link>
. After signing up, navigate to <strong>Settings &rarr; API Keys</strong> and
create a new key. Copy it -- you will need it for every request.
@@ -57,7 +58,7 @@ export default function QuickstartPage() {
on Ethereum mainnet. This returns the latest block number in hexadecimal.
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`curl -X POST https://api.bootnode.dev/v1/rpc/ethereum/mainnet \\
<code>{`curl -X POST ${docsConfig.apiUrl}/v1/rpc/ethereum/mainnet \\
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
@@ -171,7 +172,7 @@ main();`}</code>
<h2 className="text-2xl font-semibold">Using Fetch Directly</h2>
</div>
<p className="text-muted-foreground">
You do not need the SDK. Every Bootnode API is a standard REST or JSON-RPC
You do not need the SDK. Every {docsConfig.brandName} API is a standard REST or JSON-RPC
endpoint. Here is a plain <code className="bg-muted px-1.5 py-0.5 rounded text-sm">fetch</code> example:
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
+5 -4
View File
@@ -3,10 +3,11 @@ 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: "SDKs",
description: "Official Bootnode client libraries for TypeScript, Python, Go, and Rust.",
description: `Official ${docsConfig.brandName} client libraries for TypeScript, Python, Go, and Rust.`,
}
export default function SDKsPage() {
@@ -18,7 +19,7 @@ export default function SDKsPage() {
<h1 className="text-4xl font-bold mb-4">SDKs & Libraries</h1>
<p className="text-lg text-muted-foreground">
Official client libraries for every major language. Each SDK provides
type-safe access to all Bootnode APIs with built-in retry logic,
type-safe access to all {docsConfig.brandName} APIs with built-in retry logic,
authentication, and error handling.
</p>
</div>
@@ -148,7 +149,7 @@ uv add bootnode`}</code>
<code>{`import asyncio
from bootnode import Bootnode
client = Bootnode(api_key="bn_live_...")
client = Bootnode(api_key="${docsConfig.apiKeyPrefix}live_...")
async def main():
# JSON-RPC
@@ -193,7 +194,7 @@ asyncio.run(main())
# Sync usage (for scripts):
from bootnode import BootnodeSync
sync_client = BootnodeSync(api_key="bn_live_...")
sync_client = BootnodeSync(api_key="${docsConfig.apiKeyPrefix}live_...")
gas = sync_client.gas.get_prices("ethereum")
print(f"Base fee: {gas['base_fee_gwei']} gwei")`}</code>
</pre>
+7 -6
View File
@@ -3,10 +3,11 @@ import { Badge } from "@/components/ui/badge"
import { Card, CardContent } 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: "Smart Wallets Quickstart",
description: "Create and use ERC-4337 smart wallets with Bootnode.",
description: `Create and use ERC-4337 smart wallets with ${docsConfig.brandName}.`,
}
export default function WalletsQuickstartPage() {
@@ -28,7 +29,7 @@ export default function WalletsQuickstartPage() {
<section className="space-y-3">
<h2 className="text-2xl font-semibold">Prerequisites</h2>
<ul className="list-disc list-inside text-muted-foreground space-y-1">
<li>A Bootnode API key (get one at <Link href="https://dashboard.bootnode.dev" className="text-primary underline underline-offset-4">dashboard.bootnode.dev</Link>)</li>
<li>A {docsConfig.brandName} API key (get one at <Link href={docsConfig.dashboardUrl} className="text-primary underline underline-offset-4">{docsConfig.dashboardUrl.replace("https://", "")}</Link>)</li>
<li>Node.js 18+ installed</li>
<li>Basic familiarity with Next.js and TypeScript</li>
</ul>
@@ -42,13 +43,13 @@ export default function WalletsQuickstartPage() {
</div>
<p className="text-muted-foreground">
Call the wallet creation endpoint. You provide an owner address (the EOA that
controls this smart wallet), and Bootnode returns the deterministic smart wallet
controls this smart wallet), and {docsConfig.brandName} returns the deterministic smart wallet
address.
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`// lib/bootnode.ts
const BOOTNODE_API_KEY = process.env.BOOTNODE_API_KEY!;
const BASE_URL = "https://api.bootnode.dev/v1";
const BASE_URL = "${docsConfig.apiUrl}/v1";
export async function createSmartWallet(ownerAddress: string, chain: string) {
const res = await fetch(\`\${BASE_URL}/wallets/create\`, {
@@ -171,7 +172,7 @@ export async function getBalance(address: string, chain: string) {
<h2 className="text-2xl font-semibold">Send a Gasless Transaction</h2>
</div>
<p className="text-muted-foreground">
Build a UserOperation, get it sponsored by the Bootnode paymaster, and submit
Build a UserOperation, get it sponsored by the {docsConfig.brandName} paymaster, and submit
it through the bundler. The user pays zero gas.
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
@@ -282,7 +283,7 @@ export async function sendGaslessTransfer({
import { NextRequest, NextResponse } from "next/server";
const BOOTNODE_API_KEY = process.env.BOOTNODE_API_KEY!;
const BASE_URL = "https://api.bootnode.dev/v1";
const BASE_URL = "${docsConfig.apiUrl}/v1";
export async function POST(req: NextRequest) {
const { ownerAddress } = await req.json();
+8 -7
View File
@@ -3,10 +3,11 @@ 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: "Webhooks Quickstart",
description: "Set up real-time blockchain event notifications with Bootnode Webhooks.",
description: `Set up real-time blockchain event notifications with ${docsConfig.brandName} Webhooks.`,
}
export default function WebhooksQuickstartPage() {
@@ -18,7 +19,7 @@ export default function WebhooksQuickstartPage() {
<h1 className="text-4xl font-bold mb-4">Webhooks Quickstart</h1>
<p className="text-lg text-muted-foreground">
Receive real-time notifications when on-chain events happen. No polling required.
Bootnode will POST a signed JSON payload to your endpoint whenever a matching event
{docsConfig.brandName} will POST a signed JSON payload to your endpoint whenever a matching event
occurs.
</p>
</div>
@@ -68,11 +69,11 @@ export default function WebhooksQuickstartPage() {
</div>
<p className="text-muted-foreground">
Register your endpoint and specify which events you want to receive.
Bootnode returns a <code className="bg-muted px-1.5 py-0.5 rounded text-sm">signing_secret</code>{" "}
{docsConfig.brandName} returns a <code className="bg-muted px-1.5 py-0.5 rounded text-sm">signing_secret</code>{" "}
that you will use to verify webhook payloads.
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`curl -X POST https://api.bootnode.dev/v1/webhooks \\
<code>{`curl -X POST ${docsConfig.apiUrl}/v1/webhooks \\
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
@@ -124,7 +125,7 @@ export default function WebhooksQuickstartPage() {
</div>
<p className="text-muted-foreground">
Create an Express.js server (or any HTTP server) to receive webhook events.
Bootnode sends POST requests with a JSON body and an HMAC signature in the headers.
{docsConfig.brandName} sends POST requests with a JSON body and an HMAC signature in the headers.
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`// server.ts
@@ -309,7 +310,7 @@ function notifyUser(address: string, message: string) {
<section className="space-y-4">
<h2 className="text-2xl font-semibold">Example Webhook Payload</h2>
<p className="text-muted-foreground">
Here is a full example of what Bootnode sends to your endpoint:
Here is a full example of what {docsConfig.brandName} sends to your endpoint:
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`// Headers:
@@ -345,7 +346,7 @@ function notifyUser(address: string, message: string) {
<CardContent className="pt-6 space-y-3">
<p className="text-sm text-muted-foreground">
If your endpoint returns a non-2xx status code or does not respond within 15 seconds,
Bootnode retries the delivery with exponential backoff:
{docsConfig.brandName} retries the delivery with exponential backoff:
</p>
<div className="overflow-x-auto border rounded-lg">
<table className="w-full text-sm">
+10 -9
View File
@@ -3,10 +3,11 @@ import { Badge } from "@/components/ui/badge"
import { Card, CardContent } 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: "WebSockets Quickstart",
description: "Subscribe to real-time blockchain data with Bootnode WebSockets.",
description: `Subscribe to real-time blockchain data with ${docsConfig.brandName} WebSockets.`,
}
export default function WebSocketsQuickstartPage() {
@@ -66,22 +67,22 @@ export default function WebSocketsQuickstartPage() {
Pass your API key as a query parameter.
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`wss://api.bootnode.dev/v1/ws/{chain}/{network}?apiKey=YOUR_API_KEY
<code>{`${docsConfig.wsUrl}/v1/ws/{chain}/{network}?apiKey=YOUR_API_KEY
// Examples:
// wss://api.bootnode.dev/v1/ws/ethereum/mainnet?apiKey=bn_live_...
// wss://api.bootnode.dev/v1/ws/base/mainnet?apiKey=bn_live_...
// wss://api.bootnode.dev/v1/ws/polygon/mainnet?apiKey=bn_live_...`}</code>
// ${docsConfig.wsUrl}/v1/ws/ethereum/mainnet?apiKey=${docsConfig.apiKeyPrefix}live_...
// ${docsConfig.wsUrl}/v1/ws/base/mainnet?apiKey=${docsConfig.apiKeyPrefix}live_...
// ${docsConfig.wsUrl}/v1/ws/polygon/mainnet?apiKey=${docsConfig.apiKeyPrefix}live_...`}</code>
</pre>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`// Node.js / Browser
const ws = new WebSocket(
"wss://api.bootnode.dev/v1/ws/ethereum/mainnet?apiKey=" +
"${docsConfig.wsUrl}/v1/ws/ethereum/mainnet?apiKey=" +
process.env.BOOTNODE_API_KEY
);
ws.onopen = () => {
console.log("Connected to Bootnode WebSocket");
console.log("Connected to ${docsConfig.brandName} WebSocket");
};
ws.onerror = (error) => {
@@ -107,7 +108,7 @@ ws.onclose = (event) => {
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`const ws = new WebSocket(
"wss://api.bootnode.dev/v1/ws/ethereum/mainnet?apiKey=" +
"${docsConfig.wsUrl}/v1/ws/ethereum/mainnet?apiKey=" +
process.env.BOOTNODE_API_KEY
);
@@ -294,7 +295,7 @@ ws.onmessage = (event) => {
onMessage: (data: any) => void
) {
this.url =
\`wss://api.bootnode.dev/v1/ws/\${chain}/\${network}?apiKey=\${apiKey}\`;
\`${docsConfig.wsUrl}/v1/ws/\${chain}/\${network}?apiKey=\${apiKey}\`;
this.onMessage = onMessage;
this.connect();
}
+14 -11
View File
@@ -3,6 +3,7 @@ import { GeistSans } from "geist/font/sans"
import { GeistMono } from "geist/font/mono"
import { ThemeProvider } from "@/components/theme-provider"
import { AuthProvider } from "@/lib/auth"
import { Providers } from "@/app/providers"
import { Toaster } from "@/components/ui/sonner"
import "@/app/globals.css"
@@ -60,17 +61,19 @@ export default function RootLayout({
className={`${GeistSans.variable} ${GeistMono.variable}`}
>
<body className="min-h-screen bg-background font-sans antialiased">
<AuthProvider>
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem
disableTransitionOnChange
>
{children}
<Toaster />
</ThemeProvider>
</AuthProvider>
<Providers>
<AuthProvider>
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem
disableTransitionOnChange
>
{children}
<Toaster />
</ThemeProvider>
</AuthProvider>
</Providers>
</body>
</html>
)
+43 -33
View File
@@ -8,8 +8,9 @@ import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Blocks, Loader2 } from "lucide-react"
import { Loader2 } from "lucide-react"
import { useAuth, HanzoLoginButton } from "@/lib/auth"
import { BrandLogo, useBrand } from "@/components/brand-logo"
function LoginForm() {
const router = useRouter()
@@ -59,18 +60,19 @@ function LoginForm() {
)
}
const brand = useBrand()
// Production mode: Show IAM login options
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 flex items-center space-x-2">
<Blocks className="h-8 w-8" />
<span className="text-2xl font-bold">Bootnode</span>
<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 Bootnode</CardTitle>
<CardTitle className="text-xl">Sign in to {brand.name}</CardTitle>
<CardDescription>
Choose your identity provider to continue
</CardDescription>
@@ -109,9 +111,8 @@ function LoginForm() {
// Development mode: Show email/password form
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 flex items-center space-x-2">
<Blocks className="h-8 w-8" />
<span className="text-2xl font-bold">Bootnode</span>
<Link href="/" className="mb-8">
<BrandLogo size="large" />
</Link>
<Card className="w-full max-w-sm">
@@ -206,31 +207,40 @@ function LoginForm() {
{/* Demo credentials for localhost */}
<div className="mt-3 rounded-md border border-dashed border-muted-foreground/25 bg-muted/50 p-3">
<p className="text-xs font-medium text-muted-foreground mb-2">Demo Credentials</p>
<div className="space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">Email:</span>
<code
className="font-mono bg-background px-1 rounded cursor-pointer hover:bg-primary/10"
onClick={() => setEmail("[email protected]")}
title="Click to use"
>
test@bootnode.dev
</code>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Password:</span>
<code
className="font-mono bg-background px-1 rounded cursor-pointer hover:bg-primary/10"
onClick={() => setPassword("testpass123")}
title="Click to use"
>
testpass123
</code>
</div>
</div>
<p className="text-[10px] text-muted-foreground mt-2">
Click values to autofill. Or register a new account.
<Button
type="button"
variant="ghost"
size="sm"
className="w-full text-xs h-8"
disabled={loading}
onClick={async () => {
setMode("login")
setEmail("[email protected]")
setPassword("testpass123")
setLoading(true)
setError("")
try {
await login("[email protected]", "testpass123")
const returnUrl = searchParams.get("returnUrl") || "/dashboard"
router.push(returnUrl)
} catch (err) {
setError(err instanceof Error ? err.message : "Login failed")
} finally {
setLoading(false)
}
}}
>
{loading ? (
<>
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
Signing in...
</>
) : (
"Use Demo Account"
)}
</Button>
<p className="text-[10px] text-muted-foreground mt-2 text-center">
test@bootnode.dev / testpass123
</p>
</div>
</CardContent>
+22
View File
@@ -0,0 +1,22 @@
"use client"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { useState } from "react"
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false,
},
},
})
)
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
+177
View File
@@ -0,0 +1,177 @@
"use client"
import Image from "next/image"
import { useEffect, useState } from "react"
import { useTheme } from "next-themes"
import { Blocks } from "lucide-react"
// Brand configuration - reads from environment
export function useBrand() {
const [brand, setBrand] = useState<BrandConfig>(brands.bootnode)
useEffect(() => {
// Check environment variable first
const envBrand = process.env.NEXT_PUBLIC_BRAND?.toLowerCase()
if (envBrand && brands[envBrand as keyof typeof brands]) {
setBrand(brands[envBrand as keyof typeof brands])
return
}
// Auto-detect from hostname
const hostname = window.location.hostname
if (hostname.includes("hanzo.ai") || hostname.includes("web3.hanzo")) {
setBrand(brands.hanzo)
} else if (hostname.includes("lux.network")) {
setBrand(brands.lux)
} else if (hostname.includes("zoo.ngo")) {
setBrand(brands.zoo)
}
}, [])
return brand
}
type BrandConfig = {
name: string
tagline: string
logo: string
logoDark: string // For light mode (dark logo)
logoIcon: string
favicon: string
primaryColor: string
iam: {
url: string
clientId: string
domain: string
}
}
const brands = {
bootnode: {
name: "Bootnode",
tagline: "Blockchain Infrastructure for Developers",
logo: "/logo/bootnode-logo.svg",
logoDark: "/logo/bootnode-logo.svg",
logoIcon: "/logo/bootnode-icon.svg",
favicon: "/logo/bootnode-icon.svg",
primaryColor: "#000000",
iam: {
url: process.env.NEXT_PUBLIC_IAM_URL || "https://iam.hanzo.ai",
clientId: process.env.NEXT_PUBLIC_IAM_CLIENT_ID || "bootnode-platform",
domain: "hanzo.id",
},
},
hanzo: {
name: "Hanzo",
tagline: "Web3 Infrastructure",
logo: "/logo/hanzo-logo.svg", // White logo for dark mode
logoDark: "/logo/hanzo-logo-dark.svg", // Black logo for light mode
logoIcon: "/logo/hanzo-icon.svg",
favicon: "/logo/hanzo-icon.svg",
primaryColor: "#000000",
iam: {
url: "https://iam.hanzo.ai",
clientId: "hanzo-web3",
domain: "hanzo.id",
},
},
lux: {
name: "Lux Network",
tagline: "Next-Gen Blockchain",
logo: "/logo/lux-logo.svg",
logoDark: "/logo/lux-logo.svg",
logoIcon: "/logo/lux-icon.svg",
favicon: "/logo/lux-icon.svg",
primaryColor: "#8b5cf6",
iam: {
url: "https://iam.hanzo.ai",
clientId: "lux-web3",
domain: "lux.id",
},
},
zoo: {
name: "Zoo Labs",
tagline: "Decentralized AI",
logo: "/logo/zoo-logo.svg",
logoDark: "/logo/zoo-logo.svg",
logoIcon: "/logo/zoo-icon.svg",
favicon: "/logo/zoo-icon.svg",
primaryColor: "#00cc66",
iam: {
url: "https://iam.hanzo.ai",
clientId: "zoo-web3",
domain: "zoo.id",
},
},
} as const
// Brand Logo Component
export function BrandLogo({
showText = true,
size = "default",
className = ""
}: {
showText?: boolean
size?: "small" | "default" | "large"
className?: string
}) {
const brand = useBrand()
const { resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
const sizeClasses = {
small: "h-5 w-5",
default: "h-6 w-6",
large: "h-8 w-8",
}
const textSizes = {
small: "text-lg",
default: "text-xl",
large: "text-2xl",
}
const pixelSizes = {
small: 20,
default: 24,
large: 32,
}
// Use dark logo for light mode, white logo for dark mode
const logoSrc = mounted && resolvedTheme === "dark" ? brand.logo : brand.logoDark
return (
<div className={`flex items-center space-x-2 ${className}`}>
{brand.name === "Bootnode" ? (
<Blocks className={sizeClasses[size]} />
) : (
<Image
src={logoSrc}
alt={brand.name}
width={pixelSizes[size]}
height={pixelSizes[size]}
className={sizeClasses[size]}
/>
)}
{showText && (
<span className={`font-bold ${textSizes[size]}`}>{brand.name}</span>
)}
</div>
)
}
// Export brand info for use in metadata, etc.
export function getBrandName(): string {
if (typeof window === "undefined") {
return process.env.NEXT_PUBLIC_BRAND === "hanzo" ? "Hanzo" : "Bootnode"
}
const hostname = window.location.hostname
if (hostname.includes("hanzo.ai")) return "Hanzo"
if (hostname.includes("lux.network")) return "Lux Network"
if (hostname.includes("zoo.ngo")) return "Zoo Labs"
return "Bootnode"
}
+222
View File
@@ -0,0 +1,222 @@
"use client"
import { useEffect, useRef, useState } from "react"
import * as d3 from "d3"
interface NodePoint {
id: string
name: string
point: [number, number] // [longitude, latitude]
count: number
status: "running" | "stopped" | "syncing" | "error"
chain?: string
}
interface GlobeViewProps {
data: NodePoint[]
width?: number
height?: number
className?: string
}
const statusColors = {
running: "#22c55e",
syncing: "#eab308",
stopped: "#6b7280",
error: "#ef4444",
}
export function GlobeView({ data, width = 500, height = 500, className }: GlobeViewProps) {
const svgRef = useRef<SVGSVGElement>(null)
const tooltipRef = useRef<HTMLDivElement>(null)
const [geoData, setGeoData] = useState<any>(null)
const [rotation, setRotation] = useState<[number, number]>([0, 0])
useEffect(() => {
d3.json("/geo.json").then((data) => {
setGeoData(data)
}).catch(err => {
console.error("Failed to load geo.json:", err)
})
}, [])
// Calculate center and zoom based on node distribution
useEffect(() => {
if (data.length === 0) return
// Calculate centroid of all nodes
const avgLon = data.reduce((sum, d) => sum + d.point[0], 0) / data.length
const avgLat = data.reduce((sum, d) => sum + d.point[1], 0) / data.length
// Set rotation to center on the average position
setRotation([-avgLon, -avgLat])
}, [data])
useEffect(() => {
if (!svgRef.current || !geoData) return
const svg = d3.select(svgRef.current)
svg.selectAll("*").remove()
const sensitivity = 75
const size = Math.min(width, height)
const scale = size / 2.5
// Create orthographic projection (3D globe)
const projection = d3.geoOrthographic()
.scale(scale)
.center([0, 0])
.rotate(rotation)
.translate([width / 2, height / 2])
const path = d3.geoPath().projection(projection)
// Globe outline (ocean)
svg.append("circle")
.attr("cx", width / 2)
.attr("cy", height / 2)
.attr("r", scale)
.attr("fill", "hsl(var(--background))")
.attr("stroke", "hsl(var(--border))")
.attr("stroke-width", 1)
// Graticule (grid lines)
const graticule = d3.geoGraticule()
svg.append("path")
.datum(graticule())
.attr("d", path as any)
.attr("fill", "none")
.attr("stroke", "hsl(var(--border))")
.attr("stroke-width", 0.3)
.attr("opacity", 0.5)
// Countries
svg.append("path")
.datum(geoData)
.attr("d", path as any)
.attr("fill", "hsl(var(--muted))")
.attr("stroke", "hsl(var(--border))")
.attr("stroke-width", 0.5)
// Create tooltip
const tooltip = d3.select(tooltipRef.current)
// Draw node points
data.forEach((d) => {
const coords = projection(d.point)
if (!coords) return
// Check if point is on visible side of globe
const [x, y] = coords
const isVisible = d3.geoDistance(d.point, [-rotation[0], -rotation[1]]) < Math.PI / 2
if (!isVisible) return
const nodeGroup = svg.append("g")
.attr("transform", `translate(${x}, ${y})`)
.style("cursor", "pointer")
// Outer glow ring
nodeGroup.append("circle")
.attr("r", 0)
.attr("fill", "none")
.attr("stroke", statusColors[d.status])
.attr("stroke-width", 2)
.attr("opacity", 0.3)
.transition()
.duration(500)
.attr("r", 15)
// Main node circle
const mainCircle = nodeGroup.append("circle")
.attr("r", 0)
.attr("fill", statusColors[d.status])
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.attr("opacity", 0.9)
mainCircle.transition()
.duration(500)
.attr("r", 8)
// Pulse animation for running nodes
if (d.status === "running") {
function pulse() {
mainCircle
.transition()
.duration(1000)
.attr("r", 10)
.attr("opacity", 1)
.transition()
.duration(1000)
.attr("r", 8)
.attr("opacity", 0.9)
.on("end", pulse)
}
setTimeout(pulse, Math.random() * 1000)
}
// Tooltip interaction
nodeGroup
.on("mouseenter", function(event) {
tooltip
.style("display", "block")
.style("left", `${event.offsetX + 15}px`)
.style("top", `${event.offsetY - 10}px`)
.html(`
<div class="font-medium">${d.name}</div>
<div class="text-sm text-muted-foreground">${d.chain || "Local"}</div>
<div class="text-sm">
<span class="inline-block w-2 h-2 rounded-full mr-1" style="background: ${statusColors[d.status]}"></span>
${d.status.charAt(0).toUpperCase() + d.status.slice(1)}
</div>
`)
})
.on("mouseleave", function() {
tooltip.style("display", "none")
})
})
// Add drag to rotate globe
const drag = d3.drag<SVGSVGElement, unknown>()
.on("drag", (event) => {
const k = sensitivity / projection.scale()
setRotation(([x, y]) => [
x + event.dx * k,
Math.max(-90, Math.min(90, y - event.dy * k))
])
})
svg.call(drag)
}, [data, geoData, width, height, rotation])
if (!geoData) {
return (
<div className={`flex items-center justify-center ${className}`} style={{ width, height }}>
<div className="animate-pulse text-muted-foreground">Loading globe...</div>
</div>
)
}
return (
<div className={`relative ${className}`}>
<svg
ref={svgRef}
width={width}
height={height}
className="overflow-visible cursor-grab active:cursor-grabbing"
/>
<div
ref={tooltipRef}
className="absolute pointer-events-none bg-popover border border-border rounded-lg shadow-lg p-3 z-50"
style={{ display: "none" }}
/>
<div className="absolute bottom-2 left-2 text-xs text-muted-foreground">
Drag to rotate
</div>
</div>
)
}
export default GlobeView
+9 -6
View File
@@ -1,6 +1,8 @@
"use client"
import Link from "next/link"
import { Blocks } from "lucide-react"
import { Separator } from "@/components/ui/separator"
import { BrandLogo, useBrand } from "@/components/brand-logo"
const footerLinks = {
products: [
@@ -38,18 +40,19 @@ const footerLinks = {
}
export function Footer() {
const brand = useBrand()
return (
<footer className="border-t bg-muted/30">
<div className="container py-12 md:py-16">
<div className="grid gap-8 sm:grid-cols-2 md:grid-cols-5">
{/* Brand */}
<div className="md:col-span-1">
<Link href="/" className="flex items-center space-x-2">
<Blocks className="h-6 w-6" />
<span className="text-xl font-bold">Bootnode</span>
<Link href="/">
<BrandLogo />
</Link>
<p className="mt-4 text-sm text-muted-foreground">
The complete blockchain development platform for builders.
{brand.tagline}
</p>
</div>
@@ -126,7 +129,7 @@ export function Footer() {
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
<p className="text-sm text-muted-foreground">
© {new Date().getFullYear()} Bootnode. All rights reserved.
© {new Date().getFullYear()} {brand.name}. All rights reserved.
</p>
<div className="flex gap-4">
<Link
+3 -4
View File
@@ -15,7 +15,6 @@ import {
NavigationMenuTrigger,
} from "@/components/ui/navigation-menu"
import {
Blocks,
Database,
Globe,
Layers,
@@ -24,6 +23,7 @@ import {
Wallet,
X,
} from "lucide-react"
import { BrandLogo } from "@/components/brand-logo"
const products = [
{
@@ -69,9 +69,8 @@ export function Navbar() {
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-16 items-center">
{/* Logo */}
<Link href="/" className="mr-6 flex items-center space-x-2">
<Blocks className="h-6 w-6" />
<span className="text-xl font-bold">Bootnode</span>
<Link href="/" className="mr-6">
<BrandLogo />
</Link>
{/* Desktop Navigation */}
+61
View File
@@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
onValueChange?: (value: string) => void
}
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, children, onValueChange, onChange, ...props }, ref) => {
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
onChange?.(e)
onValueChange?.(e.target.value)
}
return (
<select
ref={ref}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
onChange={handleChange}
{...props}
>
{children}
</select>
)
}
)
Select.displayName = "Select"
// Simplified components that work with native select
const SelectTrigger = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, children, ...props }, ref) => (
<Select ref={ref} className={className} {...props}>
{children}
</Select>
)
)
SelectTrigger.displayName = "SelectTrigger"
const SelectValue = ({ placeholder }: { placeholder?: string }) => null
const SelectContent = ({ children }: { children: React.ReactNode }) => <>{children}</>
const SelectItem = ({
value,
children,
disabled
}: {
value: string
children: React.ReactNode
disabled?: boolean
}) => (
<option value={value} disabled={disabled}>
{children}
</option>
)
export { Select, SelectTrigger, SelectValue, SelectContent, SelectItem }
+1 -1
View File
@@ -2,7 +2,7 @@
// Uses auth token from localStorage for authenticated requests
const getApiConfig = () => {
let API_URL = "http://localhost:8100"
let API_URL = "http://localhost:8000"
if (typeof window !== 'undefined') {
API_URL = process.env.NEXT_PUBLIC_API_URL ||
+1 -1
View File
@@ -59,7 +59,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
const isProduction = isProductionMode()
const getApiUrl = useCallback(() => {
return process.env.NEXT_PUBLIC_API_URL || "http://localhost:8100"
return process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
}, [])
useEffect(() => {
+172
View File
@@ -0,0 +1,172 @@
// White-label branding configuration
// Configure via environment variables for different deployments
export type BrandConfig = {
name: string
tagline: string
description: string
logo: string
logoIcon: string
logoWhite: string
favicon: string
domain: string
statusUrl: string
colors: {
primary: string
primaryForeground: string
}
social: {
twitter?: string
github?: string
discord?: string
}
iam: {
url: string
clientId: string
domain: string // e.g., hanzo.id, zoo.id
}
}
// Brand presets
const brands: Record<string, BrandConfig> = {
bootnode: {
name: "Bootnode",
tagline: "Blockchain Infrastructure for Developers",
description: "The complete blockchain development platform. Multi-chain RPC, Token APIs, NFT APIs, Smart Wallets, Webhooks, and more.",
logo: "/logo/bootnode-logo.svg",
logoIcon: "/logo/bootnode-icon.svg",
logoWhite: "/logo/bootnode-logo-white.svg",
favicon: "/logo/bootnode-icon.svg",
domain: "bootnode.dev",
statusUrl: "https://status.bootnode.dev",
colors: {
primary: "#000000",
primaryForeground: "#ffffff",
},
social: {
twitter: "https://twitter.com/bootnode",
github: "https://github.com/bootnode",
},
iam: {
url: "https://iam.hanzo.ai",
clientId: "bootnode-platform",
domain: "hanzo.id",
},
},
hanzo: {
name: "Hanzo",
tagline: "Web3 Infrastructure",
description: "Enterprise blockchain infrastructure powered by Hanzo AI. Multi-chain RPC, Token APIs, NFT APIs, Smart Wallets, and more.",
logo: "/logo/hanzo-logo.svg",
logoIcon: "/logo/hanzo-icon.svg",
logoWhite: "/logo/hanzo-logo-white.svg",
favicon: "/logo/hanzo-icon.svg",
domain: "web3.hanzo.ai",
statusUrl: "https://status.hanzo.ai",
colors: {
primary: "#0066ff",
primaryForeground: "#ffffff",
},
social: {
twitter: "https://twitter.com/hanaboratory",
github: "https://github.com/hanzoai",
discord: "https://discord.gg/hanzo",
},
iam: {
url: "https://iam.hanzo.ai",
clientId: "hanzo-web3",
domain: "hanzo.id",
},
},
lux: {
name: "Lux Network",
tagline: "Next-Gen Blockchain Infrastructure",
description: "High-performance blockchain infrastructure for the Lux Network ecosystem.",
logo: "/logo/lux-logo.svg",
logoIcon: "/logo/lux-icon.svg",
logoWhite: "/logo/lux-logo-white.svg",
favicon: "/logo/lux-icon.svg",
domain: "web3.lux.network",
statusUrl: "https://status.lux.network",
colors: {
primary: "#8b5cf6",
primaryForeground: "#ffffff",
},
social: {
twitter: "https://twitter.com/luxdefi",
github: "https://github.com/luxfi",
},
iam: {
url: "https://iam.hanzo.ai",
clientId: "lux-web3",
domain: "lux.id",
},
},
zoo: {
name: "Zoo Labs",
tagline: "Decentralized AI Infrastructure",
description: "Blockchain infrastructure for decentralized AI and science research.",
logo: "/logo/zoo-logo.svg",
logoIcon: "/logo/zoo-icon.svg",
logoWhite: "/logo/zoo-logo-white.svg",
favicon: "/logo/zoo-icon.svg",
domain: "web3.zoo.ngo",
statusUrl: "https://status.zoo.ngo",
colors: {
primary: "#00cc66",
primaryForeground: "#ffffff",
},
social: {
twitter: "https://twitter.com/zoolabs",
github: "https://github.com/zoolabs",
},
iam: {
url: "https://iam.hanzo.ai",
clientId: "zoo-web3",
domain: "zoo.id",
},
},
}
// Get brand from environment or default to bootnode
function getBrandKey(): string {
// Check environment variable
const envBrand = process.env.NEXT_PUBLIC_BRAND?.toLowerCase()
if (envBrand && brands[envBrand]) {
return envBrand
}
// Auto-detect from domain in browser
if (typeof window !== "undefined") {
const hostname = window.location.hostname
if (hostname.includes("hanzo.ai")) return "hanzo"
if (hostname.includes("lux.network")) return "lux"
if (hostname.includes("zoo.ngo")) return "zoo"
}
// Default to bootnode
return "bootnode"
}
// Export the active brand configuration
export function getBrand(): BrandConfig {
return brands[getBrandKey()]
}
// Export brand key for conditional rendering
export function getBrandKey_(): string {
return getBrandKey()
}
// Check if using Hanzo branding
export function isHanzoBrand(): boolean {
return getBrandKey() === "hanzo"
}
// Get all available brands (for admin/testing)
export function getAllBrands(): Record<string, BrandConfig> {
return brands
}
// Default export for convenience
export const brand = getBrand()
+38
View File
@@ -0,0 +1,38 @@
// Docs configuration - brand-aware URLs and examples
// Uses environment variables for customization
export const docsConfig = {
// API endpoints - detected from env or defaults
apiUrl: process.env.NEXT_PUBLIC_API_URL || "https://api.web3.hanzo.ai",
dashboardUrl: process.env.NEXT_PUBLIC_DASHBOARD_URL || "https://web3.hanzo.ai",
wsUrl: process.env.NEXT_PUBLIC_WS_URL || "wss://ws.web3.hanzo.ai",
// Example API key prefix (brand-specific)
apiKeyPrefix: process.env.NEXT_PUBLIC_BRAND === "hanzo" ? "hz_" : "bn_",
// Brand name for docs
brandName: process.env.NEXT_PUBLIC_BRAND === "hanzo" ? "Hanzo Web3" : "Bootnode",
}
// Helper to get full API URL
export function getApiUrl(path: string = ""): string {
return `${docsConfig.apiUrl}${path}`
}
// Helper to get dashboard URL
export function getDashboardUrl(path: string = ""): string {
return `${docsConfig.dashboardUrl}${path}`
}
// Example curl command with brand-specific URL
export function exampleCurl(method: string, params: string = "[]"): string {
return `curl -X POST ${docsConfig.apiUrl}/v1/rpc/ethereum/mainnet \\
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "${method}",
"params": ${params}
}'`
}
+10
View File
@@ -0,0 +1,10 @@
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<rect width="64" height="64" rx="8" fill="#000000"/>
<g transform="translate(8, 8) scale(0.716)">
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="#ffffff"/>
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#ffffff"/>
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="#ffffff"/>
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#ffffff"/>
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#ffffff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 535 B

+9
View File
@@ -0,0 +1,9 @@
<svg viewBox="0 0 67 67" xmlns="http://www.w3.org/2000/svg">
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="#000000"/>
<path d="M0 44.6369L22.21 46.8285V44.6369H0Z" fill="#222222"/>
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#000000"/>
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="#000000"/>
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#000000"/>
<path d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z" fill="#222222"/>
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 576 B

+9
View File
@@ -0,0 +1,9 @@
<svg viewBox="0 0 67 67" xmlns="http://www.w3.org/2000/svg">
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="#ffffff"/>
<path d="M0 44.6369L22.21 46.8285V44.6369H0Z" fill="#DDDDDD"/>
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#ffffff"/>
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="#ffffff"/>
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#ffffff"/>
<path d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z" fill="#DDDDDD"/>
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 576 B

Some files were not shown because too many files have changed in this diff Show More