mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-09-30 20:46:32 +00:00
Move every caller and route to /v1
Commerce is called at /v1, the indexer at /v1/indexer, and the operator serves /v1/clusters. The console's unused fleet helpers called a /api base nothing serves; they are gone. The legacy Quart env files named an app that no longer exists. Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
# Legacy Bootnode - Development Configuration
|
||||
LEGACY_API_TOKEN=bn_legacy_dev_token_12345
|
||||
LEGACY_ADMIN_EMAIL=[email protected]
|
||||
LEGACY_ADMIN_PASSWORD=devpassword123
|
||||
MONGO_URL=mongodb://localhost:27017/bootnode
|
||||
@@ -1,12 +0,0 @@
|
||||
# Legacy Bootnode - Secured Configuration
|
||||
# For the legacy Quart application
|
||||
|
||||
# Authentication (Set secure values!)
|
||||
LEGACY_API_TOKEN=bn_legacy_secure_token_replace_with_random_32_chars
|
||||
LEGACY_ADMIN_EMAIL=[email protected]
|
||||
LEGACY_ADMIN_PASSWORD=your-strong-admin-password-here
|
||||
|
||||
# MongoDB
|
||||
MONGO_URL=mongodb://localhost:27017/bootnode
|
||||
|
||||
# WARNING: This is the legacy app - migrate to /api/bootnode for modern features
|
||||
@@ -104,6 +104,14 @@ bootnode/
|
||||
└── docs/ # Documentation
|
||||
```
|
||||
|
||||
### Paths
|
||||
|
||||
Every path bootnode serves or calls is `/v1/...`; auth is `/v1/iam/*`. The API
|
||||
mounts at `settings.api_prefix = "/v1"`, the operator at `/v1/clusters/...`,
|
||||
commerce is called at `/v1/...` and the indexer at `/v1/indexer/...`. The one
|
||||
`/api/` address is the Kubernetes API. `console/lib/no-api-prefix.test.ts`
|
||||
fails on any other.
|
||||
|
||||
### API Design
|
||||
|
||||
#### RPC API
|
||||
@@ -1076,13 +1084,13 @@ import { BrandLogo } from "@/components/brand-logo"
|
||||
Billing goes through Hanzo Commerce (Go service, `commerce.hanzo.ai`) which handles payment processing via Square. Bootnode is a client of Commerce — it never talks to payment processors directly.
|
||||
|
||||
```
|
||||
User → Dashboard → POST /v1/billing/checkout → Bootnode API → Commerce /api/v1/checkout/authorize → Square
|
||||
→ GET /v1/billing/account → Bootnode API → Commerce /api/v1/user → user+subscription
|
||||
User → Dashboard → POST /v1/billing/checkout → Bootnode API → Commerce /v1/checkout/authorize → Square
|
||||
→ GET /v1/billing/account → Bootnode API → Commerce /v1/user → user+subscription
|
||||
|
||||
Commerce Webhooks → POST /v1/billing/webhooks/commerce
|
||||
→ verify HMAC signature → update local DB → invalidate Redis cache
|
||||
|
||||
Usage: API request → Redis CU counter → hourly sync → Commerce /api/v1/subscriptions/:id/usage
|
||||
Usage: API request → Redis CU counter → hourly sync → Commerce /v1/subscriptions/:id/usage
|
||||
```
|
||||
|
||||
### Commerce Service
|
||||
@@ -1105,15 +1113,15 @@ Usage: API request → Redis CU counter → hourly sync → Commerce /api/v1/sub
|
||||
### Commerce API Endpoints (used by Bootnode)
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/v1/checkout/authorize` | POST | Authorize payment via Square (nonce) |
|
||||
| `/api/v1/checkout/capture/:orderid` | POST | Capture authorized payment |
|
||||
| `/api/v1/checkout/charge` | POST | Single-step charge |
|
||||
| `/api/v1/subscribe` | POST | Create subscription |
|
||||
| `/api/v1/subscribe/:id` | GET | Get subscription |
|
||||
| `/api/v1/subscribe/:id` | PATCH | Update subscription |
|
||||
| `/api/v1/subscribe/:id` | DELETE | Cancel subscription |
|
||||
| `/api/v1/user` | GET | Get user + subscription data |
|
||||
| `/api/v1/order` | GET | List orders |
|
||||
| `/v1/checkout/authorize` | POST | Authorize payment via Square (nonce) |
|
||||
| `/v1/checkout/capture/:orderid` | POST | Capture authorized payment |
|
||||
| `/v1/checkout/charge` | POST | Single-step charge |
|
||||
| `/v1/subscribe` | POST | Create subscription |
|
||||
| `/v1/subscribe/:id` | GET | Get subscription |
|
||||
| `/v1/subscribe/:id` | PATCH | Update subscription |
|
||||
| `/v1/subscribe/:id` | DELETE | Cancel subscription |
|
||||
| `/v1/user` | GET | Get user + subscription data |
|
||||
| `/v1/order` | GET | List orders |
|
||||
| `/health` | GET | Health check |
|
||||
|
||||
### Bootnode Billing Key Files
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""Bootnode - Blockchain Development Platform."""
|
||||
|
||||
__version__ = "1.0.4"
|
||||
__version__ = "1.0.5"
|
||||
|
||||
@@ -113,7 +113,7 @@ class HanzoCommerceClient:
|
||||
"""Create customer in Hanzo Commerce. Returns customer_id."""
|
||||
data = await self._request(
|
||||
"POST",
|
||||
"/api/v1/user",
|
||||
"/v1/user",
|
||||
json={
|
||||
"email": email,
|
||||
"name": name,
|
||||
@@ -136,7 +136,7 @@ class HanzoCommerceClient:
|
||||
|
||||
async def get_customer(self, customer_id: str) -> Customer:
|
||||
"""Get customer by ID."""
|
||||
data = await self._request("GET", f"/api/v1/user/{customer_id}")
|
||||
data = await self._request("GET", f"/v1/user/{customer_id}")
|
||||
return Customer(
|
||||
id=data["id"],
|
||||
email=data["email"],
|
||||
@@ -155,7 +155,7 @@ class HanzoCommerceClient:
|
||||
try:
|
||||
data = await self._request(
|
||||
"GET",
|
||||
"/api/v1/user",
|
||||
"/v1/user",
|
||||
params={"email": email},
|
||||
)
|
||||
customers = data.get("data", [])
|
||||
@@ -192,7 +192,7 @@ class HanzoCommerceClient:
|
||||
|
||||
data = await self._request(
|
||||
"PATCH",
|
||||
f"/api/v1/user/{customer_id}",
|
||||
f"/v1/user/{customer_id}",
|
||||
json=update_data,
|
||||
)
|
||||
return Customer(
|
||||
@@ -250,7 +250,7 @@ class HanzoCommerceClient:
|
||||
|
||||
data = await self._request(
|
||||
"POST",
|
||||
"/api/v1/checkout/authorize",
|
||||
"/v1/checkout/authorize",
|
||||
json=payload,
|
||||
)
|
||||
|
||||
@@ -273,7 +273,7 @@ class HanzoCommerceClient:
|
||||
"""
|
||||
data = await self._request(
|
||||
"POST",
|
||||
f"/api/v1/checkout/capture/{order_id}",
|
||||
f"/v1/checkout/capture/{order_id}",
|
||||
)
|
||||
|
||||
logger.info("Checkout captured", order_id=order_id)
|
||||
@@ -311,7 +311,7 @@ class HanzoCommerceClient:
|
||||
|
||||
return await self._request(
|
||||
"POST",
|
||||
"/api/v1/checkout/charge",
|
||||
"/v1/checkout/charge",
|
||||
json=payload,
|
||||
)
|
||||
|
||||
@@ -335,7 +335,7 @@ class HanzoCommerceClient:
|
||||
"""
|
||||
data = await self._request(
|
||||
"GET",
|
||||
f"/api/v1/user/{customer_id}/orders",
|
||||
f"/v1/user/{customer_id}/orders",
|
||||
params={"limit": limit},
|
||||
)
|
||||
return data.get("data", [])
|
||||
@@ -356,7 +356,7 @@ class HanzoCommerceClient:
|
||||
"""
|
||||
data = await self._request(
|
||||
"POST",
|
||||
"/api/v1/subscribe",
|
||||
"/v1/subscribe",
|
||||
json={
|
||||
"customer_id": customer_id,
|
||||
"plan_slug": plan_slug,
|
||||
@@ -390,7 +390,7 @@ class HanzoCommerceClient:
|
||||
|
||||
async def get_subscription(self, subscription_id: str) -> Subscription:
|
||||
"""Get subscription by ID."""
|
||||
data = await self._request("GET", f"/api/v1/subscribe/{subscription_id}")
|
||||
data = await self._request("GET", f"/v1/subscribe/{subscription_id}")
|
||||
return Subscription(
|
||||
id=data["id"],
|
||||
customer_id=data["customer_id"],
|
||||
@@ -414,7 +414,7 @@ class HanzoCommerceClient:
|
||||
"""Get all subscriptions for a customer (via user orders endpoint)."""
|
||||
data = await self._request(
|
||||
"GET",
|
||||
f"/api/v1/user/{customer_id}/orders",
|
||||
f"/v1/user/{customer_id}/orders",
|
||||
params={"type": "subscription"},
|
||||
)
|
||||
return [
|
||||
@@ -440,7 +440,7 @@ class HanzoCommerceClient:
|
||||
"""Upgrade or downgrade subscription."""
|
||||
data = await self._request(
|
||||
"PATCH",
|
||||
f"/api/v1/subscribe/{subscription_id}",
|
||||
f"/v1/subscribe/{subscription_id}",
|
||||
json={"plan_slug": plan_slug},
|
||||
)
|
||||
|
||||
@@ -474,7 +474,7 @@ class HanzoCommerceClient:
|
||||
"""
|
||||
data = await self._request(
|
||||
"DELETE",
|
||||
f"/api/v1/subscribe/{subscription_id}",
|
||||
f"/v1/subscribe/{subscription_id}",
|
||||
json={"immediately": immediately},
|
||||
)
|
||||
|
||||
@@ -502,7 +502,7 @@ class HanzoCommerceClient:
|
||||
"""
|
||||
await self._request(
|
||||
"POST",
|
||||
f"/api/v1/subscribe/{subscription_id}/usage",
|
||||
f"/v1/subscribe/{subscription_id}/usage",
|
||||
json={
|
||||
"quantity": compute_units,
|
||||
"timestamp": timestamp.isoformat(),
|
||||
@@ -532,7 +532,7 @@ class HanzoCommerceClient:
|
||||
|
||||
return await self._request(
|
||||
"GET",
|
||||
f"/api/v1/subscribe/{subscription_id}/usage",
|
||||
f"/v1/subscribe/{subscription_id}/usage",
|
||||
params=params or None,
|
||||
)
|
||||
|
||||
@@ -548,7 +548,7 @@ class HanzoCommerceClient:
|
||||
"""Get customer invoices (mapped from Commerce orders)."""
|
||||
data = await self._request(
|
||||
"GET",
|
||||
f"/api/v1/user/{customer_id}/orders",
|
||||
f"/v1/user/{customer_id}/orders",
|
||||
params={"limit": limit},
|
||||
)
|
||||
return [
|
||||
@@ -584,7 +584,7 @@ class HanzoCommerceClient:
|
||||
|
||||
async def get_invoice(self, invoice_id: str) -> Invoice:
|
||||
"""Get specific invoice (mapped from Commerce order)."""
|
||||
inv = await self._request("GET", f"/api/v1/order/{invoice_id}")
|
||||
inv = await self._request("GET", f"/v1/order/{invoice_id}")
|
||||
return Invoice(
|
||||
id=inv["id"],
|
||||
customer_id=inv["customer_id"],
|
||||
@@ -616,7 +616,7 @@ class HanzoCommerceClient:
|
||||
try:
|
||||
inv = await self._request(
|
||||
"GET",
|
||||
f"/api/v1/user/{customer_id}/orders",
|
||||
f"/v1/user/{customer_id}/orders",
|
||||
params={"status": "draft", "limit": 1},
|
||||
)
|
||||
return Invoice(
|
||||
@@ -652,7 +652,7 @@ class HanzoCommerceClient:
|
||||
"""Get payment methods for a customer (Square cards on file)."""
|
||||
data = await self._request(
|
||||
"GET",
|
||||
f"/api/v1/user/{customer_id}/paymentmethods",
|
||||
f"/v1/user/{customer_id}/paymentmethods",
|
||||
)
|
||||
return data.get("data", [])
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ class UnifiedBillingClient:
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
try:
|
||||
response = await client.get(
|
||||
f"{self.commerce_url}/api/v1/user",
|
||||
f"{self.commerce_url}/v1/user",
|
||||
params={"hanzo_id": hanzo_id},
|
||||
headers=self._commerce_headers(),
|
||||
)
|
||||
@@ -144,7 +144,7 @@ class UnifiedBillingClient:
|
||||
"""Create Commerce customer linked to IAM user."""
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.post(
|
||||
f"{self.commerce_url}/api/v1/user",
|
||||
f"{self.commerce_url}/v1/user",
|
||||
json={
|
||||
"email": iam_user.email,
|
||||
"name": iam_user.name,
|
||||
@@ -186,7 +186,7 @@ class UnifiedBillingClient:
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.commerce_url}/api/v1/user/{unified_user.commerce_customer_id}/orders",
|
||||
f"{self.commerce_url}/v1/user/{unified_user.commerce_customer_id}/orders",
|
||||
headers=self._commerce_headers(),
|
||||
)
|
||||
|
||||
@@ -204,7 +204,7 @@ class UnifiedBillingClient:
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.commerce_url}/api/v1/user/{unified_user.commerce_customer_id}/orders",
|
||||
f"{self.commerce_url}/v1/user/{unified_user.commerce_customer_id}/orders",
|
||||
params={"type": "invoice"},
|
||||
headers=self._commerce_headers(),
|
||||
)
|
||||
@@ -223,7 +223,7 @@ class UnifiedBillingClient:
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.commerce_url}/api/v1/user/{unified_user.commerce_customer_id}/paymentmethods",
|
||||
f"{self.commerce_url}/v1/user/{unified_user.commerce_customer_id}/paymentmethods",
|
||||
headers=self._commerce_headers(),
|
||||
)
|
||||
|
||||
@@ -247,7 +247,7 @@ class UnifiedBillingClient:
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.post(
|
||||
f"{self.commerce_url}/api/v1/subscribe",
|
||||
f"{self.commerce_url}/v1/subscribe",
|
||||
json={
|
||||
"customer_id": unified_user.commerce_customer_id,
|
||||
"plan_id": plan_id,
|
||||
@@ -280,7 +280,7 @@ class UnifiedBillingClient:
|
||||
"""Report metered usage for PAYG billing."""
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.post(
|
||||
f"{self.commerce_url}/api/v1/subscribe/{subscription_id}/usage",
|
||||
f"{self.commerce_url}/v1/subscribe/{subscription_id}/usage",
|
||||
json={
|
||||
"quantity": compute_units,
|
||||
"unit": "compute_units",
|
||||
@@ -319,7 +319,7 @@ class UnifiedBillingClient:
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.patch(
|
||||
f"{self.commerce_url}/api/v1/user/{unified_user.commerce_customer_id}",
|
||||
f"{self.commerce_url}/v1/user/{unified_user.commerce_customer_id}",
|
||||
json={
|
||||
"email": iam_user.email,
|
||||
"name": iam_user.name,
|
||||
|
||||
@@ -79,7 +79,7 @@ class UnifiedQueryClient:
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
# Query Lux Indexer
|
||||
response = await client.get(
|
||||
f"{self.indexer_url}/api/v2/{chain}/{network}/blocks/{block_number}"
|
||||
f"{self.indexer_url}/v1/indexer/{chain}/{network}/blocks/{block_number}"
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
@@ -118,7 +118,7 @@ class UnifiedQueryClient:
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.indexer_url}/api/v2/{chain}/{network}/blocks",
|
||||
f"{self.indexer_url}/v1/indexer/{chain}/{network}/blocks",
|
||||
params=params,
|
||||
)
|
||||
|
||||
@@ -158,7 +158,7 @@ class UnifiedQueryClient:
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.indexer_url}/api/v2/{chain}/{network}/transactions/{tx_hash}"
|
||||
f"{self.indexer_url}/v1/indexer/{chain}/{network}/transactions/{tx_hash}"
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
@@ -199,11 +199,11 @@ class UnifiedQueryClient:
|
||||
|
||||
# Determine endpoint based on filters
|
||||
if address:
|
||||
endpoint = f"{self.indexer_url}/api/v2/{chain}/{network}/addresses/{address}/transactions"
|
||||
endpoint = f"{self.indexer_url}/v1/indexer/{chain}/{network}/addresses/{address}/transactions"
|
||||
elif block_number:
|
||||
endpoint = f"{self.indexer_url}/api/v2/{chain}/{network}/blocks/{block_number}/transactions"
|
||||
endpoint = f"{self.indexer_url}/v1/indexer/{chain}/{network}/blocks/{block_number}/transactions"
|
||||
else:
|
||||
endpoint = f"{self.indexer_url}/api/v2/{chain}/{network}/transactions"
|
||||
endpoint = f"{self.indexer_url}/v1/indexer/{chain}/{network}/transactions"
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.get(endpoint, params=params)
|
||||
@@ -244,7 +244,7 @@ class UnifiedQueryClient:
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.indexer_url}/api/v2/{chain}/{network}/addresses/{address}"
|
||||
f"{self.indexer_url}/v1/indexer/{chain}/{network}/addresses/{address}"
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
@@ -279,7 +279,7 @@ class UnifiedQueryClient:
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.indexer_url}/api/v2/{chain}/{network}/addresses/{address}/tokens",
|
||||
f"{self.indexer_url}/v1/indexer/{chain}/{network}/addresses/{address}/tokens",
|
||||
params={"limit": limit},
|
||||
)
|
||||
|
||||
@@ -318,7 +318,7 @@ class UnifiedQueryClient:
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.indexer_url}/api/v2/{chain}/{network}/tokens/{address}"
|
||||
f"{self.indexer_url}/v1/indexer/{chain}/{network}/tokens/{address}"
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
@@ -360,7 +360,7 @@ class UnifiedQueryClient:
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.indexer_url}/api/v2/{chain}/{network}/tokens",
|
||||
f"{self.indexer_url}/v1/indexer/{chain}/{network}/tokens",
|
||||
params=params,
|
||||
)
|
||||
|
||||
@@ -400,7 +400,7 @@ class UnifiedQueryClient:
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.indexer_url}/api/v2/{chain}/{network}/smart-contracts/{address}"
|
||||
f"{self.indexer_url}/v1/indexer/{chain}/{network}/smart-contracts/{address}"
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
@@ -452,7 +452,7 @@ class UnifiedQueryClient:
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.indexer_url}/api/v2/{chain}/{network}/logs",
|
||||
f"{self.indexer_url}/v1/indexer/{chain}/{network}/logs",
|
||||
params=params,
|
||||
)
|
||||
|
||||
@@ -527,7 +527,7 @@ class UnifiedQueryClient:
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.indexer_url}/api/v2/{chain}/{network}/stats"
|
||||
f"{self.indexer_url}/v1/indexer/{chain}/{network}/stats"
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "bootnode"
|
||||
version = "1.0.4"
|
||||
version = "1.0.5"
|
||||
description = "Blockchain Development Platform - Multi-chain RPC, Token API, NFT API, Smart Wallets, and more"
|
||||
readme = "README.md"
|
||||
license = { text = "BSD-3-Clause" }
|
||||
|
||||
@@ -268,7 +268,7 @@ class TestCommerceClient:
|
||||
assert result["checkout_url"] is not None
|
||||
client._request.assert_called_once_with(
|
||||
"POST",
|
||||
"/api/v1/checkout/authorize",
|
||||
"/v1/checkout/authorize",
|
||||
json=pytest.approx(client._request.call_args[1]["json"], abs=0) if False else client._request.call_args[1].get("json", client._request.call_args[0][2] if len(client._request.call_args[0]) > 2 else None),
|
||||
)
|
||||
|
||||
@@ -286,7 +286,7 @@ class TestCommerceClient:
|
||||
|
||||
result = await client.capture_checkout("order_123")
|
||||
assert result["status"] == "completed"
|
||||
client._request.assert_called_once_with("POST", "/api/v1/checkout/capture/order_123")
|
||||
client._request.assert_called_once_with("POST", "/v1/checkout/capture/order_123")
|
||||
|
||||
@patch("bootnode.core.billing.commerce.get_settings")
|
||||
@pytest.mark.asyncio
|
||||
@@ -621,7 +621,7 @@ class TestCommerceClientErrors:
|
||||
MockAsyncClient.return_value = mock_client
|
||||
|
||||
with pytest.raises(CommerceError) as exc_info:
|
||||
await client._request("GET", "/api/v1/test")
|
||||
await client._request("GET", "/v1/test")
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "bad request" in exc_info.value.message
|
||||
@@ -648,7 +648,7 @@ class TestCommerceClientErrors:
|
||||
MockAsyncClient.return_value = mock_client
|
||||
|
||||
with pytest.raises(CommerceError) as exc_info:
|
||||
await client._request("GET", "/api/v1/test")
|
||||
await client._request("GET", "/v1/test")
|
||||
|
||||
assert exc_info.value.status_code == 503
|
||||
|
||||
|
||||
@@ -451,7 +451,7 @@ X-RateLimit-Reset: 1706400000`}</code>
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "X-API-Key: YOUR_API_KEY" \\
|
||||
-d '{
|
||||
"url": "https://myapp.com/api/webhooks/bootnode",
|
||||
"url": "https://myapp.com/webhooks/bootnode",
|
||||
"chain": "ethereum",
|
||||
"event_type": "address_activity",
|
||||
"config": {
|
||||
@@ -460,7 +460,7 @@ X-RateLimit-Reset: 1706400000`}</code>
|
||||
}'`}
|
||||
response={`{
|
||||
"id": "wh_a1b2c3d4e5f6",
|
||||
"url": "https://myapp.com/api/webhooks/bootnode",
|
||||
"url": "https://myapp.com/webhooks/bootnode",
|
||||
"chain": "ethereum",
|
||||
"event_type": "address_activity",
|
||||
"config": {
|
||||
@@ -486,7 +486,7 @@ X-RateLimit-Reset: 1706400000`}</code>
|
||||
"webhooks": [
|
||||
{
|
||||
"id": "wh_a1b2c3d4e5f6",
|
||||
"url": "https://myapp.com/api/webhooks/bootnode",
|
||||
"url": "https://myapp.com/webhooks/bootnode",
|
||||
"chain": "ethereum",
|
||||
"event_type": "address_activity",
|
||||
"status": "active",
|
||||
@@ -515,7 +515,7 @@ X-RateLimit-Reset: 1706400000`}</code>
|
||||
}'`}
|
||||
response={`{
|
||||
"id": "wh_a1b2c3d4e5f6",
|
||||
"url": "https://myapp.com/api/webhooks/bootnode",
|
||||
"url": "https://myapp.com/webhooks/bootnode",
|
||||
"chain": "ethereum",
|
||||
"event_type": "address_activity",
|
||||
"status": "paused",
|
||||
|
||||
@@ -147,7 +147,7 @@ async function monitorWallet(address: string) {
|
||||
"X-API-Key": process.env.BOOTNODE_API_KEY!,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: "https://myapp.com/api/webhooks/wallet-activity",
|
||||
url: "https://myapp.com/webhooks/wallet-activity",
|
||||
chain: "ethereum",
|
||||
event_type: "address_activity",
|
||||
config: {
|
||||
@@ -163,7 +163,7 @@ async function monitorWallet(address: string) {
|
||||
}
|
||||
|
||||
// 2. Handle incoming events (Next.js API route)
|
||||
// app/api/webhooks/wallet-activity/route.ts
|
||||
// app/webhooks/wallet-activity/route.ts
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import crypto from "crypto";
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ console.log("Smart wallet:", wallet.address);
|
||||
|
||||
// Webhooks
|
||||
const webhook = await client.webhooks.create({
|
||||
url: "https://myapp.com/api/webhooks",
|
||||
url: "https://myapp.com/webhooks",
|
||||
chain: "ethereum",
|
||||
eventType: "address_activity",
|
||||
config: { addresses: ["0xd8dA...6045"] },
|
||||
|
||||
@@ -276,7 +276,7 @@ export async function sendGaslessTransfer({
|
||||
sponsored transaction:
|
||||
</p>
|
||||
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
|
||||
<code>{`// app/api/wallet/create/route.ts
|
||||
<code>{`// app/wallet/create/route.ts
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const BOOTNODE_API_KEY = process.env.BOOTNODE_API_KEY!;
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function WebhooksQuickstartPage() {
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "X-API-Key: YOUR_API_KEY" \\
|
||||
-d '{
|
||||
"url": "https://myapp.com/api/webhooks/bootnode",
|
||||
"url": "https://myapp.com/webhooks/bootnode",
|
||||
"chain": "ethereum",
|
||||
"event_type": "address_activity",
|
||||
"config": {
|
||||
@@ -89,7 +89,7 @@ export default function WebhooksQuickstartPage() {
|
||||
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
|
||||
<code>{`{
|
||||
"id": "wh_a1b2c3d4e5f6",
|
||||
"url": "https://myapp.com/api/webhooks/bootnode",
|
||||
"url": "https://myapp.com/webhooks/bootnode",
|
||||
"chain": "ethereum",
|
||||
"event_type": "address_activity",
|
||||
"config": {
|
||||
@@ -135,7 +135,7 @@ const SIGNING_SECRET = process.env.WEBHOOK_SIGNING_SECRET!;
|
||||
|
||||
// IMPORTANT: Use raw body for signature verification
|
||||
app.post(
|
||||
"/api/webhooks/bootnode",
|
||||
"/webhooks/bootnode",
|
||||
express.raw({ type: "application/json" }),
|
||||
(req, res) => {
|
||||
const signature = req.headers["x-bootnode-signature"] as string;
|
||||
|
||||
@@ -333,7 +333,7 @@ export default function MPCProductPage() {
|
||||
<CardContent className="p-0">
|
||||
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-6 text-sm text-zinc-100">
|
||||
<code>{`# Create a distributed wallet
|
||||
curl -X POST https://mpc.yourorg.com/api/v1/wallets \\
|
||||
curl -X POST https://mpc.yourorg.com/v1/wallets \\
|
||||
-H "Authorization: Bearer \$TOKEN" \\
|
||||
-d '{"name": "treasury", "chains": ["ETH", "BTC", "SOL"]}'
|
||||
|
||||
@@ -350,7 +350,7 @@ curl -X POST https://mpc.yourorg.com/api/v1/wallets \\
|
||||
}
|
||||
|
||||
# Sign an Ethereum transaction
|
||||
curl -X POST https://mpc.yourorg.com/api/v1/sign \\
|
||||
curl -X POST https://mpc.yourorg.com/v1/sign \\
|
||||
-H "Authorization: Bearer \$TOKEN" \\
|
||||
-d '{
|
||||
"wallet_id": "w_8f3a...",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
// DigitalOcean Kubernetes regions and Lux network configuration
|
||||
// Used by the fleet management UI for cluster provisioning and monitoring.
|
||||
|
||||
@@ -57,102 +56,3 @@ export const NETWORK_CONFIGS: Record<LuxNetwork, LuxNetworkConfig> = {
|
||||
};
|
||||
|
||||
export const NETWORK_NAMES: LuxNetwork[] = ["mainnet", "testnet", "devnet"] as const;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// API Client Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// One resolver for every caller (lib/api.ts), host-derived at runtime.
|
||||
const API_BASE = getApiUrl();
|
||||
|
||||
function authHeaders(): HeadersInit {
|
||||
const headers: HeadersInit = { "Content-Type": "application/json" };
|
||||
if (typeof window !== "undefined") {
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function request<T = unknown>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
...init,
|
||||
headers: { ...authHeaders(), ...init?.headers },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => "");
|
||||
throw new Error(`${res.status} ${res.statusText}: ${body}`);
|
||||
}
|
||||
// 204 No Content
|
||||
if (res.status === 204) return undefined as T;
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
/** List all fleets, optionally filtered by cluster. */
|
||||
export async function fetchFleets(clusterId?: string) {
|
||||
const qs = clusterId ? `?clusterId=${encodeURIComponent(clusterId)}` : "";
|
||||
return request(`/api/fleets${qs}`);
|
||||
}
|
||||
|
||||
/** Get a single fleet by cluster + network. */
|
||||
export async function fetchFleet(clusterId: string, network: LuxNetwork) {
|
||||
return request(`/api/fleets/${encodeURIComponent(clusterId)}/${network}`);
|
||||
}
|
||||
|
||||
/** Create a new fleet. */
|
||||
export async function createFleet(params: Record<string, unknown>) {
|
||||
return request("/api/fleets", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
}
|
||||
|
||||
/** Update an existing fleet. */
|
||||
export async function updateFleet(
|
||||
clusterId: string,
|
||||
network: LuxNetwork,
|
||||
params: Record<string, unknown>,
|
||||
) {
|
||||
return request(`/api/fleets/${encodeURIComponent(clusterId)}/${network}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
}
|
||||
|
||||
/** Delete a fleet. */
|
||||
export async function deleteFleet(clusterId: string, network: LuxNetwork) {
|
||||
return request(`/api/fleets/${encodeURIComponent(clusterId)}/${network}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
/** Scale a fleet to the given replica count. */
|
||||
export async function scaleFleet(
|
||||
clusterId: string,
|
||||
network: LuxNetwork,
|
||||
replicas: number,
|
||||
) {
|
||||
return request(`/api/fleets/${encodeURIComponent(clusterId)}/${network}/scale`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ replicas }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Restart all nodes in a fleet. */
|
||||
export async function restartFleet(clusterId: string, network: LuxNetwork) {
|
||||
return request(`/api/fleets/${encodeURIComponent(clusterId)}/${network}/restart`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch aggregate stats across all fleets. */
|
||||
export async function fetchStats() {
|
||||
return request("/api/stats");
|
||||
}
|
||||
|
||||
/** Fetch available Lux networks. */
|
||||
export async function fetchNetworks() {
|
||||
return request("/api/networks");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Every path bootnode serves or calls is `/v1/...`; auth is `/v1/iam/*`.
|
||||
//
|
||||
// Run: `pnpm test`.
|
||||
//
|
||||
// Walks the first-party source of every component in this repo — the Python
|
||||
// API, the console, the operator, the fleet service — for an `/api/` path
|
||||
// written as a literal or appended to a base (`"/api/…"`, `` `${base}/api/…` ``,
|
||||
// `f"{url}/api/…"`). The one address allowed is the Kubernetes API
|
||||
// (`/api/v1/namespaces/…`), which is not ours. Tests are skipped: the fleet
|
||||
// suite requests `/api/…` on purpose, to prove it 404s.
|
||||
|
||||
import { strict as assert } from "node:assert"
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs"
|
||||
import { join, relative } from "node:path"
|
||||
import { describe, it } from "node:test"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const ROOT = join(fileURLToPath(new URL(".", import.meta.url)), "..", "..")
|
||||
const DIRS = ["api/bootnode", "api/handlers", "api/store", "api/middleware", "console/app", "console/components", "console/lib", "operator/src", "fleet", "cmd"]
|
||||
const SOURCE = /\.(py|go|rs|ts|tsx|js|mjs)$/
|
||||
const TEST = /(\.test\.tsx?|_test\.go|\/test_[^/]*\.py)$/
|
||||
const PATH = /['"`(}]\/api\//
|
||||
const KUBERNETES = /\/api\/v1\/namespaces/
|
||||
|
||||
function walk(dir: string, out: string[] = []): string[] {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
if (entry === "node_modules" || entry === "__pycache__") continue
|
||||
const full = join(dir, entry)
|
||||
if (statSync(full).isDirectory()) walk(full, out)
|
||||
else out.push(full)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
describe("the /v1 prefix", () => {
|
||||
it("no first-party source names an /api/ path", () => {
|
||||
const hits: string[] = []
|
||||
for (const dir of DIRS) {
|
||||
for (const file of walk(join(ROOT, dir))) {
|
||||
if (!SOURCE.test(file) || TEST.test(file)) continue
|
||||
readFileSync(file, "utf8")
|
||||
.split("\n")
|
||||
.forEach((line, i) => {
|
||||
if (PATH.test(line) && !KUBERNETES.test(line)) hits.push(`${relative(ROOT, file)}:${i + 1}: ${line.trim()}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
assert.deepEqual(hits, [])
|
||||
})
|
||||
})
|
||||
+1
-1
@@ -321,7 +321,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/v1/iam/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -62,10 +62,8 @@ spec:
|
||||
limits:
|
||||
cpu: "1000m"
|
||||
memory: "512Mi"
|
||||
# Probe `/`, not `/api/health`. The runtime is a static file server:
|
||||
# `/` is the one path guaranteed to exist and it proves the bundle is
|
||||
# actually being served. `/api/health` also broke the standing rule
|
||||
# that public paths are /v1/*, never /api/*.
|
||||
# Probe `/`. The runtime is a static file server: `/` is the one path
|
||||
# guaranteed to exist and it proves the bundle is actually being served.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
|
||||
+2
-2
@@ -28,8 +28,8 @@ pub fn router(state: Arc<ApiState>) -> Router {
|
||||
Router::new()
|
||||
.route("/healthz", get(healthz))
|
||||
.route("/readyz", get(readyz))
|
||||
.route("/api/v1/clusters/:namespace/:name/nodes", get(list_nodes))
|
||||
.route("/api/v1/clusters/:namespace/:name/restart", post(restart_cluster))
|
||||
.route("/v1/clusters/:namespace/:name/nodes", get(list_nodes))
|
||||
.route("/v1/clusters/:namespace/:name/restart", post(restart_cluster))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user