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]>
247 lines
10 KiB
YAML
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: {}
|