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
This commit is contained in:
Gulshan Yadav 2026-01-28 12:53:46 +05:30
parent e7dc8f70a0
commit 97add23062
24 changed files with 9167 additions and 0 deletions

313
sdk/c/include/synor/ibc.h Normal file
View file

@ -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 <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#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 */

View file

@ -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 <string>
#include <vector>
#include <optional>
#include <memory>
#include <cstdint>
#include <future>
#include <nlohmann/json.hpp>
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<Height> frozen_height;
static ClientState from_json(const json& j) {
ClientState state;
state.chain_id = j.at("chain_id").get<std::string>();
state.trust_level = TrustLevel::from_json(j.at("trust_level"));
state.trusting_period = j.at("trusting_period").get<uint64_t>();
state.unbonding_period = j.at("unbonding_period").get<uint64_t>();
state.max_clock_drift = j.at("max_clock_drift").get<uint64_t>();
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<std::string>(),
j.at("amount").get<std::string>(),
j.at("sender").get<std::string>(),
j.at("receiver").get<std::string>(),
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<json> responder_htlc;
static AtomicSwap from_json(const json& j) {
AtomicSwap swap;
swap.swap_id = j.at("swap_id").at("id").get<std::string>();
swap.state = swap_state_from_string(j.at("state").get<std::string>());
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<std::string> create(ClientType client_type,
const ClientState& client_state,
const json& consensus_state);
std::future<ClientState> get_state(const std::string& client_id);
std::future<std::vector<json>> list();
private:
SynorIbc& ibc_;
};
/**
* Connections sub-client
*/
class ConnectionsClient {
public:
explicit ConnectionsClient(SynorIbc& ibc) : ibc_(ibc) {}
std::future<std::string> open_init(const std::string& client_id,
const std::string& counterparty_client_id);
std::future<json> get(const std::string& connection_id);
std::future<std::vector<json>> list();
private:
SynorIbc& ibc_;
};
/**
* Channels sub-client
*/
class ChannelsClient {
public:
explicit ChannelsClient(SynorIbc& ibc) : ibc_(ibc) {}
std::future<void> bind_port(const std::string& port_id, const std::string& module);
std::future<std::string> 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<json> get(const std::string& port_id, const std::string& channel_id);
std::future<std::vector<json>> list();
private:
SynorIbc& ibc_;
};
/**
* Transfer sub-client (ICS-20)
*/
class TransferClient {
public:
explicit TransferClient(SynorIbc& ibc) : ibc_(ibc) {}
std::future<json> 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> timeout = std::nullopt,
std::optional<std::string> memo = std::nullopt);
std::future<json> 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<json> initiate(const std::string& responder,
const json& initiator_asset,
const json& responder_asset);
std::future<void> lock(const std::string& swap_id);
std::future<json> respond(const std::string& swap_id, const json& asset);
std::future<json> claim(const std::string& swap_id,
const std::vector<uint8_t>& secret);
std::future<json> refund(const std::string& swap_id);
std::future<AtomicSwap> get(const std::string& swap_id);
std::future<std::vector<AtomicSwap>> 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<json> get_chain_info();
std::future<Height> get_height();
std::future<bool> 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<json> get(const std::string& path);
std::future<json> post(const std::string& path, const json& body);
private:
void check_closed() const;
IbcConfig config_;
bool closed_ = false;
std::unique_ptr<LightClientClient> clients_;
std::unique_ptr<ConnectionsClient> connections_;
std::unique_ptr<ChannelsClient> channels_;
std::unique_ptr<TransferClient> transfer_;
std::unique_ptr<SwapsClient> swaps_;
// HTTP client implementation (pimpl)
class Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace ibc
} // namespace synor
#endif // SYNOR_IBC_HPP

View file

@ -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
{
/// <summary>
/// Synor IBC SDK for C#
///
/// Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability.
/// </summary>
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<Dictionary<string, object>> GetChainInfoAsync() => GetAsync("/chain");
public async Task<Height> 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<bool> 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<Dictionary<string, object>> 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<Dictionary<string, object>> PostAsync(string path, Dictionary<string, object> 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<Dictionary<string, object>> HandleResponseAsync(HttpResponseMessage response)
{
var content = await response.Content.ReadAsStringAsync();
if ((int)response.StatusCode >= 400)
{
Dictionary<string, object>? error = null;
try
{
error = JsonSerializer.Deserialize<Dictionary<string, object>>(content);
}
catch { }
throw new IbcException(
error?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)response.StatusCode}",
error?.GetValueOrDefault("code")?.ToString(),
(int)response.StatusCode
);
}
return JsonSerializer.Deserialize<Dictionary<string, object>>(content) ?? new();
}
private void CheckClosed()
{
if (_closed)
throw new IbcException("Client has been closed", "CLIENT_CLOSED");
}
/// <summary>
/// Light client sub-client
/// </summary>
public class LightClientClient
{
private readonly SynorIbc _ibc;
internal LightClientClient(SynorIbc ibc) => _ibc = ibc;
public async Task<ClientId> CreateAsync(
ClientType clientType,
ClientState clientState,
Dictionary<string, object> consensusState)
{
var result = await _ibc.PostAsync("/clients", new Dictionary<string, object>
{
["client_type"] = clientType.ToApiString(),
["client_state"] = new Dictionary<string, object>
{
["chain_id"] = clientState.ChainId,
["trust_level"] = new Dictionary<string, object>
{
["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<string, object>
{
["revision_number"] = clientState.LatestHeight.RevisionNumber,
["revision_height"] = clientState.LatestHeight.RevisionHeight
}
},
["consensus_state"] = consensusState
});
return new ClientId(result["client_id"].ToString()!);
}
public async Task<ClientState> 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<List<Dictionary<string, object>>> ListAsync()
{
var result = await _ibc.GetAsync("/clients");
if (result.TryGetValue("clients", out var clients) && clients is JsonElement element)
{
return JsonSerializer.Deserialize<List<Dictionary<string, object>>>(element.GetRawText()) ?? new();
}
return new List<Dictionary<string, object>>();
}
}
/// <summary>
/// Connections sub-client
/// </summary>
public class ConnectionsClient
{
private readonly SynorIbc _ibc;
internal ConnectionsClient(SynorIbc ibc) => _ibc = ibc;
public async Task<ConnectionId> OpenInitAsync(ClientId clientId, ClientId counterpartyClientId)
{
var result = await _ibc.PostAsync("/connections/init", new Dictionary<string, object>
{
["client_id"] = clientId.Id,
["counterparty_client_id"] = counterpartyClientId.Id
});
return new ConnectionId(result["connection_id"].ToString()!);
}
public Task<Dictionary<string, object>> GetAsync(ConnectionId connectionId) =>
_ibc.GetAsync($"/connections/{connectionId.Id}");
public async Task<List<Dictionary<string, object>>> ListAsync()
{
var result = await _ibc.GetAsync("/connections");
if (result.TryGetValue("connections", out var connections) && connections is JsonElement element)
{
return JsonSerializer.Deserialize<List<Dictionary<string, object>>>(element.GetRawText()) ?? new();
}
return new List<Dictionary<string, object>>();
}
}
/// <summary>
/// Channels sub-client
/// </summary>
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<string, object>
{
["port_id"] = portId.Id,
["module"] = module
});
public async Task<ChannelId> OpenInitAsync(
PortId portId,
ChannelOrder ordering,
ConnectionId connectionId,
PortId counterpartyPort,
string version)
{
var result = await _ibc.PostAsync("/channels/init", new Dictionary<string, object>
{
["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<Dictionary<string, object>> GetAsync(PortId portId, ChannelId channelId) =>
_ibc.GetAsync($"/channels/{portId.Id}/{channelId.Id}");
public async Task<List<Dictionary<string, object>>> ListAsync()
{
var result = await _ibc.GetAsync("/channels");
if (result.TryGetValue("channels", out var channels) && channels is JsonElement element)
{
return JsonSerializer.Deserialize<List<Dictionary<string, object>>>(element.GetRawText()) ?? new();
}
return new List<Dictionary<string, object>>();
}
}
/// <summary>
/// Transfer sub-client (ICS-20)
/// </summary>
public class TransferClient
{
private readonly SynorIbc _ibc;
internal TransferClient(SynorIbc ibc) => _ibc = ibc;
public async Task<Dictionary<string, object>> TransferAsync(
string sourcePort,
string sourceChannel,
string denom,
string amount,
string sender,
string receiver,
Timeout? timeout = null,
string? memo = null)
{
var body = new Dictionary<string, object>
{
["source_port"] = sourcePort,
["source_channel"] = sourceChannel,
["token"] = new Dictionary<string, object> { ["denom"] = denom, ["amount"] = amount },
["sender"] = sender,
["receiver"] = receiver
};
if (timeout != null)
{
body["timeout_height"] = new Dictionary<string, object>
{
["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<Dictionary<string, object>> GetDenomTraceAsync(string ibcDenom) =>
_ibc.GetAsync($"/transfer/denom_trace/{ibcDenom}");
}
/// <summary>
/// Swaps sub-client (HTLC)
/// </summary>
public class SwapsClient
{
private readonly SynorIbc _ibc;
internal SwapsClient(SynorIbc ibc) => _ibc = ibc;
public Task<Dictionary<string, object>> InitiateAsync(
string responder,
Dictionary<string, object> initiatorAsset,
Dictionary<string, object> responderAsset) =>
_ibc.PostAsync("/swaps/initiate", new Dictionary<string, object>
{
["responder"] = responder,
["initiator_asset"] = initiatorAsset,
["responder_asset"] = responderAsset
});
public Task LockAsync(SwapId swapId) =>
_ibc.PostAsync($"/swaps/{swapId.Id}/lock", new Dictionary<string, object>());
public Task<Dictionary<string, object>> RespondAsync(SwapId swapId, Dictionary<string, object> asset) =>
_ibc.PostAsync($"/swaps/{swapId.Id}/respond", new Dictionary<string, object> { ["asset"] = asset });
public Task<Dictionary<string, object>> ClaimAsync(SwapId swapId, byte[] secret) =>
_ibc.PostAsync($"/swaps/{swapId.Id}/claim", new Dictionary<string, object>
{
["secret"] = Convert.ToBase64String(secret)
});
public Task<Dictionary<string, object>> RefundAsync(SwapId swapId) =>
_ibc.PostAsync($"/swaps/{swapId.Id}/refund", new Dictionary<string, object>());
public async Task<AtomicSwap> GetAsync(SwapId swapId)
{
var result = await _ibc.GetAsync($"/swaps/{swapId.Id}");
return ParseAtomicSwap(result);
}
public async Task<List<AtomicSwap>> ListActiveAsync()
{
var result = await _ibc.GetAsync("/swaps/active");
var swaps = new List<AtomicSwap>();
if (result.TryGetValue("swaps", out var swapsObj) && swapsObj is JsonElement element)
{
foreach (var swap in element.EnumerateArray())
{
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(swap.GetRawText());
if (dict != null)
swaps.Add(ParseAtomicSwap(dict));
}
}
return swaps;
}
private static AtomicSwap ParseAtomicSwap(Dictionary<string, object> json)
{
var swapIdObj = (JsonElement)json["swap_id"];
var swapId = swapIdObj.GetProperty("id").GetString()!;
var stateStr = json["state"].ToString()!.ToUpperInvariant();
var state = Enum.Parse<SwapState>(stateStr, true);
var initiatorHtlc = JsonSerializer.Deserialize<Dictionary<string, object>>(
((JsonElement)json["initiator_htlc"]).GetRawText()) ?? new();
Dictionary<string, object>? responderHtlc = null;
if (json.TryGetValue("responder_htlc", out var responder) && responder is JsonElement re && re.ValueKind != JsonValueKind.Null)
{
responderHtlc = JsonSerializer.Deserialize<Dictionary<string, object>>(re.GetRawText());
}
return new AtomicSwap(new SwapId(swapId), state, initiatorHtlc, responderHtlc);
}
}
}
}

View file

@ -0,0 +1,279 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text.Json.Serialization;
namespace Synor.Ibc
{
/// <summary>
/// Synor IBC SDK Types for C#
///
/// Inter-Blockchain Communication (IBC) protocol types.
/// </summary>
/// <summary>
/// IBC Height representation
/// </summary>
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 };
}
/// <summary>
/// Chain identifier
/// </summary>
public record ChainId(string Id);
/// <summary>
/// IBC Version with features
/// </summary>
public record Version(string Identifier, List<string> Features)
{
public static Version DefaultConnection() =>
new("1", new List<string> { "ORDER_ORDERED", "ORDER_UNORDERED" });
}
/// <summary>
/// Light client types
/// </summary>
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))
};
}
/// <summary>
/// Light client identifier
/// </summary>
public record ClientId(string Id);
/// <summary>
/// Trust level configuration
/// </summary>
public record TrustLevel(int Numerator = 1, int Denominator = 3);
/// <summary>
/// Light client state
/// </summary>
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
);
/// <summary>
/// Connection state
/// </summary>
public enum ConnectionState
{
Uninitialized,
Init,
TryOpen,
Open
}
/// <summary>
/// Connection identifier
/// </summary>
public record ConnectionId(string Id)
{
public static ConnectionId NewId(int sequence) => new($"connection-{sequence}");
}
/// <summary>
/// Port identifier
/// </summary>
public record PortId(string Id)
{
public static PortId Transfer() => new("transfer");
}
/// <summary>
/// Channel identifier
/// </summary>
public record ChannelId(string Id)
{
public static ChannelId NewId(int sequence) => new($"channel-{sequence}");
}
/// <summary>
/// Channel ordering
/// </summary>
public enum ChannelOrder
{
Unordered,
Ordered
}
/// <summary>
/// Channel state
/// </summary>
public enum ChannelState
{
Uninitialized,
Init,
TryOpen,
Open,
Closed
}
/// <summary>
/// Timeout information
/// </summary>
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<string, object> ToJson() => new()
{
["height"] = new Dictionary<string, object>
{
["revision_number"] = Height.RevisionNumber,
["revision_height"] = Height.RevisionHeight
},
["timestamp"] = Timestamp.ToString()
};
}
/// <summary>
/// Transfer packet data (ICS-20)
/// </summary>
public record FungibleTokenPacketData(
string Denom,
string Amount,
string Sender,
string Receiver,
string Memo = ""
)
{
public bool IsNative => !Denom.Contains('/');
}
/// <summary>
/// Swap state
/// </summary>
public enum SwapState
{
Pending,
Locked,
Completed,
Refunded,
Expired,
Cancelled
}
/// <summary>
/// Swap identifier
/// </summary>
public record SwapId(string Id);
/// <summary>
/// Native asset for swaps
/// </summary>
public record NativeAsset(BigInteger Amount)
{
public Dictionary<string, object> ToJson() => new()
{
["native"] = new Dictionary<string, object> { ["amount"] = Amount.ToString() }
};
}
/// <summary>
/// ICS-20 asset for swaps
/// </summary>
public record Ics20Asset(string Denom, BigInteger Amount)
{
public Dictionary<string, object> ToJson() => new()
{
["ics20"] = new Dictionary<string, object>
{
["denom"] = Denom,
["amount"] = Amount.ToString()
}
};
}
/// <summary>
/// Hashlock - hash of the secret
/// </summary>
public record Hashlock(byte[] Hash);
/// <summary>
/// Timelock - expiration time
/// </summary>
public record Timelock(BigInteger Expiry)
{
public bool IsExpired(BigInteger current) => current >= Expiry;
}
/// <summary>
/// Atomic swap
/// </summary>
public record AtomicSwap(
SwapId SwapId,
SwapState State,
Dictionary<string, object> InitiatorHtlc,
Dictionary<string, object>? ResponderHtlc
);
/// <summary>
/// IBC SDK configuration
/// </summary>
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
);
/// <summary>
/// IBC exception
/// </summary>
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})" : "")}";
}
}

View file

@ -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<Map<String, dynamic>> getChainInfo() async {
return _get('/chain');
}
Future<Height> getHeight() async {
final result = await _get('/chain/height');
return Height.fromJson(result);
}
Future<bool> 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<Map<String, dynamic>> _get(String path) async {
_checkClosed();
final response = await _client.get(
Uri.parse('${config.endpoint}$path'),
headers: _headers,
);
return _handleResponse(response);
}
Future<Map<String, dynamic>> _post(String path, Map<String, dynamic> body) async {
_checkClosed();
final response = await _client.post(
Uri.parse('${config.endpoint}$path'),
headers: _headers,
body: jsonEncode(body),
);
return _handleResponse(response);
}
Map<String, String> get _headers => {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${config.apiKey}',
'X-SDK-Version': 'flutter/0.1.0',
'X-Chain-Id': config.chainId,
};
Map<String, dynamic> _handleResponse(http.Response response) {
if (response.statusCode >= 400) {
Map<String, dynamic>? 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<ClientId> create({
required ClientType clientType,
required ClientState clientState,
required Map<String, dynamic> 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<ClientState> getState(ClientId clientId) async {
final result = await _ibc._get('/clients/${clientId.id}/state');
return ClientState.fromJson(result);
}
Future<List<Map<String, dynamic>>> list() async {
final result = await _ibc._get('/clients');
return List<Map<String, dynamic>>.from(result['clients'] ?? []);
}
}
/// Connections sub-client
class ConnectionsClient {
final SynorIbc _ibc;
ConnectionsClient(this._ibc);
Future<ConnectionId> 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<Map<String, dynamic>> get(ConnectionId connectionId) async {
return _ibc._get('/connections/${connectionId.id}');
}
Future<List<Map<String, dynamic>>> list() async {
final result = await _ibc._get('/connections');
return List<Map<String, dynamic>>.from(result['connections'] ?? []);
}
}
/// Channels sub-client
class ChannelsClient {
final SynorIbc _ibc;
ChannelsClient(this._ibc);
Future<void> bindPort(PortId portId, String module) async {
await _ibc._post('/ports/bind', {'port_id': portId.id, 'module': module});
}
Future<ChannelId> 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<Map<String, dynamic>> get(PortId portId, ChannelId channelId) async {
return _ibc._get('/channels/${portId.id}/${channelId.id}');
}
Future<List<Map<String, dynamic>>> list() async {
final result = await _ibc._get('/channels');
return List<Map<String, dynamic>>.from(result['channels'] ?? []);
}
}
/// Transfer sub-client (ICS-20)
class TransferClient {
final SynorIbc _ibc;
TransferClient(this._ibc);
Future<Map<String, dynamic>> 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 = <String, dynamic>{
'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<Map<String, dynamic>> getDenomTrace(String ibcDenom) async {
return _ibc._get('/transfer/denom_trace/$ibcDenom');
}
}
/// Swaps sub-client (HTLC)
class SwapsClient {
final SynorIbc _ibc;
SwapsClient(this._ibc);
Future<Map<String, dynamic>> initiate({
required String responder,
required Map<String, dynamic> initiatorAsset,
required Map<String, dynamic> responderAsset,
}) async {
return _ibc._post('/swaps/initiate', {
'responder': responder,
'initiator_asset': initiatorAsset,
'responder_asset': responderAsset,
});
}
Future<void> lock(SwapId swapId) async {
await _ibc._post('/swaps/${swapId.id}/lock', {});
}
Future<Map<String, dynamic>> respond(SwapId swapId, Map<String, dynamic> asset) async {
return _ibc._post('/swaps/${swapId.id}/respond', {'asset': asset});
}
Future<Map<String, dynamic>> claim(SwapId swapId, List<int> secret) async {
return _ibc._post('/swaps/${swapId.id}/claim', {
'secret': base64Encode(secret),
});
}
Future<Map<String, dynamic>> refund(SwapId swapId) async {
return _ibc._post('/swaps/${swapId.id}/refund', {});
}
Future<AtomicSwap> get(SwapId swapId) async {
final result = await _ibc._get('/swaps/${swapId.id}');
return AtomicSwap.fromJson(result);
}
Future<List<AtomicSwap>> listActive() async {
final result = await _ibc._get('/swaps/active');
return (result['swaps'] as List).map((e) => AtomicSwap.fromJson(e)).toList();
}
}

View file

@ -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<String, dynamic> json) => Height(
revisionNumber: json['revision_number'] ?? 0,
revisionHeight: json['revision_height'] ?? 1,
);
Map<String, dynamic> toJson() => {
'revision_number': revisionNumber,
'revision_height': revisionHeight,
};
}
/// Chain identifier
class ChainId {
final String id;
const ChainId(this.id);
factory ChainId.fromJson(Map<String, dynamic> json) => ChainId(json['id']);
Map<String, dynamic> toJson() => {'id': id};
}
/// IBC Version with features
class Version {
final String identifier;
final List<String> 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<String, dynamic> json) => Version(
identifier: json['identifier'],
features: List<String>.from(json['features']),
);
Map<String, dynamic> 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<String, dynamic> json) => ClientId(json['id']);
Map<String, dynamic> 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<String, dynamic> json) => TrustLevel(
numerator: json['numerator'] ?? 1,
denominator: json['denominator'] ?? 3,
);
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) => ConnectionId(json['id']);
Map<String, dynamic> toJson() => {'id': id};
}
/// Port identifier
class PortId {
final String id;
const PortId(this.id);
factory PortId.transfer() => const PortId('transfer');
factory PortId.fromJson(Map<String, dynamic> json) => PortId(json['id']);
Map<String, dynamic> 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<String, dynamic> json) => ChannelId(json['id']);
Map<String, dynamic> 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<int> 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<String, dynamic> 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<int>.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<String, dynamic> 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<String, dynamic> json) => FungibleTokenPacketData(
denom: json['denom'],
amount: json['amount'],
sender: json['sender'],
receiver: json['receiver'],
memo: json['memo'] ?? '',
);
Map<String, dynamic> 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<String, dynamic> json) => SwapId(json['id']);
Map<String, dynamic> toJson() => {'id': id};
}
/// Swap asset - native token
class NativeAsset {
final BigInt amount;
const NativeAsset(this.amount);
Map<String, dynamic> 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<String, dynamic> toJson() => {'ics20': {'denom': denom, 'amount': amount.toString()}};
}
/// Hashlock - hash of the secret
class Hashlock {
final List<int> hash;
const Hashlock(this.hash);
factory Hashlock.fromJson(Map<String, dynamic> json) => Hashlock(List<int>.from(json['hash']));
Map<String, dynamic> 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<String, dynamic> json) => Timelock(
BigInt.parse(json['expiry'].toString()),
);
Map<String, dynamic> toJson() => {'expiry': expiry.toString()};
}
/// Atomic swap
class AtomicSwap {
final SwapId swapId;
final SwapState state;
final Map<String, dynamic> initiatorHtlc;
final Map<String, dynamic>? responderHtlc;
const AtomicSwap({
required this.swapId,
required this.state,
required this.initiatorHtlc,
this.responderHtlc,
});
factory AtomicSwap.fromJson(Map<String, dynamic> 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)' : ''}';
}

385
sdk/go/ibc/client.go Normal file
View file

@ -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<<attempt) * 100 * time.Millisecond)
continue
}
defer resp.Body.Close()
if resp.StatusCode >= 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[:])
}

407
sdk/go/ibc/types.go Normal file
View file

@ -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
}

View file

@ -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<Map<String, Object>> getChainInfo() {
return get("/chain");
}
public CompletableFuture<Height> getHeight() {
return get("/chain/height").thenApply(Height::fromJson);
}
public CompletableFuture<Boolean> 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<Map<String, Object>> 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<Map<String, Object>> post(String path, Map<String, Object> 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<String, Object> handleResponse(HttpResponse<String> response) {
if (response.statusCode() >= 400) {
Map<String, Object> error = null;
try {
error = gson.fromJson(response.body(),
new TypeToken<Map<String, Object>>(){}.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<Map<String, Object>>(){}.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<ClientId> create(ClientType clientType,
ClientState clientState,
Map<String, Object> consensusState) {
Map<String, Object> 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<ClientState> getState(ClientId clientId) {
return ibc.get("/clients/" + clientId.getId() + "/state")
.thenApply(ClientState::fromJson);
}
@SuppressWarnings("unchecked")
public CompletableFuture<List<Map<String, Object>>> list() {
return ibc.get("/clients")
.thenApply(result -> (List<Map<String, Object>>) result.getOrDefault("clients", List.of()));
}
}
/**
* Connections sub-client
*/
public static class ConnectionsClient {
private final SynorIbc ibc;
ConnectionsClient(SynorIbc ibc) {
this.ibc = ibc;
}
public CompletableFuture<ConnectionId> openInit(ClientId clientId,
ClientId counterpartyClientId) {
Map<String, Object> 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<Map<String, Object>> get(ConnectionId connectionId) {
return ibc.get("/connections/" + connectionId.getId());
}
@SuppressWarnings("unchecked")
public CompletableFuture<List<Map<String, Object>>> list() {
return ibc.get("/connections")
.thenApply(result -> (List<Map<String, Object>>) result.getOrDefault("connections", List.of()));
}
}
/**
* Channels sub-client
*/
public static class ChannelsClient {
private final SynorIbc ibc;
ChannelsClient(SynorIbc ibc) {
this.ibc = ibc;
}
public CompletableFuture<Void> bindPort(PortId portId, String module) {
Map<String, Object> body = new HashMap<>();
body.put("port_id", portId.getId());
body.put("module", module);
return ibc.post("/ports/bind", body).thenApply(r -> null);
}
public CompletableFuture<ChannelId> openInit(PortId portId,
ChannelOrder ordering,
ConnectionId connectionId,
PortId counterpartyPort,
String version) {
Map<String, Object> 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<Map<String, Object>> get(PortId portId, ChannelId channelId) {
return ibc.get("/channels/" + portId.getId() + "/" + channelId.getId());
}
@SuppressWarnings("unchecked")
public CompletableFuture<List<Map<String, Object>>> list() {
return ibc.get("/channels")
.thenApply(result -> (List<Map<String, Object>>) 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<Map<String, Object>> transfer(String sourcePort,
String sourceChannel,
String denom,
String amount,
String sender,
String receiver,
Timeout timeout,
String memo) {
Map<String, Object> 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<Map<String, Object>> 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<Map<String, Object>> initiate(String responder,
Map<String, Object> initiatorAsset,
Map<String, Object> responderAsset) {
Map<String, Object> 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<Void> lock(SwapId swapId) {
return ibc.post("/swaps/" + swapId.getId() + "/lock", Map.of())
.thenApply(r -> null);
}
public CompletableFuture<Map<String, Object>> respond(SwapId swapId,
Map<String, Object> asset) {
return ibc.post("/swaps/" + swapId.getId() + "/respond", Map.of("asset", asset));
}
public CompletableFuture<Map<String, Object>> claim(SwapId swapId, byte[] secret) {
return ibc.post("/swaps/" + swapId.getId() + "/claim",
Map.of("secret", Base64.getEncoder().encodeToString(secret)));
}
public CompletableFuture<Map<String, Object>> refund(SwapId swapId) {
return ibc.post("/swaps/" + swapId.getId() + "/refund", Map.of());
}
public CompletableFuture<AtomicSwap> get(SwapId swapId) {
return ibc.get("/swaps/" + swapId.getId())
.thenApply(AtomicSwap::fromJson);
}
@SuppressWarnings("unchecked")
public CompletableFuture<List<AtomicSwap>> listActive() {
return ibc.get("/swaps/active")
.thenApply(result -> {
List<Map<String, Object>> swaps =
(List<Map<String, Object>>) result.getOrDefault("swaps", List.of());
return swaps.stream().map(AtomicSwap::fromJson).toList();
});
}
}
}

View file

@ -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<String, Object> json) {
return new Height(
((Number) json.getOrDefault("revision_number", 0)).longValue(),
((Number) json.getOrDefault("revision_height", 1)).longValue()
);
}
public Map<String, Object> 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<String, Object> json) {
return new ChainId((String) json.get("id"));
}
public Map<String, Object> toJson() {
return Map.of("id", id);
}
}
/**
* IBC Version with features
*/
public static class Version {
private final String identifier;
private final List<String> features;
public Version(String identifier, List<String> features) {
this.identifier = identifier;
this.features = features;
}
public String getIdentifier() { return identifier; }
public List<String> getFeatures() { return features; }
public static Version defaultConnection() {
return new Version("1", List.of("ORDER_ORDERED", "ORDER_UNORDERED"));
}
@SuppressWarnings("unchecked")
public static Version fromJson(Map<String, Object> json) {
return new Version(
(String) json.get("identifier"),
(List<String>) json.get("features")
);
}
public Map<String, Object> 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<String, Object> json) {
return new ClientId((String) json.get("id"));
}
public Map<String, Object> 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<String, Object> json) {
return new TrustLevel(
((Number) json.getOrDefault("numerator", 1)).intValue(),
((Number) json.getOrDefault("denominator", 3)).intValue()
);
}
public Map<String, Object> 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<String, Object> json) {
return new ClientState(
(String) json.get("chain_id"),
TrustLevel.fromJson((Map<String, Object>) 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<String, Object>) json.get("latest_height")),
json.get("frozen_height") != null
? Height.fromJson((Map<String, Object>) json.get("frozen_height"))
: null
);
}
public Map<String, Object> toJson() {
var map = new java.util.HashMap<String, Object>();
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<String, Object> json) {
return new ConnectionId((String) json.get("id"));
}
public Map<String, Object> 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<String, Object> json) {
return new PortId((String) json.get("id"));
}
public Map<String, Object> 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<String, Object> json) {
return new ChannelId((String) json.get("id"));
}
public Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> json) {
return new SwapId((String) json.get("id"));
}
public Map<String, Object> 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<String, Object> json) {
List<Number> hashList = (List<Number>) 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<String, Object> 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<String, Object> json) {
return new Timelock(new BigInteger(json.get("expiry").toString()));
}
public Map<String, Object> toJson() {
return Map.of("expiry", expiry.toString());
}
}
/**
* Atomic swap
*/
public static class AtomicSwap {
private final SwapId swapId;
private final SwapState state;
private final Map<String, Object> initiatorHtlc;
private final Map<String, Object> responderHtlc;
public AtomicSwap(SwapId swapId, SwapState state,
Map<String, Object> initiatorHtlc,
Map<String, Object> 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<String, Object> getInitiatorHtlc() { return initiatorHtlc; }
public Map<String, Object> getResponderHtlc() { return responderHtlc; }
@SuppressWarnings("unchecked")
public static AtomicSwap fromJson(Map<String, Object> json) {
return new AtomicSwap(
SwapId.fromJson((Map<String, Object>) json.get("swap_id")),
SwapState.valueOf(((String) json.get("state")).toUpperCase()),
(Map<String, Object>) json.get("initiator_htlc"),
(Map<String, Object>) 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 + ")" : "");
}
}
}

760
sdk/js/src/ibc/client.ts Normal file
View file

@ -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<ClientId> {
return this.ibc.post('/clients', params);
}
/** Update a light client with new header */
async update(params: UpdateClientParams): Promise<Height> {
return this.ibc.post(`/clients/${params.clientId.id}/update`, {
header: params.header,
});
}
/** Get client state */
async getState(clientId: ClientId): Promise<ClientState> {
return this.ibc.get(`/clients/${clientId.id}/state`);
}
/** Get consensus state at height */
async getConsensusState(clientId: ClientId, height: Height): Promise<ConsensusState> {
return this.ibc.get(
`/clients/${clientId.id}/consensus/${height.revisionNumber}-${height.revisionHeight}`
);
}
/** List all clients */
async list(): Promise<Array<{ clientId: ClientId; clientState: ClientState }>> {
return this.ibc.get('/clients');
}
/** Check if client is active */
async isActive(clientId: ClientId): Promise<boolean> {
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<ConnectionId> {
return this.ibc.post('/connections/init', params);
}
/** Try connection handshake (counterparty) */
async openTry(params: ConnOpenTryParams): Promise<ConnectionId> {
return this.ibc.post('/connections/try', params);
}
/** Acknowledge connection handshake */
async openAck(
connectionId: ConnectionId,
counterpartyConnectionId: ConnectionId,
version: Version,
proofTry: Uint8Array,
proofHeight: Height
): Promise<void> {
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<void> {
return this.ibc.post(`/connections/${connectionId.id}/confirm`, {
proofAck: Buffer.from(proofAck).toString('base64'),
proofHeight,
});
}
/** Get connection by ID */
async get(connectionId: ConnectionId): Promise<ConnectionEnd> {
return this.ibc.get(`/connections/${connectionId.id}`);
}
/** List all connections */
async list(): Promise<Array<{ connectionId: ConnectionId; connection: ConnectionEnd }>> {
return this.ibc.get('/connections');
}
/** Get connections for a client */
async getByClient(clientId: ClientId): Promise<ConnectionId[]> {
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<void> {
return this.ibc.post('/ports/bind', { portId, module });
}
/** Release port binding */
async releasePort(portId: PortId): Promise<void> {
return this.ibc.post(`/ports/${portId.id}/release`, {});
}
/** Initialize channel handshake */
async openInit(params: ChanOpenInitParams): Promise<ChannelId> {
return this.ibc.post('/channels/init', params);
}
/** Try channel handshake (counterparty) */
async openTry(params: ChanOpenTryParams): Promise<ChannelId> {
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<void> {
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<void> {
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<void> {
return this.ibc.post(`/channels/${portId.id}/${channelId.id}/close`, {});
}
/** Close channel confirm */
async closeConfirm(
portId: PortId,
channelId: ChannelId,
proofInit: Uint8Array,
proofHeight: Height
): Promise<void> {
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<Channel> {
return this.ibc.get(`/channels/${portId.id}/${channelId.id}`);
}
/** List channels for a port */
async listByPort(portId: PortId): Promise<Array<{ channelId: ChannelId; channel: Channel }>> {
return this.ibc.get(`/ports/${portId.id}/channels`);
}
/** List all channels */
async list(): Promise<Array<{ portId: PortId; channelId: ChannelId; channel: Channel }>> {
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<Acknowledgement> {
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<void> {
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<void> {
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<Uint8Array | null> {
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<Acknowledgement | null> {
return this.ibc.get(`/packets/${portId.id}/${channelId.id}/${sequence}/ack`);
}
/** List unreceived packets */
async listUnreceived(
portId: PortId,
channelId: ChannelId,
sequences: bigint[]
): Promise<bigint[]> {
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<bigint[]> {
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<Array<{ path: string; baseDenom: string }>> {
return this.ibc.get('/transfer/denom_traces');
}
/** Get escrow address for a channel */
async getEscrowAddress(portId: PortId, channelId: ChannelId): Promise<string> {
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<string> {
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<void> {
return this.ibc.post(`/swaps/${swapId.id}/lock`, {});
}
/** Respond to a swap (lock responder's tokens) */
async respond(params: RespondSwapParams): Promise<Htlc> {
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<void> {
return this.ibc.post(`/swaps/${swapId.id}/cancel`, {});
}
/** Get swap by ID */
async get(swapId: SwapId): Promise<AtomicSwap> {
return this.ibc.get(`/swaps/${swapId.id}`);
}
/** Get HTLC by ID */
async getHtlc(swapId: SwapId): Promise<Htlc> {
return this.ibc.get(`/swaps/${swapId.id}/htlc`);
}
/** List active swaps */
async listActive(): Promise<AtomicSwap[]> {
return this.ibc.get('/swaps/active');
}
/** List swaps by participant */
async listByParticipant(address: string): Promise<AtomicSwap[]> {
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<IbcConfig>;
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<Height> {
return this.get('/chain/height');
}
// =========================================================================
// Proofs
// =========================================================================
/** Get client state proof */
async getClientStateProof(clientId: ClientId): Promise<CommitmentProof> {
return this.get(`/proofs/client_state/${clientId.id}`);
}
/** Get consensus state proof */
async getConsensusStateProof(clientId: ClientId, height: Height): Promise<CommitmentProof> {
return this.get(
`/proofs/consensus_state/${clientId.id}/${height.revisionNumber}-${height.revisionHeight}`
);
}
/** Get connection proof */
async getConnectionProof(connectionId: ConnectionId): Promise<CommitmentProof> {
return this.get(`/proofs/connection/${connectionId.id}`);
}
/** Get channel proof */
async getChannelProof(portId: PortId, channelId: ChannelId): Promise<CommitmentProof> {
return this.get(`/proofs/channel/${portId.id}/${channelId.id}`);
}
/** Get packet commitment proof */
async getPacketCommitmentProof(
portId: PortId,
channelId: ChannelId,
sequence: bigint
): Promise<CommitmentProof> {
return this.get(`/proofs/packet_commitment/${portId.id}/${channelId.id}/${sequence}`);
}
/** Get packet acknowledgement proof */
async getPacketAckProof(
portId: PortId,
channelId: ChannelId,
sequence: bigint
): Promise<CommitmentProof> {
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<boolean> {
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<T>(path: string): Promise<T> {
return this.request<T>('GET', path);
}
async post<T>(path: string, body: unknown): Promise<T> {
return this.request<T>('POST', path, body);
}
async delete<T>(path: string): Promise<T> {
return this.request<T>('DELETE', path);
}
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
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;

39
sdk/js/src/ibc/index.ts Normal file
View file

@ -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';

559
sdk/js/src/ibc/types.ts Normal file
View file

@ -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';
}
}

View file

@ -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<String, Any> = 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<String, Any> {
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<Map<String, Any>>() } 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<String, Any>): Map<String, Any> {
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<Map<String, Any>>() } 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<String, Any>
): 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<String, Any>
val latestHeight = result["latest_height"] as Map<String, Any>
val frozenHeight = result["frozen_height"] as? Map<String, Any>
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<Map<String, Any>> {
val result = ibc.get("/clients")
return result["clients"] as? List<Map<String, Any>> ?: 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<String, Any> =
ibc.get("/connections/${connectionId.id}")
@Suppress("UNCHECKED_CAST")
suspend fun list(): List<Map<String, Any>> {
val result = ibc.get("/connections")
return result["connections"] as? List<Map<String, Any>> ?: 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<String, Any> =
ibc.get("/channels/${portId.id}/${channelId.id}")
@Suppress("UNCHECKED_CAST")
suspend fun list(): List<Map<String, Any>> {
val result = ibc.get("/channels")
return result["channels"] as? List<Map<String, Any>> ?: 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<String, Any> {
val body = mutableMapOf<String, Any>(
"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<String, Any> =
ibc.get("/transfer/denom_trace/$ibcDenom")
}
/**
* Swaps sub-client (HTLC)
*/
class SwapsClient(private val ibc: SynorIbc) {
suspend fun initiate(
responder: String,
initiatorAsset: Map<String, Any>,
responderAsset: Map<String, Any>
): Map<String, Any> = 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<String, Any>): Map<String, Any> =
ibc.post("/swaps/${swapId.id}/respond", mapOf("asset" to asset))
suspend fun claim(swapId: SwapId, secret: ByteArray): Map<String, Any> =
ibc.post("/swaps/${swapId.id}/claim", mapOf(
"secret" to Base64.getEncoder().encodeToString(secret)
))
suspend fun refund(swapId: SwapId): Map<String, Any> =
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<AtomicSwap> {
val result = ibc.get("/swaps/active")
val swaps = result["swaps"] as? List<Map<String, Any>> ?: emptyList()
return swaps.map { AtomicSwap.fromJson(it) }
}
}
}

View file

@ -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<String>
) {
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<String, Any> = 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<String, Any> = 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<String, Any> = 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<String, Any>,
val responderHtlc: Map<String, Any>?
) {
companion object {
@Suppress("UNCHECKED_CAST")
fun fromJson(json: Map<String, Any>): AtomicSwap {
val swapIdMap = json["swap_id"] as Map<String, Any>
return AtomicSwap(
swapId = SwapId(swapIdMap["id"] as String),
state = SwapState.valueOf((json["state"] as String).uppercase()),
initiatorHtlc = json["initiator_htlc"] as Map<String, Any>,
responderHtlc = json["responder_htlc"] as? Map<String, Any>
)
}
}
}
/**
* 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)" } ?: ""}"
}

View file

@ -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"

View file

@ -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

View file

@ -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

470
sdk/ruby/lib/synor/ibc.rb Normal file
View file

@ -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

363
sdk/rust/src/ibc/client.rs Normal file
View file

@ -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<AtomicBool>,
}
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<serde_json::Value> {
self.get("/chain").await
}
/// Get current height
pub async fn get_height(&self) -> IbcResult<Height> {
self.get("/chain/height").await
}
/// Health check
pub async fn health_check(&self) -> bool {
match self.get::<serde_json::Value>("/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<ClientId> {
#[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<ClientState> {
self.get(&format!("/clients/{}/state", client_id.0)).await
}
/// List all clients
pub async fn list_clients(&self) -> IbcResult<Vec<serde_json::Value>> {
self.get("/clients").await
}
// ========================================================================
// Connection Operations
// ========================================================================
/// Initialize connection handshake
pub async fn connection_open_init(
&self,
client_id: &ClientId,
counterparty_client_id: &ClientId,
) -> IbcResult<ConnectionId> {
#[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<ConnectionEnd> {
self.get(&format!("/connections/{}", connection_id.0)).await
}
/// List all connections
pub async fn list_connections(&self) -> IbcResult<Vec<serde_json::Value>> {
self.get("/connections").await
}
// ========================================================================
// Channel Operations
// ========================================================================
/// Bind a port
pub async fn bind_port(&self, port_id: &PortId, module: &str) -> IbcResult<()> {
self.post::<serde_json::Value, _>("/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<ChannelId> {
#[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<Channel> {
self.get(&format!("/channels/{}/{}", port_id.0, channel_id.0)).await
}
/// List all channels
pub async fn list_channels(&self) -> IbcResult<Vec<serde_json::Value>> {
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<serde_json::Value> {
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<Timeout>,
memo: Option<&str>,
) -> IbcResult<serde_json::Value> {
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<serde_json::Value> {
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<serde_json::Value> {
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::<serde_json::Value, _>(&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<Htlc> {
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<serde_json::Value> {
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<serde_json::Value> {
self.post(&format!("/swaps/{}/refund", swap_id.0), &()).await
}
/// Get swap by ID
pub async fn get_swap(&self, swap_id: &SwapId) -> IbcResult<AtomicSwap> {
self.get(&format!("/swaps/{}", swap_id.0)).await
}
/// List active swaps
pub async fn list_active_swaps(&self) -> IbcResult<Vec<AtomicSwap>> {
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<T: DeserializeOwned>(&self, path: &str) -> IbcResult<T> {
self.request("GET", path, Option::<&()>::None).await
}
async fn post<T: DeserializeOwned, B: Serialize>(&self, path: &str, body: &B) -> IbcResult<T> {
self.request("POST", path, Some(body)).await
}
async fn request<T: DeserializeOwned, B: Serialize>(
&self,
method: &str,
path: &str,
body: Option<&B>,
) -> IbcResult<T> {
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,
})
}
}

9
sdk/rust/src/ibc/mod.rs Normal file
View file

@ -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::*;

492
sdk/rust/src/ibc/types.rs Normal file
View file

@ -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<String>) -> Self {
Self(id.into())
}
}
/// IBC Version with features
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Version {
pub identifier: String,
pub features: Vec<String>,
}
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<u8>,
}
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<Height>,
}
/// Consensus state at a specific height
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsensusState {
pub timestamp: Timestamp,
#[serde(with = "base64_bytes")]
pub root: Vec<u8>,
#[serde(with = "base64_bytes")]
pub next_validators_hash: Vec<u8>,
}
/// 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<ConnectionId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<CommitmentPrefix>,
}
/// Connection end
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionEnd {
pub client_id: ClientId,
pub versions: Vec<Version>,
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<ChannelId>,
}
/// Channel end
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Channel {
pub state: ChannelState,
pub ordering: ChannelOrder,
pub counterparty: ChannelCounterparty,
pub connection_hops: Vec<ConnectionId>,
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<u8>,
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<u8>);
/// Packet acknowledgement
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Acknowledgement {
Success { success: Vec<u8> },
Error { error: String },
}
impl Acknowledgement {
pub fn success(data: Vec<u8>) -> Self {
Acknowledgement::Success { success: data }
}
pub fn error(msg: impl Into<String>) -> 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<String>,
}
/// Hashlock - hash of the secret
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Hashlock(#[serde(with = "base64_bytes")] pub Vec<u8>);
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<Vec<u8>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel_id: Option<ChannelId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub port_id: Option<PortId>,
pub created_at: Timestamp,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<Timestamp>,
}
/// 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<Htlc>,
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<ProofOp>,
}
/// Proof operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProofOp {
pub r#type: String,
#[serde(with = "base64_bytes")]
pub key: Vec<u8>,
#[serde(with = "base64_bytes")]
pub data: Vec<u8>,
}
/// 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<String>) -> 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<String>,
pub status: Option<u16>,
}
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<T> = Result<T, IbcError>;
/// Base64 bytes serde helper
mod base64_bytes {
use base64::{Engine as _, engine::general_purpose::STANDARD};
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
serializer.serialize_str(&STANDARD.encode(bytes))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where D: Deserializer<'de> {
let s = String::deserialize(deserializer)?;
STANDARD.decode(&s).map_err(serde::de::Error::custom)
}
}

View file

@ -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) }
}
}
}

View file

@ -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))" } ?? "")"
}
}