Files
bridge/k8s/bridge-deployment.yaml
zeekay c817afd5c0 bridge: the operator surface gets its own listener
The swap list answered anonymously with every swap id, the MPC signature
minted for it and the signed destination tx, on the port the Ingress
publishes. /metrics named the release wallets and reported which of them
could not sign. The deposit poll turned an anonymous request into a
source-chain RPC call with caller-chosen arguments.

All three move to a second listener, BRIDGE_ADMIN_ADDR, along with the
swap mutators, which still need BRIDGE_ADMIN_ROUTES on top. The edge
routes a port, so a port is the thing that can be left unrouted: k8s
publishes 8080 and gives the operator surface 8081, and the ServiceMonitor
scrapes there. A swap read by id keeps the swap's progress and drops the
signature and the raw tx; the list on the operator port carries both.

The legacy proxy exposed the same list twice. `/api/swaps/*` matched the
empty remainder and so forwarded GET /api/swaps to the backend's list;
`+` requires a sub-path. `/v1/bridge/swaps/*` forwarded arbitrary
sub-paths into /swaps/… after the prefix strip — the backend mounts its
swap router at /api/swaps, so that path space holds no routes and the
forwarder only ever produced a 404.
2026-09-01 19:39:22 -07:00

320 lines
9.4 KiB
YAML

---
# Lux Bridge — unified Go binary (cmd/bridge).
#
# Replaces the legacy split of:
# - bridge-server (Express + Prisma Node API)
# - bridge-ui (Vite SPA on ghcr.io/hanzoai/spa)
# with a single image (ghcr.io/luxfi/bridge:<tag>) that embeds the SPA
# via go:embed and serves the /v1/bridge/* API natively.
#
# Assumes the lux-bridge namespace + bridge-secrets Secret already exist
# (created by k8s/mpc-deployment.yaml). Required secret keys:
# - bridge-secrets.mpc-api-token bearer token for the MPC daemon
# Optional secret keys (referenced only if you uncomment the env entry):
# - bridge-secrets.bchain-rpc-url BridgeVM JSON-RPC base URL
#
# Apply:
# kubectl apply -f k8s/bridge-deployment.yaml
#
# The MPC StatefulSet + Service from k8s/mpc-deployment.yaml must already
# be running — this deployment talks to mpc-api-svc.lux-bridge.svc:8081.
apiVersion: v1
kind: ConfigMap
metadata:
name: lux-bridge-config
namespace: lux-bridge
data:
networks.yaml: |
# Source of truth for the SPA + API's supported chains + tokens.
# Edit this ConfigMap and roll the deployment to add/remove networks
# without rebuilding the image.
brand:
name: Lux Bridge
title: Lux Bridge
logoUrl: /logo.svg
iconUrl: /icon.svg
brandHost: bridge.lux.network
networks:
- internalName: lux-mainnet
displayName: Lux
nativeCurrency: LUX
chainId: "96369"
type: evm
isFeatured: true
avgCompletion: 1m
txExplorerTpl: https://explore.lux.network/tx/{tx}
addrExplorerTpl: https://explore.lux.network/address/{addr}
- internalName: ethereum-mainnet
displayName: Ethereum
nativeCurrency: ETH
chainId: "1"
type: evm
avgCompletion: 5m
txExplorerTpl: https://etherscan.io/tx/{tx}
addrExplorerTpl: https://etherscan.io/address/{addr}
- internalName: base-mainnet
displayName: Base
nativeCurrency: ETH
chainId: "8453"
type: evm
avgCompletion: 2m
txExplorerTpl: https://basescan.org/tx/{tx}
addrExplorerTpl: https://basescan.org/address/{addr}
- internalName: bsc-mainnet
displayName: BNB Chain
nativeCurrency: BNB
chainId: "56"
type: evm
avgCompletion: 2m
txExplorerTpl: https://bscscan.com/tx/{tx}
addrExplorerTpl: https://bscscan.com/address/{addr}
tokens:
- asset: LUX
name: Lux
decimals: 18
network: lux-mainnet
- asset: ETH
name: Ether
decimals: 18
network: ethereum-mainnet
- asset: USDC
name: USD Coin
decimals: 6
contract: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
network: ethereum-mainnet
- asset: USDT
name: Tether USD
decimals: 6
contract: "0xdAC17F958D2ee523a2206206994597C13D831ec7"
network: ethereum-mainnet
- asset: USDC
name: USD Coin
decimals: 6
contract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
network: base-mainnet
limits:
minUSD: 10
maxUSD: 100000
per:
USDT: {minUSD: 10, maxUSD: 100000}
USDC: {minUSD: 10, maxUSD: 100000}
---
# PersistentVolumeClaim — the swap store (zapdb / Lux-flavored Badger v4)
# lives here. Single-writer: zapdb takes an exclusive directory lock,
# which is why the Deployment is pinned to replicas: 1 with Recreate.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: lux-bridge-data
namespace: lux-bridge
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: lux-bridge
namespace: lux-bridge
labels:
app: lux-bridge
component: api
spec:
# Single-writer: zapdb takes an exclusive dir lock. Scale-out
# requires migrating swap state to a shared external store first.
replicas: 1
# Recreate (not RollingUpdate) — the new pod can't claim the PV
# lock while the old pod still holds it.
strategy:
type: Recreate
selector:
matchLabels:
app: lux-bridge
template:
metadata:
labels:
app: lux-bridge
component: api
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: bridge
image: ghcr.io/luxfi/bridge:latest
imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
ports:
- containerPort: 8080
name: http
# The operator surface: /metrics, the swap list, the deposit
# poll. The Ingress below publishes the http port and only the
# http port, so this one is reachable from inside the cluster
# and nowhere else.
- containerPort: 8081
name: admin
env:
- name: BRIDGE_ADDR
value: ":8080"
- name: BRIDGE_ADMIN_ADDR
value: ":8081"
- name: BRIDGE_CONFIG
value: /etc/bridge/networks.yaml
# Persistent swap store. The PVC mounted at this path
# holds the zapdb files (memtable, vlog, sst). Removing
# this env var falls back to the in-memory store —
# restart loses state.
- name: BRIDGE_DATA_DIR
value: /var/lib/lux-bridge
# Empty disables the legacy Node backend reverse proxy. All
# /v1/bridge/* routes resolve through native handlers.
- name: BRIDGE_BACKEND_URL
value: ""
# BridgeVM (b-chain) RPC. Leave unset until the BridgeVM is
# available — the swap/quote routes still serve from native
# state when bchain is offline.
- name: BRIDGE_BCHAIN_URL
value: ""
# MPC keygen + signing daemon (mpcd) exposed by the StatefulSet
# in mpc-deployment.yaml. The service forwards :8081 → :8080.
- name: BRIDGE_MPC_URL
value: http://mpc-api-svc.lux-bridge.svc:8081
- name: BRIDGE_MPC_TOKEN
valueFrom:
secretKeyRef:
name: bridge-secrets
key: mpc-api-token
- name: BRIDGE_MPC_ORG_ID
value: bridge
# Bridge security profile. classical-compat = user-facing UI to
# external L1s. Pin strict-pq for internal Lux↔Lux only.
- name: BRIDGE_PROFILE
value: classical-compat
volumeMounts:
- name: config
mountPath: /etc/bridge
readOnly: true
- name: data
mountPath: /var/lib/lux-bridge
- name: tmp
mountPath: /tmp
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 2
periodSeconds: 5
failureThreshold: 2
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "500m"
volumes:
- name: config
configMap:
name: lux-bridge-config
- name: data
persistentVolumeClaim:
claimName: lux-bridge-data
- name: tmp
emptyDir:
sizeLimit: 50Mi
---
apiVersion: v1
kind: Service
metadata:
name: lux-bridge
namespace: lux-bridge
labels:
app: lux-bridge
spec:
type: ClusterIP
selector:
app: lux-bridge
ports:
- port: 80
targetPort: http
name: http
- port: 8081
targetPort: admin
name: admin
---
# Ingress for the unified binary. Routes both the SPA (/) and the API
# (/v1/bridge/*) to the same Service — the binary serves both. Replaces
# the legacy split routing in mpc-deployment.yaml's bridge-ingress, which
# fanned /v1/ to opnet-watcher and / to bridge-ui.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: lux-bridge
namespace: lux-bridge
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: ingress
tls:
- hosts:
- bridge.lux.network
- bridge-api.lux.network
secretName: lux-bridge-tls
rules:
- host: bridge.lux.network
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: lux-bridge
port:
number: 80
- host: bridge-api.lux.network
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: lux-bridge
port:
number: 80
---
# NOTE: no HorizontalPodAutoscaler. The swap store is a single-writer
# embedded KV (zapdb with exclusive dir lock). Scaling beyond 1 pod
# requires migrating swap state to a shared external store first.
#
# NOTE: no PodDisruptionBudget. With replicas=1 a PDB would either
# block drains entirely (minAvailable=1) or be a no-op (minAvailable=0).
# Operators scale to 0 manually for planned maintenance.