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:
@@ -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, [])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user