mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
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:
+23
-4
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user