/** * Synor IBC SDK Client * * Inter-Blockchain Communication (IBC) client for cross-chain interoperability. * Supports light client verification, connections, channels, packets, transfers, and atomic swaps. */ import type { IbcConfig, Height, ClientId, ClientState, ConsensusState, Header, ConnectionId, ConnectionEnd, PortId, ChannelId, Channel, ChannelOrder, Packet, Acknowledgement, Timeout, SwapId, SwapAsset, SwapState, AtomicSwap, Htlc, Hashlock, IbcEvent, CreateClientParams, UpdateClientParams, ConnOpenInitParams, ConnOpenTryParams, ChanOpenInitParams, ChanOpenTryParams, SendPacketParams, TransferParams, InitiateSwapParams, RespondSwapParams, ClaimSwapParams, FungibleTokenPacketData, CommitmentProof, Version, } from './types'; import { IbcException } from './types'; const DEFAULT_ENDPOINT = 'https://ibc.synor.io/v1'; const DEFAULT_WS_ENDPOINT = 'wss://ibc.synor.io/v1/ws'; const DEFAULT_TIMEOUT = 30000; const DEFAULT_RETRIES = 3; const DEFAULT_CHAIN_ID = 'synor-1'; /** Subscription handle */ export interface Subscription { unsubscribe(): void; } /** * IBC Light Client sub-client */ export class LightClientClient { constructor(private ibc: SynorIbc) {} /** Create a new light client */ async create(params: CreateClientParams): Promise { return this.ibc.post('/clients', params); } /** Update a light client with new header */ async update(params: UpdateClientParams): Promise { return this.ibc.post(`/clients/${params.clientId.id}/update`, { header: params.header, }); } /** Get client state */ async getState(clientId: ClientId): Promise { return this.ibc.get(`/clients/${clientId.id}/state`); } /** Get consensus state at height */ async getConsensusState(clientId: ClientId, height: Height): Promise { return this.ibc.get( `/clients/${clientId.id}/consensus/${height.revisionNumber}-${height.revisionHeight}` ); } /** List all clients */ async list(): Promise> { return this.ibc.get('/clients'); } /** Check if client is active */ async isActive(clientId: ClientId): Promise { const response = await this.ibc.get<{ active: boolean }>(`/clients/${clientId.id}/status`); return response.active; } } /** * IBC Connections sub-client */ export class ConnectionsClient { constructor(private ibc: SynorIbc) {} /** Initialize connection handshake */ async openInit(params: ConnOpenInitParams): Promise { return this.ibc.post('/connections/init', params); } /** Try connection handshake (counterparty) */ async openTry(params: ConnOpenTryParams): Promise { return this.ibc.post('/connections/try', params); } /** Acknowledge connection handshake */ async openAck( connectionId: ConnectionId, counterpartyConnectionId: ConnectionId, version: Version, proofTry: Uint8Array, proofHeight: Height ): Promise { return this.ibc.post(`/connections/${connectionId.id}/ack`, { counterpartyConnectionId, version, proofTry: Buffer.from(proofTry).toString('base64'), proofHeight, }); } /** Confirm connection handshake */ async openConfirm( connectionId: ConnectionId, proofAck: Uint8Array, proofHeight: Height ): Promise { return this.ibc.post(`/connections/${connectionId.id}/confirm`, { proofAck: Buffer.from(proofAck).toString('base64'), proofHeight, }); } /** Get connection by ID */ async get(connectionId: ConnectionId): Promise { return this.ibc.get(`/connections/${connectionId.id}`); } /** List all connections */ async list(): Promise> { return this.ibc.get('/connections'); } /** Get connections for a client */ async getByClient(clientId: ClientId): Promise { return this.ibc.get(`/clients/${clientId.id}/connections`); } } /** * IBC Channels sub-client */ export class ChannelsClient { constructor(private ibc: SynorIbc) {} /** Bind a port */ async bindPort(portId: PortId, module: string): Promise { return this.ibc.post('/ports/bind', { portId, module }); } /** Release port binding */ async releasePort(portId: PortId): Promise { return this.ibc.post(`/ports/${portId.id}/release`, {}); } /** Initialize channel handshake */ async openInit(params: ChanOpenInitParams): Promise { return this.ibc.post('/channels/init', params); } /** Try channel handshake (counterparty) */ async openTry(params: ChanOpenTryParams): Promise { return this.ibc.post('/channels/try', params); } /** Acknowledge channel handshake */ async openAck( portId: PortId, channelId: ChannelId, counterpartyChannelId: ChannelId, counterpartyVersion: string, proofTry: Uint8Array, proofHeight: Height ): Promise { return this.ibc.post(`/channels/${portId.id}/${channelId.id}/ack`, { counterpartyChannelId, counterpartyVersion, proofTry: Buffer.from(proofTry).toString('base64'), proofHeight, }); } /** Confirm channel handshake */ async openConfirm( portId: PortId, channelId: ChannelId, proofAck: Uint8Array, proofHeight: Height ): Promise { return this.ibc.post(`/channels/${portId.id}/${channelId.id}/confirm`, { proofAck: Buffer.from(proofAck).toString('base64'), proofHeight, }); } /** Close channel init */ async closeInit(portId: PortId, channelId: ChannelId): Promise { return this.ibc.post(`/channels/${portId.id}/${channelId.id}/close`, {}); } /** Close channel confirm */ async closeConfirm( portId: PortId, channelId: ChannelId, proofInit: Uint8Array, proofHeight: Height ): Promise { return this.ibc.post(`/channels/${portId.id}/${channelId.id}/close/confirm`, { proofInit: Buffer.from(proofInit).toString('base64'), proofHeight, }); } /** Get channel */ async get(portId: PortId, channelId: ChannelId): Promise { return this.ibc.get(`/channels/${portId.id}/${channelId.id}`); } /** List channels for a port */ async listByPort(portId: PortId): Promise> { return this.ibc.get(`/ports/${portId.id}/channels`); } /** List all channels */ async list(): Promise> { return this.ibc.get('/channels'); } } /** * IBC Packets sub-client */ export class PacketsClient { constructor(private ibc: SynorIbc) {} /** Send a packet */ async send(params: SendPacketParams): Promise<{ sequence: bigint; packet: Packet }> { return this.ibc.post('/packets/send', { ...params, data: Buffer.from(params.data).toString('base64'), }); } /** Receive a packet (called by relayer) */ async recv( packet: Packet, proof: Uint8Array, proofHeight: Height ): Promise { return this.ibc.post('/packets/recv', { packet: { ...packet, data: Buffer.from(packet.data).toString('base64'), }, proof: Buffer.from(proof).toString('base64'), proofHeight, }); } /** Acknowledge a packet */ async ack( packet: Packet, acknowledgement: Acknowledgement, proof: Uint8Array, proofHeight: Height ): Promise { return this.ibc.post('/packets/ack', { packet: { ...packet, data: Buffer.from(packet.data).toString('base64'), }, acknowledgement, proof: Buffer.from(proof).toString('base64'), proofHeight, }); } /** Timeout a packet */ async timeout( packet: Packet, proof: Uint8Array, proofHeight: Height, nextSequenceRecv: bigint ): Promise { return this.ibc.post('/packets/timeout', { packet: { ...packet, data: Buffer.from(packet.data).toString('base64'), }, proof: Buffer.from(proof).toString('base64'), proofHeight, nextSequenceRecv: nextSequenceRecv.toString(), }); } /** Get packet commitment */ async getCommitment( portId: PortId, channelId: ChannelId, sequence: bigint ): Promise { const response = await this.ibc.get<{ commitment: string | null }>( `/packets/${portId.id}/${channelId.id}/${sequence}/commitment` ); return response.commitment ? Buffer.from(response.commitment, 'base64') : null; } /** Get packet acknowledgement */ async getAcknowledgement( portId: PortId, channelId: ChannelId, sequence: bigint ): Promise { return this.ibc.get(`/packets/${portId.id}/${channelId.id}/${sequence}/ack`); } /** List unreceived packets */ async listUnreceived( portId: PortId, channelId: ChannelId, sequences: bigint[] ): Promise { return this.ibc.post(`/packets/${portId.id}/${channelId.id}/unreceived`, { sequences: sequences.map(s => s.toString()), }); } /** List unacknowledged packets */ async listUnacknowledged( portId: PortId, channelId: ChannelId, sequences: bigint[] ): Promise { return this.ibc.post(`/packets/${portId.id}/${channelId.id}/unacked`, { sequences: sequences.map(s => s.toString()), }); } } /** * IBC Transfer sub-client (ICS-20) */ export class TransferClient { constructor(private ibc: SynorIbc) {} /** Transfer tokens to another chain */ async transfer(params: TransferParams): Promise<{ sequence: bigint; txHash: string }> { return this.ibc.post('/transfer', params); } /** Get denom trace for an IBC token */ async getDenomTrace(ibcDenom: string): Promise<{ path: string; baseDenom: string }> { return this.ibc.get(`/transfer/denom_trace/${ibcDenom}`); } /** Get all denom traces */ async listDenomTraces(): Promise> { return this.ibc.get('/transfer/denom_traces'); } /** Get escrow address for a channel */ async getEscrowAddress(portId: PortId, channelId: ChannelId): Promise { const response = await this.ibc.get<{ address: string }>( `/transfer/escrow/${portId.id}/${channelId.id}` ); return response.address; } /** Get total escrow for a denom */ async getTotalEscrow(denom: string): Promise { const response = await this.ibc.get<{ amount: string }>( `/transfer/escrow/total/${encodeURIComponent(denom)}` ); return response.amount; } } /** * IBC Atomic Swap sub-client (HTLC) */ export class SwapsClient { constructor(private ibc: SynorIbc) {} /** Initiate an atomic swap */ async initiate(params: InitiateSwapParams): Promise<{ swapId: SwapId; hashlock: Hashlock }> { return this.ibc.post('/swaps/initiate', params); } /** Lock initiator's tokens */ async lock(swapId: SwapId): Promise { return this.ibc.post(`/swaps/${swapId.id}/lock`, {}); } /** Respond to a swap (lock responder's tokens) */ async respond(params: RespondSwapParams): Promise { return this.ibc.post(`/swaps/${params.swapId.id}/respond`, { asset: params.asset }); } /** Claim tokens with secret */ async claim(params: ClaimSwapParams): Promise<{ txHash: string; revealedSecret: Uint8Array }> { return this.ibc.post(`/swaps/${params.swapId.id}/claim`, { secret: Buffer.from(params.secret).toString('base64'), }); } /** Refund expired swap */ async refund(swapId: SwapId): Promise<{ txHash: string }> { return this.ibc.post(`/swaps/${swapId.id}/refund`, {}); } /** Cancel pending swap */ async cancel(swapId: SwapId): Promise { return this.ibc.post(`/swaps/${swapId.id}/cancel`, {}); } /** Get swap by ID */ async get(swapId: SwapId): Promise { return this.ibc.get(`/swaps/${swapId.id}`); } /** Get HTLC by ID */ async getHtlc(swapId: SwapId): Promise { return this.ibc.get(`/swaps/${swapId.id}/htlc`); } /** List active swaps */ async listActive(): Promise { return this.ibc.get('/swaps/active'); } /** List swaps by participant */ async listByParticipant(address: string): Promise { return this.ibc.get(`/swaps/participant/${address}`); } /** Get swap status */ async getStatus(swapId: SwapId): Promise<{ state: SwapState; remainingSeconds: number; initiatorLocked: boolean; responderLocked: boolean; }> { return this.ibc.get(`/swaps/${swapId.id}/status`); } /** Verify hashlock with secret */ verifySecret(hashlock: Hashlock, secret: Uint8Array): boolean { // SHA256 hash verification const crypto = require('crypto'); const hash = crypto.createHash('sha256').update(secret).digest(); return Buffer.from(hashlock.hash).equals(hash); } } /** * Synor IBC SDK Client * * Main client for Inter-Blockchain Communication operations. */ export class SynorIbc { private readonly config: Required; private closed = false; private ws?: WebSocket; /** Light client operations */ readonly clients: LightClientClient; /** Connection operations */ readonly connections: ConnectionsClient; /** Channel operations */ readonly channels: ChannelsClient; /** Packet operations */ readonly packets: PacketsClient; /** Token transfer operations (ICS-20) */ readonly transfer: TransferClient; /** Atomic swap operations (HTLC) */ readonly swaps: SwapsClient; constructor(config: IbcConfig) { this.config = { apiKey: config.apiKey, endpoint: config.endpoint || DEFAULT_ENDPOINT, wsEndpoint: config.wsEndpoint || DEFAULT_WS_ENDPOINT, timeout: config.timeout || DEFAULT_TIMEOUT, retries: config.retries || DEFAULT_RETRIES, chainId: config.chainId || DEFAULT_CHAIN_ID, debug: config.debug || false, }; this.clients = new LightClientClient(this); this.connections = new ConnectionsClient(this); this.channels = new ChannelsClient(this); this.packets = new PacketsClient(this); this.transfer = new TransferClient(this); this.swaps = new SwapsClient(this); } // ========================================================================= // Chain Info // ========================================================================= /** Get chain ID */ get chainId(): string { return this.config.chainId; } /** Get chain info */ async getChainInfo(): Promise<{ chainId: string; height: Height; timestamp: bigint; ibcVersion: string; }> { return this.get('/chain'); } /** Get current height */ async getHeight(): Promise { return this.get('/chain/height'); } // ========================================================================= // Proofs // ========================================================================= /** Get client state proof */ async getClientStateProof(clientId: ClientId): Promise { return this.get(`/proofs/client_state/${clientId.id}`); } /** Get consensus state proof */ async getConsensusStateProof(clientId: ClientId, height: Height): Promise { return this.get( `/proofs/consensus_state/${clientId.id}/${height.revisionNumber}-${height.revisionHeight}` ); } /** Get connection proof */ async getConnectionProof(connectionId: ConnectionId): Promise { return this.get(`/proofs/connection/${connectionId.id}`); } /** Get channel proof */ async getChannelProof(portId: PortId, channelId: ChannelId): Promise { return this.get(`/proofs/channel/${portId.id}/${channelId.id}`); } /** Get packet commitment proof */ async getPacketCommitmentProof( portId: PortId, channelId: ChannelId, sequence: bigint ): Promise { return this.get(`/proofs/packet_commitment/${portId.id}/${channelId.id}/${sequence}`); } /** Get packet acknowledgement proof */ async getPacketAckProof( portId: PortId, channelId: ChannelId, sequence: bigint ): Promise { return this.get(`/proofs/packet_ack/${portId.id}/${channelId.id}/${sequence}`); } // ========================================================================= // WebSocket Subscriptions // ========================================================================= /** Subscribe to IBC events */ subscribeEvents(callback: (event: IbcEvent) => void): Subscription { return this.subscribe('events', callback); } /** Subscribe to specific client updates */ subscribeClient(clientId: ClientId, callback: (event: IbcEvent) => void): Subscription { return this.subscribe(`client/${clientId.id}`, callback); } /** Subscribe to channel events */ subscribeChannel( portId: PortId, channelId: ChannelId, callback: (event: IbcEvent) => void ): Subscription { return this.subscribe(`channel/${portId.id}/${channelId.id}`, callback); } /** Subscribe to swap updates */ subscribeSwap(swapId: SwapId, callback: (event: IbcEvent) => void): Subscription { return this.subscribe(`swap/${swapId.id}`, callback); } private subscribe(topic: string, callback: (event: IbcEvent) => void): Subscription { this.ensureWebSocket(); const message = JSON.stringify({ type: 'subscribe', topic }); this.ws!.send(message); const handler = (event: MessageEvent) => { try { const data = JSON.parse(event.data); if (data.topic === topic) { callback(data.event); } } catch (error) { if (this.config.debug) { console.error('IBC WebSocket message parse error:', error); } } }; this.ws!.addEventListener('message', handler); return { unsubscribe: () => { this.ws?.removeEventListener('message', handler); this.ws?.send(JSON.stringify({ type: 'unsubscribe', topic })); }, }; } private ensureWebSocket(): void { if (this.ws && this.ws.readyState === WebSocket.OPEN) { return; } this.ws = new WebSocket(this.config.wsEndpoint); this.ws.onopen = () => { this.ws!.send( JSON.stringify({ type: 'auth', apiKey: this.config.apiKey, }) ); }; this.ws.onerror = (error) => { if (this.config.debug) { console.error('IBC WebSocket error:', error); } }; } // ========================================================================= // Lifecycle // ========================================================================= /** Health check */ async healthCheck(): Promise { try { const response = await this.get<{ status: string }>('/health'); return response.status === 'healthy'; } catch { return false; } } /** Close the client */ close(): void { this.closed = true; if (this.ws) { this.ws.close(); this.ws = undefined; } } /** Check if client is closed */ get isClosed(): boolean { return this.closed; } // ========================================================================= // HTTP Methods (internal) // ========================================================================= async get(path: string): Promise { return this.request('GET', path); } async post(path: string, body: unknown): Promise { return this.request('POST', path, body); } async delete(path: string): Promise { return this.request('DELETE', path); } private async request(method: string, path: string, body?: unknown): Promise { if (this.closed) { throw new IbcException('Client has been closed', 'CLIENT_CLOSED'); } const url = `${this.config.endpoint}${path}`; let lastError: Error | undefined; for (let attempt = 0; attempt <= this.config.retries; attempt++) { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), this.config.timeout); const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.config.apiKey}`, 'X-SDK-Version': 'js/0.1.0', 'X-Chain-Id': this.config.chainId, }, body: body ? JSON.stringify(body) : undefined, signal: controller.signal, }); clearTimeout(timeout); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new IbcException( error.message || `HTTP ${response.status}`, error.code, response.status ); } return response.json(); } catch (error) { lastError = error as Error; if (error instanceof IbcException) { throw error; } if (attempt < this.config.retries) { await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 100)); } } } throw lastError || new IbcException('Request failed', 'NETWORK_ERROR'); } } export default SynorIbc;