feat(sdk): implement DEX SDK with perpetual futures for all 12 languages
Complete decentralized exchange client implementation featuring: - AMM swaps (constant product, stable, concentrated liquidity) - Liquidity provision with impermanent loss tracking - Perpetual futures (up to 100x leverage, funding rates, liquidation) - Order books with limit orders (GTC, IOC, FOK, GTD) - Yield farming & staking with reward claiming - Real-time WebSocket subscriptions - Analytics (OHLCV, trade history, volume, TVL) Languages: JS/TS, Python, Go, Rust, Java, Kotlin, Swift, Flutter, C, C++, C#, Ruby
This commit is contained in:
parent
9478bc24e3
commit
e7dc8f70a0
32 changed files with 10295 additions and 0 deletions
359
sdk/c/include/synor/dex.h
Normal file
359
sdk/c/include/synor/dex.h
Normal file
|
|
@ -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 <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#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 */
|
||||
484
sdk/cpp/include/synor/dex.hpp
Normal file
484
sdk/cpp/include/synor/dex.hpp
Normal file
|
|
@ -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 <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
#include <future>
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
|
||||
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<double> price_usd;
|
||||
std::optional<std::string> 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<std::vector<std::string>> tokens;
|
||||
std::optional<double> min_tvl;
|
||||
std::optional<double> min_volume_24h;
|
||||
std::optional<bool> verified;
|
||||
std::optional<int> limit;
|
||||
std::optional<int> offset;
|
||||
};
|
||||
|
||||
struct Quote {
|
||||
std::string token_in;
|
||||
std::string token_out;
|
||||
std::string amount_in;
|
||||
std::string amount_out;
|
||||
double price_impact;
|
||||
std::vector<std::string> 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<double> slippage;
|
||||
};
|
||||
|
||||
struct SwapParams {
|
||||
std::string token_in;
|
||||
std::string token_out;
|
||||
std::string amount_in;
|
||||
std::string min_amount_out;
|
||||
std::optional<int64_t> deadline;
|
||||
std::optional<std::string> recipient;
|
||||
};
|
||||
|
||||
struct SwapResult {
|
||||
std::string transaction_hash;
|
||||
std::string amount_in;
|
||||
std::string amount_out;
|
||||
double effective_price;
|
||||
std::string fee_paid;
|
||||
std::vector<std::string> route;
|
||||
};
|
||||
|
||||
struct AddLiquidityParams {
|
||||
std::string token_a;
|
||||
std::string token_b;
|
||||
std::string amount_a;
|
||||
std::string amount_b;
|
||||
std::optional<std::string> min_amount_a;
|
||||
std::optional<std::string> min_amount_b;
|
||||
std::optional<int64_t> deadline;
|
||||
};
|
||||
|
||||
struct RemoveLiquidityParams {
|
||||
std::string pool;
|
||||
std::string lp_amount;
|
||||
std::optional<std::string> min_amount_a;
|
||||
std::optional<std::string> min_amount_b;
|
||||
std::optional<int64_t> 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<double> limit_price;
|
||||
std::optional<double> stop_loss;
|
||||
std::optional<double> take_profit;
|
||||
MarginType margin_type = MarginType::Cross;
|
||||
bool reduce_only = false;
|
||||
};
|
||||
|
||||
struct ClosePositionParams {
|
||||
std::string market;
|
||||
std::optional<std::string> size;
|
||||
OrderType order_type = OrderType::Market;
|
||||
std::optional<double> 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<double> 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<OrderBookEntry> bids;
|
||||
std::vector<OrderBookEntry> 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<Token> 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<std::string> 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<Token> get_token(const std::string& address);
|
||||
std::future<std::vector<Token>> list_tokens();
|
||||
std::future<std::vector<Token>> search_tokens(const std::string& query);
|
||||
|
||||
// Pool Operations
|
||||
std::future<Pool> get_pool(const std::string& token_a, const std::string& token_b);
|
||||
std::future<Pool> get_pool_by_id(const std::string& pool_id);
|
||||
std::future<std::vector<Pool>> list_pools(const std::optional<PoolFilter>& filter = std::nullopt);
|
||||
|
||||
// Swap Operations
|
||||
std::future<Quote> get_quote(const QuoteParams& params);
|
||||
std::future<SwapResult> swap(const SwapParams& params);
|
||||
|
||||
// Liquidity Operations
|
||||
std::future<LiquidityResult> add_liquidity(const AddLiquidityParams& params);
|
||||
std::future<LiquidityResult> remove_liquidity(const RemoveLiquidityParams& params);
|
||||
std::future<std::vector<LPPosition>> get_my_positions();
|
||||
|
||||
// Analytics
|
||||
std::future<std::vector<OHLCV>> get_price_history(const std::string& pair,
|
||||
const std::string& interval,
|
||||
int limit = 100);
|
||||
std::future<std::vector<TradeHistory>> get_trade_history(const std::string& pair,
|
||||
int limit = 50);
|
||||
std::future<VolumeStats> get_volume_stats();
|
||||
std::future<TVLStats> get_tvl();
|
||||
|
||||
// Lifecycle
|
||||
std::future<bool> health_check();
|
||||
void close();
|
||||
bool is_closed() const;
|
||||
|
||||
// Sub-clients
|
||||
PerpsClient& perps();
|
||||
OrderBookClient& orderbook();
|
||||
FarmsClient& farms();
|
||||
|
||||
private:
|
||||
std::unique_ptr<SynorDexImpl> impl_;
|
||||
};
|
||||
|
||||
// Perpetuals Client
|
||||
class PerpsClient {
|
||||
public:
|
||||
explicit PerpsClient(SynorDex& dex);
|
||||
~PerpsClient();
|
||||
|
||||
std::future<std::vector<PerpMarket>> list_markets();
|
||||
std::future<PerpMarket> get_market(const std::string& symbol);
|
||||
std::future<PerpPosition> open_position(const OpenPositionParams& params);
|
||||
std::future<PerpPosition> close_position(const ClosePositionParams& params);
|
||||
std::future<std::vector<PerpPosition>> get_positions();
|
||||
std::future<std::vector<PerpOrder>> get_orders();
|
||||
std::future<void> cancel_order(const std::string& order_id);
|
||||
std::future<int> cancel_all_orders(const std::optional<std::string>& market = std::nullopt);
|
||||
std::future<std::vector<FundingPayment>> get_funding_history(const std::string& market,
|
||||
int limit = 100);
|
||||
std::future<FundingRateInfo> get_funding_rate(const std::string& market);
|
||||
|
||||
private:
|
||||
SynorDex& dex_;
|
||||
};
|
||||
|
||||
// OrderBook Client
|
||||
class OrderBookClient {
|
||||
public:
|
||||
explicit OrderBookClient(SynorDex& dex);
|
||||
~OrderBookClient();
|
||||
|
||||
std::future<OrderBook> get_order_book(const std::string& market, int depth = 20);
|
||||
std::future<Order> place_limit_order(const LimitOrderParams& params);
|
||||
std::future<void> cancel_order(const std::string& order_id);
|
||||
std::future<std::vector<Order>> get_open_orders(const std::optional<std::string>& market = std::nullopt);
|
||||
std::future<std::vector<Order>> get_order_history(int limit = 50);
|
||||
|
||||
private:
|
||||
SynorDex& dex_;
|
||||
};
|
||||
|
||||
// Farms Client
|
||||
class FarmsClient {
|
||||
public:
|
||||
explicit FarmsClient(SynorDex& dex);
|
||||
~FarmsClient();
|
||||
|
||||
std::future<std::vector<Farm>> list_farms();
|
||||
std::future<Farm> get_farm(const std::string& farm_id);
|
||||
std::future<FarmPosition> stake(const StakeParams& params);
|
||||
std::future<FarmPosition> unstake(const std::string& farm, const std::string& amount);
|
||||
std::future<ClaimRewardsResult> claim_rewards(const std::string& farm);
|
||||
std::future<std::vector<FarmPosition>> get_my_farm_positions();
|
||||
|
||||
private:
|
||||
SynorDex& dex_;
|
||||
};
|
||||
|
||||
} // namespace dex
|
||||
} // namespace synor
|
||||
|
||||
#endif // SYNOR_DEX_HPP
|
||||
387
sdk/csharp/src/Synor.Dex/SynorDex.cs
Normal file
387
sdk/csharp/src/Synor.Dex/SynorDex.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
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<Token> GetTokenAsync(string address)
|
||||
=> GetAsync<Token>($"/tokens/{address}");
|
||||
|
||||
public Task<List<Token>> ListTokensAsync()
|
||||
=> GetAsync<List<Token>>("/tokens");
|
||||
|
||||
public Task<List<Token>> SearchTokensAsync(string query)
|
||||
=> GetAsync<List<Token>>($"/tokens/search?q={HttpUtility.UrlEncode(query)}");
|
||||
|
||||
// Pool Operations
|
||||
|
||||
public Task<Pool> GetPoolAsync(string tokenA, string tokenB)
|
||||
=> GetAsync<Pool>($"/pools/{tokenA}/{tokenB}");
|
||||
|
||||
public Task<Pool> GetPoolByIdAsync(string poolId)
|
||||
=> GetAsync<Pool>($"/pools/{poolId}");
|
||||
|
||||
public Task<List<Pool>> ListPoolsAsync(PoolFilter filter = null)
|
||||
{
|
||||
var parameters = new List<string>();
|
||||
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<List<Pool>>(path);
|
||||
}
|
||||
|
||||
// Swap Operations
|
||||
|
||||
public Task<Quote> GetQuoteAsync(QuoteParams prms)
|
||||
=> PostAsync<Quote>("/swap/quote", new
|
||||
{
|
||||
token_in = prms.TokenIn,
|
||||
token_out = prms.TokenOut,
|
||||
amount_in = prms.AmountIn.ToString(),
|
||||
slippage = prms.Slippage ?? 0.005
|
||||
});
|
||||
|
||||
public Task<SwapResult> SwapAsync(SwapParams prms)
|
||||
{
|
||||
var deadline = prms.Deadline ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 1200;
|
||||
return PostAsync<SwapResult>("/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<LiquidityResult> AddLiquidityAsync(AddLiquidityParams prms)
|
||||
{
|
||||
var deadline = prms.Deadline ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 1200;
|
||||
return PostAsync<LiquidityResult>("/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<LiquidityResult> RemoveLiquidityAsync(RemoveLiquidityParams prms)
|
||||
{
|
||||
var deadline = prms.Deadline ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 1200;
|
||||
return PostAsync<LiquidityResult>("/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<List<LPPosition>> GetMyPositionsAsync()
|
||||
=> GetAsync<List<LPPosition>>("/liquidity/positions");
|
||||
|
||||
// Analytics
|
||||
|
||||
public Task<List<OHLCV>> GetPriceHistoryAsync(string pair, string interval, int limit = 100)
|
||||
=> GetAsync<List<OHLCV>>($"/analytics/candles/{pair}?interval={interval}&limit={limit}");
|
||||
|
||||
public Task<List<TradeHistory>> GetTradeHistoryAsync(string pair, int limit = 50)
|
||||
=> GetAsync<List<TradeHistory>>($"/analytics/trades/{pair}?limit={limit}");
|
||||
|
||||
public Task<VolumeStats> GetVolumeStatsAsync()
|
||||
=> GetAsync<VolumeStats>("/analytics/volume");
|
||||
|
||||
public Task<TVLStats> GetTVLAsync()
|
||||
=> GetAsync<TVLStats>("/analytics/tvl");
|
||||
|
||||
// Lifecycle
|
||||
|
||||
public async Task<bool> HealthCheckAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await GetAsync<HealthResponse>("/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<T> GetAsync<T>(string path)
|
||||
{
|
||||
CheckClosed();
|
||||
var response = await _client.GetAsync(path);
|
||||
return await HandleResponseAsync<T>(response);
|
||||
}
|
||||
|
||||
internal async Task<T> PostAsync<T>(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<T>(response);
|
||||
}
|
||||
|
||||
internal async Task<T> DeleteAsync<T>(string path)
|
||||
{
|
||||
CheckClosed();
|
||||
var response = await _client.DeleteAsync(path);
|
||||
return await HandleResponseAsync<T>(response);
|
||||
}
|
||||
|
||||
private async Task<T> HandleResponseAsync<T>(HttpResponseMessage response)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
ErrorResponse error = null;
|
||||
try { error = JsonSerializer.Deserialize<ErrorResponse>(body, _jsonOptions); } catch { }
|
||||
throw new DexException(
|
||||
error?.Message ?? $"HTTP {(int)response.StatusCode}",
|
||||
error?.Code,
|
||||
(int)response.StatusCode);
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<T>(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; } }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DEX Configuration
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DEX Exception
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perpetual futures sub-client
|
||||
/// </summary>
|
||||
public class PerpsClient
|
||||
{
|
||||
private readonly SynorDex _dex;
|
||||
internal PerpsClient(SynorDex dex) => _dex = dex;
|
||||
|
||||
public Task<List<PerpMarket>> ListMarketsAsync()
|
||||
=> _dex.GetAsync<List<PerpMarket>>("/perps/markets");
|
||||
|
||||
public Task<PerpMarket> GetMarketAsync(string symbol)
|
||||
=> _dex.GetAsync<PerpMarket>($"/perps/markets/{symbol}");
|
||||
|
||||
public Task<PerpPosition> OpenPositionAsync(OpenPositionParams prms)
|
||||
=> _dex.PostAsync<PerpPosition>("/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<PerpPosition> ClosePositionAsync(ClosePositionParams prms)
|
||||
=> _dex.PostAsync<PerpPosition>("/perps/positions/close", new
|
||||
{
|
||||
market = prms.Market,
|
||||
size = prms.Size?.ToString(),
|
||||
order_type = prms.OrderType.ToString().ToLower(),
|
||||
limit_price = prms.LimitPrice
|
||||
});
|
||||
|
||||
public Task<List<PerpPosition>> GetPositionsAsync()
|
||||
=> _dex.GetAsync<List<PerpPosition>>("/perps/positions");
|
||||
|
||||
public Task<List<PerpOrder>> GetOrdersAsync()
|
||||
=> _dex.GetAsync<List<PerpOrder>>("/perps/orders");
|
||||
|
||||
public Task<List<FundingPayment>> GetFundingHistoryAsync(string market, int limit = 100)
|
||||
=> _dex.GetAsync<List<FundingPayment>>($"/perps/funding/{market}?limit={limit}");
|
||||
|
||||
public Task<FundingRateInfo> GetFundingRateAsync(string market)
|
||||
=> _dex.GetAsync<FundingRateInfo>($"/perps/funding/{market}/current");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Order book sub-client
|
||||
/// </summary>
|
||||
public class OrderBookClient
|
||||
{
|
||||
private readonly SynorDex _dex;
|
||||
internal OrderBookClient(SynorDex dex) => _dex = dex;
|
||||
|
||||
public Task<OrderBook> GetOrderBookAsync(string market, int depth = 20)
|
||||
=> _dex.GetAsync<OrderBook>($"/orderbook/{market}?depth={depth}");
|
||||
|
||||
public Task<Order> PlaceLimitOrderAsync(LimitOrderParams prms)
|
||||
=> _dex.PostAsync<Order>("/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<List<Order>> GetOpenOrdersAsync(string market = null)
|
||||
{
|
||||
var path = market != null ? $"/orderbook/orders?market={market}" : "/orderbook/orders";
|
||||
return _dex.GetAsync<List<Order>>(path);
|
||||
}
|
||||
|
||||
public Task<List<Order>> GetOrderHistoryAsync(int limit = 50)
|
||||
=> _dex.GetAsync<List<Order>>($"/orderbook/orders/history?limit={limit}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Farms sub-client
|
||||
/// </summary>
|
||||
public class FarmsClient
|
||||
{
|
||||
private readonly SynorDex _dex;
|
||||
internal FarmsClient(SynorDex dex) => _dex = dex;
|
||||
|
||||
public Task<List<Farm>> ListFarmsAsync()
|
||||
=> _dex.GetAsync<List<Farm>>("/farms");
|
||||
|
||||
public Task<Farm> GetFarmAsync(string farmId)
|
||||
=> _dex.GetAsync<Farm>($"/farms/{farmId}");
|
||||
|
||||
public Task<FarmPosition> StakeAsync(StakeParams prms)
|
||||
=> _dex.PostAsync<FarmPosition>("/farms/stake", new
|
||||
{
|
||||
farm = prms.Farm,
|
||||
amount = prms.Amount.ToString()
|
||||
});
|
||||
|
||||
public Task<FarmPosition> UnstakeAsync(string farm, System.Numerics.BigInteger amount)
|
||||
=> _dex.PostAsync<FarmPosition>("/farms/unstake", new
|
||||
{
|
||||
farm,
|
||||
amount = amount.ToString()
|
||||
});
|
||||
|
||||
public Task<ClaimRewardsResult> ClaimRewardsAsync(string farm)
|
||||
=> _dex.PostAsync<ClaimRewardsResult>("/farms/claim", new { farm });
|
||||
|
||||
public Task<List<FarmPosition>> GetMyFarmPositionsAsync()
|
||||
=> _dex.GetAsync<List<FarmPosition>>("/farms/positions");
|
||||
}
|
||||
}
|
||||
315
sdk/csharp/src/Synor.Dex/Types.cs
Normal file
315
sdk/csharp/src/Synor.Dex/Types.cs
Normal file
|
|
@ -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<string> 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<string> 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<string> 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<OrderBookEntry> Bids { get; set; }
|
||||
public List<OrderBookEntry> 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<Token> 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<string> 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; }
|
||||
}
|
||||
}
|
||||
355
sdk/flutter/lib/src/dex/synor_dex.dart
Normal file
355
sdk/flutter/lib/src/dex/synor_dex.dart
Normal file
|
|
@ -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<Token> getToken(String address) async {
|
||||
return _get('/tokens/$address', Token.fromJson);
|
||||
}
|
||||
|
||||
Future<List<Token>> listTokens() async {
|
||||
return _getList('/tokens', Token.fromJson);
|
||||
}
|
||||
|
||||
Future<List<Token>> searchTokens(String query) async {
|
||||
return _getList('/tokens/search?q=${Uri.encodeComponent(query)}', Token.fromJson);
|
||||
}
|
||||
|
||||
// Pool Operations
|
||||
|
||||
Future<Pool> getPool(String tokenA, String tokenB) async {
|
||||
return _get('/pools/$tokenA/$tokenB', Pool.fromJson);
|
||||
}
|
||||
|
||||
Future<Pool> getPoolById(String poolId) async {
|
||||
return _get('/pools/$poolId', Pool.fromJson);
|
||||
}
|
||||
|
||||
Future<List<Pool>> listPools({PoolFilter? filter}) async {
|
||||
final params = <String>[];
|
||||
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<Quote> 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<SwapResult> 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<LiquidityResult> 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<LiquidityResult> 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<List<LPPosition>> getMyPositions() async {
|
||||
return _getList('/liquidity/positions', LPPosition.fromJson);
|
||||
}
|
||||
|
||||
// Analytics
|
||||
|
||||
Future<List<OHLCV>> getPriceHistory(String pair, String interval, {int limit = 100}) async {
|
||||
return _getList('/analytics/candles/$pair?interval=$interval&limit=$limit', OHLCV.fromJson);
|
||||
}
|
||||
|
||||
Future<List<TradeHistory>> getTradeHistory(String pair, {int limit = 50}) async {
|
||||
return _getList('/analytics/trades/$pair?limit=$limit', TradeHistory.fromJson);
|
||||
}
|
||||
|
||||
Future<VolumeStats> getVolumeStats() async {
|
||||
return _get('/analytics/volume', VolumeStats.fromJson);
|
||||
}
|
||||
|
||||
Future<TVLStats> getTVL() async {
|
||||
return _get('/analytics/tvl', TVLStats.fromJson);
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
|
||||
Future<bool> 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<T> _get<T>(String path, T Function(Map<String, dynamic>) fromJson) async {
|
||||
_checkClosed();
|
||||
final response = await _client.get(
|
||||
Uri.parse('${config.endpoint}$path'),
|
||||
headers: _headers,
|
||||
);
|
||||
return _handleResponse(response, fromJson);
|
||||
}
|
||||
|
||||
Future<List<T>> _getList<T>(String path, T Function(Map<String, dynamic>) 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<String, dynamic>)).toList();
|
||||
}
|
||||
|
||||
Future<T> _post<T>(String path, Map<String, dynamic> body, T Function(Map<String, dynamic>) fromJson) async {
|
||||
_checkClosed();
|
||||
final response = await _client.post(
|
||||
Uri.parse('${config.endpoint}$path'),
|
||||
headers: _headers,
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
return _handleResponse(response, fromJson);
|
||||
}
|
||||
|
||||
Future<T> _delete<T>(String path, T Function(Map<String, dynamic>) fromJson) async {
|
||||
_checkClosed();
|
||||
final response = await _client.delete(
|
||||
Uri.parse('${config.endpoint}$path'),
|
||||
headers: _headers,
|
||||
);
|
||||
return _handleResponse(response, fromJson);
|
||||
}
|
||||
|
||||
Map<String, String> get _headers => {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ${config.apiKey}',
|
||||
'X-SDK-Version': 'flutter/0.1.0',
|
||||
};
|
||||
|
||||
T _handleResponse<T>(http.Response response, T Function(Map<String, dynamic>) fromJson) {
|
||||
_checkResponse(response);
|
||||
final json = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return fromJson(json);
|
||||
}
|
||||
|
||||
void _checkResponse(http.Response response) {
|
||||
if (response.statusCode >= 400) {
|
||||
Map<String, dynamic>? error;
|
||||
try {
|
||||
error = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
} 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<List<PerpMarket>> listMarkets() async {
|
||||
return _dex._getList('/perps/markets', PerpMarket.fromJson);
|
||||
}
|
||||
|
||||
Future<PerpMarket> getMarket(String symbol) async {
|
||||
return _dex._get('/perps/markets/$symbol', PerpMarket.fromJson);
|
||||
}
|
||||
|
||||
Future<PerpPosition> 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<PerpPosition> 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<List<PerpPosition>> getPositions() async {
|
||||
return _dex._getList('/perps/positions', PerpPosition.fromJson);
|
||||
}
|
||||
|
||||
Future<List<PerpOrder>> getOrders() async {
|
||||
return _dex._getList('/perps/orders', PerpOrder.fromJson);
|
||||
}
|
||||
|
||||
Future<List<FundingPayment>> getFundingHistory(String market, {int limit = 100}) async {
|
||||
return _dex._getList('/perps/funding/$market?limit=$limit', FundingPayment.fromJson);
|
||||
}
|
||||
|
||||
Future<FundingRateInfo> 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<OrderBook> getOrderBook(String market, {int depth = 20}) async {
|
||||
return _dex._get('/orderbook/$market?depth=$depth', OrderBook.fromJson);
|
||||
}
|
||||
|
||||
Future<Order> 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<List<Order>> getOpenOrders({String? market}) async {
|
||||
final path = market != null ? '/orderbook/orders?market=$market' : '/orderbook/orders';
|
||||
return _dex._getList(path, Order.fromJson);
|
||||
}
|
||||
|
||||
Future<List<Order>> 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<List<Farm>> listFarms() async {
|
||||
return _dex._getList('/farms', Farm.fromJson);
|
||||
}
|
||||
|
||||
Future<Farm> getFarm(String farmId) async {
|
||||
return _dex._get('/farms/$farmId', Farm.fromJson);
|
||||
}
|
||||
|
||||
Future<FarmPosition> stake(StakeParams params) async {
|
||||
return _dex._post('/farms/stake', {
|
||||
'farm': params.farm,
|
||||
'amount': params.amount.toString(),
|
||||
}, FarmPosition.fromJson);
|
||||
}
|
||||
|
||||
Future<FarmPosition> unstake(String farm, BigInt amount) async {
|
||||
return _dex._post('/farms/unstake', {
|
||||
'farm': farm,
|
||||
'amount': amount.toString(),
|
||||
}, FarmPosition.fromJson);
|
||||
}
|
||||
|
||||
Future<ClaimRewardsResult> claimRewards(String farm) async {
|
||||
return _dex._post('/farms/claim', {'farm': farm}, ClaimRewardsResult.fromJson);
|
||||
}
|
||||
|
||||
Future<List<FarmPosition>> getMyFarmPositions() async {
|
||||
return _dex._getList('/farms/positions', FarmPosition.fromJson);
|
||||
}
|
||||
}
|
||||
776
sdk/flutter/lib/src/dex/types.dart
Normal file
776
sdk/flutter/lib/src/dex/types.dart
Normal file
|
|
@ -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<String, dynamic> 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<String, dynamic> json) => Pool(
|
||||
id: json['id'] as String,
|
||||
tokenA: Token.fromJson(json['token_a'] as Map<String, dynamic>),
|
||||
tokenB: Token.fromJson(json['token_b'] as Map<String, dynamic>),
|
||||
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<String>? 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<String> 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<String, dynamic> 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<String>(),
|
||||
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<String> route;
|
||||
|
||||
SwapResult({
|
||||
required this.transactionHash,
|
||||
required this.amountIn,
|
||||
required this.amountOut,
|
||||
required this.effectivePrice,
|
||||
required this.feePaid,
|
||||
required this.route,
|
||||
});
|
||||
|
||||
factory SwapResult.fromJson(Map<String, dynamic> 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<String>(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<OrderBookEntry> bids;
|
||||
final List<OrderBookEntry> asks;
|
||||
final int timestamp;
|
||||
|
||||
OrderBook({required this.market, required this.bids, required this.asks, required this.timestamp});
|
||||
|
||||
factory OrderBook.fromJson(Map<String, dynamic> json) => OrderBook(
|
||||
market: json['market'] as String,
|
||||
bids: (json['bids'] as List).map((e) => OrderBookEntry.fromJson(e as Map<String, dynamic>)).toList(),
|
||||
asks: (json['asks'] as List).map((e) => OrderBookEntry.fromJson(e as Map<String, dynamic>)).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<String, dynamic> 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<Token> 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<String, dynamic> json) => Farm(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
stakeToken: Token.fromJson(json['stake_token'] as Map<String, dynamic>),
|
||||
rewardTokens: (json['reward_tokens'] as List).map((e) => Token.fromJson(e as Map<String, dynamic>)).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<BigInt> pendingRewards;
|
||||
final int stakedAt;
|
||||
|
||||
FarmPosition({
|
||||
required this.farmId,
|
||||
required this.stakedAmount,
|
||||
required this.pendingRewards,
|
||||
required this.stakedAt,
|
||||
});
|
||||
|
||||
factory FarmPosition.fromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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,
|
||||
);
|
||||
}
|
||||
805
sdk/go/dex/client.go
Normal file
805
sdk/go/dex/client.go
Normal file
|
|
@ -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<<attempt) * time.Second)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
var errResp struct {
|
||||
Message string `json:"message"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
json.Unmarshal(respBody, &errResp)
|
||||
return NewDexError(
|
||||
errResp.Message,
|
||||
errResp.Code,
|
||||
resp.StatusCode,
|
||||
)
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
return json.Unmarshal(respBody, result)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
return lastErr
|
||||
}
|
||||
return NewDexError("Unknown error", "", 0)
|
||||
}
|
||||
|
||||
func (d *SynorDex) subscribe(channel string, params map[string]interface{}, callback func([]byte)) (*Subscription, error) {
|
||||
if err := d.ensureWebSocket(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subscriptionID := uuid.New().String()[:8]
|
||||
|
||||
d.subMu.Lock()
|
||||
d.subscriptions[subscriptionID] = callback
|
||||
d.subMu.Unlock()
|
||||
|
||||
msg := map[string]interface{}{
|
||||
"type": "subscribe",
|
||||
"channel": channel,
|
||||
"subscription_id": subscriptionID,
|
||||
}
|
||||
for k, v := range params {
|
||||
msg[k] = v
|
||||
}
|
||||
|
||||
d.wsMu.Lock()
|
||||
err := d.ws.WriteJSON(msg)
|
||||
d.wsMu.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Subscription{
|
||||
ID: subscriptionID,
|
||||
Channel: channel,
|
||||
Cancel: func() {
|
||||
d.subMu.Lock()
|
||||
delete(d.subscriptions, subscriptionID)
|
||||
d.subMu.Unlock()
|
||||
|
||||
d.wsMu.Lock()
|
||||
if d.ws != nil {
|
||||
d.ws.WriteJSON(map[string]interface{}{
|
||||
"type": "unsubscribe",
|
||||
"subscription_id": subscriptionID,
|
||||
})
|
||||
}
|
||||
d.wsMu.Unlock()
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *SynorDex) ensureWebSocket() error {
|
||||
d.wsMu.Lock()
|
||||
defer d.wsMu.Unlock()
|
||||
|
||||
if d.ws != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
conn, _, err := websocket.DefaultDialer.Dial(d.config.WSEndpoint, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.ws = conn
|
||||
|
||||
// Authenticate
|
||||
err = d.ws.WriteJSON(map[string]interface{}{
|
||||
"type": "auth",
|
||||
"api_key": d.config.APIKey,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start message handler
|
||||
go d.handleMessages()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *SynorDex) handleMessages() {
|
||||
for {
|
||||
d.wsMu.Lock()
|
||||
ws := d.ws
|
||||
d.wsMu.Unlock()
|
||||
|
||||
if ws == nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, message, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var msg struct {
|
||||
SubscriptionID string `json:"subscription_id"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(message, &msg); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if msg.SubscriptionID != "" {
|
||||
d.subMu.RLock()
|
||||
callback, ok := d.subscriptions[msg.SubscriptionID]
|
||||
d.subMu.RUnlock()
|
||||
if ok {
|
||||
callback(msg.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PerpsClient handles perpetual futures operations
|
||||
type PerpsClient struct {
|
||||
dex *SynorDex
|
||||
}
|
||||
|
||||
// ListMarkets lists all perpetual markets
|
||||
func (p *PerpsClient) ListMarkets(ctx context.Context) ([]PerpMarket, error) {
|
||||
var markets []PerpMarket
|
||||
err := p.dex.get(ctx, "/perps/markets", &markets)
|
||||
return markets, err
|
||||
}
|
||||
|
||||
// GetMarket gets a specific perpetual market
|
||||
func (p *PerpsClient) GetMarket(ctx context.Context, symbol string) (*PerpMarket, error) {
|
||||
var market PerpMarket
|
||||
err := p.dex.get(ctx, "/perps/markets/"+symbol, &market)
|
||||
return &market, err
|
||||
}
|
||||
|
||||
// OpenPosition opens a perpetual position
|
||||
func (p *PerpsClient) OpenPosition(ctx context.Context, params OpenPositionParams) (*PerpPosition, error) {
|
||||
marginType := params.MarginType
|
||||
if marginType == "" {
|
||||
marginType = MarginTypeCross
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"market": params.Market,
|
||||
"side": string(params.Side),
|
||||
"size": params.Size.String(),
|
||||
"leverage": params.Leverage,
|
||||
"order_type": string(params.OrderType),
|
||||
"margin_type": string(marginType),
|
||||
"reduce_only": params.ReduceOnly,
|
||||
}
|
||||
if params.LimitPrice != nil {
|
||||
body["limit_price"] = *params.LimitPrice
|
||||
}
|
||||
if params.StopLoss != nil {
|
||||
body["stop_loss"] = *params.StopLoss
|
||||
}
|
||||
if params.TakeProfit != nil {
|
||||
body["take_profit"] = *params.TakeProfit
|
||||
}
|
||||
|
||||
var position PerpPosition
|
||||
err := p.dex.post(ctx, "/perps/positions", body, &position)
|
||||
return &position, err
|
||||
}
|
||||
|
||||
// ClosePosition closes a perpetual position
|
||||
func (p *PerpsClient) ClosePosition(ctx context.Context, params ClosePositionParams) (*PerpPosition, error) {
|
||||
body := map[string]interface{}{
|
||||
"market": params.Market,
|
||||
"order_type": string(params.OrderType),
|
||||
}
|
||||
if params.Size != nil {
|
||||
body["size"] = params.Size.String()
|
||||
}
|
||||
if params.LimitPrice != nil {
|
||||
body["limit_price"] = *params.LimitPrice
|
||||
}
|
||||
|
||||
var position PerpPosition
|
||||
err := p.dex.post(ctx, "/perps/positions/close", body, &position)
|
||||
return &position, err
|
||||
}
|
||||
|
||||
// ModifyPosition modifies a perpetual position
|
||||
func (p *PerpsClient) ModifyPosition(ctx context.Context, params ModifyPositionParams) (*PerpPosition, error) {
|
||||
body := map[string]interface{}{}
|
||||
if params.NewLeverage != nil {
|
||||
body["new_leverage"] = *params.NewLeverage
|
||||
}
|
||||
if params.NewMargin != nil {
|
||||
body["new_margin"] = params.NewMargin.String()
|
||||
}
|
||||
if params.NewStopLoss != nil {
|
||||
body["new_stop_loss"] = *params.NewStopLoss
|
||||
}
|
||||
if params.NewTakeProfit != nil {
|
||||
body["new_take_profit"] = *params.NewTakeProfit
|
||||
}
|
||||
|
||||
var position PerpPosition
|
||||
err := p.dex.post(ctx, "/perps/positions/"+params.PositionID+"/modify", body, &position)
|
||||
return &position, err
|
||||
}
|
||||
|
||||
// GetPositions gets all open positions
|
||||
func (p *PerpsClient) GetPositions(ctx context.Context) ([]PerpPosition, error) {
|
||||
var positions []PerpPosition
|
||||
err := p.dex.get(ctx, "/perps/positions", &positions)
|
||||
return positions, err
|
||||
}
|
||||
|
||||
// GetPosition gets position for a specific market
|
||||
func (p *PerpsClient) GetPosition(ctx context.Context, market string) (*PerpPosition, error) {
|
||||
var position PerpPosition
|
||||
err := p.dex.get(ctx, "/perps/positions/"+market, &position)
|
||||
return &position, err
|
||||
}
|
||||
|
||||
// GetOrders gets all open orders
|
||||
func (p *PerpsClient) GetOrders(ctx context.Context) ([]PerpOrder, error) {
|
||||
var orders []PerpOrder
|
||||
err := p.dex.get(ctx, "/perps/orders", &orders)
|
||||
return orders, err
|
||||
}
|
||||
|
||||
// CancelOrder cancels an order
|
||||
func (p *PerpsClient) CancelOrder(ctx context.Context, orderID string) error {
|
||||
return p.dex.delete(ctx, "/perps/orders/"+orderID, nil)
|
||||
}
|
||||
|
||||
// CancelAllOrders cancels all orders
|
||||
func (p *PerpsClient) CancelAllOrders(ctx context.Context, market *string) (int, error) {
|
||||
path := "/perps/orders"
|
||||
if market != nil {
|
||||
path += "?market=" + *market
|
||||
}
|
||||
var result struct {
|
||||
Cancelled int `json:"cancelled"`
|
||||
}
|
||||
err := p.dex.delete(ctx, path, &result)
|
||||
return result.Cancelled, err
|
||||
}
|
||||
|
||||
// GetFundingHistory gets funding payment history
|
||||
func (p *PerpsClient) GetFundingHistory(ctx context.Context, market string, limit int) ([]FundingPayment, error) {
|
||||
var payments []FundingPayment
|
||||
path := fmt.Sprintf("/perps/funding/%s?limit=%d", market, limit)
|
||||
err := p.dex.get(ctx, path, &payments)
|
||||
return payments, err
|
||||
}
|
||||
|
||||
// GetFundingRate gets current funding rate
|
||||
func (p *PerpsClient) GetFundingRate(ctx context.Context, market string) (*struct {
|
||||
Rate float64 `json:"rate"`
|
||||
NextTime int64 `json:"next_time"`
|
||||
}, error) {
|
||||
var result struct {
|
||||
Rate float64 `json:"rate"`
|
||||
NextTime int64 `json:"next_time"`
|
||||
}
|
||||
err := p.dex.get(ctx, "/perps/funding/"+market+"/current", &result)
|
||||
return &result, err
|
||||
}
|
||||
|
||||
// SubscribePosition subscribes to position updates
|
||||
func (p *PerpsClient) SubscribePosition(callback PositionCallback) (*Subscription, error) {
|
||||
return p.dex.subscribe("position", map[string]interface{}{}, func(data []byte) {
|
||||
var position PerpPosition
|
||||
if err := json.Unmarshal(data, &position); err == nil {
|
||||
callback(position)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// OrderBookClient handles order book operations
|
||||
type OrderBookClient struct {
|
||||
dex *SynorDex
|
||||
}
|
||||
|
||||
// GetOrderBook gets order book for a market
|
||||
func (o *OrderBookClient) GetOrderBook(ctx context.Context, market string, depth int) (*OrderBook, error) {
|
||||
var book OrderBook
|
||||
path := fmt.Sprintf("/orderbook/%s?depth=%d", market, depth)
|
||||
err := o.dex.get(ctx, path, &book)
|
||||
return &book, err
|
||||
}
|
||||
|
||||
// PlaceLimitOrder places a limit order
|
||||
func (o *OrderBookClient) PlaceLimitOrder(ctx context.Context, params LimitOrderParams) (*Order, error) {
|
||||
tif := params.TimeInForce
|
||||
if tif == "" {
|
||||
tif = TimeInForceGTC
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"market": params.Market,
|
||||
"side": params.Side,
|
||||
"price": params.Price,
|
||||
"size": params.Size.String(),
|
||||
"time_in_force": string(tif),
|
||||
"post_only": params.PostOnly,
|
||||
}
|
||||
|
||||
var order Order
|
||||
err := o.dex.post(ctx, "/orderbook/orders", body, &order)
|
||||
return &order, err
|
||||
}
|
||||
|
||||
// CancelOrder cancels an order
|
||||
func (o *OrderBookClient) CancelOrder(ctx context.Context, orderID string) error {
|
||||
return o.dex.delete(ctx, "/orderbook/orders/"+orderID, nil)
|
||||
}
|
||||
|
||||
// GetOpenOrders gets all open orders
|
||||
func (o *OrderBookClient) GetOpenOrders(ctx context.Context, market *string) ([]Order, error) {
|
||||
path := "/orderbook/orders"
|
||||
if market != nil {
|
||||
path += "?market=" + *market
|
||||
}
|
||||
var orders []Order
|
||||
err := o.dex.get(ctx, path, &orders)
|
||||
return orders, err
|
||||
}
|
||||
|
||||
// GetOrderHistory gets order history
|
||||
func (o *OrderBookClient) GetOrderHistory(ctx context.Context, limit int) ([]Order, error) {
|
||||
var orders []Order
|
||||
path := fmt.Sprintf("/orderbook/orders/history?limit=%d", limit)
|
||||
err := o.dex.get(ctx, path, &orders)
|
||||
return orders, err
|
||||
}
|
||||
|
||||
// FarmsClient handles farming operations
|
||||
type FarmsClient struct {
|
||||
dex *SynorDex
|
||||
}
|
||||
|
||||
// ListFarms lists all farms
|
||||
func (f *FarmsClient) ListFarms(ctx context.Context) ([]Farm, error) {
|
||||
var farms []Farm
|
||||
err := f.dex.get(ctx, "/farms", &farms)
|
||||
return farms, err
|
||||
}
|
||||
|
||||
// GetFarm gets a specific farm
|
||||
func (f *FarmsClient) GetFarm(ctx context.Context, farmID string) (*Farm, error) {
|
||||
var farm Farm
|
||||
err := f.dex.get(ctx, "/farms/"+farmID, &farm)
|
||||
return &farm, err
|
||||
}
|
||||
|
||||
// Stake stakes tokens in a farm
|
||||
func (f *FarmsClient) Stake(ctx context.Context, params StakeParams) (*FarmPosition, error) {
|
||||
body := map[string]interface{}{
|
||||
"farm": params.Farm,
|
||||
"amount": params.Amount.String(),
|
||||
}
|
||||
var position FarmPosition
|
||||
err := f.dex.post(ctx, "/farms/stake", body, &position)
|
||||
return &position, err
|
||||
}
|
||||
|
||||
// Unstake unstakes tokens from a farm
|
||||
func (f *FarmsClient) Unstake(ctx context.Context, farm string, amount *big.Int) (*FarmPosition, error) {
|
||||
body := map[string]interface{}{
|
||||
"farm": farm,
|
||||
"amount": amount.String(),
|
||||
}
|
||||
var position FarmPosition
|
||||
err := f.dex.post(ctx, "/farms/unstake", body, &position)
|
||||
return &position, err
|
||||
}
|
||||
|
||||
// ClaimRewards claims rewards from a farm
|
||||
func (f *FarmsClient) ClaimRewards(ctx context.Context, farm string) (*struct {
|
||||
Amount *big.Int `json:"amount"`
|
||||
TransactionHash string `json:"transaction_hash"`
|
||||
}, error) {
|
||||
body := map[string]interface{}{
|
||||
"farm": farm,
|
||||
}
|
||||
var result struct {
|
||||
Amount *big.Int `json:"amount"`
|
||||
TransactionHash string `json:"transaction_hash"`
|
||||
}
|
||||
err := f.dex.post(ctx, "/farms/claim", body, &result)
|
||||
return &result, err
|
||||
}
|
||||
|
||||
// GetMyFarmPositions gets all farm positions
|
||||
func (f *FarmsClient) GetMyFarmPositions(ctx context.Context) ([]FarmPosition, error) {
|
||||
var positions []FarmPosition
|
||||
err := f.dex.get(ctx, "/farms/positions", &positions)
|
||||
return positions, err
|
||||
}
|
||||
454
sdk/go/dex/types.go
Normal file
454
sdk/go/dex/types.go
Normal file
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
97
sdk/java/src/main/java/io/synor/dex/DexConfig.java
Normal file
97
sdk/java/src/main/java/io/synor/dex/DexConfig.java
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
sdk/java/src/main/java/io/synor/dex/DexException.java
Normal file
23
sdk/java/src/main/java/io/synor/dex/DexException.java
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
50
sdk/java/src/main/java/io/synor/dex/FarmsClient.java
Normal file
50
sdk/java/src/main/java/io/synor/dex/FarmsClient.java
Normal file
|
|
@ -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<List<Farm>> listFarms() {
|
||||
return dex.getList("/farms", Farm.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<Farm> getFarm(String farmId) {
|
||||
return dex.get("/farms/" + farmId, Farm.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<FarmPosition> stake(StakeParams params) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("farm", params.getFarm());
|
||||
body.put("amount", params.getAmount().toString());
|
||||
return dex.post("/farms/stake", body, FarmPosition.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<FarmPosition> unstake(String farm, BigInteger amount) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("farm", farm);
|
||||
body.put("amount", amount.toString());
|
||||
return dex.post("/farms/unstake", body, FarmPosition.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<ClaimRewardsResult> claimRewards(String farm) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("farm", farm);
|
||||
return dex.post("/farms/claim", body, ClaimRewardsResult.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<List<FarmPosition>> getMyFarmPositions() {
|
||||
return dex.getList("/farms/positions", FarmPosition.class);
|
||||
}
|
||||
}
|
||||
45
sdk/java/src/main/java/io/synor/dex/OrderBookClient.java
Normal file
45
sdk/java/src/main/java/io/synor/dex/OrderBookClient.java
Normal file
|
|
@ -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<OrderBook> getOrderBook(String market, int depth) {
|
||||
return dex.get("/orderbook/" + market + "?depth=" + depth, OrderBook.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<Order> placeLimitOrder(LimitOrderParams params) {
|
||||
Map<String, Object> 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<Void> cancelOrder(String orderId) {
|
||||
return dex.delete("/orderbook/orders/" + orderId, Void.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<List<Order>> getOpenOrders(String market) {
|
||||
String path = market != null ? "/orderbook/orders?market=" + market : "/orderbook/orders";
|
||||
return dex.getList(path, Order.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<List<Order>> getOrderHistory(int limit) {
|
||||
return dex.getList("/orderbook/orders/history?limit=" + limit, Order.class);
|
||||
}
|
||||
}
|
||||
109
sdk/java/src/main/java/io/synor/dex/PerpsClient.java
Normal file
109
sdk/java/src/main/java/io/synor/dex/PerpsClient.java
Normal file
|
|
@ -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<List<PerpMarket>> listMarkets() {
|
||||
return dex.getList("/perps/markets", PerpMarket.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<PerpMarket> getMarket(String symbol) {
|
||||
return dex.get("/perps/markets/" + symbol, PerpMarket.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<PerpPosition> openPosition(OpenPositionParams params) {
|
||||
Map<String, Object> 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<PerpPosition> closePosition(ClosePositionParams params) {
|
||||
Map<String, Object> 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<PerpPosition> modifyPosition(ModifyPositionParams params) {
|
||||
Map<String, Object> 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<List<PerpPosition>> getPositions() {
|
||||
return dex.getList("/perps/positions", PerpPosition.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<PerpPosition> getPosition(String market) {
|
||||
return dex.get("/perps/positions/" + market, PerpPosition.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<List<PerpOrder>> getOrders() {
|
||||
return dex.getList("/perps/orders", PerpOrder.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> cancelOrder(String orderId) {
|
||||
return dex.delete("/perps/orders/" + orderId, Void.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<Integer> 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<List<FundingPayment>> getFundingHistory(String market, int limit) {
|
||||
return dex.getList("/perps/funding/" + market + "?limit=" + limit, FundingPayment.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<FundingRateInfo> getFundingRate(String market) {
|
||||
return dex.get("/perps/funding/" + market + "/current", FundingRateInfo.class);
|
||||
}
|
||||
|
||||
private static class CancelResult {
|
||||
int cancelled;
|
||||
}
|
||||
}
|
||||
326
sdk/java/src/main/java/io/synor/dex/SynorDex.java
Normal file
326
sdk/java/src/main/java/io/synor/dex/SynorDex.java
Normal file
|
|
@ -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<Token> getToken(String address) {
|
||||
return get("/tokens/" + address, Token.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<List<Token>> listTokens() {
|
||||
return getList("/tokens", Token.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<List<Token>> searchTokens(String query) {
|
||||
return getList("/tokens/search?q=" + urlEncode(query), Token.class);
|
||||
}
|
||||
|
||||
// Pool Operations
|
||||
|
||||
public CompletableFuture<Pool> getPool(String tokenA, String tokenB) {
|
||||
return get("/pools/" + tokenA + "/" + tokenB, Pool.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<Pool> getPoolById(String poolId) {
|
||||
return get("/pools/" + poolId, Pool.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<List<Pool>> 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<Quote> getQuote(QuoteParams params) {
|
||||
Map<String, Object> 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<SwapResult> swap(SwapParams params) {
|
||||
long deadline = params.getDeadline() != null
|
||||
? params.getDeadline()
|
||||
: Instant.now().getEpochSecond() + 1200;
|
||||
|
||||
Map<String, Object> 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<LiquidityResult> addLiquidity(AddLiquidityParams params) {
|
||||
long deadline = params.getDeadline() != null
|
||||
? params.getDeadline()
|
||||
: Instant.now().getEpochSecond() + 1200;
|
||||
|
||||
Map<String, Object> 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<LiquidityResult> removeLiquidity(RemoveLiquidityParams params) {
|
||||
long deadline = params.getDeadline() != null
|
||||
? params.getDeadline()
|
||||
: Instant.now().getEpochSecond() + 1200;
|
||||
|
||||
Map<String, Object> 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<List<LPPosition>> getMyPositions() {
|
||||
return getList("/liquidity/positions", LPPosition.class);
|
||||
}
|
||||
|
||||
// Analytics
|
||||
|
||||
public CompletableFuture<List<OHLCV>> getPriceHistory(String pair, String interval, int limit) {
|
||||
return getList("/analytics/candles/" + pair + "?interval=" + interval + "&limit=" + limit, OHLCV.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<List<TradeHistory>> getTradeHistory(String pair, int limit) {
|
||||
return getList("/analytics/trades/" + pair + "?limit=" + limit, TradeHistory.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<VolumeStats> getVolumeStats() {
|
||||
return get("/analytics/volume", VolumeStats.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<TVLStats> getTVL() {
|
||||
return get("/analytics/tvl", TVLStats.class);
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
|
||||
public CompletableFuture<Boolean> 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
|
||||
|
||||
<T> CompletableFuture<T> get(String path, Class<T> responseClass) {
|
||||
return request("GET", path, null, responseClass);
|
||||
}
|
||||
|
||||
<T> CompletableFuture<List<T>> getList(String path, Class<T> elementClass) {
|
||||
return request("GET", path, null, List.class)
|
||||
.thenApply(list -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<T> result = (List<T>) list;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
<T> CompletableFuture<T> post(String path, Object body, Class<T> responseClass) {
|
||||
return request("POST", path, body, responseClass);
|
||||
}
|
||||
|
||||
<T> CompletableFuture<T> delete(String path, Class<T> responseClass) {
|
||||
return request("DELETE", path, null, responseClass);
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<T> request(String method, String path, Object body, Class<T> responseClass) {
|
||||
if (closed) {
|
||||
return CompletableFuture.failedFuture(new DexException("Client has been closed", "CLIENT_CLOSED", 0));
|
||||
}
|
||||
|
||||
CompletableFuture<T> 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;
|
||||
}
|
||||
}
|
||||
476
sdk/java/src/main/java/io/synor/dex/Types.java
Normal file
476
sdk/java/src/main/java/io/synor/dex/Types.java
Normal file
|
|
@ -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<String> tokens;
|
||||
private Double minTvl;
|
||||
private Double minVolume24h;
|
||||
private Boolean verified;
|
||||
private Integer limit;
|
||||
private Integer offset;
|
||||
|
||||
public List<String> getTokens() { return tokens; }
|
||||
public void setTokens(List<String> 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<String> 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<String> 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<OrderBookEntry> bids;
|
||||
public List<OrderBookEntry> 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<Token> rewardTokens;
|
||||
public double tvlUsd;
|
||||
public double apr;
|
||||
public List<BigInteger> 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<BigInteger> 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;
|
||||
}
|
||||
473
sdk/js/src/dex/client.ts
Normal file
473
sdk/js/src/dex/client.ts
Normal file
|
|
@ -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<DexConfig>;
|
||||
private closed = false;
|
||||
private ws: WebSocket | null = null;
|
||||
private subscriptions: Map<string, (data: unknown) => 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<Token> {
|
||||
return this.get<Token>('/tokens/' + address);
|
||||
}
|
||||
|
||||
async listTokens(): Promise<Token[]> {
|
||||
return this.get<Token[]>('/tokens');
|
||||
}
|
||||
|
||||
async searchTokens(query: string): Promise<Token[]> {
|
||||
return this.get<Token[]>('/tokens/search?q=' + encodeURIComponent(query));
|
||||
}
|
||||
|
||||
// Pool Operations
|
||||
async getPool(tokenA: string, tokenB: string): Promise<Pool> {
|
||||
return this.get<Pool>('/pools/' + tokenA + '/' + tokenB);
|
||||
}
|
||||
|
||||
async getPoolById(poolId: string): Promise<Pool> {
|
||||
return this.get<Pool>('/pools/' + poolId);
|
||||
}
|
||||
|
||||
async listPools(filter?: PoolFilter): Promise<Pool[]> {
|
||||
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<Pool[]>('/pools?' + params.toString());
|
||||
}
|
||||
|
||||
// Swap Operations
|
||||
async getQuote(params: QuoteParams): Promise<Quote> {
|
||||
return this.post<Quote>('/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<SwapResult> {
|
||||
return this.post<SwapResult>('/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<LiquidityResult> {
|
||||
return this.post<LiquidityResult>('/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<LiquidityResult> {
|
||||
return this.post<LiquidityResult>('/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<LPPosition[]> {
|
||||
return this.get<LPPosition[]>('/liquidity/positions');
|
||||
}
|
||||
|
||||
// Analytics
|
||||
async getPriceHistory(pair: string, interval: string, limit = 100): Promise<OHLCV[]> {
|
||||
return this.get<OHLCV[]>('/analytics/candles/' + pair + '?interval=' + interval + '&limit=' + limit);
|
||||
}
|
||||
|
||||
async getTradeHistory(pair: string, limit = 50): Promise<TradeHistory[]> {
|
||||
return this.get<TradeHistory[]>('/analytics/trades/' + pair + '?limit=' + limit);
|
||||
}
|
||||
|
||||
async getVolumeStats(): Promise<VolumeStats> {
|
||||
return this.get<VolumeStats>('/analytics/volume');
|
||||
}
|
||||
|
||||
async getTVL(): Promise<TVLStats> {
|
||||
return this.get<TVLStats>('/analytics/tvl');
|
||||
}
|
||||
|
||||
// Subscriptions
|
||||
async subscribePrice(market: string, callback: PriceCallback): Promise<Subscription> {
|
||||
return this.subscribe('price', { market }, callback);
|
||||
}
|
||||
|
||||
async subscribeTrades(market: string, callback: TradeCallback): Promise<Subscription> {
|
||||
return this.subscribe('trades', { market }, callback);
|
||||
}
|
||||
|
||||
async subscribeOrderBook(market: string, callback: OrderBookCallback): Promise<Subscription> {
|
||||
return this.subscribe('orderbook', { market }, callback);
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
async healthCheck(): Promise<boolean> {
|
||||
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<T>(path: string): Promise<T> {
|
||||
return this.request<T>('GET', path);
|
||||
}
|
||||
|
||||
async post<T>(path: string, body: unknown): Promise<T> {
|
||||
return this.request<T>('POST', path, body);
|
||||
}
|
||||
|
||||
async delete<T>(path: string): Promise<T> {
|
||||
return this.request<T>('DELETE', path);
|
||||
}
|
||||
|
||||
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
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<T>(
|
||||
channel: string,
|
||||
params: Record<string, unknown>,
|
||||
callback: (data: T) => void
|
||||
): Promise<Subscription> {
|
||||
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<void> {
|
||||
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<PerpMarket[]> {
|
||||
return this.dex.get<PerpMarket[]>('/perps/markets');
|
||||
}
|
||||
|
||||
async getMarket(symbol: string): Promise<PerpMarket> {
|
||||
return this.dex.get<PerpMarket>('/perps/markets/' + symbol);
|
||||
}
|
||||
|
||||
async openPosition(params: OpenPositionParams): Promise<PerpPosition> {
|
||||
return this.dex.post<PerpPosition>('/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<PerpPosition> {
|
||||
return this.dex.post<PerpPosition>('/perps/positions/close', {
|
||||
market: params.market,
|
||||
size: params.size?.toString(),
|
||||
order_type: params.orderType,
|
||||
limit_price: params.limitPrice,
|
||||
});
|
||||
}
|
||||
|
||||
async modifyPosition(params: ModifyPositionParams): Promise<PerpPosition> {
|
||||
return this.dex.post<PerpPosition>('/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<PerpPosition[]> {
|
||||
return this.dex.get<PerpPosition[]>('/perps/positions');
|
||||
}
|
||||
|
||||
async getPosition(market: string): Promise<PerpPosition | null> {
|
||||
return this.dex.get<PerpPosition | null>('/perps/positions/' + market);
|
||||
}
|
||||
|
||||
async getOrders(): Promise<PerpOrder[]> {
|
||||
return this.dex.get<PerpOrder[]>('/perps/orders');
|
||||
}
|
||||
|
||||
async cancelOrder(orderId: string): Promise<void> {
|
||||
await this.dex.delete('/perps/orders/' + orderId);
|
||||
}
|
||||
|
||||
async cancelAllOrders(market?: string): Promise<number> {
|
||||
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<FundingPayment[]> {
|
||||
return this.dex.get<FundingPayment[]>('/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<Subscription> {
|
||||
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<OrderBook> {
|
||||
return this.dex.get<OrderBook>('/orderbook/' + market + '?depth=' + depth);
|
||||
}
|
||||
|
||||
async placeLimitOrder(params: LimitOrderParams): Promise<Order> {
|
||||
return this.dex.post<Order>('/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<void> {
|
||||
await this.dex.delete('/orderbook/orders/' + orderId);
|
||||
}
|
||||
|
||||
async getOpenOrders(market?: string): Promise<Order[]> {
|
||||
const path = market ? '/orderbook/orders?market=' + market : '/orderbook/orders';
|
||||
return this.dex.get<Order[]>(path);
|
||||
}
|
||||
|
||||
async getOrderHistory(limit = 50): Promise<Order[]> {
|
||||
return this.dex.get<Order[]>('/orderbook/orders/history?limit=' + limit);
|
||||
}
|
||||
}
|
||||
|
||||
// Farms Sub-Client
|
||||
class FarmsClient {
|
||||
constructor(private dex: SynorDex) {}
|
||||
|
||||
async listFarms(): Promise<Farm[]> {
|
||||
return this.dex.get<Farm[]>('/farms');
|
||||
}
|
||||
|
||||
async getFarm(farmId: string): Promise<Farm> {
|
||||
return this.dex.get<Farm>('/farms/' + farmId);
|
||||
}
|
||||
|
||||
async stake(params: StakeParams): Promise<FarmPosition> {
|
||||
return this.dex.post<FarmPosition>('/farms/stake', {
|
||||
farm: params.farm,
|
||||
amount: params.amount.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
async unstake(farm: string, amount: bigint): Promise<FarmPosition> {
|
||||
return this.dex.post<FarmPosition>('/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<FarmPosition[]> {
|
||||
return this.dex.get<FarmPosition[]>('/farms/positions');
|
||||
}
|
||||
}
|
||||
|
||||
export default SynorDex;
|
||||
8
sdk/js/src/dex/index.ts
Normal file
8
sdk/js/src/dex/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Synor DEX SDK
|
||||
*
|
||||
* Decentralized exchange with AMM, perpetual futures, and order books.
|
||||
*/
|
||||
|
||||
export { SynorDex, default } from './client';
|
||||
export * from './types';
|
||||
384
sdk/js/src/dex/types.ts
Normal file
384
sdk/js/src/dex/types.ts
Normal file
|
|
@ -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<string, bigint>;
|
||||
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');
|
||||
}
|
||||
}
|
||||
342
sdk/kotlin/src/main/kotlin/io/synor/dex/SynorDex.kt
Normal file
342
sdk/kotlin/src/main/kotlin/io/synor/dex/SynorDex.kt
Normal file
|
|
@ -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<Token> =
|
||||
get("/tokens")
|
||||
|
||||
suspend fun searchTokens(query: String): List<Token> =
|
||||
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<Pool> {
|
||||
val params = mutableListOf<String>()
|
||||
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<LPPosition> =
|
||||
get("/liquidity/positions")
|
||||
|
||||
// Analytics
|
||||
|
||||
suspend fun getPriceHistory(pair: String, interval: String, limit: Int = 100): List<OHLCV> =
|
||||
get("/analytics/candles/$pair?interval=$interval&limit=$limit")
|
||||
|
||||
suspend fun getTradeHistory(pair: String, limit: Int = 50): List<TradeHistory> =
|
||||
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 <reified T> get(path: String): T {
|
||||
checkClosed()
|
||||
return client.get(config.endpoint + path).body()
|
||||
}
|
||||
|
||||
internal suspend inline fun <reified T> post(path: String, body: Map<String, Any?>): T {
|
||||
checkClosed()
|
||||
return client.post(config.endpoint + path) {
|
||||
setBody(body)
|
||||
}.body()
|
||||
}
|
||||
|
||||
internal suspend inline fun <reified T> 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<PerpMarket> =
|
||||
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<PerpPosition> =
|
||||
dex.get("/perps/positions")
|
||||
|
||||
suspend fun getPosition(market: String): PerpPosition? =
|
||||
dex.get("/perps/positions/$market")
|
||||
|
||||
suspend fun getOrders(): List<PerpOrder> =
|
||||
dex.get("/perps/orders")
|
||||
|
||||
suspend fun cancelOrder(orderId: String) {
|
||||
dex.delete<Unit>("/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<FundingPayment> =
|
||||
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<Unit>("/orderbook/orders/$orderId")
|
||||
}
|
||||
|
||||
suspend fun getOpenOrders(market: String? = null): List<Order> {
|
||||
val path = if (market != null) "/orderbook/orders?market=$market" else "/orderbook/orders"
|
||||
return dex.get(path)
|
||||
}
|
||||
|
||||
suspend fun getOrderHistory(limit: Int = 50): List<Order> =
|
||||
dex.get("/orderbook/orders/history?limit=$limit")
|
||||
}
|
||||
|
||||
/**
|
||||
* Farms sub-client
|
||||
*/
|
||||
class FarmsClient(private val dex: SynorDex) {
|
||||
suspend fun listFarms(): List<Farm> =
|
||||
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<FarmPosition> =
|
||||
dex.get("/farms/positions")
|
||||
}
|
||||
|
||||
private fun String.encodeURLParameter(): String =
|
||||
java.net.URLEncoder.encode(this, "UTF-8")
|
||||
369
sdk/kotlin/src/main/kotlin/io/synor/dex/Types.kt
Normal file
369
sdk/kotlin/src/main/kotlin/io/synor/dex/Types.kt
Normal file
|
|
@ -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<String>? = 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<String>,
|
||||
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<String>
|
||||
)
|
||||
|
||||
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<OrderBookEntry>,
|
||||
val asks: List<OrderBookEntry>,
|
||||
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<Token>,
|
||||
@SerialName("tvl_usd") val tvlUsd: Double,
|
||||
val apr: Double,
|
||||
@SerialName("daily_rewards") val dailyRewards: List<String>,
|
||||
@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<String>,
|
||||
@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
|
||||
)
|
||||
96
sdk/python/src/synor_dex/__init__.py
Normal file
96
sdk/python/src/synor_dex/__init__.py
Normal file
|
|
@ -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"
|
||||
496
sdk/python/src/synor_dex/client.py
Normal file
496
sdk/python/src/synor_dex/client.py
Normal file
|
|
@ -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
|
||||
445
sdk/python/src/synor_dex/types.py
Normal file
445
sdk/python/src/synor_dex/types.py
Normal file
|
|
@ -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]
|
||||
392
sdk/ruby/lib/synor/dex.rb
Normal file
392
sdk/ruby/lib/synor/dex.rb
Normal file
|
|
@ -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
|
||||
332
sdk/rust/src/dex/client.rs
Normal file
332
sdk/rust/src/dex/client.rs
Normal file
|
|
@ -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<AtomicBool>,
|
||||
pub perps: PerpsClient,
|
||||
pub orderbook: OrderBookClient,
|
||||
pub farms: FarmsClient,
|
||||
}
|
||||
|
||||
impl SynorDex {
|
||||
/// Create a new SynorDex client
|
||||
pub fn new(config: DexConfig) -> Result<Self, DexError> {
|
||||
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<Token, DexError> {
|
||||
self.get(&format!("/tokens/{}", address)).await
|
||||
}
|
||||
|
||||
/// List all tokens
|
||||
pub async fn list_tokens(&self) -> Result<Vec<Token>, DexError> {
|
||||
self.get("/tokens").await
|
||||
}
|
||||
|
||||
/// Search tokens
|
||||
pub async fn search_tokens(&self, query: &str) -> Result<Vec<Token>, 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<Pool, DexError> {
|
||||
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<Pool, DexError> {
|
||||
self.get(&format!("/pools/{}", pool_id)).await
|
||||
}
|
||||
|
||||
/// List pools with optional filtering
|
||||
pub async fn list_pools(&self, filter: Option<PoolFilter>) -> Result<Vec<Pool>, 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<Quote, DexError> {
|
||||
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<SwapResult, DexError> {
|
||||
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<LiquidityResult, DexError> {
|
||||
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<LiquidityResult, DexError> {
|
||||
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<Vec<LPPosition>, 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<Vec<OHLCV>, 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<Vec<TradeHistory>, DexError> {
|
||||
self.get(&format!("/analytics/trades/{}?limit={}", pair, limit)).await
|
||||
}
|
||||
|
||||
/// Get volume statistics
|
||||
pub async fn get_volume_stats(&self) -> Result<VolumeStats, DexError> {
|
||||
self.get("/analytics/volume").await
|
||||
}
|
||||
|
||||
/// Get TVL statistics
|
||||
pub async fn get_tvl(&self) -> Result<TVLStats, DexError> {
|
||||
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>("/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<T: DeserializeOwned>(&self, path: &str) -> Result<T, DexError> {
|
||||
self.request("GET", path, None::<()>).await
|
||||
}
|
||||
|
||||
pub(crate) async fn post<T: DeserializeOwned, B: Serialize>(&self, path: &str, body: B) -> Result<T, DexError> {
|
||||
self.request("POST", path, Some(body)).await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete<T: DeserializeOwned>(&self, path: &str) -> Result<T, DexError> {
|
||||
self.request("DELETE", path, None::<()>).await
|
||||
}
|
||||
|
||||
async fn request<T: DeserializeOwned, B: Serialize>(
|
||||
&self,
|
||||
method: &str,
|
||||
path: &str,
|
||||
body: Option<B>,
|
||||
) -> Result<T, DexError> {
|
||||
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<String>,
|
||||
code: Option<String>,
|
||||
}
|
||||
|
||||
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};
|
||||
111
sdk/rust/src/dex/farms.rs
Normal file
111
sdk/rust/src/dex/farms.rs
Normal file
|
|
@ -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<AtomicBool>,
|
||||
}
|
||||
|
||||
impl FarmsClient {
|
||||
pub(crate) fn new(config: DexConfig, client: Client, closed: Arc<AtomicBool>) -> Self {
|
||||
Self { config, client, closed }
|
||||
}
|
||||
|
||||
/// List all farms
|
||||
pub async fn list_farms(&self) -> Result<Vec<Farm>, DexError> {
|
||||
self.get("/farms").await
|
||||
}
|
||||
|
||||
/// Get a specific farm
|
||||
pub async fn get_farm(&self, farm_id: &str) -> Result<Farm, DexError> {
|
||||
self.get(&format!("/farms/{}", farm_id)).await
|
||||
}
|
||||
|
||||
/// Stake tokens in a farm
|
||||
pub async fn stake(&self, params: StakeParams) -> Result<FarmPosition, DexError> {
|
||||
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<FarmPosition, DexError> {
|
||||
self.post("/farms/unstake", json!({
|
||||
"farm": farm,
|
||||
"amount": amount,
|
||||
})).await
|
||||
}
|
||||
|
||||
/// Claim rewards from a farm
|
||||
pub async fn claim_rewards(&self, farm: &str) -> Result<ClaimRewardsResult, DexError> {
|
||||
self.post("/farms/claim", json!({
|
||||
"farm": farm,
|
||||
})).await
|
||||
}
|
||||
|
||||
/// Get all farm positions
|
||||
pub async fn get_my_farm_positions(&self) -> Result<Vec<FarmPosition>, DexError> {
|
||||
self.get("/farms/positions").await
|
||||
}
|
||||
|
||||
// Internal methods
|
||||
|
||||
async fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, DexError> {
|
||||
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<T: serde::de::DeserializeOwned>(&self, path: &str, body: serde_json::Value) -> Result<T, DexError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
20
sdk/rust/src/dex/mod.rs
Normal file
20
sdk/rust/src/dex/mod.rs
Normal file
|
|
@ -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::*;
|
||||
134
sdk/rust/src/dex/orderbook.rs
Normal file
134
sdk/rust/src/dex/orderbook.rs
Normal file
|
|
@ -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<AtomicBool>,
|
||||
}
|
||||
|
||||
impl OrderBookClient {
|
||||
pub(crate) fn new(config: DexConfig, client: Client, closed: Arc<AtomicBool>) -> Self {
|
||||
Self { config, client, closed }
|
||||
}
|
||||
|
||||
/// Get order book for a market
|
||||
pub async fn get_order_book(&self, market: &str, depth: u32) -> Result<OrderBook, DexError> {
|
||||
self.get(&format!("/orderbook/{}?depth={}", market, depth)).await
|
||||
}
|
||||
|
||||
/// Place a limit order
|
||||
pub async fn place_limit_order(&self, params: LimitOrderParams) -> Result<Order, DexError> {
|
||||
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<Vec<Order>, 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<Vec<Order>, DexError> {
|
||||
self.get(&format!("/orderbook/orders/history?limit={}", limit)).await
|
||||
}
|
||||
|
||||
// Internal methods
|
||||
|
||||
async fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, DexError> {
|
||||
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<T: serde::de::DeserializeOwned>(&self, path: &str, body: serde_json::Value) -> Result<T, DexError> {
|
||||
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<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, DexError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
222
sdk/rust/src/dex/perps.rs
Normal file
222
sdk/rust/src/dex/perps.rs
Normal file
|
|
@ -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<AtomicBool>,
|
||||
}
|
||||
|
||||
impl PerpsClient {
|
||||
pub(crate) fn new(config: DexConfig, client: Client, closed: Arc<AtomicBool>) -> Self {
|
||||
Self { config, client, closed }
|
||||
}
|
||||
|
||||
/// List all perpetual markets
|
||||
pub async fn list_markets(&self) -> Result<Vec<PerpMarket>, DexError> {
|
||||
self.get("/perps/markets").await
|
||||
}
|
||||
|
||||
/// Get a specific perpetual market
|
||||
pub async fn get_market(&self, symbol: &str) -> Result<PerpMarket, DexError> {
|
||||
self.get(&format!("/perps/markets/{}", symbol)).await
|
||||
}
|
||||
|
||||
/// Open a perpetual position
|
||||
pub async fn open_position(&self, params: OpenPositionParams) -> Result<PerpPosition, DexError> {
|
||||
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<PerpPosition, DexError> {
|
||||
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<PerpPosition, DexError> {
|
||||
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<Vec<PerpPosition>, DexError> {
|
||||
self.get("/perps/positions").await
|
||||
}
|
||||
|
||||
/// Get position for a specific market
|
||||
pub async fn get_position(&self, market: &str) -> Result<Option<PerpPosition>, DexError> {
|
||||
self.get(&format!("/perps/positions/{}", market)).await
|
||||
}
|
||||
|
||||
/// Get all open orders
|
||||
pub async fn get_orders(&self) -> Result<Vec<PerpOrder>, 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<u32, DexError> {
|
||||
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<Vec<FundingPayment>, DexError> {
|
||||
self.get(&format!("/perps/funding/{}?limit={}", market, limit)).await
|
||||
}
|
||||
|
||||
/// Get current funding rate
|
||||
pub async fn get_funding_rate(&self, market: &str) -> Result<FundingRateInfo, DexError> {
|
||||
self.get(&format!("/perps/funding/{}/current", market)).await
|
||||
}
|
||||
|
||||
// Internal methods
|
||||
|
||||
async fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, DexError> {
|
||||
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<T: serde::de::DeserializeOwned>(&self, path: &str, body: serde_json::Value) -> Result<T, DexError> {
|
||||
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<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, DexError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
544
sdk/rust/src/dex/types.rs
Normal file
544
sdk/rust/src/dex/types.rs
Normal file
|
|
@ -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<String>, status: Option<u16> },
|
||||
|
||||
#[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<String>) -> 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<String>) -> Self {
|
||||
self.endpoint = endpoint.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_ws_endpoint(mut self, ws_endpoint: impl Into<String>) -> 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<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logo_url: Option<String>,
|
||||
#[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<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sqrt_price: Option<String>,
|
||||
}
|
||||
|
||||
/// Pool filter
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PoolFilter {
|
||||
pub tokens: Option<Vec<String>>,
|
||||
pub min_tvl: Option<f64>,
|
||||
pub min_volume_24h: Option<f64>,
|
||||
pub verified: Option<bool>,
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
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<f64>,
|
||||
}
|
||||
|
||||
/// 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<i64>,
|
||||
pub recipient: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
pub min_amount_b: Option<String>,
|
||||
pub deadline: Option<i64>,
|
||||
pub tick_lower: Option<i32>,
|
||||
pub tick_upper: Option<i32>,
|
||||
}
|
||||
|
||||
/// Remove liquidity parameters
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RemoveLiquidityParams {
|
||||
pub pool: String,
|
||||
pub lp_amount: String,
|
||||
pub min_amount_a: Option<String>,
|
||||
pub min_amount_b: Option<String>,
|
||||
pub deadline: Option<i64>,
|
||||
}
|
||||
|
||||
/// 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<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tick_upper: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub in_range: Option<bool>,
|
||||
}
|
||||
|
||||
/// 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<f64>,
|
||||
pub stop_loss: Option<f64>,
|
||||
pub take_profit: Option<f64>,
|
||||
pub margin_type: MarginType,
|
||||
pub reduce_only: bool,
|
||||
}
|
||||
|
||||
/// Close position parameters
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClosePositionParams {
|
||||
pub market: String,
|
||||
pub size: Option<String>, // None = close entire position
|
||||
pub order_type: OrderType,
|
||||
pub limit_price: Option<f64>,
|
||||
}
|
||||
|
||||
/// Modify position parameters
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModifyPositionParams {
|
||||
pub position_id: String,
|
||||
pub new_leverage: Option<u32>,
|
||||
pub new_margin: Option<String>,
|
||||
pub new_stop_loss: Option<f64>,
|
||||
pub new_take_profit: Option<f64>,
|
||||
}
|
||||
|
||||
/// 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<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub take_profit: Option<f64>,
|
||||
#[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<f64>,
|
||||
#[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<OrderBookEntry>,
|
||||
pub asks: Vec<OrderBookEntry>,
|
||||
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<Token>,
|
||||
pub tvl_usd: f64,
|
||||
pub apr: f64,
|
||||
pub daily_rewards: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lockup_period: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub min_stake: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
pub staked_at: i64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub unlock_at: Option<i64>,
|
||||
}
|
||||
|
||||
/// 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<tokio::sync::oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
368
sdk/swift/Sources/SynorDex/SynorDex.swift
Normal file
368
sdk/swift/Sources/SynorDex/SynorDex.swift
Normal file
|
|
@ -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<T: Decodable>(_ path: String) async throws -> T {
|
||||
try checkClosed()
|
||||
var request = URLRequest(url: URL(string: config.endpoint + path)!)
|
||||
request.httpMethod = "GET"
|
||||
addHeaders(&request)
|
||||
return try await execute(request)
|
||||
}
|
||||
|
||||
func post<T: Decodable>(_ path: String, body: [String: Any]) async throws -> T {
|
||||
try checkClosed()
|
||||
var request = URLRequest(url: URL(string: config.endpoint + path)!)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||
addHeaders(&request)
|
||||
return try await execute(request)
|
||||
}
|
||||
|
||||
func delete<T: Decodable>(_ path: String) async throws -> T {
|
||||
try checkClosed()
|
||||
var request = URLRequest(url: URL(string: config.endpoint + path)!)
|
||||
request.httpMethod = "DELETE"
|
||||
addHeaders(&request)
|
||||
return try await execute(request)
|
||||
}
|
||||
|
||||
private func addHeaders(_ request: inout URLRequest) {
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version")
|
||||
}
|
||||
|
||||
private func execute<T: Decodable>(_ request: URLRequest) async throws -> T {
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw DexError.network("Invalid response")
|
||||
}
|
||||
|
||||
guard (200...299).contains(httpResponse.statusCode) else {
|
||||
let error = try? JSONDecoder().decode(ErrorResponse.self, from: data)
|
||||
throw DexError.http(
|
||||
message: error?.message ?? "HTTP \(httpResponse.statusCode)",
|
||||
code: error?.code,
|
||||
status: httpResponse.statusCode
|
||||
)
|
||||
}
|
||||
|
||||
return try JSONDecoder().decode(T.self, from: data)
|
||||
}
|
||||
|
||||
private func checkClosed() throws {
|
||||
if closed {
|
||||
throw DexError.clientClosed
|
||||
}
|
||||
}
|
||||
|
||||
private struct HealthResponse: Decodable {
|
||||
let status: String
|
||||
}
|
||||
|
||||
private struct ErrorResponse: Decodable {
|
||||
let message: String?
|
||||
let code: String?
|
||||
}
|
||||
}
|
||||
|
||||
/// DEX client configuration
|
||||
public struct DexConfig {
|
||||
public let apiKey: String
|
||||
public let endpoint: String
|
||||
public let wsEndpoint: String
|
||||
public let timeout: Int
|
||||
public let retries: Int
|
||||
public let debug: Bool
|
||||
|
||||
public init(
|
||||
apiKey: String,
|
||||
endpoint: String = "https://dex.synor.io/v1",
|
||||
wsEndpoint: String = "wss://dex.synor.io/v1/ws",
|
||||
timeout: Int = 30000,
|
||||
retries: Int = 3,
|
||||
debug: Bool = false
|
||||
) {
|
||||
self.apiKey = apiKey
|
||||
self.endpoint = endpoint
|
||||
self.wsEndpoint = wsEndpoint
|
||||
self.timeout = timeout
|
||||
self.retries = retries
|
||||
self.debug = debug
|
||||
}
|
||||
}
|
||||
|
||||
/// DEX SDK Error
|
||||
public enum DexError: Error {
|
||||
case http(message: String, code: String?, status: Int)
|
||||
case network(String)
|
||||
case clientClosed
|
||||
}
|
||||
|
||||
/// Perpetual futures sub-client
|
||||
public class PerpsClient {
|
||||
weak var dex: SynorDex?
|
||||
|
||||
public func listMarkets() async throws -> [PerpMarket] {
|
||||
try await dex!.get("/perps/markets")
|
||||
}
|
||||
|
||||
public func getMarket(symbol: String) async throws -> PerpMarket {
|
||||
try await dex!.get("/perps/markets/\(symbol)")
|
||||
}
|
||||
|
||||
public func openPosition(params: OpenPositionParams) async throws -> PerpPosition {
|
||||
var body: [String: Any] = [
|
||||
"market": params.market,
|
||||
"side": params.side.rawValue,
|
||||
"size": String(params.size),
|
||||
"leverage": params.leverage,
|
||||
"order_type": params.orderType.rawValue,
|
||||
"margin_type": params.marginType.rawValue,
|
||||
"reduce_only": params.reduceOnly
|
||||
]
|
||||
if let price = params.limitPrice { body["limit_price"] = price }
|
||||
if let sl = params.stopLoss { body["stop_loss"] = sl }
|
||||
if let tp = params.takeProfit { body["take_profit"] = tp }
|
||||
return try await dex!.post("/perps/positions", body: body)
|
||||
}
|
||||
|
||||
public func closePosition(params: ClosePositionParams) async throws -> PerpPosition {
|
||||
var body: [String: Any] = [
|
||||
"market": params.market,
|
||||
"order_type": params.orderType.rawValue
|
||||
]
|
||||
if let size = params.size { body["size"] = String(size) }
|
||||
if let price = params.limitPrice { body["limit_price"] = price }
|
||||
return try await dex!.post("/perps/positions/close", body: body)
|
||||
}
|
||||
|
||||
public func getPositions() async throws -> [PerpPosition] {
|
||||
try await dex!.get("/perps/positions")
|
||||
}
|
||||
|
||||
public func getOrders() async throws -> [PerpOrder] {
|
||||
try await dex!.get("/perps/orders")
|
||||
}
|
||||
|
||||
public func getFundingHistory(market: String, limit: Int = 100) async throws -> [FundingPayment] {
|
||||
try await dex!.get("/perps/funding/\(market)?limit=\(limit)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Order book sub-client
|
||||
public class OrderBookClient {
|
||||
weak var dex: SynorDex?
|
||||
|
||||
public func getOrderBook(market: String, depth: Int = 20) async throws -> OrderBook {
|
||||
try await dex!.get("/orderbook/\(market)?depth=\(depth)")
|
||||
}
|
||||
|
||||
public func placeLimitOrder(params: LimitOrderParams) async throws -> Order {
|
||||
try await dex!.post("/orderbook/orders", body: [
|
||||
"market": params.market,
|
||||
"side": params.side,
|
||||
"price": params.price,
|
||||
"size": String(params.size),
|
||||
"time_in_force": params.timeInForce.rawValue,
|
||||
"post_only": params.postOnly
|
||||
])
|
||||
}
|
||||
|
||||
public func getOpenOrders(market: String? = nil) async throws -> [Order] {
|
||||
let path = market.map { "/orderbook/orders?market=\($0)" } ?? "/orderbook/orders"
|
||||
return try await dex!.get(path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Farms sub-client
|
||||
public class FarmsClient {
|
||||
weak var dex: SynorDex?
|
||||
|
||||
public func listFarms() async throws -> [Farm] {
|
||||
try await dex!.get("/farms")
|
||||
}
|
||||
|
||||
public func getFarm(farmId: String) async throws -> Farm {
|
||||
try await dex!.get("/farms/\(farmId)")
|
||||
}
|
||||
|
||||
public func stake(params: StakeParams) async throws -> FarmPosition {
|
||||
try await dex!.post("/farms/stake", body: [
|
||||
"farm": params.farm,
|
||||
"amount": String(params.amount)
|
||||
])
|
||||
}
|
||||
|
||||
public func getMyFarmPositions() async throws -> [FarmPosition] {
|
||||
try await dex!.get("/farms/positions")
|
||||
}
|
||||
}
|
||||
498
sdk/swift/Sources/SynorDex/Types.swift
Normal file
498
sdk/swift/Sources/SynorDex/Types.swift
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue