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:
parent
59a7123535
commit
74b82d2bb2
113 changed files with 26722 additions and 531 deletions
|
|
@ -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
269
sdk/c/include/synor/rpc.h
Normal 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 */
|
||||
282
sdk/c/include/synor/storage.h
Normal file
282
sdk/c/include/synor/storage.h
Normal 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 */
|
||||
399
sdk/c/include/synor/wallet.h
Normal file
399
sdk/c/include/synor/wallet.h
Normal 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
249
sdk/c/src/common.c
Normal 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
221
sdk/c/src/rpc/rpc.c
Normal 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
234
sdk/c/src/storage/storage.c
Normal 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
355
sdk/c/src/wallet/wallet.c
Normal 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));
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
219
sdk/cpp/include/synor/rpc.hpp
Normal file
219
sdk/cpp/include/synor/rpc.hpp
Normal 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
|
||||
174
sdk/cpp/include/synor/storage.hpp
Normal file
174
sdk/cpp/include/synor/storage.hpp
Normal 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
|
||||
68
sdk/cpp/include/synor/synor.hpp
Normal file
68
sdk/cpp/include/synor/synor.hpp
Normal 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
|
||||
242
sdk/cpp/include/synor/wallet.hpp
Normal file
242
sdk/cpp/include/synor/wallet.hpp
Normal 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
145
sdk/cpp/src/rpc/rpc.cpp
Normal 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
|
||||
126
sdk/cpp/src/storage/storage.cpp
Normal file
126
sdk/cpp/src/storage/storage.cpp
Normal 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
|
||||
113
sdk/cpp/src/wallet/wallet.cpp
Normal file
113
sdk/cpp/src/wallet/wallet.cpp
Normal 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
|
||||
477
sdk/csharp/Synor.Sdk/Rpc/SynorRpc.cs
Normal file
477
sdk/csharp/Synor.Sdk/Rpc/SynorRpc.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
222
sdk/csharp/Synor.Sdk/Rpc/Types.cs
Normal file
222
sdk/csharp/Synor.Sdk/Rpc/Types.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
392
sdk/csharp/Synor.Sdk/Storage/SynorStorage.cs
Normal file
392
sdk/csharp/Synor.Sdk/Storage/SynorStorage.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
139
sdk/csharp/Synor.Sdk/Storage/Types.cs
Normal file
139
sdk/csharp/Synor.Sdk/Storage/Types.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
22
sdk/csharp/Synor.Sdk/Synor.Sdk.csproj
Normal file
22
sdk/csharp/Synor.Sdk/Synor.Sdk.csproj
Normal 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>
|
||||
256
sdk/csharp/Synor.Sdk/Wallet/SynorWallet.cs
Normal file
256
sdk/csharp/Synor.Sdk/Wallet/SynorWallet.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
167
sdk/csharp/Synor.Sdk/Wallet/Types.cs
Normal file
167
sdk/csharp/Synor.Sdk/Wallet/Types.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
335
sdk/flutter/lib/src/rpc/synor_rpc.dart
Normal file
335
sdk/flutter/lib/src/rpc/synor_rpc.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
424
sdk/flutter/lib/src/rpc/types.dart
Normal file
424
sdk/flutter/lib/src/rpc/types.dart
Normal 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';
|
||||
}
|
||||
436
sdk/flutter/lib/src/storage/synor_storage.dart
Normal file
436
sdk/flutter/lib/src/storage/synor_storage.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
279
sdk/flutter/lib/src/storage/types.dart
Normal file
279
sdk/flutter/lib/src/storage/types.dart
Normal 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';
|
||||
}
|
||||
244
sdk/flutter/lib/src/wallet/synor_wallet.dart
Normal file
244
sdk/flutter/lib/src/wallet/synor_wallet.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
330
sdk/flutter/lib/src/wallet/types.dart
Normal file
330
sdk/flutter/lib/src/wallet/types.dart
Normal 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';
|
||||
}
|
||||
60
sdk/flutter/lib/synor_sdk.dart
Normal file
60
sdk/flutter/lib/synor_sdk.dart
Normal 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';
|
||||
|
|
@ -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
625
sdk/go/bridge/client.go
Normal 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
271
sdk/go/bridge/types.go
Normal 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
658
sdk/go/database/client.go
Normal 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
278
sdk/go/database/types.go
Normal 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
470
sdk/go/hosting/client.go
Normal 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
276
sdk/go/hosting/types.go
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
371
sdk/java/src/main/java/io/synor/bridge/SynorBridge.java
Normal file
371
sdk/java/src/main/java/io/synor/bridge/SynorBridge.java
Normal 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; }
|
||||
}
|
||||
404
sdk/java/src/main/java/io/synor/bridge/Types.java
Normal file
404
sdk/java/src/main/java/io/synor/bridge/Types.java
Normal 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; }
|
||||
}
|
||||
309
sdk/java/src/main/java/io/synor/database/SynorDatabase.java
Normal file
309
sdk/java/src/main/java/io/synor/database/SynorDatabase.java
Normal 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;
|
||||
}
|
||||
}
|
||||
210
sdk/java/src/main/java/io/synor/database/Types.java
Normal file
210
sdk/java/src/main/java/io/synor/database/Types.java
Normal 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; }
|
||||
}
|
||||
304
sdk/java/src/main/java/io/synor/hosting/SynorHosting.java
Normal file
304
sdk/java/src/main/java/io/synor/hosting/SynorHosting.java
Normal 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; }
|
||||
}
|
||||
330
sdk/java/src/main/java/io/synor/hosting/Types.java
Normal file
330
sdk/java/src/main/java/io/synor/hosting/Types.java
Normal 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; }
|
||||
}
|
||||
226
sdk/java/src/main/java/io/synor/rpc/SynorRpc.java
Normal file
226
sdk/java/src/main/java/io/synor/rpc/SynorRpc.java
Normal 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; } }
|
||||
}
|
||||
240
sdk/java/src/main/java/io/synor/rpc/Types.java
Normal file
240
sdk/java/src/main/java/io/synor/rpc/Types.java
Normal 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; }
|
||||
}
|
||||
237
sdk/java/src/main/java/io/synor/storage/SynorStorage.java
Normal file
237
sdk/java/src/main/java/io/synor/storage/SynorStorage.java
Normal 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; } }
|
||||
}
|
||||
285
sdk/java/src/main/java/io/synor/storage/Types.java
Normal file
285
sdk/java/src/main/java/io/synor/storage/Types.java
Normal 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; }
|
||||
}
|
||||
163
sdk/java/src/main/java/io/synor/wallet/SynorWallet.java
Normal file
163
sdk/java/src/main/java/io/synor/wallet/SynorWallet.java
Normal 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; } }
|
||||
}
|
||||
240
sdk/java/src/main/java/io/synor/wallet/Types.java
Normal file
240
sdk/java/src/main/java/io/synor/wallet/Types.java
Normal 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
501
sdk/js/src/bridge/client.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
25
sdk/js/src/bridge/index.ts
Normal file
25
sdk/js/src/bridge/index.ts
Normal 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
279
sdk/js/src/bridge/types.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
659
sdk/js/src/database/client.ts
Normal file
659
sdk/js/src/database/client.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
7
sdk/js/src/database/index.ts
Normal file
7
sdk/js/src/database/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Synor Database SDK
|
||||
* Multi-model database: Key-Value, Document, Vector, and Time Series
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './client';
|
||||
234
sdk/js/src/database/types.ts
Normal file
234
sdk/js/src/database/types.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
396
sdk/js/src/hosting/client.ts
Normal file
396
sdk/js/src/hosting/client.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
7
sdk/js/src/hosting/index.ts
Normal file
7
sdk/js/src/hosting/index.ts
Normal 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
193
sdk/js/src/hosting/types.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
sdk/kotlin/settings.gradle.kts
Normal file
1
sdk/kotlin/settings.gradle.kts
Normal file
|
|
@ -0,0 +1 @@
|
|||
rootProject.name = "synor-sdk"
|
||||
355
sdk/kotlin/src/main/kotlin/io/synor/rpc/SynorRpc.kt
Normal file
355
sdk/kotlin/src/main/kotlin/io/synor/rpc/SynorRpc.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
237
sdk/kotlin/src/main/kotlin/io/synor/rpc/Types.kt
Normal file
237
sdk/kotlin/src/main/kotlin/io/synor/rpc/Types.kt
Normal 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)
|
||||
397
sdk/kotlin/src/main/kotlin/io/synor/storage/SynorStorage.kt
Normal file
397
sdk/kotlin/src/main/kotlin/io/synor/storage/SynorStorage.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
194
sdk/kotlin/src/main/kotlin/io/synor/storage/Types.kt
Normal file
194
sdk/kotlin/src/main/kotlin/io/synor/storage/Types.kt
Normal 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)
|
||||
246
sdk/kotlin/src/main/kotlin/io/synor/wallet/SynorWallet.kt
Normal file
246
sdk/kotlin/src/main/kotlin/io/synor/wallet/SynorWallet.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
230
sdk/kotlin/src/main/kotlin/io/synor/wallet/Types.kt
Normal file
230
sdk/kotlin/src/main/kotlin/io/synor/wallet/Types.kt
Normal 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)
|
||||
74
sdk/python/src/synor_bridge/__init__.py
Normal file
74
sdk/python/src/synor_bridge/__init__.py
Normal 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",
|
||||
]
|
||||
527
sdk/python/src/synor_bridge/client.py
Normal file
527
sdk/python/src/synor_bridge/client.py
Normal 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}")
|
||||
537
sdk/python/src/synor_bridge/types.py
Normal file
537
sdk/python/src/synor_bridge/types.py
Normal 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 {}
|
||||
71
sdk/python/src/synor_database/__init__.py
Normal file
71
sdk/python/src/synor_database/__init__.py
Normal 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",
|
||||
]
|
||||
633
sdk/python/src/synor_database/client.py
Normal file
633
sdk/python/src/synor_database/client.py
Normal 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")
|
||||
253
sdk/python/src/synor_database/types.py
Normal file
253
sdk/python/src/synor_database/types.py
Normal 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)
|
||||
54
sdk/python/src/synor_hosting/__init__.py
Normal file
54
sdk/python/src/synor_hosting/__init__.py
Normal 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",
|
||||
]
|
||||
487
sdk/python/src/synor_hosting/client.py
Normal file
487
sdk/python/src/synor_hosting/client.py
Normal 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),
|
||||
)
|
||||
241
sdk/python/src/synor_hosting/types.py
Normal file
241
sdk/python/src/synor_hosting/types.py
Normal 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
41
sdk/ruby/lib/synor_rpc.rb
Normal 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
|
||||
384
sdk/ruby/lib/synor_rpc/client.rb
Normal file
384
sdk/ruby/lib/synor_rpc/client.rb
Normal 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
|
||||
343
sdk/ruby/lib/synor_rpc/types.rb
Normal file
343
sdk/ruby/lib/synor_rpc/types.rb
Normal 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
|
||||
5
sdk/ruby/lib/synor_rpc/version.rb
Normal file
5
sdk/ruby/lib/synor_rpc/version.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SynorRpc
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
40
sdk/ruby/lib/synor_storage.rb
Normal file
40
sdk/ruby/lib/synor_storage.rb
Normal 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
|
||||
317
sdk/ruby/lib/synor_storage/client.rb
Normal file
317
sdk/ruby/lib/synor_storage/client.rb
Normal 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
|
||||
209
sdk/ruby/lib/synor_storage/types.rb
Normal file
209
sdk/ruby/lib/synor_storage/types.rb
Normal 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
|
||||
5
sdk/ruby/lib/synor_storage/version.rb
Normal file
5
sdk/ruby/lib/synor_storage/version.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SynorStorage
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
41
sdk/ruby/lib/synor_wallet.rb
Normal file
41
sdk/ruby/lib/synor_wallet.rb
Normal 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
|
||||
244
sdk/ruby/lib/synor_wallet/client.rb
Normal file
244
sdk/ruby/lib/synor_wallet/client.rb
Normal 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
|
||||
269
sdk/ruby/lib/synor_wallet/types.rb
Normal file
269
sdk/ruby/lib/synor_wallet/types.rb
Normal 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
|
||||
5
sdk/ruby/lib/synor_wallet/version.rb
Normal file
5
sdk/ruby/lib/synor_wallet/version.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SynorWallet
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
23
sdk/ruby/synor_rpc.gemspec
Normal file
23
sdk/ruby/synor_rpc.gemspec
Normal 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
|
||||
23
sdk/ruby/synor_storage.gemspec
Normal file
23
sdk/ruby/synor_storage.gemspec
Normal 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
|
||||
23
sdk/ruby/synor_wallet.gemspec
Normal file
23
sdk/ruby/synor_wallet.gemspec
Normal 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
|
||||
554
sdk/rust/src/bridge/client.rs
Normal file
554
sdk/rust/src/bridge/client.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
58
sdk/rust/src/bridge/error.rs
Normal file
58
sdk/rust/src/bridge/error.rs
Normal 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>;
|
||||
11
sdk/rust/src/bridge/mod.rs
Normal file
11
sdk/rust/src/bridge/mod.rs
Normal 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::*;
|
||||
368
sdk/rust/src/bridge/types.rs
Normal file
368
sdk/rust/src/bridge/types.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
543
sdk/rust/src/database/client.rs
Normal file
543
sdk/rust/src/database/client.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
54
sdk/rust/src/database/error.rs
Normal file
54
sdk/rust/src/database/error.rs
Normal 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>;
|
||||
37
sdk/rust/src/database/mod.rs
Normal file
37
sdk/rust/src/database/mod.rs
Normal 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
Loading…
Add table
Reference in a new issue