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
805 lines
20 KiB
Go
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, "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<<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
|
|
}
|