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
760 lines
21 KiB
TypeScript
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;
|