synor/sdk/go/dex/client.go
Gulshan Yadav e7dc8f70a0 feat(sdk): implement DEX SDK with perpetual futures for all 12 languages
Complete decentralized exchange client implementation featuring:
- AMM swaps (constant product, stable, concentrated liquidity)
- Liquidity provision with impermanent loss tracking
- Perpetual futures (up to 100x leverage, funding rates, liquidation)
- Order books with limit orders (GTC, IOC, FOK, GTD)
- Yield farming & staking with reward claiming
- Real-time WebSocket subscriptions
- Analytics (OHLCV, trade history, volume, TVL)

Languages: JS/TS, Python, Go, Rust, Java, Kotlin, Swift, Flutter,
C, C++, C#, Ruby
2026-01-28 12:32:04 +05:30

805 lines
20 KiB
Go

// 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, &quote)
return &quote, 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<<attempt) * time.Second)
continue
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
lastErr = err
continue
}
if resp.StatusCode >= 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
}