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
368 lines
12 KiB
Swift
368 lines
12 KiB
Swift
import Foundation
|
|
|
|
/// Synor DEX SDK Client
|
|
///
|
|
/// 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
|
|
public class SynorDex {
|
|
private let config: DexConfig
|
|
private let session: URLSession
|
|
private var closed = false
|
|
|
|
public let perps: PerpsClient
|
|
public let orderbook: OrderBookClient
|
|
public let farms: FarmsClient
|
|
|
|
public init(config: DexConfig) {
|
|
self.config = config
|
|
|
|
let configuration = URLSessionConfiguration.default
|
|
configuration.timeoutIntervalForRequest = TimeInterval(config.timeout) / 1000
|
|
self.session = URLSession(configuration: configuration)
|
|
|
|
self.perps = PerpsClient()
|
|
self.orderbook = OrderBookClient()
|
|
self.farms = FarmsClient()
|
|
|
|
self.perps.dex = self
|
|
self.orderbook.dex = self
|
|
self.farms.dex = self
|
|
}
|
|
|
|
// MARK: - Token Operations
|
|
|
|
public func getToken(address: String) async throws -> Token {
|
|
try await get("/tokens/\(address)")
|
|
}
|
|
|
|
public func listTokens() async throws -> [Token] {
|
|
try await get("/tokens")
|
|
}
|
|
|
|
public func searchTokens(query: String) async throws -> [Token] {
|
|
let encoded = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? query
|
|
return try await get("/tokens/search?q=\(encoded)")
|
|
}
|
|
|
|
// MARK: - Pool Operations
|
|
|
|
public func getPool(tokenA: String, tokenB: String) async throws -> Pool {
|
|
try await get("/pools/\(tokenA)/\(tokenB)")
|
|
}
|
|
|
|
public func getPoolById(poolId: String) async throws -> Pool {
|
|
try await get("/pools/\(poolId)")
|
|
}
|
|
|
|
public func listPools(filter: PoolFilter? = nil) async throws -> [Pool] {
|
|
var params: [String] = []
|
|
if let f = filter {
|
|
if let tokens = f.tokens { params.append("tokens=\(tokens.joined(separator: ","))") }
|
|
if let minTvl = f.minTvl { params.append("min_tvl=\(minTvl)") }
|
|
if let minVolume = f.minVolume24h { params.append("min_volume=\(minVolume)") }
|
|
if let verified = f.verified { params.append("verified=\(verified)") }
|
|
if let limit = f.limit { params.append("limit=\(limit)") }
|
|
if let offset = f.offset { params.append("offset=\(offset)") }
|
|
}
|
|
let path = params.isEmpty ? "/pools" : "/pools?\(params.joined(separator: "&"))"
|
|
return try await get(path)
|
|
}
|
|
|
|
// MARK: - Swap Operations
|
|
|
|
public func getQuote(params: QuoteParams) async throws -> Quote {
|
|
try await post("/swap/quote", body: [
|
|
"token_in": params.tokenIn,
|
|
"token_out": params.tokenOut,
|
|
"amount_in": String(params.amountIn),
|
|
"slippage": params.slippage ?? 0.005
|
|
])
|
|
}
|
|
|
|
public func swap(params: SwapParams) async throws -> SwapResult {
|
|
let deadline = params.deadline ?? Int(Date().timeIntervalSince1970) + 1200
|
|
var body: [String: Any] = [
|
|
"token_in": params.tokenIn,
|
|
"token_out": params.tokenOut,
|
|
"amount_in": String(params.amountIn),
|
|
"min_amount_out": String(params.minAmountOut),
|
|
"deadline": deadline
|
|
]
|
|
if let recipient = params.recipient {
|
|
body["recipient"] = recipient
|
|
}
|
|
return try await post("/swap", body: body)
|
|
}
|
|
|
|
// MARK: - Liquidity Operations
|
|
|
|
public func addLiquidity(params: AddLiquidityParams) async throws -> LiquidityResult {
|
|
let deadline = params.deadline ?? Int(Date().timeIntervalSince1970) + 1200
|
|
var body: [String: Any] = [
|
|
"token_a": params.tokenA,
|
|
"token_b": params.tokenB,
|
|
"amount_a": String(params.amountA),
|
|
"amount_b": String(params.amountB),
|
|
"deadline": deadline
|
|
]
|
|
if let minA = params.minAmountA { body["min_amount_a"] = String(minA) }
|
|
if let minB = params.minAmountB { body["min_amount_b"] = String(minB) }
|
|
return try await post("/liquidity/add", body: body)
|
|
}
|
|
|
|
public func removeLiquidity(params: RemoveLiquidityParams) async throws -> LiquidityResult {
|
|
let deadline = params.deadline ?? Int(Date().timeIntervalSince1970) + 1200
|
|
var body: [String: Any] = [
|
|
"pool": params.pool,
|
|
"lp_amount": String(params.lpAmount),
|
|
"deadline": deadline
|
|
]
|
|
if let minA = params.minAmountA { body["min_amount_a"] = String(minA) }
|
|
if let minB = params.minAmountB { body["min_amount_b"] = String(minB) }
|
|
return try await post("/liquidity/remove", body: body)
|
|
}
|
|
|
|
public func getMyPositions() async throws -> [LPPosition] {
|
|
try await get("/liquidity/positions")
|
|
}
|
|
|
|
// MARK: - Analytics
|
|
|
|
public func getPriceHistory(pair: String, interval: String, limit: Int = 100) async throws -> [OHLCV] {
|
|
try await get("/analytics/candles/\(pair)?interval=\(interval)&limit=\(limit)")
|
|
}
|
|
|
|
public func getTradeHistory(pair: String, limit: Int = 50) async throws -> [TradeHistory] {
|
|
try await get("/analytics/trades/\(pair)?limit=\(limit)")
|
|
}
|
|
|
|
public func getVolumeStats() async throws -> VolumeStats {
|
|
try await get("/analytics/volume")
|
|
}
|
|
|
|
public func getTVL() async throws -> TVLStats {
|
|
try await get("/analytics/tvl")
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
public func healthCheck() async -> Bool {
|
|
do {
|
|
let response: HealthResponse = try await get("/health")
|
|
return response.status == "healthy"
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
public func close() {
|
|
closed = true
|
|
session.invalidateAndCancel()
|
|
}
|
|
|
|
public var isClosed: Bool { closed }
|
|
|
|
// MARK: - Internal Methods
|
|
|
|
func get<T: Decodable>(_ path: String) async throws -> T {
|
|
try checkClosed()
|
|
var request = URLRequest(url: URL(string: config.endpoint + path)!)
|
|
request.httpMethod = "GET"
|
|
addHeaders(&request)
|
|
return try await execute(request)
|
|
}
|
|
|
|
func post<T: Decodable>(_ path: String, body: [String: Any]) async throws -> T {
|
|
try checkClosed()
|
|
var request = URLRequest(url: URL(string: config.endpoint + path)!)
|
|
request.httpMethod = "POST"
|
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
addHeaders(&request)
|
|
return try await execute(request)
|
|
}
|
|
|
|
func delete<T: Decodable>(_ path: String) async throws -> T {
|
|
try checkClosed()
|
|
var request = URLRequest(url: URL(string: config.endpoint + path)!)
|
|
request.httpMethod = "DELETE"
|
|
addHeaders(&request)
|
|
return try await execute(request)
|
|
}
|
|
|
|
private func addHeaders(_ request: inout URLRequest) {
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
|
|
request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version")
|
|
}
|
|
|
|
private func execute<T: Decodable>(_ request: URLRequest) async throws -> T {
|
|
let (data, response) = try await session.data(for: request)
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
throw DexError.network("Invalid response")
|
|
}
|
|
|
|
guard (200...299).contains(httpResponse.statusCode) else {
|
|
let error = try? JSONDecoder().decode(ErrorResponse.self, from: data)
|
|
throw DexError.http(
|
|
message: error?.message ?? "HTTP \(httpResponse.statusCode)",
|
|
code: error?.code,
|
|
status: httpResponse.statusCode
|
|
)
|
|
}
|
|
|
|
return try JSONDecoder().decode(T.self, from: data)
|
|
}
|
|
|
|
private func checkClosed() throws {
|
|
if closed {
|
|
throw DexError.clientClosed
|
|
}
|
|
}
|
|
|
|
private struct HealthResponse: Decodable {
|
|
let status: String
|
|
}
|
|
|
|
private struct ErrorResponse: Decodable {
|
|
let message: String?
|
|
let code: String?
|
|
}
|
|
}
|
|
|
|
/// DEX client configuration
|
|
public struct DexConfig {
|
|
public let apiKey: String
|
|
public let endpoint: String
|
|
public let wsEndpoint: String
|
|
public let timeout: Int
|
|
public let retries: Int
|
|
public let debug: Bool
|
|
|
|
public init(
|
|
apiKey: String,
|
|
endpoint: String = "https://dex.synor.io/v1",
|
|
wsEndpoint: String = "wss://dex.synor.io/v1/ws",
|
|
timeout: Int = 30000,
|
|
retries: Int = 3,
|
|
debug: Bool = false
|
|
) {
|
|
self.apiKey = apiKey
|
|
self.endpoint = endpoint
|
|
self.wsEndpoint = wsEndpoint
|
|
self.timeout = timeout
|
|
self.retries = retries
|
|
self.debug = debug
|
|
}
|
|
}
|
|
|
|
/// DEX SDK Error
|
|
public enum DexError: Error {
|
|
case http(message: String, code: String?, status: Int)
|
|
case network(String)
|
|
case clientClosed
|
|
}
|
|
|
|
/// Perpetual futures sub-client
|
|
public class PerpsClient {
|
|
weak var dex: SynorDex?
|
|
|
|
public func listMarkets() async throws -> [PerpMarket] {
|
|
try await dex!.get("/perps/markets")
|
|
}
|
|
|
|
public func getMarket(symbol: String) async throws -> PerpMarket {
|
|
try await dex!.get("/perps/markets/\(symbol)")
|
|
}
|
|
|
|
public func openPosition(params: OpenPositionParams) async throws -> PerpPosition {
|
|
var body: [String: Any] = [
|
|
"market": params.market,
|
|
"side": params.side.rawValue,
|
|
"size": String(params.size),
|
|
"leverage": params.leverage,
|
|
"order_type": params.orderType.rawValue,
|
|
"margin_type": params.marginType.rawValue,
|
|
"reduce_only": params.reduceOnly
|
|
]
|
|
if let price = params.limitPrice { body["limit_price"] = price }
|
|
if let sl = params.stopLoss { body["stop_loss"] = sl }
|
|
if let tp = params.takeProfit { body["take_profit"] = tp }
|
|
return try await dex!.post("/perps/positions", body: body)
|
|
}
|
|
|
|
public func closePosition(params: ClosePositionParams) async throws -> PerpPosition {
|
|
var body: [String: Any] = [
|
|
"market": params.market,
|
|
"order_type": params.orderType.rawValue
|
|
]
|
|
if let size = params.size { body["size"] = String(size) }
|
|
if let price = params.limitPrice { body["limit_price"] = price }
|
|
return try await dex!.post("/perps/positions/close", body: body)
|
|
}
|
|
|
|
public func getPositions() async throws -> [PerpPosition] {
|
|
try await dex!.get("/perps/positions")
|
|
}
|
|
|
|
public func getOrders() async throws -> [PerpOrder] {
|
|
try await dex!.get("/perps/orders")
|
|
}
|
|
|
|
public func getFundingHistory(market: String, limit: Int = 100) async throws -> [FundingPayment] {
|
|
try await dex!.get("/perps/funding/\(market)?limit=\(limit)")
|
|
}
|
|
}
|
|
|
|
/// Order book sub-client
|
|
public class OrderBookClient {
|
|
weak var dex: SynorDex?
|
|
|
|
public func getOrderBook(market: String, depth: Int = 20) async throws -> OrderBook {
|
|
try await dex!.get("/orderbook/\(market)?depth=\(depth)")
|
|
}
|
|
|
|
public func placeLimitOrder(params: LimitOrderParams) async throws -> Order {
|
|
try await dex!.post("/orderbook/orders", body: [
|
|
"market": params.market,
|
|
"side": params.side,
|
|
"price": params.price,
|
|
"size": String(params.size),
|
|
"time_in_force": params.timeInForce.rawValue,
|
|
"post_only": params.postOnly
|
|
])
|
|
}
|
|
|
|
public func getOpenOrders(market: String? = nil) async throws -> [Order] {
|
|
let path = market.map { "/orderbook/orders?market=\($0)" } ?? "/orderbook/orders"
|
|
return try await dex!.get(path)
|
|
}
|
|
}
|
|
|
|
/// Farms sub-client
|
|
public class FarmsClient {
|
|
weak var dex: SynorDex?
|
|
|
|
public func listFarms() async throws -> [Farm] {
|
|
try await dex!.get("/farms")
|
|
}
|
|
|
|
public func getFarm(farmId: String) async throws -> Farm {
|
|
try await dex!.get("/farms/\(farmId)")
|
|
}
|
|
|
|
public func stake(params: StakeParams) async throws -> FarmPosition {
|
|
try await dex!.post("/farms/stake", body: [
|
|
"farm": params.farm,
|
|
"amount": String(params.amount)
|
|
])
|
|
}
|
|
|
|
public func getMyFarmPositions() async throws -> [FarmPosition] {
|
|
try await dex!.get("/farms/positions")
|
|
}
|
|
}
|