From 97add230628816937fac148a05441853194fb02b Mon Sep 17 00:00:00 2001 From: Gulshan Yadav Date: Wed, 28 Jan 2026 12:53:46 +0530 Subject: [PATCH] feat(sdk): implement IBC SDK for all 12 languages Implement Inter-Blockchain Communication (IBC) SDK with full ICS protocol support across all 12 programming languages: - JavaScript/TypeScript, Python, Go, Rust - Java, Kotlin, Swift, Flutter/Dart - C, C++, C#/.NET, Ruby Features: - Light client management (Tendermint, Solo Machine, WASM) - Connection handshake (4-way: Init, Try, Ack, Confirm) - Channel management with ordered/unordered support - ICS-20 fungible token transfers - HTLC atomic swaps with hashlock (SHA256) and timelock - Packet relay with timeout handling --- sdk/c/include/synor/ibc.h | 313 ++++++++ sdk/cpp/include/synor/ibc.hpp | 469 +++++++++++ sdk/csharp/src/Synor.Ibc/SynorIbc.cs | 407 ++++++++++ sdk/csharp/src/Synor.Ibc/Types.cs | 279 +++++++ sdk/flutter/lib/src/ibc/synor_ibc.dart | 278 +++++++ sdk/flutter/lib/src/ibc/types.dart | 359 +++++++++ sdk/go/ibc/client.go | 385 +++++++++ sdk/go/ibc/types.go | 407 ++++++++++ .../src/main/java/io/synor/ibc/SynorIbc.java | 325 ++++++++ .../src/main/java/io/synor/ibc/Types.java | 637 +++++++++++++++ sdk/js/src/ibc/client.ts | 760 ++++++++++++++++++ sdk/js/src/ibc/index.ts | 39 + sdk/js/src/ibc/types.ts | 559 +++++++++++++ .../src/main/kotlin/io/synor/ibc/SynorIbc.kt | 310 +++++++ .../src/main/kotlin/io/synor/ibc/Types.kt | 265 ++++++ sdk/python/src/synor_ibc/__init__.py | 164 ++++ sdk/python/src/synor_ibc/client.py | 620 ++++++++++++++ sdk/python/src/synor_ibc/types.py | 532 ++++++++++++ sdk/ruby/lib/synor/ibc.rb | 470 +++++++++++ sdk/rust/src/ibc/client.rs | 363 +++++++++ sdk/rust/src/ibc/mod.rs | 9 + sdk/rust/src/ibc/types.rs | 492 ++++++++++++ sdk/swift/Sources/SynorIbc/SynorIbc.swift | 342 ++++++++ sdk/swift/Sources/SynorIbc/Types.swift | 383 +++++++++ 24 files changed, 9167 insertions(+) create mode 100644 sdk/c/include/synor/ibc.h create mode 100644 sdk/cpp/include/synor/ibc.hpp create mode 100644 sdk/csharp/src/Synor.Ibc/SynorIbc.cs create mode 100644 sdk/csharp/src/Synor.Ibc/Types.cs create mode 100644 sdk/flutter/lib/src/ibc/synor_ibc.dart create mode 100644 sdk/flutter/lib/src/ibc/types.dart create mode 100644 sdk/go/ibc/client.go create mode 100644 sdk/go/ibc/types.go create mode 100644 sdk/java/src/main/java/io/synor/ibc/SynorIbc.java create mode 100644 sdk/java/src/main/java/io/synor/ibc/Types.java create mode 100644 sdk/js/src/ibc/client.ts create mode 100644 sdk/js/src/ibc/index.ts create mode 100644 sdk/js/src/ibc/types.ts create mode 100644 sdk/kotlin/src/main/kotlin/io/synor/ibc/SynorIbc.kt create mode 100644 sdk/kotlin/src/main/kotlin/io/synor/ibc/Types.kt create mode 100644 sdk/python/src/synor_ibc/__init__.py create mode 100644 sdk/python/src/synor_ibc/client.py create mode 100644 sdk/python/src/synor_ibc/types.py create mode 100644 sdk/ruby/lib/synor/ibc.rb create mode 100644 sdk/rust/src/ibc/client.rs create mode 100644 sdk/rust/src/ibc/mod.rs create mode 100644 sdk/rust/src/ibc/types.rs create mode 100644 sdk/swift/Sources/SynorIbc/SynorIbc.swift create mode 100644 sdk/swift/Sources/SynorIbc/Types.swift diff --git a/sdk/c/include/synor/ibc.h b/sdk/c/include/synor/ibc.h new file mode 100644 index 0000000..4e1f7a0 --- /dev/null +++ b/sdk/c/include/synor/ibc.h @@ -0,0 +1,313 @@ +/** + * @file ibc.h + * @brief Synor IBC SDK for C + * + * Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability. + */ + +#ifndef SYNOR_IBC_H +#define SYNOR_IBC_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Error codes */ +typedef enum { + SYNOR_IBC_OK = 0, + SYNOR_IBC_ERROR_CLIENT_CLOSED = -1, + SYNOR_IBC_ERROR_HTTP = -2, + SYNOR_IBC_ERROR_NETWORK = -3, + SYNOR_IBC_ERROR_INVALID_PARAM = -4, + SYNOR_IBC_ERROR_TIMEOUT = -5, + SYNOR_IBC_ERROR_JSON = -6, + SYNOR_IBC_ERROR_ALLOCATION = -7 +} synor_ibc_error_t; + +/* Enums */ +typedef enum { + SYNOR_IBC_CLIENT_TYPE_TENDERMINT, + SYNOR_IBC_CLIENT_TYPE_SOLO_MACHINE, + SYNOR_IBC_CLIENT_TYPE_LOCALHOST, + SYNOR_IBC_CLIENT_TYPE_WASM +} synor_ibc_client_type_t; + +typedef enum { + SYNOR_IBC_CONNECTION_STATE_UNINITIALIZED, + SYNOR_IBC_CONNECTION_STATE_INIT, + SYNOR_IBC_CONNECTION_STATE_TRYOPEN, + SYNOR_IBC_CONNECTION_STATE_OPEN +} synor_ibc_connection_state_t; + +typedef enum { + SYNOR_IBC_CHANNEL_ORDER_UNORDERED, + SYNOR_IBC_CHANNEL_ORDER_ORDERED +} synor_ibc_channel_order_t; + +typedef enum { + SYNOR_IBC_CHANNEL_STATE_UNINITIALIZED, + SYNOR_IBC_CHANNEL_STATE_INIT, + SYNOR_IBC_CHANNEL_STATE_TRYOPEN, + SYNOR_IBC_CHANNEL_STATE_OPEN, + SYNOR_IBC_CHANNEL_STATE_CLOSED +} synor_ibc_channel_state_t; + +typedef enum { + SYNOR_IBC_SWAP_STATE_PENDING, + SYNOR_IBC_SWAP_STATE_LOCKED, + SYNOR_IBC_SWAP_STATE_COMPLETED, + SYNOR_IBC_SWAP_STATE_REFUNDED, + SYNOR_IBC_SWAP_STATE_EXPIRED, + SYNOR_IBC_SWAP_STATE_CANCELLED +} synor_ibc_swap_state_t; + +/* Opaque handle types */ +typedef struct synor_ibc_client synor_ibc_client_t; +typedef struct synor_ibc_light_client synor_ibc_light_client_t; +typedef struct synor_ibc_connections synor_ibc_connections_t; +typedef struct synor_ibc_channels synor_ibc_channels_t; +typedef struct synor_ibc_transfer synor_ibc_transfer_t; +typedef struct synor_ibc_swaps synor_ibc_swaps_t; + +/* Configuration */ +typedef struct { + const char* api_key; + const char* endpoint; /* Default: "https://ibc.synor.io/v1" */ + const char* ws_endpoint; /* Default: "wss://ibc.synor.io/v1/ws" */ + uint32_t timeout_ms; /* Default: 30000 */ + uint32_t retries; /* Default: 3 */ + const char* chain_id; /* Default: "synor-1" */ + bool debug; /* Default: false */ +} synor_ibc_config_t; + +/* Data types */ +typedef struct { + uint64_t revision_number; + uint64_t revision_height; +} synor_ibc_height_t; + +typedef struct { + char* id; +} synor_ibc_chain_id_t; + +typedef struct { + char* identifier; + char** features; + size_t features_count; +} synor_ibc_version_t; + +typedef struct { + char* id; +} synor_ibc_client_id_t; + +typedef struct { + int numerator; + int denominator; +} synor_ibc_trust_level_t; + +typedef struct { + char* chain_id; + synor_ibc_trust_level_t trust_level; + uint64_t trusting_period; + uint64_t unbonding_period; + uint64_t max_clock_drift; + synor_ibc_height_t latest_height; + synor_ibc_height_t* frozen_height; /* NULL if not frozen */ +} synor_ibc_client_state_t; + +typedef struct { + char* id; +} synor_ibc_connection_id_t; + +typedef struct { + char* id; +} synor_ibc_port_id_t; + +typedef struct { + char* id; +} synor_ibc_channel_id_t; + +typedef struct { + synor_ibc_height_t height; + uint64_t timestamp; +} synor_ibc_timeout_t; + +typedef struct { + char* denom; + char* amount; + char* sender; + char* receiver; + char* memo; +} synor_ibc_fungible_token_packet_data_t; + +typedef struct { + char* id; +} synor_ibc_swap_id_t; + +typedef struct { + uint8_t* hash; + size_t hash_len; +} synor_ibc_hashlock_t; + +typedef struct { + uint64_t expiry; +} synor_ibc_timelock_t; + +typedef struct { + synor_ibc_swap_id_t swap_id; + synor_ibc_swap_state_t state; + char* initiator_htlc_json; + char* responder_htlc_json; /* NULL if not set */ +} synor_ibc_atomic_swap_t; + +typedef struct { + char* transaction_hash; + char* sequence; +} synor_ibc_transfer_result_t; + +/* Create light client params */ +typedef struct { + synor_ibc_client_type_t client_type; + synor_ibc_client_state_t client_state; + const char* consensus_state_json; +} synor_ibc_create_client_params_t; + +/* Open connection params */ +typedef struct { + synor_ibc_client_id_t client_id; + synor_ibc_client_id_t counterparty_client_id; +} synor_ibc_open_connection_params_t; + +/* Open channel params */ +typedef struct { + synor_ibc_port_id_t port_id; + synor_ibc_channel_order_t ordering; + synor_ibc_connection_id_t connection_id; + synor_ibc_port_id_t counterparty_port; + const char* version; +} synor_ibc_open_channel_params_t; + +/* Transfer params */ +typedef struct { + const char* source_port; + const char* source_channel; + const char* denom; + const char* amount; + const char* sender; + const char* receiver; + synor_ibc_timeout_t* timeout; /* NULL for no timeout */ + const char* memo; /* NULL if not set */ +} synor_ibc_transfer_params_t; + +/* Swap initiate params */ +typedef struct { + const char* responder; + const char* initiator_asset_json; + const char* responder_asset_json; +} synor_ibc_swap_initiate_params_t; + +/* Client lifecycle */ +synor_ibc_error_t synor_ibc_create(synor_ibc_client_t** client, const synor_ibc_config_t* config); +void synor_ibc_destroy(synor_ibc_client_t* client); +bool synor_ibc_is_closed(const synor_ibc_client_t* client); +void synor_ibc_close(synor_ibc_client_t* client); + +/* Chain operations */ +synor_ibc_error_t synor_ibc_get_chain_info(synor_ibc_client_t* client, char** result_json); +synor_ibc_error_t synor_ibc_get_height(synor_ibc_client_t* client, synor_ibc_height_t* height); +synor_ibc_error_t synor_ibc_health_check(synor_ibc_client_t* client, bool* healthy); + +/* Sub-clients */ +synor_ibc_light_client_t* synor_ibc_clients(synor_ibc_client_t* client); +synor_ibc_connections_t* synor_ibc_connections(synor_ibc_client_t* client); +synor_ibc_channels_t* synor_ibc_channels(synor_ibc_client_t* client); +synor_ibc_transfer_t* synor_ibc_transfer(synor_ibc_client_t* client); +synor_ibc_swaps_t* synor_ibc_swaps(synor_ibc_client_t* client); + +/* Light client operations */ +synor_ibc_error_t synor_ibc_light_client_create(synor_ibc_light_client_t* lc, + const synor_ibc_create_client_params_t* params, + synor_ibc_client_id_t** result); +synor_ibc_error_t synor_ibc_light_client_get_state(synor_ibc_light_client_t* lc, + const synor_ibc_client_id_t* client_id, + synor_ibc_client_state_t** state); +synor_ibc_error_t synor_ibc_light_client_list(synor_ibc_light_client_t* lc, + char** result_json); + +/* Connection operations */ +synor_ibc_error_t synor_ibc_connections_open_init(synor_ibc_connections_t* conn, + const synor_ibc_open_connection_params_t* params, + synor_ibc_connection_id_t** result); +synor_ibc_error_t synor_ibc_connections_get(synor_ibc_connections_t* conn, + const synor_ibc_connection_id_t* connection_id, + char** result_json); +synor_ibc_error_t synor_ibc_connections_list(synor_ibc_connections_t* conn, char** result_json); + +/* Channel operations */ +synor_ibc_error_t synor_ibc_channels_bind_port(synor_ibc_channels_t* ch, + const synor_ibc_port_id_t* port_id, + const char* module); +synor_ibc_error_t synor_ibc_channels_open_init(synor_ibc_channels_t* ch, + const synor_ibc_open_channel_params_t* params, + synor_ibc_channel_id_t** result); +synor_ibc_error_t synor_ibc_channels_get(synor_ibc_channels_t* ch, + const synor_ibc_port_id_t* port_id, + const synor_ibc_channel_id_t* channel_id, + char** result_json); +synor_ibc_error_t synor_ibc_channels_list(synor_ibc_channels_t* ch, char** result_json); + +/* Transfer operations (ICS-20) */ +synor_ibc_error_t synor_ibc_transfer_send(synor_ibc_transfer_t* tr, + const synor_ibc_transfer_params_t* params, + synor_ibc_transfer_result_t** result); +synor_ibc_error_t synor_ibc_transfer_get_denom_trace(synor_ibc_transfer_t* tr, + const char* ibc_denom, + char** result_json); + +/* Swap operations (HTLC) */ +synor_ibc_error_t synor_ibc_swaps_initiate(synor_ibc_swaps_t* sw, + const synor_ibc_swap_initiate_params_t* params, + char** result_json); +synor_ibc_error_t synor_ibc_swaps_lock(synor_ibc_swaps_t* sw, + const synor_ibc_swap_id_t* swap_id); +synor_ibc_error_t synor_ibc_swaps_respond(synor_ibc_swaps_t* sw, + const synor_ibc_swap_id_t* swap_id, + const char* asset_json, + char** result_json); +synor_ibc_error_t synor_ibc_swaps_claim(synor_ibc_swaps_t* sw, + const synor_ibc_swap_id_t* swap_id, + const uint8_t* secret, size_t secret_len, + char** result_json); +synor_ibc_error_t synor_ibc_swaps_refund(synor_ibc_swaps_t* sw, + const synor_ibc_swap_id_t* swap_id, + char** result_json); +synor_ibc_error_t synor_ibc_swaps_get(synor_ibc_swaps_t* sw, + const synor_ibc_swap_id_t* swap_id, + synor_ibc_atomic_swap_t** swap); +synor_ibc_error_t synor_ibc_swaps_list_active(synor_ibc_swaps_t* sw, + synor_ibc_atomic_swap_t** swaps, + size_t* count); + +/* Memory management */ +void synor_ibc_free_string(char* str); +void synor_ibc_free_height(synor_ibc_height_t* height); +void synor_ibc_free_client_id(synor_ibc_client_id_t* client_id); +void synor_ibc_free_client_state(synor_ibc_client_state_t* state); +void synor_ibc_free_connection_id(synor_ibc_connection_id_t* connection_id); +void synor_ibc_free_port_id(synor_ibc_port_id_t* port_id); +void synor_ibc_free_channel_id(synor_ibc_channel_id_t* channel_id); +void synor_ibc_free_swap_id(synor_ibc_swap_id_t* swap_id); +void synor_ibc_free_atomic_swap(synor_ibc_atomic_swap_t* swap); +void synor_ibc_free_atomic_swaps(synor_ibc_atomic_swap_t* swaps, size_t count); +void synor_ibc_free_transfer_result(synor_ibc_transfer_result_t* result); +void synor_ibc_free_version(synor_ibc_version_t* version); + +#ifdef __cplusplus +} +#endif + +#endif /* SYNOR_IBC_H */ diff --git a/sdk/cpp/include/synor/ibc.hpp b/sdk/cpp/include/synor/ibc.hpp new file mode 100644 index 0000000..9334bd3 --- /dev/null +++ b/sdk/cpp/include/synor/ibc.hpp @@ -0,0 +1,469 @@ +/** + * @file ibc.hpp + * @brief Synor IBC SDK for C++ + * + * Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability. + */ + +#ifndef SYNOR_IBC_HPP +#define SYNOR_IBC_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace synor { +namespace ibc { + +using json = nlohmann::json; + +/** + * IBC Height representation + */ +struct Height { + uint64_t revision_number = 0; + uint64_t revision_height = 1; + + bool is_zero() const { + return revision_number == 0 && revision_height == 0; + } + + Height increment() const { + return Height{revision_number, revision_height + 1}; + } + + static Height from_json(const json& j) { + return Height{ + j.value("revision_number", uint64_t(0)), + j.value("revision_height", uint64_t(1)) + }; + } + + json to_json() const { + return { + {"revision_number", revision_number}, + {"revision_height", revision_height} + }; + } +}; + +/** + * Light client types + */ +enum class ClientType { + Tendermint, + SoloMachine, + Localhost, + Wasm +}; + +inline std::string to_string(ClientType type) { + switch (type) { + case ClientType::Tendermint: return "tendermint"; + case ClientType::SoloMachine: return "solo_machine"; + case ClientType::Localhost: return "localhost"; + case ClientType::Wasm: return "wasm"; + } + return "unknown"; +} + +/** + * Trust level configuration + */ +struct TrustLevel { + int numerator = 1; + int denominator = 3; + + static TrustLevel from_json(const json& j) { + return TrustLevel{ + j.value("numerator", 1), + j.value("denominator", 3) + }; + } + + json to_json() const { + return {{"numerator", numerator}, {"denominator", denominator}}; + } +}; + +/** + * Light client state + */ +struct ClientState { + std::string chain_id; + TrustLevel trust_level; + uint64_t trusting_period; + uint64_t unbonding_period; + uint64_t max_clock_drift; + Height latest_height; + std::optional frozen_height; + + static ClientState from_json(const json& j) { + ClientState state; + state.chain_id = j.at("chain_id").get(); + state.trust_level = TrustLevel::from_json(j.at("trust_level")); + state.trusting_period = j.at("trusting_period").get(); + state.unbonding_period = j.at("unbonding_period").get(); + state.max_clock_drift = j.at("max_clock_drift").get(); + state.latest_height = Height::from_json(j.at("latest_height")); + if (j.contains("frozen_height") && !j["frozen_height"].is_null()) { + state.frozen_height = Height::from_json(j["frozen_height"]); + } + return state; + } + + json to_json() const { + json j = { + {"chain_id", chain_id}, + {"trust_level", trust_level.to_json()}, + {"trusting_period", trusting_period}, + {"unbonding_period", unbonding_period}, + {"max_clock_drift", max_clock_drift}, + {"latest_height", latest_height.to_json()} + }; + if (frozen_height) { + j["frozen_height"] = frozen_height->to_json(); + } + return j; + } +}; + +/** + * Connection state + */ +enum class ConnectionState { + Uninitialized, + Init, + TryOpen, + Open +}; + +/** + * Channel ordering + */ +enum class ChannelOrder { + Unordered, + Ordered +}; + +inline std::string to_string(ChannelOrder order) { + return order == ChannelOrder::Ordered ? "ordered" : "unordered"; +} + +/** + * Channel state + */ +enum class ChannelState { + Uninitialized, + Init, + TryOpen, + Open, + Closed +}; + +/** + * Timeout information + */ +struct Timeout { + Height height; + uint64_t timestamp = 0; + + static Timeout from_height(uint64_t h) { + return Timeout{{0, h}, 0}; + } + + static Timeout from_timestamp(uint64_t ts) { + return Timeout{{}, ts}; + } + + json to_json() const { + return { + {"height", height.to_json()}, + {"timestamp", std::to_string(timestamp)} + }; + } +}; + +/** + * Transfer packet data (ICS-20) + */ +struct FungibleTokenPacketData { + std::string denom; + std::string amount; + std::string sender; + std::string receiver; + std::string memo; + + bool is_native() const { + return denom.find('/') == std::string::npos; + } + + static FungibleTokenPacketData from_json(const json& j) { + return FungibleTokenPacketData{ + j.at("denom").get(), + j.at("amount").get(), + j.at("sender").get(), + j.at("receiver").get(), + j.value("memo", "") + }; + } + + json to_json() const { + return { + {"denom", denom}, + {"amount", amount}, + {"sender", sender}, + {"receiver", receiver}, + {"memo", memo} + }; + } +}; + +/** + * Swap state + */ +enum class SwapState { + Pending, + Locked, + Completed, + Refunded, + Expired, + Cancelled +}; + +inline SwapState swap_state_from_string(const std::string& s) { + if (s == "pending") return SwapState::Pending; + if (s == "locked") return SwapState::Locked; + if (s == "completed") return SwapState::Completed; + if (s == "refunded") return SwapState::Refunded; + if (s == "expired") return SwapState::Expired; + return SwapState::Cancelled; +} + +/** + * Atomic swap + */ +struct AtomicSwap { + std::string swap_id; + SwapState state; + json initiator_htlc; + std::optional responder_htlc; + + static AtomicSwap from_json(const json& j) { + AtomicSwap swap; + swap.swap_id = j.at("swap_id").at("id").get(); + swap.state = swap_state_from_string(j.at("state").get()); + swap.initiator_htlc = j.at("initiator_htlc"); + if (j.contains("responder_htlc") && !j["responder_htlc"].is_null()) { + swap.responder_htlc = j["responder_htlc"]; + } + return swap; + } +}; + +/** + * IBC SDK configuration + */ +struct IbcConfig { + std::string api_key; + std::string endpoint = "https://ibc.synor.io/v1"; + std::string ws_endpoint = "wss://ibc.synor.io/v1/ws"; + int timeout = 30; + int retries = 3; + std::string chain_id = "synor-1"; + bool debug = false; +}; + +/** + * IBC exception + */ +class IbcException : public std::exception { +public: + IbcException(const std::string& message, + const std::string& code = "", + int status = 0) + : message_(message), code_(code), status_(status) {} + + const char* what() const noexcept override { return message_.c_str(); } + const std::string& code() const { return code_; } + int status() const { return status_; } + +private: + std::string message_; + std::string code_; + int status_; +}; + +// Forward declarations +class SynorIbc; + +/** + * Light client sub-client + */ +class LightClientClient { +public: + explicit LightClientClient(SynorIbc& ibc) : ibc_(ibc) {} + + std::future create(ClientType client_type, + const ClientState& client_state, + const json& consensus_state); + + std::future get_state(const std::string& client_id); + + std::future> list(); + +private: + SynorIbc& ibc_; +}; + +/** + * Connections sub-client + */ +class ConnectionsClient { +public: + explicit ConnectionsClient(SynorIbc& ibc) : ibc_(ibc) {} + + std::future open_init(const std::string& client_id, + const std::string& counterparty_client_id); + + std::future get(const std::string& connection_id); + + std::future> list(); + +private: + SynorIbc& ibc_; +}; + +/** + * Channels sub-client + */ +class ChannelsClient { +public: + explicit ChannelsClient(SynorIbc& ibc) : ibc_(ibc) {} + + std::future bind_port(const std::string& port_id, const std::string& module); + + std::future open_init(const std::string& port_id, + ChannelOrder ordering, + const std::string& connection_id, + const std::string& counterparty_port, + const std::string& version); + + std::future get(const std::string& port_id, const std::string& channel_id); + + std::future> list(); + +private: + SynorIbc& ibc_; +}; + +/** + * Transfer sub-client (ICS-20) + */ +class TransferClient { +public: + explicit TransferClient(SynorIbc& ibc) : ibc_(ibc) {} + + std::future transfer(const std::string& source_port, + const std::string& source_channel, + const std::string& denom, + const std::string& amount, + const std::string& sender, + const std::string& receiver, + std::optional timeout = std::nullopt, + std::optional memo = std::nullopt); + + std::future get_denom_trace(const std::string& ibc_denom); + +private: + SynorIbc& ibc_; +}; + +/** + * Swaps sub-client (HTLC) + */ +class SwapsClient { +public: + explicit SwapsClient(SynorIbc& ibc) : ibc_(ibc) {} + + std::future initiate(const std::string& responder, + const json& initiator_asset, + const json& responder_asset); + + std::future lock(const std::string& swap_id); + + std::future respond(const std::string& swap_id, const json& asset); + + std::future claim(const std::string& swap_id, + const std::vector& secret); + + std::future refund(const std::string& swap_id); + + std::future get(const std::string& swap_id); + + std::future> list_active(); + +private: + SynorIbc& ibc_; +}; + +/** + * Main IBC client + */ +class SynorIbc { +public: + explicit SynorIbc(const IbcConfig& config); + ~SynorIbc(); + + // Non-copyable + SynorIbc(const SynorIbc&) = delete; + SynorIbc& operator=(const SynorIbc&) = delete; + + // Movable + SynorIbc(SynorIbc&&) noexcept; + SynorIbc& operator=(SynorIbc&&) noexcept; + + const std::string& chain_id() const { return config_.chain_id; } + + std::future get_chain_info(); + std::future get_height(); + std::future health_check(); + + void close(); + bool is_closed() const { return closed_; } + + LightClientClient& clients() { return *clients_; } + ConnectionsClient& connections() { return *connections_; } + ChannelsClient& channels() { return *channels_; } + TransferClient& transfer() { return *transfer_; } + SwapsClient& swaps() { return *swaps_; } + + // Internal HTTP methods (used by sub-clients) + std::future get(const std::string& path); + std::future post(const std::string& path, const json& body); + +private: + void check_closed() const; + + IbcConfig config_; + bool closed_ = false; + + std::unique_ptr clients_; + std::unique_ptr connections_; + std::unique_ptr channels_; + std::unique_ptr transfer_; + std::unique_ptr swaps_; + + // HTTP client implementation (pimpl) + class Impl; + std::unique_ptr impl_; +}; + +} // namespace ibc +} // namespace synor + +#endif // SYNOR_IBC_HPP diff --git a/sdk/csharp/src/Synor.Ibc/SynorIbc.cs b/sdk/csharp/src/Synor.Ibc/SynorIbc.cs new file mode 100644 index 0000000..ef0e08b --- /dev/null +++ b/sdk/csharp/src/Synor.Ibc/SynorIbc.cs @@ -0,0 +1,407 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Synor.Ibc +{ + /// + /// Synor IBC SDK for C# + /// + /// Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability. + /// + public class SynorIbc : IDisposable + { + private readonly IbcConfig _config; + private readonly HttpClient _httpClient; + private bool _closed; + + public LightClientClient Clients { get; } + public ConnectionsClient Connections { get; } + public ChannelsClient Channels { get; } + public TransferClient Transfer { get; } + public SwapsClient Swaps { get; } + + public SynorIbc(IbcConfig config) + { + _config = config; + _httpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(config.Timeout) + }; + + Clients = new LightClientClient(this); + Connections = new ConnectionsClient(this); + Channels = new ChannelsClient(this); + Transfer = new TransferClient(this); + Swaps = new SwapsClient(this); + } + + public string ChainId => _config.ChainId; + + public Task> GetChainInfoAsync() => GetAsync("/chain"); + + public async Task GetHeightAsync() + { + var result = await GetAsync("/chain/height"); + return new Height( + result.TryGetValue("revision_number", out var rn) ? Convert.ToUInt64(rn) : 0, + result.TryGetValue("revision_height", out var rh) ? Convert.ToUInt64(rh) : 1 + ); + } + + public async Task HealthCheckAsync() + { + try + { + var result = await GetAsync("/health"); + return result.TryGetValue("status", out var status) && status?.ToString() == "healthy"; + } + catch + { + return false; + } + } + + public void Dispose() + { + _closed = true; + _httpClient.Dispose(); + } + + public bool IsClosed => _closed; + + // Internal HTTP methods + internal async Task> GetAsync(string path) + { + CheckClosed(); + var request = new HttpRequestMessage(HttpMethod.Get, $"{_config.Endpoint}{path}"); + AddHeaders(request); + var response = await _httpClient.SendAsync(request); + return await HandleResponseAsync(response); + } + + internal async Task> PostAsync(string path, Dictionary body) + { + CheckClosed(); + var request = new HttpRequestMessage(HttpMethod.Post, $"{_config.Endpoint}{path}") + { + Content = new StringContent( + JsonSerializer.Serialize(body), + Encoding.UTF8, + "application/json" + ) + }; + AddHeaders(request); + var response = await _httpClient.SendAsync(request); + return await HandleResponseAsync(response); + } + + private void AddHeaders(HttpRequestMessage request) + { + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _config.ApiKey); + request.Headers.Add("X-SDK-Version", "csharp/0.1.0"); + request.Headers.Add("X-Chain-Id", _config.ChainId); + } + + private static async Task> HandleResponseAsync(HttpResponseMessage response) + { + var content = await response.Content.ReadAsStringAsync(); + if ((int)response.StatusCode >= 400) + { + Dictionary? error = null; + try + { + error = JsonSerializer.Deserialize>(content); + } + catch { } + throw new IbcException( + error?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)response.StatusCode}", + error?.GetValueOrDefault("code")?.ToString(), + (int)response.StatusCode + ); + } + return JsonSerializer.Deserialize>(content) ?? new(); + } + + private void CheckClosed() + { + if (_closed) + throw new IbcException("Client has been closed", "CLIENT_CLOSED"); + } + + /// + /// Light client sub-client + /// + public class LightClientClient + { + private readonly SynorIbc _ibc; + + internal LightClientClient(SynorIbc ibc) => _ibc = ibc; + + public async Task CreateAsync( + ClientType clientType, + ClientState clientState, + Dictionary consensusState) + { + var result = await _ibc.PostAsync("/clients", new Dictionary + { + ["client_type"] = clientType.ToApiString(), + ["client_state"] = new Dictionary + { + ["chain_id"] = clientState.ChainId, + ["trust_level"] = new Dictionary + { + ["numerator"] = clientState.TrustLevel.Numerator, + ["denominator"] = clientState.TrustLevel.Denominator + }, + ["trusting_period"] = clientState.TrustingPeriod, + ["unbonding_period"] = clientState.UnbondingPeriod, + ["max_clock_drift"] = clientState.MaxClockDrift, + ["latest_height"] = new Dictionary + { + ["revision_number"] = clientState.LatestHeight.RevisionNumber, + ["revision_height"] = clientState.LatestHeight.RevisionHeight + } + }, + ["consensus_state"] = consensusState + }); + return new ClientId(result["client_id"].ToString()!); + } + + public async Task GetStateAsync(ClientId clientId) + { + var result = await _ibc.GetAsync($"/clients/{clientId.Id}/state"); + var trustLevel = (JsonElement)result["trust_level"]; + var latestHeight = (JsonElement)result["latest_height"]; + return new ClientState( + result["chain_id"].ToString()!, + new TrustLevel( + trustLevel.GetProperty("numerator").GetInt32(), + trustLevel.GetProperty("denominator").GetInt32() + ), + Convert.ToUInt64(result["trusting_period"]), + Convert.ToUInt64(result["unbonding_period"]), + Convert.ToUInt64(result["max_clock_drift"]), + new Height( + latestHeight.GetProperty("revision_number").GetUInt64(), + latestHeight.GetProperty("revision_height").GetUInt64() + ) + ); + } + + public async Task>> ListAsync() + { + var result = await _ibc.GetAsync("/clients"); + if (result.TryGetValue("clients", out var clients) && clients is JsonElement element) + { + return JsonSerializer.Deserialize>>(element.GetRawText()) ?? new(); + } + return new List>(); + } + } + + /// + /// Connections sub-client + /// + public class ConnectionsClient + { + private readonly SynorIbc _ibc; + + internal ConnectionsClient(SynorIbc ibc) => _ibc = ibc; + + public async Task OpenInitAsync(ClientId clientId, ClientId counterpartyClientId) + { + var result = await _ibc.PostAsync("/connections/init", new Dictionary + { + ["client_id"] = clientId.Id, + ["counterparty_client_id"] = counterpartyClientId.Id + }); + return new ConnectionId(result["connection_id"].ToString()!); + } + + public Task> GetAsync(ConnectionId connectionId) => + _ibc.GetAsync($"/connections/{connectionId.Id}"); + + public async Task>> ListAsync() + { + var result = await _ibc.GetAsync("/connections"); + if (result.TryGetValue("connections", out var connections) && connections is JsonElement element) + { + return JsonSerializer.Deserialize>>(element.GetRawText()) ?? new(); + } + return new List>(); + } + } + + /// + /// Channels sub-client + /// + public class ChannelsClient + { + private readonly SynorIbc _ibc; + + internal ChannelsClient(SynorIbc ibc) => _ibc = ibc; + + public Task BindPortAsync(PortId portId, string module) => + _ibc.PostAsync("/ports/bind", new Dictionary + { + ["port_id"] = portId.Id, + ["module"] = module + }); + + public async Task OpenInitAsync( + PortId portId, + ChannelOrder ordering, + ConnectionId connectionId, + PortId counterpartyPort, + string version) + { + var result = await _ibc.PostAsync("/channels/init", new Dictionary + { + ["port_id"] = portId.Id, + ["ordering"] = ordering.ToString().ToLowerInvariant(), + ["connection_id"] = connectionId.Id, + ["counterparty_port"] = counterpartyPort.Id, + ["version"] = version + }); + return new ChannelId(result["channel_id"].ToString()!); + } + + public Task> GetAsync(PortId portId, ChannelId channelId) => + _ibc.GetAsync($"/channels/{portId.Id}/{channelId.Id}"); + + public async Task>> ListAsync() + { + var result = await _ibc.GetAsync("/channels"); + if (result.TryGetValue("channels", out var channels) && channels is JsonElement element) + { + return JsonSerializer.Deserialize>>(element.GetRawText()) ?? new(); + } + return new List>(); + } + } + + /// + /// Transfer sub-client (ICS-20) + /// + public class TransferClient + { + private readonly SynorIbc _ibc; + + internal TransferClient(SynorIbc ibc) => _ibc = ibc; + + public async Task> TransferAsync( + string sourcePort, + string sourceChannel, + string denom, + string amount, + string sender, + string receiver, + Timeout? timeout = null, + string? memo = null) + { + var body = new Dictionary + { + ["source_port"] = sourcePort, + ["source_channel"] = sourceChannel, + ["token"] = new Dictionary { ["denom"] = denom, ["amount"] = amount }, + ["sender"] = sender, + ["receiver"] = receiver + }; + if (timeout != null) + { + body["timeout_height"] = new Dictionary + { + ["revision_number"] = timeout.Height.RevisionNumber, + ["revision_height"] = timeout.Height.RevisionHeight + }; + body["timeout_timestamp"] = timeout.Timestamp.ToString(); + } + if (memo != null) body["memo"] = memo; + return await _ibc.PostAsync("/transfer", body); + } + + public Task> GetDenomTraceAsync(string ibcDenom) => + _ibc.GetAsync($"/transfer/denom_trace/{ibcDenom}"); + } + + /// + /// Swaps sub-client (HTLC) + /// + public class SwapsClient + { + private readonly SynorIbc _ibc; + + internal SwapsClient(SynorIbc ibc) => _ibc = ibc; + + public Task> InitiateAsync( + string responder, + Dictionary initiatorAsset, + Dictionary responderAsset) => + _ibc.PostAsync("/swaps/initiate", new Dictionary + { + ["responder"] = responder, + ["initiator_asset"] = initiatorAsset, + ["responder_asset"] = responderAsset + }); + + public Task LockAsync(SwapId swapId) => + _ibc.PostAsync($"/swaps/{swapId.Id}/lock", new Dictionary()); + + public Task> RespondAsync(SwapId swapId, Dictionary asset) => + _ibc.PostAsync($"/swaps/{swapId.Id}/respond", new Dictionary { ["asset"] = asset }); + + public Task> ClaimAsync(SwapId swapId, byte[] secret) => + _ibc.PostAsync($"/swaps/{swapId.Id}/claim", new Dictionary + { + ["secret"] = Convert.ToBase64String(secret) + }); + + public Task> RefundAsync(SwapId swapId) => + _ibc.PostAsync($"/swaps/{swapId.Id}/refund", new Dictionary()); + + public async Task GetAsync(SwapId swapId) + { + var result = await _ibc.GetAsync($"/swaps/{swapId.Id}"); + return ParseAtomicSwap(result); + } + + public async Task> ListActiveAsync() + { + var result = await _ibc.GetAsync("/swaps/active"); + var swaps = new List(); + if (result.TryGetValue("swaps", out var swapsObj) && swapsObj is JsonElement element) + { + foreach (var swap in element.EnumerateArray()) + { + var dict = JsonSerializer.Deserialize>(swap.GetRawText()); + if (dict != null) + swaps.Add(ParseAtomicSwap(dict)); + } + } + return swaps; + } + + private static AtomicSwap ParseAtomicSwap(Dictionary json) + { + var swapIdObj = (JsonElement)json["swap_id"]; + var swapId = swapIdObj.GetProperty("id").GetString()!; + var stateStr = json["state"].ToString()!.ToUpperInvariant(); + var state = Enum.Parse(stateStr, true); + var initiatorHtlc = JsonSerializer.Deserialize>( + ((JsonElement)json["initiator_htlc"]).GetRawText()) ?? new(); + Dictionary? responderHtlc = null; + if (json.TryGetValue("responder_htlc", out var responder) && responder is JsonElement re && re.ValueKind != JsonValueKind.Null) + { + responderHtlc = JsonSerializer.Deserialize>(re.GetRawText()); + } + return new AtomicSwap(new SwapId(swapId), state, initiatorHtlc, responderHtlc); + } + } + } +} diff --git a/sdk/csharp/src/Synor.Ibc/Types.cs b/sdk/csharp/src/Synor.Ibc/Types.cs new file mode 100644 index 0000000..9bbb929 --- /dev/null +++ b/sdk/csharp/src/Synor.Ibc/Types.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text.Json.Serialization; + +namespace Synor.Ibc +{ + /// + /// Synor IBC SDK Types for C# + /// + /// Inter-Blockchain Communication (IBC) protocol types. + /// + + /// + /// IBC Height representation + /// + public record Height( + [property: JsonPropertyName("revision_number")] ulong RevisionNumber = 0, + [property: JsonPropertyName("revision_height")] ulong RevisionHeight = 1 + ) + { + public bool IsZero => RevisionNumber == 0 && RevisionHeight == 0; + + public Height Increment() => this with { RevisionHeight = RevisionHeight + 1 }; + } + + /// + /// Chain identifier + /// + public record ChainId(string Id); + + /// + /// IBC Version with features + /// + public record Version(string Identifier, List Features) + { + public static Version DefaultConnection() => + new("1", new List { "ORDER_ORDERED", "ORDER_UNORDERED" }); + } + + /// + /// Light client types + /// + public enum ClientType + { + Tendermint, + SoloMachine, + Localhost, + Wasm + } + + public static class ClientTypeExtensions + { + public static string ToApiString(this ClientType type) => type switch + { + ClientType.Tendermint => "tendermint", + ClientType.SoloMachine => "solo_machine", + ClientType.Localhost => "localhost", + ClientType.Wasm => "wasm", + _ => throw new ArgumentOutOfRangeException(nameof(type)) + }; + } + + /// + /// Light client identifier + /// + public record ClientId(string Id); + + /// + /// Trust level configuration + /// + public record TrustLevel(int Numerator = 1, int Denominator = 3); + + /// + /// Light client state + /// + public record ClientState( + [property: JsonPropertyName("chain_id")] string ChainId, + [property: JsonPropertyName("trust_level")] TrustLevel TrustLevel, + [property: JsonPropertyName("trusting_period")] ulong TrustingPeriod, + [property: JsonPropertyName("unbonding_period")] ulong UnbondingPeriod, + [property: JsonPropertyName("max_clock_drift")] ulong MaxClockDrift, + [property: JsonPropertyName("latest_height")] Height LatestHeight, + [property: JsonPropertyName("frozen_height")] Height? FrozenHeight = null + ); + + /// + /// Connection state + /// + public enum ConnectionState + { + Uninitialized, + Init, + TryOpen, + Open + } + + /// + /// Connection identifier + /// + public record ConnectionId(string Id) + { + public static ConnectionId NewId(int sequence) => new($"connection-{sequence}"); + } + + /// + /// Port identifier + /// + public record PortId(string Id) + { + public static PortId Transfer() => new("transfer"); + } + + /// + /// Channel identifier + /// + public record ChannelId(string Id) + { + public static ChannelId NewId(int sequence) => new($"channel-{sequence}"); + } + + /// + /// Channel ordering + /// + public enum ChannelOrder + { + Unordered, + Ordered + } + + /// + /// Channel state + /// + public enum ChannelState + { + Uninitialized, + Init, + TryOpen, + Open, + Closed + } + + /// + /// Timeout information + /// + public record Timeout(Height Height, BigInteger Timestamp) + { + public Timeout(Height height) : this(height, BigInteger.Zero) { } + + public static Timeout FromHeight(ulong height) => + new(new Height(0, height), BigInteger.Zero); + + public static Timeout FromTimestamp(BigInteger timestamp) => + new(new Height(), timestamp); + + public Dictionary ToJson() => new() + { + ["height"] = new Dictionary + { + ["revision_number"] = Height.RevisionNumber, + ["revision_height"] = Height.RevisionHeight + }, + ["timestamp"] = Timestamp.ToString() + }; + } + + /// + /// Transfer packet data (ICS-20) + /// + public record FungibleTokenPacketData( + string Denom, + string Amount, + string Sender, + string Receiver, + string Memo = "" + ) + { + public bool IsNative => !Denom.Contains('/'); + } + + /// + /// Swap state + /// + public enum SwapState + { + Pending, + Locked, + Completed, + Refunded, + Expired, + Cancelled + } + + /// + /// Swap identifier + /// + public record SwapId(string Id); + + /// + /// Native asset for swaps + /// + public record NativeAsset(BigInteger Amount) + { + public Dictionary ToJson() => new() + { + ["native"] = new Dictionary { ["amount"] = Amount.ToString() } + }; + } + + /// + /// ICS-20 asset for swaps + /// + public record Ics20Asset(string Denom, BigInteger Amount) + { + public Dictionary ToJson() => new() + { + ["ics20"] = new Dictionary + { + ["denom"] = Denom, + ["amount"] = Amount.ToString() + } + }; + } + + /// + /// Hashlock - hash of the secret + /// + public record Hashlock(byte[] Hash); + + /// + /// Timelock - expiration time + /// + public record Timelock(BigInteger Expiry) + { + public bool IsExpired(BigInteger current) => current >= Expiry; + } + + /// + /// Atomic swap + /// + public record AtomicSwap( + SwapId SwapId, + SwapState State, + Dictionary InitiatorHtlc, + Dictionary? ResponderHtlc + ); + + /// + /// IBC SDK configuration + /// + public record IbcConfig( + string ApiKey, + string Endpoint = "https://ibc.synor.io/v1", + string WsEndpoint = "wss://ibc.synor.io/v1/ws", + int Timeout = 30, + int Retries = 3, + string ChainId = "synor-1", + bool Debug = false + ); + + /// + /// IBC exception + /// + public class IbcException : Exception + { + public string? Code { get; } + public int? Status { get; } + + public IbcException(string message, string? code = null, int? status = null) + : base(message) + { + Code = code; + Status = status; + } + + public override string ToString() => + $"IbcException: {Message}{(Code != null ? $" ({Code})" : "")}"; + } +} diff --git a/sdk/flutter/lib/src/ibc/synor_ibc.dart b/sdk/flutter/lib/src/ibc/synor_ibc.dart new file mode 100644 index 0000000..82981c1 --- /dev/null +++ b/sdk/flutter/lib/src/ibc/synor_ibc.dart @@ -0,0 +1,278 @@ +/// Synor IBC SDK for Flutter/Dart +/// +/// Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability. +library synor_ibc; + +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'types.dart'; + +export 'types.dart'; + +/// Main IBC client +class SynorIbc { + final IbcConfig config; + final http.Client _client; + bool _closed = false; + + late final LightClientClient clients; + late final ConnectionsClient connections; + late final ChannelsClient channels; + late final TransferClient transfer; + late final SwapsClient swaps; + + SynorIbc(this.config) : _client = http.Client() { + clients = LightClientClient(this); + connections = ConnectionsClient(this); + channels = ChannelsClient(this); + transfer = TransferClient(this); + swaps = SwapsClient(this); + } + + String get chainId => config.chainId; + + Future> getChainInfo() async { + return _get('/chain'); + } + + Future getHeight() async { + final result = await _get('/chain/height'); + return Height.fromJson(result); + } + + Future healthCheck() async { + try { + final result = await _get('/health'); + return result['status'] == 'healthy'; + } catch (_) { + return false; + } + } + + void close() { + _closed = true; + _client.close(); + } + + bool get isClosed => _closed; + + // Internal HTTP methods + Future> _get(String path) async { + _checkClosed(); + final response = await _client.get( + Uri.parse('${config.endpoint}$path'), + headers: _headers, + ); + return _handleResponse(response); + } + + Future> _post(String path, Map body) async { + _checkClosed(); + final response = await _client.post( + Uri.parse('${config.endpoint}$path'), + headers: _headers, + body: jsonEncode(body), + ); + return _handleResponse(response); + } + + Map get _headers => { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${config.apiKey}', + 'X-SDK-Version': 'flutter/0.1.0', + 'X-Chain-Id': config.chainId, + }; + + Map _handleResponse(http.Response response) { + if (response.statusCode >= 400) { + Map? error; + try { + error = jsonDecode(response.body); + } catch (_) {} + throw IbcException( + error?['message'] ?? 'HTTP ${response.statusCode}', + error?['code'], + response.statusCode, + ); + } + return jsonDecode(response.body); + } + + void _checkClosed() { + if (_closed) { + throw const IbcException('Client has been closed', 'CLIENT_CLOSED'); + } + } +} + +/// Light client sub-client +class LightClientClient { + final SynorIbc _ibc; + LightClientClient(this._ibc); + + Future create({ + required ClientType clientType, + required ClientState clientState, + required Map consensusState, + }) async { + final result = await _ibc._post('/clients', { + 'client_type': clientType.name, + 'client_state': clientState.toJson(), + 'consensus_state': consensusState, + }); + return ClientId(result['client_id']); + } + + Future getState(ClientId clientId) async { + final result = await _ibc._get('/clients/${clientId.id}/state'); + return ClientState.fromJson(result); + } + + Future>> list() async { + final result = await _ibc._get('/clients'); + return List>.from(result['clients'] ?? []); + } +} + +/// Connections sub-client +class ConnectionsClient { + final SynorIbc _ibc; + ConnectionsClient(this._ibc); + + Future openInit({ + required ClientId clientId, + required ClientId counterpartyClientId, + }) async { + final result = await _ibc._post('/connections/init', { + 'client_id': clientId.id, + 'counterparty_client_id': counterpartyClientId.id, + }); + return ConnectionId(result['connection_id']); + } + + Future> get(ConnectionId connectionId) async { + return _ibc._get('/connections/${connectionId.id}'); + } + + Future>> list() async { + final result = await _ibc._get('/connections'); + return List>.from(result['connections'] ?? []); + } +} + +/// Channels sub-client +class ChannelsClient { + final SynorIbc _ibc; + ChannelsClient(this._ibc); + + Future bindPort(PortId portId, String module) async { + await _ibc._post('/ports/bind', {'port_id': portId.id, 'module': module}); + } + + Future openInit({ + required PortId portId, + required ChannelOrder ordering, + required ConnectionId connectionId, + required PortId counterpartyPort, + required String version, + }) async { + final result = await _ibc._post('/channels/init', { + 'port_id': portId.id, + 'ordering': ordering.name, + 'connection_id': connectionId.id, + 'counterparty_port': counterpartyPort.id, + 'version': version, + }); + return ChannelId(result['channel_id']); + } + + Future> get(PortId portId, ChannelId channelId) async { + return _ibc._get('/channels/${portId.id}/${channelId.id}'); + } + + Future>> list() async { + final result = await _ibc._get('/channels'); + return List>.from(result['channels'] ?? []); + } +} + +/// Transfer sub-client (ICS-20) +class TransferClient { + final SynorIbc _ibc; + TransferClient(this._ibc); + + Future> transfer({ + required String sourcePort, + required String sourceChannel, + required String denom, + required String amount, + required String sender, + required String receiver, + Timeout? timeout, + String? memo, + }) async { + final body = { + 'source_port': sourcePort, + 'source_channel': sourceChannel, + 'token': {'denom': denom, 'amount': amount}, + 'sender': sender, + 'receiver': receiver, + }; + if (timeout != null) { + body['timeout_height'] = timeout.height.toJson(); + body['timeout_timestamp'] = timeout.timestamp.toString(); + } + if (memo != null) body['memo'] = memo; + return _ibc._post('/transfer', body); + } + + Future> getDenomTrace(String ibcDenom) async { + return _ibc._get('/transfer/denom_trace/$ibcDenom'); + } +} + +/// Swaps sub-client (HTLC) +class SwapsClient { + final SynorIbc _ibc; + SwapsClient(this._ibc); + + Future> initiate({ + required String responder, + required Map initiatorAsset, + required Map responderAsset, + }) async { + return _ibc._post('/swaps/initiate', { + 'responder': responder, + 'initiator_asset': initiatorAsset, + 'responder_asset': responderAsset, + }); + } + + Future lock(SwapId swapId) async { + await _ibc._post('/swaps/${swapId.id}/lock', {}); + } + + Future> respond(SwapId swapId, Map asset) async { + return _ibc._post('/swaps/${swapId.id}/respond', {'asset': asset}); + } + + Future> claim(SwapId swapId, List secret) async { + return _ibc._post('/swaps/${swapId.id}/claim', { + 'secret': base64Encode(secret), + }); + } + + Future> refund(SwapId swapId) async { + return _ibc._post('/swaps/${swapId.id}/refund', {}); + } + + Future get(SwapId swapId) async { + final result = await _ibc._get('/swaps/${swapId.id}'); + return AtomicSwap.fromJson(result); + } + + Future> listActive() async { + final result = await _ibc._get('/swaps/active'); + return (result['swaps'] as List).map((e) => AtomicSwap.fromJson(e)).toList(); + } +} diff --git a/sdk/flutter/lib/src/ibc/types.dart b/sdk/flutter/lib/src/ibc/types.dart new file mode 100644 index 0000000..3e8f255 --- /dev/null +++ b/sdk/flutter/lib/src/ibc/types.dart @@ -0,0 +1,359 @@ +/// Synor IBC SDK Types for Flutter/Dart +/// +/// Inter-Blockchain Communication (IBC) protocol types. +library synor_ibc_types; + +/// IBC Height representation +class Height { + final int revisionNumber; + final int revisionHeight; + + const Height({this.revisionNumber = 0, this.revisionHeight = 1}); + + bool get isZero => revisionNumber == 0 && revisionHeight == 0; + + Height increment() => Height(revisionNumber: revisionNumber, revisionHeight: revisionHeight + 1); + + factory Height.fromJson(Map json) => Height( + revisionNumber: json['revision_number'] ?? 0, + revisionHeight: json['revision_height'] ?? 1, + ); + + Map toJson() => { + 'revision_number': revisionNumber, + 'revision_height': revisionHeight, + }; +} + +/// Chain identifier +class ChainId { + final String id; + const ChainId(this.id); + + factory ChainId.fromJson(Map json) => ChainId(json['id']); + Map toJson() => {'id': id}; +} + +/// IBC Version with features +class Version { + final String identifier; + final List features; + + const Version({required this.identifier, required this.features}); + + factory Version.defaultConnection() => const Version( + identifier: '1', + features: ['ORDER_ORDERED', 'ORDER_UNORDERED'], + ); + + factory Version.fromJson(Map json) => Version( + identifier: json['identifier'], + features: List.from(json['features']), + ); + + Map toJson() => {'identifier': identifier, 'features': features}; +} + +/// Light client types +enum ClientType { tendermint, soloMachine, localhost, wasm } + +/// Light client identifier +class ClientId { + final String id; + const ClientId(this.id); + + factory ClientId.fromJson(Map json) => ClientId(json['id']); + Map toJson() => {'id': id}; +} + +/// Trust level configuration +class TrustLevel { + final int numerator; + final int denominator; + + const TrustLevel({this.numerator = 1, this.denominator = 3}); + + factory TrustLevel.fromJson(Map json) => TrustLevel( + numerator: json['numerator'] ?? 1, + denominator: json['denominator'] ?? 3, + ); + + Map toJson() => {'numerator': numerator, 'denominator': denominator}; +} + +/// Light client state +class ClientState { + final String chainId; + final TrustLevel trustLevel; + final int trustingPeriod; + final int unbondingPeriod; + final int maxClockDrift; + final Height latestHeight; + final Height? frozenHeight; + + const ClientState({ + required this.chainId, + required this.trustLevel, + required this.trustingPeriod, + required this.unbondingPeriod, + required this.maxClockDrift, + required this.latestHeight, + this.frozenHeight, + }); + + factory ClientState.fromJson(Map json) => ClientState( + chainId: json['chain_id'], + trustLevel: TrustLevel.fromJson(json['trust_level']), + trustingPeriod: json['trusting_period'], + unbondingPeriod: json['unbonding_period'], + maxClockDrift: json['max_clock_drift'], + latestHeight: Height.fromJson(json['latest_height']), + frozenHeight: json['frozen_height'] != null ? Height.fromJson(json['frozen_height']) : null, + ); + + Map toJson() => { + 'chain_id': chainId, + 'trust_level': trustLevel.toJson(), + 'trusting_period': trustingPeriod, + 'unbonding_period': unbondingPeriod, + 'max_clock_drift': maxClockDrift, + 'latest_height': latestHeight.toJson(), + if (frozenHeight != null) 'frozen_height': frozenHeight!.toJson(), + }; +} + +/// Connection state +enum ConnectionState { uninitialized, init, tryopen, open } + +/// Connection identifier +class ConnectionId { + final String id; + const ConnectionId(this.id); + + factory ConnectionId.newId(int sequence) => ConnectionId('connection-$sequence'); + factory ConnectionId.fromJson(Map json) => ConnectionId(json['id']); + Map toJson() => {'id': id}; +} + +/// Port identifier +class PortId { + final String id; + const PortId(this.id); + + factory PortId.transfer() => const PortId('transfer'); + factory PortId.fromJson(Map json) => PortId(json['id']); + Map toJson() => {'id': id}; +} + +/// Channel identifier +class ChannelId { + final String id; + const ChannelId(this.id); + + factory ChannelId.newId(int sequence) => ChannelId('channel-$sequence'); + factory ChannelId.fromJson(Map json) => ChannelId(json['id']); + Map toJson() => {'id': id}; +} + +/// Channel ordering +enum ChannelOrder { unordered, ordered } + +/// Channel state +enum ChannelState { uninitialized, init, tryopen, open, closed } + +/// IBC Packet +class Packet { + final BigInt sequence; + final PortId sourcePort; + final ChannelId sourceChannel; + final PortId destPort; + final ChannelId destChannel; + final List data; + final Height timeoutHeight; + final BigInt timeoutTimestamp; + + const Packet({ + required this.sequence, + required this.sourcePort, + required this.sourceChannel, + required this.destPort, + required this.destChannel, + required this.data, + required this.timeoutHeight, + required this.timeoutTimestamp, + }); + + factory Packet.fromJson(Map json) => Packet( + sequence: BigInt.parse(json['sequence'].toString()), + sourcePort: PortId.fromJson(json['source_port']), + sourceChannel: ChannelId.fromJson(json['source_channel']), + destPort: PortId.fromJson(json['dest_port']), + destChannel: ChannelId.fromJson(json['dest_channel']), + data: List.from(json['data']), + timeoutHeight: Height.fromJson(json['timeout_height']), + timeoutTimestamp: BigInt.parse(json['timeout_timestamp'].toString()), + ); +} + +/// Timeout information +class Timeout { + final Height height; + final BigInt timestamp; + + Timeout({required this.height, BigInt? timestamp}) : timestamp = timestamp ?? BigInt.zero; + + factory Timeout.fromHeight(int height) => Timeout( + height: Height(revisionHeight: height), + ); + + factory Timeout.fromTimestamp(BigInt timestamp) => Timeout( + height: const Height(), + timestamp: timestamp, + ); + + Map toJson() => { + 'height': height.toJson(), + 'timestamp': timestamp.toString(), + }; +} + +/// Transfer packet data (ICS-20) +class FungibleTokenPacketData { + final String denom; + final String amount; + final String sender; + final String receiver; + final String memo; + + const FungibleTokenPacketData({ + required this.denom, + required this.amount, + required this.sender, + required this.receiver, + this.memo = '', + }); + + bool get isNative => !denom.contains('/'); + + factory FungibleTokenPacketData.fromJson(Map json) => FungibleTokenPacketData( + denom: json['denom'], + amount: json['amount'], + sender: json['sender'], + receiver: json['receiver'], + memo: json['memo'] ?? '', + ); + + Map toJson() => { + 'denom': denom, + 'amount': amount, + 'sender': sender, + 'receiver': receiver, + 'memo': memo, + }; +} + +/// Swap state +enum SwapState { pending, locked, completed, refunded, expired, cancelled } + +/// Swap identifier +class SwapId { + final String id; + const SwapId(this.id); + + factory SwapId.fromJson(Map json) => SwapId(json['id']); + Map toJson() => {'id': id}; +} + +/// Swap asset - native token +class NativeAsset { + final BigInt amount; + const NativeAsset(this.amount); + + Map toJson() => {'native': {'amount': amount.toString()}}; +} + +/// Swap asset - ICS-20 token +class Ics20Asset { + final String denom; + final BigInt amount; + const Ics20Asset(this.denom, this.amount); + + Map toJson() => {'ics20': {'denom': denom, 'amount': amount.toString()}}; +} + +/// Hashlock - hash of the secret +class Hashlock { + final List hash; + const Hashlock(this.hash); + + factory Hashlock.fromJson(Map json) => Hashlock(List.from(json['hash'])); + Map toJson() => {'hash': hash}; +} + +/// Timelock - expiration time +class Timelock { + final BigInt expiry; + const Timelock(this.expiry); + + bool isExpired(BigInt current) => current >= expiry; + + factory Timelock.fromJson(Map json) => Timelock( + BigInt.parse(json['expiry'].toString()), + ); + Map toJson() => {'expiry': expiry.toString()}; +} + +/// Atomic swap +class AtomicSwap { + final SwapId swapId; + final SwapState state; + final Map initiatorHtlc; + final Map? responderHtlc; + + const AtomicSwap({ + required this.swapId, + required this.state, + required this.initiatorHtlc, + this.responderHtlc, + }); + + factory AtomicSwap.fromJson(Map json) => AtomicSwap( + swapId: SwapId.fromJson(json['swap_id']), + state: SwapState.values.firstWhere((e) => e.name == json['state']), + initiatorHtlc: json['initiator_htlc'], + responderHtlc: json['responder_htlc'], + ); +} + +/// IBC SDK configuration +class IbcConfig { + final String apiKey; + final String endpoint; + final String wsEndpoint; + final int timeout; + final int retries; + final String chainId; + final bool debug; + + const IbcConfig({ + required this.apiKey, + this.endpoint = 'https://ibc.synor.io/v1', + this.wsEndpoint = 'wss://ibc.synor.io/v1/ws', + this.timeout = 30, + this.retries = 3, + this.chainId = 'synor-1', + this.debug = false, + }); +} + +/// IBC exception +class IbcException implements Exception { + final String message; + final String? code; + final int? status; + + const IbcException(this.message, [this.code, this.status]); + + @override + String toString() => 'IbcException: $message${code != null ? ' ($code)' : ''}'; +} diff --git a/sdk/go/ibc/client.go b/sdk/go/ibc/client.go new file mode 100644 index 0000000..3c0e651 --- /dev/null +++ b/sdk/go/ibc/client.go @@ -0,0 +1,385 @@ +// Package ibc provides the Synor IBC SDK for Go. +package ibc + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// SynorIbc is the main IBC client +type SynorIbc struct { + config Config + client *http.Client + closed bool + + // Sub-clients + Clients *LightClientClient + Connections *ConnectionsClient + Channels *ChannelsClient + Packets *PacketsClient + Transfer *TransferClient + Swaps *SwapsClient +} + +// NewSynorIbc creates a new IBC client +func NewSynorIbc(config Config) *SynorIbc { + ibc := &SynorIbc{ + config: config, + client: &http.Client{Timeout: config.Timeout}, + } + ibc.Clients = &LightClientClient{ibc: ibc} + ibc.Connections = &ConnectionsClient{ibc: ibc} + ibc.Channels = &ChannelsClient{ibc: ibc} + ibc.Packets = &PacketsClient{ibc: ibc} + ibc.Transfer = &TransferClient{ibc: ibc} + ibc.Swaps = &SwapsClient{ibc: ibc} + return ibc +} + +// ChainID returns the chain ID +func (c *SynorIbc) ChainID() string { + return c.config.ChainID +} + +// GetChainInfo returns chain information +func (c *SynorIbc) GetChainInfo(ctx context.Context) (map[string]interface{}, error) { + var result map[string]interface{} + err := c.get(ctx, "/chain", &result) + return result, err +} + +// GetHeight returns current height +func (c *SynorIbc) GetHeight(ctx context.Context) (Height, error) { + var result Height + err := c.get(ctx, "/chain/height", &result) + return result, err +} + +// HealthCheck performs a health check +func (c *SynorIbc) HealthCheck(ctx context.Context) bool { + var result map[string]string + if err := c.get(ctx, "/health", &result); err != nil { + return false + } + return result["status"] == "healthy" +} + +// Close closes the client +func (c *SynorIbc) Close() { + c.closed = true +} + +// IsClosed returns whether client is closed +func (c *SynorIbc) IsClosed() bool { + return c.closed +} + +func (c *SynorIbc) get(ctx context.Context, path string, result interface{}) error { + return c.request(ctx, "GET", path, nil, result) +} + +func (c *SynorIbc) post(ctx context.Context, path string, body, result interface{}) error { + return c.request(ctx, "POST", path, body, result) +} + +func (c *SynorIbc) delete(ctx context.Context, path string, result interface{}) error { + return c.request(ctx, "DELETE", path, nil, result) +} + +func (c *SynorIbc) request(ctx context.Context, method, path string, body, result interface{}) error { + if c.closed { + return &IbcError{Message: "Client has been closed", Code: "CLIENT_CLOSED"} + } + + var bodyReader io.Reader + if body != nil { + data, err := json.Marshal(body) + if err != nil { + return err + } + bodyReader = bytes.NewReader(data) + } + + url := c.config.Endpoint + path + var lastErr error + + for attempt := 0; attempt <= c.config.Retries; attempt++ { + req, err := http.NewRequestWithContext(ctx, method, url, bodyReader) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.config.APIKey) + req.Header.Set("X-SDK-Version", "go/0.1.0") + req.Header.Set("X-Chain-Id", c.config.ChainID) + + resp, err := c.client.Do(req) + if err != nil { + lastErr = err + time.Sleep(time.Duration(1<= 400 { + var errResp map[string]interface{} + json.NewDecoder(resp.Body).Decode(&errResp) + msg := fmt.Sprintf("HTTP %d", resp.StatusCode) + if m, ok := errResp["message"].(string); ok { + msg = m + } + code := "" + if c, ok := errResp["code"].(string); ok { + code = c + } + return &IbcError{Message: msg, Code: code, Status: resp.StatusCode} + } + + if result != nil { + return json.NewDecoder(resp.Body).Decode(result) + } + return nil + } + + return lastErr +} + +// LightClientClient handles light client operations +type LightClientClient struct { + ibc *SynorIbc +} + +// Create creates a new light client +func (c *LightClientClient) Create(ctx context.Context, clientType ClientType, clientState ClientState, consensusState ConsensusState) (ClientId, error) { + var result struct { + ClientID string `json:"client_id"` + } + err := c.ibc.post(ctx, "/clients", map[string]interface{}{ + "client_type": clientType, + "client_state": clientState, + "consensus_state": consensusState, + }, &result) + return ClientId{ID: result.ClientID}, err +} + +// Update updates a light client +func (c *LightClientClient) Update(ctx context.Context, clientId ClientId, header Header) (Height, error) { + var result Height + err := c.ibc.post(ctx, "/clients/"+clientId.ID+"/update", map[string]interface{}{ + "header": header, + }, &result) + return result, err +} + +// GetState returns client state +func (c *LightClientClient) GetState(ctx context.Context, clientId ClientId) (ClientState, error) { + var result ClientState + err := c.ibc.get(ctx, "/clients/"+clientId.ID+"/state", &result) + return result, err +} + +// List returns all clients +func (c *LightClientClient) List(ctx context.Context) ([]map[string]interface{}, error) { + var result []map[string]interface{} + err := c.ibc.get(ctx, "/clients", &result) + return result, err +} + +// ConnectionsClient handles connection operations +type ConnectionsClient struct { + ibc *SynorIbc +} + +// OpenInit initializes a connection +func (c *ConnectionsClient) OpenInit(ctx context.Context, clientId, counterpartyClientId ClientId) (ConnectionId, error) { + var result struct { + ConnectionID string `json:"connection_id"` + } + err := c.ibc.post(ctx, "/connections/init", map[string]interface{}{ + "client_id": clientId.ID, + "counterparty_client_id": counterpartyClientId.ID, + }, &result) + return ConnectionId{ID: result.ConnectionID}, err +} + +// Get returns a connection +func (c *ConnectionsClient) Get(ctx context.Context, connectionId ConnectionId) (ConnectionEnd, error) { + var result ConnectionEnd + err := c.ibc.get(ctx, "/connections/"+connectionId.ID, &result) + return result, err +} + +// List returns all connections +func (c *ConnectionsClient) List(ctx context.Context) ([]map[string]interface{}, error) { + var result []map[string]interface{} + err := c.ibc.get(ctx, "/connections", &result) + return result, err +} + +// ChannelsClient handles channel operations +type ChannelsClient struct { + ibc *SynorIbc +} + +// BindPort binds a port +func (c *ChannelsClient) BindPort(ctx context.Context, portId PortId, module string) error { + return c.ibc.post(ctx, "/ports/bind", map[string]interface{}{ + "port_id": portId.ID, + "module": module, + }, nil) +} + +// OpenInit initializes a channel +func (c *ChannelsClient) OpenInit(ctx context.Context, portId PortId, ordering ChannelOrder, connectionId ConnectionId, counterpartyPort PortId, version string) (ChannelId, error) { + var result struct { + ChannelID string `json:"channel_id"` + } + err := c.ibc.post(ctx, "/channels/init", map[string]interface{}{ + "port_id": portId.ID, + "ordering": ordering, + "connection_id": connectionId.ID, + "counterparty_port": counterpartyPort.ID, + "version": version, + }, &result) + return ChannelId{ID: result.ChannelID}, err +} + +// Get returns a channel +func (c *ChannelsClient) Get(ctx context.Context, portId PortId, channelId ChannelId) (Channel, error) { + var result Channel + err := c.ibc.get(ctx, "/channels/"+portId.ID+"/"+channelId.ID, &result) + return result, err +} + +// List returns all channels +func (c *ChannelsClient) List(ctx context.Context) ([]map[string]interface{}, error) { + var result []map[string]interface{} + err := c.ibc.get(ctx, "/channels", &result) + return result, err +} + +// PacketsClient handles packet operations +type PacketsClient struct { + ibc *SynorIbc +} + +// Send sends a packet +func (c *PacketsClient) Send(ctx context.Context, sourcePort PortId, sourceChannel ChannelId, data []byte, timeout Timeout) (map[string]interface{}, error) { + var result map[string]interface{} + err := c.ibc.post(ctx, "/packets/send", map[string]interface{}{ + "source_port": sourcePort.ID, + "source_channel": sourceChannel.ID, + "data": base64.StdEncoding.EncodeToString(data), + "timeout_height": timeout.Height, + "timeout_timestamp": timeout.Timestamp, + }, &result) + return result, err +} + +// TransferClient handles ICS-20 transfers +type TransferClient struct { + ibc *SynorIbc +} + +// Transfer sends tokens to another chain +func (c *TransferClient) Transfer(ctx context.Context, sourcePort, sourceChannel, denom, amount, sender, receiver string, timeout *Timeout, memo string) (map[string]interface{}, error) { + body := map[string]interface{}{ + "source_port": sourcePort, + "source_channel": sourceChannel, + "token": map[string]string{ + "denom": denom, + "amount": amount, + }, + "sender": sender, + "receiver": receiver, + "memo": memo, + } + if timeout != nil { + body["timeout_height"] = timeout.Height + body["timeout_timestamp"] = timeout.Timestamp + } + var result map[string]interface{} + err := c.ibc.post(ctx, "/transfer", body, &result) + return result, err +} + +// GetDenomTrace returns denom trace +func (c *TransferClient) GetDenomTrace(ctx context.Context, ibcDenom string) (map[string]string, error) { + var result map[string]string + err := c.ibc.get(ctx, "/transfer/denom_trace/"+ibcDenom, &result) + return result, err +} + +// SwapsClient handles atomic swap operations +type SwapsClient struct { + ibc *SynorIbc +} + +// Initiate starts an atomic swap +func (c *SwapsClient) Initiate(ctx context.Context, responder string, initiatorAsset, responderAsset SwapAsset) (map[string]interface{}, error) { + var result map[string]interface{} + err := c.ibc.post(ctx, "/swaps/initiate", map[string]interface{}{ + "responder": responder, + "initiator_asset": initiatorAsset, + "responder_asset": responderAsset, + }, &result) + return result, err +} + +// Lock locks the initiator's tokens +func (c *SwapsClient) Lock(ctx context.Context, swapId SwapId) error { + return c.ibc.post(ctx, "/swaps/"+swapId.ID+"/lock", nil, nil) +} + +// Respond responds to a swap +func (c *SwapsClient) Respond(ctx context.Context, swapId SwapId, asset SwapAsset) (Htlc, error) { + var result Htlc + err := c.ibc.post(ctx, "/swaps/"+swapId.ID+"/respond", map[string]interface{}{ + "asset": asset, + }, &result) + return result, err +} + +// Claim claims tokens with secret +func (c *SwapsClient) Claim(ctx context.Context, swapId SwapId, secret []byte) (map[string]interface{}, error) { + var result map[string]interface{} + err := c.ibc.post(ctx, "/swaps/"+swapId.ID+"/claim", map[string]interface{}{ + "secret": base64.StdEncoding.EncodeToString(secret), + }, &result) + return result, err +} + +// Refund refunds an expired swap +func (c *SwapsClient) Refund(ctx context.Context, swapId SwapId) (map[string]interface{}, error) { + var result map[string]interface{} + err := c.ibc.post(ctx, "/swaps/"+swapId.ID+"/refund", nil, &result) + return result, err +} + +// Get returns a swap +func (c *SwapsClient) Get(ctx context.Context, swapId SwapId) (AtomicSwap, error) { + var result AtomicSwap + err := c.ibc.get(ctx, "/swaps/"+swapId.ID, &result) + return result, err +} + +// ListActive returns active swaps +func (c *SwapsClient) ListActive(ctx context.Context) ([]AtomicSwap, error) { + var result []AtomicSwap + err := c.ibc.get(ctx, "/swaps/active", &result) + return result, err +} + +// VerifySecret verifies a hashlock with secret +func (c *SwapsClient) VerifySecret(hashlock Hashlock, secret []byte) bool { + hash := sha256.Sum256(secret) + return bytes.Equal(hashlock.Hash, hash[:]) +} diff --git a/sdk/go/ibc/types.go b/sdk/go/ibc/types.go new file mode 100644 index 0000000..c53a632 --- /dev/null +++ b/sdk/go/ibc/types.go @@ -0,0 +1,407 @@ +// Package ibc provides the Synor IBC SDK for Go. +// +// Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability. +package ibc + +import ( + "time" +) + +// Height represents an IBC height +type Height struct { + RevisionNumber uint64 `json:"revision_number"` + RevisionHeight uint64 `json:"revision_height"` +} + +// IsZero checks if height is zero +func (h Height) IsZero() bool { + return h.RevisionNumber == 0 && h.RevisionHeight == 0 +} + +// Increment returns the next height +func (h Height) Increment() Height { + return Height{RevisionNumber: h.RevisionNumber, RevisionHeight: h.RevisionHeight + 1} +} + +// Timestamp in nanoseconds since Unix epoch +type Timestamp uint64 + +// Now returns current timestamp +func Now() Timestamp { + return Timestamp(time.Now().UnixNano()) +} + +// ChainId represents a chain identifier +type ChainId struct { + ID string `json:"id"` +} + +// Version represents an IBC version +type Version struct { + Identifier string `json:"identifier"` + Features []string `json:"features"` +} + +// DefaultConnectionVersion returns the default connection version +func DefaultConnectionVersion() Version { + return Version{ + Identifier: "1", + Features: []string{"ORDER_ORDERED", "ORDER_UNORDERED"}, + } +} + +// CommitmentPrefix for Merkle paths +type CommitmentPrefix struct { + KeyPrefix []byte `json:"key_prefix"` +} + +// ClientId represents a light client identifier +type ClientId struct { + ID string `json:"id"` +} + +// ClientType represents light client types +type ClientType string + +const ( + ClientTypeTendermint ClientType = "tendermint" + ClientTypeSoloMachine ClientType = "solo-machine" + ClientTypeLocalhost ClientType = "localhost" + ClientTypeWasm ClientType = "wasm" +) + +// TrustLevel configuration +type TrustLevel struct { + Numerator uint64 `json:"numerator"` + Denominator uint64 `json:"denominator"` +} + +// ClientState represents light client state +type ClientState struct { + ChainID string `json:"chain_id"` + TrustLevel TrustLevel `json:"trust_level"` + TrustingPeriod uint64 `json:"trusting_period"` + UnbondingPeriod uint64 `json:"unbonding_period"` + MaxClockDrift uint64 `json:"max_clock_drift"` + LatestHeight Height `json:"latest_height"` + FrozenHeight *Height `json:"frozen_height,omitempty"` +} + +// ConsensusState at a specific height +type ConsensusState struct { + Timestamp Timestamp `json:"timestamp"` + Root []byte `json:"root"` + NextValidatorsHash []byte `json:"next_validators_hash"` +} + +// Validator info +type Validator struct { + Address []byte `json:"address"` + PubKey map[string][]byte `json:"pub_key"` + VotingPower int64 `json:"voting_power"` + ProposerPriority int64 `json:"proposer_priority"` +} + +// ValidatorSet represents a set of validators +type ValidatorSet struct { + Validators []Validator `json:"validators"` + Proposer *Validator `json:"proposer,omitempty"` + TotalVotingPower int64 `json:"total_voting_power"` +} + +// Header for light client updates +type Header struct { + SignedHeader map[string]interface{} `json:"signed_header"` + ValidatorSet ValidatorSet `json:"validator_set"` + TrustedHeight Height `json:"trusted_height"` + TrustedValidators ValidatorSet `json:"trusted_validators"` +} + +// ConnectionId represents a connection identifier +type ConnectionId struct { + ID string `json:"id"` +} + +// ConnectionState represents connection state +type ConnectionState string + +const ( + ConnectionStateUninitialized ConnectionState = "uninitialized" + ConnectionStateInit ConnectionState = "init" + ConnectionStateTryOpen ConnectionState = "tryopen" + ConnectionStateOpen ConnectionState = "open" +) + +// ConnectionCounterparty represents the counterparty +type ConnectionCounterparty struct { + ClientID ClientId `json:"client_id"` + ConnectionID *ConnectionId `json:"connection_id,omitempty"` + Prefix *CommitmentPrefix `json:"prefix,omitempty"` +} + +// ConnectionEnd represents a connection end +type ConnectionEnd struct { + ClientID ClientId `json:"client_id"` + Versions []Version `json:"versions"` + State ConnectionState `json:"state"` + Counterparty ConnectionCounterparty `json:"counterparty"` + DelayPeriod uint64 `json:"delay_period"` +} + +// PortId represents a port identifier +type PortId struct { + ID string `json:"id"` +} + +// TransferPort returns the transfer port +func TransferPort() PortId { + return PortId{ID: "transfer"} +} + +// ChannelId represents a channel identifier +type ChannelId struct { + ID string `json:"id"` +} + +// ChannelOrder represents channel ordering +type ChannelOrder string + +const ( + ChannelOrderUnordered ChannelOrder = "unordered" + ChannelOrderOrdered ChannelOrder = "ordered" +) + +// ChannelState represents channel state +type ChannelState string + +const ( + ChannelStateUninitialized ChannelState = "uninitialized" + ChannelStateInit ChannelState = "init" + ChannelStateTryOpen ChannelState = "tryopen" + ChannelStateOpen ChannelState = "open" + ChannelStateClosed ChannelState = "closed" +) + +// ChannelCounterparty represents the counterparty +type ChannelCounterparty struct { + PortID PortId `json:"port_id"` + ChannelID *ChannelId `json:"channel_id,omitempty"` +} + +// Channel represents a channel end +type Channel struct { + State ChannelState `json:"state"` + Ordering ChannelOrder `json:"ordering"` + Counterparty ChannelCounterparty `json:"counterparty"` + ConnectionHops []ConnectionId `json:"connection_hops"` + Version string `json:"version"` +} + +// Packet represents an IBC packet +type Packet struct { + Sequence uint64 `json:"sequence"` + SourcePort PortId `json:"source_port"` + SourceChannel ChannelId `json:"source_channel"` + DestPort PortId `json:"dest_port"` + DestChannel ChannelId `json:"dest_channel"` + Data []byte `json:"data"` + TimeoutHeight Height `json:"timeout_height"` + TimeoutTimestamp Timestamp `json:"timeout_timestamp"` +} + +// PacketCommitment hash +type PacketCommitment struct { + Hash []byte `json:"hash"` +} + +// Acknowledgement represents packet acknowledgement +type Acknowledgement struct { + Success *[]byte `json:"success,omitempty"` + Error *string `json:"error,omitempty"` +} + +// IsSuccess checks if acknowledgement is success +func (a Acknowledgement) IsSuccess() bool { + return a.Success != nil +} + +// Timeout information +type Timeout struct { + Height Height `json:"height"` + Timestamp Timestamp `json:"timestamp"` +} + +// HeightTimeout creates a height-only timeout +func HeightTimeout(height uint64) Timeout { + return Timeout{Height: Height{RevisionHeight: height}} +} + +// FungibleTokenPacketData for ICS-20 transfers +type FungibleTokenPacketData struct { + Denom string `json:"denom"` + Amount string `json:"amount"` + Sender string `json:"sender"` + Receiver string `json:"receiver"` + Memo string `json:"memo,omitempty"` +} + +// SwapId represents a swap identifier +type SwapId struct { + ID string `json:"id"` +} + +// SwapState represents swap state +type SwapState string + +const ( + SwapStatePending SwapState = "pending" + SwapStateLocked SwapState = "locked" + SwapStateCompleted SwapState = "completed" + SwapStateRefunded SwapState = "refunded" + SwapStateExpired SwapState = "expired" + SwapStateCancelled SwapState = "cancelled" +) + +// SwapAsset types +type SwapAsset struct { + Native *NativeAsset `json:"native,omitempty"` + Ics20 *Ics20Asset `json:"ics20,omitempty"` + Ics721 *Ics721Asset `json:"ics721,omitempty"` +} + +// NativeAsset represents native blockchain token +type NativeAsset struct { + Amount uint64 `json:"amount"` +} + +// Ics20Asset represents ICS-20 fungible token +type Ics20Asset struct { + Denom string `json:"denom"` + Amount uint64 `json:"amount"` +} + +// Ics721Asset represents ICS-721 NFT +type Ics721Asset struct { + ClassID string `json:"class_id"` + TokenIDs []string `json:"token_ids"` +} + +// Hashlock - hash of the secret +type Hashlock struct { + Hash []byte `json:"hash"` +} + +// Timelock - expiration time +type Timelock struct { + Expiry Timestamp `json:"expiry"` +} + +// IsExpired checks if timelock has expired +func (t Timelock) IsExpired(current Timestamp) bool { + return current >= t.Expiry +} + +// Htlc represents a Hashed Time-Locked Contract +type Htlc struct { + SwapID SwapId `json:"swap_id"` + State SwapState `json:"state"` + Initiator string `json:"initiator"` + Responder string `json:"responder"` + Asset SwapAsset `json:"asset"` + Hashlock Hashlock `json:"hashlock"` + Timelock Timelock `json:"timelock"` + Secret []byte `json:"secret,omitempty"` + ChannelID *ChannelId `json:"channel_id,omitempty"` + PortID *PortId `json:"port_id,omitempty"` + CreatedAt Timestamp `json:"created_at"` + CompletedAt *Timestamp `json:"completed_at,omitempty"` +} + +// AtomicSwap between two chains +type AtomicSwap struct { + SwapID SwapId `json:"swap_id"` + InitiatorHtlc Htlc `json:"initiator_htlc"` + ResponderHtlc *Htlc `json:"responder_htlc,omitempty"` + State SwapState `json:"state"` +} + +// SwapAction represents swap packet actions +type SwapAction string + +const ( + SwapActionInitiate SwapAction = "initiate" + SwapActionRespond SwapAction = "respond" + SwapActionClaim SwapAction = "claim" + SwapActionRefund SwapAction = "refund" +) + +// SwapPacketData for cross-chain swaps +type SwapPacketData struct { + SwapID SwapId `json:"swap_id"` + Action SwapAction `json:"action"` + Initiator string `json:"initiator"` + Responder string `json:"responder"` + Asset SwapAsset `json:"asset"` + Hashlock Hashlock `json:"hashlock"` + TimelockExpiry uint64 `json:"timelock_expiry"` + Secret []byte `json:"secret,omitempty"` +} + +// MerkleProof for commitment verification +type MerkleProof struct { + Proofs []ProofOp `json:"proofs"` +} + +// ProofOp represents a proof operation +type ProofOp struct { + Type string `json:"type"` + Key []byte `json:"key"` + Data []byte `json:"data"` +} + +// CommitmentProof with height +type CommitmentProof struct { + Proof MerkleProof `json:"proof"` + Height Height `json:"height"` +} + +// IbcEvent represents IBC events +type IbcEvent struct { + Type string `json:"type"` + Data map[string]interface{} `json:"data"` +} + +// Config for IBC SDK +type Config struct { + APIKey string + Endpoint string + WSEndpoint string + Timeout time.Duration + Retries int + ChainID string + Debug bool +} + +// DefaultConfig returns default configuration +func DefaultConfig(apiKey string) Config { + return Config{ + APIKey: apiKey, + Endpoint: "https://ibc.synor.io/v1", + WSEndpoint: "wss://ibc.synor.io/v1/ws", + Timeout: 30 * time.Second, + Retries: 3, + ChainID: "synor-1", + Debug: false, + } +} + +// IbcError represents IBC errors +type IbcError struct { + Message string + Code string + Status int +} + +func (e *IbcError) Error() string { + return e.Message +} diff --git a/sdk/java/src/main/java/io/synor/ibc/SynorIbc.java b/sdk/java/src/main/java/io/synor/ibc/SynorIbc.java new file mode 100644 index 0000000..2f54881 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/ibc/SynorIbc.java @@ -0,0 +1,325 @@ +package io.synor.ibc; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import io.synor.ibc.Types.*; + +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.*; +import java.util.concurrent.CompletableFuture; + +/** + * Synor IBC SDK for Java + * + * Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability. + */ +public class SynorIbc implements AutoCloseable { + private final IbcConfig config; + private final HttpClient httpClient; + private final Gson gson; + private volatile boolean closed = false; + + public final LightClientClient clients; + public final ConnectionsClient connections; + public final ChannelsClient channels; + public final TransferClient transfer; + public final SwapsClient swaps; + + public SynorIbc(IbcConfig config) { + this.config = config; + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(config.getTimeout())) + .build(); + this.gson = new Gson(); + + this.clients = new LightClientClient(this); + this.connections = new ConnectionsClient(this); + this.channels = new ChannelsClient(this); + this.transfer = new TransferClient(this); + this.swaps = new SwapsClient(this); + } + + public String getChainId() { + return config.getChainId(); + } + + public CompletableFuture> getChainInfo() { + return get("/chain"); + } + + public CompletableFuture getHeight() { + return get("/chain/height").thenApply(Height::fromJson); + } + + public CompletableFuture healthCheck() { + return get("/health") + .thenApply(result -> "healthy".equals(result.get("status"))) + .exceptionally(e -> false); + } + + @Override + public void close() { + closed = true; + } + + public boolean isClosed() { + return closed; + } + + // Internal HTTP methods + CompletableFuture> get(String path) { + checkClosed(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(config.getEndpoint() + path)) + .headers(getHeaders()) + .GET() + .build(); + return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(this::handleResponse); + } + + CompletableFuture> post(String path, Map body) { + checkClosed(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(config.getEndpoint() + path)) + .headers(getHeaders()) + .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(body))) + .build(); + return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(this::handleResponse); + } + + private String[] getHeaders() { + return new String[] { + "Content-Type", "application/json", + "Authorization", "Bearer " + config.getApiKey(), + "X-SDK-Version", "java/0.1.0", + "X-Chain-Id", config.getChainId() + }; + } + + private Map handleResponse(HttpResponse response) { + if (response.statusCode() >= 400) { + Map error = null; + try { + error = gson.fromJson(response.body(), + new TypeToken>(){}.getType()); + } catch (Exception ignored) {} + throw new IbcException( + error != null ? (String) error.get("message") : "HTTP " + response.statusCode(), + error != null ? (String) error.get("code") : null, + response.statusCode() + ); + } + return gson.fromJson(response.body(), new TypeToken>(){}.getType()); + } + + private void checkClosed() { + if (closed) { + throw new IbcException("Client has been closed", "CLIENT_CLOSED", null); + } + } + + /** + * Light client sub-client + */ + public static class LightClientClient { + private final SynorIbc ibc; + + LightClientClient(SynorIbc ibc) { + this.ibc = ibc; + } + + public CompletableFuture create(ClientType clientType, + ClientState clientState, + Map consensusState) { + Map body = new HashMap<>(); + body.put("client_type", clientType.getValue()); + body.put("client_state", clientState.toJson()); + body.put("consensus_state", consensusState); + return ibc.post("/clients", body) + .thenApply(result -> new ClientId((String) result.get("client_id"))); + } + + @SuppressWarnings("unchecked") + public CompletableFuture getState(ClientId clientId) { + return ibc.get("/clients/" + clientId.getId() + "/state") + .thenApply(ClientState::fromJson); + } + + @SuppressWarnings("unchecked") + public CompletableFuture>> list() { + return ibc.get("/clients") + .thenApply(result -> (List>) result.getOrDefault("clients", List.of())); + } + } + + /** + * Connections sub-client + */ + public static class ConnectionsClient { + private final SynorIbc ibc; + + ConnectionsClient(SynorIbc ibc) { + this.ibc = ibc; + } + + public CompletableFuture openInit(ClientId clientId, + ClientId counterpartyClientId) { + Map body = new HashMap<>(); + body.put("client_id", clientId.getId()); + body.put("counterparty_client_id", counterpartyClientId.getId()); + return ibc.post("/connections/init", body) + .thenApply(result -> new ConnectionId((String) result.get("connection_id"))); + } + + public CompletableFuture> get(ConnectionId connectionId) { + return ibc.get("/connections/" + connectionId.getId()); + } + + @SuppressWarnings("unchecked") + public CompletableFuture>> list() { + return ibc.get("/connections") + .thenApply(result -> (List>) result.getOrDefault("connections", List.of())); + } + } + + /** + * Channels sub-client + */ + public static class ChannelsClient { + private final SynorIbc ibc; + + ChannelsClient(SynorIbc ibc) { + this.ibc = ibc; + } + + public CompletableFuture bindPort(PortId portId, String module) { + Map body = new HashMap<>(); + body.put("port_id", portId.getId()); + body.put("module", module); + return ibc.post("/ports/bind", body).thenApply(r -> null); + } + + public CompletableFuture openInit(PortId portId, + ChannelOrder ordering, + ConnectionId connectionId, + PortId counterpartyPort, + String version) { + Map body = new HashMap<>(); + body.put("port_id", portId.getId()); + body.put("ordering", ordering.name().toLowerCase()); + body.put("connection_id", connectionId.getId()); + body.put("counterparty_port", counterpartyPort.getId()); + body.put("version", version); + return ibc.post("/channels/init", body) + .thenApply(result -> new ChannelId((String) result.get("channel_id"))); + } + + public CompletableFuture> get(PortId portId, ChannelId channelId) { + return ibc.get("/channels/" + portId.getId() + "/" + channelId.getId()); + } + + @SuppressWarnings("unchecked") + public CompletableFuture>> list() { + return ibc.get("/channels") + .thenApply(result -> (List>) result.getOrDefault("channels", List.of())); + } + } + + /** + * Transfer sub-client (ICS-20) + */ + public static class TransferClient { + private final SynorIbc ibc; + + TransferClient(SynorIbc ibc) { + this.ibc = ibc; + } + + public CompletableFuture> transfer(String sourcePort, + String sourceChannel, + String denom, + String amount, + String sender, + String receiver, + Timeout timeout, + String memo) { + Map body = new HashMap<>(); + body.put("source_port", sourcePort); + body.put("source_channel", sourceChannel); + body.put("token", Map.of("denom", denom, "amount", amount)); + body.put("sender", sender); + body.put("receiver", receiver); + if (timeout != null) { + body.put("timeout_height", timeout.getHeight().toJson()); + body.put("timeout_timestamp", timeout.getTimestamp().toString()); + } + if (memo != null) body.put("memo", memo); + return ibc.post("/transfer", body); + } + + public CompletableFuture> getDenomTrace(String ibcDenom) { + return ibc.get("/transfer/denom_trace/" + ibcDenom); + } + } + + /** + * Swaps sub-client (HTLC) + */ + public static class SwapsClient { + private final SynorIbc ibc; + + SwapsClient(SynorIbc ibc) { + this.ibc = ibc; + } + + public CompletableFuture> initiate(String responder, + Map initiatorAsset, + Map responderAsset) { + Map body = new HashMap<>(); + body.put("responder", responder); + body.put("initiator_asset", initiatorAsset); + body.put("responder_asset", responderAsset); + return ibc.post("/swaps/initiate", body); + } + + public CompletableFuture lock(SwapId swapId) { + return ibc.post("/swaps/" + swapId.getId() + "/lock", Map.of()) + .thenApply(r -> null); + } + + public CompletableFuture> respond(SwapId swapId, + Map asset) { + return ibc.post("/swaps/" + swapId.getId() + "/respond", Map.of("asset", asset)); + } + + public CompletableFuture> claim(SwapId swapId, byte[] secret) { + return ibc.post("/swaps/" + swapId.getId() + "/claim", + Map.of("secret", Base64.getEncoder().encodeToString(secret))); + } + + public CompletableFuture> refund(SwapId swapId) { + return ibc.post("/swaps/" + swapId.getId() + "/refund", Map.of()); + } + + public CompletableFuture get(SwapId swapId) { + return ibc.get("/swaps/" + swapId.getId()) + .thenApply(AtomicSwap::fromJson); + } + + @SuppressWarnings("unchecked") + public CompletableFuture> listActive() { + return ibc.get("/swaps/active") + .thenApply(result -> { + List> swaps = + (List>) result.getOrDefault("swaps", List.of()); + return swaps.stream().map(AtomicSwap::fromJson).toList(); + }); + } + } +} diff --git a/sdk/java/src/main/java/io/synor/ibc/Types.java b/sdk/java/src/main/java/io/synor/ibc/Types.java new file mode 100644 index 0000000..4c2db7f --- /dev/null +++ b/sdk/java/src/main/java/io/synor/ibc/Types.java @@ -0,0 +1,637 @@ +package io.synor.ibc; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +/** + * Synor IBC SDK Types for Java + * + * Inter-Blockchain Communication (IBC) protocol types. + */ +public class Types { + + /** + * IBC Height representation + */ + public static class Height { + private final long revisionNumber; + private final long revisionHeight; + + public Height() { + this(0, 1); + } + + public Height(long revisionNumber, long revisionHeight) { + this.revisionNumber = revisionNumber; + this.revisionHeight = revisionHeight; + } + + public long getRevisionNumber() { return revisionNumber; } + public long getRevisionHeight() { return revisionHeight; } + + public boolean isZero() { + return revisionNumber == 0 && revisionHeight == 0; + } + + public Height increment() { + return new Height(revisionNumber, revisionHeight + 1); + } + + public static Height fromJson(Map json) { + return new Height( + ((Number) json.getOrDefault("revision_number", 0)).longValue(), + ((Number) json.getOrDefault("revision_height", 1)).longValue() + ); + } + + public Map toJson() { + return Map.of( + "revision_number", revisionNumber, + "revision_height", revisionHeight + ); + } + } + + /** + * Chain identifier + */ + public static class ChainId { + private final String id; + + public ChainId(String id) { + this.id = id; + } + + public String getId() { return id; } + + public static ChainId fromJson(Map json) { + return new ChainId((String) json.get("id")); + } + + public Map toJson() { + return Map.of("id", id); + } + } + + /** + * IBC Version with features + */ + public static class Version { + private final String identifier; + private final List features; + + public Version(String identifier, List features) { + this.identifier = identifier; + this.features = features; + } + + public String getIdentifier() { return identifier; } + public List getFeatures() { return features; } + + public static Version defaultConnection() { + return new Version("1", List.of("ORDER_ORDERED", "ORDER_UNORDERED")); + } + + @SuppressWarnings("unchecked") + public static Version fromJson(Map json) { + return new Version( + (String) json.get("identifier"), + (List) json.get("features") + ); + } + + public Map toJson() { + return Map.of( + "identifier", identifier, + "features", features + ); + } + } + + /** + * Light client types + */ + public enum ClientType { + TENDERMINT("tendermint"), + SOLO_MACHINE("solo_machine"), + LOCALHOST("localhost"), + WASM("wasm"); + + private final String value; + + ClientType(String value) { + this.value = value; + } + + public String getValue() { return value; } + } + + /** + * Light client identifier + */ + public static class ClientId { + private final String id; + + public ClientId(String id) { + this.id = id; + } + + public String getId() { return id; } + + public static ClientId fromJson(Map json) { + return new ClientId((String) json.get("id")); + } + + public Map toJson() { + return Map.of("id", id); + } + } + + /** + * Trust level configuration + */ + public static class TrustLevel { + private final int numerator; + private final int denominator; + + public TrustLevel() { + this(1, 3); + } + + public TrustLevel(int numerator, int denominator) { + this.numerator = numerator; + this.denominator = denominator; + } + + public int getNumerator() { return numerator; } + public int getDenominator() { return denominator; } + + public static TrustLevel fromJson(Map json) { + return new TrustLevel( + ((Number) json.getOrDefault("numerator", 1)).intValue(), + ((Number) json.getOrDefault("denominator", 3)).intValue() + ); + } + + public Map toJson() { + return Map.of( + "numerator", numerator, + "denominator", denominator + ); + } + } + + /** + * Light client state + */ + public static class ClientState { + private final String chainId; + private final TrustLevel trustLevel; + private final long trustingPeriod; + private final long unbondingPeriod; + private final long maxClockDrift; + private final Height latestHeight; + private final Height frozenHeight; + + public ClientState(String chainId, TrustLevel trustLevel, long trustingPeriod, + long unbondingPeriod, long maxClockDrift, Height latestHeight, + Height frozenHeight) { + this.chainId = chainId; + this.trustLevel = trustLevel; + this.trustingPeriod = trustingPeriod; + this.unbondingPeriod = unbondingPeriod; + this.maxClockDrift = maxClockDrift; + this.latestHeight = latestHeight; + this.frozenHeight = frozenHeight; + } + + public String getChainId() { return chainId; } + public TrustLevel getTrustLevel() { return trustLevel; } + public long getTrustingPeriod() { return trustingPeriod; } + public long getUnbondingPeriod() { return unbondingPeriod; } + public long getMaxClockDrift() { return maxClockDrift; } + public Height getLatestHeight() { return latestHeight; } + public Height getFrozenHeight() { return frozenHeight; } + + @SuppressWarnings("unchecked") + public static ClientState fromJson(Map json) { + return new ClientState( + (String) json.get("chain_id"), + TrustLevel.fromJson((Map) json.get("trust_level")), + ((Number) json.get("trusting_period")).longValue(), + ((Number) json.get("unbonding_period")).longValue(), + ((Number) json.get("max_clock_drift")).longValue(), + Height.fromJson((Map) json.get("latest_height")), + json.get("frozen_height") != null + ? Height.fromJson((Map) json.get("frozen_height")) + : null + ); + } + + public Map toJson() { + var map = new java.util.HashMap(); + map.put("chain_id", chainId); + map.put("trust_level", trustLevel.toJson()); + map.put("trusting_period", trustingPeriod); + map.put("unbonding_period", unbondingPeriod); + map.put("max_clock_drift", maxClockDrift); + map.put("latest_height", latestHeight.toJson()); + if (frozenHeight != null) { + map.put("frozen_height", frozenHeight.toJson()); + } + return map; + } + } + + /** + * Connection state + */ + public enum ConnectionState { + UNINITIALIZED, INIT, TRYOPEN, OPEN + } + + /** + * Connection identifier + */ + public static class ConnectionId { + private final String id; + + public ConnectionId(String id) { + this.id = id; + } + + public String getId() { return id; } + + public static ConnectionId newId(int sequence) { + return new ConnectionId("connection-" + sequence); + } + + public static ConnectionId fromJson(Map json) { + return new ConnectionId((String) json.get("id")); + } + + public Map toJson() { + return Map.of("id", id); + } + } + + /** + * Port identifier + */ + public static class PortId { + private final String id; + + public PortId(String id) { + this.id = id; + } + + public String getId() { return id; } + + public static PortId transfer() { + return new PortId("transfer"); + } + + public static PortId fromJson(Map json) { + return new PortId((String) json.get("id")); + } + + public Map toJson() { + return Map.of("id", id); + } + } + + /** + * Channel identifier + */ + public static class ChannelId { + private final String id; + + public ChannelId(String id) { + this.id = id; + } + + public String getId() { return id; } + + public static ChannelId newId(int sequence) { + return new ChannelId("channel-" + sequence); + } + + public static ChannelId fromJson(Map json) { + return new ChannelId((String) json.get("id")); + } + + public Map toJson() { + return Map.of("id", id); + } + } + + /** + * Channel ordering + */ + public enum ChannelOrder { + UNORDERED, ORDERED + } + + /** + * Channel state + */ + public enum ChannelState { + UNINITIALIZED, INIT, TRYOPEN, OPEN, CLOSED + } + + /** + * Timeout information + */ + public static class Timeout { + private final Height height; + private final BigInteger timestamp; + + public Timeout(Height height) { + this(height, BigInteger.ZERO); + } + + public Timeout(Height height, BigInteger timestamp) { + this.height = height; + this.timestamp = timestamp; + } + + public Height getHeight() { return height; } + public BigInteger getTimestamp() { return timestamp; } + + public static Timeout fromHeight(long height) { + return new Timeout(new Height(0, height)); + } + + public static Timeout fromTimestamp(BigInteger timestamp) { + return new Timeout(new Height(), timestamp); + } + + public Map toJson() { + return Map.of( + "height", height.toJson(), + "timestamp", timestamp.toString() + ); + } + } + + /** + * Transfer packet data (ICS-20) + */ + public static class FungibleTokenPacketData { + private final String denom; + private final String amount; + private final String sender; + private final String receiver; + private final String memo; + + public FungibleTokenPacketData(String denom, String amount, String sender, + String receiver, String memo) { + this.denom = denom; + this.amount = amount; + this.sender = sender; + this.receiver = receiver; + this.memo = memo != null ? memo : ""; + } + + public String getDenom() { return denom; } + public String getAmount() { return amount; } + public String getSender() { return sender; } + public String getReceiver() { return receiver; } + public String getMemo() { return memo; } + + public boolean isNative() { + return !denom.contains("/"); + } + + public static FungibleTokenPacketData fromJson(Map json) { + return new FungibleTokenPacketData( + (String) json.get("denom"), + (String) json.get("amount"), + (String) json.get("sender"), + (String) json.get("receiver"), + (String) json.getOrDefault("memo", "") + ); + } + + public Map toJson() { + return Map.of( + "denom", denom, + "amount", amount, + "sender", sender, + "receiver", receiver, + "memo", memo + ); + } + } + + /** + * Swap state + */ + public enum SwapState { + PENDING, LOCKED, COMPLETED, REFUNDED, EXPIRED, CANCELLED + } + + /** + * Swap identifier + */ + public static class SwapId { + private final String id; + + public SwapId(String id) { + this.id = id; + } + + public String getId() { return id; } + + public static SwapId fromJson(Map json) { + return new SwapId((String) json.get("id")); + } + + public Map toJson() { + return Map.of("id", id); + } + } + + /** + * Hashlock - hash of the secret + */ + public static class Hashlock { + private final byte[] hash; + + public Hashlock(byte[] hash) { + this.hash = hash; + } + + public byte[] getHash() { return hash; } + + @SuppressWarnings("unchecked") + public static Hashlock fromJson(Map json) { + List hashList = (List) json.get("hash"); + byte[] hash = new byte[hashList.size()]; + for (int i = 0; i < hashList.size(); i++) { + hash[i] = hashList.get(i).byteValue(); + } + return new Hashlock(hash); + } + + public Map toJson() { + int[] hashArray = new int[hash.length]; + for (int i = 0; i < hash.length; i++) { + hashArray[i] = hash[i] & 0xFF; + } + return Map.of("hash", hashArray); + } + } + + /** + * Timelock - expiration time + */ + public static class Timelock { + private final BigInteger expiry; + + public Timelock(BigInteger expiry) { + this.expiry = expiry; + } + + public BigInteger getExpiry() { return expiry; } + + public boolean isExpired(BigInteger current) { + return current.compareTo(expiry) >= 0; + } + + public static Timelock fromJson(Map json) { + return new Timelock(new BigInteger(json.get("expiry").toString())); + } + + public Map toJson() { + return Map.of("expiry", expiry.toString()); + } + } + + /** + * Atomic swap + */ + public static class AtomicSwap { + private final SwapId swapId; + private final SwapState state; + private final Map initiatorHtlc; + private final Map responderHtlc; + + public AtomicSwap(SwapId swapId, SwapState state, + Map initiatorHtlc, + Map responderHtlc) { + this.swapId = swapId; + this.state = state; + this.initiatorHtlc = initiatorHtlc; + this.responderHtlc = responderHtlc; + } + + public SwapId getSwapId() { return swapId; } + public SwapState getState() { return state; } + public Map getInitiatorHtlc() { return initiatorHtlc; } + public Map getResponderHtlc() { return responderHtlc; } + + @SuppressWarnings("unchecked") + public static AtomicSwap fromJson(Map json) { + return new AtomicSwap( + SwapId.fromJson((Map) json.get("swap_id")), + SwapState.valueOf(((String) json.get("state")).toUpperCase()), + (Map) json.get("initiator_htlc"), + (Map) json.get("responder_htlc") + ); + } + } + + /** + * IBC SDK configuration + */ + public static class IbcConfig { + private final String apiKey; + private final String endpoint; + private final String wsEndpoint; + private final int timeout; + private final int retries; + private final String chainId; + private final boolean debug; + + public IbcConfig(String apiKey) { + this(apiKey, "https://ibc.synor.io/v1", "wss://ibc.synor.io/v1/ws", + 30, 3, "synor-1", false); + } + + public IbcConfig(String apiKey, String endpoint, String wsEndpoint, + int timeout, int retries, String chainId, boolean debug) { + this.apiKey = apiKey; + this.endpoint = endpoint; + this.wsEndpoint = wsEndpoint; + this.timeout = timeout; + this.retries = retries; + this.chainId = chainId; + this.debug = debug; + } + + public String getApiKey() { return apiKey; } + public String getEndpoint() { return endpoint; } + public String getWsEndpoint() { return wsEndpoint; } + public int getTimeout() { return timeout; } + public int getRetries() { return retries; } + public String getChainId() { return chainId; } + public boolean isDebug() { return debug; } + + public static Builder builder(String apiKey) { + return new Builder(apiKey); + } + + public static class Builder { + private String apiKey; + private String endpoint = "https://ibc.synor.io/v1"; + private String wsEndpoint = "wss://ibc.synor.io/v1/ws"; + private int timeout = 30; + private int retries = 3; + private String chainId = "synor-1"; + private boolean debug = false; + + public Builder(String apiKey) { + this.apiKey = apiKey; + } + + public Builder endpoint(String endpoint) { this.endpoint = endpoint; return this; } + public Builder wsEndpoint(String wsEndpoint) { this.wsEndpoint = wsEndpoint; return this; } + public Builder timeout(int timeout) { this.timeout = timeout; return this; } + public Builder retries(int retries) { this.retries = retries; return this; } + public Builder chainId(String chainId) { this.chainId = chainId; return this; } + public Builder debug(boolean debug) { this.debug = debug; return this; } + + public IbcConfig build() { + return new IbcConfig(apiKey, endpoint, wsEndpoint, timeout, retries, chainId, debug); + } + } + } + + /** + * IBC exception + */ + public static class IbcException extends RuntimeException { + private final String code; + private final Integer status; + + public IbcException(String message) { + this(message, null, null); + } + + public IbcException(String message, String code, Integer status) { + super(message); + this.code = code; + this.status = status; + } + + public String getCode() { return code; } + public Integer getStatus() { return status; } + + @Override + public String toString() { + return "IbcException: " + getMessage() + (code != null ? " (" + code + ")" : ""); + } + } +} diff --git a/sdk/js/src/ibc/client.ts b/sdk/js/src/ibc/client.ts new file mode 100644 index 0000000..47dc6ed --- /dev/null +++ b/sdk/js/src/ibc/client.ts @@ -0,0 +1,760 @@ +/** + * Synor IBC SDK Client + * + * Inter-Blockchain Communication (IBC) client for cross-chain interoperability. + * Supports light client verification, connections, channels, packets, transfers, and atomic swaps. + */ + +import type { + IbcConfig, + Height, + ClientId, + ClientState, + ConsensusState, + Header, + ConnectionId, + ConnectionEnd, + PortId, + ChannelId, + Channel, + ChannelOrder, + Packet, + Acknowledgement, + Timeout, + SwapId, + SwapAsset, + SwapState, + AtomicSwap, + Htlc, + Hashlock, + IbcEvent, + CreateClientParams, + UpdateClientParams, + ConnOpenInitParams, + ConnOpenTryParams, + ChanOpenInitParams, + ChanOpenTryParams, + SendPacketParams, + TransferParams, + InitiateSwapParams, + RespondSwapParams, + ClaimSwapParams, + FungibleTokenPacketData, + CommitmentProof, + Version, +} from './types'; +import { IbcException } from './types'; + +const DEFAULT_ENDPOINT = 'https://ibc.synor.io/v1'; +const DEFAULT_WS_ENDPOINT = 'wss://ibc.synor.io/v1/ws'; +const DEFAULT_TIMEOUT = 30000; +const DEFAULT_RETRIES = 3; +const DEFAULT_CHAIN_ID = 'synor-1'; + +/** Subscription handle */ +export interface Subscription { + unsubscribe(): void; +} + +/** + * IBC Light Client sub-client + */ +export class LightClientClient { + constructor(private ibc: SynorIbc) {} + + /** Create a new light client */ + async create(params: CreateClientParams): Promise { + return this.ibc.post('/clients', params); + } + + /** Update a light client with new header */ + async update(params: UpdateClientParams): Promise { + return this.ibc.post(`/clients/${params.clientId.id}/update`, { + header: params.header, + }); + } + + /** Get client state */ + async getState(clientId: ClientId): Promise { + return this.ibc.get(`/clients/${clientId.id}/state`); + } + + /** Get consensus state at height */ + async getConsensusState(clientId: ClientId, height: Height): Promise { + return this.ibc.get( + `/clients/${clientId.id}/consensus/${height.revisionNumber}-${height.revisionHeight}` + ); + } + + /** List all clients */ + async list(): Promise> { + return this.ibc.get('/clients'); + } + + /** Check if client is active */ + async isActive(clientId: ClientId): Promise { + const response = await this.ibc.get<{ active: boolean }>(`/clients/${clientId.id}/status`); + return response.active; + } +} + +/** + * IBC Connections sub-client + */ +export class ConnectionsClient { + constructor(private ibc: SynorIbc) {} + + /** Initialize connection handshake */ + async openInit(params: ConnOpenInitParams): Promise { + return this.ibc.post('/connections/init', params); + } + + /** Try connection handshake (counterparty) */ + async openTry(params: ConnOpenTryParams): Promise { + return this.ibc.post('/connections/try', params); + } + + /** Acknowledge connection handshake */ + async openAck( + connectionId: ConnectionId, + counterpartyConnectionId: ConnectionId, + version: Version, + proofTry: Uint8Array, + proofHeight: Height + ): Promise { + return this.ibc.post(`/connections/${connectionId.id}/ack`, { + counterpartyConnectionId, + version, + proofTry: Buffer.from(proofTry).toString('base64'), + proofHeight, + }); + } + + /** Confirm connection handshake */ + async openConfirm( + connectionId: ConnectionId, + proofAck: Uint8Array, + proofHeight: Height + ): Promise { + return this.ibc.post(`/connections/${connectionId.id}/confirm`, { + proofAck: Buffer.from(proofAck).toString('base64'), + proofHeight, + }); + } + + /** Get connection by ID */ + async get(connectionId: ConnectionId): Promise { + return this.ibc.get(`/connections/${connectionId.id}`); + } + + /** List all connections */ + async list(): Promise> { + return this.ibc.get('/connections'); + } + + /** Get connections for a client */ + async getByClient(clientId: ClientId): Promise { + return this.ibc.get(`/clients/${clientId.id}/connections`); + } +} + +/** + * IBC Channels sub-client + */ +export class ChannelsClient { + constructor(private ibc: SynorIbc) {} + + /** Bind a port */ + async bindPort(portId: PortId, module: string): Promise { + return this.ibc.post('/ports/bind', { portId, module }); + } + + /** Release port binding */ + async releasePort(portId: PortId): Promise { + return this.ibc.post(`/ports/${portId.id}/release`, {}); + } + + /** Initialize channel handshake */ + async openInit(params: ChanOpenInitParams): Promise { + return this.ibc.post('/channels/init', params); + } + + /** Try channel handshake (counterparty) */ + async openTry(params: ChanOpenTryParams): Promise { + return this.ibc.post('/channels/try', params); + } + + /** Acknowledge channel handshake */ + async openAck( + portId: PortId, + channelId: ChannelId, + counterpartyChannelId: ChannelId, + counterpartyVersion: string, + proofTry: Uint8Array, + proofHeight: Height + ): Promise { + return this.ibc.post(`/channels/${portId.id}/${channelId.id}/ack`, { + counterpartyChannelId, + counterpartyVersion, + proofTry: Buffer.from(proofTry).toString('base64'), + proofHeight, + }); + } + + /** Confirm channel handshake */ + async openConfirm( + portId: PortId, + channelId: ChannelId, + proofAck: Uint8Array, + proofHeight: Height + ): Promise { + return this.ibc.post(`/channels/${portId.id}/${channelId.id}/confirm`, { + proofAck: Buffer.from(proofAck).toString('base64'), + proofHeight, + }); + } + + /** Close channel init */ + async closeInit(portId: PortId, channelId: ChannelId): Promise { + return this.ibc.post(`/channels/${portId.id}/${channelId.id}/close`, {}); + } + + /** Close channel confirm */ + async closeConfirm( + portId: PortId, + channelId: ChannelId, + proofInit: Uint8Array, + proofHeight: Height + ): Promise { + return this.ibc.post(`/channels/${portId.id}/${channelId.id}/close/confirm`, { + proofInit: Buffer.from(proofInit).toString('base64'), + proofHeight, + }); + } + + /** Get channel */ + async get(portId: PortId, channelId: ChannelId): Promise { + return this.ibc.get(`/channels/${portId.id}/${channelId.id}`); + } + + /** List channels for a port */ + async listByPort(portId: PortId): Promise> { + return this.ibc.get(`/ports/${portId.id}/channels`); + } + + /** List all channels */ + async list(): Promise> { + return this.ibc.get('/channels'); + } +} + +/** + * IBC Packets sub-client + */ +export class PacketsClient { + constructor(private ibc: SynorIbc) {} + + /** Send a packet */ + async send(params: SendPacketParams): Promise<{ sequence: bigint; packet: Packet }> { + return this.ibc.post('/packets/send', { + ...params, + data: Buffer.from(params.data).toString('base64'), + }); + } + + /** Receive a packet (called by relayer) */ + async recv( + packet: Packet, + proof: Uint8Array, + proofHeight: Height + ): Promise { + return this.ibc.post('/packets/recv', { + packet: { + ...packet, + data: Buffer.from(packet.data).toString('base64'), + }, + proof: Buffer.from(proof).toString('base64'), + proofHeight, + }); + } + + /** Acknowledge a packet */ + async ack( + packet: Packet, + acknowledgement: Acknowledgement, + proof: Uint8Array, + proofHeight: Height + ): Promise { + return this.ibc.post('/packets/ack', { + packet: { + ...packet, + data: Buffer.from(packet.data).toString('base64'), + }, + acknowledgement, + proof: Buffer.from(proof).toString('base64'), + proofHeight, + }); + } + + /** Timeout a packet */ + async timeout( + packet: Packet, + proof: Uint8Array, + proofHeight: Height, + nextSequenceRecv: bigint + ): Promise { + return this.ibc.post('/packets/timeout', { + packet: { + ...packet, + data: Buffer.from(packet.data).toString('base64'), + }, + proof: Buffer.from(proof).toString('base64'), + proofHeight, + nextSequenceRecv: nextSequenceRecv.toString(), + }); + } + + /** Get packet commitment */ + async getCommitment( + portId: PortId, + channelId: ChannelId, + sequence: bigint + ): Promise { + const response = await this.ibc.get<{ commitment: string | null }>( + `/packets/${portId.id}/${channelId.id}/${sequence}/commitment` + ); + return response.commitment ? Buffer.from(response.commitment, 'base64') : null; + } + + /** Get packet acknowledgement */ + async getAcknowledgement( + portId: PortId, + channelId: ChannelId, + sequence: bigint + ): Promise { + return this.ibc.get(`/packets/${portId.id}/${channelId.id}/${sequence}/ack`); + } + + /** List unreceived packets */ + async listUnreceived( + portId: PortId, + channelId: ChannelId, + sequences: bigint[] + ): Promise { + return this.ibc.post(`/packets/${portId.id}/${channelId.id}/unreceived`, { + sequences: sequences.map(s => s.toString()), + }); + } + + /** List unacknowledged packets */ + async listUnacknowledged( + portId: PortId, + channelId: ChannelId, + sequences: bigint[] + ): Promise { + return this.ibc.post(`/packets/${portId.id}/${channelId.id}/unacked`, { + sequences: sequences.map(s => s.toString()), + }); + } +} + +/** + * IBC Transfer sub-client (ICS-20) + */ +export class TransferClient { + constructor(private ibc: SynorIbc) {} + + /** Transfer tokens to another chain */ + async transfer(params: TransferParams): Promise<{ sequence: bigint; txHash: string }> { + return this.ibc.post('/transfer', params); + } + + /** Get denom trace for an IBC token */ + async getDenomTrace(ibcDenom: string): Promise<{ path: string; baseDenom: string }> { + return this.ibc.get(`/transfer/denom_trace/${ibcDenom}`); + } + + /** Get all denom traces */ + async listDenomTraces(): Promise> { + return this.ibc.get('/transfer/denom_traces'); + } + + /** Get escrow address for a channel */ + async getEscrowAddress(portId: PortId, channelId: ChannelId): Promise { + const response = await this.ibc.get<{ address: string }>( + `/transfer/escrow/${portId.id}/${channelId.id}` + ); + return response.address; + } + + /** Get total escrow for a denom */ + async getTotalEscrow(denom: string): Promise { + const response = await this.ibc.get<{ amount: string }>( + `/transfer/escrow/total/${encodeURIComponent(denom)}` + ); + return response.amount; + } +} + +/** + * IBC Atomic Swap sub-client (HTLC) + */ +export class SwapsClient { + constructor(private ibc: SynorIbc) {} + + /** Initiate an atomic swap */ + async initiate(params: InitiateSwapParams): Promise<{ swapId: SwapId; hashlock: Hashlock }> { + return this.ibc.post('/swaps/initiate', params); + } + + /** Lock initiator's tokens */ + async lock(swapId: SwapId): Promise { + return this.ibc.post(`/swaps/${swapId.id}/lock`, {}); + } + + /** Respond to a swap (lock responder's tokens) */ + async respond(params: RespondSwapParams): Promise { + return this.ibc.post(`/swaps/${params.swapId.id}/respond`, { asset: params.asset }); + } + + /** Claim tokens with secret */ + async claim(params: ClaimSwapParams): Promise<{ txHash: string; revealedSecret: Uint8Array }> { + return this.ibc.post(`/swaps/${params.swapId.id}/claim`, { + secret: Buffer.from(params.secret).toString('base64'), + }); + } + + /** Refund expired swap */ + async refund(swapId: SwapId): Promise<{ txHash: string }> { + return this.ibc.post(`/swaps/${swapId.id}/refund`, {}); + } + + /** Cancel pending swap */ + async cancel(swapId: SwapId): Promise { + return this.ibc.post(`/swaps/${swapId.id}/cancel`, {}); + } + + /** Get swap by ID */ + async get(swapId: SwapId): Promise { + return this.ibc.get(`/swaps/${swapId.id}`); + } + + /** Get HTLC by ID */ + async getHtlc(swapId: SwapId): Promise { + return this.ibc.get(`/swaps/${swapId.id}/htlc`); + } + + /** List active swaps */ + async listActive(): Promise { + return this.ibc.get('/swaps/active'); + } + + /** List swaps by participant */ + async listByParticipant(address: string): Promise { + return this.ibc.get(`/swaps/participant/${address}`); + } + + /** Get swap status */ + async getStatus(swapId: SwapId): Promise<{ + state: SwapState; + remainingSeconds: number; + initiatorLocked: boolean; + responderLocked: boolean; + }> { + return this.ibc.get(`/swaps/${swapId.id}/status`); + } + + /** Verify hashlock with secret */ + verifySecret(hashlock: Hashlock, secret: Uint8Array): boolean { + // SHA256 hash verification + const crypto = require('crypto'); + const hash = crypto.createHash('sha256').update(secret).digest(); + return Buffer.from(hashlock.hash).equals(hash); + } +} + +/** + * Synor IBC SDK Client + * + * Main client for Inter-Blockchain Communication operations. + */ +export class SynorIbc { + private readonly config: Required; + private closed = false; + private ws?: WebSocket; + + /** Light client operations */ + readonly clients: LightClientClient; + /** Connection operations */ + readonly connections: ConnectionsClient; + /** Channel operations */ + readonly channels: ChannelsClient; + /** Packet operations */ + readonly packets: PacketsClient; + /** Token transfer operations (ICS-20) */ + readonly transfer: TransferClient; + /** Atomic swap operations (HTLC) */ + readonly swaps: SwapsClient; + + constructor(config: IbcConfig) { + this.config = { + apiKey: config.apiKey, + endpoint: config.endpoint || DEFAULT_ENDPOINT, + wsEndpoint: config.wsEndpoint || DEFAULT_WS_ENDPOINT, + timeout: config.timeout || DEFAULT_TIMEOUT, + retries: config.retries || DEFAULT_RETRIES, + chainId: config.chainId || DEFAULT_CHAIN_ID, + debug: config.debug || false, + }; + + this.clients = new LightClientClient(this); + this.connections = new ConnectionsClient(this); + this.channels = new ChannelsClient(this); + this.packets = new PacketsClient(this); + this.transfer = new TransferClient(this); + this.swaps = new SwapsClient(this); + } + + // ========================================================================= + // Chain Info + // ========================================================================= + + /** Get chain ID */ + get chainId(): string { + return this.config.chainId; + } + + /** Get chain info */ + async getChainInfo(): Promise<{ + chainId: string; + height: Height; + timestamp: bigint; + ibcVersion: string; + }> { + return this.get('/chain'); + } + + /** Get current height */ + async getHeight(): Promise { + return this.get('/chain/height'); + } + + // ========================================================================= + // Proofs + // ========================================================================= + + /** Get client state proof */ + async getClientStateProof(clientId: ClientId): Promise { + return this.get(`/proofs/client_state/${clientId.id}`); + } + + /** Get consensus state proof */ + async getConsensusStateProof(clientId: ClientId, height: Height): Promise { + return this.get( + `/proofs/consensus_state/${clientId.id}/${height.revisionNumber}-${height.revisionHeight}` + ); + } + + /** Get connection proof */ + async getConnectionProof(connectionId: ConnectionId): Promise { + return this.get(`/proofs/connection/${connectionId.id}`); + } + + /** Get channel proof */ + async getChannelProof(portId: PortId, channelId: ChannelId): Promise { + return this.get(`/proofs/channel/${portId.id}/${channelId.id}`); + } + + /** Get packet commitment proof */ + async getPacketCommitmentProof( + portId: PortId, + channelId: ChannelId, + sequence: bigint + ): Promise { + return this.get(`/proofs/packet_commitment/${portId.id}/${channelId.id}/${sequence}`); + } + + /** Get packet acknowledgement proof */ + async getPacketAckProof( + portId: PortId, + channelId: ChannelId, + sequence: bigint + ): Promise { + return this.get(`/proofs/packet_ack/${portId.id}/${channelId.id}/${sequence}`); + } + + // ========================================================================= + // WebSocket Subscriptions + // ========================================================================= + + /** Subscribe to IBC events */ + subscribeEvents(callback: (event: IbcEvent) => void): Subscription { + return this.subscribe('events', callback); + } + + /** Subscribe to specific client updates */ + subscribeClient(clientId: ClientId, callback: (event: IbcEvent) => void): Subscription { + return this.subscribe(`client/${clientId.id}`, callback); + } + + /** Subscribe to channel events */ + subscribeChannel( + portId: PortId, + channelId: ChannelId, + callback: (event: IbcEvent) => void + ): Subscription { + return this.subscribe(`channel/${portId.id}/${channelId.id}`, callback); + } + + /** Subscribe to swap updates */ + subscribeSwap(swapId: SwapId, callback: (event: IbcEvent) => void): Subscription { + return this.subscribe(`swap/${swapId.id}`, callback); + } + + private subscribe(topic: string, callback: (event: IbcEvent) => void): Subscription { + this.ensureWebSocket(); + + const message = JSON.stringify({ type: 'subscribe', topic }); + this.ws!.send(message); + + const handler = (event: MessageEvent) => { + try { + const data = JSON.parse(event.data); + if (data.topic === topic) { + callback(data.event); + } + } catch (error) { + if (this.config.debug) { + console.error('IBC WebSocket message parse error:', error); + } + } + }; + + this.ws!.addEventListener('message', handler); + + return { + unsubscribe: () => { + this.ws?.removeEventListener('message', handler); + this.ws?.send(JSON.stringify({ type: 'unsubscribe', topic })); + }, + }; + } + + private ensureWebSocket(): void { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + return; + } + + this.ws = new WebSocket(this.config.wsEndpoint); + this.ws.onopen = () => { + this.ws!.send( + JSON.stringify({ + type: 'auth', + apiKey: this.config.apiKey, + }) + ); + }; + this.ws.onerror = (error) => { + if (this.config.debug) { + console.error('IBC WebSocket error:', error); + } + }; + } + + // ========================================================================= + // Lifecycle + // ========================================================================= + + /** Health check */ + async healthCheck(): Promise { + try { + const response = await this.get<{ status: string }>('/health'); + return response.status === 'healthy'; + } catch { + return false; + } + } + + /** Close the client */ + close(): void { + this.closed = true; + if (this.ws) { + this.ws.close(); + this.ws = undefined; + } + } + + /** Check if client is closed */ + get isClosed(): boolean { + return this.closed; + } + + // ========================================================================= + // HTTP Methods (internal) + // ========================================================================= + + async get(path: string): Promise { + return this.request('GET', path); + } + + async post(path: string, body: unknown): Promise { + return this.request('POST', path, body); + } + + async delete(path: string): Promise { + return this.request('DELETE', path); + } + + private async request(method: string, path: string, body?: unknown): Promise { + if (this.closed) { + throw new IbcException('Client has been closed', 'CLIENT_CLOSED'); + } + + const url = `${this.config.endpoint}${path}`; + let lastError: Error | undefined; + + for (let attempt = 0; attempt <= this.config.retries; attempt++) { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.config.timeout); + + const response = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.config.apiKey}`, + 'X-SDK-Version': 'js/0.1.0', + 'X-Chain-Id': this.config.chainId, + }, + body: body ? JSON.stringify(body) : undefined, + signal: controller.signal, + }); + + clearTimeout(timeout); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new IbcException( + error.message || `HTTP ${response.status}`, + error.code, + response.status + ); + } + + return response.json(); + } catch (error) { + lastError = error as Error; + if (error instanceof IbcException) { + throw error; + } + if (attempt < this.config.retries) { + await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 100)); + } + } + } + + throw lastError || new IbcException('Request failed', 'NETWORK_ERROR'); + } +} + +export default SynorIbc; diff --git a/sdk/js/src/ibc/index.ts b/sdk/js/src/ibc/index.ts new file mode 100644 index 0000000..b9ce5be --- /dev/null +++ b/sdk/js/src/ibc/index.ts @@ -0,0 +1,39 @@ +/** + * Synor IBC SDK + * + * Inter-Blockchain Communication (IBC) protocol SDK for cross-chain interoperability. + * + * @example + * ```typescript + * import { SynorIbc } from '@synor/ibc'; + * + * const ibc = new SynorIbc({ apiKey: 'your-api-key' }); + * + * // Create a light client + * const clientId = await ibc.clients.create({ + * clientType: 'tendermint', + * clientState: { ... }, + * consensusState: { ... }, + * }); + * + * // Transfer tokens to another chain + * const { sequence, txHash } = await ibc.transfer.transfer({ + * sourcePort: { id: 'transfer' }, + * sourceChannel: { id: 'channel-0' }, + * token: { denom: 'usynor', amount: '1000000' }, + * sender: 'synor1...', + * receiver: 'cosmos1...', + * }); + * + * // Initiate atomic swap + * const { swapId, hashlock } = await ibc.swaps.initiate({ + * responder: 'cosmos1...', + * initiatorAsset: { native: { amount: 1000000n } }, + * responderAsset: { ics20: { denom: 'uatom', amount: 500000n } }, + * }); + * ``` + */ + +export * from './types'; +export * from './client'; +export { SynorIbc as default } from './client'; diff --git a/sdk/js/src/ibc/types.ts b/sdk/js/src/ibc/types.ts new file mode 100644 index 0000000..23b02c4 --- /dev/null +++ b/sdk/js/src/ibc/types.ts @@ -0,0 +1,559 @@ +/** + * Synor IBC SDK Types + * + * Inter-Blockchain Communication (IBC) protocol types for cross-chain interoperability. + */ + +// ============================================================================ +// Core Types +// ============================================================================ + +/** IBC Height representation */ +export interface Height { + /** Revision number (for hard forks) */ + revisionNumber: number; + /** Block height */ + revisionHeight: number; +} + +/** Timestamp in nanoseconds since Unix epoch */ +export type Timestamp = bigint; + +/** Chain identifier */ +export interface ChainId { + id: string; +} + +/** Signer address */ +export interface Signer { + address: string; +} + +/** IBC version with features */ +export interface Version { + identifier: string; + features: string[]; +} + +/** Commitment prefix for Merkle paths */ +export interface CommitmentPrefix { + keyPrefix: Uint8Array; +} + +// ============================================================================ +// Client Types +// ============================================================================ + +/** Light client identifier */ +export interface ClientId { + id: string; +} + +/** Light client types */ +export type ClientType = 'tendermint' | 'solo-machine' | 'localhost' | 'wasm'; + +/** Light client state */ +export interface ClientState { + chainId: string; + trustLevel: TrustLevel; + trustingPeriod: bigint; + unbondingPeriod: bigint; + maxClockDrift: bigint; + latestHeight: Height; + frozenHeight?: Height; + proofSpecs: ProofSpec[]; +} + +/** Trust level configuration */ +export interface TrustLevel { + numerator: number; + denominator: number; +} + +/** Proof specification */ +export interface ProofSpec { + leafSpec: LeafSpec; + innerSpec: InnerSpec; + maxDepth: number; + minDepth: number; +} + +export interface LeafSpec { + hash: string; + prehashKey: string; + prehashValue: string; + length: string; + prefix: Uint8Array; +} + +export interface InnerSpec { + childOrder: number[]; + childSize: number; + minPrefixLength: number; + maxPrefixLength: number; + emptyChild: Uint8Array; + hash: string; +} + +/** Consensus state at a specific height */ +export interface ConsensusState { + timestamp: Timestamp; + root: Uint8Array; + nextValidatorsHash: Uint8Array; +} + +/** Block header */ +export interface Header { + signedHeader: SignedHeader; + validatorSet: ValidatorSet; + trustedHeight: Height; + trustedValidators: ValidatorSet; +} + +export interface SignedHeader { + header: BlockHeader; + commit: Commit; +} + +export interface BlockHeader { + version: { block: bigint; app: bigint }; + chainId: string; + height: bigint; + time: Timestamp; + lastBlockId: BlockId; + lastCommitHash: Uint8Array; + dataHash: Uint8Array; + validatorsHash: Uint8Array; + nextValidatorsHash: Uint8Array; + consensusHash: Uint8Array; + appHash: Uint8Array; + lastResultsHash: Uint8Array; + evidenceHash: Uint8Array; + proposerAddress: Uint8Array; +} + +export interface BlockId { + hash: Uint8Array; + partSetHeader: { total: number; hash: Uint8Array }; +} + +export interface Commit { + height: bigint; + round: number; + blockId: BlockId; + signatures: CommitSig[]; +} + +export interface CommitSig { + blockIdFlag: number; + validatorAddress: Uint8Array; + timestamp: Timestamp; + signature: Uint8Array; +} + +export interface ValidatorSet { + validators: Validator[]; + proposer?: Validator; + totalVotingPower: bigint; +} + +export interface Validator { + address: Uint8Array; + pubKey: { type: string; value: Uint8Array }; + votingPower: bigint; + proposerPriority: bigint; +} + +// ============================================================================ +// Connection Types +// ============================================================================ + +/** Connection identifier */ +export interface ConnectionId { + id: string; +} + +/** Connection state */ +export type ConnectionState = 'uninitialized' | 'init' | 'tryopen' | 'open'; + +/** Connection counterparty */ +export interface ConnectionCounterparty { + clientId: ClientId; + connectionId?: ConnectionId; + prefix: CommitmentPrefix; +} + +/** Connection end */ +export interface ConnectionEnd { + clientId: ClientId; + versions: Version[]; + state: ConnectionState; + counterparty: ConnectionCounterparty; + delayPeriod: bigint; +} + +// ============================================================================ +// Channel Types +// ============================================================================ + +/** Port identifier */ +export interface PortId { + id: string; +} + +/** Channel identifier */ +export interface ChannelId { + id: string; +} + +/** Channel ordering */ +export type ChannelOrder = 'unordered' | 'ordered'; + +/** Channel state */ +export type ChannelState = 'uninitialized' | 'init' | 'tryopen' | 'open' | 'closed'; + +/** Channel counterparty */ +export interface ChannelCounterparty { + portId: PortId; + channelId?: ChannelId; +} + +/** Channel end */ +export interface Channel { + state: ChannelState; + ordering: ChannelOrder; + counterparty: ChannelCounterparty; + connectionHops: ConnectionId[]; + version: string; +} + +/** Channel key (port, channel) */ +export interface ChannelKey { + portId: PortId; + channelId: ChannelId; +} + +// ============================================================================ +// Packet Types +// ============================================================================ + +/** IBC Packet */ +export interface Packet { + /** Packet sequence number */ + sequence: bigint; + /** Source port */ + sourcePort: PortId; + /** Source channel */ + sourceChannel: ChannelId; + /** Destination port */ + destPort: PortId; + /** Destination channel */ + destChannel: ChannelId; + /** Packet data (app-specific) */ + data: Uint8Array; + /** Timeout height */ + timeoutHeight: Height; + /** Timeout timestamp in nanoseconds */ + timeoutTimestamp: Timestamp; +} + +/** Packet commitment hash */ +export interface PacketCommitment { + hash: Uint8Array; +} + +/** Packet acknowledgement */ +export type Acknowledgement = + | { success: Uint8Array } + | { error: string }; + +/** Timeout information */ +export interface Timeout { + height: Height; + timestamp: Timestamp; +} + +/** Packet receipt */ +export interface PacketReceipt { + receivedAt: Height; + acknowledgement?: Acknowledgement; +} + +/** Transfer packet data (ICS-20) */ +export interface FungibleTokenPacketData { + /** Token denomination */ + denom: string; + /** Amount to transfer */ + amount: string; + /** Sender address */ + sender: string; + /** Receiver address */ + receiver: string; + /** Optional memo */ + memo?: string; +} + +// ============================================================================ +// Swap Types (HTLC) +// ============================================================================ + +/** Swap identifier */ +export interface SwapId { + id: string; +} + +/** Swap state */ +export type SwapState = 'pending' | 'locked' | 'completed' | 'refunded' | 'expired' | 'cancelled'; + +/** Swap asset type */ +export type SwapAsset = + | { native: { amount: bigint } } + | { ics20: { denom: string; amount: bigint } } + | { ics721: { classId: string; tokenIds: string[] } }; + +/** Hashlock - hash of the secret */ +export interface Hashlock { + hash: Uint8Array; +} + +/** Timelock - expiration time */ +export interface Timelock { + expiry: Timestamp; +} + +/** Hashed Time-Locked Contract */ +export interface Htlc { + swapId: SwapId; + state: SwapState; + initiator: string; + responder: string; + asset: SwapAsset; + hashlock: Hashlock; + timelock: Timelock; + secret?: Uint8Array; + channelId?: ChannelId; + portId?: PortId; + createdAt: Timestamp; + completedAt?: Timestamp; +} + +/** Atomic swap between two chains */ +export interface AtomicSwap { + swapId: SwapId; + initiatorHtlc: Htlc; + responderHtlc?: Htlc; + state: SwapState; +} + +/** Swap packet actions */ +export type SwapAction = 'initiate' | 'respond' | 'claim' | 'refund'; + +/** Swap packet data */ +export interface SwapPacketData { + swapId: SwapId; + action: SwapAction; + initiator: string; + responder: string; + asset: SwapAsset; + hashlock: Hashlock; + timelockExpiry: bigint; + secret?: Uint8Array; +} + +// ============================================================================ +// Proof Types +// ============================================================================ + +/** Merkle proof */ +export interface MerkleProof { + proofs: ProofOp[]; +} + +export interface ProofOp { + type: 'iavl' | 'simple' | 'ics23'; + key: Uint8Array; + data: Uint8Array; +} + +/** Commitment proof */ +export interface CommitmentProof { + proof: MerkleProof; + height: Height; +} + +// ============================================================================ +// Event Types +// ============================================================================ + +/** IBC events */ +export type IbcEvent = + | { type: 'createClient'; clientId: ClientId; clientType: ClientType; consensusHeight: Height } + | { type: 'updateClient'; clientId: ClientId; consensusHeight: Height } + | { type: 'openInitConnection'; connectionId: ConnectionId; clientId: ClientId; counterpartyClientId: ClientId } + | { type: 'openTryConnection'; connectionId: ConnectionId; clientId: ClientId; counterpartyConnectionId: ConnectionId } + | { type: 'openAckConnection'; connectionId: ConnectionId } + | { type: 'openConfirmConnection'; connectionId: ConnectionId } + | { type: 'openInitChannel'; portId: PortId; channelId: ChannelId; connectionId: ConnectionId } + | { type: 'openTryChannel'; portId: PortId; channelId: ChannelId; counterpartyChannelId: ChannelId } + | { type: 'openAckChannel'; portId: PortId; channelId: ChannelId } + | { type: 'openConfirmChannel'; portId: PortId; channelId: ChannelId } + | { type: 'closeChannel'; portId: PortId; channelId: ChannelId } + | { type: 'sendPacket'; packet: Packet } + | { type: 'receivePacket'; packet: Packet } + | { type: 'acknowledgePacket'; packet: Packet; acknowledgement: Acknowledgement } + | { type: 'timeoutPacket'; packet: Packet } + | { type: 'swapInitiated'; swapId: SwapId; initiator: string; responder: string } + | { type: 'swapCompleted'; swapId: SwapId } + | { type: 'swapRefunded'; swapId: SwapId }; + +// ============================================================================ +// Request/Response Types +// ============================================================================ + +/** Create client parameters */ +export interface CreateClientParams { + clientType: ClientType; + clientState: ClientState; + consensusState: ConsensusState; +} + +/** Update client parameters */ +export interface UpdateClientParams { + clientId: ClientId; + header: Header; +} + +/** Connection open init parameters */ +export interface ConnOpenInitParams { + clientId: ClientId; + counterpartyClientId: ClientId; + counterpartyPrefix?: CommitmentPrefix; + version?: Version; + delayPeriod?: bigint; +} + +/** Connection open try parameters */ +export interface ConnOpenTryParams { + clientId: ClientId; + counterpartyClientId: ClientId; + counterpartyConnectionId: ConnectionId; + counterpartyPrefix: CommitmentPrefix; + counterpartyVersions: Version[]; + proofInit: Uint8Array; + proofHeight: Height; + proofConsensus: Uint8Array; + consensusHeight: Height; +} + +/** Channel open init parameters */ +export interface ChanOpenInitParams { + portId: PortId; + ordering: ChannelOrder; + connectionId: ConnectionId; + counterpartyPort: PortId; + version: string; +} + +/** Channel open try parameters */ +export interface ChanOpenTryParams { + portId: PortId; + ordering: ChannelOrder; + connectionId: ConnectionId; + counterpartyPort: PortId; + counterpartyChannel: ChannelId; + version: string; + counterpartyVersion: string; + proofInit: Uint8Array; + proofHeight: Height; +} + +/** Send packet parameters */ +export interface SendPacketParams { + sourcePort: PortId; + sourceChannel: ChannelId; + data: Uint8Array; + timeoutHeight?: Height; + timeoutTimestamp?: Timestamp; +} + +/** Transfer parameters (ICS-20) */ +export interface TransferParams { + sourcePort: PortId; + sourceChannel: ChannelId; + token: { denom: string; amount: string }; + sender: string; + receiver: string; + timeoutHeight?: Height; + timeoutTimestamp?: Timestamp; + memo?: string; +} + +/** Initiate swap parameters */ +export interface InitiateSwapParams { + responder: string; + initiatorAsset: SwapAsset; + responderAsset: SwapAsset; + channelId?: ChannelId; + portId?: PortId; +} + +/** Respond to swap parameters */ +export interface RespondSwapParams { + swapId: SwapId; + asset: SwapAsset; +} + +/** Claim swap parameters */ +export interface ClaimSwapParams { + swapId: SwapId; + secret: Uint8Array; +} + +// ============================================================================ +// Configuration +// ============================================================================ + +/** IBC SDK configuration */ +export interface IbcConfig { + /** API key for authentication */ + apiKey: string; + /** API endpoint URL */ + endpoint?: string; + /** WebSocket endpoint URL */ + wsEndpoint?: string; + /** Request timeout in milliseconds */ + timeout?: number; + /** Number of retries */ + retries?: number; + /** Chain ID */ + chainId?: string; + /** Enable debug logging */ + debug?: boolean; +} + +/** IBC error codes */ +export type IbcErrorCode = + | 'CLIENT_CLOSED' + | 'CLIENT_NOT_FOUND' + | 'CONNECTION_NOT_FOUND' + | 'CHANNEL_NOT_FOUND' + | 'PORT_NOT_BOUND' + | 'PACKET_NOT_FOUND' + | 'SWAP_NOT_FOUND' + | 'INVALID_PROOF' + | 'TIMEOUT_EXPIRED' + | 'INVALID_SECRET' + | 'NETWORK_ERROR' + | 'HTTP_ERROR'; + +/** IBC exception */ +export class IbcException extends Error { + constructor( + message: string, + public code?: IbcErrorCode, + public status?: number + ) { + super(message); + this.name = 'IbcException'; + } +} diff --git a/sdk/kotlin/src/main/kotlin/io/synor/ibc/SynorIbc.kt b/sdk/kotlin/src/main/kotlin/io/synor/ibc/SynorIbc.kt new file mode 100644 index 0000000..273646b --- /dev/null +++ b/sdk/kotlin/src/main/kotlin/io/synor/ibc/SynorIbc.kt @@ -0,0 +1,310 @@ +package io.synor.ibc + +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.json.Json +import java.io.Closeable +import java.util.Base64 + +/** + * Synor IBC SDK for Kotlin + * + * Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability. + */ +class SynorIbc(private val config: IbcConfig) : Closeable { + private val httpClient = HttpClient(CIO) { + install(ContentNegotiation) { + json(Json { ignoreUnknownKeys = true }) + } + } + private var closed = false + + val clients = LightClientClient(this) + val connections = ConnectionsClient(this) + val channels = ChannelsClient(this) + val transfer = TransferClient(this) + val swaps = SwapsClient(this) + + val chainId: String get() = config.chainId + + suspend fun getChainInfo(): Map = get("/chain") + + @Suppress("UNCHECKED_CAST") + suspend fun getHeight(): Height { + val result = get("/chain/height") + return Height( + revisionNumber = (result["revision_number"] as? Number)?.toLong() ?: 0, + revisionHeight = (result["revision_height"] as? Number)?.toLong() ?: 1 + ) + } + + suspend fun healthCheck(): Boolean = try { + val result = get("/health") + result["status"] == "healthy" + } catch (_: Exception) { + false + } + + override fun close() { + closed = true + httpClient.close() + } + + val isClosed: Boolean get() = closed + + // Internal HTTP methods + @Suppress("UNCHECKED_CAST") + internal suspend fun get(path: String): Map { + checkClosed() + val response = httpClient.get("${config.endpoint}$path") { + headers { + append(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + append(HttpHeaders.Authorization, "Bearer ${config.apiKey}") + append("X-SDK-Version", "kotlin/0.1.0") + append("X-Chain-Id", config.chainId) + } + } + if (response.status.value >= 400) { + val error = try { response.body>() } catch (_: Exception) { null } + throw IbcException( + error?.get("message") as? String ?: "HTTP ${response.status.value}", + error?.get("code") as? String, + response.status.value + ) + } + return response.body() + } + + @Suppress("UNCHECKED_CAST") + internal suspend fun post(path: String, body: Map): Map { + checkClosed() + val response = httpClient.post("${config.endpoint}$path") { + headers { + append(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + append(HttpHeaders.Authorization, "Bearer ${config.apiKey}") + append("X-SDK-Version", "kotlin/0.1.0") + append("X-Chain-Id", config.chainId) + } + setBody(body) + } + if (response.status.value >= 400) { + val error = try { response.body>() } catch (_: Exception) { null } + throw IbcException( + error?.get("message") as? String ?: "HTTP ${response.status.value}", + error?.get("code") as? String, + response.status.value + ) + } + return response.body() + } + + private fun checkClosed() { + if (closed) throw IbcException("Client has been closed", "CLIENT_CLOSED") + } + + /** + * Light client sub-client + */ + class LightClientClient(private val ibc: SynorIbc) { + suspend fun create( + clientType: ClientType, + clientState: ClientState, + consensusState: Map + ): ClientId { + val result = ibc.post("/clients", mapOf( + "client_type" to clientType.value, + "client_state" to mapOf( + "chain_id" to clientState.chainId, + "trust_level" to mapOf( + "numerator" to clientState.trustLevel.numerator, + "denominator" to clientState.trustLevel.denominator + ), + "trusting_period" to clientState.trustingPeriod, + "unbonding_period" to clientState.unbondingPeriod, + "max_clock_drift" to clientState.maxClockDrift, + "latest_height" to mapOf( + "revision_number" to clientState.latestHeight.revisionNumber, + "revision_height" to clientState.latestHeight.revisionHeight + ) + ), + "consensus_state" to consensusState + )) + return ClientId(result["client_id"] as String) + } + + @Suppress("UNCHECKED_CAST") + suspend fun getState(clientId: ClientId): ClientState { + val result = ibc.get("/clients/${clientId.id}/state") + val trustLevel = result["trust_level"] as Map + val latestHeight = result["latest_height"] as Map + val frozenHeight = result["frozen_height"] as? Map + return ClientState( + chainId = result["chain_id"] as String, + trustLevel = TrustLevel( + numerator = (trustLevel["numerator"] as Number).toInt(), + denominator = (trustLevel["denominator"] as Number).toInt() + ), + trustingPeriod = (result["trusting_period"] as Number).toLong(), + unbondingPeriod = (result["unbonding_period"] as Number).toLong(), + maxClockDrift = (result["max_clock_drift"] as Number).toLong(), + latestHeight = Height( + revisionNumber = (latestHeight["revision_number"] as Number).toLong(), + revisionHeight = (latestHeight["revision_height"] as Number).toLong() + ), + frozenHeight = frozenHeight?.let { + Height( + revisionNumber = (it["revision_number"] as Number).toLong(), + revisionHeight = (it["revision_height"] as Number).toLong() + ) + } + ) + } + + @Suppress("UNCHECKED_CAST") + suspend fun list(): List> { + val result = ibc.get("/clients") + return result["clients"] as? List> ?: emptyList() + } + } + + /** + * Connections sub-client + */ + class ConnectionsClient(private val ibc: SynorIbc) { + suspend fun openInit( + clientId: ClientId, + counterpartyClientId: ClientId + ): ConnectionId { + val result = ibc.post("/connections/init", mapOf( + "client_id" to clientId.id, + "counterparty_client_id" to counterpartyClientId.id + )) + return ConnectionId(result["connection_id"] as String) + } + + suspend fun get(connectionId: ConnectionId): Map = + ibc.get("/connections/${connectionId.id}") + + @Suppress("UNCHECKED_CAST") + suspend fun list(): List> { + val result = ibc.get("/connections") + return result["connections"] as? List> ?: emptyList() + } + } + + /** + * Channels sub-client + */ + class ChannelsClient(private val ibc: SynorIbc) { + suspend fun bindPort(portId: PortId, module: String) { + ibc.post("/ports/bind", mapOf("port_id" to portId.id, "module" to module)) + } + + suspend fun openInit( + portId: PortId, + ordering: ChannelOrder, + connectionId: ConnectionId, + counterpartyPort: PortId, + version: String + ): ChannelId { + val result = ibc.post("/channels/init", mapOf( + "port_id" to portId.id, + "ordering" to ordering.name.lowercase(), + "connection_id" to connectionId.id, + "counterparty_port" to counterpartyPort.id, + "version" to version + )) + return ChannelId(result["channel_id"] as String) + } + + suspend fun get(portId: PortId, channelId: ChannelId): Map = + ibc.get("/channels/${portId.id}/${channelId.id}") + + @Suppress("UNCHECKED_CAST") + suspend fun list(): List> { + val result = ibc.get("/channels") + return result["channels"] as? List> ?: emptyList() + } + } + + /** + * Transfer sub-client (ICS-20) + */ + class TransferClient(private val ibc: SynorIbc) { + suspend fun transfer( + sourcePort: String, + sourceChannel: String, + denom: String, + amount: String, + sender: String, + receiver: String, + timeout: Timeout? = null, + memo: String? = null + ): Map { + val body = mutableMapOf( + "source_port" to sourcePort, + "source_channel" to sourceChannel, + "token" to mapOf("denom" to denom, "amount" to amount), + "sender" to sender, + "receiver" to receiver + ) + timeout?.let { + body["timeout_height"] = mapOf( + "revision_number" to it.height.revisionNumber, + "revision_height" to it.height.revisionHeight + ) + body["timeout_timestamp"] = it.timestamp.toString() + } + memo?.let { body["memo"] = it } + return ibc.post("/transfer", body) + } + + suspend fun getDenomTrace(ibcDenom: String): Map = + ibc.get("/transfer/denom_trace/$ibcDenom") + } + + /** + * Swaps sub-client (HTLC) + */ + class SwapsClient(private val ibc: SynorIbc) { + suspend fun initiate( + responder: String, + initiatorAsset: Map, + responderAsset: Map + ): Map = ibc.post("/swaps/initiate", mapOf( + "responder" to responder, + "initiator_asset" to initiatorAsset, + "responder_asset" to responderAsset + )) + + suspend fun lock(swapId: SwapId) { + ibc.post("/swaps/${swapId.id}/lock", emptyMap()) + } + + suspend fun respond(swapId: SwapId, asset: Map): Map = + ibc.post("/swaps/${swapId.id}/respond", mapOf("asset" to asset)) + + suspend fun claim(swapId: SwapId, secret: ByteArray): Map = + ibc.post("/swaps/${swapId.id}/claim", mapOf( + "secret" to Base64.getEncoder().encodeToString(secret) + )) + + suspend fun refund(swapId: SwapId): Map = + ibc.post("/swaps/${swapId.id}/refund", emptyMap()) + + suspend fun get(swapId: SwapId): AtomicSwap = + AtomicSwap.fromJson(ibc.get("/swaps/${swapId.id}")) + + @Suppress("UNCHECKED_CAST") + suspend fun listActive(): List { + val result = ibc.get("/swaps/active") + val swaps = result["swaps"] as? List> ?: emptyList() + return swaps.map { AtomicSwap.fromJson(it) } + } + } +} diff --git a/sdk/kotlin/src/main/kotlin/io/synor/ibc/Types.kt b/sdk/kotlin/src/main/kotlin/io/synor/ibc/Types.kt new file mode 100644 index 0000000..384f5ad --- /dev/null +++ b/sdk/kotlin/src/main/kotlin/io/synor/ibc/Types.kt @@ -0,0 +1,265 @@ +package io.synor.ibc + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.math.BigInteger + +/** + * Synor IBC SDK Types for Kotlin + * + * Inter-Blockchain Communication (IBC) protocol types. + */ + +/** + * IBC Height representation + */ +@Serializable +data class Height( + @SerialName("revision_number") val revisionNumber: Long = 0, + @SerialName("revision_height") val revisionHeight: Long = 1 +) { + val isZero: Boolean get() = revisionNumber == 0L && revisionHeight == 0L + + fun increment(): Height = copy(revisionHeight = revisionHeight + 1) +} + +/** + * Chain identifier + */ +@JvmInline +value class ChainId(val id: String) + +/** + * IBC Version with features + */ +@Serializable +data class Version( + val identifier: String, + val features: List +) { + companion object { + fun defaultConnection() = Version("1", listOf("ORDER_ORDERED", "ORDER_UNORDERED")) + } +} + +/** + * Light client types + */ +enum class ClientType(val value: String) { + TENDERMINT("tendermint"), + SOLO_MACHINE("solo_machine"), + LOCALHOST("localhost"), + WASM("wasm") +} + +/** + * Light client identifier + */ +@JvmInline +value class ClientId(val id: String) + +/** + * Trust level configuration + */ +@Serializable +data class TrustLevel( + val numerator: Int = 1, + val denominator: Int = 3 +) + +/** + * Light client state + */ +@Serializable +data class ClientState( + @SerialName("chain_id") val chainId: String, + @SerialName("trust_level") val trustLevel: TrustLevel, + @SerialName("trusting_period") val trustingPeriod: Long, + @SerialName("unbonding_period") val unbondingPeriod: Long, + @SerialName("max_clock_drift") val maxClockDrift: Long, + @SerialName("latest_height") val latestHeight: Height, + @SerialName("frozen_height") val frozenHeight: Height? = null +) + +/** + * Connection state + */ +enum class ConnectionState { + UNINITIALIZED, INIT, TRYOPEN, OPEN +} + +/** + * Connection identifier + */ +@JvmInline +value class ConnectionId(val id: String) { + companion object { + fun newId(sequence: Int) = ConnectionId("connection-$sequence") + } +} + +/** + * Port identifier + */ +@JvmInline +value class PortId(val id: String) { + companion object { + fun transfer() = PortId("transfer") + } +} + +/** + * Channel identifier + */ +@JvmInline +value class ChannelId(val id: String) { + companion object { + fun newId(sequence: Int) = ChannelId("channel-$sequence") + } +} + +/** + * Channel ordering + */ +enum class ChannelOrder { + UNORDERED, ORDERED +} + +/** + * Channel state + */ +enum class ChannelState { + UNINITIALIZED, INIT, TRYOPEN, OPEN, CLOSED +} + +/** + * Timeout information + */ +data class Timeout( + val height: Height, + val timestamp: BigInteger = BigInteger.ZERO +) { + companion object { + fun fromHeight(height: Long) = Timeout(Height(revisionHeight = height)) + fun fromTimestamp(timestamp: BigInteger) = Timeout(Height(), timestamp) + } + + fun toJson(): Map = mapOf( + "height" to mapOf( + "revision_number" to height.revisionNumber, + "revision_height" to height.revisionHeight + ), + "timestamp" to timestamp.toString() + ) +} + +/** + * Transfer packet data (ICS-20) + */ +@Serializable +data class FungibleTokenPacketData( + val denom: String, + val amount: String, + val sender: String, + val receiver: String, + val memo: String = "" +) { + val isNative: Boolean get() = !denom.contains("/") +} + +/** + * Swap state + */ +enum class SwapState { + PENDING, LOCKED, COMPLETED, REFUNDED, EXPIRED, CANCELLED +} + +/** + * Swap identifier + */ +@JvmInline +value class SwapId(val id: String) + +/** + * Native asset for swaps + */ +data class NativeAsset(val amount: BigInteger) { + fun toJson(): Map = mapOf("native" to mapOf("amount" to amount.toString())) +} + +/** + * ICS-20 asset for swaps + */ +data class Ics20Asset(val denom: String, val amount: BigInteger) { + fun toJson(): Map = mapOf( + "ics20" to mapOf("denom" to denom, "amount" to amount.toString()) + ) +} + +/** + * Hashlock - hash of the secret + */ +data class Hashlock(val hash: ByteArray) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Hashlock) return false + return hash.contentEquals(other.hash) + } + + override fun hashCode(): Int = hash.contentHashCode() +} + +/** + * Timelock - expiration time + */ +data class Timelock(val expiry: BigInteger) { + fun isExpired(current: BigInteger): Boolean = current >= expiry +} + +/** + * Atomic swap + */ +data class AtomicSwap( + val swapId: SwapId, + val state: SwapState, + val initiatorHtlc: Map, + val responderHtlc: Map? +) { + companion object { + @Suppress("UNCHECKED_CAST") + fun fromJson(json: Map): AtomicSwap { + val swapIdMap = json["swap_id"] as Map + return AtomicSwap( + swapId = SwapId(swapIdMap["id"] as String), + state = SwapState.valueOf((json["state"] as String).uppercase()), + initiatorHtlc = json["initiator_htlc"] as Map, + responderHtlc = json["responder_htlc"] as? Map + ) + } + } +} + +/** + * IBC SDK configuration + */ +data class IbcConfig( + val apiKey: String, + val endpoint: String = "https://ibc.synor.io/v1", + val wsEndpoint: String = "wss://ibc.synor.io/v1/ws", + val timeout: Int = 30, + val retries: Int = 3, + val chainId: String = "synor-1", + val debug: Boolean = false +) + +/** + * IBC exception + */ +class IbcException( + message: String, + val code: String? = null, + val status: Int? = null +) : RuntimeException(message) { + override fun toString(): String = + "IbcException: $message${code?.let { " ($it)" } ?: ""}" +} diff --git a/sdk/python/src/synor_ibc/__init__.py b/sdk/python/src/synor_ibc/__init__.py new file mode 100644 index 0000000..831181c --- /dev/null +++ b/sdk/python/src/synor_ibc/__init__.py @@ -0,0 +1,164 @@ +""" +Synor IBC SDK for Python + +Inter-Blockchain Communication (IBC) protocol SDK for cross-chain interoperability. + +Features: +- Light client verification +- Connection & channel management +- Packet relay +- Token transfers (ICS-20) +- Atomic swaps (HTLC) + +Example: + >>> from synor_ibc import SynorIbc, IbcConfig + >>> + >>> ibc = SynorIbc(IbcConfig(api_key="your-api-key")) + >>> + >>> # Transfer tokens to another chain + >>> result = await ibc.transfer.transfer( + ... source_port="transfer", + ... source_channel="channel-0", + ... token={"denom": "usynor", "amount": "1000000"}, + ... sender="synor1...", + ... receiver="cosmos1...", + ... ) + >>> + >>> # Initiate atomic swap + >>> swap = await ibc.swaps.initiate( + ... responder="cosmos1...", + ... initiator_asset={"native": {"amount": 1000000}}, + ... responder_asset={"ics20": {"denom": "uatom", "amount": 500000}}, + ... ) +""" + +from .types import ( + # Core types + Height, + Timestamp, + ChainId, + Signer, + Version, + CommitmentPrefix, + # Client types + ClientId, + ClientType, + ClientState, + ConsensusState, + TrustLevel, + Header, + ValidatorSet, + Validator, + # Connection types + ConnectionId, + ConnectionState, + ConnectionEnd, + ConnectionCounterparty, + # Channel types + PortId, + ChannelId, + ChannelOrder, + ChannelState, + Channel, + ChannelCounterparty, + # Packet types + Packet, + PacketCommitment, + Acknowledgement, + Timeout, + FungibleTokenPacketData, + # Swap types + SwapId, + SwapState, + SwapAsset, + Hashlock, + Timelock, + Htlc, + AtomicSwap, + SwapAction, + SwapPacketData, + # Proof types + MerkleProof, + CommitmentProof, + # Events + IbcEvent, + # Config & errors + IbcConfig, + IbcException, +) + +from .client import ( + SynorIbc, + LightClientClient, + ConnectionsClient, + ChannelsClient, + PacketsClient, + TransferClient, + SwapsClient, +) + +__all__ = [ + # Main client + "SynorIbc", + # Sub-clients + "LightClientClient", + "ConnectionsClient", + "ChannelsClient", + "PacketsClient", + "TransferClient", + "SwapsClient", + # Core types + "Height", + "Timestamp", + "ChainId", + "Signer", + "Version", + "CommitmentPrefix", + # Client types + "ClientId", + "ClientType", + "ClientState", + "ConsensusState", + "TrustLevel", + "Header", + "ValidatorSet", + "Validator", + # Connection types + "ConnectionId", + "ConnectionState", + "ConnectionEnd", + "ConnectionCounterparty", + # Channel types + "PortId", + "ChannelId", + "ChannelOrder", + "ChannelState", + "Channel", + "ChannelCounterparty", + # Packet types + "Packet", + "PacketCommitment", + "Acknowledgement", + "Timeout", + "FungibleTokenPacketData", + # Swap types + "SwapId", + "SwapState", + "SwapAsset", + "Hashlock", + "Timelock", + "Htlc", + "AtomicSwap", + "SwapAction", + "SwapPacketData", + # Proof types + "MerkleProof", + "CommitmentProof", + # Events + "IbcEvent", + # Config & errors + "IbcConfig", + "IbcException", +] + +__version__ = "0.1.0" diff --git a/sdk/python/src/synor_ibc/client.py b/sdk/python/src/synor_ibc/client.py new file mode 100644 index 0000000..526351c --- /dev/null +++ b/sdk/python/src/synor_ibc/client.py @@ -0,0 +1,620 @@ +""" +Synor IBC SDK Client + +Inter-Blockchain Communication (IBC) client for cross-chain interoperability. +""" + +import asyncio +import base64 +import hashlib +from typing import Optional, List, Any, Dict, Callable +import httpx +import websockets + +from .types import ( + IbcConfig, + IbcException, + Height, + ClientId, + ClientState, + ConsensusState, + Header, + ConnectionId, + ConnectionEnd, + PortId, + ChannelId, + Channel, + ChannelOrder, + Packet, + Acknowledgement, + SwapId, + SwapState, + SwapAsset, + AtomicSwap, + Htlc, + Hashlock, + CommitmentProof, + IbcEvent, + Version, +) + + +class LightClientClient: + """Light client operations sub-client""" + + def __init__(self, ibc: "SynorIbc"): + self._ibc = ibc + + async def create( + self, + client_type: str, + client_state: ClientState, + consensus_state: ConsensusState, + ) -> ClientId: + """Create a new light client""" + result = await self._ibc._post("/clients", { + "client_type": client_type, + "client_state": self._serialize_client_state(client_state), + "consensus_state": self._serialize_consensus_state(consensus_state), + }) + return ClientId(id=result["client_id"]) + + async def update(self, client_id: ClientId, header: Header) -> Height: + """Update a light client with new header""" + result = await self._ibc._post(f"/clients/{client_id.id}/update", { + "header": header.__dict__, + }) + return Height(**result) + + async def get_state(self, client_id: ClientId) -> ClientState: + """Get client state""" + return await self._ibc._get(f"/clients/{client_id.id}/state") + + async def get_consensus_state(self, client_id: ClientId, height: Height) -> ConsensusState: + """Get consensus state at height""" + return await self._ibc._get( + f"/clients/{client_id.id}/consensus/{height.revision_number}-{height.revision_height}" + ) + + async def list(self) -> List[Dict[str, Any]]: + """List all clients""" + return await self._ibc._get("/clients") + + async def is_active(self, client_id: ClientId) -> bool: + """Check if client is active""" + result = await self._ibc._get(f"/clients/{client_id.id}/status") + return result.get("active", False) + + def _serialize_client_state(self, state: ClientState) -> Dict[str, Any]: + return { + "chain_id": state.chain_id, + "trust_level": {"numerator": state.trust_level.numerator, "denominator": state.trust_level.denominator}, + "trusting_period": state.trusting_period, + "unbonding_period": state.unbonding_period, + "max_clock_drift": state.max_clock_drift, + "latest_height": {"revision_number": state.latest_height.revision_number, "revision_height": state.latest_height.revision_height}, + } + + def _serialize_consensus_state(self, state: ConsensusState) -> Dict[str, Any]: + return { + "timestamp": state.timestamp, + "root": base64.b64encode(state.root).decode(), + "next_validators_hash": base64.b64encode(state.next_validators_hash).decode(), + } + + +class ConnectionsClient: + """Connection operations sub-client""" + + def __init__(self, ibc: "SynorIbc"): + self._ibc = ibc + + async def open_init( + self, + client_id: ClientId, + counterparty_client_id: ClientId, + version: Optional[Version] = None, + delay_period: int = 0, + ) -> ConnectionId: + """Initialize connection handshake""" + result = await self._ibc._post("/connections/init", { + "client_id": client_id.id, + "counterparty_client_id": counterparty_client_id.id, + "version": version.__dict__ if version else None, + "delay_period": delay_period, + }) + return ConnectionId(id=result["connection_id"]) + + async def open_try( + self, + client_id: ClientId, + counterparty_client_id: ClientId, + counterparty_connection_id: ConnectionId, + counterparty_versions: List[Version], + proof_init: bytes, + proof_height: Height, + ) -> ConnectionId: + """Try connection handshake (counterparty)""" + result = await self._ibc._post("/connections/try", { + "client_id": client_id.id, + "counterparty_client_id": counterparty_client_id.id, + "counterparty_connection_id": counterparty_connection_id.id, + "counterparty_versions": [v.__dict__ for v in counterparty_versions], + "proof_init": base64.b64encode(proof_init).decode(), + "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height}, + }) + return ConnectionId(id=result["connection_id"]) + + async def open_ack( + self, + connection_id: ConnectionId, + counterparty_connection_id: ConnectionId, + version: Version, + proof_try: bytes, + proof_height: Height, + ) -> None: + """Acknowledge connection handshake""" + await self._ibc._post(f"/connections/{connection_id.id}/ack", { + "counterparty_connection_id": counterparty_connection_id.id, + "version": version.__dict__, + "proof_try": base64.b64encode(proof_try).decode(), + "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height}, + }) + + async def open_confirm( + self, + connection_id: ConnectionId, + proof_ack: bytes, + proof_height: Height, + ) -> None: + """Confirm connection handshake""" + await self._ibc._post(f"/connections/{connection_id.id}/confirm", { + "proof_ack": base64.b64encode(proof_ack).decode(), + "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height}, + }) + + async def get(self, connection_id: ConnectionId) -> ConnectionEnd: + """Get connection by ID""" + return await self._ibc._get(f"/connections/{connection_id.id}") + + async def list(self) -> List[Dict[str, Any]]: + """List all connections""" + return await self._ibc._get("/connections") + + +class ChannelsClient: + """Channel operations sub-client""" + + def __init__(self, ibc: "SynorIbc"): + self._ibc = ibc + + async def bind_port(self, port_id: PortId, module: str) -> None: + """Bind a port""" + await self._ibc._post("/ports/bind", {"port_id": port_id.id, "module": module}) + + async def release_port(self, port_id: PortId) -> None: + """Release port binding""" + await self._ibc._post(f"/ports/{port_id.id}/release", {}) + + async def open_init( + self, + port_id: PortId, + ordering: ChannelOrder, + connection_id: ConnectionId, + counterparty_port: PortId, + version: str, + ) -> ChannelId: + """Initialize channel handshake""" + result = await self._ibc._post("/channels/init", { + "port_id": port_id.id, + "ordering": ordering.value, + "connection_id": connection_id.id, + "counterparty_port": counterparty_port.id, + "version": version, + }) + return ChannelId(id=result["channel_id"]) + + async def open_try( + self, + port_id: PortId, + ordering: ChannelOrder, + connection_id: ConnectionId, + counterparty_port: PortId, + counterparty_channel: ChannelId, + version: str, + counterparty_version: str, + proof_init: bytes, + proof_height: Height, + ) -> ChannelId: + """Try channel handshake (counterparty)""" + result = await self._ibc._post("/channels/try", { + "port_id": port_id.id, + "ordering": ordering.value, + "connection_id": connection_id.id, + "counterparty_port": counterparty_port.id, + "counterparty_channel": counterparty_channel.id, + "version": version, + "counterparty_version": counterparty_version, + "proof_init": base64.b64encode(proof_init).decode(), + "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height}, + }) + return ChannelId(id=result["channel_id"]) + + async def open_ack( + self, + port_id: PortId, + channel_id: ChannelId, + counterparty_channel_id: ChannelId, + counterparty_version: str, + proof_try: bytes, + proof_height: Height, + ) -> None: + """Acknowledge channel handshake""" + await self._ibc._post(f"/channels/{port_id.id}/{channel_id.id}/ack", { + "counterparty_channel_id": counterparty_channel_id.id, + "counterparty_version": counterparty_version, + "proof_try": base64.b64encode(proof_try).decode(), + "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height}, + }) + + async def open_confirm( + self, + port_id: PortId, + channel_id: ChannelId, + proof_ack: bytes, + proof_height: Height, + ) -> None: + """Confirm channel handshake""" + await self._ibc._post(f"/channels/{port_id.id}/{channel_id.id}/confirm", { + "proof_ack": base64.b64encode(proof_ack).decode(), + "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height}, + }) + + async def close_init(self, port_id: PortId, channel_id: ChannelId) -> None: + """Close channel init""" + await self._ibc._post(f"/channels/{port_id.id}/{channel_id.id}/close", {}) + + async def get(self, port_id: PortId, channel_id: ChannelId) -> Channel: + """Get channel""" + return await self._ibc._get(f"/channels/{port_id.id}/{channel_id.id}") + + async def list(self) -> List[Dict[str, Any]]: + """List all channels""" + return await self._ibc._get("/channels") + + +class PacketsClient: + """Packet operations sub-client""" + + def __init__(self, ibc: "SynorIbc"): + self._ibc = ibc + + async def send( + self, + source_port: PortId, + source_channel: ChannelId, + data: bytes, + timeout_height: Optional[Height] = None, + timeout_timestamp: Optional[int] = None, + ) -> Dict[str, Any]: + """Send a packet""" + return await self._ibc._post("/packets/send", { + "source_port": source_port.id, + "source_channel": source_channel.id, + "data": base64.b64encode(data).decode(), + "timeout_height": {"revision_number": timeout_height.revision_number, "revision_height": timeout_height.revision_height} if timeout_height else None, + "timeout_timestamp": timeout_timestamp, + }) + + async def recv( + self, + packet: Packet, + proof: bytes, + proof_height: Height, + ) -> Acknowledgement: + """Receive a packet (called by relayer)""" + return await self._ibc._post("/packets/recv", { + "packet": self._serialize_packet(packet), + "proof": base64.b64encode(proof).decode(), + "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height}, + }) + + async def ack( + self, + packet: Packet, + acknowledgement: Acknowledgement, + proof: bytes, + proof_height: Height, + ) -> None: + """Acknowledge a packet""" + await self._ibc._post("/packets/ack", { + "packet": self._serialize_packet(packet), + "acknowledgement": acknowledgement.__dict__, + "proof": base64.b64encode(proof).decode(), + "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height}, + }) + + async def timeout( + self, + packet: Packet, + proof: bytes, + proof_height: Height, + next_sequence_recv: int, + ) -> None: + """Timeout a packet""" + await self._ibc._post("/packets/timeout", { + "packet": self._serialize_packet(packet), + "proof": base64.b64encode(proof).decode(), + "proof_height": {"revision_number": proof_height.revision_number, "revision_height": proof_height.revision_height}, + "next_sequence_recv": str(next_sequence_recv), + }) + + def _serialize_packet(self, packet: Packet) -> Dict[str, Any]: + return { + "sequence": str(packet.sequence), + "source_port": packet.source_port.id, + "source_channel": packet.source_channel.id, + "dest_port": packet.dest_port.id, + "dest_channel": packet.dest_channel.id, + "data": base64.b64encode(packet.data).decode(), + "timeout_height": {"revision_number": packet.timeout_height.revision_number, "revision_height": packet.timeout_height.revision_height}, + "timeout_timestamp": packet.timeout_timestamp, + } + + +class TransferClient: + """Token transfer operations sub-client (ICS-20)""" + + def __init__(self, ibc: "SynorIbc"): + self._ibc = ibc + + async def transfer( + self, + source_port: str, + source_channel: str, + token: Dict[str, str], + sender: str, + receiver: str, + timeout_height: Optional[Height] = None, + timeout_timestamp: Optional[int] = None, + memo: str = "", + ) -> Dict[str, Any]: + """Transfer tokens to another chain""" + return await self._ibc._post("/transfer", { + "source_port": source_port, + "source_channel": source_channel, + "token": token, + "sender": sender, + "receiver": receiver, + "timeout_height": {"revision_number": timeout_height.revision_number, "revision_height": timeout_height.revision_height} if timeout_height else None, + "timeout_timestamp": timeout_timestamp, + "memo": memo, + }) + + async def get_denom_trace(self, ibc_denom: str) -> Dict[str, str]: + """Get denom trace for an IBC token""" + return await self._ibc._get(f"/transfer/denom_trace/{ibc_denom}") + + async def list_denom_traces(self) -> List[Dict[str, str]]: + """Get all denom traces""" + return await self._ibc._get("/transfer/denom_traces") + + +class SwapsClient: + """Atomic swap operations sub-client (HTLC)""" + + def __init__(self, ibc: "SynorIbc"): + self._ibc = ibc + + async def initiate( + self, + responder: str, + initiator_asset: Dict[str, Any], + responder_asset: Dict[str, Any], + channel_id: Optional[str] = None, + port_id: Optional[str] = None, + ) -> Dict[str, Any]: + """Initiate an atomic swap""" + return await self._ibc._post("/swaps/initiate", { + "responder": responder, + "initiator_asset": initiator_asset, + "responder_asset": responder_asset, + "channel_id": channel_id, + "port_id": port_id, + }) + + async def lock(self, swap_id: str) -> None: + """Lock initiator's tokens""" + await self._ibc._post(f"/swaps/{swap_id}/lock", {}) + + async def respond(self, swap_id: str, asset: Dict[str, Any]) -> Dict[str, Any]: + """Respond to a swap (lock responder's tokens)""" + return await self._ibc._post(f"/swaps/{swap_id}/respond", {"asset": asset}) + + async def claim(self, swap_id: str, secret: bytes) -> Dict[str, Any]: + """Claim tokens with secret""" + return await self._ibc._post(f"/swaps/{swap_id}/claim", { + "secret": base64.b64encode(secret).decode(), + }) + + async def refund(self, swap_id: str) -> Dict[str, Any]: + """Refund expired swap""" + return await self._ibc._post(f"/swaps/{swap_id}/refund", {}) + + async def cancel(self, swap_id: str) -> None: + """Cancel pending swap""" + await self._ibc._post(f"/swaps/{swap_id}/cancel", {}) + + async def get(self, swap_id: str) -> Dict[str, Any]: + """Get swap by ID""" + return await self._ibc._get(f"/swaps/{swap_id}") + + async def list_active(self) -> List[Dict[str, Any]]: + """List active swaps""" + return await self._ibc._get("/swaps/active") + + async def list_by_participant(self, address: str) -> List[Dict[str, Any]]: + """List swaps by participant""" + return await self._ibc._get(f"/swaps/participant/{address}") + + def verify_secret(self, hashlock: bytes, secret: bytes) -> bool: + """Verify hashlock with secret""" + computed = hashlib.sha256(secret).digest() + return hashlock == computed + + +class SynorIbc: + """ + Synor IBC SDK Client + + Main client for Inter-Blockchain Communication operations. + """ + + def __init__(self, config: IbcConfig): + self._config = config + self._closed = False + self._http = httpx.AsyncClient( + base_url=config.endpoint, + timeout=config.timeout, + headers={ + "Authorization": f"Bearer {config.api_key}", + "Content-Type": "application/json", + "X-SDK-Version": "python/0.1.0", + "X-Chain-Id": config.chain_id, + }, + ) + self._ws = None + + # Sub-clients + self.clients = LightClientClient(self) + self.connections = ConnectionsClient(self) + self.channels = ChannelsClient(self) + self.packets = PacketsClient(self) + self.transfer = TransferClient(self) + self.swaps = SwapsClient(self) + + @property + def chain_id(self) -> str: + """Get chain ID""" + return self._config.chain_id + + async def get_chain_info(self) -> Dict[str, Any]: + """Get chain info""" + return await self._get("/chain") + + async def get_height(self) -> Height: + """Get current height""" + result = await self._get("/chain/height") + return Height(**result) + + async def get_client_state_proof(self, client_id: str) -> CommitmentProof: + """Get client state proof""" + return await self._get(f"/proofs/client_state/{client_id}") + + async def get_connection_proof(self, connection_id: str) -> CommitmentProof: + """Get connection proof""" + return await self._get(f"/proofs/connection/{connection_id}") + + async def get_channel_proof(self, port_id: str, channel_id: str) -> CommitmentProof: + """Get channel proof""" + return await self._get(f"/proofs/channel/{port_id}/{channel_id}") + + async def health_check(self) -> bool: + """Health check""" + try: + result = await self._get("/health") + return result.get("status") == "healthy" + except Exception: + return False + + async def close(self) -> None: + """Close the client""" + self._closed = True + await self._http.aclose() + if self._ws: + await self._ws.close() + + @property + def is_closed(self) -> bool: + """Check if client is closed""" + return self._closed + + async def _get(self, path: str) -> Any: + """HTTP GET request""" + return await self._request("GET", path) + + async def _post(self, path: str, body: Dict[str, Any]) -> Any: + """HTTP POST request""" + return await self._request("POST", path, body) + + async def _delete(self, path: str) -> Any: + """HTTP DELETE request""" + return await self._request("DELETE", path) + + async def _request(self, method: str, path: str, body: Optional[Dict[str, Any]] = None) -> Any: + """Make HTTP request with retries""" + if self._closed: + raise IbcException("Client has been closed", code="CLIENT_CLOSED") + + last_error = None + for attempt in range(self._config.retries + 1): + try: + if method == "GET": + response = await self._http.get(path) + elif method == "POST": + response = await self._http.post(path, json=body) + elif method == "DELETE": + response = await self._http.delete(path) + else: + raise ValueError(f"Unknown method: {method}") + + if response.status_code >= 400: + try: + error = response.json() + except Exception: + error = {} + raise IbcException( + error.get("message", f"HTTP {response.status_code}"), + code=error.get("code"), + status=response.status_code, + ) + + return response.json() + + except IbcException: + raise + except Exception as e: + last_error = e + if attempt < self._config.retries: + await asyncio.sleep(2 ** attempt * 0.1) + + raise IbcException(str(last_error), code="NETWORK_ERROR") + + async def subscribe_events(self, callback: Callable[[IbcEvent], None]) -> Callable[[], None]: + """Subscribe to IBC events""" + return await self._subscribe("events", callback) + + async def _subscribe(self, topic: str, callback: Callable[[Any], None]) -> Callable[[], None]: + """Subscribe to WebSocket topic""" + if not self._ws: + self._ws = await websockets.connect( + self._config.ws_endpoint, + extra_headers={"Authorization": f"Bearer {self._config.api_key}"}, + ) + + await self._ws.send(f'{{"type":"subscribe","topic":"{topic}"}}') + + async def listener(): + async for message in self._ws: + import json + data = json.loads(message) + if data.get("topic") == topic: + callback(data.get("event")) + + task = asyncio.create_task(listener()) + + def unsubscribe(): + task.cancel() + asyncio.create_task(self._ws.send(f'{{"type":"unsubscribe","topic":"{topic}"}}')) + + return unsubscribe diff --git a/sdk/python/src/synor_ibc/types.py b/sdk/python/src/synor_ibc/types.py new file mode 100644 index 0000000..35411c6 --- /dev/null +++ b/sdk/python/src/synor_ibc/types.py @@ -0,0 +1,532 @@ +""" +Synor IBC SDK Types + +Type definitions for Inter-Blockchain Communication (IBC) protocol. +""" + +from dataclasses import dataclass, field +from enum import Enum +from typing import Optional, List, Dict, Any, Union + + +# ============================================================================ +# Core Types +# ============================================================================ + +@dataclass +class Height: + """IBC Height representation""" + revision_number: int = 0 + revision_height: int = 1 + + def is_zero(self) -> bool: + return self.revision_number == 0 and self.revision_height == 0 + + def increment(self) -> "Height": + return Height(self.revision_number, self.revision_height + 1) + + +Timestamp = int # Nanoseconds since Unix epoch + + +@dataclass +class ChainId: + """Chain identifier""" + id: str + + +@dataclass +class Signer: + """Signer address""" + address: str + + +@dataclass +class Version: + """IBC version with features""" + identifier: str + features: List[str] + + @classmethod + def default_connection(cls) -> "Version": + return cls(identifier="1", features=["ORDER_ORDERED", "ORDER_UNORDERED"]) + + +@dataclass +class CommitmentPrefix: + """Commitment prefix for Merkle paths""" + key_prefix: bytes = field(default_factory=lambda: b"ibc") + + +# ============================================================================ +# Client Types +# ============================================================================ + +@dataclass +class ClientId: + """Light client identifier""" + id: str + + +class ClientType(str, Enum): + """Light client types""" + TENDERMINT = "tendermint" + SOLO_MACHINE = "solo-machine" + LOCALHOST = "localhost" + WASM = "wasm" + + +@dataclass +class TrustLevel: + """Trust level configuration""" + numerator: int = 1 + denominator: int = 3 + + +@dataclass +class ClientState: + """Light client state""" + chain_id: str + trust_level: TrustLevel + trusting_period: int # nanoseconds + unbonding_period: int # nanoseconds + max_clock_drift: int # nanoseconds + latest_height: Height + frozen_height: Optional[Height] = None + + +@dataclass +class ConsensusState: + """Consensus state at a specific height""" + timestamp: Timestamp + root: bytes + next_validators_hash: bytes + + +@dataclass +class Validator: + """Validator info""" + address: bytes + pub_key: Dict[str, Any] + voting_power: int + proposer_priority: int = 0 + + +@dataclass +class ValidatorSet: + """Validator set""" + validators: List[Validator] + proposer: Optional[Validator] = None + total_voting_power: int = 0 + + +@dataclass +class Header: + """Block header for light client updates""" + signed_header: Dict[str, Any] + validator_set: ValidatorSet + trusted_height: Height + trusted_validators: ValidatorSet + + +# ============================================================================ +# Connection Types +# ============================================================================ + +@dataclass +class ConnectionId: + """Connection identifier""" + id: str + + +class ConnectionState(str, Enum): + """Connection state""" + UNINITIALIZED = "uninitialized" + INIT = "init" + TRYOPEN = "tryopen" + OPEN = "open" + + +@dataclass +class ConnectionCounterparty: + """Connection counterparty""" + client_id: ClientId + connection_id: Optional[ConnectionId] = None + prefix: CommitmentPrefix = field(default_factory=CommitmentPrefix) + + +@dataclass +class ConnectionEnd: + """Connection end""" + client_id: ClientId + versions: List[Version] + state: ConnectionState + counterparty: ConnectionCounterparty + delay_period: int = 0 + + +# ============================================================================ +# Channel Types +# ============================================================================ + +@dataclass +class PortId: + """Port identifier""" + id: str + + @classmethod + def transfer(cls) -> "PortId": + return cls(id="transfer") + + @classmethod + def ica_host(cls) -> "PortId": + return cls(id="icahost") + + +@dataclass +class ChannelId: + """Channel identifier""" + id: str + + @classmethod + def new(cls, sequence: int) -> "ChannelId": + return cls(id=f"channel-{sequence}") + + +class ChannelOrder(str, Enum): + """Channel ordering""" + UNORDERED = "unordered" + ORDERED = "ordered" + + +class ChannelState(str, Enum): + """Channel state""" + UNINITIALIZED = "uninitialized" + INIT = "init" + TRYOPEN = "tryopen" + OPEN = "open" + CLOSED = "closed" + + +@dataclass +class ChannelCounterparty: + """Channel counterparty""" + port_id: PortId + channel_id: Optional[ChannelId] = None + + +@dataclass +class Channel: + """Channel end""" + state: ChannelState + ordering: ChannelOrder + counterparty: ChannelCounterparty + connection_hops: List[ConnectionId] + version: str + + +# ============================================================================ +# Packet Types +# ============================================================================ + +@dataclass +class Packet: + """IBC Packet""" + sequence: int + source_port: PortId + source_channel: ChannelId + dest_port: PortId + dest_channel: ChannelId + data: bytes + timeout_height: Height + timeout_timestamp: Timestamp = 0 + + +@dataclass +class PacketCommitment: + """Packet commitment hash""" + hash: bytes + + +@dataclass +class AckSuccess: + """Success acknowledgement""" + data: bytes + + +@dataclass +class AckError: + """Error acknowledgement""" + message: str + + +Acknowledgement = Union[AckSuccess, AckError] + + +@dataclass +class Timeout: + """Timeout information""" + height: Height = field(default_factory=Height) + timestamp: Timestamp = 0 + + @classmethod + def from_height(cls, height: int) -> "Timeout": + return cls(height=Height(0, height)) + + @classmethod + def from_timestamp(cls, timestamp: int) -> "Timeout": + return cls(timestamp=timestamp) + + +@dataclass +class PacketReceipt: + """Packet receipt""" + received_at: Height + acknowledgement: Optional[Acknowledgement] = None + + +@dataclass +class FungibleTokenPacketData: + """Transfer packet data (ICS-20)""" + denom: str + amount: str + sender: str + receiver: str + memo: str = "" + + def is_native(self) -> bool: + return "/" not in self.denom + + def get_denom_trace(self) -> List[str]: + return self.denom.split("/") + + +# ============================================================================ +# Swap Types (HTLC) +# ============================================================================ + +@dataclass +class SwapId: + """Swap identifier""" + id: str + + +class SwapState(str, Enum): + """Swap state""" + PENDING = "pending" + LOCKED = "locked" + COMPLETED = "completed" + REFUNDED = "refunded" + EXPIRED = "expired" + CANCELLED = "cancelled" + + +@dataclass +class NativeAsset: + """Native blockchain token""" + amount: int + + +@dataclass +class Ics20Asset: + """ICS-20 fungible token""" + denom: str + amount: int + + +@dataclass +class Ics721Asset: + """ICS-721 NFT""" + class_id: str + token_ids: List[str] + + +SwapAsset = Union[NativeAsset, Ics20Asset, Ics721Asset] + + +@dataclass +class Hashlock: + """Hashlock - hash of the secret""" + hash: bytes + + @classmethod + def from_secret(cls, secret: bytes) -> "Hashlock": + import hashlib + return cls(hash=hashlib.sha256(secret).digest()) + + def verify(self, preimage: bytes) -> bool: + import hashlib + computed = hashlib.sha256(preimage).digest() + return self.hash == computed + + +@dataclass +class Timelock: + """Timelock - expiration time""" + expiry: Timestamp + + @classmethod + def from_duration(cls, current: Timestamp, duration_secs: int) -> "Timelock": + return cls(expiry=current + duration_secs * 1_000_000_000) + + def is_expired(self, current: Timestamp) -> bool: + return current >= self.expiry + + def remaining_secs(self, current: Timestamp) -> int: + if current >= self.expiry: + return 0 + return (self.expiry - current) // 1_000_000_000 + + +@dataclass +class Htlc: + """Hashed Time-Locked Contract""" + swap_id: SwapId + state: SwapState + initiator: str + responder: str + asset: SwapAsset + hashlock: Hashlock + timelock: Timelock + secret: Optional[bytes] = None + channel_id: Optional[ChannelId] = None + port_id: Optional[PortId] = None + created_at: Timestamp = 0 + completed_at: Optional[Timestamp] = None + + +@dataclass +class AtomicSwap: + """Atomic swap between two chains""" + swap_id: SwapId + initiator_htlc: Htlc + responder_htlc: Optional[Htlc] = None + state: SwapState = SwapState.PENDING + + +class SwapAction(str, Enum): + """Swap packet actions""" + INITIATE = "initiate" + RESPOND = "respond" + CLAIM = "claim" + REFUND = "refund" + + +@dataclass +class SwapPacketData: + """Swap packet data""" + swap_id: SwapId + action: SwapAction + initiator: str + responder: str + asset: SwapAsset + hashlock: Hashlock + timelock_expiry: int + secret: Optional[bytes] = None + + +# ============================================================================ +# Proof Types +# ============================================================================ + +@dataclass +class ProofOp: + """Proof operation""" + type: str + key: bytes + data: bytes + + +@dataclass +class MerkleProof: + """Merkle proof""" + proofs: List[ProofOp] + + +@dataclass +class CommitmentProof: + """Commitment proof""" + proof: MerkleProof + height: Height + + +# ============================================================================ +# Event Types +# ============================================================================ + +@dataclass +class CreateClientEvent: + type: str = "createClient" + client_id: Optional[ClientId] = None + client_type: Optional[ClientType] = None + consensus_height: Optional[Height] = None + + +@dataclass +class UpdateClientEvent: + type: str = "updateClient" + client_id: Optional[ClientId] = None + consensus_height: Optional[Height] = None + + +@dataclass +class OpenInitConnectionEvent: + type: str = "openInitConnection" + connection_id: Optional[ConnectionId] = None + client_id: Optional[ClientId] = None + counterparty_client_id: Optional[ClientId] = None + + +@dataclass +class SendPacketEvent: + type: str = "sendPacket" + packet: Optional[Packet] = None + + +@dataclass +class SwapInitiatedEvent: + type: str = "swapInitiated" + swap_id: Optional[SwapId] = None + initiator: Optional[str] = None + responder: Optional[str] = None + + +@dataclass +class SwapCompletedEvent: + type: str = "swapCompleted" + swap_id: Optional[SwapId] = None + + +IbcEvent = Union[ + CreateClientEvent, + UpdateClientEvent, + OpenInitConnectionEvent, + SendPacketEvent, + SwapInitiatedEvent, + SwapCompletedEvent, + Dict[str, Any], # Fallback for unknown events +] + + +# ============================================================================ +# Configuration +# ============================================================================ + +@dataclass +class IbcConfig: + """IBC SDK configuration""" + api_key: str + endpoint: str = "https://ibc.synor.io/v1" + ws_endpoint: str = "wss://ibc.synor.io/v1/ws" + timeout: int = 30 + retries: int = 3 + chain_id: str = "synor-1" + debug: bool = False + + +class IbcException(Exception): + """IBC exception""" + def __init__(self, message: str, code: Optional[str] = None, status: Optional[int] = None): + super().__init__(message) + self.code = code + self.status = status diff --git a/sdk/ruby/lib/synor/ibc.rb b/sdk/ruby/lib/synor/ibc.rb new file mode 100644 index 0000000..7ae0e84 --- /dev/null +++ b/sdk/ruby/lib/synor/ibc.rb @@ -0,0 +1,470 @@ +# frozen_string_literal: true + +require 'net/http' +require 'uri' +require 'json' +require 'base64' + +module Synor + # IBC SDK for Ruby + # + # Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability. + module Ibc + VERSION = '0.1.0' + + # IBC Height representation + class Height + attr_reader :revision_number, :revision_height + + def initialize(revision_number: 0, revision_height: 1) + @revision_number = revision_number + @revision_height = revision_height + end + + def zero? + @revision_number.zero? && @revision_height.zero? + end + + def increment + Height.new(revision_number: @revision_number, revision_height: @revision_height + 1) + end + + def self.from_json(json) + Height.new( + revision_number: json['revision_number'] || 0, + revision_height: json['revision_height'] || 1 + ) + end + + def to_json(*_args) + { 'revision_number' => @revision_number, 'revision_height' => @revision_height } + end + end + + # Light client types + module ClientType + TENDERMINT = 'tendermint' + SOLO_MACHINE = 'solo_machine' + LOCALHOST = 'localhost' + WASM = 'wasm' + end + + # Trust level configuration + class TrustLevel + attr_reader :numerator, :denominator + + def initialize(numerator: 1, denominator: 3) + @numerator = numerator + @denominator = denominator + end + + def self.from_json(json) + TrustLevel.new( + numerator: json['numerator'] || 1, + denominator: json['denominator'] || 3 + ) + end + + def to_json(*_args) + { 'numerator' => @numerator, 'denominator' => @denominator } + end + end + + # Light client state + class ClientState + attr_reader :chain_id, :trust_level, :trusting_period, :unbonding_period, + :max_clock_drift, :latest_height, :frozen_height + + def initialize(chain_id:, trust_level:, trusting_period:, unbonding_period:, + max_clock_drift:, latest_height:, frozen_height: nil) + @chain_id = chain_id + @trust_level = trust_level + @trusting_period = trusting_period + @unbonding_period = unbonding_period + @max_clock_drift = max_clock_drift + @latest_height = latest_height + @frozen_height = frozen_height + end + + def self.from_json(json) + ClientState.new( + chain_id: json['chain_id'], + trust_level: TrustLevel.from_json(json['trust_level']), + trusting_period: json['trusting_period'], + unbonding_period: json['unbonding_period'], + max_clock_drift: json['max_clock_drift'], + latest_height: Height.from_json(json['latest_height']), + frozen_height: json['frozen_height'] ? Height.from_json(json['frozen_height']) : nil + ) + end + + def to_json(*_args) + result = { + 'chain_id' => @chain_id, + 'trust_level' => @trust_level.to_json, + 'trusting_period' => @trusting_period, + 'unbonding_period' => @unbonding_period, + 'max_clock_drift' => @max_clock_drift, + 'latest_height' => @latest_height.to_json + } + result['frozen_height'] = @frozen_height.to_json if @frozen_height + result + end + end + + # Connection state + module ConnectionState + UNINITIALIZED = 'uninitialized' + INIT = 'init' + TRYOPEN = 'tryopen' + OPEN = 'open' + end + + # Channel ordering + module ChannelOrder + UNORDERED = 'unordered' + ORDERED = 'ordered' + end + + # Channel state + module ChannelState + UNINITIALIZED = 'uninitialized' + INIT = 'init' + TRYOPEN = 'tryopen' + OPEN = 'open' + CLOSED = 'closed' + end + + # Timeout information + class Timeout + attr_reader :height, :timestamp + + def initialize(height:, timestamp: 0) + @height = height + @timestamp = timestamp + end + + def self.from_height(h) + Timeout.new(height: Height.new(revision_height: h)) + end + + def self.from_timestamp(ts) + Timeout.new(height: Height.new, timestamp: ts) + end + + def to_json(*_args) + { 'height' => @height.to_json, 'timestamp' => @timestamp.to_s } + end + end + + # Swap state + module SwapState + PENDING = 'pending' + LOCKED = 'locked' + COMPLETED = 'completed' + REFUNDED = 'refunded' + EXPIRED = 'expired' + CANCELLED = 'cancelled' + end + + # Atomic swap + class AtomicSwap + attr_reader :swap_id, :state, :initiator_htlc, :responder_htlc + + def initialize(swap_id:, state:, initiator_htlc:, responder_htlc: nil) + @swap_id = swap_id + @state = state + @initiator_htlc = initiator_htlc + @responder_htlc = responder_htlc + end + + def self.from_json(json) + AtomicSwap.new( + swap_id: json['swap_id']['id'], + state: json['state'], + initiator_htlc: json['initiator_htlc'], + responder_htlc: json['responder_htlc'] + ) + end + end + + # IBC SDK configuration + class Config + attr_accessor :api_key, :endpoint, :ws_endpoint, :timeout, :retries, :chain_id, :debug + + def initialize( + api_key:, + endpoint: 'https://ibc.synor.io/v1', + ws_endpoint: 'wss://ibc.synor.io/v1/ws', + timeout: 30, + retries: 3, + chain_id: 'synor-1', + debug: false + ) + @api_key = api_key + @endpoint = endpoint + @ws_endpoint = ws_endpoint + @timeout = timeout + @retries = retries + @chain_id = chain_id + @debug = debug + end + end + + # IBC exception + class IbcError < StandardError + attr_reader :code, :status + + def initialize(message, code: nil, status: nil) + super(message) + @code = code + @status = status + end + end + + # Main IBC client + class Client + attr_reader :clients, :connections, :channels, :transfer, :swaps + + def initialize(config) + @config = config + @closed = false + + @clients = LightClientClient.new(self) + @connections = ConnectionsClient.new(self) + @channels = ChannelsClient.new(self) + @transfer = TransferClient.new(self) + @swaps = SwapsClient.new(self) + end + + def chain_id + @config.chain_id + end + + def get_chain_info + get('/chain') + end + + def get_height + result = get('/chain/height') + Height.from_json(result) + end + + def health_check + result = get('/health') + result['status'] == 'healthy' + rescue StandardError + false + end + + def close + @closed = true + end + + def closed? + @closed + end + + # Internal HTTP methods + + def get(path) + request(:get, path) + end + + def post(path, body) + request(:post, path, body) + end + + private + + def request(method, path, body = nil) + raise IbcError.new('Client has been closed', code: 'CLIENT_CLOSED') if @closed + + uri = URI.parse("#{@config.endpoint}#{path}") + + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = uri.scheme == 'https' + http.read_timeout = @config.timeout + + request = case method + when :get + Net::HTTP::Get.new(uri.request_uri) + when :post + req = Net::HTTP::Post.new(uri.request_uri) + req.body = body.to_json if body + req + end + + request['Content-Type'] = 'application/json' + request['Authorization'] = "Bearer #{@config.api_key}" + request['X-SDK-Version'] = 'ruby/0.1.0' + request['X-Chain-Id'] = @config.chain_id + + response = http.request(request) + + unless response.is_a?(Net::HTTPSuccess) + error = JSON.parse(response.body) rescue {} + raise IbcError.new( + error['message'] || "HTTP #{response.code}", + code: error['code'], + status: response.code.to_i + ) + end + + JSON.parse(response.body) + end + end + + # Light client sub-client + class LightClientClient + def initialize(ibc) + @ibc = ibc + end + + def create(client_type:, client_state:, consensus_state:) + result = @ibc.post('/clients', { + 'client_type' => client_type, + 'client_state' => client_state.to_json, + 'consensus_state' => consensus_state + }) + result['client_id'] + end + + def get_state(client_id) + result = @ibc.get("/clients/#{client_id}/state") + ClientState.from_json(result) + end + + def list + result = @ibc.get('/clients') + result['clients'] || [] + end + end + + # Connections sub-client + class ConnectionsClient + def initialize(ibc) + @ibc = ibc + end + + def open_init(client_id:, counterparty_client_id:) + result = @ibc.post('/connections/init', { + 'client_id' => client_id, + 'counterparty_client_id' => counterparty_client_id + }) + result['connection_id'] + end + + def get(connection_id) + @ibc.get("/connections/#{connection_id}") + end + + def list + result = @ibc.get('/connections') + result['connections'] || [] + end + end + + # Channels sub-client + class ChannelsClient + def initialize(ibc) + @ibc = ibc + end + + def bind_port(port_id:, module_name:) + @ibc.post('/ports/bind', { 'port_id' => port_id, 'module' => module_name }) + end + + def open_init(port_id:, ordering:, connection_id:, counterparty_port:, version:) + result = @ibc.post('/channels/init', { + 'port_id' => port_id, + 'ordering' => ordering, + 'connection_id' => connection_id, + 'counterparty_port' => counterparty_port, + 'version' => version + }) + result['channel_id'] + end + + def get(port_id:, channel_id:) + @ibc.get("/channels/#{port_id}/#{channel_id}") + end + + def list + result = @ibc.get('/channels') + result['channels'] || [] + end + end + + # Transfer sub-client (ICS-20) + class TransferClient + def initialize(ibc) + @ibc = ibc + end + + def transfer(source_port:, source_channel:, denom:, amount:, sender:, receiver:, + timeout: nil, memo: nil) + body = { + 'source_port' => source_port, + 'source_channel' => source_channel, + 'token' => { 'denom' => denom, 'amount' => amount }, + 'sender' => sender, + 'receiver' => receiver + } + if timeout + body['timeout_height'] = timeout.height.to_json + body['timeout_timestamp'] = timeout.timestamp.to_s + end + body['memo'] = memo if memo + @ibc.post('/transfer', body) + end + + def get_denom_trace(ibc_denom) + @ibc.get("/transfer/denom_trace/#{ibc_denom}") + end + end + + # Swaps sub-client (HTLC) + class SwapsClient + def initialize(ibc) + @ibc = ibc + end + + def initiate(responder:, initiator_asset:, responder_asset:) + @ibc.post('/swaps/initiate', { + 'responder' => responder, + 'initiator_asset' => initiator_asset, + 'responder_asset' => responder_asset + }) + end + + def lock(swap_id) + @ibc.post("/swaps/#{swap_id}/lock", {}) + end + + def respond(swap_id, asset) + @ibc.post("/swaps/#{swap_id}/respond", { 'asset' => asset }) + end + + def claim(swap_id, secret) + @ibc.post("/swaps/#{swap_id}/claim", { + 'secret' => Base64.strict_encode64(secret.pack('C*')) + }) + end + + def refund(swap_id) + @ibc.post("/swaps/#{swap_id}/refund", {}) + end + + def get(swap_id) + result = @ibc.get("/swaps/#{swap_id}") + AtomicSwap.from_json(result) + end + + def list_active + result = @ibc.get('/swaps/active') + (result['swaps'] || []).map { |s| AtomicSwap.from_json(s) } + end + end + end +end diff --git a/sdk/rust/src/ibc/client.rs b/sdk/rust/src/ibc/client.rs new file mode 100644 index 0000000..2e4db2d --- /dev/null +++ b/sdk/rust/src/ibc/client.rs @@ -0,0 +1,363 @@ +//! IBC SDK Client + +use crate::ibc::types::*; +use reqwest::Client; +use serde::{de::DeserializeOwned, Serialize}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +/// Synor IBC SDK Client +pub struct SynorIbc { + config: IbcConfig, + client: Client, + closed: Arc, +} + +impl SynorIbc { + /// Create a new IBC client + pub fn new(config: IbcConfig) -> Self { + Self { + config, + client: Client::new(), + closed: Arc::new(AtomicBool::new(false)), + } + } + + /// Get chain ID + pub fn chain_id(&self) -> &str { + &self.config.chain_id + } + + /// Get chain info + pub async fn get_chain_info(&self) -> IbcResult { + self.get("/chain").await + } + + /// Get current height + pub async fn get_height(&self) -> IbcResult { + self.get("/chain/height").await + } + + /// Health check + pub async fn health_check(&self) -> bool { + match self.get::("/health").await { + Ok(v) => v.get("status").and_then(|s| s.as_str()) == Some("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) + } + + // ======================================================================== + // Light Client Operations + // ======================================================================== + + /// Create a new light client + pub async fn create_client( + &self, + client_type: ClientType, + client_state: ClientState, + consensus_state: ConsensusState, + ) -> IbcResult { + #[derive(Deserialize)] + struct Response { client_id: String } + let resp: Response = self.post("/clients", &serde_json::json!({ + "client_type": client_type, + "client_state": client_state, + "consensus_state": consensus_state, + })).await?; + Ok(ClientId(resp.client_id)) + } + + /// Get client state + pub async fn get_client_state(&self, client_id: &ClientId) -> IbcResult { + self.get(&format!("/clients/{}/state", client_id.0)).await + } + + /// List all clients + pub async fn list_clients(&self) -> IbcResult> { + self.get("/clients").await + } + + // ======================================================================== + // Connection Operations + // ======================================================================== + + /// Initialize connection handshake + pub async fn connection_open_init( + &self, + client_id: &ClientId, + counterparty_client_id: &ClientId, + ) -> IbcResult { + #[derive(Deserialize)] + struct Response { connection_id: String } + let resp: Response = self.post("/connections/init", &serde_json::json!({ + "client_id": client_id.0, + "counterparty_client_id": counterparty_client_id.0, + })).await?; + Ok(ConnectionId(resp.connection_id)) + } + + /// Get connection + pub async fn get_connection(&self, connection_id: &ConnectionId) -> IbcResult { + self.get(&format!("/connections/{}", connection_id.0)).await + } + + /// List all connections + pub async fn list_connections(&self) -> IbcResult> { + self.get("/connections").await + } + + // ======================================================================== + // Channel Operations + // ======================================================================== + + /// Bind a port + pub async fn bind_port(&self, port_id: &PortId, module: &str) -> IbcResult<()> { + self.post::("/ports/bind", &serde_json::json!({ + "port_id": port_id.0, + "module": module, + })).await?; + Ok(()) + } + + /// Initialize channel handshake + pub async fn channel_open_init( + &self, + port_id: &PortId, + ordering: ChannelOrder, + connection_id: &ConnectionId, + counterparty_port: &PortId, + version: &str, + ) -> IbcResult { + #[derive(Deserialize)] + struct Response { channel_id: String } + let resp: Response = self.post("/channels/init", &serde_json::json!({ + "port_id": port_id.0, + "ordering": ordering, + "connection_id": connection_id.0, + "counterparty_port": counterparty_port.0, + "version": version, + })).await?; + Ok(ChannelId(resp.channel_id)) + } + + /// Get channel + pub async fn get_channel(&self, port_id: &PortId, channel_id: &ChannelId) -> IbcResult { + self.get(&format!("/channels/{}/{}", port_id.0, channel_id.0)).await + } + + /// List all channels + pub async fn list_channels(&self) -> IbcResult> { + self.get("/channels").await + } + + // ======================================================================== + // Packet Operations + // ======================================================================== + + /// Send a packet + pub async fn send_packet( + &self, + source_port: &PortId, + source_channel: &ChannelId, + data: &[u8], + timeout: Timeout, + ) -> IbcResult { + use base64::{Engine as _, engine::general_purpose::STANDARD}; + self.post("/packets/send", &serde_json::json!({ + "source_port": source_port.0, + "source_channel": source_channel.0, + "data": STANDARD.encode(data), + "timeout_height": timeout.height, + "timeout_timestamp": timeout.timestamp, + })).await + } + + // ======================================================================== + // Transfer Operations (ICS-20) + // ======================================================================== + + /// Transfer tokens to another chain + pub async fn transfer( + &self, + source_port: &str, + source_channel: &str, + denom: &str, + amount: &str, + sender: &str, + receiver: &str, + timeout: Option, + memo: Option<&str>, + ) -> IbcResult { + let mut body = serde_json::json!({ + "source_port": source_port, + "source_channel": source_channel, + "token": { "denom": denom, "amount": amount }, + "sender": sender, + "receiver": receiver, + }); + if let Some(t) = timeout { + body["timeout_height"] = serde_json::to_value(&t.height).unwrap(); + body["timeout_timestamp"] = serde_json::json!(t.timestamp); + } + if let Some(m) = memo { + body["memo"] = serde_json::json!(m); + } + self.post("/transfer", &body).await + } + + /// Get denom trace + pub async fn get_denom_trace(&self, ibc_denom: &str) -> IbcResult { + self.get(&format!("/transfer/denom_trace/{}", ibc_denom)).await + } + + // ======================================================================== + // Swap Operations (HTLC) + // ======================================================================== + + /// Initiate an atomic swap + pub async fn initiate_swap( + &self, + responder: &str, + initiator_asset: SwapAsset, + responder_asset: SwapAsset, + ) -> IbcResult { + self.post("/swaps/initiate", &serde_json::json!({ + "responder": responder, + "initiator_asset": initiator_asset, + "responder_asset": responder_asset, + })).await + } + + /// Lock initiator's tokens + pub async fn lock_swap(&self, swap_id: &SwapId) -> IbcResult<()> { + self.post::(&format!("/swaps/{}/lock", swap_id.0), &()).await?; + Ok(()) + } + + /// Respond to a swap + pub async fn respond_to_swap(&self, swap_id: &SwapId, asset: SwapAsset) -> IbcResult { + self.post(&format!("/swaps/{}/respond", swap_id.0), &serde_json::json!({ + "asset": asset, + })).await + } + + /// Claim tokens with secret + pub async fn claim_swap(&self, swap_id: &SwapId, secret: &[u8]) -> IbcResult { + use base64::{Engine as _, engine::general_purpose::STANDARD}; + self.post(&format!("/swaps/{}/claim", swap_id.0), &serde_json::json!({ + "secret": STANDARD.encode(secret), + })).await + } + + /// Refund expired swap + pub async fn refund_swap(&self, swap_id: &SwapId) -> IbcResult { + self.post(&format!("/swaps/{}/refund", swap_id.0), &()).await + } + + /// Get swap by ID + pub async fn get_swap(&self, swap_id: &SwapId) -> IbcResult { + self.get(&format!("/swaps/{}", swap_id.0)).await + } + + /// List active swaps + pub async fn list_active_swaps(&self) -> IbcResult> { + self.get("/swaps/active").await + } + + /// Verify secret against hashlock + pub fn verify_secret(&self, hashlock: &Hashlock, secret: &[u8]) -> bool { + hashlock.verify(secret) + } + + // ======================================================================== + // HTTP Methods + // ======================================================================== + + async fn get(&self, path: &str) -> IbcResult { + self.request("GET", path, Option::<&()>::None).await + } + + async fn post(&self, path: &str, body: &B) -> IbcResult { + self.request("POST", path, Some(body)).await + } + + async fn request( + &self, + method: &str, + path: &str, + body: Option<&B>, + ) -> IbcResult { + if self.is_closed() { + return Err(IbcError { + message: "Client has been closed".to_string(), + code: Some("CLIENT_CLOSED".to_string()), + status: None, + }); + } + + let url = format!("{}{}", self.config.endpoint, path); + let mut last_error = None; + + for attempt in 0..=self.config.retries { + let mut req = match method { + "GET" => self.client.get(&url), + "POST" => self.client.post(&url), + "DELETE" => self.client.delete(&url), + _ => unreachable!(), + }; + + req = req + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", self.config.api_key)) + .header("X-SDK-Version", "rust/0.1.0") + .header("X-Chain-Id", &self.config.chain_id) + .timeout(self.config.timeout); + + if let Some(b) = body { + req = req.json(b); + } + + match req.send().await { + Ok(resp) => { + let status = resp.status(); + if status.is_success() { + return resp.json().await.map_err(|e| IbcError { + message: e.to_string(), + code: None, + status: None, + }); + } else { + let error: serde_json::Value = resp.json().await.unwrap_or_default(); + return Err(IbcError { + message: error["message"].as_str().unwrap_or(&format!("HTTP {}", status)).to_string(), + code: error["code"].as_str().map(String::from), + status: Some(status.as_u16()), + }); + } + } + Err(e) => { + last_error = Some(e.to_string()); + if attempt < self.config.retries { + tokio::time::sleep(std::time::Duration::from_millis(100 * 2u64.pow(attempt))).await; + } + } + } + } + + Err(IbcError { + message: last_error.unwrap_or_else(|| "Request failed".to_string()), + code: Some("NETWORK_ERROR".to_string()), + status: None, + }) + } +} diff --git a/sdk/rust/src/ibc/mod.rs b/sdk/rust/src/ibc/mod.rs new file mode 100644 index 0000000..de72f64 --- /dev/null +++ b/sdk/rust/src/ibc/mod.rs @@ -0,0 +1,9 @@ +//! Synor IBC SDK for Rust +//! +//! Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability. + +mod types; +mod client; + +pub use types::*; +pub use client::*; diff --git a/sdk/rust/src/ibc/types.rs b/sdk/rust/src/ibc/types.rs new file mode 100644 index 0000000..64b8b88 --- /dev/null +++ b/sdk/rust/src/ibc/types.rs @@ -0,0 +1,492 @@ +//! IBC SDK Types + +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// IBC Height representation +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct Height { + pub revision_number: u64, + pub revision_height: u64, +} + +impl Height { + pub fn new(revision_number: u64, revision_height: u64) -> Self { + Self { revision_number, revision_height } + } + + pub fn from_height(height: u64) -> Self { + Self { revision_number: 0, revision_height: height } + } + + pub fn is_zero(&self) -> bool { + self.revision_number == 0 && self.revision_height == 0 + } + + pub fn increment(&self) -> Self { + Self { revision_number: self.revision_number, revision_height: self.revision_height + 1 } + } +} + +/// Timestamp in nanoseconds since Unix epoch +pub type Timestamp = u64; + +/// Chain identifier +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ChainId(pub String); + +impl ChainId { + pub fn new(id: impl Into) -> Self { + Self(id.into()) + } +} + +/// IBC Version with features +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Version { + pub identifier: String, + pub features: Vec, +} + +impl Default for Version { + fn default() -> Self { + Self { + identifier: "1".to_string(), + features: vec!["ORDER_ORDERED".to_string(), "ORDER_UNORDERED".to_string()], + } + } +} + +/// Commitment prefix for Merkle paths +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CommitmentPrefix { + pub key_prefix: Vec, +} + +impl Default for CommitmentPrefix { + fn default() -> Self { + Self { key_prefix: b"ibc".to_vec() } + } +} + +/// Light client identifier +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ClientId(pub String); + +/// Light client types +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ClientType { + Tendermint, + SoloMachine, + Localhost, + Wasm, +} + +/// Trust level configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrustLevel { + pub numerator: u64, + pub denominator: u64, +} + +impl Default for TrustLevel { + fn default() -> Self { + Self { numerator: 1, denominator: 3 } + } +} + +/// Light client state +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClientState { + pub chain_id: String, + pub trust_level: TrustLevel, + pub trusting_period: u64, + pub unbonding_period: u64, + pub max_clock_drift: u64, + pub latest_height: Height, + #[serde(skip_serializing_if = "Option::is_none")] + pub frozen_height: Option, +} + +/// Consensus state at a specific height +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConsensusState { + pub timestamp: Timestamp, + #[serde(with = "base64_bytes")] + pub root: Vec, + #[serde(with = "base64_bytes")] + pub next_validators_hash: Vec, +} + +/// Connection identifier +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ConnectionId(pub String); + +impl ConnectionId { + pub fn new(sequence: u64) -> Self { + Self(format!("connection-{}", sequence)) + } +} + +/// Connection state +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ConnectionState { + Uninitialized, + Init, + Tryopen, + Open, +} + +/// Connection counterparty +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConnectionCounterparty { + pub client_id: ClientId, + #[serde(skip_serializing_if = "Option::is_none")] + pub connection_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub prefix: Option, +} + +/// Connection end +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConnectionEnd { + pub client_id: ClientId, + pub versions: Vec, + pub state: ConnectionState, + pub counterparty: ConnectionCounterparty, + pub delay_period: u64, +} + +/// Port identifier +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PortId(pub String); + +impl PortId { + pub fn transfer() -> Self { + Self("transfer".to_string()) + } +} + +/// Channel identifier +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ChannelId(pub String); + +impl ChannelId { + pub fn new(sequence: u64) -> Self { + Self(format!("channel-{}", sequence)) + } +} + +/// Channel ordering +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ChannelOrder { + Unordered, + Ordered, +} + +/// Channel state +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ChannelState { + Uninitialized, + Init, + Tryopen, + Open, + Closed, +} + +/// Channel counterparty +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChannelCounterparty { + pub port_id: PortId, + #[serde(skip_serializing_if = "Option::is_none")] + pub channel_id: Option, +} + +/// Channel end +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Channel { + pub state: ChannelState, + pub ordering: ChannelOrder, + pub counterparty: ChannelCounterparty, + pub connection_hops: Vec, + pub version: String, +} + +/// IBC Packet +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Packet { + pub sequence: u64, + pub source_port: PortId, + pub source_channel: ChannelId, + pub dest_port: PortId, + pub dest_channel: ChannelId, + #[serde(with = "base64_bytes")] + pub data: Vec, + pub timeout_height: Height, + pub timeout_timestamp: Timestamp, +} + +/// Packet commitment hash +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PacketCommitment(#[serde(with = "base64_bytes")] pub Vec); + +/// Packet acknowledgement +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Acknowledgement { + Success { success: Vec }, + Error { error: String }, +} + +impl Acknowledgement { + pub fn success(data: Vec) -> Self { + Acknowledgement::Success { success: data } + } + + pub fn error(msg: impl Into) -> Self { + Acknowledgement::Error { error: msg.into() } + } + + pub fn is_success(&self) -> bool { + matches!(self, Acknowledgement::Success { .. }) + } +} + +/// Timeout information +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Timeout { + pub height: Height, + pub timestamp: Timestamp, +} + +impl Timeout { + pub fn from_height(height: u64) -> Self { + Self { height: Height::from_height(height), timestamp: 0 } + } + + pub fn from_timestamp(timestamp: Timestamp) -> Self { + Self { height: Height::default(), timestamp } + } +} + +/// Transfer packet data (ICS-20) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FungibleTokenPacketData { + pub denom: String, + pub amount: String, + pub sender: String, + pub receiver: String, + #[serde(default)] + pub memo: String, +} + +/// Swap identifier +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct SwapId(pub String); + +/// Swap state +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum SwapState { + Pending, + Locked, + Completed, + Refunded, + Expired, + Cancelled, +} + +/// Swap asset types +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SwapAsset { + Native { native: NativeAsset }, + Ics20 { ics20: Ics20Asset }, + Ics721 { ics721: Ics721Asset }, +} + +/// Native blockchain token +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NativeAsset { + pub amount: u128, +} + +/// ICS-20 fungible token +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Ics20Asset { + pub denom: String, + pub amount: u128, +} + +/// ICS-721 NFT +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Ics721Asset { + pub class_id: String, + pub token_ids: Vec, +} + +/// Hashlock - hash of the secret +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Hashlock(#[serde(with = "base64_bytes")] pub Vec); + +impl Hashlock { + pub fn from_secret(secret: &[u8]) -> Self { + use sha2::{Sha256, Digest}; + let hash = Sha256::digest(secret); + Self(hash.to_vec()) + } + + pub fn verify(&self, preimage: &[u8]) -> bool { + let computed = Self::from_secret(preimage); + self.0 == computed.0 + } +} + +/// Timelock - expiration time +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Timelock { + pub expiry: Timestamp, +} + +impl Timelock { + pub fn from_duration(current: Timestamp, duration_secs: u64) -> Self { + Self { expiry: current + duration_secs * 1_000_000_000 } + } + + pub fn is_expired(&self, current: Timestamp) -> bool { + current >= self.expiry + } + + pub fn remaining_secs(&self, current: Timestamp) -> u64 { + if current >= self.expiry { 0 } else { (self.expiry - current) / 1_000_000_000 } + } +} + +/// Hashed Time-Locked Contract +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Htlc { + pub swap_id: SwapId, + pub state: SwapState, + pub initiator: String, + pub responder: String, + pub asset: SwapAsset, + pub hashlock: Hashlock, + pub timelock: Timelock, + #[serde(skip_serializing_if = "Option::is_none")] + pub secret: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub channel_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub port_id: Option, + pub created_at: Timestamp, + #[serde(skip_serializing_if = "Option::is_none")] + pub completed_at: Option, +} + +/// Atomic swap between two chains +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AtomicSwap { + pub swap_id: SwapId, + pub initiator_htlc: Htlc, + #[serde(skip_serializing_if = "Option::is_none")] + pub responder_htlc: Option, + pub state: SwapState, +} + +/// Swap packet actions +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum SwapAction { + Initiate, + Respond, + Claim, + Refund, +} + +/// Merkle proof +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MerkleProof { + pub proofs: Vec, +} + +/// Proof operation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProofOp { + pub r#type: String, + #[serde(with = "base64_bytes")] + pub key: Vec, + #[serde(with = "base64_bytes")] + pub data: Vec, +} + +/// Commitment proof with height +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CommitmentProof { + pub proof: MerkleProof, + pub height: Height, +} + +/// IBC SDK configuration +#[derive(Debug, Clone)] +pub struct IbcConfig { + pub api_key: String, + pub endpoint: String, + pub ws_endpoint: String, + pub timeout: std::time::Duration, + pub retries: u32, + pub chain_id: String, + pub debug: bool, +} + +impl IbcConfig { + pub fn new(api_key: impl Into) -> Self { + Self { + api_key: api_key.into(), + endpoint: "https://ibc.synor.io/v1".to_string(), + ws_endpoint: "wss://ibc.synor.io/v1/ws".to_string(), + timeout: std::time::Duration::from_secs(30), + retries: 3, + chain_id: "synor-1".to_string(), + debug: false, + } + } +} + +/// IBC error +#[derive(Debug)] +pub struct IbcError { + pub message: String, + pub code: Option, + pub status: Option, +} + +impl fmt::Display for IbcError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for IbcError {} + +pub type IbcResult = Result; + +/// Base64 bytes serde helper +mod base64_bytes { + use base64::{Engine as _, engine::general_purpose::STANDARD}; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(bytes: &Vec, serializer: S) -> Result + where S: Serializer { + serializer.serialize_str(&STANDARD.encode(bytes)) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where D: Deserializer<'de> { + let s = String::deserialize(deserializer)?; + STANDARD.decode(&s).map_err(serde::de::Error::custom) + } +} diff --git a/sdk/swift/Sources/SynorIbc/SynorIbc.swift b/sdk/swift/Sources/SynorIbc/SynorIbc.swift new file mode 100644 index 0000000..74b7d8d --- /dev/null +++ b/sdk/swift/Sources/SynorIbc/SynorIbc.swift @@ -0,0 +1,342 @@ +import Foundation + +/// Synor IBC SDK for Swift +/// +/// Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability. +public class SynorIbc { + private let config: IbcConfig + private let session: URLSession + private var _closed = false + + public lazy var clients = LightClientClient(ibc: self) + public lazy var connections = ConnectionsClient(ibc: self) + public lazy var channels = ChannelsClient(ibc: self) + public lazy var transfer = TransferClient(ibc: self) + public lazy var swaps = SwapsClient(ibc: self) + + public init(config: IbcConfig) { + self.config = config + let sessionConfig = URLSessionConfiguration.default + sessionConfig.timeoutIntervalForRequest = TimeInterval(config.timeout) + self.session = URLSession(configuration: sessionConfig) + } + + public var chainId: String { config.chainId } + + public func getChainInfo() async throws -> [String: Any] { + try await get("/chain") + } + + public func getHeight() async throws -> Height { + let result = try await get("/chain/height") + return Height( + revisionNumber: (result["revision_number"] as? UInt64) ?? 0, + revisionHeight: (result["revision_height"] as? UInt64) ?? 1 + ) + } + + public func healthCheck() async -> Bool { + do { + let result = try await get("/health") + return result["status"] as? String == "healthy" + } catch { + return false + } + } + + public func close() { + _closed = true + session.invalidateAndCancel() + } + + public var isClosed: Bool { _closed } + + // Internal HTTP methods + func get(_ path: String) async throws -> [String: Any] { + try checkClosed() + var request = URLRequest(url: URL(string: "\(config.endpoint)\(path)")!) + request.httpMethod = "GET" + addHeaders(&request) + return try await performRequest(request) + } + + func post(_ path: String, body: [String: Any]) async throws -> [String: Any] { + try checkClosed() + var request = URLRequest(url: URL(string: "\(config.endpoint)\(path)")!) + request.httpMethod = "POST" + request.httpBody = try JSONSerialization.data(withJSONObject: body) + addHeaders(&request) + return try await performRequest(request) + } + + private func addHeaders(_ request: inout URLRequest) { + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version") + request.setValue(config.chainId, forHTTPHeaderField: "X-Chain-Id") + } + + private func performRequest(_ request: URLRequest) async throws -> [String: Any] { + let (data, response) = try await session.data(for: request) + guard let httpResponse = response as? HTTPURLResponse else { + throw IbcError("Invalid response") + } + if httpResponse.statusCode >= 400 { + let error = try? JSONSerialization.jsonObject(with: data) as? [String: Any] + throw IbcError( + error?["message"] as? String ?? "HTTP \(httpResponse.statusCode)", + code: error?["code"] as? String, + status: httpResponse.statusCode + ) + } + guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + throw IbcError("Invalid JSON response") + } + return json + } + + private func checkClosed() throws { + if _closed { + throw IbcError("Client has been closed", code: "CLIENT_CLOSED") + } + } + + /// Light client sub-client + public class LightClientClient { + private let ibc: SynorIbc + + init(ibc: SynorIbc) { + self.ibc = ibc + } + + public func create( + clientType: ClientType, + clientState: ClientState, + consensusState: [String: Any] + ) async throws -> ClientId { + let result = try await ibc.post("/clients", body: [ + "client_type": clientType.rawValue, + "client_state": [ + "chain_id": clientState.chainId, + "trust_level": [ + "numerator": clientState.trustLevel.numerator, + "denominator": clientState.trustLevel.denominator + ], + "trusting_period": clientState.trustingPeriod, + "unbonding_period": clientState.unbondingPeriod, + "max_clock_drift": clientState.maxClockDrift, + "latest_height": [ + "revision_number": clientState.latestHeight.revisionNumber, + "revision_height": clientState.latestHeight.revisionHeight + ] + ] as [String: Any], + "consensus_state": consensusState + ]) + guard let clientId = result["client_id"] as? String else { + throw IbcError("Missing client_id in response") + } + return ClientId(clientId) + } + + public func getState(clientId: ClientId) async throws -> ClientState { + let result = try await ibc.get("/clients/\(clientId.id)/state") + guard let chainId = result["chain_id"] as? String, + let trustLevelDict = result["trust_level"] as? [String: Any], + let latestHeightDict = result["latest_height"] as? [String: Any] else { + throw IbcError("Invalid client state response") + } + return ClientState( + chainId: chainId, + trustLevel: TrustLevel( + numerator: trustLevelDict["numerator"] as? Int ?? 1, + denominator: trustLevelDict["denominator"] as? Int ?? 3 + ), + trustingPeriod: result["trusting_period"] as? UInt64 ?? 0, + unbondingPeriod: result["unbonding_period"] as? UInt64 ?? 0, + maxClockDrift: result["max_clock_drift"] as? UInt64 ?? 0, + latestHeight: Height( + revisionNumber: latestHeightDict["revision_number"] as? UInt64 ?? 0, + revisionHeight: latestHeightDict["revision_height"] as? UInt64 ?? 1 + ) + ) + } + + public func list() async throws -> [[String: Any]] { + let result = try await ibc.get("/clients") + return result["clients"] as? [[String: Any]] ?? [] + } + } + + /// Connections sub-client + public class ConnectionsClient { + private let ibc: SynorIbc + + init(ibc: SynorIbc) { + self.ibc = ibc + } + + public func openInit( + clientId: ClientId, + counterpartyClientId: ClientId + ) async throws -> ConnectionId { + let result = try await ibc.post("/connections/init", body: [ + "client_id": clientId.id, + "counterparty_client_id": counterpartyClientId.id + ]) + guard let connectionId = result["connection_id"] as? String else { + throw IbcError("Missing connection_id in response") + } + return ConnectionId(connectionId) + } + + public func get(connectionId: ConnectionId) async throws -> [String: Any] { + try await ibc.get("/connections/\(connectionId.id)") + } + + public func list() async throws -> [[String: Any]] { + let result = try await ibc.get("/connections") + return result["connections"] as? [[String: Any]] ?? [] + } + } + + /// Channels sub-client + public class ChannelsClient { + private let ibc: SynorIbc + + init(ibc: SynorIbc) { + self.ibc = ibc + } + + public func bindPort(portId: PortId, module: String) async throws { + _ = try await ibc.post("/ports/bind", body: [ + "port_id": portId.id, + "module": module + ]) + } + + public func openInit( + portId: PortId, + ordering: ChannelOrder, + connectionId: ConnectionId, + counterpartyPort: PortId, + version: String + ) async throws -> ChannelId { + let result = try await ibc.post("/channels/init", body: [ + "port_id": portId.id, + "ordering": ordering.rawValue, + "connection_id": connectionId.id, + "counterparty_port": counterpartyPort.id, + "version": version + ]) + guard let channelId = result["channel_id"] as? String else { + throw IbcError("Missing channel_id in response") + } + return ChannelId(channelId) + } + + public func get(portId: PortId, channelId: ChannelId) async throws -> [String: Any] { + try await ibc.get("/channels/\(portId.id)/\(channelId.id)") + } + + public func list() async throws -> [[String: Any]] { + let result = try await ibc.get("/channels") + return result["channels"] as? [[String: Any]] ?? [] + } + } + + /// Transfer sub-client (ICS-20) + public class TransferClient { + private let ibc: SynorIbc + + init(ibc: SynorIbc) { + self.ibc = ibc + } + + public func transfer( + sourcePort: String, + sourceChannel: String, + denom: String, + amount: String, + sender: String, + receiver: String, + timeout: Timeout? = nil, + memo: String? = nil + ) async throws -> [String: Any] { + var body: [String: Any] = [ + "source_port": sourcePort, + "source_channel": sourceChannel, + "token": ["denom": denom, "amount": amount], + "sender": sender, + "receiver": receiver + ] + if let timeout = timeout { + body["timeout_height"] = [ + "revision_number": timeout.height.revisionNumber, + "revision_height": timeout.height.revisionHeight + ] + body["timeout_timestamp"] = String(timeout.timestamp) + } + if let memo = memo { body["memo"] = memo } + return try await ibc.post("/transfer", body: body) + } + + public func getDenomTrace(ibcDenom: String) async throws -> [String: Any] { + try await ibc.get("/transfer/denom_trace/\(ibcDenom)") + } + } + + /// Swaps sub-client (HTLC) + public class SwapsClient { + private let ibc: SynorIbc + + init(ibc: SynorIbc) { + self.ibc = ibc + } + + public func initiate( + responder: String, + initiatorAsset: [String: Any], + responderAsset: [String: Any] + ) async throws -> [String: Any] { + try await ibc.post("/swaps/initiate", body: [ + "responder": responder, + "initiator_asset": initiatorAsset, + "responder_asset": responderAsset + ]) + } + + public func lock(swapId: SwapId) async throws { + _ = try await ibc.post("/swaps/\(swapId.id)/lock", body: [:]) + } + + public func respond(swapId: SwapId, asset: [String: Any]) async throws -> [String: Any] { + try await ibc.post("/swaps/\(swapId.id)/respond", body: ["asset": asset]) + } + + public func claim(swapId: SwapId, secret: Data) async throws -> [String: Any] { + try await ibc.post("/swaps/\(swapId.id)/claim", body: [ + "secret": secret.base64EncodedString() + ]) + } + + public func refund(swapId: SwapId) async throws -> [String: Any] { + try await ibc.post("/swaps/\(swapId.id)/refund", body: [:]) + } + + public func get(swapId: SwapId) async throws -> AtomicSwap { + let result = try await ibc.get("/swaps/\(swapId.id)") + guard let swap = AtomicSwap.fromJson(result) else { + throw IbcError("Invalid swap response") + } + return swap + } + + public func listActive() async throws -> [AtomicSwap] { + let result = try await ibc.get("/swaps/active") + guard let swaps = result["swaps"] as? [[String: Any]] else { + return [] + } + return swaps.compactMap { AtomicSwap.fromJson($0) } + } + } +} diff --git a/sdk/swift/Sources/SynorIbc/Types.swift b/sdk/swift/Sources/SynorIbc/Types.swift new file mode 100644 index 0000000..d508141 --- /dev/null +++ b/sdk/swift/Sources/SynorIbc/Types.swift @@ -0,0 +1,383 @@ +import Foundation + +/// Synor IBC SDK Types for Swift +/// +/// Inter-Blockchain Communication (IBC) protocol types. + +/// IBC Height representation +public struct Height: Codable, Equatable { + public let revisionNumber: UInt64 + public let revisionHeight: UInt64 + + public init(revisionNumber: UInt64 = 0, revisionHeight: UInt64 = 1) { + self.revisionNumber = revisionNumber + self.revisionHeight = revisionHeight + } + + public var isZero: Bool { + revisionNumber == 0 && revisionHeight == 0 + } + + public func increment() -> Height { + Height(revisionNumber: revisionNumber, revisionHeight: revisionHeight + 1) + } + + enum CodingKeys: String, CodingKey { + case revisionNumber = "revision_number" + case revisionHeight = "revision_height" + } +} + +/// Chain identifier +public struct ChainId: Codable, Equatable { + public let id: String + + public init(_ id: String) { + self.id = id + } +} + +/// IBC Version with features +public struct Version: Codable, Equatable { + public let identifier: String + public let features: [String] + + public init(identifier: String, features: [String]) { + self.identifier = identifier + self.features = features + } + + public static func defaultConnection() -> Version { + Version(identifier: "1", features: ["ORDER_ORDERED", "ORDER_UNORDERED"]) + } +} + +/// Light client types +public enum ClientType: String, Codable { + case tendermint + case soloMachine = "solo_machine" + case localhost + case wasm +} + +/// Light client identifier +public struct ClientId: Codable, Equatable { + public let id: String + + public init(_ id: String) { + self.id = id + } +} + +/// Trust level configuration +public struct TrustLevel: Codable, Equatable { + public let numerator: Int + public let denominator: Int + + public init(numerator: Int = 1, denominator: Int = 3) { + self.numerator = numerator + self.denominator = denominator + } +} + +/// Light client state +public struct ClientState: Codable { + public let chainId: String + public let trustLevel: TrustLevel + public let trustingPeriod: UInt64 + public let unbondingPeriod: UInt64 + public let maxClockDrift: UInt64 + public let latestHeight: Height + public let frozenHeight: Height? + + public init( + chainId: String, + trustLevel: TrustLevel, + trustingPeriod: UInt64, + unbondingPeriod: UInt64, + maxClockDrift: UInt64, + latestHeight: Height, + frozenHeight: Height? = nil + ) { + self.chainId = chainId + self.trustLevel = trustLevel + self.trustingPeriod = trustingPeriod + self.unbondingPeriod = unbondingPeriod + self.maxClockDrift = maxClockDrift + self.latestHeight = latestHeight + self.frozenHeight = frozenHeight + } + + enum CodingKeys: String, CodingKey { + case chainId = "chain_id" + case trustLevel = "trust_level" + case trustingPeriod = "trusting_period" + case unbondingPeriod = "unbonding_period" + case maxClockDrift = "max_clock_drift" + case latestHeight = "latest_height" + case frozenHeight = "frozen_height" + } +} + +/// Connection state +public enum ConnectionState: String, Codable { + case uninitialized + case `init` + case tryopen + case open +} + +/// Connection identifier +public struct ConnectionId: Codable, Equatable { + public let id: String + + public init(_ id: String) { + self.id = id + } + + public static func newId(_ sequence: Int) -> ConnectionId { + ConnectionId("connection-\(sequence)") + } +} + +/// Port identifier +public struct PortId: Codable, Equatable { + public let id: String + + public init(_ id: String) { + self.id = id + } + + public static func transfer() -> PortId { + PortId("transfer") + } +} + +/// Channel identifier +public struct ChannelId: Codable, Equatable { + public let id: String + + public init(_ id: String) { + self.id = id + } + + public static func newId(_ sequence: Int) -> ChannelId { + ChannelId("channel-\(sequence)") + } +} + +/// Channel ordering +public enum ChannelOrder: String, Codable { + case unordered + case ordered +} + +/// Channel state +public enum ChannelState: String, Codable { + case uninitialized + case `init` + case tryopen + case open + case closed +} + +/// Timeout information +public struct Timeout { + public let height: Height + public let timestamp: UInt64 + + public init(height: Height, timestamp: UInt64 = 0) { + self.height = height + self.timestamp = timestamp + } + + public static func fromHeight(_ height: UInt64) -> Timeout { + Timeout(height: Height(revisionHeight: height)) + } + + public static func fromTimestamp(_ timestamp: UInt64) -> Timeout { + Timeout(height: Height(), timestamp: timestamp) + } + + public func toJson() -> [String: Any] { + [ + "height": [ + "revision_number": height.revisionNumber, + "revision_height": height.revisionHeight + ], + "timestamp": String(timestamp) + ] + } +} + +/// Transfer packet data (ICS-20) +public struct FungibleTokenPacketData: Codable { + public let denom: String + public let amount: String + public let sender: String + public let receiver: String + public let memo: String + + public init(denom: String, amount: String, sender: String, receiver: String, memo: String = "") { + self.denom = denom + self.amount = amount + self.sender = sender + self.receiver = receiver + self.memo = memo + } + + public var isNative: Bool { + !denom.contains("/") + } +} + +/// Swap state +public enum SwapState: String, Codable { + case pending + case locked + case completed + case refunded + case expired + case cancelled +} + +/// Swap identifier +public struct SwapId: Codable, Equatable { + public let id: String + + public init(_ id: String) { + self.id = id + } +} + +/// Native asset for swaps +public struct NativeAsset { + public let amount: UInt64 + + public init(amount: UInt64) { + self.amount = amount + } + + public func toJson() -> [String: Any] { + ["native": ["amount": String(amount)]] + } +} + +/// ICS-20 asset for swaps +public struct Ics20Asset { + public let denom: String + public let amount: UInt64 + + public init(denom: String, amount: UInt64) { + self.denom = denom + self.amount = amount + } + + public func toJson() -> [String: Any] { + ["ics20": ["denom": denom, "amount": String(amount)]] + } +} + +/// Hashlock - hash of the secret +public struct Hashlock { + public let hash: Data + + public init(hash: Data) { + self.hash = hash + } +} + +/// Timelock - expiration time +public struct Timelock { + public let expiry: UInt64 + + public init(expiry: UInt64) { + self.expiry = expiry + } + + public func isExpired(current: UInt64) -> Bool { + current >= expiry + } +} + +/// Atomic swap +public struct AtomicSwap { + public let swapId: SwapId + public let state: SwapState + public let initiatorHtlc: [String: Any] + public let responderHtlc: [String: Any]? + + public init( + swapId: SwapId, + state: SwapState, + initiatorHtlc: [String: Any], + responderHtlc: [String: Any]? = nil + ) { + self.swapId = swapId + self.state = state + self.initiatorHtlc = initiatorHtlc + self.responderHtlc = responderHtlc + } + + public static func fromJson(_ json: [String: Any]) -> AtomicSwap? { + guard let swapIdDict = json["swap_id"] as? [String: Any], + let swapIdStr = swapIdDict["id"] as? String, + let stateStr = json["state"] as? String, + let state = SwapState(rawValue: stateStr), + let initiatorHtlc = json["initiator_htlc"] as? [String: Any] else { + return nil + } + return AtomicSwap( + swapId: SwapId(swapIdStr), + state: state, + initiatorHtlc: initiatorHtlc, + responderHtlc: json["responder_htlc"] as? [String: Any] + ) + } +} + +/// IBC SDK configuration +public struct IbcConfig { + public let apiKey: String + public let endpoint: String + public let wsEndpoint: String + public let timeout: Int + public let retries: Int + public let chainId: String + public let debug: Bool + + public init( + apiKey: String, + endpoint: String = "https://ibc.synor.io/v1", + wsEndpoint: String = "wss://ibc.synor.io/v1/ws", + timeout: Int = 30, + retries: Int = 3, + chainId: String = "synor-1", + debug: Bool = false + ) { + self.apiKey = apiKey + self.endpoint = endpoint + self.wsEndpoint = wsEndpoint + self.timeout = timeout + self.retries = retries + self.chainId = chainId + self.debug = debug + } +} + +/// IBC exception +public struct IbcError: Error, CustomStringConvertible { + public let message: String + public let code: String? + public let status: Int? + + public init(_ message: String, code: String? = nil, status: Int? = nil) { + self.message = message + self.code = code + self.status = status + } + + public var description: String { + "IbcError: \(message)\(code.map { " (\($0))" } ?? "")" + } +}