Add Synor Storage and Wallet SDKs for Swift

- Implement SynorStorage class for decentralized storage operations including upload, download, pinning, and CAR file management.
- Create supporting types and models for storage operations such as UploadOptions, Pin, and StorageConfig.
- Implement SynorWallet class for wallet operations including wallet creation, address generation, transaction signing, and balance queries.
- Create supporting types and models for wallet operations such as Wallet, Address, and Transaction.
- Introduce error handling for both storage and wallet operations.
This commit is contained in:
Gulshan Yadav 2026-01-27 01:56:45 +05:30
parent 59a7123535
commit 74b82d2bb2
113 changed files with 26722 additions and 531 deletions

View file

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16)
project(synor_compute VERSION 0.1.0 LANGUAGES C)
project(synor_sdk VERSION 0.1.0 LANGUAGES C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
@ -11,51 +11,90 @@ option(BUILD_EXAMPLES "Build examples" ON)
# Find dependencies
find_package(CURL REQUIRED)
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
pkg_check_modules(JANSSON jansson)
endif()
# If jansson not found via pkg-config, try find_library
if(NOT JANSSON_FOUND)
find_library(JANSSON_LIBRARIES jansson)
find_path(JANSSON_INCLUDE_DIRS jansson.h)
if(JANSSON_LIBRARIES AND JANSSON_INCLUDE_DIRS)
set(JANSSON_FOUND TRUE)
endif()
endif()
if(NOT JANSSON_FOUND)
message(WARNING "jansson library not found. Using bundled JSON parser.")
set(USE_BUNDLED_JSON TRUE)
endif()
# Library sources
set(SYNOR_SOURCES
src/synor_compute.c
src/common.c
src/wallet/wallet.c
src/rpc/rpc.c
src/storage/storage.c
)
# Create library
add_library(synor_compute ${SYNOR_SOURCES})
add_library(synor_sdk ${SYNOR_SOURCES})
target_include_directories(synor_compute
target_include_directories(synor_sdk
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
${JANSSON_INCLUDE_DIRS}
)
target_link_libraries(synor_compute
target_link_libraries(synor_sdk
PRIVATE
CURL::libcurl
m
)
if(JANSSON_FOUND)
target_link_libraries(synor_sdk PRIVATE ${JANSSON_LIBRARIES})
target_compile_definitions(synor_sdk PRIVATE HAVE_JANSSON)
endif()
# Platform-specific settings
if(WIN32)
target_link_libraries(synor_sdk PRIVATE ws2_32)
endif()
# Set library properties
set_target_properties(synor_compute PROPERTIES
set_target_properties(synor_sdk PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
PUBLIC_HEADER include/synor_compute.h
)
# Installation
include(GNUInstallDirs)
install(TARGETS synor_compute
EXPORT synor_compute-targets
install(TARGETS synor_sdk
EXPORT synor_sdk-targets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(EXPORT synor_compute-targets
FILE synor_compute-targets.cmake
NAMESPACE synor::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/synor_compute
install(DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(EXPORT synor_sdk-targets
FILE synor_sdk-targets.cmake
NAMESPACE synor::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/synor_sdk
)
# Alias for backwards compatibility
add_library(synor_compute ALIAS synor_sdk)
# Tests
if(BUILD_TESTS)
enable_testing()

269
sdk/c/include/synor/rpc.h Normal file
View file

@ -0,0 +1,269 @@
/**
* @file rpc.h
* @brief Synor RPC SDK for C
*
* Provides blockchain queries, transaction submission, and real-time
* subscriptions for the Synor blockchain.
*/
#ifndef SYNOR_RPC_H
#define SYNOR_RPC_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include "wallet.h" /* For synor_error_t, synor_network_t, synor_priority_t */
#ifdef __cplusplus
extern "C" {
#endif
/* Transaction status */
typedef enum {
SYNOR_TX_STATUS_PENDING = 0,
SYNOR_TX_STATUS_CONFIRMED,
SYNOR_TX_STATUS_FAILED
} synor_tx_status_t;
/* RPC configuration */
typedef struct {
const char* api_key;
const char* endpoint; /* Default: "https://rpc.synor.io/v1" */
const char* ws_endpoint; /* Default: "wss://rpc.synor.io/v1/ws" */
synor_network_t network; /* Default: SYNOR_NETWORK_MAINNET */
uint32_t timeout_ms; /* Default: 30000 */
uint32_t retries; /* Default: 3 */
bool debug; /* Default: false */
} synor_rpc_config_t;
/* Opaque RPC client handle */
typedef struct synor_rpc synor_rpc_t;
/* Block header */
typedef struct {
char* hash;
int64_t height;
char* previous_hash;
int64_t timestamp;
int32_t version;
char* merkle_root;
int64_t nonce;
double difficulty;
} synor_block_header_t;
/* Script pubkey */
typedef struct {
char* asm_str;
char* hex;
char* type;
char* address; /* May be NULL */
} synor_script_pubkey_t;
/* Transaction input */
typedef struct {
char* txid;
int32_t vout;
char* script_sig; /* May be NULL */
int64_t sequence;
char** witness;
size_t witness_count;
} synor_rpc_tx_input_t;
/* Transaction output */
typedef struct {
int64_t value;
int32_t n;
synor_script_pubkey_t script_pubkey;
} synor_rpc_tx_output_t;
/* Transaction */
typedef struct {
char* txid;
char* hash;
int32_t version;
int32_t size;
int32_t weight;
int64_t lock_time;
synor_rpc_tx_input_t* inputs;
size_t input_count;
synor_rpc_tx_output_t* outputs;
size_t output_count;
int64_t fee;
int32_t confirmations;
char* block_hash; /* May be NULL */
int64_t block_height; /* -1 if not in block */
int64_t timestamp; /* -1 if unknown */
synor_tx_status_t status;
} synor_rpc_transaction_t;
/* Block */
typedef struct {
char* hash;
int64_t height;
char* previous_hash;
int64_t timestamp;
int32_t version;
char* merkle_root;
int64_t nonce;
double difficulty;
synor_rpc_transaction_t* transactions;
size_t transaction_count;
int32_t size;
int32_t weight;
} synor_block_t;
/* Chain info */
typedef struct {
char* chain;
int64_t blocks;
int64_t headers;
char* best_block_hash;
double difficulty;
int64_t median_time;
double verification_progress;
char* chain_work;
bool pruned;
} synor_chain_info_t;
/* Mempool info */
typedef struct {
int32_t size;
int64_t bytes;
int64_t usage;
int64_t max_mempool;
double mempool_min_fee;
double min_relay_tx_fee;
} synor_mempool_info_t;
/* Fee estimate */
typedef struct {
synor_priority_t priority;
int64_t fee_rate;
int32_t estimated_blocks;
} synor_fee_estimate_t;
/* Submit result */
typedef struct {
char* txid;
bool accepted;
char* reason; /* May be NULL */
} synor_submit_result_t;
/* Subscription handle */
typedef struct synor_subscription synor_subscription_t;
/* Subscription callbacks */
typedef void (*synor_block_callback_t)(const synor_block_t* block, void* user_data);
typedef void (*synor_tx_callback_t)(const synor_rpc_transaction_t* tx, void* user_data);
/**
* Create a new RPC client.
*/
synor_rpc_t* synor_rpc_create(const synor_rpc_config_t* config);
/**
* Destroy RPC client and free resources.
*/
void synor_rpc_destroy(synor_rpc_t* rpc);
/* Block operations */
synor_error_t synor_rpc_get_latest_block(
synor_rpc_t* rpc,
synor_block_t* result
);
synor_error_t synor_rpc_get_block(
synor_rpc_t* rpc,
const char* hash_or_height,
synor_block_t* result
);
synor_error_t synor_rpc_get_block_header(
synor_rpc_t* rpc,
const char* hash_or_height,
synor_block_header_t* result
);
/* Transaction operations */
synor_error_t synor_rpc_get_transaction(
synor_rpc_t* rpc,
const char* txid,
synor_rpc_transaction_t* result
);
synor_error_t synor_rpc_get_raw_transaction(
synor_rpc_t* rpc,
const char* txid,
char** hex
);
synor_error_t synor_rpc_send_raw_transaction(
synor_rpc_t* rpc,
const char* hex,
synor_submit_result_t* result
);
synor_error_t synor_rpc_decode_raw_transaction(
synor_rpc_t* rpc,
const char* hex,
synor_rpc_transaction_t* result
);
/* Fee estimation */
synor_error_t synor_rpc_estimate_fee(
synor_rpc_t* rpc,
synor_priority_t priority,
synor_fee_estimate_t* result
);
/* Chain information */
synor_error_t synor_rpc_get_chain_info(
synor_rpc_t* rpc,
synor_chain_info_t* result
);
synor_error_t synor_rpc_get_mempool_info(
synor_rpc_t* rpc,
synor_mempool_info_t* result
);
/* Subscriptions */
synor_subscription_t* synor_rpc_subscribe_blocks(
synor_rpc_t* rpc,
synor_block_callback_t callback,
void* user_data
);
synor_subscription_t* synor_rpc_subscribe_address(
synor_rpc_t* rpc,
const char* address,
synor_tx_callback_t callback,
void* user_data
);
synor_subscription_t* synor_rpc_subscribe_mempool(
synor_rpc_t* rpc,
synor_tx_callback_t callback,
void* user_data
);
void synor_subscription_cancel(synor_subscription_t* sub);
/* Free functions */
void synor_block_header_free(synor_block_header_t* header);
void synor_block_free(synor_block_t* block);
void synor_rpc_transaction_free(synor_rpc_transaction_t* tx);
void synor_chain_info_free(synor_chain_info_t* info);
void synor_mempool_info_free(synor_mempool_info_t* info);
void synor_submit_result_free(synor_submit_result_t* result);
#ifdef __cplusplus
}
#endif
#endif /* SYNOR_RPC_H */

View file

@ -0,0 +1,282 @@
/**
* @file storage.h
* @brief Synor Storage SDK for C
*
* Provides IPFS-compatible decentralized storage with upload, download,
* pinning, and CAR file operations.
*/
#ifndef SYNOR_STORAGE_H
#define SYNOR_STORAGE_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include "wallet.h" /* For synor_error_t */
#ifdef __cplusplus
extern "C" {
#endif
/* Pin status */
typedef enum {
SYNOR_PIN_STATUS_QUEUED = 0,
SYNOR_PIN_STATUS_PINNING,
SYNOR_PIN_STATUS_PINNED,
SYNOR_PIN_STATUS_FAILED,
SYNOR_PIN_STATUS_UNPINNED
} synor_pin_status_t;
/* Hash algorithm */
typedef enum {
SYNOR_HASH_SHA2_256 = 0,
SYNOR_HASH_BLAKE3
} synor_hash_algorithm_t;
/* Entry type */
typedef enum {
SYNOR_ENTRY_FILE = 0,
SYNOR_ENTRY_DIRECTORY
} synor_entry_type_t;
/* Storage configuration */
typedef struct {
const char* api_key;
const char* endpoint; /* Default: "https://storage.synor.io/v1" */
const char* gateway; /* Default: "https://gateway.synor.io" */
uint32_t timeout_ms; /* Default: 60000 */
uint32_t retries; /* Default: 3 */
bool debug; /* Default: false */
} synor_storage_config_t;
/* Opaque storage client handle */
typedef struct synor_storage synor_storage_t;
/* Upload options */
typedef struct {
const char* name; /* May be NULL */
bool wrap_with_directory; /* Default: false */
synor_hash_algorithm_t hash_algorithm; /* Default: SHA2_256 */
int32_t chunk_size; /* 0 for default */
int32_t pin_duration_days; /* 0 for default */
} synor_upload_options_t;
/* Upload response */
typedef struct {
char* cid;
int64_t size;
char* name; /* May be NULL */
char* created_at;
} synor_upload_response_t;
/* Pin info */
typedef struct {
char* cid;
char* name; /* May be NULL */
synor_pin_status_t status;
int64_t size;
char* created_at;
char* expires_at; /* May be NULL */
char** delegates;
size_t delegate_count;
} synor_pin_t;
/* Pin request */
typedef struct {
const char* cid;
const char* name; /* May be NULL */
int32_t duration_days; /* 0 for default */
const char** origins;
size_t origin_count;
} synor_pin_request_t;
/* Pin list */
typedef struct {
synor_pin_t* pins;
size_t count;
} synor_pin_list_t;
/* CAR file info */
typedef struct {
char* cid;
int64_t size;
char** roots;
size_t root_count;
char* created_at;
} synor_car_file_t;
/* File entry for upload */
typedef struct {
const char* path;
const uint8_t* content;
size_t content_size;
} synor_file_entry_t;
/* Directory entry */
typedef struct {
char* name;
char* cid;
int64_t size;
synor_entry_type_t type;
} synor_directory_entry_t;
/* Directory listing */
typedef struct {
synor_directory_entry_t* entries;
size_t count;
} synor_directory_listing_t;
/* Storage statistics */
typedef struct {
int64_t total_size;
int32_t pin_count;
int64_t bandwidth_used;
double quota_used;
} synor_storage_stats_t;
/* CID list (for CAR import) */
typedef struct {
char** cids;
size_t count;
} synor_cid_list_t;
/* Download callback for streaming */
typedef void (*synor_download_callback_t)(
const uint8_t* data,
size_t size,
void* user_data
);
/**
* Create a new storage client.
*/
synor_storage_t* synor_storage_create(const synor_storage_config_t* config);
/**
* Destroy storage client and free resources.
*/
void synor_storage_destroy(synor_storage_t* storage);
/* Upload operations */
synor_error_t synor_storage_upload(
synor_storage_t* storage,
const uint8_t* data,
size_t size,
const synor_upload_options_t* options,
synor_upload_response_t* result
);
synor_error_t synor_storage_upload_directory(
synor_storage_t* storage,
const synor_file_entry_t* files,
size_t file_count,
const char* dir_name,
synor_upload_response_t* result
);
/* Download operations */
synor_error_t synor_storage_download(
synor_storage_t* storage,
const char* cid,
uint8_t** data,
size_t* size
);
synor_error_t synor_storage_download_stream(
synor_storage_t* storage,
const char* cid,
synor_download_callback_t callback,
void* user_data
);
/**
* Get gateway URL for content.
* Caller must free the returned string.
*/
char* synor_storage_get_gateway_url(
synor_storage_t* storage,
const char* cid,
const char* path /* May be NULL */
);
/* Pinning operations */
synor_error_t synor_storage_pin(
synor_storage_t* storage,
const synor_pin_request_t* request,
synor_pin_t* result
);
synor_error_t synor_storage_unpin(
synor_storage_t* storage,
const char* cid
);
synor_error_t synor_storage_get_pin_status(
synor_storage_t* storage,
const char* cid,
synor_pin_t* result
);
synor_error_t synor_storage_list_pins(
synor_storage_t* storage,
synor_pin_status_t* status, /* NULL for all */
int32_t limit,
int32_t offset,
synor_pin_list_t* result
);
/* CAR file operations */
synor_error_t synor_storage_create_car(
synor_storage_t* storage,
const synor_file_entry_t* entries,
size_t entry_count,
synor_car_file_t* result
);
synor_error_t synor_storage_import_car(
synor_storage_t* storage,
const uint8_t* car_data,
size_t car_size,
synor_cid_list_t* result
);
synor_error_t synor_storage_export_car(
synor_storage_t* storage,
const char* cid,
uint8_t** data,
size_t* size
);
/* Directory operations */
synor_error_t synor_storage_list_directory(
synor_storage_t* storage,
const char* cid,
synor_directory_listing_t* result
);
/* Statistics */
synor_error_t synor_storage_get_stats(
synor_storage_t* storage,
synor_storage_stats_t* result
);
/* Free functions */
void synor_upload_response_free(synor_upload_response_t* response);
void synor_pin_free(synor_pin_t* pin);
void synor_pin_list_free(synor_pin_list_t* list);
void synor_car_file_free(synor_car_file_t* car);
void synor_cid_list_free(synor_cid_list_t* list);
void synor_directory_entry_free(synor_directory_entry_t* entry);
void synor_directory_listing_free(synor_directory_listing_t* listing);
#ifdef __cplusplus
}
#endif
#endif /* SYNOR_STORAGE_H */

View file

@ -0,0 +1,399 @@
/**
* @file wallet.h
* @brief Synor Wallet SDK for C
*
* Provides key management, transaction signing, and balance queries
* for the Synor blockchain.
*
* @example
* ```c
* #include <synor/wallet.h>
*
* int main() {
* synor_wallet_config_t config = {
* .api_key = "your-api-key",
* .endpoint = "https://wallet.synor.io/v1",
* .network = SYNOR_NETWORK_MAINNET,
* .timeout_ms = 30000,
* .retries = 3
* };
*
* synor_wallet_t* wallet = synor_wallet_create(&config);
* if (!wallet) {
* fprintf(stderr, "Failed to create wallet client\n");
* return 1;
* }
*
* synor_create_wallet_result_t result;
* synor_error_t err = synor_wallet_create_wallet(wallet, SYNOR_WALLET_STANDARD, &result);
* if (err == SYNOR_OK) {
* printf("Wallet ID: %s\n", result.wallet.id);
* printf("Mnemonic: %s\n", result.mnemonic);
* synor_create_wallet_result_free(&result);
* }
*
* synor_wallet_destroy(wallet);
* return 0;
* }
* ```
*/
#ifndef SYNOR_WALLET_H
#define SYNOR_WALLET_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Error codes */
typedef enum {
SYNOR_OK = 0,
SYNOR_ERROR_INVALID_ARGUMENT,
SYNOR_ERROR_OUT_OF_MEMORY,
SYNOR_ERROR_NETWORK,
SYNOR_ERROR_HTTP,
SYNOR_ERROR_JSON_PARSE,
SYNOR_ERROR_TIMEOUT,
SYNOR_ERROR_AUTH,
SYNOR_ERROR_NOT_FOUND,
SYNOR_ERROR_UNKNOWN
} synor_error_t;
/* Network types */
typedef enum {
SYNOR_NETWORK_MAINNET = 0,
SYNOR_NETWORK_TESTNET,
SYNOR_NETWORK_DEVNET
} synor_network_t;
/* Wallet types */
typedef enum {
SYNOR_WALLET_STANDARD = 0,
SYNOR_WALLET_MULTISIG,
SYNOR_WALLET_HARDWARE,
SYNOR_WALLET_STEALTH
} synor_wallet_type_t;
/* Priority types */
typedef enum {
SYNOR_PRIORITY_LOW = 0,
SYNOR_PRIORITY_MEDIUM,
SYNOR_PRIORITY_HIGH
} synor_priority_t;
/* Configuration */
typedef struct {
const char* api_key;
const char* endpoint; /* Default: "https://wallet.synor.io/v1" */
synor_network_t network; /* Default: SYNOR_NETWORK_MAINNET */
uint32_t timeout_ms; /* Default: 30000 */
uint32_t retries; /* Default: 3 */
bool debug; /* Default: false */
} synor_wallet_config_t;
/* Opaque wallet client handle */
typedef struct synor_wallet synor_wallet_t;
/* Address structure */
typedef struct {
char* address;
int32_t index;
bool is_change;
} synor_address_t;
/* Wallet structure */
typedef struct {
char* id;
synor_wallet_type_t type;
synor_network_t network;
synor_address_t* addresses;
size_t address_count;
char* created_at;
} synor_wallet_info_t;
/* Stealth address */
typedef struct {
char* spend_public_key;
char* view_public_key;
char* one_time_address;
} synor_stealth_address_t;
/* Create wallet result */
typedef struct {
synor_wallet_info_t wallet;
char* mnemonic; /* May be NULL for hardware wallets */
} synor_create_wallet_result_t;
/* Transaction input */
typedef struct {
char* txid;
int32_t vout;
int64_t amount;
} synor_tx_input_t;
/* Transaction output */
typedef struct {
char* address;
int64_t amount;
} synor_tx_output_t;
/* Transaction for signing */
typedef struct {
synor_tx_input_t* inputs;
size_t input_count;
synor_tx_output_t* outputs;
size_t output_count;
int64_t fee; /* 0 for auto-calculate */
synor_priority_t priority;
} synor_transaction_t;
/* Signed transaction */
typedef struct {
char* txid;
char* hex;
int32_t size;
int64_t fee;
} synor_signed_tx_t;
/* Signature */
typedef struct {
char* signature;
char* address;
int32_t recovery_id; /* -1 if not applicable */
} synor_signature_t;
/* UTXO */
typedef struct {
char* txid;
int32_t vout;
int64_t amount;
char* address;
int32_t confirmations;
char* script_pubkey; /* May be NULL */
} synor_utxo_t;
/* Balance */
typedef struct {
int64_t confirmed;
int64_t unconfirmed;
int64_t total;
} synor_balance_t;
/* UTXO list */
typedef struct {
synor_utxo_t* utxos;
size_t count;
} synor_utxo_list_t;
/**
* Create a new wallet client.
*
* @param config Configuration options
* @return Wallet client handle, or NULL on failure
*/
synor_wallet_t* synor_wallet_create(const synor_wallet_config_t* config);
/**
* Destroy wallet client and free resources.
*
* @param wallet Wallet client handle
*/
void synor_wallet_destroy(synor_wallet_t* wallet);
/**
* Create a new wallet.
*
* @param wallet Wallet client handle
* @param type Type of wallet to create
* @param result Output parameter for result
* @return Error code
*/
synor_error_t synor_wallet_create_wallet(
synor_wallet_t* wallet,
synor_wallet_type_t type,
synor_create_wallet_result_t* result
);
/**
* Import a wallet from mnemonic.
*
* @param wallet Wallet client handle
* @param mnemonic 12 or 24 word mnemonic phrase
* @param passphrase Optional BIP39 passphrase (can be NULL)
* @param result Output parameter for wallet info
* @return Error code
*/
synor_error_t synor_wallet_import(
synor_wallet_t* wallet,
const char* mnemonic,
const char* passphrase,
synor_wallet_info_t* result
);
/**
* Get a wallet by ID.
*
* @param wallet Wallet client handle
* @param wallet_id Wallet ID
* @param result Output parameter for wallet info
* @return Error code
*/
synor_error_t synor_wallet_get(
synor_wallet_t* wallet,
const char* wallet_id,
synor_wallet_info_t* result
);
/**
* Generate a new address for a wallet.
*
* @param wallet Wallet client handle
* @param wallet_id Wallet ID
* @param is_change Whether this is a change address
* @param result Output parameter for address
* @return Error code
*/
synor_error_t synor_wallet_generate_address(
synor_wallet_t* wallet,
const char* wallet_id,
bool is_change,
synor_address_t* result
);
/**
* Get a stealth address for privacy transactions.
*
* @param wallet Wallet client handle
* @param wallet_id Wallet ID
* @param result Output parameter for stealth address
* @return Error code
*/
synor_error_t synor_wallet_get_stealth_address(
synor_wallet_t* wallet,
const char* wallet_id,
synor_stealth_address_t* result
);
/**
* Sign a transaction.
*
* @param wallet Wallet client handle
* @param wallet_id Wallet ID to sign with
* @param transaction Transaction to sign
* @param result Output parameter for signed transaction
* @return Error code
*/
synor_error_t synor_wallet_sign_transaction(
synor_wallet_t* wallet,
const char* wallet_id,
const synor_transaction_t* transaction,
synor_signed_tx_t* result
);
/**
* Sign a message.
*
* @param wallet Wallet client handle
* @param wallet_id Wallet ID
* @param message Message to sign
* @param address_index Address index to use for signing
* @param result Output parameter for signature
* @return Error code
*/
synor_error_t synor_wallet_sign_message(
synor_wallet_t* wallet,
const char* wallet_id,
const char* message,
int32_t address_index,
synor_signature_t* result
);
/**
* Verify a message signature.
*
* @param wallet Wallet client handle
* @param message Original message
* @param signature Signature to verify
* @param address Address that signed the message
* @param is_valid Output parameter for validity
* @return Error code
*/
synor_error_t synor_wallet_verify_message(
synor_wallet_t* wallet,
const char* message,
const char* signature,
const char* address,
bool* is_valid
);
/**
* Get balance for an address.
*
* @param wallet Wallet client handle
* @param address Address to query
* @param result Output parameter for balance
* @return Error code
*/
synor_error_t synor_wallet_get_balance(
synor_wallet_t* wallet,
const char* address,
synor_balance_t* result
);
/**
* Get UTXOs for an address.
*
* @param wallet Wallet client handle
* @param address Address to query
* @param min_confirmations Minimum confirmations required
* @param result Output parameter for UTXO list
* @return Error code
*/
synor_error_t synor_wallet_get_utxos(
synor_wallet_t* wallet,
const char* address,
int32_t min_confirmations,
synor_utxo_list_t* result
);
/**
* Estimate transaction fee.
*
* @param wallet Wallet client handle
* @param priority Transaction priority
* @param fee_per_byte Output parameter for fee per byte
* @return Error code
*/
synor_error_t synor_wallet_estimate_fee(
synor_wallet_t* wallet,
synor_priority_t priority,
int64_t* fee_per_byte
);
/* Free functions */
void synor_address_free(synor_address_t* addr);
void synor_wallet_info_free(synor_wallet_info_t* info);
void synor_stealth_address_free(synor_stealth_address_t* addr);
void synor_create_wallet_result_free(synor_create_wallet_result_t* result);
void synor_signed_tx_free(synor_signed_tx_t* tx);
void synor_signature_free(synor_signature_t* sig);
void synor_utxo_free(synor_utxo_t* utxo);
void synor_utxo_list_free(synor_utxo_list_t* list);
/**
* Get error message for error code.
*
* @param error Error code
* @return Static string describing the error
*/
const char* synor_error_message(synor_error_t error);
#ifdef __cplusplus
}
#endif
#endif /* SYNOR_WALLET_H */

249
sdk/c/src/common.c Normal file
View file

@ -0,0 +1,249 @@
/**
* @file common.c
* @brief Common utilities for Synor C SDK
*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <curl/curl.h>
#include "synor/wallet.h"
/* Error messages */
static const char* error_messages[] = {
"OK",
"Invalid argument",
"Out of memory",
"Network error",
"HTTP error",
"JSON parse error",
"Timeout",
"Authentication error",
"Not found",
"Unknown error"
};
const char* synor_error_message(synor_error_t error) {
if (error >= 0 && error <= SYNOR_ERROR_UNKNOWN) {
return error_messages[error];
}
return "Unknown error code";
}
/* HTTP response buffer */
typedef struct {
char* data;
size_t size;
} http_response_t;
/* CURL write callback */
static size_t write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
size_t real_size = size * nmemb;
http_response_t* resp = (http_response_t*)userp;
char* ptr = realloc(resp->data, resp->size + real_size + 1);
if (!ptr) {
return 0;
}
resp->data = ptr;
memcpy(&(resp->data[resp->size]), contents, real_size);
resp->size += real_size;
resp->data[resp->size] = 0;
return real_size;
}
/* Internal HTTP client structure */
typedef struct {
CURL* curl;
char* api_key;
char* endpoint;
uint32_t timeout_ms;
uint32_t retries;
bool debug;
} 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
) {
synor_http_client_t* client = calloc(1, sizeof(synor_http_client_t));
if (!client) return NULL;
client->curl = curl_easy_init();
if (!client->curl) {
free(client);
return NULL;
}
client->api_key = strdup(api_key);
client->endpoint = strdup(endpoint);
client->timeout_ms = timeout_ms;
client->retries = retries;
client->debug = debug;
return client;
}
void synor_http_client_destroy(synor_http_client_t* client) {
if (!client) return;
if (client->curl) {
curl_easy_cleanup(client->curl);
}
free(client->api_key);
free(client->endpoint);
free(client);
}
synor_error_t synor_http_get(
synor_http_client_t* client,
const char* path,
char** response,
size_t* response_size
) {
if (!client || !path || !response) {
return SYNOR_ERROR_INVALID_ARGUMENT;
}
char url[2048];
snprintf(url, sizeof(url), "%s%s", client->endpoint, path);
http_response_t resp = {0};
struct curl_slist* headers = NULL;
char auth_header[512];
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", client->api_key);
headers = curl_slist_append(headers, auth_header);
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(client->curl, CURLOPT_URL, url);
curl_easy_setopt(client->curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(client->curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(client->curl, CURLOPT_WRITEDATA, &resp);
curl_easy_setopt(client->curl, CURLOPT_TIMEOUT_MS, client->timeout_ms);
synor_error_t result = SYNOR_OK;
for (uint32_t attempt = 0; attempt < client->retries; attempt++) {
CURLcode res = curl_easy_perform(client->curl);
if (res == CURLE_OK) {
long http_code = 0;
curl_easy_getinfo(client->curl, CURLINFO_RESPONSE_CODE, &http_code);
if (http_code >= 200 && http_code < 300) {
*response = resp.data;
if (response_size) {
*response_size = resp.size;
}
curl_slist_free_all(headers);
return SYNOR_OK;
} else if (http_code == 401) {
result = SYNOR_ERROR_AUTH;
} else if (http_code == 404) {
result = SYNOR_ERROR_NOT_FOUND;
} else {
result = SYNOR_ERROR_HTTP;
}
} else if (res == CURLE_OPERATION_TIMEDOUT) {
result = SYNOR_ERROR_TIMEOUT;
} else {
result = SYNOR_ERROR_NETWORK;
}
if (client->debug) {
fprintf(stderr, "HTTP attempt %u failed: %s\n",
attempt + 1, curl_easy_strerror(res));
}
free(resp.data);
resp.data = NULL;
resp.size = 0;
}
curl_slist_free_all(headers);
return result;
}
synor_error_t synor_http_post(
synor_http_client_t* client,
const char* path,
const char* body,
char** response,
size_t* response_size
) {
if (!client || !path || !body || !response) {
return SYNOR_ERROR_INVALID_ARGUMENT;
}
char url[2048];
snprintf(url, sizeof(url), "%s%s", client->endpoint, path);
http_response_t resp = {0};
struct curl_slist* headers = NULL;
char auth_header[512];
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", client->api_key);
headers = curl_slist_append(headers, auth_header);
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(client->curl, CURLOPT_URL, url);
curl_easy_setopt(client->curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(client->curl, CURLOPT_POSTFIELDS, body);
curl_easy_setopt(client->curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(client->curl, CURLOPT_WRITEDATA, &resp);
curl_easy_setopt(client->curl, CURLOPT_TIMEOUT_MS, client->timeout_ms);
synor_error_t result = SYNOR_OK;
for (uint32_t attempt = 0; attempt < client->retries; attempt++) {
CURLcode res = curl_easy_perform(client->curl);
if (res == CURLE_OK) {
long http_code = 0;
curl_easy_getinfo(client->curl, CURLINFO_RESPONSE_CODE, &http_code);
if (http_code >= 200 && http_code < 300) {
*response = resp.data;
if (response_size) {
*response_size = resp.size;
}
curl_slist_free_all(headers);
return SYNOR_OK;
} else if (http_code == 401) {
result = SYNOR_ERROR_AUTH;
} else if (http_code == 404) {
result = SYNOR_ERROR_NOT_FOUND;
} else {
result = SYNOR_ERROR_HTTP;
}
} else if (res == CURLE_OPERATION_TIMEDOUT) {
result = SYNOR_ERROR_TIMEOUT;
} else {
result = SYNOR_ERROR_NETWORK;
}
if (client->debug) {
fprintf(stderr, "HTTP attempt %u failed: %s\n",
attempt + 1, curl_easy_strerror(res));
}
free(resp.data);
resp.data = NULL;
resp.size = 0;
}
curl_slist_free_all(headers);
return result;
}
/* String duplication helper */
char* synor_strdup(const char* s) {
if (!s) return NULL;
return strdup(s);
}

221
sdk/c/src/rpc/rpc.c Normal file
View file

@ -0,0 +1,221 @@
/**
* @file rpc.c
* @brief Synor RPC SDK implementation
*/
#include <stdlib.h>
#include <string.h>
#include "synor/rpc.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);
/* Internal RPC structure */
struct synor_rpc {
synor_http_client_t* http;
synor_network_t network;
char* ws_endpoint;
/* WebSocket connection would go here */
};
/* Internal subscription structure */
struct synor_subscription {
char* id;
char* channel;
void* callback;
void* user_data;
struct synor_rpc* rpc;
};
synor_rpc_t* synor_rpc_create(const synor_rpc_config_t* config) {
if (!config || !config->api_key) {
return NULL;
}
synor_rpc_t* rpc = calloc(1, sizeof(synor_rpc_t));
if (!rpc) return NULL;
const char* endpoint = config->endpoint ?
config->endpoint : "https://rpc.synor.io/v1";
const char* ws_endpoint = config->ws_endpoint ?
config->ws_endpoint : "wss://rpc.synor.io/v1/ws";
uint32_t timeout = config->timeout_ms > 0 ? config->timeout_ms : 30000;
uint32_t retries = config->retries > 0 ? config->retries : 3;
rpc->http = synor_http_client_create(
config->api_key, endpoint, timeout, retries, config->debug);
if (!rpc->http) {
free(rpc);
return NULL;
}
rpc->network = config->network;
rpc->ws_endpoint = synor_strdup(ws_endpoint);
return rpc;
}
void synor_rpc_destroy(synor_rpc_t* rpc) {
if (!rpc) return;
synor_http_client_destroy(rpc->http);
free(rpc->ws_endpoint);
free(rpc);
}
/* Stub implementations - TODO: Implement with JSON parsing */
synor_error_t synor_rpc_get_latest_block(synor_rpc_t* rpc, synor_block_t* result) {
(void)rpc; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_rpc_get_block(synor_rpc_t* rpc, const char* hash_or_height,
synor_block_t* result) {
(void)rpc; (void)hash_or_height; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_rpc_get_block_header(synor_rpc_t* rpc, const char* hash_or_height,
synor_block_header_t* result) {
(void)rpc; (void)hash_or_height; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_rpc_get_transaction(synor_rpc_t* rpc, const char* txid,
synor_rpc_transaction_t* result) {
(void)rpc; (void)txid; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_rpc_get_raw_transaction(synor_rpc_t* rpc, const char* txid,
char** hex) {
(void)rpc; (void)txid; (void)hex;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_rpc_send_raw_transaction(synor_rpc_t* rpc, const char* hex,
synor_submit_result_t* result) {
(void)rpc; (void)hex; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_rpc_decode_raw_transaction(synor_rpc_t* rpc, const char* hex,
synor_rpc_transaction_t* result) {
(void)rpc; (void)hex; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_rpc_estimate_fee(synor_rpc_t* rpc, synor_priority_t priority,
synor_fee_estimate_t* result) {
(void)rpc; (void)priority; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_rpc_get_chain_info(synor_rpc_t* rpc, synor_chain_info_t* result) {
(void)rpc; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_rpc_get_mempool_info(synor_rpc_t* rpc, synor_mempool_info_t* result) {
(void)rpc; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_subscription_t* synor_rpc_subscribe_blocks(synor_rpc_t* rpc,
synor_block_callback_t callback, void* user_data) {
(void)rpc; (void)callback; (void)user_data;
return NULL; /* TODO: Implement WebSocket */
}
synor_subscription_t* synor_rpc_subscribe_address(synor_rpc_t* rpc,
const char* address, synor_tx_callback_t callback, void* user_data) {
(void)rpc; (void)address; (void)callback; (void)user_data;
return NULL;
}
synor_subscription_t* synor_rpc_subscribe_mempool(synor_rpc_t* rpc,
synor_tx_callback_t callback, void* user_data) {
(void)rpc; (void)callback; (void)user_data;
return NULL;
}
void synor_subscription_cancel(synor_subscription_t* sub) {
if (!sub) return;
free(sub->id);
free(sub->channel);
free(sub);
}
/* Free functions */
void synor_block_header_free(synor_block_header_t* header) {
if (!header) return;
free(header->hash);
free(header->previous_hash);
free(header->merkle_root);
memset(header, 0, sizeof(*header));
}
void synor_block_free(synor_block_t* block) {
if (!block) return;
free(block->hash);
free(block->previous_hash);
free(block->merkle_root);
for (size_t i = 0; i < block->transaction_count; i++) {
synor_rpc_transaction_free(&block->transactions[i]);
}
free(block->transactions);
memset(block, 0, sizeof(*block));
}
void synor_rpc_transaction_free(synor_rpc_transaction_t* tx) {
if (!tx) return;
free(tx->txid);
free(tx->hash);
free(tx->block_hash);
for (size_t i = 0; i < tx->input_count; i++) {
free(tx->inputs[i].txid);
free(tx->inputs[i].script_sig);
for (size_t j = 0; j < tx->inputs[i].witness_count; j++) {
free(tx->inputs[i].witness[j]);
}
free(tx->inputs[i].witness);
}
free(tx->inputs);
for (size_t i = 0; i < tx->output_count; i++) {
free(tx->outputs[i].script_pubkey.asm_str);
free(tx->outputs[i].script_pubkey.hex);
free(tx->outputs[i].script_pubkey.type);
free(tx->outputs[i].script_pubkey.address);
}
free(tx->outputs);
memset(tx, 0, sizeof(*tx));
}
void synor_chain_info_free(synor_chain_info_t* info) {
if (!info) return;
free(info->chain);
free(info->best_block_hash);
free(info->chain_work);
memset(info, 0, sizeof(*info));
}
void synor_mempool_info_free(synor_mempool_info_t* info) {
(void)info; /* No dynamic members */
}
void synor_submit_result_free(synor_submit_result_t* result) {
if (!result) return;
free(result->txid);
free(result->reason);
memset(result, 0, sizeof(*result));
}

234
sdk/c/src/storage/storage.c Normal file
View file

@ -0,0 +1,234 @@
/**
* @file storage.c
* @brief Synor Storage SDK implementation
*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "synor/storage.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);
/* Internal storage structure */
struct synor_storage {
synor_http_client_t* http;
char* gateway;
};
synor_storage_t* synor_storage_create(const synor_storage_config_t* config) {
if (!config || !config->api_key) {
return NULL;
}
synor_storage_t* storage = calloc(1, sizeof(synor_storage_t));
if (!storage) return NULL;
const char* endpoint = config->endpoint ?
config->endpoint : "https://storage.synor.io/v1";
const char* gateway = config->gateway ?
config->gateway : "https://gateway.synor.io";
uint32_t timeout = config->timeout_ms > 0 ? config->timeout_ms : 60000;
uint32_t retries = config->retries > 0 ? config->retries : 3;
storage->http = synor_http_client_create(
config->api_key, endpoint, timeout, retries, config->debug);
if (!storage->http) {
free(storage);
return NULL;
}
storage->gateway = synor_strdup(gateway);
return storage;
}
void synor_storage_destroy(synor_storage_t* storage) {
if (!storage) return;
synor_http_client_destroy(storage->http);
free(storage->gateway);
free(storage);
}
char* synor_storage_get_gateway_url(synor_storage_t* storage,
const char* cid, const char* path) {
if (!storage || !cid) return NULL;
size_t len = strlen(storage->gateway) + strlen("/ipfs/") + strlen(cid);
if (path) {
len += 1 + strlen(path);
}
len += 1; /* null terminator */
char* url = malloc(len);
if (!url) return NULL;
if (path) {
snprintf(url, len, "%s/ipfs/%s/%s", storage->gateway, cid, path);
} else {
snprintf(url, len, "%s/ipfs/%s", storage->gateway, cid);
}
return url;
}
/* Stub implementations - TODO: Implement with multipart upload */
synor_error_t synor_storage_upload(synor_storage_t* storage,
const uint8_t* data, size_t size,
const synor_upload_options_t* options, synor_upload_response_t* result) {
(void)storage; (void)data; (void)size; (void)options; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_storage_upload_directory(synor_storage_t* storage,
const synor_file_entry_t* files, size_t file_count,
const char* dir_name, synor_upload_response_t* result) {
(void)storage; (void)files; (void)file_count; (void)dir_name; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_storage_download(synor_storage_t* storage,
const char* cid, uint8_t** data, size_t* size) {
(void)storage; (void)cid; (void)data; (void)size;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_storage_download_stream(synor_storage_t* storage,
const char* cid, synor_download_callback_t callback, void* user_data) {
(void)storage; (void)cid; (void)callback; (void)user_data;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_storage_pin(synor_storage_t* storage,
const synor_pin_request_t* request, synor_pin_t* result) {
(void)storage; (void)request; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_storage_unpin(synor_storage_t* storage, const char* cid) {
(void)storage; (void)cid;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_storage_get_pin_status(synor_storage_t* storage,
const char* cid, synor_pin_t* result) {
(void)storage; (void)cid; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_storage_list_pins(synor_storage_t* storage,
synor_pin_status_t* status, int32_t limit, int32_t offset,
synor_pin_list_t* result) {
(void)storage; (void)status; (void)limit; (void)offset; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_storage_create_car(synor_storage_t* storage,
const synor_file_entry_t* entries, size_t entry_count,
synor_car_file_t* result) {
(void)storage; (void)entries; (void)entry_count; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_storage_import_car(synor_storage_t* storage,
const uint8_t* car_data, size_t car_size, synor_cid_list_t* result) {
(void)storage; (void)car_data; (void)car_size; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_storage_export_car(synor_storage_t* storage,
const char* cid, uint8_t** data, size_t* size) {
(void)storage; (void)cid; (void)data; (void)size;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_storage_list_directory(synor_storage_t* storage,
const char* cid, synor_directory_listing_t* result) {
(void)storage; (void)cid; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_storage_get_stats(synor_storage_t* storage,
synor_storage_stats_t* result) {
(void)storage; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
/* Free functions */
void synor_upload_response_free(synor_upload_response_t* response) {
if (!response) return;
free(response->cid);
free(response->name);
free(response->created_at);
memset(response, 0, sizeof(*response));
}
void synor_pin_free(synor_pin_t* pin) {
if (!pin) return;
free(pin->cid);
free(pin->name);
free(pin->created_at);
free(pin->expires_at);
for (size_t i = 0; i < pin->delegate_count; i++) {
free(pin->delegates[i]);
}
free(pin->delegates);
memset(pin, 0, sizeof(*pin));
}
void synor_pin_list_free(synor_pin_list_t* list) {
if (!list) return;
for (size_t i = 0; i < list->count; i++) {
synor_pin_free(&list->pins[i]);
}
free(list->pins);
memset(list, 0, sizeof(*list));
}
void synor_car_file_free(synor_car_file_t* car) {
if (!car) return;
free(car->cid);
for (size_t i = 0; i < car->root_count; i++) {
free(car->roots[i]);
}
free(car->roots);
free(car->created_at);
memset(car, 0, sizeof(*car));
}
void synor_cid_list_free(synor_cid_list_t* list) {
if (!list) return;
for (size_t i = 0; i < list->count; i++) {
free(list->cids[i]);
}
free(list->cids);
memset(list, 0, sizeof(*list));
}
void synor_directory_entry_free(synor_directory_entry_t* entry) {
if (!entry) return;
free(entry->name);
free(entry->cid);
memset(entry, 0, sizeof(*entry));
}
void synor_directory_listing_free(synor_directory_listing_t* listing) {
if (!listing) return;
for (size_t i = 0; i < listing->count; i++) {
synor_directory_entry_free(&listing->entries[i]);
}
free(listing->entries);
memset(listing, 0, sizeof(*listing));
}

355
sdk/c/src/wallet/wallet.c Normal file
View file

@ -0,0 +1,355 @@
/**
* @file wallet.c
* @brief Synor Wallet SDK implementation
*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "synor/wallet.h"
#ifdef HAVE_JANSSON
#include <jansson.h>
#endif
/* 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);
/* Internal wallet structure */
struct synor_wallet {
synor_http_client_t* http;
synor_network_t network;
};
synor_wallet_t* synor_wallet_create(const synor_wallet_config_t* config) {
if (!config || !config->api_key) {
return NULL;
}
synor_wallet_t* wallet = calloc(1, sizeof(synor_wallet_t));
if (!wallet) return NULL;
const char* endpoint = config->endpoint ?
config->endpoint : "https://wallet.synor.io/v1";
uint32_t timeout = config->timeout_ms > 0 ? config->timeout_ms : 30000;
uint32_t retries = config->retries > 0 ? config->retries : 3;
wallet->http = synor_http_client_create(
config->api_key, endpoint, timeout, retries, config->debug);
if (!wallet->http) {
free(wallet);
return NULL;
}
wallet->network = config->network;
return wallet;
}
void synor_wallet_destroy(synor_wallet_t* wallet) {
if (!wallet) return;
synor_http_client_destroy(wallet->http);
free(wallet);
}
#ifdef HAVE_JANSSON
static synor_error_t parse_wallet_info(json_t* json, synor_wallet_info_t* info) {
if (!json || !info) return SYNOR_ERROR_INVALID_ARGUMENT;
memset(info, 0, sizeof(*info));
json_t* id = json_object_get(json, "id");
if (id) info->id = synor_strdup(json_string_value(id));
json_t* type = json_object_get(json, "type");
if (type) {
const char* type_str = json_string_value(type);
if (strcmp(type_str, "standard") == 0) info->type = SYNOR_WALLET_STANDARD;
else if (strcmp(type_str, "multisig") == 0) info->type = SYNOR_WALLET_MULTISIG;
else if (strcmp(type_str, "hardware") == 0) info->type = SYNOR_WALLET_HARDWARE;
else if (strcmp(type_str, "stealth") == 0) info->type = SYNOR_WALLET_STEALTH;
}
json_t* network = json_object_get(json, "network");
if (network) {
const char* net_str = json_string_value(network);
if (strcmp(net_str, "mainnet") == 0) info->network = SYNOR_NETWORK_MAINNET;
else if (strcmp(net_str, "testnet") == 0) info->network = SYNOR_NETWORK_TESTNET;
else if (strcmp(net_str, "devnet") == 0) info->network = SYNOR_NETWORK_DEVNET;
}
json_t* created_at = json_object_get(json, "created_at");
if (created_at) info->created_at = synor_strdup(json_string_value(created_at));
json_t* addresses = json_object_get(json, "addresses");
if (addresses && json_is_array(addresses)) {
size_t count = json_array_size(addresses);
info->addresses = calloc(count, sizeof(synor_address_t));
info->address_count = count;
for (size_t i = 0; i < count; i++) {
json_t* addr = json_array_get(addresses, i);
info->addresses[i].address = synor_strdup(
json_string_value(json_object_get(addr, "address")));
info->addresses[i].index = (int32_t)json_integer_value(
json_object_get(addr, "index"));
info->addresses[i].is_change = json_boolean_value(
json_object_get(addr, "is_change"));
}
}
return SYNOR_OK;
}
synor_error_t synor_wallet_create_wallet(
synor_wallet_t* wallet,
synor_wallet_type_t type,
synor_create_wallet_result_t* result
) {
if (!wallet || !result) return SYNOR_ERROR_INVALID_ARGUMENT;
const char* type_str;
switch (type) {
case SYNOR_WALLET_STANDARD: type_str = "standard"; break;
case SYNOR_WALLET_MULTISIG: type_str = "multisig"; break;
case SYNOR_WALLET_HARDWARE: type_str = "hardware"; break;
case SYNOR_WALLET_STEALTH: type_str = "stealth"; break;
default: return SYNOR_ERROR_INVALID_ARGUMENT;
}
const char* net_str;
switch (wallet->network) {
case SYNOR_NETWORK_MAINNET: net_str = "mainnet"; break;
case SYNOR_NETWORK_TESTNET: net_str = "testnet"; break;
case SYNOR_NETWORK_DEVNET: net_str = "devnet"; break;
default: net_str = "mainnet"; break;
}
char body[256];
snprintf(body, sizeof(body),
"{\"type\":\"%s\",\"network\":\"%s\"}", type_str, net_str);
char* response = NULL;
synor_error_t err = synor_http_post(wallet->http, "/wallets", body,
&response, NULL);
if (err != SYNOR_OK) return err;
json_error_t json_err;
json_t* json = json_loads(response, 0, &json_err);
free(response);
if (!json) return SYNOR_ERROR_JSON_PARSE;
memset(result, 0, sizeof(*result));
json_t* wallet_json = json_object_get(json, "wallet");
if (wallet_json) {
parse_wallet_info(wallet_json, &result->wallet);
}
json_t* mnemonic = json_object_get(json, "mnemonic");
if (mnemonic) {
result->mnemonic = synor_strdup(json_string_value(mnemonic));
}
json_decref(json);
return SYNOR_OK;
}
synor_error_t synor_wallet_get_balance(
synor_wallet_t* wallet,
const char* address,
synor_balance_t* result
) {
if (!wallet || !address || !result) return SYNOR_ERROR_INVALID_ARGUMENT;
char path[512];
snprintf(path, sizeof(path), "/addresses/%s/balance", address);
char* response = NULL;
synor_error_t err = synor_http_get(wallet->http, path, &response, NULL);
if (err != SYNOR_OK) return err;
json_error_t json_err;
json_t* json = json_loads(response, 0, &json_err);
free(response);
if (!json) return SYNOR_ERROR_JSON_PARSE;
result->confirmed = json_integer_value(json_object_get(json, "confirmed"));
result->unconfirmed = json_integer_value(json_object_get(json, "unconfirmed"));
result->total = json_integer_value(json_object_get(json, "total"));
json_decref(json);
return SYNOR_OK;
}
#else /* No JANSSON */
synor_error_t synor_wallet_create_wallet(
synor_wallet_t* wallet,
synor_wallet_type_t type,
synor_create_wallet_result_t* result
) {
(void)wallet; (void)type; (void)result;
return SYNOR_ERROR_UNKNOWN; /* JSON parsing not available */
}
synor_error_t synor_wallet_get_balance(
synor_wallet_t* wallet,
const char* address,
synor_balance_t* result
) {
(void)wallet; (void)address; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
#endif /* HAVE_JANSSON */
/* Stub implementations for remaining functions */
synor_error_t synor_wallet_import(
synor_wallet_t* wallet, const char* mnemonic,
const char* passphrase, synor_wallet_info_t* result
) {
(void)wallet; (void)mnemonic; (void)passphrase; (void)result;
return SYNOR_ERROR_UNKNOWN; /* TODO: Implement */
}
synor_error_t synor_wallet_get(
synor_wallet_t* wallet, const char* wallet_id,
synor_wallet_info_t* result
) {
(void)wallet; (void)wallet_id; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_wallet_generate_address(
synor_wallet_t* wallet, const char* wallet_id,
bool is_change, synor_address_t* result
) {
(void)wallet; (void)wallet_id; (void)is_change; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_wallet_get_stealth_address(
synor_wallet_t* wallet, const char* wallet_id,
synor_stealth_address_t* result
) {
(void)wallet; (void)wallet_id; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_wallet_sign_transaction(
synor_wallet_t* wallet, const char* wallet_id,
const synor_transaction_t* transaction, synor_signed_tx_t* result
) {
(void)wallet; (void)wallet_id; (void)transaction; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_wallet_sign_message(
synor_wallet_t* wallet, const char* wallet_id,
const char* message, int32_t address_index, synor_signature_t* result
) {
(void)wallet; (void)wallet_id; (void)message; (void)address_index; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_wallet_verify_message(
synor_wallet_t* wallet, const char* message,
const char* signature, const char* address, bool* is_valid
) {
(void)wallet; (void)message; (void)signature; (void)address; (void)is_valid;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_wallet_get_utxos(
synor_wallet_t* wallet, const char* address,
int32_t min_confirmations, synor_utxo_list_t* result
) {
(void)wallet; (void)address; (void)min_confirmations; (void)result;
return SYNOR_ERROR_UNKNOWN;
}
synor_error_t synor_wallet_estimate_fee(
synor_wallet_t* wallet, synor_priority_t priority, int64_t* fee_per_byte
) {
(void)wallet; (void)priority; (void)fee_per_byte;
return SYNOR_ERROR_UNKNOWN;
}
/* Free functions */
void synor_address_free(synor_address_t* addr) {
if (!addr) return;
free(addr->address);
memset(addr, 0, sizeof(*addr));
}
void synor_wallet_info_free(synor_wallet_info_t* info) {
if (!info) return;
free(info->id);
free(info->created_at);
for (size_t i = 0; i < info->address_count; i++) {
synor_address_free(&info->addresses[i]);
}
free(info->addresses);
memset(info, 0, sizeof(*info));
}
void synor_stealth_address_free(synor_stealth_address_t* addr) {
if (!addr) return;
free(addr->spend_public_key);
free(addr->view_public_key);
free(addr->one_time_address);
memset(addr, 0, sizeof(*addr));
}
void synor_create_wallet_result_free(synor_create_wallet_result_t* result) {
if (!result) return;
synor_wallet_info_free(&result->wallet);
free(result->mnemonic);
memset(result, 0, sizeof(*result));
}
void synor_signed_tx_free(synor_signed_tx_t* tx) {
if (!tx) return;
free(tx->txid);
free(tx->hex);
memset(tx, 0, sizeof(*tx));
}
void synor_signature_free(synor_signature_t* sig) {
if (!sig) return;
free(sig->signature);
free(sig->address);
memset(sig, 0, sizeof(*sig));
}
void synor_utxo_free(synor_utxo_t* utxo) {
if (!utxo) return;
free(utxo->txid);
free(utxo->address);
free(utxo->script_pubkey);
memset(utxo, 0, sizeof(*utxo));
}
void synor_utxo_list_free(synor_utxo_list_t* list) {
if (!list) return;
for (size_t i = 0; i < list->count; i++) {
synor_utxo_free(&list->utxos[i]);
}
free(list->utxos);
memset(list, 0, sizeof(*list));
}

View file

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16)
project(synor_compute VERSION 0.1.0 LANGUAGES CXX)
project(synor_sdk VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@ -28,25 +28,31 @@ set(SYNOR_SOURCES
src/synor_compute.cpp
src/tensor.cpp
src/client.cpp
src/wallet/wallet.cpp
src/rpc/rpc.cpp
src/storage/storage.cpp
)
# Create library
add_library(synor_compute ${SYNOR_SOURCES})
add_library(synor_sdk ${SYNOR_SOURCES})
target_include_directories(synor_compute
# Alias for backwards compatibility
add_library(synor_compute ALIAS synor_sdk)
target_include_directories(synor_sdk
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_link_libraries(synor_compute
target_link_libraries(synor_sdk
PRIVATE
CURL::libcurl
nlohmann_json::nlohmann_json
)
# Set library properties
set_target_properties(synor_compute PROPERTIES
set_target_properties(synor_sdk PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
)
@ -54,8 +60,8 @@ set_target_properties(synor_compute PROPERTIES
# Installation
include(GNUInstallDirs)
install(TARGETS synor_compute
EXPORT synor_compute-targets
install(TARGETS synor_sdk
EXPORT synor_sdk-targets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
@ -65,8 +71,8 @@ install(DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(EXPORT synor_compute-targets
FILE synor_compute-targets.cmake
install(EXPORT synor_sdk-targets
FILE synor_sdk-targets.cmake
NAMESPACE synor::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/synor_compute
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/synor_sdk
)

View file

@ -0,0 +1,219 @@
/**
* @file rpc.hpp
* @brief Synor RPC SDK for C++
*
* Modern C++17 SDK for blockchain queries and real-time subscriptions.
*/
#pragma once
#include <string>
#include <vector>
#include <optional>
#include <future>
#include <memory>
#include <functional>
#include <cstdint>
#include "wallet.hpp" // For Network, Priority, SynorException
namespace synor {
namespace rpc {
/// Transaction status
enum class TransactionStatus {
Pending,
Confirmed,
Failed
};
/// RPC configuration
struct Config {
std::string api_key;
std::string endpoint = "https://rpc.synor.io/v1";
std::string ws_endpoint = "wss://rpc.synor.io/v1/ws";
Network network = Network::Mainnet;
uint32_t timeout_ms = 30000;
uint32_t retries = 3;
bool debug = false;
};
/// Block header
struct BlockHeader {
std::string hash;
int64_t height;
std::string previous_hash;
int64_t timestamp;
int32_t version;
std::string merkle_root;
int64_t nonce;
double difficulty;
};
/// Script public key
struct ScriptPubKey {
std::string asm_code;
std::string hex;
std::string type;
std::optional<std::string> address;
};
/// Transaction input
struct TransactionInput {
std::string txid;
int32_t vout;
std::optional<std::string> script_sig;
int64_t sequence;
std::vector<std::string> witness;
};
/// Transaction output
struct TransactionOutput {
int64_t value;
int32_t n;
ScriptPubKey script_pubkey;
};
/// Transaction
struct Transaction {
std::string txid;
std::string hash;
int32_t version;
int32_t size;
int32_t weight;
int64_t lock_time;
std::vector<TransactionInput> inputs;
std::vector<TransactionOutput> outputs;
int64_t fee;
int32_t confirmations;
std::optional<std::string> block_hash;
std::optional<int64_t> block_height;
std::optional<int64_t> timestamp;
TransactionStatus status = TransactionStatus::Pending;
};
/// Block
struct Block {
std::string hash;
int64_t height;
std::string previous_hash;
int64_t timestamp;
int32_t version;
std::string merkle_root;
int64_t nonce;
double difficulty;
std::vector<Transaction> transactions;
int32_t size;
int32_t weight;
};
/// Chain information
struct ChainInfo {
std::string chain;
int64_t blocks;
int64_t headers;
std::string best_block_hash;
double difficulty;
int64_t median_time;
double verification_progress;
std::string chain_work;
bool pruned;
};
/// Mempool information
struct MempoolInfo {
int32_t size;
int64_t bytes;
int64_t usage;
int64_t max_mempool;
double mempool_min_fee;
double min_relay_tx_fee;
};
/// Fee estimate
struct FeeEstimate {
Priority priority;
int64_t fee_rate;
int32_t estimated_blocks;
};
/// Transaction submission result
struct SubmitResult {
std::string txid;
bool accepted;
std::optional<std::string> reason;
};
/// Subscription handle
class Subscription {
public:
Subscription(const std::string& id, const std::string& channel,
std::function<void()> on_cancel);
~Subscription();
Subscription(const Subscription&) = delete;
Subscription& operator=(const Subscription&) = delete;
Subscription(Subscription&&) noexcept;
Subscription& operator=(Subscription&&) noexcept;
const std::string& id() const { return id_; }
const std::string& channel() const { return channel_; }
void cancel();
private:
std::string id_;
std::string channel_;
std::function<void()> on_cancel_;
};
/// RPC client
class Client {
public:
explicit Client(const Config& config);
~Client();
Client(const Client&) = delete;
Client& operator=(const Client&) = delete;
Client(Client&&) noexcept;
Client& operator=(Client&&) noexcept;
// Block operations
Block get_latest_block();
std::future<Block> get_latest_block_async();
Block get_block(const std::string& hash_or_height);
BlockHeader get_block_header(const std::string& hash_or_height);
std::vector<Block> get_blocks(int64_t start_height, int64_t end_height);
// Transaction operations
Transaction get_transaction(const std::string& txid);
std::string get_raw_transaction(const std::string& txid);
SubmitResult send_raw_transaction(const std::string& hex);
Transaction decode_raw_transaction(const std::string& hex);
std::vector<Transaction> get_address_transactions(
const std::string& address, int32_t limit = 50, int32_t offset = 0);
// Fee estimation
FeeEstimate estimate_fee(Priority priority = Priority::Medium);
std::map<Priority, FeeEstimate> get_all_fee_estimates();
// Chain information
ChainInfo get_chain_info();
MempoolInfo get_mempool_info();
std::vector<std::string> get_mempool_transactions(int32_t limit = 100);
// Subscriptions
std::unique_ptr<Subscription> subscribe_blocks(
std::function<void(const Block&)> callback);
std::unique_ptr<Subscription> subscribe_address(
const std::string& address,
std::function<void(const Transaction&)> callback);
std::unique_ptr<Subscription> subscribe_mempool(
std::function<void(const Transaction&)> callback);
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace rpc
} // namespace synor

View file

@ -0,0 +1,174 @@
/**
* @file storage.hpp
* @brief Synor Storage SDK for C++
*
* Modern C++17 SDK for IPFS-compatible decentralized storage.
*/
#pragma once
#include <string>
#include <vector>
#include <optional>
#include <future>
#include <memory>
#include <functional>
#include <cstdint>
#include <span>
#include "wallet.hpp" // For SynorException
namespace synor {
namespace storage {
/// Pin status
enum class PinStatus {
Queued,
Pinning,
Pinned,
Failed,
Unpinned
};
/// Hash algorithm
enum class HashAlgorithm {
Sha2_256,
Blake3
};
/// Entry type
enum class EntryType {
File,
Directory
};
/// Storage configuration
struct Config {
std::string api_key;
std::string endpoint = "https://storage.synor.io/v1";
std::string gateway = "https://gateway.synor.io";
uint32_t timeout_ms = 60000;
uint32_t retries = 3;
bool debug = false;
};
/// Upload options
struct UploadOptions {
std::optional<std::string> name;
bool wrap_with_directory = false;
HashAlgorithm hash_algorithm = HashAlgorithm::Sha2_256;
std::optional<int32_t> chunk_size;
std::optional<int32_t> pin_duration_days;
};
/// Upload response
struct UploadResponse {
std::string cid;
int64_t size;
std::optional<std::string> name;
std::string created_at;
};
/// Pin information
struct Pin {
std::string cid;
std::optional<std::string> name;
PinStatus status;
int64_t size;
std::string created_at;
std::optional<std::string> expires_at;
std::vector<std::string> delegates;
};
/// Pin request
struct PinRequest {
std::string cid;
std::optional<std::string> name;
std::optional<int32_t> duration_days;
std::vector<std::string> origins;
std::map<std::string, std::string> meta;
};
/// CAR file information
struct CarFile {
std::string cid;
int64_t size;
std::vector<std::string> roots;
std::string created_at;
};
/// File entry for upload
struct FileEntry {
std::string path;
std::vector<uint8_t> content;
};
/// Directory entry
struct DirectoryEntry {
std::string name;
std::string cid;
int64_t size;
EntryType type;
};
/// Storage statistics
struct StorageStats {
int64_t total_size;
int32_t pin_count;
int64_t bandwidth_used;
double quota_used;
};
/// Storage client
class Client {
public:
explicit Client(const Config& config);
~Client();
Client(const Client&) = delete;
Client& operator=(const Client&) = delete;
Client(Client&&) noexcept;
Client& operator=(Client&&) noexcept;
// Upload operations
UploadResponse upload(std::span<const uint8_t> data,
const UploadOptions& options = {});
std::future<UploadResponse> upload_async(std::vector<uint8_t> data,
UploadOptions options = {});
UploadResponse upload_directory(const std::vector<FileEntry>& files,
const std::optional<std::string>& dir_name = std::nullopt);
// Download operations
std::vector<uint8_t> download(const std::string& cid);
std::future<std::vector<uint8_t>> download_async(const std::string& cid);
void download_stream(const std::string& cid,
std::function<void(std::span<const uint8_t>)> callback);
/// Get gateway URL for content
std::string get_gateway_url(const std::string& cid,
const std::optional<std::string>& path = std::nullopt);
// Pinning operations
Pin pin(const PinRequest& request);
void unpin(const std::string& cid);
Pin get_pin_status(const std::string& cid);
std::vector<Pin> list_pins(std::optional<PinStatus> status = std::nullopt,
int32_t limit = 50, int32_t offset = 0);
// CAR file operations
CarFile create_car(const std::vector<FileEntry>& entries);
std::vector<std::string> import_car(std::span<const uint8_t> car_data);
std::vector<uint8_t> export_car(const std::string& cid);
// Directory operations
std::vector<DirectoryEntry> list_directory(const std::string& cid);
// Statistics
StorageStats get_stats();
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace storage
} // namespace synor

View file

@ -0,0 +1,68 @@
/**
* @file synor.hpp
* @brief Synor SDK for C++ - Main Include Header
*
* This header includes all Synor SDK components:
* - Wallet: Key management and transaction signing
* - RPC: Blockchain queries and subscriptions
* - Storage: IPFS-compatible decentralized storage
*
* @example
* ```cpp
* #include <synor/synor.hpp>
*
* int main() {
* // Create clients
* synor::wallet::Client wallet({.api_key = "your-api-key"});
* synor::rpc::Client rpc({.api_key = "your-api-key"});
* synor::storage::Client storage({.api_key = "your-api-key"});
*
* // Use the SDKs...
* return 0;
* }
* ```
*/
#pragma once
#include "wallet.hpp"
#include "rpc.hpp"
#include "storage.hpp"
namespace synor {
/// SDK version
inline constexpr const char* VERSION = "0.1.0";
/// Convert Network to string
inline const char* to_string(Network network) {
switch (network) {
case Network::Mainnet: return "mainnet";
case Network::Testnet: return "testnet";
case Network::Devnet: return "devnet";
default: return "unknown";
}
}
/// Convert WalletType to string
inline const char* to_string(WalletType type) {
switch (type) {
case WalletType::Standard: return "standard";
case WalletType::Multisig: return "multisig";
case WalletType::Hardware: return "hardware";
case WalletType::Stealth: return "stealth";
default: return "unknown";
}
}
/// Convert Priority to string
inline const char* to_string(Priority priority) {
switch (priority) {
case Priority::Low: return "low";
case Priority::Medium: return "medium";
case Priority::High: return "high";
default: return "unknown";
}
}
} // namespace synor

View file

@ -0,0 +1,242 @@
/**
* @file wallet.hpp
* @brief Synor Wallet SDK for C++
*
* Modern C++17 SDK for key management, transaction signing, and balance queries.
*
* @example
* ```cpp
* #include <synor/wallet.hpp>
*
* int main() {
* synor::wallet::Config config{
* .api_key = "your-api-key",
* .endpoint = "https://wallet.synor.io/v1",
* .network = synor::Network::Mainnet
* };
*
* synor::wallet::Client client(config);
*
* // Create wallet
* auto result = client.create_wallet(synor::WalletType::Standard);
* std::cout << "Wallet ID: " << result.wallet.id << "\n";
* std::cout << "Mnemonic: " << result.mnemonic.value_or("N/A") << "\n";
*
* // Get balance
* auto balance = client.get_balance(result.wallet.addresses[0].address);
* std::cout << "Balance: " << balance.total << "\n";
*
* return 0;
* }
* ```
*/
#pragma once
#include <string>
#include <vector>
#include <optional>
#include <future>
#include <memory>
#include <cstdint>
#include <stdexcept>
namespace synor {
// Forward declarations
namespace wallet { class Client; }
/// Network environment
enum class Network {
Mainnet,
Testnet,
Devnet
};
/// Wallet type
enum class WalletType {
Standard,
Multisig,
Hardware,
Stealth
};
/// Transaction priority
enum class Priority {
Low,
Medium,
High
};
/// SDK exception
class SynorException : public std::runtime_error {
public:
explicit SynorException(const std::string& message, int status_code = 0)
: std::runtime_error(message), status_code_(status_code) {}
int status_code() const noexcept { return status_code_; }
private:
int status_code_;
};
namespace wallet {
/// Wallet configuration
struct Config {
std::string api_key;
std::string endpoint = "https://wallet.synor.io/v1";
Network network = Network::Mainnet;
uint32_t timeout_ms = 30000;
uint32_t retries = 3;
bool debug = false;
};
/// Wallet address
struct Address {
std::string address;
int32_t index;
bool is_change = false;
};
/// Wallet information
struct Wallet {
std::string id;
WalletType type;
Network network;
std::vector<Address> addresses;
std::string created_at;
};
/// Stealth address for privacy transactions
struct StealthAddress {
std::string spend_public_key;
std::string view_public_key;
std::string one_time_address;
};
/// Result of wallet creation
struct CreateWalletResult {
Wallet wallet;
std::optional<std::string> mnemonic;
};
/// Transaction input (UTXO reference)
struct TransactionInput {
std::string txid;
int32_t vout;
int64_t amount;
};
/// Transaction output
struct TransactionOutput {
std::string address;
int64_t amount;
};
/// Transaction for signing
struct Transaction {
std::vector<TransactionInput> inputs;
std::vector<TransactionOutput> outputs;
std::optional<int64_t> fee;
Priority priority = Priority::Medium;
};
/// Signed transaction
struct SignedTransaction {
std::string txid;
std::string hex;
int32_t size;
int64_t fee;
};
/// Cryptographic signature
struct Signature {
std::string signature;
std::string address;
std::optional<int32_t> recovery_id;
};
/// Unspent transaction output
struct UTXO {
std::string txid;
int32_t vout;
int64_t amount;
std::string address;
int32_t confirmations;
std::optional<std::string> script_pubkey;
};
/// Wallet balance
struct Balance {
int64_t confirmed;
int64_t unconfirmed;
int64_t total;
};
/// Wallet client
class Client {
public:
explicit Client(const Config& config);
~Client();
// Disable copy
Client(const Client&) = delete;
Client& operator=(const Client&) = delete;
// Enable move
Client(Client&&) noexcept;
Client& operator=(Client&&) noexcept;
/// Create a new wallet
CreateWalletResult create_wallet(WalletType type = WalletType::Standard);
/// Create a new wallet asynchronously
std::future<CreateWalletResult> create_wallet_async(WalletType type = WalletType::Standard);
/// Import a wallet from mnemonic
Wallet import_wallet(const std::string& mnemonic,
const std::optional<std::string>& passphrase = std::nullopt);
/// Get a wallet by ID
Wallet get_wallet(const std::string& wallet_id);
/// Generate a new address
Address generate_address(const std::string& wallet_id, bool is_change = false);
/// Get stealth address
StealthAddress get_stealth_address(const std::string& wallet_id);
/// Sign a transaction
SignedTransaction sign_transaction(const std::string& wallet_id,
const Transaction& transaction);
/// Sign a message
Signature sign_message(const std::string& wallet_id,
const std::string& message,
int32_t address_index = 0);
/// Verify a message signature
bool verify_message(const std::string& message,
const std::string& signature,
const std::string& address);
/// Get balance for an address
Balance get_balance(const std::string& address);
/// Get balance asynchronously
std::future<Balance> get_balance_async(const std::string& address);
/// Get UTXOs for an address
std::vector<UTXO> get_utxos(const std::string& address, int32_t min_confirmations = 1);
/// Estimate transaction fee
int64_t estimate_fee(Priority priority = Priority::Medium);
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace wallet
} // namespace synor

145
sdk/cpp/src/rpc/rpc.cpp Normal file
View file

@ -0,0 +1,145 @@
/**
* @file rpc.cpp
* @brief Synor RPC SDK implementation for C++
*/
#include "synor/rpc.hpp"
#include <stdexcept>
namespace synor {
namespace rpc {
// Subscription implementation
Subscription::Subscription(const std::string& id, const std::string& channel,
std::function<void()> on_cancel)
: id_(id), channel_(channel), on_cancel_(std::move(on_cancel)) {}
Subscription::~Subscription() {
cancel();
}
Subscription::Subscription(Subscription&&) noexcept = default;
Subscription& Subscription::operator=(Subscription&&) noexcept = default;
void Subscription::cancel() {
if (on_cancel_) {
on_cancel_();
on_cancel_ = nullptr;
}
}
// Client implementation
class Client::Impl {
public:
explicit Impl(const Config& config) : config_(config) {}
Config config_;
// HTTP client and WebSocket would go here
};
Client::Client(const Config& config)
: impl_(std::make_unique<Impl>(config)) {}
Client::~Client() = default;
Client::Client(Client&&) noexcept = default;
Client& Client::operator=(Client&&) noexcept = default;
Block Client::get_latest_block() {
throw SynorException("Not implemented");
}
std::future<Block> Client::get_latest_block_async() {
return std::async(std::launch::async, [this]() {
return this->get_latest_block();
});
}
Block Client::get_block(const std::string& hash_or_height) {
(void)hash_or_height;
throw SynorException("Not implemented");
}
BlockHeader Client::get_block_header(const std::string& hash_or_height) {
(void)hash_or_height;
throw SynorException("Not implemented");
}
std::vector<Block> Client::get_blocks(int64_t start_height, int64_t end_height) {
(void)start_height;
(void)end_height;
throw SynorException("Not implemented");
}
Transaction Client::get_transaction(const std::string& txid) {
(void)txid;
throw SynorException("Not implemented");
}
std::string Client::get_raw_transaction(const std::string& txid) {
(void)txid;
throw SynorException("Not implemented");
}
SubmitResult Client::send_raw_transaction(const std::string& hex) {
(void)hex;
throw SynorException("Not implemented");
}
Transaction Client::decode_raw_transaction(const std::string& hex) {
(void)hex;
throw SynorException("Not implemented");
}
std::vector<Transaction> Client::get_address_transactions(
const std::string& address, int32_t limit, int32_t offset) {
(void)address;
(void)limit;
(void)offset;
throw SynorException("Not implemented");
}
FeeEstimate Client::estimate_fee(Priority priority) {
(void)priority;
throw SynorException("Not implemented");
}
std::map<Priority, FeeEstimate> Client::get_all_fee_estimates() {
throw SynorException("Not implemented");
}
ChainInfo Client::get_chain_info() {
throw SynorException("Not implemented");
}
MempoolInfo Client::get_mempool_info() {
throw SynorException("Not implemented");
}
std::vector<std::string> Client::get_mempool_transactions(int32_t limit) {
(void)limit;
throw SynorException("Not implemented");
}
std::unique_ptr<Subscription> Client::subscribe_blocks(
std::function<void(const Block&)> callback) {
(void)callback;
throw SynorException("Not implemented");
}
std::unique_ptr<Subscription> Client::subscribe_address(
const std::string& address,
std::function<void(const Transaction&)> callback) {
(void)address;
(void)callback;
throw SynorException("Not implemented");
}
std::unique_ptr<Subscription> Client::subscribe_mempool(
std::function<void(const Transaction&)> callback) {
(void)callback;
throw SynorException("Not implemented");
}
} // namespace rpc
} // namespace synor

View file

@ -0,0 +1,126 @@
/**
* @file storage.cpp
* @brief Synor Storage SDK implementation for C++
*/
#include "synor/storage.hpp"
#include <sstream>
namespace synor {
namespace storage {
// Client implementation
class Client::Impl {
public:
explicit Impl(const Config& config) : config_(config) {}
Config config_;
// HTTP client would go here
};
Client::Client(const Config& config)
: impl_(std::make_unique<Impl>(config)) {}
Client::~Client() = default;
Client::Client(Client&&) noexcept = default;
Client& Client::operator=(Client&&) noexcept = default;
UploadResponse Client::upload(std::span<const uint8_t> data,
const UploadOptions& options) {
(void)data;
(void)options;
throw SynorException("Not implemented");
}
std::future<UploadResponse> Client::upload_async(std::vector<uint8_t> data,
UploadOptions options) {
return std::async(std::launch::async, [this, data = std::move(data), options]() {
return this->upload(std::span{data}, options);
});
}
UploadResponse Client::upload_directory(const std::vector<FileEntry>& files,
const std::optional<std::string>& dir_name) {
(void)files;
(void)dir_name;
throw SynorException("Not implemented");
}
std::vector<uint8_t> Client::download(const std::string& cid) {
(void)cid;
throw SynorException("Not implemented");
}
std::future<std::vector<uint8_t>> Client::download_async(const std::string& cid) {
return std::async(std::launch::async, [this, cid]() {
return this->download(cid);
});
}
void Client::download_stream(const std::string& cid,
std::function<void(std::span<const uint8_t>)> callback) {
(void)cid;
(void)callback;
throw SynorException("Not implemented");
}
std::string Client::get_gateway_url(const std::string& cid,
const std::optional<std::string>& path) {
std::ostringstream oss;
oss << impl_->config_.gateway << "/ipfs/" << cid;
if (path) {
oss << "/" << *path;
}
return oss.str();
}
Pin Client::pin(const PinRequest& request) {
(void)request;
throw SynorException("Not implemented");
}
void Client::unpin(const std::string& cid) {
(void)cid;
throw SynorException("Not implemented");
}
Pin Client::get_pin_status(const std::string& cid) {
(void)cid;
throw SynorException("Not implemented");
}
std::vector<Pin> Client::list_pins(std::optional<PinStatus> status,
int32_t limit, int32_t offset) {
(void)status;
(void)limit;
(void)offset;
throw SynorException("Not implemented");
}
CarFile Client::create_car(const std::vector<FileEntry>& entries) {
(void)entries;
throw SynorException("Not implemented");
}
std::vector<std::string> Client::import_car(std::span<const uint8_t> car_data) {
(void)car_data;
throw SynorException("Not implemented");
}
std::vector<uint8_t> Client::export_car(const std::string& cid) {
(void)cid;
throw SynorException("Not implemented");
}
std::vector<DirectoryEntry> Client::list_directory(const std::string& cid) {
(void)cid;
throw SynorException("Not implemented");
}
StorageStats Client::get_stats() {
throw SynorException("Not implemented");
}
} // namespace storage
} // namespace synor

View file

@ -0,0 +1,113 @@
/**
* @file wallet.cpp
* @brief Synor Wallet SDK implementation for C++
*/
#include "synor/wallet.hpp"
#include <stdexcept>
namespace synor {
namespace wallet {
// Implementation class (pimpl pattern)
class Client::Impl {
public:
explicit Impl(const Config& config) : config_(config) {}
Config config_;
// HTTP client would go here
};
Client::Client(const Config& config)
: impl_(std::make_unique<Impl>(config)) {}
Client::~Client() = default;
Client::Client(Client&&) noexcept = default;
Client& Client::operator=(Client&&) noexcept = default;
CreateWalletResult Client::create_wallet(WalletType type) {
// TODO: Implement HTTP request
(void)type;
throw SynorException("Not implemented");
}
std::future<CreateWalletResult> Client::create_wallet_async(WalletType type) {
return std::async(std::launch::async, [this, type]() {
return this->create_wallet(type);
});
}
Wallet Client::import_wallet(const std::string& mnemonic,
const std::optional<std::string>& passphrase) {
(void)mnemonic;
(void)passphrase;
throw SynorException("Not implemented");
}
Wallet Client::get_wallet(const std::string& wallet_id) {
(void)wallet_id;
throw SynorException("Not implemented");
}
Address Client::generate_address(const std::string& wallet_id, bool is_change) {
(void)wallet_id;
(void)is_change;
throw SynorException("Not implemented");
}
StealthAddress Client::get_stealth_address(const std::string& wallet_id) {
(void)wallet_id;
throw SynorException("Not implemented");
}
SignedTransaction Client::sign_transaction(const std::string& wallet_id,
const Transaction& transaction) {
(void)wallet_id;
(void)transaction;
throw SynorException("Not implemented");
}
Signature Client::sign_message(const std::string& wallet_id,
const std::string& message,
int32_t address_index) {
(void)wallet_id;
(void)message;
(void)address_index;
throw SynorException("Not implemented");
}
bool Client::verify_message(const std::string& message,
const std::string& signature,
const std::string& address) {
(void)message;
(void)signature;
(void)address;
throw SynorException("Not implemented");
}
Balance Client::get_balance(const std::string& address) {
(void)address;
throw SynorException("Not implemented");
}
std::future<Balance> Client::get_balance_async(const std::string& address) {
return std::async(std::launch::async, [this, address]() {
return this->get_balance(address);
});
}
std::vector<UTXO> Client::get_utxos(const std::string& address,
int32_t min_confirmations) {
(void)address;
(void)min_confirmations;
throw SynorException("Not implemented");
}
int64_t Client::estimate_fee(Priority priority) {
(void)priority;
throw SynorException("Not implemented");
}
} // namespace wallet
} // namespace synor

View file

@ -0,0 +1,477 @@
using System.Net.Http.Json;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
namespace Synor.Sdk.Rpc;
/// <summary>
/// Synor RPC SDK client for C#.
///
/// Provides blockchain data queries, transaction submission,
/// and real-time subscriptions via WebSocket.
/// </summary>
/// <example>
/// <code>
/// var rpc = new SynorRpc(new RpcConfig { ApiKey = "your-api-key" });
///
/// // Get latest block
/// var block = await rpc.GetLatestBlockAsync();
/// Console.WriteLine($"Block height: {block.Height}");
///
/// // Subscribe to new blocks
/// var subscription = await rpc.SubscribeBlocksAsync(block => {
/// Console.WriteLine($"New block: {block.Height}");
/// });
///
/// // Later: cancel subscription
/// subscription.Cancel();
/// rpc.Dispose();
/// </code>
/// </example>
public class SynorRpc : IDisposable
{
private readonly RpcConfig _config;
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _jsonOptions;
private ClientWebSocket? _webSocket;
private CancellationTokenSource? _wsCancellation;
private readonly Dictionary<string, Action<JsonElement>> _subscriptionCallbacks = new();
private bool _disposed;
public SynorRpc(RpcConfig config)
{
_config = config;
_httpClient = new HttpClient
{
BaseAddress = new Uri(config.Endpoint),
Timeout = config.Timeout
};
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}");
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
};
}
/// <summary>
/// Get the latest block.
/// </summary>
public async Task<Block> GetLatestBlockAsync(CancellationToken cancellationToken = default)
{
return await GetAsync<Block>("/blocks/latest", cancellationToken);
}
/// <summary>
/// Get a block by hash or height.
/// </summary>
public async Task<Block> GetBlockAsync(
string hashOrHeight,
CancellationToken cancellationToken = default)
{
return await GetAsync<Block>($"/blocks/{hashOrHeight}", cancellationToken);
}
/// <summary>
/// Get a block header by hash or height.
/// </summary>
public async Task<BlockHeader> GetBlockHeaderAsync(
string hashOrHeight,
CancellationToken cancellationToken = default)
{
return await GetAsync<BlockHeader>($"/blocks/{hashOrHeight}/header", cancellationToken);
}
/// <summary>
/// Get a range of blocks.
/// </summary>
public async Task<List<Block>> GetBlocksAsync(
long startHeight,
long endHeight,
CancellationToken cancellationToken = default)
{
return await GetAsync<List<Block>>(
$"/blocks?start={startHeight}&end={endHeight}",
cancellationToken);
}
/// <summary>
/// Get a transaction by ID.
/// </summary>
public async Task<RpcTransaction> GetTransactionAsync(
string txid,
CancellationToken cancellationToken = default)
{
return await GetAsync<RpcTransaction>($"/transactions/{txid}", cancellationToken);
}
/// <summary>
/// Get raw transaction hex.
/// </summary>
public async Task<string> GetRawTransactionAsync(
string txid,
CancellationToken cancellationToken = default)
{
var response = await GetAsync<JsonElement>($"/transactions/{txid}/raw", cancellationToken);
return response.GetProperty("hex").GetString() ?? throw new RpcException("Invalid response");
}
/// <summary>
/// Send a raw transaction.
/// </summary>
public async Task<SubmitResult> SendRawTransactionAsync(
string hex,
CancellationToken cancellationToken = default)
{
var request = new { hex };
return await PostAsync<SubmitResult>("/transactions/send", request, cancellationToken);
}
/// <summary>
/// Decode a raw transaction.
/// </summary>
public async Task<RpcTransaction> DecodeRawTransactionAsync(
string hex,
CancellationToken cancellationToken = default)
{
var request = new { hex };
return await PostAsync<RpcTransaction>("/transactions/decode", request, cancellationToken);
}
/// <summary>
/// Get transactions for an address.
/// </summary>
public async Task<List<RpcTransaction>> GetAddressTransactionsAsync(
string address,
int limit = 20,
int offset = 0,
CancellationToken cancellationToken = default)
{
return await GetAsync<List<RpcTransaction>>(
$"/addresses/{address}/transactions?limit={limit}&offset={offset}",
cancellationToken);
}
/// <summary>
/// Estimate transaction fee.
/// </summary>
public async Task<FeeEstimate> EstimateFeeAsync(
RpcPriority priority = RpcPriority.Medium,
CancellationToken cancellationToken = default)
{
return await GetAsync<FeeEstimate>(
$"/fees/estimate?priority={priority.ToString().ToLower()}",
cancellationToken);
}
/// <summary>
/// Get all fee estimates.
/// </summary>
public async Task<Dictionary<RpcPriority, FeeEstimate>> GetAllFeeEstimatesAsync(
CancellationToken cancellationToken = default)
{
var estimates = await GetAsync<List<FeeEstimate>>("/fees/estimates", cancellationToken);
return estimates.ToDictionary(e => e.Priority, e => e);
}
/// <summary>
/// Get chain information.
/// </summary>
public async Task<ChainInfo> GetChainInfoAsync(CancellationToken cancellationToken = default)
{
return await GetAsync<ChainInfo>("/chain/info", cancellationToken);
}
/// <summary>
/// Get mempool information.
/// </summary>
public async Task<MempoolInfo> GetMempoolInfoAsync(CancellationToken cancellationToken = default)
{
return await GetAsync<MempoolInfo>("/mempool/info", cancellationToken);
}
/// <summary>
/// Get mempool transaction IDs.
/// </summary>
public async Task<List<string>> GetMempoolTransactionsAsync(
int limit = 100,
CancellationToken cancellationToken = default)
{
return await GetAsync<List<string>>($"/mempool/transactions?limit={limit}", cancellationToken);
}
/// <summary>
/// Subscribe to new blocks.
/// </summary>
public async Task<Subscription> SubscribeBlocksAsync(
Action<Block> callback,
CancellationToken cancellationToken = default)
{
await EnsureWebSocketConnectedAsync(cancellationToken);
var subscriptionId = Guid.NewGuid().ToString();
_subscriptionCallbacks[subscriptionId] = element =>
{
var block = element.Deserialize<Block>(_jsonOptions);
if (block != null) callback(block);
};
var subscribeMessage = JsonSerializer.Serialize(new
{
type = "subscribe",
channel = "blocks",
subscription_id = subscriptionId
});
await SendWebSocketMessageAsync(subscribeMessage, cancellationToken);
return new Subscription(subscriptionId, "blocks", () =>
{
_subscriptionCallbacks.Remove(subscriptionId);
var unsubscribeMessage = JsonSerializer.Serialize(new
{
type = "unsubscribe",
subscription_id = subscriptionId
});
_ = SendWebSocketMessageAsync(unsubscribeMessage, CancellationToken.None);
});
}
/// <summary>
/// Subscribe to address transactions.
/// </summary>
public async Task<Subscription> SubscribeAddressAsync(
string address,
Action<RpcTransaction> callback,
CancellationToken cancellationToken = default)
{
await EnsureWebSocketConnectedAsync(cancellationToken);
var subscriptionId = Guid.NewGuid().ToString();
_subscriptionCallbacks[subscriptionId] = element =>
{
var tx = element.Deserialize<RpcTransaction>(_jsonOptions);
if (tx != null) callback(tx);
};
var subscribeMessage = JsonSerializer.Serialize(new
{
type = "subscribe",
channel = "address",
address,
subscription_id = subscriptionId
});
await SendWebSocketMessageAsync(subscribeMessage, cancellationToken);
return new Subscription(subscriptionId, $"address:{address}", () =>
{
_subscriptionCallbacks.Remove(subscriptionId);
var unsubscribeMessage = JsonSerializer.Serialize(new
{
type = "unsubscribe",
subscription_id = subscriptionId
});
_ = SendWebSocketMessageAsync(unsubscribeMessage, CancellationToken.None);
});
}
/// <summary>
/// Subscribe to mempool transactions.
/// </summary>
public async Task<Subscription> SubscribeMempoolAsync(
Action<RpcTransaction> callback,
CancellationToken cancellationToken = default)
{
await EnsureWebSocketConnectedAsync(cancellationToken);
var subscriptionId = Guid.NewGuid().ToString();
_subscriptionCallbacks[subscriptionId] = element =>
{
var tx = element.Deserialize<RpcTransaction>(_jsonOptions);
if (tx != null) callback(tx);
};
var subscribeMessage = JsonSerializer.Serialize(new
{
type = "subscribe",
channel = "mempool",
subscription_id = subscriptionId
});
await SendWebSocketMessageAsync(subscribeMessage, cancellationToken);
return new Subscription(subscriptionId, "mempool", () =>
{
_subscriptionCallbacks.Remove(subscriptionId);
var unsubscribeMessage = JsonSerializer.Serialize(new
{
type = "unsubscribe",
subscription_id = subscriptionId
});
_ = SendWebSocketMessageAsync(unsubscribeMessage, CancellationToken.None);
});
}
private async Task EnsureWebSocketConnectedAsync(CancellationToken cancellationToken)
{
if (_webSocket?.State == WebSocketState.Open)
return;
_webSocket?.Dispose();
_wsCancellation?.Cancel();
_wsCancellation?.Dispose();
_webSocket = new ClientWebSocket();
_webSocket.Options.SetRequestHeader("Authorization", $"Bearer {_config.ApiKey}");
_wsCancellation = new CancellationTokenSource();
await _webSocket.ConnectAsync(new Uri(_config.WsEndpoint), cancellationToken);
_ = ReceiveWebSocketMessagesAsync(_wsCancellation.Token);
}
private async Task ReceiveWebSocketMessagesAsync(CancellationToken cancellationToken)
{
var buffer = new byte[8192];
try
{
while (_webSocket?.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
{
var result = await _webSocket.ReceiveAsync(buffer, cancellationToken);
if (result.MessageType == WebSocketMessageType.Close)
break;
if (result.MessageType == WebSocketMessageType.Text)
{
var json = Encoding.UTF8.GetString(buffer, 0, result.Count);
ProcessWebSocketMessage(json);
}
}
}
catch (OperationCanceledException)
{
// Expected when cancelling
}
catch (Exception ex)
{
if (_config.Debug)
{
Console.WriteLine($"WebSocket error: {ex.Message}");
}
}
}
private void ProcessWebSocketMessage(string json)
{
try
{
var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (root.TryGetProperty("subscription_id", out var subIdElement))
{
var subscriptionId = subIdElement.GetString();
if (subscriptionId != null && _subscriptionCallbacks.TryGetValue(subscriptionId, out var callback))
{
if (root.TryGetProperty("data", out var data))
{
callback(data);
}
}
}
}
catch (Exception ex)
{
if (_config.Debug)
{
Console.WriteLine($"Error processing WebSocket message: {ex.Message}");
}
}
}
private async Task SendWebSocketMessageAsync(string message, CancellationToken cancellationToken)
{
if (_webSocket?.State == WebSocketState.Open)
{
var bytes = Encoding.UTF8.GetBytes(message);
await _webSocket.SendAsync(bytes, WebSocketMessageType.Text, true, cancellationToken);
}
}
private async Task<T> GetAsync<T>(string path, CancellationToken cancellationToken)
{
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.GetAsync(path, cancellationToken);
await EnsureSuccessAsync(response);
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
?? throw new RpcException("Invalid response");
});
}
private async Task<T> PostAsync<T>(string path, object body, CancellationToken cancellationToken)
{
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, cancellationToken);
await EnsureSuccessAsync(response);
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
?? throw new RpcException("Invalid response");
});
}
private async Task EnsureSuccessAsync(HttpResponseMessage response)
{
if (!response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
throw new RpcException(
$"HTTP error: {content}",
statusCode: (int)response.StatusCode);
}
}
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation)
{
Exception? lastException = null;
for (int attempt = 0; attempt < _config.Retries; attempt++)
{
try
{
return await operation();
}
catch (Exception ex)
{
lastException = ex;
if (_config.Debug)
{
Console.WriteLine($"Attempt {attempt + 1} failed: {ex.Message}");
}
if (attempt < _config.Retries - 1)
{
await Task.Delay(TimeSpan.FromSeconds(attempt + 1));
}
}
}
throw lastException ?? new RpcException("Unknown error after retries");
}
public void Dispose()
{
if (!_disposed)
{
_wsCancellation?.Cancel();
_wsCancellation?.Dispose();
_webSocket?.Dispose();
_httpClient.Dispose();
_disposed = true;
}
GC.SuppressFinalize(this);
}
}

View file

@ -0,0 +1,222 @@
using System.Text.Json.Serialization;
namespace Synor.Sdk.Rpc;
/// <summary>
/// Network environment.
/// </summary>
public enum RpcNetwork
{
[JsonPropertyName("mainnet")] Mainnet,
[JsonPropertyName("testnet")] Testnet,
[JsonPropertyName("devnet")] Devnet
}
/// <summary>
/// Transaction priority.
/// </summary>
public enum RpcPriority
{
[JsonPropertyName("low")] Low,
[JsonPropertyName("medium")] Medium,
[JsonPropertyName("high")] High
}
/// <summary>
/// Transaction status.
/// </summary>
public enum TransactionStatus
{
[JsonPropertyName("pending")] Pending,
[JsonPropertyName("confirmed")] Confirmed,
[JsonPropertyName("failed")] Failed
}
/// <summary>
/// RPC configuration.
/// </summary>
public class RpcConfig
{
public required string ApiKey { get; init; }
public string Endpoint { get; init; } = "https://rpc.synor.io/v1";
public string WsEndpoint { get; init; } = "wss://rpc.synor.io/v1/ws";
public RpcNetwork Network { get; init; } = RpcNetwork.Mainnet;
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
public int Retries { get; init; } = 3;
public bool Debug { get; init; } = false;
}
/// <summary>
/// Block header information.
/// </summary>
public record BlockHeader(
[property: JsonPropertyName("hash")] string Hash,
[property: JsonPropertyName("height")] long Height,
[property: JsonPropertyName("previous_hash")] string PreviousHash,
[property: JsonPropertyName("timestamp")] long Timestamp,
[property: JsonPropertyName("version")] int Version,
[property: JsonPropertyName("merkle_root")] string MerkleRoot,
[property: JsonPropertyName("nonce")] long Nonce,
[property: JsonPropertyName("difficulty")] double Difficulty
);
/// <summary>
/// Script public key.
/// </summary>
public record ScriptPubKey(
[property: JsonPropertyName("asm")] string Asm,
[property: JsonPropertyName("hex")] string Hex,
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("address")] string? Address = null
);
/// <summary>
/// Transaction input.
/// </summary>
public record RpcTransactionInput(
[property: JsonPropertyName("txid")] string Txid,
[property: JsonPropertyName("vout")] int Vout,
[property: JsonPropertyName("script_sig")] string? ScriptSig,
[property: JsonPropertyName("sequence")] long Sequence,
[property: JsonPropertyName("witness")] List<string>? Witness = null
);
/// <summary>
/// Transaction output.
/// </summary>
public record RpcTransactionOutput(
[property: JsonPropertyName("value")] long Value,
[property: JsonPropertyName("n")] int N,
[property: JsonPropertyName("script_pubkey")] ScriptPubKey ScriptPubKey
);
/// <summary>
/// A blockchain transaction.
/// </summary>
public record RpcTransaction(
[property: JsonPropertyName("txid")] string Txid,
[property: JsonPropertyName("hash")] string Hash,
[property: JsonPropertyName("version")] int Version,
[property: JsonPropertyName("size")] int Size,
[property: JsonPropertyName("weight")] int Weight,
[property: JsonPropertyName("lock_time")] long LockTime,
[property: JsonPropertyName("inputs")] List<RpcTransactionInput> Inputs,
[property: JsonPropertyName("outputs")] List<RpcTransactionOutput> Outputs,
[property: JsonPropertyName("fee")] long Fee,
[property: JsonPropertyName("confirmations")] int Confirmations,
[property: JsonPropertyName("block_hash")] string? BlockHash = null,
[property: JsonPropertyName("block_height")] long? BlockHeight = null,
[property: JsonPropertyName("timestamp")] long? Timestamp = null,
[property: JsonPropertyName("status")] TransactionStatus Status = TransactionStatus.Pending
);
/// <summary>
/// A blockchain block.
/// </summary>
public record Block(
[property: JsonPropertyName("hash")] string Hash,
[property: JsonPropertyName("height")] long Height,
[property: JsonPropertyName("previous_hash")] string PreviousHash,
[property: JsonPropertyName("timestamp")] long Timestamp,
[property: JsonPropertyName("version")] int Version,
[property: JsonPropertyName("merkle_root")] string MerkleRoot,
[property: JsonPropertyName("nonce")] long Nonce,
[property: JsonPropertyName("difficulty")] double Difficulty,
[property: JsonPropertyName("transactions")] List<RpcTransaction> Transactions,
[property: JsonPropertyName("size")] int Size,
[property: JsonPropertyName("weight")] int Weight
);
/// <summary>
/// Chain information.
/// </summary>
public record ChainInfo(
[property: JsonPropertyName("chain")] string Chain,
[property: JsonPropertyName("blocks")] long Blocks,
[property: JsonPropertyName("headers")] long Headers,
[property: JsonPropertyName("best_block_hash")] string BestBlockHash,
[property: JsonPropertyName("difficulty")] double Difficulty,
[property: JsonPropertyName("median_time")] long MedianTime,
[property: JsonPropertyName("verification_progress")] double VerificationProgress,
[property: JsonPropertyName("chain_work")] string ChainWork,
[property: JsonPropertyName("pruned")] bool Pruned
);
/// <summary>
/// Mempool information.
/// </summary>
public record MempoolInfo(
[property: JsonPropertyName("size")] int Size,
[property: JsonPropertyName("bytes")] long Bytes,
[property: JsonPropertyName("usage")] long Usage,
[property: JsonPropertyName("max_mempool")] long MaxMempool,
[property: JsonPropertyName("mempool_min_fee")] double MempoolMinFee,
[property: JsonPropertyName("min_relay_tx_fee")] double MinRelayTxFee
);
/// <summary>
/// Fee estimate.
/// </summary>
public record FeeEstimate(
[property: JsonPropertyName("priority")] RpcPriority Priority,
[property: JsonPropertyName("fee_rate")] long FeeRate,
[property: JsonPropertyName("estimated_blocks")] int EstimatedBlocks
);
/// <summary>
/// Submit result.
/// </summary>
public record SubmitResult(
[property: JsonPropertyName("txid")] string Txid,
[property: JsonPropertyName("accepted")] bool Accepted,
[property: JsonPropertyName("reason")] string? Reason = null
);
/// <summary>
/// Subscription handle.
/// </summary>
public class Subscription : IDisposable
{
public string Id { get; }
public string Channel { get; }
private readonly Action _onCancel;
private bool _disposed;
public Subscription(string id, string channel, Action onCancel)
{
Id = id;
Channel = channel;
_onCancel = onCancel;
}
public void Cancel()
{
if (!_disposed)
{
_onCancel();
_disposed = true;
}
}
public void Dispose()
{
Cancel();
GC.SuppressFinalize(this);
}
}
/// <summary>
/// RPC exception.
/// </summary>
public class RpcException : Exception
{
public string? Code { get; }
public int? StatusCode { get; }
public RpcException(string message, string? code = null, int? statusCode = null)
: base(message)
{
Code = code;
StatusCode = statusCode;
}
}

View file

@ -0,0 +1,392 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
namespace Synor.Sdk.Storage;
/// <summary>
/// Synor Storage SDK client for C#.
///
/// Provides decentralized storage operations including upload, download,
/// pinning, and directory management.
/// </summary>
/// <example>
/// <code>
/// var storage = new SynorStorage(new StorageConfig { ApiKey = "your-api-key" });
///
/// // Upload a file
/// var data = File.ReadAllBytes("document.pdf");
/// var result = await storage.UploadAsync(data, new UploadOptions { Name = "document.pdf" });
/// Console.WriteLine($"CID: {result.Cid}");
///
/// // Download a file
/// var downloaded = await storage.DownloadAsync(result.Cid);
/// File.WriteAllBytes("downloaded.pdf", downloaded);
///
/// storage.Dispose();
/// </code>
/// </example>
public class SynorStorage : IDisposable
{
private readonly StorageConfig _config;
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _jsonOptions;
private bool _disposed;
public SynorStorage(StorageConfig config)
{
_config = config;
_httpClient = new HttpClient
{
BaseAddress = new Uri(config.Endpoint),
Timeout = config.Timeout
};
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}");
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
};
}
/// <summary>
/// Upload data to storage.
/// </summary>
public async Task<UploadResponse> UploadAsync(
byte[] data,
UploadOptions? options = null,
CancellationToken cancellationToken = default)
{
options ??= new UploadOptions();
using var content = new MultipartFormDataContent();
var fileContent = new ByteArrayContent(data);
if (!string.IsNullOrEmpty(options.ContentType))
{
fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(options.ContentType);
}
content.Add(fileContent, "file", options.Name ?? "file");
if (options.Metadata != null)
{
foreach (var (key, value) in options.Metadata)
{
content.Add(new StringContent(value), $"metadata[{key}]");
}
}
content.Add(new StringContent(options.Pin.ToString().ToLower()), "pin");
content.Add(new StringContent(options.WrapWithDirectory.ToString().ToLower()), "wrap_with_directory");
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.PostAsync("/upload", content, cancellationToken);
await EnsureSuccessAsync(response);
return await response.Content.ReadFromJsonAsync<UploadResponse>(_jsonOptions, cancellationToken)
?? throw new StorageException("Invalid response");
});
}
/// <summary>
/// Upload a stream to storage.
/// </summary>
public async Task<UploadResponse> UploadStreamAsync(
Stream stream,
UploadOptions? options = null,
CancellationToken cancellationToken = default)
{
options ??= new UploadOptions();
using var content = new MultipartFormDataContent();
var streamContent = new StreamContent(stream);
if (!string.IsNullOrEmpty(options.ContentType))
{
streamContent.Headers.ContentType = MediaTypeHeaderValue.Parse(options.ContentType);
}
content.Add(streamContent, "file", options.Name ?? "file");
content.Add(new StringContent(options.Pin.ToString().ToLower()), "pin");
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.PostAsync("/upload", content, cancellationToken);
await EnsureSuccessAsync(response);
return await response.Content.ReadFromJsonAsync<UploadResponse>(_jsonOptions, cancellationToken)
?? throw new StorageException("Invalid response");
});
}
/// <summary>
/// Upload a directory of files.
/// </summary>
public async Task<UploadResponse> UploadDirectoryAsync(
IEnumerable<FileEntry> files,
string? directoryName = null,
CancellationToken cancellationToken = default)
{
using var content = new MultipartFormDataContent();
foreach (var file in files)
{
var fileContent = new ByteArrayContent(file.Content);
if (!string.IsNullOrEmpty(file.ContentType))
{
fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(file.ContentType);
}
content.Add(fileContent, "files", file.Path);
}
if (!string.IsNullOrEmpty(directoryName))
{
content.Add(new StringContent(directoryName), "directory_name");
}
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.PostAsync("/upload/directory", content, cancellationToken);
await EnsureSuccessAsync(response);
return await response.Content.ReadFromJsonAsync<UploadResponse>(_jsonOptions, cancellationToken)
?? throw new StorageException("Invalid response");
});
}
/// <summary>
/// Download content by CID.
/// </summary>
public async Task<byte[]> DownloadAsync(
string cid,
CancellationToken cancellationToken = default)
{
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.GetAsync($"/download/{cid}", cancellationToken);
await EnsureSuccessAsync(response);
return await response.Content.ReadAsByteArrayAsync(cancellationToken);
});
}
/// <summary>
/// Download content as a stream.
/// </summary>
public async Task<Stream> DownloadStreamAsync(
string cid,
CancellationToken cancellationToken = default)
{
var response = await _httpClient.GetAsync(
$"/download/{cid}",
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
await EnsureSuccessAsync(response);
return await response.Content.ReadAsStreamAsync(cancellationToken);
}
/// <summary>
/// Get gateway URL for a CID.
/// </summary>
public string GetGatewayUrl(string cid, string? path = null)
{
var url = $"{_config.Gateway}/ipfs/{cid}";
if (!string.IsNullOrEmpty(path))
{
url += $"/{path.TrimStart('/')}";
}
return url;
}
/// <summary>
/// Pin content by CID.
/// </summary>
public async Task<Pin> PinAsync(
PinRequest request,
CancellationToken cancellationToken = default)
{
return await PostAsync<Pin>("/pins", request, cancellationToken);
}
/// <summary>
/// Unpin content.
/// </summary>
public async Task UnpinAsync(
string cid,
CancellationToken cancellationToken = default)
{
await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.DeleteAsync($"/pins/{cid}", cancellationToken);
await EnsureSuccessAsync(response);
return true;
});
}
/// <summary>
/// Get pin status.
/// </summary>
public async Task<Pin> GetPinStatusAsync(
string cid,
CancellationToken cancellationToken = default)
{
return await GetAsync<Pin>($"/pins/{cid}", cancellationToken);
}
/// <summary>
/// List pins.
/// </summary>
public async Task<List<Pin>> ListPinsAsync(
PinStatus? status = null,
int limit = 20,
int offset = 0,
CancellationToken cancellationToken = default)
{
var query = $"/pins?limit={limit}&offset={offset}";
if (status.HasValue)
{
query += $"&status={status.Value.ToString().ToLower()}";
}
return await GetAsync<List<Pin>>(query, cancellationToken);
}
/// <summary>
/// Create a CAR file from entries.
/// </summary>
public async Task<CarFile> CreateCarAsync(
IEnumerable<FileEntry> entries,
CancellationToken cancellationToken = default)
{
var request = entries.Select(e => new
{
path = e.Path,
content = Convert.ToBase64String(e.Content),
content_type = e.ContentType
}).ToList();
return await PostAsync<CarFile>("/car/create", request, cancellationToken);
}
/// <summary>
/// Import a CAR file.
/// </summary>
public async Task<List<string>> ImportCarAsync(
byte[] carData,
CancellationToken cancellationToken = default)
{
using var content = new ByteArrayContent(carData);
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/vnd.ipld.car");
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.PostAsync("/car/import", content, cancellationToken);
await EnsureSuccessAsync(response);
var result = await response.Content.ReadFromJsonAsync<JsonElement>(_jsonOptions, cancellationToken);
return result.GetProperty("cids").EnumerateArray()
.Select(e => e.GetString()!)
.ToList();
});
}
/// <summary>
/// Export content as a CAR file.
/// </summary>
public async Task<byte[]> ExportCarAsync(
string cid,
CancellationToken cancellationToken = default)
{
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.GetAsync($"/car/export/{cid}", cancellationToken);
await EnsureSuccessAsync(response);
return await response.Content.ReadAsByteArrayAsync(cancellationToken);
});
}
/// <summary>
/// List directory contents.
/// </summary>
public async Task<List<DirectoryEntry>> ListDirectoryAsync(
string cid,
CancellationToken cancellationToken = default)
{
return await GetAsync<List<DirectoryEntry>>($"/directory/{cid}", cancellationToken);
}
/// <summary>
/// Get storage statistics.
/// </summary>
public async Task<StorageStats> GetStatsAsync(CancellationToken cancellationToken = default)
{
return await GetAsync<StorageStats>("/stats", cancellationToken);
}
private async Task<T> GetAsync<T>(string path, CancellationToken cancellationToken)
{
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.GetAsync(path, cancellationToken);
await EnsureSuccessAsync(response);
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
?? throw new StorageException("Invalid response");
});
}
private async Task<T> PostAsync<T>(string path, object body, CancellationToken cancellationToken)
{
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, cancellationToken);
await EnsureSuccessAsync(response);
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
?? throw new StorageException("Invalid response");
});
}
private async Task EnsureSuccessAsync(HttpResponseMessage response)
{
if (!response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
throw new StorageException(
$"HTTP error: {content}",
statusCode: (int)response.StatusCode);
}
}
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation)
{
Exception? lastException = null;
for (int attempt = 0; attempt < _config.Retries; attempt++)
{
try
{
return await operation();
}
catch (Exception ex)
{
lastException = ex;
if (_config.Debug)
{
Console.WriteLine($"Attempt {attempt + 1} failed: {ex.Message}");
}
if (attempt < _config.Retries - 1)
{
await Task.Delay(TimeSpan.FromSeconds(attempt + 1));
}
}
}
throw lastException ?? new StorageException("Unknown error after retries");
}
public void Dispose()
{
if (!_disposed)
{
_httpClient.Dispose();
_disposed = true;
}
GC.SuppressFinalize(this);
}
}

View file

@ -0,0 +1,139 @@
using System.Text.Json.Serialization;
namespace Synor.Sdk.Storage;
/// <summary>
/// Storage network.
/// </summary>
public enum StorageNetwork
{
[JsonPropertyName("mainnet")] Mainnet,
[JsonPropertyName("testnet")] Testnet,
[JsonPropertyName("devnet")] Devnet
}
/// <summary>
/// Pin status.
/// </summary>
public enum PinStatus
{
[JsonPropertyName("queued")] Queued,
[JsonPropertyName("pinning")] Pinning,
[JsonPropertyName("pinned")] Pinned,
[JsonPropertyName("failed")] Failed
}
/// <summary>
/// Storage configuration.
/// </summary>
public class StorageConfig
{
public required string ApiKey { get; init; }
public string Endpoint { get; init; } = "https://storage.synor.io/v1";
public string Gateway { get; init; } = "https://gateway.synor.io";
public StorageNetwork Network { get; init; } = StorageNetwork.Mainnet;
public TimeSpan Timeout { get; init; } = TimeSpan.FromMinutes(5);
public int Retries { get; init; } = 3;
public bool Debug { get; init; } = false;
}
/// <summary>
/// Upload options.
/// </summary>
public class UploadOptions
{
public string? Name { get; init; }
public string? ContentType { get; init; }
public bool Pin { get; init; } = true;
public Dictionary<string, string>? Metadata { get; init; }
public bool WrapWithDirectory { get; init; } = false;
}
/// <summary>
/// Upload response.
/// </summary>
public record UploadResponse(
[property: JsonPropertyName("cid")] string Cid,
[property: JsonPropertyName("size")] long Size,
[property: JsonPropertyName("name")] string? Name = null,
[property: JsonPropertyName("pinned")] bool Pinned = true
);
/// <summary>
/// Pin request.
/// </summary>
public class PinRequest
{
public required string Cid { get; init; }
public string? Name { get; init; }
public Dictionary<string, string>? Metadata { get; init; }
public string[]? Origins { get; init; }
}
/// <summary>
/// Pin information.
/// </summary>
public record Pin(
[property: JsonPropertyName("cid")] string Cid,
[property: JsonPropertyName("name")] string? Name,
[property: JsonPropertyName("status")] PinStatus Status,
[property: JsonPropertyName("size")] long Size,
[property: JsonPropertyName("created_at")] DateTime CreatedAt,
[property: JsonPropertyName("metadata")] Dictionary<string, string>? Metadata = null
);
/// <summary>
/// File entry for directory operations.
/// </summary>
public class FileEntry
{
public required string Path { get; init; }
public required byte[] Content { get; init; }
public string? ContentType { get; init; }
}
/// <summary>
/// Directory entry.
/// </summary>
public record DirectoryEntry(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("cid")] string Cid,
[property: JsonPropertyName("size")] long Size,
[property: JsonPropertyName("type")] string Type
);
/// <summary>
/// CAR file.
/// </summary>
public record CarFile(
[property: JsonPropertyName("root_cid")] string RootCid,
[property: JsonPropertyName("data")] byte[] Data,
[property: JsonPropertyName("size")] long Size
);
/// <summary>
/// Storage statistics.
/// </summary>
public record StorageStats(
[property: JsonPropertyName("total_size")] long TotalSize,
[property: JsonPropertyName("pin_count")] int PinCount,
[property: JsonPropertyName("bandwidth_used")] long BandwidthUsed,
[property: JsonPropertyName("storage_limit")] long StorageLimit,
[property: JsonPropertyName("bandwidth_limit")] long BandwidthLimit
);
/// <summary>
/// Storage exception.
/// </summary>
public class StorageException : Exception
{
public string? Code { get; }
public int? StatusCode { get; }
public StorageException(string message, string? code = null, int? statusCode = null)
: base(message)
{
Code = code;
StatusCode = statusCode;
}
}

View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.1.0</Version>
<Authors>Synor</Authors>
<Company>Synor</Company>
<Description>C# SDK for Synor - Compute, Wallet, RPC, and Storage</Description>
<PackageId>Synor.Sdk</PackageId>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://synor.cc</PackageProjectUrl>
<RepositoryUrl>https://github.com/synor/synor</RepositoryUrl>
<PackageTags>synor;blockchain;wallet;rpc;storage;ipfs</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="8.0.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,256 @@
using System.Net.Http.Json;
using System.Text.Json;
namespace Synor.Sdk.Wallet;
/// <summary>
/// Synor Wallet SDK client for C#.
///
/// Provides key management, transaction signing, and balance queries
/// for the Synor blockchain.
/// </summary>
/// <example>
/// <code>
/// var wallet = new SynorWallet(new WalletConfig { ApiKey = "your-api-key" });
///
/// // Create a new wallet
/// var result = await wallet.CreateWalletAsync();
/// Console.WriteLine($"Wallet ID: {result.Wallet.Id}");
/// Console.WriteLine($"Mnemonic: {result.Mnemonic}");
///
/// // Get balance
/// var balance = await wallet.GetBalanceAsync(result.Wallet.Addresses[0].AddressString);
/// Console.WriteLine($"Balance: {balance.Total}");
///
/// wallet.Dispose();
/// </code>
/// </example>
public class SynorWallet : IDisposable
{
private readonly WalletConfig _config;
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _jsonOptions;
private bool _disposed;
public SynorWallet(WalletConfig config)
{
_config = config;
_httpClient = new HttpClient
{
BaseAddress = new Uri(config.Endpoint),
Timeout = config.Timeout
};
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}");
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
};
}
/// <summary>
/// Create a new wallet.
/// </summary>
public async Task<CreateWalletResult> CreateWalletAsync(
WalletType type = WalletType.Standard,
CancellationToken cancellationToken = default)
{
var request = new { type = type.ToString().ToLower(), network = _config.Network.ToString().ToLower() };
return await PostAsync<CreateWalletResult>("/wallets", request, cancellationToken);
}
/// <summary>
/// Import a wallet from mnemonic.
/// </summary>
public async Task<Wallet> ImportWalletAsync(
string mnemonic,
string? passphrase = null,
CancellationToken cancellationToken = default)
{
var request = new
{
mnemonic,
passphrase,
network = _config.Network.ToString().ToLower()
};
return await PostAsync<Wallet>("/wallets/import", request, cancellationToken);
}
/// <summary>
/// Get a wallet by ID.
/// </summary>
public async Task<Wallet> GetWalletAsync(
string walletId,
CancellationToken cancellationToken = default)
{
return await GetAsync<Wallet>($"/wallets/{walletId}", cancellationToken);
}
/// <summary>
/// Generate a new address for a wallet.
/// </summary>
public async Task<Address> GenerateAddressAsync(
string walletId,
bool isChange = false,
CancellationToken cancellationToken = default)
{
var request = new { is_change = isChange };
return await PostAsync<Address>($"/wallets/{walletId}/addresses", request, cancellationToken);
}
/// <summary>
/// Get a stealth address for privacy transactions.
/// </summary>
public async Task<StealthAddress> GetStealthAddressAsync(
string walletId,
CancellationToken cancellationToken = default)
{
return await GetAsync<StealthAddress>($"/wallets/{walletId}/stealth-address", cancellationToken);
}
/// <summary>
/// Sign a transaction.
/// </summary>
public async Task<SignedTransaction> SignTransactionAsync(
string walletId,
Transaction transaction,
CancellationToken cancellationToken = default)
{
var request = new { wallet_id = walletId, transaction };
return await PostAsync<SignedTransaction>("/transactions/sign", request, cancellationToken);
}
/// <summary>
/// Sign a message.
/// </summary>
public async Task<Signature> SignMessageAsync(
string walletId,
string message,
int addressIndex = 0,
CancellationToken cancellationToken = default)
{
var request = new { wallet_id = walletId, message, address_index = addressIndex };
return await PostAsync<Signature>("/messages/sign", request, cancellationToken);
}
/// <summary>
/// Verify a message signature.
/// </summary>
public async Task<bool> VerifyMessageAsync(
string message,
string signature,
string address,
CancellationToken cancellationToken = default)
{
var request = new { message, signature, address };
var response = await PostAsync<JsonElement>("/messages/verify", request, cancellationToken);
return response.GetProperty("valid").GetBoolean();
}
/// <summary>
/// Get balance for an address.
/// </summary>
public async Task<Balance> GetBalanceAsync(
string address,
CancellationToken cancellationToken = default)
{
return await GetAsync<Balance>($"/addresses/{address}/balance", cancellationToken);
}
/// <summary>
/// Get UTXOs for an address.
/// </summary>
public async Task<List<UTXO>> GetUTXOsAsync(
string address,
int minConfirmations = 1,
CancellationToken cancellationToken = default)
{
return await GetAsync<List<UTXO>>(
$"/addresses/{address}/utxos?min_confirmations={minConfirmations}",
cancellationToken);
}
/// <summary>
/// Estimate transaction fee.
/// </summary>
public async Task<long> EstimateFeeAsync(
Priority priority = Priority.Medium,
CancellationToken cancellationToken = default)
{
var response = await GetAsync<JsonElement>(
$"/fees/estimate?priority={priority.ToString().ToLower()}",
cancellationToken);
return response.GetProperty("fee_per_byte").GetInt64();
}
private async Task<T> GetAsync<T>(string path, CancellationToken cancellationToken)
{
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.GetAsync(path, cancellationToken);
await EnsureSuccessAsync(response);
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
?? throw new WalletException("Invalid response");
});
}
private async Task<T> PostAsync<T>(string path, object body, CancellationToken cancellationToken)
{
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, cancellationToken);
await EnsureSuccessAsync(response);
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
?? throw new WalletException("Invalid response");
});
}
private async Task EnsureSuccessAsync(HttpResponseMessage response)
{
if (!response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
throw new WalletException(
$"HTTP error: {content}",
statusCode: (int)response.StatusCode);
}
}
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation)
{
Exception? lastException = null;
for (int attempt = 0; attempt < _config.Retries; attempt++)
{
try
{
return await operation();
}
catch (Exception ex)
{
lastException = ex;
if (_config.Debug)
{
Console.WriteLine($"Attempt {attempt + 1} failed: {ex.Message}");
}
if (attempt < _config.Retries - 1)
{
await Task.Delay(TimeSpan.FromSeconds(attempt + 1));
}
}
}
throw lastException ?? new WalletException("Unknown error after retries");
}
public void Dispose()
{
if (!_disposed)
{
_httpClient.Dispose();
_disposed = true;
}
GC.SuppressFinalize(this);
}
}

View file

@ -0,0 +1,167 @@
using System.Text.Json.Serialization;
namespace Synor.Sdk.Wallet;
/// <summary>
/// Network environment for wallet operations.
/// </summary>
public enum Network
{
[JsonPropertyName("mainnet")] Mainnet,
[JsonPropertyName("testnet")] Testnet,
[JsonPropertyName("devnet")] Devnet
}
/// <summary>
/// Type of wallet to create.
/// </summary>
public enum WalletType
{
[JsonPropertyName("standard")] Standard,
[JsonPropertyName("multisig")] Multisig,
[JsonPropertyName("hardware")] Hardware,
[JsonPropertyName("stealth")] Stealth
}
/// <summary>
/// Transaction priority for fee estimation.
/// </summary>
public enum Priority
{
[JsonPropertyName("low")] Low,
[JsonPropertyName("medium")] Medium,
[JsonPropertyName("high")] High
}
/// <summary>
/// Configuration for the wallet client.
/// </summary>
public class WalletConfig
{
public required string ApiKey { get; init; }
public string Endpoint { get; init; } = "https://wallet.synor.io/v1";
public Network Network { get; init; } = Network.Mainnet;
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
public int Retries { get; init; } = 3;
public bool Debug { get; init; } = false;
}
/// <summary>
/// A wallet address.
/// </summary>
public record Address(
[property: JsonPropertyName("address")] string AddressString,
[property: JsonPropertyName("index")] int Index,
[property: JsonPropertyName("is_change")] bool IsChange = false
);
/// <summary>
/// A Synor wallet with addresses and keys.
/// </summary>
public record Wallet(
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("type")] WalletType Type,
[property: JsonPropertyName("network")] Network Network,
[property: JsonPropertyName("addresses")] List<Address> Addresses,
[property: JsonPropertyName("created_at")] string CreatedAt
);
/// <summary>
/// Stealth address for privacy transactions.
/// </summary>
public record StealthAddress(
[property: JsonPropertyName("spend_public_key")] string SpendPublicKey,
[property: JsonPropertyName("view_public_key")] string ViewPublicKey,
[property: JsonPropertyName("one_time_address")] string OneTimeAddress
);
/// <summary>
/// Result of wallet creation.
/// </summary>
public record CreateWalletResult(
[property: JsonPropertyName("wallet")] Wallet Wallet,
[property: JsonPropertyName("mnemonic")] string? Mnemonic = null
);
/// <summary>
/// Transaction input (UTXO reference).
/// </summary>
public record TransactionInput(
[property: JsonPropertyName("txid")] string Txid,
[property: JsonPropertyName("vout")] int Vout,
[property: JsonPropertyName("amount")] long Amount
);
/// <summary>
/// Transaction output.
/// </summary>
public record TransactionOutput(
[property: JsonPropertyName("address")] string Address,
[property: JsonPropertyName("amount")] long Amount
);
/// <summary>
/// Transaction for signing.
/// </summary>
public record Transaction(
List<TransactionInput> Inputs,
List<TransactionOutput> Outputs,
long? Fee = null,
Priority Priority = Priority.Medium
);
/// <summary>
/// Signed transaction ready for broadcast.
/// </summary>
public record SignedTransaction(
[property: JsonPropertyName("txid")] string Txid,
[property: JsonPropertyName("hex")] string Hex,
[property: JsonPropertyName("size")] int Size,
[property: JsonPropertyName("fee")] long Fee
);
/// <summary>
/// Cryptographic signature.
/// </summary>
public record Signature(
[property: JsonPropertyName("signature")] string SignatureString,
[property: JsonPropertyName("address")] string Address,
[property: JsonPropertyName("recovery_id")] int? RecoveryId = null
);
/// <summary>
/// Unspent transaction output.
/// </summary>
public record UTXO(
[property: JsonPropertyName("txid")] string Txid,
[property: JsonPropertyName("vout")] int Vout,
[property: JsonPropertyName("amount")] long Amount,
[property: JsonPropertyName("address")] string Address,
[property: JsonPropertyName("confirmations")] int Confirmations,
[property: JsonPropertyName("script_pubkey")] string? ScriptPubKey = null
);
/// <summary>
/// Wallet balance information.
/// </summary>
public record Balance(
[property: JsonPropertyName("confirmed")] long Confirmed,
[property: JsonPropertyName("unconfirmed")] long Unconfirmed,
[property: JsonPropertyName("total")] long Total
);
/// <summary>
/// Exception thrown by wallet operations.
/// </summary>
public class WalletException : Exception
{
public string? Code { get; }
public int? StatusCode { get; }
public WalletException(string message, string? code = null, int? statusCode = null)
: base(message)
{
Code = code;
StatusCode = statusCode;
}
}

View file

@ -404,7 +404,7 @@
"languageVersion": "3.4"
},
{
"name": "synor_compute",
"name": "synor_sdk",
"rootUri": "../",
"packageUri": "lib/",
"languageVersion": "3.0"

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,335 @@
/// Synor RPC SDK client for Flutter/Dart.
library synor_rpc;
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:web_socket_channel/web_socket_channel.dart';
import 'types.dart';
export 'types.dart';
/// Synor RPC SDK client for Flutter/Dart.
///
/// Provides blockchain queries, transaction submission, and real-time
/// subscriptions via WebSocket.
///
/// Example:
/// ```dart
/// final rpc = SynorRpc(RpcConfig(apiKey: 'your-api-key'));
///
/// // Get latest block
/// final block = await rpc.getLatestBlock();
/// print('Latest block: ${block.height}');
///
/// // Subscribe to new blocks
/// final subscription = rpc.subscribeBlocks((block) {
/// print('New block: ${block.height}');
/// });
///
/// // Later: cancel subscription
/// subscription.cancel();
/// rpc.close();
/// ```
class SynorRpc {
final RpcConfig config;
final http.Client _client;
WebSocketChannel? _wsChannel;
StreamSubscription? _wsSubscription;
final Map<String, void Function(String)> _subscriptions = {};
SynorRpc(this.config) : _client = http.Client();
// Block operations
/// Get the latest block.
Future<Block> getLatestBlock() async {
return _get('/blocks/latest', Block.fromJson);
}
/// Get a block by hash or height.
Future<Block> getBlock(String hashOrHeight) async {
return _get('/blocks/$hashOrHeight', Block.fromJson);
}
/// Get a block header by hash or height.
Future<BlockHeader> getBlockHeader(String hashOrHeight) async {
return _get('/blocks/$hashOrHeight/header', BlockHeader.fromJson);
}
/// Get blocks in a range.
Future<List<Block>> getBlocks(int startHeight, int endHeight) async {
return _getList(
'/blocks?start=$startHeight&end=$endHeight',
Block.fromJson,
);
}
// Transaction operations
/// Get a transaction by ID.
Future<RpcTransaction> getTransaction(String txid) async {
return _get('/transactions/$txid', RpcTransaction.fromJson);
}
/// Get raw transaction hex.
Future<String> getRawTransaction(String txid) async {
final response = await _get<Map<String, dynamic>>(
'/transactions/$txid/raw',
(json) => json,
);
return response['hex'] as String;
}
/// Send a raw transaction.
Future<SubmitResult> sendRawTransaction(String hex) async {
return _post('/transactions/send', {'hex': hex}, SubmitResult.fromJson);
}
/// Decode a raw transaction without broadcasting.
Future<RpcTransaction> decodeRawTransaction(String hex) async {
return _post('/transactions/decode', {'hex': hex}, RpcTransaction.fromJson);
}
/// Get transactions for an address.
Future<List<RpcTransaction>> getAddressTransactions(
String address, {
int limit = 50,
int offset = 0,
}) async {
return _getList(
'/addresses/$address/transactions?limit=$limit&offset=$offset',
RpcTransaction.fromJson,
);
}
// Fee estimation
/// Estimate fee for a given priority.
Future<FeeEstimate> estimateFee({RpcPriority priority = RpcPriority.medium}) async {
return _get(
'/fees/estimate?priority=${priority.toJson()}',
FeeEstimate.fromJson,
);
}
/// Get all fee estimates.
Future<Map<RpcPriority, FeeEstimate>> getAllFeeEstimates() async {
final estimates = await _getList('/fees/estimates', FeeEstimate.fromJson);
return {for (var e in estimates) e.priority: e};
}
// Chain information
/// Get chain information.
Future<ChainInfo> getChainInfo() async {
return _get('/chain/info', ChainInfo.fromJson);
}
/// Get mempool information.
Future<MempoolInfo> getMempoolInfo() async {
return _get('/mempool/info', MempoolInfo.fromJson);
}
/// Get mempool transactions.
Future<List<String>> getMempoolTransactions({int limit = 100}) async {
return _getList('/mempool/transactions?limit=$limit', (json) => json['txid'] as String);
}
// WebSocket subscriptions
/// Subscribe to new blocks.
Subscription subscribeBlocks(void Function(Block) callback) {
return _subscribe('blocks', null, (data) {
final block = Block.fromJson(jsonDecode(data) as Map<String, dynamic>);
callback(block);
});
}
/// Subscribe to transactions for a specific address.
Subscription subscribeAddress(String address, void Function(RpcTransaction) callback) {
return _subscribe('address', address, (data) {
final tx = RpcTransaction.fromJson(jsonDecode(data) as Map<String, dynamic>);
callback(tx);
});
}
/// Subscribe to mempool transactions.
Subscription subscribeMempool(void Function(RpcTransaction) callback) {
return _subscribe('mempool', null, (data) {
final tx = RpcTransaction.fromJson(jsonDecode(data) as Map<String, dynamic>);
callback(tx);
});
}
Subscription _subscribe(
String channel,
String? filter,
void Function(String) callback,
) {
_ensureWebSocketConnection();
final subscriptionId = DateTime.now().millisecondsSinceEpoch.toString();
_subscriptions[subscriptionId] = callback;
final message = jsonEncode({
'type': 'subscribe',
'channel': channel,
'filter': filter,
});
_wsChannel?.sink.add(message);
return Subscription(
id: subscriptionId,
channel: channel,
onCancel: () {
_subscriptions.remove(subscriptionId);
final unsubMessage = jsonEncode({
'type': 'unsubscribe',
'channel': channel,
});
_wsChannel?.sink.add(unsubMessage);
},
);
}
void _ensureWebSocketConnection() {
if (_wsChannel != null) return;
final uri = Uri.parse('${config.wsEndpoint}?token=${config.apiKey}');
_wsChannel = WebSocketChannel.connect(uri);
_wsSubscription = _wsChannel!.stream.listen(
(message) {
try {
final data = jsonDecode(message as String) as Map<String, dynamic>;
if (data['type'] == 'data' && data['data'] != null) {
final messageData = data['data'] as String;
for (final callback in _subscriptions.values) {
callback(messageData);
}
}
} catch (e) {
if (config.debug) {
print('Failed to parse WS message: $e');
}
}
},
onError: (error) {
if (config.debug) {
print('WebSocket error: $error');
}
},
onDone: () {
_wsChannel = null;
_wsSubscription = null;
},
);
}
// HTTP helper methods
Future<T> _get<T>(String path, T Function(Map<String, dynamic>) fromJson) async {
return _executeWithRetry(() async {
final response = await _client
.get(
Uri.parse('${config.endpoint}$path'),
headers: _headers,
)
.timeout(config.timeout);
return _handleResponse(response, fromJson);
});
}
Future<List<T>> _getList<T>(
String path,
T Function(Map<String, dynamic>) fromJson,
) async {
return _executeWithRetry(() async {
final response = await _client
.get(
Uri.parse('${config.endpoint}$path'),
headers: _headers,
)
.timeout(config.timeout);
if (response.statusCode >= 200 && response.statusCode < 300) {
final List<dynamic> jsonList = jsonDecode(response.body);
return jsonList
.map((e) => fromJson(e as Map<String, dynamic>))
.toList();
}
throw RpcException(
'HTTP error: ${response.body}',
statusCode: response.statusCode,
);
});
}
Future<T> _post<T>(
String path,
Map<String, dynamic> body,
T Function(Map<String, dynamic>) fromJson,
) async {
return _executeWithRetry(() async {
final response = await _client
.post(
Uri.parse('${config.endpoint}$path'),
headers: _headers,
body: jsonEncode(body),
)
.timeout(config.timeout);
return _handleResponse(response, fromJson);
});
}
Map<String, String> get _headers => {
'Authorization': 'Bearer ${config.apiKey}',
'Content-Type': 'application/json',
};
T _handleResponse<T>(
http.Response response,
T Function(Map<String, dynamic>) fromJson,
) {
if (response.statusCode >= 200 && response.statusCode < 300) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return fromJson(json);
}
throw RpcException(
'HTTP error: ${response.body}',
statusCode: response.statusCode,
);
}
Future<T> _executeWithRetry<T>(Future<T> Function() operation) async {
Exception? lastError;
for (var attempt = 0; attempt < config.retries; attempt++) {
try {
return await operation();
} catch (e) {
lastError = e as Exception;
if (config.debug) {
print('Attempt ${attempt + 1} failed: $e');
}
if (attempt < config.retries - 1) {
await Future.delayed(Duration(seconds: attempt + 1));
}
}
}
throw lastError ?? RpcException('Unknown error after ${config.retries} retries');
}
/// Close the client and release resources.
void close() {
_wsSubscription?.cancel();
_wsChannel?.sink.close();
_client.close();
}
}

View file

@ -0,0 +1,424 @@
/// Types for Synor RPC SDK.
library synor_rpc_types;
/// Network environment.
enum RpcNetwork {
mainnet,
testnet,
devnet;
String toJson() => name;
static RpcNetwork fromJson(String json) {
return RpcNetwork.values.firstWhere(
(e) => e.name == json,
orElse: () => RpcNetwork.mainnet,
);
}
}
/// Transaction priority for fee estimation.
enum RpcPriority {
low,
medium,
high;
String toJson() => name;
static RpcPriority fromJson(String json) {
return RpcPriority.values.firstWhere(
(e) => e.name == json,
orElse: () => RpcPriority.medium,
);
}
}
/// Transaction status.
enum TransactionStatus {
pending,
confirmed,
failed;
String toJson() => name;
static TransactionStatus fromJson(String json) {
return TransactionStatus.values.firstWhere(
(e) => e.name == json,
orElse: () => TransactionStatus.pending,
);
}
}
/// Configuration for the RPC client.
class RpcConfig {
final String apiKey;
final String endpoint;
final String wsEndpoint;
final RpcNetwork network;
final Duration timeout;
final int retries;
final bool debug;
const RpcConfig({
required this.apiKey,
this.endpoint = 'https://rpc.synor.io/v1',
this.wsEndpoint = 'wss://rpc.synor.io/v1/ws',
this.network = RpcNetwork.mainnet,
this.timeout = const Duration(seconds: 30),
this.retries = 3,
this.debug = false,
});
}
/// Block header information.
class BlockHeader {
final String hash;
final int height;
final String previousHash;
final int timestamp;
final int version;
final String merkleRoot;
final int nonce;
final double difficulty;
const BlockHeader({
required this.hash,
required this.height,
required this.previousHash,
required this.timestamp,
required this.version,
required this.merkleRoot,
required this.nonce,
required this.difficulty,
});
factory BlockHeader.fromJson(Map<String, dynamic> json) {
return BlockHeader(
hash: json['hash'] as String,
height: json['height'] as int,
previousHash: json['previous_hash'] as String,
timestamp: json['timestamp'] as int,
version: json['version'] as int,
merkleRoot: json['merkle_root'] as String,
nonce: json['nonce'] as int,
difficulty: (json['difficulty'] as num).toDouble(),
);
}
}
/// A blockchain block.
class Block {
final String hash;
final int height;
final String previousHash;
final int timestamp;
final int version;
final String merkleRoot;
final int nonce;
final double difficulty;
final List<RpcTransaction> transactions;
final int size;
final int weight;
const Block({
required this.hash,
required this.height,
required this.previousHash,
required this.timestamp,
required this.version,
required this.merkleRoot,
required this.nonce,
required this.difficulty,
required this.transactions,
required this.size,
required this.weight,
});
factory Block.fromJson(Map<String, dynamic> json) {
return Block(
hash: json['hash'] as String,
height: json['height'] as int,
previousHash: json['previous_hash'] as String,
timestamp: json['timestamp'] as int,
version: json['version'] as int,
merkleRoot: json['merkle_root'] as String,
nonce: json['nonce'] as int,
difficulty: (json['difficulty'] as num).toDouble(),
transactions: (json['transactions'] as List)
.map((e) => RpcTransaction.fromJson(e as Map<String, dynamic>))
.toList(),
size: json['size'] as int,
weight: json['weight'] as int,
);
}
}
/// A blockchain transaction.
class RpcTransaction {
final String txid;
final String hash;
final int version;
final int size;
final int weight;
final int lockTime;
final List<RpcTransactionInput> inputs;
final List<RpcTransactionOutput> outputs;
final int fee;
final int confirmations;
final String? blockHash;
final int? blockHeight;
final int? timestamp;
final TransactionStatus status;
const RpcTransaction({
required this.txid,
required this.hash,
required this.version,
required this.size,
required this.weight,
required this.lockTime,
required this.inputs,
required this.outputs,
required this.fee,
required this.confirmations,
this.blockHash,
this.blockHeight,
this.timestamp,
this.status = TransactionStatus.pending,
});
factory RpcTransaction.fromJson(Map<String, dynamic> json) {
return RpcTransaction(
txid: json['txid'] as String,
hash: json['hash'] as String,
version: json['version'] as int,
size: json['size'] as int,
weight: json['weight'] as int,
lockTime: json['lock_time'] as int,
inputs: (json['inputs'] as List)
.map((e) => RpcTransactionInput.fromJson(e as Map<String, dynamic>))
.toList(),
outputs: (json['outputs'] as List)
.map((e) => RpcTransactionOutput.fromJson(e as Map<String, dynamic>))
.toList(),
fee: json['fee'] as int,
confirmations: json['confirmations'] as int,
blockHash: json['block_hash'] as String?,
blockHeight: json['block_height'] as int?,
timestamp: json['timestamp'] as int?,
status: TransactionStatus.fromJson(json['status'] as String? ?? 'pending'),
);
}
}
/// Transaction input.
class RpcTransactionInput {
final String txid;
final int vout;
final String? scriptSig;
final int sequence;
final List<String>? witness;
const RpcTransactionInput({
required this.txid,
required this.vout,
this.scriptSig,
required this.sequence,
this.witness,
});
factory RpcTransactionInput.fromJson(Map<String, dynamic> json) {
return RpcTransactionInput(
txid: json['txid'] as String,
vout: json['vout'] as int,
scriptSig: json['script_sig'] as String?,
sequence: json['sequence'] as int,
witness: (json['witness'] as List?)?.cast<String>(),
);
}
}
/// Transaction output.
class RpcTransactionOutput {
final int value;
final int n;
final ScriptPubKey scriptPubKey;
const RpcTransactionOutput({
required this.value,
required this.n,
required this.scriptPubKey,
});
factory RpcTransactionOutput.fromJson(Map<String, dynamic> json) {
return RpcTransactionOutput(
value: json['value'] as int,
n: json['n'] as int,
scriptPubKey:
ScriptPubKey.fromJson(json['script_pubkey'] as Map<String, dynamic>),
);
}
}
/// Script public key.
class ScriptPubKey {
final String asm;
final String hex;
final String type;
final String? address;
const ScriptPubKey({
required this.asm,
required this.hex,
required this.type,
this.address,
});
factory ScriptPubKey.fromJson(Map<String, dynamic> json) {
return ScriptPubKey(
asm: json['asm'] as String,
hex: json['hex'] as String,
type: json['type'] as String,
address: json['address'] as String?,
);
}
}
/// Chain information.
class ChainInfo {
final String chain;
final int blocks;
final int headers;
final String bestBlockHash;
final double difficulty;
final int medianTime;
final double verificationProgress;
final String chainWork;
final bool pruned;
const ChainInfo({
required this.chain,
required this.blocks,
required this.headers,
required this.bestBlockHash,
required this.difficulty,
required this.medianTime,
required this.verificationProgress,
required this.chainWork,
required this.pruned,
});
factory ChainInfo.fromJson(Map<String, dynamic> json) {
return ChainInfo(
chain: json['chain'] as String,
blocks: json['blocks'] as int,
headers: json['headers'] as int,
bestBlockHash: json['best_block_hash'] as String,
difficulty: (json['difficulty'] as num).toDouble(),
medianTime: json['median_time'] as int,
verificationProgress: (json['verification_progress'] as num).toDouble(),
chainWork: json['chain_work'] as String,
pruned: json['pruned'] as bool,
);
}
}
/// Mempool information.
class MempoolInfo {
final int size;
final int bytes;
final int usage;
final int maxMempool;
final double mempoolMinFee;
final double minRelayTxFee;
const MempoolInfo({
required this.size,
required this.bytes,
required this.usage,
required this.maxMempool,
required this.mempoolMinFee,
required this.minRelayTxFee,
});
factory MempoolInfo.fromJson(Map<String, dynamic> json) {
return MempoolInfo(
size: json['size'] as int,
bytes: json['bytes'] as int,
usage: json['usage'] as int,
maxMempool: json['max_mempool'] as int,
mempoolMinFee: (json['mempool_min_fee'] as num).toDouble(),
minRelayTxFee: (json['min_relay_tx_fee'] as num).toDouble(),
);
}
}
/// Fee estimate for transactions.
class FeeEstimate {
final RpcPriority priority;
final int feeRate;
final int estimatedBlocks;
const FeeEstimate({
required this.priority,
required this.feeRate,
required this.estimatedBlocks,
});
factory FeeEstimate.fromJson(Map<String, dynamic> json) {
return FeeEstimate(
priority: RpcPriority.fromJson(json['priority'] as String),
feeRate: json['fee_rate'] as int,
estimatedBlocks: json['estimated_blocks'] as int,
);
}
}
/// Result of transaction submission.
class SubmitResult {
final String txid;
final bool accepted;
final String? reason;
const SubmitResult({
required this.txid,
required this.accepted,
this.reason,
});
factory SubmitResult.fromJson(Map<String, dynamic> json) {
return SubmitResult(
txid: json['txid'] as String,
accepted: json['accepted'] as bool,
reason: json['reason'] as String?,
);
}
}
/// Subscription handle for WebSocket events.
class Subscription {
final String id;
final String channel;
final void Function() _onCancel;
Subscription({
required this.id,
required this.channel,
required void Function() onCancel,
}) : _onCancel = onCancel;
/// Cancel the subscription.
void cancel() => _onCancel();
}
/// Exception thrown by RPC operations.
class RpcException implements Exception {
final String message;
final String? code;
final int? statusCode;
const RpcException(this.message, {this.code, this.statusCode});
@override
String toString() => 'RpcException: $message';
}

View file

@ -0,0 +1,436 @@
/// Synor Storage SDK client for Flutter/Dart.
library synor_storage;
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:http/http.dart' as http;
import 'types.dart';
export 'types.dart';
/// Synor Storage SDK client for Flutter/Dart.
///
/// Provides IPFS-compatible decentralized storage with upload, download,
/// pinning, and CAR file operations.
///
/// Example:
/// ```dart
/// final storage = SynorStorage(StorageConfig(apiKey: 'your-api-key'));
///
/// // Upload a file
/// final data = utf8.encode('Hello, World!');
/// final result = await storage.upload(
/// Uint8List.fromList(data),
/// options: UploadOptions(name: 'hello.txt'),
/// );
/// print('CID: ${result.cid}');
///
/// // Download content
/// final content = await storage.download(result.cid);
/// print('Content: ${utf8.decode(content)}');
///
/// // Pin content
/// final pin = await storage.pin(PinRequest(cid: result.cid, durationDays: 30));
/// print('Pin status: ${pin.status}');
///
/// storage.close();
/// ```
class SynorStorage {
final StorageConfig config;
final http.Client _client;
SynorStorage(this.config) : _client = http.Client();
// Upload operations
/// Upload data to storage.
Future<UploadResponse> upload(
Uint8List data, {
UploadOptions options = const UploadOptions(),
}) async {
return _executeWithRetry(() async {
final request = http.MultipartRequest(
'POST',
Uri.parse('${config.endpoint}/upload'),
);
request.headers['Authorization'] = 'Bearer ${config.apiKey}';
request.files.add(http.MultipartFile.fromBytes(
'file',
data,
filename: options.name ?? 'file',
));
if (options.name != null) {
request.fields['name'] = options.name!;
}
request.fields['wrap_with_directory'] = options.wrapWithDirectory.toString();
request.fields['hash_algorithm'] = options.hashAlgorithm.toJson();
if (options.chunkSize != null) {
request.fields['chunk_size'] = options.chunkSize.toString();
}
if (options.pinDurationDays != null) {
request.fields['pin_duration_days'] = options.pinDurationDays.toString();
}
final streamedResponse = await request.send().timeout(config.timeout);
final response = await http.Response.fromStream(streamedResponse);
if (response.statusCode >= 200 && response.statusCode < 300) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return UploadResponse.fromJson(json);
}
throw StorageException(
'Upload failed: ${response.body}',
statusCode: response.statusCode,
);
});
}
/// Upload multiple files as a directory.
Future<UploadResponse> uploadDirectory(
List<FileEntry> files, {
String? dirName,
}) async {
return _executeWithRetry(() async {
final request = http.MultipartRequest(
'POST',
Uri.parse('${config.endpoint}/upload/directory'),
);
request.headers['Authorization'] = 'Bearer ${config.apiKey}';
for (final file in files) {
request.files.add(http.MultipartFile.fromBytes(
'files',
file.content,
filename: file.path,
));
}
if (dirName != null) {
request.fields['name'] = dirName;
}
final streamedResponse = await request.send().timeout(config.timeout);
final response = await http.Response.fromStream(streamedResponse);
if (response.statusCode >= 200 && response.statusCode < 300) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return UploadResponse.fromJson(json);
}
throw StorageException(
'Directory upload failed: ${response.body}',
statusCode: response.statusCode,
);
});
}
// Download operations
/// Download content by CID.
Future<Uint8List> download(String cid) async {
return _executeWithRetry(() async {
final response = await _client
.get(
Uri.parse('${config.endpoint}/content/$cid'),
headers: {'Authorization': 'Bearer ${config.apiKey}'},
)
.timeout(config.timeout);
if (response.statusCode >= 200 && response.statusCode < 300) {
return response.bodyBytes;
}
throw StorageException(
'Download failed: ${response.body}',
statusCode: response.statusCode,
);
});
}
/// Download content as a stream.
Stream<List<int>> downloadStream(String cid) async* {
final request = http.Request(
'GET',
Uri.parse('${config.endpoint}/content/$cid'),
);
request.headers['Authorization'] = 'Bearer ${config.apiKey}';
final response = await _client.send(request);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw StorageException(
'Download failed',
statusCode: response.statusCode,
);
}
yield* response.stream;
}
/// Get the gateway URL for content.
String getGatewayUrl(String cid, {String? path}) {
if (path != null) {
return '${config.gateway}/ipfs/$cid/$path';
}
return '${config.gateway}/ipfs/$cid';
}
// Pinning operations
/// Pin content to ensure availability.
Future<Pin> pin(PinRequest request) async {
return _post('/pins', request.toJson(), Pin.fromJson);
}
/// Unpin content.
Future<void> unpin(String cid) async {
await _delete('/pins/$cid');
}
/// Get pin status.
Future<Pin> getPinStatus(String cid) async {
return _get('/pins/$cid', Pin.fromJson);
}
/// List all pins.
Future<List<Pin>> listPins({
PinStatus? status,
int limit = 50,
int offset = 0,
}) async {
var path = '/pins?limit=$limit&offset=$offset';
if (status != null) {
path += '&status=${status.toJson()}';
}
return _getList(path, Pin.fromJson);
}
// CAR file operations
/// Create a CAR file from entries.
Future<CarFile> createCar(List<CarEntry> entries) async {
return _executeWithRetry(() async {
final request = http.MultipartRequest(
'POST',
Uri.parse('${config.endpoint}/car'),
);
request.headers['Authorization'] = 'Bearer ${config.apiKey}';
for (final entry in entries) {
request.files.add(http.MultipartFile.fromBytes(
'files',
entry.content,
filename: entry.path,
));
}
final streamedResponse = await request.send().timeout(config.timeout);
final response = await http.Response.fromStream(streamedResponse);
if (response.statusCode >= 200 && response.statusCode < 300) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return CarFile.fromJson(json);
}
throw StorageException(
'CAR creation failed: ${response.body}',
statusCode: response.statusCode,
);
});
}
/// Import a CAR file.
Future<List<String>> importCar(Uint8List carData) async {
return _executeWithRetry(() async {
final request = http.MultipartRequest(
'POST',
Uri.parse('${config.endpoint}/car/import'),
);
request.headers['Authorization'] = 'Bearer ${config.apiKey}';
request.files.add(http.MultipartFile.fromBytes(
'file',
carData,
filename: 'archive.car',
));
final streamedResponse = await request.send().timeout(config.timeout);
final response = await http.Response.fromStream(streamedResponse);
if (response.statusCode >= 200 && response.statusCode < 300) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return (json['cids'] as List).cast<String>();
}
throw StorageException(
'CAR import failed: ${response.body}',
statusCode: response.statusCode,
);
});
}
/// Export content as a CAR file.
Future<Uint8List> exportCar(String cid) async {
return _executeWithRetry(() async {
final response = await _client
.get(
Uri.parse('${config.endpoint}/car/$cid'),
headers: {'Authorization': 'Bearer ${config.apiKey}'},
)
.timeout(config.timeout);
if (response.statusCode >= 200 && response.statusCode < 300) {
return response.bodyBytes;
}
throw StorageException(
'CAR export failed: ${response.body}',
statusCode: response.statusCode,
);
});
}
// Directory operations
/// List directory contents.
Future<List<DirectoryEntry>> listDirectory(String cid) async {
return _getList('/content/$cid/ls', DirectoryEntry.fromJson);
}
// Statistics
/// Get storage statistics.
Future<StorageStats> getStats() async {
return _get('/stats', StorageStats.fromJson);
}
// HTTP helper methods
Future<T> _get<T>(String path, T Function(Map<String, dynamic>) fromJson) async {
return _executeWithRetry(() async {
final response = await _client
.get(
Uri.parse('${config.endpoint}$path'),
headers: _headers,
)
.timeout(config.timeout);
return _handleResponse(response, fromJson);
});
}
Future<List<T>> _getList<T>(
String path,
T Function(Map<String, dynamic>) fromJson,
) async {
return _executeWithRetry(() async {
final response = await _client
.get(
Uri.parse('${config.endpoint}$path'),
headers: _headers,
)
.timeout(config.timeout);
if (response.statusCode >= 200 && response.statusCode < 300) {
final List<dynamic> jsonList = jsonDecode(response.body);
return jsonList
.map((e) => fromJson(e as Map<String, dynamic>))
.toList();
}
throw StorageException(
'HTTP error: ${response.body}',
statusCode: response.statusCode,
);
});
}
Future<T> _post<T>(
String path,
Map<String, dynamic> body,
T Function(Map<String, dynamic>) fromJson,
) async {
return _executeWithRetry(() async {
final response = await _client
.post(
Uri.parse('${config.endpoint}$path'),
headers: _headers,
body: jsonEncode(body),
)
.timeout(config.timeout);
return _handleResponse(response, fromJson);
});
}
Future<void> _delete(String path) async {
return _executeWithRetry(() async {
final response = await _client
.delete(
Uri.parse('${config.endpoint}$path'),
headers: _headers,
)
.timeout(config.timeout);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw StorageException(
'Delete failed: ${response.body}',
statusCode: response.statusCode,
);
}
});
}
Map<String, String> get _headers => {
'Authorization': 'Bearer ${config.apiKey}',
'Content-Type': 'application/json',
};
T _handleResponse<T>(
http.Response response,
T Function(Map<String, dynamic>) fromJson,
) {
if (response.statusCode >= 200 && response.statusCode < 300) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return fromJson(json);
}
throw StorageException(
'HTTP error: ${response.body}',
statusCode: response.statusCode,
);
}
Future<T> _executeWithRetry<T>(Future<T> Function() operation) async {
Exception? lastError;
for (var attempt = 0; attempt < config.retries; attempt++) {
try {
return await operation();
} catch (e) {
lastError = e as Exception;
if (config.debug) {
print('Attempt ${attempt + 1} failed: $e');
}
if (attempt < config.retries - 1) {
await Future.delayed(Duration(seconds: attempt + 1));
}
}
}
throw lastError ?? StorageException('Unknown error after ${config.retries} retries');
}
/// Close the client and release resources.
void close() {
_client.close();
}
}

View file

@ -0,0 +1,279 @@
/// Types for Synor Storage SDK.
library synor_storage_types;
import 'dart:typed_data';
/// Status of a pinned item.
enum PinStatus {
queued,
pinning,
pinned,
failed,
unpinned;
String toJson() => name;
static PinStatus fromJson(String json) {
return PinStatus.values.firstWhere(
(e) => e.name == json,
orElse: () => PinStatus.queued,
);
}
}
/// Hash algorithm for content addressing.
enum HashAlgorithm {
sha2_256('sha2-256'),
blake3('blake3');
final String value;
const HashAlgorithm(this.value);
String toJson() => value;
static HashAlgorithm fromJson(String json) {
return HashAlgorithm.values.firstWhere(
(e) => e.value == json,
orElse: () => HashAlgorithm.sha2_256,
);
}
}
/// Type of directory entry.
enum EntryType {
file,
directory;
String toJson() => name;
static EntryType fromJson(String json) {
return EntryType.values.firstWhere(
(e) => e.name == json,
orElse: () => EntryType.file,
);
}
}
/// Configuration for the storage client.
class StorageConfig {
final String apiKey;
final String endpoint;
final String gateway;
final Duration timeout;
final int retries;
final bool debug;
const StorageConfig({
required this.apiKey,
this.endpoint = 'https://storage.synor.io/v1',
this.gateway = 'https://gateway.synor.io',
this.timeout = const Duration(seconds: 60),
this.retries = 3,
this.debug = false,
});
}
/// Options for file upload.
class UploadOptions {
final String? name;
final bool wrapWithDirectory;
final HashAlgorithm hashAlgorithm;
final int? chunkSize;
final int? pinDurationDays;
const UploadOptions({
this.name,
this.wrapWithDirectory = false,
this.hashAlgorithm = HashAlgorithm.sha2_256,
this.chunkSize,
this.pinDurationDays,
});
}
/// Response from upload operation.
class UploadResponse {
final String cid;
final int size;
final String? name;
final String createdAt;
const UploadResponse({
required this.cid,
required this.size,
this.name,
required this.createdAt,
});
factory UploadResponse.fromJson(Map<String, dynamic> json) {
return UploadResponse(
cid: json['cid'] as String,
size: json['size'] as int,
name: json['name'] as String?,
createdAt: json['created_at'] as String,
);
}
}
/// Pin information.
class Pin {
final String cid;
final String? name;
final PinStatus status;
final int size;
final String createdAt;
final String? expiresAt;
final List<String>? delegates;
const Pin({
required this.cid,
this.name,
required this.status,
required this.size,
required this.createdAt,
this.expiresAt,
this.delegates,
});
factory Pin.fromJson(Map<String, dynamic> json) {
return Pin(
cid: json['cid'] as String,
name: json['name'] as String?,
status: PinStatus.fromJson(json['status'] as String),
size: json['size'] as int,
createdAt: json['created_at'] as String,
expiresAt: json['expires_at'] as String?,
delegates: (json['delegates'] as List?)?.cast<String>(),
);
}
}
/// Request to pin content.
class PinRequest {
final String cid;
final String? name;
final int? durationDays;
final List<String>? origins;
final Map<String, String>? meta;
const PinRequest({
required this.cid,
this.name,
this.durationDays,
this.origins,
this.meta,
});
Map<String, dynamic> toJson() => {
'cid': cid,
if (name != null) 'name': name,
if (durationDays != null) 'duration_days': durationDays,
if (origins != null) 'origins': origins,
if (meta != null) 'meta': meta,
};
}
/// CAR (Content Addressable Archive) file.
class CarFile {
final String cid;
final int size;
final List<String> roots;
final String createdAt;
const CarFile({
required this.cid,
required this.size,
required this.roots,
required this.createdAt,
});
factory CarFile.fromJson(Map<String, dynamic> json) {
return CarFile(
cid: json['cid'] as String,
size: json['size'] as int,
roots: (json['roots'] as List).cast<String>(),
createdAt: json['created_at'] as String,
);
}
}
/// Entry in a CAR file for creation.
class CarEntry {
final String path;
final Uint8List content;
const CarEntry({
required this.path,
required this.content,
});
}
/// Directory entry.
class DirectoryEntry {
final String name;
final String cid;
final int size;
final EntryType type;
const DirectoryEntry({
required this.name,
required this.cid,
required this.size,
required this.type,
});
factory DirectoryEntry.fromJson(Map<String, dynamic> json) {
return DirectoryEntry(
name: json['name'] as String,
cid: json['cid'] as String,
size: json['size'] as int,
type: EntryType.fromJson(json['type'] as String),
);
}
}
/// File entry for directory creation.
class FileEntry {
final String path;
final Uint8List content;
const FileEntry({
required this.path,
required this.content,
});
}
/// Storage usage statistics.
class StorageStats {
final int totalSize;
final int pinCount;
final int bandwidthUsed;
final double quotaUsed;
const StorageStats({
required this.totalSize,
required this.pinCount,
required this.bandwidthUsed,
required this.quotaUsed,
});
factory StorageStats.fromJson(Map<String, dynamic> json) {
return StorageStats(
totalSize: json['total_size'] as int,
pinCount: json['pin_count'] as int,
bandwidthUsed: json['bandwidth_used'] as int,
quotaUsed: (json['quota_used'] as num).toDouble(),
);
}
}
/// Exception thrown by storage operations.
class StorageException implements Exception {
final String message;
final String? code;
final int? statusCode;
const StorageException(this.message, {this.code, this.statusCode});
@override
String toString() => 'StorageException: $message';
}

View file

@ -0,0 +1,244 @@
/// Synor Wallet SDK client for Flutter/Dart.
library synor_wallet;
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'types.dart';
export 'types.dart';
/// Synor Wallet SDK client for Flutter/Dart.
///
/// Provides key management, transaction signing, and balance queries
/// for the Synor blockchain.
///
/// Example:
/// ```dart
/// final wallet = SynorWallet(WalletConfig(apiKey: 'your-api-key'));
///
/// // Create a new wallet
/// final result = await wallet.createWallet(type: WalletType.standard);
/// print('Wallet ID: ${result.wallet.id}');
/// print('Mnemonic: ${result.mnemonic}');
///
/// // Get balance
/// final balance = await wallet.getBalance(result.wallet.addresses[0].address);
/// print('Balance: ${balance.total}');
///
/// wallet.close();
/// ```
class SynorWallet {
final WalletConfig config;
final http.Client _client;
SynorWallet(this.config) : _client = http.Client();
/// Create a new wallet.
Future<CreateWalletResult> createWallet({
WalletType type = WalletType.standard,
}) async {
final body = {
'type': type.toJson(),
'network': config.network.toJson(),
};
return _post('/wallets', body, CreateWalletResult.fromJson);
}
/// Import a wallet from a mnemonic phrase.
Future<Wallet> importWallet(String mnemonic, {String? passphrase}) async {
final body = {
'mnemonic': mnemonic,
'passphrase': passphrase,
'network': config.network.toJson(),
};
return _post('/wallets/import', body, Wallet.fromJson);
}
/// Get a wallet by ID.
Future<Wallet> getWallet(String walletId) async {
return _get('/wallets/$walletId', Wallet.fromJson);
}
/// Generate a new address for a wallet.
Future<Address> generateAddress(String walletId, {bool isChange = false}) async {
final body = {'is_change': isChange};
return _post('/wallets/$walletId/addresses', body, Address.fromJson);
}
/// Get a stealth address for privacy transactions.
Future<StealthAddress> getStealthAddress(String walletId) async {
return _get('/wallets/$walletId/stealth-address', StealthAddress.fromJson);
}
/// Sign a transaction.
Future<SignedTransaction> signTransaction(
String walletId,
Transaction transaction,
) async {
final body = {
'wallet_id': walletId,
'transaction': transaction.toJson(),
};
return _post('/transactions/sign', body, SignedTransaction.fromJson);
}
/// Sign a message with a wallet address.
Future<Signature> signMessage(
String walletId,
String message, {
int addressIndex = 0,
}) async {
final body = {
'wallet_id': walletId,
'message': message,
'address_index': addressIndex,
};
return _post('/messages/sign', body, Signature.fromJson);
}
/// Verify a message signature.
Future<bool> verifyMessage(
String message,
String signature,
String address,
) async {
final body = {
'message': message,
'signature': signature,
'address': address,
};
final response = await _post<Map<String, dynamic>>(
'/messages/verify',
body,
(json) => json,
);
return response['valid'] as bool? ?? false;
}
/// Get the balance for an address.
Future<Balance> getBalance(String address) async {
return _get('/addresses/$address/balance', Balance.fromJson);
}
/// Get UTXOs for an address.
Future<List<UTXO>> getUTXOs(String address, {int minConfirmations = 1}) async {
return _getList(
'/addresses/$address/utxos?min_confirmations=$minConfirmations',
UTXO.fromJson,
);
}
/// Estimate transaction fee.
Future<int> estimateFee({Priority priority = Priority.medium}) async {
final response = await _get<Map<String, dynamic>>(
'/fees/estimate?priority=${priority.toJson()}',
(json) => json,
);
return response['fee_per_byte'] as int? ?? 0;
}
// HTTP helper methods
Future<T> _get<T>(String path, T Function(Map<String, dynamic>) fromJson) async {
return _executeWithRetry(() async {
final response = await _client
.get(
Uri.parse('${config.endpoint}$path'),
headers: _headers,
)
.timeout(config.timeout);
return _handleResponse(response, fromJson);
});
}
Future<List<T>> _getList<T>(
String path,
T Function(Map<String, dynamic>) fromJson,
) async {
return _executeWithRetry(() async {
final response = await _client
.get(
Uri.parse('${config.endpoint}$path'),
headers: _headers,
)
.timeout(config.timeout);
if (response.statusCode >= 200 && response.statusCode < 300) {
final List<dynamic> jsonList = jsonDecode(response.body);
return jsonList
.map((e) => fromJson(e as Map<String, dynamic>))
.toList();
}
throw WalletException(
'HTTP error: ${response.body}',
statusCode: response.statusCode,
);
});
}
Future<T> _post<T>(
String path,
Map<String, dynamic> body,
T Function(Map<String, dynamic>) fromJson,
) async {
return _executeWithRetry(() async {
final response = await _client
.post(
Uri.parse('${config.endpoint}$path'),
headers: _headers,
body: jsonEncode(body),
)
.timeout(config.timeout);
return _handleResponse(response, fromJson);
});
}
Map<String, String> get _headers => {
'Authorization': 'Bearer ${config.apiKey}',
'Content-Type': 'application/json',
};
T _handleResponse<T>(
http.Response response,
T Function(Map<String, dynamic>) fromJson,
) {
if (response.statusCode >= 200 && response.statusCode < 300) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return fromJson(json);
}
throw WalletException(
'HTTP error: ${response.body}',
statusCode: response.statusCode,
);
}
Future<T> _executeWithRetry<T>(Future<T> Function() operation) async {
Exception? lastError;
for (var attempt = 0; attempt < config.retries; attempt++) {
try {
return await operation();
} catch (e) {
lastError = e as Exception;
if (config.debug) {
print('Attempt ${attempt + 1} failed: $e');
}
if (attempt < config.retries - 1) {
await Future.delayed(Duration(seconds: attempt + 1));
}
}
}
throw lastError ?? WalletException('Unknown error after ${config.retries} retries');
}
/// Close the client and release resources.
void close() {
_client.close();
}
}

View file

@ -0,0 +1,330 @@
/// Types for Synor Wallet SDK.
library synor_wallet_types;
/// Network environment for wallet operations.
enum Network {
mainnet,
testnet,
devnet;
String toJson() => name;
static Network fromJson(String json) {
return Network.values.firstWhere(
(e) => e.name == json,
orElse: () => Network.mainnet,
);
}
}
/// Type of wallet to create.
enum WalletType {
standard,
multisig,
hardware,
stealth;
String toJson() => name;
static WalletType fromJson(String json) {
return WalletType.values.firstWhere(
(e) => e.name == json,
orElse: () => WalletType.standard,
);
}
}
/// Transaction priority for fee estimation.
enum Priority {
low,
medium,
high;
String toJson() => name;
static Priority fromJson(String json) {
return Priority.values.firstWhere(
(e) => e.name == json,
orElse: () => Priority.medium,
);
}
}
/// Configuration for the wallet client.
class WalletConfig {
final String apiKey;
final String endpoint;
final Network network;
final Duration timeout;
final int retries;
final bool debug;
const WalletConfig({
required this.apiKey,
this.endpoint = 'https://wallet.synor.io/v1',
this.network = Network.mainnet,
this.timeout = const Duration(seconds: 30),
this.retries = 3,
this.debug = false,
});
}
/// A wallet address.
class Address {
final String address;
final int index;
final bool isChange;
const Address({
required this.address,
required this.index,
this.isChange = false,
});
factory Address.fromJson(Map<String, dynamic> json) {
return Address(
address: json['address'] as String,
index: json['index'] as int,
isChange: json['is_change'] as bool? ?? false,
);
}
Map<String, dynamic> toJson() => {
'address': address,
'index': index,
'is_change': isChange,
};
}
/// A Synor wallet with addresses and keys.
class Wallet {
final String id;
final WalletType type;
final Network network;
final List<Address> addresses;
final String createdAt;
const Wallet({
required this.id,
required this.type,
required this.network,
required this.addresses,
required this.createdAt,
});
factory Wallet.fromJson(Map<String, dynamic> json) {
return Wallet(
id: json['id'] as String,
type: WalletType.fromJson(json['type'] as String),
network: Network.fromJson(json['network'] as String),
addresses: (json['addresses'] as List)
.map((e) => Address.fromJson(e as Map<String, dynamic>))
.toList(),
createdAt: json['created_at'] as String,
);
}
}
/// Stealth address for privacy transactions.
class StealthAddress {
final String spendPublicKey;
final String viewPublicKey;
final String oneTimeAddress;
const StealthAddress({
required this.spendPublicKey,
required this.viewPublicKey,
required this.oneTimeAddress,
});
factory StealthAddress.fromJson(Map<String, dynamic> json) {
return StealthAddress(
spendPublicKey: json['spend_public_key'] as String,
viewPublicKey: json['view_public_key'] as String,
oneTimeAddress: json['one_time_address'] as String,
);
}
}
/// Result of wallet creation.
class CreateWalletResult {
final Wallet wallet;
final String? mnemonic;
const CreateWalletResult({
required this.wallet,
this.mnemonic,
});
factory CreateWalletResult.fromJson(Map<String, dynamic> json) {
return CreateWalletResult(
wallet: Wallet.fromJson(json['wallet'] as Map<String, dynamic>),
mnemonic: json['mnemonic'] as String?,
);
}
}
/// Transaction input (UTXO reference).
class TransactionInput {
final String txid;
final int vout;
final int amount;
const TransactionInput({
required this.txid,
required this.vout,
required this.amount,
});
Map<String, dynamic> toJson() => {
'txid': txid,
'vout': vout,
'amount': amount,
};
}
/// Transaction output.
class TransactionOutput {
final String address;
final int amount;
const TransactionOutput({
required this.address,
required this.amount,
});
Map<String, dynamic> toJson() => {
'address': address,
'amount': amount,
};
}
/// A blockchain transaction for signing.
class Transaction {
final List<TransactionInput> inputs;
final List<TransactionOutput> outputs;
final int? fee;
final Priority priority;
const Transaction({
required this.inputs,
required this.outputs,
this.fee,
this.priority = Priority.medium,
});
Map<String, dynamic> toJson() => {
'inputs': inputs.map((e) => e.toJson()).toList(),
'outputs': outputs.map((e) => e.toJson()).toList(),
if (fee != null) 'fee': fee,
'priority': priority.toJson(),
};
}
/// A signed transaction ready for broadcast.
class SignedTransaction {
final String txid;
final String hex;
final int size;
final int fee;
const SignedTransaction({
required this.txid,
required this.hex,
required this.size,
required this.fee,
});
factory SignedTransaction.fromJson(Map<String, dynamic> json) {
return SignedTransaction(
txid: json['txid'] as String,
hex: json['hex'] as String,
size: json['size'] as int,
fee: json['fee'] as int,
);
}
}
/// A cryptographic signature.
class Signature {
final String signature;
final String address;
final int? recoveryId;
const Signature({
required this.signature,
required this.address,
this.recoveryId,
});
factory Signature.fromJson(Map<String, dynamic> json) {
return Signature(
signature: json['signature'] as String,
address: json['address'] as String,
recoveryId: json['recovery_id'] as int?,
);
}
}
/// Unspent transaction output.
class UTXO {
final String txid;
final int vout;
final int amount;
final String address;
final int confirmations;
final String? scriptPubKey;
const UTXO({
required this.txid,
required this.vout,
required this.amount,
required this.address,
required this.confirmations,
this.scriptPubKey,
});
factory UTXO.fromJson(Map<String, dynamic> json) {
return UTXO(
txid: json['txid'] as String,
vout: json['vout'] as int,
amount: json['amount'] as int,
address: json['address'] as String,
confirmations: json['confirmations'] as int,
scriptPubKey: json['script_pubkey'] as String?,
);
}
}
/// Wallet balance information.
class Balance {
final int confirmed;
final int unconfirmed;
final int total;
const Balance({
required this.confirmed,
required this.unconfirmed,
required this.total,
});
factory Balance.fromJson(Map<String, dynamic> json) {
return Balance(
confirmed: json['confirmed'] as int,
unconfirmed: json['unconfirmed'] as int,
total: json['total'] as int,
);
}
}
/// Exception thrown by wallet operations.
class WalletException implements Exception {
final String message;
final String? code;
final int? statusCode;
const WalletException(this.message, {this.code, this.statusCode});
@override
String toString() => 'WalletException: $message';
}

View file

@ -0,0 +1,60 @@
/// Synor SDK for Flutter/Dart
///
/// A comprehensive SDK for the Synor blockchain platform including:
/// - **Compute**: Distributed heterogeneous computing
/// - **Wallet**: Key management and transaction signing
/// - **RPC**: Blockchain queries and subscriptions
/// - **Storage**: IPFS-compatible decentralized storage
///
/// ## Quick Start
///
/// ```dart
/// import 'package:synor_sdk/synor_sdk.dart';
///
/// void main() async {
/// // Wallet operations
/// final wallet = SynorWallet(WalletConfig(apiKey: 'your-api-key'));
/// final result = await wallet.createWallet();
/// print('Wallet ID: ${result.wallet.id}');
///
/// // RPC queries
/// final rpc = SynorRpc(RpcConfig(apiKey: 'your-api-key'));
/// final block = await rpc.getLatestBlock();
/// print('Latest block: ${block.height}');
///
/// // Storage operations
/// final storage = SynorStorage(StorageConfig(apiKey: 'your-api-key'));
/// final data = utf8.encode('Hello, World!');
/// final upload = await storage.upload(Uint8List.fromList(data));
/// print('CID: ${upload.cid}');
///
/// // Clean up
/// wallet.close();
/// rpc.close();
/// storage.close();
/// }
/// ```
///
/// ## Naming Conventions
///
/// Some types have service-specific prefixes to avoid conflicts:
/// - Wallet: `Network`, `WalletType`, `Priority`
/// - RPC: `RpcNetwork`, `RpcPriority`, `RpcTransaction`
/// - Storage: `PinStatus`, `HashAlgorithm`, `EntryType`
/// - Compute: `Precision`, `ProcessorType`, `Priority` (as `ComputePriority`)
library synor_sdk;
// Compute SDK - hide Priority to avoid conflict with Wallet
export 'synor_compute.dart' hide Priority;
// Wallet SDK
export 'src/wallet/synor_wallet.dart';
// RPC SDK
export 'src/rpc/synor_rpc.dart';
// Storage SDK
export 'src/storage/synor_storage.dart';
// Re-export Compute Priority with alias-friendly access
// Users can import compute directly for Priority: import 'package:synor_sdk/synor_compute.dart';

View file

@ -1,5 +1,5 @@
name: synor_compute
description: Flutter/Dart SDK for Synor Compute - distributed heterogeneous computing platform
name: synor_sdk
description: Flutter/Dart SDK for Synor - Compute, Wallet, RPC, and Storage
version: 0.1.0
homepage: https://github.com/mrgulshanyadav/Blockchain.cc
repository: https://github.com/mrgulshanyadav/Blockchain.cc/tree/main/sdk/flutter

625
sdk/go/bridge/client.go Normal file
View file

@ -0,0 +1,625 @@
// Package bridge provides cross-chain asset transfer functionality.
package bridge
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"sync/atomic"
"time"
)
// Client is a Synor Bridge client for cross-chain transfers.
type Client struct {
config Config
httpClient *http.Client
closed atomic.Bool
}
// NewClient creates a new bridge client with the given configuration.
func NewClient(config Config) *Client {
if config.Endpoint == "" {
config.Endpoint = DefaultEndpoint
}
if config.Timeout == 0 {
config.Timeout = 60 * time.Second
}
if config.Retries == 0 {
config.Retries = 3
}
return &Client{
config: config,
httpClient: &http.Client{
Timeout: config.Timeout,
},
}
}
// ==================== Chain Operations ====================
// GetSupportedChains returns all supported chains.
func (c *Client) GetSupportedChains(ctx context.Context) ([]Chain, error) {
var resp struct {
Chains []Chain `json:"chains"`
}
if err := c.request(ctx, "GET", "/chains", nil, &resp); err != nil {
return nil, err
}
return resp.Chains, nil
}
// GetChain returns chain information by ID.
func (c *Client) GetChain(ctx context.Context, chainID ChainID) (*Chain, error) {
var chain Chain
if err := c.request(ctx, "GET", fmt.Sprintf("/chains/%s", chainID), nil, &chain); err != nil {
return nil, err
}
return &chain, nil
}
// IsChainSupported checks if a chain is supported.
func (c *Client) IsChainSupported(ctx context.Context, chainID ChainID) bool {
chain, err := c.GetChain(ctx, chainID)
if err != nil {
return false
}
return chain.Supported
}
// ==================== Asset Operations ====================
// GetSupportedAssets returns supported assets for a chain.
func (c *Client) GetSupportedAssets(ctx context.Context, chainID ChainID) ([]Asset, error) {
var resp struct {
Assets []Asset `json:"assets"`
}
if err := c.request(ctx, "GET", fmt.Sprintf("/chains/%s/assets", chainID), nil, &resp); err != nil {
return nil, err
}
return resp.Assets, nil
}
// GetAsset returns asset information by ID.
func (c *Client) GetAsset(ctx context.Context, assetID string) (*Asset, error) {
var asset Asset
if err := c.request(ctx, "GET", fmt.Sprintf("/assets/%s", assetID), nil, &asset); err != nil {
return nil, err
}
return &asset, nil
}
// GetWrappedAsset returns the wrapped asset mapping.
func (c *Client) GetWrappedAsset(ctx context.Context, originalAssetID string, targetChain ChainID) (*WrappedAsset, error) {
var wrapped WrappedAsset
path := fmt.Sprintf("/assets/%s/wrapped/%s", originalAssetID, targetChain)
if err := c.request(ctx, "GET", path, nil, &wrapped); err != nil {
return nil, err
}
return &wrapped, nil
}
// GetWrappedAssets returns all wrapped assets for a chain.
func (c *Client) GetWrappedAssets(ctx context.Context, chainID ChainID) ([]WrappedAsset, error) {
var resp struct {
Assets []WrappedAsset `json:"assets"`
}
if err := c.request(ctx, "GET", fmt.Sprintf("/chains/%s/wrapped", chainID), nil, &resp); err != nil {
return nil, err
}
return resp.Assets, nil
}
// ==================== Fee & Rate Operations ====================
// EstimateFee estimates the bridge fee for a transfer.
func (c *Client) EstimateFee(ctx context.Context, asset, amount string, sourceChain, targetChain ChainID) (*FeeEstimate, error) {
body := map[string]interface{}{
"asset": asset,
"amount": amount,
"sourceChain": sourceChain,
"targetChain": targetChain,
}
var estimate FeeEstimate
if err := c.request(ctx, "POST", "/fees/estimate", body, &estimate); err != nil {
return nil, err
}
return &estimate, nil
}
// GetExchangeRate returns the exchange rate between two assets.
func (c *Client) GetExchangeRate(ctx context.Context, fromAsset, toAsset string) (*ExchangeRate, error) {
var rate ExchangeRate
path := fmt.Sprintf("/rates/%s/%s", url.PathEscape(fromAsset), url.PathEscape(toAsset))
if err := c.request(ctx, "GET", path, nil, &rate); err != nil {
return nil, err
}
return &rate, nil
}
// ==================== Lock-Mint Flow ====================
// Lock locks assets on the source chain for cross-chain transfer.
// This is step 1 of the lock-mint flow.
func (c *Client) Lock(ctx context.Context, asset, amount string, targetChain ChainID, opts *LockOptions) (*LockReceipt, error) {
body := map[string]interface{}{
"asset": asset,
"amount": amount,
"targetChain": targetChain,
}
if opts != nil {
if opts.Recipient != "" {
body["recipient"] = opts.Recipient
}
if opts.Deadline != 0 {
body["deadline"] = opts.Deadline
}
if opts.Slippage != 0 {
body["slippage"] = opts.Slippage
}
}
var receipt LockReceipt
if err := c.request(ctx, "POST", "/transfers/lock", body, &receipt); err != nil {
return nil, err
}
return &receipt, nil
}
// GetLockProof gets the lock proof for minting.
// This is step 2 of the lock-mint flow.
func (c *Client) GetLockProof(ctx context.Context, lockReceiptID string) (*LockProof, error) {
var proof LockProof
path := fmt.Sprintf("/transfers/lock/%s/proof", lockReceiptID)
if err := c.request(ctx, "GET", path, nil, &proof); err != nil {
return nil, err
}
return &proof, nil
}
// WaitForLockProof waits for the lock proof to be ready.
func (c *Client) WaitForLockProof(ctx context.Context, lockReceiptID string, pollInterval, maxWait time.Duration) (*LockProof, error) {
if pollInterval == 0 {
pollInterval = 5 * time.Second
}
if maxWait == 0 {
maxWait = 10 * time.Minute
}
deadline := time.Now().Add(maxWait)
for time.Now().Before(deadline) {
proof, err := c.GetLockProof(ctx, lockReceiptID)
if err == nil {
return proof, nil
}
if bridgeErr, ok := err.(*Error); ok && bridgeErr.Code == "CONFIRMATIONS_PENDING" {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(pollInterval):
continue
}
}
return nil, err
}
return nil, &Error{Message: "Timeout waiting for lock proof", Code: "CONFIRMATIONS_PENDING"}
}
// Mint mints wrapped tokens on the target chain.
// This is step 3 of the lock-mint flow.
func (c *Client) Mint(ctx context.Context, proof *LockProof, targetAddress string, opts *MintOptions) (*SignedTransaction, error) {
body := map[string]interface{}{
"proof": proof,
"targetAddress": targetAddress,
}
if opts != nil {
if opts.GasLimit != "" {
body["gasLimit"] = opts.GasLimit
}
if opts.MaxFeePerGas != "" {
body["maxFeePerGas"] = opts.MaxFeePerGas
}
if opts.MaxPriorityFeePerGas != "" {
body["maxPriorityFeePerGas"] = opts.MaxPriorityFeePerGas
}
}
var tx SignedTransaction
if err := c.request(ctx, "POST", "/transfers/mint", body, &tx); err != nil {
return nil, err
}
return &tx, nil
}
// ==================== Burn-Unlock Flow ====================
// Burn burns wrapped tokens on the current chain.
// This is step 1 of the burn-unlock flow.
func (c *Client) Burn(ctx context.Context, wrappedAsset, amount string, opts *BurnOptions) (*BurnReceipt, error) {
body := map[string]interface{}{
"wrappedAsset": wrappedAsset,
"amount": amount,
}
if opts != nil {
if opts.Recipient != "" {
body["recipient"] = opts.Recipient
}
if opts.Deadline != 0 {
body["deadline"] = opts.Deadline
}
}
var receipt BurnReceipt
if err := c.request(ctx, "POST", "/transfers/burn", body, &receipt); err != nil {
return nil, err
}
return &receipt, nil
}
// GetBurnProof gets the burn proof for unlocking.
// This is step 2 of the burn-unlock flow.
func (c *Client) GetBurnProof(ctx context.Context, burnReceiptID string) (*BurnProof, error) {
var proof BurnProof
path := fmt.Sprintf("/transfers/burn/%s/proof", burnReceiptID)
if err := c.request(ctx, "GET", path, nil, &proof); err != nil {
return nil, err
}
return &proof, nil
}
// WaitForBurnProof waits for the burn proof to be ready.
func (c *Client) WaitForBurnProof(ctx context.Context, burnReceiptID string, pollInterval, maxWait time.Duration) (*BurnProof, error) {
if pollInterval == 0 {
pollInterval = 5 * time.Second
}
if maxWait == 0 {
maxWait = 10 * time.Minute
}
deadline := time.Now().Add(maxWait)
for time.Now().Before(deadline) {
proof, err := c.GetBurnProof(ctx, burnReceiptID)
if err == nil {
return proof, nil
}
if bridgeErr, ok := err.(*Error); ok && bridgeErr.Code == "CONFIRMATIONS_PENDING" {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(pollInterval):
continue
}
}
return nil, err
}
return nil, &Error{Message: "Timeout waiting for burn proof", Code: "CONFIRMATIONS_PENDING"}
}
// Unlock unlocks original tokens on the source chain.
// This is step 3 of the burn-unlock flow.
func (c *Client) Unlock(ctx context.Context, proof *BurnProof, opts *UnlockOptions) (*SignedTransaction, error) {
body := map[string]interface{}{
"proof": proof,
}
if opts != nil {
if opts.GasLimit != "" {
body["gasLimit"] = opts.GasLimit
}
if opts.GasPrice != "" {
body["gasPrice"] = opts.GasPrice
}
}
var tx SignedTransaction
if err := c.request(ctx, "POST", "/transfers/unlock", body, &tx); err != nil {
return nil, err
}
return &tx, nil
}
// ==================== Transfer Management ====================
// GetTransfer returns a transfer by ID.
func (c *Client) GetTransfer(ctx context.Context, transferID string) (*Transfer, error) {
var transfer Transfer
if err := c.request(ctx, "GET", fmt.Sprintf("/transfers/%s", transferID), nil, &transfer); err != nil {
return nil, err
}
return &transfer, nil
}
// GetTransferStatus returns the status of a transfer.
func (c *Client) GetTransferStatus(ctx context.Context, transferID string) (TransferStatus, error) {
transfer, err := c.GetTransfer(ctx, transferID)
if err != nil {
return "", err
}
return transfer.Status, nil
}
// ListTransfers returns transfers matching the filter.
func (c *Client) ListTransfers(ctx context.Context, filter *TransferFilter) ([]Transfer, error) {
params := url.Values{}
if filter != nil {
if filter.Status != "" {
params.Set("status", string(filter.Status))
}
if filter.SourceChain != "" {
params.Set("sourceChain", string(filter.SourceChain))
}
if filter.TargetChain != "" {
params.Set("targetChain", string(filter.TargetChain))
}
if filter.Asset != "" {
params.Set("asset", filter.Asset)
}
if filter.Sender != "" {
params.Set("sender", filter.Sender)
}
if filter.Recipient != "" {
params.Set("recipient", filter.Recipient)
}
if filter.FromDate != nil {
params.Set("fromDate", strconv.FormatInt(filter.FromDate.Unix(), 10))
}
if filter.ToDate != nil {
params.Set("toDate", strconv.FormatInt(filter.ToDate.Unix(), 10))
}
if filter.Limit > 0 {
params.Set("limit", strconv.Itoa(filter.Limit))
}
if filter.Offset > 0 {
params.Set("offset", strconv.Itoa(filter.Offset))
}
}
path := "/transfers"
if len(params) > 0 {
path = "/transfers?" + params.Encode()
}
var resp struct {
Transfers []Transfer `json:"transfers"`
}
if err := c.request(ctx, "GET", path, nil, &resp); err != nil {
return nil, err
}
return resp.Transfers, nil
}
// WaitForTransfer waits for a transfer to complete.
func (c *Client) WaitForTransfer(ctx context.Context, transferID string, pollInterval, maxWait time.Duration) (*Transfer, error) {
if pollInterval == 0 {
pollInterval = 10 * time.Second
}
if maxWait == 0 {
maxWait = 30 * time.Minute
}
finalStatuses := map[TransferStatus]bool{
TransferStatusCompleted: true,
TransferStatusFailed: true,
TransferStatusRefunded: true,
}
deadline := time.Now().Add(maxWait)
for time.Now().Before(deadline) {
transfer, err := c.GetTransfer(ctx, transferID)
if err != nil {
return nil, err
}
if finalStatuses[transfer.Status] {
return transfer, nil
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(pollInterval):
continue
}
}
return nil, &Error{Message: "Timeout waiting for transfer completion"}
}
// ==================== Convenience Methods ====================
// BridgeTo executes a complete lock-mint transfer.
func (c *Client) BridgeTo(ctx context.Context, asset, amount string, targetChain ChainID, targetAddress string, lockOpts *LockOptions, mintOpts *MintOptions) (*Transfer, error) {
// Lock on source chain
lockReceipt, err := c.Lock(ctx, asset, amount, targetChain, lockOpts)
if err != nil {
return nil, err
}
if c.config.Debug {
fmt.Printf("Locked: %s, waiting for confirmations...\n", lockReceipt.ID)
}
// Wait for proof
proof, err := c.WaitForLockProof(ctx, lockReceipt.ID, 0, 0)
if err != nil {
return nil, err
}
if c.config.Debug {
fmt.Printf("Proof ready, minting on %s...\n", targetChain)
}
// Mint on target chain
_, err = c.Mint(ctx, proof, targetAddress, mintOpts)
if err != nil {
return nil, err
}
// Return final transfer status
return c.WaitForTransfer(ctx, lockReceipt.ID, 0, 0)
}
// BridgeBack executes a complete burn-unlock transfer.
func (c *Client) BridgeBack(ctx context.Context, wrappedAsset, amount string, burnOpts *BurnOptions, unlockOpts *UnlockOptions) (*Transfer, error) {
// Burn wrapped tokens
burnReceipt, err := c.Burn(ctx, wrappedAsset, amount, burnOpts)
if err != nil {
return nil, err
}
if c.config.Debug {
fmt.Printf("Burned: %s, waiting for confirmations...\n", burnReceipt.ID)
}
// Wait for proof
proof, err := c.WaitForBurnProof(ctx, burnReceipt.ID, 0, 0)
if err != nil {
return nil, err
}
if c.config.Debug {
fmt.Printf("Proof ready, unlocking on %s...\n", burnReceipt.TargetChain)
}
// Unlock on original chain
_, err = c.Unlock(ctx, proof, unlockOpts)
if err != nil {
return nil, err
}
// Return final transfer status
return c.WaitForTransfer(ctx, burnReceipt.ID, 0, 0)
}
// ==================== Lifecycle ====================
// Close closes the client.
func (c *Client) Close() {
c.closed.Store(true)
}
// IsClosed returns whether the client is closed.
func (c *Client) IsClosed() bool {
return c.closed.Load()
}
// HealthCheck performs a health check.
func (c *Client) HealthCheck(ctx context.Context) bool {
var resp struct {
Status string `json:"status"`
}
if err := c.request(ctx, "GET", "/health", nil, &resp); err != nil {
return false
}
return resp.Status == "healthy"
}
// ==================== Private Methods ====================
func (c *Client) request(ctx context.Context, method, path string, body interface{}, result interface{}) error {
if c.IsClosed() {
return &Error{Message: "Client has been closed"}
}
var lastErr error
for attempt := 0; attempt < c.config.Retries; attempt++ {
err := c.doRequest(ctx, method, path, body, result)
if err == nil {
return nil
}
if c.config.Debug {
fmt.Printf("Attempt %d failed: %v\n", attempt+1, err)
}
lastErr = err
if attempt < c.config.Retries-1 {
time.Sleep(time.Duration(1<<attempt) * time.Second)
}
}
return lastErr
}
func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}, result interface{}) error {
url := c.config.Endpoint + path
var bodyReader io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
return err
}
bodyReader = bytes.NewReader(jsonBody)
}
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-SDK-Version", "go/0.1.0")
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode >= 400 {
var errorResp struct {
Message string `json:"message"`
Error string `json:"error"`
Code string `json:"code"`
}
json.Unmarshal(respBody, &errorResp)
message := errorResp.Message
if message == "" {
message = errorResp.Error
}
if message == "" {
message = fmt.Sprintf("HTTP %d", resp.StatusCode)
}
return &Error{
Message: message,
Code: errorResp.Code,
StatusCode: resp.StatusCode,
}
}
if result != nil {
if err := json.Unmarshal(respBody, result); err != nil {
return err
}
}
return nil
}
// Error represents a bridge error.
type Error struct {
Message string
Code string
StatusCode int
}
func (e *Error) Error() string {
return e.Message
}

271
sdk/go/bridge/types.go Normal file
View file

@ -0,0 +1,271 @@
// Package bridge provides cross-chain asset transfer functionality.
//
// The Bridge SDK enables transferring assets between Synor and other blockchain
// networks using a lock-mint and burn-unlock pattern.
package bridge
import (
"time"
)
// ChainID represents a supported blockchain network.
type ChainID string
const (
ChainSynor ChainID = "synor"
ChainEthereum ChainID = "ethereum"
ChainPolygon ChainID = "polygon"
ChainArbitrum ChainID = "arbitrum"
ChainOptimism ChainID = "optimism"
ChainBSC ChainID = "bsc"
ChainAvalanche ChainID = "avalanche"
ChainSolana ChainID = "solana"
ChainCosmos ChainID = "cosmos"
)
// NativeCurrency represents a chain's native currency.
type NativeCurrency struct {
Name string `json:"name"`
Symbol string `json:"symbol"`
Decimals int `json:"decimals"`
}
// Chain represents a supported blockchain network.
type Chain struct {
ID ChainID `json:"id"`
Name string `json:"name"`
ChainID int64 `json:"chainId"`
RPCURL string `json:"rpcUrl"`
ExplorerURL string `json:"explorerUrl"`
NativeCurrency NativeCurrency `json:"nativeCurrency"`
Confirmations int `json:"confirmations"`
EstimatedBlockTime int `json:"estimatedBlockTime"` // seconds
Supported bool `json:"supported"`
}
// AssetType represents the type of asset.
type AssetType string
const (
AssetTypeNative AssetType = "native"
AssetTypeERC20 AssetType = "erc20"
AssetTypeERC721 AssetType = "erc721"
AssetTypeERC1155 AssetType = "erc1155"
)
// Asset represents a blockchain asset.
type Asset struct {
ID string `json:"id"`
Symbol string `json:"symbol"`
Name string `json:"name"`
Type AssetType `json:"type"`
Chain ChainID `json:"chain"`
ContractAddress string `json:"contractAddress,omitempty"`
Decimals int `json:"decimals"`
LogoURL string `json:"logoUrl,omitempty"`
Verified bool `json:"verified"`
}
// WrappedAsset represents a mapping between original and wrapped assets.
type WrappedAsset struct {
OriginalAsset Asset `json:"originalAsset"`
WrappedAsset Asset `json:"wrappedAsset"`
Chain ChainID `json:"chain"`
BridgeContract string `json:"bridgeContract"`
}
// TransferStatus represents the status of a bridge transfer.
type TransferStatus string
const (
TransferStatusPending TransferStatus = "pending"
TransferStatusLocked TransferStatus = "locked"
TransferStatusConfirming TransferStatus = "confirming"
TransferStatusMinting TransferStatus = "minting"
TransferStatusCompleted TransferStatus = "completed"
TransferStatusFailed TransferStatus = "failed"
TransferStatusRefunded TransferStatus = "refunded"
)
// TransferDirection represents the direction of a bridge transfer.
type TransferDirection string
const (
TransferDirectionLockMint TransferDirection = "lock_mint"
TransferDirectionBurnUnlock TransferDirection = "burn_unlock"
)
// ValidatorSignature represents a validator's signature for proof.
type ValidatorSignature struct {
Validator string `json:"validator"`
Signature string `json:"signature"`
Timestamp int64 `json:"timestamp"`
}
// LockReceipt represents a lock receipt from the source chain.
type LockReceipt struct {
ID string `json:"id"`
TxHash string `json:"txHash"`
SourceChain ChainID `json:"sourceChain"`
TargetChain ChainID `json:"targetChain"`
Asset Asset `json:"asset"`
Amount string `json:"amount"`
Sender string `json:"sender"`
Recipient string `json:"recipient"`
LockTimestamp int64 `json:"lockTimestamp"`
Confirmations int `json:"confirmations"`
RequiredConfirmations int `json:"requiredConfirmations"`
}
// LockProof represents proof for minting on the target chain.
type LockProof struct {
LockReceipt LockReceipt `json:"lockReceipt"`
MerkleProof []string `json:"merkleProof"`
BlockHeader string `json:"blockHeader"`
Signatures []ValidatorSignature `json:"signatures"`
}
// BurnReceipt represents a burn receipt for unlocking.
type BurnReceipt struct {
ID string `json:"id"`
TxHash string `json:"txHash"`
SourceChain ChainID `json:"sourceChain"`
TargetChain ChainID `json:"targetChain"`
WrappedAsset Asset `json:"wrappedAsset"`
OriginalAsset Asset `json:"originalAsset"`
Amount string `json:"amount"`
Sender string `json:"sender"`
Recipient string `json:"recipient"`
BurnTimestamp int64 `json:"burnTimestamp"`
Confirmations int `json:"confirmations"`
RequiredConfirmations int `json:"requiredConfirmations"`
}
// BurnProof represents proof for unlocking on the original chain.
type BurnProof struct {
BurnReceipt BurnReceipt `json:"burnReceipt"`
MerkleProof []string `json:"merkleProof"`
BlockHeader string `json:"blockHeader"`
Signatures []ValidatorSignature `json:"signatures"`
}
// Transfer represents a complete bridge transfer record.
type Transfer struct {
ID string `json:"id"`
Direction TransferDirection `json:"direction"`
Status TransferStatus `json:"status"`
SourceChain ChainID `json:"sourceChain"`
TargetChain ChainID `json:"targetChain"`
Asset Asset `json:"asset"`
Amount string `json:"amount"`
Sender string `json:"sender"`
Recipient string `json:"recipient"`
SourceTxHash string `json:"sourceTxHash,omitempty"`
TargetTxHash string `json:"targetTxHash,omitempty"`
Fee string `json:"fee"`
FeeAsset Asset `json:"feeAsset"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
CompletedAt int64 `json:"completedAt,omitempty"`
ErrorMessage string `json:"errorMessage,omitempty"`
}
// FeeEstimate represents a fee estimate for a bridge transfer.
type FeeEstimate struct {
BridgeFee string `json:"bridgeFee"`
GasFeeSource string `json:"gasFeeSource"`
GasFeeTarget string `json:"gasFeeTarget"`
TotalFee string `json:"totalFee"`
FeeAsset Asset `json:"feeAsset"`
EstimatedTime int `json:"estimatedTime"` // seconds
ExchangeRate string `json:"exchangeRate,omitempty"`
}
// ExchangeRate represents an exchange rate between assets.
type ExchangeRate struct {
FromAsset Asset `json:"fromAsset"`
ToAsset Asset `json:"toAsset"`
Rate string `json:"rate"`
InverseRate string `json:"inverseRate"`
LastUpdated int64 `json:"lastUpdated"`
Source string `json:"source"`
}
// SignedTransaction represents a signed transaction result.
type SignedTransaction struct {
TxHash string `json:"txHash"`
Chain ChainID `json:"chain"`
From string `json:"from"`
To string `json:"to"`
Value string `json:"value"`
Data string `json:"data"`
GasLimit string `json:"gasLimit"`
GasPrice string `json:"gasPrice,omitempty"`
MaxFeePerGas string `json:"maxFeePerGas,omitempty"`
MaxPriorityFeePerGas string `json:"maxPriorityFeePerGas,omitempty"`
Nonce int `json:"nonce"`
Signature string `json:"signature"`
}
// TransferFilter represents filters for listing transfers.
type TransferFilter struct {
Status TransferStatus
SourceChain ChainID
TargetChain ChainID
Asset string
Sender string
Recipient string
FromDate *time.Time
ToDate *time.Time
Limit int
Offset int
}
// LockOptions represents options for the lock operation.
type LockOptions struct {
Recipient string
Deadline int64
Slippage float64
}
// MintOptions represents options for the mint operation.
type MintOptions struct {
GasLimit string
MaxFeePerGas string
MaxPriorityFeePerGas string
}
// BurnOptions represents options for the burn operation.
type BurnOptions struct {
Recipient string
Deadline int64
}
// UnlockOptions represents options for the unlock operation.
type UnlockOptions struct {
GasLimit string
GasPrice string
}
// Config represents the bridge client configuration.
type Config struct {
APIKey string
Endpoint string
Timeout time.Duration
Retries int
Debug bool
}
// DefaultEndpoint is the default bridge API endpoint.
const DefaultEndpoint = "https://bridge.synor.io/v1"
// DefaultConfig returns a configuration with default values.
func DefaultConfig(apiKey string) Config {
return Config{
APIKey: apiKey,
Endpoint: DefaultEndpoint,
Timeout: 60 * time.Second,
Retries: 3,
Debug: false,
}
}

658
sdk/go/database/client.go Normal file
View file

@ -0,0 +1,658 @@
package database
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
)
// Client is the Synor Database SDK client
type Client struct {
config Config
httpClient *http.Client
closed bool
// Stores
KV *KeyValueStore
Documents *DocumentStore
Vectors *VectorStore
TimeSeries *TimeSeriesStore
}
// New creates a new Database client
func New(config Config) *Client {
if config.Endpoint == "" {
config.Endpoint = "https://database.synor.io/v1"
}
if config.Timeout == 0 {
config.Timeout = 30 * time.Second
}
if config.Retries == 0 {
config.Retries = 3
}
client := &Client{
config: config,
httpClient: &http.Client{
Timeout: config.Timeout,
},
}
client.KV = &KeyValueStore{client: client}
client.Documents = &DocumentStore{client: client}
client.Vectors = &VectorStore{client: client}
client.TimeSeries = &TimeSeriesStore{client: client}
return client
}
// GetStats returns database statistics
func (c *Client) GetStats(ctx context.Context) (*DatabaseStats, error) {
var stats DatabaseStats
err := c.request(ctx, "GET", "/stats", nil, &stats)
if err != nil {
return nil, err
}
return &stats, nil
}
// HealthCheck performs a health check
func (c *Client) HealthCheck(ctx context.Context) bool {
var result struct {
Status string `json:"status"`
}
err := c.request(ctx, "GET", "/health", nil, &result)
return err == nil && result.Status == "healthy"
}
// Close closes the client
func (c *Client) Close() {
c.closed = true
}
// IsClosed returns whether the client is closed
func (c *Client) IsClosed() bool {
return c.closed
}
func (c *Client) request(ctx context.Context, method, path string, body interface{}, result interface{}) error {
if c.closed {
return NewDatabaseError("Client has been closed", "CLIENT_CLOSED", 0)
}
var lastErr error
for attempt := 0; attempt < c.config.Retries; attempt++ {
err := c.doRequest(ctx, method, path, body, result)
if err == nil {
return nil
}
lastErr = err
if c.config.Debug {
fmt.Printf("Attempt %d failed: %v\n", attempt+1, err)
}
if attempt < c.config.Retries-1 {
time.Sleep(time.Duration(attempt+1) * time.Second)
}
}
return lastErr
}
func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}, result interface{}) error {
var bodyReader io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
return err
}
bodyReader = bytes.NewReader(jsonBody)
}
req, err := http.NewRequestWithContext(ctx, method, c.config.Endpoint+path, bodyReader)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-SDK-Version", "go/0.1.0")
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode >= 400 {
var errResp struct {
Message string `json:"message"`
Error string `json:"error"`
Code string `json:"code"`
}
json.Unmarshal(respBody, &errResp)
msg := errResp.Message
if msg == "" {
msg = errResp.Error
}
if msg == "" {
msg = fmt.Sprintf("HTTP %d", resp.StatusCode)
}
return NewDatabaseError(msg, errResp.Code, resp.StatusCode)
}
if result != nil && len(respBody) > 0 {
return json.Unmarshal(respBody, result)
}
return nil
}
// ==================== Key-Value Store ====================
// KeyValueStore provides key-value operations
type KeyValueStore struct {
client *Client
}
// Get retrieves a value by key
func (kv *KeyValueStore) Get(ctx context.Context, key string) (interface{}, error) {
var result struct {
Value interface{} `json:"value"`
}
err := kv.client.request(ctx, "GET", "/kv/"+url.PathEscape(key), nil, &result)
if err != nil {
return nil, err
}
return result.Value, nil
}
// GetEntry retrieves a full entry with metadata
func (kv *KeyValueStore) GetEntry(ctx context.Context, key string) (*KeyValueEntry, error) {
var entry KeyValueEntry
err := kv.client.request(ctx, "GET", "/kv/"+url.PathEscape(key)+"/entry", nil, &entry)
if err != nil {
return nil, err
}
return &entry, nil
}
// Set sets a value
func (kv *KeyValueStore) Set(ctx context.Context, key string, value interface{}, opts *SetOptions) error {
body := map[string]interface{}{
"value": value,
}
if opts != nil {
if opts.TTL != nil {
body["ttl"] = *opts.TTL
}
if opts.Metadata != nil {
body["metadata"] = opts.Metadata
}
if opts.IfNotExists {
body["if_not_exists"] = true
}
if opts.IfExists {
body["if_exists"] = true
}
}
return kv.client.request(ctx, "PUT", "/kv/"+url.PathEscape(key), body, nil)
}
// Delete deletes a key
func (kv *KeyValueStore) Delete(ctx context.Context, key string) (bool, error) {
var result struct {
Deleted bool `json:"deleted"`
}
err := kv.client.request(ctx, "DELETE", "/kv/"+url.PathEscape(key), nil, &result)
if err != nil {
return false, err
}
return result.Deleted, nil
}
// Exists checks if a key exists
func (kv *KeyValueStore) Exists(ctx context.Context, key string) (bool, error) {
var result struct {
Exists bool `json:"exists"`
}
err := kv.client.request(ctx, "GET", "/kv/"+url.PathEscape(key)+"/exists", nil, &result)
if err != nil {
return false, err
}
return result.Exists, nil
}
// List lists keys with optional prefix filtering
func (kv *KeyValueStore) List(ctx context.Context, opts *ListOptions) (*ListResult, error) {
params := url.Values{}
if opts != nil {
if opts.Prefix != "" {
params.Set("prefix", opts.Prefix)
}
if opts.Cursor != "" {
params.Set("cursor", opts.Cursor)
}
if opts.Limit > 0 {
params.Set("limit", strconv.Itoa(opts.Limit))
}
}
path := "/kv"
if len(params) > 0 {
path += "?" + params.Encode()
}
var result ListResult
err := kv.client.request(ctx, "GET", path, nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// MGet retrieves multiple values at once
func (kv *KeyValueStore) MGet(ctx context.Context, keys []string) (map[string]interface{}, error) {
var result struct {
Entries []struct {
Key string `json:"key"`
Value interface{} `json:"value"`
} `json:"entries"`
}
err := kv.client.request(ctx, "POST", "/kv/mget", map[string]interface{}{"keys": keys}, &result)
if err != nil {
return nil, err
}
values := make(map[string]interface{})
for _, e := range result.Entries {
values[e.Key] = e.Value
}
return values, nil
}
// MSet sets multiple values at once
func (kv *KeyValueStore) MSet(ctx context.Context, entries map[string]interface{}) error {
entriesList := make([]map[string]interface{}, 0, len(entries))
for k, v := range entries {
entriesList = append(entriesList, map[string]interface{}{"key": k, "value": v})
}
return kv.client.request(ctx, "POST", "/kv/mset", map[string]interface{}{"entries": entriesList}, nil)
}
// Incr increments a numeric value
func (kv *KeyValueStore) Incr(ctx context.Context, key string, by int64) (int64, error) {
var result struct {
Value int64 `json:"value"`
}
err := kv.client.request(ctx, "POST", "/kv/"+url.PathEscape(key)+"/incr", map[string]interface{}{"by": by}, &result)
if err != nil {
return 0, err
}
return result.Value, nil
}
// ==================== Document Store ====================
// DocumentStore provides document operations
type DocumentStore struct {
client *Client
}
// Create creates a new document
func (ds *DocumentStore) Create(ctx context.Context, collection string, data map[string]interface{}, opts *CreateDocumentOptions) (*Document, error) {
body := map[string]interface{}{
"data": data,
}
if opts != nil {
if opts.ID != "" {
body["id"] = opts.ID
}
if opts.Metadata != nil {
body["metadata"] = opts.Metadata
}
}
var doc Document
err := ds.client.request(ctx, "POST", "/documents/"+url.PathEscape(collection), body, &doc)
if err != nil {
return nil, err
}
return &doc, nil
}
// Get retrieves a document by ID
func (ds *DocumentStore) Get(ctx context.Context, collection, id string) (*Document, error) {
var doc Document
err := ds.client.request(ctx, "GET", "/documents/"+url.PathEscape(collection)+"/"+url.PathEscape(id), nil, &doc)
if err != nil {
return nil, err
}
return &doc, nil
}
// Update updates a document
func (ds *DocumentStore) Update(ctx context.Context, collection, id string, update map[string]interface{}, opts *UpdateDocumentOptions) (*Document, error) {
body := map[string]interface{}{
"update": update,
}
if opts != nil {
if opts.Upsert {
body["upsert"] = true
}
if opts.ReturnDocument != "" {
body["return_document"] = opts.ReturnDocument
}
}
var doc Document
err := ds.client.request(ctx, "PATCH", "/documents/"+url.PathEscape(collection)+"/"+url.PathEscape(id), body, &doc)
if err != nil {
return nil, err
}
return &doc, nil
}
// Delete deletes a document
func (ds *DocumentStore) Delete(ctx context.Context, collection, id string) (bool, error) {
var result struct {
Deleted bool `json:"deleted"`
}
err := ds.client.request(ctx, "DELETE", "/documents/"+url.PathEscape(collection)+"/"+url.PathEscape(id), nil, &result)
if err != nil {
return false, err
}
return result.Deleted, nil
}
// Query queries documents
func (ds *DocumentStore) Query(ctx context.Context, collection string, opts *QueryOptions) (*QueryResult, error) {
body := make(map[string]interface{})
if opts != nil {
if opts.Filter != nil {
body["filter"] = opts.Filter
}
if opts.Sort != nil {
body["sort"] = opts.Sort
}
if opts.Skip > 0 {
body["skip"] = opts.Skip
}
if opts.Limit > 0 {
body["limit"] = opts.Limit
}
if opts.Projection != nil {
body["projection"] = opts.Projection
}
}
var result QueryResult
err := ds.client.request(ctx, "POST", "/documents/"+url.PathEscape(collection)+"/query", body, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// Count counts documents matching a filter
func (ds *DocumentStore) Count(ctx context.Context, collection string, filter QueryFilter) (int, error) {
var result struct {
Count int `json:"count"`
}
err := ds.client.request(ctx, "POST", "/documents/"+url.PathEscape(collection)+"/count", map[string]interface{}{"filter": filter}, &result)
if err != nil {
return 0, err
}
return result.Count, nil
}
// Aggregate runs an aggregation pipeline
func (ds *DocumentStore) Aggregate(ctx context.Context, collection string, pipeline []AggregateStage) ([]interface{}, error) {
var result struct {
Results []interface{} `json:"results"`
}
err := ds.client.request(ctx, "POST", "/documents/"+url.PathEscape(collection)+"/aggregate", map[string]interface{}{"pipeline": pipeline}, &result)
if err != nil {
return nil, err
}
return result.Results, nil
}
// ==================== Vector Store ====================
// VectorStore provides vector operations
type VectorStore struct {
client *Client
}
// CreateCollection creates a vector collection
func (vs *VectorStore) CreateCollection(ctx context.Context, config VectorCollectionConfig) error {
return vs.client.request(ctx, "POST", "/vectors/collections", config, nil)
}
// DeleteCollection deletes a vector collection
func (vs *VectorStore) DeleteCollection(ctx context.Context, name string) error {
return vs.client.request(ctx, "DELETE", "/vectors/collections/"+url.PathEscape(name), nil, nil)
}
// GetCollectionStats gets collection statistics
func (vs *VectorStore) GetCollectionStats(ctx context.Context, name string) (*VectorCollectionStats, error) {
var stats VectorCollectionStats
err := vs.client.request(ctx, "GET", "/vectors/collections/"+url.PathEscape(name)+"/stats", nil, &stats)
if err != nil {
return nil, err
}
return &stats, nil
}
// Upsert upserts vectors
func (vs *VectorStore) Upsert(ctx context.Context, collection string, vectors []VectorEntry, opts *UpsertVectorOptions) (int, error) {
body := map[string]interface{}{
"vectors": vectors,
}
if opts != nil && opts.Namespace != "" {
body["namespace"] = opts.Namespace
}
var result struct {
Upserted int `json:"upserted"`
}
err := vs.client.request(ctx, "POST", "/vectors/"+url.PathEscape(collection)+"/upsert", body, &result)
if err != nil {
return 0, err
}
return result.Upserted, nil
}
// Search searches for similar vectors
func (vs *VectorStore) Search(ctx context.Context, collection string, vector []float64, opts *SearchOptions) ([]SearchResult, error) {
body := map[string]interface{}{
"vector": vector,
}
if opts != nil {
if opts.Namespace != "" {
body["namespace"] = opts.Namespace
}
if opts.TopK > 0 {
body["top_k"] = opts.TopK
}
if opts.Filter != nil {
body["filter"] = opts.Filter
}
body["include_metadata"] = opts.IncludeMetadata
body["include_vectors"] = opts.IncludeVectors
if opts.MinScore != nil {
body["min_score"] = *opts.MinScore
}
}
var result struct {
Results []SearchResult `json:"results"`
}
err := vs.client.request(ctx, "POST", "/vectors/"+url.PathEscape(collection)+"/search", body, &result)
if err != nil {
return nil, err
}
return result.Results, nil
}
// Delete deletes vectors by ID
func (vs *VectorStore) Delete(ctx context.Context, collection string, ids []string, namespace string) (int, error) {
body := map[string]interface{}{
"ids": ids,
}
if namespace != "" {
body["namespace"] = namespace
}
var result struct {
Deleted int `json:"deleted"`
}
err := vs.client.request(ctx, "POST", "/vectors/"+url.PathEscape(collection)+"/delete", body, &result)
if err != nil {
return 0, err
}
return result.Deleted, nil
}
// Fetch fetches vectors by ID
func (vs *VectorStore) Fetch(ctx context.Context, collection string, ids []string, namespace string) ([]VectorEntry, error) {
body := map[string]interface{}{
"ids": ids,
}
if namespace != "" {
body["namespace"] = namespace
}
var result struct {
Vectors []VectorEntry `json:"vectors"`
}
err := vs.client.request(ctx, "POST", "/vectors/"+url.PathEscape(collection)+"/fetch", body, &result)
if err != nil {
return nil, err
}
return result.Vectors, nil
}
// ==================== Time Series Store ====================
// TimeSeriesStore provides time series operations
type TimeSeriesStore struct {
client *Client
}
// Write writes data points to a series
func (ts *TimeSeriesStore) Write(ctx context.Context, series string, points []DataPoint, opts *WritePointsOptions) (int, error) {
body := map[string]interface{}{
"points": points,
}
if opts != nil && opts.Precision != "" {
body["precision"] = opts.Precision
}
var result struct {
Written int `json:"written"`
}
err := ts.client.request(ctx, "POST", "/timeseries/"+url.PathEscape(series)+"/write", body, &result)
if err != nil {
return 0, err
}
return result.Written, nil
}
// Query queries a time series
func (ts *TimeSeriesStore) Query(ctx context.Context, series string, timeRange TimeRange, opts *TimeSeriesQueryOptions) (*TimeSeriesResult, error) {
body := map[string]interface{}{
"range": timeRange,
}
if opts != nil {
if opts.Tags != nil {
body["tags"] = opts.Tags
}
if opts.Aggregation != "" {
body["aggregation"] = opts.Aggregation
}
if opts.Interval != "" {
body["interval"] = opts.Interval
}
if opts.Fill != nil {
body["fill"] = opts.Fill
}
if opts.Limit > 0 {
body["limit"] = opts.Limit
}
}
var result TimeSeriesResult
err := ts.client.request(ctx, "POST", "/timeseries/"+url.PathEscape(series)+"/query", body, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// Delete deletes data points in a range
func (ts *TimeSeriesStore) Delete(ctx context.Context, series string, timeRange TimeRange, tags map[string]string) (int, error) {
body := map[string]interface{}{
"range": timeRange,
}
if tags != nil {
body["tags"] = tags
}
var result struct {
Deleted int `json:"deleted"`
}
err := ts.client.request(ctx, "POST", "/timeseries/"+url.PathEscape(series)+"/delete", body, &result)
if err != nil {
return 0, err
}
return result.Deleted, nil
}
// GetSeriesInfo gets series information
func (ts *TimeSeriesStore) GetSeriesInfo(ctx context.Context, series string) (*SeriesInfo, error) {
var info SeriesInfo
err := ts.client.request(ctx, "GET", "/timeseries/"+url.PathEscape(series)+"/info", nil, &info)
if err != nil {
return nil, err
}
return &info, nil
}
// ListSeries lists all series
func (ts *TimeSeriesStore) ListSeries(ctx context.Context, prefix string) ([]SeriesInfo, error) {
path := "/timeseries"
if prefix != "" {
path += "?prefix=" + url.QueryEscape(prefix)
}
var result struct {
Series []SeriesInfo `json:"series"`
}
err := ts.client.request(ctx, "GET", path, nil, &result)
if err != nil {
return nil, err
}
return result.Series, nil
}
// SetRetention sets retention policy for a series
func (ts *TimeSeriesStore) SetRetention(ctx context.Context, series string, retentionDays int) error {
return ts.client.request(ctx, "PUT", "/timeseries/"+url.PathEscape(series)+"/retention", map[string]interface{}{"retention_days": retentionDays}, nil)
}

278
sdk/go/database/types.go Normal file
View file

@ -0,0 +1,278 @@
// Package database provides the Synor Database SDK for Go.
// Multi-model database: Key-Value, Document, Vector, and Time Series.
package database
import (
"time"
)
// Network represents the network environment
type Network string
const (
NetworkMainnet Network = "mainnet"
NetworkTestnet Network = "testnet"
NetworkDevnet Network = "devnet"
)
// Config represents the database client configuration
type Config struct {
APIKey string
Endpoint string
Network Network
Timeout time.Duration
Retries int
Debug bool
}
// DefaultConfig returns the default configuration
func DefaultConfig(apiKey string) Config {
return Config{
APIKey: apiKey,
Endpoint: "https://database.synor.io/v1",
Network: NetworkMainnet,
Timeout: 30 * time.Second,
Retries: 3,
Debug: false,
}
}
// ==================== Key-Value Types ====================
// KeyValueEntry represents a key-value entry with metadata
type KeyValueEntry struct {
Key string `json:"key"`
Value interface{} `json:"value"`
Metadata map[string]string `json:"metadata,omitempty"`
ExpiresAt *int64 `json:"expires_at,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
// SetOptions represents options for setting a key-value pair
type SetOptions struct {
TTL *int `json:"ttl,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
IfNotExists bool `json:"if_not_exists,omitempty"`
IfExists bool `json:"if_exists,omitempty"`
}
// ListOptions represents options for listing keys
type ListOptions struct {
Prefix string `json:"prefix,omitempty"`
Cursor string `json:"cursor,omitempty"`
Limit int `json:"limit,omitempty"`
}
// ListResult represents the result of listing keys
type ListResult struct {
Entries []KeyValueEntry `json:"entries"`
Cursor string `json:"cursor,omitempty"`
HasMore bool `json:"has_more"`
}
// ==================== Document Store Types ====================
// Document represents a document with metadata
type Document struct {
ID string `json:"id"`
Data map[string]interface{} `json:"data"`
Metadata map[string]string `json:"metadata,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
Version int `json:"version"`
}
// CreateDocumentOptions represents options for creating a document
type CreateDocumentOptions struct {
ID string `json:"id,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// UpdateDocumentOptions represents options for updating a document
type UpdateDocumentOptions struct {
Upsert bool `json:"upsert,omitempty"`
ReturnDocument string `json:"return_document,omitempty"` // "before" or "after"
}
// QueryFilter represents a query filter
type QueryFilter map[string]interface{}
// SortOrder represents a sort order
type SortOrder string
const (
SortAsc SortOrder = "asc"
SortDesc SortOrder = "desc"
)
// QueryOptions represents options for querying documents
type QueryOptions struct {
Filter QueryFilter `json:"filter,omitempty"`
Sort map[string]SortOrder `json:"sort,omitempty"`
Skip int `json:"skip,omitempty"`
Limit int `json:"limit,omitempty"`
Projection []string `json:"projection,omitempty"`
}
// QueryResult represents the result of a document query
type QueryResult struct {
Documents []Document `json:"documents"`
Total int `json:"total"`
HasMore bool `json:"has_more"`
}
// AggregateStage represents a stage in an aggregation pipeline
type AggregateStage map[string]interface{}
// ==================== Vector Store Types ====================
// VectorEntry represents a vector entry
type VectorEntry struct {
ID string `json:"id"`
Vector []float64 `json:"vector"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Content string `json:"content,omitempty"`
}
// DistanceMetric represents a vector distance metric
type DistanceMetric string
const (
DistanceCosine DistanceMetric = "cosine"
DistanceEuclidean DistanceMetric = "euclidean"
DistanceDotProduct DistanceMetric = "dotProduct"
)
// UpsertVectorOptions represents options for upserting vectors
type UpsertVectorOptions struct {
Namespace string `json:"namespace,omitempty"`
}
// SearchOptions represents options for vector search
type SearchOptions struct {
Namespace string `json:"namespace,omitempty"`
TopK int `json:"top_k,omitempty"`
Filter QueryFilter `json:"filter,omitempty"`
IncludeMetadata bool `json:"include_metadata,omitempty"`
IncludeVectors bool `json:"include_vectors,omitempty"`
MinScore *float64 `json:"min_score,omitempty"`
}
// SearchResult represents a vector search result
type SearchResult struct {
ID string `json:"id"`
Score float64 `json:"score"`
Vector []float64 `json:"vector,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Content string `json:"content,omitempty"`
}
// VectorCollectionConfig represents configuration for a vector collection
type VectorCollectionConfig struct {
Name string `json:"name"`
Dimension int `json:"dimension"`
Metric DistanceMetric `json:"metric,omitempty"`
IndexType string `json:"index_type,omitempty"`
}
// VectorCollectionStats represents statistics for a vector collection
type VectorCollectionStats struct {
Name string `json:"name"`
VectorCount int `json:"vector_count"`
Dimension int `json:"dimension"`
IndexSize int64 `json:"index_size"`
}
// ==================== Time Series Types ====================
// DataPoint represents a time series data point
type DataPoint struct {
Timestamp int64 `json:"timestamp"`
Value float64 `json:"value"`
Tags map[string]string `json:"tags,omitempty"`
}
// WritePointsOptions represents options for writing data points
type WritePointsOptions struct {
Precision string `json:"precision,omitempty"` // "ns", "us", "ms", "s"
}
// Aggregation represents a time series aggregation function
type Aggregation string
const (
AggMean Aggregation = "mean"
AggSum Aggregation = "sum"
AggCount Aggregation = "count"
AggMin Aggregation = "min"
AggMax Aggregation = "max"
AggFirst Aggregation = "first"
AggLast Aggregation = "last"
AggStddev Aggregation = "stddev"
AggVariance Aggregation = "variance"
)
// TimeRange represents a time range for queries
type TimeRange struct {
Start interface{} `json:"start"` // int64, time.Time, or string
End interface{} `json:"end"` // int64, time.Time, or string
}
// TimeSeriesQueryOptions represents options for time series queries
type TimeSeriesQueryOptions struct {
Tags map[string]string `json:"tags,omitempty"`
Aggregation Aggregation `json:"aggregation,omitempty"`
Interval string `json:"interval,omitempty"` // e.g., "1m", "5m", "1h"
Fill interface{} `json:"fill,omitempty"` // "none", "null", "previous", or number
Limit int `json:"limit,omitempty"`
}
// TimeSeriesResult represents the result of a time series query
type TimeSeriesResult struct {
Series string `json:"series"`
Points []DataPoint `json:"points"`
Statistics map[string]float64 `json:"statistics,omitempty"`
}
// SeriesInfo represents information about a time series
type SeriesInfo struct {
Name string `json:"name"`
Tags []string `json:"tags"`
RetentionDays int `json:"retention_days"`
PointCount int64 `json:"point_count"`
}
// ==================== Database Stats ====================
// DatabaseStats represents overall database statistics
type DatabaseStats struct {
KVEntries int `json:"kv_entries"`
DocumentCount int `json:"document_count"`
VectorCount int `json:"vector_count"`
TimeseriesPoints int64 `json:"timeseries_points"`
StorageUsed int64 `json:"storage_used"`
StorageLimit int64 `json:"storage_limit"`
}
// ==================== Errors ====================
// DatabaseError represents a database error
type DatabaseError struct {
Message string
Code string
StatusCode int
}
func (e *DatabaseError) Error() string {
return e.Message
}
// NewDatabaseError creates a new DatabaseError
func NewDatabaseError(message string, code string, statusCode int) *DatabaseError {
return &DatabaseError{
Message: message,
Code: code,
StatusCode: statusCode,
}
}

470
sdk/go/hosting/client.go Normal file
View file

@ -0,0 +1,470 @@
package hosting
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
// Client is the Synor Hosting SDK client
type Client struct {
config Config
httpClient *http.Client
closed bool
}
// New creates a new Hosting client
func New(config Config) *Client {
if config.Endpoint == "" {
config.Endpoint = "https://hosting.synor.io/v1"
}
if config.Timeout == 0 {
config.Timeout = 60 * time.Second
}
if config.Retries == 0 {
config.Retries = 3
}
return &Client{
config: config,
httpClient: &http.Client{
Timeout: config.Timeout,
},
}
}
// ==================== Domain Operations ====================
// CheckAvailability checks domain availability
func (c *Client) CheckAvailability(ctx context.Context, name string) (*DomainAvailability, error) {
var result DomainAvailability
err := c.request(ctx, "GET", "/domains/check/"+url.PathEscape(name), nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// RegisterDomain registers a new domain
func (c *Client) RegisterDomain(ctx context.Context, name string, opts *RegisterDomainOptions) (*Domain, error) {
body := map[string]interface{}{
"name": name,
}
if opts != nil {
if opts.Years > 0 {
body["years"] = opts.Years
}
body["auto_renew"] = opts.AutoRenew
if opts.Records != nil {
body["records"] = opts.Records
}
}
var result Domain
err := c.request(ctx, "POST", "/domains", body, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// GetDomain gets domain information
func (c *Client) GetDomain(ctx context.Context, name string) (*Domain, error) {
var result Domain
err := c.request(ctx, "GET", "/domains/"+url.PathEscape(name), nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// ListDomains lists all domains
func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) {
var result struct {
Domains []Domain `json:"domains"`
}
err := c.request(ctx, "GET", "/domains", nil, &result)
if err != nil {
return nil, err
}
return result.Domains, nil
}
// UpdateDomainRecord updates domain record
func (c *Client) UpdateDomainRecord(ctx context.Context, name string, record *DomainRecord) (*Domain, error) {
var result Domain
err := c.request(ctx, "PUT", "/domains/"+url.PathEscape(name)+"/record", record, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// ResolveDomain resolves a domain
func (c *Client) ResolveDomain(ctx context.Context, name string) (*DomainRecord, error) {
var result DomainRecord
err := c.request(ctx, "GET", "/domains/"+url.PathEscape(name)+"/resolve", nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// RenewDomain renews a domain
func (c *Client) RenewDomain(ctx context.Context, name string, years int) (*Domain, error) {
var result Domain
err := c.request(ctx, "POST", "/domains/"+url.PathEscape(name)+"/renew", map[string]interface{}{"years": years}, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// ==================== DNS Operations ====================
// GetDnsZone gets DNS zone for a domain
func (c *Client) GetDnsZone(ctx context.Context, domain string) (*DnsZone, error) {
var result DnsZone
err := c.request(ctx, "GET", "/dns/"+url.PathEscape(domain), nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// SetDnsRecords sets DNS records for a domain
func (c *Client) SetDnsRecords(ctx context.Context, domain string, records []DnsRecord) (*DnsZone, error) {
var result DnsZone
err := c.request(ctx, "PUT", "/dns/"+url.PathEscape(domain), map[string]interface{}{"records": records}, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// AddDnsRecord adds a DNS record
func (c *Client) AddDnsRecord(ctx context.Context, domain string, record DnsRecord) (*DnsZone, error) {
var result DnsZone
err := c.request(ctx, "POST", "/dns/"+url.PathEscape(domain)+"/records", record, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// DeleteDnsRecord deletes a DNS record
func (c *Client) DeleteDnsRecord(ctx context.Context, domain, recordType, name string) (*DnsZone, error) {
var result DnsZone
path := fmt.Sprintf("/dns/%s/records/%s/%s", url.PathEscape(domain), recordType, url.PathEscape(name))
err := c.request(ctx, "DELETE", path, nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// ==================== Deployment Operations ====================
// Deploy deploys a site from CID
func (c *Client) Deploy(ctx context.Context, cid string, opts *DeployOptions) (*Deployment, error) {
body := map[string]interface{}{
"cid": cid,
}
if opts != nil {
if opts.Domain != "" {
body["domain"] = opts.Domain
}
if opts.Subdomain != "" {
body["subdomain"] = opts.Subdomain
}
if opts.Headers != nil {
body["headers"] = opts.Headers
}
if opts.Redirects != nil {
body["redirects"] = opts.Redirects
}
body["spa"] = opts.SPA
body["clean_urls"] = opts.CleanURLs
body["trailing_slash"] = opts.TrailingSlash
}
var result Deployment
err := c.request(ctx, "POST", "/deployments", body, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// GetDeployment gets deployment by ID
func (c *Client) GetDeployment(ctx context.Context, id string) (*Deployment, error) {
var result Deployment
err := c.request(ctx, "GET", "/deployments/"+url.PathEscape(id), nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// ListDeployments lists deployments
func (c *Client) ListDeployments(ctx context.Context, domain string) ([]Deployment, error) {
path := "/deployments"
if domain != "" {
path += "?domain=" + url.QueryEscape(domain)
}
var result struct {
Deployments []Deployment `json:"deployments"`
}
err := c.request(ctx, "GET", path, nil, &result)
if err != nil {
return nil, err
}
return result.Deployments, nil
}
// Rollback rolls back to a previous deployment
func (c *Client) Rollback(ctx context.Context, domain, deploymentID string) (*Deployment, error) {
var result Deployment
err := c.request(ctx, "POST", "/deployments/"+url.PathEscape(deploymentID)+"/rollback", map[string]interface{}{"domain": domain}, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// DeleteDeployment deletes a deployment
func (c *Client) DeleteDeployment(ctx context.Context, id string) error {
return c.request(ctx, "DELETE", "/deployments/"+url.PathEscape(id), nil, nil)
}
// GetDeploymentStats gets deployment stats
func (c *Client) GetDeploymentStats(ctx context.Context, id, period string) (*DeploymentStats, error) {
path := fmt.Sprintf("/deployments/%s/stats?period=%s", url.PathEscape(id), url.QueryEscape(period))
var result DeploymentStats
err := c.request(ctx, "GET", path, nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// ==================== SSL Operations ====================
// ProvisionSSL provisions SSL certificate
func (c *Client) ProvisionSSL(ctx context.Context, domain string, opts *ProvisionSSLOptions) (*Certificate, error) {
body := make(map[string]interface{})
if opts != nil {
body["include_www"] = opts.IncludeWWW
body["auto_renew"] = opts.AutoRenew
}
var result Certificate
err := c.request(ctx, "POST", "/ssl/"+url.PathEscape(domain), body, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// GetCertificate gets certificate status
func (c *Client) GetCertificate(ctx context.Context, domain string) (*Certificate, error) {
var result Certificate
err := c.request(ctx, "GET", "/ssl/"+url.PathEscape(domain), nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// RenewCertificate renews SSL certificate
func (c *Client) RenewCertificate(ctx context.Context, domain string) (*Certificate, error) {
var result Certificate
err := c.request(ctx, "POST", "/ssl/"+url.PathEscape(domain)+"/renew", nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// DeleteCertificate deletes/revokes SSL certificate
func (c *Client) DeleteCertificate(ctx context.Context, domain string) error {
return c.request(ctx, "DELETE", "/ssl/"+url.PathEscape(domain), nil, nil)
}
// ==================== Site Configuration ====================
// GetSiteConfig gets site configuration
func (c *Client) GetSiteConfig(ctx context.Context, domain string) (*SiteConfig, error) {
var result SiteConfig
err := c.request(ctx, "GET", "/sites/"+url.PathEscape(domain)+"/config", nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// UpdateSiteConfig updates site configuration
func (c *Client) UpdateSiteConfig(ctx context.Context, domain string, config map[string]interface{}) (*SiteConfig, error) {
var result SiteConfig
err := c.request(ctx, "PATCH", "/sites/"+url.PathEscape(domain)+"/config", config, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// PurgeCache purges CDN cache
func (c *Client) PurgeCache(ctx context.Context, domain string, paths []string) (int, error) {
body := make(map[string]interface{})
if paths != nil {
body["paths"] = paths
}
var result struct {
Purged int `json:"purged"`
}
err := c.request(ctx, "DELETE", "/sites/"+url.PathEscape(domain)+"/cache", body, &result)
if err != nil {
return 0, err
}
return result.Purged, nil
}
// ==================== Analytics ====================
// GetAnalytics gets site analytics
func (c *Client) GetAnalytics(ctx context.Context, domain string, opts *AnalyticsOptions) (*AnalyticsData, error) {
params := url.Values{}
if opts != nil {
if opts.Period != "" {
params.Set("period", opts.Period)
}
if opts.StartDate != "" {
params.Set("start", opts.StartDate)
}
if opts.EndDate != "" {
params.Set("end", opts.EndDate)
}
}
path := "/sites/" + url.PathEscape(domain) + "/analytics"
if len(params) > 0 {
path += "?" + params.Encode()
}
var result AnalyticsData
err := c.request(ctx, "GET", path, nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// ==================== Lifecycle ====================
// Close closes the client
func (c *Client) Close() {
c.closed = true
}
// IsClosed returns whether the client is closed
func (c *Client) IsClosed() bool {
return c.closed
}
// HealthCheck performs a health check
func (c *Client) HealthCheck(ctx context.Context) bool {
var result struct {
Status string `json:"status"`
}
err := c.request(ctx, "GET", "/health", nil, &result)
return err == nil && result.Status == "healthy"
}
func (c *Client) request(ctx context.Context, method, path string, body interface{}, result interface{}) error {
if c.closed {
return NewHostingError("Client has been closed", "CLIENT_CLOSED", 0)
}
var lastErr error
for attempt := 0; attempt < c.config.Retries; attempt++ {
err := c.doRequest(ctx, method, path, body, result)
if err == nil {
return nil
}
lastErr = err
if c.config.Debug {
fmt.Printf("Attempt %d failed: %v\n", attempt+1, err)
}
if attempt < c.config.Retries-1 {
time.Sleep(time.Duration(attempt+1) * time.Second)
}
}
return lastErr
}
func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}, result interface{}) error {
var bodyReader io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
return err
}
bodyReader = bytes.NewReader(jsonBody)
}
req, err := http.NewRequestWithContext(ctx, method, c.config.Endpoint+path, bodyReader)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-SDK-Version", "go/0.1.0")
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode >= 400 {
var errResp struct {
Message string `json:"message"`
Error string `json:"error"`
Code string `json:"code"`
}
json.Unmarshal(respBody, &errResp)
msg := errResp.Message
if msg == "" {
msg = errResp.Error
}
if msg == "" {
msg = fmt.Sprintf("HTTP %d", resp.StatusCode)
}
return NewHostingError(msg, errResp.Code, resp.StatusCode)
}
if result != nil && len(respBody) > 0 {
return json.Unmarshal(respBody, result)
}
return nil
}

276
sdk/go/hosting/types.go Normal file
View file

@ -0,0 +1,276 @@
// Package hosting provides the Synor Hosting SDK for Go.
// Decentralized web hosting with domain management, DNS, and deployments.
package hosting
import (
"time"
)
// Network represents the network environment
type Network string
const (
NetworkMainnet Network = "mainnet"
NetworkTestnet Network = "testnet"
NetworkDevnet Network = "devnet"
)
// Config represents the hosting client configuration
type Config struct {
APIKey string
Endpoint string
Network Network
Timeout time.Duration
Retries int
Debug bool
}
// DefaultConfig returns the default configuration
func DefaultConfig(apiKey string) Config {
return Config{
APIKey: apiKey,
Endpoint: "https://hosting.synor.io/v1",
Network: NetworkMainnet,
Timeout: 60 * time.Second,
Retries: 3,
Debug: false,
}
}
// ==================== Domain Types ====================
// DomainStatus represents a domain status
type DomainStatus string
const (
DomainStatusPending DomainStatus = "pending"
DomainStatusActive DomainStatus = "active"
DomainStatusExpired DomainStatus = "expired"
DomainStatusSuspended DomainStatus = "suspended"
)
// Domain represents a domain
type Domain struct {
Name string `json:"name"`
Status DomainStatus `json:"status"`
Owner string `json:"owner"`
RegisteredAt int64 `json:"registered_at"`
ExpiresAt int64 `json:"expires_at"`
AutoRenew bool `json:"auto_renew"`
Records *DomainRecord `json:"records,omitempty"`
}
// DomainRecord represents a domain record for resolution
type DomainRecord struct {
CID string `json:"cid,omitempty"`
IPv4 []string `json:"ipv4,omitempty"`
IPv6 []string `json:"ipv6,omitempty"`
CNAME string `json:"cname,omitempty"`
TXT []string `json:"txt,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// DomainAvailability represents domain availability check result
type DomainAvailability struct {
Name string `json:"name"`
Available bool `json:"available"`
Price *float64 `json:"price,omitempty"`
Premium bool `json:"premium"`
}
// RegisterDomainOptions represents options for registering a domain
type RegisterDomainOptions struct {
Years int `json:"years,omitempty"`
AutoRenew bool `json:"auto_renew,omitempty"`
Records *DomainRecord `json:"records,omitempty"`
}
// ==================== DNS Types ====================
// DnsRecordType represents a DNS record type
type DnsRecordType string
const (
DnsRecordTypeA DnsRecordType = "A"
DnsRecordTypeAAAA DnsRecordType = "AAAA"
DnsRecordTypeCNAME DnsRecordType = "CNAME"
DnsRecordTypeTXT DnsRecordType = "TXT"
DnsRecordTypeMX DnsRecordType = "MX"
DnsRecordTypeNS DnsRecordType = "NS"
DnsRecordTypeSRV DnsRecordType = "SRV"
DnsRecordTypeCAA DnsRecordType = "CAA"
)
// DnsRecord represents a DNS record
type DnsRecord struct {
Type DnsRecordType `json:"type"`
Name string `json:"name"`
Value string `json:"value"`
TTL int `json:"ttl,omitempty"`
Priority *int `json:"priority,omitempty"`
}
// DnsZone represents a DNS zone
type DnsZone struct {
Domain string `json:"domain"`
Records []DnsRecord `json:"records"`
UpdatedAt int64 `json:"updated_at"`
}
// ==================== Deployment Types ====================
// DeploymentStatus represents a deployment status
type DeploymentStatus string
const (
DeploymentStatusPending DeploymentStatus = "pending"
DeploymentStatusBuilding DeploymentStatus = "building"
DeploymentStatusDeploying DeploymentStatus = "deploying"
DeploymentStatusActive DeploymentStatus = "active"
DeploymentStatusFailed DeploymentStatus = "failed"
DeploymentStatusInactive DeploymentStatus = "inactive"
)
// Deployment represents a deployment
type Deployment struct {
ID string `json:"id"`
Domain string `json:"domain"`
CID string `json:"cid"`
Status DeploymentStatus `json:"status"`
URL string `json:"url"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
BuildLogs string `json:"build_logs,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
// RedirectRule represents a redirect rule
type RedirectRule struct {
Source string `json:"source"`
Destination string `json:"destination"`
StatusCode int `json:"status_code,omitempty"`
Permanent bool `json:"permanent,omitempty"`
}
// DeployOptions represents options for deploying a site
type DeployOptions struct {
Domain string `json:"domain,omitempty"`
Subdomain string `json:"subdomain,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
Redirects []RedirectRule `json:"redirects,omitempty"`
SPA bool `json:"spa,omitempty"`
CleanURLs bool `json:"clean_urls,omitempty"`
TrailingSlash bool `json:"trailing_slash,omitempty"`
}
// DeploymentStats represents deployment statistics
type DeploymentStats struct {
Requests int `json:"requests"`
Bandwidth int64 `json:"bandwidth"`
UniqueVisitors int `json:"unique_visitors"`
Period string `json:"period"`
}
// ==================== SSL Types ====================
// CertificateStatus represents a certificate status
type CertificateStatus string
const (
CertificateStatusPending CertificateStatus = "pending"
CertificateStatusIssued CertificateStatus = "issued"
CertificateStatusExpired CertificateStatus = "expired"
CertificateStatusRevoked CertificateStatus = "revoked"
)
// Certificate represents an SSL certificate
type Certificate struct {
Domain string `json:"domain"`
Status CertificateStatus `json:"status"`
AutoRenew bool `json:"auto_renew"`
Issuer string `json:"issuer"`
IssuedAt *int64 `json:"issued_at,omitempty"`
ExpiresAt *int64 `json:"expires_at,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
}
// ProvisionSSLOptions represents options for provisioning SSL
type ProvisionSSLOptions struct {
IncludeWWW bool `json:"include_www,omitempty"`
AutoRenew bool `json:"auto_renew,omitempty"`
}
// ==================== Site Configuration ====================
// SiteConfig represents site configuration
type SiteConfig struct {
Domain string `json:"domain"`
CID string `json:"cid,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
Redirects []RedirectRule `json:"redirects,omitempty"`
ErrorPages map[string]string `json:"error_pages,omitempty"`
SPA bool `json:"spa"`
CleanURLs bool `json:"clean_urls"`
TrailingSlash bool `json:"trailing_slash"`
}
// ==================== Analytics ====================
// PageView represents page view data
type PageView struct {
Path string `json:"path"`
Views int `json:"views"`
}
// Referrer represents referrer data
type Referrer struct {
Referrer string `json:"referrer"`
Count int `json:"count"`
}
// Country represents country data
type Country struct {
Country string `json:"country"`
Count int `json:"count"`
}
// AnalyticsData represents analytics data
type AnalyticsData struct {
Domain string `json:"domain"`
Period string `json:"period"`
PageViews int `json:"page_views"`
UniqueVisitors int `json:"unique_visitors"`
Bandwidth int64 `json:"bandwidth"`
TopPages []PageView `json:"top_pages"`
TopReferrers []Referrer `json:"top_referrers"`
TopCountries []Country `json:"top_countries"`
}
// AnalyticsOptions represents options for analytics query
type AnalyticsOptions struct {
Period string `json:"period,omitempty"`
StartDate string `json:"start,omitempty"`
EndDate string `json:"end,omitempty"`
}
// ==================== Errors ====================
// HostingError represents a hosting error
type HostingError struct {
Message string
Code string
StatusCode int
}
func (e *HostingError) Error() string {
return e.Message
}
// NewHostingError creates a new HostingError
func NewHostingError(message string, code string, statusCode int) *HostingError {
return &HostingError{
Message: message,
Code: code,
StatusCode: statusCode,
}
}

View file

@ -5,13 +5,13 @@
<modelVersion>4.0.0</modelVersion>
<groupId>io.synor</groupId>
<artifactId>synor-compute</artifactId>
<artifactId>synor-sdk</artifactId>
<version>0.1.0</version>
<packaging>jar</packaging>
<name>Synor Compute SDK</name>
<description>Java SDK for Synor Compute - Distributed Heterogeneous Computing</description>
<url>https://github.com/synor/synor-compute-java</url>
<name>Synor SDK</name>
<description>Java SDK for Synor - Compute, Wallet, RPC, and Storage</description>
<url>https://synor.cc</url>
<licenses>
<license>
@ -48,6 +48,11 @@
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<!-- Testing -->
<dependency>

View file

@ -0,0 +1,371 @@
package io.synor.bridge;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Synor Bridge SDK for Java.
* Cross-chain asset transfers with lock-mint and burn-unlock patterns.
*/
public class SynorBridge implements AutoCloseable {
private static final String DEFAULT_ENDPOINT = "https://bridge.synor.io/v1";
private static final Gson gson = new GsonBuilder().create();
private static final Set<TransferStatus> FINAL_STATUSES = Set.of(
TransferStatus.completed, TransferStatus.failed, TransferStatus.refunded
);
private final BridgeConfig config;
private final HttpClient httpClient;
private final AtomicBoolean closed = new AtomicBoolean(false);
public SynorBridge(String apiKey) {
this(new BridgeConfig(apiKey, DEFAULT_ENDPOINT, 60, 3, false));
}
public SynorBridge(BridgeConfig config) {
this.config = config;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(config.getTimeoutSecs()))
.build();
}
// ==================== Chain Operations ====================
public CompletableFuture<List<Chain>> getSupportedChains() {
return request("GET", "/chains", null)
.thenApply(resp -> {
Type type = new TypeToken<ChainsResponse>(){}.getType();
ChainsResponse result = gson.fromJson(resp, type);
return result.chains;
});
}
public CompletableFuture<Chain> getChain(ChainId chainId) {
return request("GET", "/chains/" + chainId.name().toLowerCase(), null)
.thenApply(resp -> gson.fromJson(resp, Chain.class));
}
public CompletableFuture<Boolean> isChainSupported(ChainId chainId) {
return getChain(chainId)
.thenApply(Chain::isSupported)
.exceptionally(e -> false);
}
// ==================== Asset Operations ====================
public CompletableFuture<List<Asset>> getSupportedAssets(ChainId chainId) {
return request("GET", "/chains/" + chainId.name().toLowerCase() + "/assets", null)
.thenApply(resp -> {
Type type = new TypeToken<AssetsResponse>(){}.getType();
AssetsResponse result = gson.fromJson(resp, type);
return result.assets;
});
}
public CompletableFuture<Asset> getAsset(String assetId) {
return request("GET", "/assets/" + encode(assetId), null)
.thenApply(resp -> gson.fromJson(resp, Asset.class));
}
public CompletableFuture<WrappedAsset> getWrappedAsset(String originalAssetId, ChainId targetChain) {
String path = "/assets/" + encode(originalAssetId) + "/wrapped/" + targetChain.name().toLowerCase();
return request("GET", path, null)
.thenApply(resp -> gson.fromJson(resp, WrappedAsset.class));
}
// ==================== Fee & Rate Operations ====================
public CompletableFuture<FeeEstimate> estimateFee(String asset, String amount, ChainId sourceChain, ChainId targetChain) {
Map<String, Object> body = new HashMap<>();
body.put("asset", asset);
body.put("amount", amount);
body.put("sourceChain", sourceChain.name().toLowerCase());
body.put("targetChain", targetChain.name().toLowerCase());
return request("POST", "/fees/estimate", body)
.thenApply(resp -> gson.fromJson(resp, FeeEstimate.class));
}
public CompletableFuture<ExchangeRate> getExchangeRate(String fromAsset, String toAsset) {
String path = "/rates/" + encode(fromAsset) + "/" + encode(toAsset);
return request("GET", path, null)
.thenApply(resp -> gson.fromJson(resp, ExchangeRate.class));
}
// ==================== Lock-Mint Flow ====================
public CompletableFuture<LockReceipt> lock(String asset, String amount, ChainId targetChain, LockOptions options) {
Map<String, Object> body = new HashMap<>();
body.put("asset", asset);
body.put("amount", amount);
body.put("targetChain", targetChain.name().toLowerCase());
if (options != null) {
if (options.getRecipient() != null) body.put("recipient", options.getRecipient());
if (options.getDeadline() != null) body.put("deadline", options.getDeadline());
if (options.getSlippage() != null) body.put("slippage", options.getSlippage());
}
return request("POST", "/transfers/lock", body)
.thenApply(resp -> gson.fromJson(resp, LockReceipt.class));
}
public CompletableFuture<LockProof> getLockProof(String lockReceiptId) {
return request("GET", "/transfers/lock/" + encode(lockReceiptId) + "/proof", null)
.thenApply(resp -> gson.fromJson(resp, LockProof.class));
}
public CompletableFuture<LockProof> waitForLockProof(String lockReceiptId, long pollIntervalMs, long maxWaitMs) {
long deadline = System.currentTimeMillis() + maxWaitMs;
return waitForLockProofInternal(lockReceiptId, pollIntervalMs, deadline);
}
private CompletableFuture<LockProof> waitForLockProofInternal(String id, long pollMs, long deadline) {
if (System.currentTimeMillis() >= deadline) {
return CompletableFuture.failedFuture(
new BridgeException("Timeout waiting for lock proof", "CONFIRMATIONS_PENDING", 0));
}
return getLockProof(id)
.exceptionallyCompose(e -> {
if (e.getCause() instanceof BridgeException &&
"CONFIRMATIONS_PENDING".equals(((BridgeException) e.getCause()).getCode())) {
return CompletableFuture.runAsync(() -> sleep(pollMs))
.thenCompose(v -> waitForLockProofInternal(id, pollMs, deadline));
}
return CompletableFuture.failedFuture(e);
});
}
public CompletableFuture<SignedTransaction> mint(LockProof proof, String targetAddress, MintOptions options) {
Map<String, Object> body = new HashMap<>();
body.put("proof", proof);
body.put("targetAddress", targetAddress);
if (options != null) {
if (options.getGasLimit() != null) body.put("gasLimit", options.getGasLimit());
if (options.getMaxFeePerGas() != null) body.put("maxFeePerGas", options.getMaxFeePerGas());
}
return request("POST", "/transfers/mint", body)
.thenApply(resp -> gson.fromJson(resp, SignedTransaction.class));
}
// ==================== Burn-Unlock Flow ====================
public CompletableFuture<BurnReceipt> burn(String wrappedAsset, String amount, BurnOptions options) {
Map<String, Object> body = new HashMap<>();
body.put("wrappedAsset", wrappedAsset);
body.put("amount", amount);
if (options != null) {
if (options.getRecipient() != null) body.put("recipient", options.getRecipient());
if (options.getDeadline() != null) body.put("deadline", options.getDeadline());
}
return request("POST", "/transfers/burn", body)
.thenApply(resp -> gson.fromJson(resp, BurnReceipt.class));
}
public CompletableFuture<BurnProof> getBurnProof(String burnReceiptId) {
return request("GET", "/transfers/burn/" + encode(burnReceiptId) + "/proof", null)
.thenApply(resp -> gson.fromJson(resp, BurnProof.class));
}
public CompletableFuture<BurnProof> waitForBurnProof(String burnReceiptId, long pollIntervalMs, long maxWaitMs) {
long deadline = System.currentTimeMillis() + maxWaitMs;
return waitForBurnProofInternal(burnReceiptId, pollIntervalMs, deadline);
}
private CompletableFuture<BurnProof> waitForBurnProofInternal(String id, long pollMs, long deadline) {
if (System.currentTimeMillis() >= deadline) {
return CompletableFuture.failedFuture(
new BridgeException("Timeout waiting for burn proof", "CONFIRMATIONS_PENDING", 0));
}
return getBurnProof(id)
.exceptionallyCompose(e -> {
if (e.getCause() instanceof BridgeException &&
"CONFIRMATIONS_PENDING".equals(((BridgeException) e.getCause()).getCode())) {
return CompletableFuture.runAsync(() -> sleep(pollMs))
.thenCompose(v -> waitForBurnProofInternal(id, pollMs, deadline));
}
return CompletableFuture.failedFuture(e);
});
}
public CompletableFuture<SignedTransaction> unlock(BurnProof proof, UnlockOptions options) {
Map<String, Object> body = new HashMap<>();
body.put("proof", proof);
if (options != null) {
if (options.getGasLimit() != null) body.put("gasLimit", options.getGasLimit());
if (options.getGasPrice() != null) body.put("gasPrice", options.getGasPrice());
}
return request("POST", "/transfers/unlock", body)
.thenApply(resp -> gson.fromJson(resp, SignedTransaction.class));
}
// ==================== Transfer Management ====================
public CompletableFuture<Transfer> getTransfer(String transferId) {
return request("GET", "/transfers/" + encode(transferId), null)
.thenApply(resp -> gson.fromJson(resp, Transfer.class));
}
public CompletableFuture<TransferStatus> getTransferStatus(String transferId) {
return getTransfer(transferId).thenApply(Transfer::getStatus);
}
public CompletableFuture<List<Transfer>> listTransfers(TransferFilter filter) {
StringBuilder path = new StringBuilder("/transfers");
if (filter != null) {
StringBuilder params = new StringBuilder();
if (filter.getStatus() != null) params.append("status=").append(filter.getStatus().name().toLowerCase());
if (filter.getSourceChain() != null) {
if (params.length() > 0) params.append("&");
params.append("sourceChain=").append(filter.getSourceChain().name().toLowerCase());
}
if (filter.getTargetChain() != null) {
if (params.length() > 0) params.append("&");
params.append("targetChain=").append(filter.getTargetChain().name().toLowerCase());
}
if (filter.getLimit() != null) {
if (params.length() > 0) params.append("&");
params.append("limit=").append(filter.getLimit());
}
if (params.length() > 0) path.append("?").append(params);
}
return request("GET", path.toString(), null)
.thenApply(resp -> {
Type type = new TypeToken<TransfersResponse>(){}.getType();
TransfersResponse result = gson.fromJson(resp, type);
return result.transfers;
});
}
public CompletableFuture<Transfer> waitForTransfer(String transferId, long pollIntervalMs, long maxWaitMs) {
long deadline = System.currentTimeMillis() + maxWaitMs;
return waitForTransferInternal(transferId, pollIntervalMs, deadline);
}
private CompletableFuture<Transfer> waitForTransferInternal(String id, long pollMs, long deadline) {
if (System.currentTimeMillis() >= deadline) {
return CompletableFuture.failedFuture(new BridgeException("Timeout waiting for transfer completion"));
}
return getTransfer(id).thenCompose(transfer -> {
if (FINAL_STATUSES.contains(transfer.getStatus())) {
return CompletableFuture.completedFuture(transfer);
}
return CompletableFuture.runAsync(() -> sleep(pollMs))
.thenCompose(v -> waitForTransferInternal(id, pollMs, deadline));
});
}
// ==================== Convenience Methods ====================
public CompletableFuture<Transfer> bridgeTo(String asset, String amount, ChainId targetChain,
String targetAddress, LockOptions lockOpts, MintOptions mintOpts) {
return lock(asset, amount, targetChain, lockOpts)
.thenCompose(receipt -> waitForLockProof(receipt.getId(), 5000, 600000)
.thenCompose(proof -> mint(proof, targetAddress, mintOpts))
.thenCompose(tx -> waitForTransfer(receipt.getId(), 10000, 1800000)));
}
public CompletableFuture<Transfer> bridgeBack(String wrappedAsset, String amount,
BurnOptions burnOpts, UnlockOptions unlockOpts) {
return burn(wrappedAsset, amount, burnOpts)
.thenCompose(receipt -> waitForBurnProof(receipt.getId(), 5000, 600000)
.thenCompose(proof -> unlock(proof, unlockOpts))
.thenCompose(tx -> waitForTransfer(receipt.getId(), 10000, 1800000)));
}
// ==================== Lifecycle ====================
@Override
public void close() {
closed.set(true);
}
public boolean isClosed() {
return closed.get();
}
public CompletableFuture<Boolean> healthCheck() {
return request("GET", "/health", null)
.thenApply(resp -> {
Map<String, Object> map = gson.fromJson(resp, new TypeToken<Map<String, Object>>(){}.getType());
return "healthy".equals(map.get("status"));
})
.exceptionally(e -> false);
}
// ==================== Private Methods ====================
private CompletableFuture<String> request(String method, String path, Object body) {
if (closed.get()) {
return CompletableFuture.failedFuture(new BridgeException("Client has been closed"));
}
return doRequest(method, path, body, 0);
}
private CompletableFuture<String> doRequest(String method, String path, Object body, int attempt) {
String url = config.getEndpoint() + path;
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + config.getApiKey())
.header("Content-Type", "application/json")
.header("X-SDK-Version", "java/0.1.0")
.timeout(Duration.ofSeconds(config.getTimeoutSecs()));
HttpRequest.BodyPublisher bodyPub = body != null
? HttpRequest.BodyPublishers.ofString(gson.toJson(body))
: HttpRequest.BodyPublishers.noBody();
switch (method) {
case "GET": builder.GET(); break;
case "POST": builder.POST(bodyPub); break;
case "PUT": builder.PUT(bodyPub); break;
case "DELETE": builder.method("DELETE", bodyPub); break;
}
return httpClient.sendAsync(builder.build(), HttpResponse.BodyHandlers.ofString())
.thenCompose(response -> {
if (response.statusCode() >= 400) {
Map<String, Object> errorBody = gson.fromJson(response.body(),
new TypeToken<Map<String, Object>>(){}.getType());
String message = errorBody != null && errorBody.get("message") != null
? (String) errorBody.get("message") : "HTTP " + response.statusCode();
String code = errorBody != null ? (String) errorBody.get("code") : null;
return CompletableFuture.failedFuture(
new BridgeException(message, code, response.statusCode()));
}
return CompletableFuture.completedFuture(response.body());
})
.exceptionally(e -> {
if (attempt < config.getRetries() - 1) {
sleep((long) Math.pow(2, attempt) * 1000);
return doRequest(method, path, body, attempt + 1).join();
}
throw new RuntimeException(e);
});
}
private static String encode(String value) {
return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8);
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
// Helper response types
private static class ChainsResponse { List<Chain> chains; }
private static class AssetsResponse { List<Asset> assets; }
private static class TransfersResponse { List<Transfer> transfers; }
}

View file

@ -0,0 +1,404 @@
package io.synor.bridge;
import java.util.List;
/**
* Bridge SDK Types for Java.
*/
public class Types {
// Organizational class
}
// ==================== Enums ====================
enum ChainId { synor, ethereum, polygon, arbitrum, optimism, bsc, avalanche, solana, cosmos }
enum AssetType { native, erc20, erc721, erc1155 }
enum TransferStatus { pending, locked, confirming, minting, completed, failed, refunded }
enum TransferDirection { lock_mint, burn_unlock }
// ==================== Config ====================
class BridgeConfig {
private final String apiKey;
private final String endpoint;
private final int timeoutSecs;
private final int retries;
private final boolean debug;
public BridgeConfig(String apiKey) {
this(apiKey, "https://bridge.synor.io/v1", 60, 3, false);
}
public BridgeConfig(String apiKey, String endpoint, int timeoutSecs, int retries, boolean debug) {
this.apiKey = apiKey;
this.endpoint = endpoint;
this.timeoutSecs = timeoutSecs;
this.retries = retries;
this.debug = debug;
}
public String getApiKey() { return apiKey; }
public String getEndpoint() { return endpoint; }
public int getTimeoutSecs() { return timeoutSecs; }
public int getRetries() { return retries; }
public boolean isDebug() { return debug; }
}
// ==================== Chain & Asset Types ====================
class NativeCurrency {
private String name;
private String symbol;
private int decimals;
public String getName() { return name; }
public String getSymbol() { return symbol; }
public int getDecimals() { return decimals; }
}
class Chain {
private ChainId id;
private String name;
private long chainId;
private String rpcUrl;
private String explorerUrl;
private NativeCurrency nativeCurrency;
private int confirmations;
private int estimatedBlockTime;
private boolean supported;
public ChainId getId() { return id; }
public String getName() { return name; }
public long getChainId() { return chainId; }
public String getRpcUrl() { return rpcUrl; }
public String getExplorerUrl() { return explorerUrl; }
public NativeCurrency getNativeCurrency() { return nativeCurrency; }
public int getConfirmations() { return confirmations; }
public int getEstimatedBlockTime() { return estimatedBlockTime; }
public boolean isSupported() { return supported; }
}
class Asset {
private String id;
private String symbol;
private String name;
private AssetType type;
private ChainId chain;
private String contractAddress;
private int decimals;
private String logoUrl;
private boolean verified;
public String getId() { return id; }
public String getSymbol() { return symbol; }
public String getName() { return name; }
public AssetType getType() { return type; }
public ChainId getChain() { return chain; }
public String getContractAddress() { return contractAddress; }
public int getDecimals() { return decimals; }
public String getLogoUrl() { return logoUrl; }
public boolean isVerified() { return verified; }
}
class WrappedAsset {
private Asset originalAsset;
private Asset wrappedAsset;
private ChainId chain;
private String bridgeContract;
public Asset getOriginalAsset() { return originalAsset; }
public Asset getWrappedAsset() { return wrappedAsset; }
public ChainId getChain() { return chain; }
public String getBridgeContract() { return bridgeContract; }
}
// ==================== Transfer Types ====================
class ValidatorSignature {
private String validator;
private String signature;
private long timestamp;
public String getValidator() { return validator; }
public String getSignature() { return signature; }
public long getTimestamp() { return timestamp; }
}
class LockReceipt {
private String id;
private String txHash;
private ChainId sourceChain;
private ChainId targetChain;
private Asset asset;
private String amount;
private String sender;
private String recipient;
private long lockTimestamp;
private int confirmations;
private int requiredConfirmations;
public String getId() { return id; }
public String getTxHash() { return txHash; }
public ChainId getSourceChain() { return sourceChain; }
public ChainId getTargetChain() { return targetChain; }
public Asset getAsset() { return asset; }
public String getAmount() { return amount; }
public String getSender() { return sender; }
public String getRecipient() { return recipient; }
public long getLockTimestamp() { return lockTimestamp; }
public int getConfirmations() { return confirmations; }
public int getRequiredConfirmations() { return requiredConfirmations; }
}
class LockProof {
private LockReceipt lockReceipt;
private List<String> merkleProof;
private String blockHeader;
private List<ValidatorSignature> signatures;
public LockReceipt getLockReceipt() { return lockReceipt; }
public List<String> getMerkleProof() { return merkleProof; }
public String getBlockHeader() { return blockHeader; }
public List<ValidatorSignature> getSignatures() { return signatures; }
}
class BurnReceipt {
private String id;
private String txHash;
private ChainId sourceChain;
private ChainId targetChain;
private Asset wrappedAsset;
private Asset originalAsset;
private String amount;
private String sender;
private String recipient;
private long burnTimestamp;
private int confirmations;
private int requiredConfirmations;
public String getId() { return id; }
public String getTxHash() { return txHash; }
public ChainId getSourceChain() { return sourceChain; }
public ChainId getTargetChain() { return targetChain; }
public Asset getWrappedAsset() { return wrappedAsset; }
public Asset getOriginalAsset() { return originalAsset; }
public String getAmount() { return amount; }
public String getSender() { return sender; }
public String getRecipient() { return recipient; }
public long getBurnTimestamp() { return burnTimestamp; }
public int getConfirmations() { return confirmations; }
public int getRequiredConfirmations() { return requiredConfirmations; }
}
class BurnProof {
private BurnReceipt burnReceipt;
private List<String> merkleProof;
private String blockHeader;
private List<ValidatorSignature> signatures;
public BurnReceipt getBurnReceipt() { return burnReceipt; }
public List<String> getMerkleProof() { return merkleProof; }
public String getBlockHeader() { return blockHeader; }
public List<ValidatorSignature> getSignatures() { return signatures; }
}
class Transfer {
private String id;
private TransferDirection direction;
private TransferStatus status;
private ChainId sourceChain;
private ChainId targetChain;
private Asset asset;
private String amount;
private String sender;
private String recipient;
private String sourceTxHash;
private String targetTxHash;
private String fee;
private Asset feeAsset;
private long createdAt;
private long updatedAt;
private Long completedAt;
private String errorMessage;
public String getId() { return id; }
public TransferDirection getDirection() { return direction; }
public TransferStatus getStatus() { return status; }
public ChainId getSourceChain() { return sourceChain; }
public ChainId getTargetChain() { return targetChain; }
public Asset getAsset() { return asset; }
public String getAmount() { return amount; }
public String getSender() { return sender; }
public String getRecipient() { return recipient; }
public String getSourceTxHash() { return sourceTxHash; }
public String getTargetTxHash() { return targetTxHash; }
public String getFee() { return fee; }
public Asset getFeeAsset() { return feeAsset; }
public long getCreatedAt() { return createdAt; }
public long getUpdatedAt() { return updatedAt; }
public Long getCompletedAt() { return completedAt; }
public String getErrorMessage() { return errorMessage; }
}
// ==================== Fee Types ====================
class FeeEstimate {
private String bridgeFee;
private String gasFeeSource;
private String gasFeeTarget;
private String totalFee;
private Asset feeAsset;
private int estimatedTime;
private String exchangeRate;
public String getBridgeFee() { return bridgeFee; }
public String getGasFeeSource() { return gasFeeSource; }
public String getGasFeeTarget() { return gasFeeTarget; }
public String getTotalFee() { return totalFee; }
public Asset getFeeAsset() { return feeAsset; }
public int getEstimatedTime() { return estimatedTime; }
public String getExchangeRate() { return exchangeRate; }
}
class ExchangeRate {
private Asset fromAsset;
private Asset toAsset;
private String rate;
private String inverseRate;
private long lastUpdated;
private String source;
public Asset getFromAsset() { return fromAsset; }
public Asset getToAsset() { return toAsset; }
public String getRate() { return rate; }
public String getInverseRate() { return inverseRate; }
public long getLastUpdated() { return lastUpdated; }
public String getSource() { return source; }
}
class SignedTransaction {
private String txHash;
private ChainId chain;
private String from;
private String to;
private String value;
private String data;
private String gasLimit;
private String gasPrice;
private String maxFeePerGas;
private String maxPriorityFeePerGas;
private int nonce;
private String signature;
public String getTxHash() { return txHash; }
public ChainId getChain() { return chain; }
public String getFrom() { return from; }
public String getTo() { return to; }
public String getValue() { return value; }
public String getData() { return data; }
public String getGasLimit() { return gasLimit; }
public String getGasPrice() { return gasPrice; }
public String getMaxFeePerGas() { return maxFeePerGas; }
public String getMaxPriorityFeePerGas() { return maxPriorityFeePerGas; }
public int getNonce() { return nonce; }
public String getSignature() { return signature; }
}
// ==================== Options & Filter ====================
class LockOptions {
private String recipient;
private Long deadline;
private Double slippage;
public String getRecipient() { return recipient; }
public Long getDeadline() { return deadline; }
public Double getSlippage() { return slippage; }
public void setRecipient(String recipient) { this.recipient = recipient; }
public void setDeadline(Long deadline) { this.deadline = deadline; }
public void setSlippage(Double slippage) { this.slippage = slippage; }
}
class MintOptions {
private String gasLimit;
private String maxFeePerGas;
private String maxPriorityFeePerGas;
public String getGasLimit() { return gasLimit; }
public String getMaxFeePerGas() { return maxFeePerGas; }
public String getMaxPriorityFeePerGas() { return maxPriorityFeePerGas; }
public void setGasLimit(String gasLimit) { this.gasLimit = gasLimit; }
public void setMaxFeePerGas(String maxFeePerGas) { this.maxFeePerGas = maxFeePerGas; }
public void setMaxPriorityFeePerGas(String v) { this.maxPriorityFeePerGas = v; }
}
class BurnOptions {
private String recipient;
private Long deadline;
public String getRecipient() { return recipient; }
public Long getDeadline() { return deadline; }
public void setRecipient(String recipient) { this.recipient = recipient; }
public void setDeadline(Long deadline) { this.deadline = deadline; }
}
class UnlockOptions {
private String gasLimit;
private String gasPrice;
public String getGasLimit() { return gasLimit; }
public String getGasPrice() { return gasPrice; }
public void setGasLimit(String gasLimit) { this.gasLimit = gasLimit; }
public void setGasPrice(String gasPrice) { this.gasPrice = gasPrice; }
}
class TransferFilter {
private TransferStatus status;
private ChainId sourceChain;
private ChainId targetChain;
private String asset;
private String sender;
private String recipient;
private Integer limit;
private Integer offset;
public TransferStatus getStatus() { return status; }
public ChainId getSourceChain() { return sourceChain; }
public ChainId getTargetChain() { return targetChain; }
public String getAsset() { return asset; }
public String getSender() { return sender; }
public String getRecipient() { return recipient; }
public Integer getLimit() { return limit; }
public Integer getOffset() { return offset; }
public void setStatus(TransferStatus status) { this.status = status; }
public void setSourceChain(ChainId sourceChain) { this.sourceChain = sourceChain; }
public void setTargetChain(ChainId targetChain) { this.targetChain = targetChain; }
public void setAsset(String asset) { this.asset = asset; }
public void setSender(String sender) { this.sender = sender; }
public void setRecipient(String recipient) { this.recipient = recipient; }
public void setLimit(Integer limit) { this.limit = limit; }
public void setOffset(Integer offset) { this.offset = offset; }
}
// ==================== Error ====================
class BridgeException extends RuntimeException {
private final String code;
private final int statusCode;
public BridgeException(String message) {
super(message);
this.code = null;
this.statusCode = 0;
}
public BridgeException(String message, String code, int statusCode) {
super(message);
this.code = code;
this.statusCode = statusCode;
}
public String getCode() { return code; }
public int getStatusCode() { return statusCode; }
}

View file

@ -0,0 +1,309 @@
package io.synor.database;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Synor Database SDK for Java.
* Multi-model database with Key-Value, Document, Vector, and Time Series stores.
*/
public class SynorDatabase implements AutoCloseable {
private static final String DEFAULT_ENDPOINT = "https://db.synor.io/v1";
private static final Gson gson = new GsonBuilder().create();
private final DatabaseConfig config;
private final HttpClient httpClient;
private final AtomicBoolean closed = new AtomicBoolean(false);
// Sub-stores
public final KeyValueStore kv;
public final DocumentStore documents;
public final VectorStore vectors;
public final TimeSeriesStore timeseries;
public SynorDatabase(String apiKey) {
this(new DatabaseConfig(apiKey, DEFAULT_ENDPOINT, 60, 3, false));
}
public SynorDatabase(DatabaseConfig config) {
this.config = config;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(config.getTimeoutSecs()))
.build();
this.kv = new KeyValueStore(this);
this.documents = new DocumentStore(this);
this.vectors = new VectorStore(this);
this.timeseries = new TimeSeriesStore(this);
}
// ==================== Key-Value Store ====================
public static class KeyValueStore {
private final SynorDatabase db;
KeyValueStore(SynorDatabase db) {
this.db = db;
}
public CompletableFuture<Object> get(String key) {
return db.request("GET", "/kv/" + encode(key), null)
.thenApply(resp -> {
Map<String, Object> map = gson.fromJson(resp, new TypeToken<Map<String, Object>>(){}.getType());
return map.get("value");
});
}
public CompletableFuture<Void> set(String key, Object value, Integer ttl) {
Map<String, Object> body = new HashMap<>();
body.put("key", key);
body.put("value", value);
if (ttl != null) body.put("ttl", ttl);
return db.request("PUT", "/kv/" + encode(key), body)
.thenApply(resp -> null);
}
public CompletableFuture<Void> delete(String key) {
return db.request("DELETE", "/kv/" + encode(key), null)
.thenApply(resp -> null);
}
public CompletableFuture<List<KeyValue>> list(String prefix) {
String path = "/kv?prefix=" + encode(prefix);
return db.request("GET", path, null)
.thenApply(resp -> {
Type type = new TypeToken<ListResponse<KeyValue>>(){}.getType();
ListResponse<KeyValue> result = gson.fromJson(resp, type);
return result.items;
});
}
}
// ==================== Document Store ====================
public static class DocumentStore {
private final SynorDatabase db;
DocumentStore(SynorDatabase db) {
this.db = db;
}
public CompletableFuture<String> create(String collection, Map<String, Object> document) {
return db.request("POST", "/collections/" + encode(collection) + "/documents", document)
.thenApply(resp -> {
Map<String, Object> map = gson.fromJson(resp, new TypeToken<Map<String, Object>>(){}.getType());
return (String) map.get("id");
});
}
public CompletableFuture<Document> get(String collection, String id) {
return db.request("GET", "/collections/" + encode(collection) + "/documents/" + encode(id), null)
.thenApply(resp -> gson.fromJson(resp, Document.class));
}
public CompletableFuture<Void> update(String collection, String id, Map<String, Object> update) {
return db.request("PATCH", "/collections/" + encode(collection) + "/documents/" + encode(id), update)
.thenApply(resp -> null);
}
public CompletableFuture<Void> delete(String collection, String id) {
return db.request("DELETE", "/collections/" + encode(collection) + "/documents/" + encode(id), null)
.thenApply(resp -> null);
}
public CompletableFuture<List<Document>> query(String collection, Query query) {
return db.request("POST", "/collections/" + encode(collection) + "/query", query)
.thenApply(resp -> {
Type type = new TypeToken<ListResponse<Document>>(){}.getType();
ListResponse<Document> result = gson.fromJson(resp, type);
return result.documents;
});
}
}
// ==================== Vector Store ====================
public static class VectorStore {
private final SynorDatabase db;
VectorStore(SynorDatabase db) {
this.db = db;
}
public CompletableFuture<Void> upsert(String collection, List<VectorEntry> vectors) {
Map<String, Object> body = new HashMap<>();
body.put("vectors", vectors);
return db.request("POST", "/vectors/" + encode(collection) + "/upsert", body)
.thenApply(resp -> null);
}
public CompletableFuture<List<SearchResult>> search(String collection, double[] vector, int k) {
Map<String, Object> body = new HashMap<>();
body.put("vector", vector);
body.put("k", k);
return db.request("POST", "/vectors/" + encode(collection) + "/search", body)
.thenApply(resp -> {
Type type = new TypeToken<ListResponse<SearchResult>>(){}.getType();
ListResponse<SearchResult> result = gson.fromJson(resp, type);
return result.results;
});
}
public CompletableFuture<Void> delete(String collection, List<String> ids) {
Map<String, Object> body = new HashMap<>();
body.put("ids", ids);
return db.request("DELETE", "/vectors/" + encode(collection), body)
.thenApply(resp -> null);
}
}
// ==================== Time Series Store ====================
public static class TimeSeriesStore {
private final SynorDatabase db;
TimeSeriesStore(SynorDatabase db) {
this.db = db;
}
public CompletableFuture<Void> write(String series, List<DataPoint> points) {
Map<String, Object> body = new HashMap<>();
body.put("points", points);
return db.request("POST", "/timeseries/" + encode(series) + "/write", body)
.thenApply(resp -> null);
}
public CompletableFuture<List<DataPoint>> query(String series, TimeRange range, Aggregation aggregation) {
Map<String, Object> body = new HashMap<>();
body.put("range", range);
if (aggregation != null) body.put("aggregation", aggregation);
return db.request("POST", "/timeseries/" + encode(series) + "/query", body)
.thenApply(resp -> {
Type type = new TypeToken<ListResponse<DataPoint>>(){}.getType();
ListResponse<DataPoint> result = gson.fromJson(resp, type);
return result.points;
});
}
}
// ==================== Lifecycle ====================
@Override
public void close() {
closed.set(true);
}
public boolean isClosed() {
return closed.get();
}
public CompletableFuture<Boolean> healthCheck() {
return request("GET", "/health", null)
.thenApply(resp -> {
Map<String, Object> map = gson.fromJson(resp, new TypeToken<Map<String, Object>>(){}.getType());
return "healthy".equals(map.get("status"));
})
.exceptionally(e -> false);
}
// ==================== Private Methods ====================
private CompletableFuture<String> request(String method, String path, Object body) {
if (closed.get()) {
return CompletableFuture.failedFuture(new DatabaseException("Client has been closed"));
}
return doRequest(method, path, body, 0);
}
private CompletableFuture<String> doRequest(String method, String path, Object body, int attempt) {
String url = config.getEndpoint() + path;
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + config.getApiKey())
.header("Content-Type", "application/json")
.header("X-SDK-Version", "java/0.1.0")
.timeout(Duration.ofSeconds(config.getTimeoutSecs()));
HttpRequest.BodyPublisher bodyPublisher = body != null
? HttpRequest.BodyPublishers.ofString(gson.toJson(body))
: HttpRequest.BodyPublishers.noBody();
switch (method) {
case "GET":
requestBuilder.GET();
break;
case "POST":
requestBuilder.POST(bodyPublisher);
break;
case "PUT":
requestBuilder.PUT(bodyPublisher);
break;
case "PATCH":
requestBuilder.method("PATCH", bodyPublisher);
break;
case "DELETE":
requestBuilder.method("DELETE", bodyPublisher);
break;
}
return httpClient.sendAsync(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
.thenCompose(response -> {
if (response.statusCode() >= 400) {
Map<String, Object> errorBody = gson.fromJson(response.body(),
new TypeToken<Map<String, Object>>(){}.getType());
String message = errorBody != null && errorBody.get("message") != null
? (String) errorBody.get("message")
: "HTTP " + response.statusCode();
return CompletableFuture.failedFuture(
new DatabaseException(message, (String) errorBody.get("code"), response.statusCode()));
}
return CompletableFuture.completedFuture(response.body());
})
.exceptionally(e -> {
if (attempt < config.getRetries() - 1) {
try {
Thread.sleep((long) Math.pow(2, attempt) * 1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
return doRequest(method, path, body, attempt + 1).join();
}
throw new RuntimeException(e);
});
}
private static String encode(String value) {
return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8);
}
// ==================== Helper Types ====================
private static class ListResponse<T> {
List<T> items;
List<T> documents;
List<T> results;
List<T> points;
}
}

View file

@ -0,0 +1,210 @@
package io.synor.database;
import java.util.Map;
/**
* Database SDK Types for Java.
*/
public class Types {
// This class exists for organizational purposes.
// Individual types are defined below.
}
// ==================== Config ====================
class DatabaseConfig {
private final String apiKey;
private final String endpoint;
private final int timeoutSecs;
private final int retries;
private final boolean debug;
public DatabaseConfig(String apiKey) {
this(apiKey, "https://db.synor.io/v1", 60, 3, false);
}
public DatabaseConfig(String apiKey, String endpoint, int timeoutSecs, int retries, boolean debug) {
this.apiKey = apiKey;
this.endpoint = endpoint;
this.timeoutSecs = timeoutSecs;
this.retries = retries;
this.debug = debug;
}
public String getApiKey() { return apiKey; }
public String getEndpoint() { return endpoint; }
public int getTimeoutSecs() { return timeoutSecs; }
public int getRetries() { return retries; }
public boolean isDebug() { return debug; }
}
// ==================== Key-Value Types ====================
class KeyValue {
private String key;
private Object value;
private Long ttl;
private Long createdAt;
private Long updatedAt;
public String getKey() { return key; }
public Object getValue() { return value; }
public Long getTtl() { return ttl; }
public Long getCreatedAt() { return createdAt; }
public Long getUpdatedAt() { return updatedAt; }
}
// ==================== Document Types ====================
class Document {
private String id;
private String collection;
private Map<String, Object> data;
private Long createdAt;
private Long updatedAt;
public String getId() { return id; }
public String getCollection() { return collection; }
public Map<String, Object> getData() { return data; }
public Long getCreatedAt() { return createdAt; }
public Long getUpdatedAt() { return updatedAt; }
}
class Query {
private Map<String, Object> filter;
private Map<String, Integer> sort;
private Integer limit;
private Integer offset;
private String[] projection;
public Query filter(Map<String, Object> filter) {
this.filter = filter;
return this;
}
public Query sort(Map<String, Integer> sort) {
this.sort = sort;
return this;
}
public Query limit(int limit) {
this.limit = limit;
return this;
}
public Query offset(int offset) {
this.offset = offset;
return this;
}
public Query projection(String... fields) {
this.projection = fields;
return this;
}
}
// ==================== Vector Types ====================
class VectorEntry {
private String id;
private double[] vector;
private Map<String, Object> metadata;
public VectorEntry(String id, double[] vector) {
this.id = id;
this.vector = vector;
}
public VectorEntry(String id, double[] vector, Map<String, Object> metadata) {
this.id = id;
this.vector = vector;
this.metadata = metadata;
}
public String getId() { return id; }
public double[] getVector() { return vector; }
public Map<String, Object> getMetadata() { return metadata; }
}
class SearchResult {
private String id;
private double score;
private double[] vector;
private Map<String, Object> metadata;
public String getId() { return id; }
public double getScore() { return score; }
public double[] getVector() { return vector; }
public Map<String, Object> getMetadata() { return metadata; }
}
// ==================== Time Series Types ====================
class DataPoint {
private long timestamp;
private double value;
private Map<String, String> tags;
public DataPoint(long timestamp, double value) {
this.timestamp = timestamp;
this.value = value;
}
public DataPoint(long timestamp, double value, Map<String, String> tags) {
this.timestamp = timestamp;
this.value = value;
this.tags = tags;
}
public long getTimestamp() { return timestamp; }
public double getValue() { return value; }
public Map<String, String> getTags() { return tags; }
}
class TimeRange {
private long start;
private long end;
public TimeRange(long start, long end) {
this.start = start;
this.end = end;
}
public long getStart() { return start; }
public long getEnd() { return end; }
}
class Aggregation {
private String function; // sum, avg, min, max, count
private String interval; // 1m, 5m, 1h, 1d
public Aggregation(String function, String interval) {
this.function = function;
this.interval = interval;
}
public String getFunction() { return function; }
public String getInterval() { return interval; }
}
// ==================== Error Types ====================
class DatabaseException extends RuntimeException {
private final String code;
private final int statusCode;
public DatabaseException(String message) {
super(message);
this.code = null;
this.statusCode = 0;
}
public DatabaseException(String message, String code, int statusCode) {
super(message);
this.code = code;
this.statusCode = statusCode;
}
public String getCode() { return code; }
public int getStatusCode() { return statusCode; }
}

View file

@ -0,0 +1,304 @@
package io.synor.hosting;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Synor Hosting SDK for Java.
* Decentralized web hosting with domain management, DNS, and deployments.
*/
public class SynorHosting implements AutoCloseable {
private static final String DEFAULT_ENDPOINT = "https://hosting.synor.io/v1";
private static final Gson gson = new GsonBuilder().create();
private final HostingConfig config;
private final HttpClient httpClient;
private final AtomicBoolean closed = new AtomicBoolean(false);
public SynorHosting(String apiKey) {
this(new HostingConfig(apiKey, DEFAULT_ENDPOINT, 60, 3, false));
}
public SynorHosting(HostingConfig config) {
this.config = config;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(config.getTimeoutSecs()))
.build();
}
// ==================== Domain Operations ====================
public CompletableFuture<DomainAvailability> checkAvailability(String name) {
return request("GET", "/domains/check/" + encode(name), null)
.thenApply(resp -> gson.fromJson(resp, DomainAvailability.class));
}
public CompletableFuture<Domain> registerDomain(String name, RegisterDomainOptions options) {
Map<String, Object> body = new HashMap<>();
body.put("name", name);
if (options != null) {
if (options.getYears() != null) body.put("years", options.getYears());
if (options.getAutoRenew() != null) body.put("autoRenew", options.getAutoRenew());
}
return request("POST", "/domains", body)
.thenApply(resp -> gson.fromJson(resp, Domain.class));
}
public CompletableFuture<Domain> getDomain(String name) {
return request("GET", "/domains/" + encode(name), null)
.thenApply(resp -> gson.fromJson(resp, Domain.class));
}
public CompletableFuture<List<Domain>> listDomains() {
return request("GET", "/domains", null)
.thenApply(resp -> {
Type type = new TypeToken<DomainsResponse>(){}.getType();
DomainsResponse result = gson.fromJson(resp, type);
return result.domains;
});
}
public CompletableFuture<Domain> updateDomainRecord(String name, DomainRecord record) {
return request("PUT", "/domains/" + encode(name) + "/record", record)
.thenApply(resp -> gson.fromJson(resp, Domain.class));
}
public CompletableFuture<DomainRecord> resolveDomain(String name) {
return request("GET", "/domains/" + encode(name) + "/resolve", null)
.thenApply(resp -> gson.fromJson(resp, DomainRecord.class));
}
public CompletableFuture<Domain> renewDomain(String name, int years) {
Map<String, Object> body = new HashMap<>();
body.put("years", years);
return request("POST", "/domains/" + encode(name) + "/renew", body)
.thenApply(resp -> gson.fromJson(resp, Domain.class));
}
// ==================== DNS Operations ====================
public CompletableFuture<DnsZone> getDnsZone(String domain) {
return request("GET", "/dns/" + encode(domain), null)
.thenApply(resp -> gson.fromJson(resp, DnsZone.class));
}
public CompletableFuture<DnsZone> setDnsRecords(String domain, List<DnsRecord> records) {
Map<String, Object> body = new HashMap<>();
body.put("records", records);
return request("PUT", "/dns/" + encode(domain), body)
.thenApply(resp -> gson.fromJson(resp, DnsZone.class));
}
public CompletableFuture<DnsZone> addDnsRecord(String domain, DnsRecord record) {
return request("POST", "/dns/" + encode(domain) + "/records", record)
.thenApply(resp -> gson.fromJson(resp, DnsZone.class));
}
public CompletableFuture<DnsZone> deleteDnsRecord(String domain, String recordType, String name) {
String path = "/dns/" + encode(domain) + "/records/" + recordType + "/" + encode(name);
return request("DELETE", path, null)
.thenApply(resp -> gson.fromJson(resp, DnsZone.class));
}
// ==================== Deployment Operations ====================
public CompletableFuture<Deployment> deploy(String cid, DeployOptions options) {
Map<String, Object> body = new HashMap<>();
body.put("cid", cid);
if (options != null) {
if (options.getDomain() != null) body.put("domain", options.getDomain());
if (options.getSubdomain() != null) body.put("subdomain", options.getSubdomain());
if (options.getSpa() != null) body.put("spa", options.getSpa());
if (options.getCleanUrls() != null) body.put("cleanUrls", options.getCleanUrls());
}
return request("POST", "/deployments", body)
.thenApply(resp -> gson.fromJson(resp, Deployment.class));
}
public CompletableFuture<Deployment> getDeployment(String id) {
return request("GET", "/deployments/" + encode(id), null)
.thenApply(resp -> gson.fromJson(resp, Deployment.class));
}
public CompletableFuture<List<Deployment>> listDeployments(String domain) {
String path = domain != null ? "/deployments?domain=" + encode(domain) : "/deployments";
return request("GET", path, null)
.thenApply(resp -> {
Type type = new TypeToken<DeploymentsResponse>(){}.getType();
DeploymentsResponse result = gson.fromJson(resp, type);
return result.deployments;
});
}
public CompletableFuture<Deployment> rollback(String domain, String deploymentId) {
Map<String, Object> body = new HashMap<>();
body.put("domain", domain);
return request("POST", "/deployments/" + encode(deploymentId) + "/rollback", body)
.thenApply(resp -> gson.fromJson(resp, Deployment.class));
}
public CompletableFuture<Void> deleteDeployment(String id) {
return request("DELETE", "/deployments/" + encode(id), null)
.thenApply(resp -> null);
}
// ==================== SSL Operations ====================
public CompletableFuture<Certificate> provisionSsl(String domain, ProvisionSslOptions options) {
return request("POST", "/ssl/" + encode(domain), options)
.thenApply(resp -> gson.fromJson(resp, Certificate.class));
}
public CompletableFuture<Certificate> getCertificate(String domain) {
return request("GET", "/ssl/" + encode(domain), null)
.thenApply(resp -> gson.fromJson(resp, Certificate.class));
}
public CompletableFuture<Certificate> renewCertificate(String domain) {
return request("POST", "/ssl/" + encode(domain) + "/renew", null)
.thenApply(resp -> gson.fromJson(resp, Certificate.class));
}
public CompletableFuture<Void> deleteCertificate(String domain) {
return request("DELETE", "/ssl/" + encode(domain), null)
.thenApply(resp -> null);
}
// ==================== Site Configuration ====================
public CompletableFuture<SiteConfig> getSiteConfig(String domain) {
return request("GET", "/sites/" + encode(domain) + "/config", null)
.thenApply(resp -> gson.fromJson(resp, SiteConfig.class));
}
public CompletableFuture<SiteConfig> updateSiteConfig(String domain, Map<String, Object> config) {
return request("PATCH", "/sites/" + encode(domain) + "/config", config)
.thenApply(resp -> gson.fromJson(resp, SiteConfig.class));
}
public CompletableFuture<Long> purgeCache(String domain, List<String> paths) {
Map<String, Object> body = new HashMap<>();
if (paths != null) body.put("paths", paths);
return request("DELETE", "/sites/" + encode(domain) + "/cache", body)
.thenApply(resp -> {
Map<String, Object> map = gson.fromJson(resp, new TypeToken<Map<String, Object>>(){}.getType());
return ((Number) map.get("purged")).longValue();
});
}
// ==================== Analytics ====================
public CompletableFuture<AnalyticsData> getAnalytics(String domain, AnalyticsOptions options) {
StringBuilder path = new StringBuilder("/sites/" + encode(domain) + "/analytics");
if (options != null) {
StringBuilder params = new StringBuilder();
if (options.getPeriod() != null) params.append("period=").append(encode(options.getPeriod()));
if (options.getStart() != null) {
if (params.length() > 0) params.append("&");
params.append("start=").append(encode(options.getStart()));
}
if (options.getEnd() != null) {
if (params.length() > 0) params.append("&");
params.append("end=").append(encode(options.getEnd()));
}
if (params.length() > 0) path.append("?").append(params);
}
return request("GET", path.toString(), null)
.thenApply(resp -> gson.fromJson(resp, AnalyticsData.class));
}
// ==================== Lifecycle ====================
@Override
public void close() {
closed.set(true);
}
public boolean isClosed() {
return closed.get();
}
public CompletableFuture<Boolean> healthCheck() {
return request("GET", "/health", null)
.thenApply(resp -> {
Map<String, Object> map = gson.fromJson(resp, new TypeToken<Map<String, Object>>(){}.getType());
return "healthy".equals(map.get("status"));
})
.exceptionally(e -> false);
}
// ==================== Private Methods ====================
private CompletableFuture<String> request(String method, String path, Object body) {
if (closed.get()) {
return CompletableFuture.failedFuture(new HostingException("Client has been closed"));
}
return doRequest(method, path, body, 0);
}
private CompletableFuture<String> doRequest(String method, String path, Object body, int attempt) {
String url = config.getEndpoint() + path;
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + config.getApiKey())
.header("Content-Type", "application/json")
.header("X-SDK-Version", "java/0.1.0")
.timeout(Duration.ofSeconds(config.getTimeoutSecs()));
HttpRequest.BodyPublisher bodyPublisher = body != null
? HttpRequest.BodyPublishers.ofString(gson.toJson(body))
: HttpRequest.BodyPublishers.noBody();
switch (method) {
case "GET": requestBuilder.GET(); break;
case "POST": requestBuilder.POST(bodyPublisher); break;
case "PUT": requestBuilder.PUT(bodyPublisher); break;
case "PATCH": requestBuilder.method("PATCH", bodyPublisher); break;
case "DELETE": requestBuilder.method("DELETE", bodyPublisher); break;
}
return httpClient.sendAsync(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
.thenCompose(response -> {
if (response.statusCode() >= 400) {
Map<String, Object> errorBody = gson.fromJson(response.body(),
new TypeToken<Map<String, Object>>(){}.getType());
String message = errorBody != null && errorBody.get("message") != null
? (String) errorBody.get("message") : "HTTP " + response.statusCode();
return CompletableFuture.failedFuture(
new HostingException(message, (String) errorBody.get("code"), response.statusCode()));
}
return CompletableFuture.completedFuture(response.body());
})
.exceptionally(e -> {
if (attempt < config.getRetries() - 1) {
try { Thread.sleep((long) Math.pow(2, attempt) * 1000); }
catch (InterruptedException ie) { Thread.currentThread().interrupt(); }
return doRequest(method, path, body, attempt + 1).join();
}
throw new RuntimeException(e);
});
}
private static String encode(String value) {
return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8);
}
// Helper response types
private static class DomainsResponse { List<Domain> domains; }
private static class DeploymentsResponse { List<Deployment> deployments; }
}

View file

@ -0,0 +1,330 @@
package io.synor.hosting;
import java.util.List;
import java.util.Map;
/**
* Hosting SDK Types for Java.
*/
public class Types {
// Organizational class
}
// ==================== Config ====================
class HostingConfig {
private final String apiKey;
private final String endpoint;
private final int timeoutSecs;
private final int retries;
private final boolean debug;
public HostingConfig(String apiKey) {
this(apiKey, "https://hosting.synor.io/v1", 60, 3, false);
}
public HostingConfig(String apiKey, String endpoint, int timeoutSecs, int retries, boolean debug) {
this.apiKey = apiKey;
this.endpoint = endpoint;
this.timeoutSecs = timeoutSecs;
this.retries = retries;
this.debug = debug;
}
public String getApiKey() { return apiKey; }
public String getEndpoint() { return endpoint; }
public int getTimeoutSecs() { return timeoutSecs; }
public int getRetries() { return retries; }
public boolean isDebug() { return debug; }
}
// ==================== Domain Types ====================
enum DomainStatus { pending, active, expired, suspended }
class Domain {
private String name;
private DomainStatus status;
private String owner;
private long registeredAt;
private long expiresAt;
private boolean autoRenew;
private DomainRecord records;
public String getName() { return name; }
public DomainStatus getStatus() { return status; }
public String getOwner() { return owner; }
public long getRegisteredAt() { return registeredAt; }
public long getExpiresAt() { return expiresAt; }
public boolean isAutoRenew() { return autoRenew; }
public DomainRecord getRecords() { return records; }
}
class DomainRecord {
private String cid;
private List<String> ipv4;
private List<String> ipv6;
private String cname;
private List<String> txt;
private Map<String, String> metadata;
public String getCid() { return cid; }
public List<String> getIpv4() { return ipv4; }
public List<String> getIpv6() { return ipv6; }
public String getCname() { return cname; }
public List<String> getTxt() { return txt; }
public Map<String, String> getMetadata() { return metadata; }
public void setCid(String cid) { this.cid = cid; }
public void setIpv4(List<String> ipv4) { this.ipv4 = ipv4; }
public void setIpv6(List<String> ipv6) { this.ipv6 = ipv6; }
public void setCname(String cname) { this.cname = cname; }
public void setTxt(List<String> txt) { this.txt = txt; }
public void setMetadata(Map<String, String> metadata) { this.metadata = metadata; }
}
class DomainAvailability {
private String name;
private boolean available;
private Double price;
private boolean premium;
public String getName() { return name; }
public boolean isAvailable() { return available; }
public Double getPrice() { return price; }
public boolean isPremium() { return premium; }
}
class RegisterDomainOptions {
private Integer years;
private Boolean autoRenew;
public Integer getYears() { return years; }
public Boolean getAutoRenew() { return autoRenew; }
public void setYears(Integer years) { this.years = years; }
public void setAutoRenew(Boolean autoRenew) { this.autoRenew = autoRenew; }
}
// ==================== DNS Types ====================
enum DnsRecordType { A, AAAA, CNAME, TXT, MX, NS, SRV, CAA }
class DnsRecord {
private DnsRecordType type;
private String name;
private String value;
private int ttl = 3600;
private Integer priority;
public DnsRecordType getType() { return type; }
public String getName() { return name; }
public String getValue() { return value; }
public int getTtl() { return ttl; }
public Integer getPriority() { return priority; }
public void setType(DnsRecordType type) { this.type = type; }
public void setName(String name) { this.name = name; }
public void setValue(String value) { this.value = value; }
public void setTtl(int ttl) { this.ttl = ttl; }
public void setPriority(Integer priority) { this.priority = priority; }
}
class DnsZone {
private String domain;
private List<DnsRecord> records;
private long updatedAt;
public String getDomain() { return domain; }
public List<DnsRecord> getRecords() { return records; }
public long getUpdatedAt() { return updatedAt; }
}
// ==================== Deployment Types ====================
enum DeploymentStatus { pending, building, deploying, active, failed, inactive }
class Deployment {
private String id;
private String domain;
private String cid;
private DeploymentStatus status;
private String url;
private long createdAt;
private long updatedAt;
private String buildLogs;
private String errorMessage;
public String getId() { return id; }
public String getDomain() { return domain; }
public String getCid() { return cid; }
public DeploymentStatus getStatus() { return status; }
public String getUrl() { return url; }
public long getCreatedAt() { return createdAt; }
public long getUpdatedAt() { return updatedAt; }
public String getBuildLogs() { return buildLogs; }
public String getErrorMessage() { return errorMessage; }
}
class DeployOptions {
private String domain;
private String subdomain;
private Boolean spa;
private Boolean cleanUrls;
private Boolean trailingSlash;
public String getDomain() { return domain; }
public String getSubdomain() { return subdomain; }
public Boolean getSpa() { return spa; }
public Boolean getCleanUrls() { return cleanUrls; }
public Boolean getTrailingSlash() { return trailingSlash; }
public void setDomain(String domain) { this.domain = domain; }
public void setSubdomain(String subdomain) { this.subdomain = subdomain; }
public void setSpa(Boolean spa) { this.spa = spa; }
public void setCleanUrls(Boolean cleanUrls) { this.cleanUrls = cleanUrls; }
public void setTrailingSlash(Boolean trailingSlash) { this.trailingSlash = trailingSlash; }
}
// ==================== SSL Types ====================
enum CertificateStatus { pending, issued, expired, revoked }
class Certificate {
private String domain;
private CertificateStatus status;
private boolean autoRenew;
private String issuer;
private Long issuedAt;
private Long expiresAt;
private String fingerprint;
public String getDomain() { return domain; }
public CertificateStatus getStatus() { return status; }
public boolean isAutoRenew() { return autoRenew; }
public String getIssuer() { return issuer; }
public Long getIssuedAt() { return issuedAt; }
public Long getExpiresAt() { return expiresAt; }
public String getFingerprint() { return fingerprint; }
}
class ProvisionSslOptions {
private Boolean includeWww;
private Boolean autoRenew;
public Boolean getIncludeWww() { return includeWww; }
public Boolean getAutoRenew() { return autoRenew; }
public void setIncludeWww(Boolean includeWww) { this.includeWww = includeWww; }
public void setAutoRenew(Boolean autoRenew) { this.autoRenew = autoRenew; }
}
// ==================== Site Config ====================
class SiteConfig {
private String domain;
private String cid;
private Map<String, String> headers;
private List<RedirectRule> redirects;
private Map<String, String> errorPages;
private boolean spa;
private boolean cleanUrls;
private boolean trailingSlash;
public String getDomain() { return domain; }
public String getCid() { return cid; }
public Map<String, String> getHeaders() { return headers; }
public List<RedirectRule> getRedirects() { return redirects; }
public Map<String, String> getErrorPages() { return errorPages; }
public boolean isSpa() { return spa; }
public boolean isCleanUrls() { return cleanUrls; }
public boolean isTrailingSlash() { return trailingSlash; }
}
class RedirectRule {
private String source;
private String destination;
private int statusCode = 301;
private boolean permanent = true;
public String getSource() { return source; }
public String getDestination() { return destination; }
public int getStatusCode() { return statusCode; }
public boolean isPermanent() { return permanent; }
}
// ==================== Analytics ====================
class AnalyticsData {
private String domain;
private String period;
private long pageViews;
private long uniqueVisitors;
private long bandwidth;
private List<PageView> topPages;
private List<Referrer> topReferrers;
private List<Country> topCountries;
public String getDomain() { return domain; }
public String getPeriod() { return period; }
public long getPageViews() { return pageViews; }
public long getUniqueVisitors() { return uniqueVisitors; }
public long getBandwidth() { return bandwidth; }
public List<PageView> getTopPages() { return topPages; }
public List<Referrer> getTopReferrers() { return topReferrers; }
public List<Country> getTopCountries() { return topCountries; }
}
class PageView {
private String path;
private long views;
public String getPath() { return path; }
public long getViews() { return views; }
}
class Referrer {
private String referrer;
private long count;
public String getReferrer() { return referrer; }
public long getCount() { return count; }
}
class Country {
private String country;
private long count;
public String getCountry() { return country; }
public long getCount() { return count; }
}
class AnalyticsOptions {
private String period;
private String start;
private String end;
public String getPeriod() { return period; }
public String getStart() { return start; }
public String getEnd() { return end; }
public void setPeriod(String period) { this.period = period; }
public void setStart(String start) { this.start = start; }
public void setEnd(String end) { this.end = end; }
}
// ==================== Error ====================
class HostingException extends RuntimeException {
private final String code;
private final int statusCode;
public HostingException(String message) {
super(message);
this.code = null;
this.statusCode = 0;
}
public HostingException(String message, String code, int statusCode) {
super(message);
this.code = code;
this.statusCode = statusCode;
}
public String getCode() { return code; }
public int getStatusCode() { return statusCode; }
}

View file

@ -0,0 +1,226 @@
package io.synor.rpc;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.WebSocket;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Synor RPC SDK for Java.
*
* Query blocks, transactions, and chain state with WebSocket subscription support.
*
* Example:
* <pre>{@code
* SynorRpc rpc = new SynorRpc("your-api-key");
* Block block = rpc.getLatestBlock().join();
* System.out.println("Height: " + block.getHeight());
* }</pre>
*/
public class SynorRpc implements AutoCloseable {
private static final String DEFAULT_ENDPOINT = "https://rpc.synor.cc/api/v1";
private static final String DEFAULT_WS_ENDPOINT = "wss://rpc.synor.cc/ws";
private final RpcConfig config;
private final HttpClient httpClient;
private final Gson gson;
private WebSocket webSocket;
private final Map<String, Consumer<String>> subscriptions = new ConcurrentHashMap<>();
public SynorRpc(String apiKey) {
this(RpcConfig.builder().apiKey(apiKey).build());
}
public SynorRpc(RpcConfig config) {
this.config = config;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(config.getTimeoutSeconds()))
.build();
this.gson = new GsonBuilder()
.setFieldNamingPolicy(com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();
}
/** Get a block by hash. */
public CompletableFuture<Block> getBlockByHash(String hash) {
return get("/blocks/" + hash, Block.class);
}
/** Get a block by height. */
public CompletableFuture<Block> getBlockByHeight(long height) {
return get("/blocks/height/" + height, Block.class);
}
/** Get the latest block. */
public CompletableFuture<Block> getLatestBlock() {
return get("/blocks/latest", Block.class);
}
/** Get a block header by hash. */
public CompletableFuture<BlockHeader> getBlockHeaderByHash(String hash) {
return get("/blocks/" + hash + "/header", BlockHeaderResponse.class)
.thenApply(BlockHeaderResponse::getHeader);
}
/** Get a transaction by ID. */
public CompletableFuture<Transaction> getTransaction(String txid) {
return get("/transactions/" + txid, Transaction.class);
}
/** Send a raw transaction. */
public CompletableFuture<String> sendRawTransaction(String rawTx) {
var body = Map.of("raw", rawTx);
return post("/transactions", body, SendTxResponse.class)
.thenApply(SendTxResponse::getTxid);
}
/** Estimate transaction fee. */
public CompletableFuture<FeeEstimate> estimateFee(Priority priority) {
return get("/fees/estimate?priority=" + priority.getValue(), FeeEstimate.class);
}
/** Get chain information. */
public CompletableFuture<ChainInfo> getChainInfo() {
return get("/chain", ChainInfo.class);
}
/** Get mempool information. */
public CompletableFuture<MempoolInfo> getMempoolInfo() {
return get("/mempool", MempoolInfo.class);
}
/** Get UTXOs for an address. */
public CompletableFuture<UTXO[]> getUTXOs(String address) {
return get("/addresses/" + address + "/utxos", UTXOsResponse.class)
.thenApply(UTXOsResponse::getUtxos);
}
/** Get balance for an address. */
public CompletableFuture<Balance> getBalance(String address) {
return get("/addresses/" + address + "/balance", Balance.class);
}
/** Subscribe to new blocks. */
public CompletableFuture<Subscription> subscribeBlocks(Consumer<Block> callback) {
return subscribe("blocks", null, data -> {
Block block = gson.fromJson(data, BlockNotification.class).getBlock();
callback.accept(block);
});
}
/** Subscribe to address transactions. */
public CompletableFuture<Subscription> subscribeAddress(String address, Consumer<Transaction> callback) {
return subscribe("address", Map.of("address", address), data -> {
Transaction tx = gson.fromJson(data, TransactionNotification.class).getTransaction();
callback.accept(tx);
});
}
private CompletableFuture<Subscription> subscribe(String type, Map<String, String> params, Consumer<String> handler) {
return ensureWebSocket().thenApply(ws -> {
String subId = type + "_" + System.currentTimeMillis();
subscriptions.put(subId, handler);
var msg = new java.util.HashMap<String, Object>();
msg.put("type", "subscribe");
msg.put("id", subId);
msg.put("subscription", type);
if (params != null) msg.putAll(params);
ws.sendText(gson.toJson(msg), true);
return new Subscription(subId, type, System.currentTimeMillis(), () -> {
subscriptions.remove(subId);
ws.sendText(gson.toJson(Map.of("type", "unsubscribe", "id", subId)), true);
});
});
}
private CompletableFuture<WebSocket> ensureWebSocket() {
if (webSocket != null) {
return CompletableFuture.completedFuture(webSocket);
}
String wsUrl = config.getWsEndpoint() + "?apiKey=" + config.getApiKey() + "&network=" + config.getNetwork().getValue();
return httpClient.newWebSocketBuilder()
.buildAsync(URI.create(wsUrl), new WebSocket.Listener() {
@Override
public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) {
var msg = gson.fromJson(data.toString(), WsMessage.class);
if (msg.subscriptionId != null && subscriptions.containsKey(msg.subscriptionId)) {
subscriptions.get(msg.subscriptionId).accept(data.toString());
}
return WebSocket.Listener.super.onText(ws, data, last);
}
})
.thenApply(ws -> {
this.webSocket = ws;
return ws;
});
}
private <T> CompletableFuture<T> get(String path, Class<T> responseType) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(config.getEndpoint() + path))
.header("Authorization", "Bearer " + config.getApiKey())
.header("Content-Type", "application/json")
.header("X-Network", config.getNetwork().getValue())
.timeout(Duration.ofSeconds(config.getTimeoutSeconds()))
.GET()
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> handleResponse(response, responseType));
}
private <T> CompletableFuture<T> post(String path, Object body, Class<T> responseType) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(config.getEndpoint() + path))
.header("Authorization", "Bearer " + config.getApiKey())
.header("Content-Type", "application/json")
.header("X-Network", config.getNetwork().getValue())
.timeout(Duration.ofSeconds(config.getTimeoutSeconds()))
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(body)))
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> handleResponse(response, responseType));
}
private <T> T handleResponse(HttpResponse<String> response, Class<T> responseType) {
if (response.statusCode() >= 400) {
ErrorResponse error = gson.fromJson(response.body(), ErrorResponse.class);
throw new RpcException(
error != null ? error.getMessage() : "Unknown error",
response.statusCode(),
error != null ? error.getCode() : null
);
}
return gson.fromJson(response.body(), responseType);
}
@Override
public void close() {
if (webSocket != null) {
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "closing");
}
}
// Internal types
private static class WsMessage { String subscriptionId; String data; }
private record BlockHeaderResponse(BlockHeader header) { BlockHeader getHeader() { return header; } }
private record SendTxResponse(String txid) { String getTxid() { return txid; } }
private record UTXOsResponse(UTXO[] utxos) { UTXO[] getUtxos() { return utxos; } }
private record BlockNotification(Block block) { Block getBlock() { return block; } }
private record TransactionNotification(Transaction transaction) { Transaction getTransaction() { return transaction; } }
private record ErrorResponse(String message, String code) { String getMessage() { return message; } String getCode() { return code; } }
}

View file

@ -0,0 +1,240 @@
package io.synor.rpc;
import java.util.List;
import java.util.concurrent.CompletionStage;
/** Network type. */
public enum Network {
MAINNET("mainnet"), TESTNET("testnet");
private final String value;
Network(String value) { this.value = value; }
public String getValue() { return value; }
}
/** Transaction priority. */
public enum Priority {
LOW("low"), MEDIUM("medium"), HIGH("high"), URGENT("urgent");
private final String value;
Priority(String value) { this.value = value; }
public String getValue() { return value; }
}
/** Transaction status. */
public enum TransactionStatus {
PENDING("pending"), CONFIRMED("confirmed"), FAILED("failed"), REPLACED("replaced");
private final String value;
TransactionStatus(String value) { this.value = value; }
public String getValue() { return value; }
}
/** RPC configuration. */
class RpcConfig {
private final String apiKey;
private final String endpoint;
private final String wsEndpoint;
private final Network network;
private final long timeoutSeconds;
private final boolean debug;
private RpcConfig(Builder builder) {
this.apiKey = builder.apiKey;
this.endpoint = builder.endpoint;
this.wsEndpoint = builder.wsEndpoint;
this.network = builder.network;
this.timeoutSeconds = builder.timeoutSeconds;
this.debug = builder.debug;
}
public String getApiKey() { return apiKey; }
public String getEndpoint() { return endpoint; }
public String getWsEndpoint() { return wsEndpoint; }
public Network getNetwork() { return network; }
public long getTimeoutSeconds() { return timeoutSeconds; }
public boolean isDebug() { return debug; }
public static Builder builder() { return new Builder(); }
public static class Builder {
private String apiKey;
private String endpoint = "https://rpc.synor.cc/api/v1";
private String wsEndpoint = "wss://rpc.synor.cc/ws";
private Network network = Network.MAINNET;
private long timeoutSeconds = 30;
private boolean debug = false;
public Builder apiKey(String apiKey) { this.apiKey = apiKey; return this; }
public Builder endpoint(String endpoint) { this.endpoint = endpoint; return this; }
public Builder wsEndpoint(String wsEndpoint) { this.wsEndpoint = wsEndpoint; return this; }
public Builder network(Network network) { this.network = network; return this; }
public Builder timeoutSeconds(long seconds) { this.timeoutSeconds = seconds; return this; }
public Builder debug(boolean debug) { this.debug = debug; return this; }
public RpcConfig build() { return new RpcConfig(this); }
}
}
/** Block header. */
class BlockHeader {
private String hash;
private long height;
private int version;
private String previousHash;
private String merkleRoot;
private long timestamp;
private String difficulty;
private long nonce;
public String getHash() { return hash; }
public long getHeight() { return height; }
public int getVersion() { return version; }
public String getPreviousHash() { return previousHash; }
public String getMerkleRoot() { return merkleRoot; }
public long getTimestamp() { return timestamp; }
public String getDifficulty() { return difficulty; }
public long getNonce() { return nonce; }
}
/** Full block. */
class Block extends BlockHeader {
private List<String> transactions;
private int size;
private int weight;
private int txCount;
public List<String> getTransactions() { return transactions; }
public int getSize() { return size; }
public int getWeight() { return weight; }
public int getTxCount() { return txCount; }
}
/** Transaction. */
class Transaction {
private String txid;
private String blockHash;
private long blockHeight;
private int confirmations;
private long timestamp;
private TransactionStatus status;
private String raw;
private int size;
private String fee;
public String getTxid() { return txid; }
public String getBlockHash() { return blockHash; }
public long getBlockHeight() { return blockHeight; }
public int getConfirmations() { return confirmations; }
public long getTimestamp() { return timestamp; }
public TransactionStatus getStatus() { return status; }
public String getRaw() { return raw; }
public int getSize() { return size; }
public String getFee() { return fee; }
}
/** Fee estimate. */
class FeeEstimate {
private Priority priority;
private String feeRate;
private int estimatedBlocks;
public Priority getPriority() { return priority; }
public String getFeeRate() { return feeRate; }
public int getEstimatedBlocks() { return estimatedBlocks; }
}
/** Chain information. */
class ChainInfo {
private String chain;
private String network;
private long height;
private String bestBlockHash;
private String difficulty;
private long medianTime;
private String chainWork;
private boolean syncing;
private double syncProgress;
public String getChain() { return chain; }
public String getNetwork() { return network; }
public long getHeight() { return height; }
public String getBestBlockHash() { return bestBlockHash; }
public String getDifficulty() { return difficulty; }
public long getMedianTime() { return medianTime; }
public String getChainWork() { return chainWork; }
public boolean isSyncing() { return syncing; }
public double getSyncProgress() { return syncProgress; }
}
/** Mempool information. */
class MempoolInfo {
private int size;
private long bytes;
private long usage;
private long maxMempool;
private String minFee;
public int getSize() { return size; }
public long getBytes() { return bytes; }
public long getUsage() { return usage; }
public long getMaxMempool() { return maxMempool; }
public String getMinFee() { return minFee; }
}
/** UTXO. */
class UTXO {
private String txid;
private int vout;
private String amount;
private String address;
private int confirmations;
public String getTxid() { return txid; }
public int getVout() { return vout; }
public String getAmount() { return amount; }
public String getAddress() { return address; }
public int getConfirmations() { return confirmations; }
}
/** Balance. */
class Balance {
private String confirmed;
private String unconfirmed;
private String total;
public String getConfirmed() { return confirmed; }
public String getUnconfirmed() { return unconfirmed; }
public String getTotal() { return total; }
}
/** Subscription. */
class Subscription {
private final String id;
private final String type;
private final long createdAt;
private final Runnable unsubscribe;
public Subscription(String id, String type, long createdAt, Runnable unsubscribe) {
this.id = id;
this.type = type;
this.createdAt = createdAt;
this.unsubscribe = unsubscribe;
}
public String getId() { return id; }
public String getType() { return type; }
public long getCreatedAt() { return createdAt; }
public void unsubscribe() { unsubscribe.run(); }
}
/** RPC exception. */
class RpcException extends RuntimeException {
private final int statusCode;
private final String code;
public RpcException(String message, int statusCode, String code) {
super(message);
this.statusCode = statusCode;
this.code = code;
}
public int getStatusCode() { return statusCode; }
public String getCode() { return code; }
}

View file

@ -0,0 +1,237 @@
package io.synor.storage;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Synor Storage SDK for Java.
*
* Decentralized storage, pinning, and content retrieval.
*
* Example:
* <pre>{@code
* SynorStorage storage = new SynorStorage("your-api-key");
* UploadResponse result = storage.upload("Hello, World!".getBytes()).join();
* System.out.println("CID: " + result.getCid());
* }</pre>
*/
public class SynorStorage {
private static final String DEFAULT_ENDPOINT = "https://storage.synor.cc/api/v1";
private static final String DEFAULT_GATEWAY = "https://gateway.synor.cc";
private final StorageConfig config;
private final HttpClient httpClient;
private final Gson gson;
public SynorStorage(String apiKey) {
this(StorageConfig.builder().apiKey(apiKey).build());
}
public SynorStorage(StorageConfig config) {
this.config = config;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(config.getTimeoutSeconds()))
.build();
this.gson = new GsonBuilder()
.setFieldNamingPolicy(com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();
}
/** Upload content. */
public CompletableFuture<UploadResponse> upload(byte[] data) {
return upload(data, UploadOptions.defaults());
}
/** Upload content with options. */
public CompletableFuture<UploadResponse> upload(byte[] data, UploadOptions options) {
String boundary = "----SynorBoundary" + System.currentTimeMillis();
String body = "--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"file\"; filename=\"file\"\r\n"
+ "Content-Type: application/octet-stream\r\n\r\n"
+ new String(data) + "\r\n"
+ "--" + boundary + "--\r\n";
StringBuilder path = new StringBuilder("/upload?");
path.append("pin=").append(options.isPin());
if (options.isWrapWithDirectory()) path.append("&wrapWithDirectory=true");
path.append("&cidVersion=").append(options.getCidVersion());
path.append("&hashAlgorithm=").append(options.getHashAlgorithm().getValue());
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(config.getEndpoint() + path))
.header("Authorization", "Bearer " + config.getApiKey())
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.timeout(Duration.ofSeconds(config.getTimeoutSeconds()))
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> handleResponse(response, UploadResponse.class));
}
/** Download content by CID. */
public CompletableFuture<byte[]> download(String cid) {
return download(cid, DownloadOptions.defaults());
}
/** Download content with options. */
public CompletableFuture<byte[]> download(String cid, DownloadOptions options) {
StringBuilder path = new StringBuilder("/content/" + cid);
if (options.getOffset() != null || options.getLength() != null) {
path.append("?");
if (options.getOffset() != null) path.append("offset=").append(options.getOffset()).append("&");
if (options.getLength() != null) path.append("length=").append(options.getLength());
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(config.getEndpoint() + path))
.header("Authorization", "Bearer " + config.getApiKey())
.timeout(Duration.ofSeconds(config.getTimeoutSeconds()))
.GET()
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofByteArray())
.thenApply(response -> {
if (response.statusCode() >= 400) {
throw new StorageException("Download failed", response.statusCode(), null);
}
return response.body();
});
}
/** Pin content. */
public CompletableFuture<Pin> pin(PinRequest pinRequest) {
return post("/pins", pinRequest, Pin.class);
}
/** Unpin content. */
public CompletableFuture<Void> unpin(String cid) {
return delete("/pins/" + cid);
}
/** Get pin status. */
public CompletableFuture<Pin> getPinStatus(String cid) {
return get("/pins/" + cid, Pin.class);
}
/** List pins. */
public CompletableFuture<ListPinsResponse> listPins(ListPinsOptions options) {
StringBuilder path = new StringBuilder("/pins?");
if (options.getLimit() != null) path.append("limit=").append(options.getLimit()).append("&");
if (options.getOffset() != null) path.append("offset=").append(options.getOffset());
return get(path.toString(), ListPinsResponse.class);
}
/** Get gateway URL. */
public GatewayUrl getGatewayUrl(String cid, String path) {
String fullPath = path != null ? "/" + cid + "/" + path : "/" + cid;
return new GatewayUrl(config.getGateway() + "/ipfs" + fullPath, cid, path);
}
/** Create CAR file. */
public CompletableFuture<CarFile> createCar(List<FileEntry> files) {
return post("/car/create", Map.of("files", files), CarFile.class);
}
/** Import CAR file. */
public CompletableFuture<ImportCarResponse> importCar(byte[] carData, boolean pin) {
String encoded = Base64.getEncoder().encodeToString(carData);
return post("/car/import", Map.of("car", encoded, "pin", pin), ImportCarResponse.class);
}
/** Create directory. */
public CompletableFuture<UploadResponse> createDirectory(List<FileEntry> files) {
return post("/directory", Map.of("files", files), UploadResponse.class);
}
/** List directory. */
public CompletableFuture<List<DirectoryEntry>> listDirectory(String cid, String path) {
String apiPath = "/directory/" + cid + (path != null ? "?path=" + path : "");
return get(apiPath, ListDirectoryResponse.class)
.thenApply(ListDirectoryResponse::getEntries);
}
/** Get storage stats. */
public CompletableFuture<StorageStats> getStats() {
return get("/stats", StorageStats.class);
}
/** Check if content exists. */
public CompletableFuture<Boolean> exists(String cid) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(config.getEndpoint() + "/content/" + cid))
.header("Authorization", "Bearer " + config.getApiKey())
.method("HEAD", HttpRequest.BodyPublishers.noBody())
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding())
.thenApply(response -> response.statusCode() == 200);
}
private <T> CompletableFuture<T> get(String path, Class<T> responseType) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(config.getEndpoint() + path))
.header("Authorization", "Bearer " + config.getApiKey())
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(config.getTimeoutSeconds()))
.GET()
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> handleResponse(response, responseType));
}
private <T> CompletableFuture<T> post(String path, Object body, Class<T> responseType) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(config.getEndpoint() + path))
.header("Authorization", "Bearer " + config.getApiKey())
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(config.getTimeoutSeconds()))
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(body)))
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> handleResponse(response, responseType));
}
private CompletableFuture<Void> delete(String path) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(config.getEndpoint() + path))
.header("Authorization", "Bearer " + config.getApiKey())
.timeout(Duration.ofSeconds(config.getTimeoutSeconds()))
.DELETE()
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding())
.thenApply(response -> {
if (response.statusCode() >= 400) {
throw new StorageException("Delete failed", response.statusCode(), null);
}
return null;
});
}
private <T> T handleResponse(HttpResponse<String> response, Class<T> responseType) {
if (response.statusCode() >= 400) {
ErrorResponse error = gson.fromJson(response.body(), ErrorResponse.class);
throw new StorageException(
error != null ? error.getMessage() : "Unknown error",
response.statusCode(),
error != null ? error.getCode() : null
);
}
return gson.fromJson(response.body(), responseType);
}
private record ListDirectoryResponse(List<DirectoryEntry> entries) { List<DirectoryEntry> getEntries() { return entries; } }
private record ErrorResponse(String message, String code) { String getMessage() { return message; } String getCode() { return code; } }
}

View file

@ -0,0 +1,285 @@
package io.synor.storage;
import java.util.List;
/** Pin status. */
public enum PinStatus {
QUEUED("queued"), PINNING("pinning"), PINNED("pinned"), FAILED("failed"), UNPINNED("unpinned");
private final String value;
PinStatus(String value) { this.value = value; }
public String getValue() { return value; }
}
/** Hash algorithm. */
public enum HashAlgorithm {
SHA2_256("sha2-256"), BLAKE3("blake3");
private final String value;
HashAlgorithm(String value) { this.value = value; }
public String getValue() { return value; }
}
/** Entry type. */
public enum EntryType {
FILE("file"), DIRECTORY("directory");
private final String value;
EntryType(String value) { this.value = value; }
public String getValue() { return value; }
}
/** Storage configuration. */
class StorageConfig {
private final String apiKey;
private final String endpoint;
private final String gateway;
private final long timeoutSeconds;
private final boolean debug;
private StorageConfig(Builder builder) {
this.apiKey = builder.apiKey;
this.endpoint = builder.endpoint;
this.gateway = builder.gateway;
this.timeoutSeconds = builder.timeoutSeconds;
this.debug = builder.debug;
}
public String getApiKey() { return apiKey; }
public String getEndpoint() { return endpoint; }
public String getGateway() { return gateway; }
public long getTimeoutSeconds() { return timeoutSeconds; }
public boolean isDebug() { return debug; }
public static Builder builder() { return new Builder(); }
public static class Builder {
private String apiKey;
private String endpoint = "https://storage.synor.cc/api/v1";
private String gateway = "https://gateway.synor.cc";
private long timeoutSeconds = 30;
private boolean debug = false;
public Builder apiKey(String apiKey) { this.apiKey = apiKey; return this; }
public Builder endpoint(String endpoint) { this.endpoint = endpoint; return this; }
public Builder gateway(String gateway) { this.gateway = gateway; return this; }
public Builder timeoutSeconds(long seconds) { this.timeoutSeconds = seconds; return this; }
public Builder debug(boolean debug) { this.debug = debug; return this; }
public StorageConfig build() { return new StorageConfig(this); }
}
}
/** Upload options. */
class UploadOptions {
private boolean pin = true;
private boolean wrapWithDirectory = false;
private int cidVersion = 1;
private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA2_256;
public static UploadOptions defaults() { return new UploadOptions(); }
public boolean isPin() { return pin; }
public boolean isWrapWithDirectory() { return wrapWithDirectory; }
public int getCidVersion() { return cidVersion; }
public HashAlgorithm getHashAlgorithm() { return hashAlgorithm; }
public UploadOptions pin(boolean pin) { this.pin = pin; return this; }
public UploadOptions wrapWithDirectory(boolean wrap) { this.wrapWithDirectory = wrap; return this; }
public UploadOptions cidVersion(int version) { this.cidVersion = version; return this; }
public UploadOptions hashAlgorithm(HashAlgorithm algo) { this.hashAlgorithm = algo; return this; }
}
/** Download options. */
class DownloadOptions {
private Long offset;
private Long length;
public static DownloadOptions defaults() { return new DownloadOptions(); }
public Long getOffset() { return offset; }
public Long getLength() { return length; }
public DownloadOptions offset(long offset) { this.offset = offset; return this; }
public DownloadOptions length(long length) { this.length = length; return this; }
}
/** Upload response. */
class UploadResponse {
private String cid;
private long size;
private String name;
private String hash;
public String getCid() { return cid; }
public long getSize() { return size; }
public String getName() { return name; }
public String getHash() { return hash; }
}
/** Pin. */
class Pin {
private String cid;
private PinStatus status;
private String name;
private Long size;
private Long createdAt;
private Long expiresAt;
private List<String> delegates;
public String getCid() { return cid; }
public PinStatus getStatus() { return status; }
public String getName() { return name; }
public Long getSize() { return size; }
public Long getCreatedAt() { return createdAt; }
public Long getExpiresAt() { return expiresAt; }
public List<String> getDelegates() { return delegates; }
}
/** Pin request. */
class PinRequest {
private String cid;
private String name;
private Long duration;
private List<String> origins;
public PinRequest(String cid) { this.cid = cid; }
public PinRequest name(String name) { this.name = name; return this; }
public PinRequest duration(long duration) { this.duration = duration; return this; }
public PinRequest origins(List<String> origins) { this.origins = origins; return this; }
}
/** List pins options. */
class ListPinsOptions {
private Integer limit;
private Integer offset;
private List<PinStatus> status;
private String name;
public Integer getLimit() { return limit; }
public Integer getOffset() { return offset; }
public ListPinsOptions limit(int limit) { this.limit = limit; return this; }
public ListPinsOptions offset(int offset) { this.offset = offset; return this; }
}
/** List pins response. */
class ListPinsResponse {
private List<Pin> pins;
private int total;
private boolean hasMore;
public List<Pin> getPins() { return pins; }
public int getTotal() { return total; }
public boolean isHasMore() { return hasMore; }
}
/** Gateway URL. */
class GatewayUrl {
private final String url;
private final String cid;
private final String path;
public GatewayUrl(String url, String cid, String path) {
this.url = url;
this.cid = cid;
this.path = path;
}
public String getUrl() { return url; }
public String getCid() { return cid; }
public String getPath() { return path; }
}
/** File entry. */
class FileEntry {
private String name;
private String content;
private String cid;
public FileEntry(String name) { this.name = name; }
public FileEntry content(byte[] content) {
this.content = java.util.Base64.getEncoder().encodeToString(content);
return this;
}
public FileEntry cid(String cid) { this.cid = cid; return this; }
}
/** Directory entry. */
class DirectoryEntry {
private String name;
private String cid;
private Long size;
private EntryType type;
public String getName() { return name; }
public String getCid() { return cid; }
public Long getSize() { return size; }
public EntryType getType() { return type; }
}
/** CAR block. */
class CarBlock {
private String cid;
private String data;
private Long size;
public String getCid() { return cid; }
public String getData() { return data; }
public Long getSize() { return size; }
}
/** CAR file. */
class CarFile {
private int version;
private List<String> roots;
private List<CarBlock> blocks;
private Long size;
public int getVersion() { return version; }
public List<String> getRoots() { return roots; }
public List<CarBlock> getBlocks() { return blocks; }
public Long getSize() { return size; }
}
/** Import CAR response. */
class ImportCarResponse {
private List<String> roots;
private int blocksImported;
public List<String> getRoots() { return roots; }
public int getBlocksImported() { return blocksImported; }
}
/** Storage stats. */
class StorageStats {
private long totalSize;
private int pinCount;
private BandwidthStats bandwidth;
public long getTotalSize() { return totalSize; }
public int getPinCount() { return pinCount; }
public BandwidthStats getBandwidth() { return bandwidth; }
}
/** Bandwidth stats. */
class BandwidthStats {
private long upload;
private long download;
public long getUpload() { return upload; }
public long getDownload() { return download; }
}
/** Storage exception. */
class StorageException extends RuntimeException {
private final int statusCode;
private final String code;
public StorageException(String message, int statusCode, String code) {
super(message);
this.statusCode = statusCode;
this.code = code;
}
public int getStatusCode() { return statusCode; }
public String getCode() { return code; }
}

View file

@ -0,0 +1,163 @@
package io.synor.wallet;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Synor Wallet SDK for Java.
*
* Manage wallets, sign transactions, and query balances on the Synor blockchain.
*
* Example:
* <pre>{@code
* SynorWallet wallet = new SynorWallet("your-api-key");
* CreateWalletResult result = wallet.createWallet(WalletType.STANDARD).join();
* System.out.println("Address: " + result.getWallet().getAddress());
* }</pre>
*/
public class SynorWallet {
private static final String DEFAULT_ENDPOINT = "https://wallet.synor.cc/api/v1";
private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30);
private final WalletConfig config;
private final HttpClient httpClient;
private final Gson gson;
public SynorWallet(String apiKey) {
this(WalletConfig.builder().apiKey(apiKey).build());
}
public SynorWallet(WalletConfig config) {
this.config = config;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(config.getTimeoutSeconds()))
.build();
this.gson = new GsonBuilder()
.setFieldNamingPolicy(com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();
}
/**
* Create a new wallet.
*/
public CompletableFuture<CreateWalletResult> createWallet(WalletType type) {
var body = new CreateWalletRequest(type, config.getNetwork());
return post("/wallets", body, CreateWalletResult.class);
}
/**
* Import a wallet from mnemonic.
*/
public CompletableFuture<Wallet> importWallet(String mnemonic, String passphrase, WalletType type) {
var body = new ImportWalletRequest(mnemonic, passphrase, type, config.getNetwork());
return post("/wallets/import", body, WalletResponse.class)
.thenApply(WalletResponse::getWallet);
}
/**
* Get a wallet by ID.
*/
public CompletableFuture<Wallet> getWallet(String walletId) {
return get("/wallets/" + walletId, WalletResponse.class)
.thenApply(WalletResponse::getWallet);
}
/**
* Sign a transaction.
*/
public CompletableFuture<SignedTransaction> signTransaction(String walletId, Transaction tx) {
var body = new SignTransactionRequest(tx);
return post("/wallets/" + walletId + "/sign", body, SignedTransactionResponse.class)
.thenApply(SignedTransactionResponse::getSignedTransaction);
}
/**
* Sign a message.
*/
public CompletableFuture<SignedMessage> signMessage(String walletId, String message, String format) {
var body = new SignMessageRequest(message, format != null ? format : "text");
return post("/wallets/" + walletId + "/sign-message", body, SignedMessage.class);
}
/**
* Get balance for an address.
*/
public CompletableFuture<BalanceResponse> getBalance(String address, boolean includeTokens) {
String path = "/balances/" + address + "?includeTokens=" + includeTokens;
return get(path, BalanceResponse.class);
}
/**
* Get UTXOs for an address.
*/
public CompletableFuture<UTXO[]> getUTXOs(String address) {
return get("/utxos/" + address, UTXOsResponse.class)
.thenApply(UTXOsResponse::getUtxos);
}
/**
* Estimate transaction fee.
*/
public CompletableFuture<FeeEstimate> estimateFee(Priority priority) {
String path = "/fees/estimate?priority=" + priority.getValue();
return get(path, FeeEstimate.class);
}
private <T> CompletableFuture<T> get(String path, Class<T> responseType) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(config.getEndpoint() + path))
.header("Authorization", "Bearer " + config.getApiKey())
.header("Content-Type", "application/json")
.header("X-Network", config.getNetwork().getValue())
.timeout(Duration.ofSeconds(config.getTimeoutSeconds()))
.GET()
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> handleResponse(response, responseType));
}
private <T> CompletableFuture<T> post(String path, Object body, Class<T> responseType) {
String jsonBody = gson.toJson(body);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(config.getEndpoint() + path))
.header("Authorization", "Bearer " + config.getApiKey())
.header("Content-Type", "application/json")
.header("X-Network", config.getNetwork().getValue())
.timeout(Duration.ofSeconds(config.getTimeoutSeconds()))
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> handleResponse(response, responseType));
}
private <T> T handleResponse(HttpResponse<String> response, Class<T> responseType) {
if (response.statusCode() >= 400) {
ErrorResponse error = gson.fromJson(response.body(), ErrorResponse.class);
throw new WalletException(
error != null ? error.getMessage() : "Unknown error",
response.statusCode(),
error != null ? error.getCode() : null
);
}
return gson.fromJson(response.body(), responseType);
}
// Request/Response DTOs
private record CreateWalletRequest(WalletType type, Network network) {}
private record ImportWalletRequest(String mnemonic, String passphrase, WalletType type, Network network) {}
private record SignTransactionRequest(Transaction transaction) {}
private record SignMessageRequest(String message, String format) {}
private record WalletResponse(Wallet wallet) { Wallet getWallet() { return wallet; } }
private record SignedTransactionResponse(SignedTransaction signedTransaction) { SignedTransaction getSignedTransaction() { return signedTransaction; } }
private record UTXOsResponse(UTXO[] utxos) { UTXO[] getUtxos() { return utxos; } }
private record ErrorResponse(String message, String code) { String getMessage() { return message; } String getCode() { return code; } }
}

View file

@ -0,0 +1,240 @@
package io.synor.wallet;
import java.util.List;
/** Network type. */
public enum Network {
MAINNET("mainnet"),
TESTNET("testnet");
private final String value;
Network(String value) { this.value = value; }
public String getValue() { return value; }
}
/** Wallet type. */
public enum WalletType {
STANDARD("standard"),
MULTISIG("multisig"),
STEALTH("stealth"),
HARDWARE("hardware");
private final String value;
WalletType(String value) { this.value = value; }
public String getValue() { return value; }
}
/** Transaction priority. */
public enum Priority {
LOW("low"),
MEDIUM("medium"),
HIGH("high"),
URGENT("urgent");
private final String value;
Priority(String value) { this.value = value; }
public String getValue() { return value; }
}
/** Wallet configuration. */
class WalletConfig {
private final String apiKey;
private final String endpoint;
private final Network network;
private final long timeoutSeconds;
private final boolean debug;
private WalletConfig(Builder builder) {
this.apiKey = builder.apiKey;
this.endpoint = builder.endpoint;
this.network = builder.network;
this.timeoutSeconds = builder.timeoutSeconds;
this.debug = builder.debug;
}
public String getApiKey() { return apiKey; }
public String getEndpoint() { return endpoint; }
public Network getNetwork() { return network; }
public long getTimeoutSeconds() { return timeoutSeconds; }
public boolean isDebug() { return debug; }
public static Builder builder() { return new Builder(); }
public static class Builder {
private String apiKey;
private String endpoint = "https://wallet.synor.cc/api/v1";
private Network network = Network.MAINNET;
private long timeoutSeconds = 30;
private boolean debug = false;
public Builder apiKey(String apiKey) { this.apiKey = apiKey; return this; }
public Builder endpoint(String endpoint) { this.endpoint = endpoint; return this; }
public Builder network(Network network) { this.network = network; return this; }
public Builder timeoutSeconds(long seconds) { this.timeoutSeconds = seconds; return this; }
public Builder debug(boolean debug) { this.debug = debug; return this; }
public WalletConfig build() { return new WalletConfig(this); }
}
}
/** Wallet instance. */
class Wallet {
private String id;
private String address;
private String publicKey;
private WalletType type;
private long createdAt;
public String getId() { return id; }
public String getAddress() { return address; }
public String getPublicKey() { return publicKey; }
public WalletType getType() { return type; }
public long getCreatedAt() { return createdAt; }
}
/** Create wallet result. */
class CreateWalletResult {
private Wallet wallet;
private String mnemonic;
public Wallet getWallet() { return wallet; }
public String getMnemonic() { return mnemonic; }
}
/** Transaction input. */
class TransactionInput {
private String txid;
private int vout;
private String amount;
private String scriptSig;
public String getTxid() { return txid; }
public int getVout() { return vout; }
public String getAmount() { return amount; }
public String getScriptSig() { return scriptSig; }
}
/** Transaction output. */
class TransactionOutput {
private String address;
private String amount;
private String scriptPubKey;
public String getAddress() { return address; }
public String getAmount() { return amount; }
public String getScriptPubKey() { return scriptPubKey; }
}
/** Transaction. */
class Transaction {
private int version;
private List<TransactionInput> inputs;
private List<TransactionOutput> outputs;
private int lockTime;
private String fee;
public int getVersion() { return version; }
public List<TransactionInput> getInputs() { return inputs; }
public List<TransactionOutput> getOutputs() { return outputs; }
public int getLockTime() { return lockTime; }
public String getFee() { return fee; }
}
/** Signed transaction. */
class SignedTransaction {
private String raw;
private String txid;
private int size;
private int weight;
public String getRaw() { return raw; }
public String getTxid() { return txid; }
public int getSize() { return size; }
public int getWeight() { return weight; }
}
/** Signed message. */
class SignedMessage {
private String signature;
private String publicKey;
private String address;
public String getSignature() { return signature; }
public String getPublicKey() { return publicKey; }
public String getAddress() { return address; }
}
/** UTXO. */
class UTXO {
private String txid;
private int vout;
private String amount;
private String address;
private int confirmations;
private String scriptPubKey;
public String getTxid() { return txid; }
public int getVout() { return vout; }
public String getAmount() { return amount; }
public String getAddress() { return address; }
public int getConfirmations() { return confirmations; }
public String getScriptPubKey() { return scriptPubKey; }
}
/** Balance. */
class Balance {
private String confirmed;
private String unconfirmed;
private String total;
public String getConfirmed() { return confirmed; }
public String getUnconfirmed() { return unconfirmed; }
public String getTotal() { return total; }
}
/** Token balance. */
class TokenBalance {
private String token;
private String symbol;
private int decimals;
private String balance;
public String getToken() { return token; }
public String getSymbol() { return symbol; }
public int getDecimals() { return decimals; }
public String getBalance() { return balance; }
}
/** Balance response. */
class BalanceResponse {
private Balance nativeBalance;
private List<TokenBalance> tokens;
public Balance getNativeBalance() { return nativeBalance; }
public List<TokenBalance> getTokens() { return tokens; }
}
/** Fee estimate. */
class FeeEstimate {
private Priority priority;
private String feeRate;
private int estimatedBlocks;
public Priority getPriority() { return priority; }
public String getFeeRate() { return feeRate; }
public int getEstimatedBlocks() { return estimatedBlocks; }
}
/** Wallet exception. */
class WalletException extends RuntimeException {
private final int statusCode;
private final String code;
public WalletException(String message, int statusCode, String code) {
super(message);
this.statusCode = statusCode;
this.code = code;
}
public int getStatusCode() { return statusCode; }
public String getCode() { return code; }
}

501
sdk/js/src/bridge/client.ts Normal file
View file

@ -0,0 +1,501 @@
/**
* Synor Bridge SDK Client
*
* Cross-chain asset transfers with lock-mint and burn-unlock patterns.
*/
import {
BridgeConfig,
BridgeError,
BridgeErrorCode,
Chain,
ChainId,
Asset,
WrappedAsset,
Transfer,
TransferFilter,
TransferStatus,
LockReceipt,
LockProof,
LockOptions,
BurnReceipt,
BurnProof,
BurnOptions,
MintOptions,
UnlockOptions,
FeeEstimate,
ExchangeRate,
SignedTransaction,
DEFAULT_BRIDGE_ENDPOINT,
} from './types';
/** Synor Bridge Client */
export class SynorBridge {
private readonly config: Required<BridgeConfig>;
private closed = false;
constructor(config: BridgeConfig) {
this.config = {
apiKey: config.apiKey,
endpoint: config.endpoint || DEFAULT_BRIDGE_ENDPOINT,
timeout: config.timeout || 60000,
retries: config.retries || 3,
debug: config.debug || false,
};
}
// ==================== Chain Operations ====================
/** Get all supported chains */
async getSupportedChains(): Promise<Chain[]> {
const response = await this.request<{ chains: Chain[] }>('GET', '/chains');
return response.chains;
}
/** Get chain by ID */
async getChain(chainId: ChainId): Promise<Chain> {
return this.request<Chain>('GET', `/chains/${chainId}`);
}
/** Check if chain is supported */
async isChainSupported(chainId: ChainId): Promise<boolean> {
try {
const chain = await this.getChain(chainId);
return chain.supported;
} catch {
return false;
}
}
// ==================== Asset Operations ====================
/** Get supported assets for a chain */
async getSupportedAssets(chainId: ChainId): Promise<Asset[]> {
const response = await this.request<{ assets: Asset[] }>(
'GET',
`/chains/${chainId}/assets`
);
return response.assets;
}
/** Get asset by ID */
async getAsset(assetId: string): Promise<Asset> {
return this.request<Asset>('GET', `/assets/${assetId}`);
}
/** Get wrapped asset mapping */
async getWrappedAsset(
originalAssetId: string,
targetChain: ChainId
): Promise<WrappedAsset> {
return this.request<WrappedAsset>(
'GET',
`/assets/${originalAssetId}/wrapped/${targetChain}`
);
}
/** Get all wrapped assets for a chain */
async getWrappedAssets(chainId: ChainId): Promise<WrappedAsset[]> {
const response = await this.request<{ assets: WrappedAsset[] }>(
'GET',
`/chains/${chainId}/wrapped`
);
return response.assets;
}
// ==================== Fee & Rate Operations ====================
/** Estimate bridge fee */
async estimateFee(
asset: string,
amount: string,
sourceChain: ChainId,
targetChain: ChainId
): Promise<FeeEstimate> {
return this.request<FeeEstimate>('POST', '/fees/estimate', {
asset,
amount,
sourceChain,
targetChain,
});
}
/** Get exchange rate between assets */
async getExchangeRate(fromAsset: string, toAsset: string): Promise<ExchangeRate> {
return this.request<ExchangeRate>(
'GET',
`/rates/${encodeURIComponent(fromAsset)}/${encodeURIComponent(toAsset)}`
);
}
// ==================== Lock-Mint Flow ====================
/**
* Lock assets on source chain for cross-chain transfer
* Step 1 of lock-mint flow
*/
async lock(
asset: string,
amount: string,
targetChain: ChainId,
options?: LockOptions
): Promise<LockReceipt> {
return this.request<LockReceipt>('POST', '/transfers/lock', {
asset,
amount,
targetChain,
recipient: options?.recipient,
deadline: options?.deadline,
slippage: options?.slippage,
});
}
/**
* Get lock proof for minting
* Step 2 of lock-mint flow (wait for confirmations)
*/
async getLockProof(lockReceiptId: string): Promise<LockProof> {
return this.request<LockProof>('GET', `/transfers/lock/${lockReceiptId}/proof`);
}
/**
* Wait for lock proof to be ready
* Polls until confirmations are sufficient
*/
async waitForLockProof(
lockReceiptId: string,
pollInterval = 5000,
maxWait = 600000
): Promise<LockProof> {
const startTime = Date.now();
while (Date.now() - startTime < maxWait) {
try {
const proof = await this.getLockProof(lockReceiptId);
return proof;
} catch (error) {
if (
error instanceof BridgeError &&
error.code === 'CONFIRMATIONS_PENDING'
) {
await this.sleep(pollInterval);
continue;
}
throw error;
}
}
throw new BridgeError(
'Timeout waiting for lock proof',
'CONFIRMATIONS_PENDING'
);
}
/**
* Mint wrapped tokens on target chain
* Step 3 of lock-mint flow
*/
async mint(
proof: LockProof,
targetAddress: string,
options?: MintOptions
): Promise<SignedTransaction> {
return this.request<SignedTransaction>('POST', '/transfers/mint', {
proof,
targetAddress,
gasLimit: options?.gasLimit,
maxFeePerGas: options?.maxFeePerGas,
maxPriorityFeePerGas: options?.maxPriorityFeePerGas,
});
}
// ==================== Burn-Unlock Flow ====================
/**
* Burn wrapped tokens on current chain
* Step 1 of burn-unlock flow
*/
async burn(
wrappedAsset: string,
amount: string,
options?: BurnOptions
): Promise<BurnReceipt> {
return this.request<BurnReceipt>('POST', '/transfers/burn', {
wrappedAsset,
amount,
recipient: options?.recipient,
deadline: options?.deadline,
});
}
/**
* Get burn proof for unlocking
* Step 2 of burn-unlock flow (wait for confirmations)
*/
async getBurnProof(burnReceiptId: string): Promise<BurnProof> {
return this.request<BurnProof>('GET', `/transfers/burn/${burnReceiptId}/proof`);
}
/**
* Wait for burn proof to be ready
* Polls until confirmations are sufficient
*/
async waitForBurnProof(
burnReceiptId: string,
pollInterval = 5000,
maxWait = 600000
): Promise<BurnProof> {
const startTime = Date.now();
while (Date.now() - startTime < maxWait) {
try {
const proof = await this.getBurnProof(burnReceiptId);
return proof;
} catch (error) {
if (
error instanceof BridgeError &&
error.code === 'CONFIRMATIONS_PENDING'
) {
await this.sleep(pollInterval);
continue;
}
throw error;
}
}
throw new BridgeError(
'Timeout waiting for burn proof',
'CONFIRMATIONS_PENDING'
);
}
/**
* Unlock original tokens on source chain
* Step 3 of burn-unlock flow
*/
async unlock(proof: BurnProof, options?: UnlockOptions): Promise<SignedTransaction> {
return this.request<SignedTransaction>('POST', '/transfers/unlock', {
proof,
gasLimit: options?.gasLimit,
gasPrice: options?.gasPrice,
});
}
// ==================== Transfer Management ====================
/** Get transfer by ID */
async getTransfer(transferId: string): Promise<Transfer> {
return this.request<Transfer>('GET', `/transfers/${transferId}`);
}
/** Get transfer status */
async getTransferStatus(transferId: string): Promise<TransferStatus> {
const transfer = await this.getTransfer(transferId);
return transfer.status;
}
/** List transfers with optional filters */
async listTransfers(filter?: TransferFilter): Promise<Transfer[]> {
const params = new URLSearchParams();
if (filter) {
if (filter.status) params.set('status', filter.status);
if (filter.sourceChain) params.set('sourceChain', filter.sourceChain);
if (filter.targetChain) params.set('targetChain', filter.targetChain);
if (filter.asset) params.set('asset', filter.asset);
if (filter.sender) params.set('sender', filter.sender);
if (filter.recipient) params.set('recipient', filter.recipient);
if (filter.fromDate) params.set('fromDate', filter.fromDate.toString());
if (filter.toDate) params.set('toDate', filter.toDate.toString());
if (filter.limit) params.set('limit', filter.limit.toString());
if (filter.offset) params.set('offset', filter.offset.toString());
}
const query = params.toString();
const path = query ? `/transfers?${query}` : '/transfers';
const response = await this.request<{ transfers: Transfer[] }>('GET', path);
return response.transfers;
}
/** Wait for transfer to complete */
async waitForTransfer(
transferId: string,
pollInterval = 10000,
maxWait = 1800000
): Promise<Transfer> {
const startTime = Date.now();
const finalStatuses: TransferStatus[] = ['completed', 'failed', 'refunded'];
while (Date.now() - startTime < maxWait) {
const transfer = await this.getTransfer(transferId);
if (finalStatuses.includes(transfer.status)) {
return transfer;
}
await this.sleep(pollInterval);
}
throw new BridgeError('Timeout waiting for transfer completion');
}
// ==================== Convenience Methods ====================
/**
* Execute complete lock-mint transfer
* Combines lock, wait for proof, and mint into single operation
*/
async bridgeTo(
asset: string,
amount: string,
targetChain: ChainId,
targetAddress: string,
options?: LockOptions & MintOptions
): Promise<Transfer> {
// Lock on source chain
const lockReceipt = await this.lock(asset, amount, targetChain, options);
if (this.config.debug) {
console.log(`Locked: ${lockReceipt.id}, waiting for confirmations...`);
}
// Wait for proof
const proof = await this.waitForLockProof(lockReceipt.id);
if (this.config.debug) {
console.log(`Proof ready, minting on ${targetChain}...`);
}
// Mint on target chain
await this.mint(proof, targetAddress, options);
// Return final transfer status
return this.waitForTransfer(lockReceipt.id);
}
/**
* Execute complete burn-unlock transfer
* Combines burn, wait for proof, and unlock into single operation
*/
async bridgeBack(
wrappedAsset: string,
amount: string,
options?: BurnOptions & UnlockOptions
): Promise<Transfer> {
// Burn wrapped tokens
const burnReceipt = await this.burn(wrappedAsset, amount, options);
if (this.config.debug) {
console.log(`Burned: ${burnReceipt.id}, waiting for confirmations...`);
}
// Wait for proof
const proof = await this.waitForBurnProof(burnReceipt.id);
if (this.config.debug) {
console.log(`Proof ready, unlocking on ${burnReceipt.targetChain}...`);
}
// Unlock on original chain
await this.unlock(proof, options);
// Return final transfer status
return this.waitForTransfer(burnReceipt.id);
}
// ==================== Lifecycle ====================
/** Close the client */
close(): void {
this.closed = true;
}
/** Check if client is closed */
isClosed(): boolean {
return this.closed;
}
/** Health check */
async healthCheck(): Promise<boolean> {
try {
const response = await this.request<{ status: string }>('GET', '/health');
return response.status === 'healthy';
} catch {
return false;
}
}
// ==================== Private Methods ====================
private async request<T>(
method: string,
path: string,
body?: Record<string, unknown>
): Promise<T> {
if (this.closed) {
throw new BridgeError('Client has been closed');
}
let lastError: Error | undefined;
for (let attempt = 0; attempt < this.config.retries; attempt++) {
try {
return await this.doRequest<T>(method, path, body);
} catch (error) {
if (this.config.debug) {
console.error(`Attempt ${attempt + 1} failed:`, error);
}
lastError = error as Error;
if (attempt < this.config.retries - 1) {
await this.sleep(Math.pow(2, attempt) * 1000);
}
}
}
throw lastError || new BridgeError('Unknown error after retries');
}
private async doRequest<T>(
method: string,
path: string,
body?: Record<string, unknown>
): Promise<T> {
const url = `${this.config.endpoint}${path}`;
const headers: Record<string, string> = {
Authorization: `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json',
'X-SDK-Version': 'js/0.1.0',
};
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
try {
const response = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
});
if (!response.ok) {
const errorBody = await response.json().catch(() => ({}));
const message =
errorBody.message || errorBody.error || `HTTP ${response.status}`;
const code = errorBody.code as BridgeErrorCode | undefined;
throw new BridgeError(message, code, response.status, errorBody);
}
return response.json();
} finally {
clearTimeout(timeoutId);
}
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}

View file

@ -0,0 +1,25 @@
/**
* Synor Bridge SDK
*
* Cross-chain asset transfers with lock-mint and burn-unlock patterns.
*
* @example
* ```typescript
* import { SynorBridge } from '@synor/bridge';
*
* const bridge = new SynorBridge({ apiKey: 'your-api-key' });
*
* // Bridge tokens from Synor to Ethereum
* const transfer = await bridge.bridgeTo(
* 'SYNR',
* '1000000000000000000', // 1 SYNR in wei
* 'ethereum',
* '0x...'
* );
*
* console.log(`Transfer ${transfer.id}: ${transfer.status}`);
* ```
*/
export * from './types';
export * from './client';

279
sdk/js/src/bridge/types.ts Normal file
View file

@ -0,0 +1,279 @@
/**
* Synor Bridge SDK Types
*
* Cross-chain asset transfers with lock-mint and burn-unlock patterns.
*/
// ==================== Chain Types ====================
/** Supported blockchain networks */
export type ChainId =
| 'synor'
| 'ethereum'
| 'polygon'
| 'arbitrum'
| 'optimism'
| 'bsc'
| 'avalanche'
| 'solana'
| 'cosmos';
/** Chain information */
export interface Chain {
id: ChainId;
name: string;
chainId: number;
rpcUrl: string;
explorerUrl: string;
nativeCurrency: {
name: string;
symbol: string;
decimals: number;
};
confirmations: number;
estimatedBlockTime: number; // seconds
supported: boolean;
}
// ==================== Asset Types ====================
/** Asset type */
export type AssetType = 'native' | 'erc20' | 'erc721' | 'erc1155';
/** Asset information */
export interface Asset {
id: string;
symbol: string;
name: string;
type: AssetType;
chain: ChainId;
contractAddress?: string;
decimals: number;
logoUrl?: string;
verified: boolean;
}
/** Wrapped asset mapping */
export interface WrappedAsset {
originalAsset: Asset;
wrappedAsset: Asset;
chain: ChainId;
bridgeContract: string;
}
// ==================== Transfer Types ====================
/** Transfer status */
export type TransferStatus =
| 'pending'
| 'locked'
| 'confirming'
| 'minting'
| 'completed'
| 'failed'
| 'refunded';
/** Transfer direction */
export type TransferDirection = 'lock_mint' | 'burn_unlock';
/** Lock receipt from source chain */
export interface LockReceipt {
id: string;
txHash: string;
sourceChain: ChainId;
targetChain: ChainId;
asset: Asset;
amount: string;
sender: string;
recipient: string;
lockTimestamp: number;
confirmations: number;
requiredConfirmations: number;
}
/** Proof for minting on target chain */
export interface LockProof {
lockReceipt: LockReceipt;
merkleProof: string[];
blockHeader: string;
signatures: ValidatorSignature[];
}
/** Validator signature for proof */
export interface ValidatorSignature {
validator: string;
signature: string;
timestamp: number;
}
/** Burn receipt for unlocking */
export interface BurnReceipt {
id: string;
txHash: string;
sourceChain: ChainId;
targetChain: ChainId;
wrappedAsset: Asset;
originalAsset: Asset;
amount: string;
sender: string;
recipient: string;
burnTimestamp: number;
confirmations: number;
requiredConfirmations: number;
}
/** Proof for unlocking on original chain */
export interface BurnProof {
burnReceipt: BurnReceipt;
merkleProof: string[];
blockHeader: string;
signatures: ValidatorSignature[];
}
/** Complete transfer record */
export interface Transfer {
id: string;
direction: TransferDirection;
status: TransferStatus;
sourceChain: ChainId;
targetChain: ChainId;
asset: Asset;
amount: string;
sender: string;
recipient: string;
sourceTxHash?: string;
targetTxHash?: string;
fee: string;
feeAsset: Asset;
createdAt: number;
updatedAt: number;
completedAt?: number;
errorMessage?: string;
}
// ==================== Fee Types ====================
/** Fee estimate for bridge transfer */
export interface FeeEstimate {
bridgeFee: string;
gasFeeSource: string;
gasFeeTarget: string;
totalFee: string;
feeAsset: Asset;
estimatedTime: number; // seconds
exchangeRate?: string;
}
/** Exchange rate between assets */
export interface ExchangeRate {
fromAsset: Asset;
toAsset: Asset;
rate: string;
inverseRate: string;
lastUpdated: number;
source: string;
}
// ==================== Transaction Types ====================
/** Signed transaction result */
export interface SignedTransaction {
txHash: string;
chain: ChainId;
from: string;
to: string;
value: string;
data: string;
gasLimit: string;
gasPrice?: string;
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
nonce: number;
signature: string;
}
// ==================== Filter Types ====================
/** Filter for listing transfers */
export interface TransferFilter {
status?: TransferStatus;
sourceChain?: ChainId;
targetChain?: ChainId;
asset?: string;
sender?: string;
recipient?: string;
fromDate?: number;
toDate?: number;
limit?: number;
offset?: number;
}
// ==================== Config Types ====================
/** Bridge configuration */
export interface BridgeConfig {
apiKey: string;
endpoint?: string;
timeout?: number;
retries?: number;
debug?: boolean;
}
/** Default bridge endpoint */
export const DEFAULT_BRIDGE_ENDPOINT = 'https://bridge.synor.io/v1';
// ==================== Options Types ====================
/** Options for lock operation */
export interface LockOptions {
recipient?: string; // Defaults to sender
deadline?: number; // Unix timestamp
slippage?: number; // Percentage (0-100)
}
/** Options for mint operation */
export interface MintOptions {
gasLimit?: string;
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
}
/** Options for burn operation */
export interface BurnOptions {
recipient?: string;
deadline?: number;
}
/** Options for unlock operation */
export interface UnlockOptions {
gasLimit?: string;
gasPrice?: string;
}
// ==================== Error Types ====================
/** Bridge-specific error codes */
export type BridgeErrorCode =
| 'INSUFFICIENT_BALANCE'
| 'UNSUPPORTED_CHAIN'
| 'UNSUPPORTED_ASSET'
| 'TRANSFER_NOT_FOUND'
| 'PROOF_INVALID'
| 'PROOF_EXPIRED'
| 'CONFIRMATIONS_PENDING'
| 'BRIDGE_PAUSED'
| 'RATE_LIMITED'
| 'SLIPPAGE_EXCEEDED';
/** Bridge error */
export class BridgeError extends Error {
constructor(
message: string,
public code?: BridgeErrorCode,
public statusCode?: number,
public details?: Record<string, unknown>
) {
super(message);
this.name = 'BridgeError';
}
}

View file

@ -0,0 +1,659 @@
/**
* Synor Database SDK Client
* Multi-model database: Key-Value, Document, Vector, and Time Series
*/
import type {
DatabaseConfig,
KeyValueEntry,
SetOptions,
ListOptions,
ListResult,
Document,
CreateDocumentOptions,
UpdateDocumentOptions,
QueryOptions,
QueryResult,
AggregateStage,
VectorEntry,
UpsertVectorOptions,
SearchOptions,
SearchResult,
VectorCollectionConfig,
VectorCollectionStats,
DataPoint,
WritePointsOptions,
TimeRange,
TimeSeriesQueryOptions,
TimeSeriesResult,
SeriesInfo,
DatabaseStats,
} from './types';
import { DatabaseError } from './types';
const DEFAULT_CONFIG = {
endpoint: 'https://database.synor.io/v1',
network: 'mainnet' as const,
timeout: 30000,
retries: 3,
debug: false,
};
/**
* Key-Value store interface
*/
class KeyValueStore {
constructor(private client: SynorDatabase) {}
/**
* Get a value by key
*/
async get<T = unknown>(key: string): Promise<T | null> {
const response = await this.client.request<{ value: T | null }>(`/kv/${encodeURIComponent(key)}`);
return response.value;
}
/**
* Get a full entry with metadata
*/
async getEntry<T = unknown>(key: string): Promise<KeyValueEntry<T> | null> {
return this.client.request<KeyValueEntry<T> | null>(`/kv/${encodeURIComponent(key)}/entry`);
}
/**
* Set a value
*/
async set<T = unknown>(key: string, value: T, options?: SetOptions): Promise<void> {
await this.client.request(`/kv/${encodeURIComponent(key)}`, {
method: 'PUT',
body: { value, ...options },
});
}
/**
* Delete a key
*/
async delete(key: string): Promise<boolean> {
const response = await this.client.request<{ deleted: boolean }>(`/kv/${encodeURIComponent(key)}`, {
method: 'DELETE',
});
return response.deleted;
}
/**
* Check if a key exists
*/
async exists(key: string): Promise<boolean> {
const response = await this.client.request<{ exists: boolean }>(`/kv/${encodeURIComponent(key)}/exists`);
return response.exists;
}
/**
* List keys with optional prefix filtering
*/
async list<T = unknown>(options?: ListOptions): Promise<ListResult<T>> {
const params = new URLSearchParams();
if (options?.prefix) params.set('prefix', options.prefix);
if (options?.cursor) params.set('cursor', options.cursor);
if (options?.limit) params.set('limit', options.limit.toString());
return this.client.request<ListResult<T>>(`/kv?${params}`);
}
/**
* Get multiple values at once
*/
async mget<T = unknown>(keys: string[]): Promise<Map<string, T | null>> {
const response = await this.client.request<{ entries: { key: string; value: T | null }[] }>('/kv/mget', {
method: 'POST',
body: { keys },
});
return new Map(response.entries.map(e => [e.key, e.value]));
}
/**
* Set multiple values at once
*/
async mset<T = unknown>(entries: { key: string; value: T }[]): Promise<void> {
await this.client.request('/kv/mset', {
method: 'POST',
body: { entries },
});
}
/**
* Increment a numeric value
*/
async incr(key: string, by: number = 1): Promise<number> {
const response = await this.client.request<{ value: number }>(`/kv/${encodeURIComponent(key)}/incr`, {
method: 'POST',
body: { by },
});
return response.value;
}
}
/**
* Document store interface
*/
class DocumentStore {
constructor(private client: SynorDatabase) {}
/**
* Create a new document
*/
async create<T extends Record<string, unknown>>(
collection: string,
data: T,
options?: CreateDocumentOptions
): Promise<Document<T>> {
return this.client.request<Document<T>>(`/documents/${encodeURIComponent(collection)}`, {
method: 'POST',
body: { data, ...options },
});
}
/**
* Get a document by ID
*/
async get<T extends Record<string, unknown>>(
collection: string,
id: string
): Promise<Document<T> | null> {
return this.client.request<Document<T> | null>(
`/documents/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`
);
}
/**
* Update a document
*/
async update<T extends Record<string, unknown>>(
collection: string,
id: string,
update: Partial<T>,
options?: UpdateDocumentOptions
): Promise<Document<T>> {
return this.client.request<Document<T>>(
`/documents/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`,
{
method: 'PATCH',
body: { update, ...options },
}
);
}
/**
* Replace a document
*/
async replace<T extends Record<string, unknown>>(
collection: string,
id: string,
data: T
): Promise<Document<T>> {
return this.client.request<Document<T>>(
`/documents/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`,
{
method: 'PUT',
body: { data },
}
);
}
/**
* Delete a document
*/
async delete(collection: string, id: string): Promise<boolean> {
const response = await this.client.request<{ deleted: boolean }>(
`/documents/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`,
{ method: 'DELETE' }
);
return response.deleted;
}
/**
* Query documents
*/
async query<T extends Record<string, unknown>>(
collection: string,
options?: QueryOptions
): Promise<QueryResult<T>> {
return this.client.request<QueryResult<T>>(
`/documents/${encodeURIComponent(collection)}/query`,
{
method: 'POST',
body: options || {},
}
);
}
/**
* Count documents matching a filter
*/
async count(collection: string, filter?: QueryOptions['filter']): Promise<number> {
const response = await this.client.request<{ count: number }>(
`/documents/${encodeURIComponent(collection)}/count`,
{
method: 'POST',
body: { filter },
}
);
return response.count;
}
/**
* Run an aggregation pipeline
*/
async aggregate<T = unknown>(
collection: string,
pipeline: AggregateStage[]
): Promise<T[]> {
const response = await this.client.request<{ results: T[] }>(
`/documents/${encodeURIComponent(collection)}/aggregate`,
{
method: 'POST',
body: { pipeline },
}
);
return response.results;
}
/**
* Create an index on a collection
*/
async createIndex(
collection: string,
fields: string[],
options?: { unique?: boolean; sparse?: boolean }
): Promise<{ name: string }> {
return this.client.request<{ name: string }>(
`/documents/${encodeURIComponent(collection)}/indexes`,
{
method: 'POST',
body: { fields, ...options },
}
);
}
/**
* List indexes on a collection
*/
async listIndexes(collection: string): Promise<{ name: string; fields: string[]; unique: boolean }[]> {
const response = await this.client.request<{ indexes: { name: string; fields: string[]; unique: boolean }[] }>(
`/documents/${encodeURIComponent(collection)}/indexes`
);
return response.indexes;
}
}
/**
* Vector store interface
*/
class VectorStore {
constructor(private client: SynorDatabase) {}
/**
* Create a vector collection
*/
async createCollection(config: VectorCollectionConfig): Promise<void> {
await this.client.request('/vectors/collections', {
method: 'POST',
body: config,
});
}
/**
* Delete a vector collection
*/
async deleteCollection(name: string): Promise<void> {
await this.client.request(`/vectors/collections/${encodeURIComponent(name)}`, {
method: 'DELETE',
});
}
/**
* Get collection stats
*/
async getCollectionStats(name: string): Promise<VectorCollectionStats> {
return this.client.request<VectorCollectionStats>(
`/vectors/collections/${encodeURIComponent(name)}/stats`
);
}
/**
* Upsert vectors
*/
async upsert(
collection: string,
vectors: VectorEntry[],
options?: UpsertVectorOptions
): Promise<{ upserted: number }> {
return this.client.request<{ upserted: number }>(
`/vectors/${encodeURIComponent(collection)}/upsert`,
{
method: 'POST',
body: { vectors, ...options },
}
);
}
/**
* Search for similar vectors
*/
async search(
collection: string,
vector: number[],
options?: SearchOptions
): Promise<SearchResult[]> {
const response = await this.client.request<{ results: SearchResult[] }>(
`/vectors/${encodeURIComponent(collection)}/search`,
{
method: 'POST',
body: { vector, ...options },
}
);
return response.results;
}
/**
* Search using text (requires embedding generation)
*/
async searchText(
collection: string,
text: string,
options?: SearchOptions
): Promise<SearchResult[]> {
const response = await this.client.request<{ results: SearchResult[] }>(
`/vectors/${encodeURIComponent(collection)}/search/text`,
{
method: 'POST',
body: { text, ...options },
}
);
return response.results;
}
/**
* Delete vectors by ID
*/
async delete(
collection: string,
ids: string[],
namespace?: string
): Promise<{ deleted: number }> {
return this.client.request<{ deleted: number }>(
`/vectors/${encodeURIComponent(collection)}/delete`,
{
method: 'POST',
body: { ids, namespace },
}
);
}
/**
* Fetch vectors by ID
*/
async fetch(
collection: string,
ids: string[],
namespace?: string
): Promise<VectorEntry[]> {
const response = await this.client.request<{ vectors: VectorEntry[] }>(
`/vectors/${encodeURIComponent(collection)}/fetch`,
{
method: 'POST',
body: { ids, namespace },
}
);
return response.vectors;
}
}
/**
* Time series store interface
*/
class TimeSeriesStore {
constructor(private client: SynorDatabase) {}
/**
* Write data points to a series
*/
async write(
series: string,
points: DataPoint[],
options?: WritePointsOptions
): Promise<{ written: number }> {
return this.client.request<{ written: number }>(
`/timeseries/${encodeURIComponent(series)}/write`,
{
method: 'POST',
body: { points, ...options },
}
);
}
/**
* Query a time series
*/
async query(
series: string,
range: TimeRange,
options?: TimeSeriesQueryOptions
): Promise<TimeSeriesResult> {
return this.client.request<TimeSeriesResult>(
`/timeseries/${encodeURIComponent(series)}/query`,
{
method: 'POST',
body: { range, ...options },
}
);
}
/**
* Query multiple series
*/
async queryMulti(
series: string[],
range: TimeRange,
options?: TimeSeriesQueryOptions
): Promise<TimeSeriesResult[]> {
const response = await this.client.request<{ results: TimeSeriesResult[] }>(
'/timeseries/query',
{
method: 'POST',
body: { series, range, ...options },
}
);
return response.results;
}
/**
* Delete data points in a range
*/
async delete(
series: string,
range: TimeRange,
tags?: Record<string, string>
): Promise<{ deleted: number }> {
return this.client.request<{ deleted: number }>(
`/timeseries/${encodeURIComponent(series)}/delete`,
{
method: 'POST',
body: { range, tags },
}
);
}
/**
* Get series information
*/
async getSeriesInfo(series: string): Promise<SeriesInfo> {
return this.client.request<SeriesInfo>(`/timeseries/${encodeURIComponent(series)}/info`);
}
/**
* List all series
*/
async listSeries(prefix?: string): Promise<SeriesInfo[]> {
const params = prefix ? `?prefix=${encodeURIComponent(prefix)}` : '';
const response = await this.client.request<{ series: SeriesInfo[] }>(`/timeseries${params}`);
return response.series;
}
/**
* Set retention policy for a series
*/
async setRetention(series: string, retentionDays: number): Promise<void> {
await this.client.request(`/timeseries/${encodeURIComponent(series)}/retention`, {
method: 'PUT',
body: { retentionDays },
});
}
}
/**
* Synor Database SDK Client
*
* Multi-model database supporting Key-Value, Document, Vector, and Time Series data.
*
* @example
* ```typescript
* const db = new SynorDatabase({ apiKey: 'your-api-key' });
*
* // Key-Value operations
* await db.kv.set('user:1', { name: 'Alice' });
* const user = await db.kv.get('user:1');
*
* // Document operations
* const doc = await db.documents.create('users', { name: 'Bob', age: 30 });
* const results = await db.documents.query('users', { filter: { age: { $gt: 25 } } });
*
* // Vector operations
* await db.vectors.upsert('embeddings', [{ id: '1', vector: [0.1, 0.2, ...] }]);
* const similar = await db.vectors.search('embeddings', queryVector, { topK: 10 });
*
* // Time series operations
* await db.timeseries.write('metrics', [{ timestamp: Date.now(), value: 42.5 }]);
* const data = await db.timeseries.query('metrics', { start: '-1h', end: 'now' });
* ```
*/
export class SynorDatabase {
private config: Required<DatabaseConfig>;
private closed = false;
/** Key-Value store */
readonly kv: KeyValueStore;
/** Document store */
readonly documents: DocumentStore;
/** Vector store */
readonly vectors: VectorStore;
/** Time series store */
readonly timeseries: TimeSeriesStore;
constructor(config: DatabaseConfig) {
this.config = {
...DEFAULT_CONFIG,
...config,
};
if (!this.config.apiKey) {
throw new DatabaseError('API key is required');
}
this.kv = new KeyValueStore(this);
this.documents = new DocumentStore(this);
this.vectors = new VectorStore(this);
this.timeseries = new TimeSeriesStore(this);
}
/**
* Get database statistics
*/
async getStats(): Promise<DatabaseStats> {
return this.request<DatabaseStats>('/stats');
}
/**
* Health check
*/
async healthCheck(): Promise<boolean> {
try {
const response = await this.request<{ status: string }>('/health');
return response.status === 'healthy';
} catch {
return false;
}
}
/**
* Close the client
*/
close(): void {
this.closed = true;
}
/**
* Check if the client is closed
*/
isClosed(): boolean {
return this.closed;
}
/**
* Internal request method
*/
async request<T>(
path: string,
options: { method?: string; body?: unknown } = {}
): Promise<T> {
if (this.closed) {
throw new DatabaseError('Client has been closed');
}
const { method = 'GET', body } = options;
let lastError: Error | null = null;
for (let attempt = 0; attempt < this.config.retries; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
const response = await fetch(`${this.config.endpoint}${path}`, {
method,
headers: {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json',
'X-SDK-Version': 'js/0.1.0',
},
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new DatabaseError(
errorData.message || errorData.error || `HTTP ${response.status}`,
errorData.code,
response.status
);
}
return await response.json();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (this.config.debug) {
console.error(`Attempt ${attempt + 1} failed:`, lastError.message);
}
if (attempt < this.config.retries - 1) {
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000));
}
}
}
throw lastError || new DatabaseError('Unknown error after retries');
}
}

View file

@ -0,0 +1,7 @@
/**
* Synor Database SDK
* Multi-model database: Key-Value, Document, Vector, and Time Series
*/
export * from './types';
export * from './client';

View file

@ -0,0 +1,234 @@
/**
* Synor Database SDK Types
* Multi-model database: Key-Value, Document, Vector, and Time Series
*/
// Network environments
export type DatabaseNetwork = 'mainnet' | 'testnet' | 'devnet';
// Database configuration
export interface DatabaseConfig {
apiKey: string;
endpoint?: string;
network?: DatabaseNetwork;
timeout?: number;
retries?: number;
debug?: boolean;
}
// ==================== Key-Value Types ====================
export interface KeyValueEntry<T = unknown> {
key: string;
value: T;
metadata?: Record<string, string>;
expiresAt?: number;
createdAt: number;
updatedAt: number;
}
export interface SetOptions {
ttl?: number; // Time-to-live in seconds
metadata?: Record<string, string>;
ifNotExists?: boolean;
ifExists?: boolean;
}
export interface ListOptions {
prefix?: string;
cursor?: string;
limit?: number;
}
export interface ListResult<T = unknown> {
entries: KeyValueEntry<T>[];
cursor?: string;
hasMore: boolean;
}
// ==================== Document Store Types ====================
export interface Document<T = Record<string, unknown>> {
id: string;
data: T;
metadata?: Record<string, string>;
createdAt: number;
updatedAt: number;
version: number;
}
export interface CreateDocumentOptions {
id?: string; // Auto-generate if not provided
metadata?: Record<string, string>;
}
export interface UpdateDocumentOptions {
upsert?: boolean;
returnDocument?: 'before' | 'after';
}
export type QueryOperator =
| '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte'
| '$in' | '$nin' | '$exists' | '$regex'
| '$and' | '$or' | '$not';
export type QueryFilter = {
[field: string]: unknown | { [K in QueryOperator]?: unknown };
};
export type SortOrder = 'asc' | 'desc';
export interface QueryOptions {
filter?: QueryFilter;
sort?: { [field: string]: SortOrder };
skip?: number;
limit?: number;
projection?: string[];
}
export interface QueryResult<T = Record<string, unknown>> {
documents: Document<T>[];
total: number;
hasMore: boolean;
}
export interface AggregateStage {
$match?: QueryFilter;
$group?: { _id: string; [field: string]: { $sum?: string; $avg?: string; $count?: boolean; $min?: string; $max?: string } };
$sort?: { [field: string]: SortOrder };
$limit?: number;
$skip?: number;
$project?: { [field: string]: boolean | string };
}
// ==================== Vector Store Types ====================
export interface VectorEntry {
id: string;
vector: number[];
metadata?: Record<string, unknown>;
content?: string; // Optional text content for hybrid search
}
export interface UpsertVectorOptions {
namespace?: string;
}
export type DistanceMetric = 'cosine' | 'euclidean' | 'dotProduct';
export interface SearchOptions {
namespace?: string;
topK?: number;
filter?: QueryFilter;
includeMetadata?: boolean;
includeVectors?: boolean;
minScore?: number;
}
export interface SearchResult {
id: string;
score: number;
vector?: number[];
metadata?: Record<string, unknown>;
content?: string;
}
export interface VectorCollectionConfig {
name: string;
dimension: number;
metric?: DistanceMetric;
indexType?: 'flat' | 'hnsw' | 'ivf';
}
export interface VectorCollectionStats {
name: string;
vectorCount: number;
dimension: number;
indexSize: number;
}
// ==================== Time Series Types ====================
export interface DataPoint {
timestamp: number;
value: number;
tags?: Record<string, string>;
}
export interface WritePointsOptions {
precision?: 'ns' | 'us' | 'ms' | 's';
}
export type Aggregation =
| 'mean' | 'sum' | 'count' | 'min' | 'max'
| 'first' | 'last' | 'stddev' | 'variance';
export interface TimeRange {
start: number | Date | string;
end: number | Date | string;
}
export interface TimeSeriesQueryOptions {
tags?: Record<string, string>;
aggregation?: Aggregation;
interval?: string; // e.g., '1m', '5m', '1h', '1d'
fill?: 'none' | 'null' | 'previous' | number;
limit?: number;
}
export interface TimeSeriesResult {
series: string;
points: DataPoint[];
statistics?: {
min: number;
max: number;
mean: number;
count: number;
};
}
export interface SeriesInfo {
name: string;
tags: string[];
retentionDays: number;
pointCount: number;
}
// ==================== Database Stats ====================
export interface DatabaseStats {
kvEntries: number;
documentCount: number;
vectorCount: number;
timeseriesPoints: number;
storageUsed: number;
storageLimit: number;
}
// ==================== Error Types ====================
export class DatabaseError extends Error {
code?: string;
statusCode?: number;
constructor(message: string, code?: string, statusCode?: number) {
super(message);
this.name = 'DatabaseError';
this.code = code;
this.statusCode = statusCode;
}
}
export class DocumentNotFoundError extends DatabaseError {
constructor(collection: string, id: string) {
super(`Document not found: ${collection}/${id}`, 'DOCUMENT_NOT_FOUND', 404);
this.name = 'DocumentNotFoundError';
}
}
export class KeyNotFoundError extends DatabaseError {
constructor(key: string) {
super(`Key not found: ${key}`, 'KEY_NOT_FOUND', 404);
this.name = 'KeyNotFoundError';
}
}

View file

@ -0,0 +1,396 @@
/**
* Synor Hosting SDK Client
* Decentralized web hosting with domain management, DNS, and deployments.
*/
import type {
HostingConfig,
Domain,
DomainRecord,
RegisterDomainOptions,
DomainAvailability,
DnsRecord,
DnsZone,
Deployment,
DeployOptions,
DeploymentStats,
Certificate,
ProvisionSslOptions,
SiteConfig,
AnalyticsData,
AnalyticsOptions,
} from './types';
import { HostingError } from './types';
const DEFAULT_CONFIG = {
endpoint: 'https://hosting.synor.io/v1',
network: 'mainnet' as const,
timeout: 60000,
retries: 3,
debug: false,
};
/**
* Synor Hosting SDK Client
*
* Provides domain registration, DNS management, site deployment, and SSL provisioning.
*
* @example
* ```typescript
* const hosting = new SynorHosting({ apiKey: 'your-api-key' });
*
* // Register a domain
* const domain = await hosting.registerDomain('mysite.synor');
*
* // Deploy a site from IPFS CID
* const deployment = await hosting.deploy('Qm...', { domain: 'mysite.synor' });
*
* // Provision SSL
* const cert = await hosting.provisionSsl('mysite.synor');
* ```
*/
export class SynorHosting {
private config: Required<HostingConfig>;
private closed = false;
constructor(config: HostingConfig) {
this.config = {
...DEFAULT_CONFIG,
...config,
};
if (!this.config.apiKey) {
throw new HostingError('API key is required');
}
}
// ==================== Domain Operations ====================
/**
* Check domain availability
*/
async checkAvailability(name: string): Promise<DomainAvailability> {
return this.request<DomainAvailability>(`/domains/check/${encodeURIComponent(name)}`);
}
/**
* Register a new domain
*/
async registerDomain(name: string, options?: RegisterDomainOptions): Promise<Domain> {
return this.request<Domain>('/domains', {
method: 'POST',
body: { name, ...options },
});
}
/**
* Get domain information
*/
async getDomain(name: string): Promise<Domain> {
return this.request<Domain>(`/domains/${encodeURIComponent(name)}`);
}
/**
* List all domains
*/
async listDomains(): Promise<Domain[]> {
const response = await this.request<{ domains: Domain[] }>('/domains');
return response.domains;
}
/**
* Update domain record (for IPFS/IPNS resolution)
*/
async updateDomainRecord(name: string, record: DomainRecord): Promise<Domain> {
return this.request<Domain>(`/domains/${encodeURIComponent(name)}/record`, {
method: 'PUT',
body: record,
});
}
/**
* Resolve a domain to its record
*/
async resolveDomain(name: string): Promise<DomainRecord> {
return this.request<DomainRecord>(`/domains/${encodeURIComponent(name)}/resolve`);
}
/**
* Renew a domain
*/
async renewDomain(name: string, years: number = 1): Promise<Domain> {
return this.request<Domain>(`/domains/${encodeURIComponent(name)}/renew`, {
method: 'POST',
body: { years },
});
}
/**
* Transfer domain ownership
*/
async transferDomain(name: string, newOwner: string): Promise<Domain> {
return this.request<Domain>(`/domains/${encodeURIComponent(name)}/transfer`, {
method: 'POST',
body: { new_owner: newOwner },
});
}
// ==================== DNS Operations ====================
/**
* Get DNS zone for a domain
*/
async getDnsZone(domain: string): Promise<DnsZone> {
return this.request<DnsZone>(`/dns/${encodeURIComponent(domain)}`);
}
/**
* Set DNS records for a domain
*/
async setDnsRecords(domain: string, records: DnsRecord[]): Promise<DnsZone> {
return this.request<DnsZone>(`/dns/${encodeURIComponent(domain)}`, {
method: 'PUT',
body: { records },
});
}
/**
* Add a DNS record
*/
async addDnsRecord(domain: string, record: DnsRecord): Promise<DnsZone> {
return this.request<DnsZone>(`/dns/${encodeURIComponent(domain)}/records`, {
method: 'POST',
body: record,
});
}
/**
* Delete a DNS record
*/
async deleteDnsRecord(domain: string, recordType: string, name: string): Promise<DnsZone> {
return this.request<DnsZone>(
`/dns/${encodeURIComponent(domain)}/records/${recordType}/${encodeURIComponent(name)}`,
{ method: 'DELETE' }
);
}
// ==================== Deployment Operations ====================
/**
* Deploy a site from CID
*/
async deploy(cid: string, options?: DeployOptions): Promise<Deployment> {
return this.request<Deployment>('/deployments', {
method: 'POST',
body: { cid, ...options },
});
}
/**
* Get deployment by ID
*/
async getDeployment(id: string): Promise<Deployment> {
return this.request<Deployment>(`/deployments/${encodeURIComponent(id)}`);
}
/**
* List deployments
*/
async listDeployments(domain?: string): Promise<Deployment[]> {
const query = domain ? `?domain=${encodeURIComponent(domain)}` : '';
const response = await this.request<{ deployments: Deployment[] }>(`/deployments${query}`);
return response.deployments;
}
/**
* Rollback to a previous deployment
*/
async rollback(domain: string, deploymentId: string): Promise<Deployment> {
return this.request<Deployment>(`/deployments/${encodeURIComponent(deploymentId)}/rollback`, {
method: 'POST',
body: { domain },
});
}
/**
* Delete a deployment
*/
async deleteDeployment(id: string): Promise<void> {
await this.request(`/deployments/${encodeURIComponent(id)}`, { method: 'DELETE' });
}
/**
* Get deployment stats
*/
async getDeploymentStats(id: string, period: string = '24h'): Promise<DeploymentStats> {
return this.request<DeploymentStats>(
`/deployments/${encodeURIComponent(id)}/stats?period=${period}`
);
}
// ==================== SSL Operations ====================
/**
* Provision SSL certificate
*/
async provisionSsl(domain: string, options?: ProvisionSslOptions): Promise<Certificate> {
return this.request<Certificate>(`/ssl/${encodeURIComponent(domain)}`, {
method: 'POST',
body: options || {},
});
}
/**
* Get certificate status
*/
async getCertificate(domain: string): Promise<Certificate> {
return this.request<Certificate>(`/ssl/${encodeURIComponent(domain)}`);
}
/**
* Renew SSL certificate
*/
async renewCertificate(domain: string): Promise<Certificate> {
return this.request<Certificate>(`/ssl/${encodeURIComponent(domain)}/renew`, {
method: 'POST',
});
}
/**
* Delete/revoke SSL certificate
*/
async deleteCertificate(domain: string): Promise<void> {
await this.request(`/ssl/${encodeURIComponent(domain)}`, { method: 'DELETE' });
}
// ==================== Site Configuration ====================
/**
* Get site configuration
*/
async getSiteConfig(domain: string): Promise<SiteConfig> {
return this.request<SiteConfig>(`/sites/${encodeURIComponent(domain)}/config`);
}
/**
* Update site configuration
*/
async updateSiteConfig(domain: string, config: Partial<SiteConfig>): Promise<SiteConfig> {
return this.request<SiteConfig>(`/sites/${encodeURIComponent(domain)}/config`, {
method: 'PATCH',
body: config,
});
}
/**
* Purge CDN cache
*/
async purgeCache(domain: string, paths?: string[]): Promise<{ purged: number }> {
return this.request<{ purged: number }>(`/sites/${encodeURIComponent(domain)}/cache`, {
method: 'DELETE',
body: paths ? { paths } : undefined,
});
}
// ==================== Analytics ====================
/**
* Get site analytics
*/
async getAnalytics(domain: string, options?: AnalyticsOptions): Promise<AnalyticsData> {
const params = new URLSearchParams();
if (options?.period) params.set('period', options.period);
if (options?.startDate) params.set('start', options.startDate);
if (options?.endDate) params.set('end', options.endDate);
const query = params.toString() ? `?${params}` : '';
return this.request<AnalyticsData>(`/sites/${encodeURIComponent(domain)}/analytics${query}`);
}
// ==================== Lifecycle ====================
/**
* Close the client
*/
close(): void {
this.closed = true;
}
/**
* Check if the client is closed
*/
isClosed(): boolean {
return this.closed;
}
/**
* Health check
*/
async healthCheck(): Promise<boolean> {
try {
const response = await this.request<{ status: string }>('/health');
return response.status === 'healthy';
} catch {
return false;
}
}
/**
* Internal request method
*/
private async request<T>(
path: string,
options: { method?: string; body?: unknown } = {}
): Promise<T> {
if (this.closed) {
throw new HostingError('Client has been closed');
}
const { method = 'GET', body } = options;
let lastError: Error | null = null;
for (let attempt = 0; attempt < this.config.retries; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
const response = await fetch(`${this.config.endpoint}${path}`, {
method,
headers: {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json',
'X-SDK-Version': 'js/0.1.0',
},
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new HostingError(
errorData.message || errorData.error || `HTTP ${response.status}`,
errorData.code,
response.status
);
}
const text = await response.text();
return text ? JSON.parse(text) : ({} as T);
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (this.config.debug) {
console.error(`Attempt ${attempt + 1} failed:`, lastError.message);
}
if (attempt < this.config.retries - 1) {
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000));
}
}
}
throw lastError || new HostingError('Unknown error after retries');
}
}

View file

@ -0,0 +1,7 @@
/**
* Synor Hosting SDK
* Decentralized web hosting with domain management, DNS, and deployments.
*/
export * from './types';
export * from './client';

193
sdk/js/src/hosting/types.ts Normal file
View file

@ -0,0 +1,193 @@
/**
* Synor Hosting SDK Types
* Decentralized web hosting with domain management, DNS, and deployments.
*/
// Network environments
export type HostingNetwork = 'mainnet' | 'testnet' | 'devnet';
// Hosting configuration
export interface HostingConfig {
apiKey: string;
endpoint?: string;
network?: HostingNetwork;
timeout?: number;
retries?: number;
debug?: boolean;
}
// ==================== Domain Types ====================
export type DomainStatus = 'pending' | 'active' | 'expired' | 'suspended';
export interface Domain {
name: string;
status: DomainStatus;
owner: string;
registeredAt: number;
expiresAt: number;
autoRenew: boolean;
records?: DomainRecord;
}
export interface DomainRecord {
cid?: string;
ipv4?: string[];
ipv6?: string[];
cname?: string;
txt?: string[];
metadata?: Record<string, string>;
}
export interface RegisterDomainOptions {
years?: number;
autoRenew?: boolean;
records?: DomainRecord;
}
export interface DomainAvailability {
name: string;
available: boolean;
price?: number;
premium?: boolean;
}
// ==================== DNS Types ====================
export type DnsRecordType = 'A' | 'AAAA' | 'CNAME' | 'TXT' | 'MX' | 'NS' | 'SRV' | 'CAA';
export interface DnsRecord {
type: DnsRecordType;
name: string;
value: string;
ttl?: number;
priority?: number; // For MX and SRV
}
export interface DnsZone {
domain: string;
records: DnsRecord[];
updatedAt: number;
}
// ==================== Deployment Types ====================
export type DeploymentStatus = 'pending' | 'building' | 'deploying' | 'active' | 'failed' | 'inactive';
export interface Deployment {
id: string;
domain: string;
cid: string;
status: DeploymentStatus;
url: string;
createdAt: number;
updatedAt: number;
buildLogs?: string;
errorMessage?: string;
}
export interface DeployOptions {
domain?: string;
subdomain?: string;
headers?: Record<string, string>;
redirects?: RedirectRule[];
spa?: boolean; // Single-page app mode
cleanUrls?: boolean;
trailingSlash?: boolean;
}
export interface RedirectRule {
source: string;
destination: string;
statusCode?: number;
permanent?: boolean;
}
export interface DeploymentStats {
requests: number;
bandwidth: number;
uniqueVisitors: number;
period: string;
}
// ==================== SSL Types ====================
export type CertificateStatus = 'pending' | 'issued' | 'expired' | 'revoked';
export interface Certificate {
domain: string;
status: CertificateStatus;
issuedAt?: number;
expiresAt?: number;
autoRenew: boolean;
issuer: string;
fingerprint?: string;
}
export interface ProvisionSslOptions {
includeWww?: boolean;
autoRenew?: boolean;
}
// ==================== Site Configuration ====================
export interface SiteConfig {
domain: string;
cid?: string;
headers?: Record<string, string>;
redirects?: RedirectRule[];
errorPages?: {
'404'?: string;
'500'?: string;
};
spa?: boolean;
cleanUrls?: boolean;
trailingSlash?: boolean;
}
// ==================== Analytics ====================
export interface AnalyticsData {
domain: string;
period: string;
pageViews: number;
uniqueVisitors: number;
bandwidth: number;
topPages: { path: string; views: number }[];
topReferrers: { referrer: string; count: number }[];
topCountries: { country: string; count: number }[];
}
export interface AnalyticsOptions {
period?: '24h' | '7d' | '30d' | '90d';
startDate?: string;
endDate?: string;
}
// ==================== Error Types ====================
export class HostingError extends Error {
code?: string;
statusCode?: number;
constructor(message: string, code?: string, statusCode?: number) {
super(message);
this.name = 'HostingError';
this.code = code;
this.statusCode = statusCode;
}
}
export class DomainNotFoundError extends HostingError {
constructor(domain: string) {
super(`Domain not found: ${domain}`, 'DOMAIN_NOT_FOUND', 404);
this.name = 'DomainNotFoundError';
}
}
export class DomainUnavailableError extends HostingError {
constructor(domain: string) {
super(`Domain is not available: ${domain}`, 'DOMAIN_UNAVAILABLE', 409);
this.name = 'DomainUnavailableError';
}
}

View file

@ -21,11 +21,13 @@ dependencies {
implementation("io.ktor:ktor-client-core:2.3.7")
implementation("io.ktor:ktor-client-cio:2.3.7")
implementation("io.ktor:ktor-client-content-negotiation:2.3.7")
implementation("io.ktor:ktor-client-websockets:2.3.7")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.7")
// Testing
testImplementation(kotlin("test"))
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
testImplementation("io.ktor:ktor-client-mock:2.3.7")
testImplementation("io.mockk:mockk:1.13.8")
}
@ -42,9 +44,16 @@ publishing {
create<MavenPublication>("maven") {
from(components["java"])
pom {
name.set("Synor Compute SDK")
description.set("Kotlin SDK for Synor Compute - Distributed Heterogeneous Computing")
url.set("https://github.com/synor/synor-compute-kotlin")
name.set("Synor SDK")
description.set("Kotlin SDK for Synor - Compute, Wallet, RPC, and Storage")
url.set("https://synor.cc")
licenses {
license {
name.set("MIT License")
url.set("https://opensource.org/licenses/MIT")
}
}
}
}
}

View file

@ -0,0 +1 @@
rootProject.name = "synor-sdk"

View file

@ -0,0 +1,355 @@
package io.synor.rpc
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.websocket.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.websocket.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.serialization.json.Json
import java.io.Closeable
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
/**
* Synor RPC SDK client for Kotlin.
*
* Provides blockchain queries, transaction submission, and real-time
* subscriptions via WebSocket.
*
* Example:
* ```kotlin
* val rpc = SynorRpc(RpcConfig.builder("your-api-key").build())
*
* // Get latest block
* val block = rpc.getLatestBlock()
* println("Latest block: ${block.height}")
*
* // Subscribe to new blocks
* val scope = CoroutineScope(Dispatchers.Default)
* val subscription = rpc.subscribeBlocks(scope) { block ->
* println("New block: ${block.height}")
* }
*
* // Later: cancel subscription
* subscription.cancel()
* rpc.close()
* ```
*/
class SynorRpc(private val config: RpcConfig) : Closeable {
private val json = Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = true
}
private val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(json)
}
install(WebSockets)
install(HttpTimeout) {
requestTimeoutMillis = config.timeout
connectTimeoutMillis = config.timeout
}
defaultRequest {
header("Authorization", "Bearer ${config.apiKey}")
header("Content-Type", "application/json")
}
}
private var wsSession: WebSocketSession? = null
private val subscriptions = ConcurrentHashMap<String, (String) -> Unit>()
private var wsJob: Job? = null
// Block operations
/**
* Get the latest block.
*/
suspend fun getLatestBlock(): Block {
return get("/blocks/latest")
}
/**
* Get a block by hash or height.
*/
suspend fun getBlock(hashOrHeight: String): Block {
return get("/blocks/$hashOrHeight")
}
/**
* Get a block header by hash or height.
*/
suspend fun getBlockHeader(hashOrHeight: String): BlockHeader {
return get("/blocks/$hashOrHeight/header")
}
/**
* Get blocks in a range.
*/
suspend fun getBlocks(startHeight: Long, endHeight: Long): List<Block> {
return get("/blocks?start=$startHeight&end=$endHeight")
}
// Transaction operations
/**
* Get a transaction by ID.
*/
suspend fun getTransaction(txid: String): Transaction {
return get("/transactions/$txid")
}
/**
* Get raw transaction hex.
*/
suspend fun getRawTransaction(txid: String): String {
val response: Map<String, String> = get("/transactions/$txid/raw")
return response["hex"] ?: throw RpcException("Missing hex in response")
}
/**
* Send a raw transaction.
*/
suspend fun sendRawTransaction(hex: String): SubmitResult {
return post("/transactions/send", mapOf("hex" to hex))
}
/**
* Decode a raw transaction without broadcasting.
*/
suspend fun decodeRawTransaction(hex: String): Transaction {
return post("/transactions/decode", mapOf("hex" to hex))
}
/**
* Get transactions for an address.
*/
suspend fun getAddressTransactions(
address: String,
limit: Int = 50,
offset: Int = 0
): List<Transaction> {
return get("/addresses/$address/transactions?limit=$limit&offset=$offset")
}
// Fee estimation
/**
* Estimate fee for a given priority.
*/
suspend fun estimateFee(priority: Priority = Priority.MEDIUM): FeeEstimate {
return get("/fees/estimate?priority=${priority.name.lowercase()}")
}
/**
* Get all fee estimates.
*/
suspend fun getAllFeeEstimates(): Map<Priority, FeeEstimate> {
val estimates: List<FeeEstimate> = get("/fees/estimates")
return estimates.associateBy { it.priority }
}
// Chain information
/**
* Get chain information.
*/
suspend fun getChainInfo(): ChainInfo {
return get("/chain/info")
}
/**
* Get mempool information.
*/
suspend fun getMempoolInfo(): MempoolInfo {
return get("/mempool/info")
}
/**
* Get mempool transactions.
*/
suspend fun getMempoolTransactions(limit: Int = 100): List<String> {
return get("/mempool/transactions?limit=$limit")
}
// WebSocket subscriptions
/**
* Subscribe to new blocks.
*/
suspend fun subscribeBlocks(
scope: CoroutineScope,
callback: (Block) -> Unit
): Subscription {
return subscribe(scope, "blocks", null) { data ->
val block = json.decodeFromString<Block>(data)
callback(block)
}
}
/**
* Subscribe to transactions for a specific address.
*/
suspend fun subscribeAddress(
scope: CoroutineScope,
address: String,
callback: (Transaction) -> Unit
): Subscription {
return subscribe(scope, "address", address) { data ->
val tx = json.decodeFromString<Transaction>(data)
callback(tx)
}
}
/**
* Subscribe to mempool transactions.
*/
suspend fun subscribeMempool(
scope: CoroutineScope,
callback: (Transaction) -> Unit
): Subscription {
return subscribe(scope, "mempool", null) { data ->
val tx = json.decodeFromString<Transaction>(data)
callback(tx)
}
}
private suspend fun subscribe(
scope: CoroutineScope,
channel: String,
filter: String?,
callback: (String) -> Unit
): Subscription {
ensureWebSocketConnection(scope)
val subscriptionId = UUID.randomUUID().toString()
subscriptions[subscriptionId] = callback
val message = WsMessage(
type = "subscribe",
channel = channel,
filter = filter
)
wsSession?.send(Frame.Text(json.encodeToString(WsMessage.serializer(), message)))
return Subscription(
id = subscriptionId,
channel = channel,
onCancel = {
subscriptions.remove(subscriptionId)
scope.launch {
val unsubMsg = WsMessage(type = "unsubscribe", channel = channel)
wsSession?.send(Frame.Text(json.encodeToString(WsMessage.serializer(), unsubMsg)))
}
}
)
}
private suspend fun ensureWebSocketConnection(scope: CoroutineScope) {
if (wsSession != null && wsSession?.isActive == true) return
wsSession = client.webSocketSession("${config.wsEndpoint}?token=${config.apiKey}")
wsJob = scope.launch {
try {
wsSession?.incoming?.consumeAsFlow()?.collect { frame ->
when (frame) {
is Frame.Text -> {
val text = frame.readText()
try {
val message = json.decodeFromString<WsMessage>(text)
if (message.type == "data" && message.data != null) {
subscriptions.values.forEach { it(message.data) }
}
} catch (e: Exception) {
if (config.debug) {
println("Failed to parse WS message: ${e.message}")
}
}
}
else -> {}
}
}
} catch (e: Exception) {
if (config.debug) {
println("WebSocket error: ${e.message}")
}
}
}
}
private suspend inline fun <reified T> get(path: String): T {
return executeWithRetry {
val response = client.get("${config.endpoint}$path")
handleResponse(response)
}
}
private suspend inline fun <reified T, reified R> post(path: String, body: R): T {
return executeWithRetry {
val response = client.post("${config.endpoint}$path") {
setBody(body)
}
handleResponse(response)
}
}
private suspend inline fun <reified T> handleResponse(response: HttpResponse): T {
if (response.status.isSuccess()) {
return response.body()
}
val errorBody = try {
response.bodyAsText()
} catch (e: Exception) {
"Unknown error"
}
throw RpcException(
message = "RPC API error: $errorBody",
statusCode = response.status.value
)
}
private suspend inline fun <T> executeWithRetry(block: () -> T): T {
var lastException: Exception? = null
repeat(config.retries) { attempt ->
try {
return block()
} catch (e: Exception) {
lastException = e
if (config.debug) {
println("Attempt ${attempt + 1} failed: ${e.message}")
}
if (attempt < config.retries - 1) {
delay(1000L * (attempt + 1))
}
}
}
throw lastException ?: RpcException("Unknown error after ${config.retries} retries")
}
override fun close() {
wsJob?.cancel()
runBlocking {
wsSession?.close()
}
client.close()
}
}

View file

@ -0,0 +1,237 @@
package io.synor.rpc
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Network environment.
*/
@Serializable
enum class Network {
@SerialName("mainnet") MAINNET,
@SerialName("testnet") TESTNET,
@SerialName("devnet") DEVNET
}
/**
* Transaction priority for fee estimation.
*/
@Serializable
enum class Priority {
@SerialName("low") LOW,
@SerialName("medium") MEDIUM,
@SerialName("high") HIGH
}
/**
* Transaction status.
*/
@Serializable
enum class TransactionStatus {
@SerialName("pending") PENDING,
@SerialName("confirmed") CONFIRMED,
@SerialName("failed") FAILED
}
/**
* Configuration for the RPC client.
*/
data class RpcConfig(
val apiKey: String,
val endpoint: String = "https://rpc.synor.io/v1",
val wsEndpoint: String = "wss://rpc.synor.io/v1/ws",
val network: Network = Network.MAINNET,
val timeout: Long = 30_000L,
val retries: Int = 3,
val debug: Boolean = false
) {
class Builder(private val apiKey: String) {
private var endpoint: String = "https://rpc.synor.io/v1"
private var wsEndpoint: String = "wss://rpc.synor.io/v1/ws"
private var network: Network = Network.MAINNET
private var timeout: Long = 30_000L
private var retries: Int = 3
private var debug: Boolean = false
fun endpoint(endpoint: String) = apply { this.endpoint = endpoint }
fun wsEndpoint(wsEndpoint: String) = apply { this.wsEndpoint = wsEndpoint }
fun network(network: Network) = apply { this.network = network }
fun timeout(timeout: Long) = apply { this.timeout = timeout }
fun retries(retries: Int) = apply { this.retries = retries }
fun debug(debug: Boolean) = apply { this.debug = debug }
fun build() = RpcConfig(apiKey, endpoint, wsEndpoint, network, timeout, retries, debug)
}
companion object {
fun builder(apiKey: String) = Builder(apiKey)
}
}
/**
* Block header information.
*/
@Serializable
data class BlockHeader(
val hash: String,
val height: Long,
@SerialName("previous_hash") val previousHash: String,
val timestamp: Long,
val version: Int,
@SerialName("merkle_root") val merkleRoot: String,
val nonce: Long,
val difficulty: Double
)
/**
* A blockchain block.
*/
@Serializable
data class Block(
val hash: String,
val height: Long,
@SerialName("previous_hash") val previousHash: String,
val timestamp: Long,
val version: Int,
@SerialName("merkle_root") val merkleRoot: String,
val nonce: Long,
val difficulty: Double,
val transactions: List<Transaction>,
val size: Int,
val weight: Int
)
/**
* A blockchain transaction.
*/
@Serializable
data class Transaction(
val txid: String,
val hash: String,
val version: Int,
val size: Int,
val weight: Int,
@SerialName("lock_time") val lockTime: Long,
val inputs: List<TransactionInput>,
val outputs: List<TransactionOutput>,
val fee: Long,
val confirmations: Int,
@SerialName("block_hash") val blockHash: String? = null,
@SerialName("block_height") val blockHeight: Long? = null,
val timestamp: Long? = null,
val status: TransactionStatus = TransactionStatus.PENDING
)
/**
* Transaction input.
*/
@Serializable
data class TransactionInput(
val txid: String,
val vout: Int,
@SerialName("script_sig") val scriptSig: String? = null,
val sequence: Long,
val witness: List<String>? = null
)
/**
* Transaction output.
*/
@Serializable
data class TransactionOutput(
val value: Long,
val n: Int,
@SerialName("script_pubkey") val scriptPubKey: ScriptPubKey
)
/**
* Script public key.
*/
@Serializable
data class ScriptPubKey(
val asm: String,
val hex: String,
val type: String,
val address: String? = null
)
/**
* Chain information.
*/
@Serializable
data class ChainInfo(
val chain: String,
val blocks: Long,
val headers: Long,
@SerialName("best_block_hash") val bestBlockHash: String,
val difficulty: Double,
@SerialName("median_time") val medianTime: Long,
@SerialName("verification_progress") val verificationProgress: Double,
@SerialName("chain_work") val chainWork: String,
val pruned: Boolean
)
/**
* Mempool information.
*/
@Serializable
data class MempoolInfo(
val size: Int,
val bytes: Long,
val usage: Long,
@SerialName("max_mempool") val maxMempool: Long,
@SerialName("mempool_min_fee") val mempoolMinFee: Double,
@SerialName("min_relay_tx_fee") val minRelayTxFee: Double
)
/**
* Fee estimate for transactions.
*/
@Serializable
data class FeeEstimate(
val priority: Priority,
@SerialName("fee_rate") val feeRate: Long,
@SerialName("estimated_blocks") val estimatedBlocks: Int
)
/**
* Result of transaction submission.
*/
@Serializable
data class SubmitResult(
val txid: String,
val accepted: Boolean,
val reason: String? = null
)
/**
* Subscription handle for WebSocket events.
*/
data class Subscription(
val id: String,
val channel: String,
private val onCancel: () -> Unit
) {
fun cancel() = onCancel()
}
/**
* WebSocket message wrapper.
*/
@Serializable
internal data class WsMessage(
val type: String,
val channel: String,
val data: String? = null,
val filter: String? = null
)
/**
* Exception thrown by RPC operations.
*/
class RpcException(
message: String,
val code: String? = null,
val statusCode: Int? = null,
cause: Throwable? = null
) : Exception(message, cause)

View file

@ -0,0 +1,397 @@
package io.synor.storage
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.utils.io.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.serialization.json.Json
import java.io.Closeable
/**
* Synor Storage SDK client for Kotlin.
*
* Provides IPFS-compatible decentralized storage with upload, download,
* pinning, and CAR file operations.
*
* Example:
* ```kotlin
* val storage = SynorStorage(StorageConfig.builder("your-api-key").build())
*
* // Upload a file
* val data = "Hello, World!".toByteArray()
* val result = storage.upload(data, UploadOptions(name = "hello.txt"))
* println("CID: ${result.cid}")
*
* // Download content
* val content = storage.download(result.cid)
* println("Content: ${String(content)}")
*
* // Pin content
* val pin = storage.pin(PinRequest(cid = result.cid, durationDays = 30))
* println("Pin status: ${pin.status}")
*
* storage.close()
* ```
*/
class SynorStorage(private val config: StorageConfig) : Closeable {
private val json = Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = true
}
private val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(json)
}
install(HttpTimeout) {
requestTimeoutMillis = config.timeout
connectTimeoutMillis = config.timeout
}
defaultRequest {
header("Authorization", "Bearer ${config.apiKey}")
}
}
// Upload operations
/**
* Upload data to storage.
*
* @param data The data to upload
* @param options Upload options
* @return Upload response with CID
*/
suspend fun upload(data: ByteArray, options: UploadOptions = UploadOptions()): UploadResponse {
return executeWithRetry {
val response = client.submitFormWithBinaryData(
url = "${config.endpoint}/upload",
formData = formData {
append("file", data, Headers.build {
append(HttpHeaders.ContentType, ContentType.Application.OctetStream)
append(HttpHeaders.ContentDisposition, "filename=\"${options.name ?: "file"}\"")
})
options.name?.let { append("name", it) }
append("wrap_with_directory", options.wrapWithDirectory.toString())
append("hash_algorithm", options.hashAlgorithm.name.lowercase().replace("_", "-"))
options.chunkSize?.let { append("chunk_size", it.toString()) }
options.pinDurationDays?.let { append("pin_duration_days", it.toString()) }
}
)
handleResponse(response)
}
}
/**
* Upload multiple files as a directory.
*
* @param files List of file entries
* @param dirName Optional directory name
* @return Upload response with root CID
*/
suspend fun uploadDirectory(files: List<FileEntry>, dirName: String? = null): UploadResponse {
return executeWithRetry {
val response = client.submitFormWithBinaryData(
url = "${config.endpoint}/upload/directory",
formData = formData {
files.forEach { file ->
append("files", file.content, Headers.build {
append(HttpHeaders.ContentType, ContentType.Application.OctetStream)
append(HttpHeaders.ContentDisposition, "filename=\"${file.path}\"")
})
}
dirName?.let { append("name", it) }
}
)
handleResponse(response)
}
}
// Download operations
/**
* Download content by CID.
*
* @param cid The content identifier
* @return The content as bytes
*/
suspend fun download(cid: String): ByteArray {
return executeWithRetry {
val response = client.get("${config.endpoint}/content/$cid")
if (response.status.isSuccess()) {
response.body()
} else {
throw StorageException(
"Failed to download content: ${response.status}",
statusCode = response.status.value
)
}
}
}
/**
* Download content as a stream.
*
* @param cid The content identifier
* @return Flow of byte chunks
*/
fun downloadStream(cid: String): Flow<ByteArray> = flow {
val response = client.get("${config.endpoint}/content/$cid")
if (!response.status.isSuccess()) {
throw StorageException(
"Failed to download content: ${response.status}",
statusCode = response.status.value
)
}
val channel: ByteReadChannel = response.body()
val buffer = ByteArray(8192)
while (!channel.isClosedForRead) {
val bytesRead = channel.readAvailable(buffer)
if (bytesRead > 0) {
emit(buffer.copyOf(bytesRead))
}
}
}
/**
* Get the gateway URL for content.
*
* @param cid The content identifier
* @param path Optional path within directory
* @return Gateway URL
*/
fun getGatewayUrl(cid: String, path: String? = null): String {
return if (path != null) {
"${config.gateway}/ipfs/$cid/$path"
} else {
"${config.gateway}/ipfs/$cid"
}
}
// Pinning operations
/**
* Pin content to ensure availability.
*
* @param request Pin request
* @return Pin status
*/
suspend fun pin(request: PinRequest): Pin {
return post("/pins", request)
}
/**
* Unpin content.
*
* @param cid The content identifier
*/
suspend fun unpin(cid: String) {
delete("/pins/$cid")
}
/**
* Get pin status.
*
* @param cid The content identifier
* @return Pin information
*/
suspend fun getPinStatus(cid: String): Pin {
return get("/pins/$cid")
}
/**
* List all pins.
*
* @param status Optional status filter
* @param limit Maximum number of results
* @param offset Pagination offset
* @return List of pins
*/
suspend fun listPins(
status: PinStatus? = null,
limit: Int = 50,
offset: Int = 0
): List<Pin> {
val query = buildString {
append("?limit=$limit&offset=$offset")
status?.let { append("&status=${it.name.lowercase()}") }
}
return get("/pins$query")
}
// CAR file operations
/**
* Create a CAR file from entries.
*
* @param entries List of entries
* @return CAR file information
*/
suspend fun createCar(entries: List<CarEntry>): CarFile {
return executeWithRetry {
val response = client.submitFormWithBinaryData(
url = "${config.endpoint}/car",
formData = formData {
entries.forEach { entry ->
append("files", entry.content, Headers.build {
append(HttpHeaders.ContentType, ContentType.Application.OctetStream)
append(HttpHeaders.ContentDisposition, "filename=\"${entry.path}\"")
})
}
}
)
handleResponse(response)
}
}
/**
* Import a CAR file.
*
* @param carData CAR file bytes
* @return List of imported CIDs
*/
suspend fun importCar(carData: ByteArray): List<String> {
return executeWithRetry {
val response = client.submitFormWithBinaryData(
url = "${config.endpoint}/car/import",
formData = formData {
append("file", carData, Headers.build {
append(HttpHeaders.ContentType, ContentType.Application.OctetStream)
append(HttpHeaders.ContentDisposition, "filename=\"archive.car\"")
})
}
)
val result: Map<String, List<String>> = handleResponse(response)
result["cids"] ?: emptyList()
}
}
/**
* Export content as a CAR file.
*
* @param cid The content identifier
* @return CAR file bytes
*/
suspend fun exportCar(cid: String): ByteArray {
return executeWithRetry {
val response = client.get("${config.endpoint}/car/$cid")
if (response.status.isSuccess()) {
response.body()
} else {
throw StorageException(
"Failed to export CAR: ${response.status}",
statusCode = response.status.value
)
}
}
}
// Directory operations
/**
* List directory contents.
*
* @param cid The directory CID
* @return List of entries
*/
suspend fun listDirectory(cid: String): List<DirectoryEntry> {
return get("/content/$cid/ls")
}
// Statistics
/**
* Get storage statistics.
*
* @return Storage stats
*/
suspend fun getStats(): StorageStats {
return get("/stats")
}
private suspend inline fun <reified T> get(path: String): T {
return executeWithRetry {
val response = client.get("${config.endpoint}$path") {
header("Content-Type", "application/json")
}
handleResponse(response)
}
}
private suspend inline fun <reified T, reified R> post(path: String, body: R): T {
return executeWithRetry {
val response = client.post("${config.endpoint}$path") {
header("Content-Type", "application/json")
setBody(body)
}
handleResponse(response)
}
}
private suspend fun delete(path: String) {
executeWithRetry {
val response = client.delete("${config.endpoint}$path")
if (!response.status.isSuccess()) {
throw StorageException(
"Delete failed: ${response.status}",
statusCode = response.status.value
)
}
}
}
private suspend inline fun <reified T> handleResponse(response: HttpResponse): T {
if (response.status.isSuccess()) {
return response.body()
}
val errorBody = try {
response.bodyAsText()
} catch (e: Exception) {
"Unknown error"
}
throw StorageException(
message = "Storage API error: $errorBody",
statusCode = response.status.value
)
}
private suspend inline fun <T> executeWithRetry(block: () -> T): T {
var lastException: Exception? = null
repeat(config.retries) { attempt ->
try {
return block()
} catch (e: Exception) {
lastException = e
if (config.debug) {
println("Attempt ${attempt + 1} failed: ${e.message}")
}
if (attempt < config.retries - 1) {
delay(1000L * (attempt + 1))
}
}
}
throw lastException ?: StorageException("Unknown error after ${config.retries} retries")
}
override fun close() {
client.close()
}
}

View file

@ -0,0 +1,194 @@
package io.synor.storage
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Status of a pinned item.
*/
@Serializable
enum class PinStatus {
@SerialName("queued") QUEUED,
@SerialName("pinning") PINNING,
@SerialName("pinned") PINNED,
@SerialName("failed") FAILED,
@SerialName("unpinned") UNPINNED
}
/**
* Hash algorithm for content addressing.
*/
@Serializable
enum class HashAlgorithm {
@SerialName("sha2-256") SHA2_256,
@SerialName("blake3") BLAKE3
}
/**
* Type of directory entry.
*/
@Serializable
enum class EntryType {
@SerialName("file") FILE,
@SerialName("directory") DIRECTORY
}
/**
* Configuration for the storage client.
*/
data class StorageConfig(
val apiKey: String,
val endpoint: String = "https://storage.synor.io/v1",
val gateway: String = "https://gateway.synor.io",
val timeout: Long = 60_000L,
val retries: Int = 3,
val debug: Boolean = false
) {
class Builder(private val apiKey: String) {
private var endpoint: String = "https://storage.synor.io/v1"
private var gateway: String = "https://gateway.synor.io"
private var timeout: Long = 60_000L
private var retries: Int = 3
private var debug: Boolean = false
fun endpoint(endpoint: String) = apply { this.endpoint = endpoint }
fun gateway(gateway: String) = apply { this.gateway = gateway }
fun timeout(timeout: Long) = apply { this.timeout = timeout }
fun retries(retries: Int) = apply { this.retries = retries }
fun debug(debug: Boolean) = apply { this.debug = debug }
fun build() = StorageConfig(apiKey, endpoint, gateway, timeout, retries, debug)
}
companion object {
fun builder(apiKey: String) = Builder(apiKey)
}
}
/**
* Options for file upload.
*/
@Serializable
data class UploadOptions(
val name: String? = null,
@SerialName("wrap_with_directory") val wrapWithDirectory: Boolean = false,
@SerialName("hash_algorithm") val hashAlgorithm: HashAlgorithm = HashAlgorithm.SHA2_256,
@SerialName("chunk_size") val chunkSize: Int? = null,
@SerialName("pin_duration_days") val pinDurationDays: Int? = null
)
/**
* Response from upload operation.
*/
@Serializable
data class UploadResponse(
val cid: String,
val size: Long,
val name: String? = null,
@SerialName("created_at") val createdAt: String
)
/**
* Pin information.
*/
@Serializable
data class Pin(
val cid: String,
val name: String? = null,
val status: PinStatus,
val size: Long,
@SerialName("created_at") val createdAt: String,
@SerialName("expires_at") val expiresAt: String? = null,
val delegates: List<String>? = null
)
/**
* Request to pin content.
*/
@Serializable
data class PinRequest(
val cid: String,
val name: String? = null,
@SerialName("duration_days") val durationDays: Int? = null,
val origins: List<String>? = null,
val meta: Map<String, String>? = null
)
/**
* CAR (Content Addressable Archive) file.
*/
@Serializable
data class CarFile(
val cid: String,
val size: Long,
val roots: List<String>,
@SerialName("created_at") val createdAt: String
)
/**
* Entry in a CAR file for creation.
*/
data class CarEntry(
val path: String,
val content: ByteArray
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CarEntry) return false
return path == other.path && content.contentEquals(other.content)
}
override fun hashCode(): Int {
return 31 * path.hashCode() + content.contentHashCode()
}
}
/**
* Directory entry.
*/
@Serializable
data class DirectoryEntry(
val name: String,
val cid: String,
val size: Long,
val type: EntryType
)
/**
* File entry for directory creation.
*/
data class FileEntry(
val path: String,
val content: ByteArray
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is FileEntry) return false
return path == other.path && content.contentEquals(other.content)
}
override fun hashCode(): Int {
return 31 * path.hashCode() + content.contentHashCode()
}
}
/**
* Storage usage statistics.
*/
@Serializable
data class StorageStats(
@SerialName("total_size") val totalSize: Long,
@SerialName("pin_count") val pinCount: Int,
@SerialName("bandwidth_used") val bandwidthUsed: Long,
@SerialName("quota_used") val quotaUsed: Double
)
/**
* Exception thrown by storage operations.
*/
class StorageException(
message: String,
val code: String? = null,
val statusCode: Int? = null,
cause: Throwable? = null
) : Exception(message, cause)

View file

@ -0,0 +1,246 @@
package io.synor.wallet
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import java.io.Closeable
/**
* Synor Wallet SDK client for Kotlin.
*
* Provides key management, transaction signing, and balance queries
* for the Synor blockchain.
*
* Example:
* ```kotlin
* val wallet = SynorWallet(WalletConfig.builder("your-api-key").build())
*
* // Create a new wallet
* val result = wallet.createWallet(WalletType.STANDARD)
* println("Wallet ID: ${result.wallet.id}")
* println("Mnemonic: ${result.mnemonic}")
*
* // Get balance
* val balance = wallet.getBalance(result.wallet.addresses[0].address)
* println("Balance: ${balance.total}")
*
* wallet.close()
* ```
*/
class SynorWallet(private val config: WalletConfig) : Closeable {
private val json = Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = true
}
private val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(json)
}
install(HttpTimeout) {
requestTimeoutMillis = config.timeout
connectTimeoutMillis = config.timeout
}
defaultRequest {
header("Authorization", "Bearer ${config.apiKey}")
header("Content-Type", "application/json")
}
}
/**
* Create a new wallet.
*
* @param type The type of wallet to create
* @return Result containing the wallet and optionally the mnemonic
*/
suspend fun createWallet(type: WalletType = WalletType.STANDARD): CreateWalletResult {
val request = CreateWalletRequest(type, config.network)
return post("/wallets", request)
}
/**
* Import a wallet from a mnemonic phrase.
*
* @param mnemonic The 12 or 24 word mnemonic phrase
* @param passphrase Optional BIP39 passphrase
* @return The imported wallet
*/
suspend fun importWallet(mnemonic: String, passphrase: String? = null): Wallet {
val request = ImportWalletRequest(mnemonic, passphrase, config.network)
return post("/wallets/import", request)
}
/**
* Get a wallet by ID.
*
* @param walletId The wallet ID
* @return The wallet
*/
suspend fun getWallet(walletId: String): Wallet {
return get("/wallets/$walletId")
}
/**
* Generate a new address for a wallet.
*
* @param walletId The wallet ID
* @param isChange Whether this is a change address
* @return The new address
*/
suspend fun generateAddress(walletId: String, isChange: Boolean = false): Address {
return post("/wallets/$walletId/addresses", mapOf("is_change" to isChange))
}
/**
* Get a stealth address for privacy transactions.
*
* @param walletId The wallet ID
* @return The stealth address
*/
suspend fun getStealthAddress(walletId: String): StealthAddress {
return get("/wallets/$walletId/stealth-address")
}
/**
* Sign a transaction.
*
* @param walletId The wallet ID to sign with
* @param transaction The transaction to sign
* @return The signed transaction
*/
suspend fun signTransaction(walletId: String, transaction: Transaction): SignedTransaction {
val request = SignTransactionRequest(walletId, transaction)
return post("/transactions/sign", request)
}
/**
* Sign a message with a wallet address.
*
* @param walletId The wallet ID
* @param message The message to sign
* @param addressIndex The address index to use for signing
* @return The signature
*/
suspend fun signMessage(walletId: String, message: String, addressIndex: Int = 0): Signature {
val request = SignMessageRequest(walletId, message, addressIndex)
return post("/messages/sign", request)
}
/**
* Verify a message signature.
*
* @param message The original message
* @param signature The signature to verify
* @param address The address that signed the message
* @return True if the signature is valid
*/
suspend fun verifyMessage(message: String, signature: String, address: String): Boolean {
val request = mapOf(
"message" to message,
"signature" to signature,
"address" to address
)
val response: Map<String, Boolean> = post("/messages/verify", request)
return response["valid"] ?: false
}
/**
* Get the balance for an address.
*
* @param address The address to query
* @return The balance information
*/
suspend fun getBalance(address: String): Balance {
return get("/addresses/$address/balance")
}
/**
* Get UTXOs for an address.
*
* @param address The address to query
* @param minConfirmations Minimum confirmations required
* @return List of UTXOs
*/
suspend fun getUTXOs(address: String, minConfirmations: Int = 1): List<UTXO> {
return get("/addresses/$address/utxos?min_confirmations=$minConfirmations")
}
/**
* Estimate transaction fee.
*
* @param priority The transaction priority
* @return Estimated fee in satoshis per byte
*/
suspend fun estimateFee(priority: Priority = Priority.MEDIUM): Long {
val response: Map<String, Long> = get("/fees/estimate?priority=${priority.name.lowercase()}")
return response["fee_per_byte"] ?: 0L
}
private suspend inline fun <reified T> get(path: String): T {
return executeWithRetry {
val response = client.get("${config.endpoint}$path")
handleResponse(response)
}
}
private suspend inline fun <reified T, reified R> post(path: String, body: R): T {
return executeWithRetry {
val response = client.post("${config.endpoint}$path") {
setBody(body)
}
handleResponse(response)
}
}
private suspend inline fun <reified T> handleResponse(response: HttpResponse): T {
if (response.status.isSuccess()) {
return response.body()
}
val errorBody = try {
response.bodyAsText()
} catch (e: Exception) {
"Unknown error"
}
throw WalletException(
message = "Wallet API error: $errorBody",
statusCode = response.status.value
)
}
private suspend inline fun <T> executeWithRetry(block: () -> T): T {
var lastException: Exception? = null
repeat(config.retries) { attempt ->
try {
return block()
} catch (e: Exception) {
lastException = e
if (config.debug) {
println("Attempt ${attempt + 1} failed: ${e.message}")
}
if (attempt < config.retries - 1) {
kotlinx.coroutines.delay(1000L * (attempt + 1))
}
}
}
throw lastException ?: WalletException("Unknown error after ${config.retries} retries")
}
override fun close() {
client.close()
}
}

View file

@ -0,0 +1,230 @@
package io.synor.wallet
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Network environment for wallet operations.
*/
@Serializable
enum class Network {
@SerialName("mainnet") MAINNET,
@SerialName("testnet") TESTNET,
@SerialName("devnet") DEVNET
}
/**
* Type of wallet to create.
*/
@Serializable
enum class WalletType {
@SerialName("standard") STANDARD,
@SerialName("multisig") MULTISIG,
@SerialName("hardware") HARDWARE,
@SerialName("stealth") STEALTH
}
/**
* Transaction priority for fee estimation.
*/
@Serializable
enum class Priority {
@SerialName("low") LOW,
@SerialName("medium") MEDIUM,
@SerialName("high") HIGH
}
/**
* Configuration for the wallet client.
*/
data class WalletConfig(
val apiKey: String,
val endpoint: String = "https://wallet.synor.io/v1",
val network: Network = Network.MAINNET,
val timeout: Long = 30_000L,
val retries: Int = 3,
val debug: Boolean = false
) {
class Builder(private val apiKey: String) {
private var endpoint: String = "https://wallet.synor.io/v1"
private var network: Network = Network.MAINNET
private var timeout: Long = 30_000L
private var retries: Int = 3
private var debug: Boolean = false
fun endpoint(endpoint: String) = apply { this.endpoint = endpoint }
fun network(network: Network) = apply { this.network = network }
fun timeout(timeout: Long) = apply { this.timeout = timeout }
fun retries(retries: Int) = apply { this.retries = retries }
fun debug(debug: Boolean) = apply { this.debug = debug }
fun build() = WalletConfig(apiKey, endpoint, network, timeout, retries, debug)
}
companion object {
fun builder(apiKey: String) = Builder(apiKey)
}
}
/**
* A Synor wallet with addresses and keys.
*/
@Serializable
data class Wallet(
val id: String,
val type: WalletType,
val network: Network,
val addresses: List<Address>,
@SerialName("created_at") val createdAt: String
)
/**
* A wallet address.
*/
@Serializable
data class Address(
val address: String,
val index: Int,
@SerialName("is_change") val isChange: Boolean = false
)
/**
* Stealth address for privacy transactions.
*/
@Serializable
data class StealthAddress(
@SerialName("spend_public_key") val spendPublicKey: String,
@SerialName("view_public_key") val viewPublicKey: String,
@SerialName("one_time_address") val oneTimeAddress: String
)
/**
* Result of wallet creation.
*/
@Serializable
data class CreateWalletResult(
val wallet: Wallet,
val mnemonic: String? = null
)
/**
* Request to create a new wallet.
*/
@Serializable
internal data class CreateWalletRequest(
val type: WalletType,
val network: Network
)
/**
* Request to import a wallet.
*/
@Serializable
internal data class ImportWalletRequest(
val mnemonic: String,
val passphrase: String?,
val network: Network
)
/**
* A blockchain transaction.
*/
@Serializable
data class Transaction(
val inputs: List<TransactionInput>,
val outputs: List<TransactionOutput>,
val fee: Long? = null,
val priority: Priority = Priority.MEDIUM
)
/**
* Transaction input (UTXO reference).
*/
@Serializable
data class TransactionInput(
val txid: String,
val vout: Int,
val amount: Long
)
/**
* Transaction output.
*/
@Serializable
data class TransactionOutput(
val address: String,
val amount: Long
)
/**
* A signed transaction ready for broadcast.
*/
@Serializable
data class SignedTransaction(
val txid: String,
val hex: String,
val size: Int,
val fee: Long
)
/**
* Request to sign a transaction.
*/
@Serializable
internal data class SignTransactionRequest(
@SerialName("wallet_id") val walletId: String,
val transaction: Transaction
)
/**
* Request to sign a message.
*/
@Serializable
internal data class SignMessageRequest(
@SerialName("wallet_id") val walletId: String,
val message: String,
@SerialName("address_index") val addressIndex: Int
)
/**
* A cryptographic signature.
*/
@Serializable
data class Signature(
val signature: String,
val address: String,
@SerialName("recovery_id") val recoveryId: Int? = null
)
/**
* Unspent transaction output.
*/
@Serializable
data class UTXO(
val txid: String,
val vout: Int,
val amount: Long,
val address: String,
val confirmations: Int,
@SerialName("script_pubkey") val scriptPubKey: String? = null
)
/**
* Wallet balance information.
*/
@Serializable
data class Balance(
val confirmed: Long,
val unconfirmed: Long,
val total: Long
)
/**
* Exception thrown by wallet operations.
*/
class WalletException(
message: String,
val code: String? = null,
val statusCode: Int? = null,
cause: Throwable? = null
) : Exception(message, cause)

View file

@ -0,0 +1,74 @@
"""
Synor Bridge SDK for Python
Cross-chain asset transfers with lock-mint and burn-unlock patterns.
Example:
>>> from synor_bridge import SynorBridge
>>>
>>> bridge = SynorBridge(api_key="your-api-key")
>>>
>>> # Bridge tokens from Synor to Ethereum
>>> transfer = await bridge.bridge_to(
... asset="SYNR",
... amount="1000000000000000000",
... target_chain="ethereum",
... target_address="0x..."
... )
>>> print(f"Transfer {transfer.id}: {transfer.status}")
"""
from .types import (
BridgeConfig,
Chain,
ChainId,
Asset,
AssetType,
WrappedAsset,
Transfer,
TransferStatus,
TransferDirection,
TransferFilter,
LockReceipt,
LockProof,
LockOptions,
BurnReceipt,
BurnProof,
BurnOptions,
MintOptions,
UnlockOptions,
FeeEstimate,
ExchangeRate,
ValidatorSignature,
SignedTransaction,
BridgeError,
)
from .client import SynorBridge
__version__ = "0.1.0"
__all__ = [
"SynorBridge",
"BridgeConfig",
"Chain",
"ChainId",
"Asset",
"AssetType",
"WrappedAsset",
"Transfer",
"TransferStatus",
"TransferDirection",
"TransferFilter",
"LockReceipt",
"LockProof",
"LockOptions",
"BurnReceipt",
"BurnProof",
"BurnOptions",
"MintOptions",
"UnlockOptions",
"FeeEstimate",
"ExchangeRate",
"ValidatorSignature",
"SignedTransaction",
"BridgeError",
]

View file

@ -0,0 +1,527 @@
"""
Synor Bridge SDK Client
Cross-chain asset transfers with lock-mint and burn-unlock patterns.
"""
import asyncio
from typing import Optional, List, Dict, Any
from urllib.parse import urlencode
import httpx
from .types import (
BridgeConfig,
BridgeError,
Chain,
ChainId,
Asset,
WrappedAsset,
Transfer,
TransferStatus,
TransferFilter,
LockReceipt,
LockProof,
LockOptions,
BurnReceipt,
BurnProof,
BurnOptions,
MintOptions,
UnlockOptions,
FeeEstimate,
ExchangeRate,
SignedTransaction,
DEFAULT_BRIDGE_ENDPOINT,
)
class SynorBridge:
"""Synor Bridge Client for cross-chain transfers."""
def __init__(
self,
api_key: str,
endpoint: str = DEFAULT_BRIDGE_ENDPOINT,
timeout: float = 60.0,
retries: int = 3,
debug: bool = False,
):
self.config = BridgeConfig(
api_key=api_key,
endpoint=endpoint,
timeout=timeout,
retries=retries,
debug=debug,
)
self._client = httpx.AsyncClient(
base_url=endpoint,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"X-SDK-Version": "python/0.1.0",
},
timeout=timeout,
)
self._closed = False
# ==================== Chain Operations ====================
async def get_supported_chains(self) -> List[Chain]:
"""Get all supported chains."""
response = await self._request("GET", "/chains")
return [Chain.from_dict(c) for c in response.get("chains", [])]
async def get_chain(self, chain_id: ChainId) -> Chain:
"""Get chain by ID."""
response = await self._request("GET", f"/chains/{chain_id}")
return Chain.from_dict(response)
async def is_chain_supported(self, chain_id: ChainId) -> bool:
"""Check if chain is supported."""
try:
chain = await self.get_chain(chain_id)
return chain.supported
except BridgeError:
return False
# ==================== Asset Operations ====================
async def get_supported_assets(self, chain_id: ChainId) -> List[Asset]:
"""Get supported assets for a chain."""
response = await self._request("GET", f"/chains/{chain_id}/assets")
return [Asset.from_dict(a) for a in response.get("assets", [])]
async def get_asset(self, asset_id: str) -> Asset:
"""Get asset by ID."""
response = await self._request("GET", f"/assets/{asset_id}")
return Asset.from_dict(response)
async def get_wrapped_asset(
self, original_asset_id: str, target_chain: ChainId
) -> WrappedAsset:
"""Get wrapped asset mapping."""
response = await self._request(
"GET", f"/assets/{original_asset_id}/wrapped/{target_chain}"
)
return WrappedAsset.from_dict(response)
async def get_wrapped_assets(self, chain_id: ChainId) -> List[WrappedAsset]:
"""Get all wrapped assets for a chain."""
response = await self._request("GET", f"/chains/{chain_id}/wrapped")
return [WrappedAsset.from_dict(a) for a in response.get("assets", [])]
# ==================== Fee & Rate Operations ====================
async def estimate_fee(
self,
asset: str,
amount: str,
source_chain: ChainId,
target_chain: ChainId,
) -> FeeEstimate:
"""Estimate bridge fee."""
response = await self._request(
"POST",
"/fees/estimate",
{
"asset": asset,
"amount": amount,
"sourceChain": source_chain,
"targetChain": target_chain,
},
)
return FeeEstimate.from_dict(response)
async def get_exchange_rate(
self, from_asset: str, to_asset: str
) -> ExchangeRate:
"""Get exchange rate between assets."""
response = await self._request("GET", f"/rates/{from_asset}/{to_asset}")
return ExchangeRate.from_dict(response)
# ==================== Lock-Mint Flow ====================
async def lock(
self,
asset: str,
amount: str,
target_chain: ChainId,
options: Optional[LockOptions] = None,
) -> LockReceipt:
"""
Lock assets on source chain for cross-chain transfer.
Step 1 of lock-mint flow.
"""
body: Dict[str, Any] = {
"asset": asset,
"amount": amount,
"targetChain": target_chain,
}
if options:
if options.recipient:
body["recipient"] = options.recipient
if options.deadline:
body["deadline"] = options.deadline
if options.slippage is not None:
body["slippage"] = options.slippage
response = await self._request("POST", "/transfers/lock", body)
return LockReceipt.from_dict(response)
async def get_lock_proof(self, lock_receipt_id: str) -> LockProof:
"""
Get lock proof for minting.
Step 2 of lock-mint flow (wait for confirmations).
"""
response = await self._request(
"GET", f"/transfers/lock/{lock_receipt_id}/proof"
)
return LockProof.from_dict(response)
async def wait_for_lock_proof(
self,
lock_receipt_id: str,
poll_interval: float = 5.0,
max_wait: float = 600.0,
) -> LockProof:
"""
Wait for lock proof to be ready.
Polls until confirmations are sufficient.
"""
elapsed = 0.0
while elapsed < max_wait:
try:
return await self.get_lock_proof(lock_receipt_id)
except BridgeError as e:
if e.code == "CONFIRMATIONS_PENDING":
await asyncio.sleep(poll_interval)
elapsed += poll_interval
continue
raise
raise BridgeError(
"Timeout waiting for lock proof",
code="CONFIRMATIONS_PENDING",
)
async def mint(
self,
proof: LockProof,
target_address: str,
options: Optional[MintOptions] = None,
) -> SignedTransaction:
"""
Mint wrapped tokens on target chain.
Step 3 of lock-mint flow.
"""
body: Dict[str, Any] = {
"proof": proof.to_dict(),
"targetAddress": target_address,
}
if options:
if options.gas_limit:
body["gasLimit"] = options.gas_limit
if options.max_fee_per_gas:
body["maxFeePerGas"] = options.max_fee_per_gas
if options.max_priority_fee_per_gas:
body["maxPriorityFeePerGas"] = options.max_priority_fee_per_gas
response = await self._request("POST", "/transfers/mint", body)
return SignedTransaction.from_dict(response)
# ==================== Burn-Unlock Flow ====================
async def burn(
self,
wrapped_asset: str,
amount: str,
options: Optional[BurnOptions] = None,
) -> BurnReceipt:
"""
Burn wrapped tokens on current chain.
Step 1 of burn-unlock flow.
"""
body: Dict[str, Any] = {
"wrappedAsset": wrapped_asset,
"amount": amount,
}
if options:
if options.recipient:
body["recipient"] = options.recipient
if options.deadline:
body["deadline"] = options.deadline
response = await self._request("POST", "/transfers/burn", body)
return BurnReceipt.from_dict(response)
async def get_burn_proof(self, burn_receipt_id: str) -> BurnProof:
"""
Get burn proof for unlocking.
Step 2 of burn-unlock flow (wait for confirmations).
"""
response = await self._request(
"GET", f"/transfers/burn/{burn_receipt_id}/proof"
)
return BurnProof.from_dict(response)
async def wait_for_burn_proof(
self,
burn_receipt_id: str,
poll_interval: float = 5.0,
max_wait: float = 600.0,
) -> BurnProof:
"""
Wait for burn proof to be ready.
Polls until confirmations are sufficient.
"""
elapsed = 0.0
while elapsed < max_wait:
try:
return await self.get_burn_proof(burn_receipt_id)
except BridgeError as e:
if e.code == "CONFIRMATIONS_PENDING":
await asyncio.sleep(poll_interval)
elapsed += poll_interval
continue
raise
raise BridgeError(
"Timeout waiting for burn proof",
code="CONFIRMATIONS_PENDING",
)
async def unlock(
self,
proof: BurnProof,
options: Optional[UnlockOptions] = None,
) -> SignedTransaction:
"""
Unlock original tokens on source chain.
Step 3 of burn-unlock flow.
"""
body: Dict[str, Any] = {"proof": proof.to_dict()}
if options:
if options.gas_limit:
body["gasLimit"] = options.gas_limit
if options.gas_price:
body["gasPrice"] = options.gas_price
response = await self._request("POST", "/transfers/unlock", body)
return SignedTransaction.from_dict(response)
# ==================== Transfer Management ====================
async def get_transfer(self, transfer_id: str) -> Transfer:
"""Get transfer by ID."""
response = await self._request("GET", f"/transfers/{transfer_id}")
return Transfer.from_dict(response)
async def get_transfer_status(self, transfer_id: str) -> TransferStatus:
"""Get transfer status."""
transfer = await self.get_transfer(transfer_id)
return transfer.status
async def list_transfers(
self, filter: Optional[TransferFilter] = None
) -> List[Transfer]:
"""List transfers with optional filters."""
params: Dict[str, str] = {}
if filter:
if filter.status:
params["status"] = filter.status.value
if filter.source_chain:
params["sourceChain"] = filter.source_chain
if filter.target_chain:
params["targetChain"] = filter.target_chain
if filter.asset:
params["asset"] = filter.asset
if filter.sender:
params["sender"] = filter.sender
if filter.recipient:
params["recipient"] = filter.recipient
if filter.from_date is not None:
params["fromDate"] = str(filter.from_date)
if filter.to_date is not None:
params["toDate"] = str(filter.to_date)
if filter.limit is not None:
params["limit"] = str(filter.limit)
if filter.offset is not None:
params["offset"] = str(filter.offset)
path = "/transfers"
if params:
path = f"/transfers?{urlencode(params)}"
response = await self._request("GET", path)
return [Transfer.from_dict(t) for t in response.get("transfers", [])]
async def wait_for_transfer(
self,
transfer_id: str,
poll_interval: float = 10.0,
max_wait: float = 1800.0,
) -> Transfer:
"""Wait for transfer to complete."""
final_statuses = {
TransferStatus.COMPLETED,
TransferStatus.FAILED,
TransferStatus.REFUNDED,
}
elapsed = 0.0
while elapsed < max_wait:
transfer = await self.get_transfer(transfer_id)
if transfer.status in final_statuses:
return transfer
await asyncio.sleep(poll_interval)
elapsed += poll_interval
raise BridgeError("Timeout waiting for transfer completion")
# ==================== Convenience Methods ====================
async def bridge_to(
self,
asset: str,
amount: str,
target_chain: ChainId,
target_address: str,
lock_options: Optional[LockOptions] = None,
mint_options: Optional[MintOptions] = None,
) -> Transfer:
"""
Execute complete lock-mint transfer.
Combines lock, wait for proof, and mint into single operation.
"""
# Lock on source chain
lock_receipt = await self.lock(asset, amount, target_chain, lock_options)
if self.config.debug:
print(f"Locked: {lock_receipt.id}, waiting for confirmations...")
# Wait for proof
proof = await self.wait_for_lock_proof(lock_receipt.id)
if self.config.debug:
print(f"Proof ready, minting on {target_chain}...")
# Mint on target chain
await self.mint(proof, target_address, mint_options)
# Return final transfer status
return await self.wait_for_transfer(lock_receipt.id)
async def bridge_back(
self,
wrapped_asset: str,
amount: str,
burn_options: Optional[BurnOptions] = None,
unlock_options: Optional[UnlockOptions] = None,
) -> Transfer:
"""
Execute complete burn-unlock transfer.
Combines burn, wait for proof, and unlock into single operation.
"""
# Burn wrapped tokens
burn_receipt = await self.burn(wrapped_asset, amount, burn_options)
if self.config.debug:
print(f"Burned: {burn_receipt.id}, waiting for confirmations...")
# Wait for proof
proof = await self.wait_for_burn_proof(burn_receipt.id)
if self.config.debug:
print(f"Proof ready, unlocking on {burn_receipt.target_chain}...")
# Unlock on original chain
await self.unlock(proof, unlock_options)
# Return final transfer status
return await self.wait_for_transfer(burn_receipt.id)
# ==================== Lifecycle ====================
async def close(self) -> None:
"""Close the client."""
self._closed = True
await self._client.aclose()
def is_closed(self) -> bool:
"""Check if client is closed."""
return self._closed
async def health_check(self) -> bool:
"""Health check."""
try:
response = await self._request("GET", "/health")
return response.get("status") == "healthy"
except BridgeError:
return False
async def __aenter__(self) -> "SynorBridge":
return self
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
await self.close()
# ==================== Private Methods ====================
async def _request(
self,
method: str,
path: str,
body: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Make HTTP request with retries."""
if self._closed:
raise BridgeError("Client has been closed")
last_error: Optional[Exception] = None
for attempt in range(self.config.retries):
try:
return await self._do_request(method, path, body)
except BridgeError as e:
if self.config.debug:
print(f"Attempt {attempt + 1} failed: {e}")
last_error = e
if attempt < self.config.retries - 1:
await asyncio.sleep(2**attempt)
if last_error:
raise last_error
raise BridgeError("Unknown error after retries")
async def _do_request(
self,
method: str,
path: str,
body: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Execute HTTP request."""
try:
response = await self._client.request(
method,
path,
json=body if body else None,
)
if response.status_code >= 400:
error_body = response.json() if response.content else {}
message = (
error_body.get("message")
or error_body.get("error")
or f"HTTP {response.status_code}"
)
raise BridgeError(
message,
code=error_body.get("code"),
status_code=response.status_code,
details=error_body,
)
return response.json()
except httpx.TimeoutException as e:
raise BridgeError(f"Request timeout: {e}")
except httpx.RequestError as e:
raise BridgeError(f"Request failed: {e}")

View file

@ -0,0 +1,537 @@
"""
Synor Bridge SDK Types
Cross-chain asset transfers with lock-mint and burn-unlock patterns.
"""
from dataclasses import dataclass, field
from typing import Optional, List, Dict, Any, Literal
from enum import Enum
# ==================== Chain Types ====================
ChainId = Literal[
"synor",
"ethereum",
"polygon",
"arbitrum",
"optimism",
"bsc",
"avalanche",
"solana",
"cosmos",
]
@dataclass
class NativeCurrency:
"""Native currency info for a chain."""
name: str
symbol: str
decimals: int
@dataclass
class Chain:
"""Blockchain network information."""
id: ChainId
name: str
chain_id: int
rpc_url: str
explorer_url: str
native_currency: NativeCurrency
confirmations: int
estimated_block_time: int # seconds
supported: bool
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Chain":
native = data.get("nativeCurrency", data.get("native_currency", {}))
return cls(
id=data["id"],
name=data["name"],
chain_id=data.get("chainId", data.get("chain_id", 0)),
rpc_url=data.get("rpcUrl", data.get("rpc_url", "")),
explorer_url=data.get("explorerUrl", data.get("explorer_url", "")),
native_currency=NativeCurrency(
name=native.get("name", ""),
symbol=native.get("symbol", ""),
decimals=native.get("decimals", 18),
),
confirmations=data.get("confirmations", 0),
estimated_block_time=data.get("estimatedBlockTime", data.get("estimated_block_time", 0)),
supported=data.get("supported", True),
)
# ==================== Asset Types ====================
class AssetType(str, Enum):
"""Asset type enumeration."""
NATIVE = "native"
ERC20 = "erc20"
ERC721 = "erc721"
ERC1155 = "erc1155"
@dataclass
class Asset:
"""Asset information."""
id: str
symbol: str
name: str
type: AssetType
chain: ChainId
decimals: int
contract_address: Optional[str] = None
logo_url: Optional[str] = None
verified: bool = False
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Asset":
return cls(
id=data["id"],
symbol=data["symbol"],
name=data["name"],
type=AssetType(data["type"]),
chain=data["chain"],
decimals=data.get("decimals", 18),
contract_address=data.get("contractAddress", data.get("contract_address")),
logo_url=data.get("logoUrl", data.get("logo_url")),
verified=data.get("verified", False),
)
@dataclass
class WrappedAsset:
"""Wrapped asset mapping."""
original_asset: Asset
wrapped_asset: Asset
chain: ChainId
bridge_contract: str
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "WrappedAsset":
return cls(
original_asset=Asset.from_dict(data.get("originalAsset", data.get("original_asset", {}))),
wrapped_asset=Asset.from_dict(data.get("wrappedAsset", data.get("wrapped_asset", {}))),
chain=data["chain"],
bridge_contract=data.get("bridgeContract", data.get("bridge_contract", "")),
)
# ==================== Transfer Types ====================
class TransferStatus(str, Enum):
"""Transfer status enumeration."""
PENDING = "pending"
LOCKED = "locked"
CONFIRMING = "confirming"
MINTING = "minting"
COMPLETED = "completed"
FAILED = "failed"
REFUNDED = "refunded"
class TransferDirection(str, Enum):
"""Transfer direction enumeration."""
LOCK_MINT = "lock_mint"
BURN_UNLOCK = "burn_unlock"
@dataclass
class ValidatorSignature:
"""Validator signature for proof."""
validator: str
signature: str
timestamp: int
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "ValidatorSignature":
return cls(
validator=data["validator"],
signature=data["signature"],
timestamp=data["timestamp"],
)
@dataclass
class LockReceipt:
"""Lock receipt from source chain."""
id: str
tx_hash: str
source_chain: ChainId
target_chain: ChainId
asset: Asset
amount: str
sender: str
recipient: str
lock_timestamp: int
confirmations: int
required_confirmations: int
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "LockReceipt":
return cls(
id=data["id"],
tx_hash=data.get("txHash", data.get("tx_hash", "")),
source_chain=data.get("sourceChain", data.get("source_chain", "")),
target_chain=data.get("targetChain", data.get("target_chain", "")),
asset=Asset.from_dict(data["asset"]),
amount=data["amount"],
sender=data["sender"],
recipient=data["recipient"],
lock_timestamp=data.get("lockTimestamp", data.get("lock_timestamp", 0)),
confirmations=data.get("confirmations", 0),
required_confirmations=data.get("requiredConfirmations", data.get("required_confirmations", 0)),
)
@dataclass
class LockProof:
"""Proof for minting on target chain."""
lock_receipt: LockReceipt
merkle_proof: List[str]
block_header: str
signatures: List[ValidatorSignature]
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "LockProof":
return cls(
lock_receipt=LockReceipt.from_dict(data.get("lockReceipt", data.get("lock_receipt", {}))),
merkle_proof=data.get("merkleProof", data.get("merkle_proof", [])),
block_header=data.get("blockHeader", data.get("block_header", "")),
signatures=[ValidatorSignature.from_dict(s) for s in data.get("signatures", [])],
)
def to_dict(self) -> Dict[str, Any]:
return {
"lockReceipt": {
"id": self.lock_receipt.id,
"txHash": self.lock_receipt.tx_hash,
"sourceChain": self.lock_receipt.source_chain,
"targetChain": self.lock_receipt.target_chain,
"asset": {
"id": self.lock_receipt.asset.id,
"symbol": self.lock_receipt.asset.symbol,
"name": self.lock_receipt.asset.name,
"type": self.lock_receipt.asset.type.value,
"chain": self.lock_receipt.asset.chain,
"decimals": self.lock_receipt.asset.decimals,
},
"amount": self.lock_receipt.amount,
"sender": self.lock_receipt.sender,
"recipient": self.lock_receipt.recipient,
"lockTimestamp": self.lock_receipt.lock_timestamp,
"confirmations": self.lock_receipt.confirmations,
"requiredConfirmations": self.lock_receipt.required_confirmations,
},
"merkleProof": self.merkle_proof,
"blockHeader": self.block_header,
"signatures": [
{"validator": s.validator, "signature": s.signature, "timestamp": s.timestamp}
for s in self.signatures
],
}
@dataclass
class BurnReceipt:
"""Burn receipt for unlocking."""
id: str
tx_hash: str
source_chain: ChainId
target_chain: ChainId
wrapped_asset: Asset
original_asset: Asset
amount: str
sender: str
recipient: str
burn_timestamp: int
confirmations: int
required_confirmations: int
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "BurnReceipt":
return cls(
id=data["id"],
tx_hash=data.get("txHash", data.get("tx_hash", "")),
source_chain=data.get("sourceChain", data.get("source_chain", "")),
target_chain=data.get("targetChain", data.get("target_chain", "")),
wrapped_asset=Asset.from_dict(data.get("wrappedAsset", data.get("wrapped_asset", {}))),
original_asset=Asset.from_dict(data.get("originalAsset", data.get("original_asset", {}))),
amount=data["amount"],
sender=data["sender"],
recipient=data["recipient"],
burn_timestamp=data.get("burnTimestamp", data.get("burn_timestamp", 0)),
confirmations=data.get("confirmations", 0),
required_confirmations=data.get("requiredConfirmations", data.get("required_confirmations", 0)),
)
@dataclass
class BurnProof:
"""Proof for unlocking on original chain."""
burn_receipt: BurnReceipt
merkle_proof: List[str]
block_header: str
signatures: List[ValidatorSignature]
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "BurnProof":
return cls(
burn_receipt=BurnReceipt.from_dict(data.get("burnReceipt", data.get("burn_receipt", {}))),
merkle_proof=data.get("merkleProof", data.get("merkle_proof", [])),
block_header=data.get("blockHeader", data.get("block_header", "")),
signatures=[ValidatorSignature.from_dict(s) for s in data.get("signatures", [])],
)
def to_dict(self) -> Dict[str, Any]:
return {
"burnReceipt": {
"id": self.burn_receipt.id,
"txHash": self.burn_receipt.tx_hash,
"sourceChain": self.burn_receipt.source_chain,
"targetChain": self.burn_receipt.target_chain,
"wrappedAsset": {
"id": self.burn_receipt.wrapped_asset.id,
"symbol": self.burn_receipt.wrapped_asset.symbol,
"name": self.burn_receipt.wrapped_asset.name,
"type": self.burn_receipt.wrapped_asset.type.value,
"chain": self.burn_receipt.wrapped_asset.chain,
"decimals": self.burn_receipt.wrapped_asset.decimals,
},
"originalAsset": {
"id": self.burn_receipt.original_asset.id,
"symbol": self.burn_receipt.original_asset.symbol,
"name": self.burn_receipt.original_asset.name,
"type": self.burn_receipt.original_asset.type.value,
"chain": self.burn_receipt.original_asset.chain,
"decimals": self.burn_receipt.original_asset.decimals,
},
"amount": self.burn_receipt.amount,
"sender": self.burn_receipt.sender,
"recipient": self.burn_receipt.recipient,
"burnTimestamp": self.burn_receipt.burn_timestamp,
"confirmations": self.burn_receipt.confirmations,
"requiredConfirmations": self.burn_receipt.required_confirmations,
},
"merkleProof": self.merkle_proof,
"blockHeader": self.block_header,
"signatures": [
{"validator": s.validator, "signature": s.signature, "timestamp": s.timestamp}
for s in self.signatures
],
}
@dataclass
class Transfer:
"""Complete transfer record."""
id: str
direction: TransferDirection
status: TransferStatus
source_chain: ChainId
target_chain: ChainId
asset: Asset
amount: str
sender: str
recipient: str
fee: str
fee_asset: Asset
created_at: int
updated_at: int
source_tx_hash: Optional[str] = None
target_tx_hash: Optional[str] = None
completed_at: Optional[int] = None
error_message: Optional[str] = None
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Transfer":
return cls(
id=data["id"],
direction=TransferDirection(data["direction"]),
status=TransferStatus(data["status"]),
source_chain=data.get("sourceChain", data.get("source_chain", "")),
target_chain=data.get("targetChain", data.get("target_chain", "")),
asset=Asset.from_dict(data["asset"]),
amount=data["amount"],
sender=data["sender"],
recipient=data["recipient"],
fee=data["fee"],
fee_asset=Asset.from_dict(data.get("feeAsset", data.get("fee_asset", {}))),
created_at=data.get("createdAt", data.get("created_at", 0)),
updated_at=data.get("updatedAt", data.get("updated_at", 0)),
source_tx_hash=data.get("sourceTxHash", data.get("source_tx_hash")),
target_tx_hash=data.get("targetTxHash", data.get("target_tx_hash")),
completed_at=data.get("completedAt", data.get("completed_at")),
error_message=data.get("errorMessage", data.get("error_message")),
)
# ==================== Fee Types ====================
@dataclass
class FeeEstimate:
"""Fee estimate for bridge transfer."""
bridge_fee: str
gas_fee_source: str
gas_fee_target: str
total_fee: str
fee_asset: Asset
estimated_time: int # seconds
exchange_rate: Optional[str] = None
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "FeeEstimate":
return cls(
bridge_fee=data.get("bridgeFee", data.get("bridge_fee", "0")),
gas_fee_source=data.get("gasFeeSource", data.get("gas_fee_source", "0")),
gas_fee_target=data.get("gasFeeTarget", data.get("gas_fee_target", "0")),
total_fee=data.get("totalFee", data.get("total_fee", "0")),
fee_asset=Asset.from_dict(data.get("feeAsset", data.get("fee_asset", {}))),
estimated_time=data.get("estimatedTime", data.get("estimated_time", 0)),
exchange_rate=data.get("exchangeRate", data.get("exchange_rate")),
)
@dataclass
class ExchangeRate:
"""Exchange rate between assets."""
from_asset: Asset
to_asset: Asset
rate: str
inverse_rate: str
last_updated: int
source: str
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "ExchangeRate":
return cls(
from_asset=Asset.from_dict(data.get("fromAsset", data.get("from_asset", {}))),
to_asset=Asset.from_dict(data.get("toAsset", data.get("to_asset", {}))),
rate=data["rate"],
inverse_rate=data.get("inverseRate", data.get("inverse_rate", "")),
last_updated=data.get("lastUpdated", data.get("last_updated", 0)),
source=data.get("source", ""),
)
# ==================== Transaction Types ====================
@dataclass
class SignedTransaction:
"""Signed transaction result."""
tx_hash: str
chain: ChainId
from_address: str
to_address: str
value: str
data: str
gas_limit: str
nonce: int
signature: str
gas_price: Optional[str] = None
max_fee_per_gas: Optional[str] = None
max_priority_fee_per_gas: Optional[str] = None
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "SignedTransaction":
return cls(
tx_hash=data.get("txHash", data.get("tx_hash", "")),
chain=data["chain"],
from_address=data.get("from", data.get("from_address", "")),
to_address=data.get("to", data.get("to_address", "")),
value=data["value"],
data=data["data"],
gas_limit=data.get("gasLimit", data.get("gas_limit", "")),
nonce=data["nonce"],
signature=data["signature"],
gas_price=data.get("gasPrice", data.get("gas_price")),
max_fee_per_gas=data.get("maxFeePerGas", data.get("max_fee_per_gas")),
max_priority_fee_per_gas=data.get("maxPriorityFeePerGas", data.get("max_priority_fee_per_gas")),
)
# ==================== Filter Types ====================
@dataclass
class TransferFilter:
"""Filter for listing transfers."""
status: Optional[TransferStatus] = None
source_chain: Optional[ChainId] = None
target_chain: Optional[ChainId] = None
asset: Optional[str] = None
sender: Optional[str] = None
recipient: Optional[str] = None
from_date: Optional[int] = None
to_date: Optional[int] = None
limit: Optional[int] = None
offset: Optional[int] = None
# ==================== Options Types ====================
@dataclass
class LockOptions:
"""Options for lock operation."""
recipient: Optional[str] = None
deadline: Optional[int] = None
slippage: Optional[float] = None
@dataclass
class MintOptions:
"""Options for mint operation."""
gas_limit: Optional[str] = None
max_fee_per_gas: Optional[str] = None
max_priority_fee_per_gas: Optional[str] = None
@dataclass
class BurnOptions:
"""Options for burn operation."""
recipient: Optional[str] = None
deadline: Optional[int] = None
@dataclass
class UnlockOptions:
"""Options for unlock operation."""
gas_limit: Optional[str] = None
gas_price: Optional[str] = None
# ==================== Config Types ====================
DEFAULT_BRIDGE_ENDPOINT = "https://bridge.synor.io/v1"
@dataclass
class BridgeConfig:
"""Bridge configuration."""
api_key: str
endpoint: str = DEFAULT_BRIDGE_ENDPOINT
timeout: float = 60.0
retries: int = 3
debug: bool = False
# ==================== Error Types ====================
class BridgeError(Exception):
"""Bridge-specific error."""
def __init__(
self,
message: str,
code: Optional[str] = None,
status_code: Optional[int] = None,
details: Optional[Dict[str, Any]] = None,
):
super().__init__(message)
self.message = message
self.code = code
self.status_code = status_code
self.details = details or {}

View file

@ -0,0 +1,71 @@
"""
Synor Database SDK for Python
Multi-model database supporting Key-Value, Document, Vector, and Time Series data.
Example:
>>> from synor_database import SynorDatabase
>>>
>>> db = SynorDatabase(api_key="your-api-key")
>>>
>>> # Key-Value operations
>>> await db.kv.set("user:1", {"name": "Alice"})
>>> user = await db.kv.get("user:1")
>>>
>>> # Document operations
>>> doc = await db.documents.create("users", {"name": "Bob", "age": 30})
>>> results = await db.documents.query("users", filter={"age": {"$gt": 25}})
>>>
>>> # Vector operations
>>> await db.vectors.upsert("embeddings", [{"id": "1", "vector": [0.1, 0.2, ...]}])
>>> similar = await db.vectors.search("embeddings", query_vector, top_k=10)
>>>
>>> # Time series operations
>>> await db.timeseries.write("metrics", [{"timestamp": time.time(), "value": 42.5}])
>>> data = await db.timeseries.query("metrics", start="-1h", end="now")
"""
from .types import (
DatabaseConfig,
KeyValueEntry,
SetOptions,
ListOptions,
ListResult,
Document,
QueryOptions,
QueryResult,
VectorEntry,
SearchOptions,
SearchResult,
VectorCollectionConfig,
DataPoint,
TimeRange,
TimeSeriesQueryOptions,
TimeSeriesResult,
DatabaseStats,
DatabaseError,
)
from .client import SynorDatabase
__version__ = "0.1.0"
__all__ = [
"SynorDatabase",
"DatabaseConfig",
"KeyValueEntry",
"SetOptions",
"ListOptions",
"ListResult",
"Document",
"QueryOptions",
"QueryResult",
"VectorEntry",
"SearchOptions",
"SearchResult",
"VectorCollectionConfig",
"DataPoint",
"TimeRange",
"TimeSeriesQueryOptions",
"TimeSeriesResult",
"DatabaseStats",
"DatabaseError",
]

View file

@ -0,0 +1,633 @@
"""
Synor Database SDK Client
"""
from typing import Any, Dict, List, Optional, Union
from datetime import datetime
from urllib.parse import urlencode
import httpx
from .types import (
DatabaseConfig,
Network,
KeyValueEntry,
SetOptions,
ListOptions,
ListResult,
Document,
QueryOptions,
QueryResult,
VectorEntry,
SearchOptions,
SearchResult,
VectorCollectionConfig,
VectorCollectionStats,
DataPoint,
TimeRange,
TimeSeriesQueryOptions,
TimeSeriesResult,
SeriesInfo,
DatabaseStats,
DatabaseError,
Aggregation,
SortOrder,
)
class KeyValueStore:
"""Key-Value store operations."""
def __init__(self, client: "SynorDatabase"):
self._client = client
async def get(self, key: str) -> Optional[Any]:
"""Get a value by key."""
response = await self._client._request("GET", f"/kv/{key}")
return response.get("value")
async def get_entry(self, key: str) -> Optional[KeyValueEntry]:
"""Get a full entry with metadata."""
response = await self._client._request("GET", f"/kv/{key}/entry")
if response is None:
return None
return KeyValueEntry(
key=response["key"],
value=response["value"],
created_at=response["created_at"],
updated_at=response["updated_at"],
metadata=response.get("metadata"),
expires_at=response.get("expires_at"),
)
async def set(
self,
key: str,
value: Any,
ttl: Optional[int] = None,
metadata: Optional[Dict[str, str]] = None,
if_not_exists: bool = False,
if_exists: bool = False,
) -> None:
"""Set a value."""
body = {"value": value}
if ttl is not None:
body["ttl"] = ttl
if metadata is not None:
body["metadata"] = metadata
if if_not_exists:
body["if_not_exists"] = True
if if_exists:
body["if_exists"] = True
await self._client._request("PUT", f"/kv/{key}", body=body)
async def delete(self, key: str) -> bool:
"""Delete a key."""
response = await self._client._request("DELETE", f"/kv/{key}")
return response.get("deleted", False)
async def exists(self, key: str) -> bool:
"""Check if a key exists."""
response = await self._client._request("GET", f"/kv/{key}/exists")
return response.get("exists", False)
async def list(
self,
prefix: Optional[str] = None,
cursor: Optional[str] = None,
limit: int = 100,
) -> ListResult:
"""List keys with optional prefix filtering."""
params = {"limit": str(limit)}
if prefix:
params["prefix"] = prefix
if cursor:
params["cursor"] = cursor
response = await self._client._request("GET", f"/kv?{urlencode(params)}")
return ListResult(
entries=[
KeyValueEntry(
key=e["key"],
value=e["value"],
created_at=e["created_at"],
updated_at=e["updated_at"],
metadata=e.get("metadata"),
expires_at=e.get("expires_at"),
)
for e in response.get("entries", [])
],
cursor=response.get("cursor"),
has_more=response.get("has_more", False),
)
async def mget(self, keys: List[str]) -> Dict[str, Optional[Any]]:
"""Get multiple values at once."""
response = await self._client._request("POST", "/kv/mget", body={"keys": keys})
return {e["key"]: e["value"] for e in response.get("entries", [])}
async def mset(self, entries: Dict[str, Any]) -> None:
"""Set multiple values at once."""
body = {"entries": [{"key": k, "value": v} for k, v in entries.items()]}
await self._client._request("POST", "/kv/mset", body=body)
async def incr(self, key: str, by: int = 1) -> int:
"""Increment a numeric value."""
response = await self._client._request("POST", f"/kv/{key}/incr", body={"by": by})
return response["value"]
class DocumentStore:
"""Document store operations."""
def __init__(self, client: "SynorDatabase"):
self._client = client
async def create(
self,
collection: str,
data: Dict[str, Any],
id: Optional[str] = None,
metadata: Optional[Dict[str, str]] = None,
) -> Document:
"""Create a new document."""
body: Dict[str, Any] = {"data": data}
if id:
body["id"] = id
if metadata:
body["metadata"] = metadata
response = await self._client._request("POST", f"/documents/{collection}", body=body)
return self._parse_document(response)
async def get(self, collection: str, id: str) -> Optional[Document]:
"""Get a document by ID."""
try:
response = await self._client._request("GET", f"/documents/{collection}/{id}")
return self._parse_document(response) if response else None
except DatabaseError as e:
if e.status_code == 404:
return None
raise
async def update(
self,
collection: str,
id: str,
update: Dict[str, Any],
upsert: bool = False,
) -> Document:
"""Update a document."""
body = {"update": update, "upsert": upsert}
response = await self._client._request("PATCH", f"/documents/{collection}/{id}", body=body)
return self._parse_document(response)
async def replace(self, collection: str, id: str, data: Dict[str, Any]) -> Document:
"""Replace a document."""
response = await self._client._request("PUT", f"/documents/{collection}/{id}", body={"data": data})
return self._parse_document(response)
async def delete(self, collection: str, id: str) -> bool:
"""Delete a document."""
response = await self._client._request("DELETE", f"/documents/{collection}/{id}")
return response.get("deleted", False)
async def query(
self,
collection: str,
filter: Optional[Dict[str, Any]] = None,
sort: Optional[Dict[str, str]] = None,
skip: int = 0,
limit: int = 100,
projection: Optional[List[str]] = None,
) -> QueryResult:
"""Query documents."""
body: Dict[str, Any] = {"skip": skip, "limit": limit}
if filter:
body["filter"] = filter
if sort:
body["sort"] = sort
if projection:
body["projection"] = projection
response = await self._client._request("POST", f"/documents/{collection}/query", body=body)
return QueryResult(
documents=[self._parse_document(d) for d in response.get("documents", [])],
total=response.get("total", 0),
has_more=response.get("has_more", False),
)
async def count(self, collection: str, filter: Optional[Dict[str, Any]] = None) -> int:
"""Count documents matching a filter."""
body = {"filter": filter} if filter else {}
response = await self._client._request("POST", f"/documents/{collection}/count", body=body)
return response.get("count", 0)
async def aggregate(
self,
collection: str,
pipeline: List[Dict[str, Any]],
) -> List[Any]:
"""Run an aggregation pipeline."""
response = await self._client._request(
"POST", f"/documents/{collection}/aggregate", body={"pipeline": pipeline}
)
return response.get("results", [])
def _parse_document(self, data: Dict[str, Any]) -> Document:
return Document(
id=data["id"],
data=data["data"],
created_at=data["created_at"],
updated_at=data["updated_at"],
version=data["version"],
metadata=data.get("metadata"),
)
class VectorStore:
"""Vector store operations."""
def __init__(self, client: "SynorDatabase"):
self._client = client
async def create_collection(
self,
name: str,
dimension: int,
metric: str = "cosine",
index_type: str = "hnsw",
) -> None:
"""Create a vector collection."""
await self._client._request(
"POST",
"/vectors/collections",
body={"name": name, "dimension": dimension, "metric": metric, "index_type": index_type},
)
async def delete_collection(self, name: str) -> None:
"""Delete a vector collection."""
await self._client._request("DELETE", f"/vectors/collections/{name}")
async def get_collection_stats(self, name: str) -> VectorCollectionStats:
"""Get collection statistics."""
response = await self._client._request("GET", f"/vectors/collections/{name}/stats")
return VectorCollectionStats(
name=response["name"],
vector_count=response["vector_count"],
dimension=response["dimension"],
index_size=response["index_size"],
)
async def upsert(
self,
collection: str,
vectors: List[Dict[str, Any]],
namespace: Optional[str] = None,
) -> int:
"""Upsert vectors."""
body: Dict[str, Any] = {"vectors": vectors}
if namespace:
body["namespace"] = namespace
response = await self._client._request("POST", f"/vectors/{collection}/upsert", body=body)
return response.get("upserted", 0)
async def search(
self,
collection: str,
vector: List[float],
top_k: int = 10,
namespace: Optional[str] = None,
filter: Optional[Dict[str, Any]] = None,
include_metadata: bool = True,
include_vectors: bool = False,
min_score: Optional[float] = None,
) -> List[SearchResult]:
"""Search for similar vectors."""
body: Dict[str, Any] = {
"vector": vector,
"top_k": top_k,
"include_metadata": include_metadata,
"include_vectors": include_vectors,
}
if namespace:
body["namespace"] = namespace
if filter:
body["filter"] = filter
if min_score is not None:
body["min_score"] = min_score
response = await self._client._request("POST", f"/vectors/{collection}/search", body=body)
return [
SearchResult(
id=r["id"],
score=r["score"],
vector=r.get("vector"),
metadata=r.get("metadata"),
content=r.get("content"),
)
for r in response.get("results", [])
]
async def search_text(
self,
collection: str,
text: str,
top_k: int = 10,
**kwargs,
) -> List[SearchResult]:
"""Search using text (requires embedding generation)."""
body = {"text": text, "top_k": top_k, **kwargs}
response = await self._client._request("POST", f"/vectors/{collection}/search/text", body=body)
return [
SearchResult(
id=r["id"],
score=r["score"],
vector=r.get("vector"),
metadata=r.get("metadata"),
content=r.get("content"),
)
for r in response.get("results", [])
]
async def delete(
self,
collection: str,
ids: List[str],
namespace: Optional[str] = None,
) -> int:
"""Delete vectors by ID."""
body: Dict[str, Any] = {"ids": ids}
if namespace:
body["namespace"] = namespace
response = await self._client._request("POST", f"/vectors/{collection}/delete", body=body)
return response.get("deleted", 0)
async def fetch(
self,
collection: str,
ids: List[str],
namespace: Optional[str] = None,
) -> List[VectorEntry]:
"""Fetch vectors by ID."""
body: Dict[str, Any] = {"ids": ids}
if namespace:
body["namespace"] = namespace
response = await self._client._request("POST", f"/vectors/{collection}/fetch", body=body)
return [
VectorEntry(
id=v["id"],
vector=v["vector"],
metadata=v.get("metadata"),
content=v.get("content"),
)
for v in response.get("vectors", [])
]
class TimeSeriesStore:
"""Time series store operations."""
def __init__(self, client: "SynorDatabase"):
self._client = client
async def write(
self,
series: str,
points: List[Dict[str, Any]],
precision: str = "ms",
) -> int:
"""Write data points to a series."""
body = {"points": points, "precision": precision}
response = await self._client._request("POST", f"/timeseries/{series}/write", body=body)
return response.get("written", 0)
async def query(
self,
series: str,
start: Union[int, datetime, str],
end: Union[int, datetime, str],
tags: Optional[Dict[str, str]] = None,
aggregation: Optional[str] = None,
interval: Optional[str] = None,
fill: Optional[Union[str, float]] = None,
limit: Optional[int] = None,
) -> TimeSeriesResult:
"""Query a time series."""
body: Dict[str, Any] = {
"range": {
"start": self._format_time(start),
"end": self._format_time(end),
}
}
if tags:
body["tags"] = tags
if aggregation:
body["aggregation"] = aggregation
if interval:
body["interval"] = interval
if fill is not None:
body["fill"] = fill
if limit:
body["limit"] = limit
response = await self._client._request("POST", f"/timeseries/{series}/query", body=body)
return TimeSeriesResult(
series=response["series"],
points=[
DataPoint(
timestamp=p["timestamp"],
value=p["value"],
tags=p.get("tags"),
)
for p in response.get("points", [])
],
statistics=response.get("statistics"),
)
async def delete(
self,
series: str,
start: Union[int, datetime, str],
end: Union[int, datetime, str],
tags: Optional[Dict[str, str]] = None,
) -> int:
"""Delete data points in a range."""
body: Dict[str, Any] = {
"range": {
"start": self._format_time(start),
"end": self._format_time(end),
}
}
if tags:
body["tags"] = tags
response = await self._client._request("POST", f"/timeseries/{series}/delete", body=body)
return response.get("deleted", 0)
async def get_series_info(self, series: str) -> SeriesInfo:
"""Get series information."""
response = await self._client._request("GET", f"/timeseries/{series}/info")
return SeriesInfo(
name=response["name"],
tags=response.get("tags", []),
retention_days=response.get("retention_days", 0),
point_count=response.get("point_count", 0),
)
async def list_series(self, prefix: Optional[str] = None) -> List[SeriesInfo]:
"""List all series."""
params = f"?prefix={prefix}" if prefix else ""
response = await self._client._request("GET", f"/timeseries{params}")
return [
SeriesInfo(
name=s["name"],
tags=s.get("tags", []),
retention_days=s.get("retention_days", 0),
point_count=s.get("point_count", 0),
)
for s in response.get("series", [])
]
async def set_retention(self, series: str, retention_days: int) -> None:
"""Set retention policy for a series."""
await self._client._request(
"PUT", f"/timeseries/{series}/retention", body={"retention_days": retention_days}
)
def _format_time(self, t: Union[int, datetime, str]) -> Union[int, str]:
if isinstance(t, datetime):
return int(t.timestamp() * 1000)
return t
class SynorDatabase:
"""
Synor Database SDK Client.
Multi-model database supporting Key-Value, Document, Vector, and Time Series data.
"""
def __init__(
self,
api_key: str,
endpoint: str = "https://database.synor.io/v1",
network: Network = Network.MAINNET,
timeout: float = 30.0,
retries: int = 3,
debug: bool = False,
):
self._config = DatabaseConfig(
api_key=api_key,
endpoint=endpoint,
network=network,
timeout=timeout,
retries=retries,
debug=debug,
)
self._client = httpx.AsyncClient(
base_url=endpoint,
timeout=timeout,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"X-SDK-Version": "python/0.1.0",
},
)
self._closed = False
# Initialize stores
self.kv = KeyValueStore(self)
self.documents = DocumentStore(self)
self.vectors = VectorStore(self)
self.timeseries = TimeSeriesStore(self)
async def get_stats(self) -> DatabaseStats:
"""Get database statistics."""
response = await self._request("GET", "/stats")
return DatabaseStats(
kv_entries=response.get("kv_entries", 0),
document_count=response.get("document_count", 0),
vector_count=response.get("vector_count", 0),
timeseries_points=response.get("timeseries_points", 0),
storage_used=response.get("storage_used", 0),
storage_limit=response.get("storage_limit", 0),
)
async def health_check(self) -> bool:
"""Health check."""
try:
response = await self._request("GET", "/health")
return response.get("status") == "healthy"
except Exception:
return False
async def close(self) -> None:
"""Close the client."""
if not self._closed:
await self._client.aclose()
self._closed = True
@property
def is_closed(self) -> bool:
"""Check if the client is closed."""
return self._closed
async def __aenter__(self) -> "SynorDatabase":
return self
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
await self.close()
async def _request(
self,
method: str,
path: str,
body: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Make an HTTP request with retry logic."""
if self._closed:
raise DatabaseError("Client has been closed")
last_error: Optional[Exception] = None
for attempt in range(self._config.retries):
try:
if method == "GET":
response = await self._client.get(path)
elif method == "POST":
response = await self._client.post(path, json=body)
elif method == "PUT":
response = await self._client.put(path, json=body)
elif method == "PATCH":
response = await self._client.patch(path, json=body)
elif method == "DELETE":
response = await self._client.delete(path)
else:
raise ValueError(f"Unknown method: {method}")
if response.status_code >= 400:
error_data = response.json() if response.content else {}
raise DatabaseError(
error_data.get("message") or error_data.get("error") or f"HTTP {response.status_code}",
error_data.get("code"),
response.status_code,
)
return response.json() if response.content else {}
except Exception as e:
last_error = e
if self._config.debug:
print(f"Attempt {attempt + 1} failed: {e}")
if attempt < self._config.retries - 1:
import asyncio
await asyncio.sleep(attempt + 1)
if last_error:
raise last_error
raise DatabaseError("Unknown error after retries")

View file

@ -0,0 +1,253 @@
"""
Synor Database SDK Types
"""
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional, Union
from datetime import datetime
class Network(str, Enum):
"""Network environment."""
MAINNET = "mainnet"
TESTNET = "testnet"
DEVNET = "devnet"
class DistanceMetric(str, Enum):
"""Vector distance metric."""
COSINE = "cosine"
EUCLIDEAN = "euclidean"
DOT_PRODUCT = "dotProduct"
class Aggregation(str, Enum):
"""Time series aggregation function."""
MEAN = "mean"
SUM = "sum"
COUNT = "count"
MIN = "min"
MAX = "max"
FIRST = "first"
LAST = "last"
STDDEV = "stddev"
VARIANCE = "variance"
class SortOrder(str, Enum):
"""Sort order."""
ASC = "asc"
DESC = "desc"
@dataclass
class DatabaseConfig:
"""Database configuration."""
api_key: str
endpoint: str = "https://database.synor.io/v1"
network: Network = Network.MAINNET
timeout: float = 30.0
retries: int = 3
debug: bool = False
# ==================== Key-Value Types ====================
@dataclass
class KeyValueEntry:
"""Key-value entry with metadata."""
key: str
value: Any
created_at: int
updated_at: int
metadata: Optional[Dict[str, str]] = None
expires_at: Optional[int] = None
@dataclass
class SetOptions:
"""Options for setting a key-value pair."""
ttl: Optional[int] = None # Time-to-live in seconds
metadata: Optional[Dict[str, str]] = None
if_not_exists: bool = False
if_exists: bool = False
@dataclass
class ListOptions:
"""Options for listing keys."""
prefix: Optional[str] = None
cursor: Optional[str] = None
limit: int = 100
@dataclass
class ListResult:
"""Result of listing keys."""
entries: List[KeyValueEntry]
cursor: Optional[str] = None
has_more: bool = False
# ==================== Document Store Types ====================
@dataclass
class Document:
"""A document with metadata."""
id: str
data: Dict[str, Any]
created_at: int
updated_at: int
version: int
metadata: Optional[Dict[str, str]] = None
@dataclass
class QueryOptions:
"""Query options for document store."""
filter: Optional[Dict[str, Any]] = None
sort: Optional[Dict[str, SortOrder]] = None
skip: int = 0
limit: int = 100
projection: Optional[List[str]] = None
@dataclass
class QueryResult:
"""Result of a document query."""
documents: List[Document]
total: int
has_more: bool
# ==================== Vector Store Types ====================
@dataclass
class VectorEntry:
"""A vector entry."""
id: str
vector: List[float]
metadata: Optional[Dict[str, Any]] = None
content: Optional[str] = None
@dataclass
class SearchOptions:
"""Options for vector search."""
namespace: Optional[str] = None
top_k: int = 10
filter: Optional[Dict[str, Any]] = None
include_metadata: bool = True
include_vectors: bool = False
min_score: Optional[float] = None
@dataclass
class SearchResult:
"""Result of a vector search."""
id: str
score: float
vector: Optional[List[float]] = None
metadata: Optional[Dict[str, Any]] = None
content: Optional[str] = None
@dataclass
class VectorCollectionConfig:
"""Configuration for a vector collection."""
name: str
dimension: int
metric: DistanceMetric = DistanceMetric.COSINE
index_type: str = "hnsw"
@dataclass
class VectorCollectionStats:
"""Statistics for a vector collection."""
name: str
vector_count: int
dimension: int
index_size: int
# ==================== Time Series Types ====================
@dataclass
class DataPoint:
"""A time series data point."""
timestamp: int
value: float
tags: Optional[Dict[str, str]] = None
@dataclass
class TimeRange:
"""Time range for queries."""
start: Union[int, datetime, str]
end: Union[int, datetime, str]
@dataclass
class TimeSeriesQueryOptions:
"""Options for time series queries."""
tags: Optional[Dict[str, str]] = None
aggregation: Optional[Aggregation] = None
interval: Optional[str] = None # e.g., "1m", "5m", "1h"
fill: Optional[Union[str, float]] = None
limit: Optional[int] = None
@dataclass
class TimeSeriesResult:
"""Result of a time series query."""
series: str
points: List[DataPoint]
statistics: Optional[Dict[str, float]] = None
@dataclass
class SeriesInfo:
"""Information about a time series."""
name: str
tags: List[str]
retention_days: int
point_count: int
# ==================== Database Stats ====================
@dataclass
class DatabaseStats:
"""Overall database statistics."""
kv_entries: int
document_count: int
vector_count: int
timeseries_points: int
storage_used: int
storage_limit: int
# ==================== Errors ====================
class DatabaseError(Exception):
"""Base exception for database errors."""
def __init__(self, message: str, code: Optional[str] = None, status_code: Optional[int] = None):
super().__init__(message)
self.code = code
self.status_code = status_code
class DocumentNotFoundError(DatabaseError):
"""Raised when a document is not found."""
def __init__(self, collection: str, id: str):
super().__init__(f"Document not found: {collection}/{id}", "DOCUMENT_NOT_FOUND", 404)
class KeyNotFoundError(DatabaseError):
"""Raised when a key is not found."""
def __init__(self, key: str):
super().__init__(f"Key not found: {key}", "KEY_NOT_FOUND", 404)

View file

@ -0,0 +1,54 @@
"""
Synor Hosting SDK for Python
Decentralized web hosting with domain management, DNS, and deployments.
Example:
>>> from synor_hosting import SynorHosting
>>>
>>> hosting = SynorHosting(api_key="your-api-key")
>>>
>>> # Register a domain
>>> domain = await hosting.register_domain("mysite.synor")
>>> print(f"Domain registered: {domain.name}")
>>>
>>> # Deploy from CID
>>> deployment = await hosting.deploy("Qm...", domain="mysite.synor")
>>> print(f"Live at: {deployment.url}")
>>>
>>> # Provision SSL
>>> cert = await hosting.provision_ssl("mysite.synor")
"""
from .types import (
HostingConfig,
Domain,
DomainRecord,
DomainAvailability,
DnsRecord,
DnsZone,
Deployment,
DeploymentStats,
Certificate,
SiteConfig,
AnalyticsData,
HostingError,
)
from .client import SynorHosting
__version__ = "0.1.0"
__all__ = [
"SynorHosting",
"HostingConfig",
"Domain",
"DomainRecord",
"DomainAvailability",
"DnsRecord",
"DnsZone",
"Deployment",
"DeploymentStats",
"Certificate",
"SiteConfig",
"AnalyticsData",
"HostingError",
]

View file

@ -0,0 +1,487 @@
"""
Synor Hosting SDK Client
"""
from typing import Any, Dict, List, Optional
from urllib.parse import urlencode
import httpx
from .types import (
HostingConfig,
Network,
Domain,
DomainRecord,
DomainStatus,
DomainAvailability,
DnsRecord,
DnsRecordType,
DnsZone,
Deployment,
DeploymentStatus,
DeploymentStats,
RedirectRule,
Certificate,
CertificateStatus,
SiteConfig,
AnalyticsData,
PageView,
Referrer,
Country,
HostingError,
)
class SynorHosting:
"""
Synor Hosting SDK Client.
Provides domain registration, DNS management, site deployment, and SSL provisioning.
"""
def __init__(
self,
api_key: str,
endpoint: str = "https://hosting.synor.io/v1",
network: Network = Network.MAINNET,
timeout: float = 60.0,
retries: int = 3,
debug: bool = False,
):
self._config = HostingConfig(
api_key=api_key,
endpoint=endpoint,
network=network,
timeout=timeout,
retries=retries,
debug=debug,
)
self._client = httpx.AsyncClient(
base_url=endpoint,
timeout=timeout,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"X-SDK-Version": "python/0.1.0",
},
)
self._closed = False
# ==================== Domain Operations ====================
async def check_availability(self, name: str) -> DomainAvailability:
"""Check domain availability."""
response = await self._request("GET", f"/domains/check/{name}")
return DomainAvailability(
name=response["name"],
available=response["available"],
price=response.get("price"),
premium=response.get("premium", False),
)
async def register_domain(
self,
name: str,
years: int = 1,
auto_renew: bool = True,
records: Optional[DomainRecord] = None,
) -> Domain:
"""Register a new domain."""
body: Dict[str, Any] = {"name": name, "years": years, "auto_renew": auto_renew}
if records:
body["records"] = self._domain_record_to_dict(records)
response = await self._request("POST", "/domains", body=body)
return self._parse_domain(response)
async def get_domain(self, name: str) -> Domain:
"""Get domain information."""
response = await self._request("GET", f"/domains/{name}")
return self._parse_domain(response)
async def list_domains(self) -> List[Domain]:
"""List all domains."""
response = await self._request("GET", "/domains")
return [self._parse_domain(d) for d in response.get("domains", [])]
async def update_domain_record(self, name: str, record: DomainRecord) -> Domain:
"""Update domain record."""
response = await self._request(
"PUT", f"/domains/{name}/record", body=self._domain_record_to_dict(record)
)
return self._parse_domain(response)
async def resolve_domain(self, name: str) -> DomainRecord:
"""Resolve a domain to its record."""
response = await self._request("GET", f"/domains/{name}/resolve")
return self._parse_domain_record(response)
async def renew_domain(self, name: str, years: int = 1) -> Domain:
"""Renew a domain."""
response = await self._request("POST", f"/domains/{name}/renew", body={"years": years})
return self._parse_domain(response)
async def transfer_domain(self, name: str, new_owner: str) -> Domain:
"""Transfer domain ownership."""
response = await self._request("POST", f"/domains/{name}/transfer", body={"new_owner": new_owner})
return self._parse_domain(response)
# ==================== DNS Operations ====================
async def get_dns_zone(self, domain: str) -> DnsZone:
"""Get DNS zone for a domain."""
response = await self._request("GET", f"/dns/{domain}")
return DnsZone(
domain=response["domain"],
records=[self._parse_dns_record(r) for r in response.get("records", [])],
updated_at=response["updated_at"],
)
async def set_dns_records(self, domain: str, records: List[DnsRecord]) -> DnsZone:
"""Set DNS records for a domain."""
body = {"records": [self._dns_record_to_dict(r) for r in records]}
response = await self._request("PUT", f"/dns/{domain}", body=body)
return DnsZone(
domain=response["domain"],
records=[self._parse_dns_record(r) for r in response.get("records", [])],
updated_at=response["updated_at"],
)
async def add_dns_record(self, domain: str, record: DnsRecord) -> DnsZone:
"""Add a DNS record."""
body = self._dns_record_to_dict(record)
response = await self._request("POST", f"/dns/{domain}/records", body=body)
return DnsZone(
domain=response["domain"],
records=[self._parse_dns_record(r) for r in response.get("records", [])],
updated_at=response["updated_at"],
)
async def delete_dns_record(self, domain: str, record_type: str, name: str) -> DnsZone:
"""Delete a DNS record."""
response = await self._request("DELETE", f"/dns/{domain}/records/{record_type}/{name}")
return DnsZone(
domain=response["domain"],
records=[self._parse_dns_record(r) for r in response.get("records", [])],
updated_at=response["updated_at"],
)
# ==================== Deployment Operations ====================
async def deploy(
self,
cid: str,
domain: Optional[str] = None,
subdomain: Optional[str] = None,
headers: Optional[Dict[str, str]] = None,
redirects: Optional[List[RedirectRule]] = None,
spa: bool = False,
clean_urls: bool = True,
trailing_slash: bool = False,
) -> Deployment:
"""Deploy a site from CID."""
body: Dict[str, Any] = {"cid": cid}
if domain:
body["domain"] = domain
if subdomain:
body["subdomain"] = subdomain
if headers:
body["headers"] = headers
if redirects:
body["redirects"] = [
{"source": r.source, "destination": r.destination, "status_code": r.status_code}
for r in redirects
]
body["spa"] = spa
body["clean_urls"] = clean_urls
body["trailing_slash"] = trailing_slash
response = await self._request("POST", "/deployments", body=body)
return self._parse_deployment(response)
async def get_deployment(self, deployment_id: str) -> Deployment:
"""Get deployment by ID."""
response = await self._request("GET", f"/deployments/{deployment_id}")
return self._parse_deployment(response)
async def list_deployments(self, domain: Optional[str] = None) -> List[Deployment]:
"""List deployments."""
path = f"/deployments?domain={domain}" if domain else "/deployments"
response = await self._request("GET", path)
return [self._parse_deployment(d) for d in response.get("deployments", [])]
async def rollback(self, domain: str, deployment_id: str) -> Deployment:
"""Rollback to a previous deployment."""
response = await self._request(
"POST", f"/deployments/{deployment_id}/rollback", body={"domain": domain}
)
return self._parse_deployment(response)
async def delete_deployment(self, deployment_id: str) -> None:
"""Delete a deployment."""
await self._request("DELETE", f"/deployments/{deployment_id}")
async def get_deployment_stats(self, deployment_id: str, period: str = "24h") -> DeploymentStats:
"""Get deployment stats."""
response = await self._request("GET", f"/deployments/{deployment_id}/stats?period={period}")
return DeploymentStats(
requests=response["requests"],
bandwidth=response["bandwidth"],
unique_visitors=response["unique_visitors"],
period=response["period"],
)
# ==================== SSL Operations ====================
async def provision_ssl(
self,
domain: str,
include_www: bool = True,
auto_renew: bool = True,
) -> Certificate:
"""Provision SSL certificate."""
body = {"include_www": include_www, "auto_renew": auto_renew}
response = await self._request("POST", f"/ssl/{domain}", body=body)
return self._parse_certificate(response)
async def get_certificate(self, domain: str) -> Certificate:
"""Get certificate status."""
response = await self._request("GET", f"/ssl/{domain}")
return self._parse_certificate(response)
async def renew_certificate(self, domain: str) -> Certificate:
"""Renew SSL certificate."""
response = await self._request("POST", f"/ssl/{domain}/renew")
return self._parse_certificate(response)
async def delete_certificate(self, domain: str) -> None:
"""Delete/revoke SSL certificate."""
await self._request("DELETE", f"/ssl/{domain}")
# ==================== Site Configuration ====================
async def get_site_config(self, domain: str) -> SiteConfig:
"""Get site configuration."""
response = await self._request("GET", f"/sites/{domain}/config")
return self._parse_site_config(response)
async def update_site_config(self, domain: str, **kwargs) -> SiteConfig:
"""Update site configuration."""
response = await self._request("PATCH", f"/sites/{domain}/config", body=kwargs)
return self._parse_site_config(response)
async def purge_cache(self, domain: str, paths: Optional[List[str]] = None) -> int:
"""Purge CDN cache."""
body = {"paths": paths} if paths else None
response = await self._request("DELETE", f"/sites/{domain}/cache", body=body)
return response.get("purged", 0)
# ==================== Analytics ====================
async def get_analytics(
self,
domain: str,
period: str = "24h",
start_date: Optional[str] = None,
end_date: Optional[str] = None,
) -> AnalyticsData:
"""Get site analytics."""
params = {"period": period}
if start_date:
params["start"] = start_date
if end_date:
params["end"] = end_date
response = await self._request("GET", f"/sites/{domain}/analytics?{urlencode(params)}")
return AnalyticsData(
domain=response["domain"],
period=response["period"],
page_views=response["page_views"],
unique_visitors=response["unique_visitors"],
bandwidth=response["bandwidth"],
top_pages=[PageView(path=p["path"], views=p["views"]) for p in response.get("top_pages", [])],
top_referrers=[Referrer(referrer=r["referrer"], count=r["count"]) for r in response.get("top_referrers", [])],
top_countries=[Country(country=c["country"], count=c["count"]) for c in response.get("top_countries", [])],
)
# ==================== Lifecycle ====================
async def close(self) -> None:
"""Close the client."""
if not self._closed:
await self._client.aclose()
self._closed = True
@property
def is_closed(self) -> bool:
"""Check if the client is closed."""
return self._closed
async def __aenter__(self) -> "SynorHosting":
return self
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
await self.close()
# ==================== Internal Methods ====================
async def _request(
self,
method: str,
path: str,
body: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Make an HTTP request with retry logic."""
if self._closed:
raise HostingError("Client has been closed")
last_error: Optional[Exception] = None
for attempt in range(self._config.retries):
try:
if method == "GET":
response = await self._client.get(path)
elif method == "POST":
response = await self._client.post(path, json=body)
elif method == "PUT":
response = await self._client.put(path, json=body)
elif method == "PATCH":
response = await self._client.patch(path, json=body)
elif method == "DELETE":
if body:
response = await self._client.request("DELETE", path, json=body)
else:
response = await self._client.delete(path)
else:
raise ValueError(f"Unknown method: {method}")
if response.status_code >= 400:
error_data = response.json() if response.content else {}
raise HostingError(
error_data.get("message") or error_data.get("error") or f"HTTP {response.status_code}",
error_data.get("code"),
response.status_code,
)
return response.json() if response.content else {}
except Exception as e:
last_error = e
if self._config.debug:
print(f"Attempt {attempt + 1} failed: {e}")
if attempt < self._config.retries - 1:
import asyncio
await asyncio.sleep(attempt + 1)
if last_error:
raise last_error
raise HostingError("Unknown error after retries")
def _parse_domain(self, data: Dict[str, Any]) -> Domain:
records = None
if "records" in data and data["records"]:
records = self._parse_domain_record(data["records"])
return Domain(
name=data["name"],
status=DomainStatus(data["status"]),
owner=data["owner"],
registered_at=data["registered_at"],
expires_at=data["expires_at"],
auto_renew=data.get("auto_renew", True),
records=records,
)
def _parse_domain_record(self, data: Dict[str, Any]) -> DomainRecord:
return DomainRecord(
cid=data.get("cid"),
ipv4=data.get("ipv4"),
ipv6=data.get("ipv6"),
cname=data.get("cname"),
txt=data.get("txt"),
metadata=data.get("metadata"),
)
def _domain_record_to_dict(self, record: DomainRecord) -> Dict[str, Any]:
result: Dict[str, Any] = {}
if record.cid:
result["cid"] = record.cid
if record.ipv4:
result["ipv4"] = record.ipv4
if record.ipv6:
result["ipv6"] = record.ipv6
if record.cname:
result["cname"] = record.cname
if record.txt:
result["txt"] = record.txt
if record.metadata:
result["metadata"] = record.metadata
return result
def _parse_dns_record(self, data: Dict[str, Any]) -> DnsRecord:
return DnsRecord(
type=DnsRecordType(data["type"]),
name=data["name"],
value=data["value"],
ttl=data.get("ttl", 3600),
priority=data.get("priority"),
)
def _dns_record_to_dict(self, record: DnsRecord) -> Dict[str, Any]:
result = {
"type": record.type.value,
"name": record.name,
"value": record.value,
"ttl": record.ttl,
}
if record.priority is not None:
result["priority"] = record.priority
return result
def _parse_deployment(self, data: Dict[str, Any]) -> Deployment:
return Deployment(
id=data["id"],
domain=data["domain"],
cid=data["cid"],
status=DeploymentStatus(data["status"]),
url=data["url"],
created_at=data["created_at"],
updated_at=data["updated_at"],
build_logs=data.get("build_logs"),
error_message=data.get("error_message"),
)
def _parse_certificate(self, data: Dict[str, Any]) -> Certificate:
return Certificate(
domain=data["domain"],
status=CertificateStatus(data["status"]),
auto_renew=data.get("auto_renew", True),
issuer=data.get("issuer", ""),
issued_at=data.get("issued_at"),
expires_at=data.get("expires_at"),
fingerprint=data.get("fingerprint"),
)
def _parse_site_config(self, data: Dict[str, Any]) -> SiteConfig:
redirects = None
if "redirects" in data and data["redirects"]:
redirects = [
RedirectRule(
source=r["source"],
destination=r["destination"],
status_code=r.get("status_code", 301),
permanent=r.get("permanent", True),
)
for r in data["redirects"]
]
return SiteConfig(
domain=data["domain"],
cid=data.get("cid"),
headers=data.get("headers"),
redirects=redirects,
error_pages=data.get("error_pages"),
spa=data.get("spa", False),
clean_urls=data.get("clean_urls", True),
trailing_slash=data.get("trailing_slash", False),
)

View file

@ -0,0 +1,241 @@
"""
Synor Hosting SDK Types
"""
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional
class Network(str, Enum):
"""Network environment."""
MAINNET = "mainnet"
TESTNET = "testnet"
DEVNET = "devnet"
class DomainStatus(str, Enum):
"""Domain status."""
PENDING = "pending"
ACTIVE = "active"
EXPIRED = "expired"
SUSPENDED = "suspended"
class DeploymentStatus(str, Enum):
"""Deployment status."""
PENDING = "pending"
BUILDING = "building"
DEPLOYING = "deploying"
ACTIVE = "active"
FAILED = "failed"
INACTIVE = "inactive"
class CertificateStatus(str, Enum):
"""Certificate status."""
PENDING = "pending"
ISSUED = "issued"
EXPIRED = "expired"
REVOKED = "revoked"
class DnsRecordType(str, Enum):
"""DNS record type."""
A = "A"
AAAA = "AAAA"
CNAME = "CNAME"
TXT = "TXT"
MX = "MX"
NS = "NS"
SRV = "SRV"
CAA = "CAA"
@dataclass
class HostingConfig:
"""Hosting configuration."""
api_key: str
endpoint: str = "https://hosting.synor.io/v1"
network: Network = Network.MAINNET
timeout: float = 60.0
retries: int = 3
debug: bool = False
# ==================== Domain Types ====================
@dataclass
class DomainRecord:
"""Domain record for resolution."""
cid: Optional[str] = None
ipv4: Optional[List[str]] = None
ipv6: Optional[List[str]] = None
cname: Optional[str] = None
txt: Optional[List[str]] = None
metadata: Optional[Dict[str, str]] = None
@dataclass
class Domain:
"""Domain information."""
name: str
status: DomainStatus
owner: str
registered_at: int
expires_at: int
auto_renew: bool = True
records: Optional[DomainRecord] = None
@dataclass
class DomainAvailability:
"""Domain availability check result."""
name: str
available: bool
price: Optional[float] = None
premium: bool = False
# ==================== DNS Types ====================
@dataclass
class DnsRecord:
"""DNS record."""
type: DnsRecordType
name: str
value: str
ttl: int = 3600
priority: Optional[int] = None # For MX and SRV
@dataclass
class DnsZone:
"""DNS zone."""
domain: str
records: List[DnsRecord]
updated_at: int
# ==================== Deployment Types ====================
@dataclass
class RedirectRule:
"""Redirect rule."""
source: str
destination: str
status_code: int = 301
permanent: bool = True
@dataclass
class Deployment:
"""Deployment information."""
id: str
domain: str
cid: str
status: DeploymentStatus
url: str
created_at: int
updated_at: int
build_logs: Optional[str] = None
error_message: Optional[str] = None
@dataclass
class DeploymentStats:
"""Deployment statistics."""
requests: int
bandwidth: int
unique_visitors: int
period: str
# ==================== SSL Types ====================
@dataclass
class Certificate:
"""SSL certificate."""
domain: str
status: CertificateStatus
auto_renew: bool
issuer: str
issued_at: Optional[int] = None
expires_at: Optional[int] = None
fingerprint: Optional[str] = None
# ==================== Site Configuration ====================
@dataclass
class SiteConfig:
"""Site configuration."""
domain: str
cid: Optional[str] = None
headers: Optional[Dict[str, str]] = None
redirects: Optional[List[RedirectRule]] = None
error_pages: Optional[Dict[str, str]] = None
spa: bool = False
clean_urls: bool = True
trailing_slash: bool = False
# ==================== Analytics ====================
@dataclass
class PageView:
"""Page view data."""
path: str
views: int
@dataclass
class Referrer:
"""Referrer data."""
referrer: str
count: int
@dataclass
class Country:
"""Country data."""
country: str
count: int
@dataclass
class AnalyticsData:
"""Analytics data."""
domain: str
period: str
page_views: int
unique_visitors: int
bandwidth: int
top_pages: List[PageView]
top_referrers: List[Referrer]
top_countries: List[Country]
# ==================== Errors ====================
class HostingError(Exception):
"""Base exception for hosting errors."""
def __init__(self, message: str, code: Optional[str] = None, status_code: Optional[int] = None):
super().__init__(message)
self.code = code
self.status_code = status_code
class DomainNotFoundError(HostingError):
"""Raised when a domain is not found."""
def __init__(self, domain: str):
super().__init__(f"Domain not found: {domain}", "DOMAIN_NOT_FOUND", 404)
class DomainUnavailableError(HostingError):
"""Raised when a domain is not available."""
def __init__(self, domain: str):
super().__init__(f"Domain is not available: {domain}", "DOMAIN_UNAVAILABLE", 409)

41
sdk/ruby/lib/synor_rpc.rb Normal file
View file

@ -0,0 +1,41 @@
# frozen_string_literal: true
require_relative "synor_rpc/version"
require_relative "synor_rpc/types"
require_relative "synor_rpc/client"
# Synor RPC SDK for Ruby
#
# Blockchain data queries, transaction submission, and real-time subscriptions.
#
# @example Quick Start
# require 'synor_rpc'
#
# # Create client
# client = SynorRpc::Client.new(api_key: 'your-api-key')
#
# # Get latest block
# block = client.get_latest_block
# puts "Block height: #{block.height}"
#
# # Get transaction
# tx = client.get_transaction(txid)
# puts "Confirmations: #{tx.confirmations}"
#
# # Subscribe to new blocks
# client.subscribe_blocks do |block|
# puts "New block: #{block.height}"
# end
#
module SynorRpc
class Error < StandardError; end
class ApiError < Error
attr_reader :status_code
def initialize(message, status_code: nil)
super(message)
@status_code = status_code
end
end
class ClientClosedError < Error; end
end

View file

@ -0,0 +1,384 @@
# frozen_string_literal: true
require "faraday"
require "json"
module SynorRpc
# Synor RPC SDK Client
#
# @example
# client = SynorRpc::Client.new(api_key: 'your-api-key')
#
# # Get latest block
# block = client.get_latest_block
#
# # Get transaction
# tx = client.get_transaction(txid)
#
class Client
attr_reader :config
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
@subscriptions = {}
@ws = nil
end
# ==================== Block Operations ====================
# Get the latest block
#
# @return [Block]
def get_latest_block
check_closed!
response = with_retry { @conn.get("/blocks/latest") }
Block.from_hash(response.body)
end
# Get a block by hash or height
#
# @param hash_or_height [String, Integer]
# @return [Block]
def get_block(hash_or_height)
check_closed!
response = with_retry { @conn.get("/blocks/#{hash_or_height}") }
Block.from_hash(response.body)
end
# Get a block header
#
# @param hash_or_height [String, Integer]
# @return [BlockHeader]
def get_block_header(hash_or_height)
check_closed!
response = with_retry { @conn.get("/blocks/#{hash_or_height}/header") }
BlockHeader.from_hash(response.body)
end
# Get a range of blocks
#
# @param start_height [Integer]
# @param end_height [Integer]
# @return [Array<Block>]
def get_blocks(start_height, end_height)
check_closed!
response = with_retry do
@conn.get("/blocks", start: start_height, end: end_height)
end
response.body.map { |b| Block.from_hash(b) }
end
# ==================== Transaction Operations ====================
# Get a transaction by ID
#
# @param txid [String]
# @return [Transaction]
def get_transaction(txid)
check_closed!
response = with_retry { @conn.get("/transactions/#{txid}") }
Transaction.from_hash(response.body)
end
# Get raw transaction hex
#
# @param txid [String]
# @return [String]
def get_raw_transaction(txid)
check_closed!
response = with_retry { @conn.get("/transactions/#{txid}/raw") }
response.body["hex"]
end
# Send a raw transaction
#
# @param hex [String]
# @return [SubmitResult]
def send_raw_transaction(hex)
check_closed!
response = with_retry { @conn.post("/transactions/send", { hex: hex }) }
SubmitResult.from_hash(response.body)
end
# Decode a raw transaction
#
# @param hex [String]
# @return [Transaction]
def decode_raw_transaction(hex)
check_closed!
response = with_retry { @conn.post("/transactions/decode", { hex: hex }) }
Transaction.from_hash(response.body)
end
# Get transactions for an address
#
# @param address [String]
# @param limit [Integer]
# @param offset [Integer]
# @return [Array<Transaction>]
def get_address_transactions(address, limit: 20, offset: 0)
check_closed!
response = with_retry do
@conn.get("/addresses/#{address}/transactions", limit: limit, offset: offset)
end
response.body.map { |t| Transaction.from_hash(t) }
end
# ==================== Fee Estimation ====================
# Estimate fee
#
# @param priority [String]
# @return [FeeEstimate]
def estimate_fee(priority: Priority::MEDIUM)
check_closed!
response = with_retry { @conn.get("/fees/estimate", priority: priority) }
FeeEstimate.from_hash(response.body)
end
# Get all fee estimates
#
# @return [Hash<String, FeeEstimate>]
def get_all_fee_estimates
check_closed!
response = with_retry { @conn.get("/fees/estimates") }
response.body.to_h { |e| [e["priority"], FeeEstimate.from_hash(e)] }
end
# ==================== Chain Information ====================
# Get chain info
#
# @return [ChainInfo]
def get_chain_info
check_closed!
response = with_retry { @conn.get("/chain/info") }
ChainInfo.from_hash(response.body)
end
# Get mempool info
#
# @return [MempoolInfo]
def get_mempool_info
check_closed!
response = with_retry { @conn.get("/mempool/info") }
MempoolInfo.from_hash(response.body)
end
# Get mempool transaction IDs
#
# @param limit [Integer]
# @return [Array<String>]
def get_mempool_transactions(limit: 100)
check_closed!
response = with_retry { @conn.get("/mempool/transactions", limit: limit) }
response.body
end
# ==================== Subscriptions ====================
# Subscribe to new blocks
#
# @yield [Block] Called for each new block
# @return [Subscription]
def subscribe_blocks(&block)
check_closed!
raise ArgumentError, "Block required" unless block_given?
ensure_websocket_connected
subscription_id = SecureRandom.uuid
@subscriptions[subscription_id] = {
channel: "blocks",
callback: ->(data) { block.call(Block.from_hash(data)) }
}
send_ws_message({
type: "subscribe",
channel: "blocks",
subscription_id: subscription_id
})
Subscription.new(id: subscription_id, channel: "blocks") do
@subscriptions.delete(subscription_id)
send_ws_message({
type: "unsubscribe",
subscription_id: subscription_id
})
end
end
# Subscribe to address transactions
#
# @param address [String]
# @yield [Transaction] Called for each transaction
# @return [Subscription]
def subscribe_address(address, &block)
check_closed!
raise ArgumentError, "Block required" unless block_given?
ensure_websocket_connected
subscription_id = SecureRandom.uuid
@subscriptions[subscription_id] = {
channel: "address:#{address}",
callback: ->(data) { block.call(Transaction.from_hash(data)) }
}
send_ws_message({
type: "subscribe",
channel: "address",
address: address,
subscription_id: subscription_id
})
Subscription.new(id: subscription_id, channel: "address:#{address}") do
@subscriptions.delete(subscription_id)
send_ws_message({
type: "unsubscribe",
subscription_id: subscription_id
})
end
end
# Subscribe to mempool transactions
#
# @yield [Transaction] Called for each transaction
# @return [Subscription]
def subscribe_mempool(&block)
check_closed!
raise ArgumentError, "Block required" unless block_given?
ensure_websocket_connected
subscription_id = SecureRandom.uuid
@subscriptions[subscription_id] = {
channel: "mempool",
callback: ->(data) { block.call(Transaction.from_hash(data)) }
}
send_ws_message({
type: "subscribe",
channel: "mempool",
subscription_id: subscription_id
})
Subscription.new(id: subscription_id, channel: "mempool") do
@subscriptions.delete(subscription_id)
send_ws_message({
type: "unsubscribe",
subscription_id: subscription_id
})
end
end
# ==================== Lifecycle ====================
def close
@closed = true
@ws&.close
@conn.close if @conn.respond_to?(:close)
end
def closed?
@closed
end
private
def check_closed!
raise ClientClosedError, "Client has been closed" if @closed
end
def with_retry
attempts = 0
begin
attempts += 1
response = yield
handle_response(response)
response
rescue Faraday::Error, ApiError => e
if attempts < @config.retries
sleep(attempts)
retry
end
raise
end
end
def handle_response(response)
return if response.success?
error_message = response.body["error"] || response.body["message"] || "Unknown error"
raise ApiError.new(error_message, status_code: response.status)
end
def ensure_websocket_connected
return if @ws && @ws_connected
require "websocket-client-simple"
@ws = WebSocket::Client::Simple.connect(@config.ws_url, headers: {
"Authorization" => "Bearer #{@config.api_key}"
})
@ws.on :message do |msg|
handle_ws_message(msg.data)
end
@ws.on :open do
@ws_connected = true
end
@ws.on :close do
@ws_connected = false
end
@ws.on :error do |e|
puts "WebSocket error: #{e.message}" if @config.debug
end
# Wait for connection
sleep(0.1) until @ws_connected || @closed
end
def send_ws_message(message)
@ws&.send(JSON.generate(message))
end
def handle_ws_message(data)
message = JSON.parse(data)
subscription_id = message["subscription_id"]
if subscription_id && @subscriptions[subscription_id]
@subscriptions[subscription_id][:callback].call(message["data"])
end
rescue JSON::ParserError
# Skip malformed messages
end
end
end

View file

@ -0,0 +1,343 @@
# frozen_string_literal: true
module SynorRpc
# Network environment
module Network
MAINNET = "mainnet"
TESTNET = "testnet"
DEVNET = "devnet"
end
# Transaction priority
module Priority
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
end
# Transaction status
module TransactionStatus
PENDING = "pending"
CONFIRMED = "confirmed"
FAILED = "failed"
end
# Configuration
class Config
attr_reader :api_key, :base_url, :ws_url, :network, :timeout, :retries, :debug
def initialize(
api_key:,
base_url: "https://rpc.synor.io/v1",
ws_url: "wss://rpc.synor.io/v1/ws",
network: Network::MAINNET,
timeout: 30,
retries: 3,
debug: false
)
@api_key = api_key
@base_url = base_url
@ws_url = ws_url
@network = network
@timeout = timeout
@retries = retries
@debug = debug
end
end
# Block header
class BlockHeader
attr_reader :hash, :height, :previous_hash, :timestamp, :version,
:merkle_root, :nonce, :difficulty
def initialize(hash:, height:, previous_hash:, timestamp:, version:,
merkle_root:, nonce:, difficulty:)
@hash = hash
@height = height
@previous_hash = previous_hash
@timestamp = timestamp
@version = version
@merkle_root = merkle_root
@nonce = nonce
@difficulty = difficulty
end
def self.from_hash(h)
new(
hash: h["hash"],
height: h["height"],
previous_hash: h["previous_hash"],
timestamp: h["timestamp"],
version: h["version"],
merkle_root: h["merkle_root"],
nonce: h["nonce"],
difficulty: h["difficulty"]
)
end
end
# Script public key
class ScriptPubKey
attr_reader :asm, :hex, :type, :address
def initialize(asm:, hex:, type:, address: nil)
@asm = asm
@hex = hex
@type = type
@address = address
end
def self.from_hash(h)
new(
asm: h["asm"],
hex: h["hex"],
type: h["type"],
address: h["address"]
)
end
end
# Transaction input
class TransactionInput
attr_reader :txid, :vout, :script_sig, :sequence, :witness
def initialize(txid:, vout:, script_sig:, sequence:, witness: nil)
@txid = txid
@vout = vout
@script_sig = script_sig
@sequence = sequence
@witness = witness
end
def self.from_hash(h)
new(
txid: h["txid"],
vout: h["vout"],
script_sig: h["script_sig"],
sequence: h["sequence"],
witness: h["witness"]
)
end
end
# Transaction output
class TransactionOutput
attr_reader :value, :n, :script_pubkey
def initialize(value:, n:, script_pubkey:)
@value = value
@n = n
@script_pubkey = script_pubkey
end
def self.from_hash(h)
new(
value: h["value"],
n: h["n"],
script_pubkey: ScriptPubKey.from_hash(h["script_pubkey"])
)
end
end
# Transaction
class Transaction
attr_reader :txid, :hash, :version, :size, :weight, :lock_time,
:inputs, :outputs, :fee, :confirmations,
:block_hash, :block_height, :timestamp, :status
def initialize(txid:, hash:, version:, size:, weight:, lock_time:,
inputs:, outputs:, fee:, confirmations:,
block_hash: nil, block_height: nil, timestamp: nil,
status: TransactionStatus::PENDING)
@txid = txid
@hash = hash
@version = version
@size = size
@weight = weight
@lock_time = lock_time
@inputs = inputs
@outputs = outputs
@fee = fee
@confirmations = confirmations
@block_hash = block_hash
@block_height = block_height
@timestamp = timestamp
@status = status
end
def self.from_hash(h)
new(
txid: h["txid"],
hash: h["hash"],
version: h["version"],
size: h["size"],
weight: h["weight"],
lock_time: h["lock_time"],
inputs: (h["inputs"] || []).map { |i| TransactionInput.from_hash(i) },
outputs: (h["outputs"] || []).map { |o| TransactionOutput.from_hash(o) },
fee: h["fee"],
confirmations: h["confirmations"],
block_hash: h["block_hash"],
block_height: h["block_height"],
timestamp: h["timestamp"],
status: h["status"] || TransactionStatus::PENDING
)
end
end
# Block
class Block
attr_reader :hash, :height, :previous_hash, :timestamp, :version,
:merkle_root, :nonce, :difficulty, :transactions, :size, :weight
def initialize(hash:, height:, previous_hash:, timestamp:, version:,
merkle_root:, nonce:, difficulty:, transactions:, size:, weight:)
@hash = hash
@height = height
@previous_hash = previous_hash
@timestamp = timestamp
@version = version
@merkle_root = merkle_root
@nonce = nonce
@difficulty = difficulty
@transactions = transactions
@size = size
@weight = weight
end
def self.from_hash(h)
new(
hash: h["hash"],
height: h["height"],
previous_hash: h["previous_hash"],
timestamp: h["timestamp"],
version: h["version"],
merkle_root: h["merkle_root"],
nonce: h["nonce"],
difficulty: h["difficulty"],
transactions: (h["transactions"] || []).map { |t| Transaction.from_hash(t) },
size: h["size"],
weight: h["weight"]
)
end
end
# Chain info
class ChainInfo
attr_reader :chain, :blocks, :headers, :best_block_hash, :difficulty,
:median_time, :verification_progress, :chain_work, :pruned
def initialize(chain:, blocks:, headers:, best_block_hash:, difficulty:,
median_time:, verification_progress:, chain_work:, pruned:)
@chain = chain
@blocks = blocks
@headers = headers
@best_block_hash = best_block_hash
@difficulty = difficulty
@median_time = median_time
@verification_progress = verification_progress
@chain_work = chain_work
@pruned = pruned
end
def self.from_hash(h)
new(
chain: h["chain"],
blocks: h["blocks"],
headers: h["headers"],
best_block_hash: h["best_block_hash"],
difficulty: h["difficulty"],
median_time: h["median_time"],
verification_progress: h["verification_progress"],
chain_work: h["chain_work"],
pruned: h["pruned"]
)
end
end
# Mempool info
class MempoolInfo
attr_reader :size, :bytes, :usage, :max_mempool, :mempool_min_fee, :min_relay_tx_fee
def initialize(size:, bytes:, usage:, max_mempool:, mempool_min_fee:, min_relay_tx_fee:)
@size = size
@bytes = bytes
@usage = usage
@max_mempool = max_mempool
@mempool_min_fee = mempool_min_fee
@min_relay_tx_fee = min_relay_tx_fee
end
def self.from_hash(h)
new(
size: h["size"],
bytes: h["bytes"],
usage: h["usage"],
max_mempool: h["max_mempool"],
mempool_min_fee: h["mempool_min_fee"],
min_relay_tx_fee: h["min_relay_tx_fee"]
)
end
end
# Fee estimate
class FeeEstimate
attr_reader :priority, :fee_rate, :estimated_blocks
def initialize(priority:, fee_rate:, estimated_blocks:)
@priority = priority
@fee_rate = fee_rate
@estimated_blocks = estimated_blocks
end
def self.from_hash(h)
new(
priority: h["priority"],
fee_rate: h["fee_rate"],
estimated_blocks: h["estimated_blocks"]
)
end
end
# Submit result
class SubmitResult
attr_reader :txid, :accepted, :reason
def initialize(txid:, accepted:, reason: nil)
@txid = txid
@accepted = accepted
@reason = reason
end
def self.from_hash(h)
new(
txid: h["txid"],
accepted: h["accepted"],
reason: h["reason"]
)
end
end
# Subscription handle
class Subscription
attr_reader :id, :channel
def initialize(id:, channel:, &on_cancel)
@id = id
@channel = channel
@on_cancel = on_cancel
@cancelled = false
end
def cancel
return if @cancelled
@on_cancel&.call
@cancelled = true
end
def cancelled?
@cancelled
end
end
end

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
module SynorRpc
VERSION = "0.1.0"
end

View file

@ -0,0 +1,40 @@
# frozen_string_literal: true
require_relative "synor_storage/version"
require_relative "synor_storage/types"
require_relative "synor_storage/client"
# Synor Storage SDK for Ruby
#
# Decentralized storage operations including upload, download, pinning, and directory management.
#
# @example Quick Start
# require 'synor_storage'
#
# # Create client
# client = SynorStorage::Client.new(api_key: 'your-api-key')
#
# # Upload a file
# result = client.upload(File.read('document.pdf'), name: 'document.pdf')
# puts "CID: #{result.cid}"
#
# # Download a file
# data = client.download(result.cid)
# File.write('downloaded.pdf', data)
#
# # Get gateway URL
# url = client.get_gateway_url(result.cid)
# puts "URL: #{url}"
#
module SynorStorage
class Error < StandardError; end
class ApiError < Error
attr_reader :status_code
def initialize(message, status_code: nil)
super(message)
@status_code = status_code
end
end
class ClientClosedError < Error; end
end

View file

@ -0,0 +1,317 @@
# frozen_string_literal: true
require "faraday"
require "faraday/multipart"
require "json"
require "base64"
module SynorStorage
# Synor Storage SDK Client
#
# @example
# client = SynorStorage::Client.new(api_key: 'your-api-key')
#
# # Upload file
# result = client.upload(data, name: 'file.txt')
#
# # Download file
# data = client.download(cid)
#
class Client
attr_reader :config
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 :multipart
f.request :url_encoded
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
end
# ==================== Upload Operations ====================
# Upload data to storage
#
# @param data [String, IO] Data to upload
# @param options [UploadOptions, Hash] Upload options
# @return [UploadResponse]
def upload(data, name: nil, content_type: nil, pin: true, metadata: nil, wrap_with_directory: false)
check_closed!
payload = {
file: Faraday::Multipart::FilePart.new(
StringIO.new(data.is_a?(String) ? data : data.read),
content_type || "application/octet-stream",
name || "file"
),
pin: pin.to_s,
wrap_with_directory: wrap_with_directory.to_s
}
if metadata
metadata.each do |key, value|
payload["metadata[#{key}]"] = value
end
end
response = with_retry { @conn.post("/upload", payload) }
UploadResponse.from_hash(response.body)
end
# Upload a directory of files
#
# @param files [Array<FileEntry>] Files to upload
# @param directory_name [String, nil] Optional directory name
# @return [UploadResponse]
def upload_directory(files, directory_name: nil)
check_closed!
payload = {}
files.each_with_index do |file, i|
payload["files[#{i}]"] = Faraday::Multipart::FilePart.new(
StringIO.new(file.content),
file.content_type || "application/octet-stream",
file.path
)
end
payload["directory_name"] = directory_name if directory_name
response = with_retry { @conn.post("/upload/directory", payload) }
UploadResponse.from_hash(response.body)
end
# ==================== Download Operations ====================
# Download content by CID
#
# @param cid [String]
# @return [String] Binary data
def download(cid)
check_closed!
response = with_retry do
@conn.get("/download/#{cid}") do |req|
req.headers["Accept"] = "*/*"
end
end
response.body
end
# Download content with streaming
#
# @param cid [String]
# @yield [String] Chunks of data
def download_stream(cid, &block)
check_closed!
raise ArgumentError, "Block required for streaming" unless block_given?
@conn.get("/download/#{cid}") do |req|
req.headers["Accept"] = "*/*"
req.options.on_data = proc do |chunk, _|
block.call(chunk)
end
end
end
# Get gateway URL for a CID
#
# @param cid [String]
# @param path [String, nil] Optional path within the CID
# @return [String]
def get_gateway_url(cid, path: nil)
url = "#{@config.gateway}/ipfs/#{cid}"
url += "/#{path.gsub(%r{^/}, '')}" if path
url
end
# ==================== Pinning Operations ====================
# Pin content by CID
#
# @param request [PinRequest, Hash] Pin request
# @return [Pin]
def pin(request)
check_closed!
body = request.is_a?(PinRequest) ? request.to_h : request
response = with_retry { post_json("/pins", body) }
Pin.from_hash(response.body)
end
# Unpin content
#
# @param cid [String]
def unpin(cid)
check_closed!
with_retry { @conn.delete("/pins/#{cid}") }
nil
end
# Get pin status
#
# @param cid [String]
# @return [Pin]
def get_pin_status(cid)
check_closed!
response = with_retry { @conn.get("/pins/#{cid}") }
Pin.from_hash(response.body)
end
# List pins
#
# @param status [String, nil] Filter by status
# @param limit [Integer]
# @param offset [Integer]
# @return [Array<Pin>]
def list_pins(status: nil, limit: 20, offset: 0)
check_closed!
params = { limit: limit, offset: offset }
params[:status] = status if status
response = with_retry { @conn.get("/pins", params) }
response.body.map { |p| Pin.from_hash(p) }
end
# ==================== CAR File Operations ====================
# Create a CAR file from entries
#
# @param entries [Array<FileEntry>]
# @return [CarFile]
def create_car(entries)
check_closed!
body = entries.map do |e|
{
path: e.path,
content: Base64.strict_encode64(e.content),
content_type: e.content_type
}
end
response = with_retry { post_json("/car/create", body) }
CarFile.from_hash(response.body)
end
# Import a CAR file
#
# @param car_data [String] Binary CAR data
# @return [Array<String>] CIDs
def import_car(car_data)
check_closed!
response = with_retry do
@conn.post("/car/import") do |req|
req.headers["Content-Type"] = "application/vnd.ipld.car"
req.body = car_data
end
end
response.body["cids"]
end
# Export content as a CAR file
#
# @param cid [String]
# @return [String] Binary CAR data
def export_car(cid)
check_closed!
response = with_retry do
@conn.get("/car/export/#{cid}") do |req|
req.headers["Accept"] = "application/vnd.ipld.car"
end
end
response.body
end
# ==================== Directory Operations ====================
# List directory contents
#
# @param cid [String]
# @return [Array<DirectoryEntry>]
def list_directory(cid)
check_closed!
response = with_retry { @conn.get("/directory/#{cid}") }
response.body.map { |e| DirectoryEntry.from_hash(e) }
end
# ==================== Statistics ====================
# Get storage statistics
#
# @return [StorageStats]
def get_stats
check_closed!
response = with_retry { @conn.get("/stats") }
StorageStats.from_hash(response.body)
end
# ==================== Lifecycle ====================
def close
@closed = true
@conn.close if @conn.respond_to?(:close)
end
def closed?
@closed
end
private
def check_closed!
raise ClientClosedError, "Client has been closed" if @closed
end
def post_json(path, body)
@conn.post(path) do |req|
req.headers["Content-Type"] = "application/json"
req.body = JSON.generate(body)
end
end
def with_retry
attempts = 0
begin
attempts += 1
response = yield
handle_response(response)
response
rescue Faraday::Error, ApiError => e
if attempts < @config.retries
sleep(attempts)
retry
end
raise
end
end
def handle_response(response)
return if response.success?
error_message = if response.body.is_a?(Hash)
response.body["error"] || response.body["message"] || "Unknown error"
else
"HTTP #{response.status}"
end
raise ApiError.new(error_message, status_code: response.status)
end
end
end

View file

@ -0,0 +1,209 @@
# frozen_string_literal: true
module SynorStorage
# Network environment
module Network
MAINNET = "mainnet"
TESTNET = "testnet"
DEVNET = "devnet"
end
# Pin status
module PinStatus
QUEUED = "queued"
PINNING = "pinning"
PINNED = "pinned"
FAILED = "failed"
end
# Configuration
class Config
attr_reader :api_key, :base_url, :gateway, :network, :timeout, :retries, :debug
def initialize(
api_key:,
base_url: "https://storage.synor.io/v1",
gateway: "https://gateway.synor.io",
network: Network::MAINNET,
timeout: 300,
retries: 3,
debug: false
)
@api_key = api_key
@base_url = base_url
@gateway = gateway
@network = network
@timeout = timeout
@retries = retries
@debug = debug
end
end
# Upload options
class UploadOptions
attr_reader :name, :content_type, :pin, :metadata, :wrap_with_directory
def initialize(
name: nil,
content_type: nil,
pin: true,
metadata: nil,
wrap_with_directory: false
)
@name = name
@content_type = content_type
@pin = pin
@metadata = metadata
@wrap_with_directory = wrap_with_directory
end
end
# Upload response
class UploadResponse
attr_reader :cid, :size, :name, :pinned
def initialize(cid:, size:, name: nil, pinned: true)
@cid = cid
@size = size
@name = name
@pinned = pinned
end
def self.from_hash(h)
new(
cid: h["cid"],
size: h["size"],
name: h["name"],
pinned: h["pinned"] || true
)
end
end
# Pin request
class PinRequest
attr_reader :cid, :name, :metadata, :origins
def initialize(cid:, name: nil, metadata: nil, origins: nil)
@cid = cid
@name = name
@metadata = metadata
@origins = origins
end
def to_h
{
cid: @cid,
name: @name,
metadata: @metadata,
origins: @origins
}.compact
end
end
# Pin information
class Pin
attr_reader :cid, :name, :status, :size, :created_at, :metadata
def initialize(cid:, name:, status:, size:, created_at:, metadata: nil)
@cid = cid
@name = name
@status = status
@size = size
@created_at = created_at
@metadata = metadata
end
def self.from_hash(h)
new(
cid: h["cid"],
name: h["name"],
status: h["status"],
size: h["size"],
created_at: h["created_at"],
metadata: h["metadata"]
)
end
end
# File entry for directory operations
class FileEntry
attr_reader :path, :content, :content_type
def initialize(path:, content:, content_type: nil)
@path = path
@content = content
@content_type = content_type
end
end
# Directory entry
class DirectoryEntry
attr_reader :name, :cid, :size, :type
def initialize(name:, cid:, size:, type:)
@name = name
@cid = cid
@size = size
@type = type
end
def self.from_hash(h)
new(
name: h["name"],
cid: h["cid"],
size: h["size"],
type: h["type"]
)
end
def file?
@type == "file"
end
def directory?
@type == "directory"
end
end
# CAR file
class CarFile
attr_reader :root_cid, :data, :size
def initialize(root_cid:, data:, size:)
@root_cid = root_cid
@data = data
@size = size
end
def self.from_hash(h)
new(
root_cid: h["root_cid"],
data: h["data"],
size: h["size"]
)
end
end
# Storage statistics
class StorageStats
attr_reader :total_size, :pin_count, :bandwidth_used, :storage_limit, :bandwidth_limit
def initialize(total_size:, pin_count:, bandwidth_used:, storage_limit:, bandwidth_limit:)
@total_size = total_size
@pin_count = pin_count
@bandwidth_used = bandwidth_used
@storage_limit = storage_limit
@bandwidth_limit = bandwidth_limit
end
def self.from_hash(h)
new(
total_size: h["total_size"],
pin_count: h["pin_count"],
bandwidth_used: h["bandwidth_used"],
storage_limit: h["storage_limit"],
bandwidth_limit: h["bandwidth_limit"]
)
end
end
end

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
module SynorStorage
VERSION = "0.1.0"
end

View file

@ -0,0 +1,41 @@
# frozen_string_literal: true
require_relative "synor_wallet/version"
require_relative "synor_wallet/types"
require_relative "synor_wallet/client"
# Synor Wallet SDK for Ruby
#
# Key management, transaction signing, and balance queries for the Synor blockchain.
#
# @example Quick Start
# require 'synor_wallet'
#
# # Create client
# client = SynorWallet::Client.new(api_key: 'your-api-key')
#
# # Create a new wallet
# result = client.create_wallet
# puts "Wallet ID: #{result.wallet.id}"
# puts "Mnemonic: #{result.mnemonic}"
#
# # Get balance
# balance = client.get_balance(result.wallet.addresses.first.address)
# puts "Balance: #{balance.total}"
#
# # Sign a message
# signature = client.sign_message(result.wallet.id, "Hello, Synor!")
# puts "Signature: #{signature.signature}"
#
module SynorWallet
class Error < StandardError; end
class ApiError < Error
attr_reader :status_code
def initialize(message, status_code: nil)
super(message)
@status_code = status_code
end
end
class ClientClosedError < Error; end
end

View file

@ -0,0 +1,244 @@
# frozen_string_literal: true
require "faraday"
require "json"
module SynorWallet
# Synor Wallet SDK Client
#
# @example
# client = SynorWallet::Client.new(api_key: 'your-api-key')
#
# # Create wallet
# result = client.create_wallet
# puts result.mnemonic
#
# # Get balance
# balance = client.get_balance(address)
#
class Client
attr_reader :config
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
end
# ==================== Wallet Operations ====================
# Create a new wallet
#
# @param type [String] Wallet type (standard, multisig, hardware)
# @return [CreateWalletResult]
def create_wallet(type: WalletType::STANDARD)
check_closed!
body = {
type: type,
network: @config.network
}
response = with_retry { @conn.post("/wallets", body) }
CreateWalletResult.from_hash(response.body)
end
# Import wallet from mnemonic
#
# @param mnemonic [String] 12 or 24 word mnemonic phrase
# @param passphrase [String, nil] Optional passphrase
# @return [Wallet]
def import_wallet(mnemonic, passphrase: nil)
check_closed!
body = {
mnemonic: mnemonic,
passphrase: passphrase,
network: @config.network
}
response = with_retry { @conn.post("/wallets/import", body) }
Wallet.from_hash(response.body)
end
# Get wallet by ID
#
# @param wallet_id [String]
# @return [Wallet]
def get_wallet(wallet_id)
check_closed!
response = with_retry { @conn.get("/wallets/#{wallet_id}") }
Wallet.from_hash(response.body)
end
# Generate a new address
#
# @param wallet_id [String]
# @param is_change [Boolean]
# @return [Address]
def generate_address(wallet_id, is_change: false)
check_closed!
body = { is_change: is_change }
response = with_retry { @conn.post("/wallets/#{wallet_id}/addresses", body) }
Address.from_hash(response.body)
end
# Get stealth address
#
# @param wallet_id [String]
# @return [StealthAddress]
def get_stealth_address(wallet_id)
check_closed!
response = with_retry { @conn.get("/wallets/#{wallet_id}/stealth-address") }
StealthAddress.from_hash(response.body)
end
# ==================== Signing Operations ====================
# Sign a transaction
#
# @param wallet_id [String]
# @param transaction [Transaction]
# @return [SignedTransaction]
def sign_transaction(wallet_id, transaction)
check_closed!
body = {
wallet_id: wallet_id,
transaction: transaction.to_h
}
response = with_retry { @conn.post("/transactions/sign", body) }
SignedTransaction.from_hash(response.body)
end
# Sign a message
#
# @param wallet_id [String]
# @param message [String]
# @param address_index [Integer]
# @return [Signature]
def sign_message(wallet_id, message, address_index: 0)
check_closed!
body = {
wallet_id: wallet_id,
message: message,
address_index: address_index
}
response = with_retry { @conn.post("/messages/sign", body) }
Signature.from_hash(response.body)
end
# Verify a message signature
#
# @param message [String]
# @param signature [String]
# @param address [String]
# @return [Boolean]
def verify_message(message, signature, address)
check_closed!
body = {
message: message,
signature: signature,
address: address
}
response = with_retry { @conn.post("/messages/verify", body) }
response.body["valid"]
end
# ==================== Balance & UTXOs ====================
# Get balance for an address
#
# @param address [String]
# @return [Balance]
def get_balance(address)
check_closed!
response = with_retry { @conn.get("/addresses/#{address}/balance") }
Balance.from_hash(response.body)
end
# Get UTXOs for an address
#
# @param address [String]
# @param min_confirmations [Integer]
# @return [Array<UTXO>]
def get_utxos(address, min_confirmations: 1)
check_closed!
response = with_retry do
@conn.get("/addresses/#{address}/utxos", min_confirmations: min_confirmations)
end
response.body.map { |u| UTXO.from_hash(u) }
end
# ==================== Fee Estimation ====================
# Estimate fee
#
# @param priority [String]
# @return [Integer] Fee per byte in satoshis
def estimate_fee(priority: Priority::MEDIUM)
check_closed!
response = with_retry { @conn.get("/fees/estimate", priority: priority) }
response.body["fee_per_byte"]
end
# ==================== Lifecycle ====================
def close
@closed = true
@conn.close if @conn.respond_to?(:close)
end
def closed?
@closed
end
private
def check_closed!
raise ClientClosedError, "Client has been closed" if @closed
end
def with_retry
attempts = 0
begin
attempts += 1
response = yield
handle_response(response)
response
rescue Faraday::Error, ApiError => e
if attempts < @config.retries
sleep(attempts)
retry
end
raise
end
end
def handle_response(response)
return if response.success?
error_message = response.body["error"] || response.body["message"] || "Unknown error"
raise ApiError.new(error_message, status_code: response.status)
end
end
end

View file

@ -0,0 +1,269 @@
# frozen_string_literal: true
module SynorWallet
# Network environment
module Network
MAINNET = "mainnet"
TESTNET = "testnet"
DEVNET = "devnet"
end
# Wallet types
module WalletType
STANDARD = "standard"
MULTISIG = "multisig"
HARDWARE = "hardware"
end
# Address types
module AddressType
P2PKH = "p2pkh"
P2SH = "p2sh"
P2WPKH = "p2wpkh"
P2WSH = "p2wsh"
end
# Transaction priority
module Priority
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
end
# Configuration
class Config
attr_reader :api_key, :base_url, :network, :timeout, :retries, :debug
def initialize(
api_key:,
base_url: "https://wallet.synor.io/v1",
network: Network::MAINNET,
timeout: 30,
retries: 3,
debug: false
)
@api_key = api_key
@base_url = base_url
@network = network
@timeout = timeout
@retries = retries
@debug = debug
end
end
# Address information
class Address
attr_reader :address, :type, :index, :is_change, :public_key
def initialize(address:, type:, index:, is_change: false, public_key: nil)
@address = address
@type = type
@index = index
@is_change = is_change
@public_key = public_key
end
def self.from_hash(h)
new(
address: h["address"],
type: h["type"],
index: h["index"],
is_change: h["is_change"] || false,
public_key: h["public_key"]
)
end
end
# Stealth address
class StealthAddress
attr_reader :scan_public_key, :spend_public_key, :address
def initialize(scan_public_key:, spend_public_key:, address:)
@scan_public_key = scan_public_key
@spend_public_key = spend_public_key
@address = address
end
def self.from_hash(h)
new(
scan_public_key: h["scan_public_key"],
spend_public_key: h["spend_public_key"],
address: h["address"]
)
end
end
# Wallet information
class Wallet
attr_reader :id, :name, :type, :network, :addresses, :created_at
def initialize(id:, name:, type:, network:, addresses:, created_at:)
@id = id
@name = name
@type = type
@network = network
@addresses = addresses
@created_at = created_at
end
def self.from_hash(h)
new(
id: h["id"],
name: h["name"],
type: h["type"],
network: h["network"],
addresses: (h["addresses"] || []).map { |a| Address.from_hash(a) },
created_at: h["created_at"]
)
end
end
# Create wallet result
class CreateWalletResult
attr_reader :wallet, :mnemonic
def initialize(wallet:, mnemonic:)
@wallet = wallet
@mnemonic = mnemonic
end
def self.from_hash(h)
new(
wallet: Wallet.from_hash(h["wallet"]),
mnemonic: h["mnemonic"]
)
end
end
# Balance information
class Balance
attr_reader :confirmed, :unconfirmed, :total
def initialize(confirmed:, unconfirmed:, total:)
@confirmed = confirmed
@unconfirmed = unconfirmed
@total = total
end
def self.from_hash(h)
new(
confirmed: h["confirmed"],
unconfirmed: h["unconfirmed"],
total: h["total"]
)
end
end
# UTXO
class UTXO
attr_reader :txid, :vout, :value, :confirmations, :script_pubkey
def initialize(txid:, vout:, value:, confirmations:, script_pubkey:)
@txid = txid
@vout = vout
@value = value
@confirmations = confirmations
@script_pubkey = script_pubkey
end
def self.from_hash(h)
new(
txid: h["txid"],
vout: h["vout"],
value: h["value"],
confirmations: h["confirmations"],
script_pubkey: h["script_pubkey"]
)
end
end
# Transaction input
class TransactionInput
attr_reader :txid, :vout, :sequence
def initialize(txid:, vout:, sequence: 0xFFFFFFFF)
@txid = txid
@vout = vout
@sequence = sequence
end
def to_h
{ txid: @txid, vout: @vout, sequence: @sequence }
end
end
# Transaction output
class TransactionOutput
attr_reader :address, :value
def initialize(address:, value:)
@address = address
@value = value
end
def to_h
{ address: @address, value: @value }
end
end
# Transaction
class Transaction
attr_reader :inputs, :outputs, :lock_time
def initialize(inputs:, outputs:, lock_time: 0)
@inputs = inputs
@outputs = outputs
@lock_time = lock_time
end
def to_h
{
inputs: @inputs.map(&:to_h),
outputs: @outputs.map(&:to_h),
lock_time: @lock_time
}
end
end
# Signed transaction
class SignedTransaction
attr_reader :txid, :raw, :size, :weight, :fee
def initialize(txid:, raw:, size:, weight:, fee:)
@txid = txid
@raw = raw
@size = size
@weight = weight
@fee = fee
end
def self.from_hash(h)
new(
txid: h["txid"],
raw: h["raw"],
size: h["size"],
weight: h["weight"],
fee: h["fee"]
)
end
end
# Signature
class Signature
attr_reader :signature, :recovery_id, :address
def initialize(signature:, recovery_id:, address:)
@signature = signature
@recovery_id = recovery_id
@address = address
end
def self.from_hash(h)
new(
signature: h["signature"],
recovery_id: h["recovery_id"],
address: h["address"]
)
end
end
end

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
module SynorWallet
VERSION = "0.1.0"
end

View file

@ -0,0 +1,23 @@
Gem::Specification.new do |spec|
spec.name = "synor_rpc"
spec.version = "0.1.0"
spec.authors = ["Synor"]
spec.email = ["sdk@synor.io"]
spec.summary = "Ruby SDK for Synor RPC - Blockchain Data Queries"
spec.description = "Blockchain data queries, transaction submission, and real-time subscriptions via WebSocket."
spec.homepage = "https://github.com/synor/synor-rpc-ruby"
spec.license = "MIT"
spec.required_ruby_version = ">= 3.0"
spec.files = Dir["lib/synor_rpc.rb", "lib/synor_rpc/**/*", "LICENSE", "README.md"]
spec.require_paths = ["lib"]
spec.add_dependency "faraday", "~> 2.0"
spec.add_dependency "websocket-client-simple", "~> 0.6"
spec.add_development_dependency "bundler", "~> 2.0"
spec.add_development_dependency "rake", "~> 13.0"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "webmock", "~> 3.0"
end

View file

@ -0,0 +1,23 @@
Gem::Specification.new do |spec|
spec.name = "synor_storage"
spec.version = "0.1.0"
spec.authors = ["Synor"]
spec.email = ["sdk@synor.io"]
spec.summary = "Ruby SDK for Synor Storage - Decentralized Storage"
spec.description = "Decentralized storage operations including upload, download, pinning, and directory management."
spec.homepage = "https://github.com/synor/synor-storage-ruby"
spec.license = "MIT"
spec.required_ruby_version = ">= 3.0"
spec.files = Dir["lib/synor_storage.rb", "lib/synor_storage/**/*", "LICENSE", "README.md"]
spec.require_paths = ["lib"]
spec.add_dependency "faraday", "~> 2.0"
spec.add_dependency "faraday-multipart", "~> 1.0"
spec.add_development_dependency "bundler", "~> 2.0"
spec.add_development_dependency "rake", "~> 13.0"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "webmock", "~> 3.0"
end

View file

@ -0,0 +1,23 @@
Gem::Specification.new do |spec|
spec.name = "synor_wallet"
spec.version = "0.1.0"
spec.authors = ["Synor"]
spec.email = ["sdk@synor.io"]
spec.summary = "Ruby SDK for Synor Wallet - Blockchain Key Management"
spec.description = "Key management, transaction signing, and balance queries for the Synor blockchain."
spec.homepage = "https://github.com/synor/synor-wallet-ruby"
spec.license = "MIT"
spec.required_ruby_version = ">= 3.0"
spec.files = Dir["lib/synor_wallet.rb", "lib/synor_wallet/**/*", "LICENSE", "README.md"]
spec.require_paths = ["lib"]
spec.add_dependency "faraday", "~> 2.0"
spec.add_dependency "faraday-multipart", "~> 1.0"
spec.add_development_dependency "bundler", "~> 2.0"
spec.add_development_dependency "rake", "~> 13.0"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "webmock", "~> 3.0"
end

View file

@ -0,0 +1,554 @@
//! Bridge client implementation
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use serde::{de::DeserializeOwned, Serialize};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use super::error::{BridgeError, Result};
use super::types::*;
/// Synor Bridge Client
pub struct BridgeClient {
config: BridgeConfig,
http_client: reqwest::Client,
closed: Arc<AtomicBool>,
}
impl BridgeClient {
/// Create a new bridge client
pub fn new(config: BridgeConfig) -> Self {
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", config.api_key)).unwrap(),
);
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
headers.insert("X-SDK-Version", HeaderValue::from_static("rust/0.1.0"));
let http_client = reqwest::Client::builder()
.default_headers(headers)
.timeout(Duration::from_secs(config.timeout_secs))
.build()
.expect("Failed to create HTTP client");
Self {
config,
http_client,
closed: Arc::new(AtomicBool::new(false)),
}
}
// ==================== Chain Operations ====================
/// Get all supported chains
pub async fn get_supported_chains(&self) -> Result<Vec<Chain>> {
#[derive(Deserialize)]
struct Response {
chains: Vec<Chain>,
}
let resp: Response = self.request("GET", "/chains", Option::<()>::None).await?;
Ok(resp.chains)
}
/// Get chain by ID
pub async fn get_chain(&self, chain_id: ChainId) -> Result<Chain> {
self.request("GET", &format!("/chains/{:?}", chain_id).to_lowercase(), Option::<()>::None).await
}
/// Check if chain is supported
pub async fn is_chain_supported(&self, chain_id: ChainId) -> bool {
match self.get_chain(chain_id).await {
Ok(chain) => chain.supported,
Err(_) => false,
}
}
// ==================== Asset Operations ====================
/// Get supported assets for a chain
pub async fn get_supported_assets(&self, chain_id: ChainId) -> Result<Vec<Asset>> {
#[derive(Deserialize)]
struct Response {
assets: Vec<Asset>,
}
let resp: Response = self.request(
"GET",
&format!("/chains/{:?}/assets", chain_id).to_lowercase(),
Option::<()>::None,
).await?;
Ok(resp.assets)
}
/// Get asset by ID
pub async fn get_asset(&self, asset_id: &str) -> Result<Asset> {
self.request("GET", &format!("/assets/{}", urlencoding::encode(asset_id)), Option::<()>::None).await
}
/// Get wrapped asset mapping
pub async fn get_wrapped_asset(&self, original_asset_id: &str, target_chain: ChainId) -> Result<WrappedAsset> {
self.request(
"GET",
&format!("/assets/{}/wrapped/{:?}", urlencoding::encode(original_asset_id), target_chain).to_lowercase(),
Option::<()>::None,
).await
}
/// Get all wrapped assets for a chain
pub async fn get_wrapped_assets(&self, chain_id: ChainId) -> Result<Vec<WrappedAsset>> {
#[derive(Deserialize)]
struct Response {
assets: Vec<WrappedAsset>,
}
let resp: Response = self.request(
"GET",
&format!("/chains/{:?}/wrapped", chain_id).to_lowercase(),
Option::<()>::None,
).await?;
Ok(resp.assets)
}
// ==================== Fee & Rate Operations ====================
/// Estimate bridge fee
pub async fn estimate_fee(
&self,
asset: &str,
amount: &str,
source_chain: ChainId,
target_chain: ChainId,
) -> Result<FeeEstimate> {
#[derive(Serialize)]
struct Request {
asset: String,
amount: String,
#[serde(rename = "sourceChain")]
source_chain: ChainId,
#[serde(rename = "targetChain")]
target_chain: ChainId,
}
self.request("POST", "/fees/estimate", Some(Request {
asset: asset.to_string(),
amount: amount.to_string(),
source_chain,
target_chain,
})).await
}
/// Get exchange rate between assets
pub async fn get_exchange_rate(&self, from_asset: &str, to_asset: &str) -> Result<ExchangeRate> {
self.request(
"GET",
&format!("/rates/{}/{}", urlencoding::encode(from_asset), urlencoding::encode(to_asset)),
Option::<()>::None,
).await
}
// ==================== Lock-Mint Flow ====================
/// Lock assets on source chain
pub async fn lock(
&self,
asset: &str,
amount: &str,
target_chain: ChainId,
options: Option<LockOptions>,
) -> Result<LockReceipt> {
#[derive(Serialize)]
struct Request {
asset: String,
amount: String,
#[serde(rename = "targetChain")]
target_chain: ChainId,
#[serde(flatten)]
options: Option<LockOptions>,
}
self.request("POST", "/transfers/lock", Some(Request {
asset: asset.to_string(),
amount: amount.to_string(),
target_chain,
options,
})).await
}
/// Get lock proof for minting
pub async fn get_lock_proof(&self, lock_receipt_id: &str) -> Result<LockProof> {
self.request(
"GET",
&format!("/transfers/lock/{}/proof", urlencoding::encode(lock_receipt_id)),
Option::<()>::None,
).await
}
/// Wait for lock proof to be ready
pub async fn wait_for_lock_proof(
&self,
lock_receipt_id: &str,
poll_interval: Option<Duration>,
max_wait: Option<Duration>,
) -> Result<LockProof> {
let poll_interval = poll_interval.unwrap_or(Duration::from_secs(5));
let max_wait = max_wait.unwrap_or(Duration::from_secs(600));
let deadline = std::time::Instant::now() + max_wait;
loop {
if std::time::Instant::now() >= deadline {
return Err(BridgeError::new("Timeout waiting for lock proof")
.with_code("CONFIRMATIONS_PENDING"));
}
match self.get_lock_proof(lock_receipt_id).await {
Ok(proof) => return Ok(proof),
Err(e) if e.is_confirmations_pending() => {
tokio::time::sleep(poll_interval).await;
}
Err(e) => return Err(e),
}
}
}
/// Mint wrapped tokens on target chain
pub async fn mint(
&self,
proof: &LockProof,
target_address: &str,
options: Option<MintOptions>,
) -> Result<SignedTransaction> {
#[derive(Serialize)]
struct Request<'a> {
proof: &'a LockProof,
#[serde(rename = "targetAddress")]
target_address: String,
#[serde(flatten)]
options: Option<MintOptions>,
}
self.request("POST", "/transfers/mint", Some(Request {
proof,
target_address: target_address.to_string(),
options,
})).await
}
// ==================== Burn-Unlock Flow ====================
/// Burn wrapped tokens
pub async fn burn(
&self,
wrapped_asset: &str,
amount: &str,
options: Option<BurnOptions>,
) -> Result<BurnReceipt> {
#[derive(Serialize)]
struct Request {
#[serde(rename = "wrappedAsset")]
wrapped_asset: String,
amount: String,
#[serde(flatten)]
options: Option<BurnOptions>,
}
self.request("POST", "/transfers/burn", Some(Request {
wrapped_asset: wrapped_asset.to_string(),
amount: amount.to_string(),
options,
})).await
}
/// Get burn proof for unlocking
pub async fn get_burn_proof(&self, burn_receipt_id: &str) -> Result<BurnProof> {
self.request(
"GET",
&format!("/transfers/burn/{}/proof", urlencoding::encode(burn_receipt_id)),
Option::<()>::None,
).await
}
/// Wait for burn proof to be ready
pub async fn wait_for_burn_proof(
&self,
burn_receipt_id: &str,
poll_interval: Option<Duration>,
max_wait: Option<Duration>,
) -> Result<BurnProof> {
let poll_interval = poll_interval.unwrap_or(Duration::from_secs(5));
let max_wait = max_wait.unwrap_or(Duration::from_secs(600));
let deadline = std::time::Instant::now() + max_wait;
loop {
if std::time::Instant::now() >= deadline {
return Err(BridgeError::new("Timeout waiting for burn proof")
.with_code("CONFIRMATIONS_PENDING"));
}
match self.get_burn_proof(burn_receipt_id).await {
Ok(proof) => return Ok(proof),
Err(e) if e.is_confirmations_pending() => {
tokio::time::sleep(poll_interval).await;
}
Err(e) => return Err(e),
}
}
}
/// Unlock original tokens
pub async fn unlock(&self, proof: &BurnProof, options: Option<UnlockOptions>) -> Result<SignedTransaction> {
#[derive(Serialize)]
struct Request<'a> {
proof: &'a BurnProof,
#[serde(flatten)]
options: Option<UnlockOptions>,
}
self.request("POST", "/transfers/unlock", Some(Request { proof, options })).await
}
// ==================== Transfer Management ====================
/// Get transfer by ID
pub async fn get_transfer(&self, transfer_id: &str) -> Result<Transfer> {
self.request(
"GET",
&format!("/transfers/{}", urlencoding::encode(transfer_id)),
Option::<()>::None,
).await
}
/// Get transfer status
pub async fn get_transfer_status(&self, transfer_id: &str) -> Result<TransferStatus> {
let transfer = self.get_transfer(transfer_id).await?;
Ok(transfer.status)
}
/// List transfers
pub async fn list_transfers(&self, filter: Option<TransferFilter>) -> Result<Vec<Transfer>> {
let mut params = vec![];
if let Some(f) = &filter {
if let Some(status) = f.status {
params.push(format!("status={:?}", status).to_lowercase());
}
if let Some(chain) = f.source_chain {
params.push(format!("sourceChain={:?}", chain).to_lowercase());
}
if let Some(chain) = f.target_chain {
params.push(format!("targetChain={:?}", chain).to_lowercase());
}
if let Some(ref asset) = f.asset {
params.push(format!("asset={}", urlencoding::encode(asset)));
}
if let Some(ref sender) = f.sender {
params.push(format!("sender={}", urlencoding::encode(sender)));
}
if let Some(ref recipient) = f.recipient {
params.push(format!("recipient={}", urlencoding::encode(recipient)));
}
if let Some(from_date) = f.from_date {
params.push(format!("fromDate={}", from_date));
}
if let Some(to_date) = f.to_date {
params.push(format!("toDate={}", to_date));
}
if let Some(limit) = f.limit {
params.push(format!("limit={}", limit));
}
if let Some(offset) = f.offset {
params.push(format!("offset={}", offset));
}
}
let path = if params.is_empty() {
"/transfers".to_string()
} else {
format!("/transfers?{}", params.join("&"))
};
#[derive(Deserialize)]
struct Response {
transfers: Vec<Transfer>,
}
let resp: Response = self.request("GET", &path, Option::<()>::None).await?;
Ok(resp.transfers)
}
/// Wait for transfer to complete
pub async fn wait_for_transfer(
&self,
transfer_id: &str,
poll_interval: Option<Duration>,
max_wait: Option<Duration>,
) -> Result<Transfer> {
let poll_interval = poll_interval.unwrap_or(Duration::from_secs(10));
let max_wait = max_wait.unwrap_or(Duration::from_secs(1800));
let deadline = std::time::Instant::now() + max_wait;
loop {
if std::time::Instant::now() >= deadline {
return Err(BridgeError::new("Timeout waiting for transfer completion"));
}
let transfer = self.get_transfer(transfer_id).await?;
match transfer.status {
TransferStatus::Completed | TransferStatus::Failed | TransferStatus::Refunded => {
return Ok(transfer);
}
_ => {
tokio::time::sleep(poll_interval).await;
}
}
}
}
// ==================== Convenience Methods ====================
/// Execute complete lock-mint transfer
pub async fn bridge_to(
&self,
asset: &str,
amount: &str,
target_chain: ChainId,
target_address: &str,
lock_options: Option<LockOptions>,
mint_options: Option<MintOptions>,
) -> Result<Transfer> {
// Lock on source chain
let lock_receipt = self.lock(asset, amount, target_chain, lock_options).await?;
if self.config.debug {
eprintln!("Locked: {}, waiting for confirmations...", lock_receipt.id);
}
// Wait for proof
let proof = self.wait_for_lock_proof(&lock_receipt.id, None, None).await?;
if self.config.debug {
eprintln!("Proof ready, minting on {:?}...", target_chain);
}
// Mint on target chain
self.mint(&proof, target_address, mint_options).await?;
// Return final transfer status
self.wait_for_transfer(&lock_receipt.id, None, None).await
}
/// Execute complete burn-unlock transfer
pub async fn bridge_back(
&self,
wrapped_asset: &str,
amount: &str,
burn_options: Option<BurnOptions>,
unlock_options: Option<UnlockOptions>,
) -> Result<Transfer> {
// Burn wrapped tokens
let burn_receipt = self.burn(wrapped_asset, amount, burn_options).await?;
if self.config.debug {
eprintln!("Burned: {}, waiting for confirmations...", burn_receipt.id);
}
// Wait for proof
let proof = self.wait_for_burn_proof(&burn_receipt.id, None, None).await?;
if self.config.debug {
eprintln!("Proof ready, unlocking on {:?}...", burn_receipt.target_chain);
}
// Unlock on original chain
self.unlock(&proof, unlock_options).await?;
// Return final transfer status
self.wait_for_transfer(&burn_receipt.id, None, None).await
}
// ==================== Lifecycle ====================
/// Close the client
pub fn close(&self) {
self.closed.store(true, Ordering::SeqCst);
}
/// Check if client is closed
pub fn is_closed(&self) -> bool {
self.closed.load(Ordering::SeqCst)
}
/// Health check
pub async fn health_check(&self) -> bool {
#[derive(Deserialize)]
struct Response {
status: String,
}
match self.request::<_, Response>("GET", "/health", Option::<()>::None).await {
Ok(resp) => resp.status == "healthy",
Err(_) => false,
}
}
async fn request<B: Serialize, R: DeserializeOwned>(
&self,
method: &str,
path: &str,
body: Option<B>,
) -> Result<R> {
if self.is_closed() {
return Err(BridgeError::new("Client has been closed"));
}
let mut last_error = None;
for attempt in 0..self.config.retries {
match self.do_request(method, path, &body).await {
Ok(result) => return Ok(result),
Err(e) => {
if self.config.debug {
eprintln!("Attempt {} failed: {}", attempt + 1, e);
}
last_error = Some(e);
if attempt < self.config.retries - 1 {
tokio::time::sleep(Duration::from_secs((attempt + 1) as u64)).await;
}
}
}
}
Err(last_error.unwrap_or_else(|| BridgeError::new("Unknown error after retries")))
}
async fn do_request<B: Serialize, R: DeserializeOwned>(
&self,
method: &str,
path: &str,
body: &Option<B>,
) -> Result<R> {
let url = format!("{}{}", self.config.endpoint, path);
let mut request = match method {
"GET" => self.http_client.get(&url),
"POST" => self.http_client.post(&url),
"PUT" => self.http_client.put(&url),
"PATCH" => self.http_client.patch(&url),
"DELETE" => self.http_client.delete(&url),
_ => return Err(BridgeError::new(format!("Unknown method: {}", method))),
};
if let Some(b) = body {
request = request.json(b);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_body: serde_json::Value = response.json().await.unwrap_or_default();
let message = error_body["message"]
.as_str()
.or_else(|| error_body["error"].as_str())
.unwrap_or(&format!("HTTP {}", status.as_u16()));
return Err(BridgeError::new(message)
.with_status(status.as_u16())
.with_code(error_body["code"].as_str().unwrap_or("").to_string()));
}
let result = response.json().await?;
Ok(result)
}
}

View file

@ -0,0 +1,58 @@
//! Bridge error types
use std::fmt;
/// Bridge error
#[derive(Debug)]
pub struct BridgeError {
pub message: String,
pub code: Option<String>,
pub status_code: Option<u16>,
}
impl BridgeError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
code: None,
status_code: None,
}
}
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
pub fn with_status(mut self, status: u16) -> Self {
self.status_code = Some(status);
self
}
pub fn is_confirmations_pending(&self) -> bool {
self.code.as_deref() == Some("CONFIRMATIONS_PENDING")
}
}
impl fmt::Display for BridgeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for BridgeError {}
impl From<reqwest::Error> for BridgeError {
fn from(err: reqwest::Error) -> Self {
Self::new(err.to_string())
}
}
impl From<serde_json::Error> for BridgeError {
fn from(err: serde_json::Error) -> Self {
Self::new(err.to_string())
}
}
/// Result type alias
pub type Result<T> = std::result::Result<T, BridgeError>;

View file

@ -0,0 +1,11 @@
//! Synor Bridge SDK for Rust
//!
//! Cross-chain asset transfers with lock-mint and burn-unlock patterns.
mod types;
mod error;
mod client;
pub use types::*;
pub use error::*;
pub use client::*;

View file

@ -0,0 +1,368 @@
//! Bridge types
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// ==================== Chain Types ====================
/// Supported blockchain networks
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ChainId {
Synor,
Ethereum,
Polygon,
Arbitrum,
Optimism,
#[serde(rename = "bsc")]
BSC,
Avalanche,
Solana,
Cosmos,
}
/// Native currency info
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NativeCurrency {
pub name: String,
pub symbol: String,
pub decimals: u8,
}
/// Chain information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Chain {
pub id: ChainId,
pub name: String,
#[serde(rename = "chainId")]
pub chain_id: u64,
#[serde(rename = "rpcUrl")]
pub rpc_url: String,
#[serde(rename = "explorerUrl")]
pub explorer_url: String,
#[serde(rename = "nativeCurrency")]
pub native_currency: NativeCurrency,
pub confirmations: u32,
#[serde(rename = "estimatedBlockTime")]
pub estimated_block_time: u32,
pub supported: bool,
}
// ==================== Asset Types ====================
/// Asset type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AssetType {
Native,
#[serde(rename = "erc20")]
ERC20,
#[serde(rename = "erc721")]
ERC721,
#[serde(rename = "erc1155")]
ERC1155,
}
/// Asset information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Asset {
pub id: String,
pub symbol: String,
pub name: String,
#[serde(rename = "type")]
pub asset_type: AssetType,
pub chain: ChainId,
#[serde(rename = "contractAddress", skip_serializing_if = "Option::is_none")]
pub contract_address: Option<String>,
pub decimals: u8,
#[serde(rename = "logoUrl", skip_serializing_if = "Option::is_none")]
pub logo_url: Option<String>,
#[serde(default)]
pub verified: bool,
}
/// Wrapped asset mapping
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WrappedAsset {
#[serde(rename = "originalAsset")]
pub original_asset: Asset,
#[serde(rename = "wrappedAsset")]
pub wrapped_asset: Asset,
pub chain: ChainId,
#[serde(rename = "bridgeContract")]
pub bridge_contract: String,
}
// ==================== Transfer Types ====================
/// Transfer status
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TransferStatus {
Pending,
Locked,
Confirming,
Minting,
Completed,
Failed,
Refunded,
}
/// Transfer direction
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TransferDirection {
LockMint,
BurnUnlock,
}
/// Validator signature
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidatorSignature {
pub validator: String,
pub signature: String,
pub timestamp: i64,
}
/// Lock receipt
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockReceipt {
pub id: String,
#[serde(rename = "txHash")]
pub tx_hash: String,
#[serde(rename = "sourceChain")]
pub source_chain: ChainId,
#[serde(rename = "targetChain")]
pub target_chain: ChainId,
pub asset: Asset,
pub amount: String,
pub sender: String,
pub recipient: String,
#[serde(rename = "lockTimestamp")]
pub lock_timestamp: i64,
pub confirmations: u32,
#[serde(rename = "requiredConfirmations")]
pub required_confirmations: u32,
}
/// Lock proof for minting
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockProof {
#[serde(rename = "lockReceipt")]
pub lock_receipt: LockReceipt,
#[serde(rename = "merkleProof")]
pub merkle_proof: Vec<String>,
#[serde(rename = "blockHeader")]
pub block_header: String,
pub signatures: Vec<ValidatorSignature>,
}
/// Burn receipt
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BurnReceipt {
pub id: String,
#[serde(rename = "txHash")]
pub tx_hash: String,
#[serde(rename = "sourceChain")]
pub source_chain: ChainId,
#[serde(rename = "targetChain")]
pub target_chain: ChainId,
#[serde(rename = "wrappedAsset")]
pub wrapped_asset: Asset,
#[serde(rename = "originalAsset")]
pub original_asset: Asset,
pub amount: String,
pub sender: String,
pub recipient: String,
#[serde(rename = "burnTimestamp")]
pub burn_timestamp: i64,
pub confirmations: u32,
#[serde(rename = "requiredConfirmations")]
pub required_confirmations: u32,
}
/// Burn proof for unlocking
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BurnProof {
#[serde(rename = "burnReceipt")]
pub burn_receipt: BurnReceipt,
#[serde(rename = "merkleProof")]
pub merkle_proof: Vec<String>,
#[serde(rename = "blockHeader")]
pub block_header: String,
pub signatures: Vec<ValidatorSignature>,
}
/// Complete transfer record
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transfer {
pub id: String,
pub direction: TransferDirection,
pub status: TransferStatus,
#[serde(rename = "sourceChain")]
pub source_chain: ChainId,
#[serde(rename = "targetChain")]
pub target_chain: ChainId,
pub asset: Asset,
pub amount: String,
pub sender: String,
pub recipient: String,
#[serde(rename = "sourceTxHash", skip_serializing_if = "Option::is_none")]
pub source_tx_hash: Option<String>,
#[serde(rename = "targetTxHash", skip_serializing_if = "Option::is_none")]
pub target_tx_hash: Option<String>,
pub fee: String,
#[serde(rename = "feeAsset")]
pub fee_asset: Asset,
#[serde(rename = "createdAt")]
pub created_at: i64,
#[serde(rename = "updatedAt")]
pub updated_at: i64,
#[serde(rename = "completedAt", skip_serializing_if = "Option::is_none")]
pub completed_at: Option<i64>,
#[serde(rename = "errorMessage", skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
}
// ==================== Fee Types ====================
/// Fee estimate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeeEstimate {
#[serde(rename = "bridgeFee")]
pub bridge_fee: String,
#[serde(rename = "gasFeeSource")]
pub gas_fee_source: String,
#[serde(rename = "gasFeeTarget")]
pub gas_fee_target: String,
#[serde(rename = "totalFee")]
pub total_fee: String,
#[serde(rename = "feeAsset")]
pub fee_asset: Asset,
#[serde(rename = "estimatedTime")]
pub estimated_time: u32,
#[serde(rename = "exchangeRate", skip_serializing_if = "Option::is_none")]
pub exchange_rate: Option<String>,
}
/// Exchange rate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExchangeRate {
#[serde(rename = "fromAsset")]
pub from_asset: Asset,
#[serde(rename = "toAsset")]
pub to_asset: Asset,
pub rate: String,
#[serde(rename = "inverseRate")]
pub inverse_rate: String,
#[serde(rename = "lastUpdated")]
pub last_updated: i64,
pub source: String,
}
// ==================== Transaction Types ====================
/// Signed transaction
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedTransaction {
#[serde(rename = "txHash")]
pub tx_hash: String,
pub chain: ChainId,
pub from: String,
pub to: String,
pub value: String,
pub data: String,
#[serde(rename = "gasLimit")]
pub gas_limit: String,
#[serde(rename = "gasPrice", skip_serializing_if = "Option::is_none")]
pub gas_price: Option<String>,
#[serde(rename = "maxFeePerGas", skip_serializing_if = "Option::is_none")]
pub max_fee_per_gas: Option<String>,
#[serde(rename = "maxPriorityFeePerGas", skip_serializing_if = "Option::is_none")]
pub max_priority_fee_per_gas: Option<String>,
pub nonce: u64,
pub signature: String,
}
// ==================== Filter Types ====================
/// Transfer filter
#[derive(Debug, Clone, Default)]
pub struct TransferFilter {
pub status: Option<TransferStatus>,
pub source_chain: Option<ChainId>,
pub target_chain: Option<ChainId>,
pub asset: Option<String>,
pub sender: Option<String>,
pub recipient: Option<String>,
pub from_date: Option<i64>,
pub to_date: Option<i64>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
// ==================== Options Types ====================
/// Lock options
#[derive(Debug, Clone, Default, Serialize)]
pub struct LockOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub recipient: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deadline: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub slippage: Option<f64>,
}
/// Mint options
#[derive(Debug, Clone, Default, Serialize)]
pub struct MintOptions {
#[serde(rename = "gasLimit", skip_serializing_if = "Option::is_none")]
pub gas_limit: Option<String>,
#[serde(rename = "maxFeePerGas", skip_serializing_if = "Option::is_none")]
pub max_fee_per_gas: Option<String>,
#[serde(rename = "maxPriorityFeePerGas", skip_serializing_if = "Option::is_none")]
pub max_priority_fee_per_gas: Option<String>,
}
/// Burn options
#[derive(Debug, Clone, Default, Serialize)]
pub struct BurnOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub recipient: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deadline: Option<i64>,
}
/// Unlock options
#[derive(Debug, Clone, Default, Serialize)]
pub struct UnlockOptions {
#[serde(rename = "gasLimit", skip_serializing_if = "Option::is_none")]
pub gas_limit: Option<String>,
#[serde(rename = "gasPrice", skip_serializing_if = "Option::is_none")]
pub gas_price: Option<String>,
}
// ==================== Config Types ====================
/// Bridge configuration
#[derive(Debug, Clone)]
pub struct BridgeConfig {
pub api_key: String,
pub endpoint: String,
pub timeout_secs: u64,
pub retries: u32,
pub debug: bool,
}
impl Default for BridgeConfig {
fn default() -> Self {
Self {
api_key: String::new(),
endpoint: "https://bridge.synor.io/v1".to_string(),
timeout_secs: 60,
retries: 3,
debug: false,
}
}
}

View file

@ -0,0 +1,543 @@
//! Database client implementation
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use serde::{de::DeserializeOwned, Serialize};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use super::error::{DatabaseError, Result};
use super::types::*;
/// Synor Database Client
pub struct Client {
config: Config,
http_client: reqwest::Client,
closed: Arc<AtomicBool>,
}
impl Client {
/// Create a new database client
pub fn new(config: Config) -> Self {
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", config.api_key)).unwrap(),
);
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
headers.insert("X-SDK-Version", HeaderValue::from_static("rust/0.1.0"));
let http_client = reqwest::Client::builder()
.default_headers(headers)
.timeout(Duration::from_secs(config.timeout_secs))
.build()
.expect("Failed to create HTTP client");
Self {
config,
http_client,
closed: Arc::new(AtomicBool::new(false)),
}
}
/// Get the key-value store
pub fn kv(&self) -> KeyValueStore<'_> {
KeyValueStore { client: self }
}
/// Get the document store
pub fn documents(&self) -> DocumentStore<'_> {
DocumentStore { client: self }
}
/// Get the vector store
pub fn vectors(&self) -> VectorStore<'_> {
VectorStore { client: self }
}
/// Get the time series store
pub fn timeseries(&self) -> TimeSeriesStore<'_> {
TimeSeriesStore { client: self }
}
/// Get database statistics
pub async fn get_stats(&self) -> Result<DatabaseStats> {
self.request("GET", "/stats", Option::<()>::None).await
}
/// Health check
pub async fn health_check(&self) -> bool {
#[derive(serde::Deserialize)]
struct HealthResponse {
status: String,
}
match self.request::<_, HealthResponse>("GET", "/health", Option::<()>::None).await {
Ok(resp) => resp.status == "healthy",
Err(_) => false,
}
}
/// Close the client
pub fn close(&self) {
self.closed.store(true, Ordering::SeqCst);
}
/// Check if client is closed
pub fn is_closed(&self) -> bool {
self.closed.load(Ordering::SeqCst)
}
async fn request<B: Serialize, R: DeserializeOwned>(
&self,
method: &str,
path: &str,
body: Option<B>,
) -> Result<R> {
if self.is_closed() {
return Err(DatabaseError::new("Client has been closed"));
}
let mut last_error = None;
for attempt in 0..self.config.retries {
match self.do_request(method, path, &body).await {
Ok(result) => return Ok(result),
Err(e) => {
if self.config.debug {
eprintln!("Attempt {} failed: {}", attempt + 1, e);
}
last_error = Some(e);
if attempt < self.config.retries - 1 {
tokio::time::sleep(Duration::from_secs((attempt + 1) as u64)).await;
}
}
}
}
Err(last_error.unwrap_or_else(|| DatabaseError::new("Unknown error after retries")))
}
async fn do_request<B: Serialize, R: DeserializeOwned>(
&self,
method: &str,
path: &str,
body: &Option<B>,
) -> Result<R> {
let url = format!("{}{}", self.config.endpoint, path);
let mut request = match method {
"GET" => self.http_client.get(&url),
"POST" => self.http_client.post(&url),
"PUT" => self.http_client.put(&url),
"PATCH" => self.http_client.patch(&url),
"DELETE" => self.http_client.delete(&url),
_ => return Err(DatabaseError::new(format!("Unknown method: {}", method))),
};
if let Some(b) = body {
request = request.json(b);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_body: serde_json::Value = response.json().await.unwrap_or_default();
let message = error_body["message"]
.as_str()
.or_else(|| error_body["error"].as_str())
.unwrap_or(&format!("HTTP {}", status.as_u16()));
return Err(DatabaseError::new(message)
.with_status(status.as_u16())
.with_code(error_body["code"].as_str().unwrap_or("").to_string()));
}
let result = response.json().await?;
Ok(result)
}
}
/// Key-Value store operations
pub struct KeyValueStore<'a> {
client: &'a Client,
}
impl<'a> KeyValueStore<'a> {
/// Get a value by key
pub async fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
#[derive(serde::Deserialize)]
struct Response<T> {
value: Option<T>,
}
let path = format!("/kv/{}", urlencoding::encode(key));
let resp: Response<T> = self.client.request("GET", &path, Option::<()>::None).await?;
Ok(resp.value)
}
/// Get a full entry with metadata
pub async fn get_entry<T: DeserializeOwned>(&self, key: &str) -> Result<Option<KeyValueEntry<T>>> {
let path = format!("/kv/{}/entry", urlencoding::encode(key));
self.client.request("GET", &path, Option::<()>::None).await
}
/// Set a value
pub async fn set<T: Serialize>(&self, key: &str, value: T, options: Option<SetOptions>) -> Result<()> {
#[derive(Serialize)]
struct SetRequest<T> {
value: T,
#[serde(flatten)]
options: Option<SetOptions>,
}
let path = format!("/kv/{}", urlencoding::encode(key));
let body = SetRequest { value, options };
self.client.request::<_, serde_json::Value>("PUT", &path, Some(body)).await?;
Ok(())
}
/// Delete a key
pub async fn delete(&self, key: &str) -> Result<bool> {
#[derive(serde::Deserialize)]
struct Response {
deleted: bool,
}
let path = format!("/kv/{}", urlencoding::encode(key));
let resp: Response = self.client.request("DELETE", &path, Option::<()>::None).await?;
Ok(resp.deleted)
}
/// Check if a key exists
pub async fn exists(&self, key: &str) -> Result<bool> {
#[derive(serde::Deserialize)]
struct Response {
exists: bool,
}
let path = format!("/kv/{}/exists", urlencoding::encode(key));
let resp: Response = self.client.request("GET", &path, Option::<()>::None).await?;
Ok(resp.exists)
}
/// List keys
pub async fn list<T: DeserializeOwned>(&self, options: Option<ListOptions>) -> Result<ListResult<T>> {
let mut path = "/kv".to_string();
if let Some(opts) = &options {
let mut params = vec![];
if let Some(prefix) = &opts.prefix {
params.push(format!("prefix={}", urlencoding::encode(prefix)));
}
if let Some(cursor) = &opts.cursor {
params.push(format!("cursor={}", urlencoding::encode(cursor)));
}
if let Some(limit) = opts.limit {
params.push(format!("limit={}", limit));
}
if !params.is_empty() {
path = format!("{}?{}", path, params.join("&"));
}
}
self.client.request("GET", &path, Option::<()>::None).await
}
/// Increment a numeric value
pub async fn incr(&self, key: &str, by: i64) -> Result<i64> {
#[derive(Serialize)]
struct IncrRequest {
by: i64,
}
#[derive(serde::Deserialize)]
struct Response {
value: i64,
}
let path = format!("/kv/{}/incr", urlencoding::encode(key));
let resp: Response = self.client.request("POST", &path, Some(IncrRequest { by })).await?;
Ok(resp.value)
}
}
/// Document store operations
pub struct DocumentStore<'a> {
client: &'a Client,
}
impl<'a> DocumentStore<'a> {
/// Create a new document
pub async fn create<T: Serialize + DeserializeOwned>(
&self,
collection: &str,
data: T,
options: Option<CreateDocumentOptions>,
) -> Result<Document<T>> {
#[derive(Serialize)]
struct CreateRequest<T> {
data: T,
#[serde(flatten)]
options: Option<CreateDocumentOptions>,
}
let path = format!("/documents/{}", urlencoding::encode(collection));
let body = CreateRequest { data, options };
self.client.request("POST", &path, Some(body)).await
}
/// Get a document by ID
pub async fn get<T: DeserializeOwned>(&self, collection: &str, id: &str) -> Result<Option<Document<T>>> {
let path = format!("/documents/{}/{}", urlencoding::encode(collection), urlencoding::encode(id));
self.client.request("GET", &path, Option::<()>::None).await
}
/// Update a document
pub async fn update<T: Serialize + DeserializeOwned>(
&self,
collection: &str,
id: &str,
update: T,
options: Option<UpdateDocumentOptions>,
) -> Result<Document<T>> {
#[derive(Serialize)]
struct UpdateRequest<T> {
update: T,
#[serde(flatten)]
options: Option<UpdateDocumentOptions>,
}
let path = format!("/documents/{}/{}", urlencoding::encode(collection), urlencoding::encode(id));
let body = UpdateRequest { update, options };
self.client.request("PATCH", &path, Some(body)).await
}
/// Delete a document
pub async fn delete(&self, collection: &str, id: &str) -> Result<bool> {
#[derive(serde::Deserialize)]
struct Response {
deleted: bool,
}
let path = format!("/documents/{}/{}", urlencoding::encode(collection), urlencoding::encode(id));
let resp: Response = self.client.request("DELETE", &path, Option::<()>::None).await?;
Ok(resp.deleted)
}
/// Query documents
pub async fn query<T: DeserializeOwned>(&self, collection: &str, options: Option<QueryOptions>) -> Result<QueryResult<T>> {
let path = format!("/documents/{}/query", urlencoding::encode(collection));
self.client.request("POST", &path, options.or(Some(QueryOptions::default()))).await
}
/// Count documents
pub async fn count(&self, collection: &str, filter: Option<serde_json::Value>) -> Result<u64> {
#[derive(Serialize)]
struct CountRequest {
filter: Option<serde_json::Value>,
}
#[derive(serde::Deserialize)]
struct Response {
count: u64,
}
let path = format!("/documents/{}/count", urlencoding::encode(collection));
let resp: Response = self.client.request("POST", &path, Some(CountRequest { filter })).await?;
Ok(resp.count)
}
}
/// Vector store operations
pub struct VectorStore<'a> {
client: &'a Client,
}
impl<'a> VectorStore<'a> {
/// Create a vector collection
pub async fn create_collection(&self, config: VectorCollectionConfig) -> Result<()> {
self.client.request::<_, serde_json::Value>("POST", "/vectors/collections", Some(config)).await?;
Ok(())
}
/// Delete a vector collection
pub async fn delete_collection(&self, name: &str) -> Result<()> {
let path = format!("/vectors/collections/{}", urlencoding::encode(name));
self.client.request::<_, serde_json::Value>("DELETE", &path, Option::<()>::None).await?;
Ok(())
}
/// Upsert vectors
pub async fn upsert(
&self,
collection: &str,
vectors: Vec<VectorEntry>,
options: Option<UpsertVectorOptions>,
) -> Result<u64> {
#[derive(Serialize)]
struct UpsertRequest {
vectors: Vec<VectorEntry>,
#[serde(flatten)]
options: Option<UpsertVectorOptions>,
}
#[derive(serde::Deserialize)]
struct Response {
upserted: u64,
}
let path = format!("/vectors/{}/upsert", urlencoding::encode(collection));
let resp: Response = self.client.request("POST", &path, Some(UpsertRequest { vectors, options })).await?;
Ok(resp.upserted)
}
/// Search for similar vectors
pub async fn search(
&self,
collection: &str,
vector: Vec<f64>,
options: Option<SearchOptions>,
) -> Result<Vec<SearchResult>> {
#[derive(Serialize)]
struct SearchRequest {
vector: Vec<f64>,
#[serde(flatten)]
options: Option<SearchOptions>,
}
#[derive(serde::Deserialize)]
struct Response {
results: Vec<SearchResult>,
}
let path = format!("/vectors/{}/search", urlencoding::encode(collection));
let resp: Response = self.client.request("POST", &path, Some(SearchRequest { vector, options })).await?;
Ok(resp.results)
}
/// Delete vectors by ID
pub async fn delete(&self, collection: &str, ids: Vec<String>, namespace: Option<String>) -> Result<u64> {
#[derive(Serialize)]
struct DeleteRequest {
ids: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
namespace: Option<String>,
}
#[derive(serde::Deserialize)]
struct Response {
deleted: u64,
}
let path = format!("/vectors/{}/delete", urlencoding::encode(collection));
let resp: Response = self.client.request("POST", &path, Some(DeleteRequest { ids, namespace })).await?;
Ok(resp.deleted)
}
}
/// Time series store operations
pub struct TimeSeriesStore<'a> {
client: &'a Client,
}
impl<'a> TimeSeriesStore<'a> {
/// Write data points
pub async fn write(
&self,
series: &str,
points: Vec<DataPoint>,
options: Option<WritePointsOptions>,
) -> Result<u64> {
#[derive(Serialize)]
struct WriteRequest {
points: Vec<DataPoint>,
#[serde(flatten)]
options: Option<WritePointsOptions>,
}
#[derive(serde::Deserialize)]
struct Response {
written: u64,
}
let path = format!("/timeseries/{}/write", urlencoding::encode(series));
let resp: Response = self.client.request("POST", &path, Some(WriteRequest { points, options })).await?;
Ok(resp.written)
}
/// Query a time series
pub async fn query(
&self,
series: &str,
range: TimeRange,
options: Option<TimeSeriesQueryOptions>,
) -> Result<TimeSeriesResult> {
#[derive(Serialize)]
struct QueryRequest {
range: TimeRange,
#[serde(flatten)]
options: Option<TimeSeriesQueryOptions>,
}
let path = format!("/timeseries/{}/query", urlencoding::encode(series));
self.client.request("POST", &path, Some(QueryRequest { range, options })).await
}
/// Delete data points
pub async fn delete(
&self,
series: &str,
range: TimeRange,
tags: Option<std::collections::HashMap<String, String>>,
) -> Result<u64> {
#[derive(Serialize)]
struct DeleteRequest {
range: TimeRange,
#[serde(skip_serializing_if = "Option::is_none")]
tags: Option<std::collections::HashMap<String, String>>,
}
#[derive(serde::Deserialize)]
struct Response {
deleted: u64,
}
let path = format!("/timeseries/{}/delete", urlencoding::encode(series));
let resp: Response = self.client.request("POST", &path, Some(DeleteRequest { range, tags })).await?;
Ok(resp.deleted)
}
/// Get series info
pub async fn get_series_info(&self, series: &str) -> Result<SeriesInfo> {
let path = format!("/timeseries/{}/info", urlencoding::encode(series));
self.client.request("GET", &path, Option::<()>::None).await
}
/// List all series
pub async fn list_series(&self, prefix: Option<&str>) -> Result<Vec<SeriesInfo>> {
#[derive(serde::Deserialize)]
struct Response {
series: Vec<SeriesInfo>,
}
let path = match prefix {
Some(p) => format!("/timeseries?prefix={}", urlencoding::encode(p)),
None => "/timeseries".to_string(),
};
let resp: Response = self.client.request("GET", &path, Option::<()>::None).await?;
Ok(resp.series)
}
/// Set retention policy
pub async fn set_retention(&self, series: &str, retention_days: i32) -> Result<()> {
#[derive(Serialize)]
struct Request {
retention_days: i32,
}
let path = format!("/timeseries/{}/retention", urlencoding::encode(series));
self.client.request::<_, serde_json::Value>("PUT", &path, Some(Request { retention_days })).await?;
Ok(())
}
}

View file

@ -0,0 +1,54 @@
//! Database error types
use std::fmt;
/// Database error
#[derive(Debug)]
pub struct DatabaseError {
pub message: String,
pub code: Option<String>,
pub status_code: Option<u16>,
}
impl DatabaseError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
code: None,
status_code: None,
}
}
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
pub fn with_status(mut self, status: u16) -> Self {
self.status_code = Some(status);
self
}
}
impl fmt::Display for DatabaseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for DatabaseError {}
impl From<reqwest::Error> for DatabaseError {
fn from(err: reqwest::Error) -> Self {
Self::new(err.to_string())
}
}
impl From<serde_json::Error> for DatabaseError {
fn from(err: serde_json::Error) -> Self {
Self::new(err.to_string())
}
}
/// Result type alias
pub type Result<T> = std::result::Result<T, DatabaseError>;

View file

@ -0,0 +1,37 @@
//! Synor Database SDK for Rust
//!
//! Multi-model database: Key-Value, Document, Vector, and Time Series.
//!
//! # Example
//!
//! ```rust,no_run
//! use synor_sdk::database::{Client, Config};
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let client = Client::new(Config {
//! api_key: "your-api-key".to_string(),
//! ..Default::default()
//! });
//!
//! // Key-Value operations
//! client.kv().set("user:1", serde_json::json!({"name": "Alice"}), None).await?;
//! let user = client.kv().get::<serde_json::Value>("user:1").await?;
//!
//! // Document operations
//! let doc = client.documents().create("users", serde_json::json!({"name": "Bob"}), None).await?;
//!
//! // Vector operations
//! client.vectors().upsert("embeddings", vec![VectorEntry { id: "1".into(), vector: vec![0.1, 0.2], ..Default::default() }], None).await?;
//!
//! Ok(())
//! }
//! ```
mod types;
mod error;
mod client;
pub use types::*;
pub use error::*;
pub use client::*;

Some files were not shown because too many files have changed in this diff Show more