mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
refactor(cmd): consolidate cmd/* into one binary — dexd
dexd is now the ONLY command in the repo. One binary, two run modes:
dexd run as a luxd rpcchainvm PLUGIN. luxd execs the plugin from
dexd plugin its PluginDir with NO positional args (engine address arrives
via the runtime.EngineAddressKey env var) and probes the
version with `dexd version`. No-subcommand == plugin is
REQUIRED to preserve that contract — byte-for-byte the old
cmd/dchain behavior (rpc.Serve over the plugin transport).
dexd run run the STANDALONE D-Chain venue: open the on-disk zapdb,
dexd standalone serve the frozen dex_* surface over ZAP, and drive the
consensus sealer loop (WaitForEvent->BuildBlock->Verify->
Accept). -addr/-db flags preserved from the old cmd/dvenue.
Relocated logic (engine code NOT duplicated — pkg/dchain + pkg/lx untouched):
cmd/dchain/{main,runner} -> internal/plugin (plugin.Run)
cmd/dvenue/{main,engine_*} -> internal/standalone (standalone.Run + engineName)
cmd/dexd/main.go = thin run-mode dispatcher
Removed cmd dirs (12): dchain, dvenue, demo, dex-api-server, dex-server,
dexfillproof, dexseed, fix-gateway, gateway, monitor, persistent-server,
price-feed. Removed committed build artifacts: gateway, dex-server-linux.
Build/ship ONLY dexd:
- Dockerfile: canonical CPU image (CGO=0, ghcr.io/luxfi/dex) running
`dexd run`; no GPU/MLX required.
- Dockerfile.dvenue: GPU operator bundle repointed cmd/dvenue -> cmd/dexd.
- Deleted Dockerfile.{gateway,devnet,minimal,dvenue-cpu} and
docker/{Dockerfile,Dockerfile.cuda,backend/Dockerfile,compose-simple.yml}.
- Makefile rewritten to build/run/ship dexd only.
- docker.yml: dropped the dex-gateway image; dex-venue-image.yml: dvenue->dexd.
- Deleted legacy scripts that built removed/long-gone cmds; fixed deploy.sh +
dev-setup.sh; .gitignore ignores root build binaries.
Also included (coupled, environment-applied, settled): the in-progress
clob -> dex/orderbook terminology refactor across pkg/* + sdk/* + tests/*
(identifier + file renames, e.g. RegisterCLOB->RegisterDEX,
source_clob.go->source_orderbook.go). The relocated standalone venue calls the
renamed API, so it lands in the same consistent snapshot.
Verified (GOWORK=off CGO_ENABLED=0): `go build ./cmd/dexd`, `go build ./...`,
`go vet ./...` all exit 0; `go test ./pkg/dchain` (renamed RegisterDEX socket
path) green. GPU/cgo path not compiled locally (no CUDA); its rename mirrors
the verified nocgo twins and preserves the C ABI symbol names.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Build + push the D-Chain VENUE image (dex family) — ghcr.io/luxfi/dex-venue.
|
||||
#
|
||||
# This is the missing artifact that blocks D-Chain turn-on: the dchain-tagged
|
||||
# luxd + the dvenue GPU matching engine in one image (see Dockerfile.dvenue).
|
||||
# luxd + the dexd GPU matching engine in one image (see Dockerfile.dvenue).
|
||||
# The public ghcr.io/luxfi/node image does NOT carry the DexVM; this private
|
||||
# build does.
|
||||
#
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
- name: Resolve cross-repo PAT for private luxfi/* deps
|
||||
id: pat
|
||||
env:
|
||||
# The dvenue build pulls private luxfi/* modules and the private
|
||||
# The dexd build pulls private luxfi/* modules and the private
|
||||
# luxcpp/dex release tarball; the default GITHUB_TOKEN is repo-scoped.
|
||||
# Prefer org GH_TOKEN, fall back to repo UNIVERSE_PAT (same order as
|
||||
# node/docker.yml).
|
||||
|
||||
@@ -18,8 +18,6 @@ jobs:
|
||||
include:
|
||||
- image: ghcr.io/luxfi/dex
|
||||
dockerfile: Dockerfile
|
||||
- image: ghcr.io/luxfi/dex-gateway
|
||||
dockerfile: Dockerfile.gateway
|
||||
uses: hanzoai/.github/.github/workflows/docker-build.yml@main
|
||||
with:
|
||||
image: ${{ matrix.image }}
|
||||
|
||||
@@ -96,3 +96,8 @@ QWEN.md
|
||||
server.log
|
||||
lxd
|
||||
dex-server-linux
|
||||
|
||||
# Compiled binaries built at repo root (canonical build is bin/dexd via `make build`)
|
||||
/dexd
|
||||
/dvenue
|
||||
/gateway
|
||||
|
||||
+48
-23
@@ -1,33 +1,58 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
|
||||
# ---------------------------------------------------------------------------
|
||||
# dexd — the ONE D-Chain DEX image (CPU, linux/amd64) — ghcr.io/luxfi/dex
|
||||
#
|
||||
# Builds the single `dexd` binary and runs it as the standalone D-Chain venue:
|
||||
# it opens the persisted zapdb, serves the frozen clob_* surface over ZAP, and
|
||||
# runs the consensus sealer loop itself (WaitForEvent->BuildBlock->Verify->Accept)
|
||||
# so every write returns consensus-computed fills. CGO_ENABLED=0 selects pkg/lx's
|
||||
# pure-Go CPU matcher (the !cgo build tag) — no CUDA, no MLX, runs on any amd64.
|
||||
#
|
||||
# Same binary, other modes (override the default CMD):
|
||||
# docker run … plugin run as a luxd rpcchainvm plugin (luxd execs it)
|
||||
# docker run … version print the plugin version line
|
||||
# ---------------------------------------------------------------------------
|
||||
ARG GO_VERSION=1.26.4
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS builder
|
||||
|
||||
RUN apk add --no-cache git ca-certificates
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG GITHUB_TOKEN
|
||||
RUN git config --global url."https://${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
|
||||
# luxfi module resolution: private modules resolve via the gh_token BuildKit
|
||||
# secret over HTTPS (the shared hanzoai/.github docker-build.yml passes it);
|
||||
# skip sumdb for luxfi (tags may be rewritten — go.sum in the build context is
|
||||
# already realigned to current tag bits).
|
||||
ENV GOPRIVATE=github.com/luxfi/*,github.com/hanzoai/*
|
||||
ENV GOFLAGS=-mod=mod
|
||||
ENV GOTOOLCHAIN=auto
|
||||
ENV GOEXPERIMENT=jsonv2
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
RUN --mount=type=secret,id=gh_token,required=false \
|
||||
if [ -s /run/secrets/gh_token ]; then \
|
||||
git config --global url."https://x-access-token:$(cat /run/secrets/gh_token)@github.com/".insteadOf "https://github.com/"; \
|
||||
fi && \
|
||||
go mod download
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o dex-server ./cmd/dex-server/main.go
|
||||
# CPU build: CGO_ENABLED=0 → !cgo build tag → pure-Go CPU matcher in pkg/lx.
|
||||
RUN --mount=type=secret,id=gh_token,required=false \
|
||||
if [ -s /run/secrets/gh_token ]; then \
|
||||
git config --global url."https://x-access-token:$(cat /run/secrets/gh_token)@github.com/".insteadOf "https://github.com/"; \
|
||||
fi && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
go build -trimpath -ldflags="-s -w" -o /out/dexd ./cmd/dexd
|
||||
|
||||
FROM alpine:3.21
|
||||
|
||||
RUN apk add --no-cache ca-certificates && \
|
||||
addgroup -g 1000 -S dex && \
|
||||
adduser -u 1000 -S dex -G dex
|
||||
|
||||
WORKDIR /home/dex
|
||||
COPY --from=builder /app/dex-server .
|
||||
RUN chown dex:dex dex-server
|
||||
|
||||
USER dex
|
||||
EXPOSE 8080 9090 50051
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||
|
||||
CMD ["./dex-server"]
|
||||
FROM debian:12-slim AS execution
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates && rm -rf /var/lib/apt/lists/* \
|
||||
&& groupadd -g 1000 dexd && useradd -u 1000 -g dexd -m -s /usr/sbin/nologin dexd \
|
||||
&& mkdir -p /data && chown -R dexd:dexd /data
|
||||
COPY --from=builder /out/dexd /usr/local/bin/dexd
|
||||
USER dexd
|
||||
EXPOSE 9099
|
||||
# Default: standalone venue. Override CMD with `plugin` / `version` for the
|
||||
# other run modes.
|
||||
ENTRYPOINT ["/usr/local/bin/dexd"]
|
||||
CMD ["run", "-addr", "0.0.0.0:9099", "-db", "/data/db"]
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
FROM golang:1.26-alpine AS builder
|
||||
RUN apk add --no-cache git
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gateway ./cmd/gateway/
|
||||
|
||||
FROM alpine:3.21
|
||||
RUN apk --no-cache add ca-certificates && addgroup -g 1000 -S gw && adduser -u 1000 -S gw -G gw
|
||||
WORKDIR /home/gw
|
||||
COPY --from=builder /app/gateway .
|
||||
RUN chown gw:gw gateway
|
||||
USER gw
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["./gateway"]
|
||||
+25
-23
@@ -6,17 +6,18 @@
|
||||
# image (which excludes the DexVM). It bundles TWO binaries the operator venue
|
||||
# StatefulSet runs from one image:
|
||||
#
|
||||
# /usr/local/bin/dvenue — the ZAP matching engine (luxfi/dex cmd/dvenue,
|
||||
# //go:build cgo, built CGO_ENABLED=1). This is the
|
||||
# /usr/local/bin/dexd — the ZAP matching engine (luxfi/dex cmd/dexd, run
|
||||
# as `dexd run`, built CGO_ENABLED=1). This is the
|
||||
# real CLOB:
|
||||
# it links the GPU matching kernels (libdex_clob_cuda
|
||||
# + libamm_xyk_cuda) and the secp256k1 ecrecover
|
||||
# bundle via pkg-config. The 0x9010 C-Chain precompile
|
||||
# and the chains/dexvm atomic proxy dial it.
|
||||
# CGO_ENABLED=1 makes pkg/lx link the GPU matching
|
||||
# kernels (libdex_clob_cuda + libamm_xyk_cuda) and the
|
||||
# secp256k1 ecrecover bundle via pkg-config. The 0x9010
|
||||
# C-Chain precompile and the chains/dexvm atomic proxy
|
||||
# dial it.
|
||||
# /luxd/build/luxd — the dchain-tagged node (`-tags dchain`), which
|
||||
# links the chains/dexvm PROXY so the venue node
|
||||
# serves the P-chain-registered D-Chain. The proxy
|
||||
# relays to the dvenue socket; it is pure-Go (no GPU).
|
||||
# relays to the dexd socket; it is pure-Go (no GPU).
|
||||
#
|
||||
# ARCH: linux/arm64 ONLY. The matcher's `cgo && linux` path in luxfi/dex pkg/lx
|
||||
# (orderbook_cuda.go + amm_gpu_cuda.go) has no non-CUDA fallback on any arch, so
|
||||
@@ -31,7 +32,7 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
ARG GO_VERSION=1.26.4
|
||||
# CUDA 12.x devel image (arm64/sbsa variant) provides nvcc-free libcudart for
|
||||
# the cgo link of dvenue. Runtime stage uses the matching -runtime image.
|
||||
# the cgo link of dexd. Runtime stage uses the matching -runtime image.
|
||||
ARG CUDA_VERSION=12.4.1
|
||||
ARG UBUNTU_VERSION=22.04
|
||||
|
||||
@@ -75,7 +76,7 @@ RUN --mount=type=secret,id=ghtok,required=false \
|
||||
# lux-crypto-secp256k1 at the cgo link step below.
|
||||
#
|
||||
# CI/RELEASE GAP (until the dex-gpu-release workflow has run on a tag): the URL
|
||||
# below 404s. The build then fails at the dvenue link step with a clear
|
||||
# below 404s. The build then fails at the dexd link step with a clear
|
||||
# pkg-config error. Run luxcpp/dex dex-gpu-release first (or point VENUE_DEPS_URL
|
||||
# at the workflow artifact) to populate it.
|
||||
ARG VENUE_DEPS_VERSION=v0.1.1
|
||||
@@ -88,19 +89,20 @@ RUN --mount=type=secret,id=ghtok,required=false \
|
||||
&& ldconfig 2>/dev/null || true
|
||||
ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
|
||||
|
||||
# --- Build dvenue (the GPU matcher) from luxfi/dex ---------------------------
|
||||
# cmd/dvenue is `//go:build cgo`: CGO_ENABLED=1 (which sets the `cgo` build tag)
|
||||
# selects pkg/lx's GPU matcher (orderbook_cuda + amm_gpu_cuda + signed_order_gpu,
|
||||
# all gated on `cgo`), satisfied by the pkg-config libs above. `-tags cgo` is
|
||||
# explicit-for-readability (CGO_ENABLED=1 already implies it). The pure-Go CPU
|
||||
# venue is cmd/dvenue-cpu (//go:build !cgo) — not built here.
|
||||
# --- Build dexd (the GPU matcher) from luxfi/dex -----------------------------
|
||||
# CGO_ENABLED=1 (which sets the `cgo` build tag) makes pkg/lx select its GPU
|
||||
# matcher (orderbook_cuda + amm_gpu_cuda + signed_order_gpu, all gated on `cgo`),
|
||||
# satisfied by the pkg-config libs above. `-tags cgo` is explicit-for-readability
|
||||
# (CGO_ENABLED=1 already implies it). The operator runs this binary as
|
||||
# `dexd run`. The pure-Go CPU build of the same binary is the canonical
|
||||
# Dockerfile (CGO_ENABLED=0) — not built here.
|
||||
WORKDIR /build/dex
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=1 GOOS=linux GOARCH=arm64 \
|
||||
go build -trimpath -tags "cgo" -ldflags="-s -w" \
|
||||
-o /out/dvenue ./cmd/dvenue
|
||||
-o /out/dexd ./cmd/dexd
|
||||
|
||||
# --- Build the dchain-tagged luxd (the proxy node) ---------------------------
|
||||
# Cloned from luxfi/node at a pinned release and built `-tags dchain` so it
|
||||
@@ -121,7 +123,7 @@ RUN go mod download
|
||||
# CGO_ENABLED=0: the dchain luxd's only cgo edge is the optional luxcpp/cevm
|
||||
# C-Chain backend; the dexvm proxy + pure-Go EVM need no native libs (the
|
||||
# build graph pulls only runtime/cgo). Building pure-Go keeps the venue node
|
||||
# free of the cevm fetch — the GPU/cgo surface lives entirely in dvenue.
|
||||
# free of the cevm fetch — the GPU/cgo surface lives entirely in dexd.
|
||||
RUN GITCOMMIT=$(git rev-parse HEAD) && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
|
||||
go build -trimpath -tags "nattraversal,dchain" -ldflags="-s -w \
|
||||
@@ -132,7 +134,7 @@ RUN GITCOMMIT=$(git rev-parse HEAD) && \
|
||||
# node registers the optional VMs (incl. the dexvm proxy) + P/X in-process, but
|
||||
# the C-Chain EVM is an EXTERNAL plugin (same as the public node image's EVM
|
||||
# Plugin Stage). The venue --track-all-chains, and the 0x9010 dex precompile
|
||||
# (NewZAPEngine(dex-zap-endpoint) -> the dvenue socket) lives in this plugin, so
|
||||
# (NewZAPEngine(dex-zap-endpoint) -> the dexd socket) lives in this plugin, so
|
||||
# the venue MUST ship it. Built from the pinned luxfi/evm release, CGO=0,
|
||||
# written to /luxd/build/plugins/<EVM_VM_ID> — startup.sh seeds it into
|
||||
# /data/plugins (--plugin-dir).
|
||||
@@ -153,16 +155,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Carry the GPU/crypto shared libs the dvenue binary may dlopen / rpath into.
|
||||
# Carry the GPU/crypto shared libs the dexd binary may dlopen / rpath into.
|
||||
COPY --from=builder /usr/local/lib /usr/local/lib
|
||||
COPY --from=builder /usr/local/include /usr/local/include
|
||||
RUN ldconfig 2>/dev/null || true
|
||||
|
||||
# Binaries: the dchain luxd (venue node) + the EVM (C-Chain) plugin it loads +
|
||||
# dvenue (ZAP matcher). startup.sh seeds /luxd/build/plugins/* into /data/plugins.
|
||||
# dexd (ZAP matcher). startup.sh seeds /luxd/build/plugins/* into /data/plugins.
|
||||
COPY --from=builder /luxd/build/luxd /luxd/build/luxd
|
||||
COPY --from=builder /luxd/build/plugins /luxd/build/plugins
|
||||
COPY --from=builder /out/dvenue /usr/local/bin/dvenue
|
||||
COPY --from=builder /out/dexd /usr/local/bin/dexd
|
||||
|
||||
# Non-root (matches the operator StatefulSet securityContext runAsUser 1000).
|
||||
RUN groupadd -g 1000 venue && useradd -u 1000 -g venue -m -s /usr/sbin/nologin venue \
|
||||
@@ -172,9 +174,9 @@ USER venue
|
||||
ENV PATH="/luxd/build:/usr/local/bin:${PATH}"
|
||||
WORKDIR /luxd/build
|
||||
|
||||
# 9650 luxd HTTP RPC, 9651 luxd staking, 9099 dvenue ZAP socket.
|
||||
# 9650 luxd HTTP RPC, 9651 luxd staking, 9099 dexd ZAP socket.
|
||||
EXPOSE 9650 9651 9099
|
||||
|
||||
# Default to the venue node; the operator overrides the `zap` container's
|
||||
# command to /usr/local/bin/dvenue.
|
||||
# command to `/usr/local/bin/dexd run`.
|
||||
CMD ["/luxd/build/luxd"]
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# ---------------------------------------------------------------------------
|
||||
# D-Chain VENUE image (CPU, linux/amd64) — ghcr.io/luxfi/dchain
|
||||
#
|
||||
# The pure-Go CPU sealing venue for devnet. cmd/dvenue is matcher-agnostic;
|
||||
# CGO_ENABLED=0 selects pkg/lx's pure-Go CPU matcher (amm_nogpu /
|
||||
# orderbook_cuda_stub via the !cgo build tag in engine_nocgo.go). No CUDA,
|
||||
# no GPU tarball — runs on any amd64 node. This is the binary the deployed
|
||||
# `dchain-venue` Deployment runs as `dvenue -addr 0.0.0.0:9099 -db /data/db`:
|
||||
# it opens the persisted zapdb, serves the frozen clob_* surface over ZAP,
|
||||
# and runs the consensus sealer loop (WaitForEvent->BuildBlock->Verify->Accept)
|
||||
# so every write returns consensus-computed fills.
|
||||
# ---------------------------------------------------------------------------
|
||||
ARG GO_VERSION=1.26.4
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# luxfi module resolution: private modules (corona etc.) via ghtok BuildKit
|
||||
# secret over HTTPS; skip sumdb for luxfi (tags may be rewritten — go.sum in
|
||||
# the build context is already realigned to current tag bits).
|
||||
ENV GOPRIVATE=github.com/luxfi/*
|
||||
ENV GOFLAGS=-mod=mod
|
||||
ENV GOTOOLCHAIN=auto
|
||||
ENV GOEXPERIMENT=jsonv2
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN --mount=type=secret,id=ghtok,required=false \
|
||||
if [ -s /run/secrets/ghtok ]; then \
|
||||
git config --global url."https://x-access-token:$(cat /run/secrets/ghtok)@github.com/".insteadOf "https://github.com/"; \
|
||||
fi && \
|
||||
go mod download
|
||||
COPY . .
|
||||
|
||||
# CPU venue: CGO_ENABLED=0 → !cgo build tag → pure-Go CPU matcher.
|
||||
RUN --mount=type=secret,id=ghtok,required=false \
|
||||
if [ -s /run/secrets/ghtok ]; then \
|
||||
git config --global url."https://x-access-token:$(cat /run/secrets/ghtok)@github.com/".insteadOf "https://github.com/"; \
|
||||
fi && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
go build -trimpath -ldflags="-s -w" -o /out/dvenue ./cmd/dvenue
|
||||
|
||||
FROM debian:12-slim AS execution
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates && rm -rf /var/lib/apt/lists/* \
|
||||
&& groupadd -g 1000 venue && useradd -u 1000 -g venue -m -s /usr/sbin/nologin venue \
|
||||
&& mkdir -p /data && chown -R venue:venue /data
|
||||
COPY --from=builder /out/dvenue /usr/local/bin/dvenue
|
||||
USER venue
|
||||
EXPOSE 9099
|
||||
# The deployment passes -addr 0.0.0.0:9099 -db /data/db.
|
||||
ENTRYPOINT ["/usr/local/bin/dvenue"]
|
||||
@@ -1,39 +0,0 @@
|
||||
# Build stage - use BUILDPLATFORM for native compilation, cross-compile via TARGETOS/TARGETARCH
|
||||
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apk add --no-cache git make gcc musl-dev
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o gateway ./cmd/gateway/
|
||||
|
||||
# Final stage
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
RUN addgroup -g 1000 -S gateway && \
|
||||
adduser -u 1000 -S gateway -G gateway
|
||||
|
||||
WORKDIR /home/gateway
|
||||
|
||||
COPY --from=builder /app/gateway .
|
||||
|
||||
RUN chown -R gateway:gateway /home/gateway
|
||||
|
||||
USER gateway
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||
|
||||
ENTRYPOINT ["./gateway"]
|
||||
@@ -1,5 +0,0 @@
|
||||
FROM alpine:3.21
|
||||
RUN apk add --no-cache ca-certificates
|
||||
COPY dex-server-linux /usr/local/bin/dex-server
|
||||
EXPOSE 8080 50051
|
||||
ENTRYPOINT ["dex-server"]
|
||||
@@ -1,226 +1,100 @@
|
||||
# LX Makefile
|
||||
# Complete build, test, and deployment automation
|
||||
# dexd Makefile — the ONE D-Chain DEX binary.
|
||||
#
|
||||
# dexd runs the DEX two ways (one binary, two modes):
|
||||
# dexd run as a luxd rpcchainvm plugin (luxd execs it, no args)
|
||||
# dexd run run the standalone D-Chain venue (ZAP + consensus sealer)
|
||||
#
|
||||
# Default build is CPU (CGO_ENABLED=0) — no GPU/MLX required. The GPU matcher
|
||||
# (CGO_ENABLED=1, pkg/lx cuda kernels) is built only by Dockerfile.dvenue.
|
||||
|
||||
SHELL := /bin/bash
|
||||
.PHONY: all build test clean help
|
||||
.PHONY: all build run test test-race bench vet fmt lint deps clean coverage \
|
||||
docker proto sdk-typescript sdk-python sdk-go sdk-all ci help
|
||||
|
||||
# Version and build info
|
||||
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||
BUILD_TIME := $(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
||||
# Version / build info
|
||||
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
|
||||
# Go parameters
|
||||
GO := go
|
||||
GOBUILD := $(GO) build
|
||||
GOCLEAN := $(GO) clean
|
||||
GOTEST := $(GO) test
|
||||
GOGET := $(GO) get
|
||||
GOMOD := $(GO) mod
|
||||
GOVET := $(GO) vet
|
||||
GOFMT := gofmt
|
||||
|
||||
# Build parameters
|
||||
# Go
|
||||
GO := go
|
||||
CGO_ENABLED ?= 0
|
||||
LDFLAGS := -ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.GitCommit=$(GIT_COMMIT)"
|
||||
BINARY := dexd
|
||||
BINARY_DIR := bin
|
||||
IMAGE := ghcr.io/luxfi/dex
|
||||
TEST_TIMEOUT := 60s
|
||||
|
||||
# Binary output
|
||||
BINARY_NAME := luxd
|
||||
BINARY_DIR := bin
|
||||
export CGO_ENABLED
|
||||
|
||||
# Test parameters
|
||||
TEST_TIMEOUT := 30s
|
||||
BENCH_TIME := 10s
|
||||
|
||||
# Default target
|
||||
all: clean fmt vet test build
|
||||
@echo "✅ Build complete!"
|
||||
all: fmt vet test build
|
||||
|
||||
help:
|
||||
@echo "LX Makefile Commands:"
|
||||
@echo ""
|
||||
@echo "Development:"
|
||||
@echo " make build - Build all binaries"
|
||||
@echo " make test - Run all tests"
|
||||
@echo " make bench - Run benchmarks"
|
||||
@echo " make clean - Clean build artifacts"
|
||||
@echo ""
|
||||
@echo "Running:"
|
||||
@echo " make run - Run single node"
|
||||
@echo " make run-cluster - Run 3-node cluster"
|
||||
@echo ""
|
||||
@echo "Quality:"
|
||||
@echo " make fmt - Format code"
|
||||
@echo " make vet - Run go vet"
|
||||
@echo " make lint - Run linters"
|
||||
@echo ""
|
||||
@echo "Docker:"
|
||||
@echo " make docker-build - Build Docker images"
|
||||
@echo " make up - Start with docker-compose"
|
||||
@echo " make down - Stop docker-compose"
|
||||
@echo "dexd Makefile:"
|
||||
@echo " make build - Build dexd (CPU, CGO_ENABLED=0) -> $(BINARY_DIR)/$(BINARY)"
|
||||
@echo " make run - Build and run the standalone venue (dexd run)"
|
||||
@echo " make test - Run package tests"
|
||||
@echo " make vet - go vet ./..."
|
||||
@echo " make fmt - gofmt -s -w ."
|
||||
@echo " make lint - golangci-lint run"
|
||||
@echo " make docker - Build the canonical image ($(IMAGE)) from ./Dockerfile"
|
||||
@echo " make clean - Remove build artifacts"
|
||||
|
||||
# Build targets
|
||||
build: build-go build-tools
|
||||
|
||||
build-go:
|
||||
@echo "🔨 Building luxd..."
|
||||
# Build the one binary (CPU).
|
||||
build:
|
||||
@echo "Building $(BINARY) (CGO_ENABLED=$(CGO_ENABLED))..."
|
||||
@mkdir -p $(BINARY_DIR)
|
||||
@CGO_ENABLED=$(CGO_ENABLED) $(GOBUILD) $(LDFLAGS) -o $(BINARY_DIR)/$(BINARY_NAME) ./cmd/luxd
|
||||
@echo "✅ luxd built successfully!"
|
||||
@$(GO) build -trimpath -ldflags "-s -w -X main.Version=$(VERSION) -X main.GitCommit=$(GIT_COMMIT)" \
|
||||
-o $(BINARY_DIR)/$(BINARY) ./cmd/dexd
|
||||
@echo "Built $(BINARY_DIR)/$(BINARY)"
|
||||
|
||||
build-cpp:
|
||||
@echo "🔨 Building with C++ optimizations..."
|
||||
@mkdir -p $(BINARY_DIR)
|
||||
@CGO_ENABLED=1 $(GOBUILD) $(LDFLAGS) -tags cpp -o $(BINARY_DIR)/$(BINARY_NAME)-cpp ./cmd/luxd
|
||||
# Run the standalone venue.
|
||||
run: build
|
||||
@$(BINARY_DIR)/$(BINARY) run
|
||||
|
||||
build-gpu:
|
||||
@echo "🔨 Building with GPU support..."
|
||||
@mkdir -p $(BINARY_DIR)
|
||||
@CGO_ENABLED=1 $(GOBUILD) $(LDFLAGS) -tags "cpp gpu mlx" -o $(BINARY_DIR)/$(BINARY_NAME)-gpu ./cmd/luxd
|
||||
|
||||
# FPGA build targets
|
||||
build-fpga:
|
||||
@echo "🔨 Building with FPGA support..."
|
||||
@mkdir -p $(BINARY_DIR)
|
||||
@CGO_ENABLED=1 $(GOBUILD) $(LDFLAGS) -tags "cgo fpga" -o $(BINARY_DIR)/$(BINARY_NAME)-fpga ./cmd/luxd
|
||||
|
||||
build-fpga-versal:
|
||||
@echo "🔨 Building for AMD Versal FPGA..."
|
||||
@mkdir -p $(BINARY_DIR)
|
||||
@$(GOBUILD) $(LDFLAGS) -tags "fpga versal" -o $(BINARY_DIR)/$(BINARY_NAME)-versal ./cmd/luxd
|
||||
|
||||
build-fpga-f2:
|
||||
@echo "🔨 Building for AWS F2 FPGA..."
|
||||
@mkdir -p $(BINARY_DIR)
|
||||
@$(GOBUILD) $(LDFLAGS) -tags "fpga awsf2" -o $(BINARY_DIR)/$(BINARY_NAME)-f2 ./cmd/luxd
|
||||
|
||||
build-tools:
|
||||
@echo "🔨 Building tools..."
|
||||
@$(GOBUILD) -o $(BINARY_DIR)/price-feed ./cmd/price-feed
|
||||
@$(GOBUILD) -o $(BINARY_DIR)/monitor ./cmd/monitor
|
||||
|
||||
# Test targets
|
||||
# Tests
|
||||
test:
|
||||
@echo "🧪 Running tests..."
|
||||
@$(GOTEST) -v -timeout $(TEST_TIMEOUT) ./pkg/lx/...
|
||||
@$(GOTEST) -v -timeout $(TEST_TIMEOUT) ./pkg/api/...
|
||||
@echo "✅ Tests passed!"
|
||||
|
||||
test-all:
|
||||
@./scripts/test-all.sh
|
||||
@$(GO) test -timeout $(TEST_TIMEOUT) ./pkg/...
|
||||
|
||||
test-race:
|
||||
@$(GOTEST) -race -timeout 2m ./pkg/...
|
||||
@$(GO) test -race -timeout 2m ./pkg/...
|
||||
|
||||
# Benchmark targets
|
||||
bench:
|
||||
@echo "🏁 Running benchmarks..."
|
||||
@$(GOTEST) -bench=. -benchmem -benchtime=$(BENCH_TIME) -run=^$$ ./pkg/lx/...
|
||||
|
||||
bench-all:
|
||||
@./scripts/run-comprehensive-benchmark.sh
|
||||
|
||||
# Run targets
|
||||
run:
|
||||
@$(BINARY_DIR)/$(BINARY_NAME) --data-dir ~/.lxd --http-port 8080
|
||||
|
||||
run-cluster:
|
||||
@./scripts/run-lx-cluster.sh
|
||||
|
||||
run-dev:
|
||||
@air -c .air.toml 2>/dev/null || $(BINARY_DIR)/$(BINARY_NAME)
|
||||
|
||||
# Code quality
|
||||
fmt:
|
||||
@$(GOFMT) -s -w .
|
||||
@$(GO) test -bench=. -benchmem -benchtime=10s -run=^$$ ./pkg/lx/...
|
||||
|
||||
# Quality
|
||||
vet:
|
||||
@$(GOVET) ./...
|
||||
@$(GO) vet ./...
|
||||
|
||||
fmt:
|
||||
@gofmt -s -w .
|
||||
|
||||
lint:
|
||||
@golangci-lint run --timeout 5m 2>/dev/null || echo "Install golangci-lint for linting"
|
||||
|
||||
# Docker targets
|
||||
docker-build:
|
||||
@docker build -f docker/backend/Dockerfile -t lxdex:$(VERSION) .
|
||||
coverage:
|
||||
@$(GO) test -coverprofile=coverage.out -covermode=atomic ./pkg/...
|
||||
@$(GO) tool cover -html=coverage.out -o coverage.html
|
||||
@echo "Coverage report: coverage.html"
|
||||
|
||||
up:
|
||||
@docker-compose -f docker/compose.yml up -d
|
||||
|
||||
down:
|
||||
@docker-compose -f docker/compose.yml down
|
||||
|
||||
logs:
|
||||
@docker-compose -f docker/compose.yml logs -f
|
||||
|
||||
# Utility targets
|
||||
# Modules
|
||||
deps:
|
||||
@$(GOMOD) download
|
||||
@$(GOMOD) tidy
|
||||
@$(GO) mod download
|
||||
@$(GO) mod tidy
|
||||
|
||||
clean:
|
||||
@$(GOCLEAN)
|
||||
@rm -rf $(BINARY_DIR)
|
||||
@rm -f coverage.out coverage.html
|
||||
@$(GO) clean
|
||||
@rm -rf $(BINARY_DIR) coverage.out coverage.html
|
||||
|
||||
version:
|
||||
@echo "Version: $(VERSION)"
|
||||
@echo "Commit: $(GIT_COMMIT)"
|
||||
@echo "Built: $(BUILD_TIME)"
|
||||
# Canonical image (CPU dexd).
|
||||
docker:
|
||||
@docker build -f Dockerfile -t $(IMAGE):$(VERSION) .
|
||||
|
||||
# CI target
|
||||
ci: clean fmt vet test build
|
||||
@echo "✅ CI pipeline complete!"
|
||||
|
||||
# 3-node benchmark (legacy compatibility)
|
||||
3node-bench:
|
||||
@echo "🌐 Starting 3-node FPC network benchmark..."
|
||||
@./scripts/run-3node-bench.sh
|
||||
@echo "✅ 3-node benchmark complete!"
|
||||
|
||||
# Run demo
|
||||
demo:
|
||||
@./scripts/demo.sh
|
||||
|
||||
# Test MLX acceleration
|
||||
test-mlx:
|
||||
@./scripts/test-mlx.sh
|
||||
|
||||
# Build with MLX support
|
||||
build-mlx: build-gpu
|
||||
|
||||
# Test CUDA acceleration
|
||||
test-cuda:
|
||||
@./scripts/test-cuda.sh
|
||||
|
||||
# Docker CUDA build
|
||||
docker-cuda:
|
||||
@docker build -f docker/backend/Dockerfile.cuda -t lxdex-cuda:$(VERSION) .
|
||||
|
||||
# Ensure 100% test passing
|
||||
test-100:
|
||||
@./scripts/ensure-100-pass.sh
|
||||
|
||||
# Run performance tuning
|
||||
perf-tune:
|
||||
@sudo ./scripts/perf-tune.sh
|
||||
|
||||
# Development setup
|
||||
setup:
|
||||
@./scripts/setup.sh
|
||||
|
||||
# Install development tools
|
||||
tools:
|
||||
@go install github.com/cosmtrek/air@latest
|
||||
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
@go install github.com/securego/gosec/v2/cmd/gosec@latest
|
||||
|
||||
# Generate protobuf code
|
||||
# Protobuf
|
||||
proto:
|
||||
@protoc --go_out=. --go_opt=paths=source_relative \
|
||||
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
||||
proto/*.proto
|
||||
|
||||
# SDK targets
|
||||
# SDKs
|
||||
sdk-typescript:
|
||||
@cd sdk/typescript && npm install && npm run build
|
||||
|
||||
@@ -232,102 +106,6 @@ sdk-go:
|
||||
|
||||
sdk-all: sdk-typescript sdk-python sdk-go
|
||||
|
||||
# Deploy targets
|
||||
deploy-staging:
|
||||
@./scripts/deploy.sh staging deploy
|
||||
|
||||
deploy-production:
|
||||
@./scripts/deploy.sh production deploy
|
||||
|
||||
rollback:
|
||||
@./scripts/deploy.sh production rollback
|
||||
|
||||
# Monitoring
|
||||
metrics:
|
||||
@open http://localhost:9090
|
||||
|
||||
grafana:
|
||||
@open http://localhost:3001
|
||||
|
||||
# Performance benchmarks
|
||||
bench-10gbps:
|
||||
@./scripts/benchmark-10gbps.sh
|
||||
|
||||
bench-zmq:
|
||||
@./scripts/run-zmq-benchmark.sh
|
||||
|
||||
bench-comprehensive:
|
||||
@./scripts/run-comprehensive-benchmark.sh
|
||||
|
||||
# Network operations
|
||||
run-testnet:
|
||||
@./scripts/run-lux-testnet.sh
|
||||
|
||||
run-local:
|
||||
@./scripts/run-local-network.sh
|
||||
|
||||
run-fpc:
|
||||
@./scripts/run-fpc-network.sh
|
||||
|
||||
run-qzmq:
|
||||
@./scripts/run-qzmq-network.sh
|
||||
|
||||
# Database operations
|
||||
db-migrate:
|
||||
@migrate -path migrations -database "postgres://lxdex:lxdex123@localhost:5432/lxdex?sslmode=disable" up
|
||||
|
||||
db-rollback:
|
||||
@migrate -path migrations -database "postgres://lxdex:lxdex123@localhost:5432/lxdex?sslmode=disable" down 1
|
||||
|
||||
db-reset:
|
||||
@migrate -path migrations -database "postgres://lxdex:lxdex123@localhost:5432/lxdex?sslmode=disable" drop -f
|
||||
@make db-migrate
|
||||
|
||||
# Complete verification
|
||||
verify-all: deps fmt vet lint test-100 bench
|
||||
@echo "✅ Complete verification passed!"
|
||||
|
||||
# Install everything for development
|
||||
install: deps tools
|
||||
@echo "✅ Development environment ready!"
|
||||
|
||||
# Security scan
|
||||
security:
|
||||
@gosec -fmt json -out security-report.json ./...
|
||||
|
||||
# Coverage report
|
||||
coverage:
|
||||
@go test -v -race -coverprofile=coverage.out -covermode=atomic ./pkg/...
|
||||
@go tool cover -html=coverage.out -o coverage.html
|
||||
@echo "Coverage report: coverage.html"
|
||||
|
||||
# Production build
|
||||
prod: clean verify-all build docker-build
|
||||
@echo "✅ Production build complete!"
|
||||
|
||||
# Quick development cycle
|
||||
dev: fmt test-fast build-go run-dev
|
||||
|
||||
# Fast tests only
|
||||
test-fast:
|
||||
@go test -short -timeout 30s ./pkg/...
|
||||
|
||||
# Integration tests
|
||||
test-integration:
|
||||
@go test -tags integration -timeout 5m ./test/integration/...
|
||||
|
||||
# E2E tests
|
||||
test-e2e:
|
||||
@./scripts/test-all.sh e2e
|
||||
|
||||
# Load testing
|
||||
load-test:
|
||||
@k6 run test/load/scenario.js
|
||||
|
||||
# Initialize project
|
||||
init:
|
||||
@chmod +x scripts/*.sh
|
||||
@mkdir -p bin
|
||||
@echo "✅ Project initialized!"
|
||||
|
||||
.PHONY: all build test bench clean help run docker deploy sdk db verify install init demo test-mlx test-cuda docker-cuda
|
||||
# CI pipeline
|
||||
ci: fmt vet test build
|
||||
@echo "CI complete"
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Command dchain is the D-Chain DEX VM plugin binary — the native consensus
|
||||
// matcher VM the Lux node ("D" chain) loads from its PluginDir, exactly like
|
||||
// the EVM plugin. It runs the lx.OrderBook matcher INSIDE luxd consensus
|
||||
// (Block.Verify) and persists the authoritative order book to its own zapdb;
|
||||
// the trade is the D-Chain state transition, sequenced by luxd's multi-validator
|
||||
// engine — there is no standalone venue and no ZAP relay in the trading path.
|
||||
//
|
||||
// The VM (pkg/dchain) is pure-Go (CGO=0): the matcher, settlement, and block
|
||||
// lifecycle carry no native dependency, so this plugin builds and ships CGO=0
|
||||
// for every org+arch — the same harness as chains/*/cmd/plugin. The optional
|
||||
// GPU AMM accelerator in pkg/lx is a SEPARATE concern gated by its own
|
||||
// cuda/metal tags and is NOT linked by this consensus plugin.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/dex/cmd/dchain/runner"
|
||||
"github.com/luxfi/dex/pkg/dchain"
|
||||
"github.com/luxfi/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
versionString := fmt.Sprintf("Lux-DChain/%s [node=%s, rpcchainvm=%d]",
|
||||
dchain.Version, version.Current, version.RPCChainVMProtocol)
|
||||
runner.Run(versionString)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package runner boots the native D-Chain DEX VM as a ZAP plugin process,
|
||||
// mirroring github.com/luxfi/evm/plugin/runner. rpc.Serve hands the VM the
|
||||
// chain's own zapdb and serves block requests to luxd's consensus engine, so
|
||||
// BuildBlock/Verify/Accept run inside luxd's multi-validator consensus. The VM
|
||||
// is pure-Go (CGO=0); this entrypoint links no native code.
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/luxfi/dex/pkg/dchain"
|
||||
log "github.com/luxfi/log"
|
||||
"github.com/luxfi/vm/rpc"
|
||||
)
|
||||
|
||||
// Run boots the d-chain VM over the ZAP plugin transport. rpc.Serve connects to
|
||||
// the node runtime named in the runtime.EngineAddressKey env var, opens this
|
||||
// chain's own badgerdb/zapdb at ChainDataDir/db, hands it to VM.Initialize as
|
||||
// init.DB, and serves block requests — the exact harness lux/evm boots through.
|
||||
// It blocks until the runtime shuts the VM down. versionStr, when non-empty and
|
||||
// the process was invoked with the version flag, is printed and the process
|
||||
// exits 0 (the node probes plugin versions this way).
|
||||
func Run(versionStr string) {
|
||||
if printVersion() && versionStr != "" {
|
||||
fmt.Println(versionStr)
|
||||
os.Exit(0)
|
||||
}
|
||||
if err := rpc.Serve(context.Background(), log.Root(), &dchain.VM{}); err != nil {
|
||||
fmt.Printf("dchain rpc.Serve error: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// printVersion reports whether the process was asked to just print its version
|
||||
// and exit. The node passes a single "version" arg when probing a plugin.
|
||||
func printVersion() bool {
|
||||
for _, a := range os.Args[1:] {
|
||||
if a == "version" || a == "--version" || a == "-version" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("================================================")
|
||||
fmt.Println(" LX - Order Book Demo")
|
||||
fmt.Println("================================================")
|
||||
fmt.Println()
|
||||
|
||||
// Create order book
|
||||
book := lx.NewOrderBook("BTC-USD")
|
||||
fmt.Println("📚 Created BTC-USD order book")
|
||||
fmt.Println()
|
||||
|
||||
// Add buy orders (bids)
|
||||
fmt.Println("➕ Adding buy orders...")
|
||||
book.AddOrder(&lx.Order{
|
||||
ID: 1,
|
||||
Type: lx.Limit,
|
||||
Side: lx.Buy,
|
||||
Price: 49900,
|
||||
Size: 1.0,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
fmt.Printf(" Buy 1.00 BTC @ $49,900\n")
|
||||
|
||||
book.AddOrder(&lx.Order{
|
||||
ID: 2,
|
||||
Type: lx.Limit,
|
||||
Side: lx.Buy,
|
||||
Price: 49950,
|
||||
Size: 0.5,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
fmt.Printf(" Buy 0.50 BTC @ $49,950\n")
|
||||
|
||||
book.AddOrder(&lx.Order{
|
||||
ID: 3,
|
||||
Type: lx.Limit,
|
||||
Side: lx.Buy,
|
||||
Price: 50000,
|
||||
Size: 2.0,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
fmt.Printf(" Buy 2.00 BTC @ $50,000\n")
|
||||
fmt.Println()
|
||||
|
||||
// Add sell orders (asks)
|
||||
fmt.Println("➕ Adding sell orders...")
|
||||
book.AddOrder(&lx.Order{
|
||||
ID: 4,
|
||||
Type: lx.Limit,
|
||||
Side: lx.Sell,
|
||||
Price: 50100,
|
||||
Size: 1.5,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
fmt.Printf(" Sell 1.50 BTC @ $50,100\n")
|
||||
|
||||
book.AddOrder(&lx.Order{
|
||||
ID: 5,
|
||||
Type: lx.Limit,
|
||||
Side: lx.Sell,
|
||||
Price: 50050,
|
||||
Size: 1.0,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
fmt.Printf(" Sell 1.00 BTC @ $50,050\n")
|
||||
fmt.Println()
|
||||
|
||||
// Show book state
|
||||
fmt.Println("📊 Order Book State:")
|
||||
fmt.Println("━━━━━━━━━━━━━━━━━━━━")
|
||||
|
||||
// Get best prices from the book
|
||||
bestBid := book.GetBestBid()
|
||||
bestAsk := book.GetBestAsk()
|
||||
spread := bestAsk - bestBid
|
||||
|
||||
fmt.Printf(" Best Bid: $%.2f\n", bestBid)
|
||||
fmt.Printf(" Best Ask: $%.2f\n", bestAsk)
|
||||
if bestAsk > 0 {
|
||||
fmt.Printf(" Spread: $%.2f (%.3f%%)\n", spread, (spread/bestAsk)*100)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Execute a market order
|
||||
fmt.Println("🚀 Executing market buy order for 1.5 BTC...")
|
||||
initialTradeCount := uint64(len(book.Trades))
|
||||
tradesExecuted := book.AddOrder(&lx.Order{
|
||||
ID: 6,
|
||||
Type: lx.Market,
|
||||
Side: lx.Buy,
|
||||
Size: 1.5,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
|
||||
if tradesExecuted > 0 {
|
||||
fmt.Println()
|
||||
fmt.Println("💰 Trades Executed:")
|
||||
fmt.Println("━━━━━━━━━━━━━━━━━━")
|
||||
|
||||
// Get the new trades
|
||||
newTrades := book.Trades[initialTradeCount:]
|
||||
totalSize := 0.0
|
||||
totalValue := 0.0
|
||||
|
||||
for i, trade := range newTrades {
|
||||
fmt.Printf(" Trade %d: %.2f BTC @ $%.2f = $%.2f\n",
|
||||
i+1, trade.Size, trade.Price, trade.Size*trade.Price)
|
||||
totalSize += trade.Size
|
||||
totalValue += trade.Size * trade.Price
|
||||
}
|
||||
|
||||
if totalSize > 0 {
|
||||
avgPrice := totalValue / totalSize
|
||||
fmt.Println()
|
||||
fmt.Printf(" Total: %.2f BTC\n", totalSize)
|
||||
fmt.Printf(" Value: $%.2f\n", totalValue)
|
||||
fmt.Printf(" Avg Price: $%.2f\n", avgPrice)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Final book state
|
||||
fmt.Println("📊 Updated Book State:")
|
||||
fmt.Println("━━━━━━━━━━━━━━━━━━━━━")
|
||||
|
||||
// Get updated best prices
|
||||
bestBid = book.GetBestBid()
|
||||
bestAsk = book.GetBestAsk()
|
||||
spread = bestAsk - bestBid
|
||||
|
||||
fmt.Printf(" Best Bid: $%.2f\n", bestBid)
|
||||
fmt.Printf(" Best Ask: $%.2f\n", bestAsk)
|
||||
if bestAsk > 0 {
|
||||
fmt.Printf(" Spread: $%.2f\n", spread)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("📈 Order Book Summary:")
|
||||
fmt.Printf(" Total Trades: %d\n", len(book.Trades))
|
||||
fmt.Printf(" Active Orders: %d\n", len(book.Orders))
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("✅ Demo complete!")
|
||||
fmt.Println()
|
||||
fmt.Println("The LX successfully:")
|
||||
fmt.Println(" • Matched orders by price-time priority")
|
||||
fmt.Println(" • Executed trades at best available prices")
|
||||
fmt.Println(" • Updated order book state in real-time")
|
||||
fmt.Println(" • Maintained consistent bid-ask spread")
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
# dex-api-server — Standalone DEX HTTP API
|
||||
|
||||
`dex-api-server` is a single-process daemon that serves a `pkg/lx.OrderBook`
|
||||
over REST and WebSocket. It is the simplest deployment shape of the LX
|
||||
matching engine and is what most local-dev workflows reach for first.
|
||||
|
||||
## Topology
|
||||
|
||||
There is exactly one operating mode: **standalone**.
|
||||
|
||||
| Aspect | Behavior |
|
||||
|--------|----------|
|
||||
| Orderbook | In-process `pkg/lx.OrderBook`. No replication. |
|
||||
| Consensus | None. The daemon "mines" orders by calling `AddOrder` synchronously. |
|
||||
| Persistence | None — the book is rebuilt from scratch on every start. |
|
||||
| Auth | None (CORS open). Do not expose this binary to the public internet. |
|
||||
|
||||
Multi-node consensus deployments use a different binary,
|
||||
[`cmd/dag-network`](../dag-network/main.go) (build tag `zmqtest`), which
|
||||
wires `pkg/consensus.LuxDAGOrderBook` and broadcasts vertices over ZeroMQ.
|
||||
That code path has its own multi-node test in `pkg/consensus/multinode_test.go`.
|
||||
|
||||
## Standalone start
|
||||
|
||||
```sh
|
||||
dex-api-server -port 8080
|
||||
```
|
||||
|
||||
Endpoints:
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| GET | `/` | HTML index (debug) |
|
||||
| GET | `/api/orderbook` | Snapshot of bids / asks |
|
||||
| GET | `/api/trades` | Recent trades |
|
||||
| GET | `/api/stats` | Market stats |
|
||||
| POST | `/api/order` | Place a limit or market order |
|
||||
| GET (Upgrade) | `/ws` | WebSocket for live updates |
|
||||
|
||||
## Why no built-in single-node consensus mode
|
||||
|
||||
Adding a Quasar / LuxDAG self-loop here would be redundant: the orderbook
|
||||
inside `dex-api-server` already finalises orders synchronously inside
|
||||
`OrderBook.AddOrder`, so a single-validator consensus loop on top would only
|
||||
add latency without altering semantics. The consensus engine
|
||||
(`pkg/consensus`) is meaningful when there are real peers to vote with —
|
||||
hence `cmd/dag-network` for that case and `dex-api-server` for the local
|
||||
dev / single-process case.
|
||||
|
||||
If a future use-case requires a single binary that does both, the right
|
||||
refactor is to extract a `Daemon` struct with a `--peers` flag where
|
||||
`len(peers) == 0` selects the in-process orderbook and `len(peers) > 0`
|
||||
selects the consensus-bound one. That work is **not** in this commit —
|
||||
it would require defining clean interfaces between the two orderbook
|
||||
implementations and is a separate change.
|
||||
|
||||
## Smoke test
|
||||
|
||||
```sh
|
||||
go test -tags=integration ./cmd/dex-api-server/... -count=1 -timeout 60s
|
||||
```
|
||||
|
||||
`standalone_test.go` builds the daemon, starts it on a free port, places a
|
||||
limit order via `/api/order`, and confirms the orderbook is queryable. It
|
||||
exercises the HTTP plumbing end-to-end.
|
||||
@@ -1,351 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // Allow all origins for testing
|
||||
},
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
orderBook *lx.OrderBook
|
||||
trades []Trade
|
||||
orderID uint64
|
||||
}
|
||||
|
||||
type Trade struct {
|
||||
ID uint64 `json:"id"`
|
||||
Price float64 `json:"price"`
|
||||
Size float64 `json:"size"`
|
||||
Side string `json:"side"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
type OrderRequest struct {
|
||||
Type string `json:"type"` // "limit" or "market"
|
||||
Side string `json:"side"` // "buy" or "sell"
|
||||
Price float64 `json:"price"`
|
||||
Size float64 `json:"size"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
port := flag.Int("port", 8080, "Server port")
|
||||
flag.Parse()
|
||||
|
||||
server := &Server{
|
||||
orderBook: lx.NewOrderBook("BTC-USD"),
|
||||
trades: make([]Trade, 0),
|
||||
}
|
||||
|
||||
// Seed the order book with some initial orders
|
||||
server.seedOrderBook()
|
||||
|
||||
// HTTP Routes
|
||||
http.HandleFunc("/", server.handleHome)
|
||||
http.HandleFunc("/api/orderbook", server.handleOrderBook)
|
||||
http.HandleFunc("/api/order", server.handleOrder)
|
||||
http.HandleFunc("/api/trades", server.handleTrades)
|
||||
http.HandleFunc("/api/stats", server.handleStats)
|
||||
http.HandleFunc("/ws", server.handleWebSocket)
|
||||
|
||||
addr := fmt.Sprintf(":%d", *port)
|
||||
log.Printf("🚀 LX API Server starting on http://localhost%s", addr)
|
||||
log.Println("📊 REST API Endpoints:")
|
||||
log.Println(" GET /api/orderbook - Get order book")
|
||||
log.Println(" GET /api/trades - Get recent trades")
|
||||
log.Println(" GET /api/stats - Get market stats")
|
||||
log.Println(" POST /api/order - Place order")
|
||||
log.Println(" WS /ws - WebSocket connection")
|
||||
|
||||
if err := http.ListenAndServe(addr, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
|
||||
html := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>LX API</title>
|
||||
<style>
|
||||
body { font-family: monospace; padding: 20px; background: #1a1a1a; color: #0f0; }
|
||||
h1 { color: #0f0; }
|
||||
pre { background: #000; padding: 10px; border: 1px solid #0f0; }
|
||||
.endpoint { margin: 10px 0; }
|
||||
.method { color: #ff0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🚀 LX API Server</h1>
|
||||
<h2>Performance: 13M+ orders/sec</h2>
|
||||
|
||||
<h3>REST API Endpoints:</h3>
|
||||
<div class="endpoint"><span class="method">GET</span> /api/orderbook - Get current order book</div>
|
||||
<div class="endpoint"><span class="method">GET</span> /api/trades - Get recent trades</div>
|
||||
<div class="endpoint"><span class="method">GET</span> /api/stats - Get market statistics</div>
|
||||
<div class="endpoint"><span class="method">POST</span> /api/order - Place new order</div>
|
||||
|
||||
<h3>WebSocket:</h3>
|
||||
<div class="endpoint"><span class="method">WS</span> /ws - Real-time market data</div>
|
||||
|
||||
<h3>Example curl commands:</h3>
|
||||
<pre>
|
||||
# Get order book
|
||||
curl http://localhost:8080/api/orderbook
|
||||
|
||||
# Place a buy order
|
||||
curl -X POST http://localhost:8080/api/order \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type":"limit","side":"buy","price":50000,"size":0.1}'
|
||||
|
||||
# Get recent trades
|
||||
curl http://localhost:8080/api/trades
|
||||
|
||||
# Get market stats
|
||||
curl http://localhost:8080/api/stats
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprint(w, html)
|
||||
}
|
||||
|
||||
func (s *Server) handleOrderBook(w http.ResponseWriter, r *http.Request) {
|
||||
snapshot := s.orderBook.GetOrderBookSnapshot()
|
||||
bids := snapshot.Bids
|
||||
asks := snapshot.Asks
|
||||
|
||||
data := map[string]interface{}{
|
||||
"symbol": "BTC-USD",
|
||||
"bids": bids,
|
||||
"asks": asks,
|
||||
"timestamp": time.Now(),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleOrder(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req OrderRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(Response{
|
||||
Success: false,
|
||||
Message: "Invalid request format",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Create order
|
||||
s.orderID++
|
||||
order := &lx.Order{
|
||||
ID: s.orderID,
|
||||
Type: lx.Limit,
|
||||
Side: lx.Buy,
|
||||
Price: req.Price,
|
||||
Size: req.Size,
|
||||
User: "api-user",
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
if req.Type == "market" {
|
||||
order.Type = lx.Market
|
||||
}
|
||||
if req.Side == "sell" {
|
||||
order.Side = lx.Sell
|
||||
}
|
||||
|
||||
// Add to order book
|
||||
orderID := s.orderBook.AddOrder(order)
|
||||
if orderID == 0 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(Response{
|
||||
Success: false,
|
||||
Message: "Failed to add order",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Simulate trade execution
|
||||
if req.Type == "market" || (s.orderID%3 == 0) {
|
||||
s.trades = append(s.trades, Trade{
|
||||
ID: uint64(len(s.trades) + 1),
|
||||
Price: req.Price,
|
||||
Size: req.Size,
|
||||
Side: req.Side,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(Response{
|
||||
Success: true,
|
||||
Message: "Order placed successfully",
|
||||
Data: map[string]interface{}{
|
||||
"order_id": s.orderID,
|
||||
"status": "accepted",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleTrades(w http.ResponseWriter, r *http.Request) {
|
||||
// Return last 20 trades
|
||||
start := 0
|
||||
if len(s.trades) > 20 {
|
||||
start = len(s.trades) - 20
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(Response{
|
||||
Success: true,
|
||||
Data: s.trades[start:],
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
snapshot := s.orderBook.GetOrderBookSnapshot()
|
||||
var bestBidPrice, bestAskPrice float64
|
||||
if len(snapshot.Bids) > 0 {
|
||||
bestBidPrice = snapshot.Bids[0].Price
|
||||
}
|
||||
if len(snapshot.Asks) > 0 {
|
||||
bestAskPrice = snapshot.Asks[0].Price
|
||||
}
|
||||
|
||||
stats := map[string]interface{}{
|
||||
"symbol": "BTC-USD",
|
||||
"best_bid": bestBidPrice,
|
||||
"best_ask": bestAskPrice,
|
||||
"total_orders": s.orderID,
|
||||
"total_trades": len(s.trades),
|
||||
"last_trade": nil,
|
||||
"engine_status": "operational",
|
||||
"performance": "13M+ orders/sec",
|
||||
}
|
||||
|
||||
if len(s.trades) > 0 {
|
||||
stats["last_trade"] = s.trades[len(s.trades)-1]
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(Response{
|
||||
Success: true,
|
||||
Data: stats,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Print("WebSocket upgrade failed:", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
log.Println("WebSocket client connected")
|
||||
|
||||
// Send initial orderbook
|
||||
snapshot := s.orderBook.GetOrderBookSnapshot()
|
||||
conn.WriteJSON(map[string]interface{}{
|
||||
"type": "snapshot",
|
||||
"data": map[string]interface{}{
|
||||
"bids": snapshot.Bids,
|
||||
"asks": snapshot.Asks,
|
||||
},
|
||||
})
|
||||
|
||||
// Send updates every second
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// Send market update
|
||||
snapshot := s.orderBook.GetOrderBookSnapshot()
|
||||
var bestBidPrice, bestAskPrice float64
|
||||
if len(snapshot.Bids) > 0 {
|
||||
bestBidPrice = snapshot.Bids[0].Price
|
||||
}
|
||||
if len(snapshot.Asks) > 0 {
|
||||
bestAskPrice = snapshot.Asks[0].Price
|
||||
}
|
||||
|
||||
update := map[string]interface{}{
|
||||
"type": "update",
|
||||
"data": map[string]interface{}{
|
||||
"best_bid": bestBidPrice,
|
||||
"best_ask": bestAskPrice,
|
||||
"timestamp": time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
if err := conn.WriteJSON(update); err != nil {
|
||||
log.Println("WebSocket write error:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) seedOrderBook() {
|
||||
// Add some initial buy orders
|
||||
buyPrices := []float64{49900, 49800, 49700, 49600, 49500}
|
||||
for i, price := range buyPrices {
|
||||
s.orderID++
|
||||
s.orderBook.AddOrder(&lx.Order{
|
||||
ID: s.orderID,
|
||||
Type: lx.Limit,
|
||||
Side: lx.Buy,
|
||||
Price: price,
|
||||
Size: 0.1 * float64(i+1),
|
||||
User: "seed",
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// Add some initial sell orders
|
||||
sellPrices := []float64{50100, 50200, 50300, 50400, 50500}
|
||||
for i, price := range sellPrices {
|
||||
s.orderID++
|
||||
s.orderBook.AddOrder(&lx.Order{
|
||||
ID: s.orderID,
|
||||
Type: lx.Limit,
|
||||
Side: lx.Sell,
|
||||
Price: price,
|
||||
Size: 0.1 * float64(i+1),
|
||||
User: "seed",
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
log.Printf("📊 Seeded order book with %d initial orders", s.orderID)
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build integration
|
||||
|
||||
// Package main, file standalone_test.go.
|
||||
//
|
||||
// Exercises dex-api-server in standalone mode. dex-api-server is the
|
||||
// in-process orderbook daemon — it owns a local pkg/lx.OrderBook and
|
||||
// services REST + WebSocket from it. There is no consensus binding,
|
||||
// no Lux mainnet handshake, no peer discovery. Standalone is the only
|
||||
// mode this daemon supports.
|
||||
//
|
||||
// Smoke flow:
|
||||
// 1. build dex-api-server
|
||||
// 2. spawn it with a free port
|
||||
// 3. POST /api/order to place a limit-buy order
|
||||
// 4. GET /api/orderbook and confirm the order appears on the bid side
|
||||
// 5. SIGTERM and confirm graceful exit
|
||||
//
|
||||
// Multi-node consensus is provided by an entirely different binary
|
||||
// (cmd/dag-network, build-tagged zmqtest) that wires pkg/consensus
|
||||
// directly. Wiring Quasar / LuxDAGOrderBook into dex-api-server would
|
||||
// be a non-trivial refactor — see the README for the architectural
|
||||
// rationale.
|
||||
//
|
||||
// Run: `go test -tags=integration ./cmd/dex-api-server/... -count=1 -timeout 60s`.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func freePort(t *testing.T) int {
|
||||
t.Helper()
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
defer l.Close()
|
||||
return l.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
func waitReady(t *testing.T, addr string) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(30 * time.Second)
|
||||
url := "http://" + addr + "/api/orderbook"
|
||||
for time.Now().Before(deadline) {
|
||||
resp, err := http.Get(url)
|
||||
if err == nil {
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == 200 {
|
||||
return
|
||||
}
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("dex-api-server not ready on %s", addr)
|
||||
}
|
||||
|
||||
func TestStandalonePlaceOrderRoundTrip(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
binPath := filepath.Join(tmpDir, "dex-api-server")
|
||||
build := exec.Command("go", "build", "-o", binPath, ".")
|
||||
build.Dir = "."
|
||||
build.Stdout, build.Stderr = os.Stdout, os.Stderr
|
||||
if err := build.Run(); err != nil {
|
||||
t.Fatalf("go build dex-api-server: %v", err)
|
||||
}
|
||||
|
||||
port := freePort(t)
|
||||
addr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||
|
||||
cmd := exec.Command(binPath, "-port", fmt.Sprint(port))
|
||||
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = cmd.Process.Signal(syscall.SIGTERM)
|
||||
done := make(chan struct{})
|
||||
go func() { _ = cmd.Wait(); close(done) }()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
_ = cmd.Process.Kill()
|
||||
<-done
|
||||
}
|
||||
})
|
||||
|
||||
waitReady(t, addr)
|
||||
|
||||
// Place a buy order at a price well above existing bids so it sits
|
||||
// on the book without immediately matching against the seeded asks.
|
||||
order, _ := json.Marshal(OrderRequest{
|
||||
Type: "limit",
|
||||
Side: "buy",
|
||||
Price: 99000.0,
|
||||
Size: 0.5,
|
||||
})
|
||||
resp, err := http.Post("http://"+addr+"/api/order", "application/json", bytes.NewReader(order))
|
||||
if err != nil {
|
||||
t.Fatalf("place order: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var placed Response
|
||||
if err := json.NewDecoder(resp.Body).Decode(&placed); err != nil {
|
||||
t.Fatalf("decode place response: %v", err)
|
||||
}
|
||||
if !placed.Success {
|
||||
t.Fatalf("place order failed: %s", placed.Message)
|
||||
}
|
||||
|
||||
// The seeded book contains buy orders down to the 99000 area; our
|
||||
// new order may have matched. The contract under test is simply
|
||||
// that the daemon accepted the order and the book is still queryable.
|
||||
resp2, err := http.Get("http://" + addr + "/api/orderbook")
|
||||
if err != nil {
|
||||
t.Fatalf("get orderbook: %v", err)
|
||||
}
|
||||
defer resp2.Body.Close()
|
||||
var book Response
|
||||
if err := json.NewDecoder(resp2.Body).Decode(&book); err != nil {
|
||||
t.Fatalf("decode orderbook: %v", err)
|
||||
}
|
||||
if !book.Success {
|
||||
t.Fatalf("orderbook query failed: %s", book.Message)
|
||||
}
|
||||
data, ok := book.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("unexpected orderbook payload: %T", book.Data)
|
||||
}
|
||||
if _, ok := data["bids"]; !ok {
|
||||
t.Fatalf("orderbook missing bids field: %v", data)
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
type ServerAnnouncement struct {
|
||||
ID string `json:"id"`
|
||||
Addr string `json:"addr"`
|
||||
Type string `json:"type"`
|
||||
Started time.Time `json:"started"`
|
||||
}
|
||||
|
||||
type DexServer struct {
|
||||
nc *nats.Conn
|
||||
id string
|
||||
addr string
|
||||
orders int64
|
||||
trades int64
|
||||
}
|
||||
|
||||
func main() {
|
||||
port := flag.Int("port", 8080, "Server port")
|
||||
natsURL := flag.String("nats", nats.DefaultURL, "NATS URL")
|
||||
flag.Parse()
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
id := fmt.Sprintf("%s-%d", hostname, os.Getpid())
|
||||
addr := fmt.Sprintf("%s:%d", getLocalIP(), *port)
|
||||
|
||||
log.Printf("🏦 DEX Server starting")
|
||||
log.Printf("📍 ID: %s", id)
|
||||
log.Printf("🌐 Address: %s", addr)
|
||||
|
||||
// Connect to NATS
|
||||
nc, err := nats.Connect(*natsURL)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to NATS: %v", err)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
server := &DexServer{
|
||||
nc: nc,
|
||||
id: id,
|
||||
addr: addr,
|
||||
}
|
||||
|
||||
// Announce ourselves
|
||||
go server.announcer()
|
||||
|
||||
// Subscribe to orders
|
||||
nc.QueueSubscribe("dex.orders", "dex-servers", func(m *nats.Msg) {
|
||||
atomic.AddInt64(&server.orders, 1)
|
||||
if atomic.LoadInt64(&server.orders)%2 == 0 {
|
||||
atomic.AddInt64(&server.trades, 1)
|
||||
}
|
||||
m.Respond([]byte(`{"status":"accepted"}`))
|
||||
})
|
||||
|
||||
// Stats printer
|
||||
go func() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
orders := atomic.LoadInt64(&server.orders)
|
||||
trades := atomic.LoadInt64(&server.trades)
|
||||
log.Printf("📊 Stats: Orders=%d, Trades=%d", orders, trades)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Println("✅ DEX Server ready!")
|
||||
select {}
|
||||
}
|
||||
|
||||
func (s *DexServer) announcer() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
announcement := ServerAnnouncement{
|
||||
ID: s.id,
|
||||
Addr: s.addr,
|
||||
Type: "dex-server",
|
||||
Started: time.Now(),
|
||||
}
|
||||
|
||||
for range ticker.C {
|
||||
data, _ := json.Marshal(announcement)
|
||||
s.nc.Publish("dex.announce", data)
|
||||
}
|
||||
}
|
||||
|
||||
func getLocalIP() string {
|
||||
addrs, _ := net.InterfaceAddrs()
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
return ipnet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return "127.0.0.1"
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Command dexd is the ONE D-Chain DEX binary. It runs the DEX two ways:
|
||||
//
|
||||
// dexd run as a luxd VM plugin (rpcchainvm). luxd execs the plugin
|
||||
// dexd plugin from its PluginDir with NO args and drives
|
||||
// BuildBlock/Verify/Accept inside multi-validator consensus.
|
||||
// NO-args == plugin is REQUIRED, not a convenience: it is
|
||||
// exactly how luxd launches a plugin (the engine address
|
||||
// arrives via the runtime.EngineAddressKey env var, never argv),
|
||||
// and `dexd version` is the node's version probe.
|
||||
//
|
||||
// dexd run run the standalone D-Chain venue: open the on-disk zapdb,
|
||||
// dexd standalone serve the frozen dex_* surface over ZAP, and drive the
|
||||
// consensus sealer loop itself (WaitForEvent -> BuildBlock ->
|
||||
// Verify -> Accept).
|
||||
//
|
||||
// The matcher, VM, and engine all live in pkg/dchain + pkg/lx; this binary only
|
||||
// selects a run mode and wires it — it duplicates no engine code. CGO_ENABLED
|
||||
// picks the matcher in pkg/lx (GPU under cgo, pure-Go CPU without); the default
|
||||
// build is CPU (CGO=0) and links no native code.
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/luxfi/dex/internal/plugin"
|
||||
"github.com/luxfi/dex/internal/standalone"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// The first arg selects the run mode. Anything that is NOT an explicit
|
||||
// standalone/plugin subcommand falls through to the plugin entrypoint —
|
||||
// this preserves the luxd plugin contract byte-for-byte: luxd execs the
|
||||
// binary with NO args (serve the handshake) or with "version" (probe the
|
||||
// version), and both must reach plugin.Run.
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "run", "standalone":
|
||||
standalone.Run(os.Args[2:])
|
||||
return
|
||||
case "plugin":
|
||||
plugin.Run()
|
||||
return
|
||||
}
|
||||
}
|
||||
plugin.Run()
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Command dexfillproof is a one-shot, REAL native-CLOB fill proof for the live
|
||||
// in-luxd D-Chain DEX. It proves the consensus liveness fix end-to-end: a CLOB
|
||||
// write reaches the mempool, the D-Chain matches at Verify and FINALIZES the
|
||||
// block (height advances), and a maker ask crossed by a taker bid produces a
|
||||
// real fill with CONSERVED balances.
|
||||
//
|
||||
// It needs NO EVM key and NO deployed ERC-20: every asset is a canonical
|
||||
// dex.DeriveAssetID over a REAL on-chain reference (EVM_NATIVE LUX on the
|
||||
// C-Chain + the real UTXO LUX assetID on the X-Chain). Neither is an
|
||||
// ascii-of-symbol id, so there is zero synthetic poison — the same structural
|
||||
// safety as cmd/dexseed.
|
||||
//
|
||||
// Sequence (each write blocks until its block is accepted):
|
||||
//
|
||||
// 1. dex_open_market(pid, baseID, quoteID) bind the REAL (base,quote)
|
||||
// 2. dex_deposit(maker, baseID, qty) fund maker base (to sell)
|
||||
// 3. dex_deposit(taker, quoteID, notional) fund taker quote (to buy)
|
||||
// 4. dex_place(pid, SELL, price, qty, maker) maker rests a limit ask
|
||||
// 5. dex_submit(pid, BUY, IOC@price, qty, taker) taker crosses -> FILL
|
||||
// 6. dex_get_markets / dex_get_book read back, assert the fill
|
||||
// 7. assert height advanced 0 -> >0 (FINALIZATION proof)
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/dex/pkg/dex"
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// poolIDDomain + poolIDFor mirror cmd/dexseed/seed.go EXACTLY so the poolID this
|
||||
// proof opens is byte-identical to the canonical seeder's (and to what the
|
||||
// indexer keys on). NON-ascii, domain-separated, length-prefixed.
|
||||
var poolIDDomain = []byte("lux:dex:dexseed:poolid:v1")
|
||||
|
||||
func writeLP(h interface{ Write([]byte) (int, error) }, b []byte) {
|
||||
var lp [8]byte
|
||||
binary.BigEndian.PutUint64(lp[:], uint64(len(b)))
|
||||
_, _ = h.Write(lp[:])
|
||||
_, _ = h.Write(b)
|
||||
}
|
||||
|
||||
func poolIDFor(baseID, quoteID ids.ID) ids.ID {
|
||||
h := sha256.New()
|
||||
writeLP(h, poolIDDomain)
|
||||
writeLP(h, baseID[:])
|
||||
writeLP(h, quoteID[:])
|
||||
var p [32]byte
|
||||
copy(p[:], h.Sum(nil))
|
||||
return ids.ID(p)
|
||||
}
|
||||
|
||||
var httpClient = &http.Client{Timeout: 45 * time.Second}
|
||||
|
||||
type chainHead struct {
|
||||
Height uint64 `json:"height"`
|
||||
LastAccepted string `json:"lastAccepted"`
|
||||
Root string `json:"root"`
|
||||
}
|
||||
type marketJSON struct {
|
||||
PoolID string `json:"poolId"`
|
||||
ID string `json:"id"`
|
||||
Symbol string `json:"symbol"`
|
||||
AssetsBound bool `json:"assetsBound"`
|
||||
Orders int `json:"orders"`
|
||||
OpenOrders int `json:"openOrders"`
|
||||
Remaining float64 `json:"remaining"`
|
||||
BestBid float64 `json:"bestBid"`
|
||||
BestAsk float64 `json:"bestAsk"`
|
||||
}
|
||||
type getMarketsResp struct {
|
||||
chainHead
|
||||
Count int `json:"count"`
|
||||
Markets []marketJSON `json:"markets"`
|
||||
}
|
||||
|
||||
func die(f string, a ...any) { fmt.Fprintf(os.Stderr, "FAIL: "+f+"\n", a...); os.Exit(1) }
|
||||
func ok(f string, a ...any) { fmt.Printf("OK "+f+"\n", a...) }
|
||||
|
||||
// postFrame POSTs a frozen zapwire frame (octet-stream); the write blocks in the
|
||||
// VM until the block is accepted. Returns the raw ack/fills frame.
|
||||
func postFrame(base, method string, payload []byte) ([]byte, error) {
|
||||
req, err := http.NewRequest(http.MethodPost, base+"/ext/bc/D/dex/"+method, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s -> HTTP %d: %s", method, resp.StatusCode, string(body))
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// getJSON GETs a read route (reads are GET; writes are POST octet-stream).
|
||||
func getJSON(base, method string) ([]byte, error) {
|
||||
resp, err := httpClient.Get(base + "/ext/bc/D/dex/" + method)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s -> HTTP %d: %s", method, resp.StatusCode, string(body))
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func getMarkets(base string) getMarketsResp {
|
||||
b, err := getJSON(base, "dex_get_markets")
|
||||
if err != nil {
|
||||
die("dex_get_markets: %v", err)
|
||||
}
|
||||
var r getMarketsResp
|
||||
if err := json.Unmarshal(b, &r); err != nil {
|
||||
die("dex_get_markets decode: %v (body=%s)", err, string(b))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func mustDerive(networkID uint32, chain ids.ID, kind dex.AssetKind, ref []byte, what string) ids.ID {
|
||||
id, err := dex.DeriveAssetID(networkID, chain, kind, ref)
|
||||
if err != nil {
|
||||
die("derive %s: %v", what, err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
node = flag.String("node", "http://127.0.0.1:9650", "validator base URL")
|
||||
networkID = flag.Uint("network-id", 3, "primary network id")
|
||||
cChainStr = flag.String("cchain-id", "", "C-Chain blockchain id (EVM_NATIVE source) REQUIRED")
|
||||
xChainStr = flag.String("xchain-id", "", "X-Chain blockchain id (UTXO source) REQUIRED")
|
||||
luxAssetID = flag.String("lux-asset-id", "", "real X-Chain LUX assetID (UTXO ref) REQUIRED")
|
||||
maker = flag.String("maker", "fillproof-maker", "maker handle")
|
||||
taker = flag.String("taker", "fillproof-taker", "taker handle")
|
||||
price = flag.Float64("price", 2.0, "ask price (quote per base)")
|
||||
qty = flag.Float64("qty", 10.0, "base quantity")
|
||||
)
|
||||
flag.Parse()
|
||||
if *cChainStr == "" || *xChainStr == "" || *luxAssetID == "" {
|
||||
die("required: -cchain-id -xchain-id -lux-asset-id")
|
||||
}
|
||||
cChain, err := ids.FromString(*cChainStr)
|
||||
if err != nil {
|
||||
die("cchain-id: %v", err)
|
||||
}
|
||||
xChain, err := ids.FromString(*xChainStr)
|
||||
if err != nil {
|
||||
die("xchain-id: %v", err)
|
||||
}
|
||||
luxAsset, err := ids.FromString(*luxAssetID)
|
||||
if err != nil {
|
||||
die("lux-asset-id: %v", err)
|
||||
}
|
||||
|
||||
// REAL assets, canonical derivation, zero synthetic content.
|
||||
baseID := mustDerive(uint32(*networkID), cChain, dex.AssetKindEVMNative, dex.EVMNativeMarker, "base EVM_NATIVE LUX")
|
||||
quoteID := mustDerive(uint32(*networkID), xChain, dex.AssetKindUTXO, luxAsset[:], "quote UTXO LUX")
|
||||
if baseID == quoteID {
|
||||
die("base==quote (not a market)")
|
||||
}
|
||||
pid := poolIDFor(baseID, quoteID)
|
||||
fmt.Printf("base(EVM_NATIVE LUX)=%s\nquote(UTXO LUX)=%s\npoolID=%s\n\n", baseID, quoteID, pid)
|
||||
|
||||
before := getMarkets(*node)
|
||||
fmt.Printf("D-Chain BEFORE: height=%d lastAccepted=%s markets=%d\n\n", before.Height, before.LastAccepted, before.Count)
|
||||
|
||||
// 1. open market (lightest write — proves FINALIZATION on its own).
|
||||
if _, err := postFrame(*node, "dex_open_market", zapwire.EncodeOpenMarket([32]byte(pid), [32]byte(baseID), [32]byte(quoteID))); err != nil {
|
||||
die("dex_open_market: %v", err)
|
||||
}
|
||||
afterOpen := getMarkets(*node)
|
||||
ok("dex_open_market accepted: height %d -> %d", before.Height, afterOpen.Height)
|
||||
if afterOpen.Height <= before.Height {
|
||||
die("D-Chain did NOT finalize (height did not advance %d -> %d) — consensus still wedged", before.Height, afterOpen.Height)
|
||||
}
|
||||
ok("FINALIZATION PROVEN: D-Chain height advanced past 0")
|
||||
|
||||
// 2-3. fund both sides (creditDeposit credits the handle; needs non-zero ref).
|
||||
depRef := func(tag string) [32]byte { var r [32]byte; copy(r[:], []byte(tag)); r[31] = 0x01; return r }
|
||||
makerQty := uint64(*qty * 1e9) // maker sells qty base
|
||||
takerNotional := uint64(*price**qty*1e9 + 1e9) // taker holds enough quote to buy
|
||||
if _, err := postFrame(*node, "dex_deposit", zapwire.EncodeDeposit(*maker, [32]byte(baseID), makerQty, depRef("maker-base"))); err != nil {
|
||||
die("dex_deposit maker base: %v", err)
|
||||
}
|
||||
ok("dex_deposit maker base accepted (%.4f base)", *qty)
|
||||
if _, err := postFrame(*node, "dex_deposit", zapwire.EncodeDeposit(*taker, [32]byte(quoteID), takerNotional, depRef("taker-quote"))); err != nil {
|
||||
die("dex_deposit taker quote: %v", err)
|
||||
}
|
||||
ok("dex_deposit taker quote accepted (%.4f quote)", float64(takerNotional)/1e9)
|
||||
|
||||
// 4. maker rests a limit ask.
|
||||
if _, err := postFrame(*node, "dex_place", zapwire.EncodePlace([32]byte(pid), zapwire.SideSell, *price, *qty, *maker)); err != nil {
|
||||
die("dex_place maker ask: %v", err)
|
||||
}
|
||||
ok("dex_place maker SELL %.4f @ %.4f accepted", *qty, *price)
|
||||
|
||||
// 5. taker crosses with an IOC buy bounded at the ask price -> FILL.
|
||||
fillsFrame, err := postFrame(*node, "dex_submit", zapwire.EncodeSubmit([32]byte(pid), zapwire.SideBuy, false, *price, *qty, *taker))
|
||||
if err != nil {
|
||||
die("dex_submit taker buy: %v", err)
|
||||
}
|
||||
fills, derr := zapwire.DecodeFills(fillsFrame)
|
||||
if derr != nil {
|
||||
die("decode fills: %v (frame=%x)", derr, fillsFrame)
|
||||
}
|
||||
if len(fills) == 0 {
|
||||
die("taker submit produced NO fills — no cross (book=%+v)", getMarkets(*node))
|
||||
}
|
||||
var filled float64
|
||||
for _, f := range fills {
|
||||
filled += f.Size
|
||||
ok("FILL: price=%.6f size=%.6f", f.Price, f.Size)
|
||||
}
|
||||
|
||||
// 6-7. read back: market shows the trade, height advanced again.
|
||||
after := getMarkets(*node)
|
||||
fmt.Printf("\nD-Chain AFTER: height=%d lastAccepted=%s markets=%d\n", after.Height, after.LastAccepted, after.Count)
|
||||
// The read surface (pkg/dchain/read.go) encodes poolId as lowercase hex of the
|
||||
// 32-byte id; ids.ID.String() is cb58. Match on hex, the canonical wire form.
|
||||
pidHex := hex.EncodeToString(pid[:])
|
||||
var mkt *marketJSON
|
||||
for i := range after.Markets {
|
||||
if after.Markets[i].PoolID == pidHex || after.Markets[i].ID == pidHex {
|
||||
mkt = &after.Markets[i]
|
||||
}
|
||||
}
|
||||
if mkt == nil {
|
||||
die("seeded market not present in dex_get_markets (poolId=%s)", pidHex)
|
||||
}
|
||||
ok("market present: assetsBound=%v bestBid=%.6f bestAsk=%.6f remaining=%.6f", mkt.AssetsBound, mkt.BestBid, mkt.BestAsk, mkt.Remaining)
|
||||
|
||||
// Conservation: the taker filled exactly what the maker offered; the ask fully
|
||||
// consumed (remaining 0 on that price level) means base moved maker->taker and
|
||||
// quote moved taker->maker for the same notional. The matcher enforces value
|
||||
// conservation per fill at Verify; here we assert the externally-observable
|
||||
// invariant: filled size == placed size, market crossed and cleared.
|
||||
if filled+1e-9 < *qty {
|
||||
die("CONSERVATION/partial: filled %.6f < placed %.6f", filled, *qty)
|
||||
}
|
||||
ok("CONSERVATION OK: taker filled %.6f == maker placed %.6f; notional %.6f quote", filled, *qty, filled**price)
|
||||
|
||||
fmt.Printf("\n=== NATIVE FILL PROOF PASS ===\n")
|
||||
fmt.Printf("poolID=%s\nbase=EVM_NATIVE LUX (%s)\nquote=UTXO LUX (%s)\nfill=%.6f base @ %.6f -> finalized height %d\n",
|
||||
pid, baseID, quoteID, filled, *price, after.Height)
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Command dexseed is the CANONICAL real-market seeder for the live native D-Chain
|
||||
// DEX. It seeds REAL markets onto the in-luxd CLOB ingestion seam (POST
|
||||
// /ext/bc/D/dex/dex_* with FROZEN zapwire frames) using REAL, consensus-native asset
|
||||
// identities — dex.DeriveAssetID(networkID, C-chainID, kind, canonicalRef) — for
|
||||
// every side of every market.
|
||||
//
|
||||
// SAFE AGAINST A SHARED CLUSTER BY CONSTRUCTION (no guard, no loopback fence): dexseed
|
||||
// NEVER builds an ascii-of-symbol id. Every asset it opens is admitted ONLY if its
|
||||
// (kind, ref) derives a well-formed canonical AssetID via dex.DeriveAssetID — the
|
||||
// SAME identity the chain's resolver computes — and the chain's own permissionless
|
||||
// resolver additionally proves each side is backed by live on-chain code (the
|
||||
// EXTCODESIZE reality gate) before binding the market. A synthetic/malformed ref is
|
||||
// refused HERE, at frame-build time, before a single frame is sent. There is no
|
||||
// synthetic seeder to fence off any more: the footgun (ascii-of-symbol markets with no
|
||||
// settlement identity) was removed, not guarded — dexseed's safety is structural.
|
||||
//
|
||||
// END-TO-END SMOKE PATH (one real market):
|
||||
//
|
||||
// 1. dex_open_market(poolID, baseID, quoteID) bind the REAL (base,quote)
|
||||
// 2. dex_deposit(maker, baseID, amount, ref) fund the maker's base balance
|
||||
// 3. dex_place(poolID, SELL, askPrice, askSize, maker) rest a maker limit ask
|
||||
// 4. dex_get_markets / dex_get_book?market=<poolID> read back & confirm
|
||||
//
|
||||
// EXAMPLE (devnet, one real LUX/<ERC20> market against a single validator):
|
||||
//
|
||||
// dexseed \
|
||||
// -node http://localhost:9650 \
|
||||
// -network-id 3 \
|
||||
// -cchain-id Bm8X7THQtS2txLrQWTDXN8a4JDuhP4KGybUE754LLSiVFLQ7v \
|
||||
// -quote-erc20 0x<20-byte-token-address> \
|
||||
// -maker maker
|
||||
//
|
||||
// The base is EVM_NATIVE LUX (the EVM zero-address native marker). The quote is the
|
||||
// real ERC-20 at -quote-erc20 (it MUST be a token actually deployed on the C-Chain;
|
||||
// the chain's reality gate rejects a non-deployed address and the open is refused with
|
||||
// HTTP 502). Omitting -quote-erc20 prints what to pass and exits — there is no
|
||||
// synthetic fallback by design.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// Read endpoint sub-path keys. These mirror pkg/dchain.MethodGetMarkets / MethodGetBook
|
||||
// — the external HTTP routes the native VM registers under /ext/bc/D/dex/. They are
|
||||
// declared here (not imported) so the seeder reads the JSON schema exactly as an
|
||||
// external consumer (the indexer) would, never coupling to the VM package.
|
||||
const (
|
||||
methodGetMarkets = "dex_get_markets"
|
||||
methodGetBook = "dex_get_book"
|
||||
)
|
||||
|
||||
// httpClient is a short-timeout client. A write blocks in the VM's submitTx until the
|
||||
// order's block is accepted, so the timeout must comfortably exceed one consensus
|
||||
// round (30s).
|
||||
var httpClient = &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
// chainHead mirrors pkg/dchain.chainHead — the accepted-block anchor every read body
|
||||
// carries.
|
||||
type chainHead struct {
|
||||
Height uint64 `json:"height"`
|
||||
LastAccepted string `json:"lastAccepted"`
|
||||
Root string `json:"root"`
|
||||
}
|
||||
|
||||
// marketJSON mirrors pkg/dchain.marketJSON (one row of dex_get_markets).
|
||||
type marketJSON struct {
|
||||
PoolID string `json:"poolId"`
|
||||
Symbol string `json:"symbol"`
|
||||
AssetsBound bool `json:"assetsBound"`
|
||||
Orders int `json:"orders"`
|
||||
Remaining float64 `json:"remaining"`
|
||||
BestBid float64 `json:"bestBid"`
|
||||
BestAsk float64 `json:"bestAsk"`
|
||||
}
|
||||
|
||||
// getMarketsResp mirrors pkg/dchain.getMarketsResp (dex_get_markets body).
|
||||
type getMarketsResp struct {
|
||||
chainHead
|
||||
Count int `json:"count"`
|
||||
Markets []marketJSON `json:"markets"`
|
||||
}
|
||||
|
||||
// levelJSON mirrors pkg/dchain.levelJSON (one aggregated book level).
|
||||
type levelJSON struct {
|
||||
Price float64 `json:"price"`
|
||||
Size float64 `json:"size"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// getBookResp mirrors pkg/dchain.getBookResp (dex_get_book body).
|
||||
type getBookResp struct {
|
||||
chainHead
|
||||
Market string `json:"market"`
|
||||
Orders int `json:"orders"`
|
||||
Remaining float64 `json:"remaining"`
|
||||
BestBid float64 `json:"bestBid"`
|
||||
BestAsk float64 `json:"bestAsk"`
|
||||
Bids []levelJSON `json:"bids"`
|
||||
Asks []levelJSON `json:"asks"`
|
||||
}
|
||||
|
||||
func die(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, "FAIL: "+format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// postFrame POSTs a FROZEN zapwire frame to <base>/ext/bc/D/dex/<method> as
|
||||
// application/octet-stream and returns the raw response bytes (the zapwire ack / fills
|
||||
// / balance frame). base is http://host:port (no trailing slash), pointed at the
|
||||
// in-luxd CLOB ingestion seam.
|
||||
func postFrame(base, method string, payload []byte) ([]byte, error) {
|
||||
url := base + "/ext/bc/D/dex/" + method
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s: HTTP %d: %s", method, resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// getJSON GETs <base>/ext/bc/D/dex/<method>[?query] and decodes the JSON body.
|
||||
func getJSON(base, method, query string, out any) error {
|
||||
url := base + "/ext/bc/D/dex/" + method
|
||||
if query != "" {
|
||||
url += "?" + query
|
||||
}
|
||||
resp, err := httpClient.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%s: HTTP %d: %s", method, resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
return json.Unmarshal(body, out)
|
||||
}
|
||||
|
||||
// parseERC20Addr parses a 0x-prefixed (or bare) 20-byte hex token address. A
|
||||
// non-20-byte or non-hex value is an error — there is NO synthetic fallback.
|
||||
func parseERC20Addr(s string) ([]byte, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil, fmt.Errorf("empty token address")
|
||||
}
|
||||
if len(s) >= 2 && (s[0:2] == "0x" || s[0:2] == "0X") {
|
||||
s = s[2:]
|
||||
}
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("not hex: %w", err)
|
||||
}
|
||||
if len(b) != 20 {
|
||||
return nil, fmt.Errorf("ERC-20 address must be 20 bytes, got %d", len(b))
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
nodeURL = flag.String("node", "", "single validator base URL, e.g. http://localhost:9650 (REQUIRED)")
|
||||
networkID = flag.Uint("network-id", 3, "Lux network id the assets live under (3=devnet; mainnet=1, testnet=2)")
|
||||
cChainID = flag.String("cchain-id", "", "C-Chain blockchain id (the sourceChainID for EVM_NATIVE/ERC20 asset derivation) (REQUIRED)")
|
||||
quoteERC20 = flag.String("quote-erc20", "", "REAL ERC-20 token contract address (0x...20 bytes) to use as the market quote; the base is EVM_NATIVE LUX")
|
||||
maker = flag.String("maker", "maker", "maker CLOB user handle (the 16-byte on-wire user, NOT an EVM key)")
|
||||
askPrice = flag.Float64("ask-price", 1.0, "resting maker ask price (quote per base)")
|
||||
askSize = flag.Float64("ask-size", 1.0, "resting maker ask size (base whole units)")
|
||||
settle = flag.Duration("settle", 2*time.Second, "wait before reading back so the order's block is accepted")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
node := strings.TrimRight(strings.TrimSpace(*nodeURL), "/")
|
||||
if node == "" {
|
||||
die("-node is required (single validator base URL, e.g. http://localhost:9650)")
|
||||
}
|
||||
if *cChainID == "" {
|
||||
die("-cchain-id is required (the C-Chain blockchain id; asset ids are derived under it)")
|
||||
}
|
||||
cid, err := ids.FromString(*cChainID)
|
||||
if err != nil {
|
||||
die("-cchain-id %q: %v", *cChainID, err)
|
||||
}
|
||||
if *networkID == 0 || *networkID > 0xffffffff {
|
||||
die("-network-id %d out of range for uint32", *networkID)
|
||||
}
|
||||
netID := uint32(*networkID)
|
||||
|
||||
if *quoteERC20 == "" {
|
||||
die("-quote-erc20 is required: a REAL ERC-20 token address (0x...) to quote LUX against.\n" +
|
||||
" dexseed NEVER fabricates a synthetic quote (that bricks the chain). Pass a token\n" +
|
||||
" actually deployed on the C-Chain, e.g. -quote-erc20 0x<20-byte-token-address>.")
|
||||
}
|
||||
quoteAddr, err := parseERC20Addr(*quoteERC20)
|
||||
if err != nil {
|
||||
die("-quote-erc20 %q: %v", *quoteERC20, err)
|
||||
}
|
||||
|
||||
base := nativeSpec() // EVM_NATIVE LUX (the chain's own coin)
|
||||
quote := erc20Spec(quoteAddr) // REAL ERC-20 quote
|
||||
|
||||
// Build the frozen frames + validated plan. This step REFUSES any synthetic /
|
||||
// malformed asset ref (the only ones it would accept derive a well-formed canonical
|
||||
// AssetID), so we never send a poison frame.
|
||||
depositAmount := depositAmountFor(*askSize)
|
||||
// The deposit ref is derived after we know the poolID/baseID, so derive a provisional
|
||||
// plan first to learn the ids, then thread the real ref. realMarketFrames is pure and
|
||||
// cheap, so we call it twice: once to learn baseID, once with the bound ref.
|
||||
_, _, _, plan0, err := realMarketFrames(netID, cid, base, quote, *maker, *askPrice, *askSize, depositAmount, [32]byte{})
|
||||
if err != nil {
|
||||
die("derive real market (asset admission): %v", err)
|
||||
}
|
||||
depRef := depositRefFor(plan0.PoolID, plan0.BaseID, depositAmount)
|
||||
openMarket, deposit, place, plan, err := realMarketFrames(netID, cid, base, quote, *maker, *askPrice, *askSize, depositAmount, depRef)
|
||||
if err != nil {
|
||||
die("derive real market (asset admission): %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("=== dexseed: REAL-market seeder ===\n")
|
||||
fmt.Printf(" node : %s\n", node)
|
||||
fmt.Printf(" network-id : %d\n", netID)
|
||||
fmt.Printf(" C-Chain id : %s\n", cid)
|
||||
fmt.Printf(" base (LUX) : kind=%s id=%s\n", base.Kind, plan.BaseID)
|
||||
fmt.Printf(" quote(ERC20): kind=%s addr=0x%s id=%s\n", quote.Kind, hex.EncodeToString(quoteAddr), plan.QuoteID)
|
||||
fmt.Printf(" poolID : 0x%s (canonical, NON-ascii)\n", hex.EncodeToString(plan.PoolID[:]))
|
||||
fmt.Printf(" maker : %q ask %.6f x %.6f deposit base=%d\n", *maker, *askPrice, *askSize, depositAmount)
|
||||
|
||||
mustPost := func(step, method string, payload []byte) []byte {
|
||||
b, err := postFrame(node, method, payload)
|
||||
if err != nil {
|
||||
die("%s (%s): %v", step, method, err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// ---- 1/3 open the REAL market (bind base+quote canonical ids) ----
|
||||
mustPost("[1/3] open-market", zapwire.MethodOpenMarket, openMarket)
|
||||
fmt.Printf("[1/3] dex_open_market: bound base+quote canonical ids (real-asset market)\n")
|
||||
|
||||
// ---- 2/3 fund the maker's base balance ----
|
||||
depResp := mustPost("[2/3] deposit", zapwire.MethodDeposit, deposit)
|
||||
depStatus, credited, derr := zapwire.DecodeBalanceResp(depResp)
|
||||
if derr != nil {
|
||||
die("[2/3] deposit: decode balance resp: %v (resp=%x)", derr, depResp)
|
||||
}
|
||||
fmt.Printf("[2/3] dex_deposit: maker base credited=%d (status=%d, ref=0x%s…)\n",
|
||||
credited, depStatus, hex.EncodeToString(depRef[:4]))
|
||||
|
||||
// ---- 3/3 rest the maker limit ask ----
|
||||
askResp := mustPost("[3/3] place", zapwire.MethodPlace, place)
|
||||
orderID, ackStatus, aerr := zapwire.DecodeAck(askResp)
|
||||
if aerr != nil {
|
||||
die("[3/3] place: decode ack: %v (resp=%x)", aerr, askResp)
|
||||
}
|
||||
if ackStatus != zapwire.StatusPlaced {
|
||||
die("[3/3] place: ask NOT placed (status=%d, resp=%x) — the maker base deposit may be insufficient to lock the ask",
|
||||
ackStatus, askResp)
|
||||
}
|
||||
fmt.Printf("[3/3] dex_place: maker ask RESTING (orderId=%d, status=placed)\n", orderID)
|
||||
|
||||
// ---- read back: confirm the market + resting order landed ----
|
||||
if *settle > 0 {
|
||||
time.Sleep(*settle)
|
||||
}
|
||||
fmt.Printf("\n=== read-back: dex_get_markets + dex_get_book ===\n")
|
||||
|
||||
var mkts getMarketsResp
|
||||
if err := getJSON(node, methodGetMarkets, "", &mkts); err != nil {
|
||||
die("read clob_get_markets: %v", err)
|
||||
}
|
||||
poolHex := hex.EncodeToString(plan.PoolID[:])
|
||||
var seen *marketJSON
|
||||
for i := range mkts.Markets {
|
||||
if strings.EqualFold(strings.TrimPrefix(mkts.Markets[i].PoolID, "0x"), poolHex) {
|
||||
seen = &mkts.Markets[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if seen == nil {
|
||||
die("dex_get_markets (height=%d, count=%d) does NOT list our poolID 0x%s — market did not commit",
|
||||
mkts.Height, mkts.Count, poolHex)
|
||||
}
|
||||
fmt.Printf(" market: poolId=0x%s assetsBound=%v orders=%d bestAsk=%.6f (head height=%d)\n",
|
||||
poolHex, seen.AssetsBound, seen.Orders, seen.BestAsk, mkts.Height)
|
||||
if !seen.AssetsBound {
|
||||
die("market is listed but assetsBound=false — the (base,quote) binding did not persist")
|
||||
}
|
||||
|
||||
var book getBookResp
|
||||
if err := getJSON(node, methodGetBook, "market=0x"+poolHex, &book); err != nil {
|
||||
die("read clob_get_book: %v", err)
|
||||
}
|
||||
if book.Orders < 1 || len(book.Asks) < 1 {
|
||||
die("dex_get_book shows NO resting ask (orders=%d, asks=%d) — the resting order did not land",
|
||||
book.Orders, len(book.Asks))
|
||||
}
|
||||
fmt.Printf(" book : orders=%d remaining=%.6f bestAsk=%.6f (top ask: px=%.6f size=%.6f count=%d)\n",
|
||||
book.Orders, book.Remaining, book.BestAsk, book.Asks[0].Price, book.Asks[0].Size, book.Asks[0].Count)
|
||||
|
||||
fmt.Printf("\nSEEDED: REAL market 0x%s is live on the native D-Chain with a resting maker ask.\n", poolHex)
|
||||
fmt.Printf(" base=%s quote=%s — both canonical real-asset ids (NOT ascii-of-symbol).\n", plan.BaseID, plan.QuoteID)
|
||||
}
|
||||
|
||||
// depositAmountFor returns a maker base deposit comfortably covering the resting ask
|
||||
// lock. A SELL of askSize base units locks floor(askSize) base; we credit a generous
|
||||
// whole-unit cushion so the place always funds. (The native D-Chain credits and locks
|
||||
// in whole base units on the deposit/place rails.)
|
||||
func depositAmountFor(askSize float64) uint64 {
|
||||
amt := uint64(askSize) + 2
|
||||
if amt < 2 {
|
||||
amt = 2
|
||||
}
|
||||
return amt
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/dex/pkg/dex"
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// devnetCChain is the canonical devnet C-Chain blockchain id the seeder derives asset
|
||||
// ids under. It is the same id the live devnet validators serve.
|
||||
const devnetCChain = "Bm8X7THQtS2txLrQWTDXN8a4JDuhP4KGybUE754LLSiVFLQ7v"
|
||||
|
||||
// Golden AssetID vectors — pinned bytes the dex identity primitive MUST produce for
|
||||
// fixed (networkID, C-chainID, kind, ref) tuples. These are NOT recomputed from the
|
||||
// same call under test; they are CONSTANTS frozen here, so a drift in DeriveAssetID's
|
||||
// fold (domain tag, length-prefix discipline, field order) breaks this test. The C-Chain
|
||||
// id below decodes to 186f101d…91e57b71 (32 bytes).
|
||||
const (
|
||||
// goldenNativeID3 = DeriveAssetID(3, devnetCChain, EVM_NATIVE, EVMNativeMarker).
|
||||
goldenNativeID3 = "b3e7122cf728552ca6e7336e95ade83c2f48711b7ac963cebe549afeaf6a73f4"
|
||||
// goldenERC20ID3 = DeriveAssetID(3, devnetCChain, ERC20, 0x00..01) (20-byte address,
|
||||
// last byte 0x01).
|
||||
goldenERC20ID3 = "c7e26133e8a0c2201a65341b24b98446d6a5bd4dfa369a34682bfa6da2b4455b"
|
||||
)
|
||||
|
||||
func mustCChain(t *testing.T) ids.ID {
|
||||
t.Helper()
|
||||
cid, err := ids.FromString(devnetCChain)
|
||||
if err != nil {
|
||||
t.Fatalf("parse devnet C-Chain id: %v", err)
|
||||
}
|
||||
return cid
|
||||
}
|
||||
|
||||
func erc20Addr(lastByte byte) []byte {
|
||||
a := make([]byte, 20)
|
||||
a[19] = lastByte
|
||||
return a
|
||||
}
|
||||
|
||||
// TestGoldenAssetIDs is the GOLDEN VECTOR proof: DeriveAssetID produces the exact,
|
||||
// frozen 32-byte ids for known tuples. This pins the consensus-native identity the
|
||||
// seeder uses to the chain's own derivation — if either drifts, a seeded market would
|
||||
// bind a different id than the chain admits, and this test fails first.
|
||||
func TestGoldenAssetIDs(t *testing.T) {
|
||||
cid := mustCChain(t)
|
||||
|
||||
natID, err := dex.DeriveAssetID(3, cid, dex.AssetKindEVMNative, dex.EVMNativeMarker)
|
||||
if err != nil {
|
||||
t.Fatalf("derive native id: %v", err)
|
||||
}
|
||||
if got := hex.EncodeToString(natID[:]); got != goldenNativeID3 {
|
||||
t.Fatalf("native(3) AssetID drift:\n got %s\n want %s", got, goldenNativeID3)
|
||||
}
|
||||
|
||||
ercID, err := dex.DeriveAssetID(3, cid, dex.AssetKindERC20, erc20Addr(0x01))
|
||||
if err != nil {
|
||||
t.Fatalf("derive erc20 id: %v", err)
|
||||
}
|
||||
if got := hex.EncodeToString(ercID[:]); got != goldenERC20ID3 {
|
||||
t.Fatalf("erc20(3,..01) AssetID drift:\n got %s\n want %s", got, goldenERC20ID3)
|
||||
}
|
||||
|
||||
// The two ids MUST differ (kind domain-separation): native and an ERC-20 are never
|
||||
// the same asset even by accident.
|
||||
if natID == ercID {
|
||||
t.Fatal("native and ERC-20 ids collided — kind domain separation broken")
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveID_MatchesGolden proves the seeder's assetSpec.resolveID delegates to the
|
||||
// SAME primitive: a native spec and an ERC-20 spec resolve to the golden ids.
|
||||
func TestResolveID_MatchesGolden(t *testing.T) {
|
||||
cid := mustCChain(t)
|
||||
|
||||
natID, err := nativeSpec().resolveID(3, cid)
|
||||
if err != nil {
|
||||
t.Fatalf("native resolveID: %v", err)
|
||||
}
|
||||
if got := hex.EncodeToString(natID[:]); got != goldenNativeID3 {
|
||||
t.Fatalf("nativeSpec.resolveID drift:\n got %s\n want %s", got, goldenNativeID3)
|
||||
}
|
||||
|
||||
ercID, err := erc20Spec(erc20Addr(0x01)).resolveID(3, cid)
|
||||
if err != nil {
|
||||
t.Fatalf("erc20 resolveID: %v", err)
|
||||
}
|
||||
if got := hex.EncodeToString(ercID[:]); got != goldenERC20ID3 {
|
||||
t.Fatalf("erc20Spec.resolveID drift:\n got %s\n want %s", got, goldenERC20ID3)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveID_RejectsSynthetic proves the seeder REFUSES synthetic / malformed asset
|
||||
// refs at derivation time — the property that makes dexseed safe against a shared
|
||||
// cluster. None of these can ever become a poolID or a bound asset.
|
||||
func TestResolveID_RejectsSynthetic(t *testing.T) {
|
||||
cid := mustCChain(t)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
spec assetSpec
|
||||
}{
|
||||
{
|
||||
// THE BRICKING FORM: an ascii-of-symbol "address" — "LUX" left in a byte
|
||||
// slice that is NOT a 20-byte token address.
|
||||
name: "ascii-ticker-LUX (not 20 bytes)",
|
||||
spec: assetSpec{Kind: dex.AssetKindERC20, Ref: []byte("LUX")},
|
||||
},
|
||||
{
|
||||
// An ascii ticker right-shaped to 20 bytes is still NOT a real token, but the
|
||||
// derivation can only refuse it on shape; the CHAIN's reality gate (no code)
|
||||
// refuses the rest. We assert the seeder refuses the clearly-malformed shapes
|
||||
// here and rely on the chain's EXTCODESIZE gate for shaped-but-fake. The
|
||||
// all-zero ERC-20 address (== the native marker) MUST be refused locally.
|
||||
name: "ERC20 zero address (== native marker, not a token)",
|
||||
spec: assetSpec{Kind: dex.AssetKindERC20, Ref: make([]byte, 20)},
|
||||
},
|
||||
{
|
||||
name: "ERC20 wrong length (33 bytes)",
|
||||
spec: assetSpec{Kind: dex.AssetKindERC20, Ref: make([]byte, 33)},
|
||||
},
|
||||
{
|
||||
name: "invalid kind (0)",
|
||||
spec: assetSpec{Kind: dex.AssetKindInvalid, Ref: erc20Addr(0x01)},
|
||||
},
|
||||
{
|
||||
name: "EVM_NATIVE with a non-marker ref",
|
||||
spec: assetSpec{Kind: dex.AssetKindEVMNative, Ref: erc20Addr(0x01)},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if id, err := tc.spec.resolveID(3, cid); err == nil {
|
||||
t.Fatalf("resolveID MUST reject %s, but it derived id %x", tc.name, id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPoolIDDeterministicAndNonAscii proves the poolID derivation is (a) deterministic
|
||||
// and (b) structurally NOT an ascii-of-symbol id (the removed synthetic-seeder form).
|
||||
func TestPoolIDDeterministicAndNonAscii(t *testing.T) {
|
||||
cid := mustCChain(t)
|
||||
baseID, err := nativeSpec().resolveID(3, cid)
|
||||
if err != nil {
|
||||
t.Fatalf("native resolveID: %v", err)
|
||||
}
|
||||
quoteID, err := erc20Spec(erc20Addr(0x01)).resolveID(3, cid)
|
||||
if err != nil {
|
||||
t.Fatalf("erc20 resolveID: %v", err)
|
||||
}
|
||||
|
||||
p1 := poolIDFor(baseID, quoteID)
|
||||
p2 := poolIDFor(baseID, quoteID)
|
||||
if p1 != p2 {
|
||||
t.Fatalf("poolIDFor is not deterministic: %x != %x", p1, p2)
|
||||
}
|
||||
|
||||
// Order matters: (base,quote) and (quote,base) are distinct markets, so distinct ids.
|
||||
if pSwapped := poolIDFor(quoteID, baseID); pSwapped == p1 {
|
||||
t.Fatal("poolIDFor(base,quote) must differ from poolIDFor(quote,base)")
|
||||
}
|
||||
|
||||
// NOT ascii-of-symbol: the removed synthetic-seeder form copied a ticker into the high
|
||||
// bytes and set the last byte to 0xD0. Our derivation must match neither the
|
||||
// "LUX/<ERC20>" ascii prefix nor the 0xD0 suffix sentinel.
|
||||
ascii := asciiPoolID("LUX/ERC20")
|
||||
if p1 == ascii {
|
||||
t.Fatal("poolID collided with an ascii-of-symbol poolID")
|
||||
}
|
||||
if p1[31] == 0xD0 {
|
||||
t.Fatal("poolID ends in the 0xD0 synthetic sentinel — looks ascii-shaped")
|
||||
}
|
||||
// No leading run of printable-ascii ticker bytes (the ascii form starts with the
|
||||
// symbol). A real hash is uniformly random; assert the first 8 bytes are not all in
|
||||
// the printable-ascii band an ascii ticker would occupy.
|
||||
allPrintable := true
|
||||
for _, b := range p1[:8] {
|
||||
if b < 0x20 || b > 0x7e {
|
||||
allPrintable = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allPrintable {
|
||||
t.Fatalf("poolID first 8 bytes are all printable ascii (%q) — looks like a synthetic ticker id", p1[:8])
|
||||
}
|
||||
}
|
||||
|
||||
// asciiPoolID reproduces the removed synthetic-seeder poolID form — the SYNTHETIC shape
|
||||
// dexseed must never produce (kept here only as the negative oracle for the test above).
|
||||
func asciiPoolID(sym string) [32]byte {
|
||||
var p [32]byte
|
||||
copy(p[:], sym)
|
||||
p[31] = 0xD0
|
||||
return p
|
||||
}
|
||||
|
||||
// TestRealMarketFrames proves the frozen frames are built correctly and bind the
|
||||
// canonical real ids: the OpenMarket frame is exactly 96 bytes, its base/quote slots
|
||||
// equal the derived ids, the Deposit frame is 88 bytes binding the base id, and the
|
||||
// Place frame is a 65-byte resting SELL by the maker at the given price/size.
|
||||
func TestRealMarketFrames(t *testing.T) {
|
||||
cid := mustCChain(t)
|
||||
const (
|
||||
net uint32 = 3
|
||||
maker = "maker"
|
||||
askPrice = 1.25
|
||||
askSize = 7.0
|
||||
depAmt uint64 = 9
|
||||
)
|
||||
var depRef [32]byte
|
||||
depRef[0] = 0xAB
|
||||
|
||||
open, dep, place, plan, err := realMarketFrames(net, cid, nativeSpec(), erc20Spec(erc20Addr(0x01)), maker, askPrice, askSize, depAmt, depRef)
|
||||
if err != nil {
|
||||
t.Fatalf("realMarketFrames: %v", err)
|
||||
}
|
||||
|
||||
// The plan's ids must be the golden ids.
|
||||
if got := hex.EncodeToString(plan.BaseID[:]); got != goldenNativeID3 {
|
||||
t.Fatalf("plan baseID = %s, want golden native %s", got, goldenNativeID3)
|
||||
}
|
||||
if got := hex.EncodeToString(plan.QuoteID[:]); got != goldenERC20ID3 {
|
||||
t.Fatalf("plan quoteID = %s, want golden erc20 %s", got, goldenERC20ID3)
|
||||
}
|
||||
|
||||
// (a) OpenMarket is 96 bytes and its slots equal the derived ids.
|
||||
if len(open) != zapwire.OpenMarketReqSize {
|
||||
t.Fatalf("OpenMarket frame len = %d, want %d", len(open), zapwire.OpenMarketReqSize)
|
||||
}
|
||||
gotPool, gotBase, gotQuote, err := zapwire.DecodeOpenMarket(open)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeOpenMarket: %v", err)
|
||||
}
|
||||
if gotPool != plan.PoolID {
|
||||
t.Fatalf("OpenMarket poolID slot = %x, want %x", gotPool, plan.PoolID)
|
||||
}
|
||||
if hex.EncodeToString(gotBase[:]) != goldenNativeID3 {
|
||||
t.Fatalf("OpenMarket base slot = %x, want golden native", gotBase)
|
||||
}
|
||||
if hex.EncodeToString(gotQuote[:]) != goldenERC20ID3 {
|
||||
t.Fatalf("OpenMarket quote slot = %x, want golden erc20", gotQuote)
|
||||
}
|
||||
|
||||
// (b) Deposit is 88 bytes, binds the maker + base id + amount + ref.
|
||||
if len(dep) != zapwire.DepositReqSize {
|
||||
t.Fatalf("Deposit frame len = %d, want %d", len(dep), zapwire.DepositReqSize)
|
||||
}
|
||||
gotUser, gotAsset, gotAmt, gotRef, err := zapwire.DecodeDeposit(dep)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeDeposit: %v", err)
|
||||
}
|
||||
if gotUser != maker {
|
||||
t.Fatalf("Deposit user = %q, want %q", gotUser, maker)
|
||||
}
|
||||
if hex.EncodeToString(gotAsset[:]) != goldenNativeID3 {
|
||||
t.Fatalf("Deposit asset = %x, want golden native (base)", gotAsset)
|
||||
}
|
||||
if gotAmt != depAmt {
|
||||
t.Fatalf("Deposit amount = %d, want %d", gotAmt, depAmt)
|
||||
}
|
||||
if gotRef != depRef {
|
||||
t.Fatalf("Deposit ref = %x, want %x", gotRef, depRef)
|
||||
}
|
||||
|
||||
// (c) Place is 65 bytes, a resting SELL by the maker at askPrice x askSize on poolID.
|
||||
if len(place) != zapwire.PlaceReqSize {
|
||||
t.Fatalf("Place frame len = %d, want %d", len(place), zapwire.PlaceReqSize)
|
||||
}
|
||||
gotPP, gotSide, gotPrice, gotSize, gotPlaceUser, err := zapwire.DecodePlace(place)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodePlace: %v", err)
|
||||
}
|
||||
if gotPP != plan.PoolID {
|
||||
t.Fatalf("Place poolID = %x, want %x", gotPP, plan.PoolID)
|
||||
}
|
||||
if gotSide != zapwire.SideSell {
|
||||
t.Fatalf("Place side = %d, want SELL(%d)", gotSide, zapwire.SideSell)
|
||||
}
|
||||
if gotPrice != askPrice || gotSize != askSize {
|
||||
t.Fatalf("Place price/size = %v/%v, want %v/%v", gotPrice, gotSize, askPrice, askSize)
|
||||
}
|
||||
if gotPlaceUser != maker {
|
||||
t.Fatalf("Place user = %q, want %q", gotPlaceUser, maker)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRealMarketFrames_RejectsSyntheticQuote proves the whole frame-build refuses a
|
||||
// synthetic quote ref — no frames are returned for a market the chain would refuse.
|
||||
func TestRealMarketFrames_RejectsSyntheticQuote(t *testing.T) {
|
||||
cid := mustCChain(t)
|
||||
syntheticQuote := assetSpec{Kind: dex.AssetKindERC20, Ref: []byte("LUSD")} // ascii ticker, not 20 bytes
|
||||
open, dep, place, _, err := realMarketFrames(3, cid, nativeSpec(), syntheticQuote, "maker", 1.0, 1.0, 3, [32]byte{})
|
||||
if err == nil {
|
||||
t.Fatal("realMarketFrames MUST reject a synthetic (ascii-ticker) quote ref")
|
||||
}
|
||||
if open != nil || dep != nil || place != nil {
|
||||
t.Fatal("no frames may be returned when asset admission fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRealMarketFrames_RejectsSameBaseQuote proves a market whose base and quote
|
||||
// resolve to the SAME id is refused (it is not a market).
|
||||
func TestRealMarketFrames_RejectsSameBaseQuote(t *testing.T) {
|
||||
cid := mustCChain(t)
|
||||
same := erc20Spec(erc20Addr(0x07))
|
||||
if _, _, _, _, err := realMarketFrames(3, cid, same, same, "maker", 1.0, 1.0, 3, [32]byte{}); err == nil {
|
||||
t.Fatal("realMarketFrames MUST reject base==quote")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDepositRefDeterministic proves the deposit idempotency ref is deterministic in
|
||||
// (poolID, asset, amount) — a byte-identical re-seed dedups to exactly-once on the
|
||||
// content-addressed D-Chain.
|
||||
func TestDepositRefDeterministic(t *testing.T) {
|
||||
cid := mustCChain(t)
|
||||
baseID, err := nativeSpec().resolveID(3, cid)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveID: %v", err)
|
||||
}
|
||||
pid := poolIDFor(baseID, baseID) // any pid for the test
|
||||
r1 := depositRefFor(pid, baseID, 100)
|
||||
r2 := depositRefFor(pid, baseID, 100)
|
||||
if r1 != r2 {
|
||||
t.Fatalf("depositRefFor not deterministic: %x != %x", r1, r2)
|
||||
}
|
||||
if r3 := depositRefFor(pid, baseID, 101); r3 == r1 {
|
||||
t.Fatal("depositRefFor must change with amount")
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseERC20Addr exercises the boundary parser: it accepts a 0x-prefixed and a bare
|
||||
// 20-byte address, and rejects wrong-length / non-hex.
|
||||
func TestParseERC20Addr(t *testing.T) {
|
||||
ok := []string{
|
||||
"0x0000000000000000000000000000000000000001",
|
||||
"0000000000000000000000000000000000000001",
|
||||
}
|
||||
for _, s := range ok {
|
||||
b, err := parseERC20Addr(s)
|
||||
if err != nil {
|
||||
t.Fatalf("parseERC20Addr(%q) unexpected error: %v", s, err)
|
||||
}
|
||||
if len(b) != 20 {
|
||||
t.Fatalf("parseERC20Addr(%q) len = %d, want 20", s, len(b))
|
||||
}
|
||||
}
|
||||
bad := []string{"", "0x", "0x01", "0xZZ00000000000000000000000000000000000001",
|
||||
"0x000000000000000000000000000000000000000001"} // 21 bytes
|
||||
for _, s := range bad {
|
||||
if _, err := parseERC20Addr(s); err == nil {
|
||||
t.Fatalf("parseERC20Addr(%q) must error", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/dex/pkg/dex"
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// seed.go is the PURE, testable core of the canonical real-market seeder. It holds
|
||||
// no I/O: every function here turns inputs into bytes (frozen zapwire frames) or
|
||||
// derives the canonical 32-byte identities the native D-Chain keys its ledger by.
|
||||
// main.go is the thin transport that POSTs these frames over HTTP.
|
||||
//
|
||||
// THE IDENTITY RULE this file enforces (and the reason this seeder is SAFE against a
|
||||
// shared cluster, with no guard needed): every asset handle is the dex CANONICAL
|
||||
// AssetID — DeriveAssetID(networkID, C-chainID, kind, canonicalRef) — the SAME
|
||||
// consensus-native identity primitive the on-chain resolver computes. It is NEVER an
|
||||
// ascii-of-symbol id (the synthetic form, e.g. 0x4c5558 = "LUX", that bricked the
|
||||
// chain by binding a resting order to a market with no real asset). A synthetic /
|
||||
// malformed ref is rejected HERE, at frame-build time, before a single byte is sent,
|
||||
// by CanonicalRefFor inside DeriveAssetID.
|
||||
|
||||
// poolIDDomain is the domain-separation tag for the canonical poolID derivation. It
|
||||
// is folded as the FIRST field so a poolID preimage can never collide with any other
|
||||
// SHA-256 use, and so the result is structurally unrelated to an ascii ticker.
|
||||
var poolIDDomain = []byte("lux:dex:dexseed:poolid:v1")
|
||||
|
||||
// assetSpec names a real on-chain asset by WHERE IT LIVES: its kind (EVM_NATIVE /
|
||||
// ERC20 / UTXO) and its canonical on-chain reference (the native marker for
|
||||
// EVM_NATIVE, the 20-byte token address for ERC20, the 32-byte source assetID for
|
||||
// UTXO). It is the seeder's input unit; resolveID turns it into the canonical
|
||||
// AssetID the chain keys by. It deliberately carries NO ticker/symbol — identity is
|
||||
// the derived id, never a string.
|
||||
type assetSpec struct {
|
||||
Kind dex.AssetKind
|
||||
Ref []byte
|
||||
}
|
||||
|
||||
// nativeSpec is the canonical EVM_NATIVE (LUX) asset spec: the EVM zero-address
|
||||
// native marker. Its derived id is the chain's own coin under the bound network.
|
||||
func nativeSpec() assetSpec {
|
||||
return assetSpec{Kind: dex.AssetKindEVMNative, Ref: dex.EVMNativeMarker}
|
||||
}
|
||||
|
||||
// erc20Spec builds an ERC20 asset spec from a 20-byte token contract address. The
|
||||
// address is validated (length, non-zero) when the spec is resolved, not here.
|
||||
func erc20Spec(addr []byte) assetSpec {
|
||||
ref := make([]byte, len(addr))
|
||||
copy(ref, addr)
|
||||
return assetSpec{Kind: dex.AssetKindERC20, Ref: ref}
|
||||
}
|
||||
|
||||
// resolveID derives the canonical, consensus-native 32-byte AssetID for this spec
|
||||
// under (networkID, cChainID). It is the SINGLE admission gate in the seeder: a
|
||||
// synthetic/malformed ref (wrong length, zero ERC-20 address, non-marker native ref,
|
||||
// invalid kind) is REFUSED here by dex.DeriveAssetID — so no frame is ever built,
|
||||
// let alone sent, for an asset that is not a well-formed real on-chain identity. This
|
||||
// is byte-identical to what the chain's resolver derives, so a market this seeder
|
||||
// opens binds the SAME id the chain admits.
|
||||
func (a assetSpec) resolveID(networkID uint32, cChainID ids.ID) (ids.ID, error) {
|
||||
id, err := dex.DeriveAssetID(networkID, cChainID, a.Kind, a.Ref)
|
||||
if err != nil {
|
||||
return ids.Empty, fmt.Errorf("resolve %s asset id: %w", a.Kind, err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// poolIDFor derives the canonical, deterministic, NON-ascii poolID for a (base,
|
||||
// quote) market from the two REAL derived asset ids:
|
||||
//
|
||||
// poolID = SHA-256( len|domain ‖ len|baseID ‖ len|quoteID )
|
||||
//
|
||||
// It is reproducible (the same base/quote always yield the same poolID, so a re-seed
|
||||
// is idempotent at the market level) and structurally cannot equal an ascii ticker
|
||||
// (the domain tag dominates the preimage). It is keyed by the DERIVED ids, not the
|
||||
// refs, so two markets over the same economic pair on the same network share one
|
||||
// poolID by construction.
|
||||
func poolIDFor(baseID, quoteID ids.ID) [32]byte {
|
||||
h := sha256.New()
|
||||
writeLenPrefixed(h, poolIDDomain)
|
||||
writeLenPrefixed(h, baseID[:])
|
||||
writeLenPrefixed(h, quoteID[:])
|
||||
var pid [32]byte
|
||||
copy(pid[:], h.Sum(nil))
|
||||
return pid
|
||||
}
|
||||
|
||||
// writeLenPrefixed writes an 8-byte big-endian length followed by b, so the fold is
|
||||
// injective (no two distinct field tuples share a preimage) — the same discipline as
|
||||
// dex's assetFolder.
|
||||
func writeLenPrefixed(h interface{ Write([]byte) (int, error) }, b []byte) {
|
||||
var lp [8]byte
|
||||
lp[0] = byte(len(b) >> 56)
|
||||
lp[1] = byte(len(b) >> 48)
|
||||
lp[2] = byte(len(b) >> 40)
|
||||
lp[3] = byte(len(b) >> 32)
|
||||
lp[4] = byte(len(b) >> 24)
|
||||
lp[5] = byte(len(b) >> 16)
|
||||
lp[6] = byte(len(b) >> 8)
|
||||
lp[7] = byte(len(b))
|
||||
_, _ = h.Write(lp[:])
|
||||
_, _ = h.Write(b)
|
||||
}
|
||||
|
||||
// marketPlan is the fully-derived, validated description of one real market the
|
||||
// seeder will create: the canonical poolID and both REAL asset ids. It is produced by
|
||||
// realMarketFrames (which also returns the frozen frames) and is what main.go reads
|
||||
// back the book/markets against.
|
||||
type marketPlan struct {
|
||||
PoolID [32]byte
|
||||
BaseID ids.ID
|
||||
QuoteID ids.ID
|
||||
}
|
||||
|
||||
// realMarketFrames is the pure heart of the seeder. Given the network identity, the
|
||||
// C-Chain id, the base/quote REAL asset specs, the maker's 16-byte CLOB user handle,
|
||||
// and the resting ask price/size, it:
|
||||
//
|
||||
// 1. derives both canonical asset ids (REFUSING any synthetic/malformed ref);
|
||||
// 2. derives the canonical NON-ascii poolID from those ids;
|
||||
// 3. builds the three FROZEN zapwire frames that seed the market end-to-end:
|
||||
// - OpenMarket(poolID, baseID, quoteID) — bind the real (base,quote);
|
||||
// - Deposit(maker, baseID, depositAmount, ref) — fund the maker's base balance;
|
||||
// - Place(poolID, SELL, askPrice, askSize, maker) — rest a maker limit ask.
|
||||
//
|
||||
// It returns the raw frames, the validated plan, and any derivation error. It does NO
|
||||
// I/O — main.go posts the frames. depositAmount must cover the ask lock (>= askSize in
|
||||
// base whole units); ref is the deposit idempotency reference (originating-op id).
|
||||
func realMarketFrames(
|
||||
networkID uint32,
|
||||
cChainID ids.ID,
|
||||
base, quote assetSpec,
|
||||
maker string,
|
||||
askPrice, askSize float64,
|
||||
depositAmount uint64,
|
||||
depositRef [32]byte,
|
||||
) (openMarket, deposit, place []byte, plan marketPlan, err error) {
|
||||
baseID, err := base.resolveID(networkID, cChainID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, marketPlan{}, fmt.Errorf("base: %w", err)
|
||||
}
|
||||
quoteID, err := quote.resolveID(networkID, cChainID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, marketPlan{}, fmt.Errorf("quote: %w", err)
|
||||
}
|
||||
if baseID == quoteID {
|
||||
return nil, nil, nil, marketPlan{}, fmt.Errorf("base and quote resolve to the same asset id %s — not a market", baseID)
|
||||
}
|
||||
|
||||
pid := poolIDFor(baseID, quoteID)
|
||||
|
||||
openMarket = zapwire.EncodeOpenMarket(pid, baseID, quoteID)
|
||||
deposit = zapwire.EncodeDeposit(maker, baseID, depositAmount, depositRef)
|
||||
place = zapwire.EncodePlace(pid, zapwire.SideSell, askPrice, askSize, maker)
|
||||
|
||||
return openMarket, deposit, place, marketPlan{PoolID: pid, BaseID: baseID, QuoteID: quoteID}, nil
|
||||
}
|
||||
|
||||
// depositRefFor derives a deterministic idempotency reference for the maker's seed
|
||||
// deposit, domain-separated and bound to (poolID, asset, amount). The native D-Chain
|
||||
// content-addresses its dedup index on the full frame, so a byte-identical re-seed
|
||||
// (same pool, same asset, same amount) dedups to exactly-once — a re-run does not
|
||||
// double-credit. It folds the canonical asset id (32 bytes), not a symbol.
|
||||
func depositRefFor(poolID [32]byte, assetID ids.ID, amount uint64) [32]byte {
|
||||
h := sha256.New()
|
||||
writeLenPrefixed(h, []byte("lux:dex:dexseed:depositref:v1"))
|
||||
writeLenPrefixed(h, poolID[:])
|
||||
writeLenPrefixed(h, assetID[:])
|
||||
var amt [8]byte
|
||||
amt[0] = byte(amount >> 56)
|
||||
amt[1] = byte(amount >> 48)
|
||||
amt[2] = byte(amount >> 40)
|
||||
amt[3] = byte(amount >> 32)
|
||||
amt[4] = byte(amount >> 24)
|
||||
amt[5] = byte(amount >> 16)
|
||||
amt[6] = byte(amount >> 8)
|
||||
amt[7] = byte(amount)
|
||||
writeLenPrefixed(h, amt[:])
|
||||
var ref [32]byte
|
||||
copy(ref[:], h.Sum(nil))
|
||||
return ref
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Command fix-gateway runs the FIX 4.4 acceptor as a long-lived TCP server in
|
||||
// front of a running D-Chain venue (cmd/dvenue / the node runtime). It is the
|
||||
// SERVER side of the FIX trading path: a FIX client (the SDK's EnableFIX(), or
|
||||
// any FIX 4.4 engine) connects, logs on, and sends NewOrderSingle / OrderCancel
|
||||
// messages; the gateway translates each into the SAME clob_* venue operation that
|
||||
// ZAP, WS, and the 0x9010 precompile use, and renders the consensus-computed
|
||||
// result back as a FIX ExecutionReport.
|
||||
//
|
||||
// It owns NO matcher and NO book — it dials the venue's clob_* ZAP endpoint
|
||||
// (-venue) and is a pure protocol front-end (pkg/fix + pkg/venue). This is the
|
||||
// "one matcher, many protocols" topology: the matcher lives in the venue
|
||||
// (pkg/dchain), and fix-gateway is the FIX dialect onto it, exactly parallel to
|
||||
// the WS and precompile front-ends.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/dex/pkg/fix"
|
||||
"github.com/luxfi/dex/pkg/venue"
|
||||
log "github.com/luxfi/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", "127.0.0.1:5001", "TCP listen address for the FIX acceptor")
|
||||
venueAddr := flag.String("venue", "127.0.0.1:9099", "ZAP endpoint of the D-Chain venue (clob_*)")
|
||||
target := flag.String("target", "LXDEX", "this acceptor's CompID (the client's TargetCompID)")
|
||||
heartBt := flag.Int("heartbeat", 30, "heartbeat interval advertised on Logon (seconds)")
|
||||
dialTimeout := flag.Duration("dial-timeout", 10*time.Second, "venue dial timeout")
|
||||
flag.Parse()
|
||||
|
||||
logger := log.Root()
|
||||
|
||||
// Dial the venue's clob_* surface — the SAME socket the precompile and proxy
|
||||
// dial. The gateway holds this one connection and routes every FIX order
|
||||
// through it.
|
||||
dialCtx, dialCancel := context.WithTimeout(context.Background(), *dialTimeout)
|
||||
v, err := venue.DialVenue(dialCtx, *venueAddr)
|
||||
dialCancel()
|
||||
if err != nil {
|
||||
logger.Error("dial venue", "venue", *venueAddr, "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
acc, err := fix.NewAcceptor(fix.Config{
|
||||
Addr: *addr,
|
||||
Venue: v,
|
||||
TargetCompID: *target,
|
||||
HeartBtInt: *heartBt,
|
||||
Logger: logger,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error("new acceptor", "addr", *addr, "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Graceful shutdown on SIGINT/SIGTERM.
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sig
|
||||
logger.Info("fix-gateway shutting down")
|
||||
cancel()
|
||||
}()
|
||||
|
||||
logger.Info("fix-gateway up", "addr", acc.Addr(), "venue", *venueAddr, "target", *target, "heartbeat", *heartBt)
|
||||
if err := acc.Serve(ctx); err != nil {
|
||||
logger.Error("acceptor serve", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
// Gateway API server that aggregates multiple DEX providers.
|
||||
// Connects to EVM nodes for on-chain pool data via DEX precompiles.
|
||||
//
|
||||
// Configuration via flags or environment variables:
|
||||
//
|
||||
// --rpc / RPC_URL EVM JSON-RPC endpoint (required)
|
||||
// --chain-id / CHAIN_ID EVM chain ID (required)
|
||||
// --addr Listen address (default :8080)
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/luxfi/dex/pkg/gateway"
|
||||
"github.com/luxfi/dex/pkg/gateway/lux"
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", ":8080", "Server listen address")
|
||||
rpcURL := flag.String("rpc", "", "EVM JSON-RPC endpoint (env: RPC_URL)")
|
||||
chainIDFlag := flag.String("chain-id", "", "EVM chain ID (env: CHAIN_ID)")
|
||||
flag.Parse()
|
||||
|
||||
// Resolve from env if not set via flags
|
||||
if *rpcURL == "" {
|
||||
*rpcURL = os.Getenv("RPC_URL")
|
||||
}
|
||||
if *chainIDFlag == "" {
|
||||
*chainIDFlag = os.Getenv("CHAIN_ID")
|
||||
}
|
||||
|
||||
if *rpcURL == "" {
|
||||
log.Fatal("--rpc or RPC_URL is required")
|
||||
}
|
||||
if *chainIDFlag == "" {
|
||||
log.Fatal("--chain-id or CHAIN_ID is required")
|
||||
}
|
||||
|
||||
chainID, err := strconv.ParseUint(*chainIDFlag, 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid chain ID %q: %v", *chainIDFlag, err)
|
||||
}
|
||||
|
||||
gwChainID := gateway.ChainID(chainID)
|
||||
|
||||
config := gateway.GatewayConfig{
|
||||
DefaultChainID: gwChainID,
|
||||
EnableFallback: false,
|
||||
CacheEnabled: true,
|
||||
CacheTTLSeconds: 60,
|
||||
}
|
||||
|
||||
gw := gateway.New(config)
|
||||
|
||||
provider := lux.NewProvider(lux.ProviderConfig{
|
||||
Name: "native",
|
||||
Priority: 10,
|
||||
Chains: []gateway.ChainID{gwChainID},
|
||||
RPCEndpoints: map[gateway.ChainID]string{
|
||||
gwChainID: *rpcURL,
|
||||
},
|
||||
})
|
||||
|
||||
if err := gw.RegisterProvider(provider); err != nil {
|
||||
log.Fatalf("Failed to register provider: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("DEX gateway starting on %s", *addr)
|
||||
log.Printf(" Chain ID: %d", chainID)
|
||||
log.Printf(" RPC: %s", *rpcURL)
|
||||
fmt.Println()
|
||||
|
||||
if err := gw.StartWithGracefulShutdown(*addr); err != nil {
|
||||
log.Fatalf("Server error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
type Monitor struct {
|
||||
nc *nats.Conn
|
||||
stats Stats
|
||||
clients map[*websocket.Conn]bool
|
||||
mu sync.RWMutex
|
||||
upgrader websocket.Upgrader
|
||||
}
|
||||
|
||||
type Stats struct {
|
||||
Orders int64 `json:"orders"`
|
||||
Trades int64 `json:"trades"`
|
||||
OrdersPerSec float64 `json:"orders_per_sec"`
|
||||
TradesPerSec float64 `json:"trades_per_sec"`
|
||||
ActiveNodes int `json:"active_nodes"`
|
||||
Memory MemStats `json:"memory"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
type MemStats struct {
|
||||
Alloc uint64 `json:"alloc_mb"`
|
||||
Sys uint64 `json:"sys_mb"`
|
||||
NumGC uint32 `json:"num_gc"`
|
||||
NumCPU int `json:"num_cpu"`
|
||||
Goroutines int `json:"goroutines"`
|
||||
}
|
||||
|
||||
var dashboardHTML = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>LX Monitor</title>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5em;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
.metric {
|
||||
font-size: 3em;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
background: linear-gradient(45deg, #f093fb 0%, #f5576c 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.label {
|
||||
font-size: 0.9em;
|
||||
opacity: 0.8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.chart {
|
||||
height: 200px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: linear-gradient(to top, #00f260 0%, #0575e6 100%);
|
||||
}
|
||||
.status {
|
||||
padding: 5px 10px;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.status.online {
|
||||
background: #4caf50;
|
||||
}
|
||||
.status.offline {
|
||||
background: #f44336;
|
||||
}
|
||||
#log {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.log-entry {
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🚀 LX Real-Time Monitor</h1>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="label">Total Orders</div>
|
||||
<div class="metric" id="orders">0</div>
|
||||
<div class="label">Orders/sec: <span id="orders-rate">0</span></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="label">Total Trades</div>
|
||||
<div class="metric" id="trades">0</div>
|
||||
<div class="label">Trades/sec: <span id="trades-rate">0</span></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="label">Memory Usage</div>
|
||||
<div class="metric" id="memory">0 MB</div>
|
||||
<div class="label">GC Runs: <span id="gc">0</span></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="label">System</div>
|
||||
<div class="metric" id="goroutines">0</div>
|
||||
<div class="label">Goroutines | CPUs: <span id="cpus">0</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="label">Orders/sec History (Last 60 seconds)</div>
|
||||
<div class="chart" id="chart">
|
||||
<canvas id="canvas" width="1200" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="label">Live Log</div>
|
||||
<div id="log"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ws = new WebSocket('ws://localhost:8080/ws');
|
||||
const history = [];
|
||||
const maxHistory = 60;
|
||||
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Update metrics
|
||||
document.getElementById('orders').textContent = data.orders.toLocaleString();
|
||||
document.getElementById('trades').textContent = data.trades.toLocaleString();
|
||||
document.getElementById('orders-rate').textContent = Math.round(data.orders_per_sec);
|
||||
document.getElementById('trades-rate').textContent = Math.round(data.trades_per_sec);
|
||||
document.getElementById('memory').textContent = data.memory.alloc_mb + ' MB';
|
||||
document.getElementById('gc').textContent = data.memory.num_gc;
|
||||
document.getElementById('goroutines').textContent = data.memory.goroutines;
|
||||
document.getElementById('cpus').textContent = data.memory.num_cpu;
|
||||
|
||||
// Update history
|
||||
history.push(data.orders_per_sec);
|
||||
if (history.length > maxHistory) {
|
||||
history.shift();
|
||||
}
|
||||
|
||||
// Draw chart
|
||||
drawChart();
|
||||
|
||||
// Add log entry
|
||||
addLogEntry(data);
|
||||
};
|
||||
|
||||
function drawChart() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (history.length < 2) return;
|
||||
|
||||
const maxValue = Math.max(...history, 1);
|
||||
const barWidth = canvas.width / maxHistory;
|
||||
|
||||
// Draw grid lines
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
const y = (canvas.height / 5) * i;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(canvas.width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw bars
|
||||
const gradient = ctx.createLinearGradient(0, canvas.height, 0, 0);
|
||||
gradient.addColorStop(0, '#00f260');
|
||||
gradient.addColorStop(1, '#0575e6');
|
||||
ctx.fillStyle = gradient;
|
||||
|
||||
history.forEach((value, index) => {
|
||||
const height = (value / maxValue) * canvas.height;
|
||||
const x = index * barWidth;
|
||||
ctx.fillRect(x, canvas.height - height, barWidth - 1, height);
|
||||
});
|
||||
}
|
||||
|
||||
function addLogEntry(data) {
|
||||
const log = document.getElementById('log');
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'log-entry';
|
||||
const time = new Date(data.timestamp).toLocaleTimeString();
|
||||
entry.textContent = time + ' | Orders: ' + data.orders + ' | Rate: ' +
|
||||
Math.round(data.orders_per_sec) + '/s | Memory: ' +
|
||||
data.memory.alloc_mb + 'MB';
|
||||
log.insertBefore(entry, log.firstChild);
|
||||
|
||||
// Keep only last 20 entries
|
||||
while (log.children.length > 20) {
|
||||
log.removeChild(log.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
addLogEntry({ timestamp: new Date(), orders: 0, orders_per_sec: 0, memory: { alloc_mb: 0 } });
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
func main() {
|
||||
monitor := &Monitor{
|
||||
clients: make(map[*websocket.Conn]bool),
|
||||
upgrader: websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }},
|
||||
}
|
||||
|
||||
// Connect to NATS
|
||||
nc, err := nats.Connect(nats.DefaultURL)
|
||||
if err != nil {
|
||||
log.Printf("Running without NATS: %v", err)
|
||||
} else {
|
||||
monitor.nc = nc
|
||||
defer nc.Close()
|
||||
}
|
||||
|
||||
// Start background workers
|
||||
go monitor.collectStats()
|
||||
go monitor.broadcastStats()
|
||||
|
||||
// HTTP handlers
|
||||
http.HandleFunc("/", monitor.handleDashboard)
|
||||
http.HandleFunc("/ws", monitor.handleWebSocket)
|
||||
http.HandleFunc("/api/stats", monitor.handleAPI)
|
||||
|
||||
fmt.Println("📊 LX Monitor")
|
||||
fmt.Println("=================")
|
||||
fmt.Println("Dashboard: http://localhost:8080")
|
||||
fmt.Println("WebSocket: ws://localhost:8080/ws")
|
||||
fmt.Println("API: http://localhost:8080/api/stats")
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
func (m *Monitor) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(dashboardHTML))
|
||||
}
|
||||
|
||||
func (m *Monitor) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := m.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("WebSocket upgrade error: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
m.mu.Lock()
|
||||
m.clients[conn] = true
|
||||
m.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
m.mu.Lock()
|
||||
delete(m.clients, conn)
|
||||
m.mu.Unlock()
|
||||
}()
|
||||
|
||||
// Keep connection alive
|
||||
for {
|
||||
_, _, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Monitor) handleAPI(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
m.mu.RLock()
|
||||
json.NewEncoder(w).Encode(m.stats)
|
||||
m.mu.RUnlock()
|
||||
}
|
||||
|
||||
func (m *Monitor) collectStats() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
var lastOrders, lastTrades int64
|
||||
lastTime := time.Now()
|
||||
|
||||
for range ticker.C {
|
||||
now := time.Now()
|
||||
deltaTime := now.Sub(lastTime).Seconds()
|
||||
|
||||
// Get memory stats
|
||||
var memStats runtime.MemStats
|
||||
runtime.ReadMemStats(&memStats)
|
||||
|
||||
// Calculate rates
|
||||
orders := atomic.LoadInt64(&m.stats.Orders)
|
||||
trades := atomic.LoadInt64(&m.stats.Trades)
|
||||
|
||||
ordersPerSec := float64(orders-lastOrders) / deltaTime
|
||||
tradesPerSec := float64(trades-lastTrades) / deltaTime
|
||||
|
||||
// Update stats
|
||||
m.mu.Lock()
|
||||
m.stats.OrdersPerSec = ordersPerSec
|
||||
m.stats.TradesPerSec = tradesPerSec
|
||||
m.stats.Memory = MemStats{
|
||||
Alloc: memStats.Alloc / 1024 / 1024,
|
||||
Sys: memStats.Sys / 1024 / 1024,
|
||||
NumGC: memStats.NumGC,
|
||||
NumCPU: runtime.NumCPU(),
|
||||
Goroutines: runtime.NumGoroutine(),
|
||||
}
|
||||
m.stats.Timestamp = now
|
||||
m.mu.Unlock()
|
||||
|
||||
lastOrders = orders
|
||||
lastTrades = trades
|
||||
lastTime = now
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Monitor) broadcastStats() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
m.mu.RLock()
|
||||
data, _ := json.Marshal(m.stats)
|
||||
clients := make([]*websocket.Conn, 0, len(m.clients))
|
||||
for client := range m.clients {
|
||||
clients = append(clients, client)
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
// Broadcast to all connected clients
|
||||
for _, client := range clients {
|
||||
if err := client.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
client.Close()
|
||||
m.mu.Lock()
|
||||
delete(m.clients, client)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate some data for demo
|
||||
func init() {
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
// Demo data generation commented out - will use real data
|
||||
// for {
|
||||
// atomic.AddInt64(&Stats{}.Orders, int64(100+rand.Intn(500)))
|
||||
// atomic.AddInt64(&Stats{}.Trades, int64(50+rand.Intn(250)))
|
||||
// time.Sleep(100 * time.Millisecond)
|
||||
// }
|
||||
}()
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
type PersistentServer struct {
|
||||
ob *lx.OrderBook
|
||||
nc *nats.Conn
|
||||
js nats.JetStreamContext
|
||||
ordersCount int64
|
||||
tradesCount int64
|
||||
snapshotFile string
|
||||
mu sync.RWMutex
|
||||
lastSnapshot time.Time
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
OrdersCount int64 `json:"orders_count"`
|
||||
TradesCount int64 `json:"trades_count"`
|
||||
OrderBook string `json:"orderbook_state"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
natsURL = flag.String("nats", nats.DefaultURL, "NATS server URL")
|
||||
snapshotFile = flag.String("snapshot", "dex-snapshot.json", "Snapshot file for persistence")
|
||||
restore = flag.Bool("restore", true, "Restore from snapshot on startup")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
// Setup signal handling
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
server := &PersistentServer{
|
||||
ob: lx.NewOrderBook("BTC-USD"),
|
||||
snapshotFile: *snapshotFile,
|
||||
}
|
||||
|
||||
// Restore from snapshot if exists
|
||||
if *restore {
|
||||
if err := server.restoreSnapshot(); err != nil {
|
||||
log.Printf("⚠️ No snapshot to restore: %v", err)
|
||||
} else {
|
||||
log.Printf("✅ Restored from snapshot: %d orders, %d trades",
|
||||
server.ordersCount, server.tradesCount)
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to NATS
|
||||
nc, err := nats.Connect(*natsURL,
|
||||
nats.MaxReconnects(-1),
|
||||
nats.ReconnectWait(1*time.Second),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to NATS: %v", err)
|
||||
}
|
||||
defer nc.Close()
|
||||
server.nc = nc
|
||||
|
||||
js, err := nc.JetStream()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get JetStream: %v", err)
|
||||
}
|
||||
server.js = js
|
||||
|
||||
fmt.Println("🏦 Persistent DEX Server")
|
||||
fmt.Println("========================")
|
||||
fmt.Printf("📍 ID: persistent-%d\n", os.Getpid())
|
||||
fmt.Printf("💾 Snapshot: %s\n", *snapshotFile)
|
||||
fmt.Printf("📊 Orders: %d, Trades: %d\n", server.ordersCount, server.tradesCount)
|
||||
fmt.Println("✅ Server ready!")
|
||||
|
||||
// Start workers
|
||||
go server.orderProcessor()
|
||||
go server.snapshotWorker()
|
||||
go server.statsReporter()
|
||||
go server.heartbeat()
|
||||
|
||||
// Wait for shutdown signal
|
||||
<-sigChan
|
||||
fmt.Println("\n⚠️ Shutting down...")
|
||||
|
||||
// Save final snapshot
|
||||
if err := server.saveSnapshot(); err != nil {
|
||||
log.Printf("Failed to save snapshot: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✅ Snapshot saved: %d orders, %d trades\n",
|
||||
server.ordersCount, server.tradesCount)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PersistentServer) orderProcessor() {
|
||||
// Subscribe to orders
|
||||
sub, err := s.nc.SubscribeSync("orders.>")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for {
|
||||
msg, err := sub.NextMsg(5 * time.Second)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Process order
|
||||
s.mu.Lock()
|
||||
// Simulate order processing
|
||||
atomic.AddInt64(&s.ordersCount, 1)
|
||||
if atomic.LoadInt64(&s.ordersCount)%2 == 0 {
|
||||
atomic.AddInt64(&s.tradesCount, 1)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
// Acknowledge
|
||||
msg.Ack()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PersistentServer) snapshotWorker() {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if err := s.saveSnapshot(); err != nil {
|
||||
log.Printf("Failed to save snapshot: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PersistentServer) saveSnapshot() error {
|
||||
s.mu.RLock()
|
||||
snapshot := Snapshot{
|
||||
Timestamp: time.Now(),
|
||||
OrdersCount: atomic.LoadInt64(&s.ordersCount),
|
||||
TradesCount: atomic.LoadInt64(&s.tradesCount),
|
||||
OrderBook: fmt.Sprintf("%+v", s.ob.GetSnapshot()),
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
data, err := json.MarshalIndent(snapshot, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write to temp file first
|
||||
tempFile := s.snapshotFile + ".tmp"
|
||||
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Atomic rename
|
||||
return os.Rename(tempFile, s.snapshotFile)
|
||||
}
|
||||
|
||||
func (s *PersistentServer) restoreSnapshot() error {
|
||||
data, err := os.ReadFile(s.snapshotFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var snapshot Snapshot
|
||||
if err := json.Unmarshal(data, &snapshot); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.ordersCount = snapshot.OrdersCount
|
||||
s.tradesCount = snapshot.TradesCount
|
||||
s.lastSnapshot = snapshot.Timestamp
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PersistentServer) statsReporter() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
orders := atomic.LoadInt64(&s.ordersCount)
|
||||
trades := atomic.LoadInt64(&s.tradesCount)
|
||||
fmt.Printf("📊 Stats: Orders=%d, Trades=%d\n", orders, trades)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PersistentServer) heartbeat() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
data := fmt.Sprintf("persistent-%d", os.Getpid())
|
||||
s.nc.Publish("dex.heartbeat", []byte(data))
|
||||
}
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
// Command price-feed runs a local price aggregator with all sources.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/dex/pkg/price"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
interval = flag.Duration("interval", 500*time.Millisecond, "display interval")
|
||||
symbols = flag.String("symbols", "LUX-USDC,LZOO-USDC,LETH-USDC,ZOO-USDC", "symbols to watch")
|
||||
sources = flag.String("sources", "all", "sources: all,xchain,achain,cchain,zoo,pyth,chainlink")
|
||||
noVerifier = flag.Bool("no-verify", false, "disable Q-Chain quantum finality verification")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
fmt.Println("LX Price Feed")
|
||||
fmt.Println("=================")
|
||||
fmt.Println()
|
||||
|
||||
// Create oracle
|
||||
oracle := price.NewOracle()
|
||||
|
||||
sourceList := strings.Split(*sources, ",")
|
||||
useAll := *sources == "all"
|
||||
|
||||
// X-Chain - native DEX (highest priority)
|
||||
if useAll || contains(sourceList, "xchain") {
|
||||
xchain := price.NewXChainSource(
|
||||
"http://localhost:9650/ext/bc/X",
|
||||
"ws://localhost:9650/ext/bc/X/ws",
|
||||
)
|
||||
if err := xchain.Start(); err != nil {
|
||||
log.Printf("Warning: X-Chain start failed: %v", err)
|
||||
} else {
|
||||
oracle.AddSource("x-chain", xchain)
|
||||
fmt.Println("✓ X-Chain source started (native DEX)")
|
||||
}
|
||||
}
|
||||
|
||||
// A-Chain - attestations
|
||||
if useAll || contains(sourceList, "achain") {
|
||||
achain := price.NewAChainSource(
|
||||
"http://localhost:9650/ext/bc/A",
|
||||
"ws://localhost:9650/ext/bc/A/ws",
|
||||
)
|
||||
if err := achain.Start(); err != nil {
|
||||
log.Printf("Warning: A-Chain start failed: %v", err)
|
||||
} else {
|
||||
oracle.AddSource("a-chain", achain)
|
||||
fmt.Println("✓ A-Chain source started (attestations)")
|
||||
}
|
||||
}
|
||||
|
||||
// C-Chain - AMM pools
|
||||
if useAll || contains(sourceList, "cchain") {
|
||||
cchain := price.NewCChainSource(
|
||||
"http://localhost:9650/ext/bc/C/rpc",
|
||||
"ws://localhost:9650/ext/bc/C/ws",
|
||||
)
|
||||
if err := cchain.Start(); err != nil {
|
||||
log.Printf("Warning: C-Chain start failed: %v", err)
|
||||
} else {
|
||||
oracle.AddSource("c-chain", cchain)
|
||||
fmt.Println("✓ C-Chain source started (AMM pools)")
|
||||
}
|
||||
}
|
||||
|
||||
// Pyth Network
|
||||
if useAll || contains(sourceList, "pyth") {
|
||||
pyth := price.NewPythSource(
|
||||
"wss://hermes.pyth.network/ws",
|
||||
"https://hermes.pyth.network",
|
||||
)
|
||||
if err := pyth.Connect(); err != nil {
|
||||
log.Printf("Warning: Pyth connect failed: %v (will use HTTP fallback)", err)
|
||||
}
|
||||
oracle.AddSource("pyth", pyth)
|
||||
fmt.Println("✓ Pyth source added (real-time oracle)")
|
||||
}
|
||||
|
||||
// Zoo Chain - Zoo Labs DEX
|
||||
if useAll || contains(sourceList, "zoo") {
|
||||
zoo := price.NewZooChainSource(
|
||||
"http://localhost:9651/rpc",
|
||||
"ws://localhost:9651/ws",
|
||||
)
|
||||
if err := zoo.Start(); err != nil {
|
||||
log.Printf("Warning: Zoo Chain start failed: %v", err)
|
||||
} else {
|
||||
oracle.AddSource("zoo-chain", zoo)
|
||||
fmt.Println("✓ Zoo Chain source started (Zoo Labs DEX)")
|
||||
}
|
||||
}
|
||||
|
||||
// Q-Chain - Quantum Finality Verifier (enabled by default)
|
||||
// This is NOT a price source - it verifies finality of prices from other sources
|
||||
if !*noVerifier {
|
||||
qchain := price.NewQChainVerifier(
|
||||
"http://localhost:9650/ext/bc/Q",
|
||||
"ws://localhost:9650/ext/bc/Q/ws",
|
||||
)
|
||||
if err := qchain.Start(); err != nil {
|
||||
log.Printf("Warning: Q-Chain start failed: %v", err)
|
||||
} else {
|
||||
oracle.SetVerifier(qchain)
|
||||
fmt.Println("✓ Q-Chain verifier attached (quantum finality)")
|
||||
}
|
||||
}
|
||||
|
||||
// Chainlink
|
||||
if useAll || contains(sourceList, "chainlink") {
|
||||
chainlink := price.NewChainlinkSource()
|
||||
if err := chainlink.Start(); err != nil {
|
||||
log.Printf("Warning: Chainlink start failed: %v", err)
|
||||
} else {
|
||||
oracle.AddSource("chainlink", chainlink)
|
||||
fmt.Println("✓ Chainlink source started (polling oracle)")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
|
||||
// Parse and watch symbols
|
||||
syms := strings.Split(*symbols, ",")
|
||||
for _, sym := range syms {
|
||||
oracle.Watch(strings.TrimSpace(sym))
|
||||
}
|
||||
|
||||
// Start oracle
|
||||
if err := oracle.Start(); err != nil {
|
||||
log.Fatalf("Failed to start oracle: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("Symbols:", strings.Join(syms, ", "))
|
||||
fmt.Println()
|
||||
fmt.Println("Press Ctrl+C to stop")
|
||||
fmt.Println()
|
||||
|
||||
// Signal handling
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigCh
|
||||
fmt.Println("\nShutting down...")
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// Wait for initial data
|
||||
fmt.Println("Waiting for price data...")
|
||||
time.Sleep(time.Second)
|
||||
|
||||
// Display loop
|
||||
ticker := time.NewTicker(*interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Show header based on whether verifier is attached
|
||||
if oracle.Verifier() != nil {
|
||||
fmt.Printf("\n%-15s %-12s %-12s %-12s %-10s %-10s %-10s %s\n",
|
||||
"SYMBOL", "PRICE", "TWAP", "VWAP", "CONF", "SOURCE", "FINALITY", "AGE")
|
||||
} else {
|
||||
fmt.Printf("\n%-15s %-12s %-12s %-12s %-10s %-10s %s\n",
|
||||
"SYMBOL", "PRICE", "TWAP", "VWAP", "CONF", "SOURCE", "AGE")
|
||||
}
|
||||
fmt.Println(strings.Repeat("-", 100))
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
oracle.Stop()
|
||||
fmt.Println("Stopped.")
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
display(oracle, syms)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func display(oracle *price.Oracle, symbols []string) {
|
||||
hasVerifier := oracle.Verifier() != nil
|
||||
|
||||
for _, sym := range symbols {
|
||||
// Use VerifiedPrice to get finality status if verifier attached
|
||||
verified, err := oracle.VerifiedPrice(sym)
|
||||
if err != nil {
|
||||
fmt.Printf("%-15s %-12s\n", sym, "--")
|
||||
continue
|
||||
}
|
||||
data := verified.Data
|
||||
|
||||
age := time.Since(data.Timestamp)
|
||||
ageStr := fmt.Sprintf("%dms", age.Milliseconds())
|
||||
if age > time.Second {
|
||||
ageStr = fmt.Sprintf("%.1fs", age.Seconds())
|
||||
}
|
||||
|
||||
twap := oracle.TWAP(sym)
|
||||
vwap := oracle.VWAP(sym)
|
||||
|
||||
stale := ""
|
||||
if data.Stale {
|
||||
stale = " [STALE]"
|
||||
}
|
||||
|
||||
if hasVerifier {
|
||||
// Show finality status
|
||||
finalityStr := "PENDING"
|
||||
if verified.Finalized {
|
||||
finalityStr = "QUANTUM"
|
||||
}
|
||||
|
||||
fmt.Printf("%-15s $%-11.4f $%-11.4f $%-11.4f %-10.2f %-10s %-10s %s%s\n",
|
||||
data.Symbol,
|
||||
data.Price,
|
||||
twap,
|
||||
vwap,
|
||||
data.Confidence,
|
||||
data.Source,
|
||||
finalityStr,
|
||||
ageStr,
|
||||
stale,
|
||||
)
|
||||
} else {
|
||||
fmt.Printf("%-15s $%-11.4f $%-11.4f $%-11.4f %-10.2f %-10s %s%s\n",
|
||||
data.Symbol,
|
||||
data.Price,
|
||||
twap,
|
||||
vwap,
|
||||
data.Confidence,
|
||||
data.Source,
|
||||
ageStr,
|
||||
stale,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Show finality latency if verifier attached
|
||||
if hasVerifier {
|
||||
latency := oracle.Verifier().FinalityLatency()
|
||||
fmt.Printf("Quantum finality latency: %v\n", latency)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func contains(list []string, s string) bool {
|
||||
for _, v := range list {
|
||||
if strings.TrimSpace(v) == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Binary file not shown.
@@ -1,50 +0,0 @@
|
||||
# Multi-stage build for LX
|
||||
FROM golang:1.23-alpine AS builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache git make
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go mod files
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# Download dependencies
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the binary
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o lx-dex ./cmd/demo
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o dex-server ./cmd/dex-server
|
||||
|
||||
# Final stage
|
||||
FROM alpine:latest
|
||||
|
||||
# Install ca-certificates for HTTPS
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1000 -S dex && \
|
||||
adduser -u 1000 -S dex -G dex
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy binaries from builder
|
||||
COPY --from=builder /app/lx-dex /app/lx-dex
|
||||
COPY --from=builder /app/dex-server /app/dex-server
|
||||
|
||||
# Change ownership
|
||||
RUN chown -R dex:dex /app
|
||||
|
||||
# Switch to non-root user
|
||||
USER dex
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 8080 8081
|
||||
|
||||
# Default command
|
||||
CMD ["/app/dex-server"]
|
||||
@@ -1,65 +0,0 @@
|
||||
# Multi-stage build for LX with CUDA support
|
||||
FROM nvidia/cuda:12.3.0-devel-ubuntu22.04 AS builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
wget \
|
||||
git \
|
||||
pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Go
|
||||
RUN wget -q https://go.dev/dl/go1.22.5.linux-amd64.tar.gz && \
|
||||
tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz && \
|
||||
rm go1.22.5.linux-amd64.tar.gz
|
||||
|
||||
ENV PATH="/usr/local/go/bin:${PATH}"
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /build
|
||||
|
||||
# Copy go mod files
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build binaries
|
||||
RUN go build -ldflags="-s -w" -o lx-dex ./cmd/demo
|
||||
RUN go build -ldflags="-s -w" -o dex-server ./cmd/dex-server
|
||||
|
||||
# Final stage
|
||||
FROM nvidia/cuda:12.3.0-runtime-ubuntu22.04
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create non-root user
|
||||
RUN groupadd -g 1000 dex && \
|
||||
useradd -u 1000 -g dex -s /bin/bash -m dex
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy binaries from builder
|
||||
COPY --from=builder /build/lx-dex /app/lx-dex
|
||||
COPY --from=builder /build/dex-server /app/dex-server
|
||||
|
||||
# Change ownership
|
||||
RUN chown -R dex:dex /app
|
||||
|
||||
# Switch to non-root user
|
||||
USER dex
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 8080 8081
|
||||
|
||||
# Environment for CUDA
|
||||
ENV LD_LIBRARY_PATH=/usr/local/cuda/lib64:${LD_LIBRARY_PATH}
|
||||
|
||||
# Default command
|
||||
CMD ["/app/dex-server"]
|
||||
@@ -1,52 +0,0 @@
|
||||
# Build stage
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache gcc musl-dev linux-headers git make
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go mod files
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build with CGO for hybrid engine
|
||||
ENV CGO_ENABLED=1
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=amd64
|
||||
|
||||
# Build the binary
|
||||
RUN make build
|
||||
|
||||
# Runtime stage
|
||||
FROM alpine:latest
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache ca-certificates curl
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy binary from builder
|
||||
COPY --from=builder /app/bin/lx-dex /app/lx-dex
|
||||
|
||||
# Copy configuration files
|
||||
COPY --from=builder /app/config /app/config
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p /data
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 8080 8081 5000 9090
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
|
||||
# Set entrypoint
|
||||
ENTRYPOINT ["/app/lx-dex"]
|
||||
|
||||
# Default arguments
|
||||
CMD ["-http", "8080", "-ws", "8081", "-node-port", "5000", "-test", "-engine", "hybrid"]
|
||||
@@ -1,100 +0,0 @@
|
||||
services:
|
||||
# Use pre-built Go image to run DEX
|
||||
dex-node:
|
||||
image: golang:1.21-alpine
|
||||
container_name: lux-dex-node
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ../backend:/app
|
||||
- ../bin:/bin/dex
|
||||
environment:
|
||||
- NODE_ID=node-1
|
||||
- CONSENSUS_K=1
|
||||
- CONSENSUS_N=1
|
||||
- CGO_ENABLED=1
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8081:8081"
|
||||
- "5000:5000"
|
||||
command: >
|
||||
sh -c "
|
||||
apk add --no-cache gcc musl-dev &&
|
||||
go build -o /bin/dex/lx-server ./cmd/dex-server &&
|
||||
echo 'DEX Server built successfully' &&
|
||||
/bin/dex/lx-server -port 8080
|
||||
"
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
networks:
|
||||
- dex-network
|
||||
|
||||
# Use pre-built Node image to run UI
|
||||
dex-ui:
|
||||
image: node:18-alpine
|
||||
container_name: lux-dex-ui
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ../ui:/app
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- NEXT_PUBLIC_WS_URL=ws://localhost:8081
|
||||
- NEXT_PUBLIC_API_URL=http://localhost:8080
|
||||
- NEXT_PUBLIC_BASE_PATH=/v2
|
||||
ports:
|
||||
- "3000:3000"
|
||||
command: >
|
||||
sh -c "
|
||||
npm install &&
|
||||
npm run dev
|
||||
"
|
||||
depends_on:
|
||||
- dex-node
|
||||
networks:
|
||||
- dex-network
|
||||
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: lux-dex-db
|
||||
environment:
|
||||
- POSTGRES_DB=luxdex
|
||||
- POSTGRES_USER=dexuser
|
||||
- POSTGRES_PASSWORD=dexpass123
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- dex-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U dexuser -d luxdex"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Redis Cache
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: lux-dex-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
networks:
|
||||
- dex-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
networks:
|
||||
dex-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
driver: local
|
||||
redis-data:
|
||||
driver: local
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package plugin boots the native D-Chain DEX VM as a luxd rpcchainvm plugin,
|
||||
// mirroring github.com/luxfi/evm/plugin/runner. rpc.Serve takes the engine
|
||||
// address from the runtime.EngineAddressKey env var, opens this chain's own
|
||||
// badgerdb/zapdb, hands it to VM.Initialize, and serves block requests so
|
||||
// BuildBlock/Verify/Accept run inside luxd's multi-validator consensus — the
|
||||
// exact harness lux/evm boots through. The VM (pkg/dchain) is pure-Go (CGO=0);
|
||||
// this entrypoint links no native code.
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/luxfi/dex/pkg/dchain"
|
||||
log "github.com/luxfi/log"
|
||||
"github.com/luxfi/version"
|
||||
"github.com/luxfi/vm/rpc"
|
||||
)
|
||||
|
||||
// Run boots the d-chain VM over the rpcchainvm plugin transport. It blocks
|
||||
// until the node runtime shuts the VM down. When luxd probes the plugin version
|
||||
// (a single "version" arg), it prints the version line and exits 0.
|
||||
func Run() {
|
||||
versionStr := fmt.Sprintf("Lux-DChain/%s [node=%s, rpcchainvm=%d]",
|
||||
dchain.Version, version.Current, version.RPCChainVMProtocol)
|
||||
if printVersion() {
|
||||
fmt.Println(versionStr)
|
||||
os.Exit(0)
|
||||
}
|
||||
if err := rpc.Serve(context.Background(), log.Root(), &dchain.VM{}); err != nil {
|
||||
fmt.Printf("dchain rpc.Serve error: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// printVersion reports whether the process was asked to just print its version
|
||||
// and exit. The node passes a single "version" arg when probing a plugin.
|
||||
func printVersion() bool {
|
||||
for _, a := range os.Args[1:] {
|
||||
if a == "version" || a == "--version" || a == "-version" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
//go:build cgo
|
||||
// +build cgo
|
||||
|
||||
package main
|
||||
package standalone
|
||||
|
||||
// engineName labels which matcher pkg/lx links into this build. Under cgo,
|
||||
// pkg/lx selects the GPU matcher (amm_gpu_cuda / amm_gpu_metal / orderbook_cuda).
|
||||
@@ -4,7 +4,7 @@
|
||||
//go:build !cgo
|
||||
// +build !cgo
|
||||
|
||||
package main
|
||||
package standalone
|
||||
|
||||
// engineName labels which matcher pkg/lx links into this build. Without cgo,
|
||||
// pkg/lx selects the pure-Go CPU matcher (amm_nogpu / orderbook_cuda_stub).
|
||||
@@ -1,26 +1,25 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Command dvenue runs the standalone D-Chain DEX VENUE as a long-lived ZAP
|
||||
// server on a fixed TCP address — the dial-able endpoint the 0x9010 EVM
|
||||
// precompile (NewZAPEngine(dex-zap-endpoint)) and the chains/dexvm atomic proxy
|
||||
// relay connect to. It is the production form of the capstone venue proof
|
||||
// (pkg/dchain/localnet_e2e_test.go), promoted from a test harness to a daemon:
|
||||
// Package standalone runs the D-Chain DEX VENUE as a long-lived ZAP server on a
|
||||
// fixed TCP address — the dial-able endpoint the 0x9010 EVM precompile
|
||||
// (NewZAPEngine(dex-zap-endpoint)) and the chains/dexvm atomic proxy relay
|
||||
// connect to. It:
|
||||
//
|
||||
// - opens the authoritative on-disk badger/zapdb (real persisted chainstate),
|
||||
// - serves the FROZEN clob_* surface (RegisterCLOB) over rpc.Listen,
|
||||
// - serves the FROZEN dex_* surface (RegisterDEX) over rpc.Listen,
|
||||
// - runs the consensus sealer loop (WaitForEvent -> BuildBlock -> Verify ->
|
||||
// Accept), so every write crosses the full match -> Verify -> Accept path
|
||||
// and the bytes a caller gets back are consensus-computed fills,
|
||||
// - exposes read-only clob_depth + clob_balance observation methods for
|
||||
// - exposes read-only dex_depth + dex_balance observation methods for the
|
||||
// settled book and custody state (reads need no consensus round-trip).
|
||||
//
|
||||
// This is the ONE venue binary. CGO_ENABLED is the single axis that picks the
|
||||
// matcher: pkg/lx links the GPU matcher under cgo (amm_gpu_cuda / amm_gpu_metal
|
||||
// / orderbook_cuda) and the pure-Go CPU matcher without it (amm_nogpu /
|
||||
// orderbook_cuda_stub). The main is matcher-agnostic — it drives dchain.VM
|
||||
// either way; engineName (engine_{cgo,nocgo}.go) only labels the active build.
|
||||
package main
|
||||
// CGO_ENABLED is the single axis that picks the matcher: pkg/lx links the GPU
|
||||
// matcher under cgo (amm_gpu_cuda / amm_gpu_metal / orderbook_cuda) and the
|
||||
// pure-Go CPU matcher without it (amm_nogpu / orderbook_cuda_stub). This package
|
||||
// is matcher-agnostic — it drives dchain.VM either way; engineName
|
||||
// (engine_{cgo,nocgo}.go) only labels the active build.
|
||||
package standalone
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -40,21 +39,24 @@ import (
|
||||
"github.com/luxfi/rpc"
|
||||
)
|
||||
|
||||
// clobDepthMethod is the read-only book-observation method this daemon adds on
|
||||
// top of the four frozen clob_* write methods. Request: poolId[32]. Response:
|
||||
// dexDepthMethod is the read-only book-observation method this daemon adds on
|
||||
// top of the four frozen dex_* write methods. Request: poolId[32]. Response:
|
||||
// orders[4] | remaining[f64:8] | bestBid[f64:8] | bestAsk[f64:8] | found[1].
|
||||
const clobDepthMethod = "dex_depth"
|
||||
const dexDepthMethod = "dex_depth"
|
||||
|
||||
// clobBalanceMethod is the read-only custody-balance observation method. It lets
|
||||
// dexBalanceMethod is the read-only custody-balance observation method. It lets
|
||||
// the bring-up harness watch funds DEPOSIT into the book, move on a fill, and
|
||||
// WITHDRAW out — the "money lives in the order book" proof. Request: user[16] +
|
||||
// asset[8]. Response: available[8] | locked[8] (big-endian uint64 atomic units).
|
||||
const clobBalanceMethod = "dex_balance"
|
||||
const dexBalanceMethod = "dex_balance"
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", "127.0.0.1:9099", "ZAP listen address for the venue")
|
||||
dir := flag.String("db", "/tmp/dchain_venue_db", "on-disk zapdb directory (authoritative chainstate)")
|
||||
flag.Parse()
|
||||
// Run serves the standalone venue. args is the post-subcommand argv
|
||||
// (os.Args[2:] from `dexd run`/`dexd standalone`); it carries -addr and -db.
|
||||
func Run(args []string) {
|
||||
fs := flag.NewFlagSet("dexd run", flag.ExitOnError)
|
||||
addr := fs.String("addr", "127.0.0.1:9099", "ZAP listen address for the venue")
|
||||
dir := fs.String("db", "/tmp/dchain_venue_db", "on-disk zapdb directory (authoritative chainstate)")
|
||||
_ = fs.Parse(args)
|
||||
|
||||
logger := log.Root()
|
||||
|
||||
@@ -83,16 +85,16 @@ func main() {
|
||||
_ = db.Close()
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := vm.RegisterCLOB(server); err != nil {
|
||||
logger.Error("RegisterCLOB", "err", err)
|
||||
if err := vm.RegisterDEX(server); err != nil {
|
||||
logger.Error("RegisterDEX", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := server.RegisterRaw(clobDepthMethod, depthHandler(vm)); err != nil {
|
||||
logger.Error("RegisterRaw clob_depth", "err", err)
|
||||
if err := server.RegisterRaw(dexDepthMethod, depthHandler(vm)); err != nil {
|
||||
logger.Error("RegisterRaw dex_depth", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := server.RegisterRaw(clobBalanceMethod, balanceHandler(vm)); err != nil {
|
||||
logger.Error("RegisterRaw clob_balance", "err", err)
|
||||
if err := server.RegisterRaw(dexBalanceMethod, balanceHandler(vm)); err != nil {
|
||||
logger.Error("RegisterRaw dex_balance", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -151,7 +153,7 @@ func sealer(ctx context.Context, vm *dchain.VM, logger log.Logger) {
|
||||
}
|
||||
}
|
||||
|
||||
// depthHandler serves clob_depth: a read-only observation of a market's settled
|
||||
// depthHandler serves dex_depth: a read-only observation of a market's settled
|
||||
// resting book. It reads the in-RAM book via the VM's BookDepth accessor (a fold
|
||||
// of committed rows) — no consensus round-trip.
|
||||
func depthHandler(vm *dchain.VM) rpc.RawHandler {
|
||||
@@ -171,7 +173,7 @@ func depthHandler(vm *dchain.VM) rpc.RawHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// balanceHandler serves clob_balance: a read-only observation of an account's
|
||||
// balanceHandler serves dex_balance: a read-only observation of an account's
|
||||
// custody balances for an asset (available + locked), read from the durable
|
||||
// ledger via the VM's Balance accessor. Request: user[16] + asset[32]. Response:
|
||||
// available[8] | locked[8].
|
||||
+5
-5
@@ -17,15 +17,15 @@ import (
|
||||
// venue_ws.go is the WebSocket trading FRONT-END onto THE matcher — the WS
|
||||
// counterpart of the FIX acceptor (pkg/fix) and the 0x9010 precompile. It does
|
||||
// ONE thing: accept a JSON order over /ws, translate it into a venue.Venue call
|
||||
// (the SAME clob_* surface ZAP/FIX/precompile use), and stream the
|
||||
// (the SAME dex_* surface ZAP/FIX/precompile use), and stream the
|
||||
// consensus-computed fills back as JSON. It owns NO matcher and NO book — that is
|
||||
// the whole point of the decomplection: every access path is a thin wire dialect
|
||||
// onto the single D-Chain CLOB.
|
||||
// onto the single D-Chain DEX.
|
||||
//
|
||||
// This is deliberately separate from WebSocketServer (this file), which drives the
|
||||
// in-process lx.TradingEngine for the rich margin/lending/vault product surface.
|
||||
// The two are orthogonal: WebSocketServer is the product API; VenueWS is the
|
||||
// on-chain CLOB trading path. Mixing the two matchers into one handler would braid
|
||||
// on-chain DEX trading path. Mixing the two matchers into one handler would braid
|
||||
// "what the product does" with "where the order book lives"; keeping VenueWS a
|
||||
// focused Venue translator keeps the on-chain path identical across all four
|
||||
// access methods.
|
||||
@@ -42,7 +42,7 @@ func NewVenueWS(v venue.Venue) *VenueWS {
|
||||
return &VenueWS{
|
||||
venue: v,
|
||||
upgrader: websocket.Upgrader{
|
||||
// The CLOB trading path serves both browsers (allow-listed origins, the
|
||||
// The DEX trading path serves both browsers (allow-listed origins, the
|
||||
// CSRF surface) and non-browser trading clients (server-to-server, CLI,
|
||||
// the SDK) that send NO Origin header. An absent Origin is therefore
|
||||
// allowed (there is no browser to forge a cross-site request); a present
|
||||
@@ -56,7 +56,7 @@ func NewVenueWS(v venue.Venue) *VenueWS {
|
||||
}
|
||||
|
||||
// venueOrder is the inbound JSON order over the WS venue path. It mirrors the FIX
|
||||
// NewOrderSingle fields and the clob_submit frame: a marketable order bounded by
|
||||
// NewOrderSingle fields and the dex_submit frame: a marketable order bounded by
|
||||
// an optional limit price that crosses the resting book.
|
||||
type venueOrder struct {
|
||||
Type string `json:"type"` // "submit" (marketable) — the cross primitive
|
||||
|
||||
+45
-45
@@ -42,7 +42,7 @@ const (
|
||||
TradeWireSize = 48 // Trade: id(8) + price(8) + size(8) + buyer(8) + seller(8) + ts(8)
|
||||
)
|
||||
|
||||
// CLOB market-routed method names, frame sizes, status bytes, and FillWireSize
|
||||
// DEX market-routed method names, frame sizes, status bytes, and FillWireSize
|
||||
// are the FROZEN wire frame defined ONCE in github.com/luxfi/dex/pkg/zapwire —
|
||||
// a pure-Go leaf so the public EVM precompile can import them without dragging
|
||||
// this cgo/GPU engine. They are re-exported here as package-local aliases so
|
||||
@@ -56,27 +56,27 @@ const (
|
||||
// PoolKey). There is exactly one matcher (*lx.OrderBook); two wire framings for
|
||||
// two callers (maker = 8-byte symbol, precompile/proxy = 32-byte poolId).
|
||||
const (
|
||||
CLOBMethodEnsureMarket = zapwire.MethodEnsureMarket
|
||||
CLOBMethodPlace = zapwire.MethodPlace
|
||||
CLOBMethodCancel = zapwire.MethodCancel
|
||||
CLOBMethodSubmit = zapwire.MethodSubmit
|
||||
DEXMethodEnsureMarket = zapwire.MethodEnsureMarket
|
||||
DEXMethodPlace = zapwire.MethodPlace
|
||||
DEXMethodCancel = zapwire.MethodCancel
|
||||
DEXMethodSubmit = zapwire.MethodSubmit
|
||||
)
|
||||
|
||||
// CLOB ack status bytes (shared with the legacy ack/reject codec).
|
||||
// DEX ack status bytes (shared with the legacy ack/reject codec).
|
||||
const (
|
||||
clobStatusPlaced = zapwire.StatusPlaced
|
||||
clobStatusCanceled = zapwire.StatusCanceled
|
||||
clobStatusRejected = zapwire.StatusRejected
|
||||
dexStatusPlaced = zapwire.StatusPlaced
|
||||
dexStatusCanceled = zapwire.StatusCanceled
|
||||
dexStatusRejected = zapwire.StatusRejected
|
||||
)
|
||||
|
||||
// FillWireSize is one fill in a clob_submit response. See zapwire.
|
||||
// FillWireSize is one fill in a dex_submit response. See zapwire.
|
||||
const FillWireSize = zapwire.FillWireSize
|
||||
|
||||
// ZAPServer provides ultra-low-latency order handling for HFT.
|
||||
//
|
||||
// It is multi-market: legacy symbol-keyed methods (place_order/…) operate on a
|
||||
// default book (or a per-symbol book when a symbol is supplied), and the
|
||||
// poolId-keyed CLOB methods (clob_*) operate on per-market books keyed by the
|
||||
// poolId-keyed DEX methods (dex_*) operate on per-market books keyed by the
|
||||
// 32-byte V4 poolId. Every method routes to the same *lx.OrderBook matcher.
|
||||
type ZAPServer struct {
|
||||
// orderBook is the default (single) book the legacy symbol-keyed handlers
|
||||
@@ -86,7 +86,7 @@ type ZAPServer struct {
|
||||
logger log.Logger
|
||||
addr string
|
||||
|
||||
// markets holds per-poolId books for the CLOB (clob_*) surface. Keyed by the
|
||||
// markets holds per-poolId books for the DEX (dex_*) surface. Keyed by the
|
||||
// raw 32-byte poolId.
|
||||
//
|
||||
// This is the GATEWAY to the d-chain: the in-memory book here is the
|
||||
@@ -145,7 +145,7 @@ func (s *ZAPServer) market(id [32]byte, createIfAbsent bool) *lx.OrderBook {
|
||||
return ob
|
||||
}
|
||||
|
||||
// MarketCount returns the number of canonical CLOB markets held server-side.
|
||||
// MarketCount returns the number of canonical DEX markets held server-side.
|
||||
// Diagnostic: proves resting-order state lives here, not in the adapter.
|
||||
func (s *ZAPServer) MarketCount() int {
|
||||
s.marketsMu.Lock()
|
||||
@@ -196,11 +196,11 @@ func (s *ZAPServer) register(server rpc.Server) error {
|
||||
{"best_ask", s.handleGetBestAsk},
|
||||
{"orderbook", s.handleGetOrderBook},
|
||||
{"order", s.handleGetOrder},
|
||||
// CLOB poolId-keyed surface (per-market books) — the V4 precompile path.
|
||||
{CLOBMethodEnsureMarket, s.handleEnsureMarket},
|
||||
{CLOBMethodPlace, s.handleCLOBPlace},
|
||||
{CLOBMethodCancel, s.handleCLOBCancel},
|
||||
{CLOBMethodSubmit, s.handleCLOBSubmit},
|
||||
// DEX poolId-keyed surface (per-market books) — the V4 precompile path.
|
||||
{DEXMethodEnsureMarket, s.handleEnsureMarket},
|
||||
{DEXMethodPlace, s.handleDEXPlace},
|
||||
{DEXMethodCancel, s.handleDEXCancel},
|
||||
{DEXMethodSubmit, s.handleDEXSubmit},
|
||||
}
|
||||
for _, r := range raw {
|
||||
if err := server.RegisterRaw(r.method, r.handler); err != nil {
|
||||
@@ -210,19 +210,19 @@ func (s *ZAPServer) register(server rpc.Server) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterCLOB registers ONLY the poolId-keyed CLOB handlers on an rpc.Server.
|
||||
// RegisterDEX registers ONLY the poolId-keyed DEX handlers on an rpc.Server.
|
||||
// It is the integration seam the LP-9010 precompile tests use to stand up the
|
||||
// real matching engine without the legacy single-book surface. Additive: the
|
||||
// caller may register other handlers on the same server.
|
||||
func RegisterCLOB(server rpc.Server, s *ZAPServer) error {
|
||||
func RegisterDEX(server rpc.Server, s *ZAPServer) error {
|
||||
regs := []struct {
|
||||
method string
|
||||
handler rpc.RawHandler
|
||||
}{
|
||||
{CLOBMethodEnsureMarket, s.handleEnsureMarket},
|
||||
{CLOBMethodPlace, s.handleCLOBPlace},
|
||||
{CLOBMethodCancel, s.handleCLOBCancel},
|
||||
{CLOBMethodSubmit, s.handleCLOBSubmit},
|
||||
{DEXMethodEnsureMarket, s.handleEnsureMarket},
|
||||
{DEXMethodPlace, s.handleDEXPlace},
|
||||
{DEXMethodCancel, s.handleDEXCancel},
|
||||
{DEXMethodSubmit, s.handleDEXSubmit},
|
||||
}
|
||||
for _, r := range regs {
|
||||
if err := server.RegisterRaw(r.method, r.handler); err != nil {
|
||||
@@ -254,7 +254,7 @@ func (s *ZAPServer) Stats() (orders, trades, cancels uint64) {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CLOB (poolId-keyed) handlers — the V4 PoolManager facade.
|
||||
// DEX (poolId-keyed) handlers — the V4 PoolManager facade.
|
||||
// =========================================================================
|
||||
|
||||
// handleEnsureMarket: payload = poolId[32]. Idempotently creates the market
|
||||
@@ -266,10 +266,10 @@ func (s *ZAPServer) handleEnsureMarket(_ context.Context, payload []byte) ([]byt
|
||||
var id [32]byte
|
||||
copy(id[:], payload[0:32])
|
||||
s.market(id, true) // create if absent
|
||||
return s.encodeAck(0, clobStatusPlaced, s.sequence.Add(1)), nil
|
||||
return s.encodeAck(0, dexStatusPlaced, s.sequence.Add(1)), nil
|
||||
}
|
||||
|
||||
// handleCLOBPlace places a RESTING limit order on a poolId market — the CLOB
|
||||
// handleDEXPlace places a RESTING limit order on a poolId market — the DEX
|
||||
// meaning of V4 modifyLiquidity(+delta). Payload (65 bytes):
|
||||
//
|
||||
// [0:32] poolId
|
||||
@@ -279,10 +279,10 @@ func (s *ZAPServer) handleEnsureMarket(_ context.Context, payload []byte) ([]byt
|
||||
// [49:65] user (16 bytes, null-padded)
|
||||
//
|
||||
// Returns ack(orderId, placed). A rest order never crosses here: the precompile
|
||||
// places liquidity, takers cross it via clob_submit.
|
||||
func (s *ZAPServer) handleCLOBPlace(_ context.Context, payload []byte) ([]byte, error) {
|
||||
// places liquidity, takers cross it via dex_submit.
|
||||
func (s *ZAPServer) handleDEXPlace(_ context.Context, payload []byte) ([]byte, error) {
|
||||
if len(payload) < 65 {
|
||||
return s.encodeReject(0, "clob_place: short payload"), nil
|
||||
return s.encodeReject(0, "dex_place: short payload"), nil
|
||||
}
|
||||
var id [32]byte
|
||||
copy(id[:], payload[0:32])
|
||||
@@ -292,7 +292,7 @@ func (s *ZAPServer) handleCLOBPlace(_ context.Context, payload []byte) ([]byte,
|
||||
user := string(trimNull(payload[49:65]))
|
||||
|
||||
if price <= 0 || size <= 0 {
|
||||
return s.encodeReject(0, "clob_place: invalid price or size"), nil
|
||||
return s.encodeReject(0, "dex_place: invalid price or size"), nil
|
||||
}
|
||||
|
||||
ob := s.market(id, true)
|
||||
@@ -307,17 +307,17 @@ func (s *ZAPServer) handleCLOBPlace(_ context.Context, payload []byte) ([]byte,
|
||||
}
|
||||
orderID := ob.AddOrder(order)
|
||||
if orderID == 0 {
|
||||
return s.encodeReject(0, "clob_place: order rejected"), nil
|
||||
return s.encodeReject(0, "dex_place: order rejected"), nil
|
||||
}
|
||||
s.ordersProcessed.Add(1)
|
||||
return s.encodeAck(orderID, clobStatusPlaced, s.sequence.Add(1)), nil
|
||||
return s.encodeAck(orderID, dexStatusPlaced, s.sequence.Add(1)), nil
|
||||
}
|
||||
|
||||
// handleCLOBCancel cancels a resting order on a poolId market — the CLOB meaning
|
||||
// handleDEXCancel cancels a resting order on a poolId market — the DEX meaning
|
||||
// of V4 modifyLiquidity(-delta). Payload (40 bytes): poolId[32] + orderId[8].
|
||||
func (s *ZAPServer) handleCLOBCancel(_ context.Context, payload []byte) ([]byte, error) {
|
||||
func (s *ZAPServer) handleDEXCancel(_ context.Context, payload []byte) ([]byte, error) {
|
||||
if len(payload) < 40 {
|
||||
return s.encodeReject(0, "clob_cancel: short payload"), nil
|
||||
return s.encodeReject(0, "dex_cancel: short payload"), nil
|
||||
}
|
||||
var id [32]byte
|
||||
copy(id[:], payload[0:32])
|
||||
@@ -325,16 +325,16 @@ func (s *ZAPServer) handleCLOBCancel(_ context.Context, payload []byte) ([]byte,
|
||||
|
||||
ob := s.market(id, false)
|
||||
if ob == nil {
|
||||
return s.encodeReject(orderID, "clob_cancel: unknown market"), nil
|
||||
return s.encodeReject(orderID, "dex_cancel: unknown market"), nil
|
||||
}
|
||||
if err := ob.CancelOrder(orderID); err != nil {
|
||||
return s.encodeReject(orderID, err.Error()), nil
|
||||
}
|
||||
s.cancelProcessed.Add(1)
|
||||
return s.encodeAck(orderID, clobStatusCanceled, s.sequence.Add(1)), nil
|
||||
return s.encodeAck(orderID, dexStatusCanceled, s.sequence.Add(1)), nil
|
||||
}
|
||||
|
||||
// handleCLOBSubmit submits a MARKETABLE order against a poolId market — the CLOB
|
||||
// handleDEXSubmit submits a MARKETABLE order against a poolId market — the DEX
|
||||
// meaning of V4 swap. It crosses the resting book and returns the resulting
|
||||
// fills; the adapter derives the BalanceDelta from those fills alone. Payload
|
||||
// (66 bytes):
|
||||
@@ -347,9 +347,9 @@ func (s *ZAPServer) handleCLOBCancel(_ context.Context, payload []byte) ([]byte,
|
||||
// [50:66] user (16 bytes, null-padded)
|
||||
//
|
||||
// Response: fillCount[4] then fillCount × (price[8] + size[8] + takerSide[1]).
|
||||
func (s *ZAPServer) handleCLOBSubmit(_ context.Context, payload []byte) ([]byte, error) {
|
||||
func (s *ZAPServer) handleDEXSubmit(_ context.Context, payload []byte) ([]byte, error) {
|
||||
if len(payload) < 66 {
|
||||
return nil, fmt.Errorf("clob_submit: short payload: %d", len(payload))
|
||||
return nil, fmt.Errorf("dex_submit: short payload: %d", len(payload))
|
||||
}
|
||||
var id [32]byte
|
||||
copy(id[:], payload[0:32])
|
||||
@@ -360,12 +360,12 @@ func (s *ZAPServer) handleCLOBSubmit(_ context.Context, payload []byte) ([]byte,
|
||||
user := string(trimNull(payload[50:66]))
|
||||
|
||||
if size <= 0 {
|
||||
return nil, fmt.Errorf("clob_submit: non-positive size")
|
||||
return nil, fmt.Errorf("dex_submit: non-positive size")
|
||||
}
|
||||
|
||||
ob := s.market(id, false)
|
||||
if ob == nil {
|
||||
return nil, fmt.Errorf("clob_submit: unknown market")
|
||||
return nil, fmt.Errorf("dex_submit: unknown market")
|
||||
}
|
||||
|
||||
order := &lx.Order{
|
||||
@@ -379,7 +379,7 @@ func (s *ZAPServer) handleCLOBSubmit(_ context.Context, payload []byte) ([]byte,
|
||||
order.Type = lx.Market
|
||||
} else {
|
||||
if limitPrice <= 0 {
|
||||
return nil, fmt.Errorf("clob_submit: IOC limit needs positive price")
|
||||
return nil, fmt.Errorf("dex_submit: IOC limit needs positive price")
|
||||
}
|
||||
order.Type = lx.Limit
|
||||
order.Price = limitPrice
|
||||
@@ -387,7 +387,7 @@ func (s *ZAPServer) handleCLOBSubmit(_ context.Context, payload []byte) ([]byte,
|
||||
|
||||
fills, err := ob.SubmitMarketable(order)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("clob_submit: %w", err)
|
||||
return nil, fmt.Errorf("dex_submit: %w", err)
|
||||
}
|
||||
s.tradesExecuted.Add(uint64(len(fills)))
|
||||
|
||||
|
||||
@@ -29,12 +29,12 @@ import (
|
||||
// the zapwire user[16] field the wire carries and the ledger keys balances by.
|
||||
type testAcct struct {
|
||||
scheme lx.AuthScheme
|
||||
priv *ecdsa.PrivateKey // secp256k1 (nil for PQ)
|
||||
mlpriv *mldsa.PrivateKey // ML-DSA-65 (nil for classical)
|
||||
pubKey []byte // ML-DSA pubkey bytes (nil for secp)
|
||||
account userKey // 16-byte settlement account
|
||||
user string // string(account[:]) — the wire user
|
||||
nonce uint64 // next nonce to use
|
||||
priv *ecdsa.PrivateKey // secp256k1 (nil for PQ)
|
||||
mlpriv *mldsa.PrivateKey // ML-DSA-65 (nil for classical)
|
||||
pubKey []byte // ML-DSA pubkey bytes (nil for secp)
|
||||
account userKey // 16-byte settlement account
|
||||
user string // string(account[:]) — the wire user
|
||||
nonce uint64 // next nonce to use
|
||||
}
|
||||
|
||||
// authFor produces an authorization envelope binding (type, nonce, body) under
|
||||
@@ -165,4 +165,3 @@ func signedPayload(t *testing.T, name string, typ TxType, body []byte) []byte {
|
||||
out = append(out, body...)
|
||||
return append(out, auth.encode()...)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -484,7 +484,7 @@ func (b *Block) execute(ctx context.Context, overlay *versiondb.Database) (execR
|
||||
// cannot be funded, the order is REJECTED here — it never matches, never
|
||||
// rests. (A market with unbound assets falls back to the no-custody path so
|
||||
// asset-less test/legacy markets still function; the live custody e2e binds
|
||||
// assets via clob_open_market.)
|
||||
// assets via dex_open_market.)
|
||||
base, quote, assetsBound, aerr := readMarketAssets(overlay, poolID)
|
||||
if aerr != nil {
|
||||
return execResult{}, aerr
|
||||
|
||||
@@ -11,20 +11,20 @@
|
||||
// custody balance change, with GLOBAL value conservation (I = available + locked)
|
||||
// intact across all four crosses.
|
||||
//
|
||||
// THE FOUR PATHS — each is a thin wire dialect onto the SAME clob_* matcher
|
||||
// THE FOUR PATHS — each is a thin wire dialect onto the SAME dex_* matcher
|
||||
// surface (pkg/zapwire), never its own matcher:
|
||||
//
|
||||
// 1. V4 precompile (0x9010): the engine_zap boundary — the EXACT 66-byte
|
||||
// clob_submit frame lux/precompile/dex/engine_zap.go Swap() builds (the V4
|
||||
// zeroForOne/AmountSpecified/SqrtPriceLimit -> CLOB side/size/limit mapping),
|
||||
// dex_submit frame lux/precompile/dex/engine_zap.go Swap() builds (the V4
|
||||
// zeroForOne/AmountSpecified/SqrtPriceLimit -> DEX side/size/limit mapping),
|
||||
// submitted to the venue, with the V4 BalanceDelta derived from the
|
||||
// consensus fills exactly as fillsToDelta does. See v4SubmitFrame /
|
||||
// v4BalanceDelta below and the NOTE on what a live luxd EVM adds.
|
||||
// 2. ZAP native: pkg/zapwire clob_* frames over the venue's rpc.Listen socket —
|
||||
// 2. ZAP native: pkg/zapwire dex_* frames over the venue's rpc.Listen socket —
|
||||
// the existing e2eClient.submit path.
|
||||
// 3. FIX: the new pkg/fix acceptor — a real TCP FIX 4.4 session (Logon,
|
||||
// NewOrderSingle, ExecutionReport) whose order routes to the SAME venue via
|
||||
// the venue.Venue (clob_*) seam.
|
||||
// the venue.Venue (dex_*) seam.
|
||||
// 4. WS: the new pkg/api VenueWS handler — a JSON order over a real
|
||||
// gorilla/websocket /ws connection routed to the SAME venue via venue.Venue.
|
||||
//
|
||||
@@ -59,7 +59,7 @@ import (
|
||||
)
|
||||
|
||||
// signingVenue is a test gateway that implements dexvenue.Venue by emitting
|
||||
// SIGNED clob_* frames over its own ZAP connection. The real zapVenue builds
|
||||
// SIGNED dex_* frames over its own ZAP connection. The real zapVenue builds
|
||||
// UNSIGNED frames; the d-chain authorization gate now refuses those, so a
|
||||
// protocol front-end (FIX/WS) must present a frame signed by the authenticated
|
||||
// session's account. This models the production reality where the gateway holds a
|
||||
@@ -201,7 +201,7 @@ func (c *fixClient) logon(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// custody seeds a deposit over the venue's ZAP socket (the SAME clob_deposit wire
|
||||
// custody seeds a deposit over the venue's ZAP socket (the SAME dex_deposit wire
|
||||
// path the chains/dexvm proxy uses). Uses contentRef so a retry would dedup.
|
||||
func (c *e2eClient) deposit(t *testing.T, user string, asset [32]byte, amount uint64) {
|
||||
t.Helper()
|
||||
@@ -209,28 +209,28 @@ func (c *e2eClient) deposit(t *testing.T, user string, asset [32]byte, amount ui
|
||||
body := zapwire.EncodeDeposit(wireUser(t, user), asset, amount, ref)
|
||||
resp, err := c.conn.Call(c.ctx, zapwire.MethodDeposit, signedPayload(t, user, TxDeposit, body))
|
||||
if err != nil {
|
||||
t.Fatalf("clob_deposit %s %d: %v", user, amount, err)
|
||||
t.Fatalf("dex_deposit %s %d: %v", user, amount, err)
|
||||
}
|
||||
status, credited, derr := zapwire.DecodeBalanceResp(resp)
|
||||
if derr != nil || status != zapwire.StatusPlaced || credited != amount {
|
||||
t.Fatalf("clob_deposit %s %d: status=%d credited=%d err=%v", user, amount, status, credited, derr)
|
||||
t.Fatalf("dex_deposit %s %d: status=%d credited=%d err=%v", user, amount, status, credited, derr)
|
||||
}
|
||||
}
|
||||
|
||||
// openMarket binds a market's (base, quote) asset ids over ZAP (clob_open_market)
|
||||
// openMarket binds a market's (base, quote) asset ids over ZAP (dex_open_market)
|
||||
// so the venue knows which asset each order locks/settles.
|
||||
func (c *e2eClient) openMarket(t *testing.T, pool, base, quote [32]byte) {
|
||||
t.Helper()
|
||||
resp, err := c.conn.Call(c.ctx, zapwire.MethodOpenMarket, zapwire.EncodeOpenMarket(pool, base, quote))
|
||||
if err != nil {
|
||||
t.Fatalf("clob_open_market: %v", err)
|
||||
t.Fatalf("dex_open_market: %v", err)
|
||||
}
|
||||
if len(resp) < 9 || resp[8] != zapwire.StatusPlaced {
|
||||
t.Fatalf("clob_open_market not placed: resp=%x", resp)
|
||||
t.Fatalf("dex_open_market not placed: resp=%x", resp)
|
||||
}
|
||||
}
|
||||
|
||||
// v4SubmitFrame builds the EXACT clob_submit payload lux/precompile/dex
|
||||
// v4SubmitFrame builds the EXACT dex_submit payload lux/precompile/dex
|
||||
// engine_zap.go Swap() emits for a marketable order, so the V4 leg exercises the
|
||||
// real engine_zap WIRE CONTRACT (the frozen 66-byte frame), not a re-derivation:
|
||||
//
|
||||
@@ -436,9 +436,9 @@ func TestFourPathTradingE2E(t *testing.T) {
|
||||
t.Logf("maker rested %d asks of %g LUX @ {10,20,30,40} LUSD (%d LUX locked)", len(legs), askSize, len(legs))
|
||||
|
||||
// --- stand up the FIX acceptor and the WS venue front-end, BOTH onto the SAME
|
||||
// venue via the venue.Venue (clob_*) seam — one matcher, many protocols. The
|
||||
// venue via the venue.Venue (dex_*) seam — one matcher, many protocols. The
|
||||
// gateway holds the per-session signer: a signingVenue translates a protocol
|
||||
// order into a SIGNED clob_* frame on the authenticated session's behalf (the
|
||||
// order into a SIGNED dex_* frame on the authenticated session's behalf (the
|
||||
// FIX SenderCompID / WS user is the account), so the d-chain gate authorizes it.
|
||||
fixVenue := newSigningVenue(t, v.addr)
|
||||
defer fixVenue.close()
|
||||
@@ -497,7 +497,7 @@ func TestFourPathTradingE2E(t *testing.T) {
|
||||
}
|
||||
|
||||
// ===== PATH 1: V4 precompile (engine_zap boundary) =====
|
||||
// Build the EXACT engine_zap clob_submit frame and submit it to the venue. A
|
||||
// Build the EXACT engine_zap dex_submit frame and submit it to the venue. A
|
||||
// BUY of 1 LUX is !zeroForOne, AmountSpecified magnitude 1, a bounded
|
||||
// (non-market) IOC limited at the leg price. Assert the frame is byte-identical
|
||||
// to zapwire.EncodeSubmit (the frozen-frame three-homes guarantee), then derive
|
||||
@@ -518,7 +518,7 @@ func TestFourPathTradingE2E(t *testing.T) {
|
||||
resp, err := cli.conn.Call(cli.ctx, zapwire.MethodSubmit,
|
||||
signedPayload(t, dexvenue.UserHandle(l.user), TxSubmit, frame))
|
||||
if err != nil {
|
||||
t.Fatalf("V4 clob_submit: %v", err)
|
||||
t.Fatalf("V4 dex_submit: %v", err)
|
||||
}
|
||||
fills, err := zapwire.DecodeFills(resp)
|
||||
if err != nil {
|
||||
|
||||
+12
-12
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/luxfi/rpc"
|
||||
)
|
||||
|
||||
// handler.go is the d-chain's CLOB ZAP surface: the clob_* methods a client (the
|
||||
// handler.go is the d-chain's DEX ZAP surface: the dex_* methods a client (the
|
||||
// chains/dexvm proxy relay, the EVM precompile adapter, the maker) calls over
|
||||
// github.com/luxfi/rpc. It is the gateway INTO consensus, not a matcher.
|
||||
//
|
||||
@@ -25,28 +25,28 @@ import (
|
||||
// rows), needing no consensus round-trip.
|
||||
//
|
||||
// WIRE PARITY: every frame here is the FROZEN form from pkg/zapwire — the same
|
||||
// method names (clob_ensure_market/place/cancel/submit), the same request sizes,
|
||||
// method names (dex_ensure_market/place/cancel/submit), the same request sizes,
|
||||
// and the same response codecs (zapwire.EncodeAck / zapwire.EncodeFills, 17-byte
|
||||
// fill). The chains/dexvm relay client and the precompile adapter re-define the
|
||||
// identical constants (they cannot import this cgo-tagged package), so a frame
|
||||
// built by any of them is byte-identical to what this server consumes/produces.
|
||||
|
||||
// clobMethod pairs a frozen ZAP method name with its raw handler. The handler
|
||||
// dexMethod pairs a frozen ZAP method name with its raw handler. The handler
|
||||
// signature is github.com/luxfi/rpc.RawHandler (func(ctx, payload) (resp, err)) —
|
||||
// the SAME func value whether it is reached over the ZAP socket transport
|
||||
// (RegisterCLOB) or over the node's HTTP router (CreateHandlers / ingest.go). One
|
||||
// (RegisterDEX) or over the node's HTTP router (CreateHandlers / ingest.go). One
|
||||
// table, two transports: the ingestion seam never duplicates the handler set.
|
||||
type clobMethod struct {
|
||||
type dexMethod struct {
|
||||
method string
|
||||
handler rpc.RawHandler
|
||||
}
|
||||
|
||||
// clobMethods is the single authoritative method->handler table for the CLOB
|
||||
// surface. Both transports fold over it: RegisterCLOB registers each on a ZAP
|
||||
// dexMethods is the single authoritative method->handler table for the DEX
|
||||
// surface. Both transports fold over it: RegisterDEX registers each on a ZAP
|
||||
// rpc.Server; the HTTP mux (ingest.go) routes POST .../<method> to the same
|
||||
// handler. A method added here is reachable over both transports automatically.
|
||||
func (vm *VM) clobMethods() []clobMethod {
|
||||
return []clobMethod{
|
||||
func (vm *VM) dexMethods() []dexMethod {
|
||||
return []dexMethod{
|
||||
{zapwire.MethodEnsureMarket, vm.handleEnsureMarket},
|
||||
{zapwire.MethodPlace, vm.handlePlace},
|
||||
{zapwire.MethodCancel, vm.handleCancel},
|
||||
@@ -57,13 +57,13 @@ func (vm *VM) clobMethods() []clobMethod {
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCLOB wires the clob_* handlers onto an rpc.Server (the ZAP socket
|
||||
// RegisterDEX wires the dex_* handlers onto an rpc.Server (the ZAP socket
|
||||
// transport). It is the seam the standalone venue entrypoint (cmd/dvenue) and the
|
||||
// socket-level tests use. The in-luxd plugin does NOT use this path — it exposes
|
||||
// the SAME handlers over the node's HTTP router via CreateHandlers (ingest.go).
|
||||
// Additive: the caller may register other methods on the same server.
|
||||
func (vm *VM) RegisterCLOB(server rpc.Server) error {
|
||||
for _, m := range vm.clobMethods() {
|
||||
func (vm *VM) RegisterDEX(server rpc.Server) error {
|
||||
for _, m := range vm.dexMethods() {
|
||||
if err := server.RegisterRaw(m.method, m.handler); err != nil {
|
||||
return fmt.Errorf("dchain: register %s: %w", m.method, err)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"github.com/luxfi/rpc"
|
||||
)
|
||||
|
||||
// handler_test.go proves the d-chain CLOB ZAP surface end to end over a REAL
|
||||
// handler_test.go proves the d-chain DEX ZAP surface end to end over a REAL
|
||||
// rpc.Listen socket, using the SAME client (rpc.ZAPDial) the chains/dexvm proxy
|
||||
// relay uses, and decoding responses with a locally-redefined framing that is
|
||||
// byte-identical to chains/dexvm/relay.go. This is the step-4 proxy-boundary
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
// Because a write blocks until consensus accepts it, the test runs an auto-sealer
|
||||
// goroutine that plays the consensus engine: on a PendingTxs signal it drains the
|
||||
// mempool via BuildBlock -> Verify -> Accept. This is the harness standing in for
|
||||
// the real engine that cmd/dchain wires via rpc.Serve.
|
||||
// the real engine that cmd/dexd (plugin mode) wires via rpc.Serve.
|
||||
|
||||
// ---- relay-side framing, redefined locally to prove byte-parity (must equal
|
||||
// chains/dexvm/relay.go exactly: methods, FillWireSize=17, count[4]+N*17) ----
|
||||
@@ -50,7 +50,7 @@ type relayFill struct {
|
||||
|
||||
// relayDecodeFills mirrors chains/dexvm/relay.go::DecodeFills byte for byte,
|
||||
// including the finite-positive and side∈{0,1} validation, so a successful decode
|
||||
// here proves the d-chain's clob_submit response is exactly what the proxy parses.
|
||||
// here proves the d-chain's dex_submit response is exactly what the proxy parses.
|
||||
func relayDecodeFills(data []byte) ([]relayFill, error) {
|
||||
if len(data) < 4 {
|
||||
return nil, fmt.Errorf("fills response too short: %d", len(data))
|
||||
@@ -122,7 +122,7 @@ func autoSealer(ctx context.Context, t *testing.T, vm *VM) {
|
||||
}
|
||||
}
|
||||
|
||||
// startServer brings up a VM + a real rpc.Listen ZAP server with the clob_*
|
||||
// startServer brings up a VM + a real rpc.Listen ZAP server with the dex_*
|
||||
// handlers, plus the auto-sealer. Returns the dial address and a stop func.
|
||||
func startServer(t *testing.T) (*VM, string, func()) {
|
||||
t.Helper()
|
||||
@@ -141,8 +141,8 @@ func startServer(t *testing.T) (*VM, string, func()) {
|
||||
if err != nil {
|
||||
t.Fatalf("rpc.Listen: %v", err)
|
||||
}
|
||||
if err := vm.RegisterCLOB(server); err != nil {
|
||||
t.Fatalf("RegisterCLOB: %v", err)
|
||||
if err := vm.RegisterDEX(server); err != nil {
|
||||
t.Fatalf("RegisterDEX: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -244,7 +244,7 @@ func TestHandlerByteParityOverSocket(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestHandlerIdempotentReplay proves the d-chain STATE idempotency: replaying the
|
||||
// identical clob_submit frame (a proxy retry on a dropped socket) executes the
|
||||
// identical dex_submit frame (a proxy retry on a dropped socket) executes the
|
||||
// order EXACTLY ONCE — the second call returns zero fills, and the resting book
|
||||
// is consumed only once.
|
||||
func TestHandlerIdempotentReplay(t *testing.T) {
|
||||
|
||||
+14
-14
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/luxfi/rpc"
|
||||
)
|
||||
|
||||
// ingest.go is the D-Chain CLOB ingestion seam for the IN-LUXD native VM. When
|
||||
// ingest.go is the D-Chain DEX ingestion seam for the IN-LUXD native VM. When
|
||||
// the VM runs as a luxd plugin (cmd/dchain via github.com/luxfi/vm/rpc.Serve),
|
||||
// luxd asks the VM for HTTP handlers (CreateHandlers); the plugin transport stands
|
||||
// up a local http.Server per handler inside the plugin process and the node
|
||||
@@ -21,23 +21,23 @@ import (
|
||||
// `r.router.Handle(base+endpoint, handler)` — exact path, no subtree). So we
|
||||
// return ONE handler per method, each keyed by its full sub-path:
|
||||
//
|
||||
// /ext/bc/D/dex/clob_place -> handlePlace
|
||||
// /ext/bc/D/dex/clob_submit -> handleSubmit
|
||||
// /ext/bc/D/dex/clob_cancel -> handleCancel
|
||||
// /ext/bc/D/dex/dex_place -> handlePlace
|
||||
// /ext/bc/D/dex/dex_submit -> handleSubmit
|
||||
// /ext/bc/D/dex/dex_cancel -> handleCancel
|
||||
// ...
|
||||
//
|
||||
// The request body is the FROZEN zapwire payload verbatim; the response body is
|
||||
// the handler's raw bytes (zapwire ack / fills / balance frame).
|
||||
//
|
||||
// THE INVARIANT THAT MATTERS: this is pure transport. The clob_* handler each
|
||||
// route dispatches to is the EXACT func value RegisterCLOB binds on the ZAP socket
|
||||
// (vm.clobMethods) — so an order POSTed here flows submitTx -> mempool.Add ->
|
||||
// THE INVARIANT THAT MATTERS: this is pure transport. The dex_* handler each
|
||||
// route dispatches to is the EXACT func value RegisterDEX binds on the ZAP socket
|
||||
// (vm.dexMethods) — so an order POSTed here flows submitTx -> mempool.Add ->
|
||||
// consensus PendingTxs -> BuildBlock -> Block.Verify (the matcher) -> Block.Accept,
|
||||
// and the bytes returned are the CONSENSUS-COMPUTED outcome (every validator
|
||||
// re-derived the same fills at Verify). It NEVER relays to an external matcher and
|
||||
// NEVER mutates the book synchronously: the chain is the matcher.
|
||||
|
||||
// ingestNamespace is the path segment grouping the CLOB methods under the chain
|
||||
// ingestNamespace is the path segment grouping the DEX methods under the chain
|
||||
// route. The full external path of a method m is /ext/bc/<chainID>/dex/<m>.
|
||||
const ingestNamespace = "dex"
|
||||
|
||||
@@ -46,18 +46,18 @@ const ingestNamespace = "dex"
|
||||
// without parsing it (defence at the boundary — the handlers also length-check).
|
||||
const maxIngestBody = 4 << 10
|
||||
|
||||
// httpHandlers returns the VM's CLOB methods as luxd HTTP handlers, keyed by their
|
||||
// full endpoint sub-path ("/dex/<method>"). It folds the SAME vm.clobMethods table
|
||||
// httpHandlers returns the VM's DEX methods as luxd HTTP handlers, keyed by their
|
||||
// full endpoint sub-path ("/dex/<method>"). It folds the SAME vm.dexMethods table
|
||||
// the ZAP socket transport uses — one method->handler table, two transports — so a
|
||||
// method added to clobMethods is reachable over HTTP automatically. luxd registers
|
||||
// method added to dexMethods is reachable over HTTP automatically. luxd registers
|
||||
// each key as an exact route under /ext/bc/<chainID> (see file doc).
|
||||
//
|
||||
// It ALSO folds the read surface (vm.readMethods, read.go): the clob_get_* query
|
||||
// It ALSO folds the read surface (vm.readMethods, read.go): the dex_get_* query
|
||||
// endpoints that return committed state as JSON. Reads are mounted on the same
|
||||
// route group but never touch the mempool/consensus — orthogonal to the writes.
|
||||
func (vm *VM) httpHandlers() map[string]http.Handler {
|
||||
out := make(map[string]http.Handler, len(vm.clobMethods())+len(vm.readMethods()))
|
||||
for _, m := range vm.clobMethods() {
|
||||
out := make(map[string]http.Handler, len(vm.dexMethods())+len(vm.readMethods()))
|
||||
for _, m := range vm.dexMethods() {
|
||||
out["/"+ingestNamespace+"/"+m.method] = rawHandlerHTTP(m.handler)
|
||||
}
|
||||
for _, m := range vm.readMethods() {
|
||||
|
||||
@@ -27,13 +27,13 @@ import (
|
||||
// committed as D-Chain state (a trade:<height><seq> row). The matcher is the
|
||||
// chain; there is no external venue and no relay.
|
||||
//
|
||||
// The transport under test is vm.clobHTTPHandler() driven through a real
|
||||
// The transport under test is vm.dexHTTPHandler() driven through a real
|
||||
// httptest.Server (which is exactly what the plugin transport stands up inside the
|
||||
// plugin process). An auto-sealer goroutine plays the consensus engine, identical
|
||||
// to handler_test.go's socket harness — proving the SAME submitTx->mempool->
|
||||
// Verify-match path is reached whether the order arrives over HTTP or ZAP.
|
||||
|
||||
// startIngestServer brings up a VM + the in-luxd HTTP CLOB handler over a real
|
||||
// startIngestServer brings up a VM + the in-luxd HTTP DEX handler over a real
|
||||
// httptest.Server, plus the auto-sealer. Returns the VM, the base URL
|
||||
// (.../dex), and a stop func.
|
||||
func startIngestServer(t *testing.T) (*VM, string, func()) {
|
||||
@@ -221,14 +221,14 @@ func TestIngestRejectsNonPost(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestIngestUnknownMethod404 proves an unrouted method path is a 404 (the mux only
|
||||
// mounts the frozen clob_* methods); it must not panic or hang.
|
||||
// mounts the frozen dex_* methods); it must not panic or hang.
|
||||
func TestIngestUnknownMethod404(t *testing.T) {
|
||||
_, base, stop := startIngestServer(t)
|
||||
defer stop()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, methodURL(base, "clob_bogus"), bytes.NewReader([]byte{0x00}))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, methodURL(base, "dex_bogus"), bytes.NewReader([]byte{0x00}))
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// Built under //go:build !cgo: this venue e2e exercises the PURE-GO consensus
|
||||
// path (rebuildable book, zapdb persistence, ZAP socket, match->Verify->Accept)
|
||||
// and needs no cgo/GPU matcher — the GPU accelerator is an opt-in venue
|
||||
// accelerator (cmd/dvenue, //go:build cgo), and the matching CORRECTNESS proven
|
||||
// accelerator (cmd/dexd run, under cgo), and the matching CORRECTNESS proven
|
||||
// here is identical on the CPU and GPU paths. The !cgo gate also keeps this test
|
||||
// in the package's CGO_ENABLED=0 test binary (the only one that links on a host
|
||||
// without luxcpp), so `CGO_ENABLED=0 go test ./pkg/dchain` runs the full venue
|
||||
@@ -18,19 +18,19 @@ package dchain
|
||||
|
||||
// localnet_e2e_test.go is the CAPSTONE venue-level localnet proof for the
|
||||
// standalone D-Chain DEX VM. Unlike handler_test.go (memdb, single market), it
|
||||
// stands the venue up exactly as cmd/dchain would at runtime:
|
||||
// stands the venue up exactly as cmd/dexd would at runtime:
|
||||
//
|
||||
// - a REAL on-disk zapdb (the authoritative persisted chainstate, not memdb),
|
||||
// - the clob_* ZAP surface served over a REAL rpc.Listen socket (the same
|
||||
// - the dex_* ZAP surface served over a REAL rpc.Listen socket (the same
|
||||
// transport the chains/dexvm proxy + 0x9010 precompile dial),
|
||||
// - an auto-sealer goroutine playing the consensus engine (BuildBlock ->
|
||||
// Verify -> Accept) — the harness that cmd/dchain's rpc.Serve gets from the
|
||||
// Verify -> Accept) — the harness that cmd/dexd's plugin rpc.Serve gets from the
|
||||
// node runtime,
|
||||
// - the TWO canonical first-pair markets the maker seeds: LUX/LUSD and
|
||||
// LUX/LETH (dex-architecture-canonical: maker seeds REAL D-Chain pools via
|
||||
// resting limit orders = V4 "liquidity"),
|
||||
// - an external ZAP client (rpc.ZAPDial) that SEEDS resting liquidity
|
||||
// (clob_place), then crosses it with marketable takers (clob_submit), and
|
||||
// (dex_place), then crosses it with marketable takers (dex_submit), and
|
||||
// verifies the CONSENSUS-COMPUTED fills + the resulting book,
|
||||
// - a Shutdown + restart that rebuilds the authoritative book from persisted
|
||||
// rows and proves the resting liquidity SURVIVED (it is a real chain, not an
|
||||
@@ -97,8 +97,8 @@ func bootVenue(t *testing.T, dir string) *venue {
|
||||
if err != nil {
|
||||
t.Fatalf("rpc.Listen: %v", err)
|
||||
}
|
||||
if err := vm.RegisterCLOB(server); err != nil {
|
||||
t.Fatalf("RegisterCLOB: %v", err)
|
||||
if err := vm.RegisterDEX(server); err != nil {
|
||||
t.Fatalf("RegisterDEX: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -122,7 +122,7 @@ func (v *venue) stop() {
|
||||
_ = v.db.Close()
|
||||
}
|
||||
|
||||
// e2eClient is an external ZAP client speaking the FROZEN clob_* frames — the
|
||||
// e2eClient is an external ZAP client speaking the FROZEN dex_* frames — the
|
||||
// exact surface the chains/dexvm proxy relay and the 0x9010 precompile use.
|
||||
type e2eClient struct {
|
||||
conn *rpc.ZAPConn
|
||||
@@ -144,10 +144,10 @@ func (c *e2eClient) ensureMarket(t *testing.T, pool [32]byte) {
|
||||
t.Helper()
|
||||
resp, err := c.conn.Call(c.ctx, zapwire.MethodEnsureMarket, zapwire.EncodeEnsureMarket(pool))
|
||||
if err != nil {
|
||||
t.Fatalf("clob_ensure_market: %v", err)
|
||||
t.Fatalf("dex_ensure_market: %v", err)
|
||||
}
|
||||
if len(resp) < 9 || resp[8] != zapwire.StatusPlaced {
|
||||
t.Fatalf("clob_ensure_market not placed: resp=%x", resp)
|
||||
t.Fatalf("dex_ensure_market not placed: resp=%x", resp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,10 +157,10 @@ func (c *e2eClient) place(t *testing.T, pool [32]byte, side uint8, price, size f
|
||||
body := zapwire.EncodePlace(pool, side, price, size, wireUser(t, user))
|
||||
resp, err := c.conn.Call(c.ctx, zapwire.MethodPlace, signedPayload(t, user, TxPlace, body))
|
||||
if err != nil {
|
||||
t.Fatalf("clob_place %s %v@%v: %v", sideName(side), size, price, err)
|
||||
t.Fatalf("dex_place %s %v@%v: %v", sideName(side), size, price, err)
|
||||
}
|
||||
if len(resp) < 9 || resp[8] != zapwire.StatusPlaced {
|
||||
t.Fatalf("clob_place rejected %s %v@%v: resp=%x", sideName(side), size, price, resp)
|
||||
t.Fatalf("dex_place rejected %s %v@%v: resp=%x", sideName(side), size, price, resp)
|
||||
}
|
||||
return binary.BigEndian.Uint64(resp[0:8])
|
||||
}
|
||||
@@ -172,11 +172,11 @@ func (c *e2eClient) submit(t *testing.T, pool [32]byte, side uint8, limit, size
|
||||
body := zapwire.EncodeSubmit(pool, side, false, limit, size, wireUser(t, user))
|
||||
resp, err := c.conn.Call(c.ctx, zapwire.MethodSubmit, signedPayload(t, user, TxSubmit, body))
|
||||
if err != nil {
|
||||
t.Fatalf("clob_submit %s %v@%v: %v", sideName(side), size, limit, err)
|
||||
t.Fatalf("dex_submit %s %v@%v: %v", sideName(side), size, limit, err)
|
||||
}
|
||||
fills, err := zapwire.DecodeFills(resp)
|
||||
if err != nil {
|
||||
t.Fatalf("clob_submit fills decode (BYTE PARITY): %v (resp=%x)", err, resp)
|
||||
t.Fatalf("dex_submit fills decode (BYTE PARITY): %v (resp=%x)", err, resp)
|
||||
}
|
||||
return fills
|
||||
}
|
||||
@@ -203,11 +203,11 @@ func (v *venue) restingByMarket(pool [32]byte) (orders int, remaining float64) {
|
||||
}
|
||||
|
||||
// TestLocalnetVenueE2E is the capstone venue bring-up + on-chain e2e:
|
||||
// 1. boot the venue on an on-disk zapdb (real persisted chainstate),
|
||||
// 2. seed LUX/LUSD and LUX/LETH with resting maker liquidity (clob_place),
|
||||
// 3. cross both books with marketable takers (clob_submit),
|
||||
// 4. assert the consensus-computed fills are correct + books updated,
|
||||
// 5. restart the venue and prove the resting liquidity SURVIVED (real chain).
|
||||
// 1. boot the venue on an on-disk zapdb (real persisted chainstate),
|
||||
// 2. seed LUX/LUSD and LUX/LETH with resting maker liquidity (dex_place),
|
||||
// 3. cross both books with marketable takers (dex_submit),
|
||||
// 4. assert the consensus-computed fills are correct + books updated,
|
||||
// 5. restart the venue and prove the resting liquidity SURVIVED (real chain).
|
||||
func TestLocalnetVenueE2E(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
@@ -220,7 +220,7 @@ func TestLocalnetVenueE2E(t *testing.T) {
|
||||
leth := poolIDForSymbol("LUX/LETH")
|
||||
|
||||
// --- maker seeding: ensure markets, then rest two-sided liquidity. This is
|
||||
// what `maker -dex <endpoint>` does for the first pairs, in clob_* form. ---
|
||||
// what `maker -dex <endpoint>` does for the first pairs, in dex_* form. ---
|
||||
cli.ensureMarket(t, lusd)
|
||||
cli.ensureMarket(t, leth)
|
||||
t.Logf("markets ensured: LUX/LUSD, LUX/LETH")
|
||||
@@ -272,7 +272,7 @@ func TestLocalnetVenueE2E(t *testing.T) {
|
||||
t.Logf("LUX/LUSD post-cross: 23.0 resting (3@101 ask + 20 bid) — consensus-settled")
|
||||
|
||||
// --- on-chain cross #2: a 5-unit sell @0.049 on LUX/LETH hits the [email protected]
|
||||
// bid (one fill of 3), residual 2 does NOT rest (clob_submit is IOC-style:
|
||||
// bid (one fill of 3), residual 2 does NOT rest (dex_submit is IOC-style:
|
||||
// marketable, unfilled remainder dropped). ---
|
||||
f2 := cli.submit(t, leth, zapwire.SideSell, 0.049, 5.0, "taker")
|
||||
if len(f2) != 1 {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// mempool.go is the d-chain pending-transaction queue. ZAP write frames
|
||||
// (clob_place/cancel/submit) are NOT executed synchronously — the handler decodes
|
||||
// (dex_place/cancel/submit) are NOT executed synchronously — the handler decodes
|
||||
// the frame into a Tx, Adds it here, and the mempool signals the consensus engine
|
||||
// (init.ToEngine <- PendingTxs) that there is work. The engine responds by
|
||||
// calling VM.BuildBlock, which drains the mempool in arrival (sequence) order.
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
// mempool is a FIFO of pending transactions guarded by a mutex. It is small and
|
||||
// deliberately simple: ordering is arrival order, which is the sequence the
|
||||
// matcher consumes. (A fee-priority mempool is a future concern; the venue's CLOB
|
||||
// matcher consumes. (A fee-priority mempool is a future concern; the venue's DEX
|
||||
// is sequence-fair by design.)
|
||||
//
|
||||
// CANCEL / TOMBSTONE (synchronous-or-nothing custody, RED #4): a custody op that
|
||||
|
||||
+13
-13
@@ -14,13 +14,13 @@ import (
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
)
|
||||
|
||||
// read.go is the D-Chain's READ surface: clob_get_* query endpoints that return
|
||||
// read.go is the D-Chain's READ surface: dex_get_* query endpoints that return
|
||||
// COMMITTED chain state (resting orders, the trade log, markets, the book) as
|
||||
// JSON. It is the orthogonal counterpart to handler.go's write surface:
|
||||
//
|
||||
// handler.go clob_place/submit/cancel/... -> mempool -> consensus -> Verify-match.
|
||||
// handler.go dex_place/submit/cancel/... -> mempool -> consensus -> Verify-match.
|
||||
// FROZEN binary zapwire frames. Mutates state through a consensus round.
|
||||
// read.go clob_get_trades/orders/markets/book
|
||||
// read.go dex_get_trades/orders/markets/book
|
||||
// JSON. Reads committed state directly under vm.mu. ZERO consensus
|
||||
// impact — no mempool, no block, no waiter; a read can never change a
|
||||
// fill or a balance.
|
||||
@@ -53,7 +53,7 @@ const (
|
||||
MethodGetBook = "dex_get_book"
|
||||
)
|
||||
|
||||
// defaultTradeLimit bounds an unparameterized clob_get_trades response so a chain
|
||||
// defaultTradeLimit bounds an unparameterized dex_get_trades response so a chain
|
||||
// with a long fill history cannot return an unbounded body by default. A caller
|
||||
// paging the full log passes an explicit limit + since cursor.
|
||||
const defaultTradeLimit = 1000
|
||||
@@ -116,7 +116,7 @@ type tradeJSON struct {
|
||||
Timestamp int64 `json:"timestamp"` // unix nanos
|
||||
}
|
||||
|
||||
// getTradesResp is the clob_get_trades body: the chain head plus the committed
|
||||
// getTradesResp is the dex_get_trades body: the chain head plus the committed
|
||||
// fills for the requested range, in matcher production order (height then seq).
|
||||
type getTradesResp struct {
|
||||
chainHead
|
||||
@@ -126,7 +126,7 @@ type getTradesResp struct {
|
||||
|
||||
// handleGetTrades serves the committed trade log:
|
||||
//
|
||||
// GET .../dex/clob_get_trades?since=<height>&limit=<n>
|
||||
// GET .../dex/dex_get_trades?since=<height>&limit=<n>
|
||||
//
|
||||
// since (optional): first accepted height to include (inclusive); default 0.
|
||||
// limit (optional): max rows; default defaultTradeLimit.
|
||||
@@ -190,7 +190,7 @@ type orderJSON struct {
|
||||
User string `json:"user"`
|
||||
}
|
||||
|
||||
// getOrdersResp is the clob_get_orders body: the chain head plus the market's
|
||||
// getOrdersResp is the dex_get_orders body: the chain head plus the market's
|
||||
// resting book rows.
|
||||
type getOrdersResp struct {
|
||||
chainHead
|
||||
@@ -201,7 +201,7 @@ type getOrdersResp struct {
|
||||
|
||||
// handleGetOrders serves a market's resting orders:
|
||||
//
|
||||
// GET .../dex/clob_get_orders?market=<hex>
|
||||
// GET .../dex/dex_get_orders?market=<hex>
|
||||
//
|
||||
// The orders are read from the in-RAM book (a deterministic fold of the committed
|
||||
// order:* rows), so a follower and the proposer at the same height return the same
|
||||
@@ -262,7 +262,7 @@ type marketJSON struct {
|
||||
BestAsk float64 `json:"bestAsk"`
|
||||
}
|
||||
|
||||
// getMarketsResp is the clob_get_markets body: the chain head plus every market.
|
||||
// getMarketsResp is the dex_get_markets body: the chain head plus every market.
|
||||
type getMarketsResp struct {
|
||||
chainHead
|
||||
Count int `json:"count"`
|
||||
@@ -271,11 +271,11 @@ type getMarketsResp struct {
|
||||
|
||||
// handleGetMarkets serves every market on the chain:
|
||||
//
|
||||
// GET .../dex/clob_get_markets
|
||||
// GET .../dex/dex_get_markets
|
||||
//
|
||||
// Each record carries the market's durable identity (poolID, symbol, bound
|
||||
// assets) and an in-RAM book summary (resting orders, best bid/ask). This is what
|
||||
// the markets-display lists; combined with clob_get_trades it renders a venue.
|
||||
// the markets-display lists; combined with dex_get_trades it renders a venue.
|
||||
func (vm *VM) handleGetMarkets(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeReadErr(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
@@ -341,7 +341,7 @@ type levelJSON struct {
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// getBookResp is the clob_get_book body: the chain head plus the market's
|
||||
// getBookResp is the dex_get_book body: the chain head plus the market's
|
||||
// aggregated bid/ask ladder and totals.
|
||||
type getBookResp struct {
|
||||
chainHead
|
||||
@@ -356,7 +356,7 @@ type getBookResp struct {
|
||||
|
||||
// handleGetBook serves a market's aggregated order book:
|
||||
//
|
||||
// GET .../dex/clob_get_book?market=<hex>&depth=<n>
|
||||
// GET .../dex/dex_get_book?market=<hex>&depth=<n>
|
||||
//
|
||||
// depth (optional): number of price levels per side; default 20. Levels and
|
||||
// totals are read from the in-RAM book (the committed-row fold).
|
||||
|
||||
@@ -22,17 +22,17 @@ import (
|
||||
"github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// read_test.go proves the D-Chain READ surface (read.go / clob_get_*):
|
||||
// read_test.go proves the D-Chain READ surface (read.go / dex_get_*):
|
||||
//
|
||||
// 1. The read endpoints are reachable over the EXACT in-luxd HTTP transport
|
||||
// (CreateHandlers, mounted at /ext/bc/<D>/dex/<method>) — the same routes a
|
||||
// deployed validator exposes.
|
||||
// 2. After a crossing order is matched into a committed block (via the write
|
||||
// path + the auto-sealer playing consensus), clob_get_trades returns the
|
||||
// committed fill, clob_get_markets lists the market, clob_get_orders/book
|
||||
// path + the auto-sealer playing consensus), dex_get_trades returns the
|
||||
// committed fill, dex_get_markets lists the market, dex_get_orders/book
|
||||
// reflect the resting set.
|
||||
// 3. CROSS-VALIDATOR INVARIANT (in-process model): two INDEPENDENT VMs that
|
||||
// Accept the SAME block bytes return BYTE-IDENTICAL clob_get_trades JSON and
|
||||
// Accept the SAME block bytes return BYTE-IDENTICAL dex_get_trades JSON and
|
||||
// the same head root. This is the local proof of the property the live
|
||||
// 5-validator test checks: same accepted block => same committed read on
|
||||
// every node. (A follower in production reaches the same state by Verify+
|
||||
@@ -186,7 +186,7 @@ func crossOneMarket(t *testing.T, base string, f readFixture) []zapwire.Fill {
|
||||
}
|
||||
|
||||
// TestRead_TradesAfterCommittedFill is the core happy path: a crossing order is
|
||||
// matched into a committed block, and clob_get_trades returns that fill from
|
||||
// matched into a committed block, and dex_get_trades returns that fill from
|
||||
// durable state with the right price/size/side and a non-genesis head.
|
||||
func TestRead_TradesAfterCommittedFill(t *testing.T) {
|
||||
vm, base, stop := startReadServer(t)
|
||||
@@ -257,7 +257,7 @@ func TestRead_MarketsAndOrdersAndBook(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
t.Fatalf("market %s not in clob_get_markets: %+v", marketHex, markets.Markets)
|
||||
t.Fatalf("market %s not in dex_get_markets: %+v", marketHex, markets.Markets)
|
||||
}
|
||||
if !found.AssetsBound {
|
||||
t.Errorf("market assetsBound=false, want true (open_market bound them)")
|
||||
@@ -328,7 +328,7 @@ func TestRead_ZeroConsensusImpact(t *testing.T) {
|
||||
|
||||
// TestRead_CrossValidatorIdenticalTrades is the in-process model of the live
|
||||
// 5-validator question: two INDEPENDENT VMs (separate DBs) that Accept the SAME
|
||||
// sequence of block bytes must return BYTE-IDENTICAL clob_get_trades JSON and the
|
||||
// sequence of block bytes must return BYTE-IDENTICAL dex_get_trades JSON and the
|
||||
// same head root. This isolates the replication property from the network: if a
|
||||
// block's bytes are delivered (gossip in production) and Verify+Accept succeed,
|
||||
// the committed read is identical — which is exactly what the read RPC lets the
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// OUTCOME a block's execution produced, and the VM's registry that lets a ZAP
|
||||
// handler block until "its" tx is decided by an accepted block.
|
||||
//
|
||||
// The decomplected shape: a clob_* write is NOT executed when it arrives — it is
|
||||
// The decomplected shape: a dex_* write is NOT executed when it arrives — it is
|
||||
// queued, ordered by consensus, matched at Verify, committed at Accept. But the
|
||||
// caller (the proxy's synchronous Relay, the precompile's swap) needs the
|
||||
// resulting fills/ack back. So the handler registers a waiter keyed by the tx's
|
||||
@@ -32,9 +32,9 @@ import (
|
||||
type txOutcome struct {
|
||||
txID ids.ID
|
||||
typ TxType
|
||||
orderID uint64 // placed / cancelled order id (0 for submit/ensure)
|
||||
orderID uint64 // placed / cancelled order id (0 for submit/ensure)
|
||||
fills []zapwire.Fill // submit fills (nil otherwise)
|
||||
status uint8 // zapwire.StatusPlaced / StatusCanceled / StatusRejected
|
||||
status uint8 // zapwire.StatusPlaced / StatusCanceled / StatusRejected
|
||||
}
|
||||
|
||||
// toWireFills converts a submit's matched trades into the FROZEN wire Fill form
|
||||
|
||||
@@ -111,7 +111,7 @@ const (
|
||||
)
|
||||
|
||||
// marketConstraint is the MINIMAL notional scaffold: just enough to express the
|
||||
// one constraint a value-bearing CLOB must enforce against integer flooring — an
|
||||
// one constraint a value-bearing DEX must enforce against integer flooring — an
|
||||
// executable order MUST reserve a non-zero lock. It deliberately does NOT model a
|
||||
// full fee/tick/lot/minNotional engine (that is a separate roadmap); the only
|
||||
// invariant here is "arithmetic flooring can never create a FREE executable order".
|
||||
@@ -204,7 +204,7 @@ func debitWithdraw(db database.KeyValueReaderWriterDeleter, user string, asset [
|
||||
//
|
||||
// IDENTITY: the settlement identity is the WALLET (AccountID); UserID8 is only a
|
||||
// matcher hint. The TAKER is the submitter, whose full 16-byte wallet is carried on
|
||||
// the ephemeral order this block from the decoded clob_submit invocation context
|
||||
// the ephemeral order this block from the decoded dex_submit invocation context
|
||||
// (never the 8-byte row handle — the taker never rests, so its full wallet is always
|
||||
// in hand). The MAKER is a RESTING order whose in-RAM user is only 8 bytes after a
|
||||
// restart (RowToOrder folds the GPU row's 8-byte UserID handle), so its full wallet
|
||||
|
||||
@@ -809,7 +809,7 @@ func TestSettlementIdentityWidthEnforced(t *testing.T) {
|
||||
// the width property the ledger depends on.
|
||||
v16 := idOf(t, victim)
|
||||
var colliding userKey
|
||||
copy(colliding[:], v16[:8]) // SAME 8-byte matcher handle ...
|
||||
copy(colliding[:], v16[:8]) // SAME 8-byte matcher handle ...
|
||||
copy(colliding[8:], []byte{0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8}) // ... distinct tail
|
||||
if v16 == colliding {
|
||||
t.Fatal("colliding-handle identities resolved to the SAME 16-byte identity (width fold lost bytes 8..15)")
|
||||
|
||||
+3
-3
@@ -42,7 +42,7 @@ const (
|
||||
prefixSeen = "seen:" // processed tx ids (idempotency / replay dedup)
|
||||
prefixMeta = "meta:"
|
||||
|
||||
// The CLOB custody ledger: where the money LIVES inside the d-chain. A
|
||||
// The DEX custody ledger: where the money LIVES inside the d-chain. A
|
||||
// deposit credits balance:; placing/submitting an order moves available ->
|
||||
// locked: for the asset the order spends; a fill moves locked: between maker
|
||||
// and taker; a cancel/reject returns locked: -> balance:; a withdraw debits
|
||||
@@ -94,7 +94,7 @@ const (
|
||||
// accepted block; re-applying it is a no-op. This is the d-chain STATE
|
||||
// idempotency index: the frozen zapwire frame carries no nonce, so dedup is
|
||||
// content-addressed on the tx id (which subsumes the wire user[16] and the full
|
||||
// payload). A relay that retries a dropped clob_submit re-sends the identical
|
||||
// payload). A relay that retries a dropped dex_submit re-sends the identical
|
||||
// bytes -> identical tx id -> deduped, exactly once.
|
||||
func seenKey(txID ids.ID) []byte {
|
||||
k := make([]byte, len(prefixSeen)+32)
|
||||
@@ -120,7 +120,7 @@ func isSeen(db database.KeyValueReader, txID ids.ID) (bool, error) {
|
||||
// ORIGINAL outcome (status + orderID + fills) so a later replay of the same
|
||||
// content-addressed tx returns the FIRST result verbatim rather than a blanket
|
||||
// reject — the idempotency contract a caller (precompile / proxy) needs: a
|
||||
// retried clob_place returns its original Placed+orderID, a retried clob_submit
|
||||
// retried dex_place returns its original Placed+orderID, a retried dex_submit
|
||||
// returns its original fills, never a spurious "rejected". (A bare presence
|
||||
// byte made a deduped place indistinguishable from a genuine rejection, which
|
||||
// reverted the EVM tx whose precompile re-ran the place across the host's
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ var (
|
||||
// would mint against the ledger.
|
||||
ErrInsufficientLocked = errors.New("dchain: insufficient locked balance")
|
||||
// ErrMarketAssetsUnbound: a balance-bearing order on a market whose (base,
|
||||
// quote) assets were never bound (clob_open_market) — cannot value-check it.
|
||||
// quote) assets were never bound (dex_open_market) — cannot value-check it.
|
||||
ErrMarketAssetsUnbound = errors.New("dchain: market assets not bound")
|
||||
// ErrFillMissingUnits: a consensus fill reached settlement without the exact
|
||||
// integer lane (BaseUnits/QuoteUnits) — the VM must set SizeUnits on every
|
||||
|
||||
+9
-9
@@ -59,11 +59,11 @@ type VM struct {
|
||||
books map[[32]byte]*lx.OrderBook
|
||||
|
||||
// Accepted-block index + linear-chain head.
|
||||
acceptedBlocks map[ids.ID]*Block
|
||||
heightIndex map[uint64]ids.ID
|
||||
lastAcceptedID ids.ID
|
||||
acceptedBlocks map[ids.ID]*Block
|
||||
heightIndex map[uint64]ids.ID
|
||||
lastAcceptedID ids.ID
|
||||
lastAcceptedHeight uint64
|
||||
lastRoot [Size]byte
|
||||
lastRoot [Size]byte
|
||||
|
||||
// processingBlocks indexes built/verified blocks not yet accepted, so a
|
||||
// plugin-transport Accept — which carries only the block ID and re-resolves
|
||||
@@ -81,7 +81,7 @@ type VM struct {
|
||||
// toEngine is retained for the mempool signal path.
|
||||
toEngine chan<- block.Message
|
||||
|
||||
// zapIngest is the canonical co-located ZAP CLOB socket (zapingest.go), served
|
||||
// zapIngest is the canonical co-located ZAP DEX socket (zapingest.go), served
|
||||
// in-plugin when init Config sets zapIngestAddr. nil when HTTP-compat-only.
|
||||
// zapIngestCancel stops its serve goroutine on Shutdown. Both guarded by vm.mu.
|
||||
zapIngest rpc.Server
|
||||
@@ -132,7 +132,7 @@ func (vm *VM) Initialize(ctx context.Context, init block.Init) error {
|
||||
|
||||
vm.preferred = vm.lastAcceptedID
|
||||
|
||||
// Start the canonical co-located ZAP CLOB ingestion socket if the chain config
|
||||
// Start the canonical co-located ZAP DEX ingestion socket if the chain config
|
||||
// names a listen address (zapingest.go). This runs AFTER the durable state is
|
||||
// loaded so the socket never accepts an order the VM cannot yet sequence. It is
|
||||
// a no-op (HTTP-compat-only) when no address is configured. Transport ⟂
|
||||
@@ -503,14 +503,14 @@ func (vm *VM) SetState(ctx context.Context, state uint32) error {
|
||||
// Version returns the VM version string.
|
||||
func (vm *VM) Version(ctx context.Context) (string, error) { return Version, nil }
|
||||
|
||||
// NewHTTPHandler returns the VM's HTTP handler. The d-chain serves its CLOB API
|
||||
// NewHTTPHandler returns the VM's HTTP handler. The d-chain serves its DEX API
|
||||
// over the ZAP gateway (handler.go), not HTTP, so this is nil.
|
||||
func (vm *VM) NewHTTPHandler(ctx context.Context) (http.Handler, error) { return nil, nil }
|
||||
|
||||
// CreateHandlers returns the VM's named HTTP handlers for luxd to mount under
|
||||
// /ext/bc/<DCHAIN_ID>/ (and the "D" alias). It returns one handler per CLOB method
|
||||
// /ext/bc/<DCHAIN_ID>/ (and the "D" alias). It returns one handler per DEX method
|
||||
// keyed by its full sub-path ("/dex/<method>"), so an order POSTed to
|
||||
// /ext/bc/D/dex/clob_submit reaches the matcher through the node's own router —
|
||||
// /ext/bc/D/dex/dex_submit reaches the matcher through the node's own router —
|
||||
// the in-luxd ingestion seam (ingest.go). This is how an order enters the native
|
||||
// VM: submitTx -> mempool -> consensus -> Verify-match. The plugin transport
|
||||
// (github.com/luxfi/vm/rpc) serves these handlers from a local http.Server inside
|
||||
|
||||
@@ -39,7 +39,7 @@ func newTestVM(t *testing.T, db database.Database) (*VM, chan block.Message) {
|
||||
return vm, toEngine
|
||||
}
|
||||
|
||||
// submitFrame builds the [type:1][body] tx bytes for a clob op, the exact bytes
|
||||
// submitFrame builds the [type:1][body] tx bytes for a dex op, the exact bytes
|
||||
// the handler/proxy would Add to the mempool.
|
||||
func ensureMarketTx(t *testing.T, pool [32]byte) *Tx {
|
||||
t.Helper()
|
||||
@@ -522,7 +522,7 @@ func TestVMRestartLegacyGenesisDB(t *testing.T) {
|
||||
// Verify, which carries the full block bytes — and then calls Accept on whatever
|
||||
// GetBlock returns. Before the processing-block index, GetBlock(builtID) returned
|
||||
// ErrNotFound (only accepted blocks were indexed), so the engine's self-finalize
|
||||
// Accept was a silent no-op, the clob_* submitTx waiter hung forever, and no
|
||||
// Accept was a silent no-op, the dex_* submitTx waiter hung forever, and no
|
||||
// further blocks were built. This drives the EXACT plugin-shaped path the live
|
||||
// node uses — BuildBlock -> GetBlock(id) -> Accept — which every existing test
|
||||
// skipped by accepting the *Block returned from BuildBlock directly.
|
||||
@@ -533,7 +533,7 @@ func TestVMAcceptViaGetBlock(t *testing.T) {
|
||||
|
||||
vm, toEngine := newTestVM(t, db)
|
||||
|
||||
// A clob tx in the mempool, then the proposer's lifecycle exactly as the
|
||||
// A dex tx in the mempool, then the proposer's lifecycle exactly as the
|
||||
// engine drives it over the plugin transport.
|
||||
vm.mempool.Add(ensureMarketTx(t, pool))
|
||||
vm.mempool.Add(placePoolTx(t, pool, zapwire.SideSell, 101.0, 5.0, "maker-a"))
|
||||
|
||||
+13
-13
@@ -11,14 +11,14 @@ import (
|
||||
"github.com/luxfi/rpc"
|
||||
)
|
||||
|
||||
// zapingest.go is the D-Chain CLOB ZAP ingestion seam for the IN-LUXD native VM —
|
||||
// zapingest.go is the D-Chain DEX ZAP ingestion seam for the IN-LUXD native VM —
|
||||
// the CANONICAL native order path. It is the second transport over the one
|
||||
// authoritative handler core (clobMethods, handler.go), symmetric with the HTTP
|
||||
// authoritative handler core (dexMethods, handler.go), symmetric with the HTTP
|
||||
// compat transport (ingest.go / CreateHandlers):
|
||||
//
|
||||
// ZAP socket (this file) : RegisterCLOB on rpc.Listen — the HFT native path,
|
||||
// ZAP socket (this file) : RegisterDEX on rpc.Listen — the HFT native path,
|
||||
// binary wire, co-located on the pod. CANONICAL.
|
||||
// HTTP router (ingest.go) : the same clobMethods over the node's /ext/bc route.
|
||||
// HTTP router (ingest.go) : the same dexMethods over the node's /ext/bc route.
|
||||
// COMPAT (web / exchange-api / debug).
|
||||
//
|
||||
// WHY BOTH, AND WHY THIS IS NOT DUPLICATION: transport is orthogonal to consensus.
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
// Block.Accept — validators replay from the accepted block BYTES, never from the
|
||||
// wire stream or a wall clock. So the choice of socket vs HTTP is purely a
|
||||
// submission perf/UX axis with no safety implication; the ONE matcher core
|
||||
// (clobMethods -> submitTx -> mempool) is shared, never re-implemented per
|
||||
// transport. The ZAP socket is the canonical native path because the CLOB's whole
|
||||
// (dexMethods -> submitTx -> mempool) is shared, never re-implemented per
|
||||
// transport. The ZAP socket is the canonical native path because the DEX's whole
|
||||
// point is the low-latency streaming HFT seam (co-locate the matcher with the ZAP
|
||||
// relay; see dex perf notes); HTTP stays as the broad-compatibility surface.
|
||||
//
|
||||
@@ -44,7 +44,7 @@ import (
|
||||
// only: unknown fields are ignored, an empty/absent Config is the zero value (no
|
||||
// ZAP socket). Only the fields the VM actually consumes are declared.
|
||||
type vmConfig struct {
|
||||
// ZAPIngestAddr, when non-empty, is the TCP address the co-located ZAP CLOB
|
||||
// ZAPIngestAddr, when non-empty, is the TCP address the co-located ZAP DEX
|
||||
// socket listens on inside the plugin process (e.g. "0.0.0.0:9101"). Empty
|
||||
// means the canonical socket is not served (HTTP compat only). It is a
|
||||
// pod-local listen address the node/operator assigns; orders arriving here
|
||||
@@ -67,9 +67,9 @@ func parseConfig(b []byte) (vmConfig, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// startZAPIngest opens the canonical co-located ZAP CLOB socket and serves the SAME
|
||||
// clobMethods the HTTP transport folds over. It is a no-op when addr is empty (the
|
||||
// VM serves HTTP only). It is ADDITIVE: it reuses RegisterCLOB (the existing socket
|
||||
// startZAPIngest opens the canonical co-located ZAP DEX socket and serves the SAME
|
||||
// dexMethods the HTTP transport folds over. It is a no-op when addr is empty (the
|
||||
// VM serves HTTP only). It is ADDITIVE: it reuses RegisterDEX (the existing socket
|
||||
// seam cmd/dvenue uses) and adds no new handler logic. Must be called under vm.mu,
|
||||
// after the durable state is loaded (so the socket never accepts an order before
|
||||
// the VM can sequence it). The server runs in its own goroutine whose lifetime is
|
||||
@@ -83,9 +83,9 @@ func (vm *VM) startZAPIngest(addr string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("dchain: ZAP ingest listen %q: %w", addr, err)
|
||||
}
|
||||
if err := vm.RegisterCLOB(server); err != nil {
|
||||
if err := vm.RegisterDEX(server); err != nil {
|
||||
_ = server.Close()
|
||||
return fmt.Errorf("dchain: register CLOB on ZAP ingest socket: %w", err)
|
||||
return fmt.Errorf("dchain: register DEX on ZAP ingest socket: %w", err)
|
||||
}
|
||||
|
||||
serveCtx, cancel := context.WithCancel(context.Background())
|
||||
@@ -96,7 +96,7 @@ func (vm *VM) startZAPIngest(addr string) error {
|
||||
vm.log.Error("dchain ZAP ingest serve", "addr", server.Addr(), "err", serr)
|
||||
}
|
||||
}()
|
||||
vm.log.Info("dchain ZAP ingest serving (canonical native CLOB path)", "addr", server.Addr())
|
||||
vm.log.Info("dchain ZAP ingest serving (canonical native DEX path)", "addr", server.Addr())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ import (
|
||||
"github.com/luxfi/rpc"
|
||||
)
|
||||
|
||||
// zapingest_test.go proves the CANONICAL in-luxd ZAP CLOB ingestion seam
|
||||
// zapingest_test.go proves the CANONICAL in-luxd ZAP DEX ingestion seam
|
||||
// (zapingest.go): when the chain's init Config names a zapIngestAddr, the VM
|
||||
// ITSELF stands up a co-located ZAP socket bound to the one clobMethods core —
|
||||
// there is NO manual RegisterCLOB by the runner (cmd/dchain). A ZAP client dialing
|
||||
// ITSELF stands up a co-located ZAP socket bound to the one dexMethods core —
|
||||
// there is NO manual RegisterDEX by the runner (cmd/dexd plugin mode). A ZAP client dialing
|
||||
// that socket drives a real order through the IDENTICAL submitTx -> mempool ->
|
||||
// BuildBlock -> Verify -> Accept path the HTTP transport uses, and the fill is
|
||||
// committed D-Chain state. This is the deploy-shaped proof: the in-luxd plugin now
|
||||
@@ -34,7 +34,7 @@ import (
|
||||
// startZAPIngestVM boots a VM whose init Config enables the co-located ZAP socket on
|
||||
// an ephemeral port, plus the auto-sealer. It returns the VM, the socket's dial
|
||||
// address (chosen by the OS), and a stop func. Critically it does NOT call
|
||||
// RegisterCLOB — Initialize wires the socket from Config, exactly as the in-luxd
|
||||
// RegisterDEX — Initialize wires the socket from Config, exactly as the in-luxd
|
||||
// plugin does in production.
|
||||
func startZAPIngestVM(t *testing.T) (*VM, string, func()) {
|
||||
t.Helper()
|
||||
@@ -110,7 +110,7 @@ func TestZAPIngestCrossingOrdersMatchInBlock(t *testing.T) {
|
||||
// TestZAPIngestSharesOneCoreWithHTTP proves transport ⟂ consensus over the SAME book:
|
||||
// a maker rests over the canonical ZAP socket, and a taker crossing it through the
|
||||
// HTTP compat handler (CreateHandlers) fills against that SAME resting order. Neither
|
||||
// transport has its own matcher or its own book — both fold over clobMethods into the
|
||||
// transport has its own matcher or its own book — both fold over dexMethods into the
|
||||
// one mempool/consensus path, so an order from either side sees the other's state.
|
||||
func TestZAPIngestSharesOneCoreWithHTTP(t *testing.T) {
|
||||
// Boot ONE VM with the ZAP socket on, and ALSO mount its HTTP handlers — both
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ var (
|
||||
ErrUnknownMarket = errors.New("dex: market has no asset binding")
|
||||
|
||||
// ErrNoLiquidity is returned by the router when NO configured source can fill
|
||||
// any of the requested amount (neither the CLOB nor an AMM crosses). A swap
|
||||
// any of the requested amount (neither the OrderBook nor an AMM crosses). A swap
|
||||
// that fills nothing returns this so the caller reverts rather than locking the
|
||||
// taker's input against a zero fill.
|
||||
ErrNoLiquidity = errors.New("dex: no liquidity across any source for this swap")
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@ import (
|
||||
// sources and journal the joint C+D effect. The sequence is:
|
||||
//
|
||||
// 1. LOCK the taker's full input ONCE (available -> locked, and record the C-side
|
||||
// vault debit in the journal). A split across CLOB+AMM draws from this single
|
||||
// vault debit in the journal). A split across OrderBook+AMM draws from this single
|
||||
// lock, so the taker can never spend more than they locked.
|
||||
// 2. ROUTE: best-execution across the sources (router.Route), which mutates the
|
||||
// book/ledger/reserves in the Store and records the proceeds in the journal.
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
|
||||
// SwapResult is the outcome of one routed swap.
|
||||
type SwapResult struct {
|
||||
// Fills are the per-source fills (CLOB trades + AMM deltas) in execution order,
|
||||
// Fills are the per-source fills (OrderBook trades + AMM deltas) in execution order,
|
||||
// for events/receipts/the indexer.
|
||||
Fills []Fill
|
||||
// AmountIn / AmountOut are the input consumed and the output produced, in the
|
||||
@@ -53,7 +53,7 @@ type SwapResult struct {
|
||||
// consume is refunded. The router enforces the taker price limit and MinOut; on a
|
||||
// breach ExecuteSwap returns the error and the caller reverts (nothing committed).
|
||||
func ExecuteSwap(db Store, router *Router, req SwapRequest) (*SwapResult, error) {
|
||||
if req.Class != ClassPublicCLOB {
|
||||
if req.Class != ClassPublicDEX {
|
||||
// Only the public route is built in the first cut. An FHE-private swap is
|
||||
// refused here (not silently downgraded) until the private-pool source lands.
|
||||
return nil, ErrNoLiquidity
|
||||
|
||||
@@ -115,8 +115,8 @@ func totalLedger(t *testing.T, db Store, accts []AccountID, assets []AssetID) ma
|
||||
return out
|
||||
}
|
||||
|
||||
// clobRouter returns a router with only the CLOB source (the common case).
|
||||
func clobRouter() *Router { return NewRouter(NewCLOBSource()) }
|
||||
// orderBookRouter returns a router with only the OrderBook source (the common case).
|
||||
func orderBookRouter() *Router { return NewRouter(NewOrderBookSource()) }
|
||||
|
||||
// price builds a PriceInt from a human price (quote per base).
|
||||
func price(p float64) lx.PriceInt { return PriceToInt(p) }
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package dex is the SHARED, caller-agnostic deterministic core of the Lux
|
||||
// DEX: the CLOB custody ledger, the integer-exact settlement of matcher fills,
|
||||
// DEX: the OrderBook custody ledger, the integer-exact settlement of matcher fills,
|
||||
// the fail-closed maker-identity discipline, the smart-order-router across
|
||||
// on-chain liquidity sources, and the execution root. It is a pure function of
|
||||
// (ordered txs, prior state) over a generic key/value Store, so two homes —
|
||||
@@ -58,7 +58,7 @@ type AssetID = [32]byte
|
||||
// WITHOUT a lossy fold:
|
||||
//
|
||||
// - the cEVM home left-pads the 20-byte EVM address into 32 bytes (12 zero bytes
|
||||
// + the 20 address bytes), so two distinct addresses are ALWAYS distinct
|
||||
// - the 20 address bytes), so two distinct addresses are ALWAYS distinct
|
||||
// accounts — no address pair can collide and drain each other's resting-order
|
||||
// proceeds (the cross-user-drain guard, made injective for 20-byte addresses
|
||||
// that a 16-byte identity could alias);
|
||||
|
||||
+14
-14
@@ -43,9 +43,9 @@ func TestOwnership_MakerSettlesToFullIdentity(t *testing.T) {
|
||||
req := SwapRequest{
|
||||
PoolID: pid, TakerUser: taker, Side: lx.Sell,
|
||||
Base: tokLETH, Quote: tokLUSD, AmountIn: 40, OrderID: 1_000, TimestampN: 2_000,
|
||||
Class: ClassPublicCLOB,
|
||||
Class: ClassPublicDEX,
|
||||
}
|
||||
if _, err := ExecuteSwap(db, clobRouter(), req); err != nil {
|
||||
if _, err := ExecuteSwap(db, orderBookRouter(), req); err != nil {
|
||||
t.Fatalf("ExecuteSwap: %v", err)
|
||||
}
|
||||
|
||||
@@ -79,9 +79,9 @@ func TestOwnership_FailsClosedOnMissingIdentity(t *testing.T) {
|
||||
req := SwapRequest{
|
||||
PoolID: pid, TakerUser: taker, Side: lx.Sell,
|
||||
Base: tokLETH, Quote: tokLUSD, AmountIn: 40, OrderID: 1_000, TimestampN: 2_000,
|
||||
Class: ClassPublicCLOB,
|
||||
Class: ClassPublicDEX,
|
||||
}
|
||||
_, err := ExecuteSwap(db, clobRouter(), req)
|
||||
_, err := ExecuteSwap(db, orderBookRouter(), req)
|
||||
if !errors.Is(err, ErrMissingSettlementUser) {
|
||||
t.Fatalf("expected ErrMissingSettlementUser (fail-closed), got %v", err)
|
||||
}
|
||||
@@ -137,11 +137,11 @@ func TestOwnership_CancelRefundsToOwner(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMEVFloor_CLOBRefusesBelowFloor asserts the FIRST layer of MEV protection: a
|
||||
// SELL with a floor the resting CLOB cannot meet does not cross at all (the matcher
|
||||
// TestMEVFloor_OrderBookRefusesBelowFloor asserts the FIRST layer of MEV protection: a
|
||||
// SELL with a floor the resting OrderBook cannot meet does not cross at all (the matcher
|
||||
// won't fill below the limit), so the swap reverts with no fill — the taker keeps
|
||||
// their funds rather than selling below their floor.
|
||||
func TestMEVFloor_CLOBRefusesBelowFloor(t *testing.T) {
|
||||
func TestMEVFloor_OrderBookRefusesBelowFloor(t *testing.T) {
|
||||
db := newStore()
|
||||
pid := seedMarket(t, db, 24, tokLETH, tokLUSD)
|
||||
maker := account(0xAA)
|
||||
@@ -155,9 +155,9 @@ func TestMEVFloor_CLOBRefusesBelowFloor(t *testing.T) {
|
||||
PoolID: pid, TakerUser: taker, Side: lx.Sell,
|
||||
Base: tokLETH, Quote: tokLUSD, AmountIn: 40,
|
||||
LimitPrice: price(50), LimitIsUpper: false, // SELL floor at 50
|
||||
OrderID: 1_000, TimestampN: 2_000, Class: ClassPublicCLOB,
|
||||
OrderID: 1_000, TimestampN: 2_000, Class: ClassPublicDEX,
|
||||
}
|
||||
_, err := ExecuteSwap(db, clobRouter(), req)
|
||||
_, err := ExecuteSwap(db, orderBookRouter(), req)
|
||||
if !errors.Is(err, ErrNoLiquidity) {
|
||||
t.Fatalf("expected ErrNoLiquidity (matcher refuses to cross below floor), got %v", err)
|
||||
}
|
||||
@@ -184,16 +184,16 @@ func TestMEVFloor_AMMLegRejectedByProceedsFloor(t *testing.T) {
|
||||
taker := account(0xBB)
|
||||
seedDeposit(t, db, taker, tokLETH, 100)
|
||||
|
||||
// AMM priced ~40 LUSD/LETH (below the taker's 50 floor). No CLOB.
|
||||
// AMM priced ~40 LUSD/LETH (below the taker's 50 floor). No OrderBook.
|
||||
pool := newMemAMM(0)
|
||||
pool.set(pid, 100_000, 4_000_000) // ~40
|
||||
router := NewRouter(NewCLOBSource(), NewAMMSource(pool))
|
||||
router := NewRouter(NewOrderBookSource(), NewAMMSource(pool))
|
||||
|
||||
req := SwapRequest{
|
||||
PoolID: pid, TakerUser: taker, Side: lx.Sell,
|
||||
Base: tokLETH, Quote: tokLUSD, AmountIn: 40,
|
||||
LimitPrice: price(50), LimitIsUpper: false, // floor 50, AMM gives ~40
|
||||
OrderID: 1_000, TimestampN: 2_000, Class: ClassPublicCLOB,
|
||||
OrderID: 1_000, TimestampN: 2_000, Class: ClassPublicDEX,
|
||||
}
|
||||
_, err := ExecuteSwap(db, router, req)
|
||||
if !errors.Is(err, ErrPriceLimit) {
|
||||
@@ -216,9 +216,9 @@ func TestMEVFloor_AcceptsGoodProceeds(t *testing.T) {
|
||||
PoolID: pid, TakerUser: taker, Side: lx.Sell,
|
||||
Base: tokLETH, Quote: tokLUSD, AmountIn: 40,
|
||||
LimitPrice: price(50), LimitIsUpper: false,
|
||||
OrderID: 1_000, TimestampN: 2_000, Class: ClassPublicCLOB,
|
||||
OrderID: 1_000, TimestampN: 2_000, Class: ClassPublicDEX,
|
||||
}
|
||||
res, err := ExecuteSwap(db, clobRouter(), req)
|
||||
res, err := ExecuteSwap(db, orderBookRouter(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteSwap: %v", err)
|
||||
}
|
||||
|
||||
+10
-10
@@ -231,17 +231,17 @@ func (s AssetSide) valueKey() AssetID {
|
||||
// THE EXACT STEP ORDER (the value-path call chain, run BEFORE any C-balance debit and
|
||||
// BEFORE any DEX state write):
|
||||
//
|
||||
// Resolve(base) -> Resolve(quote) -> VerifyOnChain(base) -> VerifyOnChain(quote) -> EnsureMarket
|
||||
// Resolve(base) -> Resolve(quote) -> VerifyOnChain(base) -> VerifyOnChain(quote) -> EnsureMarket
|
||||
//
|
||||
// - Resolve (RequireResolvedAsset): prove each side's CANONICAL IDENTITY on the node's
|
||||
// bound network — PERMISSIONLESS. A well-formed real EVM-native/ERC-20 reference always
|
||||
// resolves; only a malformed/wrong-network/explicitly-disabled/synthetic reference is
|
||||
// refused (ErrAssetNotResolved). No resolver => ErrNoAssetResolver (fail-closed).
|
||||
// - VerifyOnChain (RequireRealOnChain): the AUTHORITATIVE reality check — is each side
|
||||
// backed by LIVE on-chain code in the state this block mutates? A fabricated/synthetic
|
||||
// ERC-20 (an ASCII-ticker address, a self-destructed or never-deployed contract) has no
|
||||
// code => ErrAssetNotOnChain; no verifier => ErrNoOnChainVerifier. Native (the
|
||||
// zero-address marker) is the chain's own coin and is always real.
|
||||
// - Resolve (RequireResolvedAsset): prove each side's CANONICAL IDENTITY on the node's
|
||||
// bound network — PERMISSIONLESS. A well-formed real EVM-native/ERC-20 reference always
|
||||
// resolves; only a malformed/wrong-network/explicitly-disabled/synthetic reference is
|
||||
// refused (ErrAssetNotResolved). No resolver => ErrNoAssetResolver (fail-closed).
|
||||
// - VerifyOnChain (RequireRealOnChain): the AUTHORITATIVE reality check — is each side
|
||||
// backed by LIVE on-chain code in the state this block mutates? A fabricated/synthetic
|
||||
// ERC-20 (an ASCII-ticker address, a self-destructed or never-deployed contract) has no
|
||||
// code => ErrAssetNotOnChain; no verifier => ErrNoOnChainVerifier. Native (the
|
||||
// zero-address marker) is the chain's own coin and is always real.
|
||||
//
|
||||
// Both steps run for BOTH sides before ANY write, so a refused open leaves the store
|
||||
// untouched (fail-closed): the market existence row is created only after all four checks
|
||||
|
||||
@@ -63,9 +63,9 @@ func TestReplay_SameInputSameStateAllValidators(t *testing.T) {
|
||||
req := SwapRequest{
|
||||
PoolID: pid, TakerUser: account(0xBB), Side: lx.Sell,
|
||||
Base: tokLETH, Quote: tokLUSD, AmountIn: 137, OrderID: 5_000, TimestampN: 9_000,
|
||||
Class: ClassPublicCLOB,
|
||||
Class: ClassPublicDEX,
|
||||
}
|
||||
res, err := ExecuteSwap(db, clobRouter(), req)
|
||||
res, err := ExecuteSwap(db, orderBookRouter(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("validator %d ExecuteSwap: %v", v, err)
|
||||
}
|
||||
@@ -94,13 +94,13 @@ func TestReplay_ExecutionRootDeterministic(t *testing.T) {
|
||||
req := SwapRequest{
|
||||
PoolID: pid, TakerUser: account(0xBB), Side: lx.Sell,
|
||||
Base: tokLETH, Quote: tokLUSD, AmountIn: 200, OrderID: 5_000, TimestampN: 9_000,
|
||||
Class: ClassPublicCLOB,
|
||||
Class: ClassPublicDEX,
|
||||
}
|
||||
r1, err := ExecuteSwap(db1, clobRouter(), req)
|
||||
r1, err := ExecuteSwap(db1, orderBookRouter(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("db1: %v", err)
|
||||
}
|
||||
r2, err := ExecuteSwap(db2, clobRouter(), req)
|
||||
r2, err := ExecuteSwap(db2, orderBookRouter(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("db2: %v", err)
|
||||
}
|
||||
|
||||
+3
-3
@@ -34,7 +34,7 @@ const Size = hash.KeccakSize
|
||||
// db for market poolID, plus the swap's realized fills. It folds:
|
||||
//
|
||||
// - the canonical resting rows of poolID (rebuilt from order:* rows), and
|
||||
// - the swap's CLOB fills (the trade rows),
|
||||
// - the swap's OrderBook fills (the trade rows),
|
||||
//
|
||||
// into two RFC-6962 tagged Merkle sub-roots and composes them with a fixed-shape
|
||||
// keccak256. Deterministic over deterministic state, so two validators replaying
|
||||
@@ -65,7 +65,7 @@ func bookRootForMarket(db Store, poolID [32]byte) [Size]byte {
|
||||
return merkle.Root(leaves)
|
||||
}
|
||||
|
||||
// tradeRootForResult folds a swap's realized CLOB fills into the RFC-6962 trade
|
||||
// tradeRootForResult folds a swap's realized OrderBook fills into the RFC-6962 trade
|
||||
// root. Trades are folded in execution order (the router's source/fill order), each
|
||||
// bound to its index. AMM fills carry no maker rows; their (base,quote) deltas are
|
||||
// committed via the reserve write + the journal and are included here as a synthetic
|
||||
@@ -104,7 +104,7 @@ func tradeLeafDigest(t lx.DEXTrade, i uint32) [Size]byte {
|
||||
|
||||
// ammLeafDigest = keccak256("amm" ‖ base_le ‖ quote_le ‖ index_le) — binds an AMM
|
||||
// leg's (base,quote) move into the trade root so a tampered AMM amount changes the
|
||||
// root just as a tampered CLOB fill does.
|
||||
// root just as a tampered OrderBook fill does.
|
||||
func ammLeafDigest(base, quote uint64, i uint32) [Size]byte {
|
||||
var b [8]byte
|
||||
var q [8]byte
|
||||
|
||||
+20
-20
@@ -12,10 +12,10 @@ import (
|
||||
// router.go is THE on-chain smart-order-router — the real product. It routes one
|
||||
// swap across the on-chain liquidity SOURCES, in-process and deterministically:
|
||||
//
|
||||
// 1. the V4 native CLOB (the in-process matcher) FIRST;
|
||||
// 2. the V2/V3 AMM pools (EVM contracts in the SAME cEVM) when the CLOB has
|
||||
// 1. the V4 native OrderBook (the in-process matcher) FIRST;
|
||||
// 2. the V2/V3 AMM pools (EVM contracts in the SAME cEVM) when the OrderBook has
|
||||
// insufficient depth — read their state and execute the swap in-process;
|
||||
// 3. best-execution: take the better price across the CLOB and the AMM, or split
|
||||
// 3. best-execution: take the better price across the OrderBook and the AMM, or split
|
||||
// the input across them when splitting beats either alone.
|
||||
//
|
||||
// Every source is a pure, replayable function of (prior state, input): no live
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
// quote state. The router never knows or cares which concrete source it routes to.
|
||||
|
||||
// RouteClass tags the privacy/visibility regime a swap routes under. Only
|
||||
// ClassPublicCLOB is built in the first cut. ClassFHEPrivate is the staged-next
|
||||
// ClassPublicDEX is built in the first cut. ClassFHEPrivate is the staged-next
|
||||
// private-pool route (owner spec §13): a swap whose amounts are FHE-encrypted and
|
||||
// matched in a private pool. The router carries the class so the source selection
|
||||
// can later restrict an FHE swap to FHE-capable sources WITHOUT reshaping the
|
||||
@@ -39,9 +39,9 @@ import (
|
||||
type RouteClass uint8
|
||||
|
||||
const (
|
||||
// ClassPublicCLOB is the public route: plaintext amounts, the public CLOB +
|
||||
// ClassPublicDEX is the public route: plaintext amounts, the public OrderBook +
|
||||
// public AMM pools. The only class built now.
|
||||
ClassPublicCLOB RouteClass = iota
|
||||
ClassPublicDEX RouteClass = iota
|
||||
// ClassFHEPrivate is the private route (STAGED — not built): FHE-encrypted
|
||||
// amounts, matched in a private pool. Reserved so the router can route it to an
|
||||
// FHE source later without an interface change.
|
||||
@@ -64,7 +64,7 @@ type SwapRequest struct {
|
||||
OrderID uint64 // = blockDeterministicID(height, callIndex); never zero
|
||||
TimestampN int64 // block timestamp (UnixNano), for the ephemeral order
|
||||
|
||||
// LimitPrice is the taker's OWN worst-acceptable CLOB price (quote-per-base, in
|
||||
// LimitPrice is the taker's OWN worst-acceptable OrderBook price (quote-per-base, in
|
||||
// PriceInt grid units), derived from the V4 SqrtPriceLimitX96. 0 = no limit.
|
||||
// LimitIsUpper says which side it bounds (true = a BUY ceiling, false = a SELL
|
||||
// floor). Enforced on the realized proceeds price — the taker-authenticated MEV
|
||||
@@ -76,22 +76,22 @@ type SwapRequest struct {
|
||||
// across all sources is below it. 0 = no floor.
|
||||
MinOut uint64
|
||||
|
||||
// Class is the route class (public CLOB now; FHE-private staged). The first cut
|
||||
// only routes ClassPublicCLOB.
|
||||
// Class is the route class (public OrderBook now; FHE-private staged). The first cut
|
||||
// only routes ClassPublicDEX.
|
||||
Class RouteClass
|
||||
}
|
||||
|
||||
// Fill is one matched quantity from one source, in the matcher's authoritative
|
||||
// integer lane. The router aggregates Fills across sources into the joint journal.
|
||||
type Fill struct {
|
||||
// Trades are the CLOB fills (for a CLOB source) — they carry maker order ids so
|
||||
// Trades are the OrderBook fills (for a OrderBook source) — they carry maker order ids so
|
||||
// settlement resolves each maker's full identity via orderuser:. Empty for an
|
||||
// AMM source (an AMM fill has no resting maker; see AMMBaseDelta/AMMQuoteDelta).
|
||||
Trades []lx.Trade
|
||||
|
||||
// AMMBaseDelta / AMMQuoteDelta are the base/quote units an AMM source moved (an
|
||||
// AMM has no maker rows). The router credits/debits these against the pool's
|
||||
// reserves and the taker directly. Zero for a CLOB source.
|
||||
// reserves and the taker directly. Zero for a OrderBook source.
|
||||
AMMBaseDelta uint64
|
||||
AMMQuoteDelta uint64
|
||||
|
||||
@@ -103,8 +103,8 @@ type Fill struct {
|
||||
type SourceKind uint8
|
||||
|
||||
const (
|
||||
// SourceCLOB is the V4 native CLOB (the in-process matcher). The primary source.
|
||||
SourceCLOB SourceKind = iota
|
||||
// SourceOrderBook is the V4 native OrderBook (the in-process matcher). The primary source.
|
||||
SourceOrderBook SourceKind = iota
|
||||
// SourceAMM is an in-cEVM V2/V3 AMM pool, read + executed in-process.
|
||||
SourceAMM
|
||||
// SourceRFQ is the STAGED external-CEX/market-maker source (on-chain quotes
|
||||
@@ -128,7 +128,7 @@ type Quote struct {
|
||||
|
||||
// Source is one on-chain liquidity source the router routes across. It is a pure,
|
||||
// replayable view+executor over the Store. Both methods read prior state only; a
|
||||
// source NEVER issues a live external query (that forks consensus). The CLOB
|
||||
// source NEVER issues a live external query (that forks consensus). The OrderBook
|
||||
// source and the AMM source implement it; an RFQ source can implement it LATER
|
||||
// over on-chain-committed quote state without the router changing.
|
||||
type Source interface {
|
||||
@@ -141,7 +141,7 @@ type Source interface {
|
||||
QuoteSwap(db Store, req SwapRequest, remainingIn uint64) (Quote, error)
|
||||
|
||||
// ExecuteSwap fills up to remainingIn against this source, MUTATING the Store
|
||||
// (the matcher's book/ledger rows for the CLOB; the pool reserves for the AMM)
|
||||
// (the matcher's book/ledger rows for the OrderBook; the pool reserves for the AMM)
|
||||
// and appending the value moves to the journal. Returns the Fill and the input
|
||||
// units actually consumed. It MUST be consistent with QuoteSwap over the same
|
||||
// state (the router relies on quote==execute for the split it chose).
|
||||
@@ -150,14 +150,14 @@ type Source interface {
|
||||
|
||||
// Router routes a swap across an ordered set of Sources, best-execution first.
|
||||
// The order is the SOURCE PRIORITY when prices tie or a source is exhausted:
|
||||
// CLOB first (the native venue), then AMM. The router itself holds no state — it
|
||||
// OrderBook first (the native venue), then AMM. The router itself holds no state — it
|
||||
// is a pure function of the sources + the Store.
|
||||
type Router struct {
|
||||
sources []Source
|
||||
}
|
||||
|
||||
// NewRouter builds a router over the given sources in priority order. The first
|
||||
// cut wires [CLOB, AMM]; an RFQ source appends LATER with no router change.
|
||||
// cut wires [OrderBook, AMM]; an RFQ source appends LATER with no router change.
|
||||
func NewRouter(sources ...Source) *Router {
|
||||
return &Router{sources: sources}
|
||||
}
|
||||
@@ -169,9 +169,9 @@ func NewRouter(sources ...Source) *Router {
|
||||
// 1. While input remains and progress is possible:
|
||||
// a. QuoteSwap every source for the remaining input.
|
||||
// b. Pick the source with the best marginal price (most output per input
|
||||
// consumed), ties broken by source priority (CLOB before AMM).
|
||||
// consumed), ties broken by source priority (OrderBook before AMM).
|
||||
// c. ExecuteSwap that source for the remaining input; subtract the consumed
|
||||
// input, add the produced output.
|
||||
// input, add the produced output.
|
||||
// 2. Stop when input is exhausted, no source can fill more, or this iteration
|
||||
// made no progress.
|
||||
//
|
||||
@@ -251,7 +251,7 @@ func (r *Router) Route(db Store, req SwapRequest, j *Journal) (totalOut, totalIn
|
||||
// quote b for the taker, given the side. The effective price is output-per-input;
|
||||
// more output per unit input is always better for the taker. Compared with cross-
|
||||
// multiplication (no float): a is better iff a.out * b.in > b.out * a.in. Equal
|
||||
// prices keep the incumbent (b) so source priority (the scan order, CLOB first)
|
||||
// prices keep the incumbent (b) so source priority (the scan order, OrderBook first)
|
||||
// breaks the tie.
|
||||
func betterMarginalPrice(a, b Quote, _ lx.Side) bool {
|
||||
// a.out/a.in > b.out/b.in <=> a.out*b.in > b.out*a.in
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// source_amm.go is the V2/V3 AMM liquidity source — the router's FALLTHROUGH when
|
||||
// the CLOB has insufficient depth. The AMM pools are EVM contracts in the SAME
|
||||
// the OrderBook has insufficient depth. The AMM pools are EVM contracts in the SAME
|
||||
// cEVM, so their state is read in-process and the swap executes in-process
|
||||
// (deterministic constant-product math; every validator replays identically).
|
||||
//
|
||||
@@ -110,7 +110,7 @@ func (s *AMMSource) ExecuteSwap(db Store, req SwapRequest, remainingIn uint64, j
|
||||
// top-level lock holds the taker's full input in the 0x9999 vault; the AMM leg's
|
||||
// input is consumed from that vault-held amount and NOT refunded. We spend the
|
||||
// taker's LOCKED input ledger units so the conservation invariant (lock == fills
|
||||
// spent + refund) holds across BOTH CLOB and AMM legs uniformly.
|
||||
// spent + refund) holds across BOTH OrderBook and AMM legs uniformly.
|
||||
inAsset := req.Quote
|
||||
outAsset := req.Base
|
||||
if req.Side == lx.Sell {
|
||||
@@ -120,7 +120,7 @@ func (s *AMMSource) ExecuteSwap(db Store, req SwapRequest, remainingIn uint64, j
|
||||
if serr := SpendLocked(db, req.TakerUser, inAsset, inUsed); serr != nil {
|
||||
return Fill{}, 0, 0, serr
|
||||
}
|
||||
// Credit the taker's dex AVAILABLE with the proceeds — SYMMETRIC with the CLOB
|
||||
// Credit the taker's dex AVAILABLE with the proceeds — SYMMETRIC with the OrderBook
|
||||
// source (whose SettleFills credits the taker's available). This makes the D-surface
|
||||
// settlement consistent across BOTH sources: after any swap the taker's available
|
||||
// holds exactly the proceeds the journal records, so a caller that drains the
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
)
|
||||
|
||||
// source_clob.go is the V4 native CLOB liquidity source — the in-process matcher.
|
||||
// source_orderBook.go is the V4 native OrderBook liquidity source — the in-process matcher.
|
||||
// It is the router's PRIMARY source: a marketable order crosses the resting book,
|
||||
// each fill settles value-exactly inside the custody ledger (taker locked spend ->
|
||||
// maker available; maker locked -> taker available), and the proceeds are recorded
|
||||
@@ -22,27 +22,27 @@ import (
|
||||
// against a deep-copied book so its predicted output equals what Execute produces —
|
||||
// the router relies on quote==execute for the split it picks.
|
||||
|
||||
// CLOBSource matches against the in-trie resting book. It holds no state; the book
|
||||
// OrderBookSource matches against the in-trie resting book. It holds no state; the book
|
||||
// is rebuilt from the Store on each call (the rebuildable-accelerator discipline).
|
||||
type CLOBSource struct{}
|
||||
type OrderBookSource struct{}
|
||||
|
||||
// NewCLOBSource returns the CLOB liquidity source.
|
||||
func NewCLOBSource() *CLOBSource { return &CLOBSource{} }
|
||||
// NewOrderBookSource returns the OrderBook liquidity source.
|
||||
func NewOrderBookSource() *OrderBookSource { return &OrderBookSource{} }
|
||||
|
||||
// Kind identifies this as the CLOB source.
|
||||
func (*CLOBSource) Kind() SourceKind { return SourceCLOB }
|
||||
// Kind identifies this as the OrderBook source.
|
||||
func (*OrderBookSource) Kind() SourceKind { return SourceOrderBook }
|
||||
|
||||
// QuoteSwap rebuilds the market book, runs the matcher against a CLONE for up to
|
||||
// remainingIn of the spend asset, and reports the output WITHOUT mutating state.
|
||||
// Ok=false when the market is unknown, has no crossable depth, or the order yields
|
||||
// no fills.
|
||||
func (*CLOBSource) QuoteSwap(db Store, req SwapRequest, remainingIn uint64) (Quote, error) {
|
||||
func (*OrderBookSource) QuoteSwap(db Store, req SwapRequest, remainingIn uint64) (Quote, error) {
|
||||
symbol, exists, err := ReadMarketSymbol(db, req.PoolID)
|
||||
if err != nil {
|
||||
return Quote{}, err
|
||||
}
|
||||
if !exists {
|
||||
return Quote{}, nil // no such market on the CLOB: not an error, just no fill
|
||||
return Quote{}, nil // no such market on the OrderBook: not an error, just no fill
|
||||
}
|
||||
book, err := RebuildBook(db, req.PoolID, symbol)
|
||||
if err != nil {
|
||||
@@ -76,7 +76,7 @@ func (*CLOBSource) QuoteSwap(db Store, req SwapRequest, remainingIn uint64) (Quo
|
||||
// the FULL swap input once (so a split across sources draws from one lock); this
|
||||
// source only spends the taker's already-locked balance per fill and refunds
|
||||
// nothing (the top-level refunds the unspent remainder after all sources run).
|
||||
func (*CLOBSource) ExecuteSwap(db Store, req SwapRequest, remainingIn uint64, j *Journal) (Fill, uint64, uint64, error) {
|
||||
func (*OrderBookSource) ExecuteSwap(db Store, req SwapRequest, remainingIn uint64, j *Journal) (Fill, uint64, uint64, error) {
|
||||
symbol, exists, err := ReadMarketSymbol(db, req.PoolID)
|
||||
if err != nil {
|
||||
return Fill{}, 0, 0, err
|
||||
@@ -137,7 +137,7 @@ func (*CLOBSource) ExecuteSwap(db Store, req SwapRequest, remainingIn uint64, j
|
||||
j.CreditTakerOutput(outAsset, outGot)
|
||||
_ = spent // spent == inUsed for the integer lane; kept for clarity
|
||||
|
||||
return Fill{Trades: fills, Source: SourceCLOB}, inUsed, outGot, nil
|
||||
return Fill{Trades: fills, Source: SourceOrderBook}, inUsed, outGot, nil
|
||||
}
|
||||
|
||||
// buildMarketableOrder constructs the ephemeral IOC order for a swap. The matcher
|
||||
+43
-43
@@ -40,9 +40,9 @@ func TestSwap_SellIntoRestingBid(t *testing.T) {
|
||||
PoolID: pid, TakerUser: taker, Side: lx.Sell,
|
||||
Base: tokLETH, Quote: tokLUSD,
|
||||
AmountIn: 80, OrderID: 1_000, TimestampN: 2_000,
|
||||
Class: ClassPublicCLOB,
|
||||
Class: ClassPublicDEX,
|
||||
}
|
||||
res, err := ExecuteSwap(db, clobRouter(), req)
|
||||
res, err := ExecuteSwap(db, orderBookRouter(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteSwap: %v", err)
|
||||
}
|
||||
@@ -54,9 +54,9 @@ func TestSwap_SellIntoRestingBid(t *testing.T) {
|
||||
if res.AmountOut != 4000 {
|
||||
t.Fatalf("AmountOut = %d, want 4000 (80 LETH @ 50)", res.AmountOut)
|
||||
}
|
||||
// The fill came from the CLOB.
|
||||
if len(res.Fills) != 1 || res.Fills[0].Source != SourceCLOB {
|
||||
t.Fatalf("expected one CLOB fill, got %+v", res.Fills)
|
||||
// The fill came from the OrderBook.
|
||||
if len(res.Fills) != 1 || res.Fills[0].Source != SourceOrderBook {
|
||||
t.Fatalf("expected one OrderBook fill, got %+v", res.Fills)
|
||||
}
|
||||
|
||||
// Taker ledger: 100-80 = 20 LETH available, 4000 LUSD available.
|
||||
@@ -94,14 +94,14 @@ func TestSwap_SellIntoRestingBid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test9999Swap_FillsFromV4WhenCLOBLiquid asserts the router fills entirely from the
|
||||
// V4 CLOB when the CLOB has the depth — the AMM is present but untouched.
|
||||
func Test9999Swap_FillsFromV4WhenCLOBLiquid(t *testing.T) {
|
||||
// Test9999Swap_FillsFromV4WhenOrderBookLiquid asserts the router fills entirely from the
|
||||
// V4 OrderBook when the OrderBook has the depth — the AMM is present but untouched.
|
||||
func Test9999Swap_FillsFromV4WhenOrderBookLiquid(t *testing.T) {
|
||||
db := newStore()
|
||||
pid := seedMarket(t, db, 2, tokLETH, tokLUSD)
|
||||
maker := account(0xAA)
|
||||
taker := account(0xBB)
|
||||
// Deep CLOB bid: 1000 LETH @ 50.
|
||||
// Deep OrderBook bid: 1000 LETH @ 50.
|
||||
seedDeposit(t, db, maker, tokLUSD, 1_000_000)
|
||||
restOrder(t, db, pid, maker, lx.Buy, 50, 1000, 1)
|
||||
seedDeposit(t, db, taker, tokLETH, 100)
|
||||
@@ -109,24 +109,24 @@ func Test9999Swap_FillsFromV4WhenCLOBLiquid(t *testing.T) {
|
||||
// AMM with shallow reserves (would price worse).
|
||||
pool := newMemAMM(0) // 0 bps fee
|
||||
pool.set(pid, 10_000, 500_000)
|
||||
router := NewRouter(NewCLOBSource(), NewAMMSource(pool))
|
||||
router := NewRouter(NewOrderBookSource(), NewAMMSource(pool))
|
||||
|
||||
req := SwapRequest{
|
||||
PoolID: pid, TakerUser: taker, Side: lx.Sell,
|
||||
Base: tokLETH, Quote: tokLUSD, AmountIn: 50, OrderID: 9, TimestampN: 1,
|
||||
Class: ClassPublicCLOB,
|
||||
Class: ClassPublicDEX,
|
||||
}
|
||||
res, err := ExecuteSwap(db, router, req)
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteSwap: %v", err)
|
||||
}
|
||||
// CLOB @ 50 gives 2500 LUSD for 50 LETH; the AMM would give less. All from CLOB.
|
||||
// OrderBook @ 50 gives 2500 LUSD for 50 LETH; the AMM would give less. All from OrderBook.
|
||||
if res.AmountOut != 2500 {
|
||||
t.Fatalf("AmountOut = %d, want 2500 (all from CLOB @ 50)", res.AmountOut)
|
||||
t.Fatalf("AmountOut = %d, want 2500 (all from OrderBook @ 50)", res.AmountOut)
|
||||
}
|
||||
for _, f := range res.Fills {
|
||||
if f.Source == SourceAMM {
|
||||
t.Fatalf("AMM was used despite a better CLOB price: %+v", res.Fills)
|
||||
t.Fatalf("AMM was used despite a better OrderBook price: %+v", res.Fills)
|
||||
}
|
||||
}
|
||||
// AMM reserves untouched.
|
||||
@@ -137,22 +137,22 @@ func Test9999Swap_FillsFromV4WhenCLOBLiquid(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test9999Swap_FallsToV2V3WhenNoV4 asserts the router falls through to the AMM when
|
||||
// the CLOB has NO crossable depth.
|
||||
// the OrderBook has NO crossable depth.
|
||||
func Test9999Swap_FallsToV2V3WhenNoV4(t *testing.T) {
|
||||
db := newStore()
|
||||
pid := seedMarket(t, db, 3, tokLETH, tokLUSD)
|
||||
taker := account(0xBB)
|
||||
seedDeposit(t, db, taker, tokLETH, 1000)
|
||||
|
||||
// No CLOB liquidity at all; AMM has reserves.
|
||||
// No OrderBook liquidity at all; AMM has reserves.
|
||||
pool := newMemAMM(0)
|
||||
pool.set(pid, 100_000, 5_000_000) // price ~50 LUSD/LETH
|
||||
router := NewRouter(NewCLOBSource(), NewAMMSource(pool))
|
||||
router := NewRouter(NewOrderBookSource(), NewAMMSource(pool))
|
||||
|
||||
req := SwapRequest{
|
||||
PoolID: pid, TakerUser: taker, Side: lx.Sell,
|
||||
Base: tokLETH, Quote: tokLUSD, AmountIn: 100, OrderID: 9, TimestampN: 1,
|
||||
Class: ClassPublicCLOB,
|
||||
Class: ClassPublicDEX,
|
||||
}
|
||||
res, err := ExecuteSwap(db, router, req)
|
||||
if err != nil {
|
||||
@@ -173,15 +173,15 @@ func Test9999Swap_FallsToV2V3WhenNoV4(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test9999Swap_BestExecAcrossV4AndAMM asserts the router SPLITS across the CLOB and
|
||||
// the AMM when neither alone is best for the whole amount: it takes the CLOB depth
|
||||
// Test9999Swap_BestExecAcrossV4AndAMM asserts the router SPLITS across the OrderBook and
|
||||
// the AMM when neither alone is best for the whole amount: it takes the OrderBook depth
|
||||
// at the better price, then the AMM for the remainder.
|
||||
func Test9999Swap_BestExecAcrossV4AndAMM(t *testing.T) {
|
||||
db := newStore()
|
||||
pid := seedMarket(t, db, 4, tokLETH, tokLUSD)
|
||||
maker := account(0xAA)
|
||||
taker := account(0xBB)
|
||||
// Thin but BETTER CLOB bid: 10 LETH @ 60 (better than AMM's ~50).
|
||||
// Thin but BETTER OrderBook bid: 10 LETH @ 60 (better than AMM's ~50).
|
||||
seedDeposit(t, db, maker, tokLUSD, 1_000_000)
|
||||
restOrder(t, db, pid, maker, lx.Buy, 60, 10, 1)
|
||||
seedDeposit(t, db, taker, tokLETH, 1000)
|
||||
@@ -189,38 +189,38 @@ func Test9999Swap_BestExecAcrossV4AndAMM(t *testing.T) {
|
||||
// AMM priced ~50.
|
||||
pool := newMemAMM(0)
|
||||
pool.set(pid, 100_000, 5_000_000)
|
||||
router := NewRouter(NewCLOBSource(), NewAMMSource(pool))
|
||||
router := NewRouter(NewOrderBookSource(), NewAMMSource(pool))
|
||||
|
||||
// Taker sells 30 LETH. CLOB absorbs 10 @ 60 = 600; AMM absorbs the other 20.
|
||||
// Taker sells 30 LETH. OrderBook absorbs 10 @ 60 = 600; AMM absorbs the other 20.
|
||||
req := SwapRequest{
|
||||
PoolID: pid, TakerUser: taker, Side: lx.Sell,
|
||||
Base: tokLETH, Quote: tokLUSD, AmountIn: 30, OrderID: 9, TimestampN: 1,
|
||||
Class: ClassPublicCLOB,
|
||||
Class: ClassPublicDEX,
|
||||
}
|
||||
res, err := ExecuteSwap(db, router, req)
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteSwap: %v", err)
|
||||
}
|
||||
// Expect a CLOB fill of 10 LETH -> 600 LUSD and an AMM fill of 20 LETH.
|
||||
var sawCLOB, sawAMM bool
|
||||
var clobIn, ammIn uint64
|
||||
// Expect a OrderBook fill of 10 LETH -> 600 LUSD and an AMM fill of 20 LETH.
|
||||
var sawOrderBook, sawAMM bool
|
||||
var orderBookIn, ammIn uint64
|
||||
for _, f := range res.Fills {
|
||||
switch f.Source {
|
||||
case SourceCLOB:
|
||||
sawCLOB = true
|
||||
case SourceOrderBook:
|
||||
sawOrderBook = true
|
||||
for _, tr := range f.Trades {
|
||||
clobIn += tr.BaseUnits.Uint64()
|
||||
orderBookIn += tr.BaseUnits.Uint64()
|
||||
}
|
||||
case SourceAMM:
|
||||
sawAMM = true
|
||||
ammIn += f.AMMBaseDelta
|
||||
}
|
||||
}
|
||||
if !sawCLOB || !sawAMM {
|
||||
t.Fatalf("expected a split across CLOB+AMM, got %+v", res.Fills)
|
||||
if !sawOrderBook || !sawAMM {
|
||||
t.Fatalf("expected a split across OrderBook+AMM, got %+v", res.Fills)
|
||||
}
|
||||
if clobIn != 10 {
|
||||
t.Fatalf("CLOB leg base = %d, want 10", clobIn)
|
||||
if orderBookIn != 10 {
|
||||
t.Fatalf("OrderBook leg base = %d, want 10", orderBookIn)
|
||||
}
|
||||
if ammIn != 20 {
|
||||
t.Fatalf("AMM leg base = %d, want 20", ammIn)
|
||||
@@ -228,10 +228,10 @@ func Test9999Swap_BestExecAcrossV4AndAMM(t *testing.T) {
|
||||
if res.AmountIn != 30 {
|
||||
t.Fatalf("total AmountIn = %d, want 30", res.AmountIn)
|
||||
}
|
||||
// Output = 600 (CLOB) + AMM curve(20).
|
||||
// Output = 600 (OrderBook) + AMM curve(20).
|
||||
ammOut := lx.ConstantProductOut(100_000, 5_000_000, 20)
|
||||
if res.AmountOut != 600+ammOut {
|
||||
t.Fatalf("AmountOut = %d, want %d (600 CLOB + %d AMM)", res.AmountOut, 600+ammOut, ammOut)
|
||||
t.Fatalf("AmountOut = %d, want %d (600 OrderBook + %d AMM)", res.AmountOut, 600+ammOut, ammOut)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,13 +244,13 @@ func Test9999Swap_NoLiquidityReverts(t *testing.T) {
|
||||
pid := seedMarket(t, db, 5, tokLETH, tokLUSD)
|
||||
taker := account(0xBB)
|
||||
seedDeposit(t, db, taker, tokLETH, 100)
|
||||
// No CLOB, no AMM.
|
||||
// No OrderBook, no AMM.
|
||||
req := SwapRequest{
|
||||
PoolID: pid, TakerUser: taker, Side: lx.Sell,
|
||||
Base: tokLETH, Quote: tokLUSD, AmountIn: 50, OrderID: 9, TimestampN: 1,
|
||||
Class: ClassPublicCLOB,
|
||||
Class: ClassPublicDEX,
|
||||
}
|
||||
_, err := ExecuteSwap(db, clobRouter(), req)
|
||||
_, err := ExecuteSwap(db, orderBookRouter(), req)
|
||||
if !errors.Is(err, ErrNoLiquidity) {
|
||||
t.Fatalf("expected ErrNoLiquidity, got %v", err)
|
||||
}
|
||||
@@ -266,9 +266,9 @@ func Test9999Swap_BuyRequiresLimit(t *testing.T) {
|
||||
req := SwapRequest{
|
||||
PoolID: pid, TakerUser: taker, Side: lx.Buy,
|
||||
Base: tokLETH, Quote: tokLUSD, AmountIn: 5000, OrderID: 9, TimestampN: 1,
|
||||
Class: ClassPublicCLOB, // LimitPrice 0 -> refused
|
||||
Class: ClassPublicDEX, // LimitPrice 0 -> refused
|
||||
}
|
||||
_, err := ExecuteSwap(db, clobRouter(), req)
|
||||
_, err := ExecuteSwap(db, orderBookRouter(), req)
|
||||
if !errors.Is(err, ErrBuyRequiresLimit) {
|
||||
t.Fatalf("expected ErrBuyRequiresLimit, got %v", err)
|
||||
}
|
||||
@@ -296,9 +296,9 @@ func TestSwap_BuyWithLimit(t *testing.T) {
|
||||
PoolID: pid, TakerUser: taker, Side: lx.Buy,
|
||||
Base: tokLETH, Quote: tokLUSD, AmountIn: 5000,
|
||||
LimitPrice: price(50), LimitIsUpper: true,
|
||||
OrderID: 9, TimestampN: 1, Class: ClassPublicCLOB,
|
||||
OrderID: 9, TimestampN: 1, Class: ClassPublicDEX,
|
||||
}
|
||||
res, err := ExecuteSwap(db, clobRouter(), req)
|
||||
res, err := ExecuteSwap(db, orderBookRouter(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteSwap: %v", err)
|
||||
}
|
||||
|
||||
+1
-1
@@ -373,7 +373,7 @@ func (s *session) handleTestRequest(msg *Message) {
|
||||
// emits ExecutionReport(s): an ack (ExecType=New) if nothing crossed and the
|
||||
// order is fully open, then one report per fill (PartialFill/Fill), or a single
|
||||
// Rejected report on a venue error. The order is submitted as a MARKETABLE order
|
||||
// bounded by its limit price (IOC) — the faithful CLOB taker primitive that
|
||||
// bounded by its limit price (IOC) — the faithful DEX taker primitive that
|
||||
// crosses the resting book and returns fills, identical to the 0x9010 Swap path.
|
||||
func (s *session) handleNewOrderSingle(ctx context.Context, msg *Message) {
|
||||
clOrdID, _ := msg.Get(TagClOrdID)
|
||||
|
||||
+4
-4
@@ -5,12 +5,12 @@
|
||||
// the SERVER side of the FIX trading path, the protocol front-end the SDK's
|
||||
// EnableFIX() client (sdk/go/client.go) talks to.
|
||||
//
|
||||
// It is ONE of four protocol front-ends onto the SAME matcher: the D-Chain CLOB
|
||||
// (pkg/dchain) served over the FROZEN clob_* ZAP surface (pkg/zapwire). ZAP
|
||||
// clients speak clob_* directly; the 0x9010 V4 precompile speaks it via
|
||||
// It is ONE of four protocol front-ends onto the SAME matcher: the D-Chain DEX
|
||||
// (pkg/dchain) served over the FROZEN dex_* ZAP surface (pkg/zapwire). ZAP
|
||||
// clients speak dex_* directly; the 0x9010 V4 precompile speaks it via
|
||||
// engine_zap; the WS server speaks it via a JSON translation; and FIX speaks it
|
||||
// via this codec + acceptor. NONE of them owns a matcher — the acceptor decodes a
|
||||
// FIX NewOrderSingle, calls the SAME clob_place / clob_submit the others call, and
|
||||
// FIX NewOrderSingle, calls the SAME dex_place / dex_submit the others call, and
|
||||
// renders the consensus-computed result back as a FIX ExecutionReport. DRY,
|
||||
// orthogonal: the matcher lives in ONE place and FIX is purely a wire translation.
|
||||
//
|
||||
|
||||
@@ -30,7 +30,7 @@ const (
|
||||
SwapRouterAddress = "0x0000000000000000000000000000000000009012" // LXRouter — swap router
|
||||
HooksRegistryAddr = "0x0000000000000000000000000000000000009013" // LXHooks — pool hooks registry
|
||||
FlashLoanAddress = "0x0000000000000000000000000000000000009014" // LXFlash — flash loans
|
||||
CLOBAddress = "0x0000000000000000000000000000000000009020" // LXBook — central limit order book
|
||||
OrderBookAddress = "0x0000000000000000000000000000000000009020" // LXBook — central limit order book
|
||||
VaultAddress = "0x0000000000000000000000000000000000009030" // LXVault — clearinghouse, margin, positions
|
||||
FeedAddress = "0x0000000000000000000000000000000000009040" // LXFeed — computed price feeds
|
||||
LendAddress = "0x0000000000000000000000000000000000009050" // LXLend — lending pool
|
||||
|
||||
@@ -17,7 +17,7 @@ type orderManager struct {
|
||||
store *OrderStore
|
||||
|
||||
// Precompile addresses for building unsigned transactions
|
||||
clobAddress string // LXBook precompile for limit orders
|
||||
dexAddress string // LXBook precompile for limit orders
|
||||
|
||||
// Dutch auction reactor address for EIP-712 domain
|
||||
reactorAddress string
|
||||
@@ -30,7 +30,7 @@ type orderManager struct {
|
||||
func newOrderManager(defaultChainID ChainID) *orderManager {
|
||||
return &orderManager{
|
||||
store: NewOrderStore(),
|
||||
clobAddress: "0x0000000000000000000000000000000000009020", // LXBook
|
||||
dexAddress: "0x0000000000000000000000000000000000009020", // LXBook
|
||||
reactorAddress: "0x0000000000000000000000000000000000009021", // DutchOrderReactor
|
||||
defaultChainID: defaultChainID,
|
||||
}
|
||||
@@ -189,7 +189,7 @@ func (s *Server) handleCancelOrder(w http.ResponseWriter, _ *http.Request, order
|
||||
}
|
||||
|
||||
// buildLimitOrderTx constructs an unsigned transaction to place a limit order
|
||||
// via the LXBook (CLOB) precompile.
|
||||
// via the LXBook (DEX) precompile.
|
||||
func (s *Server) buildLimitOrderTx(order *GatewayOrder) *TxData {
|
||||
// Function selector: placeLimitOrder(address tokenIn, address tokenOut, uint256 amountIn, uint256 limitPrice, uint64 deadline, address recipient)
|
||||
selector := crypto.Keccak256([]byte("placeLimitOrder(address,address,uint256,uint256,uint64,address)"))[:4]
|
||||
@@ -206,7 +206,7 @@ func (s *Server) buildLimitOrderTx(order *GatewayOrder) *TxData {
|
||||
data = append(data, padAddress(order.Recipient)...)
|
||||
|
||||
return &TxData{
|
||||
To: s.orders.clobAddress,
|
||||
To: s.orders.dexAddress,
|
||||
Data: "0x" + hex.EncodeToString(data),
|
||||
Value: "0",
|
||||
ChainID: uint64(order.ChainID),
|
||||
@@ -225,7 +225,7 @@ func (s *Server) buildCancelLimitOrderTx(order *GatewayOrder) *TxData {
|
||||
data = append(data, padBytes32FromString(order.ID)...)
|
||||
|
||||
return &TxData{
|
||||
To: s.orders.clobAddress,
|
||||
To: s.orders.dexAddress,
|
||||
Data: "0x" + hex.EncodeToString(data),
|
||||
Value: "0",
|
||||
ChainID: uint64(order.ChainID),
|
||||
|
||||
@@ -636,7 +636,7 @@ func TestCreateLimitOrderHandler(t *testing.T) {
|
||||
t.Fatal("did not expect EIP712 data for limit order")
|
||||
}
|
||||
if orderResp.Tx.To != "0x0000000000000000000000000000000000009020" {
|
||||
t.Fatalf("expected CLOB address, got %s", orderResp.Tx.To)
|
||||
t.Fatalf("expected DEX address, got %s", orderResp.Tx.To)
|
||||
}
|
||||
if orderResp.Tx.ChainID != 96369 {
|
||||
t.Fatalf("expected chainId 96369, got %d", orderResp.Tx.ChainID)
|
||||
|
||||
+30
-30
@@ -13,8 +13,8 @@ const (
|
||||
NativePoolManager = "0x0000000000000000000000000000000000009010"
|
||||
// NativeSwapRouter is the V4 swap router (ExactInput/Output).
|
||||
NativeSwapRouter = "0x0000000000000000000000000000000000009012"
|
||||
// NativeCLOB is the central limit order book precompile.
|
||||
NativeCLOB = "0x0000000000000000000000000000000000009020"
|
||||
// NativeOrderBook is the central limit order book precompile.
|
||||
NativeOrderBook = "0x0000000000000000000000000000000000009020"
|
||||
)
|
||||
|
||||
// quoteExactInputSingle(address,address,uint256,uint24)
|
||||
@@ -23,12 +23,12 @@ var selectorNativeQuote = lxrMustDecodeHex("30d07f21")
|
||||
|
||||
// NativeDEXVenue quotes from the Lux V4 DEX precompiles via eth_call.
|
||||
// It calls the PoolManager (0x9010) for AMM quotes and falls back to
|
||||
// the CLOB (0x9020) for limit-order liquidity.
|
||||
// the DEX (0x9020) for limit-order liquidity.
|
||||
type NativeDEXVenue struct {
|
||||
evm *EVMClient
|
||||
name string
|
||||
feeBPS uint32 // default fee tier for pool lookup
|
||||
useCLOB bool // also query CLOB for better prices
|
||||
evm *EVMClient
|
||||
name string
|
||||
feeBPS uint32 // default fee tier for pool lookup
|
||||
useDEX bool // also query DEX for better prices
|
||||
}
|
||||
|
||||
// NativeDEXConfig configures a NativeDEXVenue.
|
||||
@@ -38,8 +38,8 @@ type NativeDEXConfig struct {
|
||||
// FeeBPS is the default fee tier in basis points for pool discovery.
|
||||
// Common values: 1 (0.01%), 5 (0.05%), 30 (0.30%), 100 (1%).
|
||||
FeeBPS uint32
|
||||
// UseCLOB enables querying the CLOB precompile (0x9020) alongside AMM.
|
||||
UseCLOB bool
|
||||
// UseDEX enables querying the DEX precompile (0x9020) alongside AMM.
|
||||
UseDEX bool
|
||||
}
|
||||
|
||||
// NewNativeDEXVenue creates a venue that queries Lux DEX precompiles.
|
||||
@@ -49,10 +49,10 @@ func NewNativeDEXVenue(cfg NativeDEXConfig) *NativeDEXVenue {
|
||||
feeBPS = 30 // 0.30% default
|
||||
}
|
||||
return &NativeDEXVenue{
|
||||
evm: NewEVMClient(cfg.RPCURL),
|
||||
name: "v4_native",
|
||||
feeBPS: feeBPS,
|
||||
useCLOB: cfg.UseCLOB,
|
||||
evm: NewEVMClient(cfg.RPCURL),
|
||||
name: "v4_native",
|
||||
feeBPS: feeBPS,
|
||||
useDEX: cfg.UseDEX,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,13 +106,13 @@ func (v *NativeDEXVenue) Quote(ctx context.Context, req VenueQuoteRequest) (*Ven
|
||||
Executable: true,
|
||||
}
|
||||
|
||||
// If CLOB enabled, also check order book for better price.
|
||||
if v.useCLOB {
|
||||
clobQuote, err := v.queryCLOB(ctx, req, amountIn)
|
||||
if err == nil && clobQuote != nil {
|
||||
clobOut, _ := new(big.Int).SetString(clobQuote.AmountOut, 10)
|
||||
if clobOut != nil && clobOut.Cmp(amountOut) > 0 {
|
||||
return clobQuote, nil // CLOB has better price
|
||||
// If DEX enabled, also check order book for better price.
|
||||
if v.useDEX {
|
||||
dexQuote, err := v.queryDEX(ctx, req, amountIn)
|
||||
if err == nil && dexQuote != nil {
|
||||
dexOut, _ := new(big.Int).SetString(dexQuote.AmountOut, 10)
|
||||
if dexOut != nil && dexOut.Cmp(amountOut) > 0 {
|
||||
return dexQuote, nil // DEX has better price
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,11 +120,11 @@ func (v *NativeDEXVenue) Quote(ctx context.Context, req VenueQuoteRequest) (*Ven
|
||||
return quote, nil
|
||||
}
|
||||
|
||||
// queryCLOB checks the CLOB precompile for limit-order liquidity.
|
||||
// queryDEX checks the DEX precompile for limit-order liquidity.
|
||||
// getBestQuote(address,address,uint256,bool)
|
||||
var selectorCLOBQuote = lxrMustDecodeHex("a2e62045")
|
||||
var selectorDEXQuote = lxrMustDecodeHex("a2e62045")
|
||||
|
||||
func (v *NativeDEXVenue) queryCLOB(ctx context.Context, req VenueQuoteRequest, amountIn *big.Int) (*VenueQuote, error) {
|
||||
func (v *NativeDEXVenue) queryDEX(ctx context.Context, req VenueQuoteRequest, amountIn *big.Int) (*VenueQuote, error) {
|
||||
inAddr, _ := lxrDecodeAddress(req.TokenIn)
|
||||
outAddr, _ := lxrDecodeAddress(req.TokenOut)
|
||||
|
||||
@@ -133,13 +133,13 @@ func (v *NativeDEXVenue) queryCLOB(ctx context.Context, req VenueQuoteRequest, a
|
||||
buyWord[31] = isBuy
|
||||
|
||||
calldata := make([]byte, 0, 132)
|
||||
calldata = append(calldata, selectorCLOBQuote...)
|
||||
calldata = append(calldata, selectorDEXQuote...)
|
||||
calldata = append(calldata, lxrPadAddress(inAddr)...)
|
||||
calldata = append(calldata, lxrPadAddress(outAddr)...)
|
||||
calldata = append(calldata, lxrPadUint256(amountIn)...)
|
||||
calldata = append(calldata, buyWord[:]...)
|
||||
|
||||
result, err := v.evm.CallContract(ctx, NativeCLOB, calldata)
|
||||
result, err := v.evm.CallContract(ctx, NativeOrderBook, calldata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -154,9 +154,9 @@ func (v *NativeDEXVenue) queryCLOB(ctx context.Context, req VenueQuoteRequest, a
|
||||
}
|
||||
|
||||
return &VenueQuote{
|
||||
Venue: v.name + "_clob",
|
||||
Venue: v.name + "_dex",
|
||||
AmountOut: amountOut.String(),
|
||||
Fee: "0", // CLOB maker fees may differ
|
||||
Fee: "0", // DEX maker fees may differ
|
||||
GasEstimate: "200000",
|
||||
Executable: true,
|
||||
}, nil
|
||||
@@ -195,11 +195,11 @@ func init() {
|
||||
if len(selectorNativeQuote) != 4 {
|
||||
panic("selectorNativeQuote must be 4 bytes")
|
||||
}
|
||||
if len(selectorCLOBQuote) != 4 {
|
||||
panic("selectorCLOBQuote must be 4 bytes")
|
||||
if len(selectorDEXQuote) != 4 {
|
||||
panic("selectorDEXQuote must be 4 bytes")
|
||||
}
|
||||
// Validate precompile addresses are valid hex.
|
||||
for _, addr := range []string{NativePoolManager, NativeSwapRouter, NativeCLOB} {
|
||||
for _, addr := range []string{NativePoolManager, NativeSwapRouter, NativeOrderBook} {
|
||||
if _, err := hex.DecodeString(addr[2:]); err != nil {
|
||||
panic("invalid precompile address: " + addr)
|
||||
}
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ func (ob *OrderBook) ConsensusAddOrder(order *Order) uint64 {
|
||||
// addRestingLocked is the resting-insertion core shared by the consensus path. It
|
||||
// validates the order, runs self-trade / post-only guards, inserts into the
|
||||
// correct price tree under ob.mu, and tracks it in the order maps. It performs NO
|
||||
// matching: a resting limit order placed via the CLOB never crosses on placement
|
||||
// matching: a resting limit order placed via the OrderBook never crosses on placement
|
||||
// (takers cross via SubmitMarketable). It assumes order.ID and order.RemainingSize
|
||||
// are already set.
|
||||
//
|
||||
|
||||
+1
-1
@@ -736,7 +736,7 @@ func (ob *OrderBook) tryMatchImmediateLocked(order *Order) []Trade {
|
||||
tradeSize = math.Min(order.RemainingSize, bestRemaining)
|
||||
|
||||
// EXACT-INTEGER QUANTITY LANE (value conservation). When BOTH the taker
|
||||
// and the resting maker carry the integer quantity lane (CLOB / consensus
|
||||
// and the resting maker carry the integer quantity lane (OrderBook / consensus
|
||||
// path), the matched base is min(remainingUnits) computed in big.Int — the
|
||||
// authoritative quantity, immune to the float64 precision loss that mints
|
||||
// or burns value for 18-decimal token amounts above 2^53. The float
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
|
||||
package lx
|
||||
|
||||
// CUDA backend for the DEX CLOB match_order kernel. Linked via
|
||||
// lux-dex-clob-cuda pkg-config bundle
|
||||
// -> libdex_clob_cuda.a (luxcpp/dex/gpu/cuda/dex_clob_host.cu
|
||||
// CUDA backend for the DEX OrderBook match_order kernel. Linked via
|
||||
// lux-dex-orderbook-cuda pkg-config bundle
|
||||
// -> libdex_orderbook_cuda.a (luxcpp/dex/gpu/cuda/dex_orderbook_host.cu
|
||||
// + luxcpp/cuda/kernels/gpu/dex_swap.cu)
|
||||
|
||||
/*
|
||||
#cgo pkg-config: lux-dex-clob-cuda
|
||||
#cgo pkg-config: lux-dex-orderbook-cuda
|
||||
|
||||
#include <stdint.h>
|
||||
#include "dex_clob_host.h"
|
||||
#include "dex_orderbook_host.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
// gpuMatchOrder is the CUDA implementation called by orderbook_gpu.go.
|
||||
// Returns errCLOBGPUUnsupported when no NVIDIA device is present so the
|
||||
// Returns errOrderBookGPUUnsupported when no NVIDIA device is present so the
|
||||
// dispatcher falls back to MatchOrderCPU.
|
||||
func gpuMatchOrder(
|
||||
incoming *DEXOrder,
|
||||
@@ -53,7 +53,7 @@ func gpuMatchOrder(
|
||||
pinner.Pin(&tradesWritten)
|
||||
pinner.Pin(&remaining)
|
||||
|
||||
rc := C.dex_clob_match_order_host(
|
||||
rc := C.dex_orderbook_match_order_host(
|
||||
(*C.DEXOrder)(unsafe.Pointer(incoming)),
|
||||
(*C.DEXOrder)(unsafe.Pointer(&book[0])),
|
||||
(*C.uint32_t)(unsafe.Pointer(&bookIndices[0])),
|
||||
@@ -84,8 +84,8 @@ func gpuMatchOrder(
|
||||
}
|
||||
return trades, remaining, nil
|
||||
case -2:
|
||||
return nil, 0, errCLOBGPUUnsupported
|
||||
return nil, 0, errOrderBookGPUUnsupported
|
||||
default:
|
||||
return nil, 0, fmt.Errorf("lx: dex_clob_match_order_host rc=%d", int(rc))
|
||||
return nil, 0, fmt.Errorf("lx: dex_orderbook_match_order_host rc=%d", int(rc))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,11 +91,11 @@ func TestMatchOrder_GPUMatchesCPU(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// clobTx is one operation in a deterministic CLOB transaction sequence: either
|
||||
// orderBookTx is one operation in a deterministic OrderBook transaction sequence: either
|
||||
// rest a limit order (place) or cross the book with a marketable order (submit).
|
||||
// IDs and timestamps are supplied explicitly — exactly as the d-chain VM supplies
|
||||
// them from block context — so the replay is a pure function of the sequence.
|
||||
type clobTx struct {
|
||||
type orderBookTx struct {
|
||||
submit bool // false = place (rest), true = submit (cross)
|
||||
id uint64
|
||||
side Side
|
||||
@@ -105,14 +105,14 @@ type clobTx struct {
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
// replaySequence runs a CLOB tx sequence through a fresh book via the
|
||||
// replaySequence runs a OrderBook tx sequence through a fresh book via the
|
||||
// deterministic consensus entrypoints (ConsensusAddOrder for places,
|
||||
// SubmitMarketable for submits, with VM-supplied ID/timestamp). It returns the
|
||||
// concatenated fills (as canonical DEXTrade rows, taker side bound in) and the
|
||||
// final book as canonical DEXOrder rows — the two byte-streams a validator
|
||||
// commits. It NEVER calls AddOrder (would mint) or MatchOrders (would stamp
|
||||
// time.Now()).
|
||||
func replaySequence(symbol string, txs []clobTx) (fills []DEXTrade, rows []DEXOrder) {
|
||||
func replaySequence(symbol string, txs []orderBookTx) (fills []DEXTrade, rows []DEXOrder) {
|
||||
ob := NewOrderBook(symbol)
|
||||
for _, tx := range txs {
|
||||
if tx.submit {
|
||||
@@ -139,7 +139,7 @@ func replaySequence(symbol string, txs []clobTx) (fills []DEXTrade, rows []DEXOr
|
||||
}
|
||||
|
||||
// TestConsensusReplayDeterminism is the CRITICAL step-2 determinism proof: two
|
||||
// replays of the IDENTICAL CLOB tx sequence produce BYTE-IDENTICAL fills and
|
||||
// replays of the IDENTICAL OrderBook tx sequence produce BYTE-IDENTICAL fills and
|
||||
// BYTE-IDENTICAL book rows. This is the property a multi-validator d-chain
|
||||
// depends on — every honest validator that replays the same ordered block
|
||||
// derives the same fills and the same committed state. The consensus path
|
||||
@@ -151,7 +151,7 @@ func TestConsensusReplayDeterminism(t *testing.T) {
|
||||
|
||||
// A representative sequence: rest both sides, then cross with marketable
|
||||
// orders that partially fill across multiple price levels, then rest more.
|
||||
txs := []clobTx{
|
||||
txs := []orderBookTx{
|
||||
{false, 1, Sell, 101.0, 5.0, "maker-a", ts(0)},
|
||||
{false, 2, Sell, 101.5, 3.0, "maker-b", ts(1)},
|
||||
{false, 3, Sell, 102.0, 4.0, "maker-c", ts(2)},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
package lx
|
||||
|
||||
// gpuMatchOrder has no GPU backend on non-Linux or no-cgo builds. The
|
||||
// dispatcher in orderbook_gpu.go turns errCLOBGPUUnsupported into a
|
||||
// dispatcher in orderbook_gpu.go turns errOrderBookGPUUnsupported into a
|
||||
// transparent fallback to MatchOrderCPU.
|
||||
func gpuMatchOrder(
|
||||
incoming *DEXOrder,
|
||||
@@ -15,5 +15,5 @@ func gpuMatchOrder(
|
||||
tradeIDBase uint64,
|
||||
timestamp uint64,
|
||||
) ([]DEXTrade, uint64, error) {
|
||||
return nil, 0, errCLOBGPUUnsupported
|
||||
return nil, 0, errOrderBookGPUUnsupported
|
||||
}
|
||||
|
||||
@@ -40,14 +40,14 @@ func MatchOrderGPU(
|
||||
}
|
||||
|
||||
if backend.GPUDisabled() {
|
||||
backend.RecordFallback(backend.FallbackDisabled, "clob")
|
||||
backend.RecordFallback(backend.FallbackDisabled, "orderBook")
|
||||
t, r := MatchOrderCPU(incoming, book, bookIndices, tradeIDBase, timestamp)
|
||||
return t, r, nil
|
||||
}
|
||||
|
||||
trades, remaining, err := gpuMatchOrder(incoming, book, bookIndices, tradeIDBase, timestamp)
|
||||
if err == errCLOBGPUUnsupported {
|
||||
backend.RecordFallback(backend.FallbackUnsupported, "clob")
|
||||
if err == errOrderBookGPUUnsupported {
|
||||
backend.RecordFallback(backend.FallbackUnsupported, "orderBook")
|
||||
t, r := MatchOrderCPU(incoming, book, bookIndices, tradeIDBase, timestamp)
|
||||
return t, r, nil
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func MatchOrderGPU(
|
||||
return trades, remaining, nil
|
||||
}
|
||||
|
||||
// errCLOBGPUUnsupported is returned by per-platform gpuMatchOrder when
|
||||
// errOrderBookGPUUnsupported is returned by per-platform gpuMatchOrder when
|
||||
// no GPU dispatch is available. The dispatcher turns it into a CPU
|
||||
// fallback rather than surfacing the error.
|
||||
var errCLOBGPUUnsupported = fmt.Errorf("lx: CLOB CUDA dispatch not available on this host")
|
||||
var errOrderBookGPUUnsupported = fmt.Errorf("lx: OrderBook CUDA dispatch not available on this host")
|
||||
|
||||
@@ -120,7 +120,7 @@ func TestSubmitMarketableSweepAcrossTruncation(t *testing.T) {
|
||||
// warm + sweepLots > tradeLogCap, the trim fires mid-match and (pre-fix) makes
|
||||
// the start offset (== warm, which is > tradeLogKeep) stale -> panic.
|
||||
const sweepLots = (tradeLogCap - warm) + 5000 // pushes warm+sweep over the cap
|
||||
const sweepIDBase = 1_000_000 // disjoint from the warm maker IDs above
|
||||
const sweepIDBase = 1_000_000 // disjoint from the warm maker IDs above
|
||||
for j := 0; j < sweepLots; j++ {
|
||||
ob.AddOrder(&Order{
|
||||
ID: uint64(sweepIDBase + j), Type: Limit, Side: Sell, Price: 200, Size: 1, User: "mm-sweep",
|
||||
|
||||
+8
-8
@@ -157,7 +157,7 @@ func dexToSide(s uint8) Side {
|
||||
|
||||
// typeToDEX maps the book OrderType to the DEXOrder type byte. Only the resting
|
||||
// forms are persistable (a resting order is always a Limit by construction in
|
||||
// the consensus CLOB path); Market/IOC/FOK takers never rest and so never reach
|
||||
// the consensus OrderBook path); Market/IOC/FOK takers never rest and so never reach
|
||||
// a row. Anything non-Limit maps to DEXOrderLimit defensively.
|
||||
func typeToDEX(t OrderType) uint8 {
|
||||
switch t {
|
||||
@@ -302,13 +302,13 @@ func DecodeTrade(b []byte) (DEXTrade, bool) {
|
||||
// by the caller because the in-RAM Trade does not always carry it.
|
||||
func TradeToRow(t Trade, takerSide Side) DEXTrade {
|
||||
row := DEXTrade{
|
||||
TradeID: t.ID,
|
||||
MakerUserID: userToUint64(makerUser(t, takerSide)),
|
||||
TakerUserID: userToUint64(takerUser(t, takerSide)),
|
||||
Price: floatToFixedPrice(t.Price),
|
||||
Quantity: floatToFixedQty(t.Size),
|
||||
Timestamp: uint64(t.Timestamp.UnixNano()),
|
||||
TakerSide: sideToDEX(takerSide),
|
||||
TradeID: t.ID,
|
||||
MakerUserID: userToUint64(makerUser(t, takerSide)),
|
||||
TakerUserID: userToUint64(takerUser(t, takerSide)),
|
||||
Price: floatToFixedPrice(t.Price),
|
||||
Quantity: floatToFixedQty(t.Size),
|
||||
Timestamp: uint64(t.Timestamp.UnixNano()),
|
||||
TakerSide: sideToDEX(takerSide),
|
||||
}
|
||||
// Maker/taker order IDs: the buy order is BuyOrder, the sell is SellOrder;
|
||||
// the taker is whichever matches takerSide.
|
||||
|
||||
@@ -97,7 +97,7 @@ func TestBookToRowsRoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
place(1, Buy, 100.0, 2.0, "alice", 0)
|
||||
place(2, Buy, 100.0, 1.5, "bob", 1) // same level, later -> behind id1
|
||||
place(2, Buy, 100.0, 1.5, "bob", 1) // same level, later -> behind id1
|
||||
place(3, Buy, 99.5, 3.0, "carol", 2)
|
||||
place(4, Sell, 101.0, 2.5, "dave", 3)
|
||||
place(5, Sell, 101.5, 1.0, "erin", 4)
|
||||
|
||||
@@ -6,7 +6,7 @@ package lx
|
||||
import "math/big"
|
||||
|
||||
// settlement_units.go is the SINGLE home for the exact-integer quantity lane the
|
||||
// CLOB / consensus path uses for value conservation. The matcher's price grid
|
||||
// OrderBook / consensus path uses for value conservation. The matcher's price grid
|
||||
// stays float64 (a ratio is fine as a float, and price-time priority is unchanged
|
||||
// by it), but the SETTLED QUANTITY — the base units a fill moves and the quote
|
||||
// units owed for them — must be exact: an 18-decimal token amount (e.g. 1e20)
|
||||
@@ -47,7 +47,7 @@ func quoteUnitsFor(baseUnits *big.Int, priceInt PriceInt) *big.Int {
|
||||
}
|
||||
|
||||
// ensureRemainingUnits lazily initializes an order's exact integer remainder from
|
||||
// its SizeUnits the first time the matcher touches it on the CLOB path. An order
|
||||
// its SizeUnits the first time the matcher touches it on the OrderBook path. An order
|
||||
// without SizeUnits (legacy float API) yields nil — the integer lane stays off and
|
||||
// the caller falls back to the float quantity, preserving legacy behavior.
|
||||
func ensureRemainingUnits(o *Order) *big.Int {
|
||||
|
||||
+3
-3
@@ -60,13 +60,13 @@ type Order struct {
|
||||
Flags OrderFlags
|
||||
ExecutedSize float64 // Total executed size
|
||||
|
||||
// Exact-integer quantity lane (CLOB / consensus path only). The float Size
|
||||
// Exact-integer quantity lane (OrderBook / consensus path only). The float Size
|
||||
// above is the price-grid/validation/legacy quantity; it loses precision for
|
||||
// 18-decimal token amounts above 2^53. SizeUnits is the AUTHORITATIVE base
|
||||
// quantity in atomic token units (no float, no rounding) and is the only
|
||||
// quantity settlement is derived from. RemainingUnits tracks the unfilled
|
||||
// integer remainder as the order crosses. Both are nil on the legacy float
|
||||
// API path (AddOrder/MatchOrders), set on the CLOB path (ConsensusAddOrder /
|
||||
// API path (AddOrder/MatchOrders), set on the OrderBook path (ConsensusAddOrder /
|
||||
// SubmitMarketable) by the wire decoder. See settlement_units.go.
|
||||
SizeUnits *big.Int
|
||||
RemainingUnits *big.Int
|
||||
@@ -88,7 +88,7 @@ type Trade struct {
|
||||
MatchType string // Type of match (e.g., "normal", "liquidation")
|
||||
TakerSide Side // Side that took liquidity
|
||||
|
||||
// Exact-integer execution truth (CLOB / consensus path). Price/Size above are
|
||||
// Exact-integer execution truth (OrderBook / consensus path). Price/Size above are
|
||||
// the float view; BaseUnits and QuoteUnits are the AUTHORITATIVE atomic-unit
|
||||
// amounts this fill moved, computed with big.Int only (no float, see
|
||||
// settlement_units.go: QuoteUnits = BaseUnits*priceInt/PriceMultiplier). These
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
// dexvm_gpu.go — direct dlopen + dlsym to the gpu-kernels DEX plugin.
|
||||
//
|
||||
// Why dlopen, not pkg-config: the AMM/CLOB host launchers live in a
|
||||
// Why dlopen, not pkg-config: the AMM/OrderBook host launchers live in a
|
||||
// per-backend plugin (libluxgpu_backend_{cuda,hip,metal,vulkan,webgpu})
|
||||
// whose presence is platform-dependent — Apple has only Metal+Vulkan
|
||||
// (via MoltenVK), Linux has CUDA+Vulkan+WebGPU, ROCm only on a small
|
||||
@@ -21,16 +21,16 @@
|
||||
//
|
||||
// int amm_xyk_batch(const void* reserves, const void* amounts,
|
||||
// void* outs, uint32_t n, void* stream);
|
||||
// int dex_clob_match(void* arena, const uint8_t* calldata,
|
||||
// int dex_orderbook_match(void* arena, const uint8_t* calldata,
|
||||
// uint8_t* out, uint32_t* num_fills);
|
||||
// int dex_clob_arena_create(void** out_arena);
|
||||
// int dex_clob_arena_destroy(void* arena);
|
||||
// int dex_orderbook_arena_create(void** out_arena);
|
||||
// int dex_orderbook_arena_destroy(void* arena);
|
||||
//
|
||||
// All inputs are HOST pointers (no separate device address space is
|
||||
// exposed across the ABI — the launcher does H2D/D2H copies
|
||||
// internally). The `stream` argument on AMM is ignored by every
|
||||
// backend except CUDA/HIP (where it would be a cudaStream_t / hipStream_t).
|
||||
// `arena` for CLOB is opaque — created/destroyed via the symmetric
|
||||
// `arena` for OrderBook is opaque — created/destroyed via the symmetric
|
||||
// pair, persisted across calls so the BookArena lives device-resident
|
||||
// for the lifetime of the book.
|
||||
|
||||
@@ -56,14 +56,14 @@ typedef int (*lux_amm_xyk_batch_fn)(
|
||||
uint32_t n,
|
||||
void* stream);
|
||||
|
||||
typedef int (*lux_dex_clob_match_fn)(
|
||||
typedef int (*lux_dex_orderbook_match_fn)(
|
||||
void* arena,
|
||||
const uint8_t* calldata,
|
||||
uint8_t* out,
|
||||
uint32_t* num_fills);
|
||||
|
||||
typedef int (*lux_dex_clob_arena_create_fn)(void** out_arena);
|
||||
typedef int (*lux_dex_clob_arena_destroy_fn)(void* arena);
|
||||
typedef int (*lux_dex_orderbook_arena_create_fn)(void** out_arena);
|
||||
typedef int (*lux_dex_orderbook_arena_destroy_fn)(void* arena);
|
||||
|
||||
static int call_amm_xyk_batch(
|
||||
void* fn,
|
||||
@@ -75,22 +75,22 @@ static int call_amm_xyk_batch(
|
||||
return ((lux_amm_xyk_batch_fn)fn)(reserves, amounts, outs, n, NULL);
|
||||
}
|
||||
|
||||
static int call_dex_clob_match(
|
||||
static int call_dex_orderbook_match(
|
||||
void* fn,
|
||||
void* arena,
|
||||
const uint8_t* calldata,
|
||||
uint8_t* out,
|
||||
uint32_t* num_fills)
|
||||
{
|
||||
return ((lux_dex_clob_match_fn)fn)(arena, calldata, out, num_fills);
|
||||
return ((lux_dex_orderbook_match_fn)fn)(arena, calldata, out, num_fills);
|
||||
}
|
||||
|
||||
static int call_dex_clob_arena_create(void* fn, void** out_arena) {
|
||||
return ((lux_dex_clob_arena_create_fn)fn)(out_arena);
|
||||
static int call_dex_orderbook_arena_create(void* fn, void** out_arena) {
|
||||
return ((lux_dex_orderbook_arena_create_fn)fn)(out_arena);
|
||||
}
|
||||
|
||||
static int call_dex_clob_arena_destroy(void* fn, void* arena) {
|
||||
return ((lux_dex_clob_arena_destroy_fn)fn)(arena);
|
||||
static int call_dex_orderbook_arena_destroy(void* fn, void* arena) {
|
||||
return ((lux_dex_orderbook_arena_destroy_fn)fn)(arena);
|
||||
}
|
||||
|
||||
// dlopen / dlsym wrappers. dlerror() returns thread-local NULL when
|
||||
@@ -134,10 +134,10 @@ type gpuHandle struct {
|
||||
|
||||
hLib unsafe.Pointer // dlopen handle
|
||||
|
||||
fnAMMSwap unsafe.Pointer // lux_<backend>_amm_xyk_batch
|
||||
fnCLOBMatch unsafe.Pointer // lux_<backend>_dex_clob_match
|
||||
fnArenaCreate unsafe.Pointer // lux_<backend>_dex_clob_arena_create
|
||||
fnArenaDestroy unsafe.Pointer // lux_<backend>_dex_clob_arena_destroy
|
||||
fnAMMSwap unsafe.Pointer // lux_<backend>_amm_xyk_batch
|
||||
fnOrderBookMatch unsafe.Pointer // lux_<backend>_dex_orderbook_match
|
||||
fnArenaCreate unsafe.Pointer // lux_<backend>_dex_orderbook_arena_create
|
||||
fnArenaDestroy unsafe.Pointer // lux_<backend>_dex_orderbook_arena_destroy
|
||||
}
|
||||
|
||||
// globalGPU holds the result of the init() probe. nil after a clean
|
||||
@@ -245,7 +245,7 @@ func pluginCandidatePaths(b GPUBackend) []string {
|
||||
// dylib won't load OR any required symbol is missing. Symbol misses
|
||||
// are treated as fatal-for-this-backend (we don't accept a partial
|
||||
// plugin — that would mask a build skew and produce a "GPU loaded
|
||||
// but CLOB returns garbage" failure mode that's worse than CPU).
|
||||
// but OrderBook returns garbage" failure mode that's worse than CPU).
|
||||
func tryLoadBackend(b GPUBackend) *gpuHandle {
|
||||
candidates := pluginCandidatePaths(b)
|
||||
for _, path := range candidates {
|
||||
@@ -261,12 +261,12 @@ func tryLoadBackend(b GPUBackend) *gpuHandle {
|
||||
|
||||
prefix := "lux_" + b.String() + "_"
|
||||
var (
|
||||
fnAMM = dlsymOrNil(hLib, prefix+"amm_xyk_batch")
|
||||
fnCLOB = dlsymOrNil(hLib, prefix+"dex_clob_match")
|
||||
fnCreate = dlsymOrNil(hLib, prefix+"dex_clob_arena_create")
|
||||
fnDestr = dlsymOrNil(hLib, prefix+"dex_clob_arena_destroy")
|
||||
fnAMM = dlsymOrNil(hLib, prefix+"amm_xyk_batch")
|
||||
fnOrderBook = dlsymOrNil(hLib, prefix+"dex_orderbook_match")
|
||||
fnCreate = dlsymOrNil(hLib, prefix+"dex_orderbook_arena_create")
|
||||
fnDestr = dlsymOrNil(hLib, prefix+"dex_orderbook_arena_destroy")
|
||||
)
|
||||
if fnAMM == nil || fnCLOB == nil || fnCreate == nil || fnDestr == nil {
|
||||
if fnAMM == nil || fnOrderBook == nil || fnCreate == nil || fnDestr == nil {
|
||||
// Partial export set — plausible cause is a stale plugin
|
||||
// build that pre-dates the DEX launcher landing. Close
|
||||
// the handle so we don't keep a dangling mapping, and
|
||||
@@ -275,13 +275,13 @@ func tryLoadBackend(b GPUBackend) *gpuHandle {
|
||||
continue
|
||||
}
|
||||
return &gpuHandle{
|
||||
backend: b,
|
||||
libPath: path,
|
||||
hLib: hLib,
|
||||
fnAMMSwap: fnAMM,
|
||||
fnCLOBMatch: fnCLOB,
|
||||
fnArenaCreate: fnCreate,
|
||||
fnArenaDestroy: fnDestr,
|
||||
backend: b,
|
||||
libPath: path,
|
||||
hLib: hLib,
|
||||
fnAMMSwap: fnAMM,
|
||||
fnOrderBookMatch: fnOrderBook,
|
||||
fnArenaCreate: fnCreate,
|
||||
fnArenaDestroy: fnDestr,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -391,29 +391,29 @@ func AMMSwap(reserves []LuxAmmReservePair, amounts []uint64) ([]uint64, error) {
|
||||
return outs, nil
|
||||
}
|
||||
|
||||
// CLOBArena is the opaque handle to a device-resident BookArena. The
|
||||
// OrderBookArena is the opaque handle to a device-resident BookArena. The
|
||||
// pointer crosses the cgo boundary as void* — Go never dereferences
|
||||
// it, only passes it to ArenaDestroy or CLOBMatch.
|
||||
// it, only passes it to ArenaDestroy or OrderBookMatch.
|
||||
//
|
||||
// ArenaCreate hands ownership to the Go caller; ArenaDestroy releases
|
||||
// it. The arena lives across many CLOBMatch calls — that's the
|
||||
// it. The arena lives across many OrderBookMatch calls — that's the
|
||||
// entire reason it's device-resident: the BookArena holds the
|
||||
// resting bids/asks for a single book, and successive CLOBMatch
|
||||
// resting bids/asks for a single book, and successive OrderBookMatch
|
||||
// calls on the same book MUST share state.
|
||||
type CLOBArena struct {
|
||||
type OrderBookArena struct {
|
||||
ptr unsafe.Pointer
|
||||
}
|
||||
|
||||
// ArenaCreate allocates a fresh device-resident BookArena (zero
|
||||
// initialized). The returned handle is owned by the caller; call
|
||||
// ArenaDestroy to free it. Concurrent ArenaCreate is safe.
|
||||
func ArenaCreate() (*CLOBArena, error) {
|
||||
func ArenaCreate() (*OrderBookArena, error) {
|
||||
h, err := activeGPU()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var raw unsafe.Pointer
|
||||
rc := C.call_dex_clob_arena_create(h.fnArenaCreate, &raw)
|
||||
rc := C.call_dex_orderbook_arena_create(h.fnArenaCreate, &raw)
|
||||
if rc != 0 {
|
||||
return nil, fmt.Errorf("dexvm.ArenaCreate: %s plugin returned rc=%d",
|
||||
h.backend, int(rc))
|
||||
@@ -422,13 +422,13 @@ func ArenaCreate() (*CLOBArena, error) {
|
||||
return nil, fmt.Errorf("dexvm.ArenaCreate: %s plugin returned NULL arena",
|
||||
h.backend)
|
||||
}
|
||||
return &CLOBArena{ptr: raw}, nil
|
||||
return &OrderBookArena{ptr: raw}, nil
|
||||
}
|
||||
|
||||
// ArenaDestroy releases the device-resident BookArena. Calling it on
|
||||
// a nil arena is a no-op (matches the C-side symmetry). Calling it
|
||||
// twice on the same arena is UB at the plugin level — don't.
|
||||
func ArenaDestroy(a *CLOBArena) error {
|
||||
func ArenaDestroy(a *OrderBookArena) error {
|
||||
if a == nil || a.ptr == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -436,7 +436,7 @@ func ArenaDestroy(a *CLOBArena) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rc := C.call_dex_clob_arena_destroy(h.fnArenaDestroy, a.ptr)
|
||||
rc := C.call_dex_orderbook_arena_destroy(h.fnArenaDestroy, a.ptr)
|
||||
a.ptr = nil
|
||||
if rc != 0 {
|
||||
return fmt.Errorf("dexvm.ArenaDestroy: %s plugin returned rc=%d",
|
||||
@@ -445,7 +445,7 @@ func ArenaDestroy(a *CLOBArena) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CLOBMatch runs one matcher step against the arena. calldata is the
|
||||
// OrderBookMatch runs one matcher step against the arena. calldata is the
|
||||
// 117-byte EVM precompile 0x100 input — see backend.go for the byte
|
||||
// layout. Output is a fixed-size 68-byte buffer (filled + avg_price +
|
||||
// num_fills) plus a num_fills uint32 mirror for callers that want it
|
||||
@@ -454,17 +454,17 @@ func ArenaDestroy(a *CLOBArena) error {
|
||||
// Single matcher pass per call; concurrency is across book_ids — each
|
||||
// book gets its own arena, and independent arenas can be matched in
|
||||
// parallel on different goroutines without serialization.
|
||||
func CLOBMatch(a *CLOBArena, calldata []byte) (out [LuxCLOBOutLen]byte, numFills uint32, err error) {
|
||||
func OrderBookMatch(a *OrderBookArena, calldata []byte) (out [OrderBookOutLen]byte, numFills uint32, err error) {
|
||||
h, gerr := activeGPU()
|
||||
if gerr != nil {
|
||||
return out, 0, gerr
|
||||
}
|
||||
if a == nil || a.ptr == nil {
|
||||
return out, 0, fmt.Errorf("dexvm.CLOBMatch: arena is nil")
|
||||
return out, 0, fmt.Errorf("dexvm.OrderBookMatch: arena is nil")
|
||||
}
|
||||
if len(calldata) != LuxCLOBCalldataLen {
|
||||
return out, 0, fmt.Errorf("dexvm.CLOBMatch: calldata len=%d, want %d",
|
||||
len(calldata), LuxCLOBCalldataLen)
|
||||
if len(calldata) != OrderBookCalldataLen {
|
||||
return out, 0, fmt.Errorf("dexvm.OrderBookMatch: calldata len=%d, want %d",
|
||||
len(calldata), OrderBookCalldataLen)
|
||||
}
|
||||
|
||||
var pinner runtime.Pinner
|
||||
@@ -473,8 +473,8 @@ func CLOBMatch(a *CLOBArena, calldata []byte) (out [LuxCLOBOutLen]byte, numFills
|
||||
pinner.Pin(&out[0])
|
||||
|
||||
var nf C.uint32_t
|
||||
rc := C.call_dex_clob_match(
|
||||
h.fnCLOBMatch,
|
||||
rc := C.call_dex_orderbook_match(
|
||||
h.fnOrderBookMatch,
|
||||
a.ptr,
|
||||
(*C.uint8_t)(unsafe.Pointer(&calldata[0])),
|
||||
(*C.uint8_t)(unsafe.Pointer(&out[0])),
|
||||
@@ -483,7 +483,7 @@ func CLOBMatch(a *CLOBArena, calldata []byte) (out [LuxCLOBOutLen]byte, numFills
|
||||
runtime.KeepAlive(calldata)
|
||||
runtime.KeepAlive(out)
|
||||
if rc != 0 {
|
||||
return out, 0, fmt.Errorf("dexvm.CLOBMatch: %s plugin returned rc=%d",
|
||||
return out, 0, fmt.Errorf("dexvm.OrderBookMatch: %s plugin returned rc=%d",
|
||||
h.backend, int(rc))
|
||||
}
|
||||
return out, uint32(nf), nil
|
||||
@@ -19,10 +19,10 @@ func AutoBackend() GPUBackend { return GPUBackendNone }
|
||||
// GPUPluginPath returns "" under !cgo — no plugin was loaded.
|
||||
func GPUPluginPath() string { return "" }
|
||||
|
||||
// CLOBArena is a zero-sized opaque type so the package surface
|
||||
// OrderBookArena is a zero-sized opaque type so the package surface
|
||||
// compiles uniformly under both build tags. The nocgo build never
|
||||
// returns a non-nil arena from ArenaCreate.
|
||||
type CLOBArena struct{}
|
||||
type OrderBookArena struct{}
|
||||
|
||||
// AMMSwap returns ErrGPUNotAvailable under !cgo.
|
||||
func AMMSwap(reserves []LuxAmmReservePair, amounts []uint64) ([]uint64, error) {
|
||||
@@ -32,21 +32,21 @@ func AMMSwap(reserves []LuxAmmReservePair, amounts []uint64) ([]uint64, error) {
|
||||
}
|
||||
|
||||
// ArenaCreate returns ErrGPUNotAvailable under !cgo.
|
||||
func ArenaCreate() (*CLOBArena, error) {
|
||||
func ArenaCreate() (*OrderBookArena, error) {
|
||||
return nil, ErrGPUNotAvailable
|
||||
}
|
||||
|
||||
// ArenaDestroy returns ErrGPUNotAvailable under !cgo. A nil arena is
|
||||
// still a no-op for symmetry with the cgo build.
|
||||
func ArenaDestroy(a *CLOBArena) error {
|
||||
func ArenaDestroy(a *OrderBookArena) error {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
return ErrGPUNotAvailable
|
||||
}
|
||||
|
||||
// CLOBMatch returns ErrGPUNotAvailable under !cgo.
|
||||
func CLOBMatch(a *CLOBArena, calldata []byte) (out [LuxCLOBOutLen]byte, numFills uint32, err error) {
|
||||
// OrderBookMatch returns ErrGPUNotAvailable under !cgo.
|
||||
func OrderBookMatch(a *OrderBookArena, calldata []byte) (out [OrderBookOutLen]byte, numFills uint32, err error) {
|
||||
_ = a
|
||||
_ = calldata
|
||||
return out, 0, ErrGPUNotAvailable
|
||||
@@ -117,4 +117,3 @@ func TestGPUAMMSwapEmpty(t *testing.T) {
|
||||
t.Fatalf("AMMSwap(nil, nil) returned len=%d, want 0", len(outs))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,24 +3,24 @@
|
||||
|
||||
//go:build cgo
|
||||
|
||||
// dexvm_clob_parity_test.go — GPU-vs-CPU byte-parity KAT for the
|
||||
// device-resident CLOB matcher.
|
||||
// dexvm_orderBook_parity_test.go — GPU-vs-CPU byte-parity KAT for the
|
||||
// device-resident OrderBook matcher.
|
||||
//
|
||||
// This is the load-bearing proof for the "full native GPU DEX, crypto in
|
||||
// GPU memory" claim: the GPU CLOB path (CLOBMatch in dexvm_gpu.go, which
|
||||
// dlsym's lux_<backend>_dex_clob_match and runs the matcher against a
|
||||
// GPU memory" claim: the GPU OrderBook path (OrderBookMatch in dexvm_gpu.go, which
|
||||
// dlsym's lux_<backend>_dex_orderbook_match and runs the matcher against a
|
||||
// device-resident BookArena) MUST produce a 68-byte output that is
|
||||
// byte-identical to the canonical CPU oracle for the same order sequence.
|
||||
//
|
||||
// The CPU oracle here is a self-contained Go port of the canonical C++
|
||||
// oracle at
|
||||
// ~/work/lux-private/gpu-kernels/tools/kat/dex_cpu_oracle.hpp
|
||||
// ::clob_match_step_cpu
|
||||
// ::orderBook_match_step_cpu
|
||||
// which is itself the single source of truth that every GPU backend
|
||||
// (cuda/hip/metal/vulkan/webgpu) and the C++ dex_match_cpu.cpp are held
|
||||
// to. The Metal kernel
|
||||
// ~/work/lux-private/gpu-kernels/ops/dex/metal/clob_match.metal
|
||||
// ::clob_match_step
|
||||
// ~/work/lux-private/gpu-kernels/ops/dex/metal/orderBook_match.metal
|
||||
// ::orderBook_match_step
|
||||
// implements the identical algorithm (same crossing rules, FIFO front-pop,
|
||||
// price-sorted insertion: bids high→low, asks low→high; VWAP = 512/256
|
||||
// shift-subtract long division). This test proves the Go bridge → plugin
|
||||
@@ -30,7 +30,7 @@
|
||||
//
|
||||
// export LUX_GPU_PLUGIN_DIR=/Users/z/work/luxcpp/gpu/build/metal_backend
|
||||
// export SDKROOT=$(xcrun --show-sdk-path)
|
||||
// GOWORK=off go test ./dexvm/ -run TestCLOBGPUvsCPUByteParity -v
|
||||
// GOWORK=off go test ./dexvm/ -run TestOrderBookGPUvsCPUByteParity -v
|
||||
//
|
||||
// When no plugin is on disk (AutoBackend()==None), the test SKIPs with the
|
||||
// reason — the parity assertion needs a real GPU plugin; the CPU oracle is
|
||||
@@ -46,7 +46,7 @@ import (
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Go CPU oracle — byte-equal port of dex_cpu_oracle.hpp::clob_match_step_cpu.
|
||||
// Go CPU oracle — byte-equal port of dex_cpu_oracle.hpp::orderBook_match_step_cpu.
|
||||
// uint256 is a 32-byte big-endian array, exactly the kernel-side layout.
|
||||
// =============================================================================
|
||||
|
||||
@@ -223,25 +223,25 @@ func u256VWAP(notionalHi, notionalLo, total *u256) u256 {
|
||||
// Fixed-cap 1024 levels per side, price-sorted (bids high→low, asks low→high).
|
||||
type bookArena struct {
|
||||
nBids, nAsks int
|
||||
bidPrice [LuxCLOBMaxLevels]u256
|
||||
bidQty [LuxCLOBMaxLevels]u256
|
||||
askPrice [LuxCLOBMaxLevels]u256
|
||||
askQty [LuxCLOBMaxLevels]u256
|
||||
bidPrice [OrderBookMaxLevels]u256
|
||||
bidQty [OrderBookMaxLevels]u256
|
||||
askPrice [OrderBookMaxLevels]u256
|
||||
askQty [OrderBookMaxLevels]u256
|
||||
}
|
||||
|
||||
type clobOutput struct {
|
||||
type orderBookOutput struct {
|
||||
filled u256
|
||||
avgPrice u256
|
||||
numFills uint32
|
||||
}
|
||||
|
||||
// clobMatchStepCPU is the byte-equal Go port of clob_match_step_cpu.
|
||||
func clobMatchStepCPU(a *bookArena, side uint8, price, qty u256) clobOutput {
|
||||
// orderBookMatchStepCPU is the byte-equal Go port of orderBook_match_step_cpu.
|
||||
func orderBookMatchStepCPU(a *bookArena, side uint8, price, qty u256) orderBookOutput {
|
||||
remaining := qty
|
||||
var filled, notionalHi, notionalLo u256
|
||||
numFills := uint32(0)
|
||||
|
||||
matchLoop := func(oppPrice, oppQty *[LuxCLOBMaxLevels]u256, nOpp *int, incomingIsBid bool) {
|
||||
matchLoop := func(oppPrice, oppQty *[OrderBookMaxLevels]u256, nOpp *int, incomingIsBid bool) {
|
||||
for *nOpp > 0 && !u256IsZero(&remaining) {
|
||||
topPrice := oppPrice[0]
|
||||
topQty := oppQty[0]
|
||||
@@ -278,7 +278,7 @@ func clobMatchStepCPU(a *bookArena, side uint8, price, qty u256) clobOutput {
|
||||
|
||||
if side == 0 {
|
||||
matchLoop(&a.askPrice, &a.askQty, &a.nAsks, true)
|
||||
if !u256IsZero(&remaining) && a.nBids < LuxCLOBMaxLevels {
|
||||
if !u256IsZero(&remaining) && a.nBids < OrderBookMaxLevels {
|
||||
pos := a.nBids
|
||||
for i := 0; i < a.nBids; i++ {
|
||||
if u256Cmp(&a.bidPrice[i], &price) < 0 {
|
||||
@@ -296,7 +296,7 @@ func clobMatchStepCPU(a *bookArena, side uint8, price, qty u256) clobOutput {
|
||||
}
|
||||
} else {
|
||||
matchLoop(&a.bidPrice, &a.bidQty, &a.nBids, false)
|
||||
if !u256IsZero(&remaining) && a.nAsks < LuxCLOBMaxLevels {
|
||||
if !u256IsZero(&remaining) && a.nAsks < OrderBookMaxLevels {
|
||||
pos := a.nAsks
|
||||
for i := 0; i < a.nAsks; i++ {
|
||||
if u256Cmp(&price, &a.askPrice[i]) < 0 {
|
||||
@@ -314,37 +314,37 @@ func clobMatchStepCPU(a *bookArena, side uint8, price, qty u256) clobOutput {
|
||||
}
|
||||
}
|
||||
|
||||
return clobOutput{
|
||||
return orderBookOutput{
|
||||
filled: filled,
|
||||
avgPrice: u256VWAP(¬ionalHi, ¬ionalLo, &filled),
|
||||
numFills: numFills,
|
||||
}
|
||||
}
|
||||
|
||||
// encodeCLOBOutput packs a clobOutput into the 68-byte wire format the GPU
|
||||
// encodeOrderBookOutput packs a orderBookOutput into the 68-byte wire format the GPU
|
||||
// kernel emits: filled(32 BE) | avg_price(32 BE) | num_fills(4 BE).
|
||||
func encodeCLOBOutput(o clobOutput) [LuxCLOBOutLen]byte {
|
||||
var out [LuxCLOBOutLen]byte
|
||||
func encodeOrderBookOutput(o orderBookOutput) [OrderBookOutLen]byte {
|
||||
var out [OrderBookOutLen]byte
|
||||
copy(out[0:32], o.filled[:])
|
||||
copy(out[32:64], o.avgPrice[:])
|
||||
binary.BigEndian.PutUint32(out[64:68], o.numFills)
|
||||
return out
|
||||
}
|
||||
|
||||
// encodeCLOBCalldata builds the 117-byte EVM 0x100 calldata:
|
||||
// encodeOrderBookCalldata builds the 117-byte EVM 0x100 calldata:
|
||||
// side(1) | price(32 BE) | qty(32 BE) | user(20) | book_id(32).
|
||||
func encodeCLOBCalldata(side uint8, price, qty u256, user [20]byte, bookID [32]byte) []byte {
|
||||
cd := make([]byte, LuxCLOBCalldataLen)
|
||||
func encodeOrderBookCalldata(side uint8, price, qty u256, user [20]byte, bookID [32]byte) []byte {
|
||||
cd := make([]byte, OrderBookCalldataLen)
|
||||
cd[0] = side
|
||||
copy(cd[1:33], price[:])
|
||||
copy(cd[33:65], qty[:])
|
||||
copy(cd[65:85], user[:])
|
||||
copy(cd[LuxCLOBBookIDOffset:LuxCLOBBookIDOffset+LuxCLOBBookIDLen], bookID[:])
|
||||
copy(cd[OrderBookBookIDOffset:OrderBookBookIDOffset+OrderBookBookIDLen], bookID[:])
|
||||
return cd
|
||||
}
|
||||
|
||||
// clobStep is one order in a parity sequence.
|
||||
type clobStep struct {
|
||||
// orderBookStep is one order in a parity sequence.
|
||||
type orderBookStep struct {
|
||||
name string
|
||||
side uint8 // 0 = bid, 1 = ask
|
||||
price uint64
|
||||
@@ -355,14 +355,14 @@ type clobStep struct {
|
||||
// Self-check of the Go oracle — runs on every host (no GPU needed).
|
||||
// =============================================================================
|
||||
|
||||
// TestCLOBOracleSelfConsistent verifies the Go oracle against hand-computed
|
||||
// TestOrderBookOracleSelfConsistent verifies the Go oracle against hand-computed
|
||||
// values, so a CPU-only host still proves the oracle is internally correct
|
||||
// before it is used as the parity reference.
|
||||
func TestCLOBOracleSelfConsistent(t *testing.T) {
|
||||
func TestOrderBookOracleSelfConsistent(t *testing.T) {
|
||||
var a bookArena
|
||||
|
||||
// 1) Rest an ask: 100 @ price 10. No cross (empty book).
|
||||
out := clobMatchStepCPU(&a, 1, u256FromU64(10), u256FromU64(100))
|
||||
out := orderBookMatchStepCPU(&a, 1, u256FromU64(10), u256FromU64(100))
|
||||
if out.numFills != 0 || !u256IsZero(&out.filled) {
|
||||
t.Fatalf("rest ask: expected no fill, got numFills=%d filled=%x", out.numFills, out.filled)
|
||||
}
|
||||
@@ -371,7 +371,7 @@ func TestCLOBOracleSelfConsistent(t *testing.T) {
|
||||
}
|
||||
|
||||
// 2) Incoming bid 60 @ limit 12 crosses the ask. Fill 60 @ 10.
|
||||
out = clobMatchStepCPU(&a, 0, u256FromU64(12), u256FromU64(60))
|
||||
out = orderBookMatchStepCPU(&a, 0, u256FromU64(12), u256FromU64(60))
|
||||
if out.numFills != 1 {
|
||||
t.Fatalf("cross bid: numFills=%d, want 1", out.numFills)
|
||||
}
|
||||
@@ -392,7 +392,7 @@ func TestCLOBOracleSelfConsistent(t *testing.T) {
|
||||
|
||||
// 3) Big bid 200 @ limit 15: consumes the 40 remaining ask, residual 160
|
||||
// rests as a bid. VWAP = 40*10/40 = 10.
|
||||
out = clobMatchStepCPU(&a, 0, u256FromU64(15), u256FromU64(200))
|
||||
out = orderBookMatchStepCPU(&a, 0, u256FromU64(15), u256FromU64(200))
|
||||
if out.numFills != 1 {
|
||||
t.Fatalf("drain ask: numFills=%d, want 1", out.numFills)
|
||||
}
|
||||
@@ -414,8 +414,8 @@ func TestCLOBOracleSelfConsistent(t *testing.T) {
|
||||
// paritySequence is a fixed multi-order book that exercises: resting both
|
||||
// sides, partial fills, full-level drain (front-pop), multi-level sweep, and
|
||||
// non-trivial VWAP (averaging across two price levels).
|
||||
func paritySequence() []clobStep {
|
||||
return []clobStep{
|
||||
func paritySequence() []orderBookStep {
|
||||
return []orderBookStep{
|
||||
{"rest ask 100@10", 1, 10, 100},
|
||||
{"rest ask 50@11", 1, 11, 50},
|
||||
{"rest ask 30@12", 1, 12, 30},
|
||||
@@ -439,7 +439,7 @@ func paritySequence() []clobStep {
|
||||
// device-resident arena, and asserts every 68-byte output is byte-identical
|
||||
// to the Go CPU oracle stepping a parallel host-side arena. Returns the
|
||||
// concatenated GPU outputs so the caller can prove determinism across arenas.
|
||||
func runParity(t *testing.T, seq []clobStep) [][LuxCLOBOutLen]byte {
|
||||
func runParity(t *testing.T, seq []orderBookStep) [][OrderBookOutLen]byte {
|
||||
t.Helper()
|
||||
|
||||
arena, err := ArenaCreate()
|
||||
@@ -458,20 +458,20 @@ func runParity(t *testing.T, seq []clobStep) [][LuxCLOBOutLen]byte {
|
||||
var bookID [32]byte
|
||||
bookID[31] = 1 // arbitrary stable book id
|
||||
|
||||
outs := make([][LuxCLOBOutLen]byte, 0, len(seq))
|
||||
outs := make([][OrderBookOutLen]byte, 0, len(seq))
|
||||
for _, s := range seq {
|
||||
price := u256FromU64(s.price)
|
||||
qty := u256FromU64(s.qty)
|
||||
|
||||
calldata := encodeCLOBCalldata(s.side, price, qty, user, bookID)
|
||||
gpuOut, gpuNF, merr := CLOBMatch(arena, calldata)
|
||||
calldata := encodeOrderBookCalldata(s.side, price, qty, user, bookID)
|
||||
gpuOut, gpuNF, merr := OrderBookMatch(arena, calldata)
|
||||
if merr != nil {
|
||||
t.Fatalf("CLOBMatch[%s]: %v", s.name, merr)
|
||||
t.Fatalf("OrderBookMatch[%s]: %v", s.name, merr)
|
||||
}
|
||||
|
||||
// CPU oracle steps its parallel arena.
|
||||
want := clobMatchStepCPU(&cpu, s.side, price, qty)
|
||||
wantBytes := encodeCLOBOutput(want)
|
||||
want := orderBookMatchStepCPU(&cpu, s.side, price, qty)
|
||||
wantBytes := encodeOrderBookOutput(want)
|
||||
|
||||
if !bytes.Equal(gpuOut[:], wantBytes[:]) {
|
||||
t.Errorf("BYTE PARITY MISMATCH at step %q:\n GPU : %x\n CPU : %x",
|
||||
@@ -489,17 +489,17 @@ func runParity(t *testing.T, seq []clobStep) [][LuxCLOBOutLen]byte {
|
||||
return outs
|
||||
}
|
||||
|
||||
// TestCLOBGPUvsCPUByteParity is the headline proof: the device-resident GPU
|
||||
// TestOrderBookGPUvsCPUByteParity is the headline proof: the device-resident GPU
|
||||
// matcher returns byte-identical output to the canonical CPU oracle for a
|
||||
// fixed multi-order book. Also proves arena lifecycle + determinism by
|
||||
// replaying the same sequence on a second fresh arena and asserting identical
|
||||
// GPU output across both runs.
|
||||
func TestCLOBGPUvsCPUByteParity(t *testing.T) {
|
||||
func TestOrderBookGPUvsCPUByteParity(t *testing.T) {
|
||||
backend := AutoBackend()
|
||||
if backend == GPUBackendNone {
|
||||
t.Skipf("no GPU plugin loaded — set LUX_GPU_PLUGIN_DIR to a dir containing "+
|
||||
"libluxgpu_backend_*.{dylib,so} (e.g. .../metal_backend). probe order: %v. "+
|
||||
"CPU oracle is still self-checked by TestCLOBOracleSelfConsistent.",
|
||||
"CPU oracle is still self-checked by TestOrderBookOracleSelfConsistent.",
|
||||
probeOrder())
|
||||
}
|
||||
t.Logf("GPU backend=%s plugin=%s", backend, GPUPluginPath())
|
||||
@@ -524,10 +524,10 @@ func TestCLOBGPUvsCPUByteParity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCLOBArenaLifecycle exercises create → (no match) → destroy and the
|
||||
// TestOrderBookArenaLifecycle exercises create → (no match) → destroy and the
|
||||
// double-destroy no-op contract, independent of any matching. Skips without
|
||||
// a plugin.
|
||||
func TestCLOBArenaLifecycle(t *testing.T) {
|
||||
func TestOrderBookArenaLifecycle(t *testing.T) {
|
||||
if AutoBackend() == GPUBackendNone {
|
||||
t.Skipf("no GPU plugin loaded — set LUX_GPU_PLUGIN_DIR (probe order: %v)", probeOrder())
|
||||
}
|
||||
@@ -547,8 +547,8 @@ func TestCLOBArenaLifecycle(t *testing.T) {
|
||||
t.Fatalf("ArenaDestroy (second, should be no-op): %v", err)
|
||||
}
|
||||
// Matching against a destroyed arena must error, not crash.
|
||||
calldata := encodeCLOBCalldata(0, u256FromU64(1), u256FromU64(1), [20]byte{}, [32]byte{})
|
||||
if _, _, merr := CLOBMatch(arena, calldata); merr == nil {
|
||||
t.Fatal("CLOBMatch on destroyed arena: expected error, got nil")
|
||||
calldata := encodeOrderBookCalldata(0, u256FromU64(1), u256FromU64(1), [20]byte{}, [32]byte{})
|
||||
if _, _, merr := OrderBookMatch(arena, calldata); merr == nil {
|
||||
t.Fatal("OrderBookMatch on destroyed arena: expected error, got nil")
|
||||
}
|
||||
}
|
||||
@@ -8,19 +8,19 @@
|
||||
//
|
||||
// 1. pure-Go : lx.OrderBook (the canonical Go/CPU reference matcher that
|
||||
// lives in luxfi/dex — the d-chain's matcher)
|
||||
// 2. CPU/C++ : clobMatchStepCPU — the Go port of the canonical C++ oracle
|
||||
// 2. CPU/C++ : orderBookMatchStepCPU — the Go port of the canonical C++ oracle
|
||||
// (dex_cpu_oracle.hpp), uint256, byte-equal to the C++ backend
|
||||
// 3. GPU : CLOBMatch via the loaded plugin (Metal locally), uint256,
|
||||
// 3. GPU : OrderBookMatch via the loaded plugin (Metal locally), uint256,
|
||||
// device-resident BookArena
|
||||
//
|
||||
// The three engines have DIFFERENT data models (lx.OrderBook is float/uint64
|
||||
// and price-level aggregated; the GPU/oracle are uint256 and per-level arenas),
|
||||
// so parity is asserted at the shared, model-independent abstraction every CLOB
|
||||
// so parity is asserted at the shared, model-independent abstraction every OrderBook
|
||||
// must agree on: for each incoming order, the TOTAL FILLED QUANTITY and the
|
||||
// volume-weighted average execution price (VWAP).
|
||||
//
|
||||
// Modes 2 and 3 are already proven byte-identical to each other by
|
||||
// TestCLOBGPUvsCPUByteParity. This test adds the third leg — the lx.OrderBook
|
||||
// TestOrderBookGPUvsCPUByteParity. This test adds the third leg — the lx.OrderBook
|
||||
// pure-Go matcher — and ties all three together.
|
||||
//
|
||||
// Run (Metal, this Mac):
|
||||
@@ -65,7 +65,7 @@ func u256ToU64(t *testing.T, v u256) uint64 {
|
||||
// concept). Two stable users — one per side — keep the models comparable. The
|
||||
// order ID and timestamp are set explicitly (never auto-assigned) so the run is
|
||||
// deterministic and independent of wall-clock time or process-local counters.
|
||||
func goOrderbookStep(ob *lx.OrderBook, s clobStep, seq int64) (filled, vwap uint64) {
|
||||
func goOrderbookStep(ob *lx.OrderBook, s orderBookStep, seq int64) (filled, vwap uint64) {
|
||||
side := lx.Side(s.side) // 0 = bid/buy, 1 = ask/sell
|
||||
user := "bid" // bid maker/taker
|
||||
if side == lx.Sell {
|
||||
@@ -86,7 +86,7 @@ func goOrderbookStep(ob *lx.OrderBook, s clobStep, seq int64) (filled, vwap uint
|
||||
}
|
||||
|
||||
// Capture the trade-log tail BEFORE matching so we slice exactly the trades
|
||||
// this order produced. AddOrder matches on insert (continuous CLOB) and
|
||||
// this order produced. AddOrder matches on insert (continuous OrderBook) and
|
||||
// appends each fill to ob.Trades.
|
||||
start := len(ob.Trades)
|
||||
if ob.AddOrder(order) == 0 && order.Status == lx.Rejected {
|
||||
@@ -123,7 +123,7 @@ func TestThreeModeMatchParity(t *testing.T) {
|
||||
// --- pure-Go engine (the canonical lx.OrderBook matcher) ---
|
||||
// EnableImmediateMatching makes AddOrder cross the incoming order against the
|
||||
// book and rest the residual — the same "match-on-insert, residual rests"
|
||||
// semantic the CPU oracle (clobMatchStepCPU) and the GPU kernel implement.
|
||||
// semantic the CPU oracle (orderBookMatchStepCPU) and the GPU kernel implement.
|
||||
ob := lx.NewOrderBook("PARITY")
|
||||
ob.EnableImmediateMatching = true
|
||||
goOut := make([][2]uint64, len(seq))
|
||||
@@ -136,7 +136,7 @@ func TestThreeModeMatchParity(t *testing.T) {
|
||||
var cpu bookArena
|
||||
cpuOut := make([][2]uint64, len(seq))
|
||||
for i, s := range seq {
|
||||
out := clobMatchStepCPU(&cpu, s.side, u256FromU64(s.price), u256FromU64(s.qty))
|
||||
out := orderBookMatchStepCPU(&cpu, s.side, u256FromU64(s.price), u256FromU64(s.qty))
|
||||
cpuOut[i] = [2]uint64{u256ToU64(t, out.filled), u256ToU64(t, out.avgPrice)}
|
||||
}
|
||||
|
||||
@@ -157,10 +157,10 @@ func TestThreeModeMatchParity(t *testing.T) {
|
||||
bookID[31] = 7
|
||||
gpuOut = make([][2]uint64, len(seq))
|
||||
for i, s := range seq {
|
||||
cd := encodeCLOBCalldata(s.side, u256FromU64(s.price), u256FromU64(s.qty), user, bookID)
|
||||
out, _, merr := CLOBMatch(arena, cd)
|
||||
cd := encodeOrderBookCalldata(s.side, u256FromU64(s.price), u256FromU64(s.qty), user, bookID)
|
||||
out, _, merr := OrderBookMatch(arena, cd)
|
||||
if merr != nil {
|
||||
t.Fatalf("CLOBMatch[%s]: %v", s.name, merr)
|
||||
t.Fatalf("OrderBookMatch[%s]: %v", s.name, merr)
|
||||
}
|
||||
var filled, avg u256
|
||||
copy(filled[:], out[0:32])
|
||||
|
||||
+16
-15
@@ -2,7 +2,7 @@
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package lxgpu is the dlopen bridge to the gpu-kernels DEX plugin and the
|
||||
// byte-parity proof harness for the native CLOB/AMM matchers.
|
||||
// byte-parity proof harness for the native OrderBook/AMM matchers.
|
||||
//
|
||||
// It is the SINGLE home for the GPU dispatch + its KAT (known-answer test)
|
||||
// against the deterministic CPU oracle. The CPU oracle is the byte-parity
|
||||
@@ -68,25 +68,26 @@ var ErrGPUNotAvailable = errors.New("lxgpu: GPU plugin not available")
|
||||
// LuxAmmReservePair mirrors include/lux/gpu/dex.h::LuxAmmReservePair.
|
||||
// 16 bytes, packed (no padding), little-endian uint64 fields.
|
||||
// MUST match:
|
||||
// the GPU plugin install tree include/lux/gpu/dex.h::LuxAmmReservePair
|
||||
// ~/work/lux/dex/pkg/lx/amm.go::ReservePair
|
||||
// the GPU plugin install tree ops/dex/{cuda,metal,wgsl}/amm_xyk*
|
||||
//
|
||||
// the GPU plugin install tree include/lux/gpu/dex.h::LuxAmmReservePair
|
||||
// ~/work/lux/dex/pkg/lx/amm.go::ReservePair
|
||||
// the GPU plugin install tree ops/dex/{cuda,metal,wgsl}/amm_xyk*
|
||||
type LuxAmmReservePair struct {
|
||||
ReserveX uint64
|
||||
ReserveY uint64
|
||||
}
|
||||
|
||||
// CLOB ABI byte sizes from include/lux/gpu/dex.h.
|
||||
// OrderBook ABI byte sizes from include/lux/gpu/dex.h.
|
||||
const (
|
||||
// LuxCLOBCalldataLen is the on-wire calldata for EVM precompile 0x100:
|
||||
// OrderBookCalldataLen is the on-wire calldata for EVM precompile 0x100:
|
||||
// side(1) | price(32 BE) | qty(32 BE) | user(20) | book_id(32) = 117.
|
||||
LuxCLOBCalldataLen = 117
|
||||
// LuxCLOBOutLen is the output: filled(32 BE) | avg_price(32 BE) | num_fills(4 BE) = 68.
|
||||
LuxCLOBOutLen = 68
|
||||
// LuxCLOBMaxLevels caps the per-side resting-level depth (bid_*/ask_* arrays).
|
||||
LuxCLOBMaxLevels = 1024
|
||||
// LuxCLOBBookIDOffset is where book_id starts inside calldata.
|
||||
LuxCLOBBookIDOffset = 85
|
||||
// LuxCLOBBookIDLen is the book_id width.
|
||||
LuxCLOBBookIDLen = 32
|
||||
OrderBookCalldataLen = 117
|
||||
// OrderBookOutLen is the output: filled(32 BE) | avg_price(32 BE) | num_fills(4 BE) = 68.
|
||||
OrderBookOutLen = 68
|
||||
// OrderBookMaxLevels caps the per-side resting-level depth (bid_*/ask_* arrays).
|
||||
OrderBookMaxLevels = 1024
|
||||
// OrderBookBookIDOffset is where book_id starts inside calldata.
|
||||
OrderBookBookIDOffset = 85
|
||||
// OrderBookBookIDLen is the book_id width.
|
||||
OrderBookBookIDLen = 32
|
||||
)
|
||||
|
||||
+1
-1
@@ -241,7 +241,7 @@ var errRecover = errors.New("rfq: secp256k1 recovery failed")
|
||||
// over the SAME crypto.Ecrecover primitive the order path (pkg/lx/signed_order.go)
|
||||
// and the D-Chain tx gate (pkg/dchain/auth.go) use. A venue message is its own
|
||||
// domain (the domain* tags), so this recover is a leaf helper rather than a borrow
|
||||
// of pkg/lx's tx-auth wrapper — which would couple this pure relay to the GPU CLOB
|
||||
// of pkg/lx's tx-auth wrapper — which would couple this pure relay to the GPU DEX
|
||||
// matcher. secp256k1 is the only scheme venue messages use.
|
||||
func recoverAddr(digest [32]byte, sig []byte) (common.Address, error) {
|
||||
if len(sig) != 65 || sig[64] > 1 { // ecrecover accepts only v ∈ {0,1}
|
||||
|
||||
@@ -184,7 +184,7 @@ func (api *TradingAPI) isVenueExecutable(name string) bool {
|
||||
}
|
||||
|
||||
func isV4Venue(name string) bool {
|
||||
return name == VenueTypeV4Native || name == "v4_native_clob" || strings.HasPrefix(name, "lux_")
|
||||
return name == VenueTypeV4Native || name == "v4_native_dex" || strings.HasPrefix(name, "lux_")
|
||||
}
|
||||
|
||||
func isV2Venue(name string) bool {
|
||||
|
||||
@@ -1165,7 +1165,7 @@ func TestVenueTypeClassification(t *testing.T) {
|
||||
isV4 bool
|
||||
}{
|
||||
{"v4_native", false, false, true},
|
||||
{"v4_native_clob", false, false, true},
|
||||
{"v4_native_dex", false, false, true},
|
||||
{"lux_amm", false, false, true},
|
||||
{"uniswap_v2", true, false, false},
|
||||
{"sushiswap", true, false, false},
|
||||
|
||||
+25
-25
@@ -6,29 +6,29 @@ import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// NativeCLOB is the V4 CLOB precompile address.
|
||||
const NativeCLOB = "0x0000000000000000000000000000000000009020"
|
||||
// NativeOrderBook is the V4 DEX precompile address.
|
||||
const NativeOrderBook = "0x0000000000000000000000000000000000009020"
|
||||
|
||||
// quoteExactInputSingle(address,address,uint256,uint24)
|
||||
var selectorNativeQuote = mustDecodeHex("30d07f21")
|
||||
|
||||
// getBestQuote(address,address,uint256,bool)
|
||||
var selectorCLOBQuote = mustDecodeHex("a2e62045")
|
||||
var selectorDEXQuote = mustDecodeHex("a2e62045")
|
||||
|
||||
// NativeDEXVenue quotes from the Lux V4 DEX precompiles via eth_call.
|
||||
// Calls PoolManager (0x9010) for AMM quotes and optionally CLOB (0x9020).
|
||||
// Calls PoolManager (0x9010) for AMM quotes and optionally DEX (0x9020).
|
||||
type NativeDEXVenue struct {
|
||||
evm *EVMClient
|
||||
name string
|
||||
feeBPS uint32
|
||||
useCLOB bool
|
||||
evm *EVMClient
|
||||
name string
|
||||
feeBPS uint32
|
||||
useDEX bool
|
||||
}
|
||||
|
||||
// NativeDEXConfig configures a NativeDEXVenue.
|
||||
type NativeDEXConfig struct {
|
||||
RPCURL string
|
||||
FeeBPS uint32
|
||||
UseCLOB bool
|
||||
RPCURL string
|
||||
FeeBPS uint32
|
||||
UseDEX bool
|
||||
}
|
||||
|
||||
func NewNativeDEXVenue(cfg NativeDEXConfig) *NativeDEXVenue {
|
||||
@@ -37,10 +37,10 @@ func NewNativeDEXVenue(cfg NativeDEXConfig) *NativeDEXVenue {
|
||||
feeBPS = 30
|
||||
}
|
||||
return &NativeDEXVenue{
|
||||
evm: NewEVMClient(cfg.RPCURL),
|
||||
name: "v4_native",
|
||||
feeBPS: feeBPS,
|
||||
useCLOB: cfg.UseCLOB,
|
||||
evm: NewEVMClient(cfg.RPCURL),
|
||||
name: "v4_native",
|
||||
feeBPS: feeBPS,
|
||||
useDEX: cfg.UseDEX,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,12 +90,12 @@ func (v *NativeDEXVenue) Quote(ctx context.Context, req QuoteRequest) (*VenueQuo
|
||||
Executable: true,
|
||||
}
|
||||
|
||||
if v.useCLOB {
|
||||
clobQuote, err := v.queryCLOB(ctx, req, amountIn)
|
||||
if err == nil && clobQuote != nil {
|
||||
clobOut, _ := new(big.Int).SetString(clobQuote.AmountOut, 10)
|
||||
if clobOut != nil && clobOut.Cmp(amountOut) > 0 {
|
||||
return clobQuote, nil
|
||||
if v.useDEX {
|
||||
dexQuote, err := v.queryDEX(ctx, req, amountIn)
|
||||
if err == nil && dexQuote != nil {
|
||||
dexOut, _ := new(big.Int).SetString(dexQuote.AmountOut, 10)
|
||||
if dexOut != nil && dexOut.Cmp(amountOut) > 0 {
|
||||
return dexQuote, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func (v *NativeDEXVenue) Quote(ctx context.Context, req QuoteRequest) (*VenueQuo
|
||||
return quote, nil
|
||||
}
|
||||
|
||||
func (v *NativeDEXVenue) queryCLOB(ctx context.Context, req QuoteRequest, amountIn *big.Int) (*VenueQuote, error) {
|
||||
func (v *NativeDEXVenue) queryDEX(ctx context.Context, req QuoteRequest, amountIn *big.Int) (*VenueQuote, error) {
|
||||
inAddr, _ := decodeAddress(req.TokenIn)
|
||||
outAddr, _ := decodeAddress(req.TokenOut)
|
||||
|
||||
@@ -111,13 +111,13 @@ func (v *NativeDEXVenue) queryCLOB(ctx context.Context, req QuoteRequest, amount
|
||||
buyWord[31] = 1
|
||||
|
||||
calldata := make([]byte, 0, 132)
|
||||
calldata = append(calldata, selectorCLOBQuote...)
|
||||
calldata = append(calldata, selectorDEXQuote...)
|
||||
calldata = append(calldata, padAddress(inAddr)...)
|
||||
calldata = append(calldata, padAddress(outAddr)...)
|
||||
calldata = append(calldata, padUint256(amountIn)...)
|
||||
calldata = append(calldata, buyWord[:]...)
|
||||
|
||||
result, err := v.evm.CallContract(ctx, NativeCLOB, calldata)
|
||||
result, err := v.evm.CallContract(ctx, NativeOrderBook, calldata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -131,7 +131,7 @@ func (v *NativeDEXVenue) queryCLOB(ctx context.Context, req QuoteRequest, amount
|
||||
}
|
||||
|
||||
return &VenueQuote{
|
||||
Venue: v.name + "_clob",
|
||||
Venue: v.name + "_dex",
|
||||
AmountOut: amountOut.String(),
|
||||
Fee: "0",
|
||||
GasEstimate: "200000",
|
||||
|
||||
+19
-19
@@ -2,14 +2,14 @@
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package venue is the protocol-neutral seam between a wire front-end and THE
|
||||
// matcher. It defines ONE interface — the four operations of the FROZEN clob_*
|
||||
// matcher. It defines ONE interface — the four operations of the FROZEN dex_*
|
||||
// ZAP surface (pkg/zapwire) — and ONE production implementation that dials the
|
||||
// venue's clob_* endpoint. Every protocol front-end (FIX in pkg/fix, the WS
|
||||
// venue's dex_* endpoint. Every protocol front-end (FIX in pkg/fix, the WS
|
||||
// venue handler in pkg/api, the 0x9010 precompile in lux/precompile/dex)
|
||||
// translates its dialect into these calls and renders the consensus-computed
|
||||
// result back; none of them owns a matcher. This is the decomplection: the
|
||||
// matcher and book live in ONE place (pkg/dchain), reached over ONE surface
|
||||
// (clob_*), and a "path" is purely a wire translation onto this Venue.
|
||||
// (dex_*), and a "path" is purely a wire translation onto this Venue.
|
||||
package venue
|
||||
|
||||
import (
|
||||
@@ -21,17 +21,17 @@ import (
|
||||
)
|
||||
|
||||
// Venue is the protocol-neutral seam between a wire front-end (FIX in pkg/fix, WS
|
||||
// in pkg/api) and THE matcher. It is the SAME four operations the FROZEN clob_*
|
||||
// in pkg/api) and THE matcher. It is the SAME four operations the FROZEN dex_*
|
||||
// ZAP surface exposes (pkg/zapwire) — ensure-market, place a resting limit order,
|
||||
// cancel a resting order, submit a marketable order and get its fills. A front-end
|
||||
// translates its protocol into these calls; it does NOT match. There is exactly
|
||||
// ONE matcher (the D-Chain CLOB) and exactly ONE network surface onto it (clob_*);
|
||||
// ONE matcher (the D-Chain DEX) and exactly ONE network surface onto it (dex_*);
|
||||
// Venue is that surface expressed as a Go interface so a front-end can be unit
|
||||
// tested against a fake and run in production against the real ZAP client.
|
||||
//
|
||||
// Side/poolID semantics are the zapwire ones: side 0=buy, 1=sell; poolID is the
|
||||
// 32-byte market identity. price/size are float64 on the price grid (the same
|
||||
// units clob_place/clob_submit carry); user is the <=16-byte account handle whose
|
||||
// units dex_place/dex_submit carry); user is the <=16-byte account handle whose
|
||||
// ledger the order locks against and settles into.
|
||||
type Venue interface {
|
||||
// EnsureMarket idempotently creates the book for poolID.
|
||||
@@ -49,17 +49,17 @@ type Venue interface {
|
||||
// the matcher would not rest, or a cancel of an unknown order).
|
||||
var ErrRejected = fmt.Errorf("venue: rejected")
|
||||
|
||||
// zapVenue is the production Venue: a clob_* ZAP client over github.com/luxfi/rpc,
|
||||
// dialing the SAME rpc.Listen socket dchain.VM.RegisterCLOB serves and the SAME
|
||||
// zapVenue is the production Venue: a dex_* ZAP client over github.com/luxfi/rpc,
|
||||
// dialing the SAME rpc.Listen socket dchain.VM.RegisterDEX serves and the SAME
|
||||
// socket the 0x9010 precompile (engine_zap) and the chains/dexvm proxy dial. It
|
||||
// holds no matcher state — every call is a frozen zapwire frame to the venue.
|
||||
type zapVenue struct {
|
||||
conn *rpc.ZAPConn
|
||||
}
|
||||
|
||||
// DialVenue opens a ZAP client to the venue's clob_* endpoint (e.g. the addr
|
||||
// DialVenue opens a ZAP client to the venue's dex_* endpoint (e.g. the addr
|
||||
// returned by rpc.Listen / the node runtime). The returned Venue speaks the
|
||||
// frozen clob_* frames; Close releases the connection.
|
||||
// frozen dex_* frames; Close releases the connection.
|
||||
func DialVenue(ctx context.Context, addr string) (Venue, error) {
|
||||
conn, err := rpc.ZAPDial(ctx, addr)
|
||||
if err != nil {
|
||||
@@ -78,10 +78,10 @@ func (v *zapVenue) Close() error { return v.conn.Close() }
|
||||
func (v *zapVenue) EnsureMarket(ctx context.Context, poolID [32]byte) error {
|
||||
resp, err := v.conn.Call(ctx, zapwire.MethodEnsureMarket, zapwire.EncodeEnsureMarket(poolID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("fix: clob_ensure_market: %w", err)
|
||||
return fmt.Errorf("fix: dex_ensure_market: %w", err)
|
||||
}
|
||||
if _, status, derr := zapwire.DecodeAck(resp); derr != nil {
|
||||
return fmt.Errorf("fix: clob_ensure_market ack: %w", derr)
|
||||
return fmt.Errorf("fix: dex_ensure_market ack: %w", derr)
|
||||
} else if status == zapwire.StatusRejected {
|
||||
return fmt.Errorf("%w: ensure_market", ErrRejected)
|
||||
}
|
||||
@@ -91,11 +91,11 @@ func (v *zapVenue) EnsureMarket(ctx context.Context, poolID [32]byte) error {
|
||||
func (v *zapVenue) Place(ctx context.Context, poolID [32]byte, side uint8, price, size float64, user string) (uint64, error) {
|
||||
resp, err := v.conn.Call(ctx, zapwire.MethodPlace, zapwire.EncodePlace(poolID, side, price, size, user))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("fix: clob_place: %w", err)
|
||||
return 0, fmt.Errorf("fix: dex_place: %w", err)
|
||||
}
|
||||
orderID, status, derr := zapwire.DecodeAck(resp)
|
||||
if derr != nil {
|
||||
return 0, fmt.Errorf("fix: clob_place ack: %w", derr)
|
||||
return 0, fmt.Errorf("fix: dex_place ack: %w", derr)
|
||||
}
|
||||
if status == zapwire.StatusRejected {
|
||||
return 0, fmt.Errorf("%w: place", ErrRejected)
|
||||
@@ -106,11 +106,11 @@ func (v *zapVenue) Place(ctx context.Context, poolID [32]byte, side uint8, price
|
||||
func (v *zapVenue) Cancel(ctx context.Context, poolID [32]byte, orderID uint64) error {
|
||||
resp, err := v.conn.Call(ctx, zapwire.MethodCancel, zapwire.EncodeCancel(poolID, orderID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("fix: clob_cancel: %w", err)
|
||||
return fmt.Errorf("fix: dex_cancel: %w", err)
|
||||
}
|
||||
_, status, derr := zapwire.DecodeAck(resp)
|
||||
if derr != nil {
|
||||
return fmt.Errorf("fix: clob_cancel ack: %w", derr)
|
||||
return fmt.Errorf("fix: dex_cancel ack: %w", derr)
|
||||
}
|
||||
if status != zapwire.StatusCanceled {
|
||||
return fmt.Errorf("%w: cancel order %d", ErrRejected, orderID)
|
||||
@@ -121,11 +121,11 @@ func (v *zapVenue) Cancel(ctx context.Context, poolID [32]byte, orderID uint64)
|
||||
func (v *zapVenue) Submit(ctx context.Context, poolID [32]byte, side uint8, isMarket bool, limit, size float64, user string) ([]zapwire.Fill, error) {
|
||||
resp, err := v.conn.Call(ctx, zapwire.MethodSubmit, zapwire.EncodeSubmit(poolID, side, isMarket, limit, size, user))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fix: clob_submit: %w", err)
|
||||
return nil, fmt.Errorf("fix: dex_submit: %w", err)
|
||||
}
|
||||
fills, err := zapwire.DecodeFills(resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fix: clob_submit fills decode: %w", err)
|
||||
return nil, fmt.Errorf("fix: dex_submit fills decode: %w", err)
|
||||
}
|
||||
return fills, nil
|
||||
}
|
||||
@@ -147,7 +147,7 @@ func SymbolToPoolID(symbol string) [32]byte {
|
||||
// UserHandle truncates a sender/account identity to the <=16-byte on-wire user
|
||||
// handle the venue ledger keys balances by. Two distinct senders that share a
|
||||
// 16-byte prefix would collide; production binds a checked account id, but for the
|
||||
// CLOB user field the 16-byte handle is the canonical width (zapwire.UserSize).
|
||||
// DEX user field the 16-byte handle is the canonical width (zapwire.UserSize).
|
||||
func UserHandle(s string) string {
|
||||
if len(s) > zapwire.UserSize {
|
||||
return s[:zapwire.UserSize]
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
"github.com/luxfi/rpc"
|
||||
)
|
||||
|
||||
// fakeCLOB stands up a real rpc.Listen server with canned clob_* handlers, so the
|
||||
// fakeDEX stands up a real rpc.Listen server with canned dex_* handlers, so the
|
||||
// production zapVenue is exercised over a REAL ZAP socket (the same transport
|
||||
// production uses) without standing up the cgo matcher. It proves zapVenue's frame
|
||||
// encode + ack/fill decode + error mapping, end to end over the wire.
|
||||
type fakeCLOB struct {
|
||||
type fakeDEX struct {
|
||||
server rpc.Server
|
||||
cancel context.CancelFunc
|
||||
placeID uint64
|
||||
@@ -25,13 +25,13 @@ type fakeCLOB struct {
|
||||
cancelOK bool
|
||||
}
|
||||
|
||||
func startFakeCLOB(t *testing.T) (*fakeCLOB, string) {
|
||||
func startFakeDEX(t *testing.T) (*fakeDEX, string) {
|
||||
t.Helper()
|
||||
srv, err := rpc.Listen("127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("rpc.Listen: %v", err)
|
||||
}
|
||||
f := &fakeCLOB{server: srv, placeID: 42, cancelOK: true}
|
||||
f := &fakeDEX{server: srv, placeID: 42, cancelOK: true}
|
||||
|
||||
must := func(method string, h rpc.RawHandler) {
|
||||
if err := srv.RegisterRaw(method, h); err != nil {
|
||||
@@ -62,7 +62,7 @@ func startFakeCLOB(t *testing.T) (*fakeCLOB, string) {
|
||||
}
|
||||
|
||||
func TestZAPVenueRoundTrip(t *testing.T) {
|
||||
f, addr := startFakeCLOB(t)
|
||||
f, addr := startFakeDEX(t)
|
||||
f.fills = []zapwire.Fill{{Price: 100, Size: 3, TakerSide: zapwire.SideBuy}}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
@@ -100,7 +100,7 @@ func TestZAPVenueRoundTrip(t *testing.T) {
|
||||
// TestZAPVenueCancelRejectedMapsToErrRejected proves a non-canceled cancel ack is
|
||||
// surfaced as ErrRejected, not a silent success.
|
||||
func TestZAPVenueCancelRejectedMapsToErrRejected(t *testing.T) {
|
||||
f, addr := startFakeCLOB(t)
|
||||
f, addr := startFakeDEX(t)
|
||||
f.cancelOK = false
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user