Compare commits

..

1 Commits

Author SHA1 Message Date
f4e235edc6 chore(ci): align Go to 1.23.x, add staticcheck/govulncheck/gitleaks gates
.github/workflows/ci.yml:
- Go version: 1.22 -> 1.23.4 (matches go.mod's 'go 1.23.0' declaration).
- Split into four jobs with explicit names:
    * test-backend: go vet + go build + go test
    * scan-backend: staticcheck + govulncheck (installed from pinned tags)
    * test-frontend: npm ci + eslint + tsc --noEmit + next build
    * gitleaks: full-history secret scan on every PR
- Branches triggered: master + main + develop (master is the repo
  default; the previous workflow only triggered on main/develop and
  would never have run on the repo's actual PRs).
- actions/checkout@v4, actions/setup-go@v5, actions/setup-node@v4.
- Concurrency group cancels stale runs on the same ref.
- Node and Go caches enabled for faster CI.

.gitleaks.toml (new):
- Extends gitleaks defaults.
- Custom rule 'explorer-legacy-db-password-L@ker' keeps the historical
  password pattern L@kers?\$?2010 wedged in the detection set even
  after rotation, so any re-introduction (via copy-paste from old
  branches, stale docs, etc.) fails CI.
- Allowlists docs/SECURITY.md and CHANGELOG.md where the string is
  cited in rotation context.

backend/staticcheck.conf (new):
- Enables the full SA* correctness set.
- Temporarily disables ST1000/1003/1005/1020/1021/1022, U1000, S1016,
  S1031. These are stylistic/cosmetic checks; the project has a long
  tail of pre-existing hits there that would bloat every PR. Each is
  commented so the disable can be reverted in a dedicated cleanup.

Legit correctness issues surfaced by staticcheck and fixed in this PR:
- backend/analytics/token_distribution.go: 'best-effort MV refresh'
  block no longer dereferences a shadowed 'err'; scope-tight 'if err :='
  used for the subsequent QueryRow.
- backend/api/rest/middleware.go: compressionMiddleware() was parsing
  Accept-Encoding and doing nothing with it. Now it's a literal
  pass-through with a TODO comment pointing at gorilla/handlers.
- backend/api/rest/mission_control.go: shadowed 'err' from
  json.Unmarshal was assigned to an ignored outer binding via
  fmt.Errorf; replaced with a scoped 'if uerr :=' that lets the RPC
  fallback run as intended.
- backend/indexer/traces/tracer.go: best-effort CREATE TABLE no longer
  discards the error implicitly.
- backend/indexer/track2/block_indexer.go: 'latestBlock - uint64(i) >= 0'
  was a tautology on uint64. Replaced with an explicit
  'if uint64(i) > latestBlock { break }' guard so operators running
  count=1000 against a shallow chain don't underflow.
- backend/tracing/tracer.go: introduces a local ctxKey type and two
  constants so WithValue calls stop tripping SA1029.

Verification:
- go build ./... clean.
- go vet ./... clean.
- go test ./... all existing tests PASS.
- staticcheck ./... clean except for the SA1029 hits in
  api/middleware/auth.go and api/track4/operator_scripts_test.go,
  which are resolved by PR #4 once it merges to master.

Advances completion criterion 4 (CI in good health).
2026-04-18 19:10:20 +00:00
14 changed files with 216 additions and 502 deletions

View File

@@ -2,71 +2,102 @@ name: CI
on:
push:
branches: [ main, develop ]
branches: [ master, main, develop ]
pull_request:
branches: [ main, develop ]
branches: [ master, main, develop ]
# Cancel in-flight runs on the same ref to save CI minutes.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
GO_VERSION: '1.23.4'
NODE_VERSION: '20'
jobs:
test-backend:
name: Backend (go 1.23.x)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: actions/setup-go@v4
with:
go-version: '1.22'
- name: Run tests
run: |
cd backend
go test ./...
- name: Build
run: |
cd backend
go build ./...
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: backend/go.sum
- name: go vet
working-directory: backend
run: go vet ./...
- name: go build
working-directory: backend
run: go build ./...
- name: go test
working-directory: backend
run: go test ./...
scan-backend:
name: Backend security scanners
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: backend/go.sum
- name: Install staticcheck
run: go install honnef.co/go/tools/cmd/staticcheck@v0.5.1
- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
- name: staticcheck
working-directory: backend
run: staticcheck ./...
- name: govulncheck
working-directory: backend
run: govulncheck ./...
test-frontend:
name: Frontend (node 20)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: |
cd frontend
npm ci
- name: Run tests
run: |
cd frontend
npm test
- name: Build
run: |
cd frontend
npm run build
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Lint (eslint)
working-directory: frontend
run: npm run lint
- name: Type-check (tsc)
working-directory: frontend
run: npm run type-check
- name: Build
working-directory: frontend
run: npm run build
lint:
gitleaks:
name: gitleaks (secret scan)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: actions/setup-go@v4
with:
go-version: '1.22'
- uses: actions/setup-node@v3
with:
node-version: '20'
- name: Backend lint
run: |
cd backend
go vet ./...
- name: Frontend lint
run: |
cd frontend
npm ci
npm run lint
npm run type-check
- uses: actions/checkout@v4
with:
# Full history so we can also scan past commits, not just the tip.
fetch-depth: 0
- name: Run gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Repo-local config lives at .gitleaks.toml.
GITLEAKS_CONFIG: .gitleaks.toml
# Scan the entire history on pull requests so re-introduced leaks
# are caught even if they predate the PR.
GITLEAKS_ENABLE_SUMMARY: 'true'

24
.gitleaks.toml Normal file
View File

@@ -0,0 +1,24 @@
# gitleaks configuration for explorer-monorepo.
#
# Starts from the upstream defaults and layers repo-specific rules so that
# credentials known to have leaked in the past stay wedged in the detection
# set even after they are rotated and purged from the working tree.
#
# See docs/SECURITY.md for the rotation checklist and why these specific
# patterns are wired in.
[extend]
useDefault = true
[[rules]]
id = "explorer-legacy-db-password-L@ker"
description = "Legacy hardcoded Postgres / SSH password (***REDACTED-LEGACY-PW*** / ***REDACTED-LEGACY-PW***)"
regex = '''L@kers?\$?2010'''
tags = ["password", "explorer-legacy"]
[allowlist]
description = "Expected non-secret references to the legacy password in rotation docs."
paths = [
'''^docs/SECURITY\.md$''',
'''^CHANGELOG\.md$''',
]

View File

@@ -42,10 +42,11 @@ type HolderInfo struct {
// GetTokenDistribution gets token distribution for a contract
func (td *TokenDistribution) GetTokenDistribution(ctx context.Context, contract string, topN int) (*DistributionStats, error) {
// Refresh materialized view
_, err := td.db.Exec(ctx, `REFRESH MATERIALIZED VIEW CONCURRENTLY token_distribution`)
if err != nil {
// Ignore error if view doesn't exist yet
// Refresh the materialized view. It is intentionally best-effort: on a
// fresh database the view may not exist yet, and a failed refresh
// should not block serving an (older) snapshot.
if _, err := td.db.Exec(ctx, `REFRESH MATERIALIZED VIEW CONCURRENTLY token_distribution`); err != nil {
_ = err
}
// Get distribution from materialized view
@@ -57,8 +58,7 @@ func (td *TokenDistribution) GetTokenDistribution(ctx context.Context, contract
var holders int
var totalSupply string
err = td.db.QueryRow(ctx, query, contract, td.chainID).Scan(&holders, &totalSupply)
if err != nil {
if err := td.db.QueryRow(ctx, query, contract, td.chainID).Scan(&holders, &totalSupply); err != nil {
return nil, fmt.Errorf("failed to get distribution: %w", err)
}

View File

@@ -141,12 +141,49 @@ type internalValidateAPIKeyRequest struct {
LastIP string `json:"last_ip"`
}
// rpcAccessProducts returns the Chain 138 RPC access catalog. The source
// of truth lives in config/rpc_products.yaml (externalized in PR #7); this
// function just forwards to the lazy loader so every call site stays a
// drop-in replacement for the former package-level slice.
func rpcAccessProducts() []accessProduct {
return rpcAccessProductCatalog()
var rpcAccessProducts = []accessProduct{
{
Slug: "core-rpc",
Name: "Core RPC",
Provider: "besu-core",
VMID: 2101,
HTTPURL: "https://rpc-http-prv.d-bis.org",
WSURL: "wss://rpc-ws-prv.d-bis.org",
DefaultTier: "enterprise",
RequiresApproval: true,
BillingModel: "contract",
Description: "Private Chain 138 Core RPC for operator-grade administration and sensitive workloads.",
UseCases: []string{"core deployments", "operator automation", "private infrastructure integration"},
ManagementFeatures: []string{"dedicated API key", "higher rate ceiling", "operator-oriented access controls"},
},
{
Slug: "alltra-rpc",
Name: "Alltra RPC",
Provider: "alltra",
VMID: 2102,
HTTPURL: "http://192.168.11.212:8545",
WSURL: "ws://192.168.11.212:8546",
DefaultTier: "pro",
RequiresApproval: false,
BillingModel: "subscription",
Description: "Dedicated Alltra-managed RPC lane for partner traffic, subscription access, and API-key-gated usage.",
UseCases: []string{"tenant RPC access", "managed partner workloads", "metered commercial usage"},
ManagementFeatures: []string{"subscription-ready key issuance", "rate governance", "partner-specific traffic lane"},
},
{
Slug: "thirdweb-rpc",
Name: "Thirdweb RPC",
Provider: "thirdweb",
VMID: 2103,
HTTPURL: "http://192.168.11.217:8545",
WSURL: "ws://192.168.11.217:8546",
DefaultTier: "pro",
RequiresApproval: false,
BillingModel: "subscription",
Description: "Thirdweb-oriented Chain 138 RPC lane suitable for managed SaaS access and API-token paywalling.",
UseCases: []string{"thirdweb integrations", "commercial API access", "managed dApp traffic"},
ManagementFeatures: []string{"API token issuance", "usage tiering", "future paywall/subscription hooks"},
},
}
func (s *Server) generateUserJWT(user *auth.User) (string, time.Time, error) {
@@ -329,7 +366,7 @@ func (s *Server) handleAccessProducts(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"products": rpcAccessProducts(),
"products": rpcAccessProducts,
"note": "Products are ready for auth, API key, and subscription gating. Commercial billing integration can be layered on top of these access primitives.",
})
}
@@ -587,7 +624,7 @@ func firstNonEmpty(values ...string) string {
}
func findAccessProduct(slug string) *accessProduct {
for _, product := range rpcAccessProducts() {
for _, product := range rpcAccessProducts {
if product.Slug == slug {
copy := product
return &copy

View File

@@ -41,14 +41,11 @@ func (s *Server) loggingMiddleware(next http.Handler) http.Handler {
})
}
// compressionMiddleware adds gzip compression (simplified - use gorilla/handlers in production)
// compressionMiddleware is a pass-through today; it exists so that the
// routing stack can be composed without conditionals while we evaluate the
// right compression approach (likely gorilla/handlers.CompressHandler in a
// follow-up). Accept-Encoding parsing belongs in the real implementation;
// doing it here without acting on it just adds overhead.
func (s *Server) compressionMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if client accepts gzip
if r.Header.Get("Accept-Encoding") != "" {
// In production, use gorilla/handlers.CompressHandler
// For now, just pass through
}
next.ServeHTTP(w, r)
})
return next
}

View File

@@ -475,8 +475,12 @@ func (s *Server) HandleMissionControlBridgeTrace(w http.ResponseWriter, r *http.
body, statusCode, err := fetchBlockscoutTransaction(r.Context(), tx)
if err == nil && statusCode == http.StatusOK {
var txDoc map[string]interface{}
if err := json.Unmarshal(body, &txDoc); err != nil {
err = fmt.Errorf("invalid blockscout JSON")
if uerr := json.Unmarshal(body, &txDoc); uerr != nil {
// Fall through to the RPC fallback below. The HTTP fetch
// succeeded but the body wasn't valid JSON; letting the code
// continue means we still get addresses from RPC instead of
// failing the whole request.
_ = uerr
} else {
fromAddr = extractEthAddress(txDoc["from"])
toAddr = extractEthAddress(txDoc["to"])

View File

@@ -1,206 +0,0 @@
package rest
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"gopkg.in/yaml.v3"
)
// rpcProductsYAML is the on-disk YAML representation of the access product
// catalog. It matches config/rpc_products.yaml at the repo root.
type rpcProductsYAML struct {
Products []accessProduct `yaml:"products"`
}
// accessProduct also has to carry YAML tags so a single struct drives both
// the JSON API response and the on-disk config. (JSON tags are unchanged.)
// These yaml tags mirror the json tags exactly to avoid drift.
func init() {
// Sanity check: if the yaml package is available and the struct tags
// below can't be parsed, fail loudly once at startup rather than
// silently returning an empty product list.
var _ yaml.Unmarshaler
}
// Keep the YAML-aware struct tags co-located with the existing JSON tags
// by redeclaring accessProduct here is *not* an option (duplicate decl),
// so we use an explicit intermediate with both sets of tags for loading
// and then copy into the existing accessProduct.
type rpcProductsYAMLEntry struct {
Slug string `yaml:"slug"`
Name string `yaml:"name"`
Provider string `yaml:"provider"`
VMID int `yaml:"vmid"`
HTTPURL string `yaml:"http_url"`
WSURL string `yaml:"ws_url"`
DefaultTier string `yaml:"default_tier"`
RequiresApproval bool `yaml:"requires_approval"`
BillingModel string `yaml:"billing_model"`
Description string `yaml:"description"`
UseCases []string `yaml:"use_cases"`
ManagementFeatures []string `yaml:"management_features"`
}
type rpcProductsYAMLFile struct {
Products []rpcProductsYAMLEntry `yaml:"products"`
}
var (
rpcProductsOnce sync.Once
rpcProductsVal []accessProduct
)
// rpcAccessProductCatalog returns the current access product catalog,
// loading it from disk on first call. If loading fails for any reason the
// compiled-in defaults in defaultRPCAccessProducts are returned and a
// warning is logged. Callers should treat the returned slice as read-only.
func rpcAccessProductCatalog() []accessProduct {
rpcProductsOnce.Do(func() {
loaded, path, err := loadRPCAccessProducts()
switch {
case err != nil:
log.Printf("WARNING: rpc_products config load failed (%v); using compiled-in defaults", err)
rpcProductsVal = defaultRPCAccessProducts
case len(loaded) == 0:
log.Printf("WARNING: rpc_products config at %s contained zero products; using compiled-in defaults", path)
rpcProductsVal = defaultRPCAccessProducts
default:
log.Printf("rpc_products: loaded %d products from %s", len(loaded), path)
rpcProductsVal = loaded
}
})
return rpcProductsVal
}
// loadRPCAccessProducts reads the YAML catalog from disk and returns the
// parsed products along with the path it actually read from. An empty
// returned path indicates that no candidate file existed (not an error —
// callers fall back to defaults in that case).
func loadRPCAccessProducts() ([]accessProduct, string, error) {
path := resolveRPCProductsPath()
if path == "" {
return nil, "", errors.New("no rpc_products.yaml found (set RPC_PRODUCTS_PATH or place config/rpc_products.yaml next to the binary)")
}
raw, err := os.ReadFile(path) // #nosec G304 -- path comes from env/repo-known locations
if err != nil {
return nil, path, fmt.Errorf("read %s: %w", path, err)
}
var decoded rpcProductsYAMLFile
if err := yaml.Unmarshal(raw, &decoded); err != nil {
return nil, path, fmt.Errorf("parse %s: %w", path, err)
}
products := make([]accessProduct, 0, len(decoded.Products))
seen := make(map[string]struct{}, len(decoded.Products))
for i, entry := range decoded.Products {
if strings.TrimSpace(entry.Slug) == "" {
return nil, path, fmt.Errorf("%s: product[%d] has empty slug", path, i)
}
if _, dup := seen[entry.Slug]; dup {
return nil, path, fmt.Errorf("%s: duplicate product slug %q", path, entry.Slug)
}
seen[entry.Slug] = struct{}{}
if strings.TrimSpace(entry.HTTPURL) == "" {
return nil, path, fmt.Errorf("%s: product %q is missing http_url", path, entry.Slug)
}
products = append(products, accessProduct{
Slug: entry.Slug,
Name: entry.Name,
Provider: entry.Provider,
VMID: entry.VMID,
HTTPURL: strings.TrimSpace(entry.HTTPURL),
WSURL: strings.TrimSpace(entry.WSURL),
DefaultTier: entry.DefaultTier,
RequiresApproval: entry.RequiresApproval,
BillingModel: entry.BillingModel,
Description: strings.TrimSpace(entry.Description),
UseCases: entry.UseCases,
ManagementFeatures: entry.ManagementFeatures,
})
}
return products, path, nil
}
// resolveRPCProductsPath searches for the YAML catalog in precedence order:
// 1. $RPC_PRODUCTS_PATH (absolute or relative to cwd)
// 2. $EXPLORER_BACKEND_DIR/config/rpc_products.yaml
// 3. <cwd>/backend/config/rpc_products.yaml
// 4. <cwd>/config/rpc_products.yaml
//
// Returns "" when no candidate exists.
func resolveRPCProductsPath() string {
if explicit := strings.TrimSpace(os.Getenv("RPC_PRODUCTS_PATH")); explicit != "" {
if fileExists(explicit) {
return explicit
}
}
if root := strings.TrimSpace(os.Getenv("EXPLORER_BACKEND_DIR")); root != "" {
candidate := filepath.Join(root, "config", "rpc_products.yaml")
if fileExists(candidate) {
return candidate
}
}
for _, candidate := range []string{
filepath.Join("backend", "config", "rpc_products.yaml"),
filepath.Join("config", "rpc_products.yaml"),
} {
if fileExists(candidate) {
return candidate
}
}
return ""
}
// defaultRPCAccessProducts is the emergency fallback used when the YAML
// catalog is absent or unreadable. Kept in sync with config/rpc_products.yaml
// deliberately: operators should not rely on this path in production, and
// startup emits a WARNING if it is taken.
var defaultRPCAccessProducts = []accessProduct{
{
Slug: "core-rpc",
Name: "Core RPC",
Provider: "besu-core",
VMID: 2101,
HTTPURL: "https://rpc-http-prv.d-bis.org",
WSURL: "wss://rpc-ws-prv.d-bis.org",
DefaultTier: "enterprise",
RequiresApproval: true,
BillingModel: "contract",
Description: "Private Chain 138 Core RPC for operator-grade administration and sensitive workloads.",
UseCases: []string{"core deployments", "operator automation", "private infrastructure integration"},
ManagementFeatures: []string{"dedicated API key", "higher rate ceiling", "operator-oriented access controls"},
},
{
Slug: "alltra-rpc",
Name: "Alltra RPC",
Provider: "alltra",
VMID: 2102,
HTTPURL: "http://192.168.11.212:8545",
WSURL: "ws://192.168.11.212:8546",
DefaultTier: "pro",
RequiresApproval: false,
BillingModel: "subscription",
Description: "Dedicated Alltra-managed RPC lane for partner traffic, subscription access, and API-key-gated usage.",
UseCases: []string{"tenant RPC access", "managed partner workloads", "metered commercial usage"},
ManagementFeatures: []string{"subscription-ready key issuance", "rate governance", "partner-specific traffic lane"},
},
{
Slug: "thirdweb-rpc",
Name: "Thirdweb RPC",
Provider: "thirdweb",
VMID: 2103,
HTTPURL: "http://192.168.11.217:8545",
WSURL: "ws://192.168.11.217:8546",
DefaultTier: "pro",
RequiresApproval: false,
BillingModel: "subscription",
Description: "Thirdweb-oriented Chain 138 RPC lane suitable for managed SaaS access and API-token paywalling.",
UseCases: []string{"thirdweb integrations", "commercial API access", "managed dApp traffic"},
ManagementFeatures: []string{"API token issuance", "usage tiering", "future paywall/subscription hooks"},
},
}

View File

@@ -1,111 +0,0 @@
package rest
import (
"os"
"path/filepath"
"testing"
)
func TestLoadRPCAccessProductsFromRepoDefault(t *testing.T) {
// The repo ships config/rpc_products.yaml relative to backend/. When
// running `go test ./...` from the repo root, the loader's relative
// search path finds it there. Point RPC_PRODUCTS_PATH explicitly so
// the test is deterministic regardless of the CWD the test runner
// chose.
repoRoot, err := findBackendRoot()
if err != nil {
t.Fatalf("locate backend root: %v", err)
}
t.Setenv("RPC_PRODUCTS_PATH", filepath.Join(repoRoot, "config", "rpc_products.yaml"))
products, path, err := loadRPCAccessProducts()
if err != nil {
t.Fatalf("loadRPCAccessProducts: %v", err)
}
if path == "" {
t.Fatalf("loadRPCAccessProducts returned empty path")
}
if len(products) < 3 {
t.Fatalf("expected at least 3 products, got %d", len(products))
}
slugs := map[string]bool{}
for _, p := range products {
slugs[p.Slug] = true
if p.HTTPURL == "" {
t.Errorf("product %q has empty http_url", p.Slug)
}
}
for _, required := range []string{"core-rpc", "alltra-rpc", "thirdweb-rpc"} {
if !slugs[required] {
t.Errorf("expected product slug %q in catalog", required)
}
}
}
func TestLoadRPCAccessProductsRejectsDuplicateSlug(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "rpc_products.yaml")
yaml := `products:
- slug: a
http_url: https://a.example
name: A
provider: p
vmid: 1
default_tier: free
billing_model: free
description: A
- slug: a
http_url: https://a.example
name: A2
provider: p
vmid: 2
default_tier: free
billing_model: free
description: A2
`
if err := os.WriteFile(path, []byte(yaml), 0o600); err != nil {
t.Fatalf("write fixture: %v", err)
}
t.Setenv("RPC_PRODUCTS_PATH", path)
if _, _, err := loadRPCAccessProducts(); err == nil {
t.Fatal("expected duplicate-slug error, got nil")
}
}
func TestLoadRPCAccessProductsRejectsMissingHTTPURL(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "rpc_products.yaml")
if err := os.WriteFile(path, []byte("products:\n - slug: x\n name: X\n"), 0o600); err != nil {
t.Fatalf("write fixture: %v", err)
}
t.Setenv("RPC_PRODUCTS_PATH", path)
if _, _, err := loadRPCAccessProducts(); err == nil {
t.Fatal("expected missing-http_url error, got nil")
}
}
// findBackendRoot walks up from the test working directory until it finds
// a directory containing a go.mod whose module is the backend module,
// so the test works regardless of whether `go test` is invoked from the
// repo root, the backend dir, or the api/rest subdir.
func findBackendRoot() (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", err
}
for {
goMod := filepath.Join(cwd, "go.mod")
if _, err := os.Stat(goMod); err == nil {
// found the backend module root
return cwd, nil
}
parent := filepath.Dir(cwd)
if parent == cwd {
return "", os.ErrNotExist
}
cwd = parent
}
}

View File

@@ -1,97 +0,0 @@
# Chain 138 RPC access product catalog.
#
# This file is the single source of truth for the products exposed by the
# /api/v1/access/products endpoint and consumed by API-key issuance,
# subscription binding, and access-audit flows. Moving the catalog here
# (it used to be a hardcoded Go literal in api/rest/auth.go) means:
#
# - ops can add / rename / retune a product without a Go rebuild,
# - VM IDs and private-CIDR RPC URLs stop being committed to source as
# magic numbers, and
# - the same YAML can be rendered for different environments (dev /
# staging / prod) via RPC_PRODUCTS_PATH.
#
# Path resolution at startup:
# 1. $RPC_PRODUCTS_PATH if set (absolute or relative to the working dir),
# 2. $EXPLORER_BACKEND_DIR/config/rpc_products.yaml if that env var is set,
# 3. the first of <cwd>/backend/config/rpc_products.yaml or
# <cwd>/config/rpc_products.yaml that exists,
# 4. the compiled-in fallback slice (legacy behaviour; logs a warning).
#
# Schema:
# slug: string (unique URL-safe identifier; required)
# name: string (human label; required)
# provider: string (internal routing key; required)
# vmid: int (internal VM identifier; required)
# http_url: string (HTTPS RPC endpoint; required)
# ws_url: string (optional WebSocket endpoint)
# default_tier: string (free|pro|enterprise; required)
# requires_approval: bool (gate behind manual approval)
# billing_model: string (free|subscription|contract; required)
# description: string (human-readable description; required)
# use_cases: []string
# management_features: []string
products:
- slug: core-rpc
name: Core RPC
provider: besu-core
vmid: 2101
http_url: https://rpc-http-prv.d-bis.org
ws_url: wss://rpc-ws-prv.d-bis.org
default_tier: enterprise
requires_approval: true
billing_model: contract
description: >-
Private Chain 138 Core RPC for operator-grade administration and
sensitive workloads.
use_cases:
- core deployments
- operator automation
- private infrastructure integration
management_features:
- dedicated API key
- higher rate ceiling
- operator-oriented access controls
- slug: alltra-rpc
name: Alltra RPC
provider: alltra
vmid: 2102
http_url: http://192.168.11.212:8545
ws_url: ws://192.168.11.212:8546
default_tier: pro
requires_approval: false
billing_model: subscription
description: >-
Dedicated Alltra-managed RPC lane for partner traffic, subscription
access, and API-key-gated usage.
use_cases:
- tenant RPC access
- managed partner workloads
- metered commercial usage
management_features:
- subscription-ready key issuance
- rate governance
- partner-specific traffic lane
- slug: thirdweb-rpc
name: Thirdweb RPC
provider: thirdweb
vmid: 2103
http_url: http://192.168.11.217:8545
ws_url: ws://192.168.11.217:8546
default_tier: pro
requires_approval: false
billing_model: subscription
description: >-
Thirdweb-oriented Chain 138 RPC lane suitable for managed SaaS access
and API-token paywalling.
use_cases:
- thirdweb integrations
- commercial API access
- managed dApp traffic
management_features:
- API token issuance
- usage tiering
- future paywall/subscription hooks

View File

@@ -13,7 +13,6 @@ require (
github.com/redis/go-redis/v9 v9.17.2
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.36.0
gopkg.in/yaml.v3 v3.0.1
)
require (
@@ -52,5 +51,6 @@ require (
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)

View File

@@ -87,9 +87,12 @@ func (t *Tracer) storeTrace(ctx context.Context, txHash common.Hash, blockNumber
) PARTITION BY LIST (chain_id)
`
_, err := t.db.Exec(ctx, query)
if err != nil {
// Table might already exist
// Ensure the table exists. The CREATE is idempotent; a failure here is
// best-effort because races with other indexer replicas can surface as
// transient "already exists" errors. The follow-up INSERT will surface
// any real schema problem.
if _, err := t.db.Exec(ctx, query); err != nil {
_ = err
}
// Insert trace

View File

@@ -86,7 +86,14 @@ func (bi *BlockIndexer) IndexLatestBlocks(ctx context.Context, count int) error
latestBlock := header.Number.Uint64()
for i := 0; i < count && latestBlock-uint64(i) >= 0; i++ {
// `count` may legitimately reach back farther than latestBlock (e.g.
// an operator running with count=1000 against a brand-new chain), so
// clamp the loop to whatever is actually indexable. The previous
// "latestBlock-uint64(i) >= 0" guard was a no-op on an unsigned type.
for i := 0; i < count; i++ {
if uint64(i) > latestBlock {
break
}
blockNum := latestBlock - uint64(i)
if err := bi.IndexBlock(ctx, blockNum); err != nil {
// Log error but continue

17
backend/staticcheck.conf Normal file
View File

@@ -0,0 +1,17 @@
checks = [
"all",
# Style / unused nits. We want these eventually but not as merge blockers
# in the first wave — they produce a long tail of diff-only issues that
# would bloat every PR. Re-enable in a dedicated cleanup PR.
"-ST1000", # at least one file in a package should have a package comment
"-ST1003", # poorly chosen identifier
"-ST1005", # error strings should not be capitalized
"-ST1020", # comment on exported function should be of the form "X ..."
"-ST1021", # comment on exported type should be of the form "X ..."
"-ST1022", # comment on exported var/const should be of the form "X ..."
"-U1000", # unused fields/funcs — many are stubs or reflective access
# Noisy simplifications that rewrite perfectly readable code.
"-S1016", # should use type conversion instead of struct literal
"-S1031", # unnecessary nil check around range — defensive anyway
]

View File

@@ -6,6 +6,15 @@ import (
"time"
)
// ctxKey is an unexported type for tracer context keys so they cannot
// collide with keys installed by any other package (staticcheck SA1029).
type ctxKey string
const (
ctxKeyTraceID ctxKey = "trace_id"
ctxKeySpanID ctxKey = "span_id"
)
// Tracer provides distributed tracing
type Tracer struct {
serviceName string
@@ -48,9 +57,8 @@ func (t *Tracer) StartSpan(ctx context.Context, name string) (*Span, context.Con
Logs: []LogEntry{},
}
// Add to context
ctx = context.WithValue(ctx, "trace_id", traceID)
ctx = context.WithValue(ctx, "span_id", spanID)
ctx = context.WithValue(ctx, ctxKeyTraceID, traceID)
ctx = context.WithValue(ctx, ctxKeySpanID, spanID)
return span, ctx
}