@rabby-wallet/hyperliquid-sdk 1.0.0-beta.9 → 1.0.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.
@@ -9,10 +9,11 @@ export declare class ExchangeClient {
9
9
  private agentPublicKey?;
10
10
  private agentName?;
11
11
  private readonly isTestnet;
12
- private readonly masterAddress;
12
+ private masterAddress;
13
13
  private builder?;
14
14
  private readonly symbolConversion;
15
15
  constructor(config: ExchangeClientConfig);
16
+ initAccount(masterAddress: string, agentPrivateKey: string, agentPublicKey: string, agentName?: string): void;
16
17
  /**
17
18
  * Get the wallet address
18
19
  */
@@ -48,6 +48,7 @@ const http_client_1 = require("./http-client");
48
48
  const constants_1 = require("../types/constants");
49
49
  const signer_1 = require("../utils/signer");
50
50
  const symbolConversion_1 = require("./symbolConversion");
51
+ const number_1 = require("../utils/number");
51
52
  /**
52
53
  * Client for executing trades on Hyperliquid (perpetuals only)
53
54
  * Only includes essential trading APIs as specified
@@ -79,6 +80,12 @@ class ExchangeClient {
79
80
  }
80
81
  this.isTestnet = config.isTestnet || false;
81
82
  }
83
+ initAccount(masterAddress, agentPrivateKey, agentPublicKey, agentName) {
84
+ this.masterAddress = masterAddress;
85
+ this.agentPrivateKey = agentPrivateKey.startsWith('0x') ? agentPrivateKey : ethUtil.addHexPrefix(agentPrivateKey);
86
+ this.agentPublicKey = agentPublicKey.startsWith('0x') ? agentPublicKey : ethUtil.addHexPrefix(agentPublicKey);
87
+ this.agentName = agentName || '';
88
+ }
82
89
  /**
83
90
  * Get the wallet address
84
91
  */
@@ -163,7 +170,7 @@ class ExchangeClient {
163
170
  const decimals = ((_a = midPx.toString().split('.')[1]) === null || _a === void 0 ? void 0 : _a.length) || 0;
164
171
  const prepDecimals = Math.max(0, decimals - 1);
165
172
  const px = isBuy ? (Number(midPx) * (1 + slippage)).toFixed(prepDecimals) : (Number(midPx) * (1 - slippage)).toFixed(prepDecimals);
166
- return px;
173
+ return (0, number_1.removeTrailingZeros)(px);
167
174
  }
168
175
  /**
169
176
  * Place a market order
@@ -174,11 +181,11 @@ class ExchangeClient {
174
181
  return __awaiter(this, void 0, void 0, function* () {
175
182
  try {
176
183
  const slippage = params.slippage || constants_1.SLIPPAGE;
177
- const px = this.getSlippagePx(params.midPx, slippage, params.isBuy);
184
+ const px = this.getSlippagePx((0, number_1.removeTrailingZeros)(params.midPx), slippage, params.isBuy);
178
185
  const orders = [{
179
186
  coin: params.coin,
180
187
  isBuy: params.isBuy,
181
- sz: params.size,
188
+ sz: (0, number_1.removeTrailingZeros)(params.size),
182
189
  limitPx: px,
183
190
  reduceOnly: false,
184
191
  orderType: { limit: { tif: 'Ioc' } },
@@ -187,13 +194,13 @@ class ExchangeClient {
187
194
  const tpOrder = {
188
195
  coin: params.coin,
189
196
  isBuy: !params.isBuy,
190
- sz: params.size,
191
- limitPx: params.tpTriggerPx,
197
+ sz: (0, number_1.removeTrailingZeros)(params.size),
198
+ limitPx: (0, number_1.removeTrailingZeros)(params.tpTriggerPx),
192
199
  reduceOnly: true,
193
200
  orderType: {
194
201
  trigger: {
195
202
  isMarket: true,
196
- triggerPx: params.tpTriggerPx,
203
+ triggerPx: (0, number_1.removeTrailingZeros)(params.tpTriggerPx),
197
204
  tpsl: 'tp',
198
205
  }
199
206
  }
@@ -204,13 +211,13 @@ class ExchangeClient {
204
211
  const slOrder = {
205
212
  coin: params.coin,
206
213
  isBuy: !params.isBuy,
207
- sz: params.size,
208
- limitPx: params.slTriggerPx,
214
+ sz: (0, number_1.removeTrailingZeros)(params.size),
215
+ limitPx: (0, number_1.removeTrailingZeros)(params.slTriggerPx),
209
216
  reduceOnly: true,
210
217
  orderType: {
211
218
  trigger: {
212
219
  isMarket: true,
213
- triggerPx: params.slTriggerPx,
220
+ triggerPx: (0, number_1.removeTrailingZeros)(params.slTriggerPx),
214
221
  tpsl: 'sl',
215
222
  }
216
223
  }
@@ -235,7 +242,7 @@ class ExchangeClient {
235
242
  const orders = [{
236
243
  coin: params.coin,
237
244
  isBuy: params.isBuy,
238
- sz: params.size,
245
+ sz: (0, number_1.removeTrailingZeros)(params.size),
239
246
  limitPx: px,
240
247
  reduceOnly: true,
241
248
  orderType: { limit: { tif: 'Ioc' } },
@@ -266,8 +273,8 @@ class ExchangeClient {
266
273
  return {
267
274
  a: assetIndex,
268
275
  b: order.isBuy,
269
- p: order.limitPx,
270
- s: order.sz,
276
+ p: (0, number_1.removeTrailingZeros)(order.limitPx),
277
+ s: (0, number_1.removeTrailingZeros)(order.sz),
271
278
  r: order.reduceOnly || false,
272
279
  t: order.orderType || { limit: { tif: 'Gtc' } },
273
280
  };
@@ -299,12 +306,12 @@ class ExchangeClient {
299
306
  coin: params.coin,
300
307
  isBuy: !params.isBuy,
301
308
  sz: '0',
302
- limitPx: this.getSlippagePx(params.tpTriggerPx, constants_1.SLIPPAGE, !params.isBuy),
309
+ limitPx: this.getSlippagePx((0, number_1.removeTrailingZeros)(params.tpTriggerPx), constants_1.SLIPPAGE, !params.isBuy),
303
310
  reduceOnly: true,
304
311
  orderType: {
305
312
  trigger: {
306
313
  isMarket: true,
307
- triggerPx: params.tpTriggerPx,
314
+ triggerPx: (0, number_1.removeTrailingZeros)(params.tpTriggerPx),
308
315
  tpsl: 'tp',
309
316
  }
310
317
  }
@@ -315,12 +322,12 @@ class ExchangeClient {
315
322
  coin: params.coin,
316
323
  isBuy: !params.isBuy,
317
324
  sz: '0',
318
- limitPx: this.getSlippagePx(params.slTriggerPx, constants_1.SLIPPAGE, !params.isBuy),
325
+ limitPx: this.getSlippagePx((0, number_1.removeTrailingZeros)(params.slTriggerPx), constants_1.SLIPPAGE, !params.isBuy),
319
326
  reduceOnly: true,
320
327
  orderType: {
321
328
  trigger: {
322
329
  isMarket: true,
323
- triggerPx: params.slTriggerPx,
330
+ triggerPx: (0, number_1.removeTrailingZeros)(params.slTriggerPx),
324
331
  tpsl: 'sl',
325
332
  }
326
333
  }
@@ -371,8 +378,8 @@ class ExchangeClient {
371
378
  order: {
372
379
  a: yield this.symbolConversion.getAssetIndex(params.coin),
373
380
  b: params.isBuy,
374
- p: params.limitPx,
375
- s: params.sz,
381
+ p: (0, number_1.removeTrailingZeros)(params.limitPx),
382
+ s: (0, number_1.removeTrailingZeros)(params.sz),
376
383
  r: params.reduceOnly || false,
377
384
  t: params.orderType || { limit: { tif: 'Gtc' } },
378
385
  },
@@ -469,19 +476,17 @@ class ExchangeClient {
469
476
  const action = {
470
477
  type: constants_1.ExchangeType.APPROVE_BUILDER_FEE,
471
478
  hyperliquidChain: this.isTestnet ? 'Testnet' : 'Mainnet',
472
- signatureChainId: this.isTestnet ? '0x66eee' : '0x42161',
479
+ signatureChainId: this.isTestnet ? '0x66eee' : '0xa4b1',
480
+ maxFeeRate: params.maxFee || '0.1%',
473
481
  builder: params.builder.toLowerCase(),
474
- maxFee: params.maxFee || '0.1%',
475
482
  nonce,
476
483
  };
477
484
  const { domain, types, primaryType, message } = (0, signer_1.prepareMasterSignData)({
478
485
  action,
479
486
  payloadTypes: [
480
- { name: 'type', type: 'string' },
481
487
  { name: 'hyperliquidChain', type: 'string' },
482
- { name: 'signatureChainId', type: 'string' },
483
- { name: 'builder', type: 'string' },
484
- { name: 'maxFee', type: 'string' },
488
+ { name: 'maxFeeRate', type: 'string' },
489
+ { name: 'builder', type: 'address' },
485
490
  { name: 'nonce', type: 'uint64' },
486
491
  ],
487
492
  primaryType: 'HyperliquidTransaction:ApproveBuilderFee',
@@ -518,17 +523,15 @@ class ExchangeClient {
518
523
  const action = {
519
524
  type: constants_1.ExchangeType.WITHDRAW,
520
525
  hyperliquidChain: this.isTestnet ? 'Testnet' : 'Mainnet',
521
- signatureChainId: this.isTestnet ? '0x66eee' : '0x42161',
526
+ signatureChainId: this.isTestnet ? '0x66eee' : '0xa4b1',
522
527
  destination: params.destination,
523
528
  amount: params.amount,
524
- time: Date.now(),
529
+ time: nonce,
525
530
  };
526
531
  const { domain, types, primaryType, message } = (0, signer_1.prepareMasterSignData)({
527
532
  action,
528
533
  payloadTypes: [
529
- { name: 'type', type: 'string' },
530
534
  { name: 'hyperliquidChain', type: 'string' },
531
- { name: 'signatureChainId', type: 'string' },
532
535
  { name: 'destination', type: 'string' },
533
536
  { name: 'amount', type: 'string' },
534
537
  { name: 'time', type: 'uint64' },
@@ -41,7 +41,7 @@ class HttpClient {
41
41
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
42
42
  }
43
43
  const result = yield response.json();
44
- if (result.status === 'error') {
44
+ if (result.status === 'error' || result.status === 'fail' || result.status === 'err') {
45
45
  throw new Error(JSON.stringify(result.response));
46
46
  }
47
47
  return result;
@@ -1,8 +1,8 @@
1
- import type { Meta, AssetCtx, ClearinghouseState, UserFills, CandleSnapshot, AllMids, ExtraAgent, OpenOrder, FeeResponse, UserNonFundingLedgerUpdates } from '../types';
1
+ import type { Meta, AssetCtx, ClearinghouseState, UserFills, CandleSnapshot, AllMids, ExtraAgent, OpenOrder, FeeResponse, UserNonFundingLedgerUpdates, UserHistoricalOrders } from '../types';
2
2
  export interface InfoClientConfig {
3
3
  isTestnet?: boolean;
4
4
  timeout?: number;
5
- masterAddress: string;
5
+ masterAddress?: string;
6
6
  }
7
7
  /**
8
8
  * Client for querying Hyperliquid info endpoints (perpetuals only)
@@ -10,7 +10,7 @@ export interface InfoClientConfig {
10
10
  */
11
11
  export declare class InfoClient {
12
12
  private readonly httpClient;
13
- private readonly masterAddress;
13
+ private masterAddress?;
14
14
  private metaAndAssetCtxsCache;
15
15
  private metaAndAssetCtxsInFlight;
16
16
  constructor(config: InfoClientConfig);
@@ -18,6 +18,7 @@ export declare class InfoClient {
18
18
  * Get all mid prices for all assets
19
19
  */
20
20
  getAllMids(): Promise<AllMids>;
21
+ initMasterAddress(masterAddress: string): void;
21
22
  /**
22
23
  * Get metadata and asset contexts
23
24
  */
@@ -28,16 +29,20 @@ export declare class InfoClient {
28
29
  /**
29
30
  * Get user's fill history
30
31
  */
31
- getUserFills(address?: string): Promise<UserFills>;
32
+ getUserFills(address?: string, aggregateByTime?: boolean): Promise<UserFills>;
33
+ /**
34
+ * Get user's fill and cancel and other all orders history
35
+ */
36
+ getUserHistoricalOrders(address?: string, startTime?: number, endTime?: number): Promise<UserHistoricalOrders[]>;
32
37
  /**
33
38
  * Get candlestick data snapshot
34
39
  * Supported intervals: "1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "8h", "12h", "1d", "3d", "1w", "1M"
35
40
  */
36
- candleSnapshot(coin: string, interval: string, startTime?: number, endTime?: number): Promise<CandleSnapshot>;
41
+ candleSnapshot(coin: string, interval: string, startTime: number, endTime: number): Promise<CandleSnapshot>;
37
42
  /**
38
- * Check builder fee approval status
43
+ * Check max builder fee
39
44
  */
40
- checkBuilderFeeApproval(builder: string, address?: string): Promise<any>;
45
+ getMaxBuilderFee(builder: string, address?: string): Promise<number>;
41
46
  /**
42
47
  * Get user role
43
48
  */
@@ -36,6 +36,9 @@ class InfoClient {
36
36
  });
37
37
  });
38
38
  }
39
+ initMasterAddress(masterAddress) {
40
+ this.masterAddress = masterAddress;
41
+ }
39
42
  /**
40
43
  * Get metadata and asset contexts
41
44
  */
@@ -65,18 +68,26 @@ class InfoClient {
65
68
  }
66
69
  getClearingHouseState(address) {
67
70
  return __awaiter(this, void 0, void 0, function* () {
71
+ const user = address || this.masterAddress;
72
+ if (!user) {
73
+ throw new Error('user address is empty');
74
+ }
68
75
  return this.httpClient.info({
69
76
  type: constants_1.InfoType.CLEARINGHOUSE_STATE,
70
- user: address || this.masterAddress,
77
+ user,
71
78
  });
72
79
  });
73
80
  }
74
81
  // frontendOpenOrders
75
82
  getFrontendOpenOrders(address) {
76
83
  return __awaiter(this, void 0, void 0, function* () {
84
+ const user = address || this.masterAddress;
85
+ if (!user) {
86
+ throw new Error('user address is empty');
87
+ }
77
88
  return this.httpClient.info({
78
89
  type: constants_1.InfoType.FRONTEND_OPEN_ORDERS,
79
- user: address || this.masterAddress,
90
+ user,
80
91
  });
81
92
  });
82
93
  }
@@ -89,9 +100,13 @@ class InfoClient {
89
100
  // }
90
101
  getUserNonFundingLedgerUpdates(address, startTime, endTime) {
91
102
  return __awaiter(this, void 0, void 0, function* () {
103
+ const user = address || this.masterAddress;
104
+ if (!user) {
105
+ throw new Error('user address is empty');
106
+ }
92
107
  return this.httpClient.info({
93
108
  type: constants_1.InfoType.USER_NON_FUNDING_LEDGER_UPDATES,
94
- user: address || this.masterAddress,
109
+ user,
95
110
  startTime: startTime || 0,
96
111
  endTime,
97
112
  });
@@ -100,11 +115,33 @@ class InfoClient {
100
115
  /**
101
116
  * Get user's fill history
102
117
  */
103
- getUserFills(address) {
118
+ getUserFills(address, aggregateByTime) {
104
119
  return __awaiter(this, void 0, void 0, function* () {
120
+ const user = address || this.masterAddress;
121
+ if (!user) {
122
+ throw new Error('user address is empty');
123
+ }
105
124
  return this.httpClient.info({
106
125
  type: constants_1.InfoType.USER_FILLS,
107
- user: address || this.masterAddress,
126
+ user,
127
+ aggregateByTime: aggregateByTime || true,
128
+ });
129
+ });
130
+ }
131
+ /**
132
+ * Get user's fill and cancel and other all orders history
133
+ */
134
+ getUserHistoricalOrders(address, startTime, endTime) {
135
+ return __awaiter(this, void 0, void 0, function* () {
136
+ const user = address || this.masterAddress;
137
+ if (!user) {
138
+ throw new Error('user address is empty');
139
+ }
140
+ return this.httpClient.info({
141
+ type: constants_1.InfoType.USER_HISTORICAL_ORDERS,
142
+ user,
143
+ startTime: startTime || 0,
144
+ endTime,
108
145
  });
109
146
  });
110
147
  }
@@ -116,26 +153,29 @@ class InfoClient {
116
153
  return __awaiter(this, void 0, void 0, function* () {
117
154
  // if no time range is specified, default to 24 hours ago
118
155
  const now = Date.now();
119
- const defaultStartTime = now - 24 * 60 * 60 * 1000; // 24 hours ago
120
156
  return this.httpClient.info({
121
157
  type: constants_1.InfoType.CANDLES_SNAPSHOT,
122
158
  req: {
123
159
  coin,
124
160
  interval,
125
- startTime: startTime || defaultStartTime,
126
- endTime: endTime || now,
161
+ startTime: startTime,
162
+ endTime: endTime !== null && endTime !== void 0 ? endTime : now,
127
163
  },
128
164
  });
129
165
  });
130
166
  }
131
167
  /**
132
- * Check builder fee approval status
168
+ * Check max builder fee
133
169
  */
134
- checkBuilderFeeApproval(builder, address) {
170
+ getMaxBuilderFee(builder, address) {
135
171
  return __awaiter(this, void 0, void 0, function* () {
172
+ const user = address || this.masterAddress;
173
+ if (!user) {
174
+ throw new Error('user address is empty');
175
+ }
136
176
  return this.httpClient.info({
137
- type: constants_1.InfoType.CHECK_BUILDER_FEE_APPROVAL,
138
- user: address || this.masterAddress,
177
+ type: constants_1.InfoType.MAX_BUILDER_FEE,
178
+ user,
139
179
  builder,
140
180
  });
141
181
  });
@@ -145,9 +185,13 @@ class InfoClient {
145
185
  */
146
186
  getUserRole(address) {
147
187
  return __awaiter(this, void 0, void 0, function* () {
188
+ const user = address || this.masterAddress;
189
+ if (!user) {
190
+ throw new Error('user address is empty');
191
+ }
148
192
  const res = yield this.httpClient.info({
149
193
  type: constants_1.InfoType.USER_ROLE,
150
- user: address || this.masterAddress,
194
+ user,
151
195
  });
152
196
  return res;
153
197
  });
@@ -157,9 +201,13 @@ class InfoClient {
157
201
  */
158
202
  getUsersFees(address) {
159
203
  return __awaiter(this, void 0, void 0, function* () {
204
+ const user = address || this.masterAddress;
205
+ if (!user) {
206
+ throw new Error('user address is empty');
207
+ }
160
208
  const res = yield this.httpClient.info({
161
209
  type: constants_1.InfoType.USER_FEES,
162
- user: address || this.masterAddress,
210
+ user,
163
211
  });
164
212
  // const perpFee = Number(res.userCrossRate) * (1 - Number(res.activeReferralDiscount));
165
213
  return res;
@@ -170,9 +218,13 @@ class InfoClient {
170
218
  */
171
219
  extraAgents(address) {
172
220
  return __awaiter(this, void 0, void 0, function* () {
221
+ const user = address || this.masterAddress;
222
+ if (!user) {
223
+ throw new Error('user address is empty');
224
+ }
173
225
  return this.httpClient.info({
174
226
  type: constants_1.InfoType.EXTRA_AGENTS,
175
- user: address || this.masterAddress,
227
+ user,
176
228
  });
177
229
  });
178
230
  }
@@ -1,6 +1,6 @@
1
- import type { AllMids, L2Book, Candle, WsFill, WsOrder, WebData2, WsActiveAssetCtx } from '../types';
1
+ import type { AllMids, L2Book, Candle, WsOrder, WebData2, WsActiveAssetCtx, WsUserFills } from '../types';
2
2
  export interface WebSocketClientConfig {
3
- masterAddress: string;
3
+ masterAddress?: string;
4
4
  isTestnet?: boolean;
5
5
  autoReconnect?: boolean;
6
6
  reconnectDelay?: number;
@@ -18,14 +18,27 @@ export declare class WebSocketClient {
18
18
  private readonly url;
19
19
  private readonly config;
20
20
  private subscriptions;
21
+ private activeSubscriptions;
21
22
  private reconnectAttempts;
22
23
  private isConnecting;
24
+ private isManualDisconnect;
23
25
  private pendingSubscriptions;
26
+ private heartbeatInterval;
27
+ private readonly HEARTBEAT_INTERVAL;
24
28
  constructor(config: WebSocketClientConfig);
29
+ initMasterAddress(masterAddress: string): void;
25
30
  /**
26
31
  * Connect to the WebSocket
27
32
  */
28
33
  connect(): Promise<void>;
34
+ /**
35
+ * Start heartbeat to keep connection alive
36
+ */
37
+ private startHeartbeat;
38
+ /**
39
+ * Stop heartbeat
40
+ */
41
+ private stopHeartbeat;
29
42
  /**
30
43
  * Disconnect from the WebSocket
31
44
  */
@@ -57,7 +70,7 @@ export declare class WebSocketClient {
57
70
  /**
58
71
  * Subscribe to user fills
59
72
  */
60
- subscribeToUserFills(callback: SubscriptionCallback<WsFill[]>): Subscription;
73
+ subscribeToUserFills(callback: SubscriptionCallback<WsUserFills>): Subscription;
61
74
  /**
62
75
  * Subscribe to user order updates
63
76
  */
@@ -79,4 +92,12 @@ export declare class WebSocketClient {
79
92
  * Get current connection state
80
93
  */
81
94
  get readyState(): number;
95
+ /**
96
+ * Get list of active subscription types
97
+ */
98
+ getActiveSubscriptions(): string[];
99
+ /**
100
+ * Reset reconnection attempts and enable auto-reconnect
101
+ */
102
+ resetReconnectState(): void;
82
103
  }
@@ -18,12 +18,25 @@ class WebSocketClient {
18
18
  constructor(config) {
19
19
  this.ws = null;
20
20
  this.subscriptions = new Map();
21
+ this.activeSubscriptions = new Map();
21
22
  this.reconnectAttempts = 0;
22
23
  this.isConnecting = false;
24
+ this.isManualDisconnect = false; // 标记是否为主动断开连接
23
25
  this.pendingSubscriptions = [];
24
- this.config = Object.assign({ isTestnet: false, autoReconnect: true, reconnectDelay: 3000, maxReconnectAttempts: 5 }, config);
26
+ this.heartbeatInterval = null;
27
+ this.HEARTBEAT_INTERVAL = 50000; // 50秒发送一次心跳,小于60秒超时
28
+ this.config = {
29
+ isTestnet: false,
30
+ autoReconnect: true,
31
+ reconnectDelay: 3000,
32
+ maxReconnectAttempts: 5,
33
+ masterAddress: config.masterAddress || '',
34
+ };
25
35
  this.url = this.config.isTestnet ? constants_1.WSS_URLS.TESTNET : constants_1.WSS_URLS.PRODUCTION;
26
36
  }
37
+ initMasterAddress(masterAddress) {
38
+ this.config.masterAddress = masterAddress;
39
+ }
27
40
  /**
28
41
  * Connect to the WebSocket
29
42
  */
@@ -56,21 +69,31 @@ class WebSocketClient {
56
69
  this.ws.onopen = () => {
57
70
  this.isConnecting = false;
58
71
  this.reconnectAttempts = 0;
72
+ this.isManualDisconnect = false; // 重置手动断开标志
59
73
  console.log('hyperliquid sdk WebSocket connected');
74
+ // Start heartbeat
75
+ this.startHeartbeat();
60
76
  // Resubscribe to pending subscriptions
61
77
  this.pendingSubscriptions.forEach(({ message, callback }) => {
62
78
  var _a;
63
79
  if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
64
80
  this.subscriptions.set(message.subscription.type, callback);
81
+ this.activeSubscriptions.set(message.subscription.type, { message, callback });
65
82
  this.ws.send(JSON.stringify(Object.assign(Object.assign({}, message), { method: 'subscribe' })));
66
83
  }
67
84
  });
68
85
  this.pendingSubscriptions = [];
86
+ console.log(`Resubscribed to ${this.activeSubscriptions.size} channels after reconnection`);
69
87
  resolve();
70
88
  };
71
89
  this.ws.onmessage = (event) => {
72
90
  try {
73
91
  const data = JSON.parse(event.data);
92
+ // Handle pong response
93
+ if (data.channel === 'pong') {
94
+ // console.log('Received pong from server'); // 可以注释掉以减少日志输出
95
+ return;
96
+ }
74
97
  const type = data.channel;
75
98
  const callback = this.subscriptions.get(type);
76
99
  if (callback) {
@@ -84,43 +107,91 @@ class WebSocketClient {
84
107
  this.ws.onclose = () => {
85
108
  this.isConnecting = false;
86
109
  console.log('WebSocket disconnected');
87
- if (this.config.autoReconnect && this.reconnectAttempts < this.config.maxReconnectAttempts) {
88
- this.reconnectAttempts++;
89
- console.log(`Reconnecting... (${this.reconnectAttempts}/${this.config.maxReconnectAttempts})`);
90
- setTimeout(() => this.connect(), this.config.reconnectDelay);
110
+ // Stop heartbeat
111
+ this.stopHeartbeat();
112
+ // Only attempt reconnection if it's not a manual disconnect
113
+ if (!this.isManualDisconnect && this.config.autoReconnect) {
114
+ // Save current active subscriptions to pending for reconnection
115
+ if (this.activeSubscriptions.size > 0) {
116
+ this.activeSubscriptions.forEach((subscription, type) => {
117
+ const existingPending = this.pendingSubscriptions.find(sub => sub.message.subscription.type === type);
118
+ if (!existingPending) {
119
+ this.pendingSubscriptions.push(subscription);
120
+ }
121
+ });
122
+ }
123
+ if (this.reconnectAttempts < this.config.maxReconnectAttempts) {
124
+ this.reconnectAttempts++;
125
+ console.log(`Reconnecting... (${this.reconnectAttempts}/${this.config.maxReconnectAttempts})`);
126
+ setTimeout(() => this.connect(), this.config.reconnectDelay);
127
+ }
128
+ else {
129
+ console.error('Max reconnection attempts reached. Connection failed.');
130
+ }
131
+ }
132
+ else if (this.isManualDisconnect) {
133
+ this.isManualDisconnect = false; // Reset flag
91
134
  }
92
135
  };
93
136
  this.ws.onerror = (error) => {
94
137
  this.isConnecting = false;
138
+ this.stopHeartbeat();
95
139
  console.error('WebSocket error:', error);
96
140
  reject(error);
97
141
  };
98
142
  });
99
143
  });
100
144
  }
145
+ /**
146
+ * Start heartbeat to keep connection alive
147
+ */
148
+ startHeartbeat() {
149
+ this.stopHeartbeat(); // Clear any existing heartbeat
150
+ this.heartbeatInterval = setInterval(() => {
151
+ var _a;
152
+ if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
153
+ this.ws.send(JSON.stringify({ method: 'ping' }));
154
+ }
155
+ }, this.HEARTBEAT_INTERVAL);
156
+ }
157
+ /**
158
+ * Stop heartbeat
159
+ */
160
+ stopHeartbeat() {
161
+ if (this.heartbeatInterval) {
162
+ clearInterval(this.heartbeatInterval);
163
+ this.heartbeatInterval = null;
164
+ }
165
+ }
101
166
  /**
102
167
  * Disconnect from the WebSocket
103
168
  */
104
169
  disconnect() {
170
+ this.isManualDisconnect = true; // 标记为主动断开
171
+ this.stopHeartbeat();
105
172
  if (this.ws) {
106
173
  this.ws.close();
107
174
  this.ws = null;
108
175
  }
109
176
  this.subscriptions.clear();
177
+ this.activeSubscriptions.clear();
110
178
  this.pendingSubscriptions = [];
179
+ this.reconnectAttempts = 0; // 重置重连计数
111
180
  }
112
181
  /**
113
182
  * Subscribe to a channel
114
183
  */
115
184
  subscribe(message, callback) {
116
185
  var _a;
186
+ const subscriptionData = { message, callback };
117
187
  if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
118
188
  this.subscriptions.set(message.subscription.type, callback);
189
+ this.activeSubscriptions.set(message.subscription.type, subscriptionData);
119
190
  this.ws.send(JSON.stringify(Object.assign(Object.assign({}, message), { method: 'subscribe' })));
120
191
  }
121
192
  else {
122
193
  // Store pending subscription for when connection is established
123
- this.pendingSubscriptions.push({ message, callback });
194
+ this.pendingSubscriptions.push(subscriptionData);
124
195
  // Auto-connect if not connected
125
196
  if (!this.isConnecting && (!this.ws || this.ws.readyState === WebSocket.CLOSED)) {
126
197
  this.connect().catch(console.error);
@@ -129,12 +200,14 @@ class WebSocketClient {
129
200
  return {
130
201
  unsubscribe: () => {
131
202
  var _a;
132
- this.subscriptions.delete(message.subscription.type);
203
+ const type = message.subscription.type;
204
+ this.subscriptions.delete(type);
205
+ this.activeSubscriptions.delete(type);
133
206
  if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
134
207
  this.ws.send(JSON.stringify(Object.assign(Object.assign({}, message), { method: 'unsubscribe' })));
135
208
  }
136
209
  // Remove from pending subscriptions
137
- this.pendingSubscriptions = this.pendingSubscriptions.filter(sub => sub.message.subscription.type !== message.subscription.type);
210
+ this.pendingSubscriptions = this.pendingSubscriptions.filter(sub => sub.message.subscription.type !== type);
138
211
  },
139
212
  };
140
213
  }
@@ -182,14 +255,20 @@ class WebSocketClient {
182
255
  * Subscribe to user fills
183
256
  */
184
257
  subscribeToUserFills(callback) {
258
+ if (!this.config.masterAddress) {
259
+ throw new Error('masterAddress is empty');
260
+ }
185
261
  return this.subscribe({
186
- subscription: { type: 'userFills', user: this.config.masterAddress },
262
+ subscription: { type: 'userFills', user: this.config.masterAddress, aggregateByTime: true },
187
263
  }, callback);
188
264
  }
189
265
  /**
190
266
  * Subscribe to user order updates
191
267
  */
192
268
  subscribeToUserOrders(callback) {
269
+ if (!this.config.masterAddress) {
270
+ throw new Error('masterAddress is empty');
271
+ }
193
272
  return this.subscribe({
194
273
  subscription: { type: 'orderUpdates', user: this.config.masterAddress },
195
274
  }, callback);
@@ -198,6 +277,9 @@ class WebSocketClient {
198
277
  * Subscribe to user funding updates
199
278
  */
200
279
  subscribeToUserFunding(callback) {
280
+ if (!this.config.masterAddress) {
281
+ throw new Error('masterAddress is empty');
282
+ }
201
283
  return this.subscribe({
202
284
  subscription: { type: 'userFundings', user: this.config.masterAddress },
203
285
  }, callback);
@@ -207,6 +289,9 @@ class WebSocketClient {
207
289
  * Includes positions, margin summary, open orders, and other user data
208
290
  */
209
291
  subscribeToWebData2(callback) {
292
+ if (!this.config.masterAddress) {
293
+ throw new Error('masterAddress is empty');
294
+ }
210
295
  return this.subscribe({
211
296
  subscription: { type: 'webData2', user: this.config.masterAddress },
212
297
  }, callback);
@@ -225,5 +310,18 @@ class WebSocketClient {
225
310
  var _a, _b;
226
311
  return (_b = (_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) !== null && _b !== void 0 ? _b : WebSocket.CLOSED;
227
312
  }
313
+ /**
314
+ * Get list of active subscription types
315
+ */
316
+ getActiveSubscriptions() {
317
+ return Array.from(this.activeSubscriptions.keys());
318
+ }
319
+ /**
320
+ * Reset reconnection attempts and enable auto-reconnect
321
+ */
322
+ resetReconnectState() {
323
+ this.reconnectAttempts = 0;
324
+ this.isManualDisconnect = false;
325
+ }
228
326
  }
229
327
  exports.WebSocketClient = WebSocketClient;
@@ -4,7 +4,7 @@ import type { ClearinghouseState } from './types';
4
4
  import { WebSocketClient, WebSocketClientConfig } from './client/websocket-client';
5
5
  import { SymbolConversion } from './client/symbolConversion';
6
6
  export interface HyperliquidSDKConfig {
7
- masterAddress: string;
7
+ masterAddress?: string;
8
8
  agentPrivateKey?: string;
9
9
  agentPublicKey?: string;
10
10
  agentName?: string;
@@ -22,14 +22,14 @@ export declare class HyperliquidSDK {
22
22
  exchange?: ExchangeClient;
23
23
  symbolConversion: SymbolConversion;
24
24
  readonly ws: WebSocketClient;
25
- private readonly masterAddress;
25
+ private readonly masterAddress?;
26
26
  private readonly configParams;
27
27
  constructor(config: HyperliquidSDKConfig);
28
28
  /**
29
29
  * Get wallet address (only available if private key was provided)
30
30
  */
31
31
  get address(): string;
32
- updateExchangeAgent(agentPrivateKey: string, agentPublicKey: string, agentName?: string): void;
32
+ initAccount(masterAddress: string, agentPrivateKey: string, agentPublicKey: string, agentName?: string): void;
33
33
  /**
34
34
  * Connect to WebSocket for real-time data
35
35
  */
@@ -33,7 +33,7 @@ class HyperliquidSDK {
33
33
  this.masterAddress = config.masterAddress;
34
34
  this.configParams = config;
35
35
  // Initialize exchange client only if private key is provided
36
- if (config.agentPrivateKey) {
36
+ if (config.masterAddress) {
37
37
  this.exchange = new exchange_client_1.ExchangeClient({
38
38
  masterAddress: config.masterAddress,
39
39
  agentPrivateKey: config.agentPrivateKey,
@@ -51,12 +51,25 @@ class HyperliquidSDK {
51
51
  * Get wallet address (only available if private key was provided)
52
52
  */
53
53
  get address() {
54
- return this.masterAddress;
54
+ return this.masterAddress || '';
55
55
  }
56
- updateExchangeAgent(agentPrivateKey, agentPublicKey, agentName) {
57
- this.exchange = new exchange_client_1.ExchangeClient(Object.assign(Object.assign({}, this.configParams), { symbolConversion: this.symbolConversion, agentPrivateKey,
58
- agentPublicKey,
59
- agentName }));
56
+ initAccount(masterAddress, agentPrivateKey, agentPublicKey, agentName) {
57
+ this.info.initMasterAddress(masterAddress);
58
+ this.ws.initMasterAddress(masterAddress);
59
+ if (this.exchange) {
60
+ this.exchange.initAccount(masterAddress, agentPrivateKey, agentPublicKey, agentName);
61
+ }
62
+ else {
63
+ this.exchange = new exchange_client_1.ExchangeClient({
64
+ masterAddress,
65
+ symbolConversion: this.symbolConversion,
66
+ agentPrivateKey,
67
+ agentPublicKey,
68
+ agentName,
69
+ isTestnet: this.configParams.isTestnet,
70
+ timeout: this.configParams.timeout,
71
+ });
72
+ }
60
73
  }
61
74
  /**
62
75
  * Connect to WebSocket for real-time data
@@ -28,6 +28,7 @@ export declare enum ExchangeType {
28
28
  export declare enum InfoType {
29
29
  ALL_MIDS = "allMids",
30
30
  USER_NON_FUNDING_LEDGER_UPDATES = "userNonFundingLedgerUpdates",
31
+ USER_HISTORICAL_ORDERS = "historicalOrders",
31
32
  USER_OPEN_ORDERS = "openOrders",
32
33
  USER_FILLS = "userFills",
33
34
  USER_FILLS_BY_TIME = "userFillsByTime",
@@ -42,7 +43,7 @@ export declare enum InfoType {
42
43
  FRONTEND_OPEN_ORDERS = "frontendOpenOrders",
43
44
  PREDICTED_FUNDINGS = "predictedFundings",
44
45
  ACTIVE_ASSET_DATA = "activeAssetData",
45
- CHECK_BUILDER_FEE_APPROVAL = "checkBuilderFeeApproval",
46
+ MAX_BUILDER_FEE = "maxBuilderFee",
46
47
  USER_FEES = "userFees",
47
48
  USER_ROLE = "userRole",
48
49
  EXTRA_AGENTS = "extraAgents"
@@ -36,6 +36,7 @@ var InfoType;
36
36
  (function (InfoType) {
37
37
  InfoType["ALL_MIDS"] = "allMids";
38
38
  InfoType["USER_NON_FUNDING_LEDGER_UPDATES"] = "userNonFundingLedgerUpdates";
39
+ InfoType["USER_HISTORICAL_ORDERS"] = "historicalOrders";
39
40
  InfoType["USER_OPEN_ORDERS"] = "openOrders";
40
41
  InfoType["USER_FILLS"] = "userFills";
41
42
  InfoType["USER_FILLS_BY_TIME"] = "userFillsByTime";
@@ -50,7 +51,7 @@ var InfoType;
50
51
  InfoType["FRONTEND_OPEN_ORDERS"] = "frontendOpenOrders";
51
52
  InfoType["PREDICTED_FUNDINGS"] = "predictedFundings";
52
53
  InfoType["ACTIVE_ASSET_DATA"] = "activeAssetData";
53
- InfoType["CHECK_BUILDER_FEE_APPROVAL"] = "checkBuilderFeeApproval";
54
+ InfoType["MAX_BUILDER_FEE"] = "maxBuilderFee";
54
55
  InfoType["USER_FEES"] = "userFees";
55
56
  InfoType["USER_ROLE"] = "userRole";
56
57
  InfoType["EXTRA_AGENTS"] = "extraAgents";
@@ -9,6 +9,28 @@ export interface MarketOverview {
9
9
  assetContexts: AssetCtx[];
10
10
  marginTables: [number, MarginTable][];
11
11
  }
12
+ export interface UserHistoricalOrders {
13
+ order: {
14
+ coin: string;
15
+ side: Side;
16
+ limitPx: string;
17
+ sz: string;
18
+ oid: number;
19
+ timestamp: number;
20
+ triggerCondition: string;
21
+ isTrigger: boolean;
22
+ triggerPx: string;
23
+ children: any;
24
+ isPositionTpsl: boolean;
25
+ reduceOnly: boolean;
26
+ orderType: string;
27
+ origSz: string;
28
+ tif: string;
29
+ cloid: string | null;
30
+ };
31
+ status: string;
32
+ statusTimestamp: number;
33
+ }
12
34
  export interface OpenOrder {
13
35
  coin: string;
14
36
  side: Side;
@@ -210,6 +232,11 @@ export interface WsOrder {
210
232
  children?: WsOrder[];
211
233
  user: string;
212
234
  }
235
+ export interface WsUserFills {
236
+ isSnapshot?: boolean;
237
+ user: string;
238
+ fills: WsFill[];
239
+ }
213
240
  export interface WsFill {
214
241
  coin: string;
215
242
  px: string;
@@ -224,6 +251,11 @@ export interface WsFill {
224
251
  crossed: boolean;
225
252
  fee: string;
226
253
  tid: number;
254
+ liquidation?: {
255
+ liquidatedUser: string;
256
+ markPx: string;
257
+ method: string;
258
+ };
227
259
  }
228
260
  export interface AssetCtx {
229
261
  dayBaseVlm: string;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Removes trailing zeros from a string representation of a number.
3
+ * This is useful when working with price and size fields directly.
4
+ *
5
+ * Hyperliquid API requires that price ('p') and size ('s') fields do not contain trailing zeros.
6
+ * For example, "12345.0" should be "12345" and "0.123450" should be "0.12345".
7
+ * This function ensures that all numeric string values are properly formatted.
8
+ *
9
+ * @param value - The string value to normalize
10
+ * @returns The normalized string without trailing zeros
11
+ */
12
+ export declare function removeTrailingZeros(value: string): string;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.removeTrailingZeros = removeTrailingZeros;
4
+ /**
5
+ * Removes trailing zeros from a string representation of a number.
6
+ * This is useful when working with price and size fields directly.
7
+ *
8
+ * Hyperliquid API requires that price ('p') and size ('s') fields do not contain trailing zeros.
9
+ * For example, "12345.0" should be "12345" and "0.123450" should be "0.12345".
10
+ * This function ensures that all numeric string values are properly formatted.
11
+ *
12
+ * @param value - The string value to normalize
13
+ * @returns The normalized string without trailing zeros
14
+ */
15
+ function removeTrailingZeros(value) {
16
+ if (!value.includes('.'))
17
+ return value;
18
+ const normalized = value.replace(/\.?0+$/, '');
19
+ if (normalized === '-0')
20
+ return '0';
21
+ return normalized;
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rabby-wallet/hyperliquid-sdk",
3
- "version": "1.0.0-beta.9",
3
+ "version": "1.0.2",
4
4
  "description": "Simplified Hyperliquid Perpetuals Trading SDK for Frontend Applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",