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
392 lines
10 KiB
Ruby
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
|