Files
zeekayandHanzo Dev b1059c5e6f Lux's chain archives are declared for CD
deploy/lux-{mainnet,testnet}/luxd-archive.yaml are charts/app values for
the two luxd archives that serve api.lux.network, api.lux.cloud and
api.lux-test.network. They replace the hand-applied k8s/ manifests and
deploy/lux-api/route.yaml, which declared the same objects.

Rendered and dry-run against the running objects (server-side, the way
CD applies), a sync adds the chart's labels, its config checksum and
HANZO_VERSION, and nothing else: same container, genesis, chain config,
volumes and routes. So cd.automated is off until someone syncs one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 19:31:29 -07:00

247 lines
10 KiB
YAML

# The trading surface on api.lux.cloud and api.lux.network, at /v1/trade.
#
# Eight paths: venues, quote, quotes, swap, and the four allowance ones.
# `api/openapi.yaml` in luxfi/dex is the contract and a test in that repo holds
# it to the route table, so this file never has to list them.
#
# What is NOT here is the other half of the story. Enumerating a chain — its
# tokens, pools, positions, TVL — is an index read, and the indexer beside this
# already answers it: api-explore.lux.cloud returns 16 pools and $39.9M of TVL
# for 96369 with real symbols and decimals. Those paths used to be on this
# binary too, and answered 500 on every request, because a quoting surface
# reads ONE pool by address and cannot walk every pool on Ethereum.
#
# `ghcr.io/luxfi/dex` carries two binaries. `dexd` is the D-Chain venue and is
# declared in dex.yaml; `gateway` is this one — an HTTP server that reads a
# market wherever that market lives and answers in one shape, so a screen does
# not have to know which chain it is looking at. One image because the two share
# the venue types, and shipping them apart is two versions of one contract
# waiting to disagree.
#
# It is named `trade` and not `gateway` because this namespace already has a
# `gateway`: api-gateway-gateway is the CEL filter in front of the validators'
# RPC. Two different things cannot answer to one name.
replicas: 2
containerName: trade
image:
repository: ghcr.io/luxfi/dex
tag: v1.14.56
digest: sha256:d4e547ea07c633de6166266377ad171cda37c6efc68add312e50ddcbdca66dc1
pullPolicy: IfNotPresent
imagePullSecrets:
- name: ghcr-luxfi
labels:
lux.network/component: trade
partOf: lux
# The image's ENTRYPOINT is dexd. This is the other binary in it.
command:
- /usr/local/bin/gateway
# The image is amd64 only (a single OCI manifest, not an index), so the
# selector is a property of the bytes. The apps node is where the rest of the
# lux-mainnet frontends run.
nodeSelector:
kubernetes.io/arch: amd64
hanzo.ai/role: apps
ports:
- name: http
containerPort: 8080
servicePort: 80
env:
- name: GATEWAY_ADDR
value: ":8080"
- name: LUX_CHAIN_ID
value: "96369"
# The C-Chain from inside the cluster: the same luxd-archive Service the public
# api.lux.network/v1/chain route serves, without a round trip out through
# Cloudflare and back in through the ingress that is serving the quote.
- name: LUX_RPC
value: http://luxd-archive.lux-mainnet.svc.cluster.local:9630/v1/chain/C/rpc
# The V3 QuoterV2 deployed to 96369, from luxfi/standard
# deployments/venue/96369.json. With it, 1 WLUX quotes 0.997 LUSD at the 3000
# tier; without it the chain has no arm the gateway can read and every pair on
# it answers "no venue here holds this pair".
- name: LUX_V3_QUOTER
value: "0x6EA2F79f743C809D1152657Ef5d5907f47a34cfc"
# The SwapRouter02 that executes what that quoter prices — the `Router` key in
# the same file. It is the V3 router, which is exactly why it belongs HERE and
# not on LUX_V2_ROUTER below: its factory() answers the V3 factory and its
# WETH() reverts.
#
# Without it 96369 quotes and cannot build, and /v1/trade/swap says so naming
# the venue rather than emitting calldata for some other contract. With it, the
# gateway's own calldata replayed through eth_call against this address returns
# the amount the quote gave — 997001483075056712 for 1 WLUX into LUSD, from the
# account that holds the allowance.
- name: LUX_V3_ROUTER
value: "0xbbD8d9A1E6bf5627A2F6D2aF38664bbe1cEF63Bb"
# ⚠ LUX_V2_ROUTER IS DELIBERATELY UNSET. The address above is the V3
# SwapRouter, not a V2 Router02, and handing it to the V2 arm makes every V2
# quote an `execution reverted` — a venue that always fails, reported as a
# market that has no price. The 12 V2 pairs on this chain are real and hold
# reserves; they become quotable when a Router02 is deployed and its address
# lands in luxfi/standard, and not before.
# A pool moves when somebody trades in it, so this window is short by nature.
# What it saves is the second and third reader of the same pair in the same
# second, and every eth_call saved is one a public endpoint is entitled to
# refuse.
- name: GATEWAY_CACHE_TTL
value: "10"
# Nothing here is dialled by a service link, and one of those variables is a
# name away from colliding with the four above.
enableServiceLinks: false
# /providers, not /healthz, and they now carry the same information.
#
# /healthz once read the HOSTED provider registry alone. This deployment
# registers no hosted provider on purpose — the Uniswap trade API answers
# ACCESS_DENIED without a contract — so it answered 503 forever, and a probe on
# it held every pod out of the Service while every quote worked. It reads the
# venues since v1.14.41, so it answers 200 here; but the venue count is fixed at
# startup and never drops, which makes it "the process is up" — exactly what
# /providers already says.
#
# Whether one chain's RPC is reachable is not a reason to take a pod out of
# rotation: the other six still quote, and a pod pulled for Ethereum being slow
# takes 96369 down with it.
readinessProbe:
httpGet:
path: /providers
port: 8080
initialDelaySeconds: 2
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /providers
port: 8080
initialDelaySeconds: 10
periodSeconds: 20
timeoutSeconds: 3
failureThreshold: 3
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: '1'
memory: 512Mi
# It holds no state, writes no file, and needs no privilege.
#
# The uid is stated even though the image already declares `USER dexd`, because
# the kubelet cannot check a NAME against runAsNonRoot — it reads the image's
# user field, finds a string, and refuses the container rather than guess:
# container has runAsNonRoot and image has non-numeric user (dexd)
# 1000 is the uid the Dockerfile creates dexd with.
securityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
middlewares:
# The edge answers for the surface when it refuses a request, and a refusal a
# browser cannot read is a refusal nobody can act on. A 429 is generated by the
# rate limiter, never reaches the binary, and so carries none of the CORS the
# binary sets on its own answers — lux.exchange sees a CORS failure and reports
# that instead of the rate limit that actually happened.
#
# Set on the way out, outermost in the chain, so it covers what the limiter
# writes as well as what the gateway writes. The header is Set rather than
# added, so an answer that already carries it from the binary still carries
# exactly one.
#
# The value is `*` rather than https://lux.exchange, and covers it. Every path
# here is an unauthenticated read of public pools — no cookie, no bearer, no
# credential of any kind rides these requests, so there is nothing an origin
# could be trusted with that another origin could not. Naming one origin would
# not add a guarantee; it would break docs.lux.network, a wallet, and any
# partner reading a price, each of them silently and each of them an afternoon
# to diagnose. If a path here ever takes a credential, this is the line that
# has to change first.
- name: cors
spec:
headers:
customResponseHeaders:
Access-Control-Allow-Origin: '*'
# A quote on any chain but ours is eth_calls against a public endpoint we hold
# no account with. Unmetered, one caller can spend that budget for everybody and
# the failure lands on lux.exchange, not on them. Generous enough that a screen
# opening every pair at once never sees it.
#
# The source is Cf-Connecting-Ip because these hosts are behind Cloudflare and
# the remote address the ingress sees is the tunnel.
- name: trade-rate
spec:
rateLimit:
average: 30
burst: 60
period: 1s
sourceCriterion:
requestHeaderName: Cf-Connecting-Ip
routes:
# ONE HOST, TWO ROUTERS, AND THE CHAIN'S IS THE MORE SPECIFIC.
#
# api.lux.cloud and api.lux.network already carry the chain at /v1/chain
# (priority 300 in deploy/lux-mainnet/luxd-archive.yaml) and the C-chain at the bare root
# (300). Everything else on those hosts falls through to a router in the ingress
# file provider — `api-hanzo`, a bare Host rule with no priority stated, so
# Traefik gives it its rule length, about 118 — and lands on hanzoai/cloud,
# which is where the 404 with `x-api-version:` in its headers comes from.
#
# 200 sits between: above the catch-all, below the chain. The same number
# api-hanzo-unified uses on api.hanzo.ai for exactly this shape.
#
# ONE PREFIX, and the reason the old form existed was real: the binary once
# published /v1/admin/pool/freeze, so `PathPrefix(/v1)` would have put an
# irreversible switch on the internet. Those handlers are gone and the whole
# surface moved under /v1/trade, so the allowlist and the thing it guarded
# against went away together. The conversion-tracking writes (/v1/leads,
# /v1/events) keep their old paths and stay reachable from inside the cluster
# and nowhere else — not because they are listed out here, but because they are
# not under that prefix.
#
# A route may lead a pin only where the two shapes are a union, which is why
# both were named here while the pin was behind. The order was: publish the
# image, move the pin, watch a token list answer at /v1/trade — and then delete
# the older spellings, in the commit that retires the version serving them. That
# is what this is.
# The edge publishes the paths the service answers and no others. A prefix
# listed here that no handler stands behind is a public address whose reply is
# whatever the binary at the other end happens to do with it, and the version
# pinned below is not the version this list was written against. `/v1/history`
# is off for that reason: nothing in this tree reads it and the current source
# serves no such path, so publishing it can only reach an older binary's
# handler. The remaining legacy prefixes go the same way in the commit that
# moves the pin, which is when the routes and the binary agree again.
- name: trade
spec:
entryPoints: [websecure]
routes:
- match: (Host(`api.lux.cloud`) || Host(`api.lux.network`)) && (PathPrefix(`/v1/trade`) || PathPrefix(`/trading/`))
kind: Rule
priority: 200
middlewares:
- name: cors
- name: trade-rate
services:
- name: trade
port: 80
tls: {}