@ctgexchange/sdk 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +129 -0
- package/dist/index.cjs +576 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +467 -0
- package/dist/index.d.ts +467 -0
- package/dist/index.js +529 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ApiError: () => ApiError,
|
|
24
|
+
AuthenticationError: () => AuthenticationError,
|
|
25
|
+
BadRequestError: () => BadRequestError,
|
|
26
|
+
Client: () => Client,
|
|
27
|
+
CtgExchangeError: () => CtgExchangeError,
|
|
28
|
+
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
29
|
+
DEFAULT_WS_BASE_URL: () => DEFAULT_WS_BASE_URL,
|
|
30
|
+
MarketDataStream: () => MarketDataStream,
|
|
31
|
+
NotFoundError: () => NotFoundError,
|
|
32
|
+
PermissionDeniedError: () => PermissionDeniedError,
|
|
33
|
+
RateLimitError: () => RateLimitError,
|
|
34
|
+
ServerError: () => ServerError,
|
|
35
|
+
UserStream: () => UserStream,
|
|
36
|
+
errorFromResponse: () => errorFromResponse,
|
|
37
|
+
parseFrame: () => parseFrame,
|
|
38
|
+
restCanonicalString: () => restCanonicalString,
|
|
39
|
+
restHeaders: () => restHeaders,
|
|
40
|
+
sha256Hex: () => sha256Hex,
|
|
41
|
+
signRest: () => signRest,
|
|
42
|
+
signWsAuth: () => signWsAuth,
|
|
43
|
+
wsAuthMessage: () => wsAuthMessage
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(index_exports);
|
|
46
|
+
|
|
47
|
+
// src/errors.ts
|
|
48
|
+
var CtgExchangeError = class extends Error {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = new.target.name;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var ApiError = class extends CtgExchangeError {
|
|
55
|
+
statusCode;
|
|
56
|
+
apiError;
|
|
57
|
+
apiMessage;
|
|
58
|
+
requestId;
|
|
59
|
+
constructor(statusCode, apiError, apiMessage, requestId) {
|
|
60
|
+
const detail = apiMessage ?? apiError ?? "request failed";
|
|
61
|
+
const suffix = requestId ? ` (request_id=${requestId})` : "";
|
|
62
|
+
super(`[${statusCode}] ${detail}${suffix}`);
|
|
63
|
+
this.statusCode = statusCode;
|
|
64
|
+
this.apiError = apiError;
|
|
65
|
+
this.apiMessage = apiMessage;
|
|
66
|
+
this.requestId = requestId;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var BadRequestError = class extends ApiError {
|
|
70
|
+
};
|
|
71
|
+
var AuthenticationError = class extends ApiError {
|
|
72
|
+
};
|
|
73
|
+
var PermissionDeniedError = class extends ApiError {
|
|
74
|
+
};
|
|
75
|
+
var NotFoundError = class extends ApiError {
|
|
76
|
+
};
|
|
77
|
+
var RateLimitError = class extends ApiError {
|
|
78
|
+
/** The `Retry-After` header value in seconds, when the server sent one. */
|
|
79
|
+
retryAfter;
|
|
80
|
+
constructor(statusCode, apiError, apiMessage, requestId, retryAfter) {
|
|
81
|
+
super(statusCode, apiError, apiMessage, requestId);
|
|
82
|
+
this.retryAfter = retryAfter;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var ServerError = class extends ApiError {
|
|
86
|
+
};
|
|
87
|
+
function errorFromResponse(statusCode, body, retryAfter) {
|
|
88
|
+
const { error, message, request_id: requestId } = body ?? {};
|
|
89
|
+
switch (statusCode) {
|
|
90
|
+
case 400:
|
|
91
|
+
return new BadRequestError(statusCode, error, message, requestId);
|
|
92
|
+
case 401:
|
|
93
|
+
return new AuthenticationError(statusCode, error, message, requestId);
|
|
94
|
+
case 403:
|
|
95
|
+
return new PermissionDeniedError(statusCode, error, message, requestId);
|
|
96
|
+
case 404:
|
|
97
|
+
return new NotFoundError(statusCode, error, message, requestId);
|
|
98
|
+
case 429:
|
|
99
|
+
return new RateLimitError(
|
|
100
|
+
statusCode,
|
|
101
|
+
error,
|
|
102
|
+
message,
|
|
103
|
+
requestId,
|
|
104
|
+
retryAfter
|
|
105
|
+
);
|
|
106
|
+
default:
|
|
107
|
+
return statusCode >= 500 ? new ServerError(statusCode, error, message, requestId) : new ApiError(statusCode, error, message, requestId);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/rest.ts
|
|
112
|
+
var import_node_crypto2 = require("crypto");
|
|
113
|
+
|
|
114
|
+
// src/signing.ts
|
|
115
|
+
var import_node_crypto = require("crypto");
|
|
116
|
+
function sha256Hex(body) {
|
|
117
|
+
return (0, import_node_crypto.createHash)("sha256").update(body, "utf8").digest("hex");
|
|
118
|
+
}
|
|
119
|
+
function restCanonicalString(ts, method, requestUri, body = "") {
|
|
120
|
+
return [String(ts), method.toUpperCase(), requestUri, sha256Hex(body)].join(
|
|
121
|
+
"\n"
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
function signRest(secret, ts, method, requestUri, body = "") {
|
|
125
|
+
return (0, import_node_crypto.createHmac)("sha256", secret).update(restCanonicalString(ts, method, requestUri, body)).digest("hex");
|
|
126
|
+
}
|
|
127
|
+
function restHeaders(keyId, secret, method, requestUri, body = "", ts = Math.floor(Date.now() / 1e3)) {
|
|
128
|
+
return {
|
|
129
|
+
"X-API-Key": keyId,
|
|
130
|
+
"X-Timestamp": String(ts),
|
|
131
|
+
"X-Signature": signRest(secret, ts, method, requestUri, body)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function signWsAuth(secret, ts) {
|
|
135
|
+
return (0, import_node_crypto.createHmac)("sha256", secret).update(`ws-auth
|
|
136
|
+
${ts}`).digest("hex");
|
|
137
|
+
}
|
|
138
|
+
function wsAuthMessage(keyId, secret, ts = Math.floor(Date.now() / 1e3)) {
|
|
139
|
+
return { op: "auth", args: [keyId, ts, signWsAuth(secret, ts)] };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/rest.ts
|
|
143
|
+
var DEFAULT_BASE_URL = "https://api.ctg.exchange";
|
|
144
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
145
|
+
function numToStr(value) {
|
|
146
|
+
return typeof value === "number" ? String(value) : value;
|
|
147
|
+
}
|
|
148
|
+
var Client = class {
|
|
149
|
+
apiKey;
|
|
150
|
+
apiSecret;
|
|
151
|
+
baseUrl;
|
|
152
|
+
timeoutMs;
|
|
153
|
+
maxRetries;
|
|
154
|
+
fetchImpl;
|
|
155
|
+
constructor(options = {}) {
|
|
156
|
+
this.apiKey = options.apiKey;
|
|
157
|
+
this.apiSecret = options.apiSecret;
|
|
158
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
159
|
+
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
160
|
+
this.maxRetries = options.maxRetries ?? 0;
|
|
161
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
162
|
+
if (!fetchImpl) {
|
|
163
|
+
throw new CtgExchangeError(
|
|
164
|
+
"no fetch implementation available; pass `fetch` in ClientOptions"
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
this.fetchImpl = fetchImpl;
|
|
168
|
+
}
|
|
169
|
+
buildRequestUri(path, query) {
|
|
170
|
+
if (!query) return path;
|
|
171
|
+
const params = new URLSearchParams();
|
|
172
|
+
for (const [key, value] of Object.entries(query)) {
|
|
173
|
+
if (value !== void 0) params.append(key, String(value));
|
|
174
|
+
}
|
|
175
|
+
const qs = params.toString();
|
|
176
|
+
return qs ? `${path}?${qs}` : path;
|
|
177
|
+
}
|
|
178
|
+
async request(method, path, options = {}) {
|
|
179
|
+
const { query, body, auth = false } = options;
|
|
180
|
+
const requestUri = this.buildRequestUri(path, query);
|
|
181
|
+
const bodyStr = body === void 0 ? "" : JSON.stringify(body);
|
|
182
|
+
for (let attempt = 0; ; attempt++) {
|
|
183
|
+
const headers = {};
|
|
184
|
+
if (bodyStr) headers["Content-Type"] = "application/json";
|
|
185
|
+
if (auth) {
|
|
186
|
+
if (!this.apiKey || !this.apiSecret) {
|
|
187
|
+
throw new CtgExchangeError(
|
|
188
|
+
"apiKey and apiSecret are required for private endpoints"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
Object.assign(
|
|
192
|
+
headers,
|
|
193
|
+
restHeaders(this.apiKey, this.apiSecret, method, requestUri, bodyStr)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
const response = await this.fetchImpl(this.baseUrl + requestUri, {
|
|
197
|
+
method,
|
|
198
|
+
headers,
|
|
199
|
+
body: bodyStr || void 0,
|
|
200
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
201
|
+
});
|
|
202
|
+
if (response.ok) {
|
|
203
|
+
const text = await response.text();
|
|
204
|
+
return text ? JSON.parse(text) : void 0;
|
|
205
|
+
}
|
|
206
|
+
const retryAfter = parseRetryAfter(response);
|
|
207
|
+
const error = errorFromResponse(
|
|
208
|
+
response.status,
|
|
209
|
+
await safeJson(response),
|
|
210
|
+
retryAfter
|
|
211
|
+
);
|
|
212
|
+
if (error instanceof RateLimitError && attempt < this.maxRetries) {
|
|
213
|
+
await sleep((retryAfter ?? 1) * 1e3);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// -- public market data ---------------------------------------------
|
|
220
|
+
/** All trading symbols with their order filters. */
|
|
221
|
+
getSymbols() {
|
|
222
|
+
return this.request("GET", "/api/v1/symbols");
|
|
223
|
+
}
|
|
224
|
+
/** 24h ticker for every symbol. */
|
|
225
|
+
getTickers() {
|
|
226
|
+
return this.request("GET", "/api/v1/tickers");
|
|
227
|
+
}
|
|
228
|
+
/** Current order book for `symbol`. */
|
|
229
|
+
getOrderBook(symbol) {
|
|
230
|
+
return this.request("GET", `/api/v1/${symbol}/orderbook`);
|
|
231
|
+
}
|
|
232
|
+
/** 24h ticker for `symbol`. */
|
|
233
|
+
getTicker(symbol) {
|
|
234
|
+
return this.request("GET", `/api/v1/${symbol}/ticker`);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Candles for `symbol` at `interval` (`1m` / `5m` / `15m` / `1h` /
|
|
238
|
+
* `4h` / `1d`).
|
|
239
|
+
*
|
|
240
|
+
* Omit `from` / `to` for the latest `limit` candles. Pass `from` /
|
|
241
|
+
* `to` (Unix ms) to select a historical window `[from, to)` for
|
|
242
|
+
* scrollback.
|
|
243
|
+
*/
|
|
244
|
+
getCandles(symbol, interval = "1m", opts = {}) {
|
|
245
|
+
return this.request("GET", `/api/v1/${symbol}/candles`, {
|
|
246
|
+
query: {
|
|
247
|
+
interval,
|
|
248
|
+
limit: opts.limit,
|
|
249
|
+
from: opts.from,
|
|
250
|
+
to: opts.to
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/** Recent public trade prints for `symbol`. */
|
|
255
|
+
getTrades(symbol, limit) {
|
|
256
|
+
return this.request("GET", `/api/v1/${symbol}/trades`, {
|
|
257
|
+
query: { limit }
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
// -- private: account -----------------------------------------------
|
|
261
|
+
/** Per-asset balances for the API key's owner (scope: read). */
|
|
262
|
+
getBalances() {
|
|
263
|
+
return this.request("GET", "/api/v1/me/balances", { auth: true });
|
|
264
|
+
}
|
|
265
|
+
/** Fee/rebate snapshot for the API key's owner (scope: read). */
|
|
266
|
+
getFees() {
|
|
267
|
+
return this.request("GET", "/api/v1/me/fees", { auth: true });
|
|
268
|
+
}
|
|
269
|
+
// -- private: orders ------------------------------------------------
|
|
270
|
+
/**
|
|
271
|
+
* Place an order (scope: trade).
|
|
272
|
+
*
|
|
273
|
+
* A `clientOrderId` is generated when you do not supply one — it lets
|
|
274
|
+
* the engine de-dup on safe retries.
|
|
275
|
+
*/
|
|
276
|
+
placeOrder(symbol, params) {
|
|
277
|
+
const body = {
|
|
278
|
+
client_order_id: params.clientOrderId ?? (0, import_node_crypto2.randomUUID)(),
|
|
279
|
+
side: params.side,
|
|
280
|
+
type: params.type
|
|
281
|
+
};
|
|
282
|
+
if (params.price !== void 0) body.price = numToStr(params.price);
|
|
283
|
+
if (params.qty !== void 0) body.qty = numToStr(params.qty);
|
|
284
|
+
return this.request("POST", `/api/v1/me/orders/${symbol}`, {
|
|
285
|
+
body,
|
|
286
|
+
auth: true
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/** List the owner's orders for `symbol` (scope: read). */
|
|
290
|
+
getOrders(symbol, opts = {}) {
|
|
291
|
+
return this.request("GET", `/api/v1/me/orders/${symbol}`, {
|
|
292
|
+
query: { status: opts.status, limit: opts.limit, offset: opts.offset },
|
|
293
|
+
auth: true
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* List every open order across all symbols (scope: read).
|
|
298
|
+
*
|
|
299
|
+
* Each `Order` carries its own `symbol`; decimal fields are converted
|
|
300
|
+
* per that order's scales. For closed-order history use `getOrders`
|
|
301
|
+
* per symbol.
|
|
302
|
+
*/
|
|
303
|
+
getOpenOrders() {
|
|
304
|
+
return this.request("GET", "/api/v1/me/orders/open", { auth: true });
|
|
305
|
+
}
|
|
306
|
+
/** Get one order by its canonical server id (scope: read). */
|
|
307
|
+
getOrder(symbol, orderId) {
|
|
308
|
+
return this.request("GET", `/api/v1/me/orders/${symbol}/${orderId}`, {
|
|
309
|
+
auth: true
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/** Cancel one order (scope: trade). */
|
|
313
|
+
cancelOrder(symbol, orderId) {
|
|
314
|
+
return this.request("DELETE", `/api/v1/me/orders/${symbol}/${orderId}`, {
|
|
315
|
+
auth: true
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/** Cancel every open order for `symbol` (scope: trade). */
|
|
319
|
+
async cancelAllOrders(symbol) {
|
|
320
|
+
const data = await this.request(
|
|
321
|
+
"DELETE",
|
|
322
|
+
`/api/v1/me/orders/${symbol}`,
|
|
323
|
+
{ auth: true }
|
|
324
|
+
);
|
|
325
|
+
return data?.orders ?? [];
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Modify a resting order's price and quantity (scope: trade).
|
|
329
|
+
*
|
|
330
|
+
* The API expects the full new state — pass both `newPrice` and
|
|
331
|
+
* `newQty`.
|
|
332
|
+
*/
|
|
333
|
+
async modifyOrder(symbol, orderId, params) {
|
|
334
|
+
const body = {};
|
|
335
|
+
if (params.newPrice !== void 0) {
|
|
336
|
+
body.new_price = numToStr(params.newPrice);
|
|
337
|
+
}
|
|
338
|
+
if (params.newQty !== void 0) body.new_qty = numToStr(params.newQty);
|
|
339
|
+
const data = await this.request(
|
|
340
|
+
"PATCH",
|
|
341
|
+
`/api/v1/me/orders/${symbol}/${orderId}`,
|
|
342
|
+
{ body, auth: true }
|
|
343
|
+
);
|
|
344
|
+
return data.order ?? data;
|
|
345
|
+
}
|
|
346
|
+
// -- private: trades ------------------------------------------------
|
|
347
|
+
/** The owner's trade history for `symbol` (scope: read). */
|
|
348
|
+
getMyTrades(symbol, opts = {}) {
|
|
349
|
+
return this.request("GET", `/api/v1/me/trades/${symbol}`, {
|
|
350
|
+
query: { limit: opts.limit, offset: opts.offset },
|
|
351
|
+
auth: true
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
function parseRetryAfter(response) {
|
|
356
|
+
const raw = response.headers.get("Retry-After");
|
|
357
|
+
if (raw === null) return void 0;
|
|
358
|
+
const value = Number(raw);
|
|
359
|
+
return Number.isFinite(value) ? value : void 0;
|
|
360
|
+
}
|
|
361
|
+
async function safeJson(response) {
|
|
362
|
+
try {
|
|
363
|
+
const body = await response.json();
|
|
364
|
+
return typeof body === "object" && body !== null ? body : void 0;
|
|
365
|
+
} catch {
|
|
366
|
+
return void 0;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/ws.ts
|
|
371
|
+
var import_ws = require("ws");
|
|
372
|
+
var DEFAULT_WS_BASE_URL = "wss://api.ctg.exchange";
|
|
373
|
+
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
374
|
+
function parseFrame(raw) {
|
|
375
|
+
let payload;
|
|
376
|
+
try {
|
|
377
|
+
payload = JSON.parse(raw);
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
if (typeof payload !== "object" || payload === null) return null;
|
|
382
|
+
const obj = payload;
|
|
383
|
+
if (obj.type !== "snapshot" && obj.type !== "update") return null;
|
|
384
|
+
return {
|
|
385
|
+
type: obj.type,
|
|
386
|
+
channel: typeof obj.channel === "string" ? obj.channel : "",
|
|
387
|
+
symbol: typeof obj.symbol === "string" ? obj.symbol : void 0,
|
|
388
|
+
data: obj.data,
|
|
389
|
+
raw: obj
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
var BaseStream = class {
|
|
393
|
+
url;
|
|
394
|
+
channels;
|
|
395
|
+
reconnect;
|
|
396
|
+
reconnectDelayMs;
|
|
397
|
+
ws = null;
|
|
398
|
+
closed = false;
|
|
399
|
+
queue = [];
|
|
400
|
+
socketEnded = false;
|
|
401
|
+
resolveNext = null;
|
|
402
|
+
constructor(path, options) {
|
|
403
|
+
this.url = (options.baseUrl ?? DEFAULT_WS_BASE_URL).replace(/\/+$/, "") + path;
|
|
404
|
+
this.channels = new Set(options.channels ?? []);
|
|
405
|
+
this.reconnect = options.reconnect ?? true;
|
|
406
|
+
this.reconnectDelayMs = options.reconnectDelayMs ?? 2e3;
|
|
407
|
+
}
|
|
408
|
+
/** No-op for the public stream; overridden by {@link UserStream}. */
|
|
409
|
+
async authenticate(_ws) {
|
|
410
|
+
}
|
|
411
|
+
/** Close the socket and stop reconnecting. */
|
|
412
|
+
close() {
|
|
413
|
+
this.closed = true;
|
|
414
|
+
this.reconnect = false;
|
|
415
|
+
this.socketEnded = true;
|
|
416
|
+
this.ws?.close();
|
|
417
|
+
this.ws = null;
|
|
418
|
+
this.wake();
|
|
419
|
+
}
|
|
420
|
+
/** Add channels (e.g. `orderbook@CTGUSDT`). Kept across reconnects. */
|
|
421
|
+
subscribe(channels) {
|
|
422
|
+
const list = typeof channels === "string" ? [channels] : [...channels];
|
|
423
|
+
for (const c of list) this.channels.add(c);
|
|
424
|
+
if (this.ws?.readyState === import_ws.WebSocket.OPEN) {
|
|
425
|
+
this.send({ method: "subscribe", channels: list });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/** Drop channels. */
|
|
429
|
+
unsubscribe(channels) {
|
|
430
|
+
const list = typeof channels === "string" ? [channels] : [...channels];
|
|
431
|
+
for (const c of list) this.channels.delete(c);
|
|
432
|
+
if (this.ws?.readyState === import_ws.WebSocket.OPEN) {
|
|
433
|
+
this.send({ method: "unsubscribe", channels: list });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
send(obj) {
|
|
437
|
+
this.ws?.send(JSON.stringify(obj));
|
|
438
|
+
}
|
|
439
|
+
wake() {
|
|
440
|
+
const resolve = this.resolveNext;
|
|
441
|
+
if (resolve) {
|
|
442
|
+
this.resolveNext = null;
|
|
443
|
+
resolve();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async open() {
|
|
447
|
+
this.queue = [];
|
|
448
|
+
this.socketEnded = false;
|
|
449
|
+
const ws = new import_ws.WebSocket(this.url);
|
|
450
|
+
this.ws = ws;
|
|
451
|
+
await new Promise((resolve, reject) => {
|
|
452
|
+
ws.once("open", () => resolve());
|
|
453
|
+
ws.once("error", (err) => reject(err));
|
|
454
|
+
});
|
|
455
|
+
await this.authenticate(ws);
|
|
456
|
+
ws.on("message", (data) => {
|
|
457
|
+
const message = parseFrame(data.toString());
|
|
458
|
+
if (message) {
|
|
459
|
+
this.queue.push(message);
|
|
460
|
+
this.wake();
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
const end = () => {
|
|
464
|
+
this.socketEnded = true;
|
|
465
|
+
this.wake();
|
|
466
|
+
};
|
|
467
|
+
ws.on("close", end);
|
|
468
|
+
ws.on("error", end);
|
|
469
|
+
if (this.channels.size > 0) {
|
|
470
|
+
this.send({ method: "subscribe", channels: [...this.channels] });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async nextMessage() {
|
|
474
|
+
for (; ; ) {
|
|
475
|
+
const message = this.queue.shift();
|
|
476
|
+
if (message !== void 0) return message;
|
|
477
|
+
if (this.socketEnded) return null;
|
|
478
|
+
await new Promise((resolve) => {
|
|
479
|
+
this.resolveNext = resolve;
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
async *[Symbol.asyncIterator]() {
|
|
484
|
+
while (!this.closed) {
|
|
485
|
+
try {
|
|
486
|
+
await this.open();
|
|
487
|
+
} catch (err) {
|
|
488
|
+
this.ws = null;
|
|
489
|
+
if (!this.reconnect || this.closed) throw err;
|
|
490
|
+
await sleep2(this.reconnectDelayMs);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
for (; ; ) {
|
|
494
|
+
const message = await this.nextMessage();
|
|
495
|
+
if (message === null) break;
|
|
496
|
+
yield message;
|
|
497
|
+
}
|
|
498
|
+
this.ws = null;
|
|
499
|
+
if (!this.reconnect || this.closed) return;
|
|
500
|
+
await sleep2(this.reconnectDelayMs);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
var MarketDataStream = class extends BaseStream {
|
|
505
|
+
constructor(options = {}) {
|
|
506
|
+
super("/api/v1/stream", options);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
var UserStream = class extends BaseStream {
|
|
510
|
+
apiKey;
|
|
511
|
+
apiSecret;
|
|
512
|
+
constructor(options) {
|
|
513
|
+
if (!options.apiKey || !options.apiSecret) {
|
|
514
|
+
throw new CtgExchangeError(
|
|
515
|
+
"apiKey and apiSecret are required for the private stream"
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
super("/api/v1/me/stream", options);
|
|
519
|
+
this.apiKey = options.apiKey;
|
|
520
|
+
this.apiSecret = options.apiSecret;
|
|
521
|
+
}
|
|
522
|
+
async authenticate(ws) {
|
|
523
|
+
ws.send(JSON.stringify(wsAuthMessage(this.apiKey, this.apiSecret)));
|
|
524
|
+
const reply = await new Promise(
|
|
525
|
+
(resolve, reject) => {
|
|
526
|
+
ws.once("message", (data) => {
|
|
527
|
+
try {
|
|
528
|
+
resolve(JSON.parse(data.toString()));
|
|
529
|
+
} catch (err) {
|
|
530
|
+
reject(err);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
ws.once(
|
|
534
|
+
"close",
|
|
535
|
+
() => reject(
|
|
536
|
+
new AuthenticationError(
|
|
537
|
+
401,
|
|
538
|
+
void 0,
|
|
539
|
+
"socket closed during auth"
|
|
540
|
+
)
|
|
541
|
+
)
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
if (reply.op !== "auth" || reply.success !== true) {
|
|
546
|
+
ws.close();
|
|
547
|
+
const detail = typeof reply.error === "string" ? reply.error : "WebSocket auth rejected";
|
|
548
|
+
throw new AuthenticationError(401, void 0, detail);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
553
|
+
0 && (module.exports = {
|
|
554
|
+
ApiError,
|
|
555
|
+
AuthenticationError,
|
|
556
|
+
BadRequestError,
|
|
557
|
+
Client,
|
|
558
|
+
CtgExchangeError,
|
|
559
|
+
DEFAULT_BASE_URL,
|
|
560
|
+
DEFAULT_WS_BASE_URL,
|
|
561
|
+
MarketDataStream,
|
|
562
|
+
NotFoundError,
|
|
563
|
+
PermissionDeniedError,
|
|
564
|
+
RateLimitError,
|
|
565
|
+
ServerError,
|
|
566
|
+
UserStream,
|
|
567
|
+
errorFromResponse,
|
|
568
|
+
parseFrame,
|
|
569
|
+
restCanonicalString,
|
|
570
|
+
restHeaders,
|
|
571
|
+
sha256Hex,
|
|
572
|
+
signRest,
|
|
573
|
+
signWsAuth,
|
|
574
|
+
wsAuthMessage
|
|
575
|
+
});
|
|
576
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/rest.ts","../src/signing.ts","../src/ws.ts"],"sourcesContent":["/**\n * TypeScript SDK for the CTG.EXCHANGE exchange API.\n *\n * CTG.EXCHANGE is a hybrid crypto exchange (off-chain matcher, on-chain\n * custody on BNB Smart Chain). This package is a thin, typed client\n * over its public REST + WebSocket API.\n *\n * import { Client } from \"@ctgexchange/sdk\";\n *\n * const client = new Client({ apiKey, apiSecret });\n * const balances = await client.getBalances();\n *\n * Monetary values are JSON strings (the decimal contract) — see\n * `types`. Withdrawals are intentionally not part of the API.\n */\n\nexport { Client, DEFAULT_BASE_URL } from \"./rest.js\";\nexport type { ClientOptions, FetchLike } from \"./rest.js\";\n\nexport {\n DEFAULT_WS_BASE_URL,\n MarketDataStream,\n UserStream,\n parseFrame,\n} from \"./ws.js\";\nexport type { StreamOptions, UserStreamOptions } from \"./ws.js\";\n\nexport {\n CtgExchangeError,\n ApiError,\n AuthenticationError,\n BadRequestError,\n NotFoundError,\n PermissionDeniedError,\n RateLimitError,\n ServerError,\n errorFromResponse,\n} from \"./errors.js\";\n\nexport {\n restCanonicalString,\n restHeaders,\n sha256Hex,\n signRest,\n signWsAuth,\n wsAuthMessage,\n} from \"./signing.js\";\n\nexport type * from \"./types.js\";\n","/**\n * Typed errors for the CTG.EXCHANGE SDK.\n *\n * Every non-2xx REST response is thrown as an {@link ApiError} subclass\n * chosen by status code. The exchange returns a JSON error body\n * `{ error, message, request_id }` — `requestId` is surfaced on the\n * error; quote it when reporting a problem.\n */\n\n/** Base class for every error thrown by this SDK. */\nexport class CtgExchangeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = new.target.name;\n }\n}\n\n/** A non-2xx HTTP response from the API. */\nexport class ApiError extends CtgExchangeError {\n readonly statusCode: number;\n readonly apiError?: string;\n readonly apiMessage?: string;\n readonly requestId?: string;\n\n constructor(\n statusCode: number,\n apiError?: string,\n apiMessage?: string,\n requestId?: string,\n ) {\n const detail = apiMessage ?? apiError ?? \"request failed\";\n const suffix = requestId ? ` (request_id=${requestId})` : \"\";\n super(`[${statusCode}] ${detail}${suffix}`);\n this.statusCode = statusCode;\n this.apiError = apiError;\n this.apiMessage = apiMessage;\n this.requestId = requestId;\n }\n}\n\n/** 400 — the request was malformed. */\nexport class BadRequestError extends ApiError {}\n\n/** 401 — missing, expired, unknown or wrong-signature API key. */\nexport class AuthenticationError extends ApiError {}\n\n/** 403 — the key lacks the required scope, or the IP is off-allowlist. */\nexport class PermissionDeniedError extends ApiError {}\n\n/** 404 — unknown symbol or order. */\nexport class NotFoundError extends ApiError {}\n\n/** 429 — per-key or per-IP rate limit exceeded. */\nexport class RateLimitError extends ApiError {\n /** The `Retry-After` header value in seconds, when the server sent one. */\n readonly retryAfter?: number;\n\n constructor(\n statusCode: number,\n apiError?: string,\n apiMessage?: string,\n requestId?: string,\n retryAfter?: number,\n ) {\n super(statusCode, apiError, apiMessage, requestId);\n this.retryAfter = retryAfter;\n }\n}\n\n/** 5xx — the API failed to handle a well-formed request. */\nexport class ServerError extends ApiError {}\n\ninterface ErrorBody {\n error?: string;\n message?: string;\n request_id?: string;\n}\n\n/** Map an HTTP status + JSON error body to the right error instance. */\nexport function errorFromResponse(\n statusCode: number,\n body: ErrorBody | undefined,\n retryAfter?: number,\n): ApiError {\n const { error, message, request_id: requestId } = body ?? {};\n\n switch (statusCode) {\n case 400:\n return new BadRequestError(statusCode, error, message, requestId);\n case 401:\n return new AuthenticationError(statusCode, error, message, requestId);\n case 403:\n return new PermissionDeniedError(statusCode, error, message, requestId);\n case 404:\n return new NotFoundError(statusCode, error, message, requestId);\n case 429:\n return new RateLimitError(\n statusCode,\n error,\n message,\n requestId,\n retryAfter,\n );\n default:\n return statusCode >= 500\n ? new ServerError(statusCode, error, message, requestId)\n : new ApiError(statusCode, error, message, requestId);\n }\n}\n","/**\n * REST client for the CTG.EXCHANGE API.\n *\n * Covers the whole `/api/v1` REST surface: public market data plus the\n * private, API-key-signed account and order endpoints.\n *\n * const client = new Client({ apiKey, apiSecret });\n * const book = await client.getOrderBook(\"CTGUSDT\");\n */\n\nimport { CtgExchangeError, RateLimitError, errorFromResponse } from \"./errors.js\";\nimport { randomUUID } from \"node:crypto\";\nimport { restHeaders } from \"./signing.js\";\nimport type {\n Balance,\n Candle,\n ModifyOrderParams,\n Order,\n OrderBook,\n OrderStatus,\n PlaceOrderParams,\n PlaceOrderResult,\n SymbolInfo,\n Ticker,\n Trade,\n UserFees,\n} from \"./types.js\";\n\nexport const DEFAULT_BASE_URL = \"https://api.ctg.exchange\";\n\n/** A `fetch`-compatible function — injectable for testing. */\nexport type FetchLike = (url: string, init: RequestInit) => Promise<Response>;\n\nexport interface ClientOptions {\n /** Key id (`ak_...`). Required only for private endpoints. */\n apiKey?: string;\n /** Key secret (`sk_...`). Required only for private endpoints. */\n apiSecret?: string;\n /** REST base URL. Defaults to production. */\n baseUrl?: string;\n /** Per-request timeout in milliseconds. Default 10000. */\n timeoutMs?: number;\n /** How many times to retry a `429`, honouring `Retry-After`. Default 0. */\n maxRetries?: number;\n /** A `fetch` implementation. Defaults to the global `fetch`. */\n fetch?: FetchLike;\n}\n\ninterface RequestOptions {\n query?: Record<string, string | number | undefined>;\n body?: unknown;\n auth?: boolean;\n}\n\nconst sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\nfunction numToStr(value: string | number): string {\n return typeof value === \"number\" ? String(value) : value;\n}\n\n/** A REST client for the CTG.EXCHANGE API. */\nexport class Client {\n private readonly apiKey?: string;\n private readonly apiSecret?: string;\n private readonly baseUrl: string;\n private readonly timeoutMs: number;\n private readonly maxRetries: number;\n private readonly fetchImpl: FetchLike;\n\n constructor(options: ClientOptions = {}) {\n this.apiKey = options.apiKey;\n this.apiSecret = options.apiSecret;\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n this.timeoutMs = options.timeoutMs ?? 10_000;\n this.maxRetries = options.maxRetries ?? 0;\n const fetchImpl = options.fetch ?? (globalThis.fetch as FetchLike);\n if (!fetchImpl) {\n throw new CtgExchangeError(\n \"no fetch implementation available; pass `fetch` in ClientOptions\",\n );\n }\n this.fetchImpl = fetchImpl;\n }\n\n private buildRequestUri(\n path: string,\n query?: Record<string, string | number | undefined>,\n ): string {\n if (!query) return path;\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined) params.append(key, String(value));\n }\n const qs = params.toString();\n return qs ? `${path}?${qs}` : path;\n }\n\n private async request<T>(\n method: string,\n path: string,\n options: RequestOptions = {},\n ): Promise<T> {\n const { query, body, auth = false } = options;\n // request-uri must be the path+query exactly as sent, and the body\n // hash must cover the exact bytes on the wire.\n const requestUri = this.buildRequestUri(path, query);\n const bodyStr = body === undefined ? \"\" : JSON.stringify(body);\n\n for (let attempt = 0; ; attempt++) {\n const headers: Record<string, string> = {};\n if (bodyStr) headers[\"Content-Type\"] = \"application/json\";\n if (auth) {\n if (!this.apiKey || !this.apiSecret) {\n throw new CtgExchangeError(\n \"apiKey and apiSecret are required for private endpoints\",\n );\n }\n Object.assign(\n headers,\n restHeaders(this.apiKey, this.apiSecret, method, requestUri, bodyStr),\n );\n }\n\n const response = await this.fetchImpl(this.baseUrl + requestUri, {\n method,\n headers,\n body: bodyStr || undefined,\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n\n if (response.ok) {\n const text = await response.text();\n return (text ? JSON.parse(text) : undefined) as T;\n }\n\n const retryAfter = parseRetryAfter(response);\n const error = errorFromResponse(\n response.status,\n await safeJson(response),\n retryAfter,\n );\n if (error instanceof RateLimitError && attempt < this.maxRetries) {\n await sleep((retryAfter ?? 1) * 1000);\n continue;\n }\n throw error;\n }\n }\n\n // -- public market data ---------------------------------------------\n\n /** All trading symbols with their order filters. */\n getSymbols(): Promise<SymbolInfo[]> {\n return this.request(\"GET\", \"/api/v1/symbols\");\n }\n\n /** 24h ticker for every symbol. */\n getTickers(): Promise<Ticker[]> {\n return this.request(\"GET\", \"/api/v1/tickers\");\n }\n\n /** Current order book for `symbol`. */\n getOrderBook(symbol: string): Promise<OrderBook> {\n return this.request(\"GET\", `/api/v1/${symbol}/orderbook`);\n }\n\n /** 24h ticker for `symbol`. */\n getTicker(symbol: string): Promise<Ticker> {\n return this.request(\"GET\", `/api/v1/${symbol}/ticker`);\n }\n\n /**\n * Candles for `symbol` at `interval` (`1m` / `5m` / `15m` / `1h` /\n * `4h` / `1d`).\n *\n * Omit `from` / `to` for the latest `limit` candles. Pass `from` /\n * `to` (Unix ms) to select a historical window `[from, to)` for\n * scrollback.\n */\n getCandles(\n symbol: string,\n interval = \"1m\",\n opts: { limit?: number; from?: number; to?: number } = {},\n ): Promise<Candle[]> {\n return this.request(\"GET\", `/api/v1/${symbol}/candles`, {\n query: {\n interval,\n limit: opts.limit,\n from: opts.from,\n to: opts.to,\n },\n });\n }\n\n /** Recent public trade prints for `symbol`. */\n getTrades(symbol: string, limit?: number): Promise<Trade[]> {\n return this.request(\"GET\", `/api/v1/${symbol}/trades`, {\n query: { limit },\n });\n }\n\n // -- private: account -----------------------------------------------\n\n /** Per-asset balances for the API key's owner (scope: read). */\n getBalances(): Promise<Balance[]> {\n return this.request(\"GET\", \"/api/v1/me/balances\", { auth: true });\n }\n\n /** Fee/rebate snapshot for the API key's owner (scope: read). */\n getFees(): Promise<UserFees> {\n return this.request(\"GET\", \"/api/v1/me/fees\", { auth: true });\n }\n\n // -- private: orders ------------------------------------------------\n\n /**\n * Place an order (scope: trade).\n *\n * A `clientOrderId` is generated when you do not supply one — it lets\n * the engine de-dup on safe retries.\n */\n placeOrder(\n symbol: string,\n params: PlaceOrderParams,\n ): Promise<PlaceOrderResult> {\n const body: Record<string, string> = {\n client_order_id: params.clientOrderId ?? randomUUID(),\n side: params.side,\n type: params.type,\n };\n if (params.price !== undefined) body.price = numToStr(params.price);\n if (params.qty !== undefined) body.qty = numToStr(params.qty);\n return this.request(\"POST\", `/api/v1/me/orders/${symbol}`, {\n body,\n auth: true,\n });\n }\n\n /** List the owner's orders for `symbol` (scope: read). */\n getOrders(\n symbol: string,\n opts: { status?: OrderStatus; limit?: number; offset?: number } = {},\n ): Promise<Order[]> {\n return this.request(\"GET\", `/api/v1/me/orders/${symbol}`, {\n query: { status: opts.status, limit: opts.limit, offset: opts.offset },\n auth: true,\n });\n }\n\n /**\n * List every open order across all symbols (scope: read).\n *\n * Each `Order` carries its own `symbol`; decimal fields are converted\n * per that order's scales. For closed-order history use `getOrders`\n * per symbol.\n */\n getOpenOrders(): Promise<Order[]> {\n return this.request(\"GET\", \"/api/v1/me/orders/open\", { auth: true });\n }\n\n /** Get one order by its canonical server id (scope: read). */\n getOrder(symbol: string, orderId: string): Promise<Order> {\n return this.request(\"GET\", `/api/v1/me/orders/${symbol}/${orderId}`, {\n auth: true,\n });\n }\n\n /** Cancel one order (scope: trade). */\n cancelOrder(symbol: string, orderId: string): Promise<Order> {\n return this.request(\"DELETE\", `/api/v1/me/orders/${symbol}/${orderId}`, {\n auth: true,\n });\n }\n\n /** Cancel every open order for `symbol` (scope: trade). */\n async cancelAllOrders(symbol: string): Promise<Order[]> {\n const data = await this.request<{ orders?: Order[] }>(\n \"DELETE\",\n `/api/v1/me/orders/${symbol}`,\n { auth: true },\n );\n return data?.orders ?? [];\n }\n\n /**\n * Modify a resting order's price and quantity (scope: trade).\n *\n * The API expects the full new state — pass both `newPrice` and\n * `newQty`.\n */\n async modifyOrder(\n symbol: string,\n orderId: string,\n params: ModifyOrderParams,\n ): Promise<Order> {\n const body: Record<string, string> = {};\n if (params.newPrice !== undefined) {\n body.new_price = numToStr(params.newPrice);\n }\n if (params.newQty !== undefined) body.new_qty = numToStr(params.newQty);\n const data = await this.request<Order & { order?: Order }>(\n \"PATCH\",\n `/api/v1/me/orders/${symbol}/${orderId}`,\n { body, auth: true },\n );\n // The API wraps the modified order: { order: {...}, trades: [...] }.\n return data.order ?? data;\n }\n\n // -- private: trades ------------------------------------------------\n\n /** The owner's trade history for `symbol` (scope: read). */\n getMyTrades(\n symbol: string,\n opts: { limit?: number; offset?: number } = {},\n ): Promise<Trade[]> {\n return this.request(\"GET\", `/api/v1/me/trades/${symbol}`, {\n query: { limit: opts.limit, offset: opts.offset },\n auth: true,\n });\n }\n}\n\nfunction parseRetryAfter(response: Response): number | undefined {\n const raw = response.headers.get(\"Retry-After\");\n if (raw === null) return undefined;\n const value = Number(raw);\n return Number.isFinite(value) ? value : undefined;\n}\n\nasync function safeJson(\n response: Response,\n): Promise<Record<string, unknown> | undefined> {\n try {\n const body = (await response.json()) as unknown;\n return typeof body === \"object\" && body !== null\n ? (body as Record<string, unknown>)\n : undefined;\n } catch {\n return undefined;\n }\n}\n","/**\n * HMAC-SHA256 request signing for the CTG.EXCHANGE API.\n *\n * Two signing schemes share one key secret:\n *\n * REST — the canonical string is four newline-joined fields:\n *\n * <ts>\\n<METHOD>\\n<request-uri>\\n<hex sha256 of body>\n *\n * WebSocket — the in-band auth frame signs the string `ws-auth\\n<ts>`.\n *\n * Both produce a lowercase-hex HMAC-SHA256 keyed by the API key secret.\n */\n\nimport { createHash, createHmac } from \"node:crypto\";\n\n/** Hex SHA-256 of a request body. `sha256(\"\")` for an empty body. */\nexport function sha256Hex(body: string): string {\n return createHash(\"sha256\").update(body, \"utf8\").digest(\"hex\");\n}\n\n/**\n * Build the four-field canonical string a REST signature covers.\n *\n * `requestUri` is the path plus query string exactly as sent on the\n * request line, e.g. `/api/v1/me/orders/CTGUSDT?limit=50`.\n */\nexport function restCanonicalString(\n ts: number | string,\n method: string,\n requestUri: string,\n body = \"\",\n): string {\n return [String(ts), method.toUpperCase(), requestUri, sha256Hex(body)].join(\n \"\\n\",\n );\n}\n\n/** Lowercase-hex HMAC-SHA256 of the REST canonical string. */\nexport function signRest(\n secret: string,\n ts: number | string,\n method: string,\n requestUri: string,\n body = \"\",\n): string {\n return createHmac(\"sha256\", secret)\n .update(restCanonicalString(ts, method, requestUri, body))\n .digest(\"hex\");\n}\n\n/**\n * The three signed headers a private REST request must carry.\n *\n * `ts` defaults to the current Unix time in seconds; the server rejects\n * timestamps outside its signature window (default 30s), so keep the\n * local clock in sync.\n */\nexport function restHeaders(\n keyId: string,\n secret: string,\n method: string,\n requestUri: string,\n body = \"\",\n ts: number = Math.floor(Date.now() / 1000),\n): Record<string, string> {\n return {\n \"X-API-Key\": keyId,\n \"X-Timestamp\": String(ts),\n \"X-Signature\": signRest(secret, ts, method, requestUri, body),\n };\n}\n\n/** Lowercase-hex HMAC-SHA256 over `ws-auth\\n<ts>`. */\nexport function signWsAuth(secret: string, ts: number | string): string {\n return createHmac(\"sha256\", secret).update(`ws-auth\\n${ts}`).digest(\"hex\");\n}\n\n/** The signed `auth` frame to send first on the private WebSocket stream. */\nexport function wsAuthMessage(\n keyId: string,\n secret: string,\n ts: number = Math.floor(Date.now() / 1000),\n): { op: \"auth\"; args: [string, number, string] } {\n return { op: \"auth\", args: [keyId, ts, signWsAuth(secret, ts)] };\n}\n","/**\n * WebSocket clients for the CTG.EXCHANGE streams.\n *\n * - {@link MarketDataStream} — public market data, no auth.\n * - {@link UserStream} — the caller's private `orders` / `trades` /\n * `balances`, authenticated in-band with a signed first frame.\n *\n * Both auto-reconnect by default and re-send their subscriptions on\n * every (re)connect, so a dropped socket is transparent to the\n * consumer:\n *\n * const stream = new MarketDataStream({ channels: [\"trades@CTGUSDT\"] });\n * for await (const msg of stream) {\n * console.log(msg.channel, msg.type, msg.data);\n * }\n */\n\nimport { type RawData, WebSocket } from \"ws\";\nimport { CtgExchangeError, AuthenticationError } from \"./errors.js\";\nimport type { StreamMessage } from \"./types.js\";\nimport { wsAuthMessage } from \"./signing.js\";\n\nexport const DEFAULT_WS_BASE_URL = \"wss://api.ctg.exchange\";\n\nconst sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * Parse a raw frame into a {@link StreamMessage}, or `null` for control\n * frames (auth replies, subscribe acks) — only data frames are surfaced.\n */\nexport function parseFrame(raw: string): StreamMessage | null {\n let payload: unknown;\n try {\n payload = JSON.parse(raw);\n } catch {\n return null;\n }\n if (typeof payload !== \"object\" || payload === null) return null;\n const obj = payload as Record<string, unknown>;\n if (obj.type !== \"snapshot\" && obj.type !== \"update\") return null;\n return {\n type: obj.type,\n channel: typeof obj.channel === \"string\" ? obj.channel : \"\",\n symbol: typeof obj.symbol === \"string\" ? obj.symbol : undefined,\n data: obj.data,\n raw: obj,\n };\n}\n\nexport interface StreamOptions {\n /** WebSocket base URL. Defaults to production. */\n baseUrl?: string;\n /** Channels to subscribe to on every (re)connect. */\n channels?: Iterable<string>;\n /** Auto-reconnect on a dropped socket. Default `true`. */\n reconnect?: boolean;\n /** Delay before a reconnect attempt, in milliseconds. Default 2000. */\n reconnectDelayMs?: number;\n}\n\n/** Shared connect / subscribe / reconnect machinery. */\nabstract class BaseStream implements AsyncIterable<StreamMessage> {\n private readonly url: string;\n private readonly channels: Set<string>;\n private reconnect: boolean;\n private readonly reconnectDelayMs: number;\n private ws: WebSocket | null = null;\n private closed = false;\n\n private queue: StreamMessage[] = [];\n private socketEnded = false;\n private resolveNext: (() => void) | null = null;\n\n protected constructor(path: string, options: StreamOptions) {\n this.url = (options.baseUrl ?? DEFAULT_WS_BASE_URL).replace(/\\/+$/, \"\") +\n path;\n this.channels = new Set(options.channels ?? []);\n this.reconnect = options.reconnect ?? true;\n this.reconnectDelayMs = options.reconnectDelayMs ?? 2000;\n }\n\n /** No-op for the public stream; overridden by {@link UserStream}. */\n protected async authenticate(_ws: WebSocket): Promise<void> {}\n\n /** Close the socket and stop reconnecting. */\n close(): void {\n this.closed = true;\n this.reconnect = false;\n this.socketEnded = true;\n this.ws?.close();\n this.ws = null;\n this.wake();\n }\n\n /** Add channels (e.g. `orderbook@CTGUSDT`). Kept across reconnects. */\n subscribe(channels: string | Iterable<string>): void {\n const list = typeof channels === \"string\" ? [channels] : [...channels];\n for (const c of list) this.channels.add(c);\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.send({ method: \"subscribe\", channels: list });\n }\n }\n\n /** Drop channels. */\n unsubscribe(channels: string | Iterable<string>): void {\n const list = typeof channels === \"string\" ? [channels] : [...channels];\n for (const c of list) this.channels.delete(c);\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.send({ method: \"unsubscribe\", channels: list });\n }\n }\n\n private send(obj: unknown): void {\n this.ws?.send(JSON.stringify(obj));\n }\n\n private wake(): void {\n const resolve = this.resolveNext;\n if (resolve) {\n this.resolveNext = null;\n resolve();\n }\n }\n\n private async open(): Promise<void> {\n this.queue = [];\n this.socketEnded = false;\n const ws = new WebSocket(this.url);\n this.ws = ws;\n\n await new Promise<void>((resolve, reject) => {\n ws.once(\"open\", () => resolve());\n ws.once(\"error\", (err) => reject(err));\n });\n\n await this.authenticate(ws);\n\n ws.on(\"message\", (data: RawData) => {\n const message = parseFrame(data.toString());\n if (message) {\n this.queue.push(message);\n this.wake();\n }\n });\n const end = (): void => {\n this.socketEnded = true;\n this.wake();\n };\n ws.on(\"close\", end);\n ws.on(\"error\", end);\n\n if (this.channels.size > 0) {\n this.send({ method: \"subscribe\", channels: [...this.channels] });\n }\n }\n\n private async nextMessage(): Promise<StreamMessage | null> {\n for (;;) {\n const message = this.queue.shift();\n if (message !== undefined) return message;\n if (this.socketEnded) return null;\n await new Promise<void>((resolve) => {\n this.resolveNext = resolve;\n });\n }\n }\n\n async *[Symbol.asyncIterator](): AsyncGenerator<StreamMessage> {\n while (!this.closed) {\n try {\n await this.open();\n } catch (err) {\n this.ws = null;\n if (!this.reconnect || this.closed) throw err;\n await sleep(this.reconnectDelayMs);\n continue;\n }\n for (;;) {\n const message = await this.nextMessage();\n if (message === null) break;\n yield message;\n }\n this.ws = null;\n if (!this.reconnect || this.closed) return;\n await sleep(this.reconnectDelayMs);\n }\n }\n}\n\n/**\n * Public market-data stream — `orderbook` / `ticker` / `candles` /\n * `trades`. No authentication.\n */\nexport class MarketDataStream extends BaseStream {\n constructor(options: StreamOptions = {}) {\n super(\"/api/v1/stream\", options);\n }\n}\n\nexport interface UserStreamOptions extends StreamOptions {\n /** Key id (`ak_...`). */\n apiKey: string;\n /** Key secret (`sk_...`). */\n apiSecret: string;\n}\n\n/**\n * Private stream — the caller's `orders` / `trades` / `balances`.\n * Authenticates in-band with a signed first frame.\n */\nexport class UserStream extends BaseStream {\n private readonly apiKey: string;\n private readonly apiSecret: string;\n\n constructor(options: UserStreamOptions) {\n if (!options.apiKey || !options.apiSecret) {\n throw new CtgExchangeError(\n \"apiKey and apiSecret are required for the private stream\",\n );\n }\n super(\"/api/v1/me/stream\", options);\n this.apiKey = options.apiKey;\n this.apiSecret = options.apiSecret;\n }\n\n protected override async authenticate(ws: WebSocket): Promise<void> {\n ws.send(JSON.stringify(wsAuthMessage(this.apiKey, this.apiSecret)));\n const reply = await new Promise<Record<string, unknown>>(\n (resolve, reject) => {\n ws.once(\"message\", (data: RawData) => {\n try {\n resolve(JSON.parse(data.toString()) as Record<string, unknown>);\n } catch (err) {\n reject(err as Error);\n }\n });\n ws.once(\"close\", () =>\n reject(\n new AuthenticationError(\n 401,\n undefined,\n \"socket closed during auth\",\n ),\n ),\n );\n },\n );\n if (reply.op !== \"auth\" || reply.success !== true) {\n ws.close();\n const detail =\n typeof reply.error === \"string\"\n ? reply.error\n : \"WebSocket auth rejected\";\n throw new AuthenticationError(401, undefined, detail);\n }\n }\n}\n"],"mappings":"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","names":["import_node_crypto","sleep"]}
|