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
@@ -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/
|
||||
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 149 KiB |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 |
@@ -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.
|
||||
@@ -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 |
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 │ │ │
|
||||
│ │ └──────────┘ └──────────┘ └──────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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']
|
||||
@@ -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']
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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."
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,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') || '',
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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 → 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!,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 → 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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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 }
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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()
|
||||
@@ -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}
|
||||
}'`
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |