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:
parent
e7dc8f70a0
commit
97add23062
24 changed files with 9167 additions and 0 deletions
313
sdk/c/include/synor/ibc.h
Normal file
313
sdk/c/include/synor/ibc.h
Normal 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 */
|
||||||
469
sdk/cpp/include/synor/ibc.hpp
Normal file
469
sdk/cpp/include/synor/ibc.hpp
Normal 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
|
||||||
407
sdk/csharp/src/Synor.Ibc/SynorIbc.cs
Normal file
407
sdk/csharp/src/Synor.Ibc/SynorIbc.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
279
sdk/csharp/src/Synor.Ibc/Types.cs
Normal file
279
sdk/csharp/src/Synor.Ibc/Types.cs
Normal 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})" : "")}";
|
||||||
|
}
|
||||||
|
}
|
||||||
278
sdk/flutter/lib/src/ibc/synor_ibc.dart
Normal file
278
sdk/flutter/lib/src/ibc/synor_ibc.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
359
sdk/flutter/lib/src/ibc/types.dart
Normal file
359
sdk/flutter/lib/src/ibc/types.dart
Normal 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
385
sdk/go/ibc/client.go
Normal 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
407
sdk/go/ibc/types.go
Normal 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
|
||||||
|
}
|
||||||
325
sdk/java/src/main/java/io/synor/ibc/SynorIbc.java
Normal file
325
sdk/java/src/main/java/io/synor/ibc/SynorIbc.java
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
637
sdk/java/src/main/java/io/synor/ibc/Types.java
Normal file
637
sdk/java/src/main/java/io/synor/ibc/Types.java
Normal 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
760
sdk/js/src/ibc/client.ts
Normal 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
39
sdk/js/src/ibc/index.ts
Normal 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
559
sdk/js/src/ibc/types.ts
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
310
sdk/kotlin/src/main/kotlin/io/synor/ibc/SynorIbc.kt
Normal file
310
sdk/kotlin/src/main/kotlin/io/synor/ibc/SynorIbc.kt
Normal 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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
265
sdk/kotlin/src/main/kotlin/io/synor/ibc/Types.kt
Normal file
265
sdk/kotlin/src/main/kotlin/io/synor/ibc/Types.kt
Normal 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)" } ?: ""}"
|
||||||
|
}
|
||||||
164
sdk/python/src/synor_ibc/__init__.py
Normal file
164
sdk/python/src/synor_ibc/__init__.py
Normal 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"
|
||||||
620
sdk/python/src/synor_ibc/client.py
Normal file
620
sdk/python/src/synor_ibc/client.py
Normal 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
|
||||||
532
sdk/python/src/synor_ibc/types.py
Normal file
532
sdk/python/src/synor_ibc/types.py
Normal 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
470
sdk/ruby/lib/synor/ibc.rb
Normal 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
363
sdk/rust/src/ibc/client.rs
Normal 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
9
sdk/rust/src/ibc/mod.rs
Normal 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
492
sdk/rust/src/ibc/types.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
342
sdk/swift/Sources/SynorIbc/SynorIbc.swift
Normal file
342
sdk/swift/Sources/SynorIbc/SynorIbc.swift
Normal 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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
383
sdk/swift/Sources/SynorIbc/Types.swift
Normal file
383
sdk/swift/Sources/SynorIbc/Types.swift
Normal 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))" } ?? "")"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue