mirror of
https://github.com/hanzoai/base.git
synced 2026-08-07 16:46:21 +00:00
Why: consolidate the Python bootnode backend (bootnode/api/, ~100 .py files)
onto Hanzo Base, leveraging Base's IAM client and per-org/per-user tenant
infrastructure instead of reimplementing auth, sessions, and multi-tenancy.
This is the structural foundation + the 5 most-important modules, not the whole
port.
What landed
- plugins/bootnode/: blockchain developer platform mounted under /v1.
Five modules ported end-to-end:
1. auth — multi-network OAuth2 callback (lux/pars/zoo/hanzo share the
lux-web3 IAM app; client id derived from redirect_uri) + bn_
project API keys (salted SHA-256, raw key shown once, verified
in constant time). Accepts IAM JWTs and pk-/sk-/hk- keys by
reusing github.com/hanzoai/base/iam — no IAM logic duplicated.
2. team — org/member CRUD scoped to the caller's project; invited emails
resolved against IAM (active) or held pending with an invite
token.
3. networks — applies bootno.de/v1 Network CRs (white-label brand, tier,
region, validator fleet). Replaces the Python's kubectl + raw
nginx-Ingress templating with a declarative CR for the
bootno.de operator to reconcile.
4. nodes — applies bootno.de/v1 NodeFleet CRs (CRD-driven cloud path;
the Python docker provider was a local-dev concern).
5. keys — applies bootno.de/v1 KMSSecret CRs by KMS path. NO plaintext
key material ever touches this service; the request and
response carry none, and a guard rejects any private-key field.
- plugins/bootnode/kube/: dependency-free Kubernetes REST client (net/http
server-side apply). No client-go, no CGO. In-cluster SA or KUBE_APISERVER.
- plugins/commerce/: typed Hanzo Commerce (Square billing) client behind a
Client interface. bootnode depends on the interface; commerce never depends
on bootnode/iam.
- 11 SQLAlchemy models -> Base collections (models/collections.go). No `users`
collection: IAM owns identity; bootnode references IAM user ids as text.
OrgCluster is the canonical org->k8s-cluster mapping.
- examples/base/main.go: platform plugin now runs PrincipalIsolation="sqlite"
(per-org + per-user encrypted SQLite); bootnode registered (BOOTNODE_ENABLED).
Modules pending (15, tracked in PR body): chat, zap, billing-http, bundler,
fleets, gas, infra, launch, lux, mpc, nfts, observability, rpc, tokens,
transfers, wallets, webhooks-http. (chains is also ported as a bonus 6th.)
Tests (20 functions, all green incl -race)
- auth: token classification, key gen/hash/verify (tamper + wrong-salt),
redirect->clientId derivation.
- kube: server-side-apply shape (PATCH + apply-patch+yaml + fieldManager),
error propagation, 404 get/delete idempotency.
- commerce: disabled no-op, get-or-create (create + existing), usage error
propagation, immediate cancel.
- workers: HMAC-signed delivery, non-2xx-is-failure, unreachable-is-failure.
- bootnode: full end-to-end against a fake IAM + fake apiserver — /me 401 then
200, project + bn_ key, team invite/list, Network + NodeFleet CR apply/get,
KMSSecret plaintext-rejection + apply + status, public chains. Plus
fail-fast on the insecure default salt against production IAM.
Verified: `go build ./...` exits 0; `go test -race ./plugins/bootnode/...
./plugins/commerce/...` passes; live binary serves /v1/chains (200) and gates
/v1/auth/me, /v1/networks (401); per-org SQLite isolation logged active; all
10 _bootnode_ collections created on boot.
Co-authored-by: zeekay <z@zeekay.io>
106 lines
4.8 KiB
Go
106 lines
4.8 KiB
Go
package bootnode
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/hanzoai/base/core"
|
|
)
|
|
|
|
// chainNetwork describes one network of a chain.
|
|
type chainNetwork struct {
|
|
Name string `json:"name"`
|
|
ChainID *int `json:"chainId"`
|
|
IsTestnet bool `json:"isTestnet"`
|
|
ExplorerURL string `json:"explorerUrl"`
|
|
NativeCurrency string `json:"nativeCurrency"`
|
|
NativeDecimals int `json:"nativeDecimals"`
|
|
}
|
|
|
|
// chainInfo describes a supported chain and its networks.
|
|
type chainInfo struct {
|
|
Name string `json:"name"`
|
|
Slug string `json:"slug"`
|
|
Type string `json:"type"`
|
|
Networks map[string]chainNetwork `json:"networks"`
|
|
LogoURL string `json:"logoUrl"`
|
|
}
|
|
|
|
func intPtr(v int) *int { return &v }
|
|
|
|
// chainRegistry is the canonical set of chains bootnode serves. It is the Go
|
|
// equivalent of the Python ChainRegistry, trimmed to the Lux-family L1s plus
|
|
// the headline external chains. Reference data — not configuration.
|
|
var chainRegistry = map[string]chainInfo{
|
|
"lux": {
|
|
Name: "Lux Network", Slug: "lux", Type: "evm", LogoURL: "https://cdn.lux.network/logo.svg",
|
|
Networks: map[string]chainNetwork{
|
|
"mainnet": {Name: "Lux Mainnet", ChainID: intPtr(96369), ExplorerURL: "https://explore.lux.network", NativeCurrency: "LUX", NativeDecimals: 18},
|
|
"testnet": {Name: "Lux Testnet", ChainID: intPtr(96368), IsTestnet: true, ExplorerURL: "https://explore-testnet.lux.network", NativeCurrency: "LUX", NativeDecimals: 18},
|
|
"devnet": {Name: "Lux Devnet", ChainID: intPtr(96370), IsTestnet: true, NativeCurrency: "LUX", NativeDecimals: 18},
|
|
},
|
|
},
|
|
"zoo": {
|
|
Name: "Zoo", Slug: "zoo", Type: "evm", LogoURL: "https://cdn.zoo.network/logo.svg",
|
|
Networks: map[string]chainNetwork{
|
|
"mainnet": {Name: "Zoo Mainnet", ChainID: intPtr(200200), ExplorerURL: "https://explore-zoo.lux.network", NativeCurrency: "ZOO", NativeDecimals: 18},
|
|
"testnet": {Name: "Zoo Testnet", ChainID: intPtr(200201), IsTestnet: true, NativeCurrency: "ZOO", NativeDecimals: 18},
|
|
},
|
|
},
|
|
"hanzo": {
|
|
Name: "Hanzo", Slug: "hanzo", Type: "evm", LogoURL: "https://cdn.hanzo.ai/logo.svg",
|
|
Networks: map[string]chainNetwork{
|
|
"mainnet": {Name: "Hanzo Mainnet", ChainID: intPtr(36963), ExplorerURL: "https://explore-hanzo.lux.network", NativeCurrency: "AI", NativeDecimals: 18},
|
|
"testnet": {Name: "Hanzo Testnet", ChainID: intPtr(36964), IsTestnet: true, NativeCurrency: "AI", NativeDecimals: 18},
|
|
},
|
|
},
|
|
"pars": {
|
|
Name: "Pars", Slug: "pars", Type: "evm", LogoURL: "https://cdn.pars.network/logo.svg",
|
|
Networks: map[string]chainNetwork{
|
|
"mainnet": {Name: "Pars Mainnet", ChainID: intPtr(494949), ExplorerURL: "https://explore-pars.lux.network", NativeCurrency: "PARS", NativeDecimals: 18},
|
|
"testnet": {Name: "Pars Testnet", ChainID: intPtr(7071), IsTestnet: true, NativeCurrency: "PARS", NativeDecimals: 18},
|
|
},
|
|
},
|
|
"ethereum": {
|
|
Name: "Ethereum", Slug: "ethereum", Type: "evm", LogoURL: "https://cdn.lux.network/chains/ethereum.svg",
|
|
Networks: map[string]chainNetwork{
|
|
"mainnet": {Name: "Ethereum Mainnet", ChainID: intPtr(1), ExplorerURL: "https://etherscan.io", NativeCurrency: "ETH", NativeDecimals: 18},
|
|
"sepolia": {Name: "Sepolia", ChainID: intPtr(11155111), IsTestnet: true, ExplorerURL: "https://sepolia.etherscan.io", NativeCurrency: "ETH", NativeDecimals: 18},
|
|
},
|
|
},
|
|
"base": {
|
|
Name: "Base", Slug: "base", Type: "evm", LogoURL: "https://cdn.lux.network/chains/base.svg",
|
|
Networks: map[string]chainNetwork{
|
|
"mainnet": {Name: "Base Mainnet", ChainID: intPtr(8453), ExplorerURL: "https://basescan.org", NativeCurrency: "ETH", NativeDecimals: 18},
|
|
},
|
|
},
|
|
"bitcoin": {
|
|
Name: "Bitcoin", Slug: "bitcoin", Type: "utxo", LogoURL: "https://cdn.lux.network/chains/bitcoin.svg",
|
|
Networks: map[string]chainNetwork{
|
|
"mainnet": {Name: "Bitcoin Mainnet", ExplorerURL: "https://mempool.space", NativeCurrency: "BTC", NativeDecimals: 8},
|
|
},
|
|
},
|
|
"solana": {
|
|
Name: "Solana", Slug: "solana", Type: "svm", LogoURL: "https://cdn.lux.network/chains/solana.svg",
|
|
Networks: map[string]chainNetwork{
|
|
"mainnet": {Name: "Solana Mainnet", ExplorerURL: "https://solscan.io", NativeCurrency: "SOL", NativeDecimals: 9},
|
|
"devnet": {Name: "Solana Devnet", IsTestnet: true, NativeCurrency: "SOL", NativeDecimals: 9},
|
|
},
|
|
},
|
|
}
|
|
|
|
// handleListChains lists all supported chains. Ports GET /chains. Public — no
|
|
// authentication required (matches the Python).
|
|
func (p *plugin) handleListChains(e *core.RequestEvent) error {
|
|
return e.JSON(http.StatusOK, map[string]any{"chains": chainRegistry})
|
|
}
|
|
|
|
// handleGetChain returns one chain. Ports GET /chains/{chain}.
|
|
func (p *plugin) handleGetChain(e *core.RequestEvent) error {
|
|
slug := e.Request.PathValue("chain")
|
|
chain, ok := chainRegistry[slug]
|
|
if !ok {
|
|
return e.NotFoundError("chain '"+slug+"' not found", nil)
|
|
}
|
|
return e.JSON(http.StatusOK, chain)
|
|
}
|