Files
zeekayandHanzo Dev 1ac4c806b2 Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers; omitzero is not applied, since it changes what the JSON says.

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

180 lines
7.0 KiB
Go

package maker
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/luxfi/rpc"
)
// dexhttp.go is the maker's HTTP transport onto the NATIVE in-luxd D-Chain CLOB
// surface. The native VM (github.com/luxfi/dex/pkg/dchain) exposes the dex_*
// methods over luxd's own HTTP router via VM.CreateHandlers: luxd mounts each
// method as an exact route at
//
// /v1/chain/<chainID-or-D-alias>/dex/<method> (POST, raw frozen-frame body)
//
// (see dex/pkg/dchain/ingest.go). An order POSTed there flows submitTx ->
// mempool -> consensus PendingTxs -> BuildBlock -> Block.Verify (the matcher) ->
// Block.Accept; the response body is the CONSENSUS-COMPUTED outcome (the same
// zapwire ack / fills / balance frame the ZAP transport returns). There is no
// external matcher, no relay, no standalone venue: the chain is the matcher.
//
// THE INVARIANT: this transport is a drop-in for the ZAP rpc.Client. The request
// payload and the response bytes are the IDENTICAL frozen zapwire frames dex.go
// already builds and decodes (dexcustody_test.go / dexpoolid_test.go assert that
// byte-equality against dex/pkg/{venue,zapwire}). Only the wire underneath the
// rpc.Client.CallRaw seam differs — ZAP socket vs node HTTP — so NewDEXClient,
// the codecs, and the Seeder are reused verbatim. ONE method->frame table, two
// transports.
// httpDEXClient implements rpc.Client over luxd's HTTP router for the native
// D-Chain CLOB. It maps CallRaw(method, payload) to a POST of payload to
// <base>/dex/<method> and returns the raw response body. base is the chain route
// prefix (e.g. http://luxd-0.luxd-headless:9650/v1/chain/D), WITHOUT a trailing
// slash; the "/dex" namespace and method are appended to match ingest.go's route
// keys exactly (ingestNamespace = "dex").
type httpDEXClient struct {
base string
hc *http.Client
}
// DialDEXHTTP builds an rpc.Client that reaches the native D-Chain CLOB over
// luxd's HTTP router. base is the chain route prefix — the node base URL plus
// /v1/chain/<chainID-or-D>; the caller passes the D alias (".../v1/chain/D") or the
// blockchain ID. A trailing slash is trimmed so the joined path has exactly one
// separator. The returned client is wrapped by NewDEXClient like any rpc.Client.
func DialDEXHTTP(base string, user string) *DEXClient {
return NewDEXClient(newHTTPDEXClient(base), user)
}
func newHTTPDEXClient(base string) *httpDEXClient {
return &httpDEXClient{
base: strings.TrimRight(base, "/"),
hc: &http.Client{},
}
}
// CallRaw POSTs the frozen-frame payload to <base>/dex/<method> and returns the
// raw response body. It uses the request context so a client disconnect cancels
// the node's consensus wait (the handler withdraws the tx — see ingest.go /
// submitTx RED #4). A non-2xx status is an error carrying the status and the body
// text: 502 means the VM could not sequence the op into a consensus outcome (the
// caller treats a custody/order failure as such), 404/405 means the route is not
// served (the pre-fix node, or a method typo) — never silently swallowed.
func (c *httpDEXClient) CallRaw(ctx context.Context, method string, payload []byte) ([]byte, error) {
url := c.base + "/dex/" + method
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
if err != nil {
return nil, fmt.Errorf("build request %s: %w", method, err)
}
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := c.hc.Do(req)
if err != nil {
return nil, fmt.Errorf("post %s: %w", url, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response %s: %w", method, err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s: http %d: %s", method, resp.StatusCode, strings.TrimSpace(string(body)))
}
return body, nil
}
// GetNonce reads the venue's EXPECTED-NEXT nonce for an account via
// GET <base>/dex/dex_get_nonce?account=<hex of the 16-byte settlement id>
// (pkg/dchain read.go). Returns (n, true) on success; (0, false) if the route is
// absent (a pre-nonce-read venue → the client keeps nonce 0) or on any error.
// Forward-compatible: the maker resyncs against a new venue and degrades cleanly
// against an old one. Satisfies dex.go's nonceReader.
func (c *httpDEXClient) GetNonce(ctx context.Context, accountHex string) (uint64, bool) {
url := c.base + "/dex/" + MethodGetNonce + "?account=" + accountHex
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return 0, false
}
resp, err := c.hc.Do(req)
if err != nil {
return 0, false
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return 0, false // 404/405 on a pre-nonce-read venue: keep nonce 0
}
var body struct {
Nonce uint64 `json:"nonce"`
}
if json.NewDecoder(resp.Body).Decode(&body) != nil {
return 0, false
}
return body.Nonce, true
}
// GetOrders reads a market's resting book via
// GET <base>/dex/dex_get_orders?market=<poolHex> (pkg/dchain read.go), returning
// each row's order id + owner account. (rows, true) on success; (nil, false) if
// the route is absent or on any error. Satisfies dex.go's ordersReader — used by
// the startup reconcile to cancel the maker's own stale orders.
func (c *httpDEXClient) GetOrders(ctx context.Context, poolHex string) ([]RestingOrder, bool) {
url := c.base + "/dex/" + MethodGetOrders + "?market=" + poolHex
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, false
}
resp, err := c.hc.Do(req)
if err != nil {
return nil, false
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, false
}
var body struct {
Orders []RestingOrder `json:"orders"`
}
if json.NewDecoder(resp.Body).Decode(&body) != nil {
return nil, false
}
return body.Orders, true
}
// Read route names, mirrored from dex/pkg/dchain so the maker does not import the
// cgo-tagged pkg/dchain (same reason the auth pieces are re-declared in
// dexauth.go); they MUST match the venue's routes.
const (
MethodGetNonce = "dex_get_nonce"
MethodGetOrders = "dex_get_orders"
)
// Call is not used by the CLOB surface (every method is a raw frozen frame over
// CallRaw). It is part of the rpc.Client interface only.
func (c *httpDEXClient) Call(ctx context.Context, method string, args, reply any) error {
return fmt.Errorf("httpDEXClient: Call unsupported (use CallRaw with a frozen frame)")
}
// Notify is not used by the CLOB surface (every write blocks for the consensus
// outcome; there are no fire-and-forget methods). Interface-only.
func (c *httpDEXClient) Notify(ctx context.Context, method string, args any) error {
return fmt.Errorf("httpDEXClient: Notify unsupported")
}
// Close releases idle HTTP connections. There is no persistent socket to tear
// down (each CallRaw is a request/response), so this is best-effort cleanup.
func (c *httpDEXClient) Close() error {
c.hc.CloseIdleConnections()
return nil
}
// compile-time guard: httpDEXClient is a full rpc.Client.
var _ rpc.Client = (*httpDEXClient)(nil)