mirror of
https://github.com/luxfi/explorer.git
synced 2026-09-24 00:25:12 +00:00
unified explorer: single Go binary with embedded SPA + per-chain graph
Module renamed to github.com/luxfi/explorer (was -unified). Imports
luxfi/indexer/{daemon,evm,storage,explorer} and luxfi/graph/{engine,
indexer,storage} as libraries; embeds the luxfi/explore SPA via go:embed
all:static.
A new ChainSupervisor wraps the existing ChainRegistry: adding a chain
(via config / mDNS / admin API) spawns a per-chain indexer goroutine plus
one graph engine + indexer per enabled subgraph; removing cancels them
and frees the routes from a sync.Map dispatcher mounted on /v1/indexer/,
/v1/explorer/ (legacy alias), and /v1/graph/.
Config schema is fully per-chain: indexer poll interval, list of graph
subgraphs (each can pick a built-in schema like amm, securities, fhe…),
brand (name/coin/accent + on-disk icon/logo paths), token features, and
top-level networks list. /envs.js exposes the chain list and active
brand to the SPA at runtime, so a deploy can rebrand by remounting
chains.yaml + assets — no image rebuild.
Dockerfile clones luxfi/explore + luxfi/indexer + luxfi/graph as
siblings inside the build, populates ./static from the SPA build, and
produces one binary. CI checks out the three repos as siblings so the
local replace in go.mod works without tagged releases.
This commit is contained in:
@@ -1,18 +1,33 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# go.mod uses local replace directives to ../indexer and ../graph. CI checks
|
||||
# out all three repos as siblings so `go build ./...` resolves without any
|
||||
# network hop to a tagged version.
|
||||
jobs:
|
||||
test:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { path: explorer }
|
||||
- uses: actions/checkout@v4
|
||||
with: { repository: luxfi/indexer, ref: main, path: indexer }
|
||||
- uses: actions/checkout@v4
|
||||
with: { repository: luxfi/graph, ref: main, path: graph }
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- run: go mod download
|
||||
- run: go test ./... && go build ./...
|
||||
with: { go-version-file: explorer/go.mod }
|
||||
- name: Build
|
||||
working-directory: explorer
|
||||
run: |
|
||||
go build ./...
|
||||
- name: Test
|
||||
working-directory: explorer
|
||||
run: |
|
||||
go test -race -count=1 ./...
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
# Binaries
|
||||
bin/
|
||||
explorer
|
||||
explorer-unified
|
||||
/explorer
|
||||
*.exe
|
||||
*.test
|
||||
|
||||
|
||||
+26
-47
@@ -1,81 +1,60 @@
|
||||
# luxfi/explorer — Combined: indexer + graph + explore frontend
|
||||
#
|
||||
# ghcr.io/luxfi/indexer ← chain data (blocks, txs, tokens, contracts)
|
||||
# ghcr.io/luxfi/graph ← subgraph events (swaps, pools, DeFi, GraphQL)
|
||||
# ghcr.io/luxfi/explore ← Next.js frontend
|
||||
# ghcr.io/luxfi/explorer ← This: all three combined
|
||||
#
|
||||
# Each component can also run standalone for independent scaling.
|
||||
# Unified Lux explorer image. Single Go binary, embedded SPA, indexer +
|
||||
# graph as libraries. Deploy with a chains.yaml (or env vars) and persist
|
||||
# /data; brand swap by remounting chains.yaml + assets, no rebuild needed.
|
||||
|
||||
ARG GO_VERSION=1.26
|
||||
ARG NODE_VERSION=22
|
||||
ARG EXPLORE_REPO=https://github.com/luxfi/explore.git
|
||||
ARG EXPLORE_REF=main
|
||||
ARG INDEXER_REPO=https://github.com/luxfi/indexer.git
|
||||
ARG INDEXER_REF=main
|
||||
ARG GRAPH_REPO=https://github.com/luxfi/graph.git
|
||||
ARG GRAPH_REF=main
|
||||
ARG EXPLORE_REPO=https://github.com/luxfi/explore.git
|
||||
ARG EXPLORE_REF=main
|
||||
|
||||
# ---- Stage 1: Build frontend ----
|
||||
# ---- Stage 1: build the SPA ----
|
||||
FROM node:${NODE_VERSION}-alpine AS frontend
|
||||
ARG EXPLORE_REPO
|
||||
ARG EXPLORE_REF
|
||||
RUN apk add --no-cache git
|
||||
WORKDIR /app
|
||||
RUN git clone --depth=1 --branch=${EXPLORE_REF} ${EXPLORE_REPO} .
|
||||
RUN corepack enable && pnpm install --frozen-lockfile
|
||||
ENV NEXT_PUBLIC_API_BASE_PATH=/v1/explorer
|
||||
ENV NODE_OPTIONS="--max-old-space-size=8192"
|
||||
RUN sed -i "s/output: 'standalone'/output: 'export'/" next.config.js || true
|
||||
RUN pnpm build || true
|
||||
ENV NODE_OPTIONS=--max-old-space-size=8192
|
||||
RUN corepack enable && pnpm install --frozen-lockfile && pnpm build || true
|
||||
RUN mkdir -p /app/out && [ -f /app/out/index.html ] || \
|
||||
echo '<!DOCTYPE html><html><head><title>Explorer</title></head><body></body></html>' > /app/out/index.html
|
||||
echo '<!doctype html><title>Explorer</title><div id="root"></div>' > /app/out/index.html
|
||||
|
||||
# ---- Stage 2: Build Go binaries ----
|
||||
# ---- Stage 2: clone indexer + graph siblings, build the unified binary ----
|
||||
FROM golang:${GO_VERSION}-alpine AS builder
|
||||
ARG INDEXER_REPO
|
||||
ARG INDEXER_REF
|
||||
ARG GRAPH_REPO
|
||||
ARG GRAPH_REF
|
||||
RUN apk add --no-cache gcc musl-dev sqlite-dev git
|
||||
ARG VERSION=dev
|
||||
RUN apk add --no-cache gcc musl-dev sqlite-dev git
|
||||
|
||||
# Build indexer
|
||||
WORKDIR /indexer
|
||||
RUN git clone --depth=1 --branch=${INDEXER_REF} ${INDEXER_REPO} .
|
||||
RUN go mod download
|
||||
COPY --from=frontend /app/out /indexer/cmd/explorer/static
|
||||
RUN sed -i 's|//go:embed static/\*|//go:embed all:static|' /indexer/cmd/explorer/frontend.go 2>/dev/null || true
|
||||
RUN CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build \
|
||||
-ldflags="-s -w -X main.version=${VERSION}" -o /bin/indexer ./cmd/indexer/
|
||||
RUN CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build \
|
||||
-ldflags="-s -w -X main.version=${VERSION}" -o /bin/explorer ./cmd/explorer/
|
||||
WORKDIR /src
|
||||
RUN git clone --depth=1 --branch=${INDEXER_REF} ${INDEXER_REPO} indexer && \
|
||||
git clone --depth=1 --branch=${GRAPH_REF} ${GRAPH_REPO} graph
|
||||
|
||||
# Build graph
|
||||
WORKDIR /graph
|
||||
RUN git clone --depth=1 --branch=${GRAPH_REF} ${GRAPH_REPO} .
|
||||
RUN go mod download
|
||||
RUN CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build \
|
||||
-ldflags="-s -w -X main.version=${VERSION}" -o /bin/graph ./cmd/graph/
|
||||
WORKDIR /src/explorer
|
||||
COPY . .
|
||||
COPY --from=frontend /app/out ./static
|
||||
|
||||
# ---- Stage 3: Runtime ----
|
||||
RUN CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" \
|
||||
go build -trimpath \
|
||||
-ldflags="-s -w -X main.version=${VERSION}" \
|
||||
-o /out/explorer .
|
||||
|
||||
# ---- Stage 3: runtime ----
|
||||
FROM alpine:3.21
|
||||
RUN apk add --no-cache ca-certificates sqlite-libs
|
||||
COPY --from=builder /bin/indexer /usr/local/bin/indexer
|
||||
COPY --from=builder /bin/explorer /usr/local/bin/explorer
|
||||
COPY --from=builder /bin/graph /usr/local/bin/graph
|
||||
COPY --from=frontend /app/out /srv/frontend
|
||||
|
||||
RUN apk add --no-cache ca-certificates sqlite-libs wget
|
||||
COPY --from=builder /out/explorer /usr/local/bin/explorer
|
||||
RUN adduser -D -u 65532 explorer
|
||||
USER explorer
|
||||
VOLUME /data
|
||||
ENV DATA_DIR=/data HTTP_ADDR=:8090
|
||||
EXPOSE 8090 4000
|
||||
|
||||
EXPOSE 8090
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
|
||||
CMD wget -qO- http://localhost:8090/health || exit 1
|
||||
|
||||
# Default: run the multi-chain explorer (indexer + graph + frontend)
|
||||
# Override with: docker run ... indexer --chain cchain --rpc ...
|
||||
# or: docker run ... graph --rpc ...
|
||||
ENTRYPOINT ["explorer"]
|
||||
|
||||
@@ -1,257 +1,175 @@
|
||||
# Explorer
|
||||
|
||||
Combined block explorer: chain indexer + subgraph engine + frontend in a single image.
|
||||
Single-binary Lux explorer: chain indexer, GraphQL engine, and SPA frontend
|
||||
in one Go process. Imports [`luxfi/indexer`](https://github.com/luxfi/indexer)
|
||||
and [`luxfi/graph`](https://github.com/luxfi/graph) as libraries; embeds the
|
||||
[`luxfi/explore`](https://github.com/luxfi/explore) Next.js build via
|
||||
`go:embed`.
|
||||
|
||||
```
|
||||
ghcr.io/luxfi/indexer -- chain data: blocks, txs, tokens, contracts (SQLite)
|
||||
ghcr.io/luxfi/graph -- subgraph events: swaps, pools, DeFi, GraphQL (SQLite)
|
||||
ghcr.io/luxfi/explore -- Next.js frontend
|
||||
ghcr.io/luxfi/explorer -- all three combined (this image)
|
||||
ghcr.io/luxfi/explorer
|
||||
-> indexes one or more EVM chains (luxfi/indexer)
|
||||
-> exposes per-chain GraphQL (luxfi/graph)
|
||||
-> serves embedded SPA + REST API (luxfi/explore)
|
||||
```
|
||||
|
||||
Each component runs standalone for independent scaling, or together in one container.
|
||||
## Routes
|
||||
|
||||
## Architecture
|
||||
| Path | Purpose |
|
||||
|---------------------------------------------------|----------------------------------|
|
||||
| `/` | SPA (embedded; SPA-routing fallback) |
|
||||
| `/envs.js` | Runtime config `window.ENV = {...}` |
|
||||
| `/icon.svg`, `/logo.svg` | Per-host brand assets (disk override) |
|
||||
| `/health` | Service health |
|
||||
| `/v1/indexer/*` | Default chain explorer API |
|
||||
| `/v1/indexer/{slug}/*` | Per-chain explorer API |
|
||||
| `/v1/explorer/{slug}/*` | Legacy alias for `/v1/indexer/{slug}` |
|
||||
| `/v1/graph/{slug}/{subgraph}/graphql` | Per-chain, per-subgraph GraphQL |
|
||||
| `/v1/explorer/admin/chains[/{slug}]` | Runtime registry CRUD |
|
||||
| `/v1/explorer/realtime` | WebSocket realtime hub |
|
||||
| `/v1/explorer/realtime/stats` | Realtime stats |
|
||||
|
||||
```
|
||||
+------------------+
|
||||
| Load Balancer |
|
||||
| (hanzo/ingress) |
|
||||
+--------+---------+
|
||||
|
|
||||
+--------v---------+
|
||||
| explorer:8090 |
|
||||
| (single binary) |
|
||||
+--+-----+------+--+
|
||||
| | |
|
||||
+-------------+ +--+--+ +-------------+
|
||||
| | | |
|
||||
+------v------+ +-----v-+ +v-----------+ |
|
||||
| indexer | | graph | | explore | |
|
||||
| /v1/explorer | | /gql | | /* (static) | |
|
||||
+------+------+ +---+---+ +-------------+ |
|
||||
| | |
|
||||
+------v--------------v------+ +-------v-------+
|
||||
| SQLite per-chain (WAL) | | ZapDB KV |
|
||||
| /data/{chain}/index.db | | fast lookups |
|
||||
+----------------------------+ +---------------+
|
||||
|
|
||||
+------v-----------------------+
|
||||
| Replicate -> S3 (optional) |
|
||||
| E2E PQ encrypted (ML-KEM) |
|
||||
+------------------------------+
|
||||
|
||||
RPC Sources (per network):
|
||||
+-----------------------------------------------+
|
||||
| api.lux.network/{mainnet,testnet,devnet} |
|
||||
| /ext/bc/C/rpc C-Chain |
|
||||
| /ext/bc/{blockchain_id}/rpc Subnet chains |
|
||||
+-----------------------------------------------+
|
||||
```
|
||||
|
||||
## Quick Start (Local Dev)
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Clone
|
||||
git clone https://github.com/luxfi/explorer-combined.git
|
||||
cd explorer-combined
|
||||
|
||||
# Run against testnet C-Chain
|
||||
docker compose up
|
||||
|
||||
# Or run the image directly
|
||||
docker run -p 8090:8090 \
|
||||
-e RPC_ENDPOINT=https://api.lux.network/testnet/ext/bc/C/rpc \
|
||||
-v $(pwd)/chains.example.yaml:/etc/explorer/chains.yaml:ro \
|
||||
-v explorer-data:/data \
|
||||
ghcr.io/luxfi/explorer
|
||||
ghcr.io/luxfi/explorer:latest \
|
||||
--config /etc/explorer/chains.yaml
|
||||
```
|
||||
|
||||
Open http://localhost:8090 to see the explorer frontend.
|
||||
|
||||
## Run Modes
|
||||
Or with compose:
|
||||
|
||||
```bash
|
||||
# Combined (default) -- indexes chain + subgraphs, serves API + frontend
|
||||
docker run -p 8090:8090 \
|
||||
-e RPC_ENDPOINT=http://node:9650/ext/bc/C/rpc \
|
||||
-v data:/data \
|
||||
ghcr.io/luxfi/explorer
|
||||
|
||||
# Indexer only (chain data API)
|
||||
docker run -p 8090:8090 \
|
||||
-v data:/data \
|
||||
ghcr.io/luxfi/explorer \
|
||||
indexer --chain cchain --rpc http://node:9650/ext/bc/C/rpc
|
||||
|
||||
# Graph only (subgraph GraphQL)
|
||||
docker run -p 4000:4000 \
|
||||
-v data:/data \
|
||||
ghcr.io/luxfi/explorer \
|
||||
graph --rpc http://node:9650/ext/bc/C/rpc
|
||||
docker compose up
|
||||
```
|
||||
|
||||
## Multi-Chain Configuration
|
||||
## Configuration
|
||||
|
||||
Use `chains.yaml` to index multiple chains in one instance.
|
||||
See `chains.example.yaml` for a complete template with all Lux networks.
|
||||
`chains.yaml` is the single source of truth. Every aspect of the deploy --
|
||||
chains, branding, GraphQL subgraphs, network switcher links -- is per-chain
|
||||
customizable. See [`chains.example.yaml`](chains.example.yaml) for the full
|
||||
schema.
|
||||
|
||||
```yaml
|
||||
data_dir: /data
|
||||
http_addr: :8090
|
||||
|
||||
brand_default:
|
||||
name: Lux Explorer
|
||||
coin: LUX
|
||||
accent_color: "#5cf"
|
||||
|
||||
networks:
|
||||
- { label: Mainnet, domain: explorer.lux.network, chain_id: 96369 }
|
||||
|
||||
chains:
|
||||
- slug: cchain
|
||||
name: Lux C-Chain
|
||||
chain_id: 96369
|
||||
type: evm
|
||||
rpc: https://api.lux.network/mainnet/ext/bc/C/rpc
|
||||
coin: LUX
|
||||
enabled: true
|
||||
default: true
|
||||
|
||||
indexer:
|
||||
poll_interval: 30s
|
||||
|
||||
graph:
|
||||
enabled: true
|
||||
subgraphs:
|
||||
- { name: amm, schema: amm, enabled: true }
|
||||
|
||||
brand:
|
||||
name: Lux C-Chain
|
||||
accent_color: "#5cf"
|
||||
```
|
||||
|
||||
`$ENV_VAR` substitution in `rpc:` / `ws:` lets one file template every
|
||||
network -- mount it read-only and inject secrets via env vars.
|
||||
|
||||
## Per-Network Customization
|
||||
|
||||
The same image powers every deploy. To rebrand for a different network:
|
||||
|
||||
1. Mount a different `chains.yaml`.
|
||||
2. Optionally mount `icon.svg` / `logo.svg` at the path referenced by
|
||||
`chains.yaml` -- they are read from disk per-request, not embedded.
|
||||
3. Set environment variables to fill the `${VAR}` placeholders.
|
||||
|
||||
No image rebuild needed. The SPA reads chain list and brand at runtime from
|
||||
`/envs.js`.
|
||||
|
||||
## Runtime Registry
|
||||
|
||||
Chains can be added, updated, and removed at runtime without restart:
|
||||
|
||||
```bash
|
||||
docker run -p 8090:8090 \
|
||||
-v ./chains.yaml:/etc/explorer/chains.yaml \
|
||||
-v data:/data \
|
||||
ghcr.io/luxfi/explorer --config /etc/explorer/chains.yaml
|
||||
# Add
|
||||
curl -X POST http://localhost:8090/v1/explorer/admin/chains \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"slug":"new","name":"New","chain_id":123,"rpc":"http://node:8545","type":"evm"}'
|
||||
|
||||
# List
|
||||
curl http://localhost:8090/v1/explorer/admin/chains
|
||||
|
||||
# Remove
|
||||
curl -X DELETE http://localhost:8090/v1/explorer/admin/chains/new
|
||||
```
|
||||
|
||||
## Per-Network Deployment
|
||||
Adding a chain spawns its indexer + (optional) graph workers; removing
|
||||
cancels them and frees the routes.
|
||||
|
||||
### Mainnet
|
||||
## mDNS Discovery
|
||||
|
||||
Set `EXPLORER_MDNS=true` to auto-register chains advertised by local Lux
|
||||
nodes (`_luxd._tcp`, `_zood._tcp`, `_hanzod._tcp`, `_parsd._tcp`). The
|
||||
explorer queries `info.getChains` on the discovered node and registers each
|
||||
tracked chain. mDNS-sourced chains never override config-sourced entries.
|
||||
|
||||
## Storage Layout
|
||||
|
||||
```
|
||||
/data/
|
||||
cchain/
|
||||
query/indexer.db (SQLite WAL, indexer)
|
||||
{zapdb}/ (KV store)
|
||||
graph/
|
||||
amm/graph.db (SQLite WAL, per-subgraph)
|
||||
zoo/
|
||||
...
|
||||
```
|
||||
|
||||
Each chain (and each subgraph) owns an isolated SQLite + KV store. WAL is
|
||||
optionally streamed to S3 with PQ encryption when `REPLICATE_S3_ENDPOINT`
|
||||
is set -- see `luxfi/indexer/daemon`.
|
||||
|
||||
## Local Build
|
||||
|
||||
```bash
|
||||
docker run -p 8090:8090 \
|
||||
-e RPC_ENDPOINT=https://api.lux.network/mainnet/ext/bc/C/rpc \
|
||||
-e NETWORK=mainnet \
|
||||
-e CHAIN_ID=96369 \
|
||||
-v data:/data \
|
||||
ghcr.io/luxfi/explorer:v0.1.0
|
||||
# Sibling layout: ~/work/lux/{indexer,graph,explorer}
|
||||
cd ~/work/lux/explorer
|
||||
go build -o explorer .
|
||||
```
|
||||
|
||||
Mainnet chains and their RPC endpoints:
|
||||
`go.mod` uses local `replace` directives pointing at `../indexer` and
|
||||
`../graph`. Production CI clones all three repos into the same layout
|
||||
inside the Docker build.
|
||||
|
||||
| Chain | EVM Chain ID | RPC Endpoint |
|
||||
|---------|-------------|----------------------------------------------------------------------|
|
||||
| C-Chain | 96369 | `https://api.lux.network/mainnet/ext/bc/C/rpc` |
|
||||
| Zoo | 200200 | `https://api.lux.network/mainnet/ext/bc/2Y625Kvdd.../rpc` |
|
||||
| Hanzo | 36963 | `https://api.lux.network/mainnet/ext/bc/2GiQb73Ce.../rpc` |
|
||||
| SPC | 36911 | `https://api.lux.network/mainnet/ext/bc/rtjwvtE1t.../rpc` |
|
||||
| Pars | 494949 | `https://api.lux.network/mainnet/ext/bc/2pUskxqaL.../rpc` |
|
||||
|
||||
### Testnet
|
||||
## Endpoints (curl examples)
|
||||
|
||||
```bash
|
||||
docker run -p 8090:8090 \
|
||||
-e RPC_ENDPOINT=https://api.lux.network/testnet/ext/bc/C/rpc \
|
||||
-e NETWORK=testnet \
|
||||
-e CHAIN_ID=96368 \
|
||||
-v data:/data \
|
||||
ghcr.io/luxfi/explorer:v0.1.0
|
||||
curl http://localhost:8090/health
|
||||
curl http://localhost:8090/v1/indexer/cchain/blocks/latest
|
||||
curl -X POST http://localhost:8090/v1/graph/cchain/amm/graphql \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"query":"{ swaps(first:5) { id amount0 amount1 } }"}'
|
||||
```
|
||||
|
||||
### Devnet
|
||||
|
||||
```bash
|
||||
docker run -p 8090:8090 \
|
||||
-e RPC_ENDPOINT=https://api.lux.network/devnet/ext/bc/C/rpc \
|
||||
-e NETWORK=devnet \
|
||||
-e CHAIN_ID=96370 \
|
||||
-v data:/data \
|
||||
ghcr.io/luxfi/explorer:v0.1.0
|
||||
```
|
||||
|
||||
## White-Label
|
||||
|
||||
Override branding via environment variables. No code changes needed.
|
||||
|
||||
```bash
|
||||
docker run -p 8090:8090 \
|
||||
-e RPC_ENDPOINT=http://node:8545 \
|
||||
-e CHAIN_NAME="My Chain" \
|
||||
-e COIN_SYMBOL=MYC \
|
||||
-e COIN_DECIMALS=18 \
|
||||
-e LOGO_URL=https://example.com/logo.svg \
|
||||
-e FAVICON_URL=https://example.com/favicon.ico \
|
||||
-e BRAND_COLOR="#FF6600" \
|
||||
ghcr.io/luxfi/explorer
|
||||
```
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---------------|---------------|----------------------------------|
|
||||
| CHAIN_NAME | Lux C-Chain | Display name in header/title |
|
||||
| COIN_SYMBOL | LUX | Native coin ticker |
|
||||
| COIN_DECIMALS | 18 | Native coin decimal places |
|
||||
| LOGO_URL | (Lux logo) | URL or path to header logo |
|
||||
| FAVICON_URL | (Lux favicon) | URL or path to favicon |
|
||||
| BRAND_COLOR | #1a1a2e | Primary brand color (hex) |
|
||||
|
||||
White-label detection in production uses hostname-based routing.
|
||||
Each `*.lux.network` subdomain resolves to the correct chain branding automatically.
|
||||
|
||||
## PQ Encrypted Backup
|
||||
|
||||
The explorer supports post-quantum encrypted backups to S3-compatible storage using ML-KEM-768.
|
||||
|
||||
```bash
|
||||
docker run -p 8090:8090 \
|
||||
-e RPC_ENDPOINT=https://api.lux.network/mainnet/ext/bc/C/rpc \
|
||||
-e BACKUP_ENABLED=true \
|
||||
-e BACKUP_S3_ENDPOINT=https://s3.lux.network \
|
||||
-e BACKUP_S3_BUCKET=explorer-backups \
|
||||
-e BACKUP_S3_ACCESS_KEY=<from-kms> \
|
||||
-e BACKUP_S3_SECRET_KEY=<from-kms> \
|
||||
-e BACKUP_INTERVAL=6h \
|
||||
-e BACKUP_PQ_ENCRYPT=true \
|
||||
-e BACKUP_PQ_PUBLIC_KEY=/etc/explorer/backup.pub \
|
||||
-v ./backup.pub:/etc/explorer/backup.pub:ro \
|
||||
-v data:/data \
|
||||
ghcr.io/luxfi/explorer
|
||||
```
|
||||
|
||||
Backup flow:
|
||||
1. SQLite checkpoint (WAL flush)
|
||||
2. Snapshot all chain databases
|
||||
3. Encrypt with ML-KEM-768 (post-quantum KEM + AES-256-GCM)
|
||||
4. Upload to S3 with content-addressed naming
|
||||
5. Prune old snapshots per retention policy
|
||||
|
||||
Store secrets in KMS (kms.hanzo.ai), never in environment files committed to git.
|
||||
|
||||
## Performance (LP-104)
|
||||
|
||||
Benchmarked against Blockscout (Elixir) on identical hardware (4 vCPU, 8 GB RAM).
|
||||
|
||||
| Metric | Explorer (Go+SQLite) | Blockscout (Elixir+PG) |
|
||||
|--------------------------|---------------------|------------------------|
|
||||
| Block indexing (blocks/s) | 850 | 120 |
|
||||
| API latency p50 (ms) | 2.1 | 18 |
|
||||
| API latency p99 (ms) | 8.4 | 145 |
|
||||
| Memory at 1M blocks (MB) | 180 | 2400 |
|
||||
| Disk at 1M blocks (GB) | 1.2 | 28 |
|
||||
| Cold start to serving (s) | 0.8 | 45 |
|
||||
| Concurrent requests (rps) | 12,000 | 800 |
|
||||
|
||||
### Cost Comparison (monthly, per chain)
|
||||
|
||||
| Setup | Explorer | Blockscout |
|
||||
|--------------------|-----------------|--------------------|
|
||||
| Compute | 1 vCPU / 512 MB | 4 vCPU / 8 GB |
|
||||
| Database | Embedded SQLite | PostgreSQL 2 vCPU |
|
||||
| Storage | 2 GB SSD | 30 GB SSD |
|
||||
| Estimated cost/mo | ~$5 | ~$80 |
|
||||
| 5 chains x 3 nets | ~$75/mo | ~$1,200/mo |
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Path | Description |
|
||||
|--------------------|------------------------------------|
|
||||
| `/health` | Health check (200 if indexing) |
|
||||
| `/v1/explorer/*` | Chain data REST API |
|
||||
| `/v1/explorer/stats` | Block count, tx count, sync status |
|
||||
| `/graphql` | Subgraph-compatible GraphQL |
|
||||
| `/*` | Embedded frontend (static) |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|-----------------|------------|------------------------------------------|
|
||||
| RPC_ENDPOINT | (required) | EVM JSON-RPC URL |
|
||||
| HTTP_ADDR | :8090 | Listen address |
|
||||
| DATA_DIR | /data | Database storage directory |
|
||||
| NETWORK | mainnet | Network name (mainnet/testnet/devnet) |
|
||||
| CHAIN_ID | (auto) | EVM chain ID override |
|
||||
| LOG_LEVEL | info | Log level (debug/info/warn/error) |
|
||||
|
||||
## Related
|
||||
|
||||
- [`luxfi/indexer`](https://github.com/luxfi/indexer) -- Go chain indexer
|
||||
- [`luxfi/graph`](https://github.com/luxfi/graph) -- Subgraph engine, Graph Node replacement
|
||||
- [`luxfi/explore`](https://github.com/luxfi/explore) -- Next.js frontend
|
||||
- [`luxfi/explorer-v1`](https://github.com/luxfi/explorer-v1) -- Legacy Elixir stack (archived)
|
||||
- [`luxfi/indexer`](https://github.com/luxfi/indexer) -- chain indexer library
|
||||
- [`luxfi/graph`](https://github.com/luxfi/graph) -- per-chain GraphQL library
|
||||
- [`luxfi/explore`](https://github.com/luxfi/explore) -- Next.js SPA
|
||||
|
||||
+102
-92
@@ -1,100 +1,110 @@
|
||||
# chains.example.yaml -- All Lux chains across all networks
|
||||
#
|
||||
# Copy to chains.yaml and adjust as needed:
|
||||
# cp chains.example.yaml chains.yaml
|
||||
# chains.example.yaml — full per-network customization for the unified Lux
|
||||
# explorer. Mount as /etc/explorer/chains.yaml or pass --config=chains.yaml.
|
||||
|
||||
# Mainnet (network-id=1)
|
||||
mainnet:
|
||||
cchain:
|
||||
name: "Lux C-Chain"
|
||||
data_dir: /data
|
||||
http_addr: :8090
|
||||
|
||||
# Default branding the SPA uses when the request hostname does not match a
|
||||
# chain-specific brand below. Per-chain `brand:` overrides win.
|
||||
brand_default:
|
||||
name: Lux Explorer
|
||||
coin: LUX
|
||||
accent_color: "#5cf"
|
||||
icon_url: /icon.svg
|
||||
logo_url: /logo.svg
|
||||
|
||||
# Cross-explorer link list rendered in the SPA's network switcher.
|
||||
networks:
|
||||
- label: Mainnet
|
||||
domain: explorer.lux.network
|
||||
chain_id: 96369
|
||||
rpc: "https://api.lux.network/mainnet/ext/bc/C/rpc"
|
||||
coin_symbol: "LUX"
|
||||
|
||||
zoo:
|
||||
name: "Zoo"
|
||||
chain_id: 200200
|
||||
rpc: "https://api.lux.network/mainnet/ext/bc/2Y625Kvdd4wZ5LKx9kLSf1jGALyARmXVN1YsTb4F3Hzv4bPDbp/rpc"
|
||||
coin_symbol: "ZOO"
|
||||
|
||||
hanzo:
|
||||
name: "Hanzo"
|
||||
chain_id: 36963
|
||||
rpc: "https://api.lux.network/mainnet/ext/bc/2GiQb73CeJESjc4omFv2YtQHZrRgJf25NXPzAr5J6UNHRcDV2F/rpc"
|
||||
coin_symbol: "HANZO"
|
||||
|
||||
spc:
|
||||
name: "SPC"
|
||||
chain_id: 36911
|
||||
rpc: "https://api.lux.network/mainnet/ext/bc/rtjwvtE1tEvrokmpeYdTq7b2zqZgmybKwR5MLjKMGAR1W78dQ/rpc"
|
||||
coin_symbol: "SPC"
|
||||
|
||||
pars:
|
||||
name: "Pars"
|
||||
chain_id: 494949
|
||||
rpc: "https://api.lux.network/mainnet/ext/bc/2pUskxqaL5Bpx7uRUGG1fDjPckjxQ4UKX4sLKeaS1NdSVBJd3F/rpc"
|
||||
coin_symbol: "PARS"
|
||||
|
||||
# Testnet (network-id=2)
|
||||
testnet:
|
||||
cchain:
|
||||
name: "Lux C-Chain (Testnet)"
|
||||
- label: Testnet
|
||||
domain: testnet.explorer.lux.network
|
||||
chain_id: 96368
|
||||
rpc: "https://api.lux.network/testnet/ext/bc/C/rpc"
|
||||
coin_symbol: "LUX"
|
||||
|
||||
zoo:
|
||||
name: "Zoo (Testnet)"
|
||||
chain_id: 200201
|
||||
rpc: "https://api.lux.network/testnet/ext/bc/2Uw1b5tf63vamHsWVeSW9uHcGBVbF5CYHS9JiLWttusY4Vvjsn/rpc"
|
||||
coin_symbol: "ZOO"
|
||||
|
||||
hanzo:
|
||||
name: "Hanzo (Testnet)"
|
||||
chain_id: 36964
|
||||
rpc: "https://api.lux.network/testnet/ext/bc/2wbYEFh7ELuovqXhyYeCLvweZrcEgNUoZVJUAtw145qTVAUJxE/rpc"
|
||||
coin_symbol: "HANZO"
|
||||
|
||||
spc:
|
||||
name: "SPC (Testnet)"
|
||||
chain_id: 36910
|
||||
rpc: "https://api.lux.network/testnet/ext/bc/u4pC2tT61o83pbQ8qwAyfXXGF6nyGFNSwCYHEFcUeESGiNKfu/rpc"
|
||||
coin_symbol: "SPC"
|
||||
|
||||
pars:
|
||||
name: "Pars (Testnet)"
|
||||
chain_id: 7071
|
||||
rpc: "https://api.lux.network/testnet/ext/bc/is5GUzsKHk3Z4Q5JXTjF4Ld9p4TnEs81tJFdaBiEBJVkaFrrs/rpc"
|
||||
coin_symbol: "PARS"
|
||||
|
||||
# Devnet (network-id=3)
|
||||
devnet:
|
||||
cchain:
|
||||
name: "Lux C-Chain (Devnet)"
|
||||
- label: Devnet
|
||||
domain: devnet.explorer.lux.network
|
||||
chain_id: 96370
|
||||
rpc: "https://api.lux.network/devnet/ext/bc/C/rpc"
|
||||
coin_symbol: "LUX"
|
||||
|
||||
zoo:
|
||||
name: "Zoo (Devnet)"
|
||||
chain_id: 200202
|
||||
rpc: "https://api.lux.network/devnet/ext/bc/2caKCdNop9C4oAPR1kYihN53HeSRDXD3JyMKS7ntfcpQYucmii/rpc"
|
||||
coin_symbol: "ZOO"
|
||||
chains:
|
||||
- slug: cchain
|
||||
name: Lux C-Chain
|
||||
chain_id: 96369
|
||||
type: evm
|
||||
rpc: ${LUX_CCHAIN_RPC:-https://api.lux.network/mainnet/ext/bc/C/rpc}
|
||||
coin: LUX
|
||||
enabled: true
|
||||
default: true
|
||||
|
||||
hanzo:
|
||||
name: "Hanzo (Devnet)"
|
||||
chain_id: 36964
|
||||
rpc: "https://api.lux.network/devnet/ext/bc/tecXMucYDxwN65mebPE6cvQ9GynG6Y5WEvwGFsY3xVDWHxiqT/rpc"
|
||||
coin_symbol: "HANZO"
|
||||
indexer:
|
||||
poll_interval: 30s
|
||||
start_block: 0
|
||||
|
||||
spc:
|
||||
name: "SPC (Devnet)"
|
||||
chain_id: 36912
|
||||
rpc: "https://api.lux.network/devnet/ext/bc/213ZsFLRCBSmrii4huQqa6S3SxYPP8ec6dPEn6ad4CtLk3RdXp/rpc"
|
||||
coin_symbol: "SPC"
|
||||
graph:
|
||||
enabled: true
|
||||
subgraphs:
|
||||
- { name: amm, schema: amm, enabled: true }
|
||||
- { name: securities, schema: securities, enabled: false }
|
||||
|
||||
pars:
|
||||
name: "Pars (Devnet)"
|
||||
chain_id: 494951
|
||||
rpc: "https://api.lux.network/devnet/ext/bc/MEGAgimfcodi7QaqCruFpTkNM6fqu7ZduLmWJJjGQwUg8uDPJ/rpc"
|
||||
coin_symbol: "PARS"
|
||||
brand:
|
||||
name: Lux C-Chain
|
||||
coin: LUX
|
||||
accent_color: "#5cf"
|
||||
icon_url: /icon.svg
|
||||
logo_url: /logo.svg
|
||||
|
||||
tokens:
|
||||
featured: []
|
||||
allow_list: []
|
||||
block_list: []
|
||||
|
||||
- slug: zoo
|
||||
name: Zoo
|
||||
chain_id: 200200
|
||||
type: evm
|
||||
rpc: ${ZOO_RPC:-https://api.lux.network/mainnet/ext/bc/2Y625Kvdd4wZ5LKx9kLSf1jGALyARmXVN1YsTb4F3Hzv4bPDbp/rpc}
|
||||
coin: ZOO
|
||||
enabled: true
|
||||
indexer: { poll_interval: 30s }
|
||||
graph:
|
||||
enabled: true
|
||||
subgraphs:
|
||||
- { name: amm, schema: amm, enabled: true }
|
||||
brand:
|
||||
name: Zoo
|
||||
coin: ZOO
|
||||
accent_color: "#7c3aed"
|
||||
|
||||
- slug: hanzo
|
||||
name: Hanzo
|
||||
chain_id: 36963
|
||||
type: evm
|
||||
rpc: ${HANZO_RPC:-https://api.lux.network/mainnet/ext/bc/2GiQb73CeJESjc4omFv2YtQHZrRgJf25NXPzAr5J6UNHRcDV2F/rpc}
|
||||
coin: HANZO
|
||||
enabled: true
|
||||
indexer: { poll_interval: 30s }
|
||||
graph:
|
||||
enabled: true
|
||||
subgraphs:
|
||||
- { name: amm, schema: amm, enabled: true }
|
||||
brand:
|
||||
name: Hanzo
|
||||
coin: HANZO
|
||||
accent_color: "#0b1220"
|
||||
|
||||
- slug: spc
|
||||
name: SPC
|
||||
chain_id: 36911
|
||||
type: evm
|
||||
rpc: ${SPC_RPC:-https://api.lux.network/mainnet/ext/bc/rtjwvtE1tEvrokmpeYdTq7b2zqZgmybKwR5MLjKMGAR1W78dQ/rpc}
|
||||
coin: SPC
|
||||
enabled: true
|
||||
indexer: { poll_interval: 30s }
|
||||
|
||||
- slug: pars
|
||||
name: Pars
|
||||
chain_id: 494949
|
||||
type: evm
|
||||
rpc: ${PARS_RPC:-https://api.lux.network/mainnet/ext/bc/2pUskxqaL5Bpx7uRUGG1fDjPckjxQ4UKX4sLKeaS1NdSVBJd3F/rpc}
|
||||
coin: PARS
|
||||
enabled: true
|
||||
indexer: { poll_interval: 30s }
|
||||
|
||||
+4
-5
@@ -5,17 +5,16 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
VERSION: dev
|
||||
image: ghcr.io/luxfi/explorer:dev
|
||||
ports:
|
||||
- "8090:8090"
|
||||
volumes:
|
||||
- explorer-data:/data
|
||||
- ./chains.example.yaml:/etc/explorer/chains.yaml:ro
|
||||
environment:
|
||||
- RPC_ENDPOINT=https://api.lux.network/testnet/ext/bc/C/rpc
|
||||
- NETWORK=testnet
|
||||
- DATA_DIR=/data
|
||||
- HTTP_ADDR=:8090
|
||||
- LOG_LEVEL=debug
|
||||
DATA_DIR: /data
|
||||
HTTP_ADDR: :8090
|
||||
EXPLORER_CONFIG: /etc/explorer/chains.yaml
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config is the top-level explorer configuration loaded from chains.yaml.
|
||||
type Config struct {
|
||||
DataDir string `yaml:"data_dir"`
|
||||
HTTPAddr string `yaml:"http_addr"`
|
||||
BrandDefault Brand `yaml:"brand_default"`
|
||||
Networks []Network `yaml:"networks"`
|
||||
Chains []ChainConfig `yaml:"chains"`
|
||||
}
|
||||
|
||||
// ChainConfig defines a single chain plus per-chain customization. The
|
||||
// admin/registry/mdns layer also uses this shape; fields beyond Slug/Name/
|
||||
// ChainID/RPC/Type/Default are optional and apply when the chain is loaded
|
||||
// from a config file.
|
||||
type ChainConfig struct {
|
||||
Slug string `json:"slug" yaml:"slug"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
ChainID int64 `json:"chain_id" yaml:"chain_id"`
|
||||
Type string `json:"type" yaml:"type"` // evm, dag, linear, solana, bitcoin, cosmos
|
||||
RPC string `json:"rpc" yaml:"rpc"`
|
||||
WS string `json:"ws" yaml:"ws"`
|
||||
CoinSymbol string `json:"coin" yaml:"coin"`
|
||||
Enabled bool `json:"enabled" yaml:"enabled"`
|
||||
Default bool `json:"default" yaml:"default"`
|
||||
Source string `json:"source" yaml:"-"` // config, env, mdns, admin
|
||||
|
||||
Indexer IndexerSettings `json:"indexer,omitempty" yaml:"indexer,omitempty"`
|
||||
Graph GraphSettings `json:"graph,omitempty" yaml:"graph,omitempty"`
|
||||
Brand *Brand `json:"brand,omitempty" yaml:"brand,omitempty"`
|
||||
Tokens TokensFeatures `json:"tokens,omitempty" yaml:"tokens,omitempty"`
|
||||
}
|
||||
|
||||
// IndexerSettings overrides indexer behaviour for a single chain.
|
||||
type IndexerSettings struct {
|
||||
PollInterval string `yaml:"poll_interval" json:"poll_interval,omitempty"` // e.g. "30s"
|
||||
StartBlock uint64 `yaml:"start_block" json:"start_block,omitempty"`
|
||||
}
|
||||
|
||||
// GraphSettings configures per-chain GraphQL subgraphs.
|
||||
type GraphSettings struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
Subgraphs []Subgraph `yaml:"subgraphs" json:"subgraphs,omitempty"`
|
||||
}
|
||||
|
||||
// Subgraph mounts a single named GraphQL schema under
|
||||
// /v1/graph/{chain-slug}/{subgraph-name}/. Schema is either a built-in name
|
||||
// understood by github.com/luxfi/graph/engine.LoadBuiltin (amm, uniswap-v2,
|
||||
// uniswap-v3, uniswap-v4, securities, fhe, etc.) or a path to a .graphql file.
|
||||
type Subgraph struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Schema string `yaml:"schema" json:"schema"`
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
}
|
||||
|
||||
// Brand is the per-chain (or default) UI branding shown by the SPA. Icon/Logo
|
||||
// files are read from disk on every request so a deploy can swap them without
|
||||
// rebuilding the binary.
|
||||
type Brand struct {
|
||||
Name string `yaml:"name" json:"name,omitempty"`
|
||||
Coin string `yaml:"coin" json:"coin,omitempty"`
|
||||
AccentColor string `yaml:"accent_color" json:"accentColor,omitempty"`
|
||||
IconFile string `yaml:"icon_file" json:"-"`
|
||||
LogoFile string `yaml:"logo_file" json:"-"`
|
||||
IconURL string `yaml:"icon_url" json:"iconUrl,omitempty"`
|
||||
LogoURL string `yaml:"logo_url" json:"logoUrl,omitempty"`
|
||||
}
|
||||
|
||||
// TokensFeatures controls which tokens the SPA highlights or hides.
|
||||
type TokensFeatures struct {
|
||||
Featured []string `yaml:"featured" json:"featured,omitempty"`
|
||||
AllowList []string `yaml:"allow_list" json:"allowList,omitempty"`
|
||||
BlockList []string `yaml:"block_list" json:"blockList,omitempty"`
|
||||
}
|
||||
|
||||
// Network is a peer explorer the SPA can link to in its network switcher.
|
||||
type Network struct {
|
||||
Label string `yaml:"label" json:"label"`
|
||||
Domain string `yaml:"domain" json:"domain"`
|
||||
ChainID int64 `yaml:"chain_id" json:"chainId"`
|
||||
}
|
||||
|
||||
// slugPattern restricts chain slugs to lowercase alphanumeric + hyphen so they
|
||||
// remain safe in URL routes (/v1/indexer/{slug}/) and filesystem paths.
|
||||
var slugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,63}$`)
|
||||
|
||||
// LoadConfig reads chains.yaml, expands $ENV_VAR in RPC/WS, and validates slugs.
|
||||
func LoadConfig(path string) (Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return Config{}, fmt.Errorf("parse %s: %w", path, err)
|
||||
}
|
||||
for i := range cfg.Chains {
|
||||
cfg.Chains[i].RPC = os.Expand(cfg.Chains[i].RPC, os.Getenv)
|
||||
cfg.Chains[i].WS = os.Expand(cfg.Chains[i].WS, os.Getenv)
|
||||
if !slugPattern.MatchString(cfg.Chains[i].Slug) {
|
||||
return Config{}, fmt.Errorf("chain[%d]: invalid slug %q", i, cfg.Chains[i].Slug)
|
||||
}
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// staticFS is the embedded SPA. The Dockerfile populates static/ from a
|
||||
// luxfi/explore build before the Go build runs; for local dev a placeholder
|
||||
// index.html is committed so go:embed always succeeds.
|
||||
//
|
||||
//go:embed all:static
|
||||
var staticFS embed.FS
|
||||
|
||||
// Frontend serves the embedded SPA, runtime config (/envs.js), and per-host
|
||||
// brand assets (/icon.svg, /logo.svg). Brand assets read from disk on every
|
||||
// request so a deploy can swap them without rebuilding the binary.
|
||||
type Frontend struct {
|
||||
cfg Config
|
||||
registry *ChainRegistry
|
||||
root fs.FS
|
||||
index []byte
|
||||
}
|
||||
|
||||
// NewFrontend returns a frontend handler bound to a config and chain registry.
|
||||
func NewFrontend(cfg Config, r *ChainRegistry) (*Frontend, error) {
|
||||
root, err := fs.Sub(staticFS, "static")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idx, err := fs.ReadFile(root, "index.html")
|
||||
if err != nil {
|
||||
idx = []byte("<!doctype html><title>Explorer</title>")
|
||||
}
|
||||
return &Frontend{cfg: cfg, registry: r, root: root, index: idx}, nil
|
||||
}
|
||||
|
||||
// Mount installs / (SPA), /envs.js, /icon.svg, /logo.svg on mux.
|
||||
func (f *Frontend) Mount(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /envs.js", f.handleEnvs)
|
||||
mux.HandleFunc("GET /icon.svg", f.handleIcon)
|
||||
mux.HandleFunc("GET /logo.svg", f.handleLogo)
|
||||
mux.HandleFunc("/", f.handleSPA)
|
||||
}
|
||||
|
||||
// handleSPA serves embedded static assets and falls back to index.html for
|
||||
// any path the SPA owns (client-side routing).
|
||||
func (f *Frontend) handleSPA(w http.ResponseWriter, r *http.Request) {
|
||||
p := strings.TrimPrefix(r.URL.Path, "/")
|
||||
if p == "" {
|
||||
f.writeIndex(w)
|
||||
return
|
||||
}
|
||||
file, err := f.root.Open(p)
|
||||
if err != nil {
|
||||
f.writeIndex(w)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
stat, err := file.Stat()
|
||||
if err != nil || stat.IsDir() {
|
||||
f.writeIndex(w)
|
||||
return
|
||||
}
|
||||
http.ServeFileFS(w, r, f.root, p)
|
||||
}
|
||||
|
||||
func (f *Frontend) writeIndex(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Write(f.index)
|
||||
}
|
||||
|
||||
// handleEnvs returns runtime config the SPA reads instead of build-time env
|
||||
// vars. This is what makes per-network customization possible without a
|
||||
// rebuild: deploy the same image, mount a different chains.yaml, get a
|
||||
// different brand + chain list.
|
||||
func (f *Frontend) handleEnvs(w http.ResponseWriter, r *http.Request) {
|
||||
hostBrand := f.brandForHost(r.Host)
|
||||
chains := f.chainListJSON()
|
||||
|
||||
env := map[string]any{
|
||||
"VITE_CHAINS": chains,
|
||||
"VITE_NETWORKS": f.cfg.Networks,
|
||||
"VITE_BRAND": hostBrand,
|
||||
}
|
||||
body, _ := json.Marshal(env)
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("window.ENV = ")
|
||||
buf.Write(body)
|
||||
buf.WriteString(";\n")
|
||||
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
// handleIcon serves the per-host or default icon.svg from disk.
|
||||
func (f *Frontend) handleIcon(w http.ResponseWriter, r *http.Request) {
|
||||
f.serveBrandFile(w, r, func(b Brand) string { return b.IconFile }, "icon.svg")
|
||||
}
|
||||
|
||||
// handleLogo serves the per-host or default logo.svg from disk.
|
||||
func (f *Frontend) handleLogo(w http.ResponseWriter, r *http.Request) {
|
||||
f.serveBrandFile(w, r, func(b Brand) string { return b.LogoFile }, "logo.svg")
|
||||
}
|
||||
|
||||
// serveBrandFile resolves the file path from the matched brand (per-chain or
|
||||
// default) and sends it. Falls back to the embedded SPA's named asset if no
|
||||
// disk override is configured.
|
||||
func (f *Frontend) serveBrandFile(w http.ResponseWriter, r *http.Request, pick func(Brand) string, fallback string) {
|
||||
brand := f.brandStruct(r.Host)
|
||||
if path := pick(brand); path != "" {
|
||||
if data, err := os.ReadFile(path); err == nil {
|
||||
w.Header().Set("Content-Type", contentTypeFor(path))
|
||||
w.Header().Set("Cache-Control", "public, max-age=300")
|
||||
w.Write(data)
|
||||
return
|
||||
}
|
||||
}
|
||||
if data, err := fs.ReadFile(f.root, fallback); err == nil {
|
||||
w.Header().Set("Content-Type", contentTypeFor(fallback))
|
||||
w.Header().Set("Cache-Control", "public, max-age=300")
|
||||
w.Write(data)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
// brandForHost returns a JSON-shaped brand for /envs.js. It strips file paths.
|
||||
func (f *Frontend) brandForHost(host string) map[string]any {
|
||||
b := f.brandStruct(host)
|
||||
return map[string]any{
|
||||
"name": b.Name,
|
||||
"coin": b.Coin,
|
||||
"accentColor": b.AccentColor,
|
||||
"iconUrl": b.IconURL,
|
||||
"logoUrl": b.LogoURL,
|
||||
}
|
||||
}
|
||||
|
||||
// brandStruct returns the Brand for a request host: a chain whose name or
|
||||
// slug matches the hostname wins; otherwise the global default.
|
||||
func (f *Frontend) brandStruct(host string) Brand {
|
||||
host = strings.ToLower(strings.SplitN(host, ":", 2)[0])
|
||||
for _, c := range f.cfg.Chains {
|
||||
if c.Brand == nil {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(host, c.Slug+".") || host == c.Slug {
|
||||
return *c.Brand
|
||||
}
|
||||
}
|
||||
return f.cfg.BrandDefault
|
||||
}
|
||||
|
||||
// chainListJSON returns a SPA-friendly chain list from the live registry,
|
||||
// not the static config — chains added via mDNS/admin show up immediately.
|
||||
var chainListMu sync.Mutex
|
||||
|
||||
func (f *Frontend) chainListJSON() []map[string]any {
|
||||
chainListMu.Lock()
|
||||
defer chainListMu.Unlock()
|
||||
out := []map[string]any{}
|
||||
if f.registry == nil {
|
||||
return out
|
||||
}
|
||||
for _, c := range f.registry.List() {
|
||||
out = append(out, map[string]any{
|
||||
"slug": c.Slug,
|
||||
"name": c.Name,
|
||||
"chainId": c.ChainID,
|
||||
"rpc": c.RPC,
|
||||
"coin": c.CoinSymbol,
|
||||
"type": c.Type,
|
||||
"default": c.Default,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// contentTypeFor maps a file extension to a Content-Type, defaulting to
|
||||
// application/octet-stream.
|
||||
func contentTypeFor(path string) string {
|
||||
switch strings.ToLower(filepath.Ext(path)) {
|
||||
case ".svg":
|
||||
return "image/svg+xml"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".jpg", ".jpeg":
|
||||
return "image/jpeg"
|
||||
case ".webp":
|
||||
return "image/webp"
|
||||
case ".ico":
|
||||
return "image/x-icon"
|
||||
default:
|
||||
return "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
// fingerprint returns a short, stable identifier for the embedded asset
|
||||
// bundle, useful for cache-busting log lines at startup.
|
||||
func fingerprint() string {
|
||||
root, err := fs.Sub(staticFS, "static")
|
||||
if err != nil {
|
||||
return "0"
|
||||
}
|
||||
count := 0
|
||||
_ = fs.WalkDir(root, ".", func(_ string, _ fs.DirEntry, _ error) error {
|
||||
count++
|
||||
return nil
|
||||
})
|
||||
return fmt.Sprintf("assets=%d", count)
|
||||
}
|
||||
@@ -1,20 +1,94 @@
|
||||
module github.com/luxfi/explorer-unified
|
||||
module github.com/luxfi/explorer
|
||||
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
github.com/luxfi/graph v0.1.0
|
||||
github.com/luxfi/indexer v0.0.0-00010101000000-000000000000
|
||||
github.com/luxfi/mdns v0.1.0
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/hpke v0.4.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/flatbuffers v25.12.19+incompatible // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gorilla/rpc v1.2.1 // indirect
|
||||
github.com/grandcat/zeroconf v1.0.0 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
github.com/hablullah/go-hijri v1.0.2 // indirect
|
||||
github.com/hablullah/go-juliandays v1.0.0 // indirect
|
||||
github.com/hanzoai/replicate v0.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/lib/pq v1.12.1 // indirect
|
||||
github.com/lmittmann/tint v1.1.3 // indirect
|
||||
github.com/luxfi/age v1.4.0 // indirect
|
||||
github.com/luxfi/cache v1.2.1 // indirect
|
||||
github.com/luxfi/compress v0.0.5 // indirect
|
||||
github.com/luxfi/concurrent v0.0.3 // indirect
|
||||
github.com/luxfi/crypto v1.17.45 // indirect
|
||||
github.com/luxfi/database v1.17.44 // indirect
|
||||
github.com/luxfi/ids v1.2.9 // indirect
|
||||
github.com/luxfi/log v1.4.1 // indirect
|
||||
github.com/luxfi/math v1.2.4 // indirect
|
||||
github.com/luxfi/math/big v0.1.0 // indirect
|
||||
github.com/luxfi/metric v1.5.1 // indirect
|
||||
github.com/luxfi/mock v0.1.1 // indirect
|
||||
github.com/luxfi/zap v0.2.1 // indirect
|
||||
github.com/luxfi/zapdb/v4 v4.9.3 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/markusmobius/go-dateparser v1.2.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/psanford/sqlite3vfs v0.0.0-20251127171934-4e34e03a991a // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/superfly/ltx v0.5.1 // indirect
|
||||
github.com/tetratelabs/wazero v1.2.1 // indirect
|
||||
github.com/wasilibs/go-re2 v1.3.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
modernc.org/libc v1.70.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.48.0 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/luxfi/graph => ../graph
|
||||
github.com/luxfi/indexer => ../indexer
|
||||
)
|
||||
|
||||
@@ -1,41 +1,353 @@
|
||||
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=
|
||||
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
|
||||
cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM=
|
||||
cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
|
||||
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
|
||||
cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8=
|
||||
cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
|
||||
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
|
||||
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2 h1:Hr5FTipp7SL07o2FvoVOX9HRiRH3CR3Mj8pxqCcdD5A=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 h1:MhRfI58HblXzCtWEZCO0feHs8LweePB3s90r7WaR1KU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0/go.mod h1:okZ+ZURbArNdlJ+ptXoyHNuOETzOl1Oww19rm8I2WLA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 h1:FwladfywkNirM+FZYLBR2kBz5C8Tg0fw5w5Y7meRXWI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2/go.mod h1:vv5Ad0RrIoT1lJFdWBZwt4mB1+j+V8DUroixmKDTCdk=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81 h1:TBzelXBdnzDy+HCrBMcomEnhrmigkWOI1/mIPCi2u4M=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0 h1:wQlqotpyjYPjJz+Noh5bRu7Snmydk8SKC5Z6u1CR20Y=
|
||||
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18 h1:9vWXHtaepwoAl/UuKzxwgOoJDXPCC3hvgNMfcmdS2Tk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18/go.mod h1:sKuUZ+MwUTuJbYvZ8pK0x10LvgcJK3Y4rmh63YBekwk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
|
||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
|
||||
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5JF7K9k=
|
||||
github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptRfGk=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
|
||||
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
|
||||
github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
|
||||
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
||||
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
|
||||
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
|
||||
github.com/hanzoai/replicate v0.6.0 h1:YxjIynlo3iKtpf6GeQngeB5RbexIKPPKsZIoq5EHe3E=
|
||||
github.com/hanzoai/replicate v0.6.0/go.mod h1:PBvHNQ7cXBJejTWDWxXWvtUwjT5O66pTC7npxLWu8H0=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
|
||||
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE=
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lib/pq v1.12.1 h1:x1nbl/338GLqeDJ/FAiILallhAsqubLzEZu/pXtHUow=
|
||||
github.com/lib/pq v1.12.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
|
||||
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/luxfi/age v1.4.0 h1:tU5q65RQSQdaVq64Z/DVUhiPQMoMPQT6pr41LsvG0AQ=
|
||||
github.com/luxfi/age v1.4.0/go.mod h1:iAYAxgvrXxcy746+Ovh/eWWDuF9teJLNcCSSOX9RYW0=
|
||||
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
|
||||
github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
|
||||
github.com/luxfi/compress v0.0.5 h1:4tEUHw5MK1bu5UOjfYCt4OKMiH7yykIgmGPRA/BfJTM=
|
||||
github.com/luxfi/compress v0.0.5/go.mod h1:Cc1yxD2pfzrvpO32W2GDwLKff+CylHEvzZh2Ko8RSIU=
|
||||
github.com/luxfi/concurrent v0.0.3 h1:eJyv1fhaC0jMLMw6+QS774cUmp7GK+ouMgvLCqnC7cc=
|
||||
github.com/luxfi/concurrent v0.0.3/go.mod h1:Aj/FR5NpM0cB2P4Nt3+tz9+dV6V+LUW4HuMgSjwq5hw=
|
||||
github.com/luxfi/constants v1.4.3 h1:bcqUO8twMOhHWCgN/RbjfWulVPbvUQcxDkssaRrCx4g=
|
||||
github.com/luxfi/constants v1.4.3/go.mod h1:ENkJ121cmDEkwQPDiKK4QhnTnW9u37PGpepbrdVcAmc=
|
||||
github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM=
|
||||
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
|
||||
github.com/luxfi/crypto v1.17.45 h1:uGK0y4+aLipE/M0YIQ5hcsWv0ZG0E4cPv03a94K/eLE=
|
||||
github.com/luxfi/crypto v1.17.45/go.mod h1:GnAkhQ7HNs3X0Tzx5nOONS3kl0yRmWHbDcRO5ffILsg=
|
||||
github.com/luxfi/database v1.17.44 h1:hfiTls7sqbweW+o4iaZqB8P997paC+vpgWmhN6v5MJ0=
|
||||
github.com/luxfi/database v1.17.44/go.mod h1:6Ey5y3I0WNLHbxIlIdFqUuKfBg+b0fAgTA8FgRgQ8zg=
|
||||
github.com/luxfi/geth v1.16.69 h1:CHO6xTZ+A+3itk94ts4uyVRJajNVP3RxWTjJp5qGOlk=
|
||||
github.com/luxfi/geth v1.16.69/go.mod h1:8eEO1hW5sa6OH2VeCMaCPnRz28JBxFvCPBCWPLsU2ck=
|
||||
github.com/luxfi/ids v1.2.9 h1:+yjdhXW99drnd2Zlp1u/p8k3G23W3/1btJQ4ogHawUI=
|
||||
github.com/luxfi/ids v1.2.9/go.mod h1:khJOEdOPxd22yn0jcVrnbX1ADa0GHn5Y74gvCzN5BYc=
|
||||
github.com/luxfi/log v1.4.1 h1:rIfFRodb9jrD/w7KayaUk0Oc+37PaQQdKEEMJCjR8gw=
|
||||
github.com/luxfi/log v1.4.1/go.mod h1:64IE3xRMJcpkQwnPUfJw3pDj7wU0kRS7BZ9wM7R72jk=
|
||||
github.com/luxfi/math v1.2.4 h1:iVz7s0gToCNUU/2cLT8gUa8MLzc0YRp4jBmeXBeKdXk=
|
||||
github.com/luxfi/math v1.2.4/go.mod h1:i+Am9YvFsqDE75ZW8MPE62XCXGukXNiN/7mD9SKxxZY=
|
||||
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
|
||||
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk=
|
||||
github.com/luxfi/mdns v0.1.0 h1:VB3mQcETc9j5SY1S6lAgFtuGr/rjWuDgPYnxS+OKWMQ=
|
||||
github.com/luxfi/mdns v0.1.0/go.mod h1:/3dheKVjUk2yiS/ocH1IDzeLXOIe+kpVsErIGDOZdiQ=
|
||||
github.com/luxfi/metric v1.5.1 h1:6tVarXMnNR3Xzud8FYUHjtXabTll3HI/OEqG0tgLAcI=
|
||||
github.com/luxfi/metric v1.5.1/go.mod h1:PkD4D4JoGuyKtfUkqPNYkrg9xKrJeNVZFdW/5XvAe/A=
|
||||
github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
|
||||
github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
|
||||
github.com/luxfi/sampler v1.0.0 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0=
|
||||
github.com/luxfi/sampler v1.0.0/go.mod h1:f96/ozlj9vFfZj+akLtrHn4VpulQahwB+MQQhpeIekk=
|
||||
github.com/luxfi/utils v1.1.3 h1:yDEuuJ6bm9H8DzOzzveXI1AZlu/AeSpHNRrY1akbyc0=
|
||||
github.com/luxfi/utils v1.1.3/go.mod h1:lskchZiYYRBFsSyYg3NWLPw211uRyxIs3/wAzrfSIAk=
|
||||
github.com/luxfi/zap v0.2.1 h1:5uU3Z47Nu/FNcLqYeKZ4B0f+0ZDF79/2M/9FRN/1/Hw=
|
||||
github.com/luxfi/zap v0.2.1/go.mod h1:CobyEg6MNO6qZ5wn3kVEw6QCxFVZvvGMnEKsC5sJZtI=
|
||||
github.com/luxfi/zapdb/v4 v4.9.3 h1:KV1DrbZqtZsZvrWyPw5D8WHR0DxzqW2jHjqx6obxgoM=
|
||||
github.com/luxfi/zapdb/v4 v4.9.3/go.mod h1:BrpLaDeYy+smON4ASDPCFKWrQ58CXhy3WNEPGna7vuU=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/markusmobius/go-dateparser v1.2.4 h1:2e8XJozaERVxGwsRg72coi51L2aiYqE2gukkdLc85ck=
|
||||
github.com/markusmobius/go-dateparser v1.2.4/go.mod h1:CBAUADJuMNhJpyM6IYaWAoFhtKaqnUcznY2cL7gNugY=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nats-io/nats.go v1.44.0 h1:ECKVrDLdh/kDPV1g0gAQ+2+m2KprqZK5O/eJAyAnH2M=
|
||||
github.com/nats-io/nats.go v1.44.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/psanford/sqlite3vfs v0.0.0-20251127171934-4e34e03a991a h1:r4YWl0uVObCbBFvj1VsIlyHzgZwZOHvY1KdRaQjzzUc=
|
||||
github.com/psanford/sqlite3vfs v0.0.0-20251127171934-4e34e03a991a/go.mod h1:iW4cSew5PAb1sMZiTEkVJAIBNrepaB6jTYjeP47WtI0=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/studio-b12/gowebdav v0.11.0 h1:qbQzq4USxY28ZYsGJUfO5jR+xkFtcnwWgitp4Zp1irU=
|
||||
github.com/studio-b12/gowebdav v0.11.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||
github.com/superfly/ltx v0.5.1 h1:vGUeBhKvBKZ2s2TsTrOSahz+m0PswfqOWDAD+ICiiYY=
|
||||
github.com/superfly/ltx v0.5.1/go.mod h1:Nf50QAIXU/ET4ua3AuQ2fh31MbgNQZA7r/DYx6Os77s=
|
||||
github.com/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs=
|
||||
github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
|
||||
github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw=
|
||||
github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg=
|
||||
github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
|
||||
github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.154.0 h1:X7QkVKZBskztmpPKWQXgjJRPA2dJYrL6r+sYPRLj050=
|
||||
google.golang.org/api v0.154.0/go.mod h1:qhSMkM85hgqiokIYsrRyKxrjfBeIhgl4Z2JmeRkYylc=
|
||||
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos=
|
||||
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
||||
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.48.0 h1:ElZyLop3Q2mHYk5IFPPXADejZrlHu7APbpB0sF78bq4=
|
||||
modernc.org/sqlite v1.48.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
// Package main is the unified explorer entry point.
|
||||
// Package main is the unified Lux explorer: a single Go binary that embeds
|
||||
// the SPA, indexes one or more chains via github.com/luxfi/indexer, runs a
|
||||
// per-chain GraphQL engine via github.com/luxfi/graph, and exposes the
|
||||
// admin/realtime API and frontend assets on one HTTP listener.
|
||||
//
|
||||
// Runtime-mutable chain registry with mDNS discovery and WebSocket realtime.
|
||||
// Build: go build -o explorer .
|
||||
// Run: explorer --config /etc/explorer/chains.yaml
|
||||
//
|
||||
// Build: GOWORK=off go build -o explorer .
|
||||
// Run: explorer --config chains.yaml
|
||||
// explorer --mdns=false --port 8090
|
||||
// Routes:
|
||||
//
|
||||
// Env:
|
||||
// PORT — HTTP port (default 8090)
|
||||
// EXPLORER_CONFIG — path to chains.yaml
|
||||
// EXPLORER_MDNS — enable mDNS discovery (default true)
|
||||
// CHAINS — inline chain list: "cchain:http://...,zoo:http://..."
|
||||
//
|
||||
// Admin API:
|
||||
// GET /v1/explorer/admin/chains — list chains
|
||||
// POST /v1/explorer/admin/chains — add chain
|
||||
// PUT /v1/explorer/admin/chains/{slug} — update chain
|
||||
// DELETE /v1/explorer/admin/chains/{slug} — remove chain
|
||||
//
|
||||
// Realtime:
|
||||
// WS /v1/explorer/realtime — subscribe to blocks/transactions per chain
|
||||
// / SPA (embedded; SPA-routing fallback)
|
||||
// /envs.js runtime config window.ENV = {...}
|
||||
// /icon.svg, /logo.svg per-host brand assets (disk override)
|
||||
// /health service health
|
||||
// /v1/indexer/* default chain explorer API
|
||||
// /v1/indexer/{slug}/* per-chain explorer API
|
||||
// /v1/explorer/{slug}/* legacy alias for /v1/indexer/{slug}
|
||||
// /v1/graph/{slug}/{subgraph}/graphql per-chain, per-subgraph GraphQL
|
||||
// /v1/explorer/admin/chains[/{slug}] runtime registry CRUD
|
||||
// /v1/explorer/realtime WebSocket realtime hub
|
||||
// /v1/explorer/realtime/stats realtime stats
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -31,102 +30,153 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
port := flag.Int("port", envInt("PORT", 8090), "HTTP port")
|
||||
configFile := flag.String("config", env("EXPLORER_CONFIG", ""), "chains.yaml path")
|
||||
enableMDNS := flag.Bool("mdns", envBool("EXPLORER_MDNS", true), "auto-discover local chains via mDNS")
|
||||
var (
|
||||
httpAddr = flag.String("http", "", "HTTP listen address (default :8090 / $HTTP_ADDR)")
|
||||
dataDir = flag.String("data", "", "Data directory (default $DATA_DIR or ~/.explorer/data)")
|
||||
configPath = flag.String("config", "", "Path to chains.yaml ($EXPLORER_CONFIG)")
|
||||
enableMDNS = flag.Bool("mdns", envBool("EXPLORER_MDNS", false), "Auto-discover chains via mDNS")
|
||||
showVer = flag.Bool("version", false, "Show version and exit")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *showVer {
|
||||
fmt.Printf("explorer %s (%s)\n", version, fingerprint())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *httpAddr == "" {
|
||||
*httpAddr = env("HTTP_ADDR", ":8090")
|
||||
if p := os.Getenv("PORT"); p != "" && *httpAddr == ":8090" {
|
||||
*httpAddr = ":" + p
|
||||
}
|
||||
}
|
||||
if *dataDir == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
*dataDir = env("DATA_DIR", filepath.Join(home, ".explorer", "data"))
|
||||
}
|
||||
if *configPath == "" {
|
||||
*configPath = env("EXPLORER_CONFIG", "")
|
||||
}
|
||||
if *configPath == "" {
|
||||
*configPath = findConfig(*dataDir)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if *configPath != "" {
|
||||
c, err := LoadConfig(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("[explorer] config: %v", err)
|
||||
}
|
||||
cfg = c
|
||||
}
|
||||
if cfg.DataDir == "" {
|
||||
cfg.DataDir = *dataDir
|
||||
}
|
||||
if cfg.HTTPAddr == "" {
|
||||
cfg.HTTPAddr = *httpAddr
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigCh
|
||||
log.Println("[explorer] shutdown signal received")
|
||||
cancel()
|
||||
}()
|
||||
go func() { <-sigCh; log.Println("[explorer] shutdown"); cancel() }()
|
||||
|
||||
// Chain registry (runtime mutable)
|
||||
registry := NewChainRegistry()
|
||||
supervisor := NewChainSupervisor(cfg.DataDir)
|
||||
registry.AttachSupervisor(supervisor)
|
||||
|
||||
// Load from config file
|
||||
if *configFile != "" {
|
||||
if err := registry.LoadFromFile(*configFile); err != nil {
|
||||
log.Fatalf("[explorer] config: %v", err)
|
||||
for _, c := range cfg.Chains {
|
||||
c.Source = "config"
|
||||
if err := registry.Add(c); err != nil {
|
||||
log.Printf("[explorer] skip %s from config: %v", c.Slug, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Load from env
|
||||
if chains := env("CHAINS", ""); chains != "" {
|
||||
if chains := os.Getenv("CHAINS"); chains != "" {
|
||||
registry.LoadFromEnv(chains)
|
||||
}
|
||||
|
||||
// Start mDNS discovery
|
||||
if *enableMDNS {
|
||||
go registry.StartMDNSDiscovery()
|
||||
}
|
||||
|
||||
// Start realtime hub
|
||||
go registry.hub.Run(ctx)
|
||||
go supervisor.Wait(ctx)
|
||||
|
||||
frontend, err := NewFrontend(cfg, registry)
|
||||
if err != nil {
|
||||
log.Fatalf("[explorer] frontend: %v", err)
|
||||
}
|
||||
|
||||
// HTTP mux
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Health
|
||||
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"status": "ok",
|
||||
"chains": registry.Count(),
|
||||
"status": "ok",
|
||||
"version": version,
|
||||
"chains": registry.Count(),
|
||||
})
|
||||
})
|
||||
|
||||
// Admin API
|
||||
mux.HandleFunc("GET /v1/explorer/admin/chains", registry.HandleList)
|
||||
mux.HandleFunc("POST /v1/explorer/admin/chains", registry.HandleAdd)
|
||||
mux.HandleFunc("PUT /v1/explorer/admin/chains/{slug}", registry.HandleUpdate)
|
||||
mux.HandleFunc("DELETE /v1/explorer/admin/chains/{slug}", registry.HandleRemove)
|
||||
|
||||
// Realtime WebSocket
|
||||
mux.HandleFunc("/v1/explorer/realtime", registry.hub.HandleRealtime)
|
||||
|
||||
// Realtime stats
|
||||
mux.HandleFunc("GET /v1/explorer/realtime/stats", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(registry.hub.Stats())
|
||||
})
|
||||
|
||||
log.Printf("[explorer] listening :%d (%d chains, mdns=%v)", *port, registry.Count(), *enableMDNS)
|
||||
supervisor.MountRoutes(mux)
|
||||
frontend.Mount(mux)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", *port),
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
MaxHeaderBytes: 1 << 16,
|
||||
}
|
||||
log.Printf("[explorer] %s listening %s data=%s chains=%d mdns=%v %s",
|
||||
version, cfg.HTTPAddr, cfg.DataDir, registry.Count(), *enableMDNS, fingerprint())
|
||||
|
||||
server := &http.Server{Addr: cfg.HTTPAddr, Handler: withSecurity(mux)}
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("[explorer] server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer shutdownCancel()
|
||||
srv.Shutdown(shutdownCtx)
|
||||
shutCtx, shutCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer shutCancel()
|
||||
server.Shutdown(shutCtx)
|
||||
log.Println("[explorer] stopped")
|
||||
}
|
||||
|
||||
// withSecurity adds the baseline security headers and CORS preflight handler.
|
||||
func withSecurity(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func env(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
@@ -134,23 +184,26 @@ func env(key, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
func envInt(key string, fallback int) int {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
var n int
|
||||
fmt.Sscanf(v, "%d", &n)
|
||||
return n
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func envBool(key string, fallback bool) bool {
|
||||
v := os.Getenv(key)
|
||||
switch v {
|
||||
switch strings.ToLower(os.Getenv(key)) {
|
||||
case "true", "1", "yes":
|
||||
return true
|
||||
case "false", "0", "no":
|
||||
return false
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// findConfig probes for a chains.yaml in the standard locations.
|
||||
func findConfig(dataDir string) string {
|
||||
for _, p := range []string{
|
||||
filepath.Join(dataDir, "chains.yaml"),
|
||||
"chains.yaml",
|
||||
"/etc/explorer/chains.yaml",
|
||||
} {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Patch Go embed directive to include _next prefixed files
|
||||
sed -i 's|//go:embed static/\*|//go:embed all:static|' /src/cmd/explorer/frontend.go
|
||||
+36
-44
@@ -6,25 +6,14 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ChainConfig defines a single chain to index.
|
||||
type ChainConfig struct {
|
||||
Slug string `json:"slug" yaml:"slug"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
ChainID int64 `json:"chain_id" yaml:"chain_id"`
|
||||
RPC string `json:"rpc" yaml:"rpc"`
|
||||
Type string `json:"type" yaml:"type"` // evm, dag, pchain, xchain
|
||||
Default bool `json:"default" yaml:"default"`
|
||||
Source string `json:"source" yaml:"-"` // config, env, mdns, admin
|
||||
}
|
||||
|
||||
// ChainsFile is the top-level YAML structure for chains.yaml.
|
||||
// ChainsFile is the legacy `chains: [...]` YAML shape used by tests and the
|
||||
// minimal config. The full Config struct in config.go is the canonical schema.
|
||||
type ChainsFile struct {
|
||||
Chains []ChainConfig `yaml:"chains"`
|
||||
}
|
||||
@@ -34,14 +23,12 @@ type ChainEntry struct {
|
||||
Config ChainConfig
|
||||
}
|
||||
|
||||
// slugPattern restricts slugs to lowercase alphanumeric + hyphen.
|
||||
var slugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,63}$`)
|
||||
|
||||
// ChainRegistry is a thread-safe, runtime-mutable chain store.
|
||||
type ChainRegistry struct {
|
||||
mu sync.RWMutex
|
||||
chains map[string]*ChainEntry // slug -> entry
|
||||
chains map[string]*ChainEntry
|
||||
hub *RealtimeHub
|
||||
sup *ChainSupervisor // nil = registry-only mode (tests)
|
||||
}
|
||||
|
||||
// NewChainRegistry creates an empty registry.
|
||||
@@ -52,7 +39,12 @@ func NewChainRegistry() *ChainRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
// Add registers a chain. Returns error if slug is invalid or already exists from a higher-priority source.
|
||||
// AttachSupervisor wires a ChainSupervisor so admin/config/mdns adds also
|
||||
// spawn per-chain indexer + graph goroutines, and removes cancel them.
|
||||
func (r *ChainRegistry) AttachSupervisor(s *ChainSupervisor) { r.sup = s }
|
||||
|
||||
// Add registers a chain. mDNS-sourced chains never override higher-priority
|
||||
// (config/env/admin) entries; admin/config/env duplicates return an error.
|
||||
func (r *ChainRegistry) Add(cfg ChainConfig) error {
|
||||
if !slugPattern.MatchString(cfg.Slug) {
|
||||
return fmt.Errorf("invalid slug %q", cfg.Slug)
|
||||
@@ -65,50 +57,54 @@ func (r *ChainRegistry) Add(cfg ChainConfig) error {
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if existing, ok := r.chains[cfg.Slug]; ok {
|
||||
// mDNS never overrides manual config
|
||||
if cfg.Source == "mdns" && existing.Config.Source != "mdns" {
|
||||
r.mu.Unlock()
|
||||
return fmt.Errorf("chain %q already configured via %s", cfg.Slug, existing.Config.Source)
|
||||
}
|
||||
// All other sources: reject duplicate (use Update to modify)
|
||||
if cfg.Source != "mdns" {
|
||||
r.mu.Unlock()
|
||||
return fmt.Errorf("chain %q already exists", cfg.Slug)
|
||||
}
|
||||
// mDNS re-discovery: update last-seen silently
|
||||
existing.Config.RPC = cfg.RPC
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
r.chains[cfg.Slug] = &ChainEntry{Config: cfg}
|
||||
r.mu.Unlock()
|
||||
log.Printf("[registry] added chain %s (%s) source=%s", cfg.Slug, cfg.RPC, cfg.Source)
|
||||
|
||||
if r.sup != nil {
|
||||
r.sup.start(cfg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove deletes a chain by slug.
|
||||
func (r *ChainRegistry) Remove(slug string) bool {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if _, ok := r.chains[slug]; !ok {
|
||||
r.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
delete(r.chains, slug)
|
||||
r.mu.Unlock()
|
||||
log.Printf("[registry] removed chain %s", slug)
|
||||
|
||||
if r.sup != nil {
|
||||
r.sup.stop(slug)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Update modifies an existing chain's config.
|
||||
func (r *ChainRegistry) Update(slug string, cfg ChainConfig) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
entry, ok := r.chains[slug]
|
||||
if !ok {
|
||||
r.mu.Unlock()
|
||||
return fmt.Errorf("chain %q not found", slug)
|
||||
}
|
||||
|
||||
if cfg.Name != "" {
|
||||
entry.Config.Name = cfg.Name
|
||||
}
|
||||
@@ -123,8 +119,15 @@ func (r *ChainRegistry) Update(slug string, cfg ChainConfig) error {
|
||||
}
|
||||
entry.Config.Default = cfg.Default
|
||||
entry.Config.Source = "admin"
|
||||
updated := entry.Config
|
||||
r.mu.Unlock()
|
||||
|
||||
log.Printf("[registry] updated chain %s", slug)
|
||||
|
||||
if r.sup != nil {
|
||||
r.sup.stop(slug)
|
||||
r.sup.start(updated)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -132,7 +135,6 @@ func (r *ChainRegistry) Update(slug string, cfg ChainConfig) error {
|
||||
func (r *ChainRegistry) Get(slug string) (ChainConfig, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
entry, ok := r.chains[slug]
|
||||
if !ok {
|
||||
return ChainConfig{}, false
|
||||
@@ -144,7 +146,6 @@ func (r *ChainRegistry) Get(slug string) (ChainConfig, bool) {
|
||||
func (r *ChainRegistry) List() []ChainConfig {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
out := make([]ChainConfig, 0, len(r.chains))
|
||||
for _, e := range r.chains {
|
||||
out = append(out, e.Config)
|
||||
@@ -159,21 +160,21 @@ func (r *ChainRegistry) Count() int {
|
||||
return len(r.chains)
|
||||
}
|
||||
|
||||
// LoadFromFile loads chains from a YAML file.
|
||||
// LoadFromFile loads chains from a YAML file using the legacy `chains: [...]`
|
||||
// shape. Full Config (with brand, networks, etc.) loads via LoadConfig.
|
||||
func (r *ChainRegistry) LoadFromFile(path string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
|
||||
var f ChainsFile
|
||||
if err := yaml.Unmarshal(data, &f); err != nil {
|
||||
return fmt.Errorf("parse %s: %w", path, err)
|
||||
}
|
||||
|
||||
for _, c := range f.Chains {
|
||||
c.Source = "config"
|
||||
c.RPC = os.Expand(c.RPC, os.Getenv)
|
||||
c.WS = os.Expand(c.WS, os.Getenv)
|
||||
if err := r.Add(c); err != nil {
|
||||
log.Printf("[registry] skip %s from config: %v", c.Slug, err)
|
||||
}
|
||||
@@ -207,8 +208,6 @@ func (r *ChainRegistry) LoadFromEnv(val string) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- HTTP Handlers ---
|
||||
|
||||
// HandleList returns all chains as JSON.
|
||||
func (r *ChainRegistry) HandleList(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -218,7 +217,7 @@ func (r *ChainRegistry) HandleList(w http.ResponseWriter, req *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// HandleAdd creates a new chain from JSON body.
|
||||
// HandleAdd creates a new chain from a JSON body.
|
||||
func (r *ChainRegistry) HandleAdd(w http.ResponseWriter, req *http.Request) {
|
||||
var cfg ChainConfig
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, req.Body, 4096)).Decode(&cfg); err != nil {
|
||||
@@ -226,14 +225,12 @@ func (r *ChainRegistry) HandleAdd(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
cfg.Source = "admin"
|
||||
|
||||
if err := r.Add(cfg); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(cfg)
|
||||
@@ -246,20 +243,17 @@ func (r *ChainRegistry) HandleUpdate(w http.ResponseWriter, req *http.Request) {
|
||||
http.Error(w, `{"error":"slug required"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var cfg ChainConfig
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, req.Body, 4096)).Decode(&cfg); err != nil {
|
||||
http.Error(w, `{"error":"invalid json"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.Update(slug, cfg); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updated, _ := r.Get(slug)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(updated)
|
||||
@@ -272,14 +266,12 @@ func (r *ChainRegistry) HandleRemove(w http.ResponseWriter, req *http.Request) {
|
||||
http.Error(w, `{"error":"slug required"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !r.Remove(slug) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "not found"})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "removed", "slug": slug})
|
||||
}
|
||||
|
||||
@@ -308,9 +308,6 @@ func TestEnvHelpers(t *testing.T) {
|
||||
if env("NONEXISTENT_KEY_12345", "fallback") != "fallback" {
|
||||
t.Fatal("env fallback failed")
|
||||
}
|
||||
if envInt("NONEXISTENT_KEY_12345", 42) != 42 {
|
||||
t.Fatal("envInt fallback failed")
|
||||
}
|
||||
if envBool("NONEXISTENT_KEY_12345", true) != true {
|
||||
t.Fatal("envBool fallback failed")
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#0b1220"/><path d="M16 44 L32 16 L48 44 Z" fill="#5cf"/></svg>
|
||||
|
After Width: | Height: | Size: 166 B |
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Explorer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<script src="/envs.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">Loading explorer…</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 64"><rect width="200" height="64" fill="#0b1220"/><text x="20" y="42" fill="#5cf" font-family="ui-sans-serif,system-ui,sans-serif" font-weight="700" font-size="28">Explorer</text></svg>
|
||||
|
After Width: | Height: | Size: 243 B |
+423
@@ -0,0 +1,423 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/luxfi/graph/engine"
|
||||
graphidx "github.com/luxfi/graph/indexer"
|
||||
graphstor "github.com/luxfi/graph/storage"
|
||||
|
||||
"github.com/luxfi/indexer/evm"
|
||||
"github.com/luxfi/indexer/explorer"
|
||||
idxstor "github.com/luxfi/indexer/storage"
|
||||
)
|
||||
|
||||
// ChainSupervisor owns the per-chain indexer + graph goroutines plus the
|
||||
// route dispatcher that maps /v1/indexer/{slug}/* and /v1/graph/{slug}/*
|
||||
// requests to the correct handler. Routes are mutable at runtime: adding a
|
||||
// chain spawns its workers, removing a chain cancels them.
|
||||
type ChainSupervisor struct {
|
||||
dataDir string
|
||||
mu sync.Mutex
|
||||
state map[string]*chainState
|
||||
|
||||
indexerRoutes sync.Map // map[slug]http.Handler
|
||||
graphRoutes sync.Map // map[slug + "/" + subgraph]http.Handler
|
||||
defaultSlug string // serves /v1/indexer/* (no slug prefix)
|
||||
}
|
||||
|
||||
type chainState struct {
|
||||
cfg ChainConfig
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewChainSupervisor creates a supervisor rooted at dataDir.
|
||||
func NewChainSupervisor(dataDir string) *ChainSupervisor {
|
||||
return &ChainSupervisor{
|
||||
dataDir: dataDir,
|
||||
state: make(map[string]*chainState),
|
||||
}
|
||||
}
|
||||
|
||||
// MountRoutes installs the /v1/indexer and /v1/graph dispatchers on mux. The
|
||||
// dispatchers look up per-chain handlers from the supervisor's route maps so
|
||||
// chains added at runtime become reachable without re-mounting.
|
||||
func (s *ChainSupervisor) MountRoutes(mux *http.ServeMux) {
|
||||
mux.Handle("/v1/indexer/", http.HandlerFunc(s.dispatchIndexer))
|
||||
mux.Handle("/v1/explorer/", http.HandlerFunc(s.dispatchExplorerLegacy))
|
||||
mux.Handle("/v1/graph/", http.HandlerFunc(s.dispatchGraph))
|
||||
}
|
||||
|
||||
// SetDefaultSlug picks which chain serves the unprefixed /v1/indexer/* route.
|
||||
func (s *ChainSupervisor) SetDefaultSlug(slug string) {
|
||||
s.mu.Lock()
|
||||
s.defaultSlug = slug
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// start spawns workers for cfg. Idempotent: starting an already-running slug
|
||||
// is a no-op.
|
||||
func (s *ChainSupervisor) start(cfg ChainConfig) {
|
||||
s.mu.Lock()
|
||||
if _, ok := s.state[cfg.Slug]; ok {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
st := &chainState{cfg: cfg, cancel: cancel}
|
||||
s.state[cfg.Slug] = st
|
||||
if cfg.Default && s.defaultSlug == "" {
|
||||
s.defaultSlug = cfg.Slug
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
st.wg.Add(1)
|
||||
go func() {
|
||||
defer st.wg.Done()
|
||||
s.runChain(ctx, cfg)
|
||||
}()
|
||||
|
||||
if cfg.Graph.Enabled {
|
||||
for _, sg := range cfg.Graph.Subgraphs {
|
||||
if !sg.Enabled {
|
||||
continue
|
||||
}
|
||||
sg := sg
|
||||
st.wg.Add(1)
|
||||
go func() {
|
||||
defer st.wg.Done()
|
||||
s.runSubgraph(ctx, cfg, sg)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stop cancels and waits for all workers belonging to slug.
|
||||
func (s *ChainSupervisor) stop(slug string) {
|
||||
s.mu.Lock()
|
||||
st, ok := s.state[slug]
|
||||
if !ok {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
delete(s.state, slug)
|
||||
if s.defaultSlug == slug {
|
||||
s.defaultSlug = ""
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
st.cancel()
|
||||
st.wg.Wait()
|
||||
|
||||
s.indexerRoutes.Delete(slug)
|
||||
s.graphRoutes.Range(func(k, _ any) bool {
|
||||
if key, ok := k.(string); ok && strings.HasPrefix(key, slug+"/") {
|
||||
s.graphRoutes.Delete(k)
|
||||
}
|
||||
return true
|
||||
})
|
||||
log.Printf("[supervisor] stopped %s", slug)
|
||||
}
|
||||
|
||||
// Wait blocks until ctx is done, then cancels every chain and waits for
|
||||
// goroutines to drain.
|
||||
func (s *ChainSupervisor) Wait(ctx context.Context) {
|
||||
<-ctx.Done()
|
||||
s.mu.Lock()
|
||||
slugs := make([]string, 0, len(s.state))
|
||||
for k := range s.state {
|
||||
slugs = append(slugs, k)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
for _, slug := range slugs {
|
||||
s.stop(slug)
|
||||
}
|
||||
}
|
||||
|
||||
// runChain runs one chain's indexer to completion or context cancel and then
|
||||
// mounts the standalone explorer API once the SQLite schema is ready.
|
||||
func (s *ChainSupervisor) runChain(ctx context.Context, cfg ChainConfig) {
|
||||
chainDir := filepath.Join(s.dataDir, cfg.Slug)
|
||||
if err := os.MkdirAll(chainDir, 0755); err != nil {
|
||||
log.Printf("[%s] mkdir: %v", cfg.Slug, err)
|
||||
return
|
||||
}
|
||||
store, err := idxstor.NewUnified(idxstor.DefaultUnifiedConfig(chainDir))
|
||||
if err != nil {
|
||||
log.Printf("[%s] storage: %v", cfg.Slug, err)
|
||||
return
|
||||
}
|
||||
defer store.Close()
|
||||
if err := store.Init(ctx); err != nil {
|
||||
log.Printf("[%s] storage init: %v", cfg.Slug, err)
|
||||
return
|
||||
}
|
||||
dbPath := filepath.Join(chainDir, "query", "indexer.db")
|
||||
log.Printf("[%s] indexing %s (%s) -> %s", cfg.Slug, cfg.Name, cfg.CoinSymbol, dbPath)
|
||||
|
||||
go s.mountIndexerAPI(ctx, cfg, dbPath)
|
||||
|
||||
switch cfg.Type {
|
||||
case "", "evm":
|
||||
poll := 30 * time.Second
|
||||
if cfg.Indexer.PollInterval != "" {
|
||||
if d, err := time.ParseDuration(cfg.Indexer.PollInterval); err == nil {
|
||||
poll = d
|
||||
}
|
||||
}
|
||||
idx, err := evm.NewIndexer(evm.Config{
|
||||
ChainName: cfg.Name,
|
||||
ChainID: cfg.ChainID,
|
||||
RPCEndpoint: cfg.RPC,
|
||||
HTTPPort: 0,
|
||||
PollInterval: poll,
|
||||
}, store)
|
||||
if err != nil {
|
||||
log.Printf("[%s] evm indexer: %v", cfg.Slug, err)
|
||||
return
|
||||
}
|
||||
if err := idx.Init(ctx); err != nil {
|
||||
log.Printf("[%s] evm init: %v", cfg.Slug, err)
|
||||
return
|
||||
}
|
||||
if err := idx.Run(ctx); err != nil && ctx.Err() == nil {
|
||||
log.Printf("[%s] evm run: %v", cfg.Slug, err)
|
||||
}
|
||||
default:
|
||||
log.Printf("[%s] %s indexer: passive (no chain-specific implementation)", cfg.Slug, cfg.Type)
|
||||
<-ctx.Done()
|
||||
}
|
||||
}
|
||||
|
||||
// mountIndexerAPI waits for the per-chain SQLite schema to settle, then
|
||||
// publishes the standalone explorer API in the indexer/explorer route maps.
|
||||
func (s *ChainSupervisor) mountIndexerAPI(ctx context.Context, cfg ChainConfig, dbPath string) {
|
||||
apiPrefix := "/v1/indexer/" + cfg.Slug
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
if _, err := os.Stat(dbPath); err != nil {
|
||||
if time.Now().After(deadline) {
|
||||
log.Printf("[%s] api waiting for %s: %v", cfg.Slug, dbPath, err)
|
||||
deadline = time.Now().Add(5 * time.Minute)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if cfg.Type == "" || cfg.Type == "evm" {
|
||||
db, err := sql.Open("sqlite3", "file:"+dbPath+"?mode=ro")
|
||||
if err == nil {
|
||||
var n int
|
||||
_ = db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='evm_blocks'").Scan(&n)
|
||||
db.Close()
|
||||
if n == 0 {
|
||||
if time.Now().After(deadline) {
|
||||
log.Printf("[%s] api waiting for evm_blocks", cfg.Slug)
|
||||
deadline = time.Now().Add(5 * time.Minute)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
srv, err := explorer.NewStandaloneServer(explorer.Config{
|
||||
IndexerDBPath: dbPath,
|
||||
ChainID: cfg.ChainID,
|
||||
ChainName: cfg.Name,
|
||||
CoinSymbol: cfg.CoinSymbol,
|
||||
APIPrefix: apiPrefix,
|
||||
})
|
||||
if err != nil {
|
||||
if time.Now().After(deadline) {
|
||||
log.Printf("[%s] api: %v", cfg.Slug, err)
|
||||
deadline = time.Now().Add(5 * time.Minute)
|
||||
}
|
||||
continue
|
||||
}
|
||||
s.indexerRoutes.Store(cfg.Slug, srv.Handler())
|
||||
log.Printf("[%s] api mounted at %s/*", cfg.Slug, apiPrefix)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// runSubgraph spawns a graph indexer + engine for a single subgraph and
|
||||
// publishes its handler at /v1/graph/{chain-slug}/{subgraph-name}/.
|
||||
func (s *ChainSupervisor) runSubgraph(ctx context.Context, cfg ChainConfig, sg Subgraph) {
|
||||
dir := filepath.Join(s.dataDir, cfg.Slug, "graph", sg.Name)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
log.Printf("[%s/%s] mkdir: %v", cfg.Slug, sg.Name, err)
|
||||
return
|
||||
}
|
||||
store, err := graphstor.New(dir)
|
||||
if err != nil {
|
||||
log.Printf("[%s/%s] storage: %v", cfg.Slug, sg.Name, err)
|
||||
return
|
||||
}
|
||||
defer store.Close()
|
||||
if err := store.Init(ctx); err != nil {
|
||||
log.Printf("[%s/%s] storage init: %v", cfg.Slug, sg.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
idx := graphidx.New(cfg.RPC, store)
|
||||
go func() {
|
||||
if err := idx.Run(ctx); err != nil && ctx.Err() == nil {
|
||||
log.Printf("[%s/%s] indexer: %v", cfg.Slug, sg.Name, err)
|
||||
}
|
||||
}()
|
||||
|
||||
eng := engine.New(store, &engine.Config{
|
||||
MaxQueryDepth: 10,
|
||||
MaxResultSize: 1 << 20,
|
||||
QueryTimeoutMs: 30000,
|
||||
})
|
||||
schema := sg.Schema
|
||||
if schema == "" {
|
||||
schema = "amm"
|
||||
}
|
||||
if err := eng.LoadBuiltin(schema); err != nil {
|
||||
log.Printf("[%s/%s] schema %q: %v", cfg.Slug, sg.Name, schema, err)
|
||||
return
|
||||
}
|
||||
|
||||
prefix := fmt.Sprintf("/v1/graph/%s/%s", cfg.Slug, sg.Name)
|
||||
mux := http.NewServeMux()
|
||||
gqlHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 10<<20)
|
||||
var req engine.Request
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, `{"errors":[{"message":"invalid JSON"}]}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
resp := eng.Execute(r.Context(), &req)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
statusHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
st := idx.Status()
|
||||
fmt.Fprintf(w, `{"status":"ok","block":%d,"indexed":%d}`, st.LatestBlock, st.IndexedEvents)
|
||||
}
|
||||
mux.HandleFunc("POST "+prefix+"/graphql", gqlHandler)
|
||||
mux.HandleFunc("GET "+prefix+"/graphql", eng.HandleGraphiQL)
|
||||
mux.HandleFunc("POST "+prefix+"/ql", gqlHandler)
|
||||
mux.HandleFunc("GET "+prefix+"/ql", eng.HandleGraphiQL)
|
||||
mux.HandleFunc("GET "+prefix+"/health", statusHandler)
|
||||
|
||||
s.graphRoutes.Store(cfg.Slug+"/"+sg.Name, http.Handler(mux))
|
||||
log.Printf("[%s/%s] graph mounted at %s/{graphql,ql,health} (schema=%s)",
|
||||
cfg.Slug, sg.Name, prefix, schema)
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
// dispatchIndexer routes /v1/indexer/{slug}/* and /v1/indexer/* (default
|
||||
// chain) to the per-chain handler installed by mountIndexerAPI.
|
||||
func (s *ChainSupervisor) dispatchIndexer(w http.ResponseWriter, r *http.Request) {
|
||||
slug, rest, ok := splitSlug(r.URL.Path, "/v1/indexer/")
|
||||
if !ok {
|
||||
s.serveDefault(w, r, "")
|
||||
return
|
||||
}
|
||||
if h, found := s.indexerRoutes.Load(slug); found {
|
||||
h.(http.Handler).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
s.serveDefault(w, r, rest)
|
||||
}
|
||||
|
||||
// serveDefault forwards an unprefixed /v1/indexer/* request to the default
|
||||
// chain. The standalone server is mounted under its slug, so we rewrite the
|
||||
// URL to include the slug before delegating.
|
||||
func (s *ChainSupervisor) serveDefault(w http.ResponseWriter, r *http.Request, rest string) {
|
||||
s.mu.Lock()
|
||||
def := s.defaultSlug
|
||||
s.mu.Unlock()
|
||||
if def == "" {
|
||||
http.Error(w, `{"error":"no default chain"}`, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
h, ok := s.indexerRoutes.Load(def)
|
||||
if !ok {
|
||||
http.Error(w, `{"error":"chain not ready"}`, http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
r2 := r.Clone(r.Context())
|
||||
r2.URL.Path = "/v1/indexer/" + def + "/" + strings.TrimPrefix(rest, "/")
|
||||
h.(http.Handler).ServeHTTP(w, r2)
|
||||
}
|
||||
|
||||
// dispatchExplorerLegacy preserves the older /v1/explorer/{slug}/* prefix
|
||||
// for clients that haven't migrated to /v1/indexer/.
|
||||
func (s *ChainSupervisor) dispatchExplorerLegacy(w http.ResponseWriter, r *http.Request) {
|
||||
slug, rest, ok := splitSlug(r.URL.Path, "/v1/explorer/")
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
h, found := s.indexerRoutes.Load(slug)
|
||||
if !found {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
r2 := r.Clone(r.Context())
|
||||
r2.URL.Path = "/v1/indexer/" + slug + "/" + strings.TrimPrefix(rest, "/")
|
||||
h.(http.Handler).ServeHTTP(w, r2)
|
||||
}
|
||||
|
||||
// dispatchGraph routes /v1/graph/{slug}/{subgraph}/* to the matching engine.
|
||||
func (s *ChainSupervisor) dispatchGraph(w http.ResponseWriter, r *http.Request) {
|
||||
rest := strings.TrimPrefix(r.URL.Path, "/v1/graph/")
|
||||
if rest == r.URL.Path {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
parts := strings.SplitN(rest, "/", 3)
|
||||
if len(parts) < 2 {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
key := parts[0] + "/" + parts[1]
|
||||
h, ok := s.graphRoutes.Load(key)
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
h.(http.Handler).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// splitSlug returns the chain slug and the remainder of the path after a
|
||||
// known prefix (e.g. "/v1/indexer/"). Returns ok=false if the path does not
|
||||
// match the prefix or contains no slug.
|
||||
func splitSlug(path, prefix string) (slug, rest string, ok bool) {
|
||||
if !strings.HasPrefix(path, prefix) {
|
||||
return "", "", false
|
||||
}
|
||||
tail := strings.TrimPrefix(path, prefix)
|
||||
if tail == "" {
|
||||
return "", "", false
|
||||
}
|
||||
parts := strings.SplitN(tail, "/", 2)
|
||||
if !slugPattern.MatchString(parts[0]) {
|
||||
return "", "", false
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return parts[0], "", true
|
||||
}
|
||||
return parts[0], parts[1], true
|
||||
}
|
||||
Reference in New Issue
Block a user