synor/sdk/ruby/lib/synor/dex.rb
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

392 lines
10 KiB
Ruby

# frozen_string_literal: true
require 'net/http'
require 'uri'
require 'json'
require 'websocket-client-simple'
module Synor
# DEX SDK for Ruby
#
# Complete decentralized exchange client with support for:
# - AMM swaps (constant product, stable, concentrated)
# - Liquidity provision
# - Perpetual futures (up to 100x leverage)
# - Order books (limit orders)
# - Farming & staking
module Dex
VERSION = '0.1.0'
# DEX configuration
class Config
attr_accessor :api_key, :endpoint, :ws_endpoint, :timeout, :retries, :debug
def initialize(
api_key:,
endpoint: 'https://dex.synor.io/v1',
ws_endpoint: 'wss://dex.synor.io/v1/ws',
timeout: 30,
retries: 3,
debug: false
)
@api_key = api_key
@endpoint = endpoint
@ws_endpoint = ws_endpoint
@timeout = timeout
@retries = retries
@debug = debug
end
end
# DEX exception
class DexError < StandardError
attr_reader :code, :status
def initialize(message, code: nil, status: nil)
super(message)
@code = code
@status = status
end
end
# Main DEX client
class Client
attr_reader :perps, :orderbook, :farms
def initialize(config)
@config = config
@closed = false
@perps = PerpsClient.new(self)
@orderbook = OrderBookClient.new(self)
@farms = FarmsClient.new(self)
end
# Token Operations
def get_token(address)
get("/tokens/#{address}")
end
def list_tokens
get('/tokens')
end
def search_tokens(query)
get("/tokens/search?q=#{URI.encode_www_form_component(query)}")
end
# Pool Operations
def get_pool(token_a, token_b)
get("/pools/#{token_a}/#{token_b}")
end
def get_pool_by_id(pool_id)
get("/pools/#{pool_id}")
end
def list_pools(filter = nil)
params = []
if filter
params << "tokens=#{filter[:tokens].join(',')}" if filter[:tokens]
params << "min_tvl=#{filter[:min_tvl]}" if filter[:min_tvl]
params << "min_volume=#{filter[:min_volume_24h]}" if filter[:min_volume_24h]
params << "verified=#{filter[:verified]}" unless filter[:verified].nil?
params << "limit=#{filter[:limit]}" if filter[:limit]
params << "offset=#{filter[:offset]}" if filter[:offset]
end
path = params.empty? ? '/pools' : "/pools?#{params.join('&')}"
get(path)
end
# Swap Operations
def get_quote(params)
post('/swap/quote', {
token_in: params[:token_in],
token_out: params[:token_out],
amount_in: params[:amount_in].to_s,
slippage: params[:slippage] || 0.005
})
end
def swap(params)
deadline = params[:deadline] || (Time.now.to_i + 1200)
body = {
token_in: params[:token_in],
token_out: params[:token_out],
amount_in: params[:amount_in].to_s,
min_amount_out: params[:min_amount_out].to_s,
deadline: deadline
}
body[:recipient] = params[:recipient] if params[:recipient]
post('/swap', body)
end
# Liquidity Operations
def add_liquidity(params)
deadline = params[:deadline] || (Time.now.to_i + 1200)
body = {
token_a: params[:token_a],
token_b: params[:token_b],
amount_a: params[:amount_a].to_s,
amount_b: params[:amount_b].to_s,
deadline: deadline
}
body[:min_amount_a] = params[:min_amount_a].to_s if params[:min_amount_a]
body[:min_amount_b] = params[:min_amount_b].to_s if params[:min_amount_b]
post('/liquidity/add', body)
end
def remove_liquidity(params)
deadline = params[:deadline] || (Time.now.to_i + 1200)
body = {
pool: params[:pool],
lp_amount: params[:lp_amount].to_s,
deadline: deadline
}
body[:min_amount_a] = params[:min_amount_a].to_s if params[:min_amount_a]
body[:min_amount_b] = params[:min_amount_b].to_s if params[:min_amount_b]
post('/liquidity/remove', body)
end
def get_my_positions
get('/liquidity/positions')
end
# Analytics
def get_price_history(pair, interval, limit: 100)
get("/analytics/candles/#{pair}?interval=#{interval}&limit=#{limit}")
end
def get_trade_history(pair, limit: 50)
get("/analytics/trades/#{pair}?limit=#{limit}")
end
def get_volume_stats
get('/analytics/volume')
end
def get_tvl
get('/analytics/tvl')
end
# Lifecycle
def health_check
response = get('/health')
response['status'] == 'healthy'
rescue StandardError
false
end
def close
@closed = true
end
def closed?
@closed
end
# Internal methods
def get(path)
request(:get, path)
end
def post(path, body)
request(:post, path, body)
end
def delete(path)
request(:delete, path)
end
private
def request(method, path, body = nil)
raise DexError.new('Client has been closed', code: 'CLIENT_CLOSED') if @closed
uri = URI.parse("#{@config.endpoint}#{path}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.read_timeout = @config.timeout
request = case method
when :get
Net::HTTP::Get.new(uri.request_uri)
when :post
req = Net::HTTP::Post.new(uri.request_uri)
req.body = body.to_json if body
req
when :delete
Net::HTTP::Delete.new(uri.request_uri)
end
request['Content-Type'] = 'application/json'
request['Authorization'] = "Bearer #{@config.api_key}"
request['X-SDK-Version'] = 'ruby/0.1.0'
response = http.request(request)
unless response.is_a?(Net::HTTPSuccess)
error = JSON.parse(response.body) rescue {}
raise DexError.new(
error['message'] || "HTTP #{response.code}",
code: error['code'],
status: response.code.to_i
)
end
JSON.parse(response.body)
end
end
# Perpetual futures sub-client
class PerpsClient
def initialize(dex)
@dex = dex
end
def list_markets
@dex.get('/perps/markets')
end
def get_market(symbol)
@dex.get("/perps/markets/#{symbol}")
end
def open_position(params)
body = {
market: params[:market],
side: params[:side].to_s,
size: params[:size].to_s,
leverage: params[:leverage],
order_type: params[:order_type].to_s,
margin_type: (params[:margin_type] || :cross).to_s,
reduce_only: params[:reduce_only] || false
}
body[:limit_price] = params[:limit_price] if params[:limit_price]
body[:stop_loss] = params[:stop_loss] if params[:stop_loss]
body[:take_profit] = params[:take_profit] if params[:take_profit]
@dex.post('/perps/positions', body)
end
def close_position(params)
body = {
market: params[:market],
order_type: (params[:order_type] || :market).to_s
}
body[:size] = params[:size].to_s if params[:size]
body[:limit_price] = params[:limit_price] if params[:limit_price]
@dex.post('/perps/positions/close', body)
end
def get_positions
@dex.get('/perps/positions')
end
def get_position(market)
@dex.get("/perps/positions/#{market}")
end
def get_orders
@dex.get('/perps/orders')
end
def cancel_order(order_id)
@dex.delete("/perps/orders/#{order_id}")
end
def cancel_all_orders(market: nil)
path = market ? "/perps/orders?market=#{market}" : '/perps/orders'
result = @dex.delete(path)
result['cancelled']
end
def get_funding_history(market, limit: 100)
@dex.get("/perps/funding/#{market}?limit=#{limit}")
end
def get_funding_rate(market)
@dex.get("/perps/funding/#{market}/current")
end
end
# Order book sub-client
class OrderBookClient
def initialize(dex)
@dex = dex
end
def get_order_book(market, depth: 20)
@dex.get("/orderbook/#{market}?depth=#{depth}")
end
def place_limit_order(params)
@dex.post('/orderbook/orders', {
market: params[:market],
side: params[:side],
price: params[:price],
size: params[:size].to_s,
time_in_force: (params[:time_in_force] || :GTC).to_s,
post_only: params[:post_only] || false
})
end
def cancel_order(order_id)
@dex.delete("/orderbook/orders/#{order_id}")
end
def get_open_orders(market: nil)
path = market ? "/orderbook/orders?market=#{market}" : '/orderbook/orders'
@dex.get(path)
end
def get_order_history(limit: 50)
@dex.get("/orderbook/orders/history?limit=#{limit}")
end
end
# Farms sub-client
class FarmsClient
def initialize(dex)
@dex = dex
end
def list_farms
@dex.get('/farms')
end
def get_farm(farm_id)
@dex.get("/farms/#{farm_id}")
end
def stake(params)
@dex.post('/farms/stake', {
farm: params[:farm],
amount: params[:amount].to_s
})
end
def unstake(farm, amount)
@dex.post('/farms/unstake', {
farm: farm,
amount: amount.to_s
})
end
def claim_rewards(farm)
@dex.post('/farms/claim', { farm: farm })
end
def get_my_farm_positions
@dex.get('/farms/positions')
end
end
end
end