feat: add Graph Node + IPFS + subgraph infrastructure

- Add graph-node, graph-ipfs, graph-postgres to compose.yml (subgraph profile)
- Add k8s manifests for Graph Node stack (lux/graph-node.yaml)
- Add deploy-subgraph.sh for AMM V3/V2 subgraph deployment
- Update deploy.sh with 'graph' target for Graph Node infrastructure
- Matches existing lux-k8s lux-explorer namespace setup
This commit is contained in:
2026-03-07 00:03:44 -08:00
parent 1ce1924c41
commit 97c8a5842d
4 changed files with 527 additions and 0 deletions
+73
View File
@@ -223,6 +223,77 @@ services:
condition: service_healthy
restart: unless-stopped
# =============================================================================
# OPTIONAL: SUBGRAPH (docker compose --profile subgraph up -d)
# Graph Node + IPFS + dedicated Postgres for subgraph indexing
# =============================================================================
graph-postgres:
image: postgres:16-alpine
container_name: bootnode-graph-postgres
profiles: ["subgraph"]
environment:
POSTGRES_USER: graph
POSTGRES_PASSWORD: graph-bootnode-2026
POSTGRES_DB: graph_node
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C"
ports:
- "15432:5432"
volumes:
- graph-postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U graph"]
interval: 5s
timeout: 5s
retries: 5
graph-ipfs:
image: ipfs/kubo:v0.24.0
container_name: bootnode-graph-ipfs
profiles: ["subgraph"]
ports:
- "15001:5001"
- "18080:8080"
volumes:
- graph-ipfs:/data/ipfs
environment:
IPFS_PROFILE: server
healthcheck:
test: ["CMD-SHELL", "ipfs dag stat /ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn || exit 1"]
interval: 10s
timeout: 5s
retries: 5
graph-node:
image: graphprotocol/graph-node:v0.35.1
container_name: bootnode-graph-node
profiles: ["subgraph"]
environment:
postgres_host: graph-postgres
postgres_port: 5432
postgres_user: graph
postgres_pass: graph-bootnode-2026
postgres_db: graph_node
ipfs: graph-ipfs:5001
ethereum: "lux:${LUX_RPC_URL:-http://host.docker.internal:9630/ext/bc/C/rpc}"
GRAPH_LOG: info
ETHEREUM_REORG_THRESHOLD: 1
ETHEREUM_ANCESTOR_COUNT: 1
node_id: graph_node
ports:
- "18000:8000" # GraphQL queries
- "18001:8001" # GraphQL websocket
- "18020:8020" # Admin (deploy/create subgraphs)
- "18030:8030" # Status
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
graph-postgres:
condition: service_healthy
graph-ipfs:
condition: service_healthy
restart: unless-stopped
volumes:
postgres:
redis:
@@ -230,3 +301,5 @@ volumes:
qdrant:
meilisearch:
grafana:
graph-postgres:
graph-ipfs:
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Deploy or redeploy a subgraph to the Lux graph-node.
#
# Usage:
# ./deploy-subgraph.sh # Deploy all subgraphs
# ./deploy-subgraph.sh amm-v3 # Deploy AMM V3 only
# ./deploy-subgraph.sh amm-v3 --wipe # Wipe and redeploy AMM V3
#
# Prerequisites:
# - graph-node running (k8s or local compose)
# - Port-forwards active or local compose ports
# - npx graph-cli available
#
# Environment variables:
# GRAPH_NODE_URL - Graph admin endpoint (default: http://localhost:18020)
# GRAPH_IPFS_URL - IPFS endpoint (default: http://localhost:15001)
# EXCHANGE_DIR - Exchange repo root (default: ~/work/lux/exchange)
set -euo pipefail
GRAPH_NODE_URL="${GRAPH_NODE_URL:-http://localhost:18020}"
GRAPH_IPFS_URL="${GRAPH_IPFS_URL:-http://localhost:15001}"
EXCHANGE_DIR="${EXCHANGE_DIR:-$HOME/work/lux/exchange}"
SUBGRAPH="${1:-all}"
WIPE="${2:-}"
deploy_subgraph() {
local name="$1"
local dir="$2"
local version="$3"
echo "=== Deploying $name (v$version) ==="
if [ "$WIPE" = "--wipe" ]; then
echo "Removing existing deployment..."
curl -sf -X POST "$GRAPH_NODE_URL" \
-H 'Content-Type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"subgraph_remove\",\"params\":{\"name\":\"luxfi/$name\"},\"id\":1}" \
2>/dev/null || true
fi
# Create subgraph name (idempotent)
curl -sf -X POST "$GRAPH_NODE_URL" \
-H 'Content-Type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"subgraph_create\",\"params\":{\"name\":\"luxfi/$name\"},\"id\":1}" \
2>/dev/null || true
# Build and deploy
cd "$dir"
npx graph codegen 2>&1 | tail -2
npx graph build 2>&1 | tail -2
npx graph deploy "luxfi/$name" \
--node "$GRAPH_NODE_URL" \
--ipfs "$GRAPH_IPFS_URL" \
--version-label "v$version" 2>&1 | tail -5
echo "Deployed luxfi/$name v$version"
echo ""
}
check_sync() {
local name="$1"
local query_url="${GRAPH_NODE_URL/18020/18000}"
# Replace admin port with query port
query_url="${query_url/8020/8000}"
echo "Checking sync status for luxfi/$name..."
curl -sf "$query_url/subgraphs/name/luxfi/$name" \
-H 'Content-Type: application/json' \
-d '{"query":"{ _meta { block { number } hasIndexingErrors } }"}' 2>/dev/null \
| python3 -m json.tool 2>/dev/null || echo "(not yet syncing)"
}
case "$SUBGRAPH" in
amm-v3)
deploy_subgraph "amm-v3" "$EXCHANGE_DIR/subgraphs/dex-v3" "1.8.0"
check_sync "amm-v3"
;;
amm-v2)
if [ -d "$EXCHANGE_DIR/subgraphs/dex-factory" ]; then
deploy_subgraph "amm-v2" "$EXCHANGE_DIR/subgraphs/dex-factory" "1.0.0"
check_sync "amm-v2"
else
echo "AMM V2 subgraph not found at $EXCHANGE_DIR/subgraphs/dex-factory"
fi
;;
status)
check_sync "amm-v3"
check_sync "amm-v2"
;;
all)
deploy_subgraph "amm-v3" "$EXCHANGE_DIR/subgraphs/dex-v3" "1.8.0"
if [ -d "$EXCHANGE_DIR/subgraphs/dex-factory" ]; then
deploy_subgraph "amm-v2" "$EXCHANGE_DIR/subgraphs/dex-factory" "1.0.0"
fi
echo "=== Sync Status ==="
check_sync "amm-v3"
;;
*)
echo "Usage: $0 {amm-v3|amm-v2|all|status} [--wipe]"
exit 1
;;
esac
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# Deploy bootnode-operator to lux-k8s cluster.
#
# Prerequisites:
# - kubectl configured for lux-k8s cluster
# - CRD YAMLs and RBAC applied
#
# Usage:
# ./deploy.sh # Apply all manifests
# ./deploy.sh crds # Apply CRDs only
# ./deploy.sh operator # Apply operator only
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
apply_crds() {
echo "=== Applying CRDs ==="
kubectl apply -f "$SCRIPT_DIR/crds.yaml"
echo "Waiting for CRDs to be established..."
kubectl wait --for=condition=Established crd/clusters.bootnode.dev --timeout=30s
kubectl wait --for=condition=Established crd/chaindeployments.bootnode.dev --timeout=30s
}
apply_rbac() {
echo "=== Applying RBAC ==="
kubectl apply -f "$SCRIPT_DIR/rbac.yaml"
}
apply_operator() {
echo "=== Deploying operator ==="
kubectl apply -f "$SCRIPT_DIR/operator-deployment.yaml"
echo "Waiting for operator to be ready..."
kubectl rollout status deployment/bootnode-operator -n bootnode-system --timeout=120s
}
apply_graph() {
local ns="${GRAPH_NAMESPACE:-lux-explorer}"
echo "=== Deploying Graph Node infrastructure to $ns ==="
kubectl apply -f "$SCRIPT_DIR/graph-node.yaml" -n "$ns"
echo "Waiting for graph-postgres..."
kubectl rollout status statefulset/graph-postgres -n "$ns" --timeout=120s
echo "Waiting for graph-ipfs..."
kubectl rollout status deployment/graph-ipfs -n "$ns" --timeout=120s
echo "Waiting for graph-node..."
kubectl rollout status statefulset/graph-node -n "$ns" --timeout=120s
echo "Graph Node ready. Deploy subgraphs with: ./deploy-subgraph.sh"
}
case "${1:-all}" in
crds)
apply_crds
;;
rbac)
apply_rbac
;;
operator)
apply_operator
;;
graph)
apply_graph
;;
all)
apply_crds
apply_rbac
apply_operator
echo "=== Done ==="
kubectl get pods -n bootnode-system
;;
*)
echo "Usage: $0 {crds|rbac|operator|graph|all}"
exit 1
;;
esac
+278
View File
@@ -0,0 +1,278 @@
# Graph Node infrastructure for Lux subgraph indexing
# Includes: PostgreSQL, IPFS, Graph Node
# Deploy: kubectl apply -f graph-node.yaml -n lux-explorer
---
apiVersion: v1
kind: ConfigMap
metadata:
name: graph-node-config
labels:
app: graph-node
component: exchange-backend
data:
config.toml: |
[store]
[store.primary]
connection = "postgresql://graph:graph-lux-2026@graph-postgres:5432/graph_node?sslmode=disable"
weight = 1
pool_size = 10
[deployment]
[[deployment.rule]]
shard = "primary"
indexers = ["graph_node"]
[chains]
ingestor = "graph_node"
[chains.lux]
shard = "primary"
provider = [
{ label = "lux-mainnet", url = "http://luxd-0.luxd-headless.lux-mainnet.svc:9630/ext/bc/C/rpc", features = ["archive"] }
]
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: graph-postgres
labels:
app: graph-postgres
component: exchange-backend
spec:
replicas: 1
serviceName: graph-postgres
selector:
matchLabels:
app: graph-postgres
template:
metadata:
labels:
app: graph-postgres
component: exchange-backend
spec:
containers:
- name: postgres
image: postgres:16-alpine
env:
- name: POSTGRES_USER
value: graph
- name: POSTGRES_PASSWORD
value: graph-lux-2026
- name: POSTGRES_DB
value: graph_node
- name: POSTGRES_INITDB_ARGS
value: "-E UTF8 --locale=C"
ports:
- containerPort: 5432
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "1"
memory: 2Gi
volumeMounts:
- name: graph-postgres-data
mountPath: /var/lib/postgresql/data
livenessProbe:
exec:
command: ["pg_isready", "-U", "graph"]
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
exec:
command: ["pg_isready", "-U", "graph"]
initialDelaySeconds: 5
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: graph-postgres-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: do-block-storage
resources:
requests:
storage: 50Gi
---
apiVersion: v1
kind: Service
metadata:
name: graph-postgres
labels:
app: graph-postgres
spec:
selector:
app: graph-postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: graph-ipfs
labels:
app: graph-ipfs
component: exchange-backend
spec:
replicas: 1
selector:
matchLabels:
app: graph-ipfs
template:
metadata:
labels:
app: graph-ipfs
component: exchange-backend
spec:
containers:
- name: ipfs
image: ipfs/kubo:v0.24.0
env:
- name: IPFS_PROFILE
value: server
ports:
- containerPort: 5001
name: api
- containerPort: 8080
name: gateway
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 1Gi
volumeMounts:
- name: ipfs-data
mountPath: /data/ipfs
volumes:
- name: ipfs-data
persistentVolumeClaim:
claimName: graph-ipfs-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: graph-ipfs-data
labels:
app: graph-ipfs
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: do-block-storage
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: Service
metadata:
name: graph-ipfs
labels:
app: graph-ipfs
spec:
selector:
app: graph-ipfs
ports:
- port: 5001
targetPort: 5001
name: api
- port: 8080
targetPort: 8080
name: gateway
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: graph-node
labels:
app: graph-node
component: exchange-backend
spec:
replicas: 1
serviceName: graph-node
selector:
matchLabels:
app: graph-node
template:
metadata:
labels:
app: graph-node
component: exchange-backend
spec:
containers:
- name: graph-node
image: graphprotocol/graph-node:v0.35.1
env:
- name: GRAPH_NODE_CONFIG
value: /etc/graph-node/config.toml
- name: node_id
value: graph_node
- name: GRAPH_LOG
value: info
- name: ipfs
value: graph-ipfs:5001
- name: ETHEREUM_REORG_THRESHOLD
value: "1"
- name: ETHEREUM_ANCESTOR_COUNT
value: "1"
ports:
- containerPort: 8000
name: graphql
- containerPort: 8001
name: ws
- containerPort: 8020
name: admin
- containerPort: 8030
name: status
- containerPort: 8040
name: metrics
resources:
requests:
cpu: 500m
memory: 2Gi
limits:
cpu: "2"
memory: 4Gi
volumeMounts:
- name: config
mountPath: /etc/graph-node
livenessProbe:
httpGet:
path: /
port: 8030
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 8030
initialDelaySeconds: 10
periodSeconds: 10
volumes:
- name: config
configMap:
name: graph-node-config
---
apiVersion: v1
kind: Service
metadata:
name: graph-node
labels:
app: graph-node
spec:
selector:
app: graph-node
ports:
- port: 8000
targetPort: 8000
name: graphql
- port: 8001
targetPort: 8001
name: ws
- port: 8020
targetPort: 8020
name: admin
- port: 8030
targetPort: 8030
name: status