diff --git a/sdk/c/include/synor/dex.h b/sdk/c/include/synor/dex.h new file mode 100644 index 0000000..6e732ea --- /dev/null +++ b/sdk/c/include/synor/dex.h @@ -0,0 +1,359 @@ +/** + * @file dex.h + * @brief Synor DEX SDK for C + * + * 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 + */ + +#ifndef SYNOR_DEX_H +#define SYNOR_DEX_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Error codes */ +typedef enum { + SYNOR_DEX_OK = 0, + SYNOR_DEX_ERROR_CLIENT_CLOSED = -1, + SYNOR_DEX_ERROR_HTTP = -2, + SYNOR_DEX_ERROR_NETWORK = -3, + SYNOR_DEX_ERROR_INVALID_PARAM = -4, + SYNOR_DEX_ERROR_TIMEOUT = -5, + SYNOR_DEX_ERROR_JSON = -6, + SYNOR_DEX_ERROR_ALLOCATION = -7 +} synor_dex_error_t; + +/* Enums */ +typedef enum { + SYNOR_DEX_POOL_TYPE_CONSTANT_PRODUCT, + SYNOR_DEX_POOL_TYPE_STABLE, + SYNOR_DEX_POOL_TYPE_CONCENTRATED +} synor_dex_pool_type_t; + +typedef enum { + SYNOR_DEX_POSITION_SIDE_LONG, + SYNOR_DEX_POSITION_SIDE_SHORT +} synor_dex_position_side_t; + +typedef enum { + SYNOR_DEX_ORDER_TYPE_MARKET, + SYNOR_DEX_ORDER_TYPE_LIMIT, + SYNOR_DEX_ORDER_TYPE_STOP_MARKET, + SYNOR_DEX_ORDER_TYPE_STOP_LIMIT, + SYNOR_DEX_ORDER_TYPE_TAKE_PROFIT, + SYNOR_DEX_ORDER_TYPE_TAKE_PROFIT_LIMIT +} synor_dex_order_type_t; + +typedef enum { + SYNOR_DEX_MARGIN_TYPE_CROSS, + SYNOR_DEX_MARGIN_TYPE_ISOLATED +} synor_dex_margin_type_t; + +typedef enum { + SYNOR_DEX_TIME_IN_FORCE_GTC, + SYNOR_DEX_TIME_IN_FORCE_IOC, + SYNOR_DEX_TIME_IN_FORCE_FOK, + SYNOR_DEX_TIME_IN_FORCE_GTD +} synor_dex_time_in_force_t; + +typedef enum { + SYNOR_DEX_ORDER_STATUS_PENDING, + SYNOR_DEX_ORDER_STATUS_OPEN, + SYNOR_DEX_ORDER_STATUS_PARTIALLY_FILLED, + SYNOR_DEX_ORDER_STATUS_FILLED, + SYNOR_DEX_ORDER_STATUS_CANCELLED, + SYNOR_DEX_ORDER_STATUS_EXPIRED +} synor_dex_order_status_t; + +/* Opaque handle types */ +typedef struct synor_dex_client synor_dex_client_t; +typedef struct synor_dex_perps_client synor_dex_perps_client_t; +typedef struct synor_dex_orderbook_client synor_dex_orderbook_client_t; +typedef struct synor_dex_farms_client synor_dex_farms_client_t; + +/* Configuration */ +typedef struct { + const char* api_key; + const char* endpoint; /* Default: "https://dex.synor.io/v1" */ + const char* ws_endpoint; /* Default: "wss://dex.synor.io/v1/ws" */ + uint32_t timeout_ms; /* Default: 30000 */ + uint32_t retries; /* Default: 3 */ + bool debug; /* Default: false */ +} synor_dex_config_t; + +/* Data types */ +typedef struct { + char* address; + char* symbol; + char* name; + int decimals; + char* total_supply; + double* price_usd; /* NULL if not available */ + char* logo_url; /* NULL if not available */ + bool verified; +} synor_dex_token_t; + +typedef struct { + char* id; + synor_dex_token_t token_a; + synor_dex_token_t token_b; + synor_dex_pool_type_t pool_type; + char* reserve_a; + char* reserve_b; + double fee; + double tvl_usd; + double volume_24h; + double apr; + char* lp_token_address; +} synor_dex_pool_t; + +typedef struct { + char* token_in; + char* token_out; + char* amount_in; + char* amount_out; + double price_impact; + char** route; + size_t route_count; + char* fee; + char* minimum_received; + int64_t expires_at; +} synor_dex_quote_t; + +typedef struct { + const char* token_in; + const char* token_out; + const char* amount_in; + double slippage; /* Default: 0.005 */ +} synor_dex_quote_params_t; + +typedef struct { + const char* token_in; + const char* token_out; + const char* amount_in; + const char* min_amount_out; + int64_t deadline; /* 0 for default */ + const char* recipient; /* NULL for self */ +} synor_dex_swap_params_t; + +typedef struct { + char* transaction_hash; + char* amount_in; + char* amount_out; + double effective_price; + char* fee_paid; + char** route; + size_t route_count; +} synor_dex_swap_result_t; + +typedef struct { + const char* token_a; + const char* token_b; + const char* amount_a; + const char* amount_b; + const char* min_amount_a; /* NULL for no minimum */ + const char* min_amount_b; /* NULL for no minimum */ + int64_t deadline; /* 0 for default */ +} synor_dex_add_liquidity_params_t; + +typedef struct { + const char* pool; + const char* lp_amount; + const char* min_amount_a; /* NULL for no minimum */ + const char* min_amount_b; /* NULL for no minimum */ + int64_t deadline; /* 0 for default */ +} synor_dex_remove_liquidity_params_t; + +typedef struct { + char* transaction_hash; + char* amount_a; + char* amount_b; + char* lp_tokens; + double pool_share; +} synor_dex_liquidity_result_t; + +typedef struct { + char* pool_id; + char* lp_tokens; + char* token_a_amount; + char* token_b_amount; + double value_usd; + char* unclaimed_fees_a; + char* unclaimed_fees_b; + double impermanent_loss; +} synor_dex_lp_position_t; + +typedef struct { + char* symbol; + char* base_asset; + char* quote_asset; + double index_price; + double mark_price; + double funding_rate; + int64_t next_funding_time; + char* open_interest; + double volume_24h; + int max_leverage; +} synor_dex_perp_market_t; + +typedef struct { + const char* market; + synor_dex_position_side_t side; + const char* size; + int leverage; + synor_dex_order_type_t order_type; + double* limit_price; /* NULL for market orders */ + double* stop_loss; /* NULL if not set */ + double* take_profit; /* NULL if not set */ + synor_dex_margin_type_t margin_type; + bool reduce_only; +} synor_dex_open_position_params_t; + +typedef struct { + const char* market; + const char* size; /* NULL for entire position */ + synor_dex_order_type_t order_type; + double* limit_price; /* NULL for market orders */ +} synor_dex_close_position_params_t; + +typedef struct { + char* id; + char* market; + synor_dex_position_side_t side; + char* size; + double entry_price; + double mark_price; + double liquidation_price; + char* margin; + int leverage; + char* unrealized_pnl; +} synor_dex_perp_position_t; + +typedef struct { + int64_t timestamp; + double open; + double high; + double low; + double close; + double volume; +} synor_dex_ohlcv_t; + +typedef struct { + double volume_24h; + double volume_7d; + double volume_30d; + int trades_24h; +} synor_dex_volume_stats_t; + +typedef struct { + double total_tvl; + double pools_tvl; + double farms_tvl; + double perps_tvl; +} synor_dex_tvl_stats_t; + +/* Callbacks */ +typedef void (*synor_dex_callback_t)(synor_dex_error_t error, void* result, void* user_data); + +/* Client lifecycle */ +synor_dex_error_t synor_dex_create(synor_dex_client_t** client, const synor_dex_config_t* config); +void synor_dex_destroy(synor_dex_client_t* client); +bool synor_dex_is_closed(const synor_dex_client_t* client); +void synor_dex_close(synor_dex_client_t* client); + +/* Token operations */ +synor_dex_error_t synor_dex_get_token(synor_dex_client_t* client, const char* address, + synor_dex_token_t** token); +synor_dex_error_t synor_dex_list_tokens(synor_dex_client_t* client, + synor_dex_token_t** tokens, size_t* count); +synor_dex_error_t synor_dex_search_tokens(synor_dex_client_t* client, const char* query, + synor_dex_token_t** tokens, size_t* count); + +/* Pool operations */ +synor_dex_error_t synor_dex_get_pool(synor_dex_client_t* client, + const char* token_a, const char* token_b, + synor_dex_pool_t** pool); +synor_dex_error_t synor_dex_list_pools(synor_dex_client_t* client, + synor_dex_pool_t** pools, size_t* count); + +/* Swap operations */ +synor_dex_error_t synor_dex_get_quote(synor_dex_client_t* client, + const synor_dex_quote_params_t* params, + synor_dex_quote_t** quote); +synor_dex_error_t synor_dex_swap(synor_dex_client_t* client, + const synor_dex_swap_params_t* params, + synor_dex_swap_result_t** result); + +/* Liquidity operations */ +synor_dex_error_t synor_dex_add_liquidity(synor_dex_client_t* client, + const synor_dex_add_liquidity_params_t* params, + synor_dex_liquidity_result_t** result); +synor_dex_error_t synor_dex_remove_liquidity(synor_dex_client_t* client, + const synor_dex_remove_liquidity_params_t* params, + synor_dex_liquidity_result_t** result); +synor_dex_error_t synor_dex_get_my_positions(synor_dex_client_t* client, + synor_dex_lp_position_t** positions, size_t* count); + +/* Analytics */ +synor_dex_error_t synor_dex_get_price_history(synor_dex_client_t* client, + const char* pair, const char* interval, int limit, + synor_dex_ohlcv_t** candles, size_t* count); +synor_dex_error_t synor_dex_get_volume_stats(synor_dex_client_t* client, + synor_dex_volume_stats_t** stats); +synor_dex_error_t synor_dex_get_tvl(synor_dex_client_t* client, synor_dex_tvl_stats_t** stats); + +/* Health check */ +synor_dex_error_t synor_dex_health_check(synor_dex_client_t* client, bool* healthy); + +/* Sub-clients */ +synor_dex_perps_client_t* synor_dex_perps(synor_dex_client_t* client); +synor_dex_orderbook_client_t* synor_dex_orderbook(synor_dex_client_t* client); +synor_dex_farms_client_t* synor_dex_farms(synor_dex_client_t* client); + +/* Perps operations */ +synor_dex_error_t synor_dex_perps_list_markets(synor_dex_perps_client_t* client, + synor_dex_perp_market_t** markets, size_t* count); +synor_dex_error_t synor_dex_perps_get_market(synor_dex_perps_client_t* client, + const char* symbol, + synor_dex_perp_market_t** market); +synor_dex_error_t synor_dex_perps_open_position(synor_dex_perps_client_t* client, + const synor_dex_open_position_params_t* params, + synor_dex_perp_position_t** position); +synor_dex_error_t synor_dex_perps_close_position(synor_dex_perps_client_t* client, + const synor_dex_close_position_params_t* params, + synor_dex_perp_position_t** position); +synor_dex_error_t synor_dex_perps_get_positions(synor_dex_perps_client_t* client, + synor_dex_perp_position_t** positions, size_t* count); + +/* Memory management */ +void synor_dex_free_token(synor_dex_token_t* token); +void synor_dex_free_tokens(synor_dex_token_t* tokens, size_t count); +void synor_dex_free_pool(synor_dex_pool_t* pool); +void synor_dex_free_pools(synor_dex_pool_t* pools, size_t count); +void synor_dex_free_quote(synor_dex_quote_t* quote); +void synor_dex_free_swap_result(synor_dex_swap_result_t* result); +void synor_dex_free_liquidity_result(synor_dex_liquidity_result_t* result); +void synor_dex_free_lp_positions(synor_dex_lp_position_t* positions, size_t count); +void synor_dex_free_perp_market(synor_dex_perp_market_t* market); +void synor_dex_free_perp_markets(synor_dex_perp_market_t* markets, size_t count); +void synor_dex_free_perp_position(synor_dex_perp_position_t* position); +void synor_dex_free_perp_positions(synor_dex_perp_position_t* positions, size_t count); +void synor_dex_free_ohlcv(synor_dex_ohlcv_t* candles, size_t count); +void synor_dex_free_volume_stats(synor_dex_volume_stats_t* stats); +void synor_dex_free_tvl_stats(synor_dex_tvl_stats_t* stats); + +#ifdef __cplusplus +} +#endif + +#endif /* SYNOR_DEX_H */ diff --git a/sdk/cpp/include/synor/dex.hpp b/sdk/cpp/include/synor/dex.hpp new file mode 100644 index 0000000..6a24632 --- /dev/null +++ b/sdk/cpp/include/synor/dex.hpp @@ -0,0 +1,484 @@ +/** + * @file dex.hpp + * @brief Synor DEX SDK for C++ + * + * 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 + */ + +#ifndef SYNOR_DEX_HPP +#define SYNOR_DEX_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace synor { +namespace dex { + +// Forward declarations +class SynorDexImpl; +class PerpsClient; +class OrderBookClient; +class FarmsClient; + +// Enums +enum class PoolType { + ConstantProduct, + Stable, + Concentrated +}; + +enum class PositionSide { + Long, + Short +}; + +enum class OrderType { + Market, + Limit, + StopMarket, + StopLimit, + TakeProfit, + TakeProfitLimit +}; + +enum class MarginType { + Cross, + Isolated +}; + +enum class TimeInForce { + GTC, + IOC, + FOK, + GTD +}; + +enum class OrderStatus { + Pending, + Open, + PartiallyFilled, + Filled, + Cancelled, + Expired +}; + +// Configuration +struct Config { + std::string api_key; + std::string endpoint = "https://dex.synor.io/v1"; + std::string ws_endpoint = "wss://dex.synor.io/v1/ws"; + uint32_t timeout_ms = 30000; + uint32_t retries = 3; + bool debug = false; +}; + +// Types +struct Token { + std::string address; + std::string symbol; + std::string name; + int decimals; + std::string total_supply; + std::optional price_usd; + std::optional logo_url; + bool verified = false; +}; + +struct Pool { + std::string id; + Token token_a; + Token token_b; + PoolType pool_type; + std::string reserve_a; + std::string reserve_b; + double fee; + double tvl_usd; + double volume_24h; + double apr; + std::string lp_token_address; +}; + +struct PoolFilter { + std::optional> tokens; + std::optional min_tvl; + std::optional min_volume_24h; + std::optional verified; + std::optional limit; + std::optional offset; +}; + +struct Quote { + std::string token_in; + std::string token_out; + std::string amount_in; + std::string amount_out; + double price_impact; + std::vector route; + std::string fee; + std::string minimum_received; + int64_t expires_at; +}; + +struct QuoteParams { + std::string token_in; + std::string token_out; + std::string amount_in; + std::optional slippage; +}; + +struct SwapParams { + std::string token_in; + std::string token_out; + std::string amount_in; + std::string min_amount_out; + std::optional deadline; + std::optional recipient; +}; + +struct SwapResult { + std::string transaction_hash; + std::string amount_in; + std::string amount_out; + double effective_price; + std::string fee_paid; + std::vector route; +}; + +struct AddLiquidityParams { + std::string token_a; + std::string token_b; + std::string amount_a; + std::string amount_b; + std::optional min_amount_a; + std::optional min_amount_b; + std::optional deadline; +}; + +struct RemoveLiquidityParams { + std::string pool; + std::string lp_amount; + std::optional min_amount_a; + std::optional min_amount_b; + std::optional deadline; +}; + +struct LiquidityResult { + std::string transaction_hash; + std::string amount_a; + std::string amount_b; + std::string lp_tokens; + double pool_share; +}; + +struct LPPosition { + std::string pool_id; + std::string lp_tokens; + std::string token_a_amount; + std::string token_b_amount; + double value_usd; + std::string unclaimed_fees_a; + std::string unclaimed_fees_b; + double impermanent_loss; +}; + +struct PerpMarket { + std::string symbol; + std::string base_asset; + std::string quote_asset; + double index_price; + double mark_price; + double funding_rate; + int64_t next_funding_time; + std::string open_interest; + double volume_24h; + int max_leverage; +}; + +struct OpenPositionParams { + std::string market; + PositionSide side; + std::string size; + int leverage; + OrderType order_type; + std::optional limit_price; + std::optional stop_loss; + std::optional take_profit; + MarginType margin_type = MarginType::Cross; + bool reduce_only = false; +}; + +struct ClosePositionParams { + std::string market; + std::optional size; + OrderType order_type = OrderType::Market; + std::optional limit_price; +}; + +struct PerpPosition { + std::string id; + std::string market; + PositionSide side; + std::string size; + double entry_price; + double mark_price; + double liquidation_price; + std::string margin; + int leverage; + std::string unrealized_pnl; +}; + +struct PerpOrder { + std::string id; + std::string market; + PositionSide side; + OrderType order_type; + std::string size; + std::optional price; + OrderStatus status; +}; + +struct FundingPayment { + std::string market; + std::string amount; + double rate; + std::string position_size; + int64_t timestamp; +}; + +struct OrderBookEntry { + double price; + std::string size; + int orders; +}; + +struct OrderBook { + std::string market; + std::vector bids; + std::vector asks; + int64_t timestamp; +}; + +struct LimitOrderParams { + std::string market; + std::string side; + double price; + std::string size; + TimeInForce time_in_force = TimeInForce::GTC; + bool post_only = false; +}; + +struct Order { + std::string id; + std::string market; + std::string side; + double price; + std::string size; + std::string filled_size; + OrderStatus status; + TimeInForce time_in_force; + bool post_only; +}; + +struct Farm { + std::string id; + std::string name; + Token stake_token; + std::vector reward_tokens; + double tvl_usd; + double apr; +}; + +struct StakeParams { + std::string farm; + std::string amount; +}; + +struct FarmPosition { + std::string farm_id; + std::string staked_amount; + std::vector pending_rewards; + int64_t staked_at; +}; + +struct OHLCV { + int64_t timestamp; + double open; + double high; + double low; + double close; + double volume; +}; + +struct TradeHistory { + std::string id; + std::string market; + std::string side; + double price; + std::string size; + int64_t timestamp; + std::string maker; + std::string taker; +}; + +struct VolumeStats { + double volume_24h; + double volume_7d; + double volume_30d; + int trades_24h; +}; + +struct TVLStats { + double total_tvl; + double pools_tvl; + double farms_tvl; + double perps_tvl; +}; + +struct ClaimRewardsResult { + std::string amount; + std::string transaction_hash; +}; + +struct FundingRateInfo { + double rate; + int64_t next_time; +}; + +// Exception +class DexException : public std::runtime_error { +public: + DexException(const std::string& message, + const std::string& code = "", + int status = 0) + : std::runtime_error(message), code_(code), status_(status) {} + + const std::string& code() const { return code_; } + int status() const { return status_; } + +private: + std::string code_; + int status_; +}; + +// Main Client +class SynorDex { +public: + explicit SynorDex(const Config& config); + ~SynorDex(); + + SynorDex(const SynorDex&) = delete; + SynorDex& operator=(const SynorDex&) = delete; + SynorDex(SynorDex&&) noexcept; + SynorDex& operator=(SynorDex&&) noexcept; + + // Token Operations + std::future get_token(const std::string& address); + std::future> list_tokens(); + std::future> search_tokens(const std::string& query); + + // Pool Operations + std::future get_pool(const std::string& token_a, const std::string& token_b); + std::future get_pool_by_id(const std::string& pool_id); + std::future> list_pools(const std::optional& filter = std::nullopt); + + // Swap Operations + std::future get_quote(const QuoteParams& params); + std::future swap(const SwapParams& params); + + // Liquidity Operations + std::future add_liquidity(const AddLiquidityParams& params); + std::future remove_liquidity(const RemoveLiquidityParams& params); + std::future> get_my_positions(); + + // Analytics + std::future> get_price_history(const std::string& pair, + const std::string& interval, + int limit = 100); + std::future> get_trade_history(const std::string& pair, + int limit = 50); + std::future get_volume_stats(); + std::future get_tvl(); + + // Lifecycle + std::future health_check(); + void close(); + bool is_closed() const; + + // Sub-clients + PerpsClient& perps(); + OrderBookClient& orderbook(); + FarmsClient& farms(); + +private: + std::unique_ptr impl_; +}; + +// Perpetuals Client +class PerpsClient { +public: + explicit PerpsClient(SynorDex& dex); + ~PerpsClient(); + + std::future> list_markets(); + std::future get_market(const std::string& symbol); + std::future open_position(const OpenPositionParams& params); + std::future close_position(const ClosePositionParams& params); + std::future> get_positions(); + std::future> get_orders(); + std::future cancel_order(const std::string& order_id); + std::future cancel_all_orders(const std::optional& market = std::nullopt); + std::future> get_funding_history(const std::string& market, + int limit = 100); + std::future get_funding_rate(const std::string& market); + +private: + SynorDex& dex_; +}; + +// OrderBook Client +class OrderBookClient { +public: + explicit OrderBookClient(SynorDex& dex); + ~OrderBookClient(); + + std::future get_order_book(const std::string& market, int depth = 20); + std::future place_limit_order(const LimitOrderParams& params); + std::future cancel_order(const std::string& order_id); + std::future> get_open_orders(const std::optional& market = std::nullopt); + std::future> get_order_history(int limit = 50); + +private: + SynorDex& dex_; +}; + +// Farms Client +class FarmsClient { +public: + explicit FarmsClient(SynorDex& dex); + ~FarmsClient(); + + std::future> list_farms(); + std::future get_farm(const std::string& farm_id); + std::future stake(const StakeParams& params); + std::future unstake(const std::string& farm, const std::string& amount); + std::future claim_rewards(const std::string& farm); + std::future> get_my_farm_positions(); + +private: + SynorDex& dex_; +}; + +} // namespace dex +} // namespace synor + +#endif // SYNOR_DEX_HPP diff --git a/sdk/csharp/src/Synor.Dex/SynorDex.cs b/sdk/csharp/src/Synor.Dex/SynorDex.cs new file mode 100644 index 0000000..452d1f0 --- /dev/null +++ b/sdk/csharp/src/Synor.Dex/SynorDex.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Web; + +namespace Synor.Dex +{ + /// + /// 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 : IDisposable + { + private readonly DexConfig _config; + private readonly HttpClient _client; + private readonly JsonSerializerOptions _jsonOptions; + private bool _closed; + + public PerpsClient Perps { get; } + public OrderBookClient OrderBook { get; } + public FarmsClient Farms { get; } + + public SynorDex(DexConfig config) + { + _config = config; + _client = new HttpClient + { + BaseAddress = new Uri(config.Endpoint), + Timeout = TimeSpan.FromMilliseconds(config.Timeout) + }; + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", config.ApiKey); + _client.DefaultRequestHeaders.Add("X-SDK-Version", "csharp/0.1.0"); + + _jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + PropertyNameCaseInsensitive = true + }; + + Perps = new PerpsClient(this); + OrderBook = new OrderBookClient(this); + Farms = new FarmsClient(this); + } + + // Token Operations + + public Task GetTokenAsync(string address) + => GetAsync($"/tokens/{address}"); + + public Task> ListTokensAsync() + => GetAsync>("/tokens"); + + public Task> SearchTokensAsync(string query) + => GetAsync>($"/tokens/search?q={HttpUtility.UrlEncode(query)}"); + + // Pool Operations + + public Task GetPoolAsync(string tokenA, string tokenB) + => GetAsync($"/pools/{tokenA}/{tokenB}"); + + public Task GetPoolByIdAsync(string poolId) + => GetAsync($"/pools/{poolId}"); + + public Task> ListPoolsAsync(PoolFilter filter = null) + { + var parameters = new List(); + if (filter != null) + { + if (filter.Tokens != null) parameters.Add($"tokens={string.Join(",", filter.Tokens)}"); + if (filter.MinTvl.HasValue) parameters.Add($"min_tvl={filter.MinTvl}"); + if (filter.MinVolume24h.HasValue) parameters.Add($"min_volume={filter.MinVolume24h}"); + if (filter.Verified.HasValue) parameters.Add($"verified={filter.Verified.Value.ToString().ToLower()}"); + if (filter.Limit.HasValue) parameters.Add($"limit={filter.Limit}"); + if (filter.Offset.HasValue) parameters.Add($"offset={filter.Offset}"); + } + var path = parameters.Count > 0 ? $"/pools?{string.Join("&", parameters)}" : "/pools"; + return GetAsync>(path); + } + + // Swap Operations + + public Task GetQuoteAsync(QuoteParams prms) + => PostAsync("/swap/quote", new + { + token_in = prms.TokenIn, + token_out = prms.TokenOut, + amount_in = prms.AmountIn.ToString(), + slippage = prms.Slippage ?? 0.005 + }); + + public Task SwapAsync(SwapParams prms) + { + var deadline = prms.Deadline ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 1200; + return PostAsync("/swap", new + { + token_in = prms.TokenIn, + token_out = prms.TokenOut, + amount_in = prms.AmountIn.ToString(), + min_amount_out = prms.MinAmountOut.ToString(), + deadline, + recipient = prms.Recipient + }); + } + + // Liquidity Operations + + public Task AddLiquidityAsync(AddLiquidityParams prms) + { + var deadline = prms.Deadline ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 1200; + return PostAsync("/liquidity/add", new + { + token_a = prms.TokenA, + token_b = prms.TokenB, + amount_a = prms.AmountA.ToString(), + amount_b = prms.AmountB.ToString(), + deadline, + min_amount_a = prms.MinAmountA?.ToString(), + min_amount_b = prms.MinAmountB?.ToString() + }); + } + + public Task RemoveLiquidityAsync(RemoveLiquidityParams prms) + { + var deadline = prms.Deadline ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 1200; + return PostAsync("/liquidity/remove", new + { + pool = prms.Pool, + lp_amount = prms.LpAmount.ToString(), + deadline, + min_amount_a = prms.MinAmountA?.ToString(), + min_amount_b = prms.MinAmountB?.ToString() + }); + } + + public Task> GetMyPositionsAsync() + => GetAsync>("/liquidity/positions"); + + // Analytics + + public Task> GetPriceHistoryAsync(string pair, string interval, int limit = 100) + => GetAsync>($"/analytics/candles/{pair}?interval={interval}&limit={limit}"); + + public Task> GetTradeHistoryAsync(string pair, int limit = 50) + => GetAsync>($"/analytics/trades/{pair}?limit={limit}"); + + public Task GetVolumeStatsAsync() + => GetAsync("/analytics/volume"); + + public Task GetTVLAsync() + => GetAsync("/analytics/tvl"); + + // Lifecycle + + public async Task HealthCheckAsync() + { + try + { + var response = await GetAsync("/health"); + return response.Status == "healthy"; + } + catch + { + return false; + } + } + + public void Close() + { + _closed = true; + _client.Dispose(); + } + + public bool IsClosed => _closed; + + public void Dispose() + { + Close(); + } + + // Internal methods + + internal async Task GetAsync(string path) + { + CheckClosed(); + var response = await _client.GetAsync(path); + return await HandleResponseAsync(response); + } + + internal async Task PostAsync(string path, object body) + { + CheckClosed(); + var json = JsonSerializer.Serialize(body, _jsonOptions); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + var response = await _client.PostAsync(path, content); + return await HandleResponseAsync(response); + } + + internal async Task DeleteAsync(string path) + { + CheckClosed(); + var response = await _client.DeleteAsync(path); + return await HandleResponseAsync(response); + } + + private async Task HandleResponseAsync(HttpResponseMessage response) + { + var body = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + ErrorResponse error = null; + try { error = JsonSerializer.Deserialize(body, _jsonOptions); } catch { } + throw new DexException( + error?.Message ?? $"HTTP {(int)response.StatusCode}", + error?.Code, + (int)response.StatusCode); + } + + return JsonSerializer.Deserialize(body, _jsonOptions); + } + + private void CheckClosed() + { + if (_closed) + throw new DexException("Client has been closed", "CLIENT_CLOSED", 0); + } + + private class HealthResponse { public string Status { get; set; } } + private class ErrorResponse { public string Message { get; set; } public string Code { get; set; } } + } + + /// + /// DEX Configuration + /// + public class DexConfig + { + public string ApiKey { get; set; } + public string Endpoint { get; set; } = "https://dex.synor.io/v1"; + public string WsEndpoint { get; set; } = "wss://dex.synor.io/v1/ws"; + public int Timeout { get; set; } = 30000; + public int Retries { get; set; } = 3; + public bool Debug { get; set; } = false; + } + + /// + /// DEX Exception + /// + public class DexException : Exception + { + public string Code { get; } + public int Status { get; } + + public DexException(string message, string code = null, int status = 0) : base(message) + { + Code = code; + Status = status; + } + } + + /// + /// Perpetual futures sub-client + /// + public class PerpsClient + { + private readonly SynorDex _dex; + internal PerpsClient(SynorDex dex) => _dex = dex; + + public Task> ListMarketsAsync() + => _dex.GetAsync>("/perps/markets"); + + public Task GetMarketAsync(string symbol) + => _dex.GetAsync($"/perps/markets/{symbol}"); + + public Task OpenPositionAsync(OpenPositionParams prms) + => _dex.PostAsync("/perps/positions", new + { + market = prms.Market, + side = prms.Side.ToString().ToLower(), + size = prms.Size.ToString(), + leverage = prms.Leverage, + order_type = prms.OrderType.ToString().ToLower(), + margin_type = prms.MarginType.ToString().ToLower(), + reduce_only = prms.ReduceOnly, + limit_price = prms.LimitPrice, + stop_loss = prms.StopLoss, + take_profit = prms.TakeProfit + }); + + public Task ClosePositionAsync(ClosePositionParams prms) + => _dex.PostAsync("/perps/positions/close", new + { + market = prms.Market, + size = prms.Size?.ToString(), + order_type = prms.OrderType.ToString().ToLower(), + limit_price = prms.LimitPrice + }); + + public Task> GetPositionsAsync() + => _dex.GetAsync>("/perps/positions"); + + public Task> GetOrdersAsync() + => _dex.GetAsync>("/perps/orders"); + + public Task> GetFundingHistoryAsync(string market, int limit = 100) + => _dex.GetAsync>($"/perps/funding/{market}?limit={limit}"); + + public Task GetFundingRateAsync(string market) + => _dex.GetAsync($"/perps/funding/{market}/current"); + } + + /// + /// Order book sub-client + /// + public class OrderBookClient + { + private readonly SynorDex _dex; + internal OrderBookClient(SynorDex dex) => _dex = dex; + + public Task GetOrderBookAsync(string market, int depth = 20) + => _dex.GetAsync($"/orderbook/{market}?depth={depth}"); + + public Task PlaceLimitOrderAsync(LimitOrderParams prms) + => _dex.PostAsync("/orderbook/orders", new + { + market = prms.Market, + side = prms.Side, + price = prms.Price, + size = prms.Size.ToString(), + time_in_force = prms.TimeInForce.ToString(), + post_only = prms.PostOnly + }); + + public Task> GetOpenOrdersAsync(string market = null) + { + var path = market != null ? $"/orderbook/orders?market={market}" : "/orderbook/orders"; + return _dex.GetAsync>(path); + } + + public Task> GetOrderHistoryAsync(int limit = 50) + => _dex.GetAsync>($"/orderbook/orders/history?limit={limit}"); + } + + /// + /// Farms sub-client + /// + public class FarmsClient + { + private readonly SynorDex _dex; + internal FarmsClient(SynorDex dex) => _dex = dex; + + public Task> ListFarmsAsync() + => _dex.GetAsync>("/farms"); + + public Task GetFarmAsync(string farmId) + => _dex.GetAsync($"/farms/{farmId}"); + + public Task StakeAsync(StakeParams prms) + => _dex.PostAsync("/farms/stake", new + { + farm = prms.Farm, + amount = prms.Amount.ToString() + }); + + public Task UnstakeAsync(string farm, System.Numerics.BigInteger amount) + => _dex.PostAsync("/farms/unstake", new + { + farm, + amount = amount.ToString() + }); + + public Task ClaimRewardsAsync(string farm) + => _dex.PostAsync("/farms/claim", new { farm }); + + public Task> GetMyFarmPositionsAsync() + => _dex.GetAsync>("/farms/positions"); + } +} diff --git a/sdk/csharp/src/Synor.Dex/Types.cs b/sdk/csharp/src/Synor.Dex/Types.cs new file mode 100644 index 0000000..b87d41b --- /dev/null +++ b/sdk/csharp/src/Synor.Dex/Types.cs @@ -0,0 +1,315 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text.Json.Serialization; + +namespace Synor.Dex +{ + public enum PoolType { ConstantProduct, Stable, Concentrated } + public enum PositionSide { Long, Short } + public enum OrderType { Market, Limit, StopMarket, StopLimit, TakeProfit, TakeProfitLimit } + public enum MarginType { Cross, Isolated } + public enum TimeInForce { GTC, IOC, FOK, GTD } + public enum OrderStatus { Pending, Open, PartiallyFilled, Filled, Cancelled, Expired } + + public class Token + { + public string Address { get; set; } + public string Symbol { get; set; } + public string Name { get; set; } + public int Decimals { get; set; } + public string TotalSupply { get; set; } + public double? PriceUsd { get; set; } + public string LogoUrl { get; set; } + public bool Verified { get; set; } + } + + public class Pool + { + public string Id { get; set; } + public Token TokenA { get; set; } + public Token TokenB { get; set; } + public PoolType PoolType { get; set; } + public string ReserveA { get; set; } + public string ReserveB { get; set; } + public double Fee { get; set; } + public double TvlUsd { get; set; } + public double Volume24h { get; set; } + public double Apr { get; set; } + public string LpTokenAddress { get; set; } + } + + public class PoolFilter + { + public List Tokens { get; set; } + public double? MinTvl { get; set; } + public double? MinVolume24h { get; set; } + public bool? Verified { get; set; } + public int? Limit { get; set; } + public int? Offset { get; set; } + } + + public class Quote + { + public string TokenIn { get; set; } + public string TokenOut { get; set; } + public string AmountIn { get; set; } + public string AmountOut { get; set; } + public double PriceImpact { get; set; } + public List Route { get; set; } + public string Fee { get; set; } + public string MinimumReceived { get; set; } + public long ExpiresAt { get; set; } + } + + public class QuoteParams + { + public string TokenIn { get; set; } + public string TokenOut { get; set; } + public BigInteger AmountIn { get; set; } + public double? Slippage { get; set; } + } + + public class SwapParams + { + public string TokenIn { get; set; } + public string TokenOut { get; set; } + public BigInteger AmountIn { get; set; } + public BigInteger MinAmountOut { get; set; } + public long? Deadline { get; set; } + public string Recipient { get; set; } + } + + public class SwapResult + { + public string TransactionHash { get; set; } + public string AmountIn { get; set; } + public string AmountOut { get; set; } + public double EffectivePrice { get; set; } + public string FeePaid { get; set; } + public List Route { get; set; } + } + + public class AddLiquidityParams + { + public string TokenA { get; set; } + public string TokenB { get; set; } + public BigInteger AmountA { get; set; } + public BigInteger AmountB { get; set; } + public BigInteger? MinAmountA { get; set; } + public BigInteger? MinAmountB { get; set; } + public long? Deadline { get; set; } + } + + public class RemoveLiquidityParams + { + public string Pool { get; set; } + public BigInteger LpAmount { get; set; } + public BigInteger? MinAmountA { get; set; } + public BigInteger? MinAmountB { get; set; } + public long? Deadline { get; set; } + } + + public class LiquidityResult + { + public string TransactionHash { get; set; } + public string AmountA { get; set; } + public string AmountB { get; set; } + public string LpTokens { get; set; } + public double PoolShare { get; set; } + } + + public class LPPosition + { + public string PoolId { get; set; } + public string LpTokens { get; set; } + public string TokenAAmount { get; set; } + public string TokenBAmount { get; set; } + public double ValueUsd { get; set; } + public string UnclaimedFeesA { get; set; } + public string UnclaimedFeesB { get; set; } + public double ImpermanentLoss { get; set; } + } + + public class PerpMarket + { + public string Symbol { get; set; } + public string BaseAsset { get; set; } + public string QuoteAsset { get; set; } + public double IndexPrice { get; set; } + public double MarkPrice { get; set; } + public double FundingRate { get; set; } + public long NextFundingTime { get; set; } + public string OpenInterest { get; set; } + public double Volume24h { get; set; } + public int MaxLeverage { get; set; } + } + + public class OpenPositionParams + { + public string Market { get; set; } + public PositionSide Side { get; set; } + public BigInteger Size { get; set; } + public int Leverage { get; set; } + public OrderType OrderType { get; set; } + public double? LimitPrice { get; set; } + public double? StopLoss { get; set; } + public double? TakeProfit { get; set; } + public MarginType MarginType { get; set; } = MarginType.Cross; + public bool ReduceOnly { get; set; } + } + + public class ClosePositionParams + { + public string Market { get; set; } + public BigInteger? Size { get; set; } + public OrderType OrderType { get; set; } = OrderType.Market; + public double? LimitPrice { get; set; } + } + + public class PerpPosition + { + public string Id { get; set; } + public string Market { get; set; } + public PositionSide Side { get; set; } + public string Size { get; set; } + public double EntryPrice { get; set; } + public double MarkPrice { get; set; } + public double LiquidationPrice { get; set; } + public string Margin { get; set; } + public int Leverage { get; set; } + public string UnrealizedPnl { get; set; } + } + + public class PerpOrder + { + public string Id { get; set; } + public string Market { get; set; } + public PositionSide Side { get; set; } + public OrderType OrderType { get; set; } + public string Size { get; set; } + public double? Price { get; set; } + public OrderStatus Status { get; set; } + } + + public class FundingPayment + { + public string Market { get; set; } + public string Amount { get; set; } + public double Rate { get; set; } + public string PositionSize { get; set; } + public long Timestamp { get; set; } + } + + public class FundingRateInfo + { + public double Rate { get; set; } + public long NextTime { get; set; } + } + + public class OrderBookEntry + { + public double Price { get; set; } + public string Size { get; set; } + public int Orders { get; set; } + } + + public class OrderBook + { + public string Market { get; set; } + public List Bids { get; set; } + public List Asks { get; set; } + public long Timestamp { get; set; } + } + + public class LimitOrderParams + { + public string Market { get; set; } + public string Side { get; set; } + public double Price { get; set; } + public BigInteger Size { get; set; } + public TimeInForce TimeInForce { get; set; } = TimeInForce.GTC; + public bool PostOnly { get; set; } + } + + public class Order + { + public string Id { get; set; } + public string Market { get; set; } + public string Side { get; set; } + public double Price { get; set; } + public string Size { get; set; } + public string FilledSize { get; set; } + public OrderStatus Status { get; set; } + public TimeInForce TimeInForce { get; set; } + public bool PostOnly { get; set; } + } + + public class Farm + { + public string Id { get; set; } + public string Name { get; set; } + public Token StakeToken { get; set; } + public List RewardTokens { get; set; } + public double TvlUsd { get; set; } + public double Apr { get; set; } + } + + public class StakeParams + { + public string Farm { get; set; } + public BigInteger Amount { get; set; } + } + + public class FarmPosition + { + public string FarmId { get; set; } + public string StakedAmount { get; set; } + public List PendingRewards { get; set; } + public long StakedAt { get; set; } + } + + public class ClaimRewardsResult + { + public string Amount { get; set; } + public string TransactionHash { get; set; } + } + + public class OHLCV + { + public long Timestamp { get; set; } + public double Open { get; set; } + public double High { get; set; } + public double Low { get; set; } + public double Close { get; set; } + public double Volume { get; set; } + } + + public class TradeHistory + { + public string Id { get; set; } + public string Market { get; set; } + public string Side { get; set; } + public double Price { get; set; } + public string Size { get; set; } + public long Timestamp { get; set; } + public string Maker { get; set; } + public string Taker { get; set; } + } + + public class VolumeStats + { + public double Volume24h { get; set; } + public double Volume7d { get; set; } + public double Volume30d { get; set; } + public int Trades24h { get; set; } + } + + public class TVLStats + { + public double TotalTvl { get; set; } + public double PoolsTvl { get; set; } + public double FarmsTvl { get; set; } + public double PerpsTvl { get; set; } + } +} diff --git a/sdk/flutter/lib/src/dex/synor_dex.dart b/sdk/flutter/lib/src/dex/synor_dex.dart new file mode 100644 index 0000000..c639177 --- /dev/null +++ b/sdk/flutter/lib/src/dex/synor_dex.dart @@ -0,0 +1,355 @@ +/// Synor DEX SDK for Flutter/Dart +/// +/// 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 +library synor_dex; + +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'types.dart'; + +export 'types.dart'; + +/// Main DEX client +class SynorDex { + final DexConfig config; + final http.Client _client; + bool _closed = false; + + late final PerpsClient perps; + late final OrderBookClient orderbook; + late final FarmsClient farms; + + SynorDex(this.config) : _client = http.Client() { + perps = PerpsClient(this); + orderbook = OrderBookClient(this); + farms = FarmsClient(this); + } + + // Token Operations + + Future getToken(String address) async { + return _get('/tokens/$address', Token.fromJson); + } + + Future> listTokens() async { + return _getList('/tokens', Token.fromJson); + } + + Future> searchTokens(String query) async { + return _getList('/tokens/search?q=${Uri.encodeComponent(query)}', Token.fromJson); + } + + // Pool Operations + + Future getPool(String tokenA, String tokenB) async { + return _get('/pools/$tokenA/$tokenB', Pool.fromJson); + } + + Future getPoolById(String poolId) async { + return _get('/pools/$poolId', Pool.fromJson); + } + + Future> listPools({PoolFilter? filter}) async { + final params = []; + if (filter != null) { + if (filter.tokens != null) params.add('tokens=${filter.tokens!.join(",")}'); + if (filter.minTvl != null) params.add('min_tvl=${filter.minTvl}'); + if (filter.minVolume24h != null) params.add('min_volume=${filter.minVolume24h}'); + if (filter.verified != null) params.add('verified=${filter.verified}'); + if (filter.limit != null) params.add('limit=${filter.limit}'); + if (filter.offset != null) params.add('offset=${filter.offset}'); + } + final path = params.isEmpty ? '/pools' : '/pools?${params.join("&")}'; + return _getList(path, Pool.fromJson); + } + + // Swap Operations + + Future getQuote(QuoteParams params) async { + return _post('/swap/quote', { + 'token_in': params.tokenIn, + 'token_out': params.tokenOut, + 'amount_in': params.amountIn.toString(), + 'slippage': params.slippage ?? 0.005, + }, Quote.fromJson); + } + + Future swap(SwapParams params) async { + final deadline = params.deadline ?? + (DateTime.now().millisecondsSinceEpoch ~/ 1000) + 1200; + return _post('/swap', { + 'token_in': params.tokenIn, + 'token_out': params.tokenOut, + 'amount_in': params.amountIn.toString(), + 'min_amount_out': params.minAmountOut.toString(), + 'deadline': deadline, + if (params.recipient != null) 'recipient': params.recipient, + }, SwapResult.fromJson); + } + + // Liquidity Operations + + Future addLiquidity(AddLiquidityParams params) async { + final deadline = params.deadline ?? + (DateTime.now().millisecondsSinceEpoch ~/ 1000) + 1200; + return _post('/liquidity/add', { + 'token_a': params.tokenA, + 'token_b': params.tokenB, + 'amount_a': params.amountA.toString(), + 'amount_b': params.amountB.toString(), + 'deadline': deadline, + if (params.minAmountA != null) 'min_amount_a': params.minAmountA.toString(), + if (params.minAmountB != null) 'min_amount_b': params.minAmountB.toString(), + }, LiquidityResult.fromJson); + } + + Future removeLiquidity(RemoveLiquidityParams params) async { + final deadline = params.deadline ?? + (DateTime.now().millisecondsSinceEpoch ~/ 1000) + 1200; + return _post('/liquidity/remove', { + 'pool': params.pool, + 'lp_amount': params.lpAmount.toString(), + 'deadline': deadline, + if (params.minAmountA != null) 'min_amount_a': params.minAmountA.toString(), + if (params.minAmountB != null) 'min_amount_b': params.minAmountB.toString(), + }, LiquidityResult.fromJson); + } + + Future> getMyPositions() async { + return _getList('/liquidity/positions', LPPosition.fromJson); + } + + // Analytics + + Future> getPriceHistory(String pair, String interval, {int limit = 100}) async { + return _getList('/analytics/candles/$pair?interval=$interval&limit=$limit', OHLCV.fromJson); + } + + Future> getTradeHistory(String pair, {int limit = 50}) async { + return _getList('/analytics/trades/$pair?limit=$limit', TradeHistory.fromJson); + } + + Future getVolumeStats() async { + return _get('/analytics/volume', VolumeStats.fromJson); + } + + Future getTVL() async { + return _get('/analytics/tvl', TVLStats.fromJson); + } + + // Lifecycle + + Future healthCheck() async { + try { + final response = await _get('/health', (json) => json['status'] as String); + return response == 'healthy'; + } catch (_) { + return false; + } + } + + void close() { + _closed = true; + _client.close(); + } + + bool get isClosed => _closed; + + // Internal methods + + Future _get(String path, T Function(Map) fromJson) async { + _checkClosed(); + final response = await _client.get( + Uri.parse('${config.endpoint}$path'), + headers: _headers, + ); + return _handleResponse(response, fromJson); + } + + Future> _getList(String path, T Function(Map) fromJson) async { + _checkClosed(); + final response = await _client.get( + Uri.parse('${config.endpoint}$path'), + headers: _headers, + ); + _checkResponse(response); + final list = jsonDecode(response.body) as List; + return list.map((e) => fromJson(e as Map)).toList(); + } + + Future _post(String path, Map body, T Function(Map) fromJson) async { + _checkClosed(); + final response = await _client.post( + Uri.parse('${config.endpoint}$path'), + headers: _headers, + body: jsonEncode(body), + ); + return _handleResponse(response, fromJson); + } + + Future _delete(String path, T Function(Map) fromJson) async { + _checkClosed(); + final response = await _client.delete( + Uri.parse('${config.endpoint}$path'), + headers: _headers, + ); + return _handleResponse(response, fromJson); + } + + Map get _headers => { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${config.apiKey}', + 'X-SDK-Version': 'flutter/0.1.0', + }; + + T _handleResponse(http.Response response, T Function(Map) fromJson) { + _checkResponse(response); + final json = jsonDecode(response.body) as Map; + return fromJson(json); + } + + void _checkResponse(http.Response response) { + if (response.statusCode >= 400) { + Map? error; + try { + error = jsonDecode(response.body) as Map; + } catch (_) {} + throw DexException( + error?['message'] ?? 'HTTP ${response.statusCode}', + error?['code'], + response.statusCode, + ); + } + } + + void _checkClosed() { + if (_closed) { + throw DexException('Client has been closed', 'CLIENT_CLOSED', 0); + } + } +} + +/// Perpetual futures sub-client +class PerpsClient { + final SynorDex _dex; + PerpsClient(this._dex); + + Future> listMarkets() async { + return _dex._getList('/perps/markets', PerpMarket.fromJson); + } + + Future getMarket(String symbol) async { + return _dex._get('/perps/markets/$symbol', PerpMarket.fromJson); + } + + Future openPosition(OpenPositionParams params) async { + return _dex._post('/perps/positions', { + 'market': params.market, + 'side': params.side.name, + 'size': params.size.toString(), + 'leverage': params.leverage, + 'order_type': params.orderType.name, + 'margin_type': params.marginType.name, + 'reduce_only': params.reduceOnly, + if (params.limitPrice != null) 'limit_price': params.limitPrice, + if (params.stopLoss != null) 'stop_loss': params.stopLoss, + if (params.takeProfit != null) 'take_profit': params.takeProfit, + }, PerpPosition.fromJson); + } + + Future closePosition(ClosePositionParams params) async { + return _dex._post('/perps/positions/close', { + 'market': params.market, + 'order_type': params.orderType.name, + if (params.size != null) 'size': params.size.toString(), + if (params.limitPrice != null) 'limit_price': params.limitPrice, + }, PerpPosition.fromJson); + } + + Future> getPositions() async { + return _dex._getList('/perps/positions', PerpPosition.fromJson); + } + + Future> getOrders() async { + return _dex._getList('/perps/orders', PerpOrder.fromJson); + } + + Future> getFundingHistory(String market, {int limit = 100}) async { + return _dex._getList('/perps/funding/$market?limit=$limit', FundingPayment.fromJson); + } + + Future getFundingRate(String market) async { + return _dex._get('/perps/funding/$market/current', FundingRateInfo.fromJson); + } +} + +/// Order book sub-client +class OrderBookClient { + final SynorDex _dex; + OrderBookClient(this._dex); + + Future getOrderBook(String market, {int depth = 20}) async { + return _dex._get('/orderbook/$market?depth=$depth', OrderBook.fromJson); + } + + Future placeLimitOrder(LimitOrderParams params) async { + return _dex._post('/orderbook/orders', { + 'market': params.market, + 'side': params.side, + 'price': params.price, + 'size': params.size.toString(), + 'time_in_force': params.timeInForce.name, + 'post_only': params.postOnly, + }, Order.fromJson); + } + + Future> getOpenOrders({String? market}) async { + final path = market != null ? '/orderbook/orders?market=$market' : '/orderbook/orders'; + return _dex._getList(path, Order.fromJson); + } + + Future> getOrderHistory({int limit = 50}) async { + return _dex._getList('/orderbook/orders/history?limit=$limit', Order.fromJson); + } +} + +/// Farms sub-client +class FarmsClient { + final SynorDex _dex; + FarmsClient(this._dex); + + Future> listFarms() async { + return _dex._getList('/farms', Farm.fromJson); + } + + Future getFarm(String farmId) async { + return _dex._get('/farms/$farmId', Farm.fromJson); + } + + Future stake(StakeParams params) async { + return _dex._post('/farms/stake', { + 'farm': params.farm, + 'amount': params.amount.toString(), + }, FarmPosition.fromJson); + } + + Future unstake(String farm, BigInt amount) async { + return _dex._post('/farms/unstake', { + 'farm': farm, + 'amount': amount.toString(), + }, FarmPosition.fromJson); + } + + Future claimRewards(String farm) async { + return _dex._post('/farms/claim', {'farm': farm}, ClaimRewardsResult.fromJson); + } + + Future> getMyFarmPositions() async { + return _dex._getList('/farms/positions', FarmPosition.fromJson); + } +} diff --git a/sdk/flutter/lib/src/dex/types.dart b/sdk/flutter/lib/src/dex/types.dart new file mode 100644 index 0000000..524e127 --- /dev/null +++ b/sdk/flutter/lib/src/dex/types.dart @@ -0,0 +1,776 @@ +/// Synor DEX SDK Types for Flutter/Dart + +/// Pool type +enum PoolType { constantProduct, stable, concentrated } + +/// Position side for perpetuals +enum PositionSide { long, short } + +/// Order type +enum OrderType { market, limit, stopMarket, stopLimit, takeProfit, takeProfitLimit } + +/// Margin type +enum MarginType { cross, isolated } + +/// Time in force +enum TimeInForce { GTC, IOC, FOK, GTD } + +/// Order status +enum OrderStatus { pending, open, partiallyFilled, filled, cancelled, expired } + +/// DEX exception +class DexException implements Exception { + final String message; + final String? code; + final int status; + + DexException(this.message, this.code, this.status); + + @override + String toString() => 'DexException: $message (code: $code, status: $status)'; +} + +/// DEX configuration +class DexConfig { + final String apiKey; + final String endpoint; + final String wsEndpoint; + final int timeout; + final int retries; + final bool debug; + + DexConfig({ + required this.apiKey, + this.endpoint = 'https://dex.synor.io/v1', + this.wsEndpoint = 'wss://dex.synor.io/v1/ws', + this.timeout = 30000, + this.retries = 3, + this.debug = false, + }); +} + +/// Token +class Token { + final String address; + final String symbol; + final String name; + final int decimals; + final BigInt totalSupply; + final double? priceUsd; + final String? logoUrl; + final bool verified; + + Token({ + required this.address, + required this.symbol, + required this.name, + required this.decimals, + required this.totalSupply, + this.priceUsd, + this.logoUrl, + this.verified = false, + }); + + factory Token.fromJson(Map json) => Token( + address: json['address'] as String, + symbol: json['symbol'] as String, + name: json['name'] as String, + decimals: json['decimals'] as int, + totalSupply: BigInt.parse(json['total_supply'] as String), + priceUsd: json['price_usd'] as double?, + logoUrl: json['logo_url'] as String?, + verified: json['verified'] as bool? ?? false, + ); +} + +/// Pool +class Pool { + final String id; + final Token tokenA; + final Token tokenB; + final PoolType poolType; + final BigInt reserveA; + final BigInt reserveB; + final double fee; + final double tvlUsd; + final double volume24h; + final double apr; + final String lpTokenAddress; + + Pool({ + required this.id, + required this.tokenA, + required this.tokenB, + required this.poolType, + required this.reserveA, + required this.reserveB, + required this.fee, + required this.tvlUsd, + required this.volume24h, + required this.apr, + required this.lpTokenAddress, + }); + + factory Pool.fromJson(Map json) => Pool( + id: json['id'] as String, + tokenA: Token.fromJson(json['token_a'] as Map), + tokenB: Token.fromJson(json['token_b'] as Map), + poolType: PoolType.values.firstWhere((e) => e.name == json['pool_type']), + reserveA: BigInt.parse(json['reserve_a'] as String), + reserveB: BigInt.parse(json['reserve_b'] as String), + fee: json['fee'] as double, + tvlUsd: json['tvl_usd'] as double, + volume24h: json['volume_24h'] as double, + apr: json['apr'] as double, + lpTokenAddress: json['lp_token_address'] as String, + ); +} + +/// Pool filter +class PoolFilter { + final List? tokens; + final double? minTvl; + final double? minVolume24h; + final bool? verified; + final int? limit; + final int? offset; + + PoolFilter({this.tokens, this.minTvl, this.minVolume24h, this.verified, this.limit, this.offset}); +} + +/// Quote +class Quote { + final String tokenIn; + final String tokenOut; + final BigInt amountIn; + final BigInt amountOut; + final double priceImpact; + final List route; + final BigInt fee; + final BigInt minimumReceived; + final int expiresAt; + + Quote({ + required this.tokenIn, + required this.tokenOut, + required this.amountIn, + required this.amountOut, + required this.priceImpact, + required this.route, + required this.fee, + required this.minimumReceived, + required this.expiresAt, + }); + + factory Quote.fromJson(Map json) => Quote( + tokenIn: json['token_in'] as String, + tokenOut: json['token_out'] as String, + amountIn: BigInt.parse(json['amount_in'] as String), + amountOut: BigInt.parse(json['amount_out'] as String), + priceImpact: json['price_impact'] as double, + route: (json['route'] as List).cast(), + fee: BigInt.parse(json['fee'] as String), + minimumReceived: BigInt.parse(json['minimum_received'] as String), + expiresAt: json['expires_at'] as int, + ); +} + +/// Quote params +class QuoteParams { + final String tokenIn; + final String tokenOut; + final BigInt amountIn; + final double? slippage; + + QuoteParams({required this.tokenIn, required this.tokenOut, required this.amountIn, this.slippage}); +} + +/// Swap params +class SwapParams { + final String tokenIn; + final String tokenOut; + final BigInt amountIn; + final BigInt minAmountOut; + final int? deadline; + final String? recipient; + + SwapParams({ + required this.tokenIn, + required this.tokenOut, + required this.amountIn, + required this.minAmountOut, + this.deadline, + this.recipient, + }); +} + +/// Swap result +class SwapResult { + final String transactionHash; + final BigInt amountIn; + final BigInt amountOut; + final double effectivePrice; + final BigInt feePaid; + final List route; + + SwapResult({ + required this.transactionHash, + required this.amountIn, + required this.amountOut, + required this.effectivePrice, + required this.feePaid, + required this.route, + }); + + factory SwapResult.fromJson(Map json) => SwapResult( + transactionHash: json['transaction_hash'] as String, + amountIn: BigInt.parse(json['amount_in'] as String), + amountOut: BigInt.parse(json['amount_out'] as String), + effectivePrice: json['effective_price'] as double, + feePaid: BigInt.parse(json['fee_paid'] as String), + route: (json['route'] as List).cast(), + ); +} + +/// Add liquidity params +class AddLiquidityParams { + final String tokenA; + final String tokenB; + final BigInt amountA; + final BigInt amountB; + final BigInt? minAmountA; + final BigInt? minAmountB; + final int? deadline; + + AddLiquidityParams({ + required this.tokenA, + required this.tokenB, + required this.amountA, + required this.amountB, + this.minAmountA, + this.minAmountB, + this.deadline, + }); +} + +/// Remove liquidity params +class RemoveLiquidityParams { + final String pool; + final BigInt lpAmount; + final BigInt? minAmountA; + final BigInt? minAmountB; + final int? deadline; + + RemoveLiquidityParams({ + required this.pool, + required this.lpAmount, + this.minAmountA, + this.minAmountB, + this.deadline, + }); +} + +/// Liquidity result +class LiquidityResult { + final String transactionHash; + final BigInt amountA; + final BigInt amountB; + final BigInt lpTokens; + final double poolShare; + + LiquidityResult({ + required this.transactionHash, + required this.amountA, + required this.amountB, + required this.lpTokens, + required this.poolShare, + }); + + factory LiquidityResult.fromJson(Map json) => LiquidityResult( + transactionHash: json['transaction_hash'] as String, + amountA: BigInt.parse(json['amount_a'] as String), + amountB: BigInt.parse(json['amount_b'] as String), + lpTokens: BigInt.parse(json['lp_tokens'] as String), + poolShare: json['pool_share'] as double, + ); +} + +/// LP position +class LPPosition { + final String poolId; + final BigInt lpTokens; + final BigInt tokenAAmount; + final BigInt tokenBAmount; + final double valueUsd; + final BigInt unclaimedFeesA; + final BigInt unclaimedFeesB; + final double impermanentLoss; + + LPPosition({ + required this.poolId, + required this.lpTokens, + required this.tokenAAmount, + required this.tokenBAmount, + required this.valueUsd, + required this.unclaimedFeesA, + required this.unclaimedFeesB, + required this.impermanentLoss, + }); + + factory LPPosition.fromJson(Map json) => LPPosition( + poolId: json['pool_id'] as String, + lpTokens: BigInt.parse(json['lp_tokens'] as String), + tokenAAmount: BigInt.parse(json['token_a_amount'] as String), + tokenBAmount: BigInt.parse(json['token_b_amount'] as String), + valueUsd: json['value_usd'] as double, + unclaimedFeesA: BigInt.parse(json['unclaimed_fees_a'] as String), + unclaimedFeesB: BigInt.parse(json['unclaimed_fees_b'] as String), + impermanentLoss: json['impermanent_loss'] as double, + ); +} + +/// Perp market +class PerpMarket { + final String symbol; + final String baseAsset; + final String quoteAsset; + final double indexPrice; + final double markPrice; + final double fundingRate; + final int nextFundingTime; + final BigInt openInterest; + final double volume24h; + final int maxLeverage; + + PerpMarket({ + required this.symbol, + required this.baseAsset, + required this.quoteAsset, + required this.indexPrice, + required this.markPrice, + required this.fundingRate, + required this.nextFundingTime, + required this.openInterest, + required this.volume24h, + required this.maxLeverage, + }); + + factory PerpMarket.fromJson(Map json) => PerpMarket( + symbol: json['symbol'] as String, + baseAsset: json['base_asset'] as String, + quoteAsset: json['quote_asset'] as String, + indexPrice: json['index_price'] as double, + markPrice: json['mark_price'] as double, + fundingRate: json['funding_rate'] as double, + nextFundingTime: json['next_funding_time'] as int, + openInterest: BigInt.parse(json['open_interest'] as String), + volume24h: json['volume_24h'] as double, + maxLeverage: json['max_leverage'] as int, + ); +} + +/// Open position params +class OpenPositionParams { + final String market; + final PositionSide side; + final BigInt size; + final int leverage; + final OrderType orderType; + final double? limitPrice; + final double? stopLoss; + final double? takeProfit; + final MarginType marginType; + final bool reduceOnly; + + OpenPositionParams({ + required this.market, + required this.side, + required this.size, + required this.leverage, + required this.orderType, + this.limitPrice, + this.stopLoss, + this.takeProfit, + this.marginType = MarginType.cross, + this.reduceOnly = false, + }); +} + +/// Close position params +class ClosePositionParams { + final String market; + final BigInt? size; + final OrderType orderType; + final double? limitPrice; + + ClosePositionParams({ + required this.market, + this.size, + this.orderType = OrderType.market, + this.limitPrice, + }); +} + +/// Perp position +class PerpPosition { + final String id; + final String market; + final PositionSide side; + final BigInt size; + final double entryPrice; + final double markPrice; + final double liquidationPrice; + final BigInt margin; + final int leverage; + final BigInt unrealizedPnl; + + PerpPosition({ + required this.id, + required this.market, + required this.side, + required this.size, + required this.entryPrice, + required this.markPrice, + required this.liquidationPrice, + required this.margin, + required this.leverage, + required this.unrealizedPnl, + }); + + factory PerpPosition.fromJson(Map json) => PerpPosition( + id: json['id'] as String, + market: json['market'] as String, + side: PositionSide.values.firstWhere((e) => e.name == json['side']), + size: BigInt.parse(json['size'] as String), + entryPrice: json['entry_price'] as double, + markPrice: json['mark_price'] as double, + liquidationPrice: json['liquidation_price'] as double, + margin: BigInt.parse(json['margin'] as String), + leverage: json['leverage'] as int, + unrealizedPnl: BigInt.parse(json['unrealized_pnl'] as String), + ); +} + +/// Perp order +class PerpOrder { + final String id; + final String market; + final PositionSide side; + final OrderType orderType; + final BigInt size; + final double? price; + final OrderStatus status; + + PerpOrder({ + required this.id, + required this.market, + required this.side, + required this.orderType, + required this.size, + this.price, + required this.status, + }); + + factory PerpOrder.fromJson(Map json) => PerpOrder( + id: json['id'] as String, + market: json['market'] as String, + side: PositionSide.values.firstWhere((e) => e.name == json['side']), + orderType: OrderType.values.firstWhere((e) => e.name == json['order_type']), + size: BigInt.parse(json['size'] as String), + price: json['price'] as double?, + status: OrderStatus.values.firstWhere((e) => e.name == json['status']), + ); +} + +/// Funding payment +class FundingPayment { + final String market; + final BigInt amount; + final double rate; + final BigInt positionSize; + final int timestamp; + + FundingPayment({ + required this.market, + required this.amount, + required this.rate, + required this.positionSize, + required this.timestamp, + }); + + factory FundingPayment.fromJson(Map json) => FundingPayment( + market: json['market'] as String, + amount: BigInt.parse(json['amount'] as String), + rate: json['rate'] as double, + positionSize: BigInt.parse(json['position_size'] as String), + timestamp: json['timestamp'] as int, + ); +} + +/// Funding rate info +class FundingRateInfo { + final double rate; + final int nextTime; + + FundingRateInfo({required this.rate, required this.nextTime}); + + factory FundingRateInfo.fromJson(Map json) => FundingRateInfo( + rate: json['rate'] as double, + nextTime: json['next_time'] as int, + ); +} + +/// Order book entry +class OrderBookEntry { + final double price; + final BigInt size; + final int orders; + + OrderBookEntry({required this.price, required this.size, required this.orders}); + + factory OrderBookEntry.fromJson(Map json) => OrderBookEntry( + price: json['price'] as double, + size: BigInt.parse(json['size'] as String), + orders: json['orders'] as int, + ); +} + +/// Order book +class OrderBook { + final String market; + final List bids; + final List asks; + final int timestamp; + + OrderBook({required this.market, required this.bids, required this.asks, required this.timestamp}); + + factory OrderBook.fromJson(Map json) => OrderBook( + market: json['market'] as String, + bids: (json['bids'] as List).map((e) => OrderBookEntry.fromJson(e as Map)).toList(), + asks: (json['asks'] as List).map((e) => OrderBookEntry.fromJson(e as Map)).toList(), + timestamp: json['timestamp'] as int, + ); +} + +/// Limit order params +class LimitOrderParams { + final String market; + final String side; + final double price; + final BigInt size; + final TimeInForce timeInForce; + final bool postOnly; + + LimitOrderParams({ + required this.market, + required this.side, + required this.price, + required this.size, + this.timeInForce = TimeInForce.GTC, + this.postOnly = false, + }); +} + +/// Order +class Order { + final String id; + final String market; + final String side; + final double price; + final BigInt size; + final BigInt filledSize; + final OrderStatus status; + final TimeInForce timeInForce; + final bool postOnly; + + Order({ + required this.id, + required this.market, + required this.side, + required this.price, + required this.size, + required this.filledSize, + required this.status, + required this.timeInForce, + required this.postOnly, + }); + + factory Order.fromJson(Map json) => Order( + id: json['id'] as String, + market: json['market'] as String, + side: json['side'] as String, + price: json['price'] as double, + size: BigInt.parse(json['size'] as String), + filledSize: BigInt.parse(json['filled_size'] as String), + status: OrderStatus.values.firstWhere((e) => e.name == json['status']), + timeInForce: TimeInForce.values.firstWhere((e) => e.name == json['time_in_force']), + postOnly: json['post_only'] as bool, + ); +} + +/// Farm +class Farm { + final String id; + final String name; + final Token stakeToken; + final List rewardTokens; + final double tvlUsd; + final double apr; + + Farm({ + required this.id, + required this.name, + required this.stakeToken, + required this.rewardTokens, + required this.tvlUsd, + required this.apr, + }); + + factory Farm.fromJson(Map json) => Farm( + id: json['id'] as String, + name: json['name'] as String, + stakeToken: Token.fromJson(json['stake_token'] as Map), + rewardTokens: (json['reward_tokens'] as List).map((e) => Token.fromJson(e as Map)).toList(), + tvlUsd: json['tvl_usd'] as double, + apr: json['apr'] as double, + ); +} + +/// Stake params +class StakeParams { + final String farm; + final BigInt amount; + + StakeParams({required this.farm, required this.amount}); +} + +/// Farm position +class FarmPosition { + final String farmId; + final BigInt stakedAmount; + final List pendingRewards; + final int stakedAt; + + FarmPosition({ + required this.farmId, + required this.stakedAmount, + required this.pendingRewards, + required this.stakedAt, + }); + + factory FarmPosition.fromJson(Map json) => FarmPosition( + farmId: json['farm_id'] as String, + stakedAmount: BigInt.parse(json['staked_amount'] as String), + pendingRewards: (json['pending_rewards'] as List).map((e) => BigInt.parse(e as String)).toList(), + stakedAt: json['staked_at'] as int, + ); +} + +/// Claim rewards result +class ClaimRewardsResult { + final BigInt amount; + final String transactionHash; + + ClaimRewardsResult({required this.amount, required this.transactionHash}); + + factory ClaimRewardsResult.fromJson(Map json) => ClaimRewardsResult( + amount: BigInt.parse(json['amount'] as String), + transactionHash: json['transaction_hash'] as String, + ); +} + +/// OHLCV +class OHLCV { + final int timestamp; + final double open; + final double high; + final double low; + final double close; + final double volume; + + OHLCV({ + required this.timestamp, + required this.open, + required this.high, + required this.low, + required this.close, + required this.volume, + }); + + factory OHLCV.fromJson(Map json) => OHLCV( + timestamp: json['timestamp'] as int, + open: json['open'] as double, + high: json['high'] as double, + low: json['low'] as double, + close: json['close'] as double, + volume: json['volume'] as double, + ); +} + +/// Trade history +class TradeHistory { + final String id; + final String market; + final String side; + final double price; + final BigInt size; + final int timestamp; + final String maker; + final String taker; + + TradeHistory({ + required this.id, + required this.market, + required this.side, + required this.price, + required this.size, + required this.timestamp, + required this.maker, + required this.taker, + }); + + factory TradeHistory.fromJson(Map json) => TradeHistory( + id: json['id'] as String, + market: json['market'] as String, + side: json['side'] as String, + price: json['price'] as double, + size: BigInt.parse(json['size'] as String), + timestamp: json['timestamp'] as int, + maker: json['maker'] as String, + taker: json['taker'] as String, + ); +} + +/// Volume stats +class VolumeStats { + final double volume24h; + final double volume7d; + final double volume30d; + final int trades24h; + + VolumeStats({required this.volume24h, required this.volume7d, required this.volume30d, required this.trades24h}); + + factory VolumeStats.fromJson(Map json) => VolumeStats( + volume24h: json['volume_24h'] as double, + volume7d: json['volume_7d'] as double, + volume30d: json['volume_30d'] as double, + trades24h: json['trades_24h'] as int, + ); +} + +/// TVL stats +class TVLStats { + final double totalTvl; + final double poolsTvl; + final double farmsTvl; + final double perpsTvl; + + TVLStats({required this.totalTvl, required this.poolsTvl, required this.farmsTvl, required this.perpsTvl}); + + factory TVLStats.fromJson(Map json) => TVLStats( + totalTvl: json['total_tvl'] as double, + poolsTvl: json['pools_tvl'] as double, + farmsTvl: json['farms_tvl'] as double, + perpsTvl: json['perps_tvl'] as double, + ); +} diff --git a/sdk/go/dex/client.go b/sdk/go/dex/client.go new file mode 100644 index 0000000..bfb24e0 --- /dev/null +++ b/sdk/go/dex/client.go @@ -0,0 +1,805 @@ +// 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<= 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 +} diff --git a/sdk/go/dex/types.go b/sdk/go/dex/types.go new file mode 100644 index 0000000..fc1c7b1 --- /dev/null +++ b/sdk/go/dex/types.go @@ -0,0 +1,454 @@ +// Package dex provides the Synor DEX SDK for Go +// +// 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 +package dex + +import ( + "math/big" + "time" +) + +// PoolType represents the type of liquidity pool +type PoolType string + +const ( + PoolTypeConstantProduct PoolType = "constant_product" // x * y = k + PoolTypeStable PoolType = "stable" // Optimized for stablecoins + PoolTypeConcentrated PoolType = "concentrated" // Uniswap v3 style +) + +// PositionSide represents the side of a perpetual position +type PositionSide string + +const ( + PositionSideLong PositionSide = "long" + PositionSideShort PositionSide = "short" +) + +// OrderType represents the type of order +type OrderType string + +const ( + OrderTypeMarket OrderType = "market" + OrderTypeLimit OrderType = "limit" + OrderTypeStopMarket OrderType = "stop_market" + OrderTypeStopLimit OrderType = "stop_limit" + OrderTypeTakeProfit OrderType = "take_profit" + OrderTypeTakeProfitLimit OrderType = "take_profit_limit" +) + +// MarginType represents the margin type for perpetuals +type MarginType string + +const ( + MarginTypeCross MarginType = "cross" + MarginTypeIsolated MarginType = "isolated" +) + +// TimeInForce represents the time in force for limit orders +type TimeInForce string + +const ( + TimeInForceGTC TimeInForce = "GTC" // Good Till Cancel + TimeInForceIOC TimeInForce = "IOC" // Immediate Or Cancel + TimeInForceFOK TimeInForce = "FOK" // Fill Or Kill + TimeInForceGTD TimeInForce = "GTD" // Good Till Date +) + +// OrderStatus represents the status of an order +type OrderStatus string + +const ( + OrderStatusPending OrderStatus = "pending" + OrderStatusOpen OrderStatus = "open" + OrderStatusPartiallyFilled OrderStatus = "partially_filled" + OrderStatusFilled OrderStatus = "filled" + OrderStatusCancelled OrderStatus = "cancelled" + OrderStatusExpired OrderStatus = "expired" +) + +// Config holds the DEX client configuration +type Config struct { + APIKey string + Endpoint string + WSEndpoint string + Timeout time.Duration + Retries int + Debug bool +} + +// DefaultConfig returns a Config with default values +func DefaultConfig(apiKey string) Config { + return Config{ + APIKey: apiKey, + Endpoint: "https://dex.synor.io/v1", + WSEndpoint: "wss://dex.synor.io/v1/ws", + Timeout: 30 * time.Second, + Retries: 3, + Debug: false, + } +} + +// Token represents token information +type Token struct { + Address string `json:"address"` + Symbol string `json:"symbol"` + Name string `json:"name"` + Decimals int `json:"decimals"` + TotalSupply *big.Int `json:"total_supply"` + PriceUSD *float64 `json:"price_usd,omitempty"` + LogoURL *string `json:"logo_url,omitempty"` + Verified bool `json:"verified"` +} + +// Pool represents a liquidity pool +type Pool struct { + ID string `json:"id"` + TokenA Token `json:"token_a"` + TokenB Token `json:"token_b"` + PoolType PoolType `json:"pool_type"` + ReserveA *big.Int `json:"reserve_a"` + ReserveB *big.Int `json:"reserve_b"` + Fee float64 `json:"fee"` + TVLUSD float64 `json:"tvl_usd"` + Volume24h float64 `json:"volume_24h"` + APR float64 `json:"apr"` + LPTokenAddress string `json:"lp_token_address"` + TickSpacing *int `json:"tick_spacing,omitempty"` // For concentrated liquidity + SqrtPrice *string `json:"sqrt_price,omitempty"` +} + +// PoolFilter holds pool listing filters +type PoolFilter struct { + Tokens []string + MinTVL *float64 + MinVolume24h *float64 + Verified *bool + Limit *int + Offset *int +} + +// Quote represents a swap quote +type Quote struct { + TokenIn string `json:"token_in"` + TokenOut string `json:"token_out"` + AmountIn *big.Int `json:"amount_in"` + AmountOut *big.Int `json:"amount_out"` + PriceImpact float64 `json:"price_impact"` + Route []string `json:"route"` + Fee *big.Int `json:"fee"` + MinimumReceived *big.Int `json:"minimum_received"` + ExpiresAt int64 `json:"expires_at"` +} + +// QuoteParams holds parameters for getting a quote +type QuoteParams struct { + TokenIn string + TokenOut string + AmountIn *big.Int + Slippage float64 // Default: 0.005 +} + +// SwapParams holds parameters for executing a swap +type SwapParams struct { + TokenIn string + TokenOut string + AmountIn *big.Int + MinAmountOut *big.Int + Deadline *int64 + Recipient *string +} + +// SwapResult represents the result of a swap +type SwapResult struct { + TransactionHash string `json:"transaction_hash"` + AmountIn *big.Int `json:"amount_in"` + AmountOut *big.Int `json:"amount_out"` + EffectivePrice float64 `json:"effective_price"` + FeePaid *big.Int `json:"fee_paid"` + Route []string `json:"route"` +} + +// AddLiquidityParams holds parameters for adding liquidity +type AddLiquidityParams struct { + TokenA string + TokenB string + AmountA *big.Int + AmountB *big.Int + MinAmountA *big.Int + MinAmountB *big.Int + Deadline *int64 + TickLower *int // For concentrated liquidity + TickUpper *int +} + +// RemoveLiquidityParams holds parameters for removing liquidity +type RemoveLiquidityParams struct { + Pool string + LPAmount *big.Int + MinAmountA *big.Int + MinAmountB *big.Int + Deadline *int64 +} + +// LiquidityResult represents the result of a liquidity operation +type LiquidityResult struct { + TransactionHash string `json:"transaction_hash"` + AmountA *big.Int `json:"amount_a"` + AmountB *big.Int `json:"amount_b"` + LPTokens *big.Int `json:"lp_tokens"` + PoolShare float64 `json:"pool_share"` +} + +// LPPosition represents an LP position +type LPPosition struct { + PoolID string `json:"pool_id"` + LPTokens *big.Int `json:"lp_tokens"` + TokenAAmount *big.Int `json:"token_a_amount"` + TokenBAmount *big.Int `json:"token_b_amount"` + ValueUSD float64 `json:"value_usd"` + UnclaimedFeesA *big.Int `json:"unclaimed_fees_a"` + UnclaimedFeesB *big.Int `json:"unclaimed_fees_b"` + ImpermanentLoss float64 `json:"impermanent_loss"` + TickLower *int `json:"tick_lower,omitempty"` + TickUpper *int `json:"tick_upper,omitempty"` + InRange *bool `json:"in_range,omitempty"` +} + +// PerpMarket represents a perpetual futures market +type PerpMarket struct { + Symbol string `json:"symbol"` + BaseAsset string `json:"base_asset"` + QuoteAsset string `json:"quote_asset"` + IndexPrice float64 `json:"index_price"` + MarkPrice float64 `json:"mark_price"` + FundingRate float64 `json:"funding_rate"` + NextFundingTime int64 `json:"next_funding_time"` + OpenInterest *big.Int `json:"open_interest"` + Volume24h float64 `json:"volume_24h"` + PriceChange24h float64 `json:"price_change_24h"` + MaxLeverage int `json:"max_leverage"` + MinOrderSize *big.Int `json:"min_order_size"` + TickSize float64 `json:"tick_size"` + MaintenanceMargin float64 `json:"maintenance_margin"` + InitialMargin float64 `json:"initial_margin"` +} + +// OpenPositionParams holds parameters for opening a perpetual position +type OpenPositionParams struct { + Market string + Side PositionSide + Size *big.Int + Leverage int // 1-100x + OrderType OrderType + LimitPrice *float64 + StopLoss *float64 + TakeProfit *float64 + MarginType MarginType // Default: Cross + ReduceOnly bool +} + +// ClosePositionParams holds parameters for closing a perpetual position +type ClosePositionParams struct { + Market string + Size *big.Int // nil = close entire position + OrderType OrderType + LimitPrice *float64 +} + +// ModifyPositionParams holds parameters for modifying a perpetual position +type ModifyPositionParams struct { + PositionID string + NewLeverage *int + NewMargin *big.Int + NewStopLoss *float64 + NewTakeProfit *float64 +} + +// PerpPosition represents a perpetual position +type PerpPosition struct { + ID string `json:"id"` + Market string `json:"market"` + Side PositionSide `json:"side"` + Size *big.Int `json:"size"` + EntryPrice float64 `json:"entry_price"` + MarkPrice float64 `json:"mark_price"` + LiquidationPrice float64 `json:"liquidation_price"` + Margin *big.Int `json:"margin"` + Leverage int `json:"leverage"` + UnrealizedPNL *big.Int `json:"unrealized_pnl"` + RealizedPNL *big.Int `json:"realized_pnl"` + MarginRatio float64 `json:"margin_ratio"` + StopLoss *float64 `json:"stop_loss,omitempty"` + TakeProfit *float64 `json:"take_profit,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +// PerpOrder represents a perpetual order +type PerpOrder struct { + ID string `json:"id"` + Market string `json:"market"` + Side PositionSide `json:"side"` + OrderType OrderType `json:"order_type"` + Size *big.Int `json:"size"` + Price *float64 `json:"price,omitempty"` + FilledSize *big.Int `json:"filled_size"` + Status OrderStatus `json:"status"` + ReduceOnly bool `json:"reduce_only"` + CreatedAt int64 `json:"created_at"` +} + +// FundingPayment represents a funding payment record +type FundingPayment struct { + Market string `json:"market"` + Amount *big.Int `json:"amount"` + Rate float64 `json:"rate"` + PositionSize *big.Int `json:"position_size"` + Timestamp int64 `json:"timestamp"` +} + +// OrderBookEntry represents an order book entry +type OrderBookEntry struct { + Price float64 `json:"price"` + Size *big.Int `json:"size"` + Orders int `json:"orders"` +} + +// OrderBook represents an order book +type OrderBook struct { + Market string `json:"market"` + Bids []OrderBookEntry `json:"bids"` + Asks []OrderBookEntry `json:"asks"` + Timestamp int64 `json:"timestamp"` +} + +// LimitOrderParams holds parameters for placing a limit order +type LimitOrderParams struct { + Market string + Side string // "buy" or "sell" + Price float64 + Size *big.Int + TimeInForce TimeInForce + PostOnly bool +} + +// Order represents a limit order +type Order struct { + ID string `json:"id"` + Market string `json:"market"` + Side string `json:"side"` + Price float64 `json:"price"` + Size *big.Int `json:"size"` + FilledSize *big.Int `json:"filled_size"` + Status OrderStatus `json:"status"` + TimeInForce TimeInForce `json:"time_in_force"` + PostOnly bool `json:"post_only"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +// Farm represents a yield farm +type Farm struct { + ID string `json:"id"` + Name string `json:"name"` + StakeToken Token `json:"stake_token"` + RewardTokens []Token `json:"reward_tokens"` + TVLUSD float64 `json:"tvl_usd"` + APR float64 `json:"apr"` + DailyRewards []*big.Int `json:"daily_rewards"` + LockupPeriod *int64 `json:"lockup_period,omitempty"` + MinStake *big.Int `json:"min_stake,omitempty"` +} + +// StakeParams holds parameters for staking +type StakeParams struct { + Farm string + Amount *big.Int +} + +// FarmPosition represents a farm position +type FarmPosition struct { + FarmID string `json:"farm_id"` + StakedAmount *big.Int `json:"staked_amount"` + PendingRewards []*big.Int `json:"pending_rewards"` + StakedAt int64 `json:"staked_at"` + UnlockAt *int64 `json:"unlock_at,omitempty"` +} + +// OHLCV represents OHLCV candle data +type OHLCV struct { + Timestamp int64 `json:"timestamp"` + Open float64 `json:"open"` + High float64 `json:"high"` + Low float64 `json:"low"` + Close float64 `json:"close"` + Volume float64 `json:"volume"` +} + +// TradeHistory represents a trade history entry +type TradeHistory struct { + ID string `json:"id"` + Market string `json:"market"` + Side string `json:"side"` + Price float64 `json:"price"` + Size *big.Int `json:"size"` + Timestamp int64 `json:"timestamp"` + Maker string `json:"maker"` + Taker string `json:"taker"` +} + +// VolumeStats represents volume statistics +type VolumeStats struct { + Volume24h float64 `json:"volume_24h"` + Volume7d float64 `json:"volume_7d"` + Volume30d float64 `json:"volume_30d"` + Trades24h int `json:"trades_24h"` + UniqueTraders24h int `json:"unique_traders_24h"` +} + +// TVLStats represents TVL statistics +type TVLStats struct { + TotalTVL float64 `json:"total_tvl"` + PoolsTVL float64 `json:"pools_tvl"` + FarmsTVL float64 `json:"farms_tvl"` + PerpsTVL float64 `json:"perps_tvl"` +} + +// Subscription represents a WebSocket subscription +type Subscription struct { + ID string + Channel string + Cancel func() +} + +// Callback types +type PriceCallback func(price float64) +type TradeCallback func(trade TradeHistory) +type OrderBookCallback func(book OrderBook) +type PositionCallback func(position PerpPosition) + +// DexError represents a DEX SDK error +type DexError struct { + Message string + Code string + Status int +} + +func (e *DexError) Error() string { + return e.Message +} + +// NewDexError creates a new DexError +func NewDexError(message string, code string, status int) *DexError { + return &DexError{ + Message: message, + Code: code, + Status: status, + } +} diff --git a/sdk/java/src/main/java/io/synor/dex/DexConfig.java b/sdk/java/src/main/java/io/synor/dex/DexConfig.java new file mode 100644 index 0000000..6904ec7 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/dex/DexConfig.java @@ -0,0 +1,97 @@ +package io.synor.dex; + +/** + * DEX client configuration + */ +public class DexConfig { + private static final String DEFAULT_ENDPOINT = "https://dex.synor.io/v1"; + private static final String DEFAULT_WS_ENDPOINT = "wss://dex.synor.io/v1/ws"; + private static final long DEFAULT_TIMEOUT = 30000; + private static final int DEFAULT_RETRIES = 3; + + private final String apiKey; + private final String endpoint; + private final String wsEndpoint; + private final long timeout; + private final int retries; + private final boolean debug; + + private DexConfig(Builder builder) { + this.apiKey = builder.apiKey; + this.endpoint = builder.endpoint != null ? builder.endpoint : DEFAULT_ENDPOINT; + this.wsEndpoint = builder.wsEndpoint != null ? builder.wsEndpoint : DEFAULT_WS_ENDPOINT; + this.timeout = builder.timeout > 0 ? builder.timeout : DEFAULT_TIMEOUT; + this.retries = builder.retries > 0 ? builder.retries : DEFAULT_RETRIES; + this.debug = builder.debug; + } + + public String getApiKey() { + return apiKey; + } + + public String getEndpoint() { + return endpoint; + } + + public String getWsEndpoint() { + return wsEndpoint; + } + + public long getTimeout() { + return timeout; + } + + public int getRetries() { + return retries; + } + + public boolean isDebug() { + return debug; + } + + public static Builder builder(String apiKey) { + return new Builder(apiKey); + } + + public static class Builder { + private final String apiKey; + private String endpoint; + private String wsEndpoint; + private long timeout; + private int retries; + private boolean debug; + + private Builder(String apiKey) { + this.apiKey = apiKey; + } + + public Builder endpoint(String endpoint) { + this.endpoint = endpoint; + return this; + } + + public Builder wsEndpoint(String wsEndpoint) { + this.wsEndpoint = wsEndpoint; + return this; + } + + public Builder timeout(long timeout) { + this.timeout = timeout; + return this; + } + + public Builder retries(int retries) { + this.retries = retries; + return this; + } + + public Builder debug(boolean debug) { + this.debug = debug; + return this; + } + + public DexConfig build() { + return new DexConfig(this); + } + } +} diff --git a/sdk/java/src/main/java/io/synor/dex/DexException.java b/sdk/java/src/main/java/io/synor/dex/DexException.java new file mode 100644 index 0000000..90096de --- /dev/null +++ b/sdk/java/src/main/java/io/synor/dex/DexException.java @@ -0,0 +1,23 @@ +package io.synor.dex; + +/** + * DEX SDK Exception + */ +public class DexException extends RuntimeException { + private final String code; + private final int status; + + public DexException(String message, String code, int status) { + super(message); + this.code = code; + this.status = status; + } + + public String getCode() { + return code; + } + + public int getStatus() { + return status; + } +} diff --git a/sdk/java/src/main/java/io/synor/dex/FarmsClient.java b/sdk/java/src/main/java/io/synor/dex/FarmsClient.java new file mode 100644 index 0000000..8e6f4fb --- /dev/null +++ b/sdk/java/src/main/java/io/synor/dex/FarmsClient.java @@ -0,0 +1,50 @@ +package io.synor.dex; + +import java.math.BigInteger; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Farms and staking sub-client + */ +public class FarmsClient { + private final SynorDex dex; + + FarmsClient(SynorDex dex) { + this.dex = dex; + } + + public CompletableFuture> listFarms() { + return dex.getList("/farms", Farm.class); + } + + public CompletableFuture getFarm(String farmId) { + return dex.get("/farms/" + farmId, Farm.class); + } + + public CompletableFuture stake(StakeParams params) { + Map body = new HashMap<>(); + body.put("farm", params.getFarm()); + body.put("amount", params.getAmount().toString()); + return dex.post("/farms/stake", body, FarmPosition.class); + } + + public CompletableFuture unstake(String farm, BigInteger amount) { + Map body = new HashMap<>(); + body.put("farm", farm); + body.put("amount", amount.toString()); + return dex.post("/farms/unstake", body, FarmPosition.class); + } + + public CompletableFuture claimRewards(String farm) { + Map body = new HashMap<>(); + body.put("farm", farm); + return dex.post("/farms/claim", body, ClaimRewardsResult.class); + } + + public CompletableFuture> getMyFarmPositions() { + return dex.getList("/farms/positions", FarmPosition.class); + } +} diff --git a/sdk/java/src/main/java/io/synor/dex/OrderBookClient.java b/sdk/java/src/main/java/io/synor/dex/OrderBookClient.java new file mode 100644 index 0000000..8b5c555 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/dex/OrderBookClient.java @@ -0,0 +1,45 @@ +package io.synor.dex; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Order book sub-client + */ +public class OrderBookClient { + private final SynorDex dex; + + OrderBookClient(SynorDex dex) { + this.dex = dex; + } + + public CompletableFuture getOrderBook(String market, int depth) { + return dex.get("/orderbook/" + market + "?depth=" + depth, OrderBook.class); + } + + public CompletableFuture placeLimitOrder(LimitOrderParams params) { + Map body = new HashMap<>(); + body.put("market", params.getMarket()); + body.put("side", params.getSide()); + body.put("price", params.getPrice()); + body.put("size", params.getSize().toString()); + body.put("time_in_force", params.getTimeInForce().name()); + body.put("post_only", params.isPostOnly()); + return dex.post("/orderbook/orders", body, Order.class); + } + + public CompletableFuture cancelOrder(String orderId) { + return dex.delete("/orderbook/orders/" + orderId, Void.class); + } + + public CompletableFuture> getOpenOrders(String market) { + String path = market != null ? "/orderbook/orders?market=" + market : "/orderbook/orders"; + return dex.getList(path, Order.class); + } + + public CompletableFuture> getOrderHistory(int limit) { + return dex.getList("/orderbook/orders/history?limit=" + limit, Order.class); + } +} diff --git a/sdk/java/src/main/java/io/synor/dex/PerpsClient.java b/sdk/java/src/main/java/io/synor/dex/PerpsClient.java new file mode 100644 index 0000000..67e8ea6 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/dex/PerpsClient.java @@ -0,0 +1,109 @@ +package io.synor.dex; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Perpetual futures sub-client + */ +public class PerpsClient { + private final SynorDex dex; + + PerpsClient(SynorDex dex) { + this.dex = dex; + } + + public CompletableFuture> listMarkets() { + return dex.getList("/perps/markets", PerpMarket.class); + } + + public CompletableFuture getMarket(String symbol) { + return dex.get("/perps/markets/" + symbol, PerpMarket.class); + } + + public CompletableFuture openPosition(OpenPositionParams params) { + Map body = new HashMap<>(); + body.put("market", params.getMarket()); + body.put("side", params.getSide().name().toLowerCase()); + body.put("size", params.getSize().toString()); + body.put("leverage", params.getLeverage()); + body.put("order_type", params.getOrderType().name().toLowerCase()); + body.put("margin_type", params.getMarginType().name().toLowerCase()); + body.put("reduce_only", params.isReduceOnly()); + if (params.getLimitPrice() != null) { + body.put("limit_price", params.getLimitPrice()); + } + if (params.getStopLoss() != null) { + body.put("stop_loss", params.getStopLoss()); + } + if (params.getTakeProfit() != null) { + body.put("take_profit", params.getTakeProfit()); + } + return dex.post("/perps/positions", body, PerpPosition.class); + } + + public CompletableFuture closePosition(ClosePositionParams params) { + Map body = new HashMap<>(); + body.put("market", params.getMarket()); + body.put("order_type", params.getOrderType().name().toLowerCase()); + if (params.getSize() != null) { + body.put("size", params.getSize().toString()); + } + if (params.getLimitPrice() != null) { + body.put("limit_price", params.getLimitPrice()); + } + return dex.post("/perps/positions/close", body, PerpPosition.class); + } + + public CompletableFuture modifyPosition(ModifyPositionParams params) { + Map body = new HashMap<>(); + if (params.getNewLeverage() != null) { + body.put("new_leverage", params.getNewLeverage()); + } + if (params.getNewMargin() != null) { + body.put("new_margin", params.getNewMargin().toString()); + } + if (params.getNewStopLoss() != null) { + body.put("new_stop_loss", params.getNewStopLoss()); + } + if (params.getNewTakeProfit() != null) { + body.put("new_take_profit", params.getNewTakeProfit()); + } + return dex.post("/perps/positions/" + params.getPositionId() + "/modify", body, PerpPosition.class); + } + + public CompletableFuture> getPositions() { + return dex.getList("/perps/positions", PerpPosition.class); + } + + public CompletableFuture getPosition(String market) { + return dex.get("/perps/positions/" + market, PerpPosition.class); + } + + public CompletableFuture> getOrders() { + return dex.getList("/perps/orders", PerpOrder.class); + } + + public CompletableFuture cancelOrder(String orderId) { + return dex.delete("/perps/orders/" + orderId, Void.class); + } + + public CompletableFuture cancelAllOrders(String market) { + String path = market != null ? "/perps/orders?market=" + market : "/perps/orders"; + return dex.delete(path, CancelResult.class).thenApply(r -> r.cancelled); + } + + public CompletableFuture> getFundingHistory(String market, int limit) { + return dex.getList("/perps/funding/" + market + "?limit=" + limit, FundingPayment.class); + } + + public CompletableFuture getFundingRate(String market) { + return dex.get("/perps/funding/" + market + "/current", FundingRateInfo.class); + } + + private static class CancelResult { + int cancelled; + } +} diff --git a/sdk/java/src/main/java/io/synor/dex/SynorDex.java b/sdk/java/src/main/java/io/synor/dex/SynorDex.java new file mode 100644 index 0000000..4a1e1e7 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/dex/SynorDex.java @@ -0,0 +1,326 @@ +package io.synor.dex; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import okhttp3.*; + +import java.io.IOException; +import java.math.BigInteger; +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +/** + * 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 implements AutoCloseable { + private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); + + private final DexConfig config; + private final OkHttpClient client; + private final Gson gson; + private volatile boolean closed = false; + + public final PerpsClient perps; + public final OrderBookClient orderbook; + public final FarmsClient farms; + + public SynorDex(DexConfig config) { + this.config = config; + this.client = new OkHttpClient.Builder() + .callTimeout(config.getTimeout(), TimeUnit.MILLISECONDS) + .build(); + this.gson = new GsonBuilder().create(); + + this.perps = new PerpsClient(this); + this.orderbook = new OrderBookClient(this); + this.farms = new FarmsClient(this); + } + + // Token Operations + + public CompletableFuture getToken(String address) { + return get("/tokens/" + address, Token.class); + } + + public CompletableFuture> listTokens() { + return getList("/tokens", Token.class); + } + + public CompletableFuture> searchTokens(String query) { + return getList("/tokens/search?q=" + urlEncode(query), Token.class); + } + + // Pool Operations + + public CompletableFuture getPool(String tokenA, String tokenB) { + return get("/pools/" + tokenA + "/" + tokenB, Pool.class); + } + + public CompletableFuture getPoolById(String poolId) { + return get("/pools/" + poolId, Pool.class); + } + + public CompletableFuture> listPools(PoolFilter filter) { + StringBuilder params = new StringBuilder(); + if (filter != null) { + if (filter.getTokens() != null) { + params.append("tokens=").append(String.join(",", filter.getTokens())); + } + if (filter.getMinTvl() != null) { + appendParam(params, "min_tvl", filter.getMinTvl().toString()); + } + if (filter.getMinVolume24h() != null) { + appendParam(params, "min_volume", filter.getMinVolume24h().toString()); + } + if (filter.getVerified() != null) { + appendParam(params, "verified", filter.getVerified().toString()); + } + if (filter.getLimit() != null) { + appendParam(params, "limit", filter.getLimit().toString()); + } + if (filter.getOffset() != null) { + appendParam(params, "offset", filter.getOffset().toString()); + } + } + String path = params.length() > 0 ? "/pools?" + params : "/pools"; + return getList(path, Pool.class); + } + + // Swap Operations + + public CompletableFuture getQuote(QuoteParams params) { + Map body = new HashMap<>(); + body.put("token_in", params.getTokenIn()); + body.put("token_out", params.getTokenOut()); + body.put("amount_in", params.getAmountIn().toString()); + body.put("slippage", params.getSlippage() != null ? params.getSlippage() : 0.005); + return post("/swap/quote", body, Quote.class); + } + + public CompletableFuture swap(SwapParams params) { + long deadline = params.getDeadline() != null + ? params.getDeadline() + : Instant.now().getEpochSecond() + 1200; + + Map body = new HashMap<>(); + body.put("token_in", params.getTokenIn()); + body.put("token_out", params.getTokenOut()); + body.put("amount_in", params.getAmountIn().toString()); + body.put("min_amount_out", params.getMinAmountOut().toString()); + body.put("deadline", deadline); + if (params.getRecipient() != null) { + body.put("recipient", params.getRecipient()); + } + return post("/swap", body, SwapResult.class); + } + + // Liquidity Operations + + public CompletableFuture addLiquidity(AddLiquidityParams params) { + long deadline = params.getDeadline() != null + ? params.getDeadline() + : Instant.now().getEpochSecond() + 1200; + + Map body = new HashMap<>(); + body.put("token_a", params.getTokenA()); + body.put("token_b", params.getTokenB()); + body.put("amount_a", params.getAmountA().toString()); + body.put("amount_b", params.getAmountB().toString()); + body.put("deadline", deadline); + if (params.getMinAmountA() != null) { + body.put("min_amount_a", params.getMinAmountA().toString()); + } + if (params.getMinAmountB() != null) { + body.put("min_amount_b", params.getMinAmountB().toString()); + } + return post("/liquidity/add", body, LiquidityResult.class); + } + + public CompletableFuture removeLiquidity(RemoveLiquidityParams params) { + long deadline = params.getDeadline() != null + ? params.getDeadline() + : Instant.now().getEpochSecond() + 1200; + + Map body = new HashMap<>(); + body.put("pool", params.getPool()); + body.put("lp_amount", params.getLpAmount().toString()); + body.put("deadline", deadline); + if (params.getMinAmountA() != null) { + body.put("min_amount_a", params.getMinAmountA().toString()); + } + if (params.getMinAmountB() != null) { + body.put("min_amount_b", params.getMinAmountB().toString()); + } + return post("/liquidity/remove", body, LiquidityResult.class); + } + + public CompletableFuture> getMyPositions() { + return getList("/liquidity/positions", LPPosition.class); + } + + // Analytics + + public CompletableFuture> getPriceHistory(String pair, String interval, int limit) { + return getList("/analytics/candles/" + pair + "?interval=" + interval + "&limit=" + limit, OHLCV.class); + } + + public CompletableFuture> getTradeHistory(String pair, int limit) { + return getList("/analytics/trades/" + pair + "?limit=" + limit, TradeHistory.class); + } + + public CompletableFuture getVolumeStats() { + return get("/analytics/volume", VolumeStats.class); + } + + public CompletableFuture getTVL() { + return get("/analytics/tvl", TVLStats.class); + } + + // Lifecycle + + public CompletableFuture healthCheck() { + return get("/health", HealthResponse.class) + .thenApply(r -> "healthy".equals(r.status)) + .exceptionally(e -> false); + } + + @Override + public void close() { + closed = true; + client.dispatcher().executorService().shutdown(); + client.connectionPool().evictAll(); + } + + public boolean isClosed() { + return closed; + } + + // Internal methods + + CompletableFuture get(String path, Class responseClass) { + return request("GET", path, null, responseClass); + } + + CompletableFuture> getList(String path, Class elementClass) { + return request("GET", path, null, List.class) + .thenApply(list -> { + @SuppressWarnings("unchecked") + List result = (List) list; + return result; + }); + } + + CompletableFuture post(String path, Object body, Class responseClass) { + return request("POST", path, body, responseClass); + } + + CompletableFuture delete(String path, Class responseClass) { + return request("DELETE", path, null, responseClass); + } + + private CompletableFuture request(String method, String path, Object body, Class responseClass) { + if (closed) { + return CompletableFuture.failedFuture(new DexException("Client has been closed", "CLIENT_CLOSED", 0)); + } + + CompletableFuture future = new CompletableFuture<>(); + + Request.Builder requestBuilder = new Request.Builder() + .url(config.getEndpoint() + path) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + config.getApiKey()) + .header("X-SDK-Version", "java/0.1.0"); + + RequestBody requestBody = body != null + ? RequestBody.create(gson.toJson(body), JSON) + : null; + + switch (method) { + case "GET": + requestBuilder.get(); + break; + case "POST": + requestBuilder.post(requestBody); + break; + case "DELETE": + requestBuilder.delete(requestBody); + break; + } + + client.newCall(requestBuilder.build()).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + future.completeExceptionally(e); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String bodyStr = responseBody != null ? responseBody.string() : ""; + + if (!response.isSuccessful()) { + ErrorResponse error = gson.fromJson(bodyStr, ErrorResponse.class); + future.completeExceptionally(new DexException( + error != null && error.message != null ? error.message : "HTTP " + response.code(), + error != null ? error.code : null, + response.code() + )); + return; + } + + T result = gson.fromJson(bodyStr, responseClass); + future.complete(result); + } + } + }); + + return future; + } + + private void appendParam(StringBuilder params, String key, String value) { + if (params.length() > 0) { + params.append("&"); + } + params.append(key).append("=").append(value); + } + + private String urlEncode(String value) { + try { + return java.net.URLEncoder.encode(value, "UTF-8"); + } catch (Exception e) { + return value; + } + } + + DexConfig getConfig() { + return config; + } + + OkHttpClient getClient() { + return client; + } + + Gson getGson() { + return gson; + } + + private static class HealthResponse { + String status; + } + + private static class ErrorResponse { + String message; + String code; + } +} diff --git a/sdk/java/src/main/java/io/synor/dex/Types.java b/sdk/java/src/main/java/io/synor/dex/Types.java new file mode 100644 index 0000000..96558b7 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/dex/Types.java @@ -0,0 +1,476 @@ +package io.synor.dex; + +import java.math.BigInteger; +import java.util.List; + +/** + * DEX SDK Types + */ +public class Types { + + public enum PoolType { + CONSTANT_PRODUCT, + STABLE, + CONCENTRATED + } + + public enum PositionSide { + LONG, + SHORT + } + + public enum OrderType { + MARKET, + LIMIT, + STOP_MARKET, + STOP_LIMIT, + TAKE_PROFIT, + TAKE_PROFIT_LIMIT + } + + public enum MarginType { + CROSS, + ISOLATED + } + + public enum TimeInForce { + GTC, + IOC, + FOK, + GTD + } + + public enum OrderStatus { + PENDING, + OPEN, + PARTIALLY_FILLED, + FILLED, + CANCELLED, + EXPIRED + } +} + +class Token { + public String address; + public String symbol; + public String name; + public int decimals; + public BigInteger totalSupply; + public Double priceUsd; + public String logoUrl; + public boolean verified; +} + +class Pool { + public String id; + public Token tokenA; + public Token tokenB; + public Types.PoolType poolType; + public BigInteger reserveA; + public BigInteger reserveB; + public double fee; + public double tvlUsd; + public double volume24h; + public double apr; + public String lpTokenAddress; + public Integer tickSpacing; + public String sqrtPrice; +} + +class PoolFilter { + private List tokens; + private Double minTvl; + private Double minVolume24h; + private Boolean verified; + private Integer limit; + private Integer offset; + + public List getTokens() { return tokens; } + public void setTokens(List tokens) { this.tokens = tokens; } + public Double getMinTvl() { return minTvl; } + public void setMinTvl(Double minTvl) { this.minTvl = minTvl; } + public Double getMinVolume24h() { return minVolume24h; } + public void setMinVolume24h(Double minVolume24h) { this.minVolume24h = minVolume24h; } + public Boolean getVerified() { return verified; } + public void setVerified(Boolean verified) { this.verified = verified; } + public Integer getLimit() { return limit; } + public void setLimit(Integer limit) { this.limit = limit; } + public Integer getOffset() { return offset; } + public void setOffset(Integer offset) { this.offset = offset; } +} + +class Quote { + public String tokenIn; + public String tokenOut; + public BigInteger amountIn; + public BigInteger amountOut; + public double priceImpact; + public List route; + public BigInteger fee; + public BigInteger minimumReceived; + public long expiresAt; +} + +class QuoteParams { + private String tokenIn; + private String tokenOut; + private BigInteger amountIn; + private Double slippage; + + public String getTokenIn() { return tokenIn; } + public void setTokenIn(String tokenIn) { this.tokenIn = tokenIn; } + public String getTokenOut() { return tokenOut; } + public void setTokenOut(String tokenOut) { this.tokenOut = tokenOut; } + public BigInteger getAmountIn() { return amountIn; } + public void setAmountIn(BigInteger amountIn) { this.amountIn = amountIn; } + public Double getSlippage() { return slippage; } + public void setSlippage(Double slippage) { this.slippage = slippage; } +} + +class SwapParams { + private String tokenIn; + private String tokenOut; + private BigInteger amountIn; + private BigInteger minAmountOut; + private Long deadline; + private String recipient; + + public String getTokenIn() { return tokenIn; } + public void setTokenIn(String tokenIn) { this.tokenIn = tokenIn; } + public String getTokenOut() { return tokenOut; } + public void setTokenOut(String tokenOut) { this.tokenOut = tokenOut; } + public BigInteger getAmountIn() { return amountIn; } + public void setAmountIn(BigInteger amountIn) { this.amountIn = amountIn; } + public BigInteger getMinAmountOut() { return minAmountOut; } + public void setMinAmountOut(BigInteger minAmountOut) { this.minAmountOut = minAmountOut; } + public Long getDeadline() { return deadline; } + public void setDeadline(Long deadline) { this.deadline = deadline; } + public String getRecipient() { return recipient; } + public void setRecipient(String recipient) { this.recipient = recipient; } +} + +class SwapResult { + public String transactionHash; + public BigInteger amountIn; + public BigInteger amountOut; + public double effectivePrice; + public BigInteger feePaid; + public List route; +} + +class AddLiquidityParams { + private String tokenA; + private String tokenB; + private BigInteger amountA; + private BigInteger amountB; + private BigInteger minAmountA; + private BigInteger minAmountB; + private Long deadline; + + public String getTokenA() { return tokenA; } + public void setTokenA(String tokenA) { this.tokenA = tokenA; } + public String getTokenB() { return tokenB; } + public void setTokenB(String tokenB) { this.tokenB = tokenB; } + public BigInteger getAmountA() { return amountA; } + public void setAmountA(BigInteger amountA) { this.amountA = amountA; } + public BigInteger getAmountB() { return amountB; } + public void setAmountB(BigInteger amountB) { this.amountB = amountB; } + public BigInteger getMinAmountA() { return minAmountA; } + public void setMinAmountA(BigInteger minAmountA) { this.minAmountA = minAmountA; } + public BigInteger getMinAmountB() { return minAmountB; } + public void setMinAmountB(BigInteger minAmountB) { this.minAmountB = minAmountB; } + public Long getDeadline() { return deadline; } + public void setDeadline(Long deadline) { this.deadline = deadline; } +} + +class RemoveLiquidityParams { + private String pool; + private BigInteger lpAmount; + private BigInteger minAmountA; + private BigInteger minAmountB; + private Long deadline; + + public String getPool() { return pool; } + public void setPool(String pool) { this.pool = pool; } + public BigInteger getLpAmount() { return lpAmount; } + public void setLpAmount(BigInteger lpAmount) { this.lpAmount = lpAmount; } + public BigInteger getMinAmountA() { return minAmountA; } + public void setMinAmountA(BigInteger minAmountA) { this.minAmountA = minAmountA; } + public BigInteger getMinAmountB() { return minAmountB; } + public void setMinAmountB(BigInteger minAmountB) { this.minAmountB = minAmountB; } + public Long getDeadline() { return deadline; } + public void setDeadline(Long deadline) { this.deadline = deadline; } +} + +class LiquidityResult { + public String transactionHash; + public BigInteger amountA; + public BigInteger amountB; + public BigInteger lpTokens; + public double poolShare; +} + +class LPPosition { + public String poolId; + public BigInteger lpTokens; + public BigInteger tokenAAmount; + public BigInteger tokenBAmount; + public double valueUsd; + public BigInteger unclaimedFeesA; + public BigInteger unclaimedFeesB; + public double impermanentLoss; + public Integer tickLower; + public Integer tickUpper; + public Boolean inRange; +} + +class PerpMarket { + public String symbol; + public String baseAsset; + public String quoteAsset; + public double indexPrice; + public double markPrice; + public double fundingRate; + public long nextFundingTime; + public BigInteger openInterest; + public double volume24h; + public double priceChange24h; + public int maxLeverage; + public BigInteger minOrderSize; + public double tickSize; + public double maintenanceMargin; + public double initialMargin; +} + +class OpenPositionParams { + private String market; + private Types.PositionSide side; + private BigInteger size; + private int leverage; + private Types.OrderType orderType; + private Double limitPrice; + private Double stopLoss; + private Double takeProfit; + private Types.MarginType marginType = Types.MarginType.CROSS; + private boolean reduceOnly; + + public String getMarket() { return market; } + public void setMarket(String market) { this.market = market; } + public Types.PositionSide getSide() { return side; } + public void setSide(Types.PositionSide side) { this.side = side; } + public BigInteger getSize() { return size; } + public void setSize(BigInteger size) { this.size = size; } + public int getLeverage() { return leverage; } + public void setLeverage(int leverage) { this.leverage = leverage; } + public Types.OrderType getOrderType() { return orderType; } + public void setOrderType(Types.OrderType orderType) { this.orderType = orderType; } + public Double getLimitPrice() { return limitPrice; } + public void setLimitPrice(Double limitPrice) { this.limitPrice = limitPrice; } + public Double getStopLoss() { return stopLoss; } + public void setStopLoss(Double stopLoss) { this.stopLoss = stopLoss; } + public Double getTakeProfit() { return takeProfit; } + public void setTakeProfit(Double takeProfit) { this.takeProfit = takeProfit; } + public Types.MarginType getMarginType() { return marginType; } + public void setMarginType(Types.MarginType marginType) { this.marginType = marginType; } + public boolean isReduceOnly() { return reduceOnly; } + public void setReduceOnly(boolean reduceOnly) { this.reduceOnly = reduceOnly; } +} + +class ClosePositionParams { + private String market; + private BigInteger size; + private Types.OrderType orderType = Types.OrderType.MARKET; + private Double limitPrice; + + public String getMarket() { return market; } + public void setMarket(String market) { this.market = market; } + public BigInteger getSize() { return size; } + public void setSize(BigInteger size) { this.size = size; } + public Types.OrderType getOrderType() { return orderType; } + public void setOrderType(Types.OrderType orderType) { this.orderType = orderType; } + public Double getLimitPrice() { return limitPrice; } + public void setLimitPrice(Double limitPrice) { this.limitPrice = limitPrice; } +} + +class ModifyPositionParams { + private String positionId; + private Integer newLeverage; + private BigInteger newMargin; + private Double newStopLoss; + private Double newTakeProfit; + + public String getPositionId() { return positionId; } + public void setPositionId(String positionId) { this.positionId = positionId; } + public Integer getNewLeverage() { return newLeverage; } + public void setNewLeverage(Integer newLeverage) { this.newLeverage = newLeverage; } + public BigInteger getNewMargin() { return newMargin; } + public void setNewMargin(BigInteger newMargin) { this.newMargin = newMargin; } + public Double getNewStopLoss() { return newStopLoss; } + public void setNewStopLoss(Double newStopLoss) { this.newStopLoss = newStopLoss; } + public Double getNewTakeProfit() { return newTakeProfit; } + public void setNewTakeProfit(Double newTakeProfit) { this.newTakeProfit = newTakeProfit; } +} + +class PerpPosition { + public String id; + public String market; + public Types.PositionSide side; + public BigInteger size; + public double entryPrice; + public double markPrice; + public double liquidationPrice; + public BigInteger margin; + public int leverage; + public BigInteger unrealizedPnl; + public BigInteger realizedPnl; + public double marginRatio; + public Double stopLoss; + public Double takeProfit; + public long createdAt; + public long updatedAt; +} + +class PerpOrder { + public String id; + public String market; + public Types.PositionSide side; + public Types.OrderType orderType; + public BigInteger size; + public Double price; + public BigInteger filledSize; + public Types.OrderStatus status; + public boolean reduceOnly; + public long createdAt; +} + +class FundingPayment { + public String market; + public BigInteger amount; + public double rate; + public BigInteger positionSize; + public long timestamp; +} + +class OrderBookEntry { + public double price; + public BigInteger size; + public int orders; +} + +class OrderBook { + public String market; + public List bids; + public List asks; + public long timestamp; +} + +class LimitOrderParams { + private String market; + private String side; + private double price; + private BigInteger size; + private Types.TimeInForce timeInForce = Types.TimeInForce.GTC; + private boolean postOnly; + + public String getMarket() { return market; } + public void setMarket(String market) { this.market = market; } + public String getSide() { return side; } + public void setSide(String side) { this.side = side; } + public double getPrice() { return price; } + public void setPrice(double price) { this.price = price; } + public BigInteger getSize() { return size; } + public void setSize(BigInteger size) { this.size = size; } + public Types.TimeInForce getTimeInForce() { return timeInForce; } + public void setTimeInForce(Types.TimeInForce timeInForce) { this.timeInForce = timeInForce; } + public boolean isPostOnly() { return postOnly; } + public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } +} + +class Order { + public String id; + public String market; + public String side; + public double price; + public BigInteger size; + public BigInteger filledSize; + public Types.OrderStatus status; + public Types.TimeInForce timeInForce; + public boolean postOnly; + public long createdAt; + public long updatedAt; +} + +class Farm { + public String id; + public String name; + public Token stakeToken; + public List rewardTokens; + public double tvlUsd; + public double apr; + public List dailyRewards; + public Long lockupPeriod; + public BigInteger minStake; +} + +class StakeParams { + private String farm; + private BigInteger amount; + + public String getFarm() { return farm; } + public void setFarm(String farm) { this.farm = farm; } + public BigInteger getAmount() { return amount; } + public void setAmount(BigInteger amount) { this.amount = amount; } +} + +class FarmPosition { + public String farmId; + public BigInteger stakedAmount; + public List pendingRewards; + public long stakedAt; + public Long unlockAt; +} + +class OHLCV { + public long timestamp; + public double open; + public double high; + public double low; + public double close; + public double volume; +} + +class TradeHistory { + public String id; + public String market; + public String side; + public double price; + public BigInteger size; + public long timestamp; + public String maker; + public String taker; +} + +class VolumeStats { + public double volume24h; + public double volume7d; + public double volume30d; + public int trades24h; + public int uniqueTraders24h; +} + +class TVLStats { + public double totalTvl; + public double poolsTvl; + public double farmsTvl; + public double perpsTvl; +} + +class ClaimRewardsResult { + public BigInteger amount; + public String transactionHash; +} + +class FundingRateInfo { + public double rate; + public long nextTime; +} diff --git a/sdk/js/src/dex/client.ts b/sdk/js/src/dex/client.ts new file mode 100644 index 0000000..1a394da --- /dev/null +++ b/sdk/js/src/dex/client.ts @@ -0,0 +1,473 @@ +/** + * 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 + */ + +import { + DexConfig, + Token, + Pool, + PoolFilter, + Quote, + QuoteParams, + SwapParams, + SwapResult, + AddLiquidityParams, + RemoveLiquidityParams, + LiquidityResult, + LPPosition, + PerpMarket, + OpenPositionParams, + ClosePositionParams, + PerpPosition, + PerpOrder, + ModifyPositionParams, + FundingPayment, + OrderBook, + LimitOrderParams, + Order, + Farm, + StakeParams, + FarmPosition, + OHLCV, + TradeHistory, + VolumeStats, + TVLStats, + Subscription, + PriceCallback, + TradeCallback, + OrderBookCallback, + PositionCallback, + DexError, +} from './types'; + +const DEFAULT_ENDPOINT = 'https://dex.synor.io/v1'; +const DEFAULT_WS_ENDPOINT = 'wss://dex.synor.io/v1/ws'; +const DEFAULT_TIMEOUT = 30000; +const DEFAULT_RETRIES = 3; + +export class SynorDex { + private config: Required; + private closed = false; + private ws: WebSocket | null = null; + private subscriptions: Map void> = new Map(); + + public readonly perps: PerpsClient; + public readonly orderbook: OrderBookClient; + public readonly farms: FarmsClient; + + constructor(config: DexConfig) { + this.config = { + apiKey: config.apiKey, + endpoint: config.endpoint || DEFAULT_ENDPOINT, + wsEndpoint: config.wsEndpoint || DEFAULT_WS_ENDPOINT, + timeout: config.timeout || DEFAULT_TIMEOUT, + retries: config.retries || DEFAULT_RETRIES, + debug: config.debug || false, + }; + + this.perps = new PerpsClient(this); + this.orderbook = new OrderBookClient(this); + this.farms = new FarmsClient(this); + } + + // Token Operations + async getToken(address: string): Promise { + return this.get('/tokens/' + address); + } + + async listTokens(): Promise { + return this.get('/tokens'); + } + + async searchTokens(query: string): Promise { + return this.get('/tokens/search?q=' + encodeURIComponent(query)); + } + + // Pool Operations + async getPool(tokenA: string, tokenB: string): Promise { + return this.get('/pools/' + tokenA + '/' + tokenB); + } + + async getPoolById(poolId: string): Promise { + return this.get('/pools/' + poolId); + } + + async listPools(filter?: PoolFilter): Promise { + const params = new URLSearchParams(); + if (filter?.tokens) params.set('tokens', filter.tokens.join(',')); + if (filter?.minTvl) params.set('min_tvl', filter.minTvl.toString()); + if (filter?.minVolume24h) params.set('min_volume', filter.minVolume24h.toString()); + if (filter?.verified !== undefined) params.set('verified', String(filter.verified)); + if (filter?.limit) params.set('limit', String(filter.limit)); + if (filter?.offset) params.set('offset', String(filter.offset)); + return this.get('/pools?' + params.toString()); + } + + // Swap Operations + async getQuote(params: QuoteParams): Promise { + return this.post('/swap/quote', { + token_in: params.tokenIn, + token_out: params.tokenOut, + amount_in: params.amountIn.toString(), + slippage: params.slippage || 0.005, + }); + } + + async swap(params: SwapParams): Promise { + return this.post('/swap', { + token_in: params.tokenIn, + token_out: params.tokenOut, + amount_in: params.amountIn.toString(), + min_amount_out: params.minAmountOut.toString(), + deadline: params.deadline || Math.floor(Date.now() / 1000) + 1200, + recipient: params.recipient, + }); + } + + // Liquidity Operations + async addLiquidity(params: AddLiquidityParams): Promise { + return this.post('/liquidity/add', { + token_a: params.tokenA, + token_b: params.tokenB, + amount_a: params.amountA.toString(), + amount_b: params.amountB.toString(), + min_amount_a: params.minAmountA?.toString(), + min_amount_b: params.minAmountB?.toString(), + deadline: params.deadline || Math.floor(Date.now() / 1000) + 1200, + }); + } + + async removeLiquidity(params: RemoveLiquidityParams): Promise { + return this.post('/liquidity/remove', { + pool: params.pool, + lp_amount: params.lpAmount.toString(), + min_amount_a: params.minAmountA?.toString(), + min_amount_b: params.minAmountB?.toString(), + deadline: params.deadline || Math.floor(Date.now() / 1000) + 1200, + }); + } + + async getMyPositions(): Promise { + return this.get('/liquidity/positions'); + } + + // Analytics + async getPriceHistory(pair: string, interval: string, limit = 100): Promise { + return this.get('/analytics/candles/' + pair + '?interval=' + interval + '&limit=' + limit); + } + + async getTradeHistory(pair: string, limit = 50): Promise { + return this.get('/analytics/trades/' + pair + '?limit=' + limit); + } + + async getVolumeStats(): Promise { + return this.get('/analytics/volume'); + } + + async getTVL(): Promise { + return this.get('/analytics/tvl'); + } + + // Subscriptions + async subscribePrice(market: string, callback: PriceCallback): Promise { + return this.subscribe('price', { market }, callback); + } + + async subscribeTrades(market: string, callback: TradeCallback): Promise { + return this.subscribe('trades', { market }, callback); + } + + async subscribeOrderBook(market: string, callback: OrderBookCallback): Promise { + return this.subscribe('orderbook', { market }, callback); + } + + // Lifecycle + async healthCheck(): Promise { + try { + const response = await this.get<{ status: string }>('/health'); + return response.status === 'healthy'; + } catch { + return false; + } + } + + close(): void { + this.closed = true; + if (this.ws) { + this.ws.close(); + this.ws = null; + } + this.subscriptions.clear(); + } + + // Internal Methods + async get(path: string): Promise { + return this.request('GET', path); + } + + async post(path: string, body: unknown): Promise { + return this.request('POST', path, body); + } + + async delete(path: string): Promise { + return this.request('DELETE', path); + } + + private async request(method: string, path: string, body?: unknown): Promise { + if (this.closed) { + throw new DexError('Client has been closed', 'CLIENT_CLOSED'); + } + + let lastError: Error | null = null; + + for (let attempt = 0; attempt < this.config.retries; attempt++) { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); + + const response = await fetch(this.config.endpoint + path, { + method, + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + this.config.apiKey, + 'X-SDK-Version': 'js/0.1.0', + }, + body: body ? JSON.stringify(body) : undefined, + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new DexError( + error.message || 'HTTP ' + response.status, + error.code, + response.status + ); + } + + return await response.json(); + } catch (error) { + lastError = error as Error; + if (this.config.debug) { + console.error('Attempt ' + (attempt + 1) + ' failed:', error); + } + if (attempt < this.config.retries - 1) { + await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000)); + } + } + } + + throw lastError || new DexError('Unknown error'); + } + + private async subscribe( + channel: string, + params: Record, + callback: (data: T) => void + ): Promise { + await this.ensureWebSocket(); + + const subscriptionId = Math.random().toString(36).substring(2); + this.subscriptions.set(subscriptionId, callback as (data: unknown) => void); + + this.ws!.send(JSON.stringify({ + type: 'subscribe', + channel, + subscription_id: subscriptionId, + ...params, + })); + + return { + id: subscriptionId, + channel, + cancel: () => { + this.subscriptions.delete(subscriptionId); + this.ws?.send(JSON.stringify({ + type: 'unsubscribe', + subscription_id: subscriptionId, + })); + }, + }; + } + + private async ensureWebSocket(): Promise { + if (this.ws?.readyState === WebSocket.OPEN) return; + + return new Promise((resolve, reject) => { + this.ws = new WebSocket(this.config.wsEndpoint); + this.ws.onopen = () => { + this.ws!.send(JSON.stringify({ + type: 'auth', + api_key: this.config.apiKey, + })); + resolve(); + }; + this.ws.onerror = reject; + this.ws.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data.subscription_id && this.subscriptions.has(data.subscription_id)) { + this.subscriptions.get(data.subscription_id)!(data.data); + } + }; + }); + } +} + +// Perpetuals Sub-Client +class PerpsClient { + constructor(private dex: SynorDex) {} + + async listMarkets(): Promise { + return this.dex.get('/perps/markets'); + } + + async getMarket(symbol: string): Promise { + return this.dex.get('/perps/markets/' + symbol); + } + + async openPosition(params: OpenPositionParams): Promise { + return this.dex.post('/perps/positions', { + market: params.market, + side: params.side, + size: params.size.toString(), + leverage: params.leverage, + order_type: params.orderType, + limit_price: params.limitPrice, + stop_loss: params.stopLoss, + take_profit: params.takeProfit, + margin_type: params.marginType || 'cross', + reduce_only: params.reduceOnly || false, + }); + } + + async closePosition(params: ClosePositionParams): Promise { + return this.dex.post('/perps/positions/close', { + market: params.market, + size: params.size?.toString(), + order_type: params.orderType, + limit_price: params.limitPrice, + }); + } + + async modifyPosition(params: ModifyPositionParams): Promise { + return this.dex.post('/perps/positions/' + params.positionId + '/modify', { + new_leverage: params.newLeverage, + new_margin: params.newMargin?.toString(), + new_stop_loss: params.newStopLoss, + new_take_profit: params.newTakeProfit, + }); + } + + async getPositions(): Promise { + return this.dex.get('/perps/positions'); + } + + async getPosition(market: string): Promise { + return this.dex.get('/perps/positions/' + market); + } + + async getOrders(): Promise { + return this.dex.get('/perps/orders'); + } + + async cancelOrder(orderId: string): Promise { + await this.dex.delete('/perps/orders/' + orderId); + } + + async cancelAllOrders(market?: string): Promise { + const path = market ? '/perps/orders?market=' + market : '/perps/orders'; + const result = await this.dex.delete<{ cancelled: number }>(path); + return result.cancelled; + } + + async getFundingHistory(market: string, limit = 100): Promise { + return this.dex.get('/perps/funding/' + market + '?limit=' + limit); + } + + async getFundingRate(market: string): Promise<{ rate: number; nextTime: number }> { + return this.dex.get('/perps/funding/' + market + '/current'); + } + + async subscribePosition(callback: PositionCallback): Promise { + return (this.dex as any).subscribe('position', {}, callback); + } +} + +// Order Book Sub-Client +class OrderBookClient { + constructor(private dex: SynorDex) {} + + async getOrderBook(market: string, depth = 20): Promise { + return this.dex.get('/orderbook/' + market + '?depth=' + depth); + } + + async placeLimitOrder(params: LimitOrderParams): Promise { + return this.dex.post('/orderbook/orders', { + market: params.market, + side: params.side, + price: params.price, + size: params.size.toString(), + time_in_force: params.timeInForce || 'GTC', + post_only: params.postOnly || false, + }); + } + + async cancelOrder(orderId: string): Promise { + await this.dex.delete('/orderbook/orders/' + orderId); + } + + async getOpenOrders(market?: string): Promise { + const path = market ? '/orderbook/orders?market=' + market : '/orderbook/orders'; + return this.dex.get(path); + } + + async getOrderHistory(limit = 50): Promise { + return this.dex.get('/orderbook/orders/history?limit=' + limit); + } +} + +// Farms Sub-Client +class FarmsClient { + constructor(private dex: SynorDex) {} + + async listFarms(): Promise { + return this.dex.get('/farms'); + } + + async getFarm(farmId: string): Promise { + return this.dex.get('/farms/' + farmId); + } + + async stake(params: StakeParams): Promise { + return this.dex.post('/farms/stake', { + farm: params.farm, + amount: params.amount.toString(), + }); + } + + async unstake(farm: string, amount: bigint): Promise { + return this.dex.post('/farms/unstake', { + farm, + amount: amount.toString(), + }); + } + + async claimRewards(farm: string): Promise<{ amount: bigint; transactionHash: string }> { + return this.dex.post('/farms/claim', { farm }); + } + + async getMyFarmPositions(): Promise { + return this.dex.get('/farms/positions'); + } +} + +export default SynorDex; diff --git a/sdk/js/src/dex/index.ts b/sdk/js/src/dex/index.ts new file mode 100644 index 0000000..2af5e85 --- /dev/null +++ b/sdk/js/src/dex/index.ts @@ -0,0 +1,8 @@ +/** + * Synor DEX SDK + * + * Decentralized exchange with AMM, perpetual futures, and order books. + */ + +export { SynorDex, default } from './client'; +export * from './types'; diff --git a/sdk/js/src/dex/types.ts b/sdk/js/src/dex/types.ts new file mode 100644 index 0000000..27e3fbb --- /dev/null +++ b/sdk/js/src/dex/types.ts @@ -0,0 +1,384 @@ +/** + * Synor DEX SDK Types + * + * Comprehensive types for decentralized exchange operations including + * AMM swaps, liquidity pools, perpetual futures, and order books. + */ + +// ==================== Configuration ==================== + +export interface DexConfig { + apiKey: string; + endpoint?: string; + wsEndpoint?: string; + timeout?: number; + retries?: number; + debug?: boolean; +} + +// ==================== Token Types ==================== + +export interface Token { + address: string; + symbol: string; + name: string; + decimals: number; + logoUri?: string; + verified?: boolean; +} + +export interface TokenAmount { + token: Token; + amount: bigint; + formatted: string; +} + +// ==================== Pool Types ==================== + +export interface Pool { + id: string; + tokenA: Token; + tokenB: Token; + reserveA: bigint; + reserveB: bigint; + fee: number; // e.g., 0.003 = 0.3% + tvl: bigint; + volume24h: bigint; + volume7d: bigint; + apr: number; + lpTokenAddress: string; + lpTokenSupply: bigint; + createdAt: number; +} + +export interface PoolFilter { + tokens?: string[]; + minTvl?: bigint; + minVolume24h?: bigint; + verified?: boolean; + limit?: number; + offset?: number; +} + +export type PoolType = 'constant_product' | 'stable' | 'concentrated'; + +// ==================== Swap Types ==================== + +export interface QuoteParams { + tokenIn: string; + tokenOut: string; + amountIn: bigint; + slippage?: number; // Default: 0.005 (0.5%) +} + +export interface Quote { + tokenIn: Token; + tokenOut: Token; + amountIn: bigint; + amountOut: bigint; + minAmountOut: bigint; + priceImpact: number; + fee: bigint; + route: string[]; + executionPrice: number; + pools: string[]; +} + +export interface SwapParams { + tokenIn: string; + tokenOut: string; + amountIn: bigint; + minAmountOut: bigint; + deadline?: number; + recipient?: string; +} + +export interface SwapResult { + transactionHash: string; + amountIn: bigint; + amountOut: bigint; + executionPrice: number; + priceImpact: number; + fee: bigint; + gasUsed: bigint; + route: string[]; +} + +// ==================== Liquidity Types ==================== + +export interface AddLiquidityParams { + tokenA: string; + tokenB: string; + amountA: bigint; + amountB: bigint; + minAmountA?: bigint; + minAmountB?: bigint; + deadline?: number; +} + +export interface RemoveLiquidityParams { + pool: string; + lpAmount: bigint; + minAmountA?: bigint; + minAmountB?: bigint; + deadline?: number; +} + +export interface LiquidityResult { + transactionHash: string; + pool: string; + amountA: bigint; + amountB: bigint; + lpTokens: bigint; + share: number; +} + +export interface LPPosition { + pool: Pool; + lpTokens: bigint; + share: number; + valueUsd: number; + tokenAAmount: bigint; + tokenBAmount: bigint; + fees24h: bigint; + feesTotal: bigint; + impermanentLoss: number; +} + +// ==================== Perpetual Futures Types ==================== + +export type PositionSide = 'long' | 'short'; +export type OrderType = 'market' | 'limit' | 'stop_loss' | 'take_profit'; +export type MarginType = 'cross' | 'isolated'; + +export interface PerpMarket { + symbol: string; // e.g., "BTC-PERP" + baseToken: Token; + quoteToken: Token; // Usually USDC + indexPrice: number; + markPrice: number; + fundingRate: number; // Current funding rate (8h) + nextFundingTime: number; + openInterest: bigint; + volume24h: bigint; + maxLeverage: number; // e.g., 100 for 100x + maintenanceMargin: number; // e.g., 0.005 = 0.5% + initialMargin: number; // e.g., 0.01 = 1% + tickSize: number; + minOrderSize: bigint; + maxOrderSize: bigint; +} + +export interface OpenPositionParams { + market: string; + side: PositionSide; + size: bigint; // Position size in base token + leverage: number; // 1-100x + orderType: OrderType; + limitPrice?: number; // For limit orders + stopLoss?: number; + takeProfit?: number; + marginType?: MarginType; + reduceOnly?: boolean; +} + +export interface ClosePositionParams { + market: string; + size?: bigint; // Partial close if specified + orderType: OrderType; + limitPrice?: number; +} + +export interface PerpPosition { + id: string; + market: string; + side: PositionSide; + size: bigint; + entryPrice: number; + markPrice: number; + liquidationPrice: number; + margin: bigint; + leverage: number; + marginType: MarginType; + unrealizedPnl: bigint; + realizedPnl: bigint; + fundingPayment: bigint; + createdAt: number; + updatedAt: number; +} + +export interface PerpOrder { + id: string; + market: string; + side: PositionSide; + size: bigint; + price: number; + orderType: OrderType; + filled: bigint; + remaining: bigint; + status: 'open' | 'filled' | 'cancelled' | 'expired'; + reduceOnly: boolean; + createdAt: number; +} + +export interface ModifyPositionParams { + positionId: string; + newLeverage?: number; + newMargin?: bigint; // Add or remove margin + newStopLoss?: number; + newTakeProfit?: number; +} + +export interface FundingPayment { + market: string; + amount: bigint; + rate: number; + timestamp: number; +} + +// ==================== Order Book Types ==================== + +export interface OrderBookLevel { + price: number; + size: bigint; + total: bigint; +} + +export interface OrderBook { + market: string; + bids: OrderBookLevel[]; + asks: OrderBookLevel[]; + spread: number; + timestamp: number; +} + +export interface LimitOrderParams { + market: string; + side: 'buy' | 'sell'; + price: number; + size: bigint; + timeInForce?: 'GTC' | 'IOC' | 'FOK'; + postOnly?: boolean; +} + +export interface Order { + id: string; + market: string; + side: 'buy' | 'sell'; + price: number; + size: bigint; + filled: bigint; + status: 'open' | 'filled' | 'partial' | 'cancelled'; + createdAt: number; +} + +// ==================== Farming & Staking ==================== + +export interface Farm { + id: string; + pool: Pool; + rewardToken: Token; + rewardPerSecond: bigint; + totalStaked: bigint; + apr: number; + startTime: number; + endTime?: number; +} + +export interface StakeParams { + farm: string; + amount: bigint; +} + +export interface FarmPosition { + farm: Farm; + staked: bigint; + pendingRewards: bigint; + share: number; +} + +// ==================== Analytics ==================== + +export interface OHLCV { + timestamp: number; + open: number; + high: number; + low: number; + close: number; + volume: bigint; +} + +export interface TradeHistory { + id: string; + market: string; + side: 'buy' | 'sell'; + price: number; + size: bigint; + timestamp: number; + maker: string; + taker: string; +} + +export interface VolumeStats { + volume24h: bigint; + volume7d: bigint; + volume30d: bigint; + trades24h: number; + uniqueTraders24h: number; +} + +export interface TVLStats { + total: bigint; + byPool: Record; + history: { timestamp: number; tvl: bigint }[]; +} + +// ==================== Events & Subscriptions ==================== + +export interface Subscription { + id: string; + channel: string; + cancel: () => void; +} + +export type PriceCallback = (price: { market: string; price: number; timestamp: number }) => void; +export type TradeCallback = (trade: TradeHistory) => void; +export type OrderBookCallback = (orderBook: OrderBook) => void; +export type PositionCallback = (position: PerpPosition) => void; + +// ==================== Errors ==================== + +export class DexError extends Error { + code?: string; + statusCode?: number; + constructor(message: string, code?: string, statusCode?: number) { + super(message); + this.name = 'DexError'; + this.code = code; + this.statusCode = statusCode; + } +} + +export class InsufficientLiquidityError extends DexError { + constructor(message = 'Insufficient liquidity for this trade') { + super(message, 'INSUFFICIENT_LIQUIDITY'); + } +} + +export class SlippageExceededError extends DexError { + constructor(message = 'Slippage tolerance exceeded') { + super(message, 'SLIPPAGE_EXCEEDED'); + } +} + +export class InsufficientMarginError extends DexError { + constructor(message = 'Insufficient margin for position') { + super(message, 'INSUFFICIENT_MARGIN'); + } +} + +export class LiquidationError extends DexError { + constructor(message = 'Position would be liquidated') { + super(message, 'LIQUIDATION_RISK'); + } +} diff --git a/sdk/kotlin/src/main/kotlin/io/synor/dex/SynorDex.kt b/sdk/kotlin/src/main/kotlin/io/synor/dex/SynorDex.kt new file mode 100644 index 0000000..e3c04dd --- /dev/null +++ b/sdk/kotlin/src/main/kotlin/io/synor/dex/SynorDex.kt @@ -0,0 +1,342 @@ +package io.synor.dex + +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.coroutines.* +import kotlinx.serialization.json.Json +import java.math.BigInteger +import java.time.Instant +import java.util.concurrent.atomic.AtomicBoolean + +/** + * 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 + */ +class SynorDex(private val config: DexConfig) : AutoCloseable { + private val client = HttpClient(CIO) { + install(ContentNegotiation) { + json(Json { + ignoreUnknownKeys = true + isLenient = true + }) + } + install(HttpTimeout) { + requestTimeoutMillis = config.timeout + } + defaultRequest { + header("Content-Type", "application/json") + header("Authorization", "Bearer ${config.apiKey}") + header("X-SDK-Version", "kotlin/0.1.0") + } + } + + private val closed = AtomicBoolean(false) + + val perps = PerpsClient(this) + val orderbook = OrderBookClient(this) + val farms = FarmsClient(this) + + // Token Operations + + suspend fun getToken(address: String): Token = + get("/tokens/$address") + + suspend fun listTokens(): List = + get("/tokens") + + suspend fun searchTokens(query: String): List = + get("/tokens/search?q=${query.encodeURLParameter()}") + + // Pool Operations + + suspend fun getPool(tokenA: String, tokenB: String): Pool = + get("/pools/$tokenA/$tokenB") + + suspend fun getPoolById(poolId: String): Pool = + get("/pools/$poolId") + + suspend fun listPools(filter: PoolFilter? = null): List { + val params = mutableListOf() + filter?.let { f -> + f.tokens?.let { params.add("tokens=${it.joinToString(",")}") } + f.minTvl?.let { params.add("min_tvl=$it") } + f.minVolume24h?.let { params.add("min_volume=$it") } + f.verified?.let { params.add("verified=$it") } + f.limit?.let { params.add("limit=$it") } + f.offset?.let { params.add("offset=$it") } + } + val path = if (params.isEmpty()) "/pools" else "/pools?${params.joinToString("&")}" + return get(path) + } + + // Swap Operations + + suspend fun getQuote(params: QuoteParams): Quote = + post("/swap/quote", mapOf( + "token_in" to params.tokenIn, + "token_out" to params.tokenOut, + "amount_in" to params.amountIn.toString(), + "slippage" to (params.slippage ?: 0.005) + )) + + suspend fun swap(params: SwapParams): SwapResult { + val deadline = params.deadline ?: (Instant.now().epochSecond + 1200) + return post("/swap", buildMap { + put("token_in", params.tokenIn) + put("token_out", params.tokenOut) + put("amount_in", params.amountIn.toString()) + put("min_amount_out", params.minAmountOut.toString()) + put("deadline", deadline) + params.recipient?.let { put("recipient", it) } + }) + } + + // Liquidity Operations + + suspend fun addLiquidity(params: AddLiquidityParams): LiquidityResult { + val deadline = params.deadline ?: (Instant.now().epochSecond + 1200) + return post("/liquidity/add", buildMap { + put("token_a", params.tokenA) + put("token_b", params.tokenB) + put("amount_a", params.amountA.toString()) + put("amount_b", params.amountB.toString()) + put("deadline", deadline) + params.minAmountA?.let { put("min_amount_a", it.toString()) } + params.minAmountB?.let { put("min_amount_b", it.toString()) } + }) + } + + suspend fun removeLiquidity(params: RemoveLiquidityParams): LiquidityResult { + val deadline = params.deadline ?: (Instant.now().epochSecond + 1200) + return post("/liquidity/remove", buildMap { + put("pool", params.pool) + put("lp_amount", params.lpAmount.toString()) + put("deadline", deadline) + params.minAmountA?.let { put("min_amount_a", it.toString()) } + params.minAmountB?.let { put("min_amount_b", it.toString()) } + }) + } + + suspend fun getMyPositions(): List = + get("/liquidity/positions") + + // Analytics + + suspend fun getPriceHistory(pair: String, interval: String, limit: Int = 100): List = + get("/analytics/candles/$pair?interval=$interval&limit=$limit") + + suspend fun getTradeHistory(pair: String, limit: Int = 50): List = + get("/analytics/trades/$pair?limit=$limit") + + suspend fun getVolumeStats(): VolumeStats = + get("/analytics/volume") + + suspend fun getTVL(): TVLStats = + get("/analytics/tvl") + + // Lifecycle + + suspend fun healthCheck(): Boolean = try { + val response: HealthResponse = get("/health") + response.status == "healthy" + } catch (e: Exception) { + false + } + + override fun close() { + closed.set(true) + client.close() + } + + fun isClosed(): Boolean = closed.get() + + // Internal methods + + internal suspend inline fun get(path: String): T { + checkClosed() + return client.get(config.endpoint + path).body() + } + + internal suspend inline fun post(path: String, body: Map): T { + checkClosed() + return client.post(config.endpoint + path) { + setBody(body) + }.body() + } + + internal suspend inline fun delete(path: String): T { + checkClosed() + return client.delete(config.endpoint + path).body() + } + + private fun checkClosed() { + if (closed.get()) { + throw DexException("Client has been closed", "CLIENT_CLOSED", 0) + } + } + + private data class HealthResponse(val status: String) +} + +/** + * DEX client configuration + */ +data class DexConfig( + val apiKey: String, + val endpoint: String = "https://dex.synor.io/v1", + val wsEndpoint: String = "wss://dex.synor.io/v1/ws", + val timeout: Long = 30000, + val retries: Int = 3, + val debug: Boolean = false +) + +/** + * DEX SDK Exception + */ +class DexException( + message: String, + val code: String?, + val status: Int +) : RuntimeException(message) + +/** + * Perpetual futures sub-client + */ +class PerpsClient(private val dex: SynorDex) { + suspend fun listMarkets(): List = + dex.get("/perps/markets") + + suspend fun getMarket(symbol: String): PerpMarket = + dex.get("/perps/markets/$symbol") + + suspend fun openPosition(params: OpenPositionParams): PerpPosition = + dex.post("/perps/positions", buildMap { + put("market", params.market) + put("side", params.side.name.lowercase()) + put("size", params.size.toString()) + put("leverage", params.leverage) + put("order_type", params.orderType.name.lowercase()) + put("margin_type", params.marginType.name.lowercase()) + put("reduce_only", params.reduceOnly) + params.limitPrice?.let { put("limit_price", it) } + params.stopLoss?.let { put("stop_loss", it) } + params.takeProfit?.let { put("take_profit", it) } + }) + + suspend fun closePosition(params: ClosePositionParams): PerpPosition = + dex.post("/perps/positions/close", buildMap { + put("market", params.market) + put("order_type", params.orderType.name.lowercase()) + params.size?.let { put("size", it.toString()) } + params.limitPrice?.let { put("limit_price", it) } + }) + + suspend fun modifyPosition(params: ModifyPositionParams): PerpPosition = + dex.post("/perps/positions/${params.positionId}/modify", buildMap { + params.newLeverage?.let { put("new_leverage", it) } + params.newMargin?.let { put("new_margin", it.toString()) } + params.newStopLoss?.let { put("new_stop_loss", it) } + params.newTakeProfit?.let { put("new_take_profit", it) } + }) + + suspend fun getPositions(): List = + dex.get("/perps/positions") + + suspend fun getPosition(market: String): PerpPosition? = + dex.get("/perps/positions/$market") + + suspend fun getOrders(): List = + dex.get("/perps/orders") + + suspend fun cancelOrder(orderId: String) { + dex.delete("/perps/orders/$orderId") + } + + suspend fun cancelAllOrders(market: String? = null): Int { + val path = if (market != null) "/perps/orders?market=$market" else "/perps/orders" + val result: CancelResult = dex.delete(path) + return result.cancelled + } + + suspend fun getFundingHistory(market: String, limit: Int = 100): List = + dex.get("/perps/funding/$market?limit=$limit") + + suspend fun getFundingRate(market: String): FundingRateInfo = + dex.get("/perps/funding/$market/current") + + private data class CancelResult(val cancelled: Int) +} + +/** + * Order book sub-client + */ +class OrderBookClient(private val dex: SynorDex) { + suspend fun getOrderBook(market: String, depth: Int = 20): OrderBook = + dex.get("/orderbook/$market?depth=$depth") + + suspend fun placeLimitOrder(params: LimitOrderParams): Order = + dex.post("/orderbook/orders", mapOf( + "market" to params.market, + "side" to params.side, + "price" to params.price, + "size" to params.size.toString(), + "time_in_force" to params.timeInForce.name, + "post_only" to params.postOnly + )) + + suspend fun cancelOrder(orderId: String) { + dex.delete("/orderbook/orders/$orderId") + } + + suspend fun getOpenOrders(market: String? = null): List { + val path = if (market != null) "/orderbook/orders?market=$market" else "/orderbook/orders" + return dex.get(path) + } + + suspend fun getOrderHistory(limit: Int = 50): List = + dex.get("/orderbook/orders/history?limit=$limit") +} + +/** + * Farms sub-client + */ +class FarmsClient(private val dex: SynorDex) { + suspend fun listFarms(): List = + dex.get("/farms") + + suspend fun getFarm(farmId: String): Farm = + dex.get("/farms/$farmId") + + suspend fun stake(params: StakeParams): FarmPosition = + dex.post("/farms/stake", mapOf( + "farm" to params.farm, + "amount" to params.amount.toString() + )) + + suspend fun unstake(farm: String, amount: BigInteger): FarmPosition = + dex.post("/farms/unstake", mapOf( + "farm" to farm, + "amount" to amount.toString() + )) + + suspend fun claimRewards(farm: String): ClaimRewardsResult = + dex.post("/farms/claim", mapOf("farm" to farm)) + + suspend fun getMyFarmPositions(): List = + dex.get("/farms/positions") +} + +private fun String.encodeURLParameter(): String = + java.net.URLEncoder.encode(this, "UTF-8") diff --git a/sdk/kotlin/src/main/kotlin/io/synor/dex/Types.kt b/sdk/kotlin/src/main/kotlin/io/synor/dex/Types.kt new file mode 100644 index 0000000..cba3485 --- /dev/null +++ b/sdk/kotlin/src/main/kotlin/io/synor/dex/Types.kt @@ -0,0 +1,369 @@ +package io.synor.dex + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.math.BigInteger + +enum class PoolType { + @SerialName("constant_product") CONSTANT_PRODUCT, + @SerialName("stable") STABLE, + @SerialName("concentrated") CONCENTRATED +} + +enum class PositionSide { + @SerialName("long") LONG, + @SerialName("short") SHORT +} + +enum class OrderType { + @SerialName("market") MARKET, + @SerialName("limit") LIMIT, + @SerialName("stop_market") STOP_MARKET, + @SerialName("stop_limit") STOP_LIMIT, + @SerialName("take_profit") TAKE_PROFIT, + @SerialName("take_profit_limit") TAKE_PROFIT_LIMIT +} + +enum class MarginType { + @SerialName("cross") CROSS, + @SerialName("isolated") ISOLATED +} + +enum class TimeInForce { + GTC, IOC, FOK, GTD +} + +enum class OrderStatus { + @SerialName("pending") PENDING, + @SerialName("open") OPEN, + @SerialName("partially_filled") PARTIALLY_FILLED, + @SerialName("filled") FILLED, + @SerialName("cancelled") CANCELLED, + @SerialName("expired") EXPIRED +} + +@Serializable +data class Token( + val address: String, + val symbol: String, + val name: String, + val decimals: Int, + @SerialName("total_supply") val totalSupply: String, + @SerialName("price_usd") val priceUsd: Double? = null, + @SerialName("logo_url") val logoUrl: String? = null, + val verified: Boolean = false +) + +@Serializable +data class Pool( + val id: String, + @SerialName("token_a") val tokenA: Token, + @SerialName("token_b") val tokenB: Token, + @SerialName("pool_type") val poolType: PoolType, + @SerialName("reserve_a") val reserveA: String, + @SerialName("reserve_b") val reserveB: String, + val fee: Double, + @SerialName("tvl_usd") val tvlUsd: Double, + @SerialName("volume_24h") val volume24h: Double, + val apr: Double, + @SerialName("lp_token_address") val lpTokenAddress: String, + @SerialName("tick_spacing") val tickSpacing: Int? = null, + @SerialName("sqrt_price") val sqrtPrice: String? = null +) + +data class PoolFilter( + val tokens: List? = null, + val minTvl: Double? = null, + val minVolume24h: Double? = null, + val verified: Boolean? = null, + val limit: Int? = null, + val offset: Int? = null +) + +@Serializable +data class Quote( + @SerialName("token_in") val tokenIn: String, + @SerialName("token_out") val tokenOut: String, + @SerialName("amount_in") val amountIn: String, + @SerialName("amount_out") val amountOut: String, + @SerialName("price_impact") val priceImpact: Double, + val route: List, + val fee: String, + @SerialName("minimum_received") val minimumReceived: String, + @SerialName("expires_at") val expiresAt: Long +) + +data class QuoteParams( + val tokenIn: String, + val tokenOut: String, + val amountIn: BigInteger, + val slippage: Double? = null +) + +data class SwapParams( + val tokenIn: String, + val tokenOut: String, + val amountIn: BigInteger, + val minAmountOut: BigInteger, + val deadline: Long? = null, + val recipient: String? = null +) + +@Serializable +data class SwapResult( + @SerialName("transaction_hash") val transactionHash: String, + @SerialName("amount_in") val amountIn: String, + @SerialName("amount_out") val amountOut: String, + @SerialName("effective_price") val effectivePrice: Double, + @SerialName("fee_paid") val feePaid: String, + val route: List +) + +data class AddLiquidityParams( + val tokenA: String, + val tokenB: String, + val amountA: BigInteger, + val amountB: BigInteger, + val minAmountA: BigInteger? = null, + val minAmountB: BigInteger? = null, + val deadline: Long? = null +) + +data class RemoveLiquidityParams( + val pool: String, + val lpAmount: BigInteger, + val minAmountA: BigInteger? = null, + val minAmountB: BigInteger? = null, + val deadline: Long? = null +) + +@Serializable +data class LiquidityResult( + @SerialName("transaction_hash") val transactionHash: String, + @SerialName("amount_a") val amountA: String, + @SerialName("amount_b") val amountB: String, + @SerialName("lp_tokens") val lpTokens: String, + @SerialName("pool_share") val poolShare: Double +) + +@Serializable +data class LPPosition( + @SerialName("pool_id") val poolId: String, + @SerialName("lp_tokens") val lpTokens: String, + @SerialName("token_a_amount") val tokenAAmount: String, + @SerialName("token_b_amount") val tokenBAmount: String, + @SerialName("value_usd") val valueUsd: Double, + @SerialName("unclaimed_fees_a") val unclaimedFeesA: String, + @SerialName("unclaimed_fees_b") val unclaimedFeesB: String, + @SerialName("impermanent_loss") val impermanentLoss: Double, + @SerialName("tick_lower") val tickLower: Int? = null, + @SerialName("tick_upper") val tickUpper: Int? = null, + @SerialName("in_range") val inRange: Boolean? = null +) + +@Serializable +data class PerpMarket( + val symbol: String, + @SerialName("base_asset") val baseAsset: String, + @SerialName("quote_asset") val quoteAsset: String, + @SerialName("index_price") val indexPrice: Double, + @SerialName("mark_price") val markPrice: Double, + @SerialName("funding_rate") val fundingRate: Double, + @SerialName("next_funding_time") val nextFundingTime: Long, + @SerialName("open_interest") val openInterest: String, + @SerialName("volume_24h") val volume24h: Double, + @SerialName("price_change_24h") val priceChange24h: Double, + @SerialName("max_leverage") val maxLeverage: Int, + @SerialName("min_order_size") val minOrderSize: String, + @SerialName("tick_size") val tickSize: Double, + @SerialName("maintenance_margin") val maintenanceMargin: Double, + @SerialName("initial_margin") val initialMargin: Double +) + +data class OpenPositionParams( + val market: String, + val side: PositionSide, + val size: BigInteger, + val leverage: Int, + val orderType: OrderType, + val limitPrice: Double? = null, + val stopLoss: Double? = null, + val takeProfit: Double? = null, + val marginType: MarginType = MarginType.CROSS, + val reduceOnly: Boolean = false +) + +data class ClosePositionParams( + val market: String, + val size: BigInteger? = null, + val orderType: OrderType = OrderType.MARKET, + val limitPrice: Double? = null +) + +data class ModifyPositionParams( + val positionId: String, + val newLeverage: Int? = null, + val newMargin: BigInteger? = null, + val newStopLoss: Double? = null, + val newTakeProfit: Double? = null +) + +@Serializable +data class PerpPosition( + val id: String, + val market: String, + val side: PositionSide, + val size: String, + @SerialName("entry_price") val entryPrice: Double, + @SerialName("mark_price") val markPrice: Double, + @SerialName("liquidation_price") val liquidationPrice: Double, + val margin: String, + val leverage: Int, + @SerialName("unrealized_pnl") val unrealizedPnl: String, + @SerialName("realized_pnl") val realizedPnl: String, + @SerialName("margin_ratio") val marginRatio: Double, + @SerialName("stop_loss") val stopLoss: Double? = null, + @SerialName("take_profit") val takeProfit: Double? = null, + @SerialName("created_at") val createdAt: Long = 0, + @SerialName("updated_at") val updatedAt: Long = 0 +) + +@Serializable +data class PerpOrder( + val id: String, + val market: String, + val side: PositionSide, + @SerialName("order_type") val orderType: OrderType, + val size: String, + val price: Double? = null, + @SerialName("filled_size") val filledSize: String = "0", + val status: OrderStatus, + @SerialName("reduce_only") val reduceOnly: Boolean = false, + @SerialName("created_at") val createdAt: Long = 0 +) + +@Serializable +data class FundingPayment( + val market: String, + val amount: String, + val rate: Double, + @SerialName("position_size") val positionSize: String, + val timestamp: Long +) + +@Serializable +data class FundingRateInfo( + val rate: Double, + @SerialName("next_time") val nextTime: Long +) + +@Serializable +data class OrderBookEntry( + val price: Double, + val size: String, + val orders: Int +) + +@Serializable +data class OrderBook( + val market: String, + val bids: List, + val asks: List, + val timestamp: Long +) + +data class LimitOrderParams( + val market: String, + val side: String, + val price: Double, + val size: BigInteger, + val timeInForce: TimeInForce = TimeInForce.GTC, + val postOnly: Boolean = false +) + +@Serializable +data class Order( + val id: String, + val market: String, + val side: String, + val price: Double, + val size: String, + @SerialName("filled_size") val filledSize: String, + val status: OrderStatus, + @SerialName("time_in_force") val timeInForce: TimeInForce, + @SerialName("post_only") val postOnly: Boolean, + @SerialName("created_at") val createdAt: Long, + @SerialName("updated_at") val updatedAt: Long +) + +@Serializable +data class Farm( + val id: String, + val name: String, + @SerialName("stake_token") val stakeToken: Token, + @SerialName("reward_tokens") val rewardTokens: List, + @SerialName("tvl_usd") val tvlUsd: Double, + val apr: Double, + @SerialName("daily_rewards") val dailyRewards: List, + @SerialName("lockup_period") val lockupPeriod: Long? = null, + @SerialName("min_stake") val minStake: String? = null +) + +data class StakeParams( + val farm: String, + val amount: BigInteger +) + +@Serializable +data class FarmPosition( + @SerialName("farm_id") val farmId: String, + @SerialName("staked_amount") val stakedAmount: String, + @SerialName("pending_rewards") val pendingRewards: List, + @SerialName("staked_at") val stakedAt: Long, + @SerialName("unlock_at") val unlockAt: Long? = null +) + +@Serializable +data class OHLCV( + val timestamp: Long, + val open: Double, + val high: Double, + val low: Double, + val close: Double, + val volume: Double +) + +@Serializable +data class TradeHistory( + val id: String, + val market: String, + val side: String, + val price: Double, + val size: String, + val timestamp: Long, + val maker: String, + val taker: String +) + +@Serializable +data class VolumeStats( + @SerialName("volume_24h") val volume24h: Double, + @SerialName("volume_7d") val volume7d: Double, + @SerialName("volume_30d") val volume30d: Double, + @SerialName("trades_24h") val trades24h: Int, + @SerialName("unique_traders_24h") val uniqueTraders24h: Int +) + +@Serializable +data class TVLStats( + @SerialName("total_tvl") val totalTvl: Double, + @SerialName("pools_tvl") val poolsTvl: Double, + @SerialName("farms_tvl") val farmsTvl: Double, + @SerialName("perps_tvl") val perpsTvl: Double +) + +@Serializable +data class ClaimRewardsResult( + val amount: String, + @SerialName("transaction_hash") val transactionHash: String +) diff --git a/sdk/python/src/synor_dex/__init__.py b/sdk/python/src/synor_dex/__init__.py new file mode 100644 index 0000000..771934a --- /dev/null +++ b/sdk/python/src/synor_dex/__init__.py @@ -0,0 +1,96 @@ +""" +Synor DEX SDK for Python + +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 +""" + +from .client import SynorDex +from .types import ( + DexConfig, + Token, + Pool, + PoolType, + PoolFilter, + Quote, + QuoteParams, + SwapParams, + SwapResult, + AddLiquidityParams, + RemoveLiquidityParams, + LiquidityResult, + LPPosition, + PerpMarket, + PositionSide, + OrderType, + MarginType, + OpenPositionParams, + ClosePositionParams, + ModifyPositionParams, + PerpPosition, + PerpOrder, + FundingPayment, + OrderBook, + OrderBookEntry, + LimitOrderParams, + TimeInForce, + Order, + OrderStatus, + Farm, + StakeParams, + FarmPosition, + OHLCV, + TradeHistory, + VolumeStats, + TVLStats, + Subscription, + DexError, +) + +__all__ = [ + "SynorDex", + "DexConfig", + "Token", + "Pool", + "PoolType", + "PoolFilter", + "Quote", + "QuoteParams", + "SwapParams", + "SwapResult", + "AddLiquidityParams", + "RemoveLiquidityParams", + "LiquidityResult", + "LPPosition", + "PerpMarket", + "PositionSide", + "OrderType", + "MarginType", + "OpenPositionParams", + "ClosePositionParams", + "ModifyPositionParams", + "PerpPosition", + "PerpOrder", + "FundingPayment", + "OrderBook", + "OrderBookEntry", + "LimitOrderParams", + "TimeInForce", + "Order", + "OrderStatus", + "Farm", + "StakeParams", + "FarmPosition", + "OHLCV", + "TradeHistory", + "VolumeStats", + "TVLStats", + "Subscription", + "DexError", +] + +__version__ = "0.1.0" diff --git a/sdk/python/src/synor_dex/client.py b/sdk/python/src/synor_dex/client.py new file mode 100644 index 0000000..861c8a4 --- /dev/null +++ b/sdk/python/src/synor_dex/client.py @@ -0,0 +1,496 @@ +""" +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 +""" + +import asyncio +import json +import time +import uuid +from typing import Optional, List, Callable, Any, TypeVar, Dict +from urllib.parse import urlencode + +import httpx +import websockets + +from .types import ( + DexConfig, + Token, + Pool, + PoolFilter, + Quote, + QuoteParams, + SwapParams, + SwapResult, + AddLiquidityParams, + RemoveLiquidityParams, + LiquidityResult, + LPPosition, + PerpMarket, + OpenPositionParams, + ClosePositionParams, + ModifyPositionParams, + PerpPosition, + PerpOrder, + FundingPayment, + OrderBook, + LimitOrderParams, + Order, + Farm, + StakeParams, + FarmPosition, + OHLCV, + TradeHistory, + VolumeStats, + TVLStats, + Subscription, + DexError, +) + +T = TypeVar("T") + + +class PerpsClient: + """Perpetual futures sub-client""" + + def __init__(self, dex: "SynorDex"): + self._dex = dex + + async def list_markets(self) -> List[PerpMarket]: + """List all perpetual markets""" + return await self._dex._get("/perps/markets") + + async def get_market(self, symbol: str) -> PerpMarket: + """Get a specific perpetual market""" + return await self._dex._get(f"/perps/markets/{symbol}") + + async def open_position(self, params: OpenPositionParams) -> PerpPosition: + """Open a perpetual position""" + return await self._dex._post("/perps/positions", { + "market": params.market, + "side": params.side.value, + "size": str(params.size), + "leverage": params.leverage, + "order_type": params.order_type.value, + "limit_price": params.limit_price, + "stop_loss": params.stop_loss, + "take_profit": params.take_profit, + "margin_type": params.margin_type.value, + "reduce_only": params.reduce_only, + }) + + async def close_position(self, params: ClosePositionParams) -> PerpPosition: + """Close a perpetual position""" + return await self._dex._post("/perps/positions/close", { + "market": params.market, + "size": str(params.size) if params.size else None, + "order_type": params.order_type.value, + "limit_price": params.limit_price, + }) + + async def modify_position(self, params: ModifyPositionParams) -> PerpPosition: + """Modify a perpetual position""" + return await self._dex._post(f"/perps/positions/{params.position_id}/modify", { + "new_leverage": params.new_leverage, + "new_margin": str(params.new_margin) if params.new_margin else None, + "new_stop_loss": params.new_stop_loss, + "new_take_profit": params.new_take_profit, + }) + + async def get_positions(self) -> List[PerpPosition]: + """Get all open positions""" + return await self._dex._get("/perps/positions") + + async def get_position(self, market: str) -> Optional[PerpPosition]: + """Get position for a specific market""" + return await self._dex._get(f"/perps/positions/{market}") + + async def get_orders(self) -> List[PerpOrder]: + """Get all open orders""" + return await self._dex._get("/perps/orders") + + async def cancel_order(self, order_id: str) -> None: + """Cancel an order""" + await self._dex._delete(f"/perps/orders/{order_id}") + + async def cancel_all_orders(self, market: Optional[str] = None) -> int: + """Cancel all orders, optionally for a specific market""" + path = f"/perps/orders?market={market}" if market else "/perps/orders" + result = await self._dex._delete(path) + return result.get("cancelled", 0) + + async def get_funding_history(self, market: str, limit: int = 100) -> List[FundingPayment]: + """Get funding payment history""" + return await self._dex._get(f"/perps/funding/{market}?limit={limit}") + + async def get_funding_rate(self, market: str) -> Dict[str, Any]: + """Get current funding rate""" + return await self._dex._get(f"/perps/funding/{market}/current") + + async def subscribe_position(self, callback: Callable[[PerpPosition], None]) -> Subscription: + """Subscribe to position updates""" + return await self._dex._subscribe("position", {}, callback) + + +class OrderBookClient: + """Order book sub-client""" + + def __init__(self, dex: "SynorDex"): + self._dex = dex + + async def get_order_book(self, market: str, depth: int = 20) -> OrderBook: + """Get order book for a market""" + return await self._dex._get(f"/orderbook/{market}?depth={depth}") + + async def place_limit_order(self, params: LimitOrderParams) -> Order: + """Place a limit order""" + return await self._dex._post("/orderbook/orders", { + "market": params.market, + "side": params.side, + "price": params.price, + "size": str(params.size), + "time_in_force": params.time_in_force.value, + "post_only": params.post_only, + }) + + async def cancel_order(self, order_id: str) -> None: + """Cancel an order""" + await self._dex._delete(f"/orderbook/orders/{order_id}") + + async def get_open_orders(self, market: Optional[str] = None) -> List[Order]: + """Get all open orders""" + path = f"/orderbook/orders?market={market}" if market else "/orderbook/orders" + return await self._dex._get(path) + + async def get_order_history(self, limit: int = 50) -> List[Order]: + """Get order history""" + return await self._dex._get(f"/orderbook/orders/history?limit={limit}") + + +class FarmsClient: + """Farms and staking sub-client""" + + def __init__(self, dex: "SynorDex"): + self._dex = dex + + async def list_farms(self) -> List[Farm]: + """List all farms""" + return await self._dex._get("/farms") + + async def get_farm(self, farm_id: str) -> Farm: + """Get a specific farm""" + return await self._dex._get(f"/farms/{farm_id}") + + async def stake(self, params: StakeParams) -> FarmPosition: + """Stake tokens in a farm""" + return await self._dex._post("/farms/stake", { + "farm": params.farm, + "amount": str(params.amount), + }) + + async def unstake(self, farm: str, amount: int) -> FarmPosition: + """Unstake tokens from a farm""" + return await self._dex._post("/farms/unstake", { + "farm": farm, + "amount": str(amount), + }) + + async def claim_rewards(self, farm: str) -> Dict[str, Any]: + """Claim rewards from a farm""" + return await self._dex._post("/farms/claim", {"farm": farm}) + + async def get_my_farm_positions(self) -> List[FarmPosition]: + """Get all farm positions""" + return await self._dex._get("/farms/positions") + + +class SynorDex: + """ + 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 + """ + + def __init__(self, config: DexConfig): + self._config = config + self._closed = False + self._ws = None + self._subscriptions: Dict[str, Callable] = {} + + self._client = httpx.AsyncClient( + base_url=config.endpoint, + timeout=config.timeout / 1000, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {config.api_key}", + "X-SDK-Version": "python/0.1.0", + }, + ) + + # Sub-clients + self.perps = PerpsClient(self) + self.orderbook = OrderBookClient(self) + self.farms = FarmsClient(self) + + # Token Operations + async def get_token(self, address: str) -> Token: + """Get token information""" + return await self._get(f"/tokens/{address}") + + async def list_tokens(self) -> List[Token]: + """List all tokens""" + return await self._get("/tokens") + + async def search_tokens(self, query: str) -> List[Token]: + """Search tokens""" + return await self._get(f"/tokens/search?q={query}") + + # Pool Operations + async def get_pool(self, token_a: str, token_b: str) -> Pool: + """Get pool for a token pair""" + return await self._get(f"/pools/{token_a}/{token_b}") + + async def get_pool_by_id(self, pool_id: str) -> Pool: + """Get pool by ID""" + return await self._get(f"/pools/{pool_id}") + + async def list_pools(self, filter: Optional[PoolFilter] = None) -> List[Pool]: + """List pools with optional filtering""" + params = {} + if filter: + if filter.tokens: + params["tokens"] = ",".join(filter.tokens) + if filter.min_tvl: + params["min_tvl"] = str(filter.min_tvl) + if filter.min_volume_24h: + params["min_volume"] = str(filter.min_volume_24h) + if filter.verified is not None: + params["verified"] = str(filter.verified).lower() + if filter.limit: + params["limit"] = str(filter.limit) + if filter.offset: + params["offset"] = str(filter.offset) + + query = urlencode(params) if params else "" + return await self._get(f"/pools?{query}") + + # Swap Operations + async def get_quote(self, params: QuoteParams) -> Quote: + """Get a swap quote""" + return await self._post("/swap/quote", { + "token_in": params.token_in, + "token_out": params.token_out, + "amount_in": str(params.amount_in), + "slippage": params.slippage, + }) + + async def swap(self, params: SwapParams) -> SwapResult: + """Execute a swap""" + deadline = params.deadline or int(time.time()) + 1200 + return await self._post("/swap", { + "token_in": params.token_in, + "token_out": params.token_out, + "amount_in": str(params.amount_in), + "min_amount_out": str(params.min_amount_out), + "deadline": deadline, + "recipient": params.recipient, + }) + + # Liquidity Operations + async def add_liquidity(self, params: AddLiquidityParams) -> LiquidityResult: + """Add liquidity to a pool""" + deadline = params.deadline or int(time.time()) + 1200 + return await self._post("/liquidity/add", { + "token_a": params.token_a, + "token_b": params.token_b, + "amount_a": str(params.amount_a), + "amount_b": str(params.amount_b), + "min_amount_a": str(params.min_amount_a) if params.min_amount_a else None, + "min_amount_b": str(params.min_amount_b) if params.min_amount_b else None, + "deadline": deadline, + }) + + async def remove_liquidity(self, params: RemoveLiquidityParams) -> LiquidityResult: + """Remove liquidity from a pool""" + deadline = params.deadline or int(time.time()) + 1200 + return await self._post("/liquidity/remove", { + "pool": params.pool, + "lp_amount": str(params.lp_amount), + "min_amount_a": str(params.min_amount_a) if params.min_amount_a else None, + "min_amount_b": str(params.min_amount_b) if params.min_amount_b else None, + "deadline": deadline, + }) + + async def get_my_positions(self) -> List[LPPosition]: + """Get all LP positions""" + return await self._get("/liquidity/positions") + + # Analytics + async def get_price_history(self, pair: str, interval: str, limit: int = 100) -> List[OHLCV]: + """Get price history (OHLCV candles)""" + return await self._get(f"/analytics/candles/{pair}?interval={interval}&limit={limit}") + + async def get_trade_history(self, pair: str, limit: int = 50) -> List[TradeHistory]: + """Get trade history""" + return await self._get(f"/analytics/trades/{pair}?limit={limit}") + + async def get_volume_stats(self) -> VolumeStats: + """Get volume statistics""" + return await self._get("/analytics/volume") + + async def get_tvl(self) -> TVLStats: + """Get TVL statistics""" + return await self._get("/analytics/tvl") + + # Subscriptions + async def subscribe_price(self, market: str, callback: Callable[[float], None]) -> Subscription: + """Subscribe to price updates""" + return await self._subscribe("price", {"market": market}, callback) + + async def subscribe_trades(self, market: str, callback: Callable[[TradeHistory], None]) -> Subscription: + """Subscribe to trade updates""" + return await self._subscribe("trades", {"market": market}, callback) + + async def subscribe_order_book(self, market: str, callback: Callable[[OrderBook], None]) -> Subscription: + """Subscribe to order book updates""" + return await self._subscribe("orderbook", {"market": market}, callback) + + # Lifecycle + async def health_check(self) -> bool: + """Check if the service is healthy""" + try: + response = await self._get("/health") + return response.get("status") == "healthy" + except Exception: + return False + + async def close(self) -> None: + """Close the client""" + self._closed = True + if self._ws: + await self._ws.close() + self._ws = None + self._subscriptions.clear() + await self._client.aclose() + + # Internal Methods + async def _get(self, path: str) -> Any: + """Make a GET request""" + return await self._request("GET", path) + + async def _post(self, path: str, body: dict) -> Any: + """Make a POST request""" + return await self._request("POST", path, body) + + async def _delete(self, path: str) -> Any: + """Make a DELETE request""" + return await self._request("DELETE", path) + + async def _request(self, method: str, path: str, body: Optional[dict] = None) -> Any: + """Make an HTTP request with retries""" + if self._closed: + raise DexError("Client has been closed", "CLIENT_CLOSED") + + last_error = None + + for attempt in range(self._config.retries): + try: + if method == "GET": + response = await self._client.get(path) + elif method == "POST": + response = await self._client.post(path, json=body) + elif method == "DELETE": + response = await self._client.delete(path) + else: + raise DexError(f"Unknown method: {method}") + + if not response.is_success: + try: + error = response.json() + except Exception: + error = {} + raise DexError( + error.get("message", f"HTTP {response.status_code}"), + error.get("code"), + response.status_code, + ) + + return response.json() + + except DexError: + raise + except Exception as e: + last_error = e + if self._config.debug: + print(f"Attempt {attempt + 1} failed: {e}") + if attempt < self._config.retries - 1: + await asyncio.sleep(2 ** attempt) + + raise last_error or DexError("Unknown error") + + async def _subscribe( + self, + channel: str, + params: dict, + callback: Callable, + ) -> Subscription: + """Subscribe to a WebSocket channel""" + await self._ensure_websocket() + + subscription_id = str(uuid.uuid4())[:8] + self._subscriptions[subscription_id] = callback + + await self._ws.send(json.dumps({ + "type": "subscribe", + "channel": channel, + "subscription_id": subscription_id, + **params, + })) + + def cancel(): + self._subscriptions.pop(subscription_id, None) + if self._ws: + asyncio.create_task(self._ws.send(json.dumps({ + "type": "unsubscribe", + "subscription_id": subscription_id, + }))) + + return Subscription(id=subscription_id, channel=channel, cancel=cancel) + + async def _ensure_websocket(self) -> None: + """Ensure WebSocket connection is established""" + if self._ws and self._ws.open: + return + + self._ws = await websockets.connect(self._config.ws_endpoint) + + # Authenticate + await self._ws.send(json.dumps({ + "type": "auth", + "api_key": self._config.api_key, + })) + + # Start message handler + asyncio.create_task(self._handle_messages()) + + async def _handle_messages(self) -> None: + """Handle incoming WebSocket messages""" + try: + async for message in self._ws: + data = json.loads(message) + subscription_id = data.get("subscription_id") + if subscription_id and subscription_id in self._subscriptions: + self._subscriptions[subscription_id](data.get("data")) + except Exception: + pass # Connection closed diff --git a/sdk/python/src/synor_dex/types.py b/sdk/python/src/synor_dex/types.py new file mode 100644 index 0000000..621b888 --- /dev/null +++ b/sdk/python/src/synor_dex/types.py @@ -0,0 +1,445 @@ +""" +Synor DEX SDK Types +""" + +from dataclasses import dataclass, field +from enum import Enum +from typing import Optional, List, Callable, Any +from decimal import Decimal + + +class PoolType(Enum): + """Type of liquidity pool""" + CONSTANT_PRODUCT = "constant_product" # x * y = k + STABLE = "stable" # Optimized for stablecoins + CONCENTRATED = "concentrated" # Uniswap v3 style + + +class PositionSide(Enum): + """Perpetual position side""" + LONG = "long" + SHORT = "short" + + +class OrderType(Enum): + """Order type for perpetuals""" + MARKET = "market" + LIMIT = "limit" + STOP_MARKET = "stop_market" + STOP_LIMIT = "stop_limit" + TAKE_PROFIT = "take_profit" + TAKE_PROFIT_LIMIT = "take_profit_limit" + + +class MarginType(Enum): + """Margin type for perpetuals""" + CROSS = "cross" + ISOLATED = "isolated" + + +class TimeInForce(Enum): + """Time in force for limit orders""" + GTC = "GTC" # Good Till Cancel + IOC = "IOC" # Immediate Or Cancel + FOK = "FOK" # Fill Or Kill + GTD = "GTD" # Good Till Date + + +class OrderStatus(Enum): + """Order status""" + PENDING = "pending" + OPEN = "open" + PARTIALLY_FILLED = "partially_filled" + FILLED = "filled" + CANCELLED = "cancelled" + EXPIRED = "expired" + + +class DexError(Exception): + """DEX SDK error""" + def __init__(self, message: str, code: Optional[str] = None, status: Optional[int] = None): + super().__init__(message) + self.message = message + self.code = code + self.status = status + + +@dataclass +class DexConfig: + """DEX client configuration""" + api_key: str + endpoint: str = "https://dex.synor.io/v1" + ws_endpoint: str = "wss://dex.synor.io/v1/ws" + timeout: int = 30000 + retries: int = 3 + debug: bool = False + + +@dataclass +class Token: + """Token information""" + address: str + symbol: str + name: str + decimals: int + total_supply: int + price_usd: Optional[float] = None + logo_url: Optional[str] = None + verified: bool = False + + +@dataclass +class Pool: + """Liquidity pool information""" + id: str + token_a: Token + token_b: Token + pool_type: PoolType + reserve_a: int + reserve_b: int + fee: float + tvl_usd: float + volume_24h: float + apr: float + lp_token_address: str + tick_spacing: Optional[int] = None # For concentrated liquidity + sqrt_price: Optional[str] = None + + +@dataclass +class PoolFilter: + """Pool list filter""" + tokens: Optional[List[str]] = None + min_tvl: Optional[float] = None + min_volume_24h: Optional[float] = None + verified: Optional[bool] = None + limit: Optional[int] = None + offset: Optional[int] = None + + +@dataclass +class Quote: + """Swap quote""" + token_in: str + token_out: str + amount_in: int + amount_out: int + price_impact: float + route: List[str] + fee: int + minimum_received: int + expires_at: int + + +@dataclass +class QuoteParams: + """Parameters for getting a quote""" + token_in: str + token_out: str + amount_in: int + slippage: float = 0.005 + + +@dataclass +class SwapParams: + """Parameters for executing a swap""" + token_in: str + token_out: str + amount_in: int + min_amount_out: int + deadline: Optional[int] = None + recipient: Optional[str] = None + + +@dataclass +class SwapResult: + """Swap execution result""" + transaction_hash: str + amount_in: int + amount_out: int + effective_price: float + fee_paid: int + route: List[str] + + +@dataclass +class AddLiquidityParams: + """Parameters for adding liquidity""" + token_a: str + token_b: str + amount_a: int + amount_b: int + min_amount_a: Optional[int] = None + min_amount_b: Optional[int] = None + deadline: Optional[int] = None + tick_lower: Optional[int] = None # For concentrated liquidity + tick_upper: Optional[int] = None + + +@dataclass +class RemoveLiquidityParams: + """Parameters for removing liquidity""" + pool: str + lp_amount: int + min_amount_a: Optional[int] = None + min_amount_b: Optional[int] = None + deadline: Optional[int] = None + + +@dataclass +class LiquidityResult: + """Liquidity operation result""" + transaction_hash: str + amount_a: int + amount_b: int + lp_tokens: int + pool_share: float + + +@dataclass +class LPPosition: + """LP position""" + pool_id: str + lp_tokens: int + token_a_amount: int + token_b_amount: int + value_usd: float + unclaimed_fees_a: int + unclaimed_fees_b: int + impermanent_loss: float + tick_lower: Optional[int] = None + tick_upper: Optional[int] = None + in_range: Optional[bool] = None + + +@dataclass +class PerpMarket: + """Perpetual futures market""" + symbol: str + base_asset: str + quote_asset: str + index_price: float + mark_price: float + funding_rate: float + next_funding_time: int + open_interest: int + volume_24h: float + price_change_24h: float + max_leverage: int + min_order_size: int + tick_size: float + maintenance_margin: float + initial_margin: float + + +@dataclass +class OpenPositionParams: + """Parameters for opening a perpetual position""" + market: str + side: PositionSide + size: int + leverage: int # 1-100x + order_type: OrderType + limit_price: Optional[float] = None + stop_loss: Optional[float] = None + take_profit: Optional[float] = None + margin_type: MarginType = MarginType.CROSS + reduce_only: bool = False + + +@dataclass +class ClosePositionParams: + """Parameters for closing a perpetual position""" + market: str + size: Optional[int] = None # None = close entire position + order_type: OrderType = OrderType.MARKET + limit_price: Optional[float] = None + + +@dataclass +class ModifyPositionParams: + """Parameters for modifying a perpetual position""" + position_id: str + new_leverage: Optional[int] = None + new_margin: Optional[int] = None + new_stop_loss: Optional[float] = None + new_take_profit: Optional[float] = None + + +@dataclass +class PerpPosition: + """Perpetual position""" + id: str + market: str + side: PositionSide + size: int + entry_price: float + mark_price: float + liquidation_price: float + margin: int + leverage: int + unrealized_pnl: int + realized_pnl: int + margin_ratio: float + stop_loss: Optional[float] = None + take_profit: Optional[float] = None + created_at: int = 0 + updated_at: int = 0 + + +@dataclass +class PerpOrder: + """Perpetual order""" + id: str + market: str + side: PositionSide + order_type: OrderType + size: int + price: Optional[float] = None + filled_size: int = 0 + status: OrderStatus = OrderStatus.PENDING + reduce_only: bool = False + created_at: int = 0 + + +@dataclass +class FundingPayment: + """Funding payment record""" + market: str + amount: int + rate: float + position_size: int + timestamp: int + + +@dataclass +class OrderBookEntry: + """Order book entry""" + price: float + size: int + orders: int + + +@dataclass +class OrderBook: + """Order book""" + market: str + bids: List[OrderBookEntry] + asks: List[OrderBookEntry] + timestamp: int + + +@dataclass +class LimitOrderParams: + """Parameters for limit orders""" + market: str + side: str # 'buy' or 'sell' + price: float + size: int + time_in_force: TimeInForce = TimeInForce.GTC + post_only: bool = False + + +@dataclass +class Order: + """Limit order""" + id: str + market: str + side: str + price: float + size: int + filled_size: int + status: OrderStatus + time_in_force: TimeInForce + post_only: bool + created_at: int + updated_at: int + + +@dataclass +class Farm: + """Yield farm""" + id: str + name: str + stake_token: Token + reward_tokens: List[Token] + tvl_usd: float + apr: float + daily_rewards: List[int] + lockup_period: Optional[int] = None + min_stake: Optional[int] = None + + +@dataclass +class StakeParams: + """Parameters for staking""" + farm: str + amount: int + + +@dataclass +class FarmPosition: + """Farm position""" + farm_id: str + staked_amount: int + pending_rewards: List[int] + staked_at: int + unlock_at: Optional[int] = None + + +@dataclass +class OHLCV: + """OHLCV candle data""" + timestamp: int + open: float + high: float + low: float + close: float + volume: float + + +@dataclass +class TradeHistory: + """Trade history entry""" + id: str + market: str + side: str + price: float + size: int + timestamp: int + maker: str + taker: str + + +@dataclass +class VolumeStats: + """Volume statistics""" + volume_24h: float + volume_7d: float + volume_30d: float + trades_24h: int + unique_traders_24h: int + + +@dataclass +class TVLStats: + """TVL statistics""" + total_tvl: float + pools_tvl: float + farms_tvl: float + perps_tvl: float + + +@dataclass +class Subscription: + """WebSocket subscription""" + id: str + channel: str + cancel: Callable[[], None] + + +# Callback types +PriceCallback = Callable[[float], None] +TradeCallback = Callable[[TradeHistory], None] +OrderBookCallback = Callable[[OrderBook], None] +PositionCallback = Callable[[PerpPosition], None] diff --git a/sdk/ruby/lib/synor/dex.rb b/sdk/ruby/lib/synor/dex.rb new file mode 100644 index 0000000..f9dbc91 --- /dev/null +++ b/sdk/ruby/lib/synor/dex.rb @@ -0,0 +1,392 @@ +# 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 diff --git a/sdk/rust/src/dex/client.rs b/sdk/rust/src/dex/client.rs new file mode 100644 index 0000000..7b2d6b4 --- /dev/null +++ b/sdk/rust/src/dex/client.rs @@ -0,0 +1,332 @@ +//! Synor DEX Client + +use crate::dex::types::*; +use reqwest::Client; +use serde::{de::DeserializeOwned, Serialize}; +use serde_json::json; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +/// Main DEX client +pub struct SynorDex { + config: DexConfig, + client: Client, + closed: Arc, + pub perps: PerpsClient, + pub orderbook: OrderBookClient, + pub farms: FarmsClient, +} + +impl SynorDex { + /// Create a new SynorDex client + pub fn new(config: DexConfig) -> Result { + let client = Client::builder() + .timeout(Duration::from_millis(config.timeout_ms)) + .build()?; + + let closed = Arc::new(AtomicBool::new(false)); + + Ok(Self { + perps: PerpsClient::new(config.clone(), client.clone(), closed.clone()), + orderbook: OrderBookClient::new(config.clone(), client.clone(), closed.clone()), + farms: FarmsClient::new(config.clone(), client.clone(), closed.clone()), + config, + client, + closed, + }) + } + + // Token Operations + + /// Get token information + pub async fn get_token(&self, address: &str) -> Result { + self.get(&format!("/tokens/{}", address)).await + } + + /// List all tokens + pub async fn list_tokens(&self) -> Result, DexError> { + self.get("/tokens").await + } + + /// Search tokens + pub async fn search_tokens(&self, query: &str) -> Result, DexError> { + self.get(&format!("/tokens/search?q={}", urlencoding::encode(query))).await + } + + // Pool Operations + + /// Get pool by token pair + pub async fn get_pool(&self, token_a: &str, token_b: &str) -> Result { + self.get(&format!("/pools/{}/{}", token_a, token_b)).await + } + + /// Get pool by ID + pub async fn get_pool_by_id(&self, pool_id: &str) -> Result { + self.get(&format!("/pools/{}", pool_id)).await + } + + /// List pools with optional filtering + pub async fn list_pools(&self, filter: Option) -> Result, DexError> { + let mut params = Vec::new(); + + if let Some(f) = filter { + if let Some(tokens) = f.tokens { + params.push(format!("tokens={}", tokens.join(","))); + } + if let Some(min_tvl) = f.min_tvl { + params.push(format!("min_tvl={}", min_tvl)); + } + if let Some(min_volume) = f.min_volume_24h { + params.push(format!("min_volume={}", min_volume)); + } + if let Some(verified) = f.verified { + params.push(format!("verified={}", verified)); + } + if let Some(limit) = f.limit { + params.push(format!("limit={}", limit)); + } + if let Some(offset) = f.offset { + params.push(format!("offset={}", offset)); + } + } + + let path = if params.is_empty() { + "/pools".to_string() + } else { + format!("/pools?{}", params.join("&")) + }; + + self.get(&path).await + } + + // Swap Operations + + /// Get swap quote + pub async fn get_quote(&self, params: QuoteParams) -> Result { + self.post("/swap/quote", json!({ + "token_in": params.token_in, + "token_out": params.token_out, + "amount_in": params.amount_in, + "slippage": params.slippage.unwrap_or(0.005), + })).await + } + + /// Execute swap + pub async fn swap(&self, params: SwapParams) -> Result { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + let deadline = params.deadline.unwrap_or(now + 1200); + + let mut body = json!({ + "token_in": params.token_in, + "token_out": params.token_out, + "amount_in": params.amount_in, + "min_amount_out": params.min_amount_out, + "deadline": deadline, + }); + + if let Some(recipient) = params.recipient { + body["recipient"] = json!(recipient); + } + + self.post("/swap", body).await + } + + // Liquidity Operations + + /// Add liquidity to a pool + pub async fn add_liquidity(&self, params: AddLiquidityParams) -> Result { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + let deadline = params.deadline.unwrap_or(now + 1200); + + let mut body = json!({ + "token_a": params.token_a, + "token_b": params.token_b, + "amount_a": params.amount_a, + "amount_b": params.amount_b, + "deadline": deadline, + }); + + if let Some(min_a) = params.min_amount_a { + body["min_amount_a"] = json!(min_a); + } + if let Some(min_b) = params.min_amount_b { + body["min_amount_b"] = json!(min_b); + } + + self.post("/liquidity/add", body).await + } + + /// Remove liquidity from a pool + pub async fn remove_liquidity(&self, params: RemoveLiquidityParams) -> Result { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + let deadline = params.deadline.unwrap_or(now + 1200); + + let mut body = json!({ + "pool": params.pool, + "lp_amount": params.lp_amount, + "deadline": deadline, + }); + + if let Some(min_a) = params.min_amount_a { + body["min_amount_a"] = json!(min_a); + } + if let Some(min_b) = params.min_amount_b { + body["min_amount_b"] = json!(min_b); + } + + self.post("/liquidity/remove", body).await + } + + /// Get all LP positions + pub async fn get_my_positions(&self) -> Result, DexError> { + self.get("/liquidity/positions").await + } + + // Analytics + + /// Get price history (OHLCV candles) + pub async fn get_price_history(&self, pair: &str, interval: &str, limit: u32) -> Result, DexError> { + self.get(&format!("/analytics/candles/{}?interval={}&limit={}", pair, interval, limit)).await + } + + /// Get trade history + pub async fn get_trade_history(&self, pair: &str, limit: u32) -> Result, DexError> { + self.get(&format!("/analytics/trades/{}?limit={}", pair, limit)).await + } + + /// Get volume statistics + pub async fn get_volume_stats(&self) -> Result { + self.get("/analytics/volume").await + } + + /// Get TVL statistics + pub async fn get_tvl(&self) -> Result { + self.get("/analytics/tvl").await + } + + // Lifecycle + + /// Health check + pub async fn health_check(&self) -> bool { + #[derive(Deserialize)] + struct Health { + status: String, + } + + match self.get::("/health").await { + Ok(h) => h.status == "healthy", + Err(_) => false, + } + } + + /// Close the client + pub fn close(&self) { + self.closed.store(true, Ordering::SeqCst); + } + + /// Check if client is closed + pub fn is_closed(&self) -> bool { + self.closed.load(Ordering::SeqCst) + } + + // Internal methods + + pub(crate) async fn get(&self, path: &str) -> Result { + self.request("GET", path, None::<()>).await + } + + pub(crate) async fn post(&self, path: &str, body: B) -> Result { + self.request("POST", path, Some(body)).await + } + + pub(crate) async fn delete(&self, path: &str) -> Result { + self.request("DELETE", path, None::<()>).await + } + + async fn request( + &self, + method: &str, + path: &str, + body: Option, + ) -> Result { + if self.closed.load(Ordering::SeqCst) { + return Err(DexError::ClientClosed); + } + + let mut last_error = None; + + for attempt in 0..self.config.retries { + let url = format!("{}{}", self.config.endpoint, path); + + let mut request = match method { + "GET" => self.client.get(&url), + "POST" => self.client.post(&url), + "DELETE" => self.client.delete(&url), + _ => return Err(DexError::Http { + message: format!("Unknown method: {}", method), + code: None, + status: None, + }), + }; + + request = request + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", self.config.api_key)) + .header("X-SDK-Version", "rust/0.1.0"); + + if let Some(ref b) = body { + request = request.json(b); + } + + match request.send().await { + Ok(response) => { + let status = response.status(); + + if !status.is_success() { + #[derive(Deserialize, Default)] + struct ErrorResponse { + message: Option, + code: Option, + } + + let error: ErrorResponse = response.json().await.unwrap_or_default(); + return Err(DexError::Http { + message: error.message.unwrap_or_else(|| format!("HTTP {}", status)), + code: error.code, + status: Some(status.as_u16()), + }); + } + + return response.json().await.map_err(DexError::from); + } + Err(e) => { + last_error = Some(e); + if self.config.debug { + eprintln!("Attempt {} failed: {:?}", attempt + 1, last_error); + } + if attempt < self.config.retries - 1 { + tokio::time::sleep(Duration::from_secs(1 << attempt)).await; + } + } + } + } + + Err(last_error.map(DexError::from).unwrap_or(DexError::Http { + message: "Unknown error".to_string(), + code: None, + status: None, + })) + } +} + +use crate::dex::{PerpsClient, OrderBookClient, FarmsClient}; diff --git a/sdk/rust/src/dex/farms.rs b/sdk/rust/src/dex/farms.rs new file mode 100644 index 0000000..3cdb5d2 --- /dev/null +++ b/sdk/rust/src/dex/farms.rs @@ -0,0 +1,111 @@ +//! Farms Client + +use crate::dex::types::*; +use reqwest::Client; +use serde_json::json; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +/// Farms sub-client +pub struct FarmsClient { + config: DexConfig, + client: Client, + closed: Arc, +} + +impl FarmsClient { + pub(crate) fn new(config: DexConfig, client: Client, closed: Arc) -> Self { + Self { config, client, closed } + } + + /// List all farms + pub async fn list_farms(&self) -> Result, DexError> { + self.get("/farms").await + } + + /// Get a specific farm + pub async fn get_farm(&self, farm_id: &str) -> Result { + self.get(&format!("/farms/{}", farm_id)).await + } + + /// Stake tokens in a farm + pub async fn stake(&self, params: StakeParams) -> Result { + self.post("/farms/stake", json!({ + "farm": params.farm, + "amount": params.amount, + })).await + } + + /// Unstake tokens from a farm + pub async fn unstake(&self, farm: &str, amount: &str) -> Result { + self.post("/farms/unstake", json!({ + "farm": farm, + "amount": amount, + })).await + } + + /// Claim rewards from a farm + pub async fn claim_rewards(&self, farm: &str) -> Result { + self.post("/farms/claim", json!({ + "farm": farm, + })).await + } + + /// Get all farm positions + pub async fn get_my_farm_positions(&self) -> Result, DexError> { + self.get("/farms/positions").await + } + + // Internal methods + + async fn get(&self, path: &str) -> Result { + if self.closed.load(Ordering::SeqCst) { + return Err(DexError::ClientClosed); + } + + let url = format!("{}{}", self.config.endpoint, path); + let response = self.client + .get(&url) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", self.config.api_key)) + .header("X-SDK-Version", "rust/0.1.0") + .send() + .await?; + + if !response.status().is_success() { + return Err(DexError::Http { + message: format!("HTTP {}", response.status()), + code: None, + status: Some(response.status().as_u16()), + }); + } + + response.json().await.map_err(DexError::from) + } + + async fn post(&self, path: &str, body: serde_json::Value) -> Result { + if self.closed.load(Ordering::SeqCst) { + return Err(DexError::ClientClosed); + } + + let url = format!("{}{}", self.config.endpoint, path); + let response = self.client + .post(&url) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", self.config.api_key)) + .header("X-SDK-Version", "rust/0.1.0") + .json(&body) + .send() + .await?; + + if !response.status().is_success() { + return Err(DexError::Http { + message: format!("HTTP {}", response.status()), + code: None, + status: Some(response.status().as_u16()), + }); + } + + response.json().await.map_err(DexError::from) + } +} diff --git a/sdk/rust/src/dex/mod.rs b/sdk/rust/src/dex/mod.rs new file mode 100644 index 0000000..458028c --- /dev/null +++ b/sdk/rust/src/dex/mod.rs @@ -0,0 +1,20 @@ +//! Synor DEX SDK for Rust +//! +//! 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 + +mod types; +mod client; +mod perps; +mod orderbook; +mod farms; + +pub use types::*; +pub use client::*; +pub use perps::*; +pub use orderbook::*; +pub use farms::*; diff --git a/sdk/rust/src/dex/orderbook.rs b/sdk/rust/src/dex/orderbook.rs new file mode 100644 index 0000000..fb91152 --- /dev/null +++ b/sdk/rust/src/dex/orderbook.rs @@ -0,0 +1,134 @@ +//! Order Book Client + +use crate::dex::types::*; +use reqwest::Client; +use serde_json::json; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +/// Order book sub-client +pub struct OrderBookClient { + config: DexConfig, + client: Client, + closed: Arc, +} + +impl OrderBookClient { + pub(crate) fn new(config: DexConfig, client: Client, closed: Arc) -> Self { + Self { config, client, closed } + } + + /// Get order book for a market + pub async fn get_order_book(&self, market: &str, depth: u32) -> Result { + self.get(&format!("/orderbook/{}?depth={}", market, depth)).await + } + + /// Place a limit order + pub async fn place_limit_order(&self, params: LimitOrderParams) -> Result { + self.post("/orderbook/orders", json!({ + "market": params.market, + "side": params.side, + "price": params.price, + "size": params.size, + "time_in_force": params.time_in_force, + "post_only": params.post_only, + })).await + } + + /// Cancel an order + pub async fn cancel_order(&self, order_id: &str) -> Result<(), DexError> { + self.delete(&format!("/orderbook/orders/{}", order_id)).await + } + + /// Get all open orders + pub async fn get_open_orders(&self, market: Option<&str>) -> Result, DexError> { + let path = match market { + Some(m) => format!("/orderbook/orders?market={}", m), + None => "/orderbook/orders".to_string(), + }; + self.get(&path).await + } + + /// Get order history + pub async fn get_order_history(&self, limit: u32) -> Result, DexError> { + self.get(&format!("/orderbook/orders/history?limit={}", limit)).await + } + + // Internal methods + + async fn get(&self, path: &str) -> Result { + if self.closed.load(Ordering::SeqCst) { + return Err(DexError::ClientClosed); + } + + let url = format!("{}{}", self.config.endpoint, path); + let response = self.client + .get(&url) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", self.config.api_key)) + .header("X-SDK-Version", "rust/0.1.0") + .send() + .await?; + + if !response.status().is_success() { + return Err(DexError::Http { + message: format!("HTTP {}", response.status()), + code: None, + status: Some(response.status().as_u16()), + }); + } + + response.json().await.map_err(DexError::from) + } + + async fn post(&self, path: &str, body: serde_json::Value) -> Result { + if self.closed.load(Ordering::SeqCst) { + return Err(DexError::ClientClosed); + } + + let url = format!("{}{}", self.config.endpoint, path); + let response = self.client + .post(&url) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", self.config.api_key)) + .header("X-SDK-Version", "rust/0.1.0") + .json(&body) + .send() + .await?; + + if !response.status().is_success() { + return Err(DexError::Http { + message: format!("HTTP {}", response.status()), + code: None, + status: Some(response.status().as_u16()), + }); + } + + response.json().await.map_err(DexError::from) + } + + async fn delete(&self, path: &str) -> Result { + if self.closed.load(Ordering::SeqCst) { + return Err(DexError::ClientClosed); + } + + let url = format!("{}{}", self.config.endpoint, path); + let response = self.client + .delete(&url) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", self.config.api_key)) + .header("X-SDK-Version", "rust/0.1.0") + .send() + .await?; + + if !response.status().is_success() { + return Err(DexError::Http { + message: format!("HTTP {}", response.status()), + code: None, + status: Some(response.status().as_u16()), + }); + } + + response.json().await.map_err(DexError::from) + } +} diff --git a/sdk/rust/src/dex/perps.rs b/sdk/rust/src/dex/perps.rs new file mode 100644 index 0000000..17f635a --- /dev/null +++ b/sdk/rust/src/dex/perps.rs @@ -0,0 +1,222 @@ +//! Perpetuals Client + +use crate::dex::types::*; +use reqwest::Client; +use serde_json::json; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; + +/// Perpetuals sub-client +pub struct PerpsClient { + config: DexConfig, + client: Client, + closed: Arc, +} + +impl PerpsClient { + pub(crate) fn new(config: DexConfig, client: Client, closed: Arc) -> Self { + Self { config, client, closed } + } + + /// List all perpetual markets + pub async fn list_markets(&self) -> Result, DexError> { + self.get("/perps/markets").await + } + + /// Get a specific perpetual market + pub async fn get_market(&self, symbol: &str) -> Result { + self.get(&format!("/perps/markets/{}", symbol)).await + } + + /// Open a perpetual position + pub async fn open_position(&self, params: OpenPositionParams) -> Result { + let mut body = json!({ + "market": params.market, + "side": params.side, + "size": params.size, + "leverage": params.leverage, + "order_type": params.order_type, + "margin_type": params.margin_type, + "reduce_only": params.reduce_only, + }); + + if let Some(price) = params.limit_price { + body["limit_price"] = json!(price); + } + if let Some(sl) = params.stop_loss { + body["stop_loss"] = json!(sl); + } + if let Some(tp) = params.take_profit { + body["take_profit"] = json!(tp); + } + + self.post("/perps/positions", body).await + } + + /// Close a perpetual position + pub async fn close_position(&self, params: ClosePositionParams) -> Result { + let mut body = json!({ + "market": params.market, + "order_type": params.order_type, + }); + + if let Some(size) = params.size { + body["size"] = json!(size); + } + if let Some(price) = params.limit_price { + body["limit_price"] = json!(price); + } + + self.post("/perps/positions/close", body).await + } + + /// Modify a perpetual position + pub async fn modify_position(&self, params: ModifyPositionParams) -> Result { + let mut body = json!({}); + + if let Some(leverage) = params.new_leverage { + body["new_leverage"] = json!(leverage); + } + if let Some(margin) = params.new_margin { + body["new_margin"] = json!(margin); + } + if let Some(sl) = params.new_stop_loss { + body["new_stop_loss"] = json!(sl); + } + if let Some(tp) = params.new_take_profit { + body["new_take_profit"] = json!(tp); + } + + self.post(&format!("/perps/positions/{}/modify", params.position_id), body).await + } + + /// Get all open positions + pub async fn get_positions(&self) -> Result, DexError> { + self.get("/perps/positions").await + } + + /// Get position for a specific market + pub async fn get_position(&self, market: &str) -> Result, DexError> { + self.get(&format!("/perps/positions/{}", market)).await + } + + /// Get all open orders + pub async fn get_orders(&self) -> Result, DexError> { + self.get("/perps/orders").await + } + + /// Cancel an order + pub async fn cancel_order(&self, order_id: &str) -> Result<(), DexError> { + self.delete(&format!("/perps/orders/{}", order_id)).await + } + + /// Cancel all orders + pub async fn cancel_all_orders(&self, market: Option<&str>) -> Result { + let path = match market { + Some(m) => format!("/perps/orders?market={}", m), + None => "/perps/orders".to_string(), + }; + + #[derive(serde::Deserialize)] + struct CancelResult { + cancelled: u32, + } + + let result: CancelResult = self.delete(&path).await?; + Ok(result.cancelled) + } + + /// Get funding payment history + pub async fn get_funding_history(&self, market: &str, limit: u32) -> Result, DexError> { + self.get(&format!("/perps/funding/{}?limit={}", market, limit)).await + } + + /// Get current funding rate + pub async fn get_funding_rate(&self, market: &str) -> Result { + self.get(&format!("/perps/funding/{}/current", market)).await + } + + // Internal methods + + async fn get(&self, path: &str) -> Result { + use std::sync::atomic::Ordering; + + if self.closed.load(Ordering::SeqCst) { + return Err(DexError::ClientClosed); + } + + let url = format!("{}{}", self.config.endpoint, path); + let response = self.client + .get(&url) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", self.config.api_key)) + .header("X-SDK-Version", "rust/0.1.0") + .send() + .await?; + + if !response.status().is_success() { + return Err(DexError::Http { + message: format!("HTTP {}", response.status()), + code: None, + status: Some(response.status().as_u16()), + }); + } + + response.json().await.map_err(DexError::from) + } + + async fn post(&self, path: &str, body: serde_json::Value) -> Result { + use std::sync::atomic::Ordering; + + if self.closed.load(Ordering::SeqCst) { + return Err(DexError::ClientClosed); + } + + let url = format!("{}{}", self.config.endpoint, path); + let response = self.client + .post(&url) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", self.config.api_key)) + .header("X-SDK-Version", "rust/0.1.0") + .json(&body) + .send() + .await?; + + if !response.status().is_success() { + return Err(DexError::Http { + message: format!("HTTP {}", response.status()), + code: None, + status: Some(response.status().as_u16()), + }); + } + + response.json().await.map_err(DexError::from) + } + + async fn delete(&self, path: &str) -> Result { + use std::sync::atomic::Ordering; + + if self.closed.load(Ordering::SeqCst) { + return Err(DexError::ClientClosed); + } + + let url = format!("{}{}", self.config.endpoint, path); + let response = self.client + .delete(&url) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", self.config.api_key)) + .header("X-SDK-Version", "rust/0.1.0") + .send() + .await?; + + if !response.status().is_success() { + return Err(DexError::Http { + message: format!("HTTP {}", response.status()), + code: None, + status: Some(response.status().as_u16()), + }); + } + + response.json().await.map_err(DexError::from) + } +} diff --git a/sdk/rust/src/dex/types.rs b/sdk/rust/src/dex/types.rs new file mode 100644 index 0000000..85e562b --- /dev/null +++ b/sdk/rust/src/dex/types.rs @@ -0,0 +1,544 @@ +//! Synor DEX SDK Types + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use thiserror::Error; + +/// DEX SDK Error +#[derive(Error, Debug)] +pub enum DexError { + #[error("HTTP error: {message}")] + Http { message: String, code: Option, status: Option }, + + #[error("Client closed")] + ClientClosed, + + #[error("WebSocket error: {0}")] + WebSocket(String), + + #[error("Serialization error: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("Request error: {0}")] + Request(#[from] reqwest::Error), +} + +/// Pool type +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum PoolType { + ConstantProduct, // x * y = k + Stable, // Optimized for stablecoins + Concentrated, // Uniswap v3 style +} + +/// Position side for perpetuals +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum PositionSide { + Long, + Short, +} + +/// Order type +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum OrderType { + Market, + Limit, + StopMarket, + StopLimit, + TakeProfit, + TakeProfitLimit, +} + +/// Margin type +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +#[serde(rename_all = "lowercase")] +pub enum MarginType { + #[default] + Cross, + Isolated, +} + +/// Time in force +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub enum TimeInForce { + #[default] + GTC, // Good Till Cancel + IOC, // Immediate Or Cancel + FOK, // Fill Or Kill + GTD, // Good Till Date +} + +/// Order status +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum OrderStatus { + Pending, + Open, + PartiallyFilled, + Filled, + Cancelled, + Expired, +} + +/// DEX client configuration +#[derive(Debug, Clone)] +pub struct DexConfig { + pub api_key: String, + pub endpoint: String, + pub ws_endpoint: String, + pub timeout_ms: u64, + pub retries: u32, + pub debug: bool, +} + +impl DexConfig { + pub fn new(api_key: impl Into) -> Self { + Self { + api_key: api_key.into(), + endpoint: "https://dex.synor.io/v1".to_string(), + ws_endpoint: "wss://dex.synor.io/v1/ws".to_string(), + timeout_ms: 30000, + retries: 3, + debug: false, + } + } + + pub fn with_endpoint(mut self, endpoint: impl Into) -> Self { + self.endpoint = endpoint.into(); + self + } + + pub fn with_ws_endpoint(mut self, ws_endpoint: impl Into) -> Self { + self.ws_endpoint = ws_endpoint.into(); + self + } + + pub fn with_timeout(mut self, timeout_ms: u64) -> Self { + self.timeout_ms = timeout_ms; + self + } + + pub fn with_retries(mut self, retries: u32) -> Self { + self.retries = retries; + self + } + + pub fn with_debug(mut self, debug: bool) -> Self { + self.debug = debug; + self + } +} + +/// Token information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Token { + pub address: String, + pub symbol: String, + pub name: String, + pub decimals: u8, + pub total_supply: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub price_usd: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub logo_url: Option, + #[serde(default)] + pub verified: bool, +} + +/// Liquidity pool +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Pool { + pub id: String, + pub token_a: Token, + pub token_b: Token, + pub pool_type: PoolType, + pub reserve_a: String, + pub reserve_b: String, + pub fee: f64, + pub tvl_usd: f64, + pub volume_24h: f64, + pub apr: f64, + pub lp_token_address: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub tick_spacing: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sqrt_price: Option, +} + +/// Pool filter +#[derive(Debug, Clone, Default)] +pub struct PoolFilter { + pub tokens: Option>, + pub min_tvl: Option, + pub min_volume_24h: Option, + pub verified: Option, + pub limit: Option, + pub offset: Option, +} + +/// Swap quote +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Quote { + pub token_in: String, + pub token_out: String, + pub amount_in: String, + pub amount_out: String, + pub price_impact: f64, + pub route: Vec, + pub fee: String, + pub minimum_received: String, + pub expires_at: i64, +} + +/// Quote parameters +#[derive(Debug, Clone)] +pub struct QuoteParams { + pub token_in: String, + pub token_out: String, + pub amount_in: String, + pub slippage: Option, +} + +/// Swap parameters +#[derive(Debug, Clone)] +pub struct SwapParams { + pub token_in: String, + pub token_out: String, + pub amount_in: String, + pub min_amount_out: String, + pub deadline: Option, + pub recipient: Option, +} + +/// Swap result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SwapResult { + pub transaction_hash: String, + pub amount_in: String, + pub amount_out: String, + pub effective_price: f64, + pub fee_paid: String, + pub route: Vec, +} + +/// Add liquidity parameters +#[derive(Debug, Clone)] +pub struct AddLiquidityParams { + pub token_a: String, + pub token_b: String, + pub amount_a: String, + pub amount_b: String, + pub min_amount_a: Option, + pub min_amount_b: Option, + pub deadline: Option, + pub tick_lower: Option, + pub tick_upper: Option, +} + +/// Remove liquidity parameters +#[derive(Debug, Clone)] +pub struct RemoveLiquidityParams { + pub pool: String, + pub lp_amount: String, + pub min_amount_a: Option, + pub min_amount_b: Option, + pub deadline: Option, +} + +/// Liquidity result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LiquidityResult { + pub transaction_hash: String, + pub amount_a: String, + pub amount_b: String, + pub lp_tokens: String, + pub pool_share: f64, +} + +/// LP position +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LPPosition { + pub pool_id: String, + pub lp_tokens: String, + pub token_a_amount: String, + pub token_b_amount: String, + pub value_usd: f64, + pub unclaimed_fees_a: String, + pub unclaimed_fees_b: String, + pub impermanent_loss: f64, + #[serde(skip_serializing_if = "Option::is_none")] + pub tick_lower: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tick_upper: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub in_range: Option, +} + +/// Perpetual market +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PerpMarket { + pub symbol: String, + pub base_asset: String, + pub quote_asset: String, + pub index_price: f64, + pub mark_price: f64, + pub funding_rate: f64, + pub next_funding_time: i64, + pub open_interest: String, + pub volume_24h: f64, + pub price_change_24h: f64, + pub max_leverage: u32, + pub min_order_size: String, + pub tick_size: f64, + pub maintenance_margin: f64, + pub initial_margin: f64, +} + +/// Open position parameters +#[derive(Debug, Clone)] +pub struct OpenPositionParams { + pub market: String, + pub side: PositionSide, + pub size: String, + pub leverage: u32, // 1-100x + pub order_type: OrderType, + pub limit_price: Option, + pub stop_loss: Option, + pub take_profit: Option, + pub margin_type: MarginType, + pub reduce_only: bool, +} + +/// Close position parameters +#[derive(Debug, Clone)] +pub struct ClosePositionParams { + pub market: String, + pub size: Option, // None = close entire position + pub order_type: OrderType, + pub limit_price: Option, +} + +/// Modify position parameters +#[derive(Debug, Clone)] +pub struct ModifyPositionParams { + pub position_id: String, + pub new_leverage: Option, + pub new_margin: Option, + pub new_stop_loss: Option, + pub new_take_profit: Option, +} + +/// Perpetual position +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PerpPosition { + pub id: String, + pub market: String, + pub side: PositionSide, + pub size: String, + pub entry_price: f64, + pub mark_price: f64, + pub liquidation_price: f64, + pub margin: String, + pub leverage: u32, + pub unrealized_pnl: String, + pub realized_pnl: String, + pub margin_ratio: f64, + #[serde(skip_serializing_if = "Option::is_none")] + pub stop_loss: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub take_profit: Option, + #[serde(default)] + pub created_at: i64, + #[serde(default)] + pub updated_at: i64, +} + +/// Perpetual order +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PerpOrder { + pub id: String, + pub market: String, + pub side: PositionSide, + pub order_type: OrderType, + pub size: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub price: Option, + #[serde(default)] + pub filled_size: String, + pub status: OrderStatus, + #[serde(default)] + pub reduce_only: bool, + #[serde(default)] + pub created_at: i64, +} + +/// Funding payment +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FundingPayment { + pub market: String, + pub amount: String, + pub rate: f64, + pub position_size: String, + pub timestamp: i64, +} + +/// Order book entry +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OrderBookEntry { + pub price: f64, + pub size: String, + pub orders: u32, +} + +/// Order book +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OrderBook { + pub market: String, + pub bids: Vec, + pub asks: Vec, + pub timestamp: i64, +} + +/// Limit order parameters +#[derive(Debug, Clone)] +pub struct LimitOrderParams { + pub market: String, + pub side: String, // "buy" or "sell" + pub price: f64, + pub size: String, + pub time_in_force: TimeInForce, + pub post_only: bool, +} + +/// Limit order +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Order { + pub id: String, + pub market: String, + pub side: String, + pub price: f64, + pub size: String, + pub filled_size: String, + pub status: OrderStatus, + pub time_in_force: TimeInForce, + pub post_only: bool, + pub created_at: i64, + pub updated_at: i64, +} + +/// Yield farm +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Farm { + pub id: String, + pub name: String, + pub stake_token: Token, + pub reward_tokens: Vec, + pub tvl_usd: f64, + pub apr: f64, + pub daily_rewards: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub lockup_period: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub min_stake: Option, +} + +/// Stake parameters +#[derive(Debug, Clone)] +pub struct StakeParams { + pub farm: String, + pub amount: String, +} + +/// Farm position +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FarmPosition { + pub farm_id: String, + pub staked_amount: String, + pub pending_rewards: Vec, + pub staked_at: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub unlock_at: Option, +} + +/// OHLCV candle data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OHLCV { + pub timestamp: i64, + pub open: f64, + pub high: f64, + pub low: f64, + pub close: f64, + pub volume: f64, +} + +/// Trade history entry +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TradeHistory { + pub id: String, + pub market: String, + pub side: String, + pub price: f64, + pub size: String, + pub timestamp: i64, + pub maker: String, + pub taker: String, +} + +/// Volume statistics +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VolumeStats { + pub volume_24h: f64, + pub volume_7d: f64, + pub volume_30d: f64, + pub trades_24h: u64, + pub unique_traders_24h: u64, +} + +/// TVL statistics +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TVLStats { + pub total_tvl: f64, + pub pools_tvl: f64, + pub farms_tvl: f64, + pub perps_tvl: f64, +} + +/// Subscription handle +pub struct Subscription { + pub id: String, + pub channel: String, + cancel_tx: Option>, +} + +impl Subscription { + pub fn new(id: String, channel: String, cancel_tx: tokio::sync::oneshot::Sender<()>) -> Self { + Self { + id, + channel, + cancel_tx: Some(cancel_tx), + } + } + + pub fn cancel(mut self) { + if let Some(tx) = self.cancel_tx.take() { + let _ = tx.send(()); + } + } +} + +/// Claim rewards result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClaimRewardsResult { + pub amount: String, + pub transaction_hash: String, +} + +/// Funding rate info +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FundingRateInfo { + pub rate: f64, + pub next_time: i64, +} diff --git a/sdk/swift/Sources/SynorDex/SynorDex.swift b/sdk/swift/Sources/SynorDex/SynorDex.swift new file mode 100644 index 0000000..95691e4 --- /dev/null +++ b/sdk/swift/Sources/SynorDex/SynorDex.swift @@ -0,0 +1,368 @@ +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(_ 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(_ 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(_ 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(_ 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") + } +} diff --git a/sdk/swift/Sources/SynorDex/Types.swift b/sdk/swift/Sources/SynorDex/Types.swift new file mode 100644 index 0000000..15771aa --- /dev/null +++ b/sdk/swift/Sources/SynorDex/Types.swift @@ -0,0 +1,498 @@ +import Foundation + +public enum PoolType: String, Codable { + case constantProduct = "constant_product" + case stable = "stable" + case concentrated = "concentrated" +} + +public enum PositionSide: String, Codable { + case long = "long" + case short = "short" +} + +public enum OrderType: String, Codable { + case market = "market" + case limit = "limit" + case stopMarket = "stop_market" + case stopLimit = "stop_limit" + case takeProfit = "take_profit" + case takeProfitLimit = "take_profit_limit" +} + +public enum MarginType: String, Codable { + case cross = "cross" + case isolated = "isolated" +} + +public enum TimeInForce: String, Codable { + case GTC, IOC, FOK, GTD +} + +public enum OrderStatus: String, Codable { + case pending = "pending" + case open = "open" + case partiallyFilled = "partially_filled" + case filled = "filled" + case cancelled = "cancelled" + case expired = "expired" +} + +public struct Token: Codable { + public let address: String + public let symbol: String + public let name: String + public let decimals: Int + public let totalSupply: String + public let priceUsd: Double? + public let logoUrl: String? + public let verified: Bool + + enum CodingKeys: String, CodingKey { + case address, symbol, name, decimals, verified + case totalSupply = "total_supply" + case priceUsd = "price_usd" + case logoUrl = "logo_url" + } +} + +public struct Pool: Codable { + public let id: String + public let tokenA: Token + public let tokenB: Token + public let poolType: PoolType + public let reserveA: String + public let reserveB: String + public let fee: Double + public let tvlUsd: Double + public let volume24h: Double + public let apr: Double + public let lpTokenAddress: String + public let tickSpacing: Int? + public let sqrtPrice: String? + + enum CodingKeys: String, CodingKey { + case id, fee, apr + case tokenA = "token_a" + case tokenB = "token_b" + case poolType = "pool_type" + case reserveA = "reserve_a" + case reserveB = "reserve_b" + case tvlUsd = "tvl_usd" + case volume24h = "volume_24h" + case lpTokenAddress = "lp_token_address" + case tickSpacing = "tick_spacing" + case sqrtPrice = "sqrt_price" + } +} + +public struct PoolFilter { + public var tokens: [String]? + public var minTvl: Double? + public var minVolume24h: Double? + public var verified: Bool? + public var limit: Int? + public var offset: Int? + + public init() {} +} + +public struct Quote: Codable { + public let tokenIn: String + public let tokenOut: String + public let amountIn: String + public let amountOut: String + public let priceImpact: Double + public let route: [String] + public let fee: String + public let minimumReceived: String + public let expiresAt: Int + + enum CodingKeys: String, CodingKey { + case route, fee + case tokenIn = "token_in" + case tokenOut = "token_out" + case amountIn = "amount_in" + case amountOut = "amount_out" + case priceImpact = "price_impact" + case minimumReceived = "minimum_received" + case expiresAt = "expires_at" + } +} + +public struct QuoteParams { + public let tokenIn: String + public let tokenOut: String + public let amountIn: UInt64 + public let slippage: Double? + + public init(tokenIn: String, tokenOut: String, amountIn: UInt64, slippage: Double? = nil) { + self.tokenIn = tokenIn + self.tokenOut = tokenOut + self.amountIn = amountIn + self.slippage = slippage + } +} + +public struct SwapParams { + public let tokenIn: String + public let tokenOut: String + public let amountIn: UInt64 + public let minAmountOut: UInt64 + public let deadline: Int? + public let recipient: String? + + public init(tokenIn: String, tokenOut: String, amountIn: UInt64, minAmountOut: UInt64, deadline: Int? = nil, recipient: String? = nil) { + self.tokenIn = tokenIn + self.tokenOut = tokenOut + self.amountIn = amountIn + self.minAmountOut = minAmountOut + self.deadline = deadline + self.recipient = recipient + } +} + +public struct SwapResult: Codable { + public let transactionHash: String + public let amountIn: String + public let amountOut: String + public let effectivePrice: Double + public let feePaid: String + public let route: [String] + + enum CodingKeys: String, CodingKey { + case route + case transactionHash = "transaction_hash" + case amountIn = "amount_in" + case amountOut = "amount_out" + case effectivePrice = "effective_price" + case feePaid = "fee_paid" + } +} + +public struct AddLiquidityParams { + public let tokenA: String + public let tokenB: String + public let amountA: UInt64 + public let amountB: UInt64 + public let minAmountA: UInt64? + public let minAmountB: UInt64? + public let deadline: Int? + + public init(tokenA: String, tokenB: String, amountA: UInt64, amountB: UInt64, minAmountA: UInt64? = nil, minAmountB: UInt64? = nil, deadline: Int? = nil) { + self.tokenA = tokenA + self.tokenB = tokenB + self.amountA = amountA + self.amountB = amountB + self.minAmountA = minAmountA + self.minAmountB = minAmountB + self.deadline = deadline + } +} + +public struct RemoveLiquidityParams { + public let pool: String + public let lpAmount: UInt64 + public let minAmountA: UInt64? + public let minAmountB: UInt64? + public let deadline: Int? + + public init(pool: String, lpAmount: UInt64, minAmountA: UInt64? = nil, minAmountB: UInt64? = nil, deadline: Int? = nil) { + self.pool = pool + self.lpAmount = lpAmount + self.minAmountA = minAmountA + self.minAmountB = minAmountB + self.deadline = deadline + } +} + +public struct LiquidityResult: Codable { + public let transactionHash: String + public let amountA: String + public let amountB: String + public let lpTokens: String + public let poolShare: Double + + enum CodingKeys: String, CodingKey { + case transactionHash = "transaction_hash" + case amountA = "amount_a" + case amountB = "amount_b" + case lpTokens = "lp_tokens" + case poolShare = "pool_share" + } +} + +public struct LPPosition: Codable { + public let poolId: String + public let lpTokens: String + public let tokenAAmount: String + public let tokenBAmount: String + public let valueUsd: Double + public let unclaimedFeesA: String + public let unclaimedFeesB: String + public let impermanentLoss: Double + + enum CodingKeys: String, CodingKey { + case poolId = "pool_id" + case lpTokens = "lp_tokens" + case tokenAAmount = "token_a_amount" + case tokenBAmount = "token_b_amount" + case valueUsd = "value_usd" + case unclaimedFeesA = "unclaimed_fees_a" + case unclaimedFeesB = "unclaimed_fees_b" + case impermanentLoss = "impermanent_loss" + } +} + +public struct PerpMarket: Codable { + public let symbol: String + public let baseAsset: String + public let quoteAsset: String + public let indexPrice: Double + public let markPrice: Double + public let fundingRate: Double + public let nextFundingTime: Int + public let openInterest: String + public let volume24h: Double + public let maxLeverage: Int + + enum CodingKeys: String, CodingKey { + case symbol + case baseAsset = "base_asset" + case quoteAsset = "quote_asset" + case indexPrice = "index_price" + case markPrice = "mark_price" + case fundingRate = "funding_rate" + case nextFundingTime = "next_funding_time" + case openInterest = "open_interest" + case volume24h = "volume_24h" + case maxLeverage = "max_leverage" + } +} + +public struct OpenPositionParams { + public let market: String + public let side: PositionSide + public let size: UInt64 + public let leverage: Int + public let orderType: OrderType + public let limitPrice: Double? + public let stopLoss: Double? + public let takeProfit: Double? + public let marginType: MarginType + public let reduceOnly: Bool + + public init(market: String, side: PositionSide, size: UInt64, leverage: Int, orderType: OrderType, limitPrice: Double? = nil, stopLoss: Double? = nil, takeProfit: Double? = nil, marginType: MarginType = .cross, reduceOnly: Bool = false) { + self.market = market + self.side = side + self.size = size + self.leverage = leverage + self.orderType = orderType + self.limitPrice = limitPrice + self.stopLoss = stopLoss + self.takeProfit = takeProfit + self.marginType = marginType + self.reduceOnly = reduceOnly + } +} + +public struct ClosePositionParams { + public let market: String + public let size: UInt64? + public let orderType: OrderType + public let limitPrice: Double? + + public init(market: String, size: UInt64? = nil, orderType: OrderType = .market, limitPrice: Double? = nil) { + self.market = market + self.size = size + self.orderType = orderType + self.limitPrice = limitPrice + } +} + +public struct PerpPosition: Codable { + public let id: String + public let market: String + public let side: PositionSide + public let size: String + public let entryPrice: Double + public let markPrice: Double + public let liquidationPrice: Double + public let margin: String + public let leverage: Int + public let unrealizedPnl: String + + enum CodingKeys: String, CodingKey { + case id, market, side, size, margin, leverage + case entryPrice = "entry_price" + case markPrice = "mark_price" + case liquidationPrice = "liquidation_price" + case unrealizedPnl = "unrealized_pnl" + } +} + +public struct PerpOrder: Codable { + public let id: String + public let market: String + public let side: PositionSide + public let orderType: OrderType + public let size: String + public let price: Double? + public let status: OrderStatus + + enum CodingKeys: String, CodingKey { + case id, market, side, size, price, status + case orderType = "order_type" + } +} + +public struct FundingPayment: Codable { + public let market: String + public let amount: String + public let rate: Double + public let positionSize: String + public let timestamp: Int + + enum CodingKeys: String, CodingKey { + case market, amount, rate, timestamp + case positionSize = "position_size" + } +} + +public struct OrderBookEntry: Codable { + public let price: Double + public let size: String + public let orders: Int +} + +public struct OrderBook: Codable { + public let market: String + public let bids: [OrderBookEntry] + public let asks: [OrderBookEntry] + public let timestamp: Int +} + +public struct LimitOrderParams { + public let market: String + public let side: String + public let price: Double + public let size: UInt64 + public let timeInForce: TimeInForce + public let postOnly: Bool + + public init(market: String, side: String, price: Double, size: UInt64, timeInForce: TimeInForce = .GTC, postOnly: Bool = false) { + self.market = market + self.side = side + self.price = price + self.size = size + self.timeInForce = timeInForce + self.postOnly = postOnly + } +} + +public struct Order: Codable { + public let id: String + public let market: String + public let side: String + public let price: Double + public let size: String + public let filledSize: String + public let status: OrderStatus + public let timeInForce: TimeInForce + public let postOnly: Bool + + enum CodingKeys: String, CodingKey { + case id, market, side, price, size, status + case filledSize = "filled_size" + case timeInForce = "time_in_force" + case postOnly = "post_only" + } +} + +public struct Farm: Codable { + public let id: String + public let name: String + public let stakeToken: Token + public let rewardTokens: [Token] + public let tvlUsd: Double + public let apr: Double + + enum CodingKeys: String, CodingKey { + case id, name, apr + case stakeToken = "stake_token" + case rewardTokens = "reward_tokens" + case tvlUsd = "tvl_usd" + } +} + +public struct StakeParams { + public let farm: String + public let amount: UInt64 + + public init(farm: String, amount: UInt64) { + self.farm = farm + self.amount = amount + } +} + +public struct FarmPosition: Codable { + public let farmId: String + public let stakedAmount: String + public let pendingRewards: [String] + public let stakedAt: Int + + enum CodingKeys: String, CodingKey { + case farmId = "farm_id" + case stakedAmount = "staked_amount" + case pendingRewards = "pending_rewards" + case stakedAt = "staked_at" + } +} + +public struct OHLCV: Codable { + public let timestamp: Int + public let open: Double + public let high: Double + public let low: Double + public let close: Double + public let volume: Double +} + +public struct TradeHistory: Codable { + public let id: String + public let market: String + public let side: String + public let price: Double + public let size: String + public let timestamp: Int + public let maker: String + public let taker: String +} + +public struct VolumeStats: Codable { + public let volume24h: Double + public let volume7d: Double + public let volume30d: Double + public let trades24h: Int + + enum CodingKeys: String, CodingKey { + case trades24h = "trades_24h" + case volume24h = "volume_24h" + case volume7d = "volume_7d" + case volume30d = "volume_30d" + } +} + +public struct TVLStats: Codable { + public let totalTvl: Double + public let poolsTvl: Double + public let farmsTvl: Double + public let perpsTvl: Double + + enum CodingKeys: String, CodingKey { + case totalTvl = "total_tvl" + case poolsTvl = "pools_tvl" + case farmsTvl = "farms_tvl" + case perpsTvl = "perps_tvl" + } +}