synor/sdk/js/src/ibc/client.ts
Gulshan Yadav 97add23062 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
2026-01-28 12:53:46 +05:30

760 lines
21 KiB
TypeScript

/**
* 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;