// Package dex provides the Synor DEX SDK for Go package dex import ( "bytes" "context" "encoding/json" "fmt" "io" "math/big" "net/http" "net/url" "strconv" "sync" "time" "github.com/google/uuid" "github.com/gorilla/websocket" ) // SynorDex is the main DEX client type SynorDex struct { config Config client *http.Client closed bool ws *websocket.Conn wsMu sync.Mutex subscriptions map[string]func([]byte) subMu sync.RWMutex // Sub-clients Perps *PerpsClient OrderBook *OrderBookClient Farms *FarmsClient } // New creates a new SynorDex client func New(config Config) *SynorDex { if config.Endpoint == "" { config.Endpoint = "https://dex.synor.io/v1" } if config.WSEndpoint == "" { config.WSEndpoint = "wss://dex.synor.io/v1/ws" } if config.Timeout == 0 { config.Timeout = 30 * time.Second } if config.Retries == 0 { config.Retries = 3 } dex := &SynorDex{ config: config, client: &http.Client{ Timeout: config.Timeout, }, subscriptions: make(map[string]func([]byte)), } dex.Perps = &PerpsClient{dex: dex} dex.OrderBook = &OrderBookClient{dex: dex} dex.Farms = &FarmsClient{dex: dex} return dex } // Token Operations // GetToken gets token information func (d *SynorDex) GetToken(ctx context.Context, address string) (*Token, error) { var token Token err := d.get(ctx, "/tokens/"+address, &token) return &token, err } // ListTokens lists all tokens func (d *SynorDex) ListTokens(ctx context.Context) ([]Token, error) { var tokens []Token err := d.get(ctx, "/tokens", &tokens) return tokens, err } // SearchTokens searches for tokens func (d *SynorDex) SearchTokens(ctx context.Context, query string) ([]Token, error) { var tokens []Token err := d.get(ctx, "/tokens/search?q="+url.QueryEscape(query), &tokens) return tokens, err } // Pool Operations // GetPool gets a pool by token pair func (d *SynorDex) GetPool(ctx context.Context, tokenA, tokenB string) (*Pool, error) { var pool Pool err := d.get(ctx, fmt.Sprintf("/pools/%s/%s", tokenA, tokenB), &pool) return &pool, err } // GetPoolByID gets a pool by ID func (d *SynorDex) GetPoolByID(ctx context.Context, poolID string) (*Pool, error) { var pool Pool err := d.get(ctx, "/pools/"+poolID, &pool) return &pool, err } // ListPools lists pools with optional filtering func (d *SynorDex) ListPools(ctx context.Context, filter *PoolFilter) ([]Pool, error) { params := url.Values{} if filter != nil { if len(filter.Tokens) > 0 { for _, t := range filter.Tokens { params.Add("tokens", t) } } if filter.MinTVL != nil { params.Set("min_tvl", strconv.FormatFloat(*filter.MinTVL, 'f', -1, 64)) } if filter.MinVolume24h != nil { params.Set("min_volume", strconv.FormatFloat(*filter.MinVolume24h, 'f', -1, 64)) } if filter.Verified != nil { params.Set("verified", strconv.FormatBool(*filter.Verified)) } if filter.Limit != nil { params.Set("limit", strconv.Itoa(*filter.Limit)) } if filter.Offset != nil { params.Set("offset", strconv.Itoa(*filter.Offset)) } } var pools []Pool path := "/pools" if len(params) > 0 { path += "?" + params.Encode() } err := d.get(ctx, path, &pools) return pools, err } // Swap Operations // GetQuote gets a swap quote func (d *SynorDex) GetQuote(ctx context.Context, params QuoteParams) (*Quote, error) { slippage := params.Slippage if slippage == 0 { slippage = 0.005 } body := map[string]interface{}{ "token_in": params.TokenIn, "token_out": params.TokenOut, "amount_in": params.AmountIn.String(), "slippage": slippage, } var quote Quote err := d.post(ctx, "/swap/quote", body, "e) return "e, err } // Swap executes a swap func (d *SynorDex) Swap(ctx context.Context, params SwapParams) (*SwapResult, error) { deadline := time.Now().Unix() + 1200 if params.Deadline != nil { deadline = *params.Deadline } body := map[string]interface{}{ "token_in": params.TokenIn, "token_out": params.TokenOut, "amount_in": params.AmountIn.String(), "min_amount_out": params.MinAmountOut.String(), "deadline": deadline, } if params.Recipient != nil { body["recipient"] = *params.Recipient } var result SwapResult err := d.post(ctx, "/swap", body, &result) return &result, err } // Liquidity Operations // AddLiquidity adds liquidity to a pool func (d *SynorDex) AddLiquidity(ctx context.Context, params AddLiquidityParams) (*LiquidityResult, error) { deadline := time.Now().Unix() + 1200 if params.Deadline != nil { deadline = *params.Deadline } body := map[string]interface{}{ "token_a": params.TokenA, "token_b": params.TokenB, "amount_a": params.AmountA.String(), "amount_b": params.AmountB.String(), "deadline": deadline, } if params.MinAmountA != nil { body["min_amount_a"] = params.MinAmountA.String() } if params.MinAmountB != nil { body["min_amount_b"] = params.MinAmountB.String() } var result LiquidityResult err := d.post(ctx, "/liquidity/add", body, &result) return &result, err } // RemoveLiquidity removes liquidity from a pool func (d *SynorDex) RemoveLiquidity(ctx context.Context, params RemoveLiquidityParams) (*LiquidityResult, error) { deadline := time.Now().Unix() + 1200 if params.Deadline != nil { deadline = *params.Deadline } body := map[string]interface{}{ "pool": params.Pool, "lp_amount": params.LPAmount.String(), "deadline": deadline, } if params.MinAmountA != nil { body["min_amount_a"] = params.MinAmountA.String() } if params.MinAmountB != nil { body["min_amount_b"] = params.MinAmountB.String() } var result LiquidityResult err := d.post(ctx, "/liquidity/remove", body, &result) return &result, err } // GetMyPositions gets all LP positions func (d *SynorDex) GetMyPositions(ctx context.Context) ([]LPPosition, error) { var positions []LPPosition err := d.get(ctx, "/liquidity/positions", &positions) return positions, err } // Analytics // GetPriceHistory gets price history (OHLCV candles) func (d *SynorDex) GetPriceHistory(ctx context.Context, pair, interval string, limit int) ([]OHLCV, error) { var candles []OHLCV path := fmt.Sprintf("/analytics/candles/%s?interval=%s&limit=%d", pair, interval, limit) err := d.get(ctx, path, &candles) return candles, err } // GetTradeHistory gets trade history func (d *SynorDex) GetTradeHistory(ctx context.Context, pair string, limit int) ([]TradeHistory, error) { var trades []TradeHistory path := fmt.Sprintf("/analytics/trades/%s?limit=%d", pair, limit) err := d.get(ctx, path, &trades) return trades, err } // GetVolumeStats gets volume statistics func (d *SynorDex) GetVolumeStats(ctx context.Context) (*VolumeStats, error) { var stats VolumeStats err := d.get(ctx, "/analytics/volume", &stats) return &stats, err } // GetTVL gets TVL statistics func (d *SynorDex) GetTVL(ctx context.Context) (*TVLStats, error) { var stats TVLStats err := d.get(ctx, "/analytics/tvl", &stats) return &stats, err } // Subscriptions // SubscribePrice subscribes to price updates func (d *SynorDex) SubscribePrice(market string, callback PriceCallback) (*Subscription, error) { return d.subscribe("price", map[string]interface{}{"market": market}, func(data []byte) { var price float64 if err := json.Unmarshal(data, &price); err == nil { callback(price) } }) } // SubscribeTrades subscribes to trade updates func (d *SynorDex) SubscribeTrades(market string, callback TradeCallback) (*Subscription, error) { return d.subscribe("trades", map[string]interface{}{"market": market}, func(data []byte) { var trade TradeHistory if err := json.Unmarshal(data, &trade); err == nil { callback(trade) } }) } // SubscribeOrderBook subscribes to order book updates func (d *SynorDex) SubscribeOrderBook(market string, callback OrderBookCallback) (*Subscription, error) { return d.subscribe("orderbook", map[string]interface{}{"market": market}, func(data []byte) { var book OrderBook if err := json.Unmarshal(data, &book); err == nil { callback(book) } }) } // Lifecycle // HealthCheck checks if the service is healthy func (d *SynorDex) HealthCheck(ctx context.Context) bool { var result struct { Status string `json:"status"` } if err := d.get(ctx, "/health", &result); err != nil { return false } return result.Status == "healthy" } // Close closes the client func (d *SynorDex) Close() error { d.closed = true d.wsMu.Lock() defer d.wsMu.Unlock() if d.ws != nil { err := d.ws.Close() d.ws = nil return err } return nil } // Internal methods func (d *SynorDex) get(ctx context.Context, path string, result interface{}) error { return d.request(ctx, "GET", path, nil, result) } func (d *SynorDex) post(ctx context.Context, path string, body interface{}, result interface{}) error { return d.request(ctx, "POST", path, body, result) } func (d *SynorDex) delete(ctx context.Context, path string, result interface{}) error { return d.request(ctx, "DELETE", path, nil, result) } func (d *SynorDex) request(ctx context.Context, method, path string, body interface{}, result interface{}) error { if d.closed { return NewDexError("Client has been closed", "CLIENT_CLOSED", 0) } var lastErr error for attempt := 0; attempt < d.config.Retries; attempt++ { var bodyReader io.Reader if body != nil { jsonBody, err := json.Marshal(body) if err != nil { return err } bodyReader = bytes.NewReader(jsonBody) } req, err := http.NewRequestWithContext(ctx, method, d.config.Endpoint+path, bodyReader) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+d.config.APIKey) req.Header.Set("X-SDK-Version", "go/0.1.0") resp, err := d.client.Do(req) if err != nil { lastErr = err if d.config.Debug { fmt.Printf("Attempt %d failed: %v\n", attempt+1, err) } time.Sleep(time.Duration(1<= 400 { var errResp struct { Message string `json:"message"` Code string `json:"code"` } json.Unmarshal(respBody, &errResp) return NewDexError( errResp.Message, errResp.Code, resp.StatusCode, ) } if result != nil { return json.Unmarshal(respBody, result) } return nil } if lastErr != nil { return lastErr } return NewDexError("Unknown error", "", 0) } func (d *SynorDex) subscribe(channel string, params map[string]interface{}, callback func([]byte)) (*Subscription, error) { if err := d.ensureWebSocket(); err != nil { return nil, err } subscriptionID := uuid.New().String()[:8] d.subMu.Lock() d.subscriptions[subscriptionID] = callback d.subMu.Unlock() msg := map[string]interface{}{ "type": "subscribe", "channel": channel, "subscription_id": subscriptionID, } for k, v := range params { msg[k] = v } d.wsMu.Lock() err := d.ws.WriteJSON(msg) d.wsMu.Unlock() if err != nil { return nil, err } return &Subscription{ ID: subscriptionID, Channel: channel, Cancel: func() { d.subMu.Lock() delete(d.subscriptions, subscriptionID) d.subMu.Unlock() d.wsMu.Lock() if d.ws != nil { d.ws.WriteJSON(map[string]interface{}{ "type": "unsubscribe", "subscription_id": subscriptionID, }) } d.wsMu.Unlock() }, }, nil } func (d *SynorDex) ensureWebSocket() error { d.wsMu.Lock() defer d.wsMu.Unlock() if d.ws != nil { return nil } conn, _, err := websocket.DefaultDialer.Dial(d.config.WSEndpoint, nil) if err != nil { return err } d.ws = conn // Authenticate err = d.ws.WriteJSON(map[string]interface{}{ "type": "auth", "api_key": d.config.APIKey, }) if err != nil { return err } // Start message handler go d.handleMessages() return nil } func (d *SynorDex) handleMessages() { for { d.wsMu.Lock() ws := d.ws d.wsMu.Unlock() if ws == nil { return } _, message, err := ws.ReadMessage() if err != nil { return } var msg struct { SubscriptionID string `json:"subscription_id"` Data json.RawMessage `json:"data"` } if err := json.Unmarshal(message, &msg); err != nil { continue } if msg.SubscriptionID != "" { d.subMu.RLock() callback, ok := d.subscriptions[msg.SubscriptionID] d.subMu.RUnlock() if ok { callback(msg.Data) } } } } // PerpsClient handles perpetual futures operations type PerpsClient struct { dex *SynorDex } // ListMarkets lists all perpetual markets func (p *PerpsClient) ListMarkets(ctx context.Context) ([]PerpMarket, error) { var markets []PerpMarket err := p.dex.get(ctx, "/perps/markets", &markets) return markets, err } // GetMarket gets a specific perpetual market func (p *PerpsClient) GetMarket(ctx context.Context, symbol string) (*PerpMarket, error) { var market PerpMarket err := p.dex.get(ctx, "/perps/markets/"+symbol, &market) return &market, err } // OpenPosition opens a perpetual position func (p *PerpsClient) OpenPosition(ctx context.Context, params OpenPositionParams) (*PerpPosition, error) { marginType := params.MarginType if marginType == "" { marginType = MarginTypeCross } body := map[string]interface{}{ "market": params.Market, "side": string(params.Side), "size": params.Size.String(), "leverage": params.Leverage, "order_type": string(params.OrderType), "margin_type": string(marginType), "reduce_only": params.ReduceOnly, } if params.LimitPrice != nil { body["limit_price"] = *params.LimitPrice } if params.StopLoss != nil { body["stop_loss"] = *params.StopLoss } if params.TakeProfit != nil { body["take_profit"] = *params.TakeProfit } var position PerpPosition err := p.dex.post(ctx, "/perps/positions", body, &position) return &position, err } // ClosePosition closes a perpetual position func (p *PerpsClient) ClosePosition(ctx context.Context, params ClosePositionParams) (*PerpPosition, error) { body := map[string]interface{}{ "market": params.Market, "order_type": string(params.OrderType), } if params.Size != nil { body["size"] = params.Size.String() } if params.LimitPrice != nil { body["limit_price"] = *params.LimitPrice } var position PerpPosition err := p.dex.post(ctx, "/perps/positions/close", body, &position) return &position, err } // ModifyPosition modifies a perpetual position func (p *PerpsClient) ModifyPosition(ctx context.Context, params ModifyPositionParams) (*PerpPosition, error) { body := map[string]interface{}{} if params.NewLeverage != nil { body["new_leverage"] = *params.NewLeverage } if params.NewMargin != nil { body["new_margin"] = params.NewMargin.String() } if params.NewStopLoss != nil { body["new_stop_loss"] = *params.NewStopLoss } if params.NewTakeProfit != nil { body["new_take_profit"] = *params.NewTakeProfit } var position PerpPosition err := p.dex.post(ctx, "/perps/positions/"+params.PositionID+"/modify", body, &position) return &position, err } // GetPositions gets all open positions func (p *PerpsClient) GetPositions(ctx context.Context) ([]PerpPosition, error) { var positions []PerpPosition err := p.dex.get(ctx, "/perps/positions", &positions) return positions, err } // GetPosition gets position for a specific market func (p *PerpsClient) GetPosition(ctx context.Context, market string) (*PerpPosition, error) { var position PerpPosition err := p.dex.get(ctx, "/perps/positions/"+market, &position) return &position, err } // GetOrders gets all open orders func (p *PerpsClient) GetOrders(ctx context.Context) ([]PerpOrder, error) { var orders []PerpOrder err := p.dex.get(ctx, "/perps/orders", &orders) return orders, err } // CancelOrder cancels an order func (p *PerpsClient) CancelOrder(ctx context.Context, orderID string) error { return p.dex.delete(ctx, "/perps/orders/"+orderID, nil) } // CancelAllOrders cancels all orders func (p *PerpsClient) CancelAllOrders(ctx context.Context, market *string) (int, error) { path := "/perps/orders" if market != nil { path += "?market=" + *market } var result struct { Cancelled int `json:"cancelled"` } err := p.dex.delete(ctx, path, &result) return result.Cancelled, err } // GetFundingHistory gets funding payment history func (p *PerpsClient) GetFundingHistory(ctx context.Context, market string, limit int) ([]FundingPayment, error) { var payments []FundingPayment path := fmt.Sprintf("/perps/funding/%s?limit=%d", market, limit) err := p.dex.get(ctx, path, &payments) return payments, err } // GetFundingRate gets current funding rate func (p *PerpsClient) GetFundingRate(ctx context.Context, market string) (*struct { Rate float64 `json:"rate"` NextTime int64 `json:"next_time"` }, error) { var result struct { Rate float64 `json:"rate"` NextTime int64 `json:"next_time"` } err := p.dex.get(ctx, "/perps/funding/"+market+"/current", &result) return &result, err } // SubscribePosition subscribes to position updates func (p *PerpsClient) SubscribePosition(callback PositionCallback) (*Subscription, error) { return p.dex.subscribe("position", map[string]interface{}{}, func(data []byte) { var position PerpPosition if err := json.Unmarshal(data, &position); err == nil { callback(position) } }) } // OrderBookClient handles order book operations type OrderBookClient struct { dex *SynorDex } // GetOrderBook gets order book for a market func (o *OrderBookClient) GetOrderBook(ctx context.Context, market string, depth int) (*OrderBook, error) { var book OrderBook path := fmt.Sprintf("/orderbook/%s?depth=%d", market, depth) err := o.dex.get(ctx, path, &book) return &book, err } // PlaceLimitOrder places a limit order func (o *OrderBookClient) PlaceLimitOrder(ctx context.Context, params LimitOrderParams) (*Order, error) { tif := params.TimeInForce if tif == "" { tif = TimeInForceGTC } body := map[string]interface{}{ "market": params.Market, "side": params.Side, "price": params.Price, "size": params.Size.String(), "time_in_force": string(tif), "post_only": params.PostOnly, } var order Order err := o.dex.post(ctx, "/orderbook/orders", body, &order) return &order, err } // CancelOrder cancels an order func (o *OrderBookClient) CancelOrder(ctx context.Context, orderID string) error { return o.dex.delete(ctx, "/orderbook/orders/"+orderID, nil) } // GetOpenOrders gets all open orders func (o *OrderBookClient) GetOpenOrders(ctx context.Context, market *string) ([]Order, error) { path := "/orderbook/orders" if market != nil { path += "?market=" + *market } var orders []Order err := o.dex.get(ctx, path, &orders) return orders, err } // GetOrderHistory gets order history func (o *OrderBookClient) GetOrderHistory(ctx context.Context, limit int) ([]Order, error) { var orders []Order path := fmt.Sprintf("/orderbook/orders/history?limit=%d", limit) err := o.dex.get(ctx, path, &orders) return orders, err } // FarmsClient handles farming operations type FarmsClient struct { dex *SynorDex } // ListFarms lists all farms func (f *FarmsClient) ListFarms(ctx context.Context) ([]Farm, error) { var farms []Farm err := f.dex.get(ctx, "/farms", &farms) return farms, err } // GetFarm gets a specific farm func (f *FarmsClient) GetFarm(ctx context.Context, farmID string) (*Farm, error) { var farm Farm err := f.dex.get(ctx, "/farms/"+farmID, &farm) return &farm, err } // Stake stakes tokens in a farm func (f *FarmsClient) Stake(ctx context.Context, params StakeParams) (*FarmPosition, error) { body := map[string]interface{}{ "farm": params.Farm, "amount": params.Amount.String(), } var position FarmPosition err := f.dex.post(ctx, "/farms/stake", body, &position) return &position, err } // Unstake unstakes tokens from a farm func (f *FarmsClient) Unstake(ctx context.Context, farm string, amount *big.Int) (*FarmPosition, error) { body := map[string]interface{}{ "farm": farm, "amount": amount.String(), } var position FarmPosition err := f.dex.post(ctx, "/farms/unstake", body, &position) return &position, err } // ClaimRewards claims rewards from a farm func (f *FarmsClient) ClaimRewards(ctx context.Context, farm string) (*struct { Amount *big.Int `json:"amount"` TransactionHash string `json:"transaction_hash"` }, error) { body := map[string]interface{}{ "farm": farm, } var result struct { Amount *big.Int `json:"amount"` TransactionHash string `json:"transaction_hash"` } err := f.dex.post(ctx, "/farms/claim", body, &result) return &result, err } // GetMyFarmPositions gets all farm positions func (f *FarmsClient) GetMyFarmPositions(ctx context.Context) ([]FarmPosition, error) { var positions []FarmPosition err := f.dex.get(ctx, "/farms/positions", &positions) return positions, err }