mirror of
https://github.com/luxfi/maker.git
synced 2026-10-30 16:20:04 +00:00
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]>
180 lines
7.0 KiB
Go
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)
|