Files
zeekay 0d72ccf5a3 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.
2026-05-16 17:54:11 -07:00

282 lines
6.4 KiB
Go

//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
}
}