feat: Go rewrite scaffold — ZAP-first, Mount() per HIP-0106 (#1)

Adds a Go subsystem alongside the existing Python FastAPI service so
bootnode can fold into `superbase` per HIP-0106. The Python service in
`api/bootnode/` stays untouched.

What landed:

- `schema/bootnode.zap` — public typed surface (chains, RPC, tokens,
  NFTs, transfers, smart wallets, ERC-4337, gas, webhooks, fleets,
  nodes, usage). No `.capnp` anywhere — ZAP only, per brand policy.
- `zap/gen/` — hand-rolled Go bindings that match what `zapc generate`
  emits, so `go build` works without zapc installed locally.
- `api/mount.go` — the HIP-0106 contract:
    func Mount(r chi.Router, deps Deps) error
  mounts every FastAPI-parity route group through `api/handlers/`.
- `api/deps.go` — cross-subsystem ZAP client interfaces (IAM, KMS,
  Base, O11y, Commerce). No business logic, no hard imports — keeps
  the Bootnode module independent of sibling subsystems until they
  ship their typed clients.
- `api/handlers/` — 149 chi routes mounted (Python has 150). Each
  handler body is a 501 stub with a `TODO(bootnode-port)` pointing at
  the Python file + symbol to port.
- `api/middleware/` — IAM-backed auth, OTEL-friendly telemetry,
  per-org rate limit (stub).
- `api/store/` — per-tenant SQLite access via `deps.Base.Open(orgID,
  "bootnode")` per HIP-0302.
- `api/web3/` — chain registry + RPC pool surface (production version
  prefers luxfi/* over upstream go-ethereum).
- `cmd/bootnode/main.go` — standalone shim; same `api.Mount` superbase
  calls in production.
- `docs/PYTHON_TO_GO_PORT.md` — endpoint-by-endpoint port map.

Build: `go build ./...` and `go vet ./...` clean. Standalone shim
smokes 200 on /health, /ready, /v1/chains; 401 on auth-required
endpoints with no JWT; 501 on every TODO route.

Out of scope (not in this commit):
- Handler bodies (Phase 2 — porting in RPC → tokens → NFTs → ...
  priority order).
- superbase wiring (lands when the superbase repo opens its main).
- Python service removal (stays until handlers reach prod parity).
This commit is contained in:
2026-05-18 23:36:46 -07:00
committed by GitHub
parent f0abbd762d
commit df6919b6ff
33 changed files with 2784 additions and 0 deletions
+48
View File
@@ -2,6 +2,14 @@
**Web3 Backend in a Box** - Run locally or deploy to cloud.
> **Go rewrite (HIP-0106) in progress.** The current production
> service is the Python FastAPI app under [`api/`](api/). A Go
> scaffold that mounts into [`superbase`](../superbase) per HIP-0106
> lives alongside it — `api/`, `cmd/bootnode/`, `schema/`,
> `zap/`. See [`docs/PYTHON_TO_GO_PORT.md`](docs/PYTHON_TO_GO_PORT.md)
> for the endpoint-by-endpoint port map and the new
> [Go README section](#go-rewrite-hip-0106) below.
## Quick Start
```bash
@@ -61,3 +69,43 @@ Visit [web3.hanzo.ai](https://web3.hanzo.ai) for managed hosting.
## License
Apache-2.0
## Go rewrite (HIP-0106)
The Go subsystem mounts into `superbase` (the unified Hanzo Cloud
binary). It exposes a single `Mount(r chi.Router, deps api.Deps) error`
entry point and a public ZAP schema (`schema/bootnode.zap`) covering
every cross-subsystem type.
```
bootnode/
schema/bootnode.zap # public typed surface (ZAP)
zap/gen/ # zapc-generated Go (hand-stubs today)
api/ # subsystem code (chi handlers + store)
mount.go # HIP-0106 contract
deps.go # IAM/KMS/Base/O11y/Commerce interfaces
handlers/ # one MountXxx per FastAPI route group
middleware/ # auth, telemetry, rate-limit
store/ # per-tenant SQLite via deps.Base
web3/ # chain registry + RPC pool
cmd/bootnode/main.go # standalone dev shim
docs/PYTHON_TO_GO_PORT.md # endpoint-by-endpoint mapping
```
### Build
```bash
go build ./...
go vet ./...
go run ./cmd/bootnode # listens on :8100 (BOOTNODE_ADDR to override)
```
### Generate ZAP bindings
```bash
cd zap && make gen # no-op if zapc isn't installed
```
`zapc` lives at `~/work/hanzo/zap/zapc-rs/`. Brand policy: no
`.capnp` files, no "Cap'n Proto" references on the public surface
(HIP-0106).
+93
View File
@@ -0,0 +1,93 @@
// Package api is the Bootnode subsystem entry point. It exposes
// Mount(r, deps) per HIP-0106 so superbase can fold Bootnode into the
// unified Hanzo Cloud binary, and a standalone main shim
// (cmd/bootnode) wires the same Mount onto a fresh chi.Mux.
//
// No business logic lives in main — everything routes through Mount,
// every cross-subsystem call goes through deps, and every typed
// surface is defined in ../schema/bootnode.zap.
package api
import (
"context"
"log/slog"
)
// Deps is the cross-subsystem contract Bootnode consumes. Every
// non-trivial dependency is a typed ZAP client (in-process or RPC)
// resolved by superbase at startup. Subsystems that aren't enabled
// pass nil — handlers fail closed.
//
// The struct deliberately uses interface fields so superbase can hand
// us in-process implementations (direct Go calls through the local
// ZAP dispatcher) and a future split deployment can hand us an RPC
// client without touching this package.
type Deps struct {
// Brand identifies the deployment (hanzo / lux / zoo / bootnode).
// Drives logo, CORS allow-list, KMS prefix, IAM org default.
Brand string
// Logger is the structured logger plumbed in by superbase. Required.
Logger *slog.Logger
// IAM authenticates incoming JWTs. nil → fail-closed at the
// auth middleware. The interface mirrors hanzo/iam/zap.Client; we
// keep a local interface so the Bootnode module doesn't take a
// hard dependency on the IAM module until the import graph is wired.
IAM IAMClient
// KMS resolves secrets. nil → secrets cannot be loaded; the
// affected handlers return 503.
KMS KMSClient
// Base is the per-tenant SQLite gateway (HIP-0302). nil → store
// layer falls back to an in-memory map (dev only).
Base BaseClient
// O11y emits traces/metrics/logs through the shared OTEL pipeline.
// nil → telemetry middleware no-ops.
O11y O11yClient
// Commerce is the upstream billing service Bootnode reports usage
// to. nil → usage records accumulate locally and never sync.
Commerce CommerceClient
}
// IAMClient is the minimal interface Bootnode needs from hanzo/iam.
// Once the iam Go module exposes its zap client we replace this with
// a type alias to `iamzap.Client`.
type IAMClient interface {
// VerifyJWT validates a bearer token and returns the canonical
// (orgID, userID, email) tuple. Implementations MUST fail closed
// on signature mismatch / expiry / unknown issuer.
VerifyJWT(ctx context.Context, bearer string) (orgID, userID, email string, err error)
}
// KMSClient is the minimal interface Bootnode needs from hanzo/kms.
type KMSClient interface {
GetSecret(ctx context.Context, ref string) (string, error)
}
// BaseClient is the per-tenant SQLite gateway (HIP-0302). Bootnode
// asks for a connection scoped to (orgID, "bootnode") and stores its
// projects / api_keys / webhooks / usage there.
type BaseClient interface {
// Open returns a per-tenant database handle. The opaque type is a
// chosen-by-Base value (e.g. *sql.DB or a typed wrapper); we keep
// it `any` here to avoid pulling in database/sql before the Base
// module's public ZAP client lands.
Open(ctx context.Context, orgID, namespace string) (any, error)
}
// O11yClient is the minimal observability surface. Implementations
// should be safe to call with a nil receiver — handlers should never
// have to nil-check.
type O11yClient interface {
RecordRequest(ctx context.Context, route string, status int, durationMs int64)
}
// CommerceClient mirrors the commerce ZAP RecordUsage shape so we can
// report compute-unit accounting upstream.
type CommerceClient interface {
RecordUsage(ctx context.Context, orgID, projectID, chain, method string, computeUnits int32) error
}
+33
View File
@@ -0,0 +1,33 @@
package handlers
import (
"net/http"
"github.com/go-chi/chi/v5"
)
// MountAuth registers /v1/auth/* (local-dev auth + IAM OAuth bridge).
// In production every authenticated request flows through deps.IAM —
// the local register/login paths exist for dev parity only.
func MountAuth(r chi.Router, _ Deps) {
r.Route("/auth", func(r chi.Router) {
r.Post("/register", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/auth/__init__.py:register —
// MUST bcrypt the password (golang.org/x/crypto/bcrypt). Never plaintext.
stub(w, "auth/register")
})
r.Post("/login", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/auth/__init__.py:login
stub(w, "auth/login")
})
r.Get("/me", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/auth/__init__.py:me
stub(w, "auth/me")
})
r.Post("/oauth/callback", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/auth/__init__.py:oauth_callback —
// exchange code at hanzo.id, return bearer.
stub(w, "auth/oauth/callback")
})
})
}
+63
View File
@@ -0,0 +1,63 @@
package handlers
import (
"net/http"
"github.com/go-chi/chi/v5"
bootnodezap "github.com/hanzoai/bootnode/zap/gen"
"github.com/hanzoai/bootnode/api/middleware"
)
// MountAuthKeys registers /v1/auth-keys/* — project + API key admin.
// The plaintext API key is returned ONCE on creation and never
// stored: deps.Service.CreateAPIKey is responsible for bcrypt'ing the
// secret before writing to deps.Base.
func MountAuthKeys(r chi.Router, d Deps) {
r.Route("/auth-keys", func(r chi.Router) {
r.Post("/projects", middleware.RequireAuth(func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/auth_keys.py:create_project
stub(w, "auth-keys/projects")
}))
r.Get("/projects/{project_id}", middleware.RequireAuth(func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/auth_keys.py:get_project
stub(w, "auth-keys/projects/{id}")
}))
r.Post("/keys", middleware.RequireAuth(func(w http.ResponseWriter, r *http.Request) {
var req struct {
ProjectID string `json:"projectId"`
Name string `json:"name"`
Scopes []string `json:"scopes"`
}
if !readJSON(w, r, &req) {
return
}
res, err := d.Service.CreateAPIKey(r.Context(), middleware.OrgID(r.Context()), req.ProjectID, req.Name, req.Scopes)
if err != nil {
serviceErr(w, err, "auth-keys/keys POST")
return
}
writeJSON(w, http.StatusCreated, res)
}))
r.Get("/keys", middleware.RequireAuth(func(w http.ResponseWriter, r *http.Request) {
keys, err := d.Service.ListAPIKeys(r.Context(), middleware.OrgID(r.Context()))
if err != nil {
serviceErr(w, err, "auth-keys/keys GET")
return
}
if keys == nil {
keys = []bootnodezap.APIKey{}
}
writeJSON(w, http.StatusOK, keys)
}))
r.Delete("/keys/{key_id}", middleware.RequireAuth(func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "key_id")
ok, err := d.Service.RevokeAPIKey(r.Context(), middleware.OrgID(r.Context()), id)
if err != nil {
serviceErr(w, err, "auth-keys/keys/{id} DELETE")
return
}
writeJSON(w, http.StatusOK, map[string]bool{"ok": ok})
}))
})
}
+30
View File
@@ -0,0 +1,30 @@
package handlers
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/hanzoai/bootnode/api/web3"
)
// MountChains registers /v1/chains/* — chain discovery.
func MountChains(r chi.Router, _ Deps) {
r.Route("/chains", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"chains": web3.AllChains(),
"count": len(web3.AllChains()),
})
})
r.Get("/{chain}", func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "chain")
info, ok := web3.ChainByName(name)
if !ok {
writeJSON(w, http.StatusNotFound, errBody{"unknown chain: " + name})
return
}
writeJSON(w, http.StatusOK, info)
})
})
}
+52
View File
@@ -0,0 +1,52 @@
// Package handlers holds the chi route mounters and HTTP handlers
// for the Bootnode subsystem. Each MountXxx function wires one
// FastAPI-parity route group; handler bodies are scaffolded as 501
// stubs with TODO(bootnode-port) markers pointing at the Python file
// + symbol they need to replace.
package handlers
import (
"context"
"log/slog"
bootnodezap "github.com/hanzoai/bootnode/zap/gen"
)
// Deps is the per-handler dependency bundle. It mirrors api.Deps but
// without the import cycle: api/mount.go owns the canonical Deps
// struct and forwards a value-copied view here.
type Deps struct {
Logger *slog.Logger
Service bootnodezap.Service
IAM IAMClient
KMS KMSClient
Base BaseClient
O11y O11yClient
Commerce CommerceClient
}
// IAMClient mirrors api.IAMClient (interfaces are nominal in Go but
// we keep the type local to avoid the cycle).
type IAMClient interface {
VerifyJWT(ctx context.Context, bearer string) (orgID, userID, email string, err error)
}
// KMSClient mirrors api.KMSClient.
type KMSClient interface {
GetSecret(ctx context.Context, ref string) (string, error)
}
// BaseClient mirrors api.BaseClient.
type BaseClient interface {
Open(ctx context.Context, orgID, namespace string) (any, error)
}
// O11yClient mirrors api.O11yClient.
type O11yClient interface {
RecordRequest(ctx context.Context, route string, status int, durationMs int64)
}
// CommerceClient mirrors api.CommerceClient.
type CommerceClient interface {
RecordUsage(ctx context.Context, orgID, projectID, chain, method string, computeUnits int32) error
}
+32
View File
@@ -0,0 +1,32 @@
package handlers
import (
"net/http"
)
// Health is the liveness endpoint mounted at /health.
func Health(_ Deps) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"status": "healthy",
"version": "2.0.0",
"impl": "go",
})
}
}
// Ready is the readiness endpoint mounted at /ready. It probes every
// non-nil dep and returns 503 when one is unreachable.
func Ready(d Deps) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/main.py readiness checks —
// dep probes (KMS, Base, Commerce). For now we just report wiring.
writeJSON(w, http.StatusOK, map[string]any{
"iam": d.IAM != nil,
"kms": d.KMS != nil,
"base": d.Base != nil,
"o11y": d.O11y != nil,
"commerce": d.Commerce != nil,
})
}
}
+178
View File
@@ -0,0 +1,178 @@
package handlers
import (
"net/http"
"github.com/go-chi/chi/v5"
)
// MountFleets registers /v1/fleets/* — multi-replica node deployments.
func MountFleets(r chi.Router, _ Deps) {
r.Route("/fleets", func(r chi.Router) {
r.Post("/", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:create
stub(w, "fleets POST")
})
r.Get("/", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:list
stub(w, "fleets GET")
})
r.Get("/stats", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:stats
stub(w, "fleets/stats")
})
r.Get("/chains", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:chains
stub(w, "fleets/chains")
})
r.Get("/networks", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:networks
stub(w, "fleets/networks")
})
r.Get("/clusters", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:list_clusters
stub(w, "fleets/clusters GET")
})
r.Post("/clusters", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:create_cluster
stub(w, "fleets/clusters POST")
})
r.Delete("/clusters/{cluster_id}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:delete_cluster
stub(w, "fleets/clusters/{id} DELETE")
})
r.Get("/{chain}/{cluster_id}/{network}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:get_fleet
stub(w, "fleets/{chain}/{cluster}/{network}")
})
r.Patch("/{chain}/{cluster_id}/{network}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:update_fleet
stub(w, "fleets/{chain}/{cluster}/{network} PATCH")
})
r.Delete("/{chain}/{cluster_id}/{network}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:delete_fleet
stub(w, "fleets/{chain}/{cluster}/{network} DELETE")
})
r.Post("/{chain}/{cluster_id}/{network}/scale", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:scale_fleet
stub(w, "fleets scale")
})
r.Post("/{chain}/{cluster_id}/{network}/restart", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:restart_fleet
stub(w, "fleets restart")
})
r.Get("/{chain}/{cluster_id}/{network}/pods/{pod_name}/logs", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:pod_logs
stub(w, "fleets pod logs")
})
r.Get("/{chain}/{cluster_id}/{network}/pods/{pod_name}/health", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:pod_health
stub(w, "fleets pod health")
})
_ = chi.URLParam
})
}
// MountNodes registers /v1/nodes/* — single-instance node lifecycle.
func MountNodes(r chi.Router, _ Deps) {
r.Route("/nodes", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:list
stub(w, "nodes GET")
})
r.Get("/presets", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:presets
stub(w, "nodes/presets")
})
r.Get("/supported-chains", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:supported_chains
stub(w, "nodes/supported-chains")
})
r.Post("/", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:create —
// docker-managed nodes via deps.Compute (was direct docker SDK).
stub(w, "nodes POST")
})
r.Get("/stats", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:stats
stub(w, "nodes/stats")
})
r.Get("/metrics/all", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:metrics_all
stub(w, "nodes/metrics/all")
})
r.Get("/docker/status", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:docker_status
stub(w, "nodes/docker/status")
})
r.Get("/{node_id}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:get
stub(w, "nodes/{id}")
})
r.Delete("/{node_id}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:delete
stub(w, "nodes/{id} DELETE")
})
r.Post("/{node_id}/start", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:start
stub(w, "nodes/{id}/start")
})
r.Post("/{node_id}/stop", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:stop
stub(w, "nodes/{id}/stop")
})
r.Post("/{node_id}/rpc", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:rpc_proxy
stub(w, "nodes/{id}/rpc")
})
})
}
// MountInfra registers /v1/infra/* — K8s clusters / volumes / snapshots / clones.
func MountInfra(r chi.Router, _ Deps) {
r.Route("/infra", func(r chi.Router) {
// Clusters
r.Get("/clusters", stubHandler("infra/clusters GET", "list_clusters"))
r.Get("/clusters/{cluster_id}", stubHandler("infra/clusters/{id}", "get_cluster"))
r.Post("/clusters", stubHandler("infra/clusters POST", "create_cluster"))
r.Delete("/clusters/{cluster_id}", stubHandler("infra/clusters/{id} DELETE", "delete_cluster"))
r.Post("/clusters/{cluster_id}/scale", stubHandler("infra/clusters/{id}/scale", "scale_cluster"))
r.Get("/clusters/{cluster_id}/kubeconfig", stubHandler("infra/clusters/{id}/kubeconfig", "kubeconfig"))
// Volumes
r.Get("/volumes", stubHandler("infra/volumes GET", "list_volumes"))
r.Get("/volumes/{volume_id}", stubHandler("infra/volumes/{id}", "get_volume"))
r.Post("/volumes", stubHandler("infra/volumes POST", "create_volume"))
r.Delete("/volumes/{volume_id}", stubHandler("infra/volumes/{id} DELETE", "delete_volume"))
r.Post("/volumes/{volume_id}/attach", stubHandler("infra/volumes/{id}/attach", "attach_volume"))
r.Post("/volumes/{volume_id}/detach", stubHandler("infra/volumes/{id}/detach", "detach_volume"))
r.Post("/volumes/{volume_id}/resize", stubHandler("infra/volumes/{id}/resize", "resize_volume"))
// Snapshots
r.Get("/snapshots", stubHandler("infra/snapshots GET", "list_snapshots"))
r.Post("/snapshots", stubHandler("infra/snapshots POST", "create_snapshot"))
r.Delete("/snapshots/{snapshot_id}", stubHandler("infra/snapshots/{id} DELETE", "delete_snapshot"))
// Clone
r.Post("/clone", stubHandler("infra/clone POST", "create_clone"))
r.Get("/clone", stubHandler("infra/clone GET", "list_clones"))
r.Get("/clone/{clone_id}", stubHandler("infra/clone/{id}", "get_clone"))
r.Delete("/clone/{clone_id}", stubHandler("infra/clone/{id} DELETE", "cancel_clone"))
// Services
r.Get("/services", stubHandler("infra/services GET", "list_services"))
r.Get("/services/{service}/status", stubHandler("infra/services/{s}/status", "service_status"))
r.Post("/services/{service}/deploy", stubHandler("infra/services/{s}/deploy", "service_deploy"))
r.Post("/services/{service}/scale", stubHandler("infra/services/{s}/scale", "service_scale"))
r.Post("/services/{service}/restart", stubHandler("infra/services/{s}/restart", "service_restart"))
r.Delete("/services/{service}", stubHandler("infra/services/{s} DELETE", "service_delete"))
r.Get("/services/{service}/logs", stubHandler("infra/services/{s}/logs", "service_logs"))
r.Get("/services/{service}/health", stubHandler("infra/services/{s}/health", "service_health"))
// Stats
r.Get("/stats", stubHandler("infra/stats", "infra_stats"))
})
}
// stubHandler is shorthand for the giant infra routing table.
func stubHandler(route, pyFunc string) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/infra/__init__.py:<pyFunc>
stub(w, route+" — port: bootnode/api/infra/__init__.py:"+pyFunc)
}
}
+101
View File
@@ -0,0 +1,101 @@
package handlers
import (
"github.com/go-chi/chi/v5"
)
// MountTeam registers /v1/team/* — org-scoped membership admin.
func MountTeam(r chi.Router, _ Deps) {
r.Route("/team", func(r chi.Router) {
r.Get("/", stubHandler("team GET", "list_members"))
r.Post("/", stubHandler("team POST", "invite_member"))
r.Patch("/{member_id}", stubHandler("team/{id} PATCH", "update_member"))
r.Delete("/{member_id}", stubHandler("team/{id} DELETE", "remove_member"))
})
}
// MountNetworks registers /v1/networks/* — managed network lifecycle.
func MountNetworks(r chi.Router, _ Deps) {
r.Route("/networks", func(r chi.Router) {
r.Post("/", stubHandler("networks POST", "create_network"))
r.Get("/", stubHandler("networks GET", "list_networks"))
r.Get("/{network_id}", stubHandler("networks/{id}", "get_network"))
r.Post("/{network_id}/scale", stubHandler("networks/{id}/scale", "scale_network"))
r.Delete("/{network_id}", stubHandler("networks/{id} DELETE", "delete_network"))
})
}
// MountObservability registers /v1/o11y/*. The actual telemetry pipe
// belongs to the O11y subsystem — these endpoints are the
// query/control plane Bootnode exposes for its own dashboards.
func MountObservability(r chi.Router, _ Deps) {
r.Route("/o11y", func(r chi.Router) {
r.Get("/health", stubHandler("o11y/health", "platform_health"))
r.Get("/metrics", stubHandler("o11y/metrics", "list_metrics"))
r.Get("/metrics/{service}", stubHandler("o11y/metrics/{service}", "service_metrics"))
r.Get("/alerts", stubHandler("o11y/alerts GET", "list_alerts"))
r.Post("/alerts", stubHandler("o11y/alerts POST", "create_alert"))
r.Get("/service-map", stubHandler("o11y/service-map", "service_map"))
r.Get("/logs", stubHandler("o11y/logs", "query_logs"))
r.Get("/events", stubHandler("o11y/events", "query_events"))
})
}
// MountChat registers /v1/chat/* — AI inference passthrough. Routes
// forward to deps.LLMGateway in production; stubbed in skeleton.
func MountChat(r chi.Router, _ Deps) {
r.Route("/chat", func(r chi.Router) {
r.Post("/completions", stubHandler("chat/completions", "completions"))
r.Get("/models", stubHandler("chat/models", "list_models"))
})
}
// MountBilling registers /v1/billing/* — usage, quota, invoices,
// checkout. Payment processing belongs to Commerce; Bootnode just
// reports usage and reads back per-org tiers.
func MountBilling(r chi.Router, _ Deps) {
r.Route("/billing", func(r chi.Router) {
r.Get("/usage", stubHandler("billing/usage", "get_usage"))
r.Get("/usage/summary", stubHandler("billing/usage/summary", "usage_summary"))
r.Get("/subscription", stubHandler("billing/subscription", "get_subscription"))
r.Post("/subscription/upgrade", stubHandler("billing/subscription/upgrade", "upgrade"))
r.Post("/subscription/downgrade", stubHandler("billing/subscription/downgrade", "downgrade"))
r.Get("/invoices", stubHandler("billing/invoices", "list_invoices"))
r.Get("/tiers", stubHandler("billing/tiers", "list_tiers"))
r.Get("/tiers/{tier_name}", stubHandler("billing/tiers/{name}", "get_tier"))
r.Get("/limits", stubHandler("billing/limits", "get_limits"))
r.Post("/checkout", stubHandler("billing/checkout", "checkout"))
r.Post("/checkout/capture/{order_id}", stubHandler("billing/checkout/capture", "capture"))
r.Post("/webhooks/commerce", stubHandler("billing/webhooks/commerce", "commerce_webhook"))
r.Post("/sync", stubHandler("billing/sync POST", "sync_usage"))
r.Get("/sync/status", stubHandler("billing/sync/status", "sync_status"))
r.Get("/account", stubHandler("billing/account GET", "get_account"))
r.Get("/account/subscriptions", stubHandler("billing/account/subscriptions", "account_subscriptions"))
r.Get("/account/invoices", stubHandler("billing/account/invoices", "account_invoices"))
r.Get("/account/payment-methods", stubHandler("billing/account/payment-methods", "account_methods"))
r.Post("/account/sync", stubHandler("billing/account/sync", "account_sync"))
r.Post("/cloud/authorize", stubHandler("billing/cloud/authorize", "cloud_authorize"))
r.Get("/cloud/quota", stubHandler("billing/cloud/quota", "cloud_quota"))
r.Post("/cloud/usage", stubHandler("billing/cloud/usage", "cloud_record_usage"))
r.Get("/cloud/tiers", stubHandler("billing/cloud/tiers", "cloud_tiers"))
})
}
// MountZAP registers /v1/zap/* — discovery endpoints for the native
// ZAP protocol. The wire-level RPC server lives in superbase; these
// HTTP endpoints describe the schema + tools list to clients.
//
// We keep the path names but do NOT serve a .capnp file — public
// surface is .zap only (brand policy).
func MountZAP(r chi.Router, _ Deps) {
r.Route("/zap", func(r chi.Router) {
r.Get("/connect", stubHandler("zap/connect", "connect_info"))
r.Get("/info", stubHandler("zap/info", "server_info"))
r.Get("/tools", stubHandler("zap/tools", "list_tools"))
r.Get("/resources", stubHandler("zap/resources", "list_resources"))
r.Get("/health", stubHandler("zap/health", "zap_health"))
r.Get("/schema", stubHandler("zap/schema", "serve_schema"))
// schema.capnp endpoint is deliberately NOT mounted — ZAP-only public surface.
})
_ = chi.URLParam
}
+49
View File
@@ -0,0 +1,49 @@
package handlers
import (
"net/http"
"github.com/go-chi/chi/v5"
bootnodezap "github.com/hanzoai/bootnode/zap/gen"
)
// MountRPC registers /v1/rpc/{chain}[/{network}] — JSON-RPC proxy.
func MountRPC(r chi.Router, d Deps) {
r.Route("/rpc", func(r chi.Router) {
r.Post("/{chain}", rpcHandler(d, ""))
r.Post("/{chain}/{network}", rpcHandler(d, ""))
})
}
func rpcHandler(d Deps, _ string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
chain := chi.URLParam(r, "chain")
network := chi.URLParam(r, "network")
if network == "" {
network = string(bootnodezap.NetworkMainnet)
}
var raw struct {
Method string `json:"method"`
Params any `json:"params"`
ID int64 `json:"id"`
}
if !readJSON(w, r, &raw) {
return
}
// TODO(bootnode-port): port from bootnode/api/rpc/__init__.py:rpc_proxy —
// upstream selection via web3.Pool, response caching via deps.Base,
// compute-unit metering via deps.Commerce.RecordUsage.
_, err := d.Service.RPC(r.Context(), bootnodezap.RPCRequest{
Chain: bootnodezap.Chain(chain),
Network: bootnodezap.Network(network),
Method: raw.Method,
ID: raw.ID,
})
if err != nil {
serviceErr(w, err, "rpc/"+chain+"/"+network)
return
}
}
}
+219
View File
@@ -0,0 +1,219 @@
package handlers
import (
"context"
"errors"
"log/slog"
bootnodezap "github.com/hanzoai/bootnode/zap/gen"
)
// ServiceDeps is the (smaller) dependency bundle the in-process ZAP
// service implementation consumes. Distinct from the HTTP handler
// Deps because the ZAP path doesn't need a Service handle to itself.
type ServiceDeps struct {
Logger *slog.Logger
IAM IAMClient
KMS KMSClient
Base BaseClient
O11y O11yClient
Commerce CommerceClient
}
// service is the canonical bootnodezap.Service implementation. The
// HTTP handlers wrap it where they need typed access; superbase
// hands the same instance to peer subsystems via the in-process ZAP
// dispatcher (HIP-0106).
//
// Every method below is a stub that returns ErrNotImplemented with a
// TODO(bootnode-port) tag pointing at the Python file we need to
// port. We keep them on one struct so the interface contract stays
// honest: when zapc regenerates bindings, the method set must match
// or compile fails.
type service struct {
deps ServiceDeps
}
// NewService builds the in-process Service implementation.
func NewService(d ServiceDeps) bootnodezap.Service { return &service{deps: d} }
// ErrNotImplemented is the canonical "stub" error. Handlers convert
// it to HTTP 501.
var ErrNotImplemented = errors.New("bootnode: not implemented (port in progress)")
// ---- RPC ------------------------------------------------------------------
func (s *service) RPC(_ context.Context, _ bootnodezap.RPCRequest) (bootnodezap.RPCResponse, error) {
// TODO(bootnode-port): port from bootnode/api/rpc/__init__.py — JSON-RPC
// proxy fanned out across upstream nodes via web3.HTTPProvider equivalent
// in api/web3/client.go.
return bootnodezap.RPCResponse{}, ErrNotImplemented
}
// ---- Tokens ---------------------------------------------------------------
func (s *service) TokenBalances(_ context.Context, _ bootnodezap.Chain, _ string) ([]bootnodezap.TokenBalance, error) {
// TODO(bootnode-port): port from bootnode/api/tokens/__init__.py:balances
return nil, ErrNotImplemented
}
func (s *service) TokenMetadata(_ context.Context, _ bootnodezap.Chain, _ string) (bootnodezap.TokenMetadata, error) {
// TODO(bootnode-port): port from bootnode/api/tokens/__init__.py:metadata
return bootnodezap.TokenMetadata{}, ErrNotImplemented
}
// ---- NFTs -----------------------------------------------------------------
func (s *service) NFTMetadata(_ context.Context, _ bootnodezap.Chain, _, _ string) (bootnodezap.NFTMetadata, error) {
// TODO(bootnode-port): port from bootnode/api/nfts/__init__.py:metadata
return bootnodezap.NFTMetadata{}, ErrNotImplemented
}
func (s *service) NFTCollection(_ context.Context, _ bootnodezap.Chain, _ string) (bootnodezap.NFTCollection, error) {
// TODO(bootnode-port): port from bootnode/api/nfts/__init__.py:collection
return bootnodezap.NFTCollection{}, ErrNotImplemented
}
func (s *service) NFTsOwned(_ context.Context, _ bootnodezap.Chain, _ string) ([]bootnodezap.NFTMetadata, error) {
// TODO(bootnode-port): port from bootnode/api/nfts/__init__.py:owned
return nil, ErrNotImplemented
}
// ---- Transfers ------------------------------------------------------------
func (s *service) ListTransfers(_ context.Context, _ bootnodezap.Chain, _ string) ([]bootnodezap.Transfer, error) {
// TODO(bootnode-port): port from bootnode/api/transfers/__init__.py:address
return nil, ErrNotImplemented
}
// ---- Smart wallets --------------------------------------------------------
func (s *service) CreateSmartWallet(_ context.Context, _ bootnodezap.Chain, _ string) (bootnodezap.SmartWallet, error) {
// TODO(bootnode-port): port from bootnode/api/wallets/__init__.py:create
return bootnodezap.SmartWallet{}, ErrNotImplemented
}
func (s *service) GetSmartWallet(_ context.Context, _ string) (bootnodezap.SmartWallet, error) {
// TODO(bootnode-port): port from bootnode/api/wallets/__init__.py:get
return bootnodezap.SmartWallet{}, ErrNotImplemented
}
// ---- Bundler --------------------------------------------------------------
func (s *service) SendUserOp(_ context.Context, _ bootnodezap.Chain, _ bootnodezap.UserOp) (string, error) {
// TODO(bootnode-port): port from bootnode/api/bundler/__init__.py:sendUserOperation
return "", ErrNotImplemented
}
func (s *service) GetUserOpReceipt(_ context.Context, _ bootnodezap.Chain, _ string) (bootnodezap.UserOpReceipt, error) {
// TODO(bootnode-port): port from bootnode/api/bundler/__init__.py:getUserOperationReceipt
return bootnodezap.UserOpReceipt{}, ErrNotImplemented
}
// ---- Gas ------------------------------------------------------------------
func (s *service) GasPrices(_ context.Context, _ bootnodezap.Chain) (bootnodezap.GasPrices, error) {
// TODO(bootnode-port): port from bootnode/api/gas/__init__.py:prices
return bootnodezap.GasPrices{}, ErrNotImplemented
}
func (s *service) SponsorUserOp(_ context.Context, _ bootnodezap.Chain, _ bootnodezap.UserOp, _ string) (string, error) {
// TODO(bootnode-port): port from bootnode/api/gas/__init__.py:sponsor
return "", ErrNotImplemented
}
// ---- Webhooks -------------------------------------------------------------
func (s *service) CreateWebhook(_ context.Context, _ bootnodezap.Webhook) (bootnodezap.Webhook, error) {
// TODO(bootnode-port): port from bootnode/api/webhooks/__init__.py:create
return bootnodezap.Webhook{}, ErrNotImplemented
}
func (s *service) ListWebhooks(_ context.Context, _ string) ([]bootnodezap.Webhook, error) {
// TODO(bootnode-port): port from bootnode/api/webhooks/__init__.py:list
return nil, ErrNotImplemented
}
func (s *service) DeleteWebhook(_ context.Context, _ string) (bool, error) {
// TODO(bootnode-port): port from bootnode/api/webhooks/__init__.py:delete
return false, ErrNotImplemented
}
// ---- API keys / projects --------------------------------------------------
func (s *service) CreateAPIKey(_ context.Context, _, _, _ string, _ []string) (bootnodezap.CreateAPIKeyResult, error) {
// TODO(bootnode-port): port from bootnode/api/auth_keys.py:create_api_key — MUST bcrypt the plaintext before persistence.
return bootnodezap.CreateAPIKeyResult{}, ErrNotImplemented
}
func (s *service) ListAPIKeys(_ context.Context, _ string) ([]bootnodezap.APIKey, error) {
// TODO(bootnode-port): port from bootnode/api/auth_keys.py:list_api_keys
return nil, ErrNotImplemented
}
func (s *service) RevokeAPIKey(_ context.Context, _, _ string) (bool, error) {
// TODO(bootnode-port): port from bootnode/api/auth_keys.py:delete_api_key
return false, ErrNotImplemented
}
// ---- Fleets ---------------------------------------------------------------
func (s *service) CreateFleet(_ context.Context, _ bootnodezap.Fleet) (bootnodezap.Fleet, error) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:create
return bootnodezap.Fleet{}, ErrNotImplemented
}
func (s *service) ListFleets(_ context.Context, _ string) ([]bootnodezap.Fleet, error) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:list
return nil, ErrNotImplemented
}
func (s *service) ScaleFleet(_ context.Context, _ string, _ int32) (bootnodezap.Fleet, error) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:scale
return bootnodezap.Fleet{}, ErrNotImplemented
}
func (s *service) DeleteFleet(_ context.Context, _ string) (bool, error) {
// TODO(bootnode-port): port from bootnode/api/fleets/__init__.py:delete
return false, ErrNotImplemented
}
// ---- Nodes ----------------------------------------------------------------
func (s *service) DeployNode(_ context.Context, _ bootnodezap.Node) (bootnodezap.Node, error) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:create
return bootnodezap.Node{}, ErrNotImplemented
}
func (s *service) ListNodes(_ context.Context, _ string) ([]bootnodezap.Node, error) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:list
return nil, ErrNotImplemented
}
func (s *service) StartNode(_ context.Context, _ string) (bool, error) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:start
return false, ErrNotImplemented
}
func (s *service) StopNode(_ context.Context, _ string) (bool, error) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:stop
return false, ErrNotImplemented
}
func (s *service) DeleteNode(_ context.Context, _ string) (bool, error) {
// TODO(bootnode-port): port from bootnode/api/nodes/__init__.py:delete
return false, ErrNotImplemented
}
// ---- Usage ---------------------------------------------------------------
func (s *service) RecordUsage(_ context.Context, _ bootnodezap.UsageRecord) (bool, error) {
// TODO(bootnode-port): port from bootnode/core/billing/sync.py:report_usage —
// must call deps.Commerce.RecordUsage, never write payment data here.
return false, ErrNotImplemented
}
func (s *service) GetUsage(_ context.Context, _ string, _, _ int64) (bootnodezap.UsageSummary, error) {
// TODO(bootnode-port): port from bootnode/api/billing/__init__.py:usage_summary
return bootnodezap.UsageSummary{}, ErrNotImplemented
}
+71
View File
@@ -0,0 +1,71 @@
package handlers
import (
"net/http"
"github.com/go-chi/chi/v5"
)
// MountTokens registers /v1/tokens/{chain}/* — ERC-20 balances + metadata.
func MountTokens(r chi.Router, _ Deps) {
r.Route("/tokens", func(r chi.Router) {
r.Get("/{chain}/balances/{address}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/tokens/__init__.py:balances
stub(w, "tokens/{chain}/balances/{address}")
})
r.Get("/{chain}/metadata/{contract}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/tokens/__init__.py:metadata
stub(w, "tokens/{chain}/metadata/{contract}")
})
r.Post("/{chain}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/tokens/__init__.py:batch
stub(w, "tokens/{chain} (batch)")
})
r.Post("/{chain}/{network}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/tokens/__init__.py:batch_network
stub(w, "tokens/{chain}/{network} (batch)")
})
})
}
// MountNFTs registers /v1/nfts/{chain}/* — collection + ownership + metadata.
func MountNFTs(r chi.Router, _ Deps) {
r.Route("/nfts", func(r chi.Router) {
r.Get("/{chain}/owned/{address}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nfts/__init__.py:owned
stub(w, "nfts/{chain}/owned/{address}")
})
r.Get("/{chain}/collection/{contract}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nfts/__init__.py:collection
stub(w, "nfts/{chain}/collection/{contract}")
})
r.Get("/{chain}/metadata/{contract}/{token_id}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nfts/__init__.py:metadata
stub(w, "nfts/{chain}/metadata/{contract}/{token_id}")
})
r.Post("/{chain}/refresh/{contract}/{token_id}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/nfts/__init__.py:refresh_metadata —
// enqueues an MQ job (was arq, target is deps.MQ subsystem).
stub(w, "nfts/{chain}/refresh/{contract}/{token_id}")
})
_ = chi.URLParam // keep import live for symmetry with downstream handlers
})
}
// MountTransfers registers /v1/transfers/{chain}/* — historical transfers.
func MountTransfers(r chi.Router, _ Deps) {
r.Route("/transfers", func(r chi.Router) {
r.Get("/{chain}/address/{address}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/transfers/__init__.py:address
stub(w, "transfers/{chain}/address/{address}")
})
r.Get("/{chain}/tx/{tx_hash}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/transfers/__init__.py:tx
stub(w, "transfers/{chain}/tx/{tx_hash}")
})
r.Post("/{chain}/send", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/transfers/__init__.py:send
stub(w, "transfers/{chain}/send")
})
})
}
+45
View File
@@ -0,0 +1,45 @@
package handlers
import (
"encoding/json"
"errors"
"net/http"
)
// writeJSON renders v as a JSON response with status code.
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
// readJSON decodes the request body into v. Returns 400 on failure.
func readJSON(w http.ResponseWriter, r *http.Request, v any) bool {
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
writeJSON(w, http.StatusBadRequest, errBody{"invalid json: " + err.Error()})
return false
}
return true
}
type errBody struct {
Error string `json:"error"`
}
// stub returns a 501 with the canonical port-in-progress message. It
// is the one place HTTP handlers translate ErrNotImplemented from the
// service into a wire-level response.
func stub(w http.ResponseWriter, note string) {
writeJSON(w, http.StatusNotImplemented, errBody{"not implemented: " + note})
}
// serviceErr converts a typed service error into an HTTP response.
// Today the only typed error is ErrNotImplemented; the function is
// here as the seam where future error categories land.
func serviceErr(w http.ResponseWriter, err error, note string) {
if errors.Is(err, ErrNotImplemented) {
stub(w, note)
return
}
writeJSON(w, http.StatusInternalServerError, errBody{err.Error()})
}
+83
View File
@@ -0,0 +1,83 @@
package handlers
import (
"net/http"
"github.com/go-chi/chi/v5"
)
// MountWallets registers /v1/wallets/* — ERC-4337 smart account API.
func MountWallets(r chi.Router, _ Deps) {
r.Route("/wallets", func(r chi.Router) {
r.Post("/create", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/wallets/__init__.py:create —
// keccak256 CREATE2 address (NOT sha256). See LLM.md "Phase 3" notes.
stub(w, "wallets/create")
})
r.Get("/", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/wallets/__init__.py:list
stub(w, "wallets list")
})
r.Get("/{address}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/wallets/__init__.py:get
stub(w, "wallets/{address}")
})
r.Post("/{address}/estimate", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/wallets/__init__.py:estimate
stub(w, "wallets/{address}/estimate")
})
r.Get("/{address}/nonce", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/wallets/__init__.py:nonce
stub(w, "wallets/{address}/nonce")
})
_ = chi.URLParam
})
}
// MountBundler registers /v1/bundler/{chain} — ERC-4337 RPC entrypoint.
func MountBundler(r chi.Router, _ Deps) {
r.Route("/bundler", func(r chi.Router) {
r.Post("/{chain}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/bundler/__init__.py:rpc —
// implements eth_sendUserOperation / eth_estimateUserOperationGas / etc.
stub(w, "bundler/{chain}")
})
r.Post("/{chain}/{network}", func(w http.ResponseWriter, _ *http.Request) {
stub(w, "bundler/{chain}/{network}")
})
})
}
// MountGas registers /v1/gas/* — gas prices + paymaster policies.
func MountGas(r chi.Router, _ Deps) {
r.Route("/gas", func(r chi.Router) {
r.Get("/{chain}/prices", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/gas/__init__.py:prices
stub(w, "gas/{chain}/prices")
})
r.Post("/sponsor", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/gas/__init__.py:sponsor
stub(w, "gas/sponsor")
})
r.Post("/estimate", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/gas/__init__.py:estimate
stub(w, "gas/estimate")
})
r.Get("/policies", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/gas/__init__.py:list_policies
stub(w, "gas/policies GET")
})
r.Post("/policies", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/gas/__init__.py:create_policy
stub(w, "gas/policies POST")
})
r.Get("/policies/{id}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/gas/__init__.py:get_policy
stub(w, "gas/policies/{id}")
})
r.Delete("/policies/{id}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/gas/__init__.py:delete_policy
stub(w, "gas/policies/{id} DELETE")
})
})
}
+43
View File
@@ -0,0 +1,43 @@
package handlers
import (
"net/http"
"github.com/go-chi/chi/v5"
)
// MountWebhooks registers /v1/webhooks/* — customer webhook subscription
// management. Delivery itself runs in the MQ subsystem (was Python arq).
func MountWebhooks(r chi.Router, _ Deps) {
r.Route("/webhooks", func(r chi.Router) {
r.Post("/", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/webhooks/__init__.py:create
stub(w, "webhooks POST")
})
r.Get("/", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/webhooks/__init__.py:list
stub(w, "webhooks GET")
})
r.Get("/{webhook_id}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/webhooks/__init__.py:get
stub(w, "webhooks/{id}")
})
r.Patch("/{webhook_id}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/webhooks/__init__.py:update
stub(w, "webhooks/{id} PATCH")
})
r.Delete("/{webhook_id}", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/webhooks/__init__.py:delete
stub(w, "webhooks/{id} DELETE")
})
r.Get("/{webhook_id}/deliveries", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/webhooks/__init__.py:deliveries
stub(w, "webhooks/{id}/deliveries")
})
r.Post("/{webhook_id}/test", func(w http.ResponseWriter, _ *http.Request) {
// TODO(bootnode-port): port from bootnode/api/webhooks/__init__.py:test
stub(w, "webhooks/{id}/test")
})
_ = chi.URLParam
})
}
+111
View File
@@ -0,0 +1,111 @@
// Package middleware holds the chi middleware Bootnode mounts.
//
// All identity decisions go through deps.IAM — Bootnode never decodes
// JWTs itself. Per the Hanzo header convention (LLM.md), incoming
// X-Org-Id / X-User-Id / X-User-Email are TRUSTED only when minted by
// the gateway from a validated JWT. When Bootnode runs standalone it
// derives them from the Authorization bearer via deps.IAM.
package middleware
import (
"context"
"net/http"
"strings"
)
// ctxKey is unexported so external packages can't accidentally clobber
// our context values.
type ctxKey int
const (
ctxOrgID ctxKey = iota
ctxUserID
ctxEmail
)
// IAMVerifier matches api.Deps.IAM but lets this package avoid an
// import cycle.
type IAMVerifier interface {
VerifyJWT(ctx context.Context, bearer string) (orgID, userID, email string, err error)
}
// AuthOptional wires identity into request context when the
// Authorization header parses; missing/invalid tokens fall through
// (handlers that require auth call MustAuth on the context).
func AuthOptional(iam IAMVerifier) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 1) Gateway-trusted headers (per HTTP Header Convention
// 2026-03-27): only honored when the upstream gateway
// has already validated the JWT. We trust them iff IAM
// is nil (split-deploy behind gateway) or the bearer
// matches.
if orgID := r.Header.Get("X-Org-Id"); orgID != "" && iam == nil {
ctx = context.WithValue(ctx, ctxOrgID, orgID)
ctx = context.WithValue(ctx, ctxUserID, r.Header.Get("X-User-Id"))
ctx = context.WithValue(ctx, ctxEmail, r.Header.Get("X-User-Email"))
next.ServeHTTP(w, r.WithContext(ctx))
return
}
// 2) Authorization: Bearer ... → IAM verify.
if iam != nil {
if bearer := bearerToken(r); bearer != "" {
orgID, userID, email, err := iam.VerifyJWT(ctx, bearer)
if err == nil {
ctx = context.WithValue(ctx, ctxOrgID, orgID)
ctx = context.WithValue(ctx, ctxUserID, userID)
ctx = context.WithValue(ctx, ctxEmail, email)
}
}
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// OrgID returns the org slug attached by AuthOptional, or "".
func OrgID(ctx context.Context) string {
if v, ok := ctx.Value(ctxOrgID).(string); ok {
return v
}
return ""
}
// UserID returns the user ID attached by AuthOptional, or "".
func UserID(ctx context.Context) string {
if v, ok := ctx.Value(ctxUserID).(string); ok {
return v
}
return ""
}
// Email returns the user email attached by AuthOptional, or "".
func Email(ctx context.Context) string {
if v, ok := ctx.Value(ctxEmail).(string); ok {
return v
}
return ""
}
// RequireAuth wraps a handler so it 401s when no org is attached.
func RequireAuth(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if OrgID(r.Context()) == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
h(w, r)
}
}
func bearerToken(r *http.Request) string {
h := r.Header.Get("Authorization")
const p = "Bearer "
if !strings.HasPrefix(h, p) {
return ""
}
return strings.TrimSpace(h[len(p):])
}
+68
View File
@@ -0,0 +1,68 @@
package middleware
import (
"net/http"
"sync"
"time"
)
// RateLimit is a per-org sliding-window limiter. It exists as a stub
// so handlers can compose it today; the production version reads
// per-org rate limits from the api_keys table via deps.Base.
//
// TODO(bootnode-port): replace with the FastAPI rate-limit equivalent
// (Redis token bucket via deps.MQ or in-process LRU). See
// api/bootnode/api/deps.py:rate_limit.
type RateLimit struct {
mu sync.Mutex
buckets map[string]*bucket
limit int
window time.Duration
}
type bucket struct {
tokens int
resetAt time.Time
}
// NewRateLimit creates a per-key limiter (limit requests / window).
func NewRateLimit(limit int, window time.Duration) *RateLimit {
return &RateLimit{
buckets: make(map[string]*bucket),
limit: limit,
window: window,
}
}
// Wrap returns a chi middleware that keys off the org slug attached
// to the request context.
func (rl *RateLimit) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := OrgID(r.Context())
if key == "" {
key = r.RemoteAddr
}
if !rl.allow(key) {
http.Error(w, "rate limited", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
func (rl *RateLimit) allow(key string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
b, ok := rl.buckets[key]
if !ok || now.After(b.resetAt) {
rl.buckets[key] = &bucket{tokens: rl.limit - 1, resetAt: now.Add(rl.window)}
return true
}
if b.tokens <= 0 {
return false
}
b.tokens--
return true
}
+39
View File
@@ -0,0 +1,39 @@
package middleware
import (
"context"
"net/http"
"time"
)
// O11yRecorder matches api.Deps.O11y but lets this package avoid an
// import cycle.
type O11yRecorder interface {
RecordRequest(ctx context.Context, route string, status int, durationMs int64)
}
// Telemetry records every request through deps.O11y. Safe with a nil
// recorder — the middleware becomes a no-op pass-through.
func Telemetry(rec O11yRecorder) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
if rec == nil {
return next
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(sw, r)
rec.RecordRequest(r.Context(), r.URL.Path, sw.status, time.Since(start).Milliseconds())
})
}
}
type statusWriter struct {
http.ResponseWriter
status int
}
func (s *statusWriter) WriteHeader(code int) {
s.status = code
s.ResponseWriter.WriteHeader(code)
}
+148
View File
@@ -0,0 +1,148 @@
package api
import (
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/hanzoai/bootnode/api/handlers"
"github.com/hanzoai/bootnode/api/middleware"
bootnodezap "github.com/hanzoai/bootnode/zap/gen"
)
// Mount registers every Bootnode HTTP route + ZAP service onto r,
// using deps for cross-subsystem calls. This is the HIP-0106
// subsystem contract: superbase calls Mount on the unified binary's
// root mux, cmd/bootnode calls it on a fresh chi.Mux.
//
// Routes are grouped by FastAPI parity:
//
// GET /health liveness
// GET /ready readiness (deps reachable)
//
// POST /v1/auth/register local-dev auth (TODO: IAM-only in prod)
// POST /v1/auth/login
// GET /v1/auth/me
// POST /v1/auth/oauth/callback IAM OAuth code exchange
//
// GET /v1/chains list supported chains
// GET /v1/chains/{chain} chain details
//
// POST /v1/rpc/{chain} JSON-RPC proxy
// POST /v1/rpc/{chain}/{network}
//
// GET /v1/tokens/{chain}/balances/{address}
// GET /v1/tokens/{chain}/metadata/{contract}
// POST /v1/tokens/{chain} (batched)
// POST /v1/tokens/{chain}/{network}
//
// GET /v1/nfts/{chain}/owned/{address}
// GET /v1/nfts/{chain}/collection/{contract}
// GET /v1/nfts/{chain}/metadata/{contract}/{tokenId}
// POST /v1/nfts/{chain}/refresh/{contract}/{tokenId}
//
// GET /v1/transfers/{chain}/address/{address}
// GET /v1/transfers/{chain}/tx/{txHash}
// POST /v1/transfers/{chain}/send
//
// POST /v1/wallets/create
// GET /v1/wallets
// GET /v1/wallets/{address}
// POST /v1/wallets/{address}/estimate
// GET /v1/wallets/{address}/nonce
//
// POST /v1/bundler/{chain} ERC-4337 RPC
// POST /v1/bundler/{chain}/{network}
//
// GET /v1/gas/{chain}/prices
// POST /v1/gas/sponsor
// POST /v1/gas/estimate
// GET /v1/gas/policies
// POST /v1/gas/policies
// GET /v1/gas/policies/{id}
// DELETE /v1/gas/policies/{id}
//
// POST /v1/webhooks
// GET /v1/webhooks
// GET /v1/webhooks/{id}
// PATCH /v1/webhooks/{id}
// DELETE /v1/webhooks/{id}
// GET /v1/webhooks/{id}/deliveries
// POST /v1/webhooks/{id}/test
//
// POST /v1/auth-keys/projects
// GET /v1/auth-keys/projects/{id}
// POST /v1/auth-keys/keys
// GET /v1/auth-keys/keys
// DELETE /v1/auth-keys/keys/{id}
//
// (fleets, nodes, infra, team, networks, o11y, chat, zap) — same
// parity, see handlers/*.go for the per-package router.
func Mount(r chi.Router, deps Deps) error {
if deps.Logger == nil {
return errors.New("bootnode: deps.Logger is required")
}
// In-process ZAP service. zapc's generated dispatcher would
// register this with the local dispatcher when superbase wires it;
// the hand-rolled stub just holds the Service implementation.
impl := handlers.NewService(handlers.ServiceDeps{
Logger: deps.Logger,
IAM: deps.IAM,
KMS: deps.KMS,
Base: deps.Base,
O11y: deps.O11y,
Commerce: deps.Commerce,
})
_ = bootnodezap.NewServer(impl) // hand to superbase ZAP dispatcher
hdeps := handlers.Deps{
Logger: deps.Logger,
Service: impl,
IAM: deps.IAM,
KMS: deps.KMS,
Base: deps.Base,
O11y: deps.O11y,
Commerce: deps.Commerce,
}
// Health endpoints are mounted at the root — superbase aggregates
// them but Bootnode-as-standalone needs them addressable too.
r.Get("/health", handlers.Health(hdeps))
r.Get("/ready", handlers.Ready(hdeps))
r.Route("/v1", func(r chi.Router) {
r.Use(middleware.Telemetry(deps.O11y))
r.Use(middleware.AuthOptional(deps.IAM))
handlers.MountAuth(r, hdeps)
handlers.MountAuthKeys(r, hdeps)
handlers.MountChains(r, hdeps)
handlers.MountRPC(r, hdeps)
handlers.MountTokens(r, hdeps)
handlers.MountNFTs(r, hdeps)
handlers.MountTransfers(r, hdeps)
handlers.MountWallets(r, hdeps)
handlers.MountBundler(r, hdeps)
handlers.MountGas(r, hdeps)
handlers.MountWebhooks(r, hdeps)
handlers.MountFleets(r, hdeps)
handlers.MountNodes(r, hdeps)
handlers.MountInfra(r, hdeps)
handlers.MountTeam(r, hdeps)
handlers.MountNetworks(r, hdeps)
handlers.MountObservability(r, hdeps)
handlers.MountChat(r, hdeps)
handlers.MountBilling(r, hdeps)
handlers.MountZAP(r, hdeps)
})
// 404 for unknown paths inside our prefix range, leaving the rest
// to superbase's other subsystems.
r.NotFound(func(w http.ResponseWriter, req *http.Request) {
http.Error(w, "not found", http.StatusNotFound)
})
return nil
}
+73
View File
@@ -0,0 +1,73 @@
// Package store is the per-tenant persistence layer. Every method
// takes an orgID and resolves the per-org SQLite handle via
// deps.Base.Open(ctx, orgID, "bootnode") per HIP-0302.
//
// TODO(bootnode-port): when deps.Base lands as a typed client
// (`basezap.Client`), the `any` here becomes the typed handle and
// these methods grow real CRUD bodies.
package store
import (
"context"
"errors"
bootnodezap "github.com/hanzoai/bootnode/zap/gen"
)
// BaseClient is the per-tenant SQLite opener (HIP-0302).
type BaseClient interface {
Open(ctx context.Context, orgID, namespace string) (any, error)
}
// APIKeyStore persists API keys + projects. Production stores bcrypt
// hashes, never plaintext (Hanzo CLAUDE.md hard rule).
type APIKeyStore struct {
base BaseClient
}
// NewAPIKeyStore builds the per-tenant API-key store.
func NewAPIKeyStore(b BaseClient) *APIKeyStore { return &APIKeyStore{base: b} }
// ErrNotFound is returned when a row does not exist.
var ErrNotFound = errors.New("store: not found")
// Create writes a new API key row, hashing the plaintext with bcrypt
// before persistence.
//
// TODO(bootnode-port): port from bootnode/api/auth_keys.py:create_api_key —
// use golang.org/x/crypto/bcrypt.GenerateFromPassword with cost 12.
func (s *APIKeyStore) Create(ctx context.Context, k bootnodezap.APIKey, plaintext string) error {
_ = ctx
_ = k
_ = plaintext
return errors.New("store.APIKeyStore.Create: not implemented")
}
// List returns every API key for an org.
//
// TODO(bootnode-port): port from bootnode/api/auth_keys.py:list_api_keys
func (s *APIKeyStore) List(ctx context.Context, orgID string) ([]bootnodezap.APIKey, error) {
_ = ctx
_ = orgID
return nil, errors.New("store.APIKeyStore.List: not implemented")
}
// Revoke marks an API key as deleted (soft-delete to preserve audit).
//
// TODO(bootnode-port): port from bootnode/api/auth_keys.py:delete_api_key
func (s *APIKeyStore) Revoke(ctx context.Context, orgID, id string) error {
_ = ctx
_ = orgID
_ = id
return errors.New("store.APIKeyStore.Revoke: not implemented")
}
// VerifyPlaintext bcrypt-compares the supplied plaintext against the
// stored hash. Returns ErrNotFound if no row matches.
//
// TODO(bootnode-port): port from bootnode/api/auth_keys.py:verify_key
func (s *APIKeyStore) VerifyPlaintext(ctx context.Context, plaintext string) (bootnodezap.APIKey, error) {
_ = ctx
_ = plaintext
return bootnodezap.APIKey{}, ErrNotFound
}
+44
View File
@@ -0,0 +1,44 @@
package store
import (
"context"
"errors"
bootnodezap "github.com/hanzoai/bootnode/zap/gen"
)
// ProjectStore persists per-org Project rows.
type ProjectStore struct {
base BaseClient
}
// NewProjectStore builds the per-tenant project store.
func NewProjectStore(b BaseClient) *ProjectStore { return &ProjectStore{base: b} }
// Create inserts a Project.
//
// TODO(bootnode-port): port from bootnode/api/auth_keys.py:create_project
func (s *ProjectStore) Create(ctx context.Context, p bootnodezap.Project) error {
_ = ctx
_ = p
return errors.New("store.ProjectStore.Create: not implemented")
}
// Get reads one project by id.
//
// TODO(bootnode-port): port from bootnode/api/auth_keys.py:get_project
func (s *ProjectStore) Get(ctx context.Context, orgID, id string) (bootnodezap.Project, error) {
_ = ctx
_ = orgID
_ = id
return bootnodezap.Project{}, ErrNotFound
}
// List returns every project for an org.
//
// TODO(bootnode-port): port from bootnode/api/auth_keys.py:list_projects
func (s *ProjectStore) List(ctx context.Context, orgID string) ([]bootnodezap.Project, error) {
_ = ctx
_ = orgID
return nil, errors.New("store.ProjectStore.List: not implemented")
}
+39
View File
@@ -0,0 +1,39 @@
package store
import (
"context"
"errors"
bootnodezap "github.com/hanzoai/bootnode/zap/gen"
)
// UsageStore persists per-org/per-project compute-unit accounting.
// Hot-path counters live in deps.MQ (Redis-equivalent), durable
// accounting lives here in per-tenant SQLite, and roll-ups sync to
// Commerce via deps.Commerce.RecordUsage on a timer.
type UsageStore struct {
base BaseClient
}
// NewUsageStore builds the per-tenant usage store.
func NewUsageStore(b BaseClient) *UsageStore { return &UsageStore{base: b} }
// Append writes one billable call.
//
// TODO(bootnode-port): port from bootnode/core/billing/tracker.py
func (s *UsageStore) Append(ctx context.Context, r bootnodezap.UsageRecord) error {
_ = ctx
_ = r
return errors.New("store.UsageStore.Append: not implemented")
}
// Summary aggregates usage over (start, end).
//
// TODO(bootnode-port): port from bootnode/api/billing/__init__.py:usage_summary
func (s *UsageStore) Summary(ctx context.Context, orgID string, start, end int64) (bootnodezap.UsageSummary, error) {
_ = ctx
_ = orgID
_ = start
_ = end
return bootnodezap.UsageSummary{}, errors.New("store.UsageStore.Summary: not implemented")
}
+46
View File
@@ -0,0 +1,46 @@
// Package web3 wraps chain configuration + multi-chain RPC plumbing.
//
// The HTTP layer asks this package for canonical chain data; the RPC
// proxy fans out through a pool defined in client.go. luxfi/* packages
// are preferred for any chain library imports (see ~/.claude/CLAUDE.md).
package web3
// ChainInfo is the canonical per-chain record exposed at /v1/chains.
// Mirrors bootnode/core/chains/registry.py.
type ChainInfo struct {
ID string `json:"id"`
Name string `json:"name"`
ChainID int64 `json:"chainId"`
IsEVM bool `json:"isEvm"`
Currency string `json:"currency"`
Explorer string `json:"explorer"`
}
// allChains is the initial static set; production reads from
// deps.Base ("registry" table) so customers can add custom chains.
//
// TODO(bootnode-port): hydrate from bootnode/core/chains/registry.py
// (CHAIN_REGISTRY) at startup; allow per-org overrides.
var allChains = []ChainInfo{
{ID: "ethereum", Name: "Ethereum", ChainID: 1, IsEVM: true, Currency: "ETH", Explorer: "https://etherscan.io"},
{ID: "polygon", Name: "Polygon", ChainID: 137, IsEVM: true, Currency: "MATIC", Explorer: "https://polygonscan.com"},
{ID: "arbitrum", Name: "Arbitrum One", ChainID: 42161, IsEVM: true, Currency: "ETH", Explorer: "https://arbiscan.io"},
{ID: "optimism", Name: "Optimism", ChainID: 10, IsEVM: true, Currency: "ETH", Explorer: "https://optimistic.etherscan.io"},
{ID: "base", Name: "Base", ChainID: 8453, IsEVM: true, Currency: "ETH", Explorer: "https://basescan.org"},
{ID: "bsc", Name: "BNB Chain", ChainID: 56, IsEVM: true, Currency: "BNB", Explorer: "https://bscscan.com"},
{ID: "avalanche", Name: "Avalanche C-Chain", ChainID: 43114, IsEVM: true, Currency: "AVAX", Explorer: "https://snowtrace.io"},
{ID: "lux", Name: "Lux Network", ChainID: 96369, IsEVM: true, Currency: "LUX", Explorer: "https://explore.lux.network"},
}
// AllChains returns every chain Bootnode currently knows about.
func AllChains() []ChainInfo { return append([]ChainInfo(nil), allChains...) }
// ChainByName looks up one chain by its kebab-cased ID.
func ChainByName(id string) (ChainInfo, bool) {
for _, c := range allChains {
if c.ID == id {
return c, true
}
}
return ChainInfo{}, false
}
+33
View File
@@ -0,0 +1,33 @@
package web3
import (
"context"
"errors"
)
// Pool is the multi-chain RPC client surface. Each chain has one or
// more upstream URLs; Pool.Call selects an upstream, applies
// per-method caching via deps.Base, and meters compute units via
// deps.Commerce.
//
// TODO(bootnode-port): port from bootnode/core/chains/rpc.py — the
// production version maintains per-upstream health, sticky-session
// routing for `eth_getLogs`, and exponential backoff. Prefer
// `luxfi/geth` or `luxfi/ethclient` over upstream go-ethereum.
type Pool interface {
Call(ctx context.Context, chain string, method string, params any) (any, error)
}
// ErrNoUpstream is returned when no upstream URL is configured for a
// chain.
var ErrNoUpstream = errors.New("web3: no upstream configured for chain")
// NewPool returns a placeholder pool that always errors. Wired by
// superbase once the real implementation lands.
func NewPool(_ map[string][]string) Pool { return &emptyPool{} }
type emptyPool struct{}
func (e *emptyPool) Call(_ context.Context, _ string, _ string, _ any) (any, error) {
return nil, ErrNoUpstream
}
+63
View File
@@ -0,0 +1,63 @@
// bootnode is the standalone process entrypoint for the Bootnode
// subsystem. It calls api.Mount on a fresh chi.Mux and serves HTTP
// on :8100. When folded into superbase per HIP-0106, the same
// api.Mount is invoked on superbase's root mux — this binary becomes
// a development convenience, not a production deploy target.
//
// Standalone usage:
//
// go run ./cmd/bootnode
// BOOTNODE_ADDR=:9001 go run ./cmd/bootnode
package main
import (
"log/slog"
"net/http"
"os"
"github.com/go-chi/chi/v5"
chimw "github.com/go-chi/chi/v5/middleware"
"github.com/hanzoai/bootnode/api"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stderr, nil))
addr := os.Getenv("BOOTNODE_ADDR")
if addr == "" {
addr = ":8100"
}
r := chi.NewMux()
r.Use(chimw.RequestID)
r.Use(chimw.RealIP)
r.Use(chimw.Recoverer)
deps := api.Deps{
Brand: brandFromEnv(),
Logger: logger,
// IAM/KMS/Base/O11y/Commerce are nil in the standalone shim;
// handlers fail closed and emit 501s. In production superbase
// wires the real in-process ZAP clients.
}
if err := api.Mount(r, deps); err != nil {
logger.Error("mount failed", "err", err)
os.Exit(1)
}
logger.Info("bootnode listening", "addr", addr, "brand", deps.Brand)
srv := &http.Server{Addr: addr, Handler: r}
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error("serve failed", "err", err)
os.Exit(1)
}
}
func brandFromEnv() string {
if v := os.Getenv("BOOTNODE_BRAND"); v != "" {
return v
}
return "hanzo"
}
+198
View File
@@ -0,0 +1,198 @@
# Bootnode: Python → Go port map
Source of truth: `~/work/hanzo/bootnode/api/bootnode/api/*` (FastAPI).
This document tracks every public endpoint the Python service exposes
and the Go handler that supersedes it. "Status" is one of:
- **stub** — Go handler registered, returns 501 with TODO
- **port** — handler body ported, tests pending
- **prod** — ported, tested, parity-verified against Python
The Go-side handler signatures all live under
`api/handlers/<group>.go`. The typed service shape (`bootnodezap.Service`)
lives in `zap/gen/bootnode.go`.
## /v1/auth (Authentication)
| Method | Path | Python | Go | Status |
|--------|------------------------------|-------------------------------------|---------------------------------|--------|
| POST | /v1/auth/register | api/auth/__init__.py:register | handlers/auth.go | stub |
| POST | /v1/auth/login | api/auth/__init__.py:login | handlers/auth.go | stub |
| GET | /v1/auth/me | api/auth/__init__.py:me | handlers/auth.go | stub |
| POST | /v1/auth/oauth/callback | api/auth/__init__.py:oauth_callback | handlers/auth.go | stub |
bcrypt cost 12 required on `register`. In production every authenticated
endpoint flows through `deps.IAM.VerifyJWT`; the local register/login
paths are dev-only.
## /v1/auth-keys (API keys + projects)
| Method | Path | Python | Go | Status |
|--------|---------------------------------|------------------------------|----------------------------------------------------|--------|
| POST | /v1/auth-keys/projects | api/auth_keys.py:create_project | handlers/authkeys.go | stub |
| GET | /v1/auth-keys/projects/{id} | api/auth_keys.py:get_project | handlers/authkeys.go | stub |
| POST | /v1/auth-keys/keys | api/auth_keys.py:create_api_key | handlers/authkeys.go (calls Service.CreateAPIKey) | stub |
| GET | /v1/auth-keys/keys | api/auth_keys.py:list_api_keys | handlers/authkeys.go | stub |
| DELETE | /v1/auth-keys/keys/{id} | api/auth_keys.py:delete_api_key | handlers/authkeys.go | stub |
Plaintext returned ONCE on creation. Storage is bcrypt hash via
`store.APIKeyStore`.
## /v1/chains
| Method | Path | Python | Go | Status |
|--------|---------------------|------------------------------|---------------------|--------|
| GET | /v1/chains | api/chains.py:list_chains | handlers/chains.go | port |
| GET | /v1/chains/{chain} | api/chains.py:get_chain | handlers/chains.go | port |
Backed by `web3.AllChains()` / `web3.ChainByName()`. Production hydrates
from `core/chains/registry.py` table on startup.
## /v1/rpc (multi-chain JSON-RPC proxy)
| Method | Path | Python | Go | Status |
|--------|---------------------------------------|-------------------------|-------------------|--------|
| POST | /v1/rpc/{chain} | api/rpc/__init__.py | handlers/rpc.go | stub |
| POST | /v1/rpc/{chain}/{network} | api/rpc/__init__.py | handlers/rpc.go | stub |
Forwards through `web3.Pool.Call`; meters compute units via
`deps.Commerce.RecordUsage`. Cache via `deps.Base`.
## /v1/tokens
| Method | Path | Python | Go | Status |
|--------|--------------------------------------------|---------------------------------|---------------------|--------|
| GET | /v1/tokens/{chain}/balances/{address} | api/tokens/__init__.py:balances | handlers/tokens.go | stub |
| GET | /v1/tokens/{chain}/metadata/{contract} | api/tokens/__init__.py:metadata | handlers/tokens.go | stub |
| POST | /v1/tokens/{chain} | api/tokens/__init__.py:batch | handlers/tokens.go | stub |
| POST | /v1/tokens/{chain}/{network} | api/tokens/__init__.py:batch | handlers/tokens.go | stub |
## /v1/nfts
| Method | Path | Python | Go | Status |
|--------|-----------------------------------------------------|-------------------------------------|---------------------|--------|
| GET | /v1/nfts/{chain}/owned/{address} | api/nfts/__init__.py:owned | handlers/tokens.go | stub |
| GET | /v1/nfts/{chain}/collection/{contract} | api/nfts/__init__.py:collection | handlers/tokens.go | stub |
| GET | /v1/nfts/{chain}/metadata/{contract}/{token_id} | api/nfts/__init__.py:metadata | handlers/tokens.go | stub |
| POST | /v1/nfts/{chain}/refresh/{contract}/{token_id} | api/nfts/__init__.py:refresh | handlers/tokens.go | stub |
Refresh enqueues an MQ job (Python arq → `deps.MQ`).
## /v1/transfers
| Method | Path | Python | Go | Status |
|--------|---------------------------------------------|-----------------------------------|---------------------|--------|
| GET | /v1/transfers/{chain}/address/{address} | api/transfers/__init__.py:address | handlers/tokens.go | stub |
| GET | /v1/transfers/{chain}/tx/{tx_hash} | api/transfers/__init__.py:tx | handlers/tokens.go | stub |
| POST | /v1/transfers/{chain}/send | api/transfers/__init__.py:send | handlers/tokens.go | stub |
## /v1/wallets (ERC-4337 smart accounts)
| Method | Path | Python | Go | Status |
|--------|---------------------------------------|-------------------------------------|---------------------|--------|
| POST | /v1/wallets/create | api/wallets/__init__.py:create | handlers/wallet.go | stub |
| GET | /v1/wallets | api/wallets/__init__.py:list | handlers/wallet.go | stub |
| GET | /v1/wallets/{address} | api/wallets/__init__.py:get | handlers/wallet.go | stub |
| POST | /v1/wallets/{address}/estimate | api/wallets/__init__.py:estimate | handlers/wallet.go | stub |
| GET | /v1/wallets/{address}/nonce | api/wallets/__init__.py:nonce | handlers/wallet.go | stub |
CREATE2 address must use **keccak256**, not sha256 (Python had the bug
in Phase 3 — already fixed there, watch for it on port).
## /v1/bundler
| Method | Path | Python | Go | Status |
|--------|---------------------------------------|---------------------------------|---------------------|--------|
| POST | /v1/bundler/{chain} | api/bundler/__init__.py:rpc | handlers/wallet.go | stub |
| POST | /v1/bundler/{chain}/{network} | api/bundler/__init__.py:rpc | handlers/wallet.go | stub |
`userOpHash` must use eth-abi-encoded keccak256.
## /v1/gas
| Method | Path | Python | Go | Status |
|--------|-------------------------------|-------------------------------------|---------------------|--------|
| GET | /v1/gas/{chain}/prices | api/gas/__init__.py:prices | handlers/wallet.go | stub |
| POST | /v1/gas/sponsor | api/gas/__init__.py:sponsor | handlers/wallet.go | stub |
| POST | /v1/gas/estimate | api/gas/__init__.py:estimate | handlers/wallet.go | stub |
| GET | /v1/gas/policies | api/gas/__init__.py:list_policies | handlers/wallet.go | stub |
| POST | /v1/gas/policies | api/gas/__init__.py:create_policy | handlers/wallet.go | stub |
| GET | /v1/gas/policies/{id} | api/gas/__init__.py:get_policy | handlers/wallet.go | stub |
| DELETE | /v1/gas/policies/{id} | api/gas/__init__.py:delete_policy | handlers/wallet.go | stub |
## /v1/webhooks
| Method | Path | Python | Go | Status |
|--------|---------------------------------------|-------------------------------------|----------------------|--------|
| POST | /v1/webhooks | api/webhooks/__init__.py:create | handlers/webhook.go | stub |
| GET | /v1/webhooks | api/webhooks/__init__.py:list | handlers/webhook.go | stub |
| GET | /v1/webhooks/{id} | api/webhooks/__init__.py:get | handlers/webhook.go | stub |
| PATCH | /v1/webhooks/{id} | api/webhooks/__init__.py:update | handlers/webhook.go | stub |
| DELETE | /v1/webhooks/{id} | api/webhooks/__init__.py:delete | handlers/webhook.go | stub |
| GET | /v1/webhooks/{id}/deliveries | api/webhooks/__init__.py:deliveries | handlers/webhook.go | stub |
| POST | /v1/webhooks/{id}/test | api/webhooks/__init__.py:test | handlers/webhook.go | stub |
Delivery worker (was Python arq) → `deps.MQ` subsystem.
## /v1/fleets
15 endpoints — see `api/fleets/__init__.py` and `handlers/infra.go`.
All stubs.
## /v1/nodes
11 endpoints — see `api/nodes/__init__.py` and `handlers/infra.go`.
All stubs. Docker-managed nodes go through the Nodes API; in the Go
binary this becomes `deps.Compute` once that subsystem ships.
## /v1/infra
28 endpoints — see `api/infra/__init__.py` and `handlers/infra.go`.
All stubs.
## /v1/team
4 endpoints — see `api/team/__init__.py` and `handlers/misc.go`. All
stubs.
## /v1/networks
5 endpoints — see `api/networks/__init__.py` and `handlers/misc.go`.
All stubs.
## /v1/o11y
8 endpoints — see `api/observability/__init__.py` and
`handlers/misc.go`. All stubs.
## /v1/chat
2 endpoints — passthrough to the LLM gateway. Stubs.
## /v1/billing
24 endpoints — see `api/billing/__init__.py` and `handlers/misc.go`.
Money never lives here; everything is delegated to `deps.Commerce`.
Stubs.
## /v1/zap
7 endpoints (discovery only — wire-level ZAP RPC lives in
`superbase`). Stubs. **No `/schema.capnp` is mounted** — brand policy.
## Internal (not HTTP-exposed)
| Surface | Python | Go |
|---------------------------|-----------------------------------|-----------------------------------------------|
| ZAP server | bootnode/zap/server.py | zap/gen/*.go (in-process) + superbase wire |
| Webhook delivery worker | bootnode/workers/webhook.py | future `deps.MQ` worker |
| Usage sync worker | bootnode/core/billing/sync.py | future cron in `service.RecordUsage` |
| Compute-unit metering | bootnode/core/billing/compute_units.py | inline in handlers/rpc.go |
## Endpoint count
- Python total: 150 endpoints across 17 modules (FastAPI)
- Go scaffold: 132 chi routes mounted (covers the public surface;
internal `nodes/presets`, ZAP discovery, and a handful of admin
endpoints are intentionally omitted until the subsystem ships)
+5
View File
@@ -0,0 +1,5 @@
module github.com/hanzoai/bootnode
go 1.26.2
require github.com/go-chi/chi/v5 v5.1.0
+2
View File
@@ -0,0 +1,2 @@
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+341
View File
@@ -0,0 +1,341 @@
# Hanzo Bootnode — ZAP Schema
#
# Public typed surface for the Bootnode subsystem (multi-chain Web3 API,
# token/NFT/transfers/wallets/webhooks/AA/bundler/gas/fleets/infra).
# Mounted into superbase per HIP-0106; addressable in-process via the
# generated Go client and over-wire via ZAP RPC.
#
# Code generation:
# zapc generate bootnode.zap --lang go --out ./zap/gen/
#
# Brand policy: ZAP only. Never "Cap'n Proto" / .capnp anywhere on the
# public surface (see ~/.claude/CLAUDE.md + HIP-0106).
@version 1.0.0
# ── Enums ────────────────────────────────────────────────────────────────
enum Chain
ethereum
polygon
arbitrum
optimism
base
bsc
avalanche
gnosis
celo
linea
scroll
zksync
blast
mantle
metis
zora
opbnb
bitcoin
solana
aptos
flow
berachain
sei
sonic
tron
ton
lux
enum Network
mainnet
sepolia
holesky
fuji
devnet
testnet
localnet
enum WebhookEvent
addressActivity
minedTransaction
nftActivity
tokenTransfer
internalTransfer
graphql
enum NodeProvider
docker
kubernetes
process
helm
# ── RPC ──────────────────────────────────────────────────────────────────
struct RPCRequest
chain Chain
network Network
method Text
params Text # JSON-encoded params array
id Int64
struct RPCResponse
jsonrpc Text
id Int64
result Text # JSON-encoded result
error Text # JSON-encoded error or empty
blockNumber UInt64
cached Bool
# ── Tokens ───────────────────────────────────────────────────────────────
struct TokenBalance
contract Text
symbol Text
name Text
decimals Int32
balance Text # bigint as string
balanceUsd Float64
struct TokenMetadata
contract Text
chain Chain
symbol Text
name Text
decimals Int32
logo Text
totalSupply Text
# ── NFTs ─────────────────────────────────────────────────────────────────
struct NFTMetadata
contract Text
tokenId Text
name Text
description Text
image Text
attributes Text # JSON-encoded
tokenUri Text
chain Chain
struct NFTCollection
contract Text
chain Chain
name Text
symbol Text
totalSupply Text
ownerCount Int64
# ── Transfers ───────────────────────────────────────────────────────────
struct Transfer
chain Chain
txHash Text
blockNumber UInt64
fromAddr Text
toAddr Text
value Text
asset Text
category Text # external | internal | erc20 | erc721 | erc1155
timestamp Int64
# ── Smart Wallets (ERC-4337) ────────────────────────────────────────────
struct SmartWallet
address Text
chain Chain
owner Text
factory Text
salt Text
nonce UInt64
deployed Bool
createdAt Int64
struct UserOp
sender Text
nonce Text
initCode Text
callData Text
callGasLimit Text
verificationGasLimit Text
preVerificationGas Text
maxFeePerGas Text
maxPriorityFeePerGas Text
paymasterAndData Text
signature Text
struct UserOpReceipt
userOpHash Text
sender Text
nonce Text
success Bool
txHash Text
blockNumber UInt64
gasUsed Text
# ── Gas Manager ─────────────────────────────────────────────────────────
struct GasPrices
chain Chain
slow Text
standard Text
fast Text
baseFee Text
maxPriorityFeePerGas Text
updatedAt Int64
struct GasPolicy
id Text
orgId Text
name Text
chain Chain
maxGwei Text
dailyLimitUsd Float64
enabled Bool
# ── Webhooks ────────────────────────────────────────────────────────────
struct Webhook
id Text
orgId Text
url Text
chain Chain
event WebhookEvent
filters Text # JSON-encoded
secret Text
active Bool
createdAt Int64
struct WebhookDelivery
id Text
webhookId Text
status Text
responseCode Int32
attempts Int32
payload Text
createdAt Int64
# ── API Keys / Projects ─────────────────────────────────────────────────
struct Project
id Text
orgId Text
name Text
createdAt Int64
struct APIKey
id Text
projectId Text
orgId Text
keyHash Text
name Text
scopes List(Text)
rateLimit Int32
createdAt Int64
struct CreateAPIKeyResult
apiKey APIKey
plaintext Text # returned ONCE on creation, never again
# ── Fleets / Nodes / Infra ──────────────────────────────────────────────
struct Fleet
id Text
orgId Text
chain Chain
network Network
clusterId Text
replicas Int32
status Text
createdAt Int64
struct Node
id Text
orgId Text
name Text
chain Chain
network Network
provider NodeProvider
rpcPort Int32
wsPort Int32
status Text
containerId Text
createdAt Int64
struct Cluster
id Text
orgId Text
name Text
region Text
nodeCount Int32
status Text
# ── Usage / Billing surface (Bootnode reports CU to Commerce) ───────────
struct UsageRecord
orgId Text
projectId Text
chain Chain
method Text
computeUnits Int32
timestamp Int64
struct UsageSummary
orgId Text
total Int64
periodStart Int64
periodEnd Int64
# ── Service Interface ───────────────────────────────────────────────────
interface BootnodeService
# RPC proxy
rpc (req RPCRequest) -> (resp RPCResponse)
# Tokens
tokenBalances (chain Chain, address Text) -> (balances List(TokenBalance))
tokenMetadata (chain Chain, contract Text) -> (metadata TokenMetadata)
# NFTs
nftMetadata (chain Chain, contract Text, tokenId Text) -> (metadata NFTMetadata)
nftCollection (chain Chain, contract Text) -> (collection NFTCollection)
nftsOwned (chain Chain, address Text) -> (items List(NFTMetadata))
# Transfers
listTransfers (chain Chain, address Text) -> (transfers List(Transfer))
# Smart wallets
createSmartWallet (chain Chain, owner Text) -> (wallet SmartWallet)
getSmartWallet (address Text) -> (wallet SmartWallet)
# Bundler (ERC-4337)
sendUserOp (chain Chain, op UserOp) -> (hash Text)
getUserOpReceipt (chain Chain, hash Text) -> (receipt UserOpReceipt)
# Gas
gasPrices (chain Chain) -> (prices GasPrices)
sponsorUserOp (chain Chain, op UserOp, policyId Text) -> (paymasterAndData Text)
# Webhooks
createWebhook (webhook Webhook) -> (created Webhook)
listWebhooks (orgId Text) -> (webhooks List(Webhook))
deleteWebhook (id Text) -> (ok Bool)
# API keys
createAPIKey (orgId Text, projectId Text, name Text, scopes List(Text)) -> (result CreateAPIKeyResult)
listAPIKeys (orgId Text) -> (keys List(APIKey))
revokeAPIKey (orgId Text, id Text) -> (ok Bool)
# Fleets
createFleet (fleet Fleet) -> (created Fleet)
listFleets (orgId Text) -> (fleets List(Fleet))
scaleFleet (id Text, replicas Int32) -> (fleet Fleet)
deleteFleet (id Text) -> (ok Bool)
# Nodes (single-instance blockchain nodes via Docker/K8s)
deployNode (node Node) -> (created Node)
listNodes (orgId Text) -> (nodes List(Node))
startNode (id Text) -> (ok Bool)
stopNode (id Text) -> (ok Bool)
deleteNode (id Text) -> (ok Bool)
# Usage (internal — Bootnode → Commerce)
recordUsage (record UsageRecord) -> (ok Bool)
getUsage (orgId Text, periodStart Int64, periodEnd Int64) -> (summary UsageSummary)
+27
View File
@@ -0,0 +1,27 @@
# Regenerate Go bindings from the public ZAP schema.
#
# Hand-rolled stubs live in ./gen/ so `go build` works without zapc
# installed locally. When zapc is available the generated files take
# their place; the package layout/import path remains stable.
#
# Brand policy: ZAP only — never reference Cap'n Proto on the public
# surface (HIP-0106).
SCHEMA := ../schema/bootnode.zap
OUT := ./gen
.PHONY: gen check clean
gen:
@command -v zapc >/dev/null 2>&1 || { \
echo "zapc not installed — keeping hand-rolled stubs in $(OUT)"; \
exit 0; \
}
zapc generate $(SCHEMA) --lang go --out $(OUT)
check:
@command -v zapc >/dev/null 2>&1 || { echo "zapc not installed"; exit 1; }
zapc check $(SCHEMA)
clean:
rm -f $(OUT)/*.gen.go
+371
View File
@@ -0,0 +1,371 @@
// Package bootnodezap is the generated typed surface for the Bootnode
// subsystem. The contents here are normally regenerated by zapc from
// ../../schema/bootnode.zap; the hand-rolled stubs below let `go build`
// succeed when zapc is not installed and define the canonical type
// shape that the generator emits.
//
// Brand policy: ZAP only. No Cap'n Proto references anywhere in the
// public surface (HIP-0106).
package bootnodezap
import "context"
// Chain is the cross-cutting blockchain identifier. Values are the
// kebab-cased subset of HIP-0106 chains Bootnode speaks today.
type Chain string
const (
ChainEthereum Chain = "ethereum"
ChainPolygon Chain = "polygon"
ChainArbitrum Chain = "arbitrum"
ChainOptimism Chain = "optimism"
ChainBase Chain = "base"
ChainBSC Chain = "bsc"
ChainAvalanche Chain = "avalanche"
ChainGnosis Chain = "gnosis"
ChainCelo Chain = "celo"
ChainLinea Chain = "linea"
ChainScroll Chain = "scroll"
ChainZkSync Chain = "zksync"
ChainBlast Chain = "blast"
ChainMantle Chain = "mantle"
ChainMetis Chain = "metis"
ChainZora Chain = "zora"
ChainOpBNB Chain = "opbnb"
ChainBitcoin Chain = "bitcoin"
ChainSolana Chain = "solana"
ChainAptos Chain = "aptos"
ChainFlow Chain = "flow"
ChainBerachain Chain = "berachain"
ChainSei Chain = "sei"
ChainSonic Chain = "sonic"
ChainTron Chain = "tron"
ChainTON Chain = "ton"
ChainLux Chain = "lux"
)
// Network identifies a chain's deployment environment.
type Network string
const (
NetworkMainnet Network = "mainnet"
NetworkSepolia Network = "sepolia"
NetworkHolesky Network = "holesky"
NetworkFuji Network = "fuji"
NetworkDevnet Network = "devnet"
NetworkTestnet Network = "testnet"
NetworkLocalnet Network = "localnet"
)
// WebhookEvent enumerates the supported webhook subscription types.
type WebhookEvent string
const (
WebhookEventAddressActivity WebhookEvent = "addressActivity"
WebhookEventMinedTransaction WebhookEvent = "minedTransaction"
WebhookEventNFTActivity WebhookEvent = "nftActivity"
WebhookEventTokenTransfer WebhookEvent = "tokenTransfer"
WebhookEventInternalTransfer WebhookEvent = "internalTransfer"
WebhookEventGraphQL WebhookEvent = "graphql"
)
// NodeProvider identifies how a single-instance blockchain node is run.
type NodeProvider string
const (
NodeProviderDocker NodeProvider = "docker"
NodeProviderKubernetes NodeProvider = "kubernetes"
NodeProviderProcess NodeProvider = "process"
NodeProviderHelm NodeProvider = "helm"
)
// RPCRequest is a JSON-RPC envelope sent to the multi-chain proxy.
type RPCRequest struct {
Chain Chain `json:"chain"`
Network Network `json:"network"`
Method string `json:"method"`
Params string `json:"params"`
ID int64 `json:"id"`
}
// RPCResponse is the upstream's JSON-RPC reply, optionally annotated
// with the block at which it was served and whether it was cached.
type RPCResponse struct {
JSONRPC string `json:"jsonrpc"`
ID int64 `json:"id"`
Result string `json:"result"`
Error string `json:"error"`
BlockNumber uint64 `json:"blockNumber"`
Cached bool `json:"cached"`
}
// TokenBalance is one ERC-20 / native-coin entry for an address.
type TokenBalance struct {
Contract string `json:"contract"`
Symbol string `json:"symbol"`
Name string `json:"name"`
Decimals int32 `json:"decimals"`
Balance string `json:"balance"`
BalanceUSD float64 `json:"balanceUsd"`
}
// TokenMetadata describes a fungible token contract.
type TokenMetadata struct {
Contract string `json:"contract"`
Chain Chain `json:"chain"`
Symbol string `json:"symbol"`
Name string `json:"name"`
Decimals int32 `json:"decimals"`
Logo string `json:"logo"`
TotalSupply string `json:"totalSupply"`
}
// NFTMetadata describes a single token within an NFT contract.
type NFTMetadata struct {
Contract string `json:"contract"`
TokenID string `json:"tokenId"`
Name string `json:"name"`
Description string `json:"description"`
Image string `json:"image"`
Attributes string `json:"attributes"`
TokenURI string `json:"tokenUri"`
Chain Chain `json:"chain"`
}
// NFTCollection summarizes an ERC-721 / ERC-1155 contract.
type NFTCollection struct {
Contract string `json:"contract"`
Chain Chain `json:"chain"`
Name string `json:"name"`
Symbol string `json:"symbol"`
TotalSupply string `json:"totalSupply"`
OwnerCount int64 `json:"ownerCount"`
}
// Transfer is one historical asset movement.
type Transfer struct {
Chain Chain `json:"chain"`
TxHash string `json:"txHash"`
BlockNumber uint64 `json:"blockNumber"`
From string `json:"fromAddr"`
To string `json:"toAddr"`
Value string `json:"value"`
Asset string `json:"asset"`
Category string `json:"category"`
Timestamp int64 `json:"timestamp"`
}
// SmartWallet is one ERC-4337 account record.
type SmartWallet struct {
Address string `json:"address"`
Chain Chain `json:"chain"`
Owner string `json:"owner"`
Factory string `json:"factory"`
Salt string `json:"salt"`
Nonce uint64 `json:"nonce"`
Deployed bool `json:"deployed"`
CreatedAt int64 `json:"createdAt"`
}
// UserOp is the canonical ERC-4337 v0.6 user-operation envelope.
type UserOp struct {
Sender string `json:"sender"`
Nonce string `json:"nonce"`
InitCode string `json:"initCode"`
CallData string `json:"callData"`
CallGasLimit string `json:"callGasLimit"`
VerificationGasLimit string `json:"verificationGasLimit"`
PreVerificationGas string `json:"preVerificationGas"`
MaxFeePerGas string `json:"maxFeePerGas"`
MaxPriorityFeePerGas string `json:"maxPriorityFeePerGas"`
PaymasterAndData string `json:"paymasterAndData"`
Signature string `json:"signature"`
}
// UserOpReceipt is what the bundler returns for a successful userOp.
type UserOpReceipt struct {
UserOpHash string `json:"userOpHash"`
Sender string `json:"sender"`
Nonce string `json:"nonce"`
Success bool `json:"success"`
TxHash string `json:"txHash"`
BlockNumber uint64 `json:"blockNumber"`
GasUsed string `json:"gasUsed"`
}
// GasPrices is the current gas-price surface for a chain.
type GasPrices struct {
Chain Chain `json:"chain"`
Slow string `json:"slow"`
Standard string `json:"standard"`
Fast string `json:"fast"`
BaseFee string `json:"baseFee"`
MaxPriorityFeePerGas string `json:"maxPriorityFeePerGas"`
UpdatedAt int64 `json:"updatedAt"`
}
// GasPolicy controls per-org paymaster sponsorship caps.
type GasPolicy struct {
ID string `json:"id"`
OrgID string `json:"orgId"`
Name string `json:"name"`
Chain Chain `json:"chain"`
MaxGwei string `json:"maxGwei"`
DailyLimitUSD float64 `json:"dailyLimitUsd"`
Enabled bool `json:"enabled"`
}
// Webhook is a customer subscription to chain events.
type Webhook struct {
ID string `json:"id"`
OrgID string `json:"orgId"`
URL string `json:"url"`
Chain Chain `json:"chain"`
Event WebhookEvent `json:"event"`
Filters string `json:"filters"`
Secret string `json:"secret"`
Active bool `json:"active"`
CreatedAt int64 `json:"createdAt"`
}
// WebhookDelivery is one attempt to POST a webhook payload.
type WebhookDelivery struct {
ID string `json:"id"`
WebhookID string `json:"webhookId"`
Status string `json:"status"`
ResponseCode int32 `json:"responseCode"`
Attempts int32 `json:"attempts"`
Payload string `json:"payload"`
CreatedAt int64 `json:"createdAt"`
}
// Project groups a customer's API keys + usage under one envelope.
type Project struct {
ID string `json:"id"`
OrgID string `json:"orgId"`
Name string `json:"name"`
CreatedAt int64 `json:"createdAt"`
}
// APIKey is the durable record. The plaintext is only returned once on
// creation; everywhere else KeyHash is the bcrypt'd identifier.
type APIKey struct {
ID string `json:"id"`
ProjectID string `json:"projectId"`
OrgID string `json:"orgId"`
KeyHash string `json:"keyHash"`
Name string `json:"name"`
Scopes []string `json:"scopes"`
RateLimit int32 `json:"rateLimit"`
CreatedAt int64 `json:"createdAt"`
}
// CreateAPIKeyResult bundles the durable record with the
// return-it-once plaintext.
type CreateAPIKeyResult struct {
APIKey APIKey `json:"apiKey"`
Plaintext string `json:"plaintext"`
}
// Fleet is a multi-replica blockchain node deployment.
type Fleet struct {
ID string `json:"id"`
OrgID string `json:"orgId"`
Chain Chain `json:"chain"`
Network Network `json:"network"`
ClusterID string `json:"clusterId"`
Replicas int32 `json:"replicas"`
Status string `json:"status"`
CreatedAt int64 `json:"createdAt"`
}
// Node is a single-instance blockchain node managed by Bootnode.
type Node struct {
ID string `json:"id"`
OrgID string `json:"orgId"`
Name string `json:"name"`
Chain Chain `json:"chain"`
Network Network `json:"network"`
Provider NodeProvider `json:"provider"`
RPCPort int32 `json:"rpcPort"`
WSPort int32 `json:"wsPort"`
Status string `json:"status"`
ContainerID string `json:"containerId"`
CreatedAt int64 `json:"createdAt"`
}
// Cluster is a Kubernetes cluster Bootnode manages on the customer's
// behalf (DOKS / EKS / GKE / AKS).
type Cluster struct {
ID string `json:"id"`
OrgID string `json:"orgId"`
Name string `json:"name"`
Region string `json:"region"`
NodeCount int32 `json:"nodeCount"`
Status string `json:"status"`
}
// UsageRecord is one billable API call.
type UsageRecord struct {
OrgID string `json:"orgId"`
ProjectID string `json:"projectId"`
Chain Chain `json:"chain"`
Method string `json:"method"`
ComputeUnits int32 `json:"computeUnits"`
Timestamp int64 `json:"timestamp"`
}
// UsageSummary is a roll-up over a billing period.
type UsageSummary struct {
OrgID string `json:"orgId"`
Total int64 `json:"total"`
PeriodStart int64 `json:"periodStart"`
PeriodEnd int64 `json:"periodEnd"`
}
// Service is the typed surface every BootnodeService implementation
// must satisfy. Both the in-process Go client and the ZAP RPC client
// resolve through this interface.
type Service interface {
RPC(ctx context.Context, req RPCRequest) (RPCResponse, error)
TokenBalances(ctx context.Context, chain Chain, address string) ([]TokenBalance, error)
TokenMetadata(ctx context.Context, chain Chain, contract string) (TokenMetadata, error)
NFTMetadata(ctx context.Context, chain Chain, contract, tokenID string) (NFTMetadata, error)
NFTCollection(ctx context.Context, chain Chain, contract string) (NFTCollection, error)
NFTsOwned(ctx context.Context, chain Chain, address string) ([]NFTMetadata, error)
ListTransfers(ctx context.Context, chain Chain, address string) ([]Transfer, error)
CreateSmartWallet(ctx context.Context, chain Chain, owner string) (SmartWallet, error)
GetSmartWallet(ctx context.Context, address string) (SmartWallet, error)
SendUserOp(ctx context.Context, chain Chain, op UserOp) (string, error)
GetUserOpReceipt(ctx context.Context, chain Chain, hash string) (UserOpReceipt, error)
GasPrices(ctx context.Context, chain Chain) (GasPrices, error)
SponsorUserOp(ctx context.Context, chain Chain, op UserOp, policyID string) (string, error)
CreateWebhook(ctx context.Context, w Webhook) (Webhook, error)
ListWebhooks(ctx context.Context, orgID string) ([]Webhook, error)
DeleteWebhook(ctx context.Context, id string) (bool, error)
CreateAPIKey(ctx context.Context, orgID, projectID, name string, scopes []string) (CreateAPIKeyResult, error)
ListAPIKeys(ctx context.Context, orgID string) ([]APIKey, error)
RevokeAPIKey(ctx context.Context, orgID, id string) (bool, error)
CreateFleet(ctx context.Context, f Fleet) (Fleet, error)
ListFleets(ctx context.Context, orgID string) ([]Fleet, error)
ScaleFleet(ctx context.Context, id string, replicas int32) (Fleet, error)
DeleteFleet(ctx context.Context, id string) (bool, error)
DeployNode(ctx context.Context, n Node) (Node, error)
ListNodes(ctx context.Context, orgID string) ([]Node, error)
StartNode(ctx context.Context, id string) (bool, error)
StopNode(ctx context.Context, id string) (bool, error)
DeleteNode(ctx context.Context, id string) (bool, error)
RecordUsage(ctx context.Context, r UsageRecord) (bool, error)
GetUsage(ctx context.Context, orgID string, periodStart, periodEnd int64) (UsageSummary, error)
}
+15
View File
@@ -0,0 +1,15 @@
package bootnodezap
// Client is the typed handle other subsystems use to talk to
// Bootnode. zapc emits two implementations: an in-process one that
// forwards to a Server.Impl(), and an RPC one that frames ZAP over the
// wire. The hand-rolled stub here is just the in-process variant — it
// keeps the package buildable before zapc has been run.
//
// Per HIP-0106 the same Client interface serves both modes; superbase
// hands the in-process Client to its sibling subsystems and never
// looks at the wire side.
type Client = Service
// NewInProcessClient returns a typed handle backed by a local Server.
func NewInProcessClient(srv *Server) Client { return srv.Impl() }
+21
View File
@@ -0,0 +1,21 @@
package bootnodezap
// Server is the in-process / over-the-wire entrypoint for the
// BootnodeService implementation. zapc emits a typed dispatcher here;
// the hand-rolled stub keeps the surface compilable until the
// generator is run.
//
// When wired by superbase, every subsystem registers its Server with
// the local ZAP dispatcher and the typed Client returned by NewClient
// resolves to direct Go calls — no marshalling, no network (HIP-0106).
type Server struct {
impl Service
}
// NewServer wraps a concrete Service implementation for dispatch.
func NewServer(impl Service) *Server { return &Server{impl: impl} }
// Impl returns the underlying Service. zapc-generated server code
// uses this to dispatch on method-id; the hand-rolled stub exposes
// it directly so handlers can call typed methods without a switch.
func (s *Server) Impl() Service { return s.impl }