feat: complete Phase 3 SDKs for Swift, C, C++, C#, and Ruby
Implements Database, Hosting, and Bridge SDKs for remaining languages: Swift SDKs: - SynorDatabase with KV, Document, Vector, TimeSeries stores - SynorHosting with domain, DNS, deployment, SSL operations - SynorBridge with lock-mint and burn-unlock cross-chain flows C SDKs: - database.h/c - multi-model database client - hosting.h/c - hosting and domain management - bridge.h/c - cross-chain asset transfers C++ SDKs: - database.hpp - modern C++17 with std::future async - hosting.hpp - domain and deployment operations - bridge.hpp - cross-chain bridge with wait operations C# SDKs: - SynorDatabase.cs - async/await with inner store classes - SynorHosting.cs - domain management and analytics - SynorBridge.cs - cross-chain with BridgeException handling Ruby SDKs: - synor_database - Struct-based types with Faraday HTTP - synor_hosting - domain, DNS, SSL, analytics - synor_bridge - lock-mint/burn-unlock with retry logic Phase 3 complete: Database/Hosting/Bridge now available in all 12 languages.
This commit is contained in:
parent
14cd439552
commit
a874faef13
33 changed files with 8006 additions and 0 deletions
397
sdk/c/include/synor/bridge.h
Normal file
397
sdk/c/include/synor/bridge.h
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
/**
|
||||
* @file bridge.h
|
||||
* @brief Synor Bridge SDK for C
|
||||
*
|
||||
* Cross-chain asset transfers with lock-mint and burn-unlock patterns.
|
||||
*/
|
||||
|
||||
#ifndef SYNOR_BRIDGE_H
|
||||
#define SYNOR_BRIDGE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ==================== Error Types ==================== */
|
||||
|
||||
typedef enum {
|
||||
SYNOR_BRIDGE_OK = 0,
|
||||
SYNOR_BRIDGE_ERROR_CLIENT_CLOSED,
|
||||
SYNOR_BRIDGE_ERROR_NETWORK,
|
||||
SYNOR_BRIDGE_ERROR_HTTP,
|
||||
SYNOR_BRIDGE_ERROR_ENCODING,
|
||||
SYNOR_BRIDGE_ERROR_DECODING,
|
||||
SYNOR_BRIDGE_ERROR_INVALID_RESPONSE,
|
||||
SYNOR_BRIDGE_ERROR_MEMORY,
|
||||
SYNOR_BRIDGE_ERROR_TIMEOUT,
|
||||
SYNOR_BRIDGE_ERROR_CONFIRMATIONS_PENDING,
|
||||
SYNOR_BRIDGE_ERROR_UNKNOWN
|
||||
} synor_bridge_error_t;
|
||||
|
||||
/* ==================== Configuration ==================== */
|
||||
|
||||
typedef struct {
|
||||
const char* api_key;
|
||||
const char* endpoint; /* Default: https://bridge.synor.io/v1 */
|
||||
uint32_t timeout_ms; /* Default: 60000 */
|
||||
uint32_t retries; /* Default: 3 */
|
||||
bool debug;
|
||||
} synor_bridge_config_t;
|
||||
|
||||
/* ==================== Enums ==================== */
|
||||
|
||||
typedef enum {
|
||||
SYNOR_CHAIN_SYNOR,
|
||||
SYNOR_CHAIN_ETHEREUM,
|
||||
SYNOR_CHAIN_POLYGON,
|
||||
SYNOR_CHAIN_ARBITRUM,
|
||||
SYNOR_CHAIN_OPTIMISM,
|
||||
SYNOR_CHAIN_BSC,
|
||||
SYNOR_CHAIN_AVALANCHE,
|
||||
SYNOR_CHAIN_SOLANA,
|
||||
SYNOR_CHAIN_COSMOS
|
||||
} synor_chain_id_t;
|
||||
|
||||
typedef enum {
|
||||
SYNOR_ASSET_NATIVE,
|
||||
SYNOR_ASSET_ERC20,
|
||||
SYNOR_ASSET_ERC721,
|
||||
SYNOR_ASSET_ERC1155
|
||||
} synor_asset_type_t;
|
||||
|
||||
typedef enum {
|
||||
SYNOR_TRANSFER_PENDING,
|
||||
SYNOR_TRANSFER_LOCKED,
|
||||
SYNOR_TRANSFER_CONFIRMING,
|
||||
SYNOR_TRANSFER_MINTING,
|
||||
SYNOR_TRANSFER_COMPLETED,
|
||||
SYNOR_TRANSFER_FAILED,
|
||||
SYNOR_TRANSFER_REFUNDED
|
||||
} synor_transfer_status_t;
|
||||
|
||||
typedef enum {
|
||||
SYNOR_DIRECTION_LOCK_MINT,
|
||||
SYNOR_DIRECTION_BURN_UNLOCK
|
||||
} synor_transfer_direction_t;
|
||||
|
||||
/* ==================== Chain Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
char* name;
|
||||
char* symbol;
|
||||
int32_t decimals;
|
||||
} synor_native_currency_t;
|
||||
|
||||
typedef struct {
|
||||
synor_chain_id_t id;
|
||||
char* name;
|
||||
int64_t chain_id;
|
||||
char* rpc_url;
|
||||
char* explorer_url;
|
||||
synor_native_currency_t native_currency;
|
||||
int32_t confirmations;
|
||||
int32_t estimated_block_time;
|
||||
bool supported;
|
||||
} synor_chain_t;
|
||||
|
||||
typedef struct {
|
||||
synor_chain_t* chains;
|
||||
size_t count;
|
||||
} synor_chain_list_t;
|
||||
|
||||
/* ==================== Asset Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
char* id;
|
||||
char* symbol;
|
||||
char* name;
|
||||
synor_asset_type_t type;
|
||||
synor_chain_id_t chain;
|
||||
char* contract_address;
|
||||
int32_t decimals;
|
||||
char* logo_url;
|
||||
bool verified;
|
||||
} synor_asset_t;
|
||||
|
||||
typedef struct {
|
||||
synor_asset_t* assets;
|
||||
size_t count;
|
||||
} synor_asset_list_t;
|
||||
|
||||
typedef struct {
|
||||
synor_asset_t original_asset;
|
||||
synor_asset_t wrapped_asset;
|
||||
synor_chain_id_t chain;
|
||||
char* bridge_contract;
|
||||
} synor_wrapped_asset_t;
|
||||
|
||||
/* ==================== Transfer Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
char* id;
|
||||
synor_transfer_direction_t direction;
|
||||
synor_transfer_status_t status;
|
||||
synor_chain_id_t source_chain;
|
||||
synor_chain_id_t target_chain;
|
||||
synor_asset_t asset;
|
||||
char* amount;
|
||||
char* sender;
|
||||
char* recipient;
|
||||
char* source_tx_hash;
|
||||
char* target_tx_hash;
|
||||
char* fee;
|
||||
synor_asset_t fee_asset;
|
||||
int64_t created_at;
|
||||
int64_t updated_at;
|
||||
int64_t completed_at;
|
||||
char* error_message;
|
||||
} synor_transfer_t;
|
||||
|
||||
typedef struct {
|
||||
synor_transfer_t* transfers;
|
||||
size_t count;
|
||||
} synor_transfer_list_t;
|
||||
|
||||
typedef struct {
|
||||
synor_transfer_status_t* status; /* NULL for all */
|
||||
synor_chain_id_t* source_chain; /* NULL for all */
|
||||
synor_chain_id_t* target_chain; /* NULL for all */
|
||||
int32_t limit;
|
||||
int32_t offset;
|
||||
} synor_transfer_filter_t;
|
||||
|
||||
/* ==================== Lock-Mint Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
char* validator;
|
||||
char* signature;
|
||||
int64_t timestamp;
|
||||
} synor_validator_signature_t;
|
||||
|
||||
typedef struct {
|
||||
char* id;
|
||||
char* tx_hash;
|
||||
synor_chain_id_t source_chain;
|
||||
synor_chain_id_t target_chain;
|
||||
synor_asset_t asset;
|
||||
char* amount;
|
||||
char* sender;
|
||||
char* recipient;
|
||||
int64_t lock_timestamp;
|
||||
int32_t confirmations;
|
||||
int32_t required_confirmations;
|
||||
} synor_lock_receipt_t;
|
||||
|
||||
typedef struct {
|
||||
synor_lock_receipt_t lock_receipt;
|
||||
char** merkle_proof;
|
||||
size_t merkle_proof_count;
|
||||
char* block_header;
|
||||
synor_validator_signature_t* signatures;
|
||||
size_t signature_count;
|
||||
} synor_lock_proof_t;
|
||||
|
||||
typedef struct {
|
||||
const char* recipient;
|
||||
int64_t deadline;
|
||||
double slippage;
|
||||
} synor_lock_options_t;
|
||||
|
||||
typedef struct {
|
||||
const char* gas_limit;
|
||||
const char* max_fee_per_gas;
|
||||
const char* max_priority_fee_per_gas;
|
||||
} synor_mint_options_t;
|
||||
|
||||
/* ==================== Burn-Unlock Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
char* id;
|
||||
char* tx_hash;
|
||||
synor_chain_id_t source_chain;
|
||||
synor_chain_id_t target_chain;
|
||||
synor_asset_t wrapped_asset;
|
||||
synor_asset_t original_asset;
|
||||
char* amount;
|
||||
char* sender;
|
||||
char* recipient;
|
||||
int64_t burn_timestamp;
|
||||
int32_t confirmations;
|
||||
int32_t required_confirmations;
|
||||
} synor_burn_receipt_t;
|
||||
|
||||
typedef struct {
|
||||
synor_burn_receipt_t burn_receipt;
|
||||
char** merkle_proof;
|
||||
size_t merkle_proof_count;
|
||||
char* block_header;
|
||||
synor_validator_signature_t* signatures;
|
||||
size_t signature_count;
|
||||
} synor_burn_proof_t;
|
||||
|
||||
typedef struct {
|
||||
const char* recipient;
|
||||
int64_t deadline;
|
||||
} synor_burn_options_t;
|
||||
|
||||
typedef struct {
|
||||
const char* gas_limit;
|
||||
const char* gas_price;
|
||||
} synor_unlock_options_t;
|
||||
|
||||
/* ==================== Fee Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
char* bridge_fee;
|
||||
char* gas_fee_source;
|
||||
char* gas_fee_target;
|
||||
char* total_fee;
|
||||
synor_asset_t fee_asset;
|
||||
int32_t estimated_time;
|
||||
char* exchange_rate;
|
||||
} synor_fee_estimate_t;
|
||||
|
||||
typedef struct {
|
||||
synor_asset_t from_asset;
|
||||
synor_asset_t to_asset;
|
||||
char* rate;
|
||||
char* inverse_rate;
|
||||
int64_t last_updated;
|
||||
char* source;
|
||||
} synor_exchange_rate_t;
|
||||
|
||||
/* ==================== Transaction Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
char* tx_hash;
|
||||
synor_chain_id_t chain;
|
||||
char* from;
|
||||
char* to;
|
||||
char* value;
|
||||
char* data;
|
||||
char* gas_limit;
|
||||
char* gas_price;
|
||||
char* max_fee_per_gas;
|
||||
char* max_priority_fee_per_gas;
|
||||
int32_t nonce;
|
||||
char* signature;
|
||||
} synor_signed_tx_t;
|
||||
|
||||
/* ==================== Client Handle ==================== */
|
||||
|
||||
typedef struct synor_bridge synor_bridge_t;
|
||||
|
||||
/* ==================== Lifecycle Functions ==================== */
|
||||
|
||||
synor_bridge_t* synor_bridge_create(const synor_bridge_config_t* config);
|
||||
void synor_bridge_destroy(synor_bridge_t* bridge);
|
||||
bool synor_bridge_is_closed(synor_bridge_t* bridge);
|
||||
bool synor_bridge_health_check(synor_bridge_t* bridge);
|
||||
|
||||
/* ==================== Chain Operations ==================== */
|
||||
|
||||
synor_bridge_error_t synor_bridge_get_supported_chains(synor_bridge_t* bridge,
|
||||
synor_chain_list_t* result);
|
||||
synor_bridge_error_t synor_bridge_get_chain(synor_bridge_t* bridge,
|
||||
synor_chain_id_t chain_id, synor_chain_t* result);
|
||||
bool synor_bridge_is_chain_supported(synor_bridge_t* bridge, synor_chain_id_t chain_id);
|
||||
|
||||
void synor_chain_free(synor_chain_t* chain);
|
||||
void synor_chain_list_free(synor_chain_list_t* list);
|
||||
|
||||
/* ==================== Asset Operations ==================== */
|
||||
|
||||
synor_bridge_error_t synor_bridge_get_supported_assets(synor_bridge_t* bridge,
|
||||
synor_chain_id_t chain_id, synor_asset_list_t* result);
|
||||
synor_bridge_error_t synor_bridge_get_asset(synor_bridge_t* bridge,
|
||||
const char* asset_id, synor_asset_t* result);
|
||||
synor_bridge_error_t synor_bridge_get_wrapped_asset(synor_bridge_t* bridge,
|
||||
const char* original_asset_id, synor_chain_id_t target_chain,
|
||||
synor_wrapped_asset_t* result);
|
||||
|
||||
void synor_asset_free(synor_asset_t* asset);
|
||||
void synor_asset_list_free(synor_asset_list_t* list);
|
||||
void synor_wrapped_asset_free(synor_wrapped_asset_t* asset);
|
||||
|
||||
/* ==================== Fee Operations ==================== */
|
||||
|
||||
synor_bridge_error_t synor_bridge_estimate_fee(synor_bridge_t* bridge,
|
||||
const char* asset, const char* amount,
|
||||
synor_chain_id_t source_chain, synor_chain_id_t target_chain,
|
||||
synor_fee_estimate_t* result);
|
||||
synor_bridge_error_t synor_bridge_get_exchange_rate(synor_bridge_t* bridge,
|
||||
const char* from_asset, const char* to_asset, synor_exchange_rate_t* result);
|
||||
|
||||
void synor_fee_estimate_free(synor_fee_estimate_t* fee);
|
||||
void synor_exchange_rate_free(synor_exchange_rate_t* rate);
|
||||
|
||||
/* ==================== Lock-Mint Flow ==================== */
|
||||
|
||||
synor_bridge_error_t synor_bridge_lock(synor_bridge_t* bridge,
|
||||
const char* asset, const char* amount, synor_chain_id_t target_chain,
|
||||
const synor_lock_options_t* options, synor_lock_receipt_t* result);
|
||||
synor_bridge_error_t synor_bridge_get_lock_proof(synor_bridge_t* bridge,
|
||||
const char* lock_receipt_id, synor_lock_proof_t* result);
|
||||
synor_bridge_error_t synor_bridge_wait_for_lock_proof(synor_bridge_t* bridge,
|
||||
const char* lock_receipt_id, uint32_t poll_interval_ms, uint32_t max_wait_ms,
|
||||
synor_lock_proof_t* result);
|
||||
synor_bridge_error_t synor_bridge_mint(synor_bridge_t* bridge,
|
||||
const synor_lock_proof_t* proof, const char* target_address,
|
||||
const synor_mint_options_t* options, synor_signed_tx_t* result);
|
||||
|
||||
void synor_lock_receipt_free(synor_lock_receipt_t* receipt);
|
||||
void synor_lock_proof_free(synor_lock_proof_t* proof);
|
||||
|
||||
/* ==================== Burn-Unlock Flow ==================== */
|
||||
|
||||
synor_bridge_error_t synor_bridge_burn(synor_bridge_t* bridge,
|
||||
const char* wrapped_asset, const char* amount,
|
||||
const synor_burn_options_t* options, synor_burn_receipt_t* result);
|
||||
synor_bridge_error_t synor_bridge_get_burn_proof(synor_bridge_t* bridge,
|
||||
const char* burn_receipt_id, synor_burn_proof_t* result);
|
||||
synor_bridge_error_t synor_bridge_wait_for_burn_proof(synor_bridge_t* bridge,
|
||||
const char* burn_receipt_id, uint32_t poll_interval_ms, uint32_t max_wait_ms,
|
||||
synor_burn_proof_t* result);
|
||||
synor_bridge_error_t synor_bridge_unlock(synor_bridge_t* bridge,
|
||||
const synor_burn_proof_t* proof, const synor_unlock_options_t* options,
|
||||
synor_signed_tx_t* result);
|
||||
|
||||
void synor_burn_receipt_free(synor_burn_receipt_t* receipt);
|
||||
void synor_burn_proof_free(synor_burn_proof_t* proof);
|
||||
|
||||
/* ==================== Transfer Operations ==================== */
|
||||
|
||||
synor_bridge_error_t synor_bridge_get_transfer(synor_bridge_t* bridge,
|
||||
const char* transfer_id, synor_transfer_t* result);
|
||||
synor_bridge_error_t synor_bridge_get_transfer_status(synor_bridge_t* bridge,
|
||||
const char* transfer_id, synor_transfer_status_t* status);
|
||||
synor_bridge_error_t synor_bridge_list_transfers(synor_bridge_t* bridge,
|
||||
const synor_transfer_filter_t* filter, synor_transfer_list_t* result);
|
||||
synor_bridge_error_t synor_bridge_wait_for_transfer(synor_bridge_t* bridge,
|
||||
const char* transfer_id, uint32_t poll_interval_ms, uint32_t max_wait_ms,
|
||||
synor_transfer_t* result);
|
||||
|
||||
void synor_transfer_free(synor_transfer_t* transfer);
|
||||
void synor_transfer_list_free(synor_transfer_list_t* list);
|
||||
void synor_signed_tx_free(synor_signed_tx_t* tx);
|
||||
|
||||
/* ==================== Convenience Functions ==================== */
|
||||
|
||||
synor_bridge_error_t synor_bridge_bridge_to(synor_bridge_t* bridge,
|
||||
const char* asset, const char* amount, synor_chain_id_t target_chain,
|
||||
const char* target_address, const synor_lock_options_t* lock_options,
|
||||
const synor_mint_options_t* mint_options, synor_transfer_t* result);
|
||||
synor_bridge_error_t synor_bridge_bridge_back(synor_bridge_t* bridge,
|
||||
const char* wrapped_asset, const char* amount,
|
||||
const synor_burn_options_t* burn_options,
|
||||
const synor_unlock_options_t* unlock_options, synor_transfer_t* result);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SYNOR_BRIDGE_H */
|
||||
279
sdk/c/include/synor/database.h
Normal file
279
sdk/c/include/synor/database.h
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
/**
|
||||
* @file database.h
|
||||
* @brief Synor Database SDK for C
|
||||
*
|
||||
* Multi-model database with Key-Value, Document, Vector, and Time Series stores.
|
||||
*/
|
||||
|
||||
#ifndef SYNOR_DATABASE_H
|
||||
#define SYNOR_DATABASE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ==================== Error Types ==================== */
|
||||
|
||||
typedef enum {
|
||||
SYNOR_DB_OK = 0,
|
||||
SYNOR_DB_ERROR_CLIENT_CLOSED,
|
||||
SYNOR_DB_ERROR_NETWORK,
|
||||
SYNOR_DB_ERROR_HTTP,
|
||||
SYNOR_DB_ERROR_ENCODING,
|
||||
SYNOR_DB_ERROR_DECODING,
|
||||
SYNOR_DB_ERROR_INVALID_RESPONSE,
|
||||
SYNOR_DB_ERROR_MEMORY,
|
||||
SYNOR_DB_ERROR_UNKNOWN
|
||||
} synor_db_error_t;
|
||||
|
||||
/* ==================== Configuration ==================== */
|
||||
|
||||
typedef struct {
|
||||
const char* api_key;
|
||||
const char* endpoint; /* Default: https://db.synor.io/v1 */
|
||||
uint32_t timeout_ms; /* Default: 60000 */
|
||||
uint32_t retries; /* Default: 3 */
|
||||
bool debug;
|
||||
} synor_db_config_t;
|
||||
|
||||
/* ==================== Key-Value Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
char* key;
|
||||
char* value; /* JSON string */
|
||||
int32_t ttl;
|
||||
int64_t created_at;
|
||||
int64_t updated_at;
|
||||
} synor_kv_entry_t;
|
||||
|
||||
typedef struct {
|
||||
synor_kv_entry_t* items;
|
||||
size_t count;
|
||||
} synor_kv_list_t;
|
||||
|
||||
/* ==================== Document Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
char* id;
|
||||
char* collection;
|
||||
char* data; /* JSON string */
|
||||
int64_t created_at;
|
||||
int64_t updated_at;
|
||||
} synor_document_t;
|
||||
|
||||
typedef struct {
|
||||
synor_document_t* documents;
|
||||
size_t count;
|
||||
} synor_document_list_t;
|
||||
|
||||
typedef struct {
|
||||
const char* filter; /* JSON string */
|
||||
const char* sort; /* JSON string */
|
||||
int32_t limit;
|
||||
int32_t offset;
|
||||
} synor_document_query_t;
|
||||
|
||||
/* ==================== Vector Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
const char* id;
|
||||
const double* vector;
|
||||
size_t vector_size;
|
||||
const char* metadata; /* JSON string, optional */
|
||||
} synor_vector_entry_t;
|
||||
|
||||
typedef struct {
|
||||
char* id;
|
||||
double score;
|
||||
double* vector;
|
||||
size_t vector_size;
|
||||
char* metadata;
|
||||
} synor_search_result_t;
|
||||
|
||||
typedef struct {
|
||||
synor_search_result_t* results;
|
||||
size_t count;
|
||||
} synor_search_result_list_t;
|
||||
|
||||
/* ==================== Time Series Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
int64_t timestamp;
|
||||
double value;
|
||||
const char* tags; /* JSON string, optional */
|
||||
} synor_data_point_t;
|
||||
|
||||
typedef struct {
|
||||
int64_t start;
|
||||
int64_t end;
|
||||
} synor_time_range_t;
|
||||
|
||||
typedef struct {
|
||||
const char* function; /* avg, sum, min, max, count */
|
||||
const char* interval; /* 1m, 5m, 1h, 1d */
|
||||
} synor_aggregation_t;
|
||||
|
||||
typedef struct {
|
||||
synor_data_point_t* points;
|
||||
size_t count;
|
||||
} synor_data_point_list_t;
|
||||
|
||||
/* ==================== Client Handle ==================== */
|
||||
|
||||
typedef struct synor_database synor_database_t;
|
||||
|
||||
/* ==================== Lifecycle Functions ==================== */
|
||||
|
||||
/**
|
||||
* Create a new database client.
|
||||
*/
|
||||
synor_database_t* synor_database_create(const synor_db_config_t* config);
|
||||
|
||||
/**
|
||||
* Destroy the database client and free resources.
|
||||
*/
|
||||
void synor_database_destroy(synor_database_t* db);
|
||||
|
||||
/**
|
||||
* Check if the client is closed.
|
||||
*/
|
||||
bool synor_database_is_closed(synor_database_t* db);
|
||||
|
||||
/**
|
||||
* Perform a health check.
|
||||
*/
|
||||
bool synor_database_health_check(synor_database_t* db);
|
||||
|
||||
/* ==================== Key-Value Operations ==================== */
|
||||
|
||||
/**
|
||||
* Get a value by key.
|
||||
* Caller must free result->value.
|
||||
*/
|
||||
synor_db_error_t synor_kv_get(synor_database_t* db, const char* key, char** value);
|
||||
|
||||
/**
|
||||
* Set a value for a key.
|
||||
*/
|
||||
synor_db_error_t synor_kv_set(synor_database_t* db, const char* key,
|
||||
const char* value, int32_t ttl);
|
||||
|
||||
/**
|
||||
* Delete a key.
|
||||
*/
|
||||
synor_db_error_t synor_kv_delete(synor_database_t* db, const char* key);
|
||||
|
||||
/**
|
||||
* List keys by prefix.
|
||||
* Caller must free result with synor_kv_list_free.
|
||||
*/
|
||||
synor_db_error_t synor_kv_list(synor_database_t* db, const char* prefix,
|
||||
synor_kv_list_t* result);
|
||||
|
||||
/**
|
||||
* Free a key-value list.
|
||||
*/
|
||||
void synor_kv_list_free(synor_kv_list_t* list);
|
||||
|
||||
/* ==================== Document Operations ==================== */
|
||||
|
||||
/**
|
||||
* Create a document.
|
||||
* Caller must free returned id.
|
||||
*/
|
||||
synor_db_error_t synor_doc_create(synor_database_t* db, const char* collection,
|
||||
const char* document, char** id);
|
||||
|
||||
/**
|
||||
* Get a document by ID.
|
||||
* Caller must free result with synor_document_free.
|
||||
*/
|
||||
synor_db_error_t synor_doc_get(synor_database_t* db, const char* collection,
|
||||
const char* id, synor_document_t* result);
|
||||
|
||||
/**
|
||||
* Update a document.
|
||||
*/
|
||||
synor_db_error_t synor_doc_update(synor_database_t* db, const char* collection,
|
||||
const char* id, const char* update);
|
||||
|
||||
/**
|
||||
* Delete a document.
|
||||
*/
|
||||
synor_db_error_t synor_doc_delete(synor_database_t* db, const char* collection,
|
||||
const char* id);
|
||||
|
||||
/**
|
||||
* Query documents.
|
||||
* Caller must free result with synor_document_list_free.
|
||||
*/
|
||||
synor_db_error_t synor_doc_query(synor_database_t* db, const char* collection,
|
||||
const synor_document_query_t* query, synor_document_list_t* result);
|
||||
|
||||
/**
|
||||
* Free a document.
|
||||
*/
|
||||
void synor_document_free(synor_document_t* doc);
|
||||
|
||||
/**
|
||||
* Free a document list.
|
||||
*/
|
||||
void synor_document_list_free(synor_document_list_t* list);
|
||||
|
||||
/* ==================== Vector Operations ==================== */
|
||||
|
||||
/**
|
||||
* Upsert vectors.
|
||||
*/
|
||||
synor_db_error_t synor_vector_upsert(synor_database_t* db, const char* collection,
|
||||
const synor_vector_entry_t* vectors, size_t count);
|
||||
|
||||
/**
|
||||
* Search for similar vectors.
|
||||
* Caller must free result with synor_search_result_list_free.
|
||||
*/
|
||||
synor_db_error_t synor_vector_search(synor_database_t* db, const char* collection,
|
||||
const double* vector, size_t vector_size, int32_t k,
|
||||
synor_search_result_list_t* result);
|
||||
|
||||
/**
|
||||
* Delete vectors by IDs.
|
||||
*/
|
||||
synor_db_error_t synor_vector_delete(synor_database_t* db, const char* collection,
|
||||
const char** ids, size_t count);
|
||||
|
||||
/**
|
||||
* Free search results.
|
||||
*/
|
||||
void synor_search_result_list_free(synor_search_result_list_t* list);
|
||||
|
||||
/* ==================== Time Series Operations ==================== */
|
||||
|
||||
/**
|
||||
* Write data points.
|
||||
*/
|
||||
synor_db_error_t synor_ts_write(synor_database_t* db, const char* series,
|
||||
const synor_data_point_t* points, size_t count);
|
||||
|
||||
/**
|
||||
* Query data points.
|
||||
* Caller must free result with synor_data_point_list_free.
|
||||
*/
|
||||
synor_db_error_t synor_ts_query(synor_database_t* db, const char* series,
|
||||
const synor_time_range_t* range, const synor_aggregation_t* aggregation,
|
||||
synor_data_point_list_t* result);
|
||||
|
||||
/**
|
||||
* Free data point list.
|
||||
*/
|
||||
void synor_data_point_list_free(synor_data_point_list_t* list);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SYNOR_DATABASE_H */
|
||||
295
sdk/c/include/synor/hosting.h
Normal file
295
sdk/c/include/synor/hosting.h
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
/**
|
||||
* @file hosting.h
|
||||
* @brief Synor Hosting SDK for C
|
||||
*
|
||||
* Decentralized web hosting with domain management, DNS, deployments, and SSL.
|
||||
*/
|
||||
|
||||
#ifndef SYNOR_HOSTING_H
|
||||
#define SYNOR_HOSTING_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ==================== Error Types ==================== */
|
||||
|
||||
typedef enum {
|
||||
SYNOR_HOSTING_OK = 0,
|
||||
SYNOR_HOSTING_ERROR_CLIENT_CLOSED,
|
||||
SYNOR_HOSTING_ERROR_NETWORK,
|
||||
SYNOR_HOSTING_ERROR_HTTP,
|
||||
SYNOR_HOSTING_ERROR_ENCODING,
|
||||
SYNOR_HOSTING_ERROR_DECODING,
|
||||
SYNOR_HOSTING_ERROR_INVALID_RESPONSE,
|
||||
SYNOR_HOSTING_ERROR_MEMORY,
|
||||
SYNOR_HOSTING_ERROR_UNKNOWN
|
||||
} synor_hosting_error_t;
|
||||
|
||||
/* ==================== Configuration ==================== */
|
||||
|
||||
typedef struct {
|
||||
const char* api_key;
|
||||
const char* endpoint; /* Default: https://hosting.synor.io/v1 */
|
||||
uint32_t timeout_ms; /* Default: 60000 */
|
||||
uint32_t retries; /* Default: 3 */
|
||||
bool debug;
|
||||
} synor_hosting_config_t;
|
||||
|
||||
/* ==================== Enums ==================== */
|
||||
|
||||
typedef enum {
|
||||
SYNOR_DOMAIN_PENDING,
|
||||
SYNOR_DOMAIN_ACTIVE,
|
||||
SYNOR_DOMAIN_EXPIRED,
|
||||
SYNOR_DOMAIN_SUSPENDED
|
||||
} synor_domain_status_t;
|
||||
|
||||
typedef enum {
|
||||
SYNOR_DEPLOYMENT_PENDING,
|
||||
SYNOR_DEPLOYMENT_BUILDING,
|
||||
SYNOR_DEPLOYMENT_DEPLOYING,
|
||||
SYNOR_DEPLOYMENT_ACTIVE,
|
||||
SYNOR_DEPLOYMENT_FAILED,
|
||||
SYNOR_DEPLOYMENT_INACTIVE
|
||||
} synor_deployment_status_t;
|
||||
|
||||
typedef enum {
|
||||
SYNOR_CERT_PENDING,
|
||||
SYNOR_CERT_ISSUED,
|
||||
SYNOR_CERT_EXPIRED,
|
||||
SYNOR_CERT_REVOKED
|
||||
} synor_cert_status_t;
|
||||
|
||||
typedef enum {
|
||||
SYNOR_DNS_A,
|
||||
SYNOR_DNS_AAAA,
|
||||
SYNOR_DNS_CNAME,
|
||||
SYNOR_DNS_TXT,
|
||||
SYNOR_DNS_MX,
|
||||
SYNOR_DNS_NS,
|
||||
SYNOR_DNS_SRV,
|
||||
SYNOR_DNS_CAA
|
||||
} synor_dns_record_type_t;
|
||||
|
||||
/* ==================== Domain Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
char* cid;
|
||||
char** ipv4;
|
||||
size_t ipv4_count;
|
||||
char** ipv6;
|
||||
size_t ipv6_count;
|
||||
char* cname;
|
||||
char** txt;
|
||||
size_t txt_count;
|
||||
} synor_domain_record_t;
|
||||
|
||||
typedef struct {
|
||||
char* name;
|
||||
synor_domain_status_t status;
|
||||
char* owner;
|
||||
int64_t registered_at;
|
||||
int64_t expires_at;
|
||||
bool auto_renew;
|
||||
synor_domain_record_t* records;
|
||||
} synor_domain_t;
|
||||
|
||||
typedef struct {
|
||||
synor_domain_t* domains;
|
||||
size_t count;
|
||||
} synor_domain_list_t;
|
||||
|
||||
typedef struct {
|
||||
char* name;
|
||||
bool available;
|
||||
double price;
|
||||
bool premium;
|
||||
} synor_domain_availability_t;
|
||||
|
||||
typedef struct {
|
||||
int32_t years;
|
||||
bool auto_renew;
|
||||
} synor_register_domain_options_t;
|
||||
|
||||
/* ==================== DNS Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
synor_dns_record_type_t type;
|
||||
char* name;
|
||||
char* value;
|
||||
int32_t ttl;
|
||||
int32_t priority;
|
||||
} synor_dns_record_t;
|
||||
|
||||
typedef struct {
|
||||
char* domain;
|
||||
synor_dns_record_t* records;
|
||||
size_t record_count;
|
||||
int64_t updated_at;
|
||||
} synor_dns_zone_t;
|
||||
|
||||
/* ==================== Deployment Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
char* id;
|
||||
char* domain;
|
||||
char* cid;
|
||||
synor_deployment_status_t status;
|
||||
char* url;
|
||||
int64_t created_at;
|
||||
int64_t updated_at;
|
||||
char* build_logs;
|
||||
char* error_message;
|
||||
} synor_deployment_t;
|
||||
|
||||
typedef struct {
|
||||
synor_deployment_t* deployments;
|
||||
size_t count;
|
||||
} synor_deployment_list_t;
|
||||
|
||||
typedef struct {
|
||||
const char* domain;
|
||||
const char* subdomain;
|
||||
bool spa;
|
||||
bool clean_urls;
|
||||
bool trailing_slash;
|
||||
} synor_deploy_options_t;
|
||||
|
||||
/* ==================== SSL Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
char* domain;
|
||||
synor_cert_status_t status;
|
||||
bool auto_renew;
|
||||
char* issuer;
|
||||
int64_t issued_at;
|
||||
int64_t expires_at;
|
||||
char* fingerprint;
|
||||
} synor_certificate_t;
|
||||
|
||||
typedef struct {
|
||||
bool include_www;
|
||||
bool auto_renew;
|
||||
} synor_provision_ssl_options_t;
|
||||
|
||||
/* ==================== Analytics Types ==================== */
|
||||
|
||||
typedef struct {
|
||||
char* path;
|
||||
int64_t views;
|
||||
} synor_page_view_t;
|
||||
|
||||
typedef struct {
|
||||
char* domain;
|
||||
char* period;
|
||||
int64_t page_views;
|
||||
int64_t unique_visitors;
|
||||
int64_t bandwidth;
|
||||
synor_page_view_t* top_pages;
|
||||
size_t top_pages_count;
|
||||
} synor_analytics_data_t;
|
||||
|
||||
typedef struct {
|
||||
const char* period;
|
||||
const char* start;
|
||||
const char* end;
|
||||
} synor_analytics_options_t;
|
||||
|
||||
/* ==================== Client Handle ==================== */
|
||||
|
||||
typedef struct synor_hosting synor_hosting_t;
|
||||
|
||||
/* ==================== Lifecycle Functions ==================== */
|
||||
|
||||
synor_hosting_t* synor_hosting_create(const synor_hosting_config_t* config);
|
||||
void synor_hosting_destroy(synor_hosting_t* hosting);
|
||||
bool synor_hosting_is_closed(synor_hosting_t* hosting);
|
||||
bool synor_hosting_health_check(synor_hosting_t* hosting);
|
||||
|
||||
/* ==================== Domain Operations ==================== */
|
||||
|
||||
synor_hosting_error_t synor_hosting_check_availability(synor_hosting_t* hosting,
|
||||
const char* name, synor_domain_availability_t* result);
|
||||
synor_hosting_error_t synor_hosting_register_domain(synor_hosting_t* hosting,
|
||||
const char* name, const synor_register_domain_options_t* options,
|
||||
synor_domain_t* result);
|
||||
synor_hosting_error_t synor_hosting_get_domain(synor_hosting_t* hosting,
|
||||
const char* name, synor_domain_t* result);
|
||||
synor_hosting_error_t synor_hosting_list_domains(synor_hosting_t* hosting,
|
||||
synor_domain_list_t* result);
|
||||
synor_hosting_error_t synor_hosting_resolve_domain(synor_hosting_t* hosting,
|
||||
const char* name, synor_domain_record_t* result);
|
||||
synor_hosting_error_t synor_hosting_renew_domain(synor_hosting_t* hosting,
|
||||
const char* name, int32_t years, synor_domain_t* result);
|
||||
|
||||
void synor_domain_free(synor_domain_t* domain);
|
||||
void synor_domain_list_free(synor_domain_list_t* list);
|
||||
void synor_domain_record_free(synor_domain_record_t* record);
|
||||
void synor_domain_availability_free(synor_domain_availability_t* avail);
|
||||
|
||||
/* ==================== DNS Operations ==================== */
|
||||
|
||||
synor_hosting_error_t synor_hosting_get_dns_zone(synor_hosting_t* hosting,
|
||||
const char* domain, synor_dns_zone_t* result);
|
||||
synor_hosting_error_t synor_hosting_set_dns_records(synor_hosting_t* hosting,
|
||||
const char* domain, const synor_dns_record_t* records, size_t count,
|
||||
synor_dns_zone_t* result);
|
||||
synor_hosting_error_t synor_hosting_add_dns_record(synor_hosting_t* hosting,
|
||||
const char* domain, const synor_dns_record_t* record, synor_dns_zone_t* result);
|
||||
synor_hosting_error_t synor_hosting_delete_dns_record(synor_hosting_t* hosting,
|
||||
const char* domain, const char* record_type, const char* name,
|
||||
synor_dns_zone_t* result);
|
||||
|
||||
void synor_dns_zone_free(synor_dns_zone_t* zone);
|
||||
|
||||
/* ==================== Deployment Operations ==================== */
|
||||
|
||||
synor_hosting_error_t synor_hosting_deploy(synor_hosting_t* hosting,
|
||||
const char* cid, const synor_deploy_options_t* options,
|
||||
synor_deployment_t* result);
|
||||
synor_hosting_error_t synor_hosting_get_deployment(synor_hosting_t* hosting,
|
||||
const char* id, synor_deployment_t* result);
|
||||
synor_hosting_error_t synor_hosting_list_deployments(synor_hosting_t* hosting,
|
||||
const char* domain, synor_deployment_list_t* result);
|
||||
synor_hosting_error_t synor_hosting_rollback(synor_hosting_t* hosting,
|
||||
const char* domain, const char* deployment_id, synor_deployment_t* result);
|
||||
synor_hosting_error_t synor_hosting_delete_deployment(synor_hosting_t* hosting,
|
||||
const char* id);
|
||||
|
||||
void synor_deployment_free(synor_deployment_t* deployment);
|
||||
void synor_deployment_list_free(synor_deployment_list_t* list);
|
||||
|
||||
/* ==================== SSL Operations ==================== */
|
||||
|
||||
synor_hosting_error_t synor_hosting_provision_ssl(synor_hosting_t* hosting,
|
||||
const char* domain, const synor_provision_ssl_options_t* options,
|
||||
synor_certificate_t* result);
|
||||
synor_hosting_error_t synor_hosting_get_certificate(synor_hosting_t* hosting,
|
||||
const char* domain, synor_certificate_t* result);
|
||||
synor_hosting_error_t synor_hosting_renew_certificate(synor_hosting_t* hosting,
|
||||
const char* domain, synor_certificate_t* result);
|
||||
synor_hosting_error_t synor_hosting_delete_certificate(synor_hosting_t* hosting,
|
||||
const char* domain);
|
||||
|
||||
void synor_certificate_free(synor_certificate_t* cert);
|
||||
|
||||
/* ==================== Analytics Operations ==================== */
|
||||
|
||||
synor_hosting_error_t synor_hosting_get_analytics(synor_hosting_t* hosting,
|
||||
const char* domain, const synor_analytics_options_t* options,
|
||||
synor_analytics_data_t* result);
|
||||
synor_hosting_error_t synor_hosting_purge_cache(synor_hosting_t* hosting,
|
||||
const char* domain, const char** paths, size_t path_count, int64_t* purged);
|
||||
|
||||
void synor_analytics_data_free(synor_analytics_data_t* data);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SYNOR_HOSTING_H */
|
||||
441
sdk/c/src/bridge/bridge.c
Normal file
441
sdk/c/src/bridge/bridge.c
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
/**
|
||||
* @file bridge.c
|
||||
* @brief Synor Bridge SDK implementation
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include "synor/bridge.h"
|
||||
|
||||
/* Forward declarations from common.c */
|
||||
typedef struct synor_http_client synor_http_client_t;
|
||||
synor_http_client_t* synor_http_client_create(
|
||||
const char* api_key, const char* endpoint,
|
||||
uint32_t timeout_ms, uint32_t retries, bool debug);
|
||||
void synor_http_client_destroy(synor_http_client_t* client);
|
||||
synor_error_t synor_http_get(synor_http_client_t* client, const char* path,
|
||||
char** response, size_t* response_size);
|
||||
synor_error_t synor_http_post(synor_http_client_t* client, const char* path,
|
||||
const char* body, char** response, size_t* response_size);
|
||||
char* synor_strdup(const char* s);
|
||||
char* synor_url_encode(const char* s);
|
||||
|
||||
/* Internal bridge structure */
|
||||
struct synor_bridge {
|
||||
synor_http_client_t* http;
|
||||
bool closed;
|
||||
bool debug;
|
||||
};
|
||||
|
||||
static const char* chain_id_to_string(synor_chain_id_t id) {
|
||||
switch (id) {
|
||||
case SYNOR_CHAIN_SYNOR: return "synor";
|
||||
case SYNOR_CHAIN_ETHEREUM: return "ethereum";
|
||||
case SYNOR_CHAIN_POLYGON: return "polygon";
|
||||
case SYNOR_CHAIN_ARBITRUM: return "arbitrum";
|
||||
case SYNOR_CHAIN_OPTIMISM: return "optimism";
|
||||
case SYNOR_CHAIN_BSC: return "bsc";
|
||||
case SYNOR_CHAIN_AVALANCHE: return "avalanche";
|
||||
case SYNOR_CHAIN_SOLANA: return "solana";
|
||||
case SYNOR_CHAIN_COSMOS: return "cosmos";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
synor_bridge_t* synor_bridge_create(const synor_bridge_config_t* config) {
|
||||
if (!config || !config->api_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
synor_bridge_t* bridge = calloc(1, sizeof(synor_bridge_t));
|
||||
if (!bridge) return NULL;
|
||||
|
||||
const char* endpoint = config->endpoint ?
|
||||
config->endpoint : "https://bridge.synor.io/v1";
|
||||
uint32_t timeout = config->timeout_ms > 0 ? config->timeout_ms : 60000;
|
||||
uint32_t retries = config->retries > 0 ? config->retries : 3;
|
||||
|
||||
bridge->http = synor_http_client_create(
|
||||
config->api_key, endpoint, timeout, retries, config->debug);
|
||||
|
||||
if (!bridge->http) {
|
||||
free(bridge);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bridge->closed = false;
|
||||
bridge->debug = config->debug;
|
||||
return bridge;
|
||||
}
|
||||
|
||||
void synor_bridge_destroy(synor_bridge_t* bridge) {
|
||||
if (!bridge) return;
|
||||
bridge->closed = true;
|
||||
synor_http_client_destroy(bridge->http);
|
||||
free(bridge);
|
||||
}
|
||||
|
||||
bool synor_bridge_is_closed(synor_bridge_t* bridge) {
|
||||
return bridge ? bridge->closed : true;
|
||||
}
|
||||
|
||||
bool synor_bridge_health_check(synor_bridge_t* bridge) {
|
||||
if (!bridge || bridge->closed) return false;
|
||||
|
||||
char* response = NULL;
|
||||
size_t response_size = 0;
|
||||
|
||||
if (synor_http_get(bridge->http, "/health", &response, &response_size) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool healthy = response && strstr(response, "\"healthy\"") != NULL;
|
||||
free(response);
|
||||
return healthy;
|
||||
}
|
||||
|
||||
/* ==================== Chain Operations ==================== */
|
||||
|
||||
synor_bridge_error_t synor_bridge_get_supported_chains(synor_bridge_t* bridge,
|
||||
synor_chain_list_t* result) {
|
||||
(void)bridge; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_bridge_error_t synor_bridge_get_chain(synor_bridge_t* bridge,
|
||||
synor_chain_id_t chain_id, synor_chain_t* result) {
|
||||
(void)bridge; (void)chain_id; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
bool synor_bridge_is_chain_supported(synor_bridge_t* bridge, synor_chain_id_t chain_id) {
|
||||
synor_chain_t chain;
|
||||
if (synor_bridge_get_chain(bridge, chain_id, &chain) != SYNOR_BRIDGE_OK) {
|
||||
return false;
|
||||
}
|
||||
bool supported = chain.supported;
|
||||
synor_chain_free(&chain);
|
||||
return supported;
|
||||
}
|
||||
|
||||
void synor_chain_free(synor_chain_t* chain) {
|
||||
if (!chain) return;
|
||||
free(chain->name);
|
||||
free(chain->rpc_url);
|
||||
free(chain->explorer_url);
|
||||
free(chain->native_currency.name);
|
||||
free(chain->native_currency.symbol);
|
||||
}
|
||||
|
||||
void synor_chain_list_free(synor_chain_list_t* list) {
|
||||
if (!list) return;
|
||||
for (size_t i = 0; i < list->count; i++) {
|
||||
synor_chain_free(&list->chains[i]);
|
||||
}
|
||||
free(list->chains);
|
||||
list->chains = NULL;
|
||||
list->count = 0;
|
||||
}
|
||||
|
||||
/* ==================== Asset Operations ==================== */
|
||||
|
||||
synor_bridge_error_t synor_bridge_get_supported_assets(synor_bridge_t* bridge,
|
||||
synor_chain_id_t chain_id, synor_asset_list_t* result) {
|
||||
(void)bridge; (void)chain_id; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_bridge_error_t synor_bridge_get_asset(synor_bridge_t* bridge,
|
||||
const char* asset_id, synor_asset_t* result) {
|
||||
(void)bridge; (void)asset_id; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_bridge_error_t synor_bridge_get_wrapped_asset(synor_bridge_t* bridge,
|
||||
const char* original_asset_id, synor_chain_id_t target_chain,
|
||||
synor_wrapped_asset_t* result) {
|
||||
(void)bridge; (void)original_asset_id; (void)target_chain; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
void synor_asset_free(synor_asset_t* asset) {
|
||||
if (!asset) return;
|
||||
free(asset->id);
|
||||
free(asset->symbol);
|
||||
free(asset->name);
|
||||
free(asset->contract_address);
|
||||
free(asset->logo_url);
|
||||
}
|
||||
|
||||
void synor_asset_list_free(synor_asset_list_t* list) {
|
||||
if (!list) return;
|
||||
for (size_t i = 0; i < list->count; i++) {
|
||||
synor_asset_free(&list->assets[i]);
|
||||
}
|
||||
free(list->assets);
|
||||
list->assets = NULL;
|
||||
list->count = 0;
|
||||
}
|
||||
|
||||
void synor_wrapped_asset_free(synor_wrapped_asset_t* asset) {
|
||||
if (!asset) return;
|
||||
synor_asset_free(&asset->original_asset);
|
||||
synor_asset_free(&asset->wrapped_asset);
|
||||
free(asset->bridge_contract);
|
||||
}
|
||||
|
||||
/* ==================== Fee Operations ==================== */
|
||||
|
||||
synor_bridge_error_t synor_bridge_estimate_fee(synor_bridge_t* bridge,
|
||||
const char* asset, const char* amount,
|
||||
synor_chain_id_t source_chain, synor_chain_id_t target_chain,
|
||||
synor_fee_estimate_t* result) {
|
||||
if (!bridge || bridge->closed) return SYNOR_BRIDGE_ERROR_CLIENT_CLOSED;
|
||||
if (!asset || !amount || !result) return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
|
||||
char body[1024];
|
||||
snprintf(body, sizeof(body),
|
||||
"{\"asset\":\"%s\",\"amount\":\"%s\",\"sourceChain\":\"%s\",\"targetChain\":\"%s\"}",
|
||||
asset, amount, chain_id_to_string(source_chain), chain_id_to_string(target_chain));
|
||||
|
||||
char* response = NULL;
|
||||
size_t response_size = 0;
|
||||
|
||||
int err = synor_http_post(bridge->http, "/fees/estimate", body, &response, &response_size);
|
||||
if (err != 0) {
|
||||
free(response);
|
||||
return SYNOR_BRIDGE_ERROR_HTTP;
|
||||
}
|
||||
|
||||
/* TODO: Parse JSON response */
|
||||
free(response);
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_bridge_error_t synor_bridge_get_exchange_rate(synor_bridge_t* bridge,
|
||||
const char* from_asset, const char* to_asset, synor_exchange_rate_t* result) {
|
||||
(void)bridge; (void)from_asset; (void)to_asset; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
void synor_fee_estimate_free(synor_fee_estimate_t* fee) {
|
||||
if (!fee) return;
|
||||
free(fee->bridge_fee);
|
||||
free(fee->gas_fee_source);
|
||||
free(fee->gas_fee_target);
|
||||
free(fee->total_fee);
|
||||
synor_asset_free(&fee->fee_asset);
|
||||
free(fee->exchange_rate);
|
||||
}
|
||||
|
||||
void synor_exchange_rate_free(synor_exchange_rate_t* rate) {
|
||||
if (!rate) return;
|
||||
synor_asset_free(&rate->from_asset);
|
||||
synor_asset_free(&rate->to_asset);
|
||||
free(rate->rate);
|
||||
free(rate->inverse_rate);
|
||||
free(rate->source);
|
||||
}
|
||||
|
||||
/* ==================== Lock-Mint Flow ==================== */
|
||||
|
||||
synor_bridge_error_t synor_bridge_lock(synor_bridge_t* bridge,
|
||||
const char* asset, const char* amount, synor_chain_id_t target_chain,
|
||||
const synor_lock_options_t* options, synor_lock_receipt_t* result) {
|
||||
(void)bridge; (void)asset; (void)amount; (void)target_chain;
|
||||
(void)options; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_bridge_error_t synor_bridge_get_lock_proof(synor_bridge_t* bridge,
|
||||
const char* lock_receipt_id, synor_lock_proof_t* result) {
|
||||
(void)bridge; (void)lock_receipt_id; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_bridge_error_t synor_bridge_wait_for_lock_proof(synor_bridge_t* bridge,
|
||||
const char* lock_receipt_id, uint32_t poll_interval_ms, uint32_t max_wait_ms,
|
||||
synor_lock_proof_t* result) {
|
||||
(void)bridge; (void)lock_receipt_id; (void)poll_interval_ms;
|
||||
(void)max_wait_ms; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_bridge_error_t synor_bridge_mint(synor_bridge_t* bridge,
|
||||
const synor_lock_proof_t* proof, const char* target_address,
|
||||
const synor_mint_options_t* options, synor_signed_tx_t* result) {
|
||||
(void)bridge; (void)proof; (void)target_address; (void)options; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
void synor_lock_receipt_free(synor_lock_receipt_t* receipt) {
|
||||
if (!receipt) return;
|
||||
free(receipt->id);
|
||||
free(receipt->tx_hash);
|
||||
synor_asset_free(&receipt->asset);
|
||||
free(receipt->amount);
|
||||
free(receipt->sender);
|
||||
free(receipt->recipient);
|
||||
}
|
||||
|
||||
void synor_lock_proof_free(synor_lock_proof_t* proof) {
|
||||
if (!proof) return;
|
||||
synor_lock_receipt_free(&proof->lock_receipt);
|
||||
for (size_t i = 0; i < proof->merkle_proof_count; i++) {
|
||||
free(proof->merkle_proof[i]);
|
||||
}
|
||||
free(proof->merkle_proof);
|
||||
free(proof->block_header);
|
||||
for (size_t i = 0; i < proof->signature_count; i++) {
|
||||
free(proof->signatures[i].validator);
|
||||
free(proof->signatures[i].signature);
|
||||
}
|
||||
free(proof->signatures);
|
||||
}
|
||||
|
||||
/* ==================== Burn-Unlock Flow ==================== */
|
||||
|
||||
synor_bridge_error_t synor_bridge_burn(synor_bridge_t* bridge,
|
||||
const char* wrapped_asset, const char* amount,
|
||||
const synor_burn_options_t* options, synor_burn_receipt_t* result) {
|
||||
(void)bridge; (void)wrapped_asset; (void)amount; (void)options; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_bridge_error_t synor_bridge_get_burn_proof(synor_bridge_t* bridge,
|
||||
const char* burn_receipt_id, synor_burn_proof_t* result) {
|
||||
(void)bridge; (void)burn_receipt_id; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_bridge_error_t synor_bridge_wait_for_burn_proof(synor_bridge_t* bridge,
|
||||
const char* burn_receipt_id, uint32_t poll_interval_ms, uint32_t max_wait_ms,
|
||||
synor_burn_proof_t* result) {
|
||||
(void)bridge; (void)burn_receipt_id; (void)poll_interval_ms;
|
||||
(void)max_wait_ms; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_bridge_error_t synor_bridge_unlock(synor_bridge_t* bridge,
|
||||
const synor_burn_proof_t* proof, const synor_unlock_options_t* options,
|
||||
synor_signed_tx_t* result) {
|
||||
(void)bridge; (void)proof; (void)options; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
void synor_burn_receipt_free(synor_burn_receipt_t* receipt) {
|
||||
if (!receipt) return;
|
||||
free(receipt->id);
|
||||
free(receipt->tx_hash);
|
||||
synor_asset_free(&receipt->wrapped_asset);
|
||||
synor_asset_free(&receipt->original_asset);
|
||||
free(receipt->amount);
|
||||
free(receipt->sender);
|
||||
free(receipt->recipient);
|
||||
}
|
||||
|
||||
void synor_burn_proof_free(synor_burn_proof_t* proof) {
|
||||
if (!proof) return;
|
||||
synor_burn_receipt_free(&proof->burn_receipt);
|
||||
for (size_t i = 0; i < proof->merkle_proof_count; i++) {
|
||||
free(proof->merkle_proof[i]);
|
||||
}
|
||||
free(proof->merkle_proof);
|
||||
free(proof->block_header);
|
||||
for (size_t i = 0; i < proof->signature_count; i++) {
|
||||
free(proof->signatures[i].validator);
|
||||
free(proof->signatures[i].signature);
|
||||
}
|
||||
free(proof->signatures);
|
||||
}
|
||||
|
||||
/* ==================== Transfer Operations ==================== */
|
||||
|
||||
synor_bridge_error_t synor_bridge_get_transfer(synor_bridge_t* bridge,
|
||||
const char* transfer_id, synor_transfer_t* result) {
|
||||
(void)bridge; (void)transfer_id; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_bridge_error_t synor_bridge_get_transfer_status(synor_bridge_t* bridge,
|
||||
const char* transfer_id, synor_transfer_status_t* status) {
|
||||
synor_transfer_t transfer;
|
||||
synor_bridge_error_t err = synor_bridge_get_transfer(bridge, transfer_id, &transfer);
|
||||
if (err != SYNOR_BRIDGE_OK) return err;
|
||||
*status = transfer.status;
|
||||
synor_transfer_free(&transfer);
|
||||
return SYNOR_BRIDGE_OK;
|
||||
}
|
||||
|
||||
synor_bridge_error_t synor_bridge_list_transfers(synor_bridge_t* bridge,
|
||||
const synor_transfer_filter_t* filter, synor_transfer_list_t* result) {
|
||||
(void)bridge; (void)filter; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_bridge_error_t synor_bridge_wait_for_transfer(synor_bridge_t* bridge,
|
||||
const char* transfer_id, uint32_t poll_interval_ms, uint32_t max_wait_ms,
|
||||
synor_transfer_t* result) {
|
||||
(void)bridge; (void)transfer_id; (void)poll_interval_ms;
|
||||
(void)max_wait_ms; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
void synor_transfer_free(synor_transfer_t* transfer) {
|
||||
if (!transfer) return;
|
||||
free(transfer->id);
|
||||
synor_asset_free(&transfer->asset);
|
||||
free(transfer->amount);
|
||||
free(transfer->sender);
|
||||
free(transfer->recipient);
|
||||
free(transfer->source_tx_hash);
|
||||
free(transfer->target_tx_hash);
|
||||
free(transfer->fee);
|
||||
synor_asset_free(&transfer->fee_asset);
|
||||
free(transfer->error_message);
|
||||
}
|
||||
|
||||
void synor_transfer_list_free(synor_transfer_list_t* list) {
|
||||
if (!list) return;
|
||||
for (size_t i = 0; i < list->count; i++) {
|
||||
synor_transfer_free(&list->transfers[i]);
|
||||
}
|
||||
free(list->transfers);
|
||||
list->transfers = NULL;
|
||||
list->count = 0;
|
||||
}
|
||||
|
||||
void synor_signed_tx_free(synor_signed_tx_t* tx) {
|
||||
if (!tx) return;
|
||||
free(tx->tx_hash);
|
||||
free(tx->from);
|
||||
free(tx->to);
|
||||
free(tx->value);
|
||||
free(tx->data);
|
||||
free(tx->gas_limit);
|
||||
free(tx->gas_price);
|
||||
free(tx->max_fee_per_gas);
|
||||
free(tx->max_priority_fee_per_gas);
|
||||
free(tx->signature);
|
||||
}
|
||||
|
||||
/* ==================== Convenience Functions ==================== */
|
||||
|
||||
synor_bridge_error_t synor_bridge_bridge_to(synor_bridge_t* bridge,
|
||||
const char* asset, const char* amount, synor_chain_id_t target_chain,
|
||||
const char* target_address, const synor_lock_options_t* lock_options,
|
||||
const synor_mint_options_t* mint_options, synor_transfer_t* result) {
|
||||
(void)bridge; (void)asset; (void)amount; (void)target_chain;
|
||||
(void)target_address; (void)lock_options; (void)mint_options; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_bridge_error_t synor_bridge_bridge_back(synor_bridge_t* bridge,
|
||||
const char* wrapped_asset, const char* amount,
|
||||
const synor_burn_options_t* burn_options,
|
||||
const synor_unlock_options_t* unlock_options, synor_transfer_t* result) {
|
||||
(void)bridge; (void)wrapped_asset; (void)amount;
|
||||
(void)burn_options; (void)unlock_options; (void)result;
|
||||
return SYNOR_BRIDGE_ERROR_UNKNOWN;
|
||||
}
|
||||
303
sdk/c/src/database/database.c
Normal file
303
sdk/c/src/database/database.c
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
/**
|
||||
* @file database.c
|
||||
* @brief Synor Database SDK implementation
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include "synor/database.h"
|
||||
|
||||
/* Forward declarations from common.c */
|
||||
typedef struct synor_http_client synor_http_client_t;
|
||||
synor_http_client_t* synor_http_client_create(
|
||||
const char* api_key, const char* endpoint,
|
||||
uint32_t timeout_ms, uint32_t retries, bool debug);
|
||||
void synor_http_client_destroy(synor_http_client_t* client);
|
||||
synor_error_t synor_http_get(synor_http_client_t* client, const char* path,
|
||||
char** response, size_t* response_size);
|
||||
synor_error_t synor_http_post(synor_http_client_t* client, const char* path,
|
||||
const char* body, char** response, size_t* response_size);
|
||||
synor_error_t synor_http_put(synor_http_client_t* client, const char* path,
|
||||
const char* body, char** response, size_t* response_size);
|
||||
synor_error_t synor_http_delete(synor_http_client_t* client, const char* path,
|
||||
char** response, size_t* response_size);
|
||||
char* synor_strdup(const char* s);
|
||||
char* synor_url_encode(const char* s);
|
||||
|
||||
/* Internal database structure */
|
||||
struct synor_database {
|
||||
synor_http_client_t* http;
|
||||
bool closed;
|
||||
};
|
||||
|
||||
synor_database_t* synor_database_create(const synor_db_config_t* config) {
|
||||
if (!config || !config->api_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
synor_database_t* db = calloc(1, sizeof(synor_database_t));
|
||||
if (!db) return NULL;
|
||||
|
||||
const char* endpoint = config->endpoint ?
|
||||
config->endpoint : "https://db.synor.io/v1";
|
||||
uint32_t timeout = config->timeout_ms > 0 ? config->timeout_ms : 60000;
|
||||
uint32_t retries = config->retries > 0 ? config->retries : 3;
|
||||
|
||||
db->http = synor_http_client_create(
|
||||
config->api_key, endpoint, timeout, retries, config->debug);
|
||||
|
||||
if (!db->http) {
|
||||
free(db);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
db->closed = false;
|
||||
return db;
|
||||
}
|
||||
|
||||
void synor_database_destroy(synor_database_t* db) {
|
||||
if (!db) return;
|
||||
db->closed = true;
|
||||
synor_http_client_destroy(db->http);
|
||||
free(db);
|
||||
}
|
||||
|
||||
bool synor_database_is_closed(synor_database_t* db) {
|
||||
return db ? db->closed : true;
|
||||
}
|
||||
|
||||
bool synor_database_health_check(synor_database_t* db) {
|
||||
if (!db || db->closed) return false;
|
||||
|
||||
char* response = NULL;
|
||||
size_t response_size = 0;
|
||||
|
||||
if (synor_http_get(db->http, "/health", &response, &response_size) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool healthy = response && strstr(response, "\"healthy\"") != NULL;
|
||||
free(response);
|
||||
return healthy;
|
||||
}
|
||||
|
||||
/* ==================== Key-Value Operations ==================== */
|
||||
|
||||
synor_db_error_t synor_kv_get(synor_database_t* db, const char* key, char** value) {
|
||||
if (!db || db->closed) return SYNOR_DB_ERROR_CLIENT_CLOSED;
|
||||
if (!key || !value) return SYNOR_DB_ERROR_UNKNOWN;
|
||||
|
||||
char* encoded_key = synor_url_encode(key);
|
||||
if (!encoded_key) return SYNOR_DB_ERROR_MEMORY;
|
||||
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/kv/%s", encoded_key);
|
||||
free(encoded_key);
|
||||
|
||||
char* response = NULL;
|
||||
size_t response_size = 0;
|
||||
|
||||
int err = synor_http_get(db->http, path, &response, &response_size);
|
||||
if (err != 0) {
|
||||
free(response);
|
||||
return SYNOR_DB_ERROR_HTTP;
|
||||
}
|
||||
|
||||
/* Parse JSON response to extract value - simplified */
|
||||
*value = response;
|
||||
return SYNOR_DB_OK;
|
||||
}
|
||||
|
||||
synor_db_error_t synor_kv_set(synor_database_t* db, const char* key,
|
||||
const char* value, int32_t ttl) {
|
||||
if (!db || db->closed) return SYNOR_DB_ERROR_CLIENT_CLOSED;
|
||||
if (!key || !value) return SYNOR_DB_ERROR_UNKNOWN;
|
||||
|
||||
char* encoded_key = synor_url_encode(key);
|
||||
if (!encoded_key) return SYNOR_DB_ERROR_MEMORY;
|
||||
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/kv/%s", encoded_key);
|
||||
free(encoded_key);
|
||||
|
||||
char body[4096];
|
||||
if (ttl > 0) {
|
||||
snprintf(body, sizeof(body), "{\"key\":\"%s\",\"value\":%s,\"ttl\":%d}",
|
||||
key, value, ttl);
|
||||
} else {
|
||||
snprintf(body, sizeof(body), "{\"key\":\"%s\",\"value\":%s}", key, value);
|
||||
}
|
||||
|
||||
char* response = NULL;
|
||||
size_t response_size = 0;
|
||||
|
||||
int err = synor_http_put(db->http, path, body, &response, &response_size);
|
||||
free(response);
|
||||
|
||||
return err == 0 ? SYNOR_DB_OK : SYNOR_DB_ERROR_HTTP;
|
||||
}
|
||||
|
||||
synor_db_error_t synor_kv_delete(synor_database_t* db, const char* key) {
|
||||
if (!db || db->closed) return SYNOR_DB_ERROR_CLIENT_CLOSED;
|
||||
if (!key) return SYNOR_DB_ERROR_UNKNOWN;
|
||||
|
||||
char* encoded_key = synor_url_encode(key);
|
||||
if (!encoded_key) return SYNOR_DB_ERROR_MEMORY;
|
||||
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/kv/%s", encoded_key);
|
||||
free(encoded_key);
|
||||
|
||||
char* response = NULL;
|
||||
size_t response_size = 0;
|
||||
|
||||
int err = synor_http_delete(db->http, path, &response, &response_size);
|
||||
free(response);
|
||||
|
||||
return err == 0 ? SYNOR_DB_OK : SYNOR_DB_ERROR_HTTP;
|
||||
}
|
||||
|
||||
synor_db_error_t synor_kv_list(synor_database_t* db, const char* prefix,
|
||||
synor_kv_list_t* result) {
|
||||
(void)db; (void)prefix; (void)result;
|
||||
return SYNOR_DB_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
void synor_kv_list_free(synor_kv_list_t* list) {
|
||||
if (!list) return;
|
||||
for (size_t i = 0; i < list->count; i++) {
|
||||
free(list->items[i].key);
|
||||
free(list->items[i].value);
|
||||
}
|
||||
free(list->items);
|
||||
list->items = NULL;
|
||||
list->count = 0;
|
||||
}
|
||||
|
||||
/* ==================== Document Operations ==================== */
|
||||
|
||||
synor_db_error_t synor_doc_create(synor_database_t* db, const char* collection,
|
||||
const char* document, char** id) {
|
||||
if (!db || db->closed) return SYNOR_DB_ERROR_CLIENT_CLOSED;
|
||||
if (!collection || !document || !id) return SYNOR_DB_ERROR_UNKNOWN;
|
||||
|
||||
char* encoded = synor_url_encode(collection);
|
||||
if (!encoded) return SYNOR_DB_ERROR_MEMORY;
|
||||
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/collections/%s/documents", encoded);
|
||||
free(encoded);
|
||||
|
||||
char* response = NULL;
|
||||
size_t response_size = 0;
|
||||
|
||||
int err = synor_http_post(db->http, path, document, &response, &response_size);
|
||||
if (err != 0) {
|
||||
free(response);
|
||||
return SYNOR_DB_ERROR_HTTP;
|
||||
}
|
||||
|
||||
/* Parse response to extract id - simplified */
|
||||
*id = response;
|
||||
return SYNOR_DB_OK;
|
||||
}
|
||||
|
||||
synor_db_error_t synor_doc_get(synor_database_t* db, const char* collection,
|
||||
const char* id, synor_document_t* result) {
|
||||
(void)db; (void)collection; (void)id; (void)result;
|
||||
return SYNOR_DB_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
synor_db_error_t synor_doc_update(synor_database_t* db, const char* collection,
|
||||
const char* id, const char* update) {
|
||||
(void)db; (void)collection; (void)id; (void)update;
|
||||
return SYNOR_DB_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
synor_db_error_t synor_doc_delete(synor_database_t* db, const char* collection,
|
||||
const char* id) {
|
||||
(void)db; (void)collection; (void)id;
|
||||
return SYNOR_DB_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
synor_db_error_t synor_doc_query(synor_database_t* db, const char* collection,
|
||||
const synor_document_query_t* query, synor_document_list_t* result) {
|
||||
(void)db; (void)collection; (void)query; (void)result;
|
||||
return SYNOR_DB_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
void synor_document_free(synor_document_t* doc) {
|
||||
if (!doc) return;
|
||||
free(doc->id);
|
||||
free(doc->collection);
|
||||
free(doc->data);
|
||||
doc->id = NULL;
|
||||
doc->collection = NULL;
|
||||
doc->data = NULL;
|
||||
}
|
||||
|
||||
void synor_document_list_free(synor_document_list_t* list) {
|
||||
if (!list) return;
|
||||
for (size_t i = 0; i < list->count; i++) {
|
||||
synor_document_free(&list->documents[i]);
|
||||
}
|
||||
free(list->documents);
|
||||
list->documents = NULL;
|
||||
list->count = 0;
|
||||
}
|
||||
|
||||
/* ==================== Vector Operations ==================== */
|
||||
|
||||
synor_db_error_t synor_vector_upsert(synor_database_t* db, const char* collection,
|
||||
const synor_vector_entry_t* vectors, size_t count) {
|
||||
(void)db; (void)collection; (void)vectors; (void)count;
|
||||
return SYNOR_DB_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
synor_db_error_t synor_vector_search(synor_database_t* db, const char* collection,
|
||||
const double* vector, size_t vector_size, int32_t k,
|
||||
synor_search_result_list_t* result) {
|
||||
(void)db; (void)collection; (void)vector; (void)vector_size; (void)k; (void)result;
|
||||
return SYNOR_DB_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
synor_db_error_t synor_vector_delete(synor_database_t* db, const char* collection,
|
||||
const char** ids, size_t count) {
|
||||
(void)db; (void)collection; (void)ids; (void)count;
|
||||
return SYNOR_DB_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
void synor_search_result_list_free(synor_search_result_list_t* list) {
|
||||
if (!list) return;
|
||||
for (size_t i = 0; i < list->count; i++) {
|
||||
free(list->results[i].id);
|
||||
free(list->results[i].vector);
|
||||
free(list->results[i].metadata);
|
||||
}
|
||||
free(list->results);
|
||||
list->results = NULL;
|
||||
list->count = 0;
|
||||
}
|
||||
|
||||
/* ==================== Time Series Operations ==================== */
|
||||
|
||||
synor_db_error_t synor_ts_write(synor_database_t* db, const char* series,
|
||||
const synor_data_point_t* points, size_t count) {
|
||||
(void)db; (void)series; (void)points; (void)count;
|
||||
return SYNOR_DB_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
synor_db_error_t synor_ts_query(synor_database_t* db, const char* series,
|
||||
const synor_time_range_t* range, const synor_aggregation_t* aggregation,
|
||||
synor_data_point_list_t* result) {
|
||||
(void)db; (void)series; (void)range; (void)aggregation; (void)result;
|
||||
return SYNOR_DB_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
void synor_data_point_list_free(synor_data_point_list_t* list) {
|
||||
if (!list) return;
|
||||
free(list->points);
|
||||
list->points = NULL;
|
||||
list->count = 0;
|
||||
}
|
||||
309
sdk/c/src/hosting/hosting.c
Normal file
309
sdk/c/src/hosting/hosting.c
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
/**
|
||||
* @file hosting.c
|
||||
* @brief Synor Hosting SDK implementation
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include "synor/hosting.h"
|
||||
|
||||
/* Forward declarations from common.c */
|
||||
typedef struct synor_http_client synor_http_client_t;
|
||||
synor_http_client_t* synor_http_client_create(
|
||||
const char* api_key, const char* endpoint,
|
||||
uint32_t timeout_ms, uint32_t retries, bool debug);
|
||||
void synor_http_client_destroy(synor_http_client_t* client);
|
||||
synor_error_t synor_http_get(synor_http_client_t* client, const char* path,
|
||||
char** response, size_t* response_size);
|
||||
synor_error_t synor_http_post(synor_http_client_t* client, const char* path,
|
||||
const char* body, char** response, size_t* response_size);
|
||||
synor_error_t synor_http_put(synor_http_client_t* client, const char* path,
|
||||
const char* body, char** response, size_t* response_size);
|
||||
synor_error_t synor_http_delete(synor_http_client_t* client, const char* path,
|
||||
char** response, size_t* response_size);
|
||||
char* synor_strdup(const char* s);
|
||||
char* synor_url_encode(const char* s);
|
||||
|
||||
/* Internal hosting structure */
|
||||
struct synor_hosting {
|
||||
synor_http_client_t* http;
|
||||
bool closed;
|
||||
};
|
||||
|
||||
synor_hosting_t* synor_hosting_create(const synor_hosting_config_t* config) {
|
||||
if (!config || !config->api_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
synor_hosting_t* hosting = calloc(1, sizeof(synor_hosting_t));
|
||||
if (!hosting) return NULL;
|
||||
|
||||
const char* endpoint = config->endpoint ?
|
||||
config->endpoint : "https://hosting.synor.io/v1";
|
||||
uint32_t timeout = config->timeout_ms > 0 ? config->timeout_ms : 60000;
|
||||
uint32_t retries = config->retries > 0 ? config->retries : 3;
|
||||
|
||||
hosting->http = synor_http_client_create(
|
||||
config->api_key, endpoint, timeout, retries, config->debug);
|
||||
|
||||
if (!hosting->http) {
|
||||
free(hosting);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
hosting->closed = false;
|
||||
return hosting;
|
||||
}
|
||||
|
||||
void synor_hosting_destroy(synor_hosting_t* hosting) {
|
||||
if (!hosting) return;
|
||||
hosting->closed = true;
|
||||
synor_http_client_destroy(hosting->http);
|
||||
free(hosting);
|
||||
}
|
||||
|
||||
bool synor_hosting_is_closed(synor_hosting_t* hosting) {
|
||||
return hosting ? hosting->closed : true;
|
||||
}
|
||||
|
||||
bool synor_hosting_health_check(synor_hosting_t* hosting) {
|
||||
if (!hosting || hosting->closed) return false;
|
||||
|
||||
char* response = NULL;
|
||||
size_t response_size = 0;
|
||||
|
||||
if (synor_http_get(hosting->http, "/health", &response, &response_size) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool healthy = response && strstr(response, "\"healthy\"") != NULL;
|
||||
free(response);
|
||||
return healthy;
|
||||
}
|
||||
|
||||
/* ==================== Domain Operations ==================== */
|
||||
|
||||
synor_hosting_error_t synor_hosting_check_availability(synor_hosting_t* hosting,
|
||||
const char* name, synor_domain_availability_t* result) {
|
||||
(void)hosting; (void)name; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_register_domain(synor_hosting_t* hosting,
|
||||
const char* name, const synor_register_domain_options_t* options,
|
||||
synor_domain_t* result) {
|
||||
(void)hosting; (void)name; (void)options; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_get_domain(synor_hosting_t* hosting,
|
||||
const char* name, synor_domain_t* result) {
|
||||
(void)hosting; (void)name; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_list_domains(synor_hosting_t* hosting,
|
||||
synor_domain_list_t* result) {
|
||||
(void)hosting; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_resolve_domain(synor_hosting_t* hosting,
|
||||
const char* name, synor_domain_record_t* result) {
|
||||
(void)hosting; (void)name; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_renew_domain(synor_hosting_t* hosting,
|
||||
const char* name, int32_t years, synor_domain_t* result) {
|
||||
(void)hosting; (void)name; (void)years; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN; /* TODO: Implement */
|
||||
}
|
||||
|
||||
void synor_domain_free(synor_domain_t* domain) {
|
||||
if (!domain) return;
|
||||
free(domain->name);
|
||||
free(domain->owner);
|
||||
if (domain->records) {
|
||||
synor_domain_record_free(domain->records);
|
||||
free(domain->records);
|
||||
}
|
||||
}
|
||||
|
||||
void synor_domain_list_free(synor_domain_list_t* list) {
|
||||
if (!list) return;
|
||||
for (size_t i = 0; i < list->count; i++) {
|
||||
synor_domain_free(&list->domains[i]);
|
||||
}
|
||||
free(list->domains);
|
||||
list->domains = NULL;
|
||||
list->count = 0;
|
||||
}
|
||||
|
||||
void synor_domain_record_free(synor_domain_record_t* record) {
|
||||
if (!record) return;
|
||||
free(record->cid);
|
||||
for (size_t i = 0; i < record->ipv4_count; i++) free(record->ipv4[i]);
|
||||
free(record->ipv4);
|
||||
for (size_t i = 0; i < record->ipv6_count; i++) free(record->ipv6[i]);
|
||||
free(record->ipv6);
|
||||
free(record->cname);
|
||||
for (size_t i = 0; i < record->txt_count; i++) free(record->txt[i]);
|
||||
free(record->txt);
|
||||
}
|
||||
|
||||
void synor_domain_availability_free(synor_domain_availability_t* avail) {
|
||||
if (!avail) return;
|
||||
free(avail->name);
|
||||
}
|
||||
|
||||
/* ==================== DNS Operations ==================== */
|
||||
|
||||
synor_hosting_error_t synor_hosting_get_dns_zone(synor_hosting_t* hosting,
|
||||
const char* domain, synor_dns_zone_t* result) {
|
||||
(void)hosting; (void)domain; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_set_dns_records(synor_hosting_t* hosting,
|
||||
const char* domain, const synor_dns_record_t* records, size_t count,
|
||||
synor_dns_zone_t* result) {
|
||||
(void)hosting; (void)domain; (void)records; (void)count; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_add_dns_record(synor_hosting_t* hosting,
|
||||
const char* domain, const synor_dns_record_t* record, synor_dns_zone_t* result) {
|
||||
(void)hosting; (void)domain; (void)record; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_delete_dns_record(synor_hosting_t* hosting,
|
||||
const char* domain, const char* record_type, const char* name,
|
||||
synor_dns_zone_t* result) {
|
||||
(void)hosting; (void)domain; (void)record_type; (void)name; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
void synor_dns_zone_free(synor_dns_zone_t* zone) {
|
||||
if (!zone) return;
|
||||
free(zone->domain);
|
||||
for (size_t i = 0; i < zone->record_count; i++) {
|
||||
free(zone->records[i].name);
|
||||
free(zone->records[i].value);
|
||||
}
|
||||
free(zone->records);
|
||||
}
|
||||
|
||||
/* ==================== Deployment Operations ==================== */
|
||||
|
||||
synor_hosting_error_t synor_hosting_deploy(synor_hosting_t* hosting,
|
||||
const char* cid, const synor_deploy_options_t* options,
|
||||
synor_deployment_t* result) {
|
||||
(void)hosting; (void)cid; (void)options; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_get_deployment(synor_hosting_t* hosting,
|
||||
const char* id, synor_deployment_t* result) {
|
||||
(void)hosting; (void)id; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_list_deployments(synor_hosting_t* hosting,
|
||||
const char* domain, synor_deployment_list_t* result) {
|
||||
(void)hosting; (void)domain; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_rollback(synor_hosting_t* hosting,
|
||||
const char* domain, const char* deployment_id, synor_deployment_t* result) {
|
||||
(void)hosting; (void)domain; (void)deployment_id; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_delete_deployment(synor_hosting_t* hosting,
|
||||
const char* id) {
|
||||
(void)hosting; (void)id;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
void synor_deployment_free(synor_deployment_t* deployment) {
|
||||
if (!deployment) return;
|
||||
free(deployment->id);
|
||||
free(deployment->domain);
|
||||
free(deployment->cid);
|
||||
free(deployment->url);
|
||||
free(deployment->build_logs);
|
||||
free(deployment->error_message);
|
||||
}
|
||||
|
||||
void synor_deployment_list_free(synor_deployment_list_t* list) {
|
||||
if (!list) return;
|
||||
for (size_t i = 0; i < list->count; i++) {
|
||||
synor_deployment_free(&list->deployments[i]);
|
||||
}
|
||||
free(list->deployments);
|
||||
list->deployments = NULL;
|
||||
list->count = 0;
|
||||
}
|
||||
|
||||
/* ==================== SSL Operations ==================== */
|
||||
|
||||
synor_hosting_error_t synor_hosting_provision_ssl(synor_hosting_t* hosting,
|
||||
const char* domain, const synor_provision_ssl_options_t* options,
|
||||
synor_certificate_t* result) {
|
||||
(void)hosting; (void)domain; (void)options; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_get_certificate(synor_hosting_t* hosting,
|
||||
const char* domain, synor_certificate_t* result) {
|
||||
(void)hosting; (void)domain; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_renew_certificate(synor_hosting_t* hosting,
|
||||
const char* domain, synor_certificate_t* result) {
|
||||
(void)hosting; (void)domain; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_delete_certificate(synor_hosting_t* hosting,
|
||||
const char* domain) {
|
||||
(void)hosting; (void)domain;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
void synor_certificate_free(synor_certificate_t* cert) {
|
||||
if (!cert) return;
|
||||
free(cert->domain);
|
||||
free(cert->issuer);
|
||||
free(cert->fingerprint);
|
||||
}
|
||||
|
||||
/* ==================== Analytics Operations ==================== */
|
||||
|
||||
synor_hosting_error_t synor_hosting_get_analytics(synor_hosting_t* hosting,
|
||||
const char* domain, const synor_analytics_options_t* options,
|
||||
synor_analytics_data_t* result) {
|
||||
(void)hosting; (void)domain; (void)options; (void)result;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
synor_hosting_error_t synor_hosting_purge_cache(synor_hosting_t* hosting,
|
||||
const char* domain, const char** paths, size_t path_count, int64_t* purged) {
|
||||
(void)hosting; (void)domain; (void)paths; (void)path_count; (void)purged;
|
||||
return SYNOR_HOSTING_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
void synor_analytics_data_free(synor_analytics_data_t* data) {
|
||||
if (!data) return;
|
||||
free(data->domain);
|
||||
free(data->period);
|
||||
for (size_t i = 0; i < data->top_pages_count; i++) {
|
||||
free(data->top_pages[i].path);
|
||||
}
|
||||
free(data->top_pages);
|
||||
}
|
||||
340
sdk/cpp/include/synor/bridge.hpp
Normal file
340
sdk/cpp/include/synor/bridge.hpp
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
/**
|
||||
* @file bridge.hpp
|
||||
* @brief Synor Bridge SDK for C++
|
||||
*
|
||||
* Modern C++17 SDK for cross-chain asset transfers with lock-mint and burn-unlock patterns.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include "wallet.hpp" // For SynorException
|
||||
|
||||
namespace synor {
|
||||
namespace bridge {
|
||||
|
||||
/// Chain identifier
|
||||
enum class ChainId {
|
||||
Synor,
|
||||
Ethereum,
|
||||
Polygon,
|
||||
Arbitrum,
|
||||
Optimism,
|
||||
Bsc,
|
||||
Avalanche,
|
||||
Solana,
|
||||
Cosmos
|
||||
};
|
||||
|
||||
/// Asset type
|
||||
enum class AssetType {
|
||||
Native,
|
||||
Erc20,
|
||||
Erc721,
|
||||
Erc1155
|
||||
};
|
||||
|
||||
/// Transfer status
|
||||
enum class TransferStatus {
|
||||
Pending,
|
||||
Locked,
|
||||
Confirming,
|
||||
Minting,
|
||||
Completed,
|
||||
Failed,
|
||||
Refunded
|
||||
};
|
||||
|
||||
/// Transfer direction
|
||||
enum class TransferDirection {
|
||||
LockMint,
|
||||
BurnUnlock
|
||||
};
|
||||
|
||||
/// Bridge configuration
|
||||
struct Config {
|
||||
std::string api_key;
|
||||
std::string endpoint = "https://bridge.synor.io/v1";
|
||||
uint32_t timeout_ms = 60000;
|
||||
uint32_t retries = 3;
|
||||
bool debug = false;
|
||||
};
|
||||
|
||||
/// Native currency
|
||||
struct NativeCurrency {
|
||||
std::string name;
|
||||
std::string symbol;
|
||||
int32_t decimals;
|
||||
};
|
||||
|
||||
/// A blockchain network
|
||||
struct Chain {
|
||||
ChainId id;
|
||||
std::string name;
|
||||
int64_t chain_id;
|
||||
std::string rpc_url;
|
||||
std::string explorer_url;
|
||||
NativeCurrency native_currency;
|
||||
int32_t confirmations;
|
||||
int32_t estimated_block_time;
|
||||
bool supported;
|
||||
};
|
||||
|
||||
/// An asset
|
||||
struct Asset {
|
||||
std::string id;
|
||||
std::string symbol;
|
||||
std::string name;
|
||||
AssetType type;
|
||||
ChainId chain;
|
||||
std::optional<std::string> contract_address;
|
||||
int32_t decimals;
|
||||
std::optional<std::string> logo_url;
|
||||
bool verified = false;
|
||||
};
|
||||
|
||||
/// A wrapped asset
|
||||
struct WrappedAsset {
|
||||
Asset original_asset;
|
||||
Asset wrapped_asset;
|
||||
ChainId chain;
|
||||
std::string bridge_contract;
|
||||
};
|
||||
|
||||
/// Validator signature
|
||||
struct ValidatorSignature {
|
||||
std::string validator;
|
||||
std::string signature;
|
||||
int64_t timestamp;
|
||||
};
|
||||
|
||||
/// Lock receipt
|
||||
struct LockReceipt {
|
||||
std::string id;
|
||||
std::string tx_hash;
|
||||
ChainId source_chain;
|
||||
ChainId target_chain;
|
||||
Asset asset;
|
||||
std::string amount;
|
||||
std::string sender;
|
||||
std::string recipient;
|
||||
int64_t lock_timestamp;
|
||||
int32_t confirmations;
|
||||
int32_t required_confirmations;
|
||||
};
|
||||
|
||||
/// Lock proof
|
||||
struct LockProof {
|
||||
LockReceipt lock_receipt;
|
||||
std::vector<std::string> merkle_proof;
|
||||
std::string block_header;
|
||||
std::vector<ValidatorSignature> signatures;
|
||||
};
|
||||
|
||||
/// Burn receipt
|
||||
struct BurnReceipt {
|
||||
std::string id;
|
||||
std::string tx_hash;
|
||||
ChainId source_chain;
|
||||
ChainId target_chain;
|
||||
Asset wrapped_asset;
|
||||
Asset original_asset;
|
||||
std::string amount;
|
||||
std::string sender;
|
||||
std::string recipient;
|
||||
int64_t burn_timestamp;
|
||||
int32_t confirmations;
|
||||
int32_t required_confirmations;
|
||||
};
|
||||
|
||||
/// Burn proof
|
||||
struct BurnProof {
|
||||
BurnReceipt burn_receipt;
|
||||
std::vector<std::string> merkle_proof;
|
||||
std::string block_header;
|
||||
std::vector<ValidatorSignature> signatures;
|
||||
};
|
||||
|
||||
/// A transfer
|
||||
struct Transfer {
|
||||
std::string id;
|
||||
TransferDirection direction;
|
||||
TransferStatus status;
|
||||
ChainId source_chain;
|
||||
ChainId target_chain;
|
||||
Asset asset;
|
||||
std::string amount;
|
||||
std::string sender;
|
||||
std::string recipient;
|
||||
std::optional<std::string> source_tx_hash;
|
||||
std::optional<std::string> target_tx_hash;
|
||||
std::string fee;
|
||||
Asset fee_asset;
|
||||
int64_t created_at;
|
||||
int64_t updated_at;
|
||||
std::optional<int64_t> completed_at;
|
||||
std::optional<std::string> error_message;
|
||||
};
|
||||
|
||||
/// Transfer filter
|
||||
struct TransferFilter {
|
||||
std::optional<TransferStatus> status;
|
||||
std::optional<ChainId> source_chain;
|
||||
std::optional<ChainId> target_chain;
|
||||
std::optional<std::string> asset;
|
||||
std::optional<std::string> sender;
|
||||
std::optional<std::string> recipient;
|
||||
std::optional<int32_t> limit;
|
||||
std::optional<int32_t> offset;
|
||||
};
|
||||
|
||||
/// Fee estimate
|
||||
struct FeeEstimate {
|
||||
std::string bridge_fee;
|
||||
std::string gas_fee_source;
|
||||
std::string gas_fee_target;
|
||||
std::string total_fee;
|
||||
Asset fee_asset;
|
||||
int32_t estimated_time;
|
||||
std::optional<std::string> exchange_rate;
|
||||
};
|
||||
|
||||
/// Exchange rate
|
||||
struct ExchangeRate {
|
||||
Asset from_asset;
|
||||
Asset to_asset;
|
||||
std::string rate;
|
||||
std::string inverse_rate;
|
||||
int64_t last_updated;
|
||||
std::string source;
|
||||
};
|
||||
|
||||
/// Signed transaction
|
||||
struct SignedTransaction {
|
||||
std::string tx_hash;
|
||||
ChainId chain;
|
||||
std::string from;
|
||||
std::string to;
|
||||
std::string value;
|
||||
std::string data;
|
||||
std::string gas_limit;
|
||||
std::optional<std::string> gas_price;
|
||||
std::optional<std::string> max_fee_per_gas;
|
||||
std::optional<std::string> max_priority_fee_per_gas;
|
||||
int32_t nonce;
|
||||
std::string signature;
|
||||
};
|
||||
|
||||
/// Lock options
|
||||
struct LockOptions {
|
||||
std::optional<std::string> recipient;
|
||||
std::optional<int64_t> deadline;
|
||||
std::optional<double> slippage;
|
||||
};
|
||||
|
||||
/// Mint options
|
||||
struct MintOptions {
|
||||
std::optional<std::string> gas_limit;
|
||||
std::optional<std::string> max_fee_per_gas;
|
||||
std::optional<std::string> max_priority_fee_per_gas;
|
||||
};
|
||||
|
||||
/// Burn options
|
||||
struct BurnOptions {
|
||||
std::optional<std::string> recipient;
|
||||
std::optional<int64_t> deadline;
|
||||
};
|
||||
|
||||
/// Unlock options
|
||||
struct UnlockOptions {
|
||||
std::optional<std::string> gas_limit;
|
||||
std::optional<std::string> gas_price;
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
class SynorBridgeImpl;
|
||||
|
||||
/// Synor Bridge client
|
||||
class SynorBridge {
|
||||
public:
|
||||
explicit SynorBridge(const Config& config);
|
||||
~SynorBridge();
|
||||
|
||||
SynorBridge(const SynorBridge&) = delete;
|
||||
SynorBridge& operator=(const SynorBridge&) = delete;
|
||||
SynorBridge(SynorBridge&&) noexcept;
|
||||
SynorBridge& operator=(SynorBridge&&) noexcept;
|
||||
|
||||
// Chain operations
|
||||
std::future<std::vector<Chain>> get_supported_chains();
|
||||
std::future<Chain> get_chain(ChainId chain_id);
|
||||
std::future<bool> is_chain_supported(ChainId chain_id);
|
||||
|
||||
// Asset operations
|
||||
std::future<std::vector<Asset>> get_supported_assets(ChainId chain_id);
|
||||
std::future<Asset> get_asset(const std::string& asset_id);
|
||||
std::future<WrappedAsset> get_wrapped_asset(const std::string& original_asset_id,
|
||||
ChainId target_chain);
|
||||
|
||||
// Fee & rate operations
|
||||
std::future<FeeEstimate> estimate_fee(const std::string& asset, const std::string& amount,
|
||||
ChainId source_chain, ChainId target_chain);
|
||||
std::future<ExchangeRate> get_exchange_rate(const std::string& from_asset,
|
||||
const std::string& to_asset);
|
||||
|
||||
// Lock-Mint flow
|
||||
std::future<LockReceipt> lock(const std::string& asset, const std::string& amount,
|
||||
ChainId target_chain, const LockOptions& options = {});
|
||||
std::future<LockProof> get_lock_proof(const std::string& lock_receipt_id);
|
||||
std::future<LockProof> wait_for_lock_proof(
|
||||
const std::string& lock_receipt_id,
|
||||
std::chrono::milliseconds poll_interval = std::chrono::seconds(5),
|
||||
std::chrono::milliseconds max_wait = std::chrono::minutes(10));
|
||||
std::future<SignedTransaction> mint(const LockProof& proof, const std::string& target_address,
|
||||
const MintOptions& options = {});
|
||||
|
||||
// Burn-Unlock flow
|
||||
std::future<BurnReceipt> burn(const std::string& wrapped_asset, const std::string& amount,
|
||||
const BurnOptions& options = {});
|
||||
std::future<BurnProof> get_burn_proof(const std::string& burn_receipt_id);
|
||||
std::future<BurnProof> wait_for_burn_proof(
|
||||
const std::string& burn_receipt_id,
|
||||
std::chrono::milliseconds poll_interval = std::chrono::seconds(5),
|
||||
std::chrono::milliseconds max_wait = std::chrono::minutes(10));
|
||||
std::future<SignedTransaction> unlock(const BurnProof& proof, const UnlockOptions& options = {});
|
||||
|
||||
// Transfer management
|
||||
std::future<Transfer> get_transfer(const std::string& transfer_id);
|
||||
std::future<TransferStatus> get_transfer_status(const std::string& transfer_id);
|
||||
std::future<std::vector<Transfer>> list_transfers(const TransferFilter& filter = {});
|
||||
std::future<Transfer> wait_for_transfer(
|
||||
const std::string& transfer_id,
|
||||
std::chrono::milliseconds poll_interval = std::chrono::seconds(10),
|
||||
std::chrono::milliseconds max_wait = std::chrono::minutes(30));
|
||||
|
||||
// Convenience methods
|
||||
std::future<Transfer> bridge_to(const std::string& asset, const std::string& amount,
|
||||
ChainId target_chain, const std::string& target_address,
|
||||
const LockOptions& lock_options = {},
|
||||
const MintOptions& mint_options = {});
|
||||
std::future<Transfer> bridge_back(const std::string& wrapped_asset, const std::string& amount,
|
||||
const BurnOptions& burn_options = {},
|
||||
const UnlockOptions& unlock_options = {});
|
||||
|
||||
// Lifecycle
|
||||
void close();
|
||||
bool is_closed() const;
|
||||
std::future<bool> health_check();
|
||||
|
||||
private:
|
||||
std::unique_ptr<SynorBridgeImpl> impl_;
|
||||
};
|
||||
|
||||
} // namespace bridge
|
||||
} // namespace synor
|
||||
194
sdk/cpp/include/synor/database.hpp
Normal file
194
sdk/cpp/include/synor/database.hpp
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
/**
|
||||
* @file database.hpp
|
||||
* @brief Synor Database SDK for C++
|
||||
*
|
||||
* Modern C++17 SDK for multi-model database with Key-Value, Document, Vector, and Time Series stores.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <cstdint>
|
||||
#include "wallet.hpp" // For SynorException
|
||||
|
||||
namespace synor {
|
||||
namespace database {
|
||||
|
||||
/// Database configuration
|
||||
struct Config {
|
||||
std::string api_key;
|
||||
std::string endpoint = "https://db.synor.io/v1";
|
||||
uint32_t timeout_ms = 60000;
|
||||
uint32_t retries = 3;
|
||||
bool debug = false;
|
||||
};
|
||||
|
||||
/// A key-value entry
|
||||
struct KeyValue {
|
||||
std::string key;
|
||||
std::string value; // JSON string
|
||||
std::optional<int32_t> ttl;
|
||||
std::optional<int64_t> created_at;
|
||||
std::optional<int64_t> updated_at;
|
||||
};
|
||||
|
||||
/// A document
|
||||
struct Document {
|
||||
std::string id;
|
||||
std::string collection;
|
||||
std::map<std::string, std::string> data; // JSON data
|
||||
int64_t created_at;
|
||||
int64_t updated_at;
|
||||
};
|
||||
|
||||
/// Query for documents
|
||||
struct Query {
|
||||
std::optional<std::string> filter; // JSON
|
||||
std::optional<std::string> sort; // JSON
|
||||
std::optional<int32_t> limit;
|
||||
std::optional<int32_t> offset;
|
||||
std::optional<std::vector<std::string>> projection;
|
||||
};
|
||||
|
||||
/// A vector entry
|
||||
struct VectorEntry {
|
||||
std::string id;
|
||||
std::vector<double> vector;
|
||||
std::optional<std::map<std::string, std::string>> metadata;
|
||||
};
|
||||
|
||||
/// A search result
|
||||
struct SearchResult {
|
||||
std::string id;
|
||||
double score;
|
||||
std::optional<std::vector<double>> vector;
|
||||
std::optional<std::map<std::string, std::string>> metadata;
|
||||
};
|
||||
|
||||
/// A time series data point
|
||||
struct DataPoint {
|
||||
int64_t timestamp;
|
||||
double value;
|
||||
std::optional<std::map<std::string, std::string>> tags;
|
||||
};
|
||||
|
||||
/// Time range for queries
|
||||
struct TimeRange {
|
||||
int64_t start;
|
||||
int64_t end;
|
||||
};
|
||||
|
||||
/// Aggregation settings
|
||||
struct Aggregation {
|
||||
std::string function; // avg, sum, min, max, count
|
||||
std::string interval; // 1m, 5m, 1h, 1d
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
class SynorDatabaseImpl;
|
||||
|
||||
/// Key-Value store interface
|
||||
class KeyValueStore {
|
||||
public:
|
||||
explicit KeyValueStore(std::shared_ptr<SynorDatabaseImpl> impl);
|
||||
|
||||
std::future<std::optional<std::string>> get(const std::string& key);
|
||||
std::future<void> set(const std::string& key, const std::string& value,
|
||||
std::optional<int32_t> ttl = std::nullopt);
|
||||
std::future<void> remove(const std::string& key);
|
||||
std::future<std::vector<KeyValue>> list(const std::string& prefix);
|
||||
|
||||
private:
|
||||
std::shared_ptr<SynorDatabaseImpl> impl_;
|
||||
};
|
||||
|
||||
/// Document store interface
|
||||
class DocumentStore {
|
||||
public:
|
||||
explicit DocumentStore(std::shared_ptr<SynorDatabaseImpl> impl);
|
||||
|
||||
std::future<std::string> create(const std::string& collection, const std::string& document);
|
||||
std::future<Document> get(const std::string& collection, const std::string& id);
|
||||
std::future<void> update(const std::string& collection, const std::string& id,
|
||||
const std::string& update);
|
||||
std::future<void> remove(const std::string& collection, const std::string& id);
|
||||
std::future<std::vector<Document>> query(const std::string& collection, const Query& query);
|
||||
|
||||
private:
|
||||
std::shared_ptr<SynorDatabaseImpl> impl_;
|
||||
};
|
||||
|
||||
/// Vector store interface
|
||||
class VectorStore {
|
||||
public:
|
||||
explicit VectorStore(std::shared_ptr<SynorDatabaseImpl> impl);
|
||||
|
||||
std::future<void> upsert(const std::string& collection, const std::vector<VectorEntry>& vectors);
|
||||
std::future<std::vector<SearchResult>> search(const std::string& collection,
|
||||
const std::vector<double>& vector, int32_t k);
|
||||
std::future<void> remove(const std::string& collection, const std::vector<std::string>& ids);
|
||||
|
||||
private:
|
||||
std::shared_ptr<SynorDatabaseImpl> impl_;
|
||||
};
|
||||
|
||||
/// Time series store interface
|
||||
class TimeSeriesStore {
|
||||
public:
|
||||
explicit TimeSeriesStore(std::shared_ptr<SynorDatabaseImpl> impl);
|
||||
|
||||
std::future<void> write(const std::string& series, const std::vector<DataPoint>& points);
|
||||
std::future<std::vector<DataPoint>> query(const std::string& series, const TimeRange& range,
|
||||
std::optional<Aggregation> aggregation = std::nullopt);
|
||||
|
||||
private:
|
||||
std::shared_ptr<SynorDatabaseImpl> impl_;
|
||||
};
|
||||
|
||||
/// Synor Database client
|
||||
class SynorDatabase {
|
||||
public:
|
||||
explicit SynorDatabase(const Config& config);
|
||||
~SynorDatabase();
|
||||
|
||||
SynorDatabase(const SynorDatabase&) = delete;
|
||||
SynorDatabase& operator=(const SynorDatabase&) = delete;
|
||||
SynorDatabase(SynorDatabase&&) noexcept;
|
||||
SynorDatabase& operator=(SynorDatabase&&) noexcept;
|
||||
|
||||
/// Key-Value store operations
|
||||
KeyValueStore& kv();
|
||||
|
||||
/// Document store operations
|
||||
DocumentStore& documents();
|
||||
|
||||
/// Vector store operations
|
||||
VectorStore& vectors();
|
||||
|
||||
/// Time series store operations
|
||||
TimeSeriesStore& timeseries();
|
||||
|
||||
/// Close the client
|
||||
void close();
|
||||
|
||||
/// Check if the client is closed
|
||||
bool is_closed() const;
|
||||
|
||||
/// Perform a health check
|
||||
std::future<bool> health_check();
|
||||
|
||||
private:
|
||||
std::shared_ptr<SynorDatabaseImpl> impl_;
|
||||
std::unique_ptr<KeyValueStore> kv_;
|
||||
std::unique_ptr<DocumentStore> documents_;
|
||||
std::unique_ptr<VectorStore> vectors_;
|
||||
std::unique_ptr<TimeSeriesStore> timeseries_;
|
||||
};
|
||||
|
||||
} // namespace database
|
||||
} // namespace synor
|
||||
271
sdk/cpp/include/synor/hosting.hpp
Normal file
271
sdk/cpp/include/synor/hosting.hpp
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
/**
|
||||
* @file hosting.hpp
|
||||
* @brief Synor Hosting SDK for C++
|
||||
*
|
||||
* Modern C++17 SDK for decentralized web hosting with domain management, DNS, deployments, and SSL.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <cstdint>
|
||||
#include "wallet.hpp" // For SynorException
|
||||
|
||||
namespace synor {
|
||||
namespace hosting {
|
||||
|
||||
/// Domain status
|
||||
enum class DomainStatus {
|
||||
Pending,
|
||||
Active,
|
||||
Expired,
|
||||
Suspended
|
||||
};
|
||||
|
||||
/// Deployment status
|
||||
enum class DeploymentStatus {
|
||||
Pending,
|
||||
Building,
|
||||
Deploying,
|
||||
Active,
|
||||
Failed,
|
||||
Inactive
|
||||
};
|
||||
|
||||
/// Certificate status
|
||||
enum class CertificateStatus {
|
||||
Pending,
|
||||
Issued,
|
||||
Expired,
|
||||
Revoked
|
||||
};
|
||||
|
||||
/// DNS record type
|
||||
enum class DnsRecordType {
|
||||
A,
|
||||
AAAA,
|
||||
CNAME,
|
||||
TXT,
|
||||
MX,
|
||||
NS,
|
||||
SRV,
|
||||
CAA
|
||||
};
|
||||
|
||||
/// Hosting configuration
|
||||
struct Config {
|
||||
std::string api_key;
|
||||
std::string endpoint = "https://hosting.synor.io/v1";
|
||||
uint32_t timeout_ms = 60000;
|
||||
uint32_t retries = 3;
|
||||
bool debug = false;
|
||||
};
|
||||
|
||||
/// Domain record
|
||||
struct DomainRecord {
|
||||
std::optional<std::string> cid;
|
||||
std::optional<std::vector<std::string>> ipv4;
|
||||
std::optional<std::vector<std::string>> ipv6;
|
||||
std::optional<std::string> cname;
|
||||
std::optional<std::vector<std::string>> txt;
|
||||
std::optional<std::map<std::string, std::string>> metadata;
|
||||
};
|
||||
|
||||
/// A domain
|
||||
struct Domain {
|
||||
std::string name;
|
||||
DomainStatus status;
|
||||
std::string owner;
|
||||
int64_t registered_at;
|
||||
int64_t expires_at;
|
||||
bool auto_renew;
|
||||
std::optional<DomainRecord> records;
|
||||
};
|
||||
|
||||
/// Domain availability
|
||||
struct DomainAvailability {
|
||||
std::string name;
|
||||
bool available;
|
||||
std::optional<double> price;
|
||||
bool premium = false;
|
||||
};
|
||||
|
||||
/// Domain registration options
|
||||
struct RegisterDomainOptions {
|
||||
std::optional<int32_t> years;
|
||||
std::optional<bool> auto_renew;
|
||||
};
|
||||
|
||||
/// DNS record
|
||||
struct DnsRecord {
|
||||
DnsRecordType type;
|
||||
std::string name;
|
||||
std::string value;
|
||||
int32_t ttl = 3600;
|
||||
std::optional<int32_t> priority;
|
||||
};
|
||||
|
||||
/// DNS zone
|
||||
struct DnsZone {
|
||||
std::string domain;
|
||||
std::vector<DnsRecord> records;
|
||||
int64_t updated_at;
|
||||
};
|
||||
|
||||
/// A deployment
|
||||
struct Deployment {
|
||||
std::string id;
|
||||
std::string domain;
|
||||
std::string cid;
|
||||
DeploymentStatus status;
|
||||
std::string url;
|
||||
int64_t created_at;
|
||||
int64_t updated_at;
|
||||
std::optional<std::string> build_logs;
|
||||
std::optional<std::string> error_message;
|
||||
};
|
||||
|
||||
/// Deploy options
|
||||
struct DeployOptions {
|
||||
std::optional<std::string> domain;
|
||||
std::optional<std::string> subdomain;
|
||||
std::optional<bool> spa;
|
||||
std::optional<bool> clean_urls;
|
||||
std::optional<bool> trailing_slash;
|
||||
};
|
||||
|
||||
/// SSL certificate
|
||||
struct Certificate {
|
||||
std::string domain;
|
||||
CertificateStatus status;
|
||||
bool auto_renew;
|
||||
std::string issuer;
|
||||
std::optional<int64_t> issued_at;
|
||||
std::optional<int64_t> expires_at;
|
||||
std::optional<std::string> fingerprint;
|
||||
};
|
||||
|
||||
/// SSL provisioning options
|
||||
struct ProvisionSslOptions {
|
||||
std::optional<bool> include_www;
|
||||
std::optional<bool> auto_renew;
|
||||
};
|
||||
|
||||
/// Redirect rule
|
||||
struct RedirectRule {
|
||||
std::string source;
|
||||
std::string destination;
|
||||
int32_t status_code = 301;
|
||||
bool permanent = true;
|
||||
};
|
||||
|
||||
/// Site configuration
|
||||
struct SiteConfig {
|
||||
std::string domain;
|
||||
std::optional<std::string> cid;
|
||||
std::optional<std::map<std::string, std::string>> headers;
|
||||
std::optional<std::vector<RedirectRule>> redirects;
|
||||
std::optional<std::map<std::string, std::string>> error_pages;
|
||||
bool spa = false;
|
||||
bool clean_urls = true;
|
||||
bool trailing_slash = false;
|
||||
};
|
||||
|
||||
/// Page view stats
|
||||
struct PageView {
|
||||
std::string path;
|
||||
int64_t views;
|
||||
};
|
||||
|
||||
/// Analytics data
|
||||
struct AnalyticsData {
|
||||
std::string domain;
|
||||
std::string period;
|
||||
int64_t page_views;
|
||||
int64_t unique_visitors;
|
||||
int64_t bandwidth;
|
||||
std::vector<PageView> top_pages;
|
||||
};
|
||||
|
||||
/// Analytics options
|
||||
struct AnalyticsOptions {
|
||||
std::optional<std::string> period;
|
||||
std::optional<std::string> start;
|
||||
std::optional<std::string> end;
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
class SynorHostingImpl;
|
||||
|
||||
/// Synor Hosting client
|
||||
class SynorHosting {
|
||||
public:
|
||||
explicit SynorHosting(const Config& config);
|
||||
~SynorHosting();
|
||||
|
||||
SynorHosting(const SynorHosting&) = delete;
|
||||
SynorHosting& operator=(const SynorHosting&) = delete;
|
||||
SynorHosting(SynorHosting&&) noexcept;
|
||||
SynorHosting& operator=(SynorHosting&&) noexcept;
|
||||
|
||||
// Domain operations
|
||||
std::future<DomainAvailability> check_availability(const std::string& name);
|
||||
std::future<Domain> register_domain(const std::string& name,
|
||||
const RegisterDomainOptions& options = {});
|
||||
std::future<Domain> get_domain(const std::string& name);
|
||||
std::future<std::vector<Domain>> list_domains();
|
||||
std::future<Domain> update_domain_record(const std::string& name, const DomainRecord& record);
|
||||
std::future<DomainRecord> resolve_domain(const std::string& name);
|
||||
std::future<Domain> renew_domain(const std::string& name, int32_t years);
|
||||
|
||||
// DNS operations
|
||||
std::future<DnsZone> get_dns_zone(const std::string& domain);
|
||||
std::future<DnsZone> set_dns_records(const std::string& domain,
|
||||
const std::vector<DnsRecord>& records);
|
||||
std::future<DnsZone> add_dns_record(const std::string& domain, const DnsRecord& record);
|
||||
std::future<DnsZone> delete_dns_record(const std::string& domain,
|
||||
const std::string& record_type,
|
||||
const std::string& name);
|
||||
|
||||
// Deployment operations
|
||||
std::future<Deployment> deploy(const std::string& cid, const DeployOptions& options = {});
|
||||
std::future<Deployment> get_deployment(const std::string& id);
|
||||
std::future<std::vector<Deployment>> list_deployments(
|
||||
std::optional<std::string> domain = std::nullopt);
|
||||
std::future<Deployment> rollback(const std::string& domain, const std::string& deployment_id);
|
||||
std::future<void> delete_deployment(const std::string& id);
|
||||
|
||||
// SSL operations
|
||||
std::future<Certificate> provision_ssl(const std::string& domain,
|
||||
const ProvisionSslOptions& options = {});
|
||||
std::future<Certificate> get_certificate(const std::string& domain);
|
||||
std::future<Certificate> renew_certificate(const std::string& domain);
|
||||
std::future<void> delete_certificate(const std::string& domain);
|
||||
|
||||
// Site configuration
|
||||
std::future<SiteConfig> get_site_config(const std::string& domain);
|
||||
std::future<SiteConfig> update_site_config(const std::string& domain,
|
||||
const std::map<std::string, std::string>& config);
|
||||
std::future<int64_t> purge_cache(const std::string& domain,
|
||||
std::optional<std::vector<std::string>> paths = std::nullopt);
|
||||
|
||||
// Analytics
|
||||
std::future<AnalyticsData> get_analytics(const std::string& domain,
|
||||
const AnalyticsOptions& options = {});
|
||||
|
||||
// Lifecycle
|
||||
void close();
|
||||
bool is_closed() const;
|
||||
std::future<bool> health_check();
|
||||
|
||||
private:
|
||||
std::unique_ptr<SynorHostingImpl> impl_;
|
||||
};
|
||||
|
||||
} // namespace hosting
|
||||
} // namespace synor
|
||||
237
sdk/csharp/Synor.Sdk/Bridge/SynorBridge.cs
Normal file
237
sdk/csharp/Synor.Sdk/Bridge/SynorBridge.cs
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Synor.Sdk.Bridge;
|
||||
|
||||
/// <summary>
|
||||
/// Synor Bridge SDK client for C#.
|
||||
/// Cross-chain asset transfers with lock-mint and burn-unlock patterns.
|
||||
/// </summary>
|
||||
public class SynorBridge : IDisposable
|
||||
{
|
||||
private readonly BridgeConfig _config;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private bool _disposed;
|
||||
private static readonly HashSet<TransferStatus> FinalStatuses = new() { TransferStatus.Completed, TransferStatus.Failed, TransferStatus.Refunded };
|
||||
|
||||
public SynorBridge(BridgeConfig config)
|
||||
{
|
||||
_config = config;
|
||||
_httpClient = new HttpClient { BaseAddress = new Uri(config.Endpoint), Timeout = config.Timeout };
|
||||
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}");
|
||||
_httpClient.DefaultRequestHeaders.Add("X-SDK-Version", "csharp/0.1.0");
|
||||
_jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNameCaseInsensitive = true };
|
||||
}
|
||||
|
||||
// Chain Operations
|
||||
public async Task<List<Chain>> GetSupportedChainsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var r = await GetAsync<ChainsResponse>("/chains", ct);
|
||||
return r.Chains ?? new List<Chain>();
|
||||
}
|
||||
|
||||
public async Task<Chain> GetChainAsync(ChainId chainId, CancellationToken ct = default)
|
||||
=> await GetAsync<Chain>($"/chains/{chainId.ToString().ToLower()}", ct);
|
||||
|
||||
public async Task<bool> IsChainSupportedAsync(ChainId chainId, CancellationToken ct = default)
|
||||
{
|
||||
try { var c = await GetChainAsync(chainId, ct); return c.Supported; }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
// Asset Operations
|
||||
public async Task<List<Asset>> GetSupportedAssetsAsync(ChainId chainId, CancellationToken ct = default)
|
||||
{
|
||||
var r = await GetAsync<AssetsResponse>($"/chains/{chainId.ToString().ToLower()}/assets", ct);
|
||||
return r.Assets ?? new List<Asset>();
|
||||
}
|
||||
|
||||
public async Task<Asset> GetAssetAsync(string assetId, CancellationToken ct = default)
|
||||
=> await GetAsync<Asset>($"/assets/{Uri.EscapeDataString(assetId)}", ct);
|
||||
|
||||
public async Task<WrappedAsset> GetWrappedAssetAsync(string originalAssetId, ChainId targetChain, CancellationToken ct = default)
|
||||
=> await GetAsync<WrappedAsset>($"/assets/{Uri.EscapeDataString(originalAssetId)}/wrapped/{targetChain.ToString().ToLower()}", ct);
|
||||
|
||||
// Fee Operations
|
||||
public async Task<FeeEstimate> EstimateFeeAsync(string asset, string amount, ChainId sourceChain, ChainId targetChain, CancellationToken ct = default)
|
||||
=> await PostAsync<FeeEstimate>("/fees/estimate", new { asset, amount, sourceChain = sourceChain.ToString().ToLower(), targetChain = targetChain.ToString().ToLower() }, ct);
|
||||
|
||||
public async Task<ExchangeRate> GetExchangeRateAsync(string fromAsset, string toAsset, CancellationToken ct = default)
|
||||
=> await GetAsync<ExchangeRate>($"/rates/{Uri.EscapeDataString(fromAsset)}/{Uri.EscapeDataString(toAsset)}", ct);
|
||||
|
||||
// Lock-Mint Flow
|
||||
public async Task<LockReceipt> LockAsync(string asset, string amount, ChainId targetChain, LockOptions? options = null, CancellationToken ct = default)
|
||||
{
|
||||
var body = new Dictionary<string, object> { ["asset"] = asset, ["amount"] = amount, ["targetChain"] = targetChain.ToString().ToLower() };
|
||||
if (options?.Recipient != null) body["recipient"] = options.Recipient;
|
||||
if (options?.Deadline != null) body["deadline"] = options.Deadline;
|
||||
if (options?.Slippage != null) body["slippage"] = options.Slippage;
|
||||
return await PostAsync<LockReceipt>("/transfers/lock", body, ct);
|
||||
}
|
||||
|
||||
public async Task<LockProof> GetLockProofAsync(string lockReceiptId, CancellationToken ct = default)
|
||||
=> await GetAsync<LockProof>($"/transfers/lock/{Uri.EscapeDataString(lockReceiptId)}/proof", ct);
|
||||
|
||||
public async Task<LockProof> WaitForLockProofAsync(string lockReceiptId, TimeSpan? pollInterval = null, TimeSpan? maxWait = null, CancellationToken ct = default)
|
||||
{
|
||||
var interval = pollInterval ?? TimeSpan.FromSeconds(5);
|
||||
var deadline = DateTime.UtcNow + (maxWait ?? TimeSpan.FromMinutes(10));
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
try { return await GetLockProofAsync(lockReceiptId, ct); }
|
||||
catch (BridgeException ex) when (ex.IsConfirmationsPending) { await Task.Delay(interval, ct); }
|
||||
}
|
||||
throw new BridgeException("Timeout waiting for lock proof", "CONFIRMATIONS_PENDING");
|
||||
}
|
||||
|
||||
public async Task<SignedTransaction> MintAsync(LockProof proof, string targetAddress, MintOptions? options = null, CancellationToken ct = default)
|
||||
{
|
||||
var body = new Dictionary<string, object> { ["proof"] = proof, ["targetAddress"] = targetAddress };
|
||||
if (options?.GasLimit != null) body["gasLimit"] = options.GasLimit;
|
||||
if (options?.MaxFeePerGas != null) body["maxFeePerGas"] = options.MaxFeePerGas;
|
||||
if (options?.MaxPriorityFeePerGas != null) body["maxPriorityFeePerGas"] = options.MaxPriorityFeePerGas;
|
||||
return await PostAsync<SignedTransaction>("/transfers/mint", body, ct);
|
||||
}
|
||||
|
||||
// Burn-Unlock Flow
|
||||
public async Task<BurnReceipt> BurnAsync(string wrappedAsset, string amount, BurnOptions? options = null, CancellationToken ct = default)
|
||||
{
|
||||
var body = new Dictionary<string, object> { ["wrappedAsset"] = wrappedAsset, ["amount"] = amount };
|
||||
if (options?.Recipient != null) body["recipient"] = options.Recipient;
|
||||
if (options?.Deadline != null) body["deadline"] = options.Deadline;
|
||||
return await PostAsync<BurnReceipt>("/transfers/burn", body, ct);
|
||||
}
|
||||
|
||||
public async Task<BurnProof> GetBurnProofAsync(string burnReceiptId, CancellationToken ct = default)
|
||||
=> await GetAsync<BurnProof>($"/transfers/burn/{Uri.EscapeDataString(burnReceiptId)}/proof", ct);
|
||||
|
||||
public async Task<BurnProof> WaitForBurnProofAsync(string burnReceiptId, TimeSpan? pollInterval = null, TimeSpan? maxWait = null, CancellationToken ct = default)
|
||||
{
|
||||
var interval = pollInterval ?? TimeSpan.FromSeconds(5);
|
||||
var deadline = DateTime.UtcNow + (maxWait ?? TimeSpan.FromMinutes(10));
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
try { return await GetBurnProofAsync(burnReceiptId, ct); }
|
||||
catch (BridgeException ex) when (ex.IsConfirmationsPending) { await Task.Delay(interval, ct); }
|
||||
}
|
||||
throw new BridgeException("Timeout waiting for burn proof", "CONFIRMATIONS_PENDING");
|
||||
}
|
||||
|
||||
public async Task<SignedTransaction> UnlockAsync(BurnProof proof, UnlockOptions? options = null, CancellationToken ct = default)
|
||||
{
|
||||
var body = new Dictionary<string, object> { ["proof"] = proof };
|
||||
if (options?.GasLimit != null) body["gasLimit"] = options.GasLimit;
|
||||
if (options?.GasPrice != null) body["gasPrice"] = options.GasPrice;
|
||||
return await PostAsync<SignedTransaction>("/transfers/unlock", body, ct);
|
||||
}
|
||||
|
||||
// Transfer Management
|
||||
public async Task<Transfer> GetTransferAsync(string transferId, CancellationToken ct = default)
|
||||
=> await GetAsync<Transfer>($"/transfers/{Uri.EscapeDataString(transferId)}", ct);
|
||||
|
||||
public async Task<TransferStatus> GetTransferStatusAsync(string transferId, CancellationToken ct = default)
|
||||
{
|
||||
var t = await GetTransferAsync(transferId, ct);
|
||||
return t.Status;
|
||||
}
|
||||
|
||||
public async Task<List<Transfer>> ListTransfersAsync(TransferFilter? filter = null, CancellationToken ct = default)
|
||||
{
|
||||
var q = new List<string>();
|
||||
if (filter?.Status != null) q.Add($"status={filter.Status.ToString()!.ToLower()}");
|
||||
if (filter?.SourceChain != null) q.Add($"sourceChain={filter.SourceChain.ToString()!.ToLower()}");
|
||||
if (filter?.TargetChain != null) q.Add($"targetChain={filter.TargetChain.ToString()!.ToLower()}");
|
||||
if (filter?.Limit != null) q.Add($"limit={filter.Limit}");
|
||||
if (filter?.Offset != null) q.Add($"offset={filter.Offset}");
|
||||
var path = q.Count > 0 ? $"/transfers?{string.Join("&", q)}" : "/transfers";
|
||||
var r = await GetAsync<TransfersResponse>(path, ct);
|
||||
return r.Transfers ?? new List<Transfer>();
|
||||
}
|
||||
|
||||
public async Task<Transfer> WaitForTransferAsync(string transferId, TimeSpan? pollInterval = null, TimeSpan? maxWait = null, CancellationToken ct = default)
|
||||
{
|
||||
var interval = pollInterval ?? TimeSpan.FromSeconds(10);
|
||||
var deadline = DateTime.UtcNow + (maxWait ?? TimeSpan.FromMinutes(30));
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
var t = await GetTransferAsync(transferId, ct);
|
||||
if (FinalStatuses.Contains(t.Status)) return t;
|
||||
await Task.Delay(interval, ct);
|
||||
}
|
||||
throw new BridgeException("Timeout waiting for transfer completion");
|
||||
}
|
||||
|
||||
// Convenience Methods
|
||||
public async Task<Transfer> BridgeToAsync(string asset, string amount, ChainId targetChain, string targetAddress, LockOptions? lockOptions = null, MintOptions? mintOptions = null, CancellationToken ct = default)
|
||||
{
|
||||
var receipt = await LockAsync(asset, amount, targetChain, lockOptions, ct);
|
||||
if (_config.Debug) Console.WriteLine($"Locked: {receipt.Id}");
|
||||
var proof = await WaitForLockProofAsync(receipt.Id, ct: ct);
|
||||
if (_config.Debug) Console.WriteLine($"Proof ready, minting on {targetChain}");
|
||||
await MintAsync(proof, targetAddress, mintOptions, ct);
|
||||
return await WaitForTransferAsync(receipt.Id, ct: ct);
|
||||
}
|
||||
|
||||
public async Task<Transfer> BridgeBackAsync(string wrappedAsset, string amount, BurnOptions? burnOptions = null, UnlockOptions? unlockOptions = null, CancellationToken ct = default)
|
||||
{
|
||||
var receipt = await BurnAsync(wrappedAsset, amount, burnOptions, ct);
|
||||
if (_config.Debug) Console.WriteLine($"Burned: {receipt.Id}");
|
||||
var proof = await WaitForBurnProofAsync(receipt.Id, ct: ct);
|
||||
if (_config.Debug) Console.WriteLine($"Proof ready, unlocking on {receipt.TargetChain}");
|
||||
await UnlockAsync(proof, unlockOptions, ct);
|
||||
return await WaitForTransferAsync(receipt.Id, ct: ct);
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
public async Task<bool> HealthCheckAsync(CancellationToken ct = default)
|
||||
{
|
||||
try { var r = await GetAsync<BridgeHealthResponse>("/health", ct); return r.Status == "healthy"; }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public bool IsClosed => _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed) { _httpClient.Dispose(); _disposed = true; }
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
// Private HTTP methods
|
||||
private async Task<T> GetAsync<T>(string path, CancellationToken ct)
|
||||
=> await ExecuteAsync(async () => {
|
||||
var r = await _httpClient.GetAsync(path, ct);
|
||||
await EnsureSuccessAsync(r);
|
||||
return await r.Content.ReadFromJsonAsync<T>(_jsonOptions, ct)!;
|
||||
});
|
||||
|
||||
private async Task<T> PostAsync<T>(string path, object body, CancellationToken ct)
|
||||
=> await ExecuteAsync(async () => {
|
||||
var r = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct);
|
||||
await EnsureSuccessAsync(r);
|
||||
return await r.Content.ReadFromJsonAsync<T>(_jsonOptions, ct)!;
|
||||
});
|
||||
|
||||
private async Task<T> ExecuteAsync<T>(Func<Task<T>> op)
|
||||
{
|
||||
Exception? err = null;
|
||||
for (int i = 0; i < _config.Retries; i++)
|
||||
{
|
||||
try { return await op(); }
|
||||
catch (Exception ex) { err = ex; if (i < _config.Retries - 1) await Task.Delay(1000 << i); }
|
||||
}
|
||||
throw err!;
|
||||
}
|
||||
|
||||
private async Task EnsureSuccessAsync(HttpResponseMessage r)
|
||||
{
|
||||
if (!r.IsSuccessStatusCode)
|
||||
{
|
||||
var c = await r.Content.ReadAsStringAsync();
|
||||
var e = JsonSerializer.Deserialize<Dictionary<string, object>>(c, _jsonOptions);
|
||||
throw new BridgeException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}",
|
||||
e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
185
sdk/csharp/Synor.Sdk/Bridge/Types.cs
Normal file
185
sdk/csharp/Synor.Sdk/Bridge/Types.cs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
namespace Synor.Sdk.Bridge;
|
||||
|
||||
public record BridgeConfig
|
||||
{
|
||||
public required string ApiKey { get; init; }
|
||||
public string Endpoint { get; init; } = "https://bridge.synor.io/v1";
|
||||
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(60);
|
||||
public int Retries { get; init; } = 3;
|
||||
public bool Debug { get; init; } = false;
|
||||
}
|
||||
|
||||
public enum ChainId { Synor, Ethereum, Polygon, Arbitrum, Optimism, Bsc, Avalanche, Solana, Cosmos }
|
||||
public enum AssetType { Native, Erc20, Erc721, Erc1155 }
|
||||
public enum TransferStatus { Pending, Locked, Confirming, Minting, Completed, Failed, Refunded }
|
||||
public enum TransferDirection { LockMint, BurnUnlock }
|
||||
|
||||
public record NativeCurrency { public required string Name { get; init; } public required string Symbol { get; init; } public int Decimals { get; init; } }
|
||||
|
||||
public record Chain
|
||||
{
|
||||
public ChainId Id { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public long ChainIdNumber { get; init; }
|
||||
public required string RpcUrl { get; init; }
|
||||
public required string ExplorerUrl { get; init; }
|
||||
public required NativeCurrency NativeCurrency { get; init; }
|
||||
public int Confirmations { get; init; }
|
||||
public int EstimatedBlockTime { get; init; }
|
||||
public bool Supported { get; init; }
|
||||
}
|
||||
|
||||
public record Asset
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Symbol { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public AssetType Type { get; init; }
|
||||
public ChainId Chain { get; init; }
|
||||
public string? ContractAddress { get; init; }
|
||||
public int Decimals { get; init; }
|
||||
public string? LogoUrl { get; init; }
|
||||
public bool Verified { get; init; }
|
||||
}
|
||||
|
||||
public record WrappedAsset
|
||||
{
|
||||
public required Asset OriginalAsset { get; init; }
|
||||
public required Asset WrappedAssetData { get; init; }
|
||||
public ChainId Chain { get; init; }
|
||||
public required string BridgeContract { get; init; }
|
||||
}
|
||||
|
||||
public record ValidatorSignature { public required string Validator { get; init; } public required string Signature { get; init; } public long Timestamp { get; init; } }
|
||||
|
||||
public record LockReceipt
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string TxHash { get; init; }
|
||||
public ChainId SourceChain { get; init; }
|
||||
public ChainId TargetChain { get; init; }
|
||||
public required Asset Asset { get; init; }
|
||||
public required string Amount { get; init; }
|
||||
public required string Sender { get; init; }
|
||||
public required string Recipient { get; init; }
|
||||
public long LockTimestamp { get; init; }
|
||||
public int Confirmations { get; init; }
|
||||
public int RequiredConfirmations { get; init; }
|
||||
}
|
||||
|
||||
public record LockProof
|
||||
{
|
||||
public required LockReceipt LockReceipt { get; init; }
|
||||
public required List<string> MerkleProof { get; init; }
|
||||
public required string BlockHeader { get; init; }
|
||||
public required List<ValidatorSignature> Signatures { get; init; }
|
||||
}
|
||||
|
||||
public record BurnReceipt
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string TxHash { get; init; }
|
||||
public ChainId SourceChain { get; init; }
|
||||
public ChainId TargetChain { get; init; }
|
||||
public required Asset WrappedAsset { get; init; }
|
||||
public required Asset OriginalAsset { get; init; }
|
||||
public required string Amount { get; init; }
|
||||
public required string Sender { get; init; }
|
||||
public required string Recipient { get; init; }
|
||||
public long BurnTimestamp { get; init; }
|
||||
public int Confirmations { get; init; }
|
||||
public int RequiredConfirmations { get; init; }
|
||||
}
|
||||
|
||||
public record BurnProof
|
||||
{
|
||||
public required BurnReceipt BurnReceipt { get; init; }
|
||||
public required List<string> MerkleProof { get; init; }
|
||||
public required string BlockHeader { get; init; }
|
||||
public required List<ValidatorSignature> Signatures { get; init; }
|
||||
}
|
||||
|
||||
public record Transfer
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public TransferDirection Direction { get; init; }
|
||||
public TransferStatus Status { get; init; }
|
||||
public ChainId SourceChain { get; init; }
|
||||
public ChainId TargetChain { get; init; }
|
||||
public required Asset Asset { get; init; }
|
||||
public required string Amount { get; init; }
|
||||
public required string Sender { get; init; }
|
||||
public required string Recipient { get; init; }
|
||||
public string? SourceTxHash { get; init; }
|
||||
public string? TargetTxHash { get; init; }
|
||||
public required string Fee { get; init; }
|
||||
public required Asset FeeAsset { get; init; }
|
||||
public long CreatedAt { get; init; }
|
||||
public long UpdatedAt { get; init; }
|
||||
public long? CompletedAt { get; init; }
|
||||
public string? ErrorMessage { get; init; }
|
||||
}
|
||||
|
||||
public record TransferFilter
|
||||
{
|
||||
public TransferStatus? Status { get; init; }
|
||||
public ChainId? SourceChain { get; init; }
|
||||
public ChainId? TargetChain { get; init; }
|
||||
public int? Limit { get; init; }
|
||||
public int? Offset { get; init; }
|
||||
}
|
||||
|
||||
public record FeeEstimate
|
||||
{
|
||||
public required string BridgeFee { get; init; }
|
||||
public required string GasFeeSource { get; init; }
|
||||
public required string GasFeeTarget { get; init; }
|
||||
public required string TotalFee { get; init; }
|
||||
public required Asset FeeAsset { get; init; }
|
||||
public int EstimatedTime { get; init; }
|
||||
public string? ExchangeRate { get; init; }
|
||||
}
|
||||
|
||||
public record ExchangeRate
|
||||
{
|
||||
public required Asset FromAsset { get; init; }
|
||||
public required Asset ToAsset { get; init; }
|
||||
public required string Rate { get; init; }
|
||||
public required string InverseRate { get; init; }
|
||||
public long LastUpdated { get; init; }
|
||||
public required string Source { get; init; }
|
||||
}
|
||||
|
||||
public record SignedTransaction
|
||||
{
|
||||
public required string TxHash { get; init; }
|
||||
public ChainId Chain { get; init; }
|
||||
public required string From { get; init; }
|
||||
public required string To { get; init; }
|
||||
public required string Value { get; init; }
|
||||
public required string Data { get; init; }
|
||||
public required string GasLimit { get; init; }
|
||||
public string? GasPrice { get; init; }
|
||||
public string? MaxFeePerGas { get; init; }
|
||||
public string? MaxPriorityFeePerGas { get; init; }
|
||||
public int Nonce { get; init; }
|
||||
public required string Signature { get; init; }
|
||||
}
|
||||
|
||||
public record LockOptions { public string? Recipient { get; init; } public long? Deadline { get; init; } public double? Slippage { get; init; } }
|
||||
public record MintOptions { public string? GasLimit { get; init; } public string? MaxFeePerGas { get; init; } public string? MaxPriorityFeePerGas { get; init; } }
|
||||
public record BurnOptions { public string? Recipient { get; init; } public long? Deadline { get; init; } }
|
||||
public record UnlockOptions { public string? GasLimit { get; init; } public string? GasPrice { get; init; } }
|
||||
|
||||
internal record ChainsResponse { public List<Chain>? Chains { get; init; } }
|
||||
internal record AssetsResponse { public List<Asset>? Assets { get; init; } }
|
||||
internal record TransfersResponse { public List<Transfer>? Transfers { get; init; } }
|
||||
internal record BridgeHealthResponse { public required string Status { get; init; } }
|
||||
|
||||
public class BridgeException : Exception
|
||||
{
|
||||
public string? Code { get; }
|
||||
public int StatusCode { get; }
|
||||
public bool IsConfirmationsPending => Code == "CONFIRMATIONS_PENDING";
|
||||
public BridgeException(string message, string? code = null, int statusCode = 0) : base(message) { Code = code; StatusCode = statusCode; }
|
||||
}
|
||||
335
sdk/csharp/Synor.Sdk/Database/SynorDatabase.cs
Normal file
335
sdk/csharp/Synor.Sdk/Database/SynorDatabase.cs
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Web;
|
||||
|
||||
namespace Synor.Sdk.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Synor Database SDK client for C#.
|
||||
///
|
||||
/// Provides multi-model database with Key-Value, Document, Vector, and Time Series stores.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var db = new SynorDatabase(new DatabaseConfig { ApiKey = "your-api-key" });
|
||||
///
|
||||
/// // Key-Value operations
|
||||
/// await db.Kv.SetAsync("mykey", "myvalue");
|
||||
/// var value = await db.Kv.GetAsync("mykey");
|
||||
///
|
||||
/// // Document operations
|
||||
/// var id = await db.Documents.CreateAsync("users", new { name = "Alice", age = 30 });
|
||||
/// var doc = await db.Documents.GetAsync("users", id);
|
||||
///
|
||||
/// db.Dispose();
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class SynorDatabase : IDisposable
|
||||
{
|
||||
private readonly DatabaseConfig _config;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private bool _disposed;
|
||||
|
||||
public KeyValueStore Kv { get; }
|
||||
public DocumentStore Documents { get; }
|
||||
public VectorStore Vectors { get; }
|
||||
public TimeSeriesStore TimeSeries { get; }
|
||||
|
||||
public SynorDatabase(DatabaseConfig config)
|
||||
{
|
||||
_config = config;
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(config.Endpoint),
|
||||
Timeout = config.Timeout
|
||||
};
|
||||
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}");
|
||||
_httpClient.DefaultRequestHeaders.Add("X-SDK-Version", "csharp/0.1.0");
|
||||
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
Kv = new KeyValueStore(this);
|
||||
Documents = new DocumentStore(this);
|
||||
Vectors = new VectorStore(this);
|
||||
TimeSeries = new TimeSeriesStore(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Key-Value store operations.
|
||||
/// </summary>
|
||||
public class KeyValueStore
|
||||
{
|
||||
private readonly SynorDatabase _db;
|
||||
|
||||
internal KeyValueStore(SynorDatabase db) => _db = db;
|
||||
|
||||
public async Task<object?> GetAsync(string key, CancellationToken ct = default)
|
||||
{
|
||||
var response = await _db.GetAsync<KvGetResponse>($"/kv/{Uri.EscapeDataString(key)}", ct);
|
||||
return response.Value;
|
||||
}
|
||||
|
||||
public async Task SetAsync(string key, object value, int? ttl = null, CancellationToken ct = default)
|
||||
{
|
||||
var body = new Dictionary<string, object> { ["key"] = key, ["value"] = value };
|
||||
if (ttl.HasValue) body["ttl"] = ttl.Value;
|
||||
await _db.PutAsync<object>($"/kv/{Uri.EscapeDataString(key)}", body, ct);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string key, CancellationToken ct = default)
|
||||
{
|
||||
await _db.DeleteAsync($"/kv/{Uri.EscapeDataString(key)}", ct);
|
||||
}
|
||||
|
||||
public async Task<List<KeyValue>> ListAsync(string prefix, CancellationToken ct = default)
|
||||
{
|
||||
var response = await _db.GetAsync<KvListResponse>($"/kv?prefix={Uri.EscapeDataString(prefix)}", ct);
|
||||
return response.Items ?? new List<KeyValue>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Document store operations.
|
||||
/// </summary>
|
||||
public class DocumentStore
|
||||
{
|
||||
private readonly SynorDatabase _db;
|
||||
|
||||
internal DocumentStore(SynorDatabase db) => _db = db;
|
||||
|
||||
public async Task<string> CreateAsync(string collection, object document, CancellationToken ct = default)
|
||||
{
|
||||
var response = await _db.PostAsync<CreateDocumentResponse>(
|
||||
$"/collections/{Uri.EscapeDataString(collection)}/documents", document, ct);
|
||||
return response.Id;
|
||||
}
|
||||
|
||||
public async Task<Document> GetAsync(string collection, string id, CancellationToken ct = default)
|
||||
{
|
||||
return await _db.GetAsync<Document>(
|
||||
$"/collections/{Uri.EscapeDataString(collection)}/documents/{Uri.EscapeDataString(id)}", ct);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(string collection, string id, object update, CancellationToken ct = default)
|
||||
{
|
||||
await _db.PatchAsync<object>(
|
||||
$"/collections/{Uri.EscapeDataString(collection)}/documents/{Uri.EscapeDataString(id)}", update, ct);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string collection, string id, CancellationToken ct = default)
|
||||
{
|
||||
await _db.DeleteAsync(
|
||||
$"/collections/{Uri.EscapeDataString(collection)}/documents/{Uri.EscapeDataString(id)}", ct);
|
||||
}
|
||||
|
||||
public async Task<List<Document>> QueryAsync(string collection, DocumentQuery query, CancellationToken ct = default)
|
||||
{
|
||||
var response = await _db.PostAsync<DocumentListResponse>(
|
||||
$"/collections/{Uri.EscapeDataString(collection)}/query", query, ct);
|
||||
return response.Documents ?? new List<Document>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vector store operations.
|
||||
/// </summary>
|
||||
public class VectorStore
|
||||
{
|
||||
private readonly SynorDatabase _db;
|
||||
|
||||
internal VectorStore(SynorDatabase db) => _db = db;
|
||||
|
||||
public async Task UpsertAsync(string collection, IEnumerable<VectorEntry> vectors, CancellationToken ct = default)
|
||||
{
|
||||
await _db.PostAsync<object>(
|
||||
$"/vectors/{Uri.EscapeDataString(collection)}/upsert",
|
||||
new { vectors = vectors }, ct);
|
||||
}
|
||||
|
||||
public async Task<List<SearchResult>> SearchAsync(string collection, double[] vector, int k, CancellationToken ct = default)
|
||||
{
|
||||
var response = await _db.PostAsync<SearchListResponse>(
|
||||
$"/vectors/{Uri.EscapeDataString(collection)}/search",
|
||||
new { vector, k }, ct);
|
||||
return response.Results ?? new List<SearchResult>();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string collection, IEnumerable<string> ids, CancellationToken ct = default)
|
||||
{
|
||||
await _db.DeleteAsync($"/vectors/{Uri.EscapeDataString(collection)}", new { ids = ids }, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Time series store operations.
|
||||
/// </summary>
|
||||
public class TimeSeriesStore
|
||||
{
|
||||
private readonly SynorDatabase _db;
|
||||
|
||||
internal TimeSeriesStore(SynorDatabase db) => _db = db;
|
||||
|
||||
public async Task WriteAsync(string series, IEnumerable<DataPoint> points, CancellationToken ct = default)
|
||||
{
|
||||
await _db.PostAsync<object>(
|
||||
$"/timeseries/{Uri.EscapeDataString(series)}/write",
|
||||
new { points = points }, ct);
|
||||
}
|
||||
|
||||
public async Task<List<DataPoint>> QueryAsync(
|
||||
string series, TimeRange range, Aggregation? aggregation = null, CancellationToken ct = default)
|
||||
{
|
||||
var body = new Dictionary<string, object> { ["range"] = range };
|
||||
if (aggregation != null) body["aggregation"] = aggregation;
|
||||
|
||||
var response = await _db.PostAsync<DataPointListResponse>(
|
||||
$"/timeseries/{Uri.EscapeDataString(series)}/query", body, ct);
|
||||
return response.Points ?? new List<DataPoint>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a health check.
|
||||
/// </summary>
|
||||
public async Task<bool> HealthCheckAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await GetAsync<HealthResponse>("/health", ct);
|
||||
return response.Status == "healthy";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsClosed => _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
// Private HTTP methods
|
||||
internal async Task<T> GetAsync<T>(string path, CancellationToken ct)
|
||||
{
|
||||
return await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var response = await _httpClient.GetAsync(path, ct);
|
||||
await EnsureSuccessAsync(response);
|
||||
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, ct)
|
||||
?? throw new DatabaseException("Failed to deserialize response");
|
||||
});
|
||||
}
|
||||
|
||||
internal async Task<T> PostAsync<T>(string path, object body, CancellationToken ct)
|
||||
{
|
||||
return await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct);
|
||||
await EnsureSuccessAsync(response);
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
if (string.IsNullOrEmpty(content)) return default!;
|
||||
return JsonSerializer.Deserialize<T>(content, _jsonOptions)!;
|
||||
});
|
||||
}
|
||||
|
||||
internal async Task<T> PutAsync<T>(string path, object body, CancellationToken ct)
|
||||
{
|
||||
return await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var response = await _httpClient.PutAsJsonAsync(path, body, _jsonOptions, ct);
|
||||
await EnsureSuccessAsync(response);
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
if (string.IsNullOrEmpty(content)) return default!;
|
||||
return JsonSerializer.Deserialize<T>(content, _jsonOptions)!;
|
||||
});
|
||||
}
|
||||
|
||||
internal async Task<T> PatchAsync<T>(string path, object body, CancellationToken ct)
|
||||
{
|
||||
return await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Patch, path)
|
||||
{
|
||||
Content = JsonContent.Create(body, options: _jsonOptions)
|
||||
};
|
||||
var response = await _httpClient.SendAsync(request, ct);
|
||||
await EnsureSuccessAsync(response);
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
if (string.IsNullOrEmpty(content)) return default!;
|
||||
return JsonSerializer.Deserialize<T>(content, _jsonOptions)!;
|
||||
});
|
||||
}
|
||||
|
||||
internal async Task DeleteAsync(string path, CancellationToken ct)
|
||||
{
|
||||
await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync(path, ct);
|
||||
await EnsureSuccessAsync(response);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
internal async Task DeleteAsync(string path, object body, CancellationToken ct)
|
||||
{
|
||||
await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Delete, path)
|
||||
{
|
||||
Content = JsonContent.Create(body, options: _jsonOptions)
|
||||
};
|
||||
var response = await _httpClient.SendAsync(request, ct);
|
||||
await EnsureSuccessAsync(response);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation)
|
||||
{
|
||||
Exception? lastError = null;
|
||||
|
||||
for (int attempt = 0; attempt < _config.Retries; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await operation();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastError = ex;
|
||||
if (_config.Debug) Console.WriteLine($"Attempt {attempt + 1} failed: {ex.Message}");
|
||||
if (attempt < _config.Retries - 1)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1 << attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? new DatabaseException("Unknown error");
|
||||
}
|
||||
|
||||
private async Task EnsureSuccessAsync(HttpResponseMessage response)
|
||||
{
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var error = JsonSerializer.Deserialize<Dictionary<string, object>>(content, _jsonOptions);
|
||||
var message = error?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)response.StatusCode}";
|
||||
var code = error?.GetValueOrDefault("code")?.ToString();
|
||||
throw new DatabaseException(message, code, (int)response.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
152
sdk/csharp/Synor.Sdk/Database/Types.cs
Normal file
152
sdk/csharp/Synor.Sdk/Database/Types.cs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Synor.Sdk.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Database configuration.
|
||||
/// </summary>
|
||||
public record DatabaseConfig
|
||||
{
|
||||
public required string ApiKey { get; init; }
|
||||
public string Endpoint { get; init; } = "https://db.synor.io/v1";
|
||||
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(60);
|
||||
public int Retries { get; init; } = 3;
|
||||
public bool Debug { get; init; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A key-value entry.
|
||||
/// </summary>
|
||||
public record KeyValue
|
||||
{
|
||||
public required string Key { get; init; }
|
||||
public object? Value { get; init; }
|
||||
public int? Ttl { get; init; }
|
||||
public long? CreatedAt { get; init; }
|
||||
public long? UpdatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A document.
|
||||
/// </summary>
|
||||
public record Document
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Collection { get; init; }
|
||||
public required Dictionary<string, object> Data { get; init; }
|
||||
public long CreatedAt { get; init; }
|
||||
public long UpdatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Query parameters for documents.
|
||||
/// </summary>
|
||||
public record DocumentQuery
|
||||
{
|
||||
public Dictionary<string, object>? Filter { get; init; }
|
||||
public Dictionary<string, int>? Sort { get; init; }
|
||||
public int? Limit { get; init; }
|
||||
public int? Offset { get; init; }
|
||||
public List<string>? Projection { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A vector entry.
|
||||
/// </summary>
|
||||
public record VectorEntry
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required double[] Vector { get; init; }
|
||||
public Dictionary<string, object>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A search result.
|
||||
/// </summary>
|
||||
public record SearchResult
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public double Score { get; init; }
|
||||
public double[]? Vector { get; init; }
|
||||
public Dictionary<string, object>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A time series data point.
|
||||
/// </summary>
|
||||
public record DataPoint
|
||||
{
|
||||
public long Timestamp { get; init; }
|
||||
public double Value { get; init; }
|
||||
public Dictionary<string, string>? Tags { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Time range for queries.
|
||||
/// </summary>
|
||||
public record TimeRange
|
||||
{
|
||||
public long Start { get; init; }
|
||||
public long End { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregation settings.
|
||||
/// </summary>
|
||||
public record Aggregation
|
||||
{
|
||||
public required string Function { get; init; } // avg, sum, min, max, count
|
||||
public required string Interval { get; init; } // 1m, 5m, 1h, 1d
|
||||
}
|
||||
|
||||
// Internal response types
|
||||
internal record KvGetResponse
|
||||
{
|
||||
public object? Value { get; init; }
|
||||
}
|
||||
|
||||
internal record KvListResponse
|
||||
{
|
||||
public List<KeyValue>? Items { get; init; }
|
||||
}
|
||||
|
||||
internal record CreateDocumentResponse
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
}
|
||||
|
||||
internal record DocumentListResponse
|
||||
{
|
||||
public List<Document>? Documents { get; init; }
|
||||
}
|
||||
|
||||
internal record SearchListResponse
|
||||
{
|
||||
public List<SearchResult>? Results { get; init; }
|
||||
}
|
||||
|
||||
internal record DataPointListResponse
|
||||
{
|
||||
public List<DataPoint>? Points { get; init; }
|
||||
}
|
||||
|
||||
internal record HealthResponse
|
||||
{
|
||||
public required string Status { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Database exception.
|
||||
/// </summary>
|
||||
public class DatabaseException : Exception
|
||||
{
|
||||
public string? Code { get; }
|
||||
public int StatusCode { get; }
|
||||
|
||||
public DatabaseException(string message, string? code = null, int statusCode = 0)
|
||||
: base(message)
|
||||
{
|
||||
Code = code;
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
}
|
||||
204
sdk/csharp/Synor.Sdk/Hosting/SynorHosting.cs
Normal file
204
sdk/csharp/Synor.Sdk/Hosting/SynorHosting.cs
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Synor.Sdk.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Synor Hosting SDK client for C#.
|
||||
/// Provides decentralized web hosting with domain management, DNS, deployments, and SSL.
|
||||
/// </summary>
|
||||
public class SynorHosting : IDisposable
|
||||
{
|
||||
private readonly HostingConfig _config;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private bool _disposed;
|
||||
|
||||
public SynorHosting(HostingConfig config)
|
||||
{
|
||||
_config = config;
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(config.Endpoint),
|
||||
Timeout = config.Timeout
|
||||
};
|
||||
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}");
|
||||
_httpClient.DefaultRequestHeaders.Add("X-SDK-Version", "csharp/0.1.0");
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
}
|
||||
|
||||
// Domain Operations
|
||||
public async Task<DomainAvailability> CheckAvailabilityAsync(string name, CancellationToken ct = default)
|
||||
=> await GetAsync<DomainAvailability>($"/domains/check/{Uri.EscapeDataString(name)}", ct);
|
||||
|
||||
public async Task<Domain> RegisterDomainAsync(string name, RegisterDomainOptions? options = null, CancellationToken ct = default)
|
||||
{
|
||||
var body = new Dictionary<string, object> { ["name"] = name };
|
||||
if (options?.Years != null) body["years"] = options.Years;
|
||||
if (options?.AutoRenew != null) body["autoRenew"] = options.AutoRenew;
|
||||
return await PostAsync<Domain>("/domains", body, ct);
|
||||
}
|
||||
|
||||
public async Task<Domain> GetDomainAsync(string name, CancellationToken ct = default)
|
||||
=> await GetAsync<Domain>($"/domains/{Uri.EscapeDataString(name)}", ct);
|
||||
|
||||
public async Task<List<Domain>> ListDomainsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var response = await GetAsync<DomainsResponse>("/domains", ct);
|
||||
return response.Domains ?? new List<Domain>();
|
||||
}
|
||||
|
||||
public async Task<DomainRecord> ResolveDomainAsync(string name, CancellationToken ct = default)
|
||||
=> await GetAsync<DomainRecord>($"/domains/{Uri.EscapeDataString(name)}/resolve", ct);
|
||||
|
||||
public async Task<Domain> RenewDomainAsync(string name, int years, CancellationToken ct = default)
|
||||
=> await PostAsync<Domain>($"/domains/{Uri.EscapeDataString(name)}/renew", new { years }, ct);
|
||||
|
||||
// DNS Operations
|
||||
public async Task<DnsZone> GetDnsZoneAsync(string domain, CancellationToken ct = default)
|
||||
=> await GetAsync<DnsZone>($"/dns/{Uri.EscapeDataString(domain)}", ct);
|
||||
|
||||
public async Task<DnsZone> SetDnsRecordsAsync(string domain, IEnumerable<DnsRecord> records, CancellationToken ct = default)
|
||||
=> await PutAsync<DnsZone>($"/dns/{Uri.EscapeDataString(domain)}", new { records }, ct);
|
||||
|
||||
public async Task<DnsZone> AddDnsRecordAsync(string domain, DnsRecord record, CancellationToken ct = default)
|
||||
=> await PostAsync<DnsZone>($"/dns/{Uri.EscapeDataString(domain)}/records", record, ct);
|
||||
|
||||
// Deployment Operations
|
||||
public async Task<Deployment> DeployAsync(string cid, DeployOptions? options = null, CancellationToken ct = default)
|
||||
{
|
||||
var body = new Dictionary<string, object> { ["cid"] = cid };
|
||||
if (options?.Domain != null) body["domain"] = options.Domain;
|
||||
if (options?.Subdomain != null) body["subdomain"] = options.Subdomain;
|
||||
if (options?.Spa != null) body["spa"] = options.Spa;
|
||||
if (options?.CleanUrls != null) body["cleanUrls"] = options.CleanUrls;
|
||||
return await PostAsync<Deployment>("/deployments", body, ct);
|
||||
}
|
||||
|
||||
public async Task<Deployment> GetDeploymentAsync(string id, CancellationToken ct = default)
|
||||
=> await GetAsync<Deployment>($"/deployments/{Uri.EscapeDataString(id)}", ct);
|
||||
|
||||
public async Task<List<Deployment>> ListDeploymentsAsync(string? domain = null, CancellationToken ct = default)
|
||||
{
|
||||
var path = domain != null ? $"/deployments?domain={Uri.EscapeDataString(domain)}" : "/deployments";
|
||||
var response = await GetAsync<DeploymentsResponse>(path, ct);
|
||||
return response.Deployments ?? new List<Deployment>();
|
||||
}
|
||||
|
||||
public async Task<Deployment> RollbackAsync(string domain, string deploymentId, CancellationToken ct = default)
|
||||
=> await PostAsync<Deployment>($"/deployments/{Uri.EscapeDataString(deploymentId)}/rollback", new { domain }, ct);
|
||||
|
||||
public async Task DeleteDeploymentAsync(string id, CancellationToken ct = default)
|
||||
=> await DeleteAsync($"/deployments/{Uri.EscapeDataString(id)}", ct);
|
||||
|
||||
// SSL Operations
|
||||
public async Task<Certificate> ProvisionSslAsync(string domain, ProvisionSslOptions? options = null, CancellationToken ct = default)
|
||||
{
|
||||
var body = options ?? new ProvisionSslOptions();
|
||||
return await PostAsync<Certificate>($"/ssl/{Uri.EscapeDataString(domain)}", body, ct);
|
||||
}
|
||||
|
||||
public async Task<Certificate> GetCertificateAsync(string domain, CancellationToken ct = default)
|
||||
=> await GetAsync<Certificate>($"/ssl/{Uri.EscapeDataString(domain)}", ct);
|
||||
|
||||
public async Task<Certificate> RenewCertificateAsync(string domain, CancellationToken ct = default)
|
||||
=> await PostAsync<Certificate>($"/ssl/{Uri.EscapeDataString(domain)}/renew", new { }, ct);
|
||||
|
||||
// Analytics
|
||||
public async Task<AnalyticsData> GetAnalyticsAsync(string domain, AnalyticsOptions? options = null, CancellationToken ct = default)
|
||||
{
|
||||
var path = $"/sites/{Uri.EscapeDataString(domain)}/analytics";
|
||||
var query = new List<string>();
|
||||
if (options?.Period != null) query.Add($"period={Uri.EscapeDataString(options.Period)}");
|
||||
if (options?.Start != null) query.Add($"start={Uri.EscapeDataString(options.Start)}");
|
||||
if (options?.End != null) query.Add($"end={Uri.EscapeDataString(options.End)}");
|
||||
if (query.Count > 0) path += "?" + string.Join("&", query);
|
||||
return await GetAsync<AnalyticsData>(path, ct);
|
||||
}
|
||||
|
||||
public async Task<long> PurgeCacheAsync(string domain, IEnumerable<string>? paths = null, CancellationToken ct = default)
|
||||
{
|
||||
var body = paths != null ? new { paths } : null;
|
||||
var response = await DeleteAsync<PurgeResponse>($"/sites/{Uri.EscapeDataString(domain)}/cache", body, ct);
|
||||
return response.Purged;
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
public async Task<bool> HealthCheckAsync(CancellationToken ct = default)
|
||||
{
|
||||
try { var r = await GetAsync<HostingHealthResponse>("/health", ct); return r.Status == "healthy"; }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public bool IsClosed => _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed) { _httpClient.Dispose(); _disposed = true; }
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
// Private HTTP methods
|
||||
private async Task<T> GetAsync<T>(string path, CancellationToken ct)
|
||||
=> await ExecuteAsync(async () => {
|
||||
var r = await _httpClient.GetAsync(path, ct);
|
||||
await EnsureSuccessAsync(r);
|
||||
return await r.Content.ReadFromJsonAsync<T>(_jsonOptions, ct)!;
|
||||
});
|
||||
|
||||
private async Task<T> PostAsync<T>(string path, object body, CancellationToken ct)
|
||||
=> await ExecuteAsync(async () => {
|
||||
var r = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct);
|
||||
await EnsureSuccessAsync(r);
|
||||
return await r.Content.ReadFromJsonAsync<T>(_jsonOptions, ct)!;
|
||||
});
|
||||
|
||||
private async Task<T> PutAsync<T>(string path, object body, CancellationToken ct)
|
||||
=> await ExecuteAsync(async () => {
|
||||
var r = await _httpClient.PutAsJsonAsync(path, body, _jsonOptions, ct);
|
||||
await EnsureSuccessAsync(r);
|
||||
return await r.Content.ReadFromJsonAsync<T>(_jsonOptions, ct)!;
|
||||
});
|
||||
|
||||
private async Task DeleteAsync(string path, CancellationToken ct)
|
||||
=> await ExecuteAsync(async () => {
|
||||
var r = await _httpClient.DeleteAsync(path, ct);
|
||||
await EnsureSuccessAsync(r);
|
||||
return true;
|
||||
});
|
||||
|
||||
private async Task<T> DeleteAsync<T>(string path, object? body, CancellationToken ct)
|
||||
=> await ExecuteAsync(async () => {
|
||||
var req = new HttpRequestMessage(HttpMethod.Delete, path);
|
||||
if (body != null) req.Content = JsonContent.Create(body, options: _jsonOptions);
|
||||
var r = await _httpClient.SendAsync(req, ct);
|
||||
await EnsureSuccessAsync(r);
|
||||
return await r.Content.ReadFromJsonAsync<T>(_jsonOptions, ct)!;
|
||||
});
|
||||
|
||||
private async Task<T> ExecuteAsync<T>(Func<Task<T>> op)
|
||||
{
|
||||
Exception? err = null;
|
||||
for (int i = 0; i < _config.Retries; i++)
|
||||
{
|
||||
try { return await op(); }
|
||||
catch (Exception ex) { err = ex; if (i < _config.Retries - 1) await Task.Delay(1000 << i); }
|
||||
}
|
||||
throw err!;
|
||||
}
|
||||
|
||||
private async Task EnsureSuccessAsync(HttpResponseMessage r)
|
||||
{
|
||||
if (!r.IsSuccessStatusCode)
|
||||
{
|
||||
var c = await r.Content.ReadAsStringAsync();
|
||||
var e = JsonSerializer.Deserialize<Dictionary<string, object>>(c, _jsonOptions);
|
||||
throw new HostingException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}",
|
||||
e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
152
sdk/csharp/Synor.Sdk/Hosting/Types.cs
Normal file
152
sdk/csharp/Synor.Sdk/Hosting/Types.cs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
namespace Synor.Sdk.Hosting;
|
||||
|
||||
public record HostingConfig
|
||||
{
|
||||
public required string ApiKey { get; init; }
|
||||
public string Endpoint { get; init; } = "https://hosting.synor.io/v1";
|
||||
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(60);
|
||||
public int Retries { get; init; } = 3;
|
||||
public bool Debug { get; init; } = false;
|
||||
}
|
||||
|
||||
public enum DomainStatus { Pending, Active, Expired, Suspended }
|
||||
public enum DeploymentStatus { Pending, Building, Deploying, Active, Failed, Inactive }
|
||||
public enum CertificateStatus { Pending, Issued, Expired, Revoked }
|
||||
public enum DnsRecordType { A, AAAA, CNAME, TXT, MX, NS, SRV, CAA }
|
||||
|
||||
public record DomainRecord
|
||||
{
|
||||
public string? Cid { get; init; }
|
||||
public List<string>? Ipv4 { get; init; }
|
||||
public List<string>? Ipv6 { get; init; }
|
||||
public string? Cname { get; init; }
|
||||
public List<string>? Txt { get; init; }
|
||||
public Dictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
public record Domain
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public DomainStatus Status { get; init; }
|
||||
public required string Owner { get; init; }
|
||||
public long RegisteredAt { get; init; }
|
||||
public long ExpiresAt { get; init; }
|
||||
public bool AutoRenew { get; init; }
|
||||
public DomainRecord? Records { get; init; }
|
||||
}
|
||||
|
||||
public record DomainAvailability
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public bool Available { get; init; }
|
||||
public double? Price { get; init; }
|
||||
public bool Premium { get; init; }
|
||||
}
|
||||
|
||||
public record RegisterDomainOptions
|
||||
{
|
||||
public int? Years { get; init; }
|
||||
public bool? AutoRenew { get; init; }
|
||||
}
|
||||
|
||||
public record DnsRecord
|
||||
{
|
||||
public DnsRecordType Type { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required string Value { get; init; }
|
||||
public int Ttl { get; init; } = 3600;
|
||||
public int? Priority { get; init; }
|
||||
}
|
||||
|
||||
public record DnsZone
|
||||
{
|
||||
public required string Domain { get; init; }
|
||||
public required List<DnsRecord> Records { get; init; }
|
||||
public long UpdatedAt { get; init; }
|
||||
}
|
||||
|
||||
public record Deployment
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Domain { get; init; }
|
||||
public required string Cid { get; init; }
|
||||
public DeploymentStatus Status { get; init; }
|
||||
public required string Url { get; init; }
|
||||
public long CreatedAt { get; init; }
|
||||
public long UpdatedAt { get; init; }
|
||||
public string? BuildLogs { get; init; }
|
||||
public string? ErrorMessage { get; init; }
|
||||
}
|
||||
|
||||
public record DeployOptions
|
||||
{
|
||||
public string? Domain { get; init; }
|
||||
public string? Subdomain { get; init; }
|
||||
public bool? Spa { get; init; }
|
||||
public bool? CleanUrls { get; init; }
|
||||
public bool? TrailingSlash { get; init; }
|
||||
}
|
||||
|
||||
public record Certificate
|
||||
{
|
||||
public required string Domain { get; init; }
|
||||
public CertificateStatus Status { get; init; }
|
||||
public bool AutoRenew { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public long? IssuedAt { get; init; }
|
||||
public long? ExpiresAt { get; init; }
|
||||
public string? Fingerprint { get; init; }
|
||||
}
|
||||
|
||||
public record ProvisionSslOptions
|
||||
{
|
||||
public bool? IncludeWww { get; init; }
|
||||
public bool? AutoRenew { get; init; }
|
||||
}
|
||||
|
||||
public record SiteConfig
|
||||
{
|
||||
public required string Domain { get; init; }
|
||||
public string? Cid { get; init; }
|
||||
public Dictionary<string, string>? Headers { get; init; }
|
||||
public bool Spa { get; init; }
|
||||
public bool CleanUrls { get; init; } = true;
|
||||
public bool TrailingSlash { get; init; }
|
||||
}
|
||||
|
||||
public record PageView { public required string Path { get; init; } public long Views { get; init; } }
|
||||
public record Referrer { public required string ReferrerUrl { get; init; } public long Count { get; init; } }
|
||||
public record Country { public required string CountryCode { get; init; } public long Count { get; init; } }
|
||||
|
||||
public record AnalyticsData
|
||||
{
|
||||
public required string Domain { get; init; }
|
||||
public required string Period { get; init; }
|
||||
public long PageViews { get; init; }
|
||||
public long UniqueVisitors { get; init; }
|
||||
public long Bandwidth { get; init; }
|
||||
public required List<PageView> TopPages { get; init; }
|
||||
}
|
||||
|
||||
public record AnalyticsOptions
|
||||
{
|
||||
public string? Period { get; init; }
|
||||
public string? Start { get; init; }
|
||||
public string? End { get; init; }
|
||||
}
|
||||
|
||||
internal record DomainsResponse { public List<Domain>? Domains { get; init; } }
|
||||
internal record DeploymentsResponse { public List<Deployment>? Deployments { get; init; } }
|
||||
internal record PurgeResponse { public long Purged { get; init; } }
|
||||
internal record HostingHealthResponse { public required string Status { get; init; } }
|
||||
|
||||
public class HostingException : Exception
|
||||
{
|
||||
public string? Code { get; }
|
||||
public int StatusCode { get; }
|
||||
public HostingException(string message, string? code = null, int statusCode = 0) : base(message)
|
||||
{
|
||||
Code = code;
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
}
|
||||
23
sdk/ruby/lib/synor_bridge.rb
Normal file
23
sdk/ruby/lib/synor_bridge.rb
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative "synor_bridge/version"
|
||||
require_relative "synor_bridge/types"
|
||||
require_relative "synor_bridge/client"
|
||||
|
||||
module SynorBridge
|
||||
class Error < StandardError; end
|
||||
class ClientClosedError < Error; end
|
||||
class HttpError < Error
|
||||
attr_reader :status_code, :code
|
||||
|
||||
def initialize(message, status_code: 0, code: nil)
|
||||
super(message)
|
||||
@status_code = status_code
|
||||
@code = code
|
||||
end
|
||||
|
||||
def confirmations_pending?
|
||||
@code == "CONFIRMATIONS_PENDING"
|
||||
end
|
||||
end
|
||||
end
|
||||
579
sdk/ruby/lib/synor_bridge/client.rb
Normal file
579
sdk/ruby/lib/synor_bridge/client.rb
Normal file
|
|
@ -0,0 +1,579 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "faraday"
|
||||
require "json"
|
||||
require "uri"
|
||||
|
||||
module SynorBridge
|
||||
# Synor Bridge SDK client for Ruby.
|
||||
# Cross-chain asset transfers with lock-mint and burn-unlock patterns.
|
||||
class Client
|
||||
FINAL_STATUSES = [
|
||||
TransferStatus::COMPLETED,
|
||||
TransferStatus::FAILED,
|
||||
TransferStatus::REFUNDED
|
||||
].freeze
|
||||
|
||||
attr_reader :closed
|
||||
|
||||
def initialize(config)
|
||||
@config = config
|
||||
@closed = false
|
||||
@conn = Faraday.new(url: config.endpoint) do |f|
|
||||
f.request :json
|
||||
f.response :json
|
||||
f.options.timeout = config.timeout
|
||||
f.headers["Authorization"] = "Bearer #{config.api_key}"
|
||||
f.headers["Content-Type"] = "application/json"
|
||||
f.headers["X-SDK-Version"] = "ruby/#{VERSION}"
|
||||
end
|
||||
end
|
||||
|
||||
# ==================== Chain Operations ====================
|
||||
|
||||
# Get supported chains
|
||||
def get_supported_chains
|
||||
response = get("/chains")
|
||||
(response["chains"] || []).map { |c| parse_chain(c) }
|
||||
end
|
||||
|
||||
# Get chain details
|
||||
def get_chain(chain_id)
|
||||
response = get("/chains/#{chain_id.downcase}")
|
||||
parse_chain(response)
|
||||
end
|
||||
|
||||
# Check if chain is supported
|
||||
def chain_supported?(chain_id)
|
||||
chain = get_chain(chain_id)
|
||||
chain.supported
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
|
||||
# ==================== Asset Operations ====================
|
||||
|
||||
# Get supported assets for a chain
|
||||
def get_supported_assets(chain_id)
|
||||
response = get("/chains/#{chain_id.downcase}/assets")
|
||||
(response["assets"] || []).map { |a| parse_asset(a) }
|
||||
end
|
||||
|
||||
# Get asset details
|
||||
def get_asset(asset_id)
|
||||
response = get("/assets/#{encode(asset_id)}")
|
||||
parse_asset(response)
|
||||
end
|
||||
|
||||
# Get wrapped asset info
|
||||
def get_wrapped_asset(original_asset_id, target_chain)
|
||||
response = get("/assets/#{encode(original_asset_id)}/wrapped/#{target_chain.downcase}")
|
||||
parse_wrapped_asset(response)
|
||||
end
|
||||
|
||||
# ==================== Fee Operations ====================
|
||||
|
||||
# Estimate transfer fee
|
||||
def estimate_fee(asset:, amount:, source_chain:, target_chain:)
|
||||
body = {
|
||||
asset: asset,
|
||||
amount: amount,
|
||||
source_chain: source_chain.downcase,
|
||||
target_chain: target_chain.downcase
|
||||
}
|
||||
response = post("/fees/estimate", body)
|
||||
parse_fee_estimate(response)
|
||||
end
|
||||
|
||||
# Get exchange rate
|
||||
def get_exchange_rate(from_asset, to_asset)
|
||||
response = get("/rates/#{encode(from_asset)}/#{encode(to_asset)}")
|
||||
parse_exchange_rate(response)
|
||||
end
|
||||
|
||||
# ==================== Lock-Mint Flow ====================
|
||||
|
||||
# Lock assets for cross-chain transfer
|
||||
def lock(asset:, amount:, target_chain:, options: nil)
|
||||
body = {
|
||||
asset: asset,
|
||||
amount: amount,
|
||||
target_chain: target_chain.downcase
|
||||
}
|
||||
if options
|
||||
body[:recipient] = options.recipient if options.recipient
|
||||
body[:deadline] = options.deadline if options.deadline
|
||||
body[:slippage] = options.slippage if options.slippage
|
||||
end
|
||||
response = post("/transfers/lock", body)
|
||||
parse_lock_receipt(response)
|
||||
end
|
||||
|
||||
# Get lock proof
|
||||
def get_lock_proof(lock_receipt_id)
|
||||
response = get("/transfers/lock/#{encode(lock_receipt_id)}/proof")
|
||||
parse_lock_proof(response)
|
||||
end
|
||||
|
||||
# Wait for lock proof
|
||||
def wait_for_lock_proof(lock_receipt_id, poll_interval: 5, max_wait: 600)
|
||||
deadline = Time.now + max_wait
|
||||
while Time.now < deadline
|
||||
begin
|
||||
return get_lock_proof(lock_receipt_id)
|
||||
rescue HttpError => e
|
||||
raise e unless e.confirmations_pending?
|
||||
|
||||
sleep(poll_interval)
|
||||
end
|
||||
end
|
||||
raise HttpError.new("Timeout waiting for lock proof", code: "CONFIRMATIONS_PENDING")
|
||||
end
|
||||
|
||||
# Mint wrapped assets
|
||||
def mint(proof:, target_address:, options: nil)
|
||||
body = { proof: proof_to_hash(proof), target_address: target_address }
|
||||
if options
|
||||
body[:gas_limit] = options.gas_limit if options.gas_limit
|
||||
body[:max_fee_per_gas] = options.max_fee_per_gas if options.max_fee_per_gas
|
||||
body[:max_priority_fee_per_gas] = options.max_priority_fee_per_gas if options.max_priority_fee_per_gas
|
||||
end
|
||||
response = post("/transfers/mint", body)
|
||||
parse_signed_transaction(response)
|
||||
end
|
||||
|
||||
# ==================== Burn-Unlock Flow ====================
|
||||
|
||||
# Burn wrapped assets
|
||||
def burn(wrapped_asset:, amount:, options: nil)
|
||||
body = { wrapped_asset: wrapped_asset, amount: amount }
|
||||
if options
|
||||
body[:recipient] = options.recipient if options.recipient
|
||||
body[:deadline] = options.deadline if options.deadline
|
||||
end
|
||||
response = post("/transfers/burn", body)
|
||||
parse_burn_receipt(response)
|
||||
end
|
||||
|
||||
# Get burn proof
|
||||
def get_burn_proof(burn_receipt_id)
|
||||
response = get("/transfers/burn/#{encode(burn_receipt_id)}/proof")
|
||||
parse_burn_proof(response)
|
||||
end
|
||||
|
||||
# Wait for burn proof
|
||||
def wait_for_burn_proof(burn_receipt_id, poll_interval: 5, max_wait: 600)
|
||||
deadline = Time.now + max_wait
|
||||
while Time.now < deadline
|
||||
begin
|
||||
return get_burn_proof(burn_receipt_id)
|
||||
rescue HttpError => e
|
||||
raise e unless e.confirmations_pending?
|
||||
|
||||
sleep(poll_interval)
|
||||
end
|
||||
end
|
||||
raise HttpError.new("Timeout waiting for burn proof", code: "CONFIRMATIONS_PENDING")
|
||||
end
|
||||
|
||||
# Unlock original assets
|
||||
def unlock(proof:, options: nil)
|
||||
body = { proof: burn_proof_to_hash(proof) }
|
||||
if options
|
||||
body[:gas_limit] = options.gas_limit if options.gas_limit
|
||||
body[:gas_price] = options.gas_price if options.gas_price
|
||||
end
|
||||
response = post("/transfers/unlock", body)
|
||||
parse_signed_transaction(response)
|
||||
end
|
||||
|
||||
# ==================== Transfer Management ====================
|
||||
|
||||
# Get transfer details
|
||||
def get_transfer(transfer_id)
|
||||
response = get("/transfers/#{encode(transfer_id)}")
|
||||
parse_transfer(response)
|
||||
end
|
||||
|
||||
# Get transfer status
|
||||
def get_transfer_status(transfer_id)
|
||||
transfer = get_transfer(transfer_id)
|
||||
transfer.status
|
||||
end
|
||||
|
||||
# List transfers
|
||||
def list_transfers(filter: nil)
|
||||
params = {}
|
||||
if filter
|
||||
params[:status] = filter.status.downcase if filter.status
|
||||
params[:source_chain] = filter.source_chain.downcase if filter.source_chain
|
||||
params[:target_chain] = filter.target_chain.downcase if filter.target_chain
|
||||
params[:limit] = filter.limit if filter.limit
|
||||
params[:offset] = filter.offset if filter.offset
|
||||
end
|
||||
response = get("/transfers", params)
|
||||
(response["transfers"] || []).map { |t| parse_transfer(t) }
|
||||
end
|
||||
|
||||
# Wait for transfer completion
|
||||
def wait_for_transfer(transfer_id, poll_interval: 10, max_wait: 1800)
|
||||
deadline = Time.now + max_wait
|
||||
while Time.now < deadline
|
||||
transfer = get_transfer(transfer_id)
|
||||
return transfer if FINAL_STATUSES.include?(transfer.status)
|
||||
|
||||
sleep(poll_interval)
|
||||
end
|
||||
raise Error, "Timeout waiting for transfer completion"
|
||||
end
|
||||
|
||||
# ==================== Convenience Methods ====================
|
||||
|
||||
# Bridge assets to another chain (lock-mint flow)
|
||||
def bridge_to(asset:, amount:, target_chain:, target_address:, lock_options: nil, mint_options: nil)
|
||||
receipt = lock(asset: asset, amount: amount, target_chain: target_chain, options: lock_options)
|
||||
puts "Locked: #{receipt.id}, waiting for confirmations..." if @config.debug
|
||||
|
||||
proof = wait_for_lock_proof(receipt.id)
|
||||
puts "Proof ready, minting on #{target_chain}..." if @config.debug
|
||||
|
||||
mint(proof: proof, target_address: target_address, options: mint_options)
|
||||
wait_for_transfer(receipt.id)
|
||||
end
|
||||
|
||||
# Bridge assets back to original chain (burn-unlock flow)
|
||||
def bridge_back(wrapped_asset:, amount:, burn_options: nil, unlock_options: nil)
|
||||
receipt = burn(wrapped_asset: wrapped_asset, amount: amount, options: burn_options)
|
||||
puts "Burned: #{receipt.id}, waiting for confirmations..." if @config.debug
|
||||
|
||||
proof = wait_for_burn_proof(receipt.id)
|
||||
puts "Proof ready, unlocking on #{receipt.target_chain}..." if @config.debug
|
||||
|
||||
unlock(proof: proof, options: unlock_options)
|
||||
wait_for_transfer(receipt.id)
|
||||
end
|
||||
|
||||
# ==================== Lifecycle ====================
|
||||
|
||||
# Health check
|
||||
def health_check
|
||||
response = get("/health")
|
||||
response["status"] == "healthy"
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
|
||||
# Close the client
|
||||
def close
|
||||
@closed = true
|
||||
@conn.close if @conn.respond_to?(:close)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get(path, params = {})
|
||||
execute { @conn.get(path, params).body }
|
||||
end
|
||||
|
||||
def post(path, body)
|
||||
execute { @conn.post(path, body).body }
|
||||
end
|
||||
|
||||
def execute
|
||||
raise ClientClosedError, "Client has been closed" if @closed
|
||||
|
||||
last_error = nil
|
||||
@config.retries.times do |attempt|
|
||||
begin
|
||||
response = yield
|
||||
check_error(response) if response.is_a?(Hash)
|
||||
return response
|
||||
rescue StandardError => e
|
||||
last_error = e
|
||||
sleep(2**attempt) if attempt < @config.retries - 1
|
||||
end
|
||||
end
|
||||
raise last_error
|
||||
end
|
||||
|
||||
def check_error(response)
|
||||
return unless response["error"] || (response["code"] && response["message"])
|
||||
|
||||
message = response["message"] || response["error"] || "Unknown error"
|
||||
code = response["code"]
|
||||
status = response["status_code"] || 0
|
||||
raise HttpError.new(message, status_code: status, code: code)
|
||||
end
|
||||
|
||||
def encode(str)
|
||||
URI.encode_www_form_component(str)
|
||||
end
|
||||
|
||||
# Parsing methods
|
||||
|
||||
def parse_chain(data)
|
||||
return nil unless data
|
||||
|
||||
Chain.new(
|
||||
id: data["id"],
|
||||
name: data["name"],
|
||||
chain_id: data["chain_id"],
|
||||
rpc_url: data["rpc_url"],
|
||||
explorer_url: data["explorer_url"],
|
||||
native_currency: data["native_currency"] ? NativeCurrency.new(
|
||||
name: data["native_currency"]["name"],
|
||||
symbol: data["native_currency"]["symbol"],
|
||||
decimals: data["native_currency"]["decimals"]
|
||||
) : nil,
|
||||
confirmations: data["confirmations"],
|
||||
estimated_block_time: data["estimated_block_time"],
|
||||
supported: data["supported"]
|
||||
)
|
||||
end
|
||||
|
||||
def parse_asset(data)
|
||||
return nil unless data
|
||||
|
||||
Asset.new(
|
||||
id: data["id"],
|
||||
symbol: data["symbol"],
|
||||
name: data["name"],
|
||||
type: data["type"],
|
||||
chain: data["chain"],
|
||||
contract_address: data["contract_address"],
|
||||
decimals: data["decimals"],
|
||||
logo_url: data["logo_url"],
|
||||
verified: data["verified"]
|
||||
)
|
||||
end
|
||||
|
||||
def parse_wrapped_asset(data)
|
||||
return nil unless data
|
||||
|
||||
WrappedAsset.new(
|
||||
original_asset: parse_asset(data["original_asset"]),
|
||||
wrapped_asset: parse_asset(data["wrapped_asset"]),
|
||||
chain: data["chain"],
|
||||
bridge_contract: data["bridge_contract"]
|
||||
)
|
||||
end
|
||||
|
||||
def parse_lock_receipt(data)
|
||||
return nil unless data
|
||||
|
||||
LockReceipt.new(
|
||||
id: data["id"],
|
||||
tx_hash: data["tx_hash"],
|
||||
source_chain: data["source_chain"],
|
||||
target_chain: data["target_chain"],
|
||||
asset: parse_asset(data["asset"]),
|
||||
amount: data["amount"],
|
||||
sender: data["sender"],
|
||||
recipient: data["recipient"],
|
||||
lock_timestamp: data["lock_timestamp"],
|
||||
confirmations: data["confirmations"],
|
||||
required_confirmations: data["required_confirmations"]
|
||||
)
|
||||
end
|
||||
|
||||
def parse_lock_proof(data)
|
||||
return nil unless data
|
||||
|
||||
LockProof.new(
|
||||
lock_receipt: parse_lock_receipt(data["lock_receipt"]),
|
||||
merkle_proof: data["merkle_proof"],
|
||||
block_header: data["block_header"],
|
||||
signatures: (data["signatures"] || []).map do |s|
|
||||
ValidatorSignature.new(
|
||||
validator: s["validator"],
|
||||
signature: s["signature"],
|
||||
timestamp: s["timestamp"]
|
||||
)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def parse_burn_receipt(data)
|
||||
return nil unless data
|
||||
|
||||
BurnReceipt.new(
|
||||
id: data["id"],
|
||||
tx_hash: data["tx_hash"],
|
||||
source_chain: data["source_chain"],
|
||||
target_chain: data["target_chain"],
|
||||
wrapped_asset: parse_asset(data["wrapped_asset"]),
|
||||
original_asset: parse_asset(data["original_asset"]),
|
||||
amount: data["amount"],
|
||||
sender: data["sender"],
|
||||
recipient: data["recipient"],
|
||||
burn_timestamp: data["burn_timestamp"],
|
||||
confirmations: data["confirmations"],
|
||||
required_confirmations: data["required_confirmations"]
|
||||
)
|
||||
end
|
||||
|
||||
def parse_burn_proof(data)
|
||||
return nil unless data
|
||||
|
||||
BurnProof.new(
|
||||
burn_receipt: parse_burn_receipt(data["burn_receipt"]),
|
||||
merkle_proof: data["merkle_proof"],
|
||||
block_header: data["block_header"],
|
||||
signatures: (data["signatures"] || []).map do |s|
|
||||
ValidatorSignature.new(
|
||||
validator: s["validator"],
|
||||
signature: s["signature"],
|
||||
timestamp: s["timestamp"]
|
||||
)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def parse_transfer(data)
|
||||
return nil unless data
|
||||
|
||||
Transfer.new(
|
||||
id: data["id"],
|
||||
direction: data["direction"],
|
||||
status: data["status"],
|
||||
source_chain: data["source_chain"],
|
||||
target_chain: data["target_chain"],
|
||||
asset: parse_asset(data["asset"]),
|
||||
amount: data["amount"],
|
||||
sender: data["sender"],
|
||||
recipient: data["recipient"],
|
||||
source_tx_hash: data["source_tx_hash"],
|
||||
target_tx_hash: data["target_tx_hash"],
|
||||
fee: data["fee"],
|
||||
fee_asset: parse_asset(data["fee_asset"]),
|
||||
created_at: data["created_at"],
|
||||
updated_at: data["updated_at"],
|
||||
completed_at: data["completed_at"],
|
||||
error_message: data["error_message"]
|
||||
)
|
||||
end
|
||||
|
||||
def parse_fee_estimate(data)
|
||||
return nil unless data
|
||||
|
||||
FeeEstimate.new(
|
||||
bridge_fee: data["bridge_fee"],
|
||||
gas_fee_source: data["gas_fee_source"],
|
||||
gas_fee_target: data["gas_fee_target"],
|
||||
total_fee: data["total_fee"],
|
||||
fee_asset: parse_asset(data["fee_asset"]),
|
||||
estimated_time: data["estimated_time"],
|
||||
exchange_rate: data["exchange_rate"]
|
||||
)
|
||||
end
|
||||
|
||||
def parse_exchange_rate(data)
|
||||
return nil unless data
|
||||
|
||||
ExchangeRate.new(
|
||||
from_asset: parse_asset(data["from_asset"]),
|
||||
to_asset: parse_asset(data["to_asset"]),
|
||||
rate: data["rate"],
|
||||
inverse_rate: data["inverse_rate"],
|
||||
last_updated: data["last_updated"],
|
||||
source: data["source"]
|
||||
)
|
||||
end
|
||||
|
||||
def parse_signed_transaction(data)
|
||||
return nil unless data
|
||||
|
||||
SignedTransaction.new(
|
||||
tx_hash: data["tx_hash"],
|
||||
chain: data["chain"],
|
||||
from: data["from"],
|
||||
to: data["to"],
|
||||
value: data["value"],
|
||||
data: data["data"],
|
||||
gas_limit: data["gas_limit"],
|
||||
gas_price: data["gas_price"],
|
||||
max_fee_per_gas: data["max_fee_per_gas"],
|
||||
max_priority_fee_per_gas: data["max_priority_fee_per_gas"],
|
||||
nonce: data["nonce"],
|
||||
signature: data["signature"]
|
||||
)
|
||||
end
|
||||
|
||||
# Conversion methods
|
||||
|
||||
def proof_to_hash(proof)
|
||||
{
|
||||
lock_receipt: lock_receipt_to_hash(proof.lock_receipt),
|
||||
merkle_proof: proof.merkle_proof,
|
||||
block_header: proof.block_header,
|
||||
signatures: proof.signatures.map { |s| signature_to_hash(s) }
|
||||
}
|
||||
end
|
||||
|
||||
def lock_receipt_to_hash(receipt)
|
||||
{
|
||||
id: receipt.id,
|
||||
tx_hash: receipt.tx_hash,
|
||||
source_chain: receipt.source_chain,
|
||||
target_chain: receipt.target_chain,
|
||||
asset: asset_to_hash(receipt.asset),
|
||||
amount: receipt.amount,
|
||||
sender: receipt.sender,
|
||||
recipient: receipt.recipient,
|
||||
lock_timestamp: receipt.lock_timestamp,
|
||||
confirmations: receipt.confirmations,
|
||||
required_confirmations: receipt.required_confirmations
|
||||
}
|
||||
end
|
||||
|
||||
def burn_proof_to_hash(proof)
|
||||
{
|
||||
burn_receipt: burn_receipt_to_hash(proof.burn_receipt),
|
||||
merkle_proof: proof.merkle_proof,
|
||||
block_header: proof.block_header,
|
||||
signatures: proof.signatures.map { |s| signature_to_hash(s) }
|
||||
}
|
||||
end
|
||||
|
||||
def burn_receipt_to_hash(receipt)
|
||||
{
|
||||
id: receipt.id,
|
||||
tx_hash: receipt.tx_hash,
|
||||
source_chain: receipt.source_chain,
|
||||
target_chain: receipt.target_chain,
|
||||
wrapped_asset: asset_to_hash(receipt.wrapped_asset),
|
||||
original_asset: asset_to_hash(receipt.original_asset),
|
||||
amount: receipt.amount,
|
||||
sender: receipt.sender,
|
||||
recipient: receipt.recipient,
|
||||
burn_timestamp: receipt.burn_timestamp,
|
||||
confirmations: receipt.confirmations,
|
||||
required_confirmations: receipt.required_confirmations
|
||||
}
|
||||
end
|
||||
|
||||
def asset_to_hash(asset)
|
||||
return nil unless asset
|
||||
|
||||
{
|
||||
id: asset.id,
|
||||
symbol: asset.symbol,
|
||||
name: asset.name,
|
||||
type: asset.type,
|
||||
chain: asset.chain,
|
||||
contract_address: asset.contract_address,
|
||||
decimals: asset.decimals,
|
||||
logo_url: asset.logo_url,
|
||||
verified: asset.verified
|
||||
}
|
||||
end
|
||||
|
||||
def signature_to_hash(sig)
|
||||
{
|
||||
validator: sig.validator,
|
||||
signature: sig.signature,
|
||||
timestamp: sig.timestamp
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
264
sdk/ruby/lib/synor_bridge/types.rb
Normal file
264
sdk/ruby/lib/synor_bridge/types.rb
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SynorBridge
|
||||
# Chain identifiers
|
||||
module ChainId
|
||||
SYNOR = "synor"
|
||||
ETHEREUM = "ethereum"
|
||||
POLYGON = "polygon"
|
||||
ARBITRUM = "arbitrum"
|
||||
OPTIMISM = "optimism"
|
||||
BSC = "bsc"
|
||||
AVALANCHE = "avalanche"
|
||||
SOLANA = "solana"
|
||||
COSMOS = "cosmos"
|
||||
end
|
||||
|
||||
# Asset types
|
||||
module AssetType
|
||||
NATIVE = "native"
|
||||
ERC20 = "erc20"
|
||||
ERC721 = "erc721"
|
||||
ERC1155 = "erc1155"
|
||||
end
|
||||
|
||||
# Transfer status
|
||||
module TransferStatus
|
||||
PENDING = "pending"
|
||||
LOCKED = "locked"
|
||||
CONFIRMING = "confirming"
|
||||
MINTING = "minting"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
REFUNDED = "refunded"
|
||||
end
|
||||
|
||||
# Transfer direction
|
||||
module TransferDirection
|
||||
LOCK_MINT = "lock_mint"
|
||||
BURN_UNLOCK = "burn_unlock"
|
||||
end
|
||||
|
||||
# Native currency
|
||||
NativeCurrency = Struct.new(
|
||||
:name,
|
||||
:symbol,
|
||||
:decimals,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Chain
|
||||
Chain = Struct.new(
|
||||
:id,
|
||||
:name,
|
||||
:chain_id,
|
||||
:rpc_url,
|
||||
:explorer_url,
|
||||
:native_currency,
|
||||
:confirmations,
|
||||
:estimated_block_time,
|
||||
:supported,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Asset
|
||||
Asset = Struct.new(
|
||||
:id,
|
||||
:symbol,
|
||||
:name,
|
||||
:type,
|
||||
:chain,
|
||||
:contract_address,
|
||||
:decimals,
|
||||
:logo_url,
|
||||
:verified,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Wrapped asset
|
||||
WrappedAsset = Struct.new(
|
||||
:original_asset,
|
||||
:wrapped_asset,
|
||||
:chain,
|
||||
:bridge_contract,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Validator signature
|
||||
ValidatorSignature = Struct.new(
|
||||
:validator,
|
||||
:signature,
|
||||
:timestamp,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Lock receipt
|
||||
LockReceipt = Struct.new(
|
||||
:id,
|
||||
:tx_hash,
|
||||
:source_chain,
|
||||
:target_chain,
|
||||
:asset,
|
||||
:amount,
|
||||
:sender,
|
||||
:recipient,
|
||||
:lock_timestamp,
|
||||
:confirmations,
|
||||
:required_confirmations,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Lock proof
|
||||
LockProof = Struct.new(
|
||||
:lock_receipt,
|
||||
:merkle_proof,
|
||||
:block_header,
|
||||
:signatures,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Burn receipt
|
||||
BurnReceipt = Struct.new(
|
||||
:id,
|
||||
:tx_hash,
|
||||
:source_chain,
|
||||
:target_chain,
|
||||
:wrapped_asset,
|
||||
:original_asset,
|
||||
:amount,
|
||||
:sender,
|
||||
:recipient,
|
||||
:burn_timestamp,
|
||||
:confirmations,
|
||||
:required_confirmations,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Burn proof
|
||||
BurnProof = Struct.new(
|
||||
:burn_receipt,
|
||||
:merkle_proof,
|
||||
:block_header,
|
||||
:signatures,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Transfer
|
||||
Transfer = Struct.new(
|
||||
:id,
|
||||
:direction,
|
||||
:status,
|
||||
:source_chain,
|
||||
:target_chain,
|
||||
:asset,
|
||||
:amount,
|
||||
:sender,
|
||||
:recipient,
|
||||
:source_tx_hash,
|
||||
:target_tx_hash,
|
||||
:fee,
|
||||
:fee_asset,
|
||||
:created_at,
|
||||
:updated_at,
|
||||
:completed_at,
|
||||
:error_message,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Fee estimate
|
||||
FeeEstimate = Struct.new(
|
||||
:bridge_fee,
|
||||
:gas_fee_source,
|
||||
:gas_fee_target,
|
||||
:total_fee,
|
||||
:fee_asset,
|
||||
:estimated_time,
|
||||
:exchange_rate,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Exchange rate
|
||||
ExchangeRate = Struct.new(
|
||||
:from_asset,
|
||||
:to_asset,
|
||||
:rate,
|
||||
:inverse_rate,
|
||||
:last_updated,
|
||||
:source,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Signed transaction
|
||||
SignedTransaction = Struct.new(
|
||||
:tx_hash,
|
||||
:chain,
|
||||
:from,
|
||||
:to,
|
||||
:value,
|
||||
:data,
|
||||
:gas_limit,
|
||||
:gas_price,
|
||||
:max_fee_per_gas,
|
||||
:max_priority_fee_per_gas,
|
||||
:nonce,
|
||||
:signature,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Lock options
|
||||
LockOptions = Struct.new(
|
||||
:recipient,
|
||||
:deadline,
|
||||
:slippage,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Mint options
|
||||
MintOptions = Struct.new(
|
||||
:gas_limit,
|
||||
:max_fee_per_gas,
|
||||
:max_priority_fee_per_gas,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Burn options
|
||||
BurnOptions = Struct.new(
|
||||
:recipient,
|
||||
:deadline,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Unlock options
|
||||
UnlockOptions = Struct.new(
|
||||
:gas_limit,
|
||||
:gas_price,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Transfer filter
|
||||
TransferFilter = Struct.new(
|
||||
:status,
|
||||
:source_chain,
|
||||
:target_chain,
|
||||
:asset,
|
||||
:sender,
|
||||
:recipient,
|
||||
:limit,
|
||||
:offset,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Bridge config
|
||||
BridgeConfig = Struct.new(
|
||||
:api_key,
|
||||
:endpoint,
|
||||
:timeout,
|
||||
:retries,
|
||||
:debug,
|
||||
keyword_init: true
|
||||
) do
|
||||
def initialize(api_key:, endpoint: "https://bridge.synor.io/v1", timeout: 60, retries: 3, debug: false)
|
||||
super(api_key: api_key, endpoint: endpoint, timeout: timeout, retries: retries, debug: debug)
|
||||
end
|
||||
end
|
||||
end
|
||||
5
sdk/ruby/lib/synor_bridge/version.rb
Normal file
5
sdk/ruby/lib/synor_bridge/version.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SynorBridge
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
19
sdk/ruby/lib/synor_database.rb
Normal file
19
sdk/ruby/lib/synor_database.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative "synor_database/version"
|
||||
require_relative "synor_database/types"
|
||||
require_relative "synor_database/client"
|
||||
|
||||
module SynorDatabase
|
||||
class Error < StandardError; end
|
||||
class ClientClosedError < Error; end
|
||||
class HttpError < Error
|
||||
attr_reader :status_code, :code
|
||||
|
||||
def initialize(message, status_code: 0, code: nil)
|
||||
super(message)
|
||||
@status_code = status_code
|
||||
@code = code
|
||||
end
|
||||
end
|
||||
end
|
||||
206
sdk/ruby/lib/synor_database/client.rb
Normal file
206
sdk/ruby/lib/synor_database/client.rb
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "faraday"
|
||||
require "json"
|
||||
require "cgi"
|
||||
|
||||
module SynorDatabase
|
||||
# Synor Database SDK Client
|
||||
#
|
||||
# @example
|
||||
# client = SynorDatabase::Client.new(api_key: 'your-api-key')
|
||||
#
|
||||
# # Key-Value operations
|
||||
# client.kv.set("key", "value")
|
||||
# value = client.kv.get("key")
|
||||
#
|
||||
# # Document operations
|
||||
# id = client.documents.create("users", { name: "Alice" })
|
||||
# doc = client.documents.get("users", id)
|
||||
#
|
||||
class Client
|
||||
attr_reader :config, :kv, :documents, :vectors, :timeseries
|
||||
|
||||
def initialize(api_key: nil, **options)
|
||||
@config = Config.new(api_key: api_key, **options)
|
||||
raise ArgumentError, "API key is required" unless @config.api_key
|
||||
|
||||
@conn = Faraday.new(url: @config.base_url) do |f|
|
||||
f.request :json
|
||||
f.response :json
|
||||
f.options.timeout = @config.timeout
|
||||
f.headers["Authorization"] = "Bearer #{@config.api_key}"
|
||||
f.headers["X-SDK-Version"] = "ruby/#{VERSION}"
|
||||
end
|
||||
@closed = false
|
||||
|
||||
@kv = KeyValueStore.new(self)
|
||||
@documents = DocumentStore.new(self)
|
||||
@vectors = VectorStore.new(self)
|
||||
@timeseries = TimeSeriesStore.new(self)
|
||||
end
|
||||
|
||||
def closed?
|
||||
@closed
|
||||
end
|
||||
|
||||
def close
|
||||
@closed = true
|
||||
end
|
||||
|
||||
def health_check
|
||||
response = request(:get, "/health")
|
||||
response["status"] == "healthy"
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
|
||||
# Key-Value Store
|
||||
class KeyValueStore
|
||||
def initialize(client)
|
||||
@client = client
|
||||
end
|
||||
|
||||
def get(key)
|
||||
response = @client.request(:get, "/kv/#{CGI.escape(key)}")
|
||||
response["value"]
|
||||
end
|
||||
|
||||
def set(key, value, ttl: nil)
|
||||
body = { key: key, value: value }
|
||||
body[:ttl] = ttl if ttl
|
||||
@client.request(:put, "/kv/#{CGI.escape(key)}", body)
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
@client.request(:delete, "/kv/#{CGI.escape(key)}")
|
||||
end
|
||||
|
||||
def list(prefix)
|
||||
response = @client.request(:get, "/kv?prefix=#{CGI.escape(prefix)}")
|
||||
(response["items"] || []).map { |h| KeyValue.from_hash(h) }
|
||||
end
|
||||
end
|
||||
|
||||
# Document Store
|
||||
class DocumentStore
|
||||
def initialize(client)
|
||||
@client = client
|
||||
end
|
||||
|
||||
def create(collection, document)
|
||||
response = @client.request(:post, "/collections/#{CGI.escape(collection)}/documents", document)
|
||||
response["id"]
|
||||
end
|
||||
|
||||
def get(collection, id)
|
||||
response = @client.request(:get, "/collections/#{CGI.escape(collection)}/documents/#{CGI.escape(id)}")
|
||||
Document.from_hash(response)
|
||||
end
|
||||
|
||||
def update(collection, id, update)
|
||||
@client.request(:patch, "/collections/#{CGI.escape(collection)}/documents/#{CGI.escape(id)}", update)
|
||||
end
|
||||
|
||||
def delete(collection, id)
|
||||
@client.request(:delete, "/collections/#{CGI.escape(collection)}/documents/#{CGI.escape(id)}")
|
||||
end
|
||||
|
||||
def query(collection, query)
|
||||
body = query.to_h.compact
|
||||
response = @client.request(:post, "/collections/#{CGI.escape(collection)}/query", body)
|
||||
(response["documents"] || []).map { |h| Document.from_hash(h) }
|
||||
end
|
||||
end
|
||||
|
||||
# Vector Store
|
||||
class VectorStore
|
||||
def initialize(client)
|
||||
@client = client
|
||||
end
|
||||
|
||||
def upsert(collection, vectors)
|
||||
entries = vectors.map { |v| v.is_a?(Hash) ? v : v.to_h }
|
||||
@client.request(:post, "/vectors/#{CGI.escape(collection)}/upsert", { vectors: entries })
|
||||
end
|
||||
|
||||
def search(collection, vector, k)
|
||||
response = @client.request(:post, "/vectors/#{CGI.escape(collection)}/search", { vector: vector, k: k })
|
||||
(response["results"] || []).map { |h| SearchResult.from_hash(h) }
|
||||
end
|
||||
|
||||
def delete(collection, ids)
|
||||
@client.request(:delete, "/vectors/#{CGI.escape(collection)}", { ids: ids })
|
||||
end
|
||||
end
|
||||
|
||||
# Time Series Store
|
||||
class TimeSeriesStore
|
||||
def initialize(client)
|
||||
@client = client
|
||||
end
|
||||
|
||||
def write(series, points)
|
||||
entries = points.map { |p| p.is_a?(Hash) ? p : p.to_h }
|
||||
@client.request(:post, "/timeseries/#{CGI.escape(series)}/write", { points: entries })
|
||||
end
|
||||
|
||||
def query(series, range, aggregation: nil)
|
||||
body = { range: range.to_h }
|
||||
body[:aggregation] = aggregation.to_h if aggregation
|
||||
response = @client.request(:post, "/timeseries/#{CGI.escape(series)}/query", body)
|
||||
(response["points"] || []).map { |h| DataPoint.from_hash(h) }
|
||||
end
|
||||
end
|
||||
|
||||
# Internal request method
|
||||
def request(method, path, body = nil)
|
||||
check_closed!
|
||||
|
||||
with_retry do
|
||||
response = case method
|
||||
when :get then @conn.get(path)
|
||||
when :post then @conn.post(path, body)
|
||||
when :put then @conn.put(path, body)
|
||||
when :patch then @conn.patch(path, body)
|
||||
when :delete
|
||||
if body
|
||||
@conn.delete(path) { |req| req.body = body.to_json }
|
||||
else
|
||||
@conn.delete(path)
|
||||
end
|
||||
end
|
||||
|
||||
raise_on_error(response) unless response.success?
|
||||
response.body || {}
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_closed!
|
||||
raise ClientClosedError, "Client has been closed" if @closed
|
||||
end
|
||||
|
||||
def with_retry
|
||||
last_error = nil
|
||||
@config.retries.times do |attempt|
|
||||
begin
|
||||
return yield
|
||||
rescue StandardError => e
|
||||
last_error = e
|
||||
puts "Attempt #{attempt + 1} failed: #{e.message}" if @config.debug
|
||||
sleep(2**attempt) if attempt < @config.retries - 1
|
||||
end
|
||||
end
|
||||
raise last_error
|
||||
end
|
||||
|
||||
def raise_on_error(response)
|
||||
body = response.body || {}
|
||||
message = body["message"] || "HTTP #{response.status}"
|
||||
code = body["code"]
|
||||
raise HttpError.new(message, status_code: response.status, code: code)
|
||||
end
|
||||
end
|
||||
end
|
||||
71
sdk/ruby/lib/synor_database/types.rb
Normal file
71
sdk/ruby/lib/synor_database/types.rb
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SynorDatabase
|
||||
# Configuration for the database client
|
||||
Config = Struct.new(:api_key, :base_url, :timeout, :retries, :debug, keyword_init: true) do
|
||||
def initialize(api_key:, base_url: "https://db.synor.io/v1", timeout: 60, retries: 3, debug: false)
|
||||
super(api_key: api_key, base_url: base_url, timeout: timeout, retries: retries, debug: debug)
|
||||
end
|
||||
end
|
||||
|
||||
# A key-value entry
|
||||
KeyValue = Struct.new(:key, :value, :ttl, :created_at, :updated_at, keyword_init: true) do
|
||||
def self.from_hash(hash)
|
||||
new(
|
||||
key: hash["key"],
|
||||
value: hash["value"],
|
||||
ttl: hash["ttl"],
|
||||
created_at: hash["created_at"],
|
||||
updated_at: hash["updated_at"]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# A document
|
||||
Document = Struct.new(:id, :collection, :data, :created_at, :updated_at, keyword_init: true) do
|
||||
def self.from_hash(hash)
|
||||
new(
|
||||
id: hash["id"],
|
||||
collection: hash["collection"],
|
||||
data: hash["data"],
|
||||
created_at: hash["created_at"],
|
||||
updated_at: hash["updated_at"]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Query parameters for documents
|
||||
Query = Struct.new(:filter, :sort, :limit, :offset, :projection, keyword_init: true)
|
||||
|
||||
# A vector entry
|
||||
VectorEntry = Struct.new(:id, :vector, :metadata, keyword_init: true)
|
||||
|
||||
# A search result
|
||||
SearchResult = Struct.new(:id, :score, :vector, :metadata, keyword_init: true) do
|
||||
def self.from_hash(hash)
|
||||
new(
|
||||
id: hash["id"],
|
||||
score: hash["score"],
|
||||
vector: hash["vector"],
|
||||
metadata: hash["metadata"]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# A time series data point
|
||||
DataPoint = Struct.new(:timestamp, :value, :tags, keyword_init: true) do
|
||||
def self.from_hash(hash)
|
||||
new(
|
||||
timestamp: hash["timestamp"],
|
||||
value: hash["value"],
|
||||
tags: hash["tags"]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Time range for queries
|
||||
TimeRange = Struct.new(:start, :end, keyword_init: true)
|
||||
|
||||
# Aggregation settings
|
||||
Aggregation = Struct.new(:function, :interval, keyword_init: true)
|
||||
end
|
||||
5
sdk/ruby/lib/synor_database/version.rb
Normal file
5
sdk/ruby/lib/synor_database/version.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SynorDatabase
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
18
sdk/ruby/lib/synor_hosting.rb
Normal file
18
sdk/ruby/lib/synor_hosting.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative "synor_hosting/version"
|
||||
require_relative "synor_hosting/types"
|
||||
require_relative "synor_hosting/client"
|
||||
|
||||
module SynorHosting
|
||||
class Error < StandardError; end
|
||||
class ClientClosedError < Error; end
|
||||
class HttpError < Error
|
||||
attr_reader :status_code, :code
|
||||
def initialize(message, status_code: 0, code: nil)
|
||||
super(message)
|
||||
@status_code = status_code
|
||||
@code = code
|
||||
end
|
||||
end
|
||||
end
|
||||
357
sdk/ruby/lib/synor_hosting/client.rb
Normal file
357
sdk/ruby/lib/synor_hosting/client.rb
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "faraday"
|
||||
require "json"
|
||||
require "uri"
|
||||
|
||||
module SynorHosting
|
||||
# Synor Hosting SDK client for Ruby.
|
||||
# Provides domain management, DNS, deployments, SSL, and analytics.
|
||||
class Client
|
||||
attr_reader :closed
|
||||
|
||||
def initialize(config)
|
||||
@config = config
|
||||
@closed = false
|
||||
@conn = Faraday.new(url: config.endpoint) do |f|
|
||||
f.request :json
|
||||
f.response :json
|
||||
f.options.timeout = config.timeout
|
||||
f.headers["Authorization"] = "Bearer #{config.api_key}"
|
||||
f.headers["Content-Type"] = "application/json"
|
||||
f.headers["X-SDK-Version"] = "ruby/#{VERSION}"
|
||||
end
|
||||
end
|
||||
|
||||
# ==================== Domain Operations ====================
|
||||
|
||||
# Register a new domain
|
||||
def register_domain(name, auto_renew: true)
|
||||
body = { name: name, auto_renew: auto_renew }
|
||||
response = post("/domains", body)
|
||||
parse_domain(response)
|
||||
end
|
||||
|
||||
# Get domain details
|
||||
def get_domain(name)
|
||||
response = get("/domains/#{encode(name)}")
|
||||
parse_domain(response)
|
||||
end
|
||||
|
||||
# Resolve domain to its record
|
||||
def resolve_domain(name)
|
||||
response = get("/domains/#{encode(name)}/resolve")
|
||||
DomainRecord.new(
|
||||
cid: response["cid"],
|
||||
ttl: response["ttl"],
|
||||
updated_at: response["updated_at"]
|
||||
)
|
||||
end
|
||||
|
||||
# Update domain record
|
||||
def update_domain(name, cid:, ttl: nil)
|
||||
body = { cid: cid }
|
||||
body[:ttl] = ttl if ttl
|
||||
response = put("/domains/#{encode(name)}", body)
|
||||
parse_domain(response)
|
||||
end
|
||||
|
||||
# Transfer domain ownership
|
||||
def transfer_domain(name, new_owner)
|
||||
body = { new_owner: new_owner }
|
||||
response = post("/domains/#{encode(name)}/transfer", body)
|
||||
parse_domain(response)
|
||||
end
|
||||
|
||||
# List domains
|
||||
def list_domains(limit: nil, offset: nil)
|
||||
params = {}
|
||||
params[:limit] = limit if limit
|
||||
params[:offset] = offset if offset
|
||||
response = get("/domains", params)
|
||||
(response["domains"] || []).map { |d| parse_domain(d) }
|
||||
end
|
||||
|
||||
# ==================== DNS Operations ====================
|
||||
|
||||
# Set DNS records for a domain
|
||||
def set_dns_records(domain, records)
|
||||
body = { records: records.map { |r| record_to_hash(r) } }
|
||||
response = put("/domains/#{encode(domain)}/dns", body)
|
||||
(response["records"] || []).map { |r| parse_dns_record(r) }
|
||||
end
|
||||
|
||||
# Get DNS records for a domain
|
||||
def get_dns_records(domain)
|
||||
response = get("/domains/#{encode(domain)}/dns")
|
||||
(response["records"] || []).map { |r| parse_dns_record(r) }
|
||||
end
|
||||
|
||||
# Add a single DNS record
|
||||
def add_dns_record(domain, record)
|
||||
body = record_to_hash(record)
|
||||
response = post("/domains/#{encode(domain)}/dns", body)
|
||||
parse_dns_record(response)
|
||||
end
|
||||
|
||||
# Delete a DNS record
|
||||
def delete_dns_record(domain, record_type, name)
|
||||
delete("/domains/#{encode(domain)}/dns/#{record_type}/#{encode(name)}")
|
||||
nil
|
||||
end
|
||||
|
||||
# ==================== Deployment Operations ====================
|
||||
|
||||
# Deploy content to a domain
|
||||
def deploy(cid, domain, options = {})
|
||||
body = { cid: cid, domain: domain }
|
||||
body[:build_command] = options[:build_command] if options[:build_command]
|
||||
body[:env_vars] = options[:env_vars] if options[:env_vars]
|
||||
response = post("/deployments", body)
|
||||
parse_deployment(response)
|
||||
end
|
||||
|
||||
# Get deployment details
|
||||
def get_deployment(id)
|
||||
response = get("/deployments/#{encode(id)}")
|
||||
parse_deployment(response)
|
||||
end
|
||||
|
||||
# List deployments
|
||||
def list_deployments(domain: nil, limit: nil, offset: nil)
|
||||
params = {}
|
||||
params[:domain] = domain if domain
|
||||
params[:limit] = limit if limit
|
||||
params[:offset] = offset if offset
|
||||
response = get("/deployments", params)
|
||||
(response["deployments"] || []).map { |d| parse_deployment(d) }
|
||||
end
|
||||
|
||||
# Wait for deployment to complete
|
||||
def wait_for_deployment(id, poll_interval: 5, max_wait: 600)
|
||||
deadline = Time.now + max_wait
|
||||
final_statuses = [DeploymentStatus::ACTIVE, DeploymentStatus::FAILED, DeploymentStatus::ROLLED_BACK]
|
||||
|
||||
while Time.now < deadline
|
||||
deployment = get_deployment(id)
|
||||
return deployment if final_statuses.include?(deployment.status)
|
||||
sleep(poll_interval)
|
||||
end
|
||||
|
||||
raise Error, "Timeout waiting for deployment"
|
||||
end
|
||||
|
||||
# Rollback deployment
|
||||
def rollback(domain, deployment_id)
|
||||
body = { deployment_id: deployment_id }
|
||||
response = post("/domains/#{encode(domain)}/rollback", body)
|
||||
parse_deployment(response)
|
||||
end
|
||||
|
||||
# ==================== SSL Operations ====================
|
||||
|
||||
# Provision SSL certificate for a domain
|
||||
def provision_ssl(domain)
|
||||
response = post("/domains/#{encode(domain)}/ssl", {})
|
||||
parse_certificate(response)
|
||||
end
|
||||
|
||||
# Get SSL certificate for a domain
|
||||
def get_certificate(domain)
|
||||
response = get("/domains/#{encode(domain)}/ssl")
|
||||
parse_certificate(response)
|
||||
end
|
||||
|
||||
# Renew SSL certificate
|
||||
def renew_certificate(domain)
|
||||
response = post("/domains/#{encode(domain)}/ssl/renew", {})
|
||||
parse_certificate(response)
|
||||
end
|
||||
|
||||
# ==================== Analytics Operations ====================
|
||||
|
||||
# Get analytics for a domain
|
||||
def get_analytics(domain, start_time:, end_time:)
|
||||
params = { start: start_time.to_i, end: end_time.to_i }
|
||||
response = get("/domains/#{encode(domain)}/analytics", params)
|
||||
parse_analytics(response)
|
||||
end
|
||||
|
||||
# Get bandwidth stats
|
||||
def get_bandwidth(domain, start_time:, end_time:)
|
||||
params = { start: start_time.to_i, end: end_time.to_i }
|
||||
response = get("/domains/#{encode(domain)}/analytics/bandwidth", params)
|
||||
BandwidthStats.new(
|
||||
total_bytes: response["total_bytes"],
|
||||
cached_bytes: response["cached_bytes"],
|
||||
uncached_bytes: response["uncached_bytes"]
|
||||
)
|
||||
end
|
||||
|
||||
# ==================== Lifecycle ====================
|
||||
|
||||
# Health check
|
||||
def health_check
|
||||
response = get("/health")
|
||||
response["status"] == "healthy"
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
|
||||
# Close the client
|
||||
def close
|
||||
@closed = true
|
||||
@conn.close if @conn.respond_to?(:close)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get(path, params = {})
|
||||
execute { @conn.get(path, params).body }
|
||||
end
|
||||
|
||||
def post(path, body)
|
||||
execute { @conn.post(path, body).body }
|
||||
end
|
||||
|
||||
def put(path, body)
|
||||
execute { @conn.put(path, body).body }
|
||||
end
|
||||
|
||||
def delete(path)
|
||||
execute { @conn.delete(path).body }
|
||||
end
|
||||
|
||||
def execute
|
||||
raise ClientClosedError, "Client has been closed" if @closed
|
||||
|
||||
last_error = nil
|
||||
@config.retries.times do |attempt|
|
||||
begin
|
||||
response = yield
|
||||
check_error(response) if response.is_a?(Hash)
|
||||
return response
|
||||
rescue StandardError => e
|
||||
last_error = e
|
||||
sleep(2**attempt) if attempt < @config.retries - 1
|
||||
end
|
||||
end
|
||||
raise last_error
|
||||
end
|
||||
|
||||
def check_error(response)
|
||||
return unless response["error"] || response["code"]
|
||||
|
||||
message = response["message"] || response["error"] || "Unknown error"
|
||||
code = response["code"]
|
||||
status = response["status_code"] || 0
|
||||
raise HttpError.new(message, status_code: status, code: code)
|
||||
end
|
||||
|
||||
def encode(str)
|
||||
URI.encode_www_form_component(str)
|
||||
end
|
||||
|
||||
def record_to_hash(record)
|
||||
hash = {
|
||||
type: record.type,
|
||||
name: record.name,
|
||||
value: record.value,
|
||||
ttl: record.ttl
|
||||
}
|
||||
hash[:priority] = record.priority if record.priority
|
||||
hash
|
||||
end
|
||||
|
||||
def parse_domain(data)
|
||||
return nil unless data
|
||||
|
||||
Domain.new(
|
||||
name: data["name"],
|
||||
owner: data["owner"],
|
||||
status: data["status"],
|
||||
record: data["record"] ? DomainRecord.new(
|
||||
cid: data["record"]["cid"],
|
||||
ttl: data["record"]["ttl"],
|
||||
updated_at: data["record"]["updated_at"]
|
||||
) : nil,
|
||||
expires_at: data["expires_at"],
|
||||
created_at: data["created_at"],
|
||||
auto_renew: data["auto_renew"]
|
||||
)
|
||||
end
|
||||
|
||||
def parse_dns_record(data)
|
||||
return nil unless data
|
||||
|
||||
DnsRecord.new(
|
||||
type: data["type"],
|
||||
name: data["name"],
|
||||
value: data["value"],
|
||||
ttl: data["ttl"],
|
||||
priority: data["priority"]
|
||||
)
|
||||
end
|
||||
|
||||
def parse_deployment(data)
|
||||
return nil unless data
|
||||
|
||||
Deployment.new(
|
||||
id: data["id"],
|
||||
domain: data["domain"],
|
||||
cid: data["cid"],
|
||||
status: data["status"],
|
||||
url: data["url"],
|
||||
created_at: data["created_at"],
|
||||
deployed_at: data["deployed_at"],
|
||||
build_logs: data["build_logs"],
|
||||
error_message: data["error_message"]
|
||||
)
|
||||
end
|
||||
|
||||
def parse_certificate(data)
|
||||
return nil unless data
|
||||
|
||||
Certificate.new(
|
||||
domain: data["domain"],
|
||||
status: data["status"],
|
||||
issuer: data["issuer"],
|
||||
issued_at: data["issued_at"],
|
||||
expires_at: data["expires_at"],
|
||||
auto_renew: data["auto_renew"],
|
||||
fingerprint: data["fingerprint"]
|
||||
)
|
||||
end
|
||||
|
||||
def parse_analytics(data)
|
||||
return nil unless data
|
||||
|
||||
Analytics.new(
|
||||
domain: data["domain"],
|
||||
time_range: AnalyticsTimeRange.new(
|
||||
start_time: data["time_range"]&.dig("start"),
|
||||
end_time: data["time_range"]&.dig("end")
|
||||
),
|
||||
requests: data["requests"] ? RequestStats.new(
|
||||
total: data["requests"]["total"],
|
||||
success: data["requests"]["success"],
|
||||
error: data["requests"]["error"],
|
||||
cached: data["requests"]["cached"]
|
||||
) : nil,
|
||||
bandwidth: data["bandwidth"] ? BandwidthStats.new(
|
||||
total_bytes: data["bandwidth"]["total_bytes"],
|
||||
cached_bytes: data["bandwidth"]["cached_bytes"],
|
||||
uncached_bytes: data["bandwidth"]["uncached_bytes"]
|
||||
) : nil,
|
||||
visitors: data["visitors"],
|
||||
page_views: data["page_views"],
|
||||
top_paths: (data["top_paths"] || []).map do |p|
|
||||
PathStats.new(path: p["path"], requests: p["requests"], bandwidth: p["bandwidth"])
|
||||
end,
|
||||
geo_distribution: (data["geo_distribution"] || []).map do |g|
|
||||
GeoStats.new(country: g["country"], requests: g["requests"], bandwidth: g["bandwidth"])
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
170
sdk/ruby/lib/synor_hosting/types.rb
Normal file
170
sdk/ruby/lib/synor_hosting/types.rb
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SynorHosting
|
||||
# Domain status
|
||||
module DomainStatus
|
||||
PENDING = "pending"
|
||||
ACTIVE = "active"
|
||||
SUSPENDED = "suspended"
|
||||
EXPIRED = "expired"
|
||||
end
|
||||
|
||||
# DNS record types
|
||||
module DnsRecordType
|
||||
A = "A"
|
||||
AAAA = "AAAA"
|
||||
CNAME = "CNAME"
|
||||
TXT = "TXT"
|
||||
MX = "MX"
|
||||
NS = "NS"
|
||||
SRV = "SRV"
|
||||
end
|
||||
|
||||
# Deployment status
|
||||
module DeploymentStatus
|
||||
PENDING = "pending"
|
||||
BUILDING = "building"
|
||||
DEPLOYING = "deploying"
|
||||
ACTIVE = "active"
|
||||
FAILED = "failed"
|
||||
ROLLED_BACK = "rolled_back"
|
||||
end
|
||||
|
||||
# Certificate status
|
||||
module CertificateStatus
|
||||
PENDING = "pending"
|
||||
ISSUED = "issued"
|
||||
EXPIRED = "expired"
|
||||
REVOKED = "revoked"
|
||||
FAILED = "failed"
|
||||
end
|
||||
|
||||
# Domain record
|
||||
DomainRecord = Struct.new(
|
||||
:cid,
|
||||
:ttl,
|
||||
:updated_at,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Domain
|
||||
Domain = Struct.new(
|
||||
:name,
|
||||
:owner,
|
||||
:status,
|
||||
:record,
|
||||
:expires_at,
|
||||
:created_at,
|
||||
:auto_renew,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# DNS record
|
||||
DnsRecord = Struct.new(
|
||||
:type,
|
||||
:name,
|
||||
:value,
|
||||
:ttl,
|
||||
:priority,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Deployment
|
||||
Deployment = Struct.new(
|
||||
:id,
|
||||
:domain,
|
||||
:cid,
|
||||
:status,
|
||||
:url,
|
||||
:created_at,
|
||||
:deployed_at,
|
||||
:build_logs,
|
||||
:error_message,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Certificate
|
||||
Certificate = Struct.new(
|
||||
:domain,
|
||||
:status,
|
||||
:issuer,
|
||||
:issued_at,
|
||||
:expires_at,
|
||||
:auto_renew,
|
||||
:fingerprint,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Analytics time range
|
||||
AnalyticsTimeRange = Struct.new(
|
||||
:start_time,
|
||||
:end_time,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Bandwidth stats
|
||||
BandwidthStats = Struct.new(
|
||||
:total_bytes,
|
||||
:cached_bytes,
|
||||
:uncached_bytes,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Request stats
|
||||
RequestStats = Struct.new(
|
||||
:total,
|
||||
:success,
|
||||
:error,
|
||||
:cached,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Geographic stats
|
||||
GeoStats = Struct.new(
|
||||
:country,
|
||||
:requests,
|
||||
:bandwidth,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Analytics
|
||||
Analytics = Struct.new(
|
||||
:domain,
|
||||
:time_range,
|
||||
:requests,
|
||||
:bandwidth,
|
||||
:visitors,
|
||||
:page_views,
|
||||
:top_paths,
|
||||
:geo_distribution,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Path stats
|
||||
PathStats = Struct.new(
|
||||
:path,
|
||||
:requests,
|
||||
:bandwidth,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
# Hosting config
|
||||
HostingConfig = Struct.new(
|
||||
:api_key,
|
||||
:endpoint,
|
||||
:timeout,
|
||||
:retries,
|
||||
:debug,
|
||||
keyword_init: true
|
||||
) do
|
||||
def initialize(api_key:, endpoint: "https://hosting.synor.io/v1", timeout: 30, retries: 3, debug: false)
|
||||
super(api_key: api_key, endpoint: endpoint, timeout: timeout, retries: retries, debug: debug)
|
||||
end
|
||||
end
|
||||
|
||||
# Health response
|
||||
HealthResponse = Struct.new(
|
||||
:status,
|
||||
keyword_init: true
|
||||
)
|
||||
end
|
||||
5
sdk/ruby/lib/synor_hosting/version.rb
Normal file
5
sdk/ruby/lib/synor_hosting/version.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SynorHosting
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
467
sdk/swift/Sources/Synor/Bridge/SynorBridge.swift
Normal file
467
sdk/swift/Sources/Synor/Bridge/SynorBridge.swift
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
import Foundation
|
||||
|
||||
/// Synor Bridge SDK client for Swift.
|
||||
///
|
||||
/// Provides cross-chain asset transfers with lock-mint and burn-unlock patterns.
|
||||
///
|
||||
/// Example:
|
||||
/// ```swift
|
||||
/// let bridge = SynorBridge(config: BridgeConfig(apiKey: "your-api-key"))
|
||||
///
|
||||
/// // Get supported chains
|
||||
/// let chains = try await bridge.getSupportedChains()
|
||||
///
|
||||
/// // Estimate fee
|
||||
/// let fee = try await bridge.estimateFee(
|
||||
/// asset: "SYNR",
|
||||
/// amount: "1000000000000000000",
|
||||
/// sourceChain: .synor,
|
||||
/// targetChain: .ethereum
|
||||
/// )
|
||||
///
|
||||
/// // Full bridge flow
|
||||
/// let transfer = try await bridge.bridgeTo(
|
||||
/// asset: "SYNR",
|
||||
/// amount: "1000000000000000000",
|
||||
/// targetChain: .ethereum,
|
||||
/// targetAddress: "0x..."
|
||||
/// )
|
||||
/// print("Transfer completed: \(transfer.id)")
|
||||
/// ```
|
||||
public class SynorBridge {
|
||||
private let config: BridgeConfig
|
||||
private let session: URLSession
|
||||
private let decoder: JSONDecoder
|
||||
private let encoder: JSONEncoder
|
||||
private var closed = false
|
||||
|
||||
private static let finalStatuses: Set<TransferStatus> = [.completed, .failed, .refunded]
|
||||
|
||||
public init(config: BridgeConfig) {
|
||||
self.config = config
|
||||
|
||||
let sessionConfig = URLSessionConfiguration.default
|
||||
sessionConfig.timeoutIntervalForRequest = config.timeout
|
||||
sessionConfig.timeoutIntervalForResource = config.timeout * 2
|
||||
self.session = URLSession(configuration: sessionConfig)
|
||||
|
||||
self.decoder = JSONDecoder()
|
||||
self.encoder = JSONEncoder()
|
||||
}
|
||||
|
||||
// MARK: - Chain Operations
|
||||
|
||||
/// Get all supported chains.
|
||||
public func getSupportedChains() async throws -> [Chain] {
|
||||
let response: ChainsResponse = try await get(path: "/chains")
|
||||
return response.chains ?? []
|
||||
}
|
||||
|
||||
/// Get chain details.
|
||||
public func getChain(chainId: ChainId) async throws -> Chain {
|
||||
return try await get(path: "/chains/\(chainId.rawValue)")
|
||||
}
|
||||
|
||||
/// Check if a chain is supported.
|
||||
public func isChainSupported(chainId: ChainId) async -> Bool {
|
||||
do {
|
||||
let chain = try await getChain(chainId: chainId)
|
||||
return chain.supported
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Asset Operations
|
||||
|
||||
/// Get supported assets for a chain.
|
||||
public func getSupportedAssets(chainId: ChainId) async throws -> [Asset] {
|
||||
let response: AssetsResponse = try await get(path: "/chains/\(chainId.rawValue)/assets")
|
||||
return response.assets ?? []
|
||||
}
|
||||
|
||||
/// Get asset details.
|
||||
public func getAsset(assetId: String) async throws -> Asset {
|
||||
return try await get(path: "/assets/\(assetId.urlEncoded)")
|
||||
}
|
||||
|
||||
/// Get wrapped asset on a target chain.
|
||||
public func getWrappedAsset(originalAssetId: String, targetChain: ChainId) async throws -> WrappedAsset {
|
||||
return try await get(path: "/assets/\(originalAssetId.urlEncoded)/wrapped/\(targetChain.rawValue)")
|
||||
}
|
||||
|
||||
// MARK: - Fee & Rate Operations
|
||||
|
||||
/// Estimate transfer fee.
|
||||
public func estimateFee(
|
||||
asset: String,
|
||||
amount: String,
|
||||
sourceChain: ChainId,
|
||||
targetChain: ChainId
|
||||
) async throws -> BridgeFeeEstimate {
|
||||
let body: [String: String] = [
|
||||
"asset": asset,
|
||||
"amount": amount,
|
||||
"sourceChain": sourceChain.rawValue,
|
||||
"targetChain": targetChain.rawValue
|
||||
]
|
||||
return try await post(path: "/fees/estimate", body: body)
|
||||
}
|
||||
|
||||
/// Get exchange rate between assets.
|
||||
public func getExchangeRate(fromAsset: String, toAsset: String) async throws -> ExchangeRate {
|
||||
return try await get(path: "/rates/\(fromAsset.urlEncoded)/\(toAsset.urlEncoded)")
|
||||
}
|
||||
|
||||
// MARK: - Lock-Mint Flow
|
||||
|
||||
/// Lock assets on the source chain.
|
||||
public func lock(
|
||||
asset: String,
|
||||
amount: String,
|
||||
targetChain: ChainId,
|
||||
options: LockOptions? = nil
|
||||
) async throws -> LockReceipt {
|
||||
var body: [String: Any] = [
|
||||
"asset": asset,
|
||||
"amount": amount,
|
||||
"targetChain": targetChain.rawValue
|
||||
]
|
||||
if let recipient = options?.recipient { body["recipient"] = recipient }
|
||||
if let deadline = options?.deadline { body["deadline"] = deadline }
|
||||
if let slippage = options?.slippage { body["slippage"] = slippage }
|
||||
return try await post(path: "/transfers/lock", body: body)
|
||||
}
|
||||
|
||||
/// Get lock proof.
|
||||
public func getLockProof(lockReceiptId: String) async throws -> LockProof {
|
||||
return try await get(path: "/transfers/lock/\(lockReceiptId.urlEncoded)/proof")
|
||||
}
|
||||
|
||||
/// Wait for lock proof with confirmations.
|
||||
public func waitForLockProof(
|
||||
lockReceiptId: String,
|
||||
pollInterval: TimeInterval = 5,
|
||||
maxWait: TimeInterval = 600
|
||||
) async throws -> LockProof {
|
||||
let deadline = Date().addingTimeInterval(maxWait)
|
||||
|
||||
while Date() < deadline {
|
||||
do {
|
||||
return try await getLockProof(lockReceiptId: lockReceiptId)
|
||||
} catch let error as BridgeError {
|
||||
if error.isConfirmationsPending {
|
||||
try await Task.sleep(nanoseconds: UInt64(pollInterval * 1_000_000_000))
|
||||
continue
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
throw BridgeError.timeout("Waiting for lock proof")
|
||||
}
|
||||
|
||||
/// Mint assets on the target chain.
|
||||
public func mint(
|
||||
proof: LockProof,
|
||||
targetAddress: String,
|
||||
options: MintOptions? = nil
|
||||
) async throws -> BridgeSignedTransaction {
|
||||
var body: [String: Any] = [
|
||||
"proof": try proofToDictionary(proof),
|
||||
"targetAddress": targetAddress
|
||||
]
|
||||
if let gasLimit = options?.gasLimit { body["gasLimit"] = gasLimit }
|
||||
if let maxFeePerGas = options?.maxFeePerGas { body["maxFeePerGas"] = maxFeePerGas }
|
||||
if let maxPriorityFeePerGas = options?.maxPriorityFeePerGas { body["maxPriorityFeePerGas"] = maxPriorityFeePerGas }
|
||||
return try await post(path: "/transfers/mint", body: body)
|
||||
}
|
||||
|
||||
// MARK: - Burn-Unlock Flow
|
||||
|
||||
/// Burn wrapped assets.
|
||||
public func burn(
|
||||
wrappedAsset: String,
|
||||
amount: String,
|
||||
options: BurnOptions? = nil
|
||||
) async throws -> BurnReceipt {
|
||||
var body: [String: Any] = [
|
||||
"wrappedAsset": wrappedAsset,
|
||||
"amount": amount
|
||||
]
|
||||
if let recipient = options?.recipient { body["recipient"] = recipient }
|
||||
if let deadline = options?.deadline { body["deadline"] = deadline }
|
||||
return try await post(path: "/transfers/burn", body: body)
|
||||
}
|
||||
|
||||
/// Get burn proof.
|
||||
public func getBurnProof(burnReceiptId: String) async throws -> BurnProof {
|
||||
return try await get(path: "/transfers/burn/\(burnReceiptId.urlEncoded)/proof")
|
||||
}
|
||||
|
||||
/// Wait for burn proof with confirmations.
|
||||
public func waitForBurnProof(
|
||||
burnReceiptId: String,
|
||||
pollInterval: TimeInterval = 5,
|
||||
maxWait: TimeInterval = 600
|
||||
) async throws -> BurnProof {
|
||||
let deadline = Date().addingTimeInterval(maxWait)
|
||||
|
||||
while Date() < deadline {
|
||||
do {
|
||||
return try await getBurnProof(burnReceiptId: burnReceiptId)
|
||||
} catch let error as BridgeError {
|
||||
if error.isConfirmationsPending {
|
||||
try await Task.sleep(nanoseconds: UInt64(pollInterval * 1_000_000_000))
|
||||
continue
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
throw BridgeError.timeout("Waiting for burn proof")
|
||||
}
|
||||
|
||||
/// Unlock original assets.
|
||||
public func unlock(
|
||||
proof: BurnProof,
|
||||
options: UnlockOptions? = nil
|
||||
) async throws -> BridgeSignedTransaction {
|
||||
var body: [String: Any] = [
|
||||
"proof": try burnProofToDictionary(proof)
|
||||
]
|
||||
if let gasLimit = options?.gasLimit { body["gasLimit"] = gasLimit }
|
||||
if let gasPrice = options?.gasPrice { body["gasPrice"] = gasPrice }
|
||||
return try await post(path: "/transfers/unlock", body: body)
|
||||
}
|
||||
|
||||
// MARK: - Transfer Management
|
||||
|
||||
/// Get transfer details.
|
||||
public func getTransfer(transferId: String) async throws -> Transfer {
|
||||
return try await get(path: "/transfers/\(transferId.urlEncoded)")
|
||||
}
|
||||
|
||||
/// Get transfer status.
|
||||
public func getTransferStatus(transferId: String) async throws -> TransferStatus {
|
||||
let transfer = try await getTransfer(transferId: transferId)
|
||||
return transfer.status
|
||||
}
|
||||
|
||||
/// List transfers with optional filter.
|
||||
public func listTransfers(filter: TransferFilter? = nil) async throws -> [Transfer] {
|
||||
var params: [String] = []
|
||||
if let status = filter?.status { params.append("status=\(status.rawValue)") }
|
||||
if let sourceChain = filter?.sourceChain { params.append("sourceChain=\(sourceChain.rawValue)") }
|
||||
if let targetChain = filter?.targetChain { params.append("targetChain=\(targetChain.rawValue)") }
|
||||
if let limit = filter?.limit { params.append("limit=\(limit)") }
|
||||
if let offset = filter?.offset { params.append("offset=\(offset)") }
|
||||
|
||||
var path = "/transfers"
|
||||
if !params.isEmpty {
|
||||
path += "?\(params.joined(separator: "&"))"
|
||||
}
|
||||
|
||||
let response: TransfersResponse = try await get(path: path)
|
||||
return response.transfers ?? []
|
||||
}
|
||||
|
||||
/// Wait for transfer to complete.
|
||||
public func waitForTransfer(
|
||||
transferId: String,
|
||||
pollInterval: TimeInterval = 10,
|
||||
maxWait: TimeInterval = 1800
|
||||
) async throws -> Transfer {
|
||||
let deadline = Date().addingTimeInterval(maxWait)
|
||||
|
||||
while Date() < deadline {
|
||||
let transfer = try await getTransfer(transferId: transferId)
|
||||
if Self.finalStatuses.contains(transfer.status) {
|
||||
return transfer
|
||||
}
|
||||
try await Task.sleep(nanoseconds: UInt64(pollInterval * 1_000_000_000))
|
||||
}
|
||||
|
||||
throw BridgeError.timeout("Waiting for transfer completion")
|
||||
}
|
||||
|
||||
// MARK: - Convenience Methods
|
||||
|
||||
/// Complete lock-mint bridge flow.
|
||||
public func bridgeTo(
|
||||
asset: String,
|
||||
amount: String,
|
||||
targetChain: ChainId,
|
||||
targetAddress: String,
|
||||
lockOptions: LockOptions? = nil,
|
||||
mintOptions: MintOptions? = nil
|
||||
) async throws -> Transfer {
|
||||
let lockReceipt = try await lock(asset: asset, amount: amount, targetChain: targetChain, options: lockOptions)
|
||||
if config.debug {
|
||||
print("Locked: \(lockReceipt.id), waiting for confirmations...")
|
||||
}
|
||||
|
||||
let proof = try await waitForLockProof(lockReceiptId: lockReceipt.id)
|
||||
if config.debug {
|
||||
print("Proof ready, minting on \(targetChain.rawValue)...")
|
||||
}
|
||||
|
||||
_ = try await mint(proof: proof, targetAddress: targetAddress, options: mintOptions)
|
||||
return try await waitForTransfer(transferId: lockReceipt.id)
|
||||
}
|
||||
|
||||
/// Complete burn-unlock bridge flow.
|
||||
public func bridgeBack(
|
||||
wrappedAsset: String,
|
||||
amount: String,
|
||||
burnOptions: BurnOptions? = nil,
|
||||
unlockOptions: UnlockOptions? = nil
|
||||
) async throws -> Transfer {
|
||||
let burnReceipt = try await burn(wrappedAsset: wrappedAsset, amount: amount, options: burnOptions)
|
||||
if config.debug {
|
||||
print("Burned: \(burnReceipt.id), waiting for confirmations...")
|
||||
}
|
||||
|
||||
let proof = try await waitForBurnProof(burnReceiptId: burnReceipt.id)
|
||||
if config.debug {
|
||||
print("Proof ready, unlocking on \(burnReceipt.targetChain.rawValue)...")
|
||||
}
|
||||
|
||||
_ = try await unlock(proof: proof, options: unlockOptions)
|
||||
return try await waitForTransfer(transferId: burnReceipt.id)
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
/// Close the client.
|
||||
public func close() {
|
||||
closed = true
|
||||
session.invalidateAndCancel()
|
||||
}
|
||||
|
||||
/// Check if the client is closed.
|
||||
public var isClosed: Bool { closed }
|
||||
|
||||
/// Perform a health check.
|
||||
public func healthCheck() async -> Bool {
|
||||
do {
|
||||
let response: BridgeHealthResponse = try await get(path: "/health")
|
||||
return response.status == "healthy"
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private HTTP Methods
|
||||
|
||||
private func get<T: Decodable>(path: String) async throws -> T {
|
||||
return try await executeWithRetry {
|
||||
try await self.performRequest(method: "GET", path: path, body: nil as [String: Any]?)
|
||||
}
|
||||
}
|
||||
|
||||
private func post<T: Decodable>(path: String, body: [String: Any]) async throws -> T {
|
||||
return try await executeWithRetry {
|
||||
try await self.performRequest(method: "POST", path: path, body: body)
|
||||
}
|
||||
}
|
||||
|
||||
private func performRequest<T: Decodable>(
|
||||
method: String,
|
||||
path: String,
|
||||
body: [String: Any]?
|
||||
) async throws -> T {
|
||||
if closed { throw BridgeError.clientClosed }
|
||||
|
||||
guard let url = URL(string: "\(config.endpoint)\(path)") else {
|
||||
throw BridgeError.invalidResponse
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version")
|
||||
|
||||
if let body = body {
|
||||
do {
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||
} catch {
|
||||
throw BridgeError.encodingError(error)
|
||||
}
|
||||
}
|
||||
|
||||
let (data, response): (Data, URLResponse)
|
||||
do {
|
||||
(data, response) = try await session.data(for: request)
|
||||
} catch {
|
||||
throw BridgeError.networkError(error)
|
||||
}
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw BridgeError.invalidResponse
|
||||
}
|
||||
|
||||
if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
|
||||
if data.isEmpty {
|
||||
throw BridgeError.invalidResponse
|
||||
}
|
||||
do {
|
||||
return try decoder.decode(T.self, from: data)
|
||||
} catch {
|
||||
throw BridgeError.decodingError(error)
|
||||
}
|
||||
}
|
||||
|
||||
let errorInfo = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
let message = errorInfo?["message"] as? String ?? "HTTP \(httpResponse.statusCode)"
|
||||
let code = errorInfo?["code"] as? String
|
||||
throw BridgeError.httpError(statusCode: httpResponse.statusCode, message: message, code: code)
|
||||
}
|
||||
|
||||
private func executeWithRetry<T>(_ operation: () async throws -> T) async throws -> T {
|
||||
var lastError: Error?
|
||||
|
||||
for attempt in 0..<config.retries {
|
||||
do {
|
||||
return try await operation()
|
||||
} catch {
|
||||
lastError = error
|
||||
if config.debug {
|
||||
print("Attempt \(attempt + 1) failed: \(error)")
|
||||
}
|
||||
if attempt < config.retries - 1 {
|
||||
try await Task.sleep(nanoseconds: UInt64(1_000_000_000 * (1 << attempt)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? BridgeError.invalidResponse
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
private func proofToDictionary(_ proof: LockProof) throws -> [String: Any] {
|
||||
let data = try encoder.encode(proof)
|
||||
guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
throw BridgeError.encodingError(NSError(domain: "BridgeError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to encode proof"]))
|
||||
}
|
||||
return dict
|
||||
}
|
||||
|
||||
private func burnProofToDictionary(_ proof: BurnProof) throws -> [String: Any] {
|
||||
let data = try encoder.encode(proof)
|
||||
guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
throw BridgeError.encodingError(NSError(domain: "BridgeError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to encode proof"]))
|
||||
}
|
||||
return dict
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
private extension String {
|
||||
var urlEncoded: String {
|
||||
addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self
|
||||
}
|
||||
}
|
||||
373
sdk/swift/Sources/Synor/Bridge/Types.swift
Normal file
373
sdk/swift/Sources/Synor/Bridge/Types.swift
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
import Foundation
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
/// Configuration for the Synor Bridge client.
|
||||
public struct BridgeConfig {
|
||||
public let apiKey: String
|
||||
public let endpoint: String
|
||||
public let timeout: TimeInterval
|
||||
public let retries: Int
|
||||
public let debug: Bool
|
||||
|
||||
public init(
|
||||
apiKey: String,
|
||||
endpoint: String = "https://bridge.synor.io/v1",
|
||||
timeout: TimeInterval = 60,
|
||||
retries: Int = 3,
|
||||
debug: Bool = false
|
||||
) {
|
||||
self.apiKey = apiKey
|
||||
self.endpoint = endpoint
|
||||
self.timeout = timeout
|
||||
self.retries = retries
|
||||
self.debug = debug
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Chain Types
|
||||
|
||||
/// Supported blockchain identifiers.
|
||||
public enum ChainId: String, Codable, Sendable {
|
||||
case synor
|
||||
case ethereum
|
||||
case polygon
|
||||
case arbitrum
|
||||
case optimism
|
||||
case bsc
|
||||
case avalanche
|
||||
case solana
|
||||
case cosmos
|
||||
}
|
||||
|
||||
/// A blockchain network.
|
||||
public struct Chain: Codable, Sendable {
|
||||
public let id: ChainId
|
||||
public let name: String
|
||||
public let chainId: Int64
|
||||
public let rpcUrl: String
|
||||
public let explorerUrl: String
|
||||
public let nativeCurrency: NativeCurrency
|
||||
public let confirmations: Int
|
||||
public let estimatedBlockTime: Int
|
||||
public let supported: Bool
|
||||
}
|
||||
|
||||
/// Native currency of a chain.
|
||||
public struct NativeCurrency: Codable, Sendable {
|
||||
public let name: String
|
||||
public let symbol: String
|
||||
public let decimals: Int
|
||||
}
|
||||
|
||||
/// Response for chain list operations.
|
||||
public struct ChainsResponse: Codable {
|
||||
public let chains: [Chain]?
|
||||
}
|
||||
|
||||
// MARK: - Asset Types
|
||||
|
||||
/// Type of asset.
|
||||
public enum AssetType: String, Codable, Sendable {
|
||||
case native
|
||||
case erc20
|
||||
case erc721
|
||||
case erc1155
|
||||
}
|
||||
|
||||
/// An asset on a blockchain.
|
||||
public struct Asset: Codable, Sendable {
|
||||
public let id: String
|
||||
public let symbol: String
|
||||
public let name: String
|
||||
public let type: AssetType
|
||||
public let chain: ChainId
|
||||
public let contractAddress: String?
|
||||
public let decimals: Int
|
||||
public let logoUrl: String?
|
||||
public let verified: Bool
|
||||
}
|
||||
|
||||
/// A wrapped asset on a target chain.
|
||||
public struct WrappedAsset: Codable, Sendable {
|
||||
public let originalAsset: Asset
|
||||
public let wrappedAsset: Asset
|
||||
public let chain: ChainId
|
||||
public let bridgeContract: String
|
||||
}
|
||||
|
||||
/// Response for asset list operations.
|
||||
public struct AssetsResponse: Codable {
|
||||
public let assets: [Asset]?
|
||||
}
|
||||
|
||||
// MARK: - Transfer Types
|
||||
|
||||
/// Status of a transfer.
|
||||
public enum TransferStatus: String, Codable, Sendable {
|
||||
case pending
|
||||
case locked
|
||||
case confirming
|
||||
case minting
|
||||
case completed
|
||||
case failed
|
||||
case refunded
|
||||
}
|
||||
|
||||
/// Direction of a transfer.
|
||||
public enum TransferDirection: String, Codable, Sendable {
|
||||
case lock_mint
|
||||
case burn_unlock
|
||||
}
|
||||
|
||||
/// A cross-chain transfer.
|
||||
public struct Transfer: Codable, Sendable {
|
||||
public let id: String
|
||||
public let direction: TransferDirection
|
||||
public let status: TransferStatus
|
||||
public let sourceChain: ChainId
|
||||
public let targetChain: ChainId
|
||||
public let asset: Asset
|
||||
public let amount: String
|
||||
public let sender: String
|
||||
public let recipient: String
|
||||
public let sourceTxHash: String?
|
||||
public let targetTxHash: String?
|
||||
public let fee: String
|
||||
public let feeAsset: Asset
|
||||
public let createdAt: Int64
|
||||
public let updatedAt: Int64
|
||||
public let completedAt: Int64?
|
||||
public let errorMessage: String?
|
||||
}
|
||||
|
||||
/// Filter for listing transfers.
|
||||
public struct TransferFilter {
|
||||
public let status: TransferStatus?
|
||||
public let sourceChain: ChainId?
|
||||
public let targetChain: ChainId?
|
||||
public let asset: String?
|
||||
public let sender: String?
|
||||
public let recipient: String?
|
||||
public let limit: Int?
|
||||
public let offset: Int?
|
||||
|
||||
public init(
|
||||
status: TransferStatus? = nil,
|
||||
sourceChain: ChainId? = nil,
|
||||
targetChain: ChainId? = nil,
|
||||
asset: String? = nil,
|
||||
sender: String? = nil,
|
||||
recipient: String? = nil,
|
||||
limit: Int? = nil,
|
||||
offset: Int? = nil
|
||||
) {
|
||||
self.status = status
|
||||
self.sourceChain = sourceChain
|
||||
self.targetChain = targetChain
|
||||
self.asset = asset
|
||||
self.sender = sender
|
||||
self.recipient = recipient
|
||||
self.limit = limit
|
||||
self.offset = offset
|
||||
}
|
||||
}
|
||||
|
||||
/// Response for transfer list operations.
|
||||
public struct TransfersResponse: Codable {
|
||||
public let transfers: [Transfer]?
|
||||
}
|
||||
|
||||
// MARK: - Lock-Mint Types
|
||||
|
||||
/// Validator signature for proofs.
|
||||
public struct ValidatorSignature: Codable, Sendable {
|
||||
public let validator: String
|
||||
public let signature: String
|
||||
public let timestamp: Int64
|
||||
}
|
||||
|
||||
/// Receipt from locking assets.
|
||||
public struct LockReceipt: Codable, Sendable {
|
||||
public let id: String
|
||||
public let txHash: String
|
||||
public let sourceChain: ChainId
|
||||
public let targetChain: ChainId
|
||||
public let asset: Asset
|
||||
public let amount: String
|
||||
public let sender: String
|
||||
public let recipient: String
|
||||
public let lockTimestamp: Int64
|
||||
public let confirmations: Int
|
||||
public let requiredConfirmations: Int
|
||||
}
|
||||
|
||||
/// Proof of lock for minting.
|
||||
public struct LockProof: Codable, Sendable {
|
||||
public let lockReceipt: LockReceipt
|
||||
public let merkleProof: [String]
|
||||
public let blockHeader: String
|
||||
public let signatures: [ValidatorSignature]
|
||||
}
|
||||
|
||||
/// Options for locking assets.
|
||||
public struct LockOptions: Codable, Sendable {
|
||||
public let recipient: String?
|
||||
public let deadline: Int64?
|
||||
public let slippage: Double?
|
||||
|
||||
public init(recipient: String? = nil, deadline: Int64? = nil, slippage: Double? = nil) {
|
||||
self.recipient = recipient
|
||||
self.deadline = deadline
|
||||
self.slippage = slippage
|
||||
}
|
||||
}
|
||||
|
||||
/// Options for minting assets.
|
||||
public struct MintOptions: Codable, Sendable {
|
||||
public let gasLimit: String?
|
||||
public let maxFeePerGas: String?
|
||||
public let maxPriorityFeePerGas: String?
|
||||
|
||||
public init(gasLimit: String? = nil, maxFeePerGas: String? = nil, maxPriorityFeePerGas: String? = nil) {
|
||||
self.gasLimit = gasLimit
|
||||
self.maxFeePerGas = maxFeePerGas
|
||||
self.maxPriorityFeePerGas = maxPriorityFeePerGas
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Burn-Unlock Types
|
||||
|
||||
/// Receipt from burning wrapped assets.
|
||||
public struct BurnReceipt: Codable, Sendable {
|
||||
public let id: String
|
||||
public let txHash: String
|
||||
public let sourceChain: ChainId
|
||||
public let targetChain: ChainId
|
||||
public let wrappedAsset: Asset
|
||||
public let originalAsset: Asset
|
||||
public let amount: String
|
||||
public let sender: String
|
||||
public let recipient: String
|
||||
public let burnTimestamp: Int64
|
||||
public let confirmations: Int
|
||||
public let requiredConfirmations: Int
|
||||
}
|
||||
|
||||
/// Proof of burn for unlocking.
|
||||
public struct BurnProof: Codable, Sendable {
|
||||
public let burnReceipt: BurnReceipt
|
||||
public let merkleProof: [String]
|
||||
public let blockHeader: String
|
||||
public let signatures: [ValidatorSignature]
|
||||
}
|
||||
|
||||
/// Options for burning wrapped assets.
|
||||
public struct BurnOptions: Codable, Sendable {
|
||||
public let recipient: String?
|
||||
public let deadline: Int64?
|
||||
|
||||
public init(recipient: String? = nil, deadline: Int64? = nil) {
|
||||
self.recipient = recipient
|
||||
self.deadline = deadline
|
||||
}
|
||||
}
|
||||
|
||||
/// Options for unlocking original assets.
|
||||
public struct UnlockOptions: Codable, Sendable {
|
||||
public let gasLimit: String?
|
||||
public let gasPrice: String?
|
||||
|
||||
public init(gasLimit: String? = nil, gasPrice: String? = nil) {
|
||||
self.gasLimit = gasLimit
|
||||
self.gasPrice = gasPrice
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fee & Rate Types
|
||||
|
||||
/// Fee estimate for a transfer.
|
||||
public struct BridgeFeeEstimate: Codable, Sendable {
|
||||
public let bridgeFee: String
|
||||
public let gasFeeSource: String
|
||||
public let gasFeeTarget: String
|
||||
public let totalFee: String
|
||||
public let feeAsset: Asset
|
||||
public let estimatedTime: Int
|
||||
public let exchangeRate: String?
|
||||
}
|
||||
|
||||
/// Exchange rate between assets.
|
||||
public struct ExchangeRate: Codable, Sendable {
|
||||
public let fromAsset: Asset
|
||||
public let toAsset: Asset
|
||||
public let rate: String
|
||||
public let inverseRate: String
|
||||
public let lastUpdated: Int64
|
||||
public let source: String
|
||||
}
|
||||
|
||||
// MARK: - Transaction Types
|
||||
|
||||
/// A signed transaction.
|
||||
public struct BridgeSignedTransaction: Codable, Sendable {
|
||||
public let txHash: String
|
||||
public let chain: ChainId
|
||||
public let from: String
|
||||
public let to: String
|
||||
public let value: String
|
||||
public let data: String
|
||||
public let gasLimit: String
|
||||
public let gasPrice: String?
|
||||
public let maxFeePerGas: String?
|
||||
public let maxPriorityFeePerGas: String?
|
||||
public let nonce: Int
|
||||
public let signature: String
|
||||
}
|
||||
|
||||
// MARK: - Error Types
|
||||
|
||||
/// Errors that can occur during bridge operations.
|
||||
public enum BridgeError: Error, LocalizedError {
|
||||
case clientClosed
|
||||
case httpError(statusCode: Int, message: String, code: String?)
|
||||
case networkError(Error)
|
||||
case encodingError(Error)
|
||||
case decodingError(Error)
|
||||
case invalidResponse
|
||||
case timeout(String)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .clientClosed:
|
||||
return "Client has been closed"
|
||||
case .httpError(let statusCode, let message, _):
|
||||
return "HTTP \(statusCode): \(message)"
|
||||
case .networkError(let error):
|
||||
return "Network error: \(error.localizedDescription)"
|
||||
case .encodingError(let error):
|
||||
return "Encoding error: \(error.localizedDescription)"
|
||||
case .decodingError(let error):
|
||||
return "Decoding error: \(error.localizedDescription)"
|
||||
case .invalidResponse:
|
||||
return "Invalid response"
|
||||
case .timeout(let message):
|
||||
return "Timeout: \(message)"
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this error indicates confirmations are still pending.
|
||||
public var isConfirmationsPending: Bool {
|
||||
if case .httpError(_, _, let code) = self {
|
||||
return code == "CONFIRMATIONS_PENDING"
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Health Check
|
||||
|
||||
/// Health check response.
|
||||
public struct BridgeHealthResponse: Codable {
|
||||
public let status: String
|
||||
}
|
||||
369
sdk/swift/Sources/Synor/Database/SynorDatabase.swift
Normal file
369
sdk/swift/Sources/Synor/Database/SynorDatabase.swift
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
import Foundation
|
||||
|
||||
/// Synor Database SDK client for Swift.
|
||||
///
|
||||
/// Provides multi-model database with Key-Value, Document, Vector, and Time Series stores.
|
||||
///
|
||||
/// Example:
|
||||
/// ```swift
|
||||
/// let db = SynorDatabase(config: DatabaseConfig(apiKey: "your-api-key"))
|
||||
///
|
||||
/// // Key-Value operations
|
||||
/// try await db.kv.set(key: "mykey", value: "myvalue", ttl: 3600)
|
||||
/// let value = try await db.kv.get(key: "mykey")
|
||||
///
|
||||
/// // Document operations
|
||||
/// let docId = try await db.documents.create(collection: "users", document: ["name": "Alice", "age": 30])
|
||||
/// let doc = try await db.documents.get(collection: "users", id: docId)
|
||||
///
|
||||
/// // Vector operations
|
||||
/// try await db.vectors.upsert(collection: "embeddings", vectors: [
|
||||
/// VectorEntry(id: "vec1", vector: [0.1, 0.2, 0.3], metadata: ["label": "test"])
|
||||
/// ])
|
||||
///
|
||||
/// // Time series operations
|
||||
/// try await db.timeseries.write(series: "metrics", points: [
|
||||
/// DataPoint(timestamp: Date().timeIntervalSince1970, value: 42.0)
|
||||
/// ])
|
||||
/// ```
|
||||
public class SynorDatabase {
|
||||
private let config: DatabaseConfig
|
||||
private let session: URLSession
|
||||
private let decoder: JSONDecoder
|
||||
private let encoder: JSONEncoder
|
||||
private var closed = false
|
||||
|
||||
/// Key-Value store operations.
|
||||
public private(set) lazy var kv = KeyValueStore(db: self)
|
||||
|
||||
/// Document store operations.
|
||||
public private(set) lazy var documents = DocumentStore(db: self)
|
||||
|
||||
/// Vector store operations.
|
||||
public private(set) lazy var vectors = VectorStore(db: self)
|
||||
|
||||
/// Time series store operations.
|
||||
public private(set) lazy var timeseries = TimeSeriesStore(db: self)
|
||||
|
||||
public init(config: DatabaseConfig) {
|
||||
self.config = config
|
||||
|
||||
let sessionConfig = URLSessionConfiguration.default
|
||||
sessionConfig.timeoutIntervalForRequest = config.timeout
|
||||
sessionConfig.timeoutIntervalForResource = config.timeout * 2
|
||||
self.session = URLSession(configuration: sessionConfig)
|
||||
|
||||
self.decoder = JSONDecoder()
|
||||
self.encoder = JSONEncoder()
|
||||
}
|
||||
|
||||
// MARK: - Key-Value Store
|
||||
|
||||
/// Key-Value store for simple key-value operations.
|
||||
public class KeyValueStore {
|
||||
private let db: SynorDatabase
|
||||
|
||||
internal init(db: SynorDatabase) {
|
||||
self.db = db
|
||||
}
|
||||
|
||||
/// Get a value by key.
|
||||
public func get(key: String) async throws -> Any? {
|
||||
let response: KVGetResponse = try await db.request(
|
||||
method: "GET",
|
||||
path: "/kv/\(key.urlEncoded)"
|
||||
)
|
||||
return response.value?.value
|
||||
}
|
||||
|
||||
/// Set a value for a key with optional TTL.
|
||||
public func set(key: String, value: Any, ttl: Int? = nil) async throws {
|
||||
var body: [String: AnyCodable] = [
|
||||
"key": AnyCodable(key),
|
||||
"value": AnyCodable(value)
|
||||
]
|
||||
if let ttl = ttl {
|
||||
body["ttl"] = AnyCodable(ttl)
|
||||
}
|
||||
let _: EmptyResponse = try await db.request(
|
||||
method: "PUT",
|
||||
path: "/kv/\(key.urlEncoded)",
|
||||
body: body
|
||||
)
|
||||
}
|
||||
|
||||
/// Delete a key.
|
||||
public func delete(key: String) async throws {
|
||||
let _: EmptyResponse = try await db.request(
|
||||
method: "DELETE",
|
||||
path: "/kv/\(key.urlEncoded)"
|
||||
)
|
||||
}
|
||||
|
||||
/// List keys by prefix.
|
||||
public func list(prefix: String) async throws -> [KeyValue] {
|
||||
let response: KVListResponse = try await db.request(
|
||||
method: "GET",
|
||||
path: "/kv?prefix=\(prefix.urlEncoded)"
|
||||
)
|
||||
return response.items ?? []
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Document Store
|
||||
|
||||
/// Document store for document-oriented operations.
|
||||
public class DocumentStore {
|
||||
private let db: SynorDatabase
|
||||
|
||||
internal init(db: SynorDatabase) {
|
||||
self.db = db
|
||||
}
|
||||
|
||||
/// Create a new document.
|
||||
public func create(collection: String, document: [String: Any]) async throws -> String {
|
||||
let body = document.mapValues { AnyCodable($0) }
|
||||
let response: CreateDocumentResponse = try await db.request(
|
||||
method: "POST",
|
||||
path: "/collections/\(collection.urlEncoded)/documents",
|
||||
body: body
|
||||
)
|
||||
return response.id
|
||||
}
|
||||
|
||||
/// Get a document by ID.
|
||||
public func get(collection: String, id: String) async throws -> Document {
|
||||
return try await db.request(
|
||||
method: "GET",
|
||||
path: "/collections/\(collection.urlEncoded)/documents/\(id.urlEncoded)"
|
||||
)
|
||||
}
|
||||
|
||||
/// Update a document.
|
||||
public func update(collection: String, id: String, update: [String: Any]) async throws {
|
||||
let body = update.mapValues { AnyCodable($0) }
|
||||
let _: EmptyResponse = try await db.request(
|
||||
method: "PATCH",
|
||||
path: "/collections/\(collection.urlEncoded)/documents/\(id.urlEncoded)",
|
||||
body: body
|
||||
)
|
||||
}
|
||||
|
||||
/// Delete a document.
|
||||
public func delete(collection: String, id: String) async throws {
|
||||
let _: EmptyResponse = try await db.request(
|
||||
method: "DELETE",
|
||||
path: "/collections/\(collection.urlEncoded)/documents/\(id.urlEncoded)"
|
||||
)
|
||||
}
|
||||
|
||||
/// Query documents.
|
||||
public func query(collection: String, query: DocumentQuery) async throws -> [Document] {
|
||||
let response: DocumentListResponse = try await db.request(
|
||||
method: "POST",
|
||||
path: "/collections/\(collection.urlEncoded)/query",
|
||||
body: query
|
||||
)
|
||||
return response.documents ?? []
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Vector Store
|
||||
|
||||
/// Vector store for similarity search operations.
|
||||
public class VectorStore {
|
||||
private let db: SynorDatabase
|
||||
|
||||
internal init(db: SynorDatabase) {
|
||||
self.db = db
|
||||
}
|
||||
|
||||
/// Upsert vectors into a collection.
|
||||
public func upsert(collection: String, vectors: [VectorEntry]) async throws {
|
||||
let body: [String: [VectorEntry]] = ["vectors": vectors]
|
||||
let _: EmptyResponse = try await db.request(
|
||||
method: "POST",
|
||||
path: "/vectors/\(collection.urlEncoded)/upsert",
|
||||
body: body
|
||||
)
|
||||
}
|
||||
|
||||
/// Search for similar vectors.
|
||||
public func search(collection: String, vector: [Double], k: Int) async throws -> [SearchResult] {
|
||||
let body: [String: AnyCodable] = [
|
||||
"vector": AnyCodable(vector),
|
||||
"k": AnyCodable(k)
|
||||
]
|
||||
let response: SearchListResponse = try await db.request(
|
||||
method: "POST",
|
||||
path: "/vectors/\(collection.urlEncoded)/search",
|
||||
body: body
|
||||
)
|
||||
return response.results ?? []
|
||||
}
|
||||
|
||||
/// Delete vectors by IDs.
|
||||
public func delete(collection: String, ids: [String]) async throws {
|
||||
let body: [String: [String]] = ["ids": ids]
|
||||
let _: EmptyResponse = try await db.request(
|
||||
method: "DELETE",
|
||||
path: "/vectors/\(collection.urlEncoded)",
|
||||
body: body
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Time Series Store
|
||||
|
||||
/// Time series store for time-based data operations.
|
||||
public class TimeSeriesStore {
|
||||
private let db: SynorDatabase
|
||||
|
||||
internal init(db: SynorDatabase) {
|
||||
self.db = db
|
||||
}
|
||||
|
||||
/// Write data points to a series.
|
||||
public func write(series: String, points: [DataPoint]) async throws {
|
||||
let body: [String: [DataPoint]] = ["points": points]
|
||||
let _: EmptyResponse = try await db.request(
|
||||
method: "POST",
|
||||
path: "/timeseries/\(series.urlEncoded)/write",
|
||||
body: body
|
||||
)
|
||||
}
|
||||
|
||||
/// Query data points from a series.
|
||||
public func query(series: String, range: TimeRange, aggregation: Aggregation? = nil) async throws -> [DataPoint] {
|
||||
var body: [String: AnyCodable] = ["range": AnyCodable(["start": range.start, "end": range.end])]
|
||||
if let aggregation = aggregation {
|
||||
body["aggregation"] = AnyCodable(["function": aggregation.function, "interval": aggregation.interval])
|
||||
}
|
||||
let response: DataPointListResponse = try await db.request(
|
||||
method: "POST",
|
||||
path: "/timeseries/\(series.urlEncoded)/query",
|
||||
body: body
|
||||
)
|
||||
return response.points ?? []
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
/// Close the client.
|
||||
public func close() {
|
||||
closed = true
|
||||
session.invalidateAndCancel()
|
||||
}
|
||||
|
||||
/// Check if the client is closed.
|
||||
public var isClosed: Bool { closed }
|
||||
|
||||
/// Perform a health check.
|
||||
public func healthCheck() async -> Bool {
|
||||
do {
|
||||
let response: HealthResponse = try await request(method: "GET", path: "/health")
|
||||
return response.status == "healthy"
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Internal HTTP Methods
|
||||
|
||||
internal func request<T: Decodable>(method: String, path: String) async throws -> T {
|
||||
return try await executeWithRetry {
|
||||
try await self.performRequest(method: method, path: path, body: nil as EmptyBody?)
|
||||
}
|
||||
}
|
||||
|
||||
internal func request<T: Decodable, R: Encodable>(method: String, path: String, body: R) async throws -> T {
|
||||
return try await executeWithRetry {
|
||||
try await self.performRequest(method: method, path: path, body: body)
|
||||
}
|
||||
}
|
||||
|
||||
private func performRequest<T: Decodable, R: Encodable>(
|
||||
method: String,
|
||||
path: String,
|
||||
body: R?
|
||||
) async throws -> T {
|
||||
if closed { throw DatabaseError.clientClosed }
|
||||
|
||||
guard let url = URL(string: "\(config.endpoint)\(path)") else {
|
||||
throw DatabaseError.invalidResponse
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version")
|
||||
|
||||
if let body = body {
|
||||
do {
|
||||
request.httpBody = try encoder.encode(body)
|
||||
} catch {
|
||||
throw DatabaseError.encodingError(error)
|
||||
}
|
||||
}
|
||||
|
||||
let (data, response): (Data, URLResponse)
|
||||
do {
|
||||
(data, response) = try await session.data(for: request)
|
||||
} catch {
|
||||
throw DatabaseError.networkError(error)
|
||||
}
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw DatabaseError.invalidResponse
|
||||
}
|
||||
|
||||
if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
|
||||
if data.isEmpty && T.self == EmptyResponse.self {
|
||||
return EmptyResponse() as! T
|
||||
}
|
||||
do {
|
||||
return try decoder.decode(T.self, from: data)
|
||||
} catch {
|
||||
throw DatabaseError.decodingError(error)
|
||||
}
|
||||
}
|
||||
|
||||
let errorInfo = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
let message = errorInfo?["message"] as? String ?? "HTTP \(httpResponse.statusCode)"
|
||||
let code = errorInfo?["code"] as? String
|
||||
throw DatabaseError.httpError(statusCode: httpResponse.statusCode, message: message, code: code)
|
||||
}
|
||||
|
||||
private func executeWithRetry<T>(_ operation: () async throws -> T) async throws -> T {
|
||||
var lastError: Error?
|
||||
|
||||
for attempt in 0..<config.retries {
|
||||
do {
|
||||
return try await operation()
|
||||
} catch {
|
||||
lastError = error
|
||||
if config.debug {
|
||||
print("Attempt \(attempt + 1) failed: \(error)")
|
||||
}
|
||||
if attempt < config.retries - 1 {
|
||||
try await Task.sleep(nanoseconds: UInt64(1_000_000_000 * (1 << attempt)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? DatabaseError.invalidResponse
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
private struct EmptyBody: Encodable {}
|
||||
private struct EmptyResponse: Decodable {}
|
||||
|
||||
private extension String {
|
||||
var urlEncoded: String {
|
||||
addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self
|
||||
}
|
||||
}
|
||||
251
sdk/swift/Sources/Synor/Database/Types.swift
Normal file
251
sdk/swift/Sources/Synor/Database/Types.swift
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import Foundation
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
/// Configuration for the Synor Database client.
|
||||
public struct DatabaseConfig {
|
||||
public let apiKey: String
|
||||
public let endpoint: String
|
||||
public let timeout: TimeInterval
|
||||
public let retries: Int
|
||||
public let debug: Bool
|
||||
|
||||
public init(
|
||||
apiKey: String,
|
||||
endpoint: String = "https://db.synor.io/v1",
|
||||
timeout: TimeInterval = 60,
|
||||
retries: Int = 3,
|
||||
debug: Bool = false
|
||||
) {
|
||||
self.apiKey = apiKey
|
||||
self.endpoint = endpoint
|
||||
self.timeout = timeout
|
||||
self.retries = retries
|
||||
self.debug = debug
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Key-Value Store Types
|
||||
|
||||
/// A key-value pair with metadata.
|
||||
public struct KeyValue: Codable, Sendable {
|
||||
public let key: String
|
||||
public let value: AnyCodable
|
||||
public let ttl: Int?
|
||||
public let createdAt: Int64?
|
||||
public let updatedAt: Int64?
|
||||
}
|
||||
|
||||
/// Response for key-value get operations.
|
||||
public struct KVGetResponse: Codable {
|
||||
public let value: AnyCodable?
|
||||
}
|
||||
|
||||
/// Response for key-value list operations.
|
||||
public struct KVListResponse: Codable {
|
||||
public let items: [KeyValue]?
|
||||
}
|
||||
|
||||
// MARK: - Document Store Types
|
||||
|
||||
/// A document with metadata.
|
||||
public struct Document: Codable, Sendable {
|
||||
public let id: String
|
||||
public let collection: String
|
||||
public let data: [String: AnyCodable]
|
||||
public let createdAt: Int64
|
||||
public let updatedAt: Int64
|
||||
}
|
||||
|
||||
/// Response for document creation.
|
||||
public struct CreateDocumentResponse: Codable {
|
||||
public let id: String
|
||||
}
|
||||
|
||||
/// Response for document queries.
|
||||
public struct DocumentListResponse: Codable {
|
||||
public let documents: [Document]?
|
||||
}
|
||||
|
||||
/// Query parameters for document store.
|
||||
public struct DocumentQuery: Codable {
|
||||
public let filter: [String: AnyCodable]?
|
||||
public let sort: [String: Int]?
|
||||
public let limit: Int?
|
||||
public let offset: Int?
|
||||
public let projection: [String]?
|
||||
|
||||
public init(
|
||||
filter: [String: AnyCodable]? = nil,
|
||||
sort: [String: Int]? = nil,
|
||||
limit: Int? = nil,
|
||||
offset: Int? = nil,
|
||||
projection: [String]? = nil
|
||||
) {
|
||||
self.filter = filter
|
||||
self.sort = sort
|
||||
self.limit = limit
|
||||
self.offset = offset
|
||||
self.projection = projection
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Vector Store Types
|
||||
|
||||
/// A vector entry with metadata.
|
||||
public struct VectorEntry: Codable, Sendable {
|
||||
public let id: String
|
||||
public let vector: [Double]
|
||||
public let metadata: [String: AnyCodable]?
|
||||
|
||||
public init(id: String, vector: [Double], metadata: [String: AnyCodable]? = nil) {
|
||||
self.id = id
|
||||
self.vector = vector
|
||||
self.metadata = metadata
|
||||
}
|
||||
}
|
||||
|
||||
/// Result from a vector search.
|
||||
public struct SearchResult: Codable, Sendable {
|
||||
public let id: String
|
||||
public let score: Double
|
||||
public let vector: [Double]?
|
||||
public let metadata: [String: AnyCodable]?
|
||||
}
|
||||
|
||||
/// Response for vector search operations.
|
||||
public struct SearchListResponse: Codable {
|
||||
public let results: [SearchResult]?
|
||||
}
|
||||
|
||||
// MARK: - Time Series Types
|
||||
|
||||
/// A time series data point.
|
||||
public struct DataPoint: Codable, Sendable {
|
||||
public let timestamp: Int64
|
||||
public let value: Double
|
||||
public let tags: [String: String]?
|
||||
|
||||
public init(timestamp: Int64, value: Double, tags: [String: String]? = nil) {
|
||||
self.timestamp = timestamp
|
||||
self.value = value
|
||||
self.tags = tags
|
||||
}
|
||||
}
|
||||
|
||||
/// Time range for queries.
|
||||
public struct TimeRange: Codable, Sendable {
|
||||
public let start: Int64
|
||||
public let end: Int64
|
||||
|
||||
public init(start: Int64, end: Int64) {
|
||||
self.start = start
|
||||
self.end = end
|
||||
}
|
||||
}
|
||||
|
||||
/// Aggregation settings for time series queries.
|
||||
public struct Aggregation: Codable, Sendable {
|
||||
public let function: String
|
||||
public let interval: String
|
||||
|
||||
public init(function: String, interval: String) {
|
||||
self.function = function
|
||||
self.interval = interval
|
||||
}
|
||||
}
|
||||
|
||||
/// Response for time series queries.
|
||||
public struct DataPointListResponse: Codable {
|
||||
public let points: [DataPoint]?
|
||||
}
|
||||
|
||||
// MARK: - Error Types
|
||||
|
||||
/// Errors that can occur during database operations.
|
||||
public enum DatabaseError: Error, LocalizedError {
|
||||
case clientClosed
|
||||
case httpError(statusCode: Int, message: String, code: String?)
|
||||
case networkError(Error)
|
||||
case encodingError(Error)
|
||||
case decodingError(Error)
|
||||
case invalidResponse
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .clientClosed:
|
||||
return "Client has been closed"
|
||||
case .httpError(let statusCode, let message, _):
|
||||
return "HTTP \(statusCode): \(message)"
|
||||
case .networkError(let error):
|
||||
return "Network error: \(error.localizedDescription)"
|
||||
case .encodingError(let error):
|
||||
return "Encoding error: \(error.localizedDescription)"
|
||||
case .decodingError(let error):
|
||||
return "Decoding error: \(error.localizedDescription)"
|
||||
case .invalidResponse:
|
||||
return "Invalid response"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Types
|
||||
|
||||
/// Type-erased codable value for dynamic JSON.
|
||||
public struct AnyCodable: Codable, Sendable {
|
||||
public let value: Any
|
||||
|
||||
public init(_ value: Any) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
if container.decodeNil() {
|
||||
value = NSNull()
|
||||
} else if let bool = try? container.decode(Bool.self) {
|
||||
value = bool
|
||||
} else if let int = try? container.decode(Int.self) {
|
||||
value = int
|
||||
} else if let double = try? container.decode(Double.self) {
|
||||
value = double
|
||||
} else if let string = try? container.decode(String.self) {
|
||||
value = string
|
||||
} else if let array = try? container.decode([AnyCodable].self) {
|
||||
value = array.map { $0.value }
|
||||
} else if let dict = try? container.decode([String: AnyCodable].self) {
|
||||
value = dict.mapValues { $0.value }
|
||||
} else {
|
||||
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unable to decode value")
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
switch value {
|
||||
case is NSNull:
|
||||
try container.encodeNil()
|
||||
case let bool as Bool:
|
||||
try container.encode(bool)
|
||||
case let int as Int:
|
||||
try container.encode(int)
|
||||
case let double as Double:
|
||||
try container.encode(double)
|
||||
case let string as String:
|
||||
try container.encode(string)
|
||||
case let array as [Any]:
|
||||
try container.encode(array.map { AnyCodable($0) })
|
||||
case let dict as [String: Any]:
|
||||
try container.encode(dict.mapValues { AnyCodable($0) })
|
||||
default:
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Unable to encode value"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Health check response.
|
||||
public struct HealthResponse: Codable {
|
||||
public let status: String
|
||||
}
|
||||
402
sdk/swift/Sources/Synor/Hosting/SynorHosting.swift
Normal file
402
sdk/swift/Sources/Synor/Hosting/SynorHosting.swift
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
import Foundation
|
||||
|
||||
/// Synor Hosting SDK client for Swift.
|
||||
///
|
||||
/// Provides decentralized web hosting with domain management, DNS, deployments, SSL, and analytics.
|
||||
///
|
||||
/// Example:
|
||||
/// ```swift
|
||||
/// let hosting = SynorHosting(config: HostingConfig(apiKey: "your-api-key"))
|
||||
///
|
||||
/// // Check domain availability
|
||||
/// let availability = try await hosting.checkAvailability(name: "mydomain.synor")
|
||||
/// if availability.available {
|
||||
/// // Register domain
|
||||
/// let domain = try await hosting.registerDomain(name: "mydomain.synor")
|
||||
/// print("Registered: \(domain.name)")
|
||||
/// }
|
||||
///
|
||||
/// // Deploy content
|
||||
/// let deployment = try await hosting.deploy(cid: "Qm...", options: DeployOptions(domain: "mydomain.synor"))
|
||||
/// print("Deployed to: \(deployment.url)")
|
||||
///
|
||||
/// // Provision SSL
|
||||
/// let cert = try await hosting.provisionSsl(domain: "mydomain.synor")
|
||||
/// print("SSL status: \(cert.status)")
|
||||
/// ```
|
||||
public class SynorHosting {
|
||||
private let config: HostingConfig
|
||||
private let session: URLSession
|
||||
private let decoder: JSONDecoder
|
||||
private let encoder: JSONEncoder
|
||||
private var closed = false
|
||||
|
||||
public init(config: HostingConfig) {
|
||||
self.config = config
|
||||
|
||||
let sessionConfig = URLSessionConfiguration.default
|
||||
sessionConfig.timeoutIntervalForRequest = config.timeout
|
||||
sessionConfig.timeoutIntervalForResource = config.timeout * 2
|
||||
self.session = URLSession(configuration: sessionConfig)
|
||||
|
||||
self.decoder = JSONDecoder()
|
||||
self.encoder = JSONEncoder()
|
||||
}
|
||||
|
||||
// MARK: - Domain Operations
|
||||
|
||||
/// Check domain availability.
|
||||
public func checkAvailability(name: String) async throws -> DomainAvailability {
|
||||
return try await get(path: "/domains/check/\(name.urlEncoded)")
|
||||
}
|
||||
|
||||
/// Register a new domain.
|
||||
public func registerDomain(name: String, options: RegisterDomainOptions? = nil) async throws -> Domain {
|
||||
var body: [String: Any] = ["name": name]
|
||||
if let options = options {
|
||||
if let years = options.years { body["years"] = years }
|
||||
if let autoRenew = options.autoRenew { body["autoRenew"] = autoRenew }
|
||||
}
|
||||
return try await post(path: "/domains", body: body)
|
||||
}
|
||||
|
||||
/// Get domain details.
|
||||
public func getDomain(name: String) async throws -> Domain {
|
||||
return try await get(path: "/domains/\(name.urlEncoded)")
|
||||
}
|
||||
|
||||
/// List all domains.
|
||||
public func listDomains() async throws -> [Domain] {
|
||||
let response: DomainsResponse = try await get(path: "/domains")
|
||||
return response.domains ?? []
|
||||
}
|
||||
|
||||
/// Update domain record.
|
||||
public func updateDomainRecord(name: String, record: DomainRecord) async throws -> Domain {
|
||||
return try await put(path: "/domains/\(name.urlEncoded)/record", body: record)
|
||||
}
|
||||
|
||||
/// Resolve domain to its record.
|
||||
public func resolveDomain(name: String) async throws -> DomainRecord {
|
||||
return try await get(path: "/domains/\(name.urlEncoded)/resolve")
|
||||
}
|
||||
|
||||
/// Renew a domain.
|
||||
public func renewDomain(name: String, years: Int) async throws -> Domain {
|
||||
return try await post(path: "/domains/\(name.urlEncoded)/renew", body: ["years": years])
|
||||
}
|
||||
|
||||
// MARK: - DNS Operations
|
||||
|
||||
/// Get DNS zone for a domain.
|
||||
public func getDnsZone(domain: String) async throws -> DnsZone {
|
||||
return try await get(path: "/dns/\(domain.urlEncoded)")
|
||||
}
|
||||
|
||||
/// Set DNS records for a domain.
|
||||
public func setDnsRecords(domain: String, records: [DnsRecord]) async throws -> DnsZone {
|
||||
return try await put(path: "/dns/\(domain.urlEncoded)", body: ["records": records])
|
||||
}
|
||||
|
||||
/// Add a DNS record.
|
||||
public func addDnsRecord(domain: String, record: DnsRecord) async throws -> DnsZone {
|
||||
return try await post(path: "/dns/\(domain.urlEncoded)/records", body: record)
|
||||
}
|
||||
|
||||
/// Delete a DNS record.
|
||||
public func deleteDnsRecord(domain: String, recordType: String, name: String) async throws -> DnsZone {
|
||||
return try await delete(path: "/dns/\(domain.urlEncoded)/records/\(recordType)/\(name.urlEncoded)")
|
||||
}
|
||||
|
||||
// MARK: - Deployment Operations
|
||||
|
||||
/// Deploy content to a domain.
|
||||
public func deploy(cid: String, options: DeployOptions? = nil) async throws -> Deployment {
|
||||
var body: [String: Any] = ["cid": cid]
|
||||
if let options = options {
|
||||
if let domain = options.domain { body["domain"] = domain }
|
||||
if let subdomain = options.subdomain { body["subdomain"] = subdomain }
|
||||
if let spa = options.spa { body["spa"] = spa }
|
||||
if let cleanUrls = options.cleanUrls { body["cleanUrls"] = cleanUrls }
|
||||
if let trailingSlash = options.trailingSlash { body["trailingSlash"] = trailingSlash }
|
||||
}
|
||||
return try await post(path: "/deployments", body: body)
|
||||
}
|
||||
|
||||
/// Get deployment details.
|
||||
public func getDeployment(id: String) async throws -> Deployment {
|
||||
return try await get(path: "/deployments/\(id.urlEncoded)")
|
||||
}
|
||||
|
||||
/// List deployments.
|
||||
public func listDeployments(domain: String? = nil) async throws -> [Deployment] {
|
||||
var path = "/deployments"
|
||||
if let domain = domain {
|
||||
path += "?domain=\(domain.urlEncoded)"
|
||||
}
|
||||
let response: DeploymentsResponse = try await get(path: path)
|
||||
return response.deployments ?? []
|
||||
}
|
||||
|
||||
/// Rollback to a previous deployment.
|
||||
public func rollback(domain: String, deploymentId: String) async throws -> Deployment {
|
||||
return try await post(
|
||||
path: "/deployments/\(deploymentId.urlEncoded)/rollback",
|
||||
body: ["domain": domain]
|
||||
)
|
||||
}
|
||||
|
||||
/// Delete a deployment.
|
||||
public func deleteDeployment(id: String) async throws {
|
||||
let _: EmptyHostingResponse = try await deleteRequest(path: "/deployments/\(id.urlEncoded)")
|
||||
}
|
||||
|
||||
// MARK: - SSL Operations
|
||||
|
||||
/// Provision SSL certificate for a domain.
|
||||
public func provisionSsl(domain: String, options: ProvisionSslOptions? = nil) async throws -> Certificate {
|
||||
var body: [String: Any] = [:]
|
||||
if let options = options {
|
||||
if let includeWww = options.includeWww { body["includeWww"] = includeWww }
|
||||
if let autoRenew = options.autoRenew { body["autoRenew"] = autoRenew }
|
||||
}
|
||||
return try await post(path: "/ssl/\(domain.urlEncoded)", body: body.isEmpty ? nil : body)
|
||||
}
|
||||
|
||||
/// Get certificate details.
|
||||
public func getCertificate(domain: String) async throws -> Certificate {
|
||||
return try await get(path: "/ssl/\(domain.urlEncoded)")
|
||||
}
|
||||
|
||||
/// Renew SSL certificate.
|
||||
public func renewCertificate(domain: String) async throws -> Certificate {
|
||||
return try await post(path: "/ssl/\(domain.urlEncoded)/renew", body: nil as [String: Any]?)
|
||||
}
|
||||
|
||||
/// Delete SSL certificate.
|
||||
public func deleteCertificate(domain: String) async throws {
|
||||
let _: EmptyHostingResponse = try await deleteRequest(path: "/ssl/\(domain.urlEncoded)")
|
||||
}
|
||||
|
||||
// MARK: - Site Configuration
|
||||
|
||||
/// Get site configuration.
|
||||
public func getSiteConfig(domain: String) async throws -> SiteConfig {
|
||||
return try await get(path: "/sites/\(domain.urlEncoded)/config")
|
||||
}
|
||||
|
||||
/// Update site configuration.
|
||||
public func updateSiteConfig(domain: String, config: [String: Any]) async throws -> SiteConfig {
|
||||
return try await patch(path: "/sites/\(domain.urlEncoded)/config", body: config)
|
||||
}
|
||||
|
||||
/// Purge cache.
|
||||
public func purgeCache(domain: String, paths: [String]? = nil) async throws -> Int64 {
|
||||
let body: [String: Any]? = paths.map { ["paths": $0] }
|
||||
let response: PurgeResponse = try await deleteRequest(
|
||||
path: "/sites/\(domain.urlEncoded)/cache",
|
||||
body: body
|
||||
)
|
||||
return response.purged
|
||||
}
|
||||
|
||||
// MARK: - Analytics
|
||||
|
||||
/// Get analytics data for a domain.
|
||||
public func getAnalytics(domain: String, options: AnalyticsOptions? = nil) async throws -> AnalyticsData {
|
||||
var params: [String] = []
|
||||
if let period = options?.period { params.append("period=\(period.urlEncoded)") }
|
||||
if let start = options?.start { params.append("start=\(start.urlEncoded)") }
|
||||
if let end = options?.end { params.append("end=\(end.urlEncoded)") }
|
||||
|
||||
var path = "/sites/\(domain.urlEncoded)/analytics"
|
||||
if !params.isEmpty {
|
||||
path += "?\(params.joined(separator: "&"))"
|
||||
}
|
||||
return try await get(path: path)
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
/// Close the client.
|
||||
public func close() {
|
||||
closed = true
|
||||
session.invalidateAndCancel()
|
||||
}
|
||||
|
||||
/// Check if the client is closed.
|
||||
public var isClosed: Bool { closed }
|
||||
|
||||
/// Perform a health check.
|
||||
public func healthCheck() async -> Bool {
|
||||
do {
|
||||
let response: HostingHealthResponse = try await get(path: "/health")
|
||||
return response.status == "healthy"
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private HTTP Methods
|
||||
|
||||
private func get<T: Decodable>(path: String) async throws -> T {
|
||||
return try await executeWithRetry {
|
||||
try await self.performRequest(method: "GET", path: path, body: nil as [String: Any]?)
|
||||
}
|
||||
}
|
||||
|
||||
private func post<T: Decodable>(path: String, body: [String: Any]?) async throws -> T {
|
||||
return try await executeWithRetry {
|
||||
try await self.performRequest(method: "POST", path: path, body: body)
|
||||
}
|
||||
}
|
||||
|
||||
private func post<T: Decodable, R: Encodable>(path: String, body: R) async throws -> T {
|
||||
return try await executeWithRetry {
|
||||
try await self.performEncodableRequest(method: "POST", path: path, body: body)
|
||||
}
|
||||
}
|
||||
|
||||
private func put<T: Decodable>(path: String, body: [String: Any]) async throws -> T {
|
||||
return try await executeWithRetry {
|
||||
try await self.performRequest(method: "PUT", path: path, body: body)
|
||||
}
|
||||
}
|
||||
|
||||
private func put<T: Decodable, R: Encodable>(path: String, body: R) async throws -> T {
|
||||
return try await executeWithRetry {
|
||||
try await self.performEncodableRequest(method: "PUT", path: path, body: body)
|
||||
}
|
||||
}
|
||||
|
||||
private func patch<T: Decodable>(path: String, body: [String: Any]) async throws -> T {
|
||||
return try await executeWithRetry {
|
||||
try await self.performRequest(method: "PATCH", path: path, body: body)
|
||||
}
|
||||
}
|
||||
|
||||
private func delete<T: Decodable>(path: String) async throws -> T {
|
||||
return try await executeWithRetry {
|
||||
try await self.performRequest(method: "DELETE", path: path, body: nil as [String: Any]?)
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteRequest<T: Decodable>(path: String, body: [String: Any]? = nil) async throws -> T {
|
||||
return try await executeWithRetry {
|
||||
try await self.performRequest(method: "DELETE", path: path, body: body)
|
||||
}
|
||||
}
|
||||
|
||||
private func performRequest<T: Decodable>(
|
||||
method: String,
|
||||
path: String,
|
||||
body: [String: Any]?
|
||||
) async throws -> T {
|
||||
if closed { throw HostingError.clientClosed }
|
||||
|
||||
guard let url = URL(string: "\(config.endpoint)\(path)") else {
|
||||
throw HostingError.invalidResponse
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version")
|
||||
|
||||
if let body = body {
|
||||
do {
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||
} catch {
|
||||
throw HostingError.encodingError(error)
|
||||
}
|
||||
}
|
||||
|
||||
return try await executeRequest(request)
|
||||
}
|
||||
|
||||
private func performEncodableRequest<T: Decodable, R: Encodable>(
|
||||
method: String,
|
||||
path: String,
|
||||
body: R
|
||||
) async throws -> T {
|
||||
if closed { throw HostingError.clientClosed }
|
||||
|
||||
guard let url = URL(string: "\(config.endpoint)\(path)") else {
|
||||
throw HostingError.invalidResponse
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version")
|
||||
|
||||
do {
|
||||
request.httpBody = try encoder.encode(body)
|
||||
} catch {
|
||||
throw HostingError.encodingError(error)
|
||||
}
|
||||
|
||||
return try await executeRequest(request)
|
||||
}
|
||||
|
||||
private func executeRequest<T: Decodable>(_ request: URLRequest) async throws -> T {
|
||||
let (data, response): (Data, URLResponse)
|
||||
do {
|
||||
(data, response) = try await session.data(for: request)
|
||||
} catch {
|
||||
throw HostingError.networkError(error)
|
||||
}
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw HostingError.invalidResponse
|
||||
}
|
||||
|
||||
if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
|
||||
if data.isEmpty && T.self == EmptyHostingResponse.self {
|
||||
return EmptyHostingResponse() as! T
|
||||
}
|
||||
do {
|
||||
return try decoder.decode(T.self, from: data)
|
||||
} catch {
|
||||
throw HostingError.decodingError(error)
|
||||
}
|
||||
}
|
||||
|
||||
let errorInfo = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
let message = errorInfo?["message"] as? String ?? "HTTP \(httpResponse.statusCode)"
|
||||
let code = errorInfo?["code"] as? String
|
||||
throw HostingError.httpError(statusCode: httpResponse.statusCode, message: message, code: code)
|
||||
}
|
||||
|
||||
private func executeWithRetry<T>(_ operation: () async throws -> T) async throws -> T {
|
||||
var lastError: Error?
|
||||
|
||||
for attempt in 0..<config.retries {
|
||||
do {
|
||||
return try await operation()
|
||||
} catch {
|
||||
lastError = error
|
||||
if config.debug {
|
||||
print("Attempt \(attempt + 1) failed: \(error)")
|
||||
}
|
||||
if attempt < config.retries - 1 {
|
||||
try await Task.sleep(nanoseconds: UInt64(1_000_000_000 * (1 << attempt)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? HostingError.invalidResponse
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
private struct EmptyHostingResponse: Decodable {}
|
||||
|
||||
private extension String {
|
||||
var urlEncoded: String {
|
||||
addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self
|
||||
}
|
||||
}
|
||||
328
sdk/swift/Sources/Synor/Hosting/Types.swift
Normal file
328
sdk/swift/Sources/Synor/Hosting/Types.swift
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
import Foundation
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
/// Configuration for the Synor Hosting client.
|
||||
public struct HostingConfig {
|
||||
public let apiKey: String
|
||||
public let endpoint: String
|
||||
public let timeout: TimeInterval
|
||||
public let retries: Int
|
||||
public let debug: Bool
|
||||
|
||||
public init(
|
||||
apiKey: String,
|
||||
endpoint: String = "https://hosting.synor.io/v1",
|
||||
timeout: TimeInterval = 60,
|
||||
retries: Int = 3,
|
||||
debug: Bool = false
|
||||
) {
|
||||
self.apiKey = apiKey
|
||||
self.endpoint = endpoint
|
||||
self.timeout = timeout
|
||||
self.retries = retries
|
||||
self.debug = debug
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Domain Types
|
||||
|
||||
/// Status of a domain.
|
||||
public enum DomainStatus: String, Codable, Sendable {
|
||||
case pending
|
||||
case active
|
||||
case expired
|
||||
case suspended
|
||||
}
|
||||
|
||||
/// A domain registration.
|
||||
public struct Domain: Codable, Sendable {
|
||||
public let name: String
|
||||
public let status: DomainStatus
|
||||
public let owner: String
|
||||
public let registeredAt: Int64
|
||||
public let expiresAt: Int64
|
||||
public let autoRenew: Bool
|
||||
public let records: DomainRecord?
|
||||
}
|
||||
|
||||
/// DNS record for a domain.
|
||||
public struct DomainRecord: Codable, Sendable {
|
||||
public let cid: String?
|
||||
public let ipv4: [String]?
|
||||
public let ipv6: [String]?
|
||||
public let cname: String?
|
||||
public let txt: [String]?
|
||||
public let metadata: [String: String]?
|
||||
|
||||
public init(
|
||||
cid: String? = nil,
|
||||
ipv4: [String]? = nil,
|
||||
ipv6: [String]? = nil,
|
||||
cname: String? = nil,
|
||||
txt: [String]? = nil,
|
||||
metadata: [String: String]? = nil
|
||||
) {
|
||||
self.cid = cid
|
||||
self.ipv4 = ipv4
|
||||
self.ipv6 = ipv6
|
||||
self.cname = cname
|
||||
self.txt = txt
|
||||
self.metadata = metadata
|
||||
}
|
||||
}
|
||||
|
||||
/// Domain availability check result.
|
||||
public struct DomainAvailability: Codable, Sendable {
|
||||
public let name: String
|
||||
public let available: Bool
|
||||
public let price: Double?
|
||||
public let premium: Bool
|
||||
}
|
||||
|
||||
/// Options for domain registration.
|
||||
public struct RegisterDomainOptions: Codable, Sendable {
|
||||
public let years: Int?
|
||||
public let autoRenew: Bool?
|
||||
|
||||
public init(years: Int? = nil, autoRenew: Bool? = nil) {
|
||||
self.years = years
|
||||
self.autoRenew = autoRenew
|
||||
}
|
||||
}
|
||||
|
||||
/// Response for domain list operations.
|
||||
public struct DomainsResponse: Codable {
|
||||
public let domains: [Domain]?
|
||||
}
|
||||
|
||||
// MARK: - DNS Types
|
||||
|
||||
/// Type of DNS record.
|
||||
public enum DnsRecordType: String, Codable, Sendable {
|
||||
case A, AAAA, CNAME, TXT, MX, NS, SRV, CAA
|
||||
}
|
||||
|
||||
/// A DNS record.
|
||||
public struct DnsRecord: Codable, Sendable {
|
||||
public let type: DnsRecordType
|
||||
public let name: String
|
||||
public let value: String
|
||||
public let ttl: Int
|
||||
public let priority: Int?
|
||||
|
||||
public init(type: DnsRecordType, name: String, value: String, ttl: Int = 3600, priority: Int? = nil) {
|
||||
self.type = type
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.ttl = ttl
|
||||
self.priority = priority
|
||||
}
|
||||
}
|
||||
|
||||
/// A DNS zone containing records.
|
||||
public struct DnsZone: Codable, Sendable {
|
||||
public let domain: String
|
||||
public let records: [DnsRecord]
|
||||
public let updatedAt: Int64
|
||||
}
|
||||
|
||||
// MARK: - Deployment Types
|
||||
|
||||
/// Status of a deployment.
|
||||
public enum DeploymentStatus: String, Codable, Sendable {
|
||||
case pending
|
||||
case building
|
||||
case deploying
|
||||
case active
|
||||
case failed
|
||||
case inactive
|
||||
}
|
||||
|
||||
/// A deployment.
|
||||
public struct Deployment: Codable, Sendable {
|
||||
public let id: String
|
||||
public let domain: String
|
||||
public let cid: String
|
||||
public let status: DeploymentStatus
|
||||
public let url: String
|
||||
public let createdAt: Int64
|
||||
public let updatedAt: Int64
|
||||
public let buildLogs: String?
|
||||
public let errorMessage: String?
|
||||
}
|
||||
|
||||
/// Options for deploying content.
|
||||
public struct DeployOptions: Codable, Sendable {
|
||||
public let domain: String?
|
||||
public let subdomain: String?
|
||||
public let spa: Bool?
|
||||
public let cleanUrls: Bool?
|
||||
public let trailingSlash: Bool?
|
||||
|
||||
public init(
|
||||
domain: String? = nil,
|
||||
subdomain: String? = nil,
|
||||
spa: Bool? = nil,
|
||||
cleanUrls: Bool? = nil,
|
||||
trailingSlash: Bool? = nil
|
||||
) {
|
||||
self.domain = domain
|
||||
self.subdomain = subdomain
|
||||
self.spa = spa
|
||||
self.cleanUrls = cleanUrls
|
||||
self.trailingSlash = trailingSlash
|
||||
}
|
||||
}
|
||||
|
||||
/// Response for deployment list operations.
|
||||
public struct DeploymentsResponse: Codable {
|
||||
public let deployments: [Deployment]?
|
||||
}
|
||||
|
||||
// MARK: - SSL Types
|
||||
|
||||
/// Status of an SSL certificate.
|
||||
public enum CertificateStatus: String, Codable, Sendable {
|
||||
case pending
|
||||
case issued
|
||||
case expired
|
||||
case revoked
|
||||
}
|
||||
|
||||
/// An SSL certificate.
|
||||
public struct Certificate: Codable, Sendable {
|
||||
public let domain: String
|
||||
public let status: CertificateStatus
|
||||
public let autoRenew: Bool
|
||||
public let issuer: String
|
||||
public let issuedAt: Int64?
|
||||
public let expiresAt: Int64?
|
||||
public let fingerprint: String?
|
||||
}
|
||||
|
||||
/// Options for SSL provisioning.
|
||||
public struct ProvisionSslOptions: Codable, Sendable {
|
||||
public let includeWww: Bool?
|
||||
public let autoRenew: Bool?
|
||||
|
||||
public init(includeWww: Bool? = nil, autoRenew: Bool? = nil) {
|
||||
self.includeWww = includeWww
|
||||
self.autoRenew = autoRenew
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Site Configuration Types
|
||||
|
||||
/// Site configuration.
|
||||
public struct SiteConfig: Codable, Sendable {
|
||||
public let domain: String
|
||||
public let cid: String?
|
||||
public let headers: [String: String]?
|
||||
public let redirects: [RedirectRule]?
|
||||
public let errorPages: [String: String]?
|
||||
public let spa: Bool
|
||||
public let cleanUrls: Bool
|
||||
public let trailingSlash: Bool
|
||||
}
|
||||
|
||||
/// A redirect rule.
|
||||
public struct RedirectRule: Codable, Sendable {
|
||||
public let source: String
|
||||
public let destination: String
|
||||
public let statusCode: Int
|
||||
public let permanent: Bool
|
||||
|
||||
public init(source: String, destination: String, statusCode: Int = 301, permanent: Bool = true) {
|
||||
self.source = source
|
||||
self.destination = destination
|
||||
self.statusCode = statusCode
|
||||
self.permanent = permanent
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Analytics Types
|
||||
|
||||
/// Analytics data for a site.
|
||||
public struct AnalyticsData: Codable, Sendable {
|
||||
public let domain: String
|
||||
public let period: String
|
||||
public let pageViews: Int64
|
||||
public let uniqueVisitors: Int64
|
||||
public let bandwidth: Int64
|
||||
public let topPages: [PageView]
|
||||
public let topReferrers: [Referrer]
|
||||
public let topCountries: [Country]
|
||||
}
|
||||
|
||||
/// Page view statistics.
|
||||
public struct PageView: Codable, Sendable {
|
||||
public let path: String
|
||||
public let views: Int64
|
||||
}
|
||||
|
||||
/// Referrer statistics.
|
||||
public struct Referrer: Codable, Sendable {
|
||||
public let referrer: String
|
||||
public let count: Int64
|
||||
}
|
||||
|
||||
/// Country statistics.
|
||||
public struct Country: Codable, Sendable {
|
||||
public let country: String
|
||||
public let count: Int64
|
||||
}
|
||||
|
||||
/// Options for analytics queries.
|
||||
public struct AnalyticsOptions: Codable, Sendable {
|
||||
public let period: String?
|
||||
public let start: String?
|
||||
public let end: String?
|
||||
|
||||
public init(period: String? = nil, start: String? = nil, end: String? = nil) {
|
||||
self.period = period
|
||||
self.start = start
|
||||
self.end = end
|
||||
}
|
||||
}
|
||||
|
||||
/// Response for cache purge operations.
|
||||
public struct PurgeResponse: Codable {
|
||||
public let purged: Int64
|
||||
}
|
||||
|
||||
// MARK: - Error Types
|
||||
|
||||
/// Errors that can occur during hosting operations.
|
||||
public enum HostingError: Error, LocalizedError {
|
||||
case clientClosed
|
||||
case httpError(statusCode: Int, message: String, code: String?)
|
||||
case networkError(Error)
|
||||
case encodingError(Error)
|
||||
case decodingError(Error)
|
||||
case invalidResponse
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .clientClosed:
|
||||
return "Client has been closed"
|
||||
case .httpError(let statusCode, let message, _):
|
||||
return "HTTP \(statusCode): \(message)"
|
||||
case .networkError(let error):
|
||||
return "Network error: \(error.localizedDescription)"
|
||||
case .encodingError(let error):
|
||||
return "Encoding error: \(error.localizedDescription)"
|
||||
case .decodingError(let error):
|
||||
return "Decoding error: \(error.localizedDescription)"
|
||||
case .invalidResponse:
|
||||
return "Invalid response"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Health Check
|
||||
|
||||
/// Health check response.
|
||||
public struct HostingHealthResponse: Codable {
|
||||
public let status: String
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue