lx/dex: gate gRPC transport behind the grpc build tag

Default build pulls zero gRPC code; rebuild with `-tags=grpc` to opt
into gRPC.

Decomplected via a `grpcBackend` interface in `sdk/go/client` and a
`grpcSubClient` interface in `client/go`. The default build wires a
no-op backend (`*_nogrpc.go`); the `grpc` build wires the real
protobuf-backed client (`*_grpc.go`). Public methods stay the same in
both builds — the gRPC fast path is preferred when wired, else
JSON-RPC / WebSocket.

Files gated:
- pkg/grpc/pb/lxdex_grpc.pb.go            (//go:build grpc)
- pkg/proto/engine/proto/lx_engine_grpc.pb.go (//go:build grpc)

Files refactored:
- sdk/go/client.go: //go:build grpc with stub in client_nogrpc.go
- sdk/go/client/client.go: gRPC removed from method bodies; backed by
  the grpcBackend interface (grpc_backend{,_grpc,_nogrpc}.go)
- client/go/main.go: GrpcClient + ConnectGrpc moved to grpc.go
  (//go:build grpc); nogrpc.go stubs ConnectGrpc with a clear error

Verification:
- go build ./... (default) clean across sdk/go, client/go, top-level
- go build -tags=grpc ./... clean across all three modules
- go test ./... and go test -tags=grpc ./... green
- go list -deps shows 0 google.golang.org/grpc packages in default
  builds of sdk/go/client and client/go; 62 packages with -tags=grpc

READMEs in sdk/go and client/go document the build-tag opt-in.
This commit is contained in:
2026-05-16 17:54:11 -07:00
parent f3e0e32ed4
commit 960e97152f
19 changed files with 996 additions and 670 deletions
+23 -4
View File
@@ -1,20 +1,31 @@
# LX Trading Client
Multi-protocol programmatic trading client for LX. Supports WebSocket and gRPC protocols with a unified interface.
Multi-protocol programmatic trading client for LX. Default build is
WebSocket-only; gRPC is opt-in via a build tag.
## Features
- **Dual Protocol Support**: WebSocket and gRPC with runtime switching
- **Unified Interface**: Same API across both protocols
- **WebSocket transport** (always available, default)
- **gRPC transport** (opt-in, behind the `grpc` build tag)
- **Unified Interface**: Same API across both transports
- **Multiple Usage Modes**: Package import, CLI commands, or interactive shell
- **Real-time Streaming**: Market data subscriptions via WebSocket or gRPC streams
## Installation
```bash
# Default build — WebSocket only, zero gRPC code compiled in.
go build -o lx-client .
# With gRPC support (adds google.golang.org/grpc to the binary).
go build -tags=grpc -o lx-client .
```
Without the `grpc` build tag, `-protocol=grpc`, the `switch grpc`
interactive command, and the `ping` / `info` commands report that gRPC
is not compiled in. WebSocket-only consumers ship a smaller binary
with no gRPC runtime.
## Quick Start
### WebSocket (Default)
@@ -30,9 +41,12 @@ go build -o lx-client .
./lx-client -key mykey -secret mysecret -i
```
### gRPC
### gRPC (requires `-tags=grpc` build)
```bash
# Rebuild with the grpc tag first.
go build -tags=grpc -o lx-client .
# Interactive mode via gRPC
./lx-client -protocol grpc -i
@@ -318,7 +332,12 @@ if err != nil {
```bash
cd /path/to/lx/dex/client/go
go mod tidy
# Default — WebSocket only.
go build -o lx-client .
# With gRPC support.
go build -tags=grpc -o lx-client .
```
## Testing
+11 -9
View File
@@ -1,20 +1,22 @@
module github.com/luxfi/dex/client/go
go 1.24.5
go 1.26.3
// google.golang.org/grpc is only compiled when the `grpc` build tag is
// set. Default builds (WebSocket only) pull zero gRPC code.
require (
github.com/gorilla/websocket v1.5.3
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/luxfi/dex v0.0.0
google.golang.org/grpc v1.74.2
google.golang.org/grpc v1.79.1
)
require (
go.opentelemetry.io/otel v1.39.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 // indirect
google.golang.org/protobuf v1.36.7 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/protobuf v1.36.11 // indirect
)
replace github.com/luxfi/dex => ../..
+26 -24
View File
@@ -10,29 +10,31 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 h1:qJW29YvkiJmXOYMu5Tf8lyrTp3dOS+K4z6IixtLaCf8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+281
View File
@@ -0,0 +1,281 @@
//go:build grpc
// gRPC transport for the LX CLI. Compiled only when the `grpc` build
// tag is set; the default build uses nogrpc.go which wires nil.
package main
import (
"context"
"fmt"
"strings"
"time"
pb "github.com/luxfi/dex/pkg/grpc/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// GrpcClient implements Client over gRPC.
type GrpcClient struct {
conn *grpc.ClientConn
client pb.LXDEXServiceClient
address string
userID string
verbose bool
}
// NewGrpcClient creates a gRPC client.
func NewGrpcClient(address string, verbose bool) (*GrpcClient, error) {
conn, err := grpc.NewClient(address,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(100*1024*1024),
grpc.MaxCallSendMsgSize(100*1024*1024),
),
)
if err != nil {
return nil, fmt.Errorf("grpc dial failed: %w", err)
}
return &GrpcClient{
conn: conn,
client: pb.NewLXDEXServiceClient(conn),
address: address,
verbose: verbose,
}, nil
}
// Protocol returns the protocol type.
func (c *GrpcClient) Protocol() Protocol {
return ProtocolGRPC
}
// SetUserID sets the userID used in requests.
func (c *GrpcClient) SetUserID(userID string) {
c.userID = userID
}
// PlaceOrder places an order via gRPC.
func (c *GrpcClient) PlaceOrder(ctx context.Context, order *Order) (*OrderResponse, error) {
req := &pb.PlaceOrderRequest{
Symbol: order.Symbol,
Side: parseOrderSide(order.Side),
Type: parseOrderType(order.Type),
Price: order.Price,
Size: order.Size,
UserId: c.userID,
ClientId: fmt.Sprintf("cli-%d", time.Now().UnixNano()),
}
if c.verbose {
fmt.Printf(">> gRPC PlaceOrder: %+v\n", req)
}
resp, err := c.client.PlaceOrder(ctx, req)
if err != nil {
return nil, fmt.Errorf("grpc PlaceOrder failed: %w", err)
}
if c.verbose {
fmt.Printf("<< gRPC Response: %+v\n", resp)
}
return &OrderResponse{
OrderID: resp.OrderId,
Status: resp.Status.String(),
Message: resp.Message,
}, nil
}
// CancelOrder cancels an order via gRPC.
func (c *GrpcClient) CancelOrder(ctx context.Context, orderID uint64) error {
req := &pb.CancelOrderRequest{
OrderId: orderID,
UserId: c.userID,
}
if c.verbose {
fmt.Printf(">> gRPC CancelOrder: %+v\n", req)
}
resp, err := c.client.CancelOrder(ctx, req)
if err != nil {
return fmt.Errorf("grpc CancelOrder failed: %w", err)
}
if c.verbose {
fmt.Printf("<< gRPC Response: %+v\n", resp)
}
return nil
}
// GetOrders retrieves open orders via gRPC.
func (c *GrpcClient) GetOrders(ctx context.Context) ([]Order, error) {
req := &pb.GetOrdersRequest{
UserId: c.userID,
}
if c.verbose {
fmt.Printf(">> gRPC GetOrders: %+v\n", req)
}
resp, err := c.client.GetOrders(ctx, req)
if err != nil {
return nil, fmt.Errorf("grpc GetOrders failed: %w", err)
}
if c.verbose {
fmt.Printf("<< gRPC Response: %d orders\n", len(resp.Orders))
}
orders := make([]Order, 0, len(resp.Orders))
for _, o := range resp.Orders {
orders = append(orders, Order{
Symbol: o.Symbol,
Side: o.Side.String(),
Type: o.Type.String(),
Price: o.Price,
Size: o.Size,
})
}
return orders, nil
}
// GetPositions retrieves positions via gRPC.
func (c *GrpcClient) GetPositions(ctx context.Context) ([]Position, error) {
req := &pb.GetPositionsRequest{
UserId: c.userID,
}
if c.verbose {
fmt.Printf(">> gRPC GetPositions: %+v\n", req)
}
resp, err := c.client.GetPositions(ctx, req)
if err != nil {
return nil, fmt.Errorf("grpc GetPositions failed: %w", err)
}
if c.verbose {
fmt.Printf("<< gRPC Response: %d positions\n", len(resp.Positions))
}
positions := make([]Position, 0, len(resp.Positions))
for _, p := range resp.Positions {
positions = append(positions, Position{
Symbol: p.Symbol,
Size: p.Size,
EntryPrice: p.EntryPrice,
MarkPrice: p.MarkPrice,
PnL: p.Pnl,
})
}
return positions, nil
}
// Subscribe subscribes to market data via gRPC streaming.
func (c *GrpcClient) Subscribe(ctx context.Context, symbol string) error {
req := &pb.StreamOrderBookRequest{
Symbol: symbol,
Depth: 10,
}
if c.verbose {
fmt.Printf(">> gRPC StreamOrderBook: %+v\n", req)
}
stream, err := c.client.StreamOrderBook(ctx, req)
if err != nil {
return fmt.Errorf("grpc StreamOrderBook failed: %w", err)
}
go func() {
for {
update, err := stream.Recv()
if err != nil {
if c.verbose {
fmt.Printf("Stream ended: %v\n", err)
}
return
}
fmt.Printf("OrderBook %s: %d bids, %d asks\n", update.Symbol, len(update.BidUpdates), len(update.AskUpdates))
}
}()
return nil
}
// Ping tests connectivity via gRPC.
func (c *GrpcClient) Ping(ctx context.Context) (time.Duration, error) {
start := time.Now()
req := &pb.PingRequest{
Timestamp: start.UnixNano(),
}
resp, err := c.client.Ping(ctx, req)
if err != nil {
return 0, fmt.Errorf("grpc Ping failed: %w", err)
}
latency := time.Since(start)
if c.verbose {
fmt.Printf("Ping response: %s (latency: %v)\n", resp.Message, latency)
}
return latency, nil
}
// GetNodeInfo retrieves node information via gRPC. The returned value
// is the generated *pb.NodeInfo; the interface{} return matches the
// build-agnostic grpcSubClient signature in main.go.
func (c *GrpcClient) GetNodeInfo(ctx context.Context) (interface{}, error) {
resp, err := c.client.GetNodeInfo(ctx, &pb.GetNodeInfoRequest{})
if err != nil {
return nil, fmt.Errorf("grpc GetNodeInfo failed: %w", err)
}
return resp, nil
}
// Close closes the gRPC connection.
func (c *GrpcClient) Close() error {
return c.conn.Close()
}
// ConnectGrpc connects via gRPC.
func (m *ClientManager) ConnectGrpc(address string, verbose bool) error {
m.mu.Lock()
defer m.mu.Unlock()
client, err := NewGrpcClient(address, verbose)
if err != nil {
return err
}
m.grpcClient = client
if m.active == nil {
m.active = client
}
return nil
}
// parseOrderType maps the CLI's string type to a proto OrderType.
func parseOrderType(t string) pb.OrderType {
switch strings.ToLower(t) {
case "market":
return pb.OrderType_MARKET
case "stop":
return pb.OrderType_STOP
case "stop_limit":
return pb.OrderType_STOP_LIMIT
default:
return pb.OrderType_LIMIT
}
}
// parseOrderSide maps the CLI's string side to a proto OrderSide.
func parseOrderSide(s string) pb.OrderSide {
switch strings.ToLower(s) {
case "sell":
return pb.OrderSide_SELL
default:
return pb.OrderSide_BUY
}
}
+62 -324
View File
@@ -1,10 +1,12 @@
// LX Trading Client
// LX Trading Client.
//
// Multi-protocol programmatic trading client for LX.
// Supports WebSocket and gRPC protocols with unified interface.
// Default build supports WebSocket only. gRPC support is opt-in via the
// `grpc` build tag (see grpc.go); without it `-protocol=grpc`, the
// `ping`/`info` commands, and the `switch grpc` interactive command
// report that gRPC is not compiled in.
//
// Usage:
// - As package: import "github.com/luxfi/dex/client/go/lxclient"
// - As CLI: ./lx-client [flags] [command]
// - Interactive: ./lx-client -i
package main
@@ -24,13 +26,9 @@ import (
"time"
"github.com/gorilla/websocket"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "github.com/luxfi/dex/pkg/grpc/pb"
)
// Protocol identifies the transport protocol
// Protocol identifies the transport protocol.
type Protocol string
const (
@@ -38,7 +36,7 @@ const (
ProtocolGRPC Protocol = "grpc"
)
// Order represents a trading order
// Order represents a trading order.
type Order struct {
Symbol string `json:"symbol"`
Side string `json:"side"`
@@ -47,7 +45,7 @@ type Order struct {
Size float64 `json:"size"`
}
// OrderResponse represents order operation response
// OrderResponse represents order operation response.
type OrderResponse struct {
OrderID uint64 `json:"order_id"`
Status string `json:"status"`
@@ -55,7 +53,7 @@ type OrderResponse struct {
Error string `json:"error,omitempty"`
}
// Position represents an open position
// Position represents an open position.
type Position struct {
Symbol string `json:"symbol"`
Size float64 `json:"size"`
@@ -64,7 +62,7 @@ type Position struct {
PnL float64 `json:"pnl"`
}
// Client defines the trading client interface
// Client defines the trading client interface.
type Client interface {
// Trading operations
PlaceOrder(ctx context.Context, order *Order) (*OrderResponse, error)
@@ -80,9 +78,19 @@ type Client interface {
Close() error
}
// grpcSubClient is the subset of *GrpcClient consumed by ClientManager
// and the CLI. The real implementation lives in grpc.go and is gated by
// the `grpc` build tag; nogrpc.go wires nil so the field is always
// addressable.
type grpcSubClient interface {
Client
Ping(ctx context.Context) (time.Duration, error)
GetNodeInfo(ctx context.Context) (interface{}, error)
}
// ----- WebSocket Client Implementation -----
// WsMessage represents a WebSocket message
// WsMessage represents a WebSocket message.
type WsMessage struct {
Type string `json:"type"`
Data map[string]interface{} `json:"data,omitempty"`
@@ -91,7 +99,7 @@ type WsMessage struct {
Timestamp int64 `json:"timestamp,omitempty"`
}
// WsClient implements Client over WebSocket
// WsClient implements Client over WebSocket.
type WsClient struct {
conn *websocket.Conn
mu sync.Mutex
@@ -101,7 +109,7 @@ type WsClient struct {
url string
}
// NewWsClient creates a WebSocket client
// NewWsClient creates a WebSocket client.
func NewWsClient(url string, verbose bool) (*WsClient, error) {
dialer := websocket.Dialer{
HandshakeTimeout: 10 * time.Second,
@@ -186,12 +194,12 @@ func (c *WsClient) waitResponse(reqID string, timeout time.Duration) (*WsMessage
}
}
// Protocol returns the protocol type
// Protocol returns the protocol type.
func (c *WsClient) Protocol() Protocol {
return ProtocolWS
}
// PlaceOrder places an order via WebSocket
// PlaceOrder places an order via WebSocket.
func (c *WsClient) PlaceOrder(ctx context.Context, order *Order) (*OrderResponse, error) {
reqID, err := c.send("place_order", map[string]interface{}{
"order": order,
@@ -219,7 +227,7 @@ func (c *WsClient) PlaceOrder(ctx context.Context, order *Order) (*OrderResponse
return resp, nil
}
// CancelOrder cancels an order via WebSocket
// CancelOrder cancels an order via WebSocket.
func (c *WsClient) CancelOrder(ctx context.Context, orderID uint64) error {
reqID, err := c.send("cancel_order", map[string]interface{}{
"orderID": orderID,
@@ -239,7 +247,7 @@ func (c *WsClient) CancelOrder(ctx context.Context, orderID uint64) error {
return nil
}
// GetOrders retrieves open orders via WebSocket
// GetOrders retrieves open orders via WebSocket.
func (c *WsClient) GetOrders(ctx context.Context) ([]Order, error) {
reqID, err := c.send("get_orders", nil)
if err != nil {
@@ -255,7 +263,6 @@ func (c *WsClient) GetOrders(ctx context.Context) ([]Order, error) {
return nil, fmt.Errorf("get orders failed: %s", msg.Error)
}
// Parse orders from response
var orders []Order
if ordersData, ok := msg.Data["orders"].([]interface{}); ok {
for _, o := range ordersData {
@@ -273,7 +280,7 @@ func (c *WsClient) GetOrders(ctx context.Context) ([]Order, error) {
return orders, nil
}
// GetPositions retrieves positions via WebSocket
// GetPositions retrieves positions via WebSocket.
func (c *WsClient) GetPositions(ctx context.Context) ([]Position, error) {
reqID, err := c.send("get_positions", nil)
if err != nil {
@@ -306,7 +313,7 @@ func (c *WsClient) GetPositions(ctx context.Context) ([]Position, error) {
return positions, nil
}
// Subscribe subscribes to market data via WebSocket
// Subscribe subscribes to market data via WebSocket.
func (c *WsClient) Subscribe(ctx context.Context, symbol string) error {
_, err := c.send("subscribe", map[string]interface{}{
"symbols": []string{symbol},
@@ -314,7 +321,7 @@ func (c *WsClient) Subscribe(ctx context.Context, symbol string) error {
return err
}
// Auth authenticates via WebSocket
// Auth authenticates via WebSocket.
func (c *WsClient) Auth(apiKey, apiSecret string) error {
reqID, err := c.send("auth", map[string]interface{}{
"apiKey": apiKey,
@@ -335,7 +342,7 @@ func (c *WsClient) Auth(apiKey, apiSecret string) error {
return nil
}
// WaitConnected waits for WebSocket connection confirmation
// WaitConnected waits for WebSocket connection confirmation.
func (c *WsClient) WaitConnected(timeout time.Duration) error {
select {
case msg := <-c.responses:
@@ -348,263 +355,32 @@ func (c *WsClient) WaitConnected(timeout time.Duration) error {
}
}
// Responses returns the response channel for streaming
// Responses returns the response channel for streaming.
func (c *WsClient) Responses() <-chan WsMessage {
return c.responses
}
// Close closes the WebSocket connection
// Close closes the WebSocket connection.
func (c *WsClient) Close() error {
return c.conn.Close()
}
// ----- gRPC Client Implementation -----
// GrpcClient implements Client over gRPC
type GrpcClient struct {
conn *grpc.ClientConn
client pb.LXDEXServiceClient
address string
userID string
verbose bool
}
// NewGrpcClient creates a gRPC client
func NewGrpcClient(address string, verbose bool) (*GrpcClient, error) {
conn, err := grpc.NewClient(address,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(100*1024*1024),
grpc.MaxCallSendMsgSize(100*1024*1024),
),
)
if err != nil {
return nil, fmt.Errorf("grpc dial failed: %w", err)
}
return &GrpcClient{
conn: conn,
client: pb.NewLXDEXServiceClient(conn),
address: address,
userID: "default",
verbose: verbose,
}, nil
}
// Protocol returns the protocol type
func (c *GrpcClient) Protocol() Protocol {
return ProtocolGRPC
}
// SetUserID sets the user ID for requests
func (c *GrpcClient) SetUserID(userID string) {
c.userID = userID
}
// PlaceOrder places an order via gRPC
func (c *GrpcClient) PlaceOrder(ctx context.Context, order *Order) (*OrderResponse, error) {
req := &pb.PlaceOrderRequest{
Symbol: order.Symbol,
Type: parseOrderType(order.Type),
Side: parseOrderSide(order.Side),
Price: order.Price,
Size: order.Size,
UserId: c.userID,
}
if c.verbose {
fmt.Printf(">> gRPC PlaceOrder: %+v\n", req)
}
resp, err := c.client.PlaceOrder(ctx, req)
if err != nil {
return nil, fmt.Errorf("grpc PlaceOrder failed: %w", err)
}
if c.verbose {
fmt.Printf("<< gRPC Response: %+v\n", resp)
}
return &OrderResponse{
OrderID: resp.OrderId,
Status: resp.Status.String(),
Message: resp.Message,
}, nil
}
// CancelOrder cancels an order via gRPC
func (c *GrpcClient) CancelOrder(ctx context.Context, orderID uint64) error {
req := &pb.CancelOrderRequest{
OrderId: orderID,
UserId: c.userID,
}
if c.verbose {
fmt.Printf(">> gRPC CancelOrder: %+v\n", req)
}
resp, err := c.client.CancelOrder(ctx, req)
if err != nil {
return fmt.Errorf("grpc CancelOrder failed: %w", err)
}
if c.verbose {
fmt.Printf("<< gRPC Response: %+v\n", resp)
}
if !resp.Success {
return fmt.Errorf("cancel failed: %s", resp.Message)
}
return nil
}
// GetOrders retrieves open orders via gRPC
func (c *GrpcClient) GetOrders(ctx context.Context) ([]Order, error) {
req := &pb.GetOrdersRequest{
UserId: c.userID,
}
if c.verbose {
fmt.Printf(">> gRPC GetOrders: %+v\n", req)
}
resp, err := c.client.GetOrders(ctx, req)
if err != nil {
return nil, fmt.Errorf("grpc GetOrders failed: %w", err)
}
if c.verbose {
fmt.Printf("<< gRPC Response: %d orders\n", len(resp.Orders))
}
var orders []Order
for _, o := range resp.Orders {
orders = append(orders, Order{
Symbol: o.Symbol,
Side: o.Side.String(),
Type: o.Type.String(),
Price: o.Price,
Size: o.Size,
})
}
return orders, nil
}
// GetPositions retrieves positions via gRPC
func (c *GrpcClient) GetPositions(ctx context.Context) ([]Position, error) {
req := &pb.GetPositionsRequest{
UserId: c.userID,
}
if c.verbose {
fmt.Printf(">> gRPC GetPositions: %+v\n", req)
}
resp, err := c.client.GetPositions(ctx, req)
if err != nil {
return nil, fmt.Errorf("grpc GetPositions failed: %w", err)
}
if c.verbose {
fmt.Printf("<< gRPC Response: %d positions\n", len(resp.Positions))
}
var positions []Position
for _, p := range resp.Positions {
positions = append(positions, Position{
Symbol: p.Symbol,
Size: p.Size,
EntryPrice: p.EntryPrice,
MarkPrice: p.MarkPrice,
PnL: p.Pnl,
})
}
return positions, nil
}
// Subscribe subscribes to market data via gRPC streaming
func (c *GrpcClient) Subscribe(ctx context.Context, symbol string) error {
req := &pb.StreamOrderBookRequest{
Symbol: symbol,
Depth: 20,
}
if c.verbose {
fmt.Printf(">> gRPC StreamOrderBook: %+v\n", req)
}
stream, err := c.client.StreamOrderBook(ctx, req)
if err != nil {
return fmt.Errorf("grpc StreamOrderBook failed: %w", err)
}
// Start goroutine to read stream
go func() {
for {
update, err := stream.Recv()
if err != nil {
if c.verbose {
fmt.Printf("stream ended: %v\n", err)
}
return
}
fmt.Printf("OrderBook [%s]: bids=%d asks=%d\n",
update.Symbol, len(update.BidUpdates), len(update.AskUpdates))
}
}()
return nil
}
// Ping tests connectivity via gRPC
func (c *GrpcClient) Ping(ctx context.Context) (time.Duration, error) {
start := time.Now()
req := &pb.PingRequest{
Timestamp: start.UnixNano(),
}
resp, err := c.client.Ping(ctx, req)
if err != nil {
return 0, fmt.Errorf("grpc Ping failed: %w", err)
}
latency := time.Since(start)
if c.verbose {
fmt.Printf("Ping response: %s (latency: %v)\n", resp.Message, latency)
}
return latency, nil
}
// GetNodeInfo retrieves node information via gRPC
func (c *GrpcClient) GetNodeInfo(ctx context.Context) (*pb.NodeInfo, error) {
resp, err := c.client.GetNodeInfo(ctx, &pb.GetNodeInfoRequest{})
if err != nil {
return nil, fmt.Errorf("grpc GetNodeInfo failed: %w", err)
}
return resp, nil
}
// Close closes the gRPC connection
func (c *GrpcClient) Close() error {
return c.conn.Close()
}
// ----- Multi-Protocol Manager -----
// ClientManager manages multiple protocol clients
// ClientManager manages multiple protocol clients.
type ClientManager struct {
wsClient *WsClient
grpcClient *GrpcClient
grpcClient grpcSubClient // nil unless built with -tags=grpc and ConnectGrpc succeeded
active Client
mu sync.RWMutex
}
// NewClientManager creates a client manager
// NewClientManager creates a client manager.
func NewClientManager() *ClientManager {
return &ClientManager{}
}
// ConnectWs connects via WebSocket
// ConnectWs connects via WebSocket.
func (m *ClientManager) ConnectWs(url string, verbose bool) error {
m.mu.Lock()
defer m.mu.Unlock()
@@ -621,24 +397,7 @@ func (m *ClientManager) ConnectWs(url string, verbose bool) error {
return nil
}
// ConnectGrpc connects via gRPC
func (m *ClientManager) ConnectGrpc(address string, verbose bool) error {
m.mu.Lock()
defer m.mu.Unlock()
client, err := NewGrpcClient(address, verbose)
if err != nil {
return err
}
m.grpcClient = client
if m.active == nil {
m.active = client
}
return nil
}
// SwitchProtocol switches the active protocol
// SwitchProtocol switches the active protocol.
func (m *ClientManager) SwitchProtocol(proto Protocol) error {
m.mu.Lock()
defer m.mu.Unlock()
@@ -660,28 +419,29 @@ func (m *ClientManager) SwitchProtocol(proto Protocol) error {
return nil
}
// Active returns the active client
// Active returns the active client.
func (m *ClientManager) Active() Client {
m.mu.RLock()
defer m.mu.RUnlock()
return m.active
}
// WsClient returns the WebSocket client
// WsClient returns the WebSocket client.
func (m *ClientManager) WsClient() *WsClient {
m.mu.RLock()
defer m.mu.RUnlock()
return m.wsClient
}
// GrpcClient returns the gRPC client
func (m *ClientManager) GrpcClient() *GrpcClient {
// GrpcClient returns the gRPC client (nil unless built with -tags=grpc
// and ConnectGrpc succeeded).
func (m *ClientManager) GrpcClient() grpcSubClient {
m.mu.RLock()
defer m.mu.RUnlock()
return m.grpcClient
}
// Close closes all connections
// Close closes all connections.
func (m *ClientManager) Close() error {
m.mu.Lock()
defer m.mu.Unlock()
@@ -720,32 +480,10 @@ func getFloat(m map[string]interface{}, key string) float64 {
return 0
}
func parseOrderType(t string) pb.OrderType {
switch strings.ToLower(t) {
case "market":
return pb.OrderType_MARKET
case "stop":
return pb.OrderType_STOP
case "stop_limit":
return pb.OrderType_STOP_LIMIT
default:
return pb.OrderType_LIMIT
}
}
func parseOrderSide(s string) pb.OrderSide {
switch strings.ToLower(s) {
case "sell":
return pb.OrderSide_SELL
default:
return pb.OrderSide_BUY
}
}
// ----- CLI Implementation -----
func printHelp() {
fmt.Println(`
fmt.Print(`
LX Trading Client Commands:
place_order <symbol> <side> <type> <price> <size>
@@ -770,13 +508,13 @@ LX Trading Client Commands:
Authenticate with credentials (WebSocket only)
switch <ws|grpc>
Switch active protocol
Switch active protocol (gRPC requires -tags=grpc build)
ping
Test connectivity (gRPC only)
Test connectivity (gRPC only, requires -tags=grpc build)
info
Show node info (gRPC only)
Show node info (gRPC only, requires -tags=grpc build)
protocol
Show current protocol
@@ -848,27 +586,27 @@ func runInteractive(mgr *ClientManager) {
}
case "ping":
if grpc := mgr.GrpcClient(); grpc != nil {
latency, err := grpc.Ping(ctx)
if g := mgr.GrpcClient(); g != nil {
latency, err := g.Ping(ctx)
if err != nil {
fmt.Printf("Ping failed: %v\n", err)
} else {
fmt.Printf("Pong! Latency: %v\n", latency)
}
} else {
fmt.Println("Ping requires gRPC connection")
fmt.Println("Ping requires gRPC connection (rebuild with -tags=grpc)")
}
case "info":
if grpc := mgr.GrpcClient(); grpc != nil {
info, err := grpc.GetNodeInfo(ctx)
if g := mgr.GrpcClient(); g != nil {
info, err := g.GetNodeInfo(ctx)
if err != nil {
fmt.Printf("GetNodeInfo failed: %v\n", err)
} else {
printMessage(info)
}
} else {
fmt.Println("Info requires gRPC connection")
fmt.Println("Info requires gRPC connection (rebuild with -tags=grpc)")
}
case "place_order":
@@ -1030,28 +768,28 @@ func runCommand(mgr *ClientManager, args []string) {
printMessage(orders)
case "ping":
if grpc := mgr.GrpcClient(); grpc != nil {
latency, err := grpc.Ping(ctx)
if g := mgr.GrpcClient(); g != nil {
latency, err := g.Ping(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Printf(`{"latency_ns":%d,"latency_ms":%.3f}`+"\n", latency.Nanoseconds(), float64(latency.Nanoseconds())/1e6)
} else {
fmt.Fprintln(os.Stderr, "Ping requires gRPC connection")
fmt.Fprintln(os.Stderr, "Ping requires gRPC connection (rebuild with -tags=grpc)")
os.Exit(1)
}
case "info":
if grpc := mgr.GrpcClient(); grpc != nil {
info, err := grpc.GetNodeInfo(ctx)
if g := mgr.GrpcClient(); g != nil {
info, err := g.GetNodeInfo(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
printMessage(info)
} else {
fmt.Fprintln(os.Stderr, "Info requires gRPC connection")
fmt.Fprintln(os.Stderr, "Info requires gRPC connection (rebuild with -tags=grpc)")
os.Exit(1)
}
@@ -1063,9 +801,9 @@ func runCommand(mgr *ClientManager, args []string) {
func main() {
// Flags
protocol := flag.String("protocol", "ws", "Protocol: ws or grpc")
protocol := flag.String("protocol", "ws", "Protocol: ws or grpc (grpc requires -tags=grpc build)")
wsURL := flag.String("ws-url", "ws://localhost:8081", "WebSocket server URL")
grpcAddr := flag.String("grpc-addr", "localhost:9090", "gRPC server address")
grpcAddr := flag.String("grpc-addr", "localhost:9090", "gRPC server address (requires -tags=grpc build)")
apiKey := flag.String("key", "", "API key for authentication")
apiSecret := flag.String("secret", "", "API secret for authentication")
interactive := flag.Bool("i", false, "Interactive mode")
+19
View File
@@ -0,0 +1,19 @@
//go:build !grpc
// Default-build stubs for the gRPC transport. The CLI is fully
// functional via WebSocket without gRPC compiled in; the gRPC paths
// (the `ping` / `info` commands, `-protocol=grpc`, and the `switch
// grpc` interactive command) report that gRPC is not compiled in.
package main
import "errors"
// errGRPCNotBuilt is returned by ConnectGrpc when the binary was built
// without the `grpc` build tag. Rebuild with `go build -tags=grpc` to
// enable gRPC support.
var errGRPCNotBuilt = errors.New("gRPC support requires the `grpc` build tag (rebuild with -tags=grpc)")
// ConnectGrpc reports that gRPC is not compiled in.
func (m *ClientManager) ConnectGrpc(address string, verbose bool) error {
return errGRPCNotBuilt
}
+2
View File
@@ -4,6 +4,8 @@
// - protoc v5.29.3
// source: proto/lxdex.proto
//go:build grpc
package pb
import (
@@ -4,6 +4,8 @@
// - protoc v5.29.3
// source: proto/lx_engine.proto
//go:build grpc
package engine
import (
+38 -11
View File
@@ -2,14 +2,23 @@
Official Go SDK for interacting with the LX trading platform.
## Features
## Transports
- **Multiple Protocol Support**: JSON-RPC, gRPC, and WebSocket
- **High Performance**: Direct gRPC connection for low-latency operations
- **Real-time Data**: WebSocket subscriptions for live market data
- **Type Safety**: Strongly typed Go interfaces
- **Concurrent Safe**: Thread-safe operations with proper locking
- **Automatic Failover**: Falls back to JSON-RPC if gRPC unavailable
The SDK ships two transports by default and a third behind a build tag:
- **JSON-RPC** — request/response, always built.
- **WebSocket** — streaming subscriptions, always built.
- **gRPC** — binary low-latency transport, **opt-in via the `grpc`
build tag**. The default build pulls zero gRPC code.
Consumers that want gRPC rebuild their binary with `-tags=grpc`. The
public API is identical across both builds: methods (`PlaceOrder`,
`CancelOrder`, `GetOrderBook`, `GetTrades`) transparently use gRPC
when it is wired and connected, otherwise fall back to JSON-RPC.
`ConnectGRPC` returns `client.ErrGRPCNotBuilt` without the tag, and
`StreamOrderBook` returns the same error. Calling them is safe in any
build.
## Installation
@@ -17,6 +26,18 @@ Official Go SDK for interacting with the LX trading platform.
go get github.com/luxfi/dex/sdk/go
```
Default build (JSON-RPC + WebSocket only):
```bash
go build ./...
```
Build with gRPC support:
```bash
go build -tags=grpc ./...
```
## Quick Start
```go
@@ -258,10 +279,12 @@ balance.Utilization() // Calculate balance utilization
## Performance Tips
1. **Use gRPC**: Connect via gRPC for lowest latency
2. **Reuse Client**: Create one client and reuse it
3. **Batch Operations**: Use concurrent requests when possible
4. **Stream vs Poll**: Use streaming for real-time data instead of polling
1. **Use gRPC for HFT**: Rebuild with `-tags=grpc`, then call
`ConnectGRPC`. Methods automatically prefer the gRPC transport when
it is connected, otherwise use JSON-RPC.
2. **Reuse Client**: Create one client and reuse it.
3. **Batch Operations**: Use concurrent requests when possible.
4. **Stream vs Poll**: Use streaming for real-time data instead of polling.
## Thread Safety
@@ -315,7 +338,11 @@ go test ./...
### Building
```bash
# Default — JSON-RPC + WebSocket only.
go build ./...
# With gRPC support.
go build -tags=grpc ./...
```
## Support
+8 -1
View File
@@ -1,4 +1,11 @@
// Go SDK for LX Trading Platform
//go:build grpc
// Go SDK for LX Trading Platform.
//
// This file contains the gRPC-backed client and is built only when the
// `grpc` build tag is set. The default build provides a stub in
// client_nogrpc.go whose NewClient returns an error directing callers
// to rebuild with `-tags=grpc`.
package lxsdk
import (
+53 -234
View File
@@ -1,4 +1,9 @@
// Package client provides a Go SDK for interacting with LX
// Package client provides a Go SDK for interacting with LX.
//
// The default build ships JSON-RPC and WebSocket transports only and
// pulls in zero gRPC code. gRPC dial support is an opt-in capability
// gated behind the `grpc` build tag (see client_grpc.go). Consumers
// that want the binary gRPC transport rebuild with `-tags=grpc`.
package client
import (
@@ -12,12 +17,13 @@ import (
"time"
"github.com/gorilla/websocket"
pb "github.com/luxfi/dex/pkg/grpc/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// Client is the main client for interacting with LX
// Client is the main client for interacting with LX.
//
// Public methods (PlaceOrder, CancelOrder, GetOrderBook, GetTrades)
// route to the gRPC backend first when it is wired and otherwise fall
// back to JSON-RPC. The grpc backend is a no-op in the default build.
type Client struct {
// Configuration
jsonRPCURL string
@@ -36,14 +42,13 @@ type Client struct {
wsRunning bool
wsStop chan struct{}
// gRPC
grpcConn *grpc.ClientConn
grpcClient pb.LXDEXServiceClient
// gRPC backend (no-op unless built with -tags=grpc).
grpc grpcBackend
mu sync.RWMutex
}
// NewClient creates a new LX client
// NewClient creates a new LX client.
func NewClient(opts ...Option) (*Client, error) {
c := &Client{
jsonRPCURL: "http://localhost:8080",
@@ -52,6 +57,7 @@ func NewClient(opts ...Option) (*Client, error) {
httpClient: &http.Client{Timeout: 30 * time.Second},
wsCallbacks: make(map[string]func(interface{})),
wsStop: make(chan struct{}),
grpc: newGRPCBackend(),
}
// Apply options
@@ -62,53 +68,45 @@ func NewClient(opts ...Option) (*Client, error) {
return c, nil
}
// Option is a client configuration option
// Option is a client configuration option.
type Option func(*Client)
// WithJSONRPCURL sets the JSON-RPC URL
// WithJSONRPCURL sets the JSON-RPC URL.
func WithJSONRPCURL(url string) Option {
return func(c *Client) {
c.jsonRPCURL = url
}
}
// WithWebSocketURL sets the WebSocket URL
// WithWebSocketURL sets the WebSocket URL.
func WithWebSocketURL(url string) Option {
return func(c *Client) {
c.wsURL = url
}
}
// WithGRPCURL sets the gRPC URL
// WithGRPCURL sets the gRPC URL. The URL is only dialed when the
// client is built with the `grpc` build tag and ConnectGRPC is called.
func WithGRPCURL(url string) Option {
return func(c *Client) {
c.grpcURL = url
}
}
// WithAPIKey sets the API key for authentication
// WithAPIKey sets the API key for authentication.
func WithAPIKey(key string) Option {
return func(c *Client) {
c.apiKey = key
}
}
// ConnectGRPC establishes a gRPC connection
// ConnectGRPC establishes a gRPC connection. Returns an error in the
// default build instructing callers to rebuild with `-tags=grpc`.
func (c *Client) ConnectGRPC(ctx context.Context) error {
conn, err := grpc.DialContext(ctx, c.grpcURL,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
)
if err != nil {
return fmt.Errorf("failed to connect to gRPC: %w", err)
}
c.grpcConn = conn
c.grpcClient = pb.NewLXDEXServiceClient(conn)
return nil
return c.grpc.connect(ctx, c.grpcURL)
}
// ConnectWebSocket establishes a WebSocket connection
// ConnectWebSocket establishes a WebSocket connection.
func (c *Client) ConnectWebSocket(ctx context.Context) error {
c.mu.Lock()
defer c.mu.Unlock()
@@ -135,7 +133,7 @@ func (c *Client) ConnectWebSocket(ctx context.Context) error {
return nil
}
// handleWebSocketMessages processes incoming WebSocket messages
// handleWebSocketMessages processes incoming WebSocket messages.
func (c *Client) handleWebSocketMessages() {
defer func() {
c.mu.Lock()
@@ -175,7 +173,7 @@ func (c *Client) handleWebSocketMessages() {
}
}
// Disconnect closes all connections
// Disconnect closes all connections.
func (c *Client) Disconnect() error {
// Close WebSocket
if c.wsRunning {
@@ -187,44 +185,19 @@ func (c *Client) Disconnect() error {
c.mu.Unlock()
}
// Close gRPC
if c.grpcConn != nil {
c.grpcConn.Close()
}
// Close gRPC backend (no-op in default build).
_ = c.grpc.close()
return nil
}
// PlaceOrder places a new order
// PlaceOrder places a new order. Uses the gRPC backend when available,
// otherwise falls back to JSON-RPC.
func (c *Client) PlaceOrder(ctx context.Context, order *Order) (*OrderResponse, error) {
// Use gRPC if connected
if c.grpcClient != nil {
req := &pb.PlaceOrderRequest{
Symbol: order.Symbol,
Type: pb.OrderType(order.Type),
Side: pb.OrderSide(order.Side),
Price: order.Price,
Size: order.Size,
UserId: order.UserID,
ClientId: order.ClientID,
TimeInForce: timeInForceToProto(order.TimeInForce),
PostOnly: order.PostOnly,
ReduceOnly: order.ReduceOnly,
}
resp, err := c.grpcClient.PlaceOrder(ctx, req)
if err != nil {
return nil, err
}
return &OrderResponse{
OrderID: resp.OrderId,
Status: orderStatusFromProto(resp.Status),
Message: resp.Message,
}, nil
if resp, ok, err := c.grpc.placeOrder(ctx, order); ok {
return resp, err
}
// Fallback to JSON-RPC
params := map[string]interface{}{
"symbol": order.Symbol,
"type": order.Type,
@@ -249,17 +222,12 @@ func (c *Client) PlaceOrder(ctx context.Context, order *Order) (*OrderResponse,
}, nil
}
// CancelOrder cancels an existing order
// CancelOrder cancels an existing order.
func (c *Client) CancelOrder(ctx context.Context, orderID uint64) error {
// Use gRPC if connected
if c.grpcClient != nil {
_, err := c.grpcClient.CancelOrder(ctx, &pb.CancelOrderRequest{
OrderId: orderID,
})
if ok, err := c.grpc.cancelOrder(ctx, orderID); ok {
return err
}
// Fallback to JSON-RPC
params := map[string]interface{}{
"orderId": orderID,
}
@@ -268,43 +236,12 @@ func (c *Client) CancelOrder(ctx context.Context, orderID uint64) error {
return c.callJSONRPC(ctx, "lx_cancelOrder", params, &result)
}
// GetOrderBook retrieves the order book for a symbol
// GetOrderBook retrieves the order book for a symbol.
func (c *Client) GetOrderBook(ctx context.Context, symbol string, depth int32) (*OrderBook, error) {
// Use gRPC if connected
if c.grpcClient != nil {
resp, err := c.grpcClient.GetOrderBook(ctx, &pb.GetOrderBookRequest{
Symbol: symbol,
Depth: depth,
})
if err != nil {
return nil, err
}
ob := &OrderBook{
Symbol: resp.Symbol,
Timestamp: resp.Timestamp,
Bids: make([]PriceLevel, len(resp.Bids)),
Asks: make([]PriceLevel, len(resp.Asks)),
}
for i, bid := range resp.Bids {
ob.Bids[i] = PriceLevel{
Price: bid.Price,
Size: bid.Size,
}
}
for i, ask := range resp.Asks {
ob.Asks[i] = PriceLevel{
Price: ask.Price,
Size: ask.Size,
}
}
return ob, nil
if ob, ok, err := c.grpc.getOrderBook(ctx, symbol, depth); ok {
return ob, err
}
// Fallback to JSON-RPC
params := map[string]interface{}{
"symbol": symbol,
"depth": depth,
@@ -320,7 +257,6 @@ func (c *Client) GetOrderBook(ctx context.Context, symbol string, depth int32) (
Timestamp: int64(result["Timestamp"].(float64)),
}
// Parse bids
bids := result["Bids"].([]interface{})
ob.Bids = make([]PriceLevel, len(bids))
for i, bid := range bids {
@@ -331,7 +267,6 @@ func (c *Client) GetOrderBook(ctx context.Context, symbol string, depth int32) (
}
}
// Parse asks
asks := result["Asks"].([]interface{})
ob.Asks = make([]PriceLevel, len(asks))
for i, ask := range asks {
@@ -345,38 +280,12 @@ func (c *Client) GetOrderBook(ctx context.Context, symbol string, depth int32) (
return ob, nil
}
// GetTrades retrieves recent trades
// GetTrades retrieves recent trades.
func (c *Client) GetTrades(ctx context.Context, symbol string, limit int32) ([]*Trade, error) {
// Use gRPC if connected
if c.grpcClient != nil {
resp, err := c.grpcClient.GetTrades(ctx, &pb.GetTradesRequest{
Symbol: symbol,
Limit: limit,
})
if err != nil {
return nil, err
}
trades := make([]*Trade, len(resp.Trades))
for i, t := range resp.Trades {
trades[i] = &Trade{
TradeID: t.TradeId,
Symbol: t.Symbol,
Price: t.Price,
Size: t.Size,
Side: OrderSide(t.Side),
BuyOrderID: t.BuyOrderId,
SellOrderID: t.SellOrderId,
BuyerID: t.BuyerId,
SellerID: t.SellerId,
Timestamp: t.Timestamp,
}
}
return trades, nil
if trades, ok, err := c.grpc.getTrades(ctx, symbol, limit); ok {
return trades, err
}
// Fallback to JSON-RPC
params := map[string]interface{}{
"symbol": symbol,
"limit": limit,
@@ -407,13 +316,12 @@ func (c *Client) GetTrades(ctx context.Context, symbol string, limit int32) ([]*
return trades, nil
}
// Subscribe subscribes to a WebSocket channel
// Subscribe subscribes to a WebSocket channel.
func (c *Client) Subscribe(channel string, callback func(interface{})) error {
c.wsMu.Lock()
c.wsCallbacks[channel] = callback
c.wsMu.Unlock()
// Send subscription message
msg := map[string]interface{}{
"type": "subscribe",
"channel": channel,
@@ -429,13 +337,12 @@ func (c *Client) Subscribe(channel string, callback func(interface{})) error {
return c.wsConn.WriteJSON(msg)
}
// Unsubscribe unsubscribes from a WebSocket channel
// Unsubscribe unsubscribes from a WebSocket channel.
func (c *Client) Unsubscribe(channel string) error {
c.wsMu.Lock()
delete(c.wsCallbacks, channel)
c.wsMu.Unlock()
// Send unsubscription message
msg := map[string]interface{}{
"type": "unsubscribe",
"channel": channel,
@@ -451,26 +358,22 @@ func (c *Client) Unsubscribe(channel string) error {
return c.wsConn.WriteJSON(msg)
}
// SubscribeOrderBook subscribes to order book updates
// SubscribeOrderBook subscribes to order book updates.
func (c *Client) SubscribeOrderBook(symbol string, callback func(*OrderBook)) error {
return c.Subscribe(fmt.Sprintf("orderbook:%s", symbol), func(data interface{}) {
// Convert data to OrderBook
if m, ok := data.(map[string]interface{}); ok {
ob := &OrderBook{
Symbol: m["symbol"].(string),
Timestamp: int64(m["timestamp"].(float64)),
}
// Parse bids and asks
// ... conversion logic ...
callback(ob)
}
})
}
// SubscribeTrades subscribes to trade updates
// SubscribeTrades subscribes to trade updates.
func (c *Client) SubscribeTrades(symbol string, callback func(*Trade)) error {
return c.Subscribe(fmt.Sprintf("trades:%s", symbol), func(data interface{}) {
// Convert data to Trade
if m, ok := data.(map[string]interface{}); ok {
trade := &Trade{
TradeID: uint64(m["tradeId"].(float64)),
@@ -484,62 +387,13 @@ func (c *Client) SubscribeTrades(symbol string, callback func(*Trade)) error {
})
}
// StreamOrderBook streams order book updates via gRPC
// StreamOrderBook streams order book updates. Requires the `grpc`
// build tag; returns ErrGRPCNotBuilt otherwise.
func (c *Client) StreamOrderBook(ctx context.Context, symbol string) (<-chan *OrderBook, error) {
if c.grpcClient == nil {
return nil, fmt.Errorf("gRPC not connected")
}
stream, err := c.grpcClient.StreamOrderBook(ctx, &pb.StreamOrderBookRequest{
Symbol: symbol,
})
if err != nil {
return nil, err
}
ch := make(chan *OrderBook, 100)
go func() {
defer close(ch)
for {
update, err := stream.Recv()
if err != nil {
return
}
ob := &OrderBook{
Symbol: update.Symbol,
Timestamp: update.Timestamp,
Bids: make([]PriceLevel, len(update.GetBidUpdates())),
Asks: make([]PriceLevel, len(update.GetAskUpdates())),
}
for i, bid := range update.GetBidUpdates() {
ob.Bids[i] = PriceLevel{
Price: bid.Price,
Size: bid.Size,
}
}
for i, ask := range update.GetAskUpdates() {
ob.Asks[i] = PriceLevel{
Price: ask.Price,
Size: ask.Size,
}
}
select {
case ch <- ob:
case <-ctx.Done():
return
}
}
}()
return ch, nil
return c.grpc.streamOrderBook(ctx, symbol)
}
// callJSONRPC makes a JSON-RPC call
// callJSONRPC makes a JSON-RPC call.
func (c *Client) callJSONRPC(ctx context.Context, method string, params interface{}, result interface{}) error {
id := atomic.AddUint64(&c.idCounter, 1)
@@ -588,7 +442,6 @@ func (c *Client) callJSONRPC(ctx context.Context, method string, params interfac
return fmt.Errorf("RPC error %d: %s", response.Error.Code, response.Error.Message)
}
// Convert response.Result to the expected type
if result != nil {
resultBytes, _ := json.Marshal(response.Result)
return json.Unmarshal(resultBytes, result)
@@ -597,7 +450,7 @@ func (c *Client) callJSONRPC(ctx context.Context, method string, params interfac
return nil
}
// sendJSONRPCRequest sends a JSON-RPC request and returns the response
// sendJSONRPCRequest sends a JSON-RPC request and returns the response.
func (c *Client) sendJSONRPCRequest(ctx context.Context, req map[string]interface{}) (*struct {
Result json.RawMessage `json:"result"`
Error *struct {
@@ -647,7 +500,7 @@ func (c *Client) sendJSONRPCRequest(ctx context.Context, req map[string]interfac
return &response, nil
}
// sendWSMessage sends a message through the WebSocket connection
// sendWSMessage sends a message through the WebSocket connection.
func (c *Client) sendWSMessage(msg map[string]interface{}) error {
c.mu.RLock()
defer c.mu.RUnlock()
@@ -659,13 +512,13 @@ func (c *Client) sendWSMessage(msg map[string]interface{}) error {
return c.wsConn.WriteJSON(msg)
}
// Ping checks if the server is responsive
// Ping checks if the server is responsive.
func (c *Client) Ping(ctx context.Context) error {
var result string
return c.callJSONRPC(ctx, "lx_ping", nil, &result)
}
// GetInfo retrieves node information
// GetInfo retrieves node information.
func (c *Client) GetInfo(ctx context.Context) (*NodeInfo, error) {
var result NodeInfo
if err := c.callJSONRPC(ctx, "lx_getInfo", nil, &result); err != nil {
@@ -673,37 +526,3 @@ func (c *Client) GetInfo(ctx context.Context) (*NodeInfo, error) {
}
return &result, nil
}
// timeInForceToProto converts SDK TimeInForce to proto TimeInForce
func timeInForceToProto(tif TimeInForce) pb.TimeInForce {
switch tif {
case TimeInForceGTC:
return pb.TimeInForce_GTC
case TimeInForceIOC:
return pb.TimeInForce_IOC
case TimeInForceFOK:
return pb.TimeInForce_FOK
case TimeInForceDAY:
return pb.TimeInForce_DAY
default:
return pb.TimeInForce_GTC
}
}
// orderStatusFromProto converts proto OrderStatus to SDK string status
func orderStatusFromProto(status pb.OrderStatus) string {
switch status {
case pb.OrderStatus_OPEN:
return string(OrderStatusOpen)
case pb.OrderStatus_PARTIAL:
return string(OrderStatusPartial)
case pb.OrderStatus_FILLED:
return string(OrderStatusFilled)
case pb.OrderStatus_CANCELLED:
return string(OrderStatusCancelled)
case pb.OrderStatus_REJECTED:
return string(OrderStatusRejected)
default:
return string(OrderStatusOpen)
}
}
-30
View File
@@ -58,33 +58,3 @@ func TestClientDisconnect(t *testing.T) {
t.Errorf("Disconnect() error = %v", err)
}
}
func TestTimeInForceConversion(t *testing.T) {
tests := []struct {
name string
tif TimeInForce
}{
{"GTC", TimeInForceGTC},
{"IOC", TimeInForceIOC},
{"FOK", TimeInForceFOK},
{"DAY", TimeInForceDAY},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Just ensure it doesn't panic
result := timeInForceToProto(tt.tif)
if result < 0 || result > 3 {
t.Errorf("timeInForceToProto(%s) returned invalid value %d", tt.tif, result)
}
})
}
}
func TestTimeInForceToProtoDefault(t *testing.T) {
// Unknown value should default to GTC (0)
result := timeInForceToProto(TimeInForce("UNKNOWN"))
if result != 0 {
t.Errorf("Expected default to be 0 (GTC), got %d", result)
}
}
+30
View File
@@ -0,0 +1,30 @@
// Package client — gRPC backend interface.
//
// Always compiled. The default build provides a no-op implementation in
// grpc_backend_nogrpc.go; building with `-tags=grpc` substitutes the
// real implementation in grpc_backend_grpc.go.
package client
import (
"context"
"errors"
)
// ErrGRPCNotBuilt is returned when a gRPC-only method is invoked in
// the default build. Rebuild with `go build -tags=grpc` to enable
// gRPC support.
var ErrGRPCNotBuilt = errors.New("client: gRPC support requires the `grpc` build tag (rebuild with -tags=grpc)")
// grpcBackend abstracts the gRPC transport. The ok return is true when
// the backend handled the call; the JSON-RPC fallback in the public
// method is used when ok is false.
type grpcBackend interface {
connect(ctx context.Context, addr string) error
close() error
placeOrder(ctx context.Context, order *Order) (resp *OrderResponse, ok bool, err error)
cancelOrder(ctx context.Context, orderID uint64) (ok bool, err error)
getOrderBook(ctx context.Context, symbol string, depth int32) (ob *OrderBook, ok bool, err error)
getTrades(ctx context.Context, symbol string, limit int32) (trades []*Trade, ok bool, err error)
streamOrderBook(ctx context.Context, symbol string) (<-chan *OrderBook, error)
}
+203
View File
@@ -0,0 +1,203 @@
//go:build grpc
package client
import (
"context"
"fmt"
pb "github.com/luxfi/dex/pkg/grpc/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// newGRPCBackend returns the real gRPC-backed implementation.
func newGRPCBackend() grpcBackend { return &grpcImpl{} }
// grpcImpl wraps a generated LX DEX gRPC client.
type grpcImpl struct {
conn *grpc.ClientConn
client pb.LXDEXServiceClient
}
func (g *grpcImpl) connect(ctx context.Context, addr string) error {
conn, err := grpc.DialContext(ctx, addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
)
if err != nil {
return fmt.Errorf("failed to connect to gRPC: %w", err)
}
g.conn = conn
g.client = pb.NewLXDEXServiceClient(conn)
return nil
}
func (g *grpcImpl) close() error {
if g.conn != nil {
return g.conn.Close()
}
return nil
}
func (g *grpcImpl) placeOrder(ctx context.Context, order *Order) (*OrderResponse, bool, error) {
if g.client == nil {
return nil, false, nil
}
req := &pb.PlaceOrderRequest{
Symbol: order.Symbol,
Type: pb.OrderType(order.Type),
Side: pb.OrderSide(order.Side),
Price: order.Price,
Size: order.Size,
UserId: order.UserID,
ClientId: order.ClientID,
TimeInForce: timeInForceToProto(order.TimeInForce),
PostOnly: order.PostOnly,
ReduceOnly: order.ReduceOnly,
}
resp, err := g.client.PlaceOrder(ctx, req)
if err != nil {
return nil, true, err
}
return &OrderResponse{
OrderID: resp.OrderId,
Status: orderStatusFromProto(resp.Status),
Message: resp.Message,
}, true, nil
}
func (g *grpcImpl) cancelOrder(ctx context.Context, orderID uint64) (bool, error) {
if g.client == nil {
return false, nil
}
_, err := g.client.CancelOrder(ctx, &pb.CancelOrderRequest{OrderId: orderID})
return true, err
}
func (g *grpcImpl) getOrderBook(ctx context.Context, symbol string, depth int32) (*OrderBook, bool, error) {
if g.client == nil {
return nil, false, nil
}
resp, err := g.client.GetOrderBook(ctx, &pb.GetOrderBookRequest{
Symbol: symbol,
Depth: depth,
})
if err != nil {
return nil, true, err
}
ob := &OrderBook{
Symbol: resp.Symbol,
Timestamp: resp.Timestamp,
Bids: make([]PriceLevel, len(resp.Bids)),
Asks: make([]PriceLevel, len(resp.Asks)),
}
for i, bid := range resp.Bids {
ob.Bids[i] = PriceLevel{Price: bid.Price, Size: bid.Size}
}
for i, ask := range resp.Asks {
ob.Asks[i] = PriceLevel{Price: ask.Price, Size: ask.Size}
}
return ob, true, nil
}
func (g *grpcImpl) getTrades(ctx context.Context, symbol string, limit int32) ([]*Trade, bool, error) {
if g.client == nil {
return nil, false, nil
}
resp, err := g.client.GetTrades(ctx, &pb.GetTradesRequest{
Symbol: symbol,
Limit: limit,
})
if err != nil {
return nil, true, err
}
trades := make([]*Trade, len(resp.Trades))
for i, t := range resp.Trades {
trades[i] = &Trade{
TradeID: t.TradeId,
Symbol: t.Symbol,
Price: t.Price,
Size: t.Size,
Side: OrderSide(t.Side),
BuyOrderID: t.BuyOrderId,
SellOrderID: t.SellOrderId,
BuyerID: t.BuyerId,
SellerID: t.SellerId,
Timestamp: t.Timestamp,
}
}
return trades, true, nil
}
func (g *grpcImpl) streamOrderBook(ctx context.Context, symbol string) (<-chan *OrderBook, error) {
if g.client == nil {
return nil, fmt.Errorf("gRPC not connected")
}
stream, err := g.client.StreamOrderBook(ctx, &pb.StreamOrderBookRequest{Symbol: symbol})
if err != nil {
return nil, err
}
ch := make(chan *OrderBook, 100)
go func() {
defer close(ch)
for {
update, err := stream.Recv()
if err != nil {
return
}
ob := &OrderBook{
Symbol: update.Symbol,
Timestamp: update.Timestamp,
Bids: make([]PriceLevel, len(update.GetBidUpdates())),
Asks: make([]PriceLevel, len(update.GetAskUpdates())),
}
for i, bid := range update.GetBidUpdates() {
ob.Bids[i] = PriceLevel{Price: bid.Price, Size: bid.Size}
}
for i, ask := range update.GetAskUpdates() {
ob.Asks[i] = PriceLevel{Price: ask.Price, Size: ask.Size}
}
select {
case ch <- ob:
case <-ctx.Done():
return
}
}
}()
return ch, nil
}
// timeInForceToProto converts SDK TimeInForce to proto TimeInForce.
func timeInForceToProto(tif TimeInForce) pb.TimeInForce {
switch tif {
case TimeInForceGTC:
return pb.TimeInForce_GTC
case TimeInForceIOC:
return pb.TimeInForce_IOC
case TimeInForceFOK:
return pb.TimeInForce_FOK
case TimeInForceDAY:
return pb.TimeInForce_DAY
default:
return pb.TimeInForce_GTC
}
}
// orderStatusFromProto converts proto OrderStatus to SDK string status.
func orderStatusFromProto(status pb.OrderStatus) string {
switch status {
case pb.OrderStatus_OPEN:
return string(OrderStatusOpen)
case pb.OrderStatus_PARTIAL:
return string(OrderStatusPartial)
case pb.OrderStatus_FILLED:
return string(OrderStatusFilled)
case pb.OrderStatus_CANCELLED:
return string(OrderStatusCancelled)
case pb.OrderStatus_REJECTED:
return string(OrderStatusRejected)
default:
return string(OrderStatusOpen)
}
}
+35
View File
@@ -0,0 +1,35 @@
//go:build grpc
package client
import "testing"
func TestTimeInForceConversion(t *testing.T) {
tests := []struct {
name string
tif TimeInForce
}{
{"GTC", TimeInForceGTC},
{"IOC", TimeInForceIOC},
{"FOK", TimeInForceFOK},
{"DAY", TimeInForceDAY},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Just ensure it doesn't panic
result := timeInForceToProto(tt.tif)
if result < 0 || result > 3 {
t.Errorf("timeInForceToProto(%s) returned invalid value %d", tt.tif, result)
}
})
}
}
func TestTimeInForceToProtoDefault(t *testing.T) {
// Unknown value should default to GTC (0)
result := timeInForceToProto(TimeInForce("UNKNOWN"))
if result != 0 {
t.Errorf("Expected default to be 0 (GTC), got %d", result)
}
}
+33
View File
@@ -0,0 +1,33 @@
//go:build !grpc
package client
import "context"
// newGRPCBackend returns the no-op backend used in the default build.
func newGRPCBackend() grpcBackend { return noopGRPC{} }
// noopGRPC is the default-build backend. Every method reports
// "not handled" so the JSON-RPC fallback runs. ConnectGRPC and
// StreamOrderBook return ErrGRPCNotBuilt to make the missing
// capability explicit.
type noopGRPC struct{}
func (noopGRPC) connect(ctx context.Context, addr string) error { return ErrGRPCNotBuilt }
func (noopGRPC) close() error { return nil }
func (noopGRPC) placeOrder(ctx context.Context, order *Order) (*OrderResponse, bool, error) {
return nil, false, nil
}
func (noopGRPC) cancelOrder(ctx context.Context, orderID uint64) (bool, error) {
return false, nil
}
func (noopGRPC) getOrderBook(ctx context.Context, symbol string, depth int32) (*OrderBook, bool, error) {
return nil, false, nil
}
func (noopGRPC) getTrades(ctx context.Context, symbol string, limit int32) ([]*Trade, bool, error) {
return nil, false, nil
}
func (noopGRPC) streamOrderBook(ctx context.Context, symbol string) (<-chan *OrderBook, error) {
return nil, ErrGRPCNotBuilt
}
+133
View File
@@ -0,0 +1,133 @@
//go:build !grpc
// Package lxsdk is the gRPC-backed Go SDK for the LX trading platform.
//
// In the default build (no `grpc` build tag), gRPC support is excluded
// so consumers compile zero gRPC code. NewClient and NewHighFrequencyClient
// return an explanatory error. Rebuild with `-tags=grpc` to get the
// real client.
//
// External trading clients that do not need the binary gRPC transport
// should use the JSON-RPC + WebSocket SDK at
// github.com/luxfi/dex/sdk/go/client instead.
package lxsdk
import (
"errors"
"sync"
"sync/atomic"
"time"
)
// ErrGRPCNotBuilt is returned when the SDK is used without the `grpc`
// build tag. Rebuild with `go build -tags=grpc` to enable.
var ErrGRPCNotBuilt = errors.New("lxsdk: gRPC client requires the `grpc` build tag (rebuild with -tags=grpc)")
// Side represents order side.
type Side int
const (
Buy Side = iota
Sell
)
// OrderType represents order type.
type OrderType int
const (
Market OrderType = iota
Limit
Stop
StopLimit
)
// Order represents a trading order.
type Order struct {
ID string
Symbol string
Price float64
Quantity float64
Side Side
Type OrderType
Timestamp time.Time
}
// Trade represents an executed trade.
type Trade struct {
ID string
OrderID string
Symbol string
Price float64
Quantity float64
Side Side
Timestamp time.Time
}
// Client is the no-op placeholder for the gRPC client. All operations
// return ErrGRPCNotBuilt.
type Client struct {
address string
// Match the gRPC build's exported observability surface so consumers
// compile against both builds.
ordersSent atomic.Int64
tradesRecv atomic.Int64
latencyNanos atomic.Int64
mu sync.RWMutex
}
// NewClient returns an error in the default build. Rebuild with `-tags=grpc`.
func NewClient(address string) (*Client, error) {
return nil, ErrGRPCNotBuilt
}
// EnableFIX is a no-op stub.
func (c *Client) EnableFIX(senderID, targetID string) {}
// SendOrder returns ErrGRPCNotBuilt.
func (c *Client) SendOrder(symbol string, price, quantity float64, side Side) (*Order, error) {
return nil, ErrGRPCNotBuilt
}
// SendBulkOrders returns ErrGRPCNotBuilt.
func (c *Client) SendBulkOrders(orders []*Order) error { return ErrGRPCNotBuilt }
// CancelOrder returns ErrGRPCNotBuilt.
func (c *Client) CancelOrder(orderID string) error { return ErrGRPCNotBuilt }
// Subscribe returns ErrGRPCNotBuilt.
func (c *Client) Subscribe(symbols []string, callback func(*Trade)) error { return ErrGRPCNotBuilt }
// GetOrderBook returns ErrGRPCNotBuilt.
func (c *Client) GetOrderBook(symbol string) (bids, asks []Order, err error) {
return nil, nil, ErrGRPCNotBuilt
}
// GetMetrics returns zeroed metrics.
func (c *Client) GetMetrics() map[string]interface{} {
return map[string]interface{}{
"orders_sent": c.ordersSent.Load(),
"trades_received": c.tradesRecv.Load(),
"latency_ns": c.latencyNanos.Load(),
"latency_ms": float64(c.latencyNanos.Load()) / 1e6,
}
}
// GetThroughput returns 0.
func (c *Client) GetThroughput() float64 { return 0 }
// Close is a no-op.
func (c *Client) Close() error { return nil }
// HighFrequencyClient placeholder.
type HighFrequencyClient struct{ *Client }
// NewHighFrequencyClient returns ErrGRPCNotBuilt.
func NewHighFrequencyClient(address string) (*HighFrequencyClient, error) {
return nil, ErrGRPCNotBuilt
}
// SendOrderAsync is a no-op.
func (hft *HighFrequencyClient) SendOrderAsync(symbol string, price, quantity float64, side Side) {
}
+11 -9
View File
@@ -1,20 +1,22 @@
module github.com/luxfi/dex/sdk/go
go 1.24.5
go 1.26.3
// google.golang.org/grpc is only compiled when the `grpc` build tag is
// set. Default builds (JSON-RPC + WebSocket) pull zero gRPC code.
require (
github.com/gorilla/websocket v1.5.3
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/luxfi/dex v0.0.0
google.golang.org/grpc v1.74.2
google.golang.org/grpc v1.79.1
)
require (
go.opentelemetry.io/otel v1.39.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 // indirect
google.golang.org/protobuf v1.36.7 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/protobuf v1.36.11 // indirect
)
replace github.com/luxfi/dex => ../../
+26 -24
View File
@@ -10,29 +10,31 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 h1:qJW29YvkiJmXOYMu5Tf8lyrTp3dOS+K4z6IixtLaCf8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=