Helm values, the staging and production manifests, and the operations
guide name the image .github/workflows/docker.yml publishes, so a
rollout pulls and a copied `docker pull` resolves.
deploy.sh deploys that published image; CI owns the multi-arch build.
The image table lists the tag the Dockerfile produces.
A chain's pools say what a pair trades at and nothing about what is listed on
it: an AMM has no list. Our own chains have an indexer that keeps one, and
every other chain had nothing — so an interface switched to Ethereum asked for
nothing at all and drew an empty table, which is exactly what a reader has been
looking at. The hosted API would answer and refuses without a contract.
CoinGecko publishes both halves openly: a per-chain token list with names,
decimals and logos, and a price by contract address with the day's change and
volume. No key, no account; a key only raises the rate limit. Measured on the
open endpoints: 5,821 tokens on Ethereum, 4,214 on BNB, 2,679 on Base, every
one with a logo.
The lists are large and change slowly, so they are held for six hours; a price
is a reading of a market and is held for a minute. Both are held in the gateway
rather than in a browser, so a thousand readers are one request to a service
that asks nothing of us. A token it does not price is absent from the answer
rather than present at zero — a screen can draw a dash, and it cannot un-draw a
wrong number. One batch refused is not every price unknown.
Measured through the running binary: /v1/trade/tokens?chainId=1 answers WETH,
USDC, 1INCH, AAVE with logos; a search for USDC returns USD Coin alone; and the
price still comes from our own pool read — WETH at 2474.5369 via uniswap_v3
against USDC — with this provider supplying only what the pool cannot.
Co-authored-by: Hanzo Dev <[email protected]>
Nothing could be read, so it answered that the chain could not be reached. Under
load that is this deployment running out of its own time budget with the chain
answering normally the whole while, and the two send a reader to different
places. A timeout says so and names what to do about it.
Co-authored-by: Hanzo Dev <[email protected]>
The answer is two readings compared, so one of them failing is the chain
failing. It was being treated as a pair no pool holds, which is remembered for a
minute where a chain that could not be read is asked again in ten seconds.
Co-authored-by: Hanzo Dev <[email protected]>
A price is a fan-out. The edge limits requests — thirty a second from one
address — and each may name fifty tokens, read at two sizes against two
numéraires across every arm. Nothing bounded what came out the other side, and
what comes out lands on public endpoints we hold no account with; being refused
there takes quoting down for everybody, on every chain.
So the budget is sixteen questions in flight against one chain for the whole
process, rather than eight per request multiplied by however many requests
arrive. It is taken around a single reading and not around a whole price,
because a price against the hub waits for the hub's own price, and a slot held
across that wait deadlocks the first time every slot is held by something
waiting on the hub.
Measured: one request of 50 cold Ethereum tokens went from 10.1s to 8.6s, and
four chains of 40 at once from 10.7s to 9.3s — and returned the same counts on
a repeat, where the same burst before this returned a different set of prices
each time.
Co-authored-by: Hanzo Dev <[email protected]>
The first cut read each market once, at a hundred dollars. That is right for a
pool with depth and nonsense for one without: a hundred dollars into an
abandoned pool buys a rounding error, and the implied price of a rounding error
is enormous. Measured over the fifty tokens Ethereum lists first, half of what
came back was more than one percent off an independent source and AUCTION — a
token worth $3.44 — read $889,434,176.
So the pools are asked twice, at ten dollars and at a hundred, and a price that
does not survive ten times the size is not returned at all. Twenty-five of the
twenty-seven that now come back agree with that source within two percent. The
two that do not are real pools here sitting off the wider market, and that is
this venue's price: /v1/trade/quote for the same size implies $0.036806 for ASM
where /v1/trade/price says $0.036834, so the screen and the swap panel agree,
which is the thing that has to be true.
The gate is on the best reading at each size, not on each numéraire separately.
Gating per numéraire refuses the cheapest route for being thin and then quotes a
dearer one that was not — ANT came back at $3.72 against a market of $0.07 that
way, because the route that knew better had been thrown out.
Both sizes are asked at once, so the second one costs calls and not latency:
fifty cold tokens on Ethereum in 10.1s, the same fifty warm in 36ms.
Co-authored-by: Hanzo Dev <[email protected]>
A second account of the trading surface sat further down the file and
contradicted the first: pools and order among the routes, a provider registry
where Uniswap is a fallback on every chain rather than something registered only
when a key exists, a native provider of stubs against a precompile at 0x0400,
and a future-work list asking for the quote and token-list caching that is now
in the binary. A document that disagrees with itself is worse than a shorter
one. The account at the top is the one held to the route table by a test.
Co-authored-by: Hanzo Dev <[email protected]>
The limit defaulted to a page size, and Ethereum lists 403. A client that did
not know to ask for more got two thirds of the chain with nothing to say so.
There is no paging here to need one: the largest chain lists 403 and all seven
together list 935, so the limit is a ceiling on a request and the default is
everything.
And a chain this deployment cannot quote now answers the way /v1/trade/venues
already answers about it, rather than with an empty list a screen would draw as
a market with nothing in it.
Co-authored-by: Hanzo Dev <[email protected]>
Two things the first cut got the wrong way round.
Search ranked a name starting with what was typed above a symbol containing it,
so "eth" reached Ethena and Ethereum Name Service before WETH. People type
tickers at a market screen. The rule is now one sentence — the symbol beats the
name, and within each an exact match beats a start beats a mention.
And a reading abandoned because the caller went away was still published to
whoever was waiting on it, and kept for as long as an unreadable chain. It is
neither: the answer and the entry now agree, and nothing is kept.
Co-authored-by: Hanzo Dev <[email protected]>
Explore on Ethereum drew nothing at all — not an empty table, no request made
— because enumerating a chain went to an indexer that only carries ours, and
the hosted API that would answer costs a contract. Two paths close that without
asking anybody for one.
/v1/trade/tokens serves a list the binary carries: 935 tokens over the seven
chains this gateway prices, identity only, checked at build time rather than
per request. No logo, and the struct has nowhere to put one — every public list
gives logos as URLs on four hosts belonging to somebody else, fetched by every
viewer's browser on every market screen.
/v1/trade/price is a reading of the same pools /v1/trade/quote reads: spend a
fixed hundred dollars of a numéraire, see what comes back, put the pool's fee
back in. Not one whole token — that is a fifty-cent trade in one market and a
hundred-thousand-dollar one in the next, and the second moves the pool it is
measuring. Not the raw quote either: a pool hands back the mid less its tier,
which is $1.0031 for a $1.00 token and $3018.51 for a $3000 one two hops deep.
Two numéraires per chain, the dollar and the wrapped native token, and the
cheaper reading wins. Measured against an independent source on seven chains,
every price inside half a percent.
A token no pool holds comes back WITHOUT priceUSD, never with a zero: a screen
can draw a dash and it cannot un-draw a wrong number. A reading stands for a
minute; an unreadable chain for ten seconds, because the pools have not changed
— our access to them has. Fifty per request, eight in flight, and thirty-two
readers of one cold price produce one fan-out.
Two things this needed that were wrong on their own account. A fee had two
units — the V2 arm answered 30 where the V3 arm answered 3000 for the same
three tenths of a percent — so feeFromBPS converts once and nothing else means
anything else by that number. And the arm reading our own precompiles answered
"no liquidity" to an endpoint that never replied, which is enough on its own to
report a chain nobody could reach as a pair nobody holds.
Co-authored-by: Hanzo Dev <[email protected]>
Both came out as an empty list, so WETH/USDC on Ethereum — one of the deepest
pools in existence — was reported as "no venue here holds this pair" whenever
its RPC was down. That sends a reader looking for liquidity instead of at their
endpoint, which is the same mistake as blaming the pair for an exact-output
request and blaming an upstream registry for our own chain.
The venues already distinguished the two answers: no liquidity is (nil, nil), a
chain they could not reach is (nil, err). QueryAllVenues discarded both. It now
returns the reason, and only when EVERY arm it asked failed — one arm down
while another answers is a quote, not an outage. It also stops returning the
routing label, which named V4_NATIVE for a V3 pool on somebody else's chain and
was read by one test assertion and nothing else.
Underneath, EVMClient marks the answers it never received: a connection
failure, a 5xx, a 429, a body that is not JSON-RPC. A contract answering by
REVERTING stays what it always was — this pool does not exist — because three
of the four V3 fee tiers revert on most pairs and that is the ordinary case.
Measured with chain 1 pointed at a closed port: chain 1 answers 502 naming the
dial error, 96369 still quotes 997001483075056712, a pair with no pool still
answers 404, and /providers stays 200 so one dark chain does not take a pod out
of its own Service.
Co-authored-by: Hanzo Dev <[email protected]>
The v4_native arm is configured on 96369 and silent: 0x9010 answers 0x there,
measured with the venue's own quoter selector, so every price on that chain
today comes from uniswap_v3 reading the QuoterV2 the venue deployment put
there. `native: true` says the arm is attached, not that it answered.
Also the addresses from luxfi/standard deployments/venue/96369.json, the reason
its `Router` key belongs to LUX_V3_ROUTER and never LUX_V2_ROUTER, and one
measured price per configured chain so a later reader can tell a broken arm
from a moved market.
Co-authored-by: Hanzo Dev <[email protected]>
lxrPadUint256 wrote a number wider than 32 bytes as b[:32] — its LEADING bytes,
not its low ones — so a value one bit over came out as a completely unrelated
quantity in the field that says how much to spend. The quote accepted any
string and the swap checked only the sign, so nothing stopped one arriving.
Two changes, and the boundary one is the real fix. wholeUnits is one parser for
every path that takes an amount: a whole number of a token's smallest unit, in
decimal, that fits in a uint256 — refused at the edge, where the sentence can
say what is wrong with it. And the encoder now keeps the LOW bytes, which is
what the EVM does to a uint256, so the failure mode of anything that slips past
is truncation rather than a different number entirely.
Co-authored-by: Hanzo Dev <[email protected]>
tail() read the id out of a subtree route and no route is a subtree any more;
lxrDecodeBytes32 and lxrPadBytes served the V4 router builders that went with
the precompile answering 0x. staticcheck is clean on pkg/gateway and
cmd/gateway.
Co-authored-by: Hanzo Dev <[email protected]>
SwapRouter02 dropped the deadline from exactInputSingle's tuple and takes it
through multicall(uint256,bytes[]) instead, which is how Uniswap's own
interface sends one. Without that wrap a deadline handed to /v1/trade/swap was
accepted and silently dropped on every V3 route while the V2 route honoured it
— one field meaning two things depending on which pool happened to win the
quote.
amountOutMinimum bounds the price; the deadline bounds the time. A signed swap
that sits in a mempool and lands an hour later still passes its floor, at a
price nobody would choose now. Unset means twenty minutes out, because zero is
a deadline in 1970 and every swap would revert as too old.
Measured against 96369's live SwapRouter02, both directions:
deadline ahead of block.timestamp -> 997001483075056712, the quoted amount
deadline behind it -> execution reverted: Transaction too old
Worth knowing for anything that tests this: 96369 seals a block when there is a
transaction, so block.timestamp ran 4709 seconds behind wall clock while this
was measured. A deadline computed from wall clock is generous here rather than
tight — the safe direction, but `now - 60` will not read as expired.
Co-authored-by: Hanzo Dev <[email protected]>
Access-Control-Allow-Headers named Authorization and X-API-Key, which nothing
here has ever read — every path is an unauthenticated read of public pools —
and X-Universal-Router-Version and X-Permit2-Disabled, which belonged to the
Uniswap-shaped /trading/* routes that are gone. A permitted header nobody sends
is an invitation to send it. It is Content-Type and X-Request-ID now, which are
the two the binary reads.
The methods dropped PUT and DELETE with the order and position paths that used
them.
The origin stays `*` and covers lux.exchange. There is nothing one origin could
be trusted with here that another could not, and naming one would break the
docs site, a wallet and any partner reading a price, each of them silently. If
a path here ever takes a credential, that is the line that changes first.
Co-authored-by: Hanzo Dev <[email protected]>
The swap looked for the venue that had priced the trade in the per-chain set
alone, while the quote falls back to the single set a one-chain deployment is
handed. Configured the second way, this surface priced a pair and then reported
the venue that had just priced it as unregistered. Both go through Server.arms
now, and Server.arm is that set looked up by the name a venue calls itself.
The swap also rebuilt the quote request by hand beside convertQuoteRequest,
which does the same thing for the quote path — six of seven fields, twice, free
to drift about what slippage or isExactIn mean. swapRequest embeds
quoteRequest; encoding/json flattens it, so the wire shape is unchanged and
there is one conversion.
Co-authored-by: Hanzo Dev <[email protected]>
leastAccepted computed the swap floor in whole basis points, so a tolerance
below 0.01% truncated to zero and amountOutMinimum came back EQUAL to the
quote. A caller asking for very tight protection got none: the router reverts
on the first wei of movement, including the movement their own trade causes.
The arithmetic is in hundredths of a basis point now, which is the unit a V3
fee tier is already stated in. Measured on 96369 against 997001483075056712:
slippage 0.005 gives 996951633000902959, four nines of the quote, where it used
to give the quote itself.
A direction is not a missing market. getAmountsOut and quoteExactInputSingle
price a known input and have no other direction, so an exact-output request
gets nothing back however deep the pool is — and "no venue here holds this
pair" sent someone looking for liquidity sitting right there. It says which of
the two it was, and what to ask instead. Named as the likely cause rather than
asserted, because a deployment carrying an arm that prices both ways reaches
that line only when the pair really is unheld.
A quote that named no pool built a swap against Route[0] of an empty slice.
venueQuotes always writes one hop, so this was reachable only by changing that
function — which is exactly when a panic in the swap path would be found in
production.
Co-authored-by: Hanzo Dev <[email protected]>
/v1/trade/swap asked the hosted provider registry for a price. This deployment
registers none on purpose, so it answered 500 while /v1/trade/quote priced the
same pair from the pools a line away — and then encoded whatever came back for
the PoolManager precompile at 0x9010 regardless of where the price came from. A
router on Ethereum quoted and a precompile on our chain addressed, in one
response. 0x9010 answers 0x on 96369, measured with the venue's own quoter
selector, so the calldata named a contract that is not there.
Venue has a Swap method now. Each one builds against the contract it read: the
V3 arm against the SwapRouter02 beside its quoter, the V2 arm against the same
Router02 that answered getAmountsOut. A venue that prices a market it cannot
settle returns nothing, and the surface says so naming it.
The selector was wrong in the way that spends money. calldata_lxrouter.go sent
04e45aaf under a signature it called ExactInputSingle(address,address,uint256,
uint256,uint160,bytes32); that hashes to f48f227e. 04e45aaf is SwapRouter02's
exactInputSingle((address,address,uint24,address,uint256,uint256,uint160)) — a
seven-field tuple. A router handed the six-field form reads amountIn where the
fee tier belongs and amountOutMinimum where the recipient belongs. Those
builders were reachable only from their own tests; the padding and decoding
survive in abi.go.
Proved end to end: the gateway's own calldata replayed through eth_call against
96369's real SwapRouter02, from the account holding the allowance, returns
997001483075056712 — the number the quote gave. The tier travels on
route[0].fee because a V3 quote asks four tiers and keeps one, and a swap that
does not name the same pool executes at a different price. amountOutMinimum is
99.5% of the quote by default, because a router handed the quoted amount as its
floor reverts on the movement the caller's own trade causes.
Permit2 is at one address everywhere it exists and does not exist everywhere:
9152 bytes on Ethereum, nothing on 96369. Both paths check first, so our chain
gets a sentence instead of calldata addressed to an empty account.
Nine more paths are gone because they could not work here. Five answered 500
"no providers available" — tokens, pools, pool/, positions, stats, price,
prices — because enumerating a chain is an index read and this reads one pool
by address, right now. api-explore.lux.cloud already answers all of it: 16
pools, $39.9M of TVL, real symbols and decimals for 96369. /v1/route is the
same read in a hat and returned {"routes":[]} forever, which a caller reads as
"no path exists" rather than "I cannot answer". The order paths were an
in-memory map behind two replicas, so an order placed on one pod was a 404 on
the other about half the time and every order died on restart — the D-Chain
venue is the order book and it is deployed beside this. The position builders
all addressed 0x9010.
Eight paths remain and all eight were exercised against the live chain.
Co-authored-by: Hanzo Dev <[email protected]>
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]>
The three that cost time putting it behind api.lux.cloud — a single-arch image
in a mixed cluster, runAsNonRoot against a named user, and a health path that
answers 503 about a working gateway — plus why there is no admin surface.
Co-authored-by: Hanzo Dev <[email protected]>
Three things this gateway said were not true, all of them found by putting it
behind api.lux.cloud and asking it the questions a screen asks.
/v1/admin/* paused the DEX, paused a pool and froze one — "IRREVERSIBLE", its
own word — on the strength of `X-User-Role: admin`, a header the caller writes.
Nothing read the state it wrote: PauseState had no caller outside its own file,
so the freeze froze nothing and the pause paused nothing, per replica, until the
process restarted. A switch wired to nothing is worse than no switch, because in
an incident someone pulls it, reads {"status":"paused"}, and stops looking. The
real gate is checkPauseState in the pool manager, on chain, where a market is.
Deleted rather than authenticated: there is nothing here to protect.
/healthz reported the hosted provider registry alone. This deployment registers
no hosted provider on purpose — the trade API answers ACCESS_DENIED without a
contract — so the platform's own health path answered 503 forever about a
gateway quoting seven chains, and a readiness probe on it holds every replica
out of its own Service while every quote works. It reads the venues now, and
names them.
/v1/quotes is /v1/quote's question without the last step, and it was reading a
different source: the registry, so a chain we settle ourselves answered "no
providers available" while /v1/quote priced the same pair from the same pools a
line away. One source now — venueQuotes returns what each venue holds, best
first, and venueQuote is its head.
Co-authored-by: Hanzo Dev <[email protected]>
One toolchain across the estate. The tree carried 1.26.0, .1, .3, .4 and .5
at once, so which compiler built a binary depended on which repo it came
from.
Co-authored-by: Hanzo Dev <[email protected]>
This never built. The workflow lived in .hanzo/workflows, which GitHub does not
read, and the forge does not carry this repository — so a v* tag published
nothing and the newest image on ghcr predates the gateway by two weeks. luxfi's
runners are self-hosted and registered against GitHub; the pool is
lux-build-linux-amd64, which is what every other luxfi image is built on. Same
lane as luxfi/node, and still never a GitHub-hosted builder.
Co-authored-by: Hanzo Dev <[email protected]>
One image, two entrypoints. They share the venues, the types and the router;
building them apart is two versions of one contract waiting to disagree, and
the binary is a few megabytes beside a daemon that already ships.
Co-authored-by: Hanzo Dev <[email protected]>
The hosted API answers ACCESS_DENIED without a contract, which made every
market outside our own chain unquotable and turned a pair with no pool into an
error about somebody else's quota. A pool answers anyone. So the gateway reads
contracts: our chain through the V4 precompiles, with V2 beside them when a
router is deployed, and every other chain through the V2 router and the V3
quoter Uniswap published there. The hosted provider is registered only when a
key is given, because a provider that always refuses is worse than none.
V3 was the missing arm and it is where the liquidity is. The gateway could read
a V2 router and our precompiles and nothing in between, so most markets on most
chains were invisible. The venue asks each of the four fee tiers and keeps the
best — which tier holds a pair is a fact about that pair — and a tier with no
pool reverts, which is the ordinary answer to three of the four rather than a
failure to report.
Venues are per chain now. One router for all of them could not tell which chain
it was answering about, so the shape only ever fit a single-chain deployment.
Each chain names its RPC, its router and its quoter, and any of them can be
overridden by name.
Quotes are cached for ten seconds. A pool moves when somebody trades in it, so
the window is short by nature; what it saves is the second reader of the same
pair in the same second, and every eth_call saved is one a public endpoint is
entitled to refuse. Measured: 1.26s cold, 22ms warm.
And a route names the venue that answered. The router's own routing label
describes its strategy, so a V3 pool on Ethereum came back marked V4_NATIVE —
our precompile, on somebody else's chain. That is the one thing a route must
never misreport, since it is what a reader checks to see where their order goes.
Measured live, no key: 1 WETH is 2494.500698 USDC on Ethereum via uniswap_v3,
2494.591730 on Base.
Co-authored-by: Hanzo Dev <[email protected]>
pkg/gateway has carried the whole thing for a while — quote, swap, approval,
orders, pools, prices, tokens, stats, history — with a Uniswap provider that
speaks the shapes at trade-api.gateway.uniswap.org, and nothing ever started
it. `dexd run -http` mounts the D-Chain venue's own handlers under /v1/dex,
which is a different surface for a different reader, so an interface asking for
a quote reached nothing on any chain. It has an entrypoint now.
Two things it did wrong the moment it ran, both the same mistake in different
places. The router asked every provider about every chain, so a quote for a Lux
market went to Uniswap and came back ACCESS_DENIED — somebody else's API error
dressed as a fact about our liquidity. A provider is asked only about the
chains it lists now, and one that lists none is taken to serve any, which is
the shape a single-chain deployment has.
And the venues were attached to the server and never read: `s.venues` was
stored by an option nothing consulted, so the chain we settle ourselves was
quoted by the chain we do not. Our own venues answer first — an upstream
provider that also lists the chain is a second opinion about a market we
settle — and a venue holding no liquidity returns nothing rather than an
error, so falling through to the providers stays the honest next question.
A chain of ours with no provider behind it then says no venue holds the pair,
rather than "no providers available", which sends a reader looking for a
misconfiguration that is not there.
Co-authored-by: Hanzo Dev <[email protected]>
The primary-network chains answer to either case; one spelling beats two.
/v1/chain/c/rpc, /v1/chain/p, /v1/chain/x across the estate.
Co-authored-by: Hanzo Dev <[email protected]>
hanzo-build-linux-amd64, lux-build-linux-amd64, zoo-build-linux-amd64 and
every other org-prefixed spelling all meant the same machine. Which org a
job belongs to does not change which host can build it, so the org bought
one pool name per org per arch and each could go stale on its own -- the
evo host alone answered to fourteen names for one label.
One vocabulary now: linux-amd64, linux-arm64, macos-arm64, win-amd64. The
generic labels every plain workflow uses (ubuntu-latest, self-hosted) are
untouched, so naive CI keeps working.
Co-authored-by: Hanzo Dev <[email protected]>
The id is declared twice — here beside the Factory and in luxfi/constants,
which is the one genesis writes into the CreateChainTx — and nothing made them
agree. If this repo's literal moved, a node would look for a plugin under one
name while genesis named the other, and the chain would never appear: a plugin
that is absent and a plugin nobody asked for look identical to a directory
scan.
The second test records the property that decides how a plugin gets pinned. The
id is an ASCII name padded to 32 bytes, not a digest of the binary, and it must
not become one: genesis references it from a transaction already signed and
already in the chain, so an id derived from the build would change on every
rebuild and stop resolving the chain it created. Identity is stable across
versions by construction.
Which leaves integrity as a separate value. Nothing states which build of dexd
is the right one, and the registry runs whatever sits at the filename. What
CB58 does buy is that a mistyped filename fails its checksum instead of
resolving to nothing.
The VM had no factory, so the only way to build it was the literal in the
plugin entrypoint. That kept the D-Chain reachable exactly one way — as an
execed binary — and left the node unable to register it beside the chains it
links, which is why luxd came up without a D-Chain at all.
Factory is now the one constructor: the node registers it in-process and the
plugin entrypoint builds through it before handing the VM to rpc.Serve. VMID
moves in beside it so the chain and the id it answers to cannot drift apart.
luxd serves chain routes under /v1/chain, and the node, cli and sdk releases
already ship it. The deployed gateways still answer the old prefix, so remote
reads break until they are redeployed; the estate moves together rather than
sitting half converted.
Lines that record an observation - a probe, a status code, a past release -
keep the prefix they were written against, as do URLs on hosts that are not
ours. Avalanche endpoints move to /ext/bc, which is the path that host serves.
Co-authored-by: Hanzo Dev <[email protected]>
The rename to /v1/chain is real in luxd source, but the deployed fleet has not
taken it yet: probed just now, api.lux.network, api.lux-test.network,
api.zoo.network and api.hanzo.network each answer 200 on /v1/bc/C/rpc and 404 on
/v1/chain/C/rpc. Composing /v1/chain against those hosts reaches nothing, which
is the failure the rename set out to prevent.
The move belongs with the node redeploy, not ahead of it.
Co-authored-by: Hanzo Dev <[email protected]>
luxd serves chain routes under /v1/chain; the /v1/bc prefix it replaced is
gone, so anything composing it reaches nothing. Lines that record an
observation - a probe, a status code, a past release - keep the prefix they
were written against, as do URLs on hosts that are not ours.
Co-authored-by: Hanzo Dev <[email protected]>
luxd serves chain APIs under one segment from constants.ChainAliasPrefix
(constants v1.6.4, node v1.36.181):
/v1/bc/<alias>/... -> /v1/chain/<alias>/...
/ext/bc/<alias>/... -> /v1/chain/<alias>/...
Observations keep their original addresses: a dated note or a recorded status
code is a fact about when it was written, not an instruction to follow.
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.
lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.
Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.
Co-authored-by: Hanzo Dev <[email protected]>
dexd run could not start. The VM refuses to found a chain from a creation
document it was not given — correctly, since a genesis the binary derived
is one no other node would derive — and standalone called Initialize with
no document at all, so every invocation ended at that refusal. The mode
the binary's own help describes was unreachable.
It takes --genesis now: under luxd the document arrives as
InitializeRequest.GenesisBytes, and standalone has no P-Chain to read it
from, so an operator names it. The bytes are opaque; what matters is that
somebody chose them, so two nodes told to be the same chain are.
The surface also mounted a segment twice. httpHandlers returns keys that
already carry the chain's namespace — "/dex/<method>", the same segment a
node serves under /v1/bc/<D>/ — so a prefix of "/v1/dex" produced
/v1/dex/dex/<method>: a path no client written against a node would try,
and not the one the flag promises. The prefix supplies the version now.
Verified by running it: the venue starts against a document and answers
dex_get_markets and dex_get_trades at /v1/dex/<method>.
Co-authored-by: Hanzo Dev <[email protected]>
The clean-room posture is worth keeping; the surface it described had moved.
Settlement is 0x9999, which carries the V4 PoolManager ABI this path already
encodes. The LXPool precompile at 0x9010 it pointed to is deprecated —
eth_getCode returns empty for it on mainnet, so the calls it built went to an
address with no code behind it.
FIX is FIXT.1.1 at the session layer and FIX.5.0SP2 (EP307) at the application
layer. pkg/fix asserts Parse errors on 8=FIX.4.4 and 8=FIX.4.2, so the 4.4
profile advertised here could not open a session.
The ws subpath now says nothing serves it: pkg/api compiles and is tested, but
NewWebSocketServer and NewVenueWS have no caller outside those tests and no
binary in cmd/ imports the package. A client for a listener nobody started is
worth shipping only if it says so.
zap and the precompile path were checked the same way and both reach something
live.
Co-authored-by: Hanzo Dev <[email protected]>
Two source files behind a package name that was never published, and
nothing in the tree imports it — every reference is the package naming
itself in its own README, manifest and lockfile. The grpc client it
describes is served by @luxfi/trading, which is published and is what
the 33 call sites actually use.
Co-authored-by: Hanzo Dev <[email protected]>
sdk/typescript declared itself @luxfi/[email protected] with a prepublish script,
but @luxfi/dex on npm is the viem precompile client and its registry
history starts at 2.0.1 — 1.0.0 was never published and nothing here
imports this package. Two codebases claiming one name is a publish
accident waiting to happen, and the axios/ws REST client it holds is not
what that name means any more.
Co-authored-by: Hanzo Dev <[email protected]>
flare.mdx published an OrderTransactions that sorts by priority fee,
highest first. No such function exists, nothing anywhere reads a priority
fee, and the mempool says the opposite in as many words: ordering is
arrival order, and a fee-priority mempool is a future concern the venue
does not have.
That matters more than an ordinary doc error. This repository is public,
and a venue documented as selling position is a different venue from one
where order ids are (height << 32) | txIndex and cannot be bid for. The
page now describes the comparator that runs — price, then the id, FIFO
within a level — and says plainly that fee priority is not it.
Co-authored-by: Hanzo Dev <[email protected]>
docs/app/layout.tsx referenced ZenMono without importing it and without @hanzo/font in its dependencies at all, so the docs site could not compile: tsc reports "Cannot find name 'ZenMono'" on the bare tree. Both faces are imported from the package now, and the package is declared. The README hero card still asked for Inter.
The docs layout named three different faces in one place: a binding called
`inter` or `geist`, constructed from next/font/google so it actually held Inter,
publishing `--font-geist-sans`, which @hanzo/font does not set. Whichever a
reader trusted, two were wrong.
It takes Zen and ZenMono from @hanzo/font — named `sans` and `mono` for the role
they fill, setting the properties the package publishes.
TxOpenMarket does not require auth and execute() wrote its (base,quote)
binding unconditionally, so any unsigned open_market could rebind a live
market. A maker that rested a sell had locked its base; after a rebind
the settle path spent a different, unlocked asset and returned
ErrInsufficientLocked out of execute() as a block abort — so every later
BuildBlock/Verify failed identically and the chain could not advance past
that height. A market's assets are now decided once: bind when unbound,
no-op an identical rebind, and reject a conflicting one as a decided tx
while the block still succeeds. Separately, the maker leg was read from
the taker-side byte with opposite defaults in makerSettleKey and
decrementMakerReserves, so an out-of-domain side (the byte is uncontrolled
on the wire) made them name different legs; both now read one canonical
makerOf, unchanged on the valid {buy,sell} domain.
Co-authored-by: Hanzo Dev <[email protected]>
The classical SignedOrder binds ChainID, NetworkID and Expiry so a
captured order cannot replay onto another chain or after it expires.
The strict-PQ order carried none of them, so an ML-DSA-signed order was
good on every chain forever — the exact replay the classical path
already closed. Mirror the three scope fields onto SignedOrderPQ and
fold them into the shared SigningHash, so one digest binds the same
scope under either scheme. Mutating any scope field now breaks the
signature (regression test).
Co-authored-by: Hanzo Dev <[email protected]>
The signing digest covered the order's economic fields but named no chain, no
network, and no deadline, so a signature made for one chain recovered its signer
on every chain and never expired. On a permissionless exchange a signed order
could be lifted onto another D-Chain, or replayed forever.
ChainID, NetworkID and Expiry are packed into SigningHash as the authorization
scope, ahead of the economic fields, in the same fixed-width hand-written layout
so a future field cannot silently drop out of the signature. The D-Chain tx
digest binds the same scope for the same reason: a signature must name the chain
it spends on to spend on only one.
COORDINATED: this changes the order signing digest, so every signer and every
verifier moves together. It is a protocol change, not a lone patch, and the
order-authorization layout belongs in an LP alongside the finality-message spec.
Co-authored-by: Hanzo Dev <[email protected]>
SigningHash packed uint64(math.Round(Size*1e8)), so the digest committed to a
rounded tick count while the matcher's crossing loop and RemainingSize read the
float. Two sizes inside one rounding interval produced the SAME digest: a
signature taken over one authorized the other. On a permissionless exchange that
is a signed order whose quantity the holder can change.
An off-tick size is refused rather than snapped. Snapping decides, silently, that
the signer meant a quantity they did not write, and that is the one thing a
signature exists to prevent. Two on-tick sizes differ by at least a tick, so
their digests differ; an off-tick size cannot be signed at all. The collision is
unreachable from either side.
The epsilon absorbs the binary representation error of a decimal quantity and
nothing more — it is orders of magnitude below one tick, so it can never let two
distinct ticks meet.
Co-authored-by: Hanzo Dev <[email protected]>