mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
Three things claimed to be this repo's trading API and no two agreed. pkg/trading carried its own Venue interface, its own V4/native/V2/V3 and broker venues, its own constant-product math and its own eth_call client — a copy of what pkg/gateway already had — and nothing mounted it: grep for the import outside that directory returns empty. api/openapi.yaml documented it anyway, so the one published contract in the repo described the one package no binary served. Meanwhile the binary answered at /v1/* and again at /trading/* in Uniswap's shapes: two contracts for one API, reachable by both names, free to drift. Deleted, both of them, rather than picked between and left beside. Everything answers under /v1/trade now. api.lux.cloud carries the chain at /v1/chain, the C-chain at the root and other services besides, so /v1/quote was a generic name claimed on a shared host; one prefix is one namespace with one owner, and the edge rule that publishes it collapses from a nine-clause allowlist to a prefix. Server.routes() is the surface written once, and TestTheDocumentIsTheSurface reads api/openapi.yaml and holds the two to each other in both directions. /v1/trade/venues is new. It is the question a screen asks before it asks anything else — which chains this deployment can price, and from what — grouped by chain, because a venue is a contract on a chain and the same name means a different pool on each. Without it a client learns a chain is unquotable by quoting on it and reading an error. The allowance is read rather than assumed. approval/check and permit2/check answered allowance 0 and needsApproval on every request, under a comment saying the gateway had no way to reach a chain; it has one per chain, one field away — the same endpoint the pool is quoted from. Measured against 96369: LUSD to the position manager reads 6000000000000000000000000, which is what eth_call returns for that pair, and the approve tx now rides along only when it is needed. Handed back beside "you already approved" it is a transaction somebody sends, and the user pays gas for a state change that changes nothing before every swap. Permit2 reads the expiration too, because a grant that has run out is not a grant. Nothing here invents a number any more. /v1/history/prices and /v1/history/tvl returned a random walk seeded from hardcoded base prices — LUX at 2.47, WBTC at 96420, 12.5M of TVL on a chain whose pools were seeded hours ago — and both were routed to the public internet, where a chart drawn from them is indistinguishable from a chart of the market. History belongs to the indexer, which reads the chain. /v1/tokens/search answered 501 on a TODO, a route lying about the surface in the other direction. Gone, with a test that keeps them gone. Co-authored-by: Hanzo Dev <[email protected]>
79 lines
2.1 KiB
Go
79 lines
2.1 KiB
Go
package gateway
|
|
|
|
import (
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The document and the binary describe one surface, so they are held to each
|
|
// other rather than to a reader's memory.
|
|
//
|
|
// api/openapi.yaml used to describe /v1/trade/* paths that no binary served,
|
|
// while the binary served /v1/* and again /trading/*, and the three drifted
|
|
// for as long as nothing compared them. A route added in code and not written
|
|
// down is a route no client can be told about; a path written down and not
|
|
// served is a promise the binary breaks the first time somebody believes it.
|
|
func TestTheDocumentIsTheSurface(t *testing.T) {
|
|
doc, err := os.ReadFile("../../api/openapi.yaml")
|
|
if err != nil {
|
|
t.Fatalf("the published contract: %v", err)
|
|
}
|
|
|
|
// A path key at the top level of `paths:`, which is the only thing indented
|
|
// exactly two spaces and starting with a slash.
|
|
key := regexp.MustCompile(`(?m)^ (/\S*):$`)
|
|
|
|
written := map[string]bool{}
|
|
for _, m := range key.FindAllStringSubmatch(string(doc), -1) {
|
|
path := m[1]
|
|
if !strings.HasPrefix(path, tradePrefix) {
|
|
t.Errorf("%s is documented outside %s", path, tradePrefix)
|
|
continue
|
|
}
|
|
// /v1/trade/pool/{chainId}/{address} is served as the subtree
|
|
// /v1/trade/pool/ — everything after the first placeholder is read
|
|
// out of the path by the handler.
|
|
route := strings.TrimPrefix(path, tradePrefix)
|
|
if i := strings.Index(route, "{"); i >= 0 {
|
|
route = route[:i]
|
|
}
|
|
written[route] = true
|
|
}
|
|
|
|
served := map[string]bool{}
|
|
for route := range (&Server{}).routes() {
|
|
served[route] = true
|
|
}
|
|
|
|
for route := range served {
|
|
if !written[route] {
|
|
t.Errorf("%s%s is served and not in api/openapi.yaml", tradePrefix, route)
|
|
}
|
|
}
|
|
for route := range written {
|
|
if !served[route] {
|
|
t.Errorf("%s%s is in api/openapi.yaml and not served", tradePrefix, route)
|
|
}
|
|
}
|
|
|
|
if len(served) == 0 {
|
|
t.Fatal("no routes")
|
|
}
|
|
if t.Failed() {
|
|
t.Logf("served: %s", sorted(served))
|
|
t.Logf("written: %s", sorted(written))
|
|
}
|
|
}
|
|
|
|
func sorted(set map[string]bool) []string {
|
|
out := make([]string, 0, len(set))
|
|
for k := range set {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|