diff --git a/sdk/c/include/synor/bridge.h b/sdk/c/include/synor/bridge.h new file mode 100644 index 0000000..fe8f53d --- /dev/null +++ b/sdk/c/include/synor/bridge.h @@ -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 +#include +#include + +#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 */ diff --git a/sdk/c/include/synor/database.h b/sdk/c/include/synor/database.h new file mode 100644 index 0000000..ba65897 --- /dev/null +++ b/sdk/c/include/synor/database.h @@ -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 +#include +#include + +#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 */ diff --git a/sdk/c/include/synor/hosting.h b/sdk/c/include/synor/hosting.h new file mode 100644 index 0000000..1732174 --- /dev/null +++ b/sdk/c/include/synor/hosting.h @@ -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 +#include +#include + +#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 */ diff --git a/sdk/c/src/bridge/bridge.c b/sdk/c/src/bridge/bridge.c new file mode 100644 index 0000000..7592e47 --- /dev/null +++ b/sdk/c/src/bridge/bridge.c @@ -0,0 +1,441 @@ +/** + * @file bridge.c + * @brief Synor Bridge SDK implementation + */ + +#include +#include +#include +#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; +} diff --git a/sdk/c/src/database/database.c b/sdk/c/src/database/database.c new file mode 100644 index 0000000..685f9a4 --- /dev/null +++ b/sdk/c/src/database/database.c @@ -0,0 +1,303 @@ +/** + * @file database.c + * @brief Synor Database SDK implementation + */ + +#include +#include +#include +#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; +} diff --git a/sdk/c/src/hosting/hosting.c b/sdk/c/src/hosting/hosting.c new file mode 100644 index 0000000..81bcbe8 --- /dev/null +++ b/sdk/c/src/hosting/hosting.c @@ -0,0 +1,309 @@ +/** + * @file hosting.c + * @brief Synor Hosting SDK implementation + */ + +#include +#include +#include +#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); +} diff --git a/sdk/cpp/include/synor/bridge.hpp b/sdk/cpp/include/synor/bridge.hpp new file mode 100644 index 0000000..5d79869 --- /dev/null +++ b/sdk/cpp/include/synor/bridge.hpp @@ -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 +#include +#include +#include +#include +#include +#include +#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 contract_address; + int32_t decimals; + std::optional 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 merkle_proof; + std::string block_header; + std::vector 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 merkle_proof; + std::string block_header; + std::vector 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 source_tx_hash; + std::optional target_tx_hash; + std::string fee; + Asset fee_asset; + int64_t created_at; + int64_t updated_at; + std::optional completed_at; + std::optional error_message; +}; + +/// Transfer filter +struct TransferFilter { + std::optional status; + std::optional source_chain; + std::optional target_chain; + std::optional asset; + std::optional sender; + std::optional recipient; + std::optional limit; + std::optional 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 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 gas_price; + std::optional max_fee_per_gas; + std::optional max_priority_fee_per_gas; + int32_t nonce; + std::string signature; +}; + +/// Lock options +struct LockOptions { + std::optional recipient; + std::optional deadline; + std::optional slippage; +}; + +/// Mint options +struct MintOptions { + std::optional gas_limit; + std::optional max_fee_per_gas; + std::optional max_priority_fee_per_gas; +}; + +/// Burn options +struct BurnOptions { + std::optional recipient; + std::optional deadline; +}; + +/// Unlock options +struct UnlockOptions { + std::optional gas_limit; + std::optional 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> get_supported_chains(); + std::future get_chain(ChainId chain_id); + std::future is_chain_supported(ChainId chain_id); + + // Asset operations + std::future> get_supported_assets(ChainId chain_id); + std::future get_asset(const std::string& asset_id); + std::future get_wrapped_asset(const std::string& original_asset_id, + ChainId target_chain); + + // Fee & rate operations + std::future estimate_fee(const std::string& asset, const std::string& amount, + ChainId source_chain, ChainId target_chain); + std::future get_exchange_rate(const std::string& from_asset, + const std::string& to_asset); + + // Lock-Mint flow + std::future lock(const std::string& asset, const std::string& amount, + ChainId target_chain, const LockOptions& options = {}); + std::future get_lock_proof(const std::string& lock_receipt_id); + std::future 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 mint(const LockProof& proof, const std::string& target_address, + const MintOptions& options = {}); + + // Burn-Unlock flow + std::future burn(const std::string& wrapped_asset, const std::string& amount, + const BurnOptions& options = {}); + std::future get_burn_proof(const std::string& burn_receipt_id); + std::future 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 unlock(const BurnProof& proof, const UnlockOptions& options = {}); + + // Transfer management + std::future get_transfer(const std::string& transfer_id); + std::future get_transfer_status(const std::string& transfer_id); + std::future> list_transfers(const TransferFilter& filter = {}); + std::future 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 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 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 health_check(); + +private: + std::unique_ptr impl_; +}; + +} // namespace bridge +} // namespace synor diff --git a/sdk/cpp/include/synor/database.hpp b/sdk/cpp/include/synor/database.hpp new file mode 100644 index 0000000..1ce916b --- /dev/null +++ b/sdk/cpp/include/synor/database.hpp @@ -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 +#include +#include +#include +#include +#include +#include +#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 ttl; + std::optional created_at; + std::optional updated_at; +}; + +/// A document +struct Document { + std::string id; + std::string collection; + std::map data; // JSON data + int64_t created_at; + int64_t updated_at; +}; + +/// Query for documents +struct Query { + std::optional filter; // JSON + std::optional sort; // JSON + std::optional limit; + std::optional offset; + std::optional> projection; +}; + +/// A vector entry +struct VectorEntry { + std::string id; + std::vector vector; + std::optional> metadata; +}; + +/// A search result +struct SearchResult { + std::string id; + double score; + std::optional> vector; + std::optional> metadata; +}; + +/// A time series data point +struct DataPoint { + int64_t timestamp; + double value; + std::optional> 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 impl); + + std::future> get(const std::string& key); + std::future set(const std::string& key, const std::string& value, + std::optional ttl = std::nullopt); + std::future remove(const std::string& key); + std::future> list(const std::string& prefix); + +private: + std::shared_ptr impl_; +}; + +/// Document store interface +class DocumentStore { +public: + explicit DocumentStore(std::shared_ptr impl); + + std::future create(const std::string& collection, const std::string& document); + std::future get(const std::string& collection, const std::string& id); + std::future update(const std::string& collection, const std::string& id, + const std::string& update); + std::future remove(const std::string& collection, const std::string& id); + std::future> query(const std::string& collection, const Query& query); + +private: + std::shared_ptr impl_; +}; + +/// Vector store interface +class VectorStore { +public: + explicit VectorStore(std::shared_ptr impl); + + std::future upsert(const std::string& collection, const std::vector& vectors); + std::future> search(const std::string& collection, + const std::vector& vector, int32_t k); + std::future remove(const std::string& collection, const std::vector& ids); + +private: + std::shared_ptr impl_; +}; + +/// Time series store interface +class TimeSeriesStore { +public: + explicit TimeSeriesStore(std::shared_ptr impl); + + std::future write(const std::string& series, const std::vector& points); + std::future> query(const std::string& series, const TimeRange& range, + std::optional aggregation = std::nullopt); + +private: + std::shared_ptr 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 health_check(); + +private: + std::shared_ptr impl_; + std::unique_ptr kv_; + std::unique_ptr documents_; + std::unique_ptr vectors_; + std::unique_ptr timeseries_; +}; + +} // namespace database +} // namespace synor diff --git a/sdk/cpp/include/synor/hosting.hpp b/sdk/cpp/include/synor/hosting.hpp new file mode 100644 index 0000000..2b300cd --- /dev/null +++ b/sdk/cpp/include/synor/hosting.hpp @@ -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 +#include +#include +#include +#include +#include +#include +#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 cid; + std::optional> ipv4; + std::optional> ipv6; + std::optional cname; + std::optional> txt; + std::optional> 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 records; +}; + +/// Domain availability +struct DomainAvailability { + std::string name; + bool available; + std::optional price; + bool premium = false; +}; + +/// Domain registration options +struct RegisterDomainOptions { + std::optional years; + std::optional auto_renew; +}; + +/// DNS record +struct DnsRecord { + DnsRecordType type; + std::string name; + std::string value; + int32_t ttl = 3600; + std::optional priority; +}; + +/// DNS zone +struct DnsZone { + std::string domain; + std::vector 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 build_logs; + std::optional error_message; +}; + +/// Deploy options +struct DeployOptions { + std::optional domain; + std::optional subdomain; + std::optional spa; + std::optional clean_urls; + std::optional trailing_slash; +}; + +/// SSL certificate +struct Certificate { + std::string domain; + CertificateStatus status; + bool auto_renew; + std::string issuer; + std::optional issued_at; + std::optional expires_at; + std::optional fingerprint; +}; + +/// SSL provisioning options +struct ProvisionSslOptions { + std::optional include_www; + std::optional 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 cid; + std::optional> headers; + std::optional> redirects; + std::optional> 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 top_pages; +}; + +/// Analytics options +struct AnalyticsOptions { + std::optional period; + std::optional start; + std::optional 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 check_availability(const std::string& name); + std::future register_domain(const std::string& name, + const RegisterDomainOptions& options = {}); + std::future get_domain(const std::string& name); + std::future> list_domains(); + std::future update_domain_record(const std::string& name, const DomainRecord& record); + std::future resolve_domain(const std::string& name); + std::future renew_domain(const std::string& name, int32_t years); + + // DNS operations + std::future get_dns_zone(const std::string& domain); + std::future set_dns_records(const std::string& domain, + const std::vector& records); + std::future add_dns_record(const std::string& domain, const DnsRecord& record); + std::future delete_dns_record(const std::string& domain, + const std::string& record_type, + const std::string& name); + + // Deployment operations + std::future deploy(const std::string& cid, const DeployOptions& options = {}); + std::future get_deployment(const std::string& id); + std::future> list_deployments( + std::optional domain = std::nullopt); + std::future rollback(const std::string& domain, const std::string& deployment_id); + std::future delete_deployment(const std::string& id); + + // SSL operations + std::future provision_ssl(const std::string& domain, + const ProvisionSslOptions& options = {}); + std::future get_certificate(const std::string& domain); + std::future renew_certificate(const std::string& domain); + std::future delete_certificate(const std::string& domain); + + // Site configuration + std::future get_site_config(const std::string& domain); + std::future update_site_config(const std::string& domain, + const std::map& config); + std::future purge_cache(const std::string& domain, + std::optional> paths = std::nullopt); + + // Analytics + std::future get_analytics(const std::string& domain, + const AnalyticsOptions& options = {}); + + // Lifecycle + void close(); + bool is_closed() const; + std::future health_check(); + +private: + std::unique_ptr impl_; +}; + +} // namespace hosting +} // namespace synor diff --git a/sdk/csharp/Synor.Sdk/Bridge/SynorBridge.cs b/sdk/csharp/Synor.Sdk/Bridge/SynorBridge.cs new file mode 100644 index 0000000..fb68afc --- /dev/null +++ b/sdk/csharp/Synor.Sdk/Bridge/SynorBridge.cs @@ -0,0 +1,237 @@ +using System.Net.Http.Json; +using System.Text.Json; + +namespace Synor.Sdk.Bridge; + +/// +/// Synor Bridge SDK client for C#. +/// Cross-chain asset transfers with lock-mint and burn-unlock patterns. +/// +public class SynorBridge : IDisposable +{ + private readonly BridgeConfig _config; + private readonly HttpClient _httpClient; + private readonly JsonSerializerOptions _jsonOptions; + private bool _disposed; + private static readonly HashSet 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> GetSupportedChainsAsync(CancellationToken ct = default) + { + var r = await GetAsync("/chains", ct); + return r.Chains ?? new List(); + } + + public async Task GetChainAsync(ChainId chainId, CancellationToken ct = default) + => await GetAsync($"/chains/{chainId.ToString().ToLower()}", ct); + + public async Task IsChainSupportedAsync(ChainId chainId, CancellationToken ct = default) + { + try { var c = await GetChainAsync(chainId, ct); return c.Supported; } + catch { return false; } + } + + // Asset Operations + public async Task> GetSupportedAssetsAsync(ChainId chainId, CancellationToken ct = default) + { + var r = await GetAsync($"/chains/{chainId.ToString().ToLower()}/assets", ct); + return r.Assets ?? new List(); + } + + public async Task GetAssetAsync(string assetId, CancellationToken ct = default) + => await GetAsync($"/assets/{Uri.EscapeDataString(assetId)}", ct); + + public async Task GetWrappedAssetAsync(string originalAssetId, ChainId targetChain, CancellationToken ct = default) + => await GetAsync($"/assets/{Uri.EscapeDataString(originalAssetId)}/wrapped/{targetChain.ToString().ToLower()}", ct); + + // Fee Operations + public async Task EstimateFeeAsync(string asset, string amount, ChainId sourceChain, ChainId targetChain, CancellationToken ct = default) + => await PostAsync("/fees/estimate", new { asset, amount, sourceChain = sourceChain.ToString().ToLower(), targetChain = targetChain.ToString().ToLower() }, ct); + + public async Task GetExchangeRateAsync(string fromAsset, string toAsset, CancellationToken ct = default) + => await GetAsync($"/rates/{Uri.EscapeDataString(fromAsset)}/{Uri.EscapeDataString(toAsset)}", ct); + + // Lock-Mint Flow + public async Task LockAsync(string asset, string amount, ChainId targetChain, LockOptions? options = null, CancellationToken ct = default) + { + var body = new Dictionary { ["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("/transfers/lock", body, ct); + } + + public async Task GetLockProofAsync(string lockReceiptId, CancellationToken ct = default) + => await GetAsync($"/transfers/lock/{Uri.EscapeDataString(lockReceiptId)}/proof", ct); + + public async Task 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 MintAsync(LockProof proof, string targetAddress, MintOptions? options = null, CancellationToken ct = default) + { + var body = new Dictionary { ["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("/transfers/mint", body, ct); + } + + // Burn-Unlock Flow + public async Task BurnAsync(string wrappedAsset, string amount, BurnOptions? options = null, CancellationToken ct = default) + { + var body = new Dictionary { ["wrappedAsset"] = wrappedAsset, ["amount"] = amount }; + if (options?.Recipient != null) body["recipient"] = options.Recipient; + if (options?.Deadline != null) body["deadline"] = options.Deadline; + return await PostAsync("/transfers/burn", body, ct); + } + + public async Task GetBurnProofAsync(string burnReceiptId, CancellationToken ct = default) + => await GetAsync($"/transfers/burn/{Uri.EscapeDataString(burnReceiptId)}/proof", ct); + + public async Task 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 UnlockAsync(BurnProof proof, UnlockOptions? options = null, CancellationToken ct = default) + { + var body = new Dictionary { ["proof"] = proof }; + if (options?.GasLimit != null) body["gasLimit"] = options.GasLimit; + if (options?.GasPrice != null) body["gasPrice"] = options.GasPrice; + return await PostAsync("/transfers/unlock", body, ct); + } + + // Transfer Management + public async Task GetTransferAsync(string transferId, CancellationToken ct = default) + => await GetAsync($"/transfers/{Uri.EscapeDataString(transferId)}", ct); + + public async Task GetTransferStatusAsync(string transferId, CancellationToken ct = default) + { + var t = await GetTransferAsync(transferId, ct); + return t.Status; + } + + public async Task> ListTransfersAsync(TransferFilter? filter = null, CancellationToken ct = default) + { + var q = new List(); + 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(path, ct); + return r.Transfers ?? new List(); + } + + public async Task 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 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 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 HealthCheckAsync(CancellationToken ct = default) + { + try { var r = await GetAsync("/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 GetAsync(string path, CancellationToken ct) + => await ExecuteAsync(async () => { + var r = await _httpClient.GetAsync(path, ct); + await EnsureSuccessAsync(r); + return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; + }); + + private async Task PostAsync(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(_jsonOptions, ct)!; + }); + + private async Task ExecuteAsync(Func> 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>(c, _jsonOptions); + throw new BridgeException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}", + e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode); + } + } +} diff --git a/sdk/csharp/Synor.Sdk/Bridge/Types.cs b/sdk/csharp/Synor.Sdk/Bridge/Types.cs new file mode 100644 index 0000000..676b9aa --- /dev/null +++ b/sdk/csharp/Synor.Sdk/Bridge/Types.cs @@ -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 MerkleProof { get; init; } + public required string BlockHeader { get; init; } + public required List 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 MerkleProof { get; init; } + public required string BlockHeader { get; init; } + public required List 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? Chains { get; init; } } +internal record AssetsResponse { public List? Assets { get; init; } } +internal record TransfersResponse { public List? 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; } +} diff --git a/sdk/csharp/Synor.Sdk/Database/SynorDatabase.cs b/sdk/csharp/Synor.Sdk/Database/SynorDatabase.cs new file mode 100644 index 0000000..22add05 --- /dev/null +++ b/sdk/csharp/Synor.Sdk/Database/SynorDatabase.cs @@ -0,0 +1,335 @@ +using System.Net.Http.Json; +using System.Text.Json; +using System.Web; + +namespace Synor.Sdk.Database; + +/// +/// Synor Database SDK client for C#. +/// +/// Provides multi-model database with Key-Value, Document, Vector, and Time Series stores. +/// +/// +/// +/// 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(); +/// +/// +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); + } + + /// + /// Key-Value store operations. + /// + public class KeyValueStore + { + private readonly SynorDatabase _db; + + internal KeyValueStore(SynorDatabase db) => _db = db; + + public async Task GetAsync(string key, CancellationToken ct = default) + { + var response = await _db.GetAsync($"/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 { ["key"] = key, ["value"] = value }; + if (ttl.HasValue) body["ttl"] = ttl.Value; + await _db.PutAsync($"/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> ListAsync(string prefix, CancellationToken ct = default) + { + var response = await _db.GetAsync($"/kv?prefix={Uri.EscapeDataString(prefix)}", ct); + return response.Items ?? new List(); + } + } + + /// + /// Document store operations. + /// + public class DocumentStore + { + private readonly SynorDatabase _db; + + internal DocumentStore(SynorDatabase db) => _db = db; + + public async Task CreateAsync(string collection, object document, CancellationToken ct = default) + { + var response = await _db.PostAsync( + $"/collections/{Uri.EscapeDataString(collection)}/documents", document, ct); + return response.Id; + } + + public async Task GetAsync(string collection, string id, CancellationToken ct = default) + { + return await _db.GetAsync( + $"/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( + $"/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> QueryAsync(string collection, DocumentQuery query, CancellationToken ct = default) + { + var response = await _db.PostAsync( + $"/collections/{Uri.EscapeDataString(collection)}/query", query, ct); + return response.Documents ?? new List(); + } + } + + /// + /// Vector store operations. + /// + public class VectorStore + { + private readonly SynorDatabase _db; + + internal VectorStore(SynorDatabase db) => _db = db; + + public async Task UpsertAsync(string collection, IEnumerable vectors, CancellationToken ct = default) + { + await _db.PostAsync( + $"/vectors/{Uri.EscapeDataString(collection)}/upsert", + new { vectors = vectors }, ct); + } + + public async Task> SearchAsync(string collection, double[] vector, int k, CancellationToken ct = default) + { + var response = await _db.PostAsync( + $"/vectors/{Uri.EscapeDataString(collection)}/search", + new { vector, k }, ct); + return response.Results ?? new List(); + } + + public async Task DeleteAsync(string collection, IEnumerable ids, CancellationToken ct = default) + { + await _db.DeleteAsync($"/vectors/{Uri.EscapeDataString(collection)}", new { ids = ids }, ct); + } + } + + /// + /// Time series store operations. + /// + public class TimeSeriesStore + { + private readonly SynorDatabase _db; + + internal TimeSeriesStore(SynorDatabase db) => _db = db; + + public async Task WriteAsync(string series, IEnumerable points, CancellationToken ct = default) + { + await _db.PostAsync( + $"/timeseries/{Uri.EscapeDataString(series)}/write", + new { points = points }, ct); + } + + public async Task> QueryAsync( + string series, TimeRange range, Aggregation? aggregation = null, CancellationToken ct = default) + { + var body = new Dictionary { ["range"] = range }; + if (aggregation != null) body["aggregation"] = aggregation; + + var response = await _db.PostAsync( + $"/timeseries/{Uri.EscapeDataString(series)}/query", body, ct); + return response.Points ?? new List(); + } + } + + /// + /// Perform a health check. + /// + public async Task HealthCheckAsync(CancellationToken ct = default) + { + try + { + var response = await GetAsync("/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 GetAsync(string path, CancellationToken ct) + { + return await ExecuteWithRetryAsync(async () => + { + var response = await _httpClient.GetAsync(path, ct); + await EnsureSuccessAsync(response); + return await response.Content.ReadFromJsonAsync(_jsonOptions, ct) + ?? throw new DatabaseException("Failed to deserialize response"); + }); + } + + internal async Task PostAsync(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(content, _jsonOptions)!; + }); + } + + internal async Task PutAsync(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(content, _jsonOptions)!; + }); + } + + internal async Task PatchAsync(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(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 ExecuteWithRetryAsync(Func> 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>(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); + } + } +} diff --git a/sdk/csharp/Synor.Sdk/Database/Types.cs b/sdk/csharp/Synor.Sdk/Database/Types.cs new file mode 100644 index 0000000..e2b2089 --- /dev/null +++ b/sdk/csharp/Synor.Sdk/Database/Types.cs @@ -0,0 +1,152 @@ +using System.Text.Json.Serialization; + +namespace Synor.Sdk.Database; + +/// +/// Database configuration. +/// +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; +} + +/// +/// A key-value entry. +/// +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; } +} + +/// +/// A document. +/// +public record Document +{ + public required string Id { get; init; } + public required string Collection { get; init; } + public required Dictionary Data { get; init; } + public long CreatedAt { get; init; } + public long UpdatedAt { get; init; } +} + +/// +/// Query parameters for documents. +/// +public record DocumentQuery +{ + public Dictionary? Filter { get; init; } + public Dictionary? Sort { get; init; } + public int? Limit { get; init; } + public int? Offset { get; init; } + public List? Projection { get; init; } +} + +/// +/// A vector entry. +/// +public record VectorEntry +{ + public required string Id { get; init; } + public required double[] Vector { get; init; } + public Dictionary? Metadata { get; init; } +} + +/// +/// A search result. +/// +public record SearchResult +{ + public required string Id { get; init; } + public double Score { get; init; } + public double[]? Vector { get; init; } + public Dictionary? Metadata { get; init; } +} + +/// +/// A time series data point. +/// +public record DataPoint +{ + public long Timestamp { get; init; } + public double Value { get; init; } + public Dictionary? Tags { get; init; } +} + +/// +/// Time range for queries. +/// +public record TimeRange +{ + public long Start { get; init; } + public long End { get; init; } +} + +/// +/// Aggregation settings. +/// +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? Items { get; init; } +} + +internal record CreateDocumentResponse +{ + public required string Id { get; init; } +} + +internal record DocumentListResponse +{ + public List? Documents { get; init; } +} + +internal record SearchListResponse +{ + public List? Results { get; init; } +} + +internal record DataPointListResponse +{ + public List? Points { get; init; } +} + +internal record HealthResponse +{ + public required string Status { get; init; } +} + +/// +/// Database exception. +/// +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; + } +} diff --git a/sdk/csharp/Synor.Sdk/Hosting/SynorHosting.cs b/sdk/csharp/Synor.Sdk/Hosting/SynorHosting.cs new file mode 100644 index 0000000..ab21033 --- /dev/null +++ b/sdk/csharp/Synor.Sdk/Hosting/SynorHosting.cs @@ -0,0 +1,204 @@ +using System.Net.Http.Json; +using System.Text.Json; + +namespace Synor.Sdk.Hosting; + +/// +/// Synor Hosting SDK client for C#. +/// Provides decentralized web hosting with domain management, DNS, deployments, and SSL. +/// +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 CheckAvailabilityAsync(string name, CancellationToken ct = default) + => await GetAsync($"/domains/check/{Uri.EscapeDataString(name)}", ct); + + public async Task RegisterDomainAsync(string name, RegisterDomainOptions? options = null, CancellationToken ct = default) + { + var body = new Dictionary { ["name"] = name }; + if (options?.Years != null) body["years"] = options.Years; + if (options?.AutoRenew != null) body["autoRenew"] = options.AutoRenew; + return await PostAsync("/domains", body, ct); + } + + public async Task GetDomainAsync(string name, CancellationToken ct = default) + => await GetAsync($"/domains/{Uri.EscapeDataString(name)}", ct); + + public async Task> ListDomainsAsync(CancellationToken ct = default) + { + var response = await GetAsync("/domains", ct); + return response.Domains ?? new List(); + } + + public async Task ResolveDomainAsync(string name, CancellationToken ct = default) + => await GetAsync($"/domains/{Uri.EscapeDataString(name)}/resolve", ct); + + public async Task RenewDomainAsync(string name, int years, CancellationToken ct = default) + => await PostAsync($"/domains/{Uri.EscapeDataString(name)}/renew", new { years }, ct); + + // DNS Operations + public async Task GetDnsZoneAsync(string domain, CancellationToken ct = default) + => await GetAsync($"/dns/{Uri.EscapeDataString(domain)}", ct); + + public async Task SetDnsRecordsAsync(string domain, IEnumerable records, CancellationToken ct = default) + => await PutAsync($"/dns/{Uri.EscapeDataString(domain)}", new { records }, ct); + + public async Task AddDnsRecordAsync(string domain, DnsRecord record, CancellationToken ct = default) + => await PostAsync($"/dns/{Uri.EscapeDataString(domain)}/records", record, ct); + + // Deployment Operations + public async Task DeployAsync(string cid, DeployOptions? options = null, CancellationToken ct = default) + { + var body = new Dictionary { ["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("/deployments", body, ct); + } + + public async Task GetDeploymentAsync(string id, CancellationToken ct = default) + => await GetAsync($"/deployments/{Uri.EscapeDataString(id)}", ct); + + public async Task> ListDeploymentsAsync(string? domain = null, CancellationToken ct = default) + { + var path = domain != null ? $"/deployments?domain={Uri.EscapeDataString(domain)}" : "/deployments"; + var response = await GetAsync(path, ct); + return response.Deployments ?? new List(); + } + + public async Task RollbackAsync(string domain, string deploymentId, CancellationToken ct = default) + => await PostAsync($"/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 ProvisionSslAsync(string domain, ProvisionSslOptions? options = null, CancellationToken ct = default) + { + var body = options ?? new ProvisionSslOptions(); + return await PostAsync($"/ssl/{Uri.EscapeDataString(domain)}", body, ct); + } + + public async Task GetCertificateAsync(string domain, CancellationToken ct = default) + => await GetAsync($"/ssl/{Uri.EscapeDataString(domain)}", ct); + + public async Task RenewCertificateAsync(string domain, CancellationToken ct = default) + => await PostAsync($"/ssl/{Uri.EscapeDataString(domain)}/renew", new { }, ct); + + // Analytics + public async Task GetAnalyticsAsync(string domain, AnalyticsOptions? options = null, CancellationToken ct = default) + { + var path = $"/sites/{Uri.EscapeDataString(domain)}/analytics"; + var query = new List(); + 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(path, ct); + } + + public async Task PurgeCacheAsync(string domain, IEnumerable? paths = null, CancellationToken ct = default) + { + var body = paths != null ? new { paths } : null; + var response = await DeleteAsync($"/sites/{Uri.EscapeDataString(domain)}/cache", body, ct); + return response.Purged; + } + + // Lifecycle + public async Task HealthCheckAsync(CancellationToken ct = default) + { + try { var r = await GetAsync("/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 GetAsync(string path, CancellationToken ct) + => await ExecuteAsync(async () => { + var r = await _httpClient.GetAsync(path, ct); + await EnsureSuccessAsync(r); + return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; + }); + + private async Task PostAsync(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(_jsonOptions, ct)!; + }); + + private async Task PutAsync(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(_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 DeleteAsync(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(_jsonOptions, ct)!; + }); + + private async Task ExecuteAsync(Func> 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>(c, _jsonOptions); + throw new HostingException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}", + e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode); + } + } +} diff --git a/sdk/csharp/Synor.Sdk/Hosting/Types.cs b/sdk/csharp/Synor.Sdk/Hosting/Types.cs new file mode 100644 index 0000000..4a331a9 --- /dev/null +++ b/sdk/csharp/Synor.Sdk/Hosting/Types.cs @@ -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? Ipv4 { get; init; } + public List? Ipv6 { get; init; } + public string? Cname { get; init; } + public List? Txt { get; init; } + public Dictionary? 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 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? 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 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? Domains { get; init; } } +internal record DeploymentsResponse { public List? 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; + } +} diff --git a/sdk/ruby/lib/synor_bridge.rb b/sdk/ruby/lib/synor_bridge.rb new file mode 100644 index 0000000..31882c2 --- /dev/null +++ b/sdk/ruby/lib/synor_bridge.rb @@ -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 diff --git a/sdk/ruby/lib/synor_bridge/client.rb b/sdk/ruby/lib/synor_bridge/client.rb new file mode 100644 index 0000000..5e0fbe2 --- /dev/null +++ b/sdk/ruby/lib/synor_bridge/client.rb @@ -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 diff --git a/sdk/ruby/lib/synor_bridge/types.rb b/sdk/ruby/lib/synor_bridge/types.rb new file mode 100644 index 0000000..23cab8a --- /dev/null +++ b/sdk/ruby/lib/synor_bridge/types.rb @@ -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 diff --git a/sdk/ruby/lib/synor_bridge/version.rb b/sdk/ruby/lib/synor_bridge/version.rb new file mode 100644 index 0000000..354bb94 --- /dev/null +++ b/sdk/ruby/lib/synor_bridge/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module SynorBridge + VERSION = "0.1.0" +end diff --git a/sdk/ruby/lib/synor_database.rb b/sdk/ruby/lib/synor_database.rb new file mode 100644 index 0000000..bd45720 --- /dev/null +++ b/sdk/ruby/lib/synor_database.rb @@ -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 diff --git a/sdk/ruby/lib/synor_database/client.rb b/sdk/ruby/lib/synor_database/client.rb new file mode 100644 index 0000000..a21b42f --- /dev/null +++ b/sdk/ruby/lib/synor_database/client.rb @@ -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 diff --git a/sdk/ruby/lib/synor_database/types.rb b/sdk/ruby/lib/synor_database/types.rb new file mode 100644 index 0000000..1758e03 --- /dev/null +++ b/sdk/ruby/lib/synor_database/types.rb @@ -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 diff --git a/sdk/ruby/lib/synor_database/version.rb b/sdk/ruby/lib/synor_database/version.rb new file mode 100644 index 0000000..2aa6225 --- /dev/null +++ b/sdk/ruby/lib/synor_database/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module SynorDatabase + VERSION = "0.1.0" +end diff --git a/sdk/ruby/lib/synor_hosting.rb b/sdk/ruby/lib/synor_hosting.rb new file mode 100644 index 0000000..2b94615 --- /dev/null +++ b/sdk/ruby/lib/synor_hosting.rb @@ -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 diff --git a/sdk/ruby/lib/synor_hosting/client.rb b/sdk/ruby/lib/synor_hosting/client.rb new file mode 100644 index 0000000..7697ef8 --- /dev/null +++ b/sdk/ruby/lib/synor_hosting/client.rb @@ -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 diff --git a/sdk/ruby/lib/synor_hosting/types.rb b/sdk/ruby/lib/synor_hosting/types.rb new file mode 100644 index 0000000..e1b0685 --- /dev/null +++ b/sdk/ruby/lib/synor_hosting/types.rb @@ -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 diff --git a/sdk/ruby/lib/synor_hosting/version.rb b/sdk/ruby/lib/synor_hosting/version.rb new file mode 100644 index 0000000..de7c9b0 --- /dev/null +++ b/sdk/ruby/lib/synor_hosting/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module SynorHosting + VERSION = "0.1.0" +end diff --git a/sdk/swift/Sources/Synor/Bridge/SynorBridge.swift b/sdk/swift/Sources/Synor/Bridge/SynorBridge.swift new file mode 100644 index 0000000..1fc09d9 --- /dev/null +++ b/sdk/swift/Sources/Synor/Bridge/SynorBridge.swift @@ -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 = [.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(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(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( + 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(_ operation: () async throws -> T) async throws -> T { + var lastError: Error? + + for attempt in 0.. [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 + } +} diff --git a/sdk/swift/Sources/Synor/Bridge/Types.swift b/sdk/swift/Sources/Synor/Bridge/Types.swift new file mode 100644 index 0000000..d31bc19 --- /dev/null +++ b/sdk/swift/Sources/Synor/Bridge/Types.swift @@ -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 +} diff --git a/sdk/swift/Sources/Synor/Database/SynorDatabase.swift b/sdk/swift/Sources/Synor/Database/SynorDatabase.swift new file mode 100644 index 0000000..48c8e62 --- /dev/null +++ b/sdk/swift/Sources/Synor/Database/SynorDatabase.swift @@ -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(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(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( + 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(_ operation: () async throws -> T) async throws -> T { + var lastError: Error? + + for attempt in 0.. 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(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(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(path: String, body: R) async throws -> T { + return try await executeWithRetry { + try await self.performEncodableRequest(method: "POST", path: path, body: body) + } + } + + private func put(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(path: String, body: R) async throws -> T { + return try await executeWithRetry { + try await self.performEncodableRequest(method: "PUT", path: path, body: body) + } + } + + private func patch(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(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(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( + 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( + 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(_ 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(_ operation: () async throws -> T) async throws -> T { + var lastError: Error? + + for attempt in 0..