@tigeropenapi/tigeropen 0.4.0 → 0.4.6
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/README.md +2 -2
- package/dist/cjs/client/api-response.js +21 -2
- package/dist/cjs/client/case-convert.js +4 -3
- package/dist/cjs/client/http-client.js +122 -5
- package/dist/cjs/config/client-config.js +56 -12
- package/dist/cjs/config/index.js +3 -1
- package/dist/cjs/config/token-manager.js +54 -4
- package/dist/cjs/index.js +51 -2
- package/dist/cjs/model/enums.js +14 -1
- package/dist/cjs/model/order-helpers.js +44 -0
- package/dist/cjs/push/push-client.js +40 -15
- package/dist/cjs/quote/quote-client.js +14 -2
- package/dist/cjs/trade/trade-client.js +57 -8
- package/dist/esm/client/api-request.js +1 -1
- package/dist/esm/client/api-response.js +22 -3
- package/dist/esm/client/case-convert.js +4 -3
- package/dist/esm/client/http-client.js +127 -10
- package/dist/esm/client/index.js +6 -6
- package/dist/esm/config/client-config.js +59 -15
- package/dist/esm/config/index.js +4 -3
- package/dist/esm/config/token-manager.js +55 -5
- package/dist/esm/index.js +15 -1
- package/dist/esm/logger/index.js +1 -1
- package/dist/esm/model/contract-helpers.js +1 -1
- package/dist/esm/model/enums.js +13 -0
- package/dist/esm/model/order-helpers.js +43 -1
- package/dist/esm/model/order-status.js +1 -1
- package/dist/esm/push/index.js +5 -5
- package/dist/esm/push/pb/PushData.js +11 -11
- package/dist/esm/push/pb/QuoteBasicData.js +1 -1
- package/dist/esm/push/pb/Response.js +1 -1
- package/dist/esm/push/pb/index.js +17 -17
- package/dist/esm/push/proto-message.js +3 -3
- package/dist/esm/push/push-client.js +47 -22
- package/dist/esm/quote/index.js +1 -1
- package/dist/esm/quote/quote-client.js +16 -4
- package/dist/esm/signer/index.js +2 -2
- package/dist/esm/trade/index.js +1 -1
- package/dist/esm/trade/trade-client.js +60 -11
- package/dist/types/client/api-response.d.ts +1 -0
- package/dist/types/client/case-convert.d.ts +3 -1
- package/dist/types/client/http-client.d.ts +32 -1
- package/dist/types/config/client-config.d.ts +16 -1
- package/dist/types/config/index.d.ts +2 -0
- package/dist/types/config/token-manager.d.ts +27 -3
- package/dist/types/index.d.ts +22 -1
- package/dist/types/model/enums.d.ts +13 -1
- package/dist/types/model/order-helpers.d.ts +11 -0
- package/dist/types/model/order.d.ts +17 -2
- package/dist/types/model/trade-requests.d.ts +65 -0
- package/dist/types/model/trade.d.ts +108 -15
- package/dist/types/push/push-client.d.ts +5 -3
- package/dist/types/quote/quote-client.d.ts +3 -1
- package/dist/types/trade/trade-client.d.ts +19 -7
- package/package.json +3 -2
|
@@ -86,11 +86,13 @@ class PushClient {
|
|
|
86
86
|
constructor(config, options) {
|
|
87
87
|
this.socket = null;
|
|
88
88
|
this._state = ConnectionState.Disconnected;
|
|
89
|
+
/** Whether this connect attempt already fell back to unverified TLS */
|
|
90
|
+
this.tlsFallback = false;
|
|
89
91
|
this.callbacks = {};
|
|
90
92
|
/** Subscription state: subject -> Set<symbol> */
|
|
91
93
|
this.subscriptions = new Map();
|
|
92
|
-
/** Account-level subscriptions */
|
|
93
|
-
this.accountSubs = new
|
|
94
|
+
/** Account-level subscriptions: subject -> account string used at subscribe time */
|
|
95
|
+
this.accountSubs = new Map();
|
|
94
96
|
this.heartbeatTimer = null;
|
|
95
97
|
this.reconnectTimer = null;
|
|
96
98
|
this.stopped = false;
|
|
@@ -124,6 +126,7 @@ class PushClient {
|
|
|
124
126
|
}
|
|
125
127
|
this._state = ConnectionState.Connecting;
|
|
126
128
|
this.stopped = false;
|
|
129
|
+
this.tlsFallback = false;
|
|
127
130
|
this.recvBuffer = Buffer.alloc(0);
|
|
128
131
|
const socket = this.createSocket(this.pushHost, this.pushPort);
|
|
129
132
|
this.socket = socket;
|
|
@@ -180,6 +183,24 @@ class PushClient {
|
|
|
180
183
|
});
|
|
181
184
|
socket.on('error', (err) => {
|
|
182
185
|
if (!settled) {
|
|
186
|
+
// If this is a TLS certificate error, warn and retry without verification.
|
|
187
|
+
const isTlsCertError = err.message.includes('certificate') ||
|
|
188
|
+
err.message.includes('CERT') ||
|
|
189
|
+
err.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' ||
|
|
190
|
+
err.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' ||
|
|
191
|
+
err.code === 'CERT_HAS_EXPIRED';
|
|
192
|
+
if (isTlsCertError && !this.tlsFallback) {
|
|
193
|
+
this.tlsFallback = true;
|
|
194
|
+
settled = true;
|
|
195
|
+
clearTimeout(timeout);
|
|
196
|
+
socket.destroy();
|
|
197
|
+
this.socket = null;
|
|
198
|
+
this._state = ConnectionState.Disconnected;
|
|
199
|
+
console.warn(`[TigerOpen] TLS certificate verification failed: ${err.message}. Retrying without verification.`);
|
|
200
|
+
// Retry immediately using unverified TLS
|
|
201
|
+
this.connect().then(resolve).catch(reject);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
183
204
|
settled = true;
|
|
184
205
|
clearTimeout(timeout);
|
|
185
206
|
this._state = ConnectionState.Disconnected;
|
|
@@ -232,7 +253,7 @@ class PushClient {
|
|
|
232
253
|
createSocket(host, port) {
|
|
233
254
|
if (this.socketFactory)
|
|
234
255
|
return this.socketFactory(host, port);
|
|
235
|
-
return tls.connect({ host, port, rejectUnauthorized:
|
|
256
|
+
return tls.connect({ host, port, rejectUnauthorized: !this.tlsFallback });
|
|
236
257
|
}
|
|
237
258
|
/** Handle incoming TCP data: buffer + decode varint32 frames */
|
|
238
259
|
handleData(chunk, onConnected) {
|
|
@@ -331,8 +352,8 @@ class PushClient {
|
|
|
331
352
|
for (const [subject, symbols] of this.subscriptions) {
|
|
332
353
|
this.doSubscribe(subject, Array.from(symbols));
|
|
333
354
|
}
|
|
334
|
-
for (const subject of this.accountSubs) {
|
|
335
|
-
this.doSubscribe(subject, undefined,
|
|
355
|
+
for (const [subject, account] of this.accountSubs) {
|
|
356
|
+
this.doSubscribe(subject, undefined, account);
|
|
336
357
|
}
|
|
337
358
|
}
|
|
338
359
|
/** Dispatch push data to the appropriate callback based on dataType */
|
|
@@ -566,8 +587,9 @@ class PushClient {
|
|
|
566
587
|
// ===== Account push subscribe/unsubscribe =====
|
|
567
588
|
/** Subscribe to asset changes */
|
|
568
589
|
subscribeAsset(account) {
|
|
569
|
-
|
|
570
|
-
this.
|
|
590
|
+
const acct = account || this.config.account;
|
|
591
|
+
this.doSubscribe(push_message_1.SubjectType.Asset, undefined, acct);
|
|
592
|
+
this.accountSubs.set(push_message_1.SubjectType.Asset, acct);
|
|
571
593
|
}
|
|
572
594
|
/** Unsubscribe from asset changes */
|
|
573
595
|
unsubscribeAsset() {
|
|
@@ -576,8 +598,9 @@ class PushClient {
|
|
|
576
598
|
}
|
|
577
599
|
/** Subscribe to position changes */
|
|
578
600
|
subscribePosition(account) {
|
|
579
|
-
|
|
580
|
-
this.
|
|
601
|
+
const acct = account || this.config.account;
|
|
602
|
+
this.doSubscribe(push_message_1.SubjectType.Position, undefined, acct);
|
|
603
|
+
this.accountSubs.set(push_message_1.SubjectType.Position, acct);
|
|
581
604
|
}
|
|
582
605
|
/** Unsubscribe from position changes */
|
|
583
606
|
unsubscribePosition() {
|
|
@@ -586,8 +609,9 @@ class PushClient {
|
|
|
586
609
|
}
|
|
587
610
|
/** Subscribe to order status */
|
|
588
611
|
subscribeOrder(account) {
|
|
589
|
-
|
|
590
|
-
this.
|
|
612
|
+
const acct = account || this.config.account;
|
|
613
|
+
this.doSubscribe(push_message_1.SubjectType.Order, undefined, acct);
|
|
614
|
+
this.accountSubs.set(push_message_1.SubjectType.Order, acct);
|
|
591
615
|
}
|
|
592
616
|
/** Unsubscribe from order status */
|
|
593
617
|
unsubscribeOrder() {
|
|
@@ -596,8 +620,9 @@ class PushClient {
|
|
|
596
620
|
}
|
|
597
621
|
/** Subscribe to transaction details */
|
|
598
622
|
subscribeTransaction(account) {
|
|
599
|
-
|
|
600
|
-
this.
|
|
623
|
+
const acct = account || this.config.account;
|
|
624
|
+
this.doSubscribe(push_message_1.SubjectType.Transaction, undefined, acct);
|
|
625
|
+
this.accountSubs.set(push_message_1.SubjectType.Transaction, acct);
|
|
601
626
|
}
|
|
602
627
|
/** Unsubscribe from transaction details */
|
|
603
628
|
unsubscribeTransaction() {
|
|
@@ -613,9 +638,9 @@ class PushClient {
|
|
|
613
638
|
}
|
|
614
639
|
return result;
|
|
615
640
|
}
|
|
616
|
-
/** Get account-level subscriptions */
|
|
641
|
+
/** Get account-level subscriptions: subject -> account */
|
|
617
642
|
getAccountSubscriptions() {
|
|
618
|
-
return
|
|
643
|
+
return new Map(this.accountSubs);
|
|
619
644
|
}
|
|
620
645
|
}
|
|
621
646
|
exports.PushClient = PushClient;
|
|
@@ -361,9 +361,21 @@ class QuoteClient {
|
|
|
361
361
|
}
|
|
362
362
|
return acc;
|
|
363
363
|
}
|
|
364
|
-
/** Futures tick trades. wire: future_tick (API v3.0)
|
|
364
|
+
/** Futures tick trades. wire: future_tick (API v3.0)
|
|
365
|
+
* Server returns {contractCode, items:[...]}; unwrap items and backfill contractCode.
|
|
366
|
+
* endIndex defaults to 30 when not set (consistent with Python/Go SDK). */
|
|
365
367
|
async getFutureTradeTicks(req) {
|
|
366
|
-
|
|
368
|
+
if (!req.endIndex)
|
|
369
|
+
req.endIndex = 30;
|
|
370
|
+
const wrap = await this.callInto('future_tick', req, '3.0');
|
|
371
|
+
const items = wrap?.items ?? [];
|
|
372
|
+
if (wrap?.contractCode) {
|
|
373
|
+
for (const item of items) {
|
|
374
|
+
if (!item.contractCode)
|
|
375
|
+
item.contractCode = wrap.contractCode;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return items;
|
|
367
379
|
}
|
|
368
380
|
/** Futures depth. wire: future_depth */
|
|
369
381
|
async getFutureDepth(req) {
|
|
@@ -25,9 +25,10 @@ function withAccount(req, account) {
|
|
|
25
25
|
}
|
|
26
26
|
/** Trading client wrapping all trade-related APIs. */
|
|
27
27
|
class TradeClient {
|
|
28
|
-
constructor(httpClient, account) {
|
|
28
|
+
constructor(httpClient, account, secretKey) {
|
|
29
29
|
this.httpClient = httpClient;
|
|
30
30
|
this.account = account;
|
|
31
|
+
this.secretKey = secretKey;
|
|
31
32
|
}
|
|
32
33
|
async callInto(method, bizParams) {
|
|
33
34
|
const request = (0, api_request_1.createApiRequest)(method, bizParams);
|
|
@@ -63,16 +64,26 @@ class TradeClient {
|
|
|
63
64
|
}
|
|
64
65
|
// === Order operations ===
|
|
65
66
|
async placeOrder(order) {
|
|
66
|
-
|
|
67
|
+
const params = { ...order, account: this.account };
|
|
68
|
+
if (!params.secretKey && this.secretKey)
|
|
69
|
+
params.secretKey = this.secretKey;
|
|
70
|
+
return this.callInto('place_order', params);
|
|
67
71
|
}
|
|
68
72
|
async previewOrder(order) {
|
|
69
73
|
return this.callInto('preview_order', { ...order, account: this.account });
|
|
70
74
|
}
|
|
71
75
|
async modifyOrder(id, order) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return this.callInto('
|
|
76
|
+
const params = { ...order, account: this.account, id };
|
|
77
|
+
if (!params.secretKey && this.secretKey)
|
|
78
|
+
params.secretKey = this.secretKey;
|
|
79
|
+
return this.callInto('modify_order', params);
|
|
80
|
+
}
|
|
81
|
+
async cancelOrder(id, secretKey) {
|
|
82
|
+
const sk = secretKey ?? this.secretKey;
|
|
83
|
+
const params = { account: this.account, id };
|
|
84
|
+
if (sk)
|
|
85
|
+
params.secretKey = sk;
|
|
86
|
+
return this.callInto('cancel_order', params);
|
|
76
87
|
}
|
|
77
88
|
// === Order queries (BREAKING v0.4.0) ===
|
|
78
89
|
/**
|
|
@@ -194,9 +205,10 @@ class TradeClient {
|
|
|
194
205
|
async getFundDetails(req) {
|
|
195
206
|
return this.callIntoItems('fund_details', withAccount(req, this.account));
|
|
196
207
|
}
|
|
197
|
-
/** Funding history (wire method: `transfer_fund`).
|
|
208
|
+
/** Funding history (wire method: `transfer_fund`).
|
|
209
|
+
* Server returns bare list (no items wrapper). */
|
|
198
210
|
async getFundingHistory(req) {
|
|
199
|
-
return this.
|
|
211
|
+
return this.callInto('transfer_fund', withAccount(req, this.account));
|
|
200
212
|
}
|
|
201
213
|
// ===== v0.4.0 NEW: Internal / external position transfer =====
|
|
202
214
|
/**
|
|
@@ -230,5 +242,42 @@ class TradeClient {
|
|
|
230
242
|
merged.accountId = this.account;
|
|
231
243
|
return this.callIntoItems('position_transfer_external_records', merged);
|
|
232
244
|
}
|
|
245
|
+
/** 行权检验:预估行权/作废后正股持仓变化 (wire: option_exercise_check) */
|
|
246
|
+
async checkOptionExercise(req) {
|
|
247
|
+
const merged = { ...req };
|
|
248
|
+
if (!merged.account)
|
|
249
|
+
merged.account = this.account;
|
|
250
|
+
return this.callInto('option_exercise_check', merged);
|
|
251
|
+
}
|
|
252
|
+
/** 查询可行权持仓列表 (wire: option_exercise_position) */
|
|
253
|
+
async getOptionExercisePositions(req) {
|
|
254
|
+
const merged = { ...req };
|
|
255
|
+
if (!merged.account)
|
|
256
|
+
merged.account = this.account;
|
|
257
|
+
return this.callInto('option_exercise_position', merged);
|
|
258
|
+
}
|
|
259
|
+
/** 提交行权/作废申请 (wire: option_exercise_submit) */
|
|
260
|
+
async submitOptionExercise(req) {
|
|
261
|
+
const merged = { ...req };
|
|
262
|
+
if (!merged.account)
|
|
263
|
+
merged.account = this.account;
|
|
264
|
+
const result = await this.callInto('option_exercise_submit', merged);
|
|
265
|
+
return result ?? false;
|
|
266
|
+
}
|
|
267
|
+
/** 分页查询行权记录 (wire: option_exercise_record) */
|
|
268
|
+
async getOptionExerciseRecords(req) {
|
|
269
|
+
const merged = { ...req };
|
|
270
|
+
if (!merged.account)
|
|
271
|
+
merged.account = this.account;
|
|
272
|
+
return this.callInto('option_exercise_record', merged);
|
|
273
|
+
}
|
|
274
|
+
/** 撤销行权申请 (wire: option_exercise_cancel) */
|
|
275
|
+
async cancelOptionExercise(req) {
|
|
276
|
+
const merged = { ...req };
|
|
277
|
+
if (!merged.account)
|
|
278
|
+
merged.account = this.account;
|
|
279
|
+
const result = await this.callInto('option_exercise_cancel', merged);
|
|
280
|
+
return result ?? false;
|
|
281
|
+
}
|
|
233
282
|
}
|
|
234
283
|
exports.TradeClient = TradeClient;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Requests use camelCase parameters in TypeScript (idiomatic), but the
|
|
5
5
|
* server wire format is snake_case, so we convert keys before serializing.
|
|
6
6
|
*/
|
|
7
|
-
import { keysToSnakeCase } from './case-convert';
|
|
7
|
+
import { keysToSnakeCase } from './case-convert.js';
|
|
8
8
|
/**
|
|
9
9
|
* Create an API request, serializing business parameters to JSON as bizContent.
|
|
10
10
|
* camelCase keys are automatically converted to snake_case to match the server.
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* API response parsing.
|
|
3
3
|
*/
|
|
4
|
-
import { TigerError } from './errors';
|
|
4
|
+
import { TigerError } from './errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Replace bare integers (not inside JSON string values) that exceed
|
|
7
|
+
* Number.MAX_SAFE_INTEGER (> 15 digits) with quoted string equivalents,
|
|
8
|
+
* so that int64 order IDs survive JSON.parse without precision loss.
|
|
9
|
+
*/
|
|
10
|
+
function patchLargeIntegers(text) {
|
|
11
|
+
// Mask all existing JSON string values so their content isn't touched.
|
|
12
|
+
const strings = [];
|
|
13
|
+
const masked = text.replace(/"(?:[^"\\]|\\.)*"/g, (match) => {
|
|
14
|
+
strings.push(match);
|
|
15
|
+
return '\x01' + (strings.length - 1) + '\x01';
|
|
16
|
+
});
|
|
17
|
+
// Quote any bare integer with >15 digits.
|
|
18
|
+
const patched = masked.replace(/-?\d{16,}/g, (m) => '"' + m + '"');
|
|
19
|
+
// Restore original string values.
|
|
20
|
+
return patched.replace(/\x01(\d+)\x01/g, (_, i) => strings[parseInt(i, 10)]);
|
|
21
|
+
}
|
|
5
22
|
/**
|
|
6
23
|
* Parse the raw HTTP response body.
|
|
7
24
|
* Throws TigerError when code != 0.
|
|
25
|
+
* Large integers in the outer envelope are not an issue (only inner `data` contains int64 IDs).
|
|
8
26
|
*/
|
|
9
27
|
export function parseApiResponse(body) {
|
|
10
28
|
const resp = JSON.parse(body);
|
|
@@ -22,9 +40,10 @@ export function unmarshalData(data) {
|
|
|
22
40
|
if (data == null)
|
|
23
41
|
return undefined;
|
|
24
42
|
if (typeof data === 'string') {
|
|
25
|
-
// Attempt to parse JSON string (double-encoded case)
|
|
43
|
+
// Attempt to parse JSON string (double-encoded case).
|
|
44
|
+
// Patch large integers before parsing so int64 IDs survive as strings.
|
|
26
45
|
try {
|
|
27
|
-
return JSON.parse(data);
|
|
46
|
+
return JSON.parse(patchLargeIntegers(data));
|
|
28
47
|
}
|
|
29
48
|
catch {
|
|
30
49
|
return data;
|
|
@@ -13,7 +13,9 @@ export function camelToSnake(key) {
|
|
|
13
13
|
/**
|
|
14
14
|
* Recursively convert all object keys from camelCase to snake_case.
|
|
15
15
|
* Preserves arrays, primitives, and Date/null/undefined.
|
|
16
|
-
*
|
|
16
|
+
*
|
|
17
|
+
* Note: camelToSnake is idempotent for pure snake_case keys (no uppercase
|
|
18
|
+
* letters → no replacements), so no pre-check is needed.
|
|
17
19
|
*/
|
|
18
20
|
export function keysToSnakeCase(value) {
|
|
19
21
|
if (Array.isArray(value)) {
|
|
@@ -22,8 +24,7 @@ export function keysToSnakeCase(value) {
|
|
|
22
24
|
if (value && typeof value === 'object' && value.constructor === Object) {
|
|
23
25
|
const out = {};
|
|
24
26
|
for (const [k, v] of Object.entries(value)) {
|
|
25
|
-
|
|
26
|
-
out[newKey] = keysToSnakeCase(v);
|
|
27
|
+
out[camelToSnake(k)] = keysToSnakeCase(v);
|
|
27
28
|
}
|
|
28
29
|
return out;
|
|
29
30
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { TokenManager } from '../config/token-manager.js';
|
|
2
|
+
import { getSignContent } from '../signer/sign-content.js';
|
|
3
|
+
import { signWithRSA, verifyWithRSA } from '../signer/signer.js';
|
|
4
|
+
import { TigerError } from './errors.js';
|
|
5
|
+
import { defaultRetryPolicy } from './retry.js';
|
|
6
|
+
import { parseApiResponse, unmarshalData } from './api-response.js';
|
|
6
7
|
/** SDK 版本号 */
|
|
7
8
|
const SDK_VERSION = '0.4.0';
|
|
8
9
|
/** User-Agent 字符串 */
|
|
@@ -13,14 +14,45 @@ const DEFAULT_CHARSET = 'UTF-8';
|
|
|
13
14
|
const DEFAULT_SIGN_TYPE = 'RSA';
|
|
14
15
|
/** 默认 API 版本 */
|
|
15
16
|
const DEFAULT_VERSION = '2.0';
|
|
17
|
+
/** Token 刷新 API 方法名 */
|
|
18
|
+
const METHOD_TOKEN_REFRESH = 'user_token_refresh';
|
|
19
|
+
/** 默认后台 token 检查间隔(毫秒):5 分钟 */
|
|
20
|
+
const DEFAULT_TOKEN_CHECK_INTERVAL = 5 * 60 * 1000;
|
|
16
21
|
/**
|
|
17
22
|
* HttpClient handles HTTP requests, signing, retry, and timeout
|
|
18
23
|
*/
|
|
19
24
|
export class HttpClient {
|
|
20
25
|
constructor(config, retryPolicy, options) {
|
|
26
|
+
/** non-null when auto-refresh is active */
|
|
27
|
+
this.tokenManager = null;
|
|
21
28
|
this.config = config;
|
|
22
29
|
this.retryPolicy = retryPolicy ?? defaultRetryPolicy();
|
|
23
30
|
this.targetUrl = options?.useQuoteServerUrl ? config.quoteServerUrl : config.serverUrl;
|
|
31
|
+
// Auto-start token refresh when tokenRefreshDuration is configured (mirrors Go SDK behaviour)
|
|
32
|
+
if (config.tokenRefreshDuration && config.tokenRefreshDuration > 0) {
|
|
33
|
+
const interval = config.tokenCheckInterval ?? DEFAULT_TOKEN_CHECK_INTERVAL;
|
|
34
|
+
const opts = {
|
|
35
|
+
refreshDuration: config.tokenRefreshDuration,
|
|
36
|
+
refreshInterval: interval,
|
|
37
|
+
};
|
|
38
|
+
if (config.tokenWriter) {
|
|
39
|
+
opts.tokenWriter = config.tokenWriter;
|
|
40
|
+
}
|
|
41
|
+
if (config.tokenLoader) {
|
|
42
|
+
opts.tokenLoader = config.tokenLoader;
|
|
43
|
+
}
|
|
44
|
+
this.tokenManager = this.startTokenAutoRefresh(null, opts);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Stop the background token auto-refresh timer.
|
|
49
|
+
* Call this when the HttpClient is no longer needed to avoid timer leaks.
|
|
50
|
+
*/
|
|
51
|
+
close() {
|
|
52
|
+
if (this.tokenManager) {
|
|
53
|
+
this.tokenManager.stopAutoRefresh();
|
|
54
|
+
this.tokenManager = null;
|
|
55
|
+
}
|
|
24
56
|
}
|
|
25
57
|
/**
|
|
26
58
|
* Build common request parameters
|
|
@@ -31,7 +63,11 @@ export class HttpClient {
|
|
|
31
63
|
method: apiMethod,
|
|
32
64
|
charset: DEFAULT_CHARSET,
|
|
33
65
|
sign_type: DEFAULT_SIGN_TYPE,
|
|
34
|
-
timestamp:
|
|
66
|
+
timestamp: (() => {
|
|
67
|
+
const d = new Date();
|
|
68
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
69
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
70
|
+
})(),
|
|
35
71
|
version: version || DEFAULT_VERSION,
|
|
36
72
|
biz_content: bizContent,
|
|
37
73
|
};
|
|
@@ -58,6 +94,7 @@ export class HttpClient {
|
|
|
58
94
|
const headers = {
|
|
59
95
|
'Content-Type': 'application/json;charset=UTF-8',
|
|
60
96
|
'User-Agent': USER_AGENT,
|
|
97
|
+
'Connection': 'close',
|
|
61
98
|
};
|
|
62
99
|
if (this.config.token) {
|
|
63
100
|
headers['Authorization'] = this.config.token;
|
|
@@ -68,7 +105,8 @@ export class HttpClient {
|
|
|
68
105
|
body,
|
|
69
106
|
signal: AbortSignal.timeout(this.config.timeout * 1000),
|
|
70
107
|
});
|
|
71
|
-
|
|
108
|
+
const text = await response.text();
|
|
109
|
+
return text;
|
|
72
110
|
}
|
|
73
111
|
/**
|
|
74
112
|
* Execute a structured API request, returning a parsed ApiResponse.
|
|
@@ -95,6 +133,10 @@ export class HttpClient {
|
|
|
95
133
|
}
|
|
96
134
|
catch (err) {
|
|
97
135
|
lastErr = err;
|
|
136
|
+
// Only retry on network errors, not API business errors (TigerError with code)
|
|
137
|
+
if (err.code !== undefined && err.code !== -1) {
|
|
138
|
+
throw err; // API returned a business error code, don't retry
|
|
139
|
+
}
|
|
98
140
|
if (!this.retryPolicy.shouldRetry(request.method)) {
|
|
99
141
|
throw err;
|
|
100
142
|
}
|
|
@@ -107,9 +149,10 @@ export class HttpClient {
|
|
|
107
149
|
*
|
|
108
150
|
* @param apiMethod - API method name (e.g. "market_state", "place_order")
|
|
109
151
|
* @param requestJson - Raw biz_content JSON string
|
|
152
|
+
* @param version - Optional API version override (default: "2.0")
|
|
110
153
|
* @returns Raw response JSON string, unparsed
|
|
111
154
|
*/
|
|
112
|
-
async execute(apiMethod, requestJson) {
|
|
155
|
+
async execute(apiMethod, requestJson, version) {
|
|
113
156
|
// Parameter validation
|
|
114
157
|
if (!apiMethod) {
|
|
115
158
|
throw new TigerError(-1, 'api_method must not be empty');
|
|
@@ -120,7 +163,7 @@ export class HttpClient {
|
|
|
120
163
|
catch {
|
|
121
164
|
throw new TigerError(-1, 'request_json is not valid JSON');
|
|
122
165
|
}
|
|
123
|
-
const params = this.buildCommonParams(apiMethod, requestJson);
|
|
166
|
+
const params = this.buildCommonParams(apiMethod, requestJson, version);
|
|
124
167
|
const sign = this.signParams(params);
|
|
125
168
|
params['sign'] = sign;
|
|
126
169
|
let lastErr;
|
|
@@ -141,6 +184,10 @@ export class HttpClient {
|
|
|
141
184
|
}
|
|
142
185
|
catch (err) {
|
|
143
186
|
lastErr = err;
|
|
187
|
+
// Don't retry API business errors
|
|
188
|
+
if (err.code !== undefined && err.code !== -1) {
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
144
191
|
if (!this.retryPolicy.shouldRetry(apiMethod)) {
|
|
145
192
|
throw err;
|
|
146
193
|
}
|
|
@@ -148,6 +195,75 @@ export class HttpClient {
|
|
|
148
195
|
}
|
|
149
196
|
throw lastErr;
|
|
150
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* 调用 user_token_refresh 接口获取新 token,返回新 token 字符串。
|
|
200
|
+
* 此方法仅查询,不修改 config 中的 token。
|
|
201
|
+
*/
|
|
202
|
+
async queryToken() {
|
|
203
|
+
const params = this.buildCommonParams(METHOD_TOKEN_REFRESH, '{}');
|
|
204
|
+
const sign = this.signParams(params);
|
|
205
|
+
params['sign'] = sign;
|
|
206
|
+
const body = await this.doHttpPost(params);
|
|
207
|
+
const response = parseApiResponse(body);
|
|
208
|
+
this.verifyResponseSign(params['timestamp'], response.sign);
|
|
209
|
+
const data = unmarshalData(response.data);
|
|
210
|
+
const newToken = data?.token;
|
|
211
|
+
if (!newToken) {
|
|
212
|
+
throw new TigerError(-1, '服务端返回空 token');
|
|
213
|
+
}
|
|
214
|
+
return newToken;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 刷新 token:调用接口获取新 token 后更新 config.token。
|
|
218
|
+
*
|
|
219
|
+
* @param tokenManager - 可选外部 TokenManager;若提供则调用 setToken() 写文件并触发回调。
|
|
220
|
+
* 若 NewHttpClient 已自动启动内部 TokenManager,其内存 token 也会同步更新。
|
|
221
|
+
*/
|
|
222
|
+
async refreshToken(tokenManager) {
|
|
223
|
+
const newToken = await this.queryToken();
|
|
224
|
+
this.config.token = newToken;
|
|
225
|
+
// Keep internal tokenManager in sync so shouldTokenRefresh stays accurate.
|
|
226
|
+
if (this.tokenManager && tokenManager !== this.tokenManager) {
|
|
227
|
+
this.tokenManager.syncToken(newToken);
|
|
228
|
+
}
|
|
229
|
+
if (tokenManager) {
|
|
230
|
+
tokenManager.setToken(newToken);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* 启动后台 token 自动刷新。
|
|
235
|
+
*
|
|
236
|
+
* @param tokenManager - 可为 null:SDK 内部自动创建一个仅内存的 TokenManager,
|
|
237
|
+
* 适合直接通过 token 在代码里设置、不使用文件的场景。
|
|
238
|
+
* @param options - 仅在 tokenManager 为 null 时生效,用于配置内部 TokenManager
|
|
239
|
+
* (refreshDuration / refreshInterval / tokenWriter / tokenLoader)。
|
|
240
|
+
* @returns 使用或创建的 TokenManager 实例
|
|
241
|
+
*/
|
|
242
|
+
startTokenAutoRefresh(tokenManager, options) {
|
|
243
|
+
if (!tokenManager) {
|
|
244
|
+
tokenManager = new TokenManager(options);
|
|
245
|
+
// 从 config.token 同步当前 token,使 shouldTokenRefresh 能正确判断。
|
|
246
|
+
// 若 config.token 暂时为空(异步 tokenLoader 尚未 resolve),则先异步加载。
|
|
247
|
+
if (this.config.token) {
|
|
248
|
+
tokenManager.syncToken(this.config.token);
|
|
249
|
+
}
|
|
250
|
+
else if (options?.tokenLoader) {
|
|
251
|
+
// tokenLoader 可能是异步的;在后台 resolve 后同步到 config.token 和 TokenManager。
|
|
252
|
+
Promise.resolve(options.tokenLoader()).then((t) => {
|
|
253
|
+
if (t) {
|
|
254
|
+
this.config.token = t;
|
|
255
|
+
tokenManager.syncToken(t);
|
|
256
|
+
}
|
|
257
|
+
}).catch(() => { });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
tokenManager.startAutoRefresh(async () => {
|
|
261
|
+
const newToken = await this.queryToken();
|
|
262
|
+
this.config.token = newToken;
|
|
263
|
+
return newToken;
|
|
264
|
+
});
|
|
265
|
+
return tokenManager;
|
|
266
|
+
}
|
|
151
267
|
/** Delay for the specified milliseconds */
|
|
152
268
|
sleep(ms) {
|
|
153
269
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -161,7 +277,8 @@ export class HttpClient {
|
|
|
161
277
|
*/
|
|
162
278
|
verifyResponseSign(timestamp, signBase64) {
|
|
163
279
|
if (!signBase64) {
|
|
164
|
-
|
|
280
|
+
// Some endpoints don't return a signature; skip verification silently.
|
|
281
|
+
return;
|
|
165
282
|
}
|
|
166
283
|
const valid = verifyWithRSA(this.config.tigerPublicKey, timestamp, signBase64);
|
|
167
284
|
if (!valid) {
|
package/dist/esm/client/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Transport layer exports.
|
|
3
3
|
*/
|
|
4
|
-
export { HttpClient } from './http-client';
|
|
5
|
-
export { TigerError, classifyErrorCode } from './errors';
|
|
6
|
-
export { RetryPolicy, defaultRetryPolicy, isTradeOperation } from './retry';
|
|
7
|
-
export { createApiRequest } from './api-request';
|
|
8
|
-
export { parseApiResponse, unmarshalData } from './api-response';
|
|
9
|
-
export { camelToSnake, keysToSnakeCase } from './case-convert';
|
|
4
|
+
export { HttpClient } from './http-client.js';
|
|
5
|
+
export { TigerError, classifyErrorCode } from './errors.js';
|
|
6
|
+
export { RetryPolicy, defaultRetryPolicy, isTradeOperation } from './retry.js';
|
|
7
|
+
export { createApiRequest } from './api-request.js';
|
|
8
|
+
export { parseApiResponse, unmarshalData } from './api-response.js';
|
|
9
|
+
export { camelToSnake, keysToSnakeCase } from './case-convert.js';
|