@pipsend/sdk 0.1.0 → 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/dist/index.js CHANGED
@@ -32,11 +32,30 @@ module.exports = __toCommonJS(index_exports);
32
32
 
33
33
  // src/types/index.ts
34
34
  var PipsendError = class extends Error {
35
- constructor(message, code, statusCode) {
35
+ constructor(message, code, statusCode, errors, response) {
36
36
  super(message);
37
37
  this.code = code;
38
38
  this.statusCode = statusCode;
39
39
  this.name = "PipsendError";
40
+ this.errors = errors;
41
+ this.response = response;
42
+ }
43
+ /**
44
+ * Get formatted error message with validation details
45
+ */
46
+ getDetailedMessage() {
47
+ if (!this.errors || this.errors.length === 0) {
48
+ return this.message;
49
+ }
50
+ const errorDetails = this.errors.map((err) => ` - ${err.field}: ${err.message}`).join("\n");
51
+ return `${this.message}
52
+ ${errorDetails}`;
53
+ }
54
+ /**
55
+ * Check if this is a validation error
56
+ */
57
+ isValidationError() {
58
+ return this.statusCode === 422 || this.code === "VALIDATION_ERROR";
40
59
  }
41
60
  };
42
61
  var AuthenticationError = class extends PipsendError {
@@ -91,17 +110,35 @@ var AuthManager = class {
91
110
  }
92
111
  /**
93
112
  * Performs authentication with the server
94
- * TODO: Implement real API call when available
95
113
  */
96
114
  async authenticate() {
115
+ if (!this.config.login || !this.config.password) {
116
+ throw new AuthenticationError(
117
+ "Login and password are required for authentication. Please provide them in the config."
118
+ );
119
+ }
97
120
  try {
98
- const mockResponse = {
99
- token: "mock_token_" + Date.now(),
100
- expiresIn: 180 * 60
101
- // 180 minutes in seconds
102
- };
103
- this.setTokenInfo(mockResponse);
104
- console.log("\u2705 Authentication successful (mock)");
121
+ const response = await fetch(`${this.config.server}/api/v1/auth/login`, {
122
+ method: "POST",
123
+ headers: { "Content-Type": "application/json" },
124
+ body: JSON.stringify({
125
+ login: this.config.login,
126
+ password: this.config.password
127
+ })
128
+ });
129
+ if (!response.ok) {
130
+ const errorData = await response.json().catch(() => ({}));
131
+ throw new AuthenticationError(
132
+ errorData.message || `Authentication failed: ${response.status} ${response.statusText}`
133
+ );
134
+ }
135
+ const data = await response.json();
136
+ this.setTokenInfo(data);
137
+ console.log("\u2705 Authentication successful");
138
+ console.log(` User: ${data.user.first_name} ${data.user.last_name} (${data.user.email})`);
139
+ console.log(` Login: ${data.user.login}`);
140
+ console.log(` Balance: $${data.user.balance.toFixed(2)}`);
141
+ console.log(` Logged with: ${data.logged_with}`);
105
142
  } catch (error) {
106
143
  if (error instanceof AuthenticationError) {
107
144
  throw error;
@@ -112,11 +149,45 @@ var AuthManager = class {
112
149
  }
113
150
  }
114
151
  /**
115
- * Forces a re-authentication
152
+ * Refreshes the access token using the refresh token
116
153
  */
117
154
  async refreshToken() {
118
- this.tokenInfo = void 0;
119
- await this.authenticate();
155
+ if (!this.tokenInfo?.refreshToken) {
156
+ console.log("\u26A0\uFE0F No refresh token available, performing full authentication");
157
+ this.tokenInfo = void 0;
158
+ await this.authenticate();
159
+ return;
160
+ }
161
+ try {
162
+ const response = await fetch(`${this.config.server}/api/v1/auth/refresh`, {
163
+ method: "POST",
164
+ headers: { "Content-Type": "application/json" },
165
+ body: JSON.stringify({
166
+ refresh_token: this.tokenInfo.refreshToken
167
+ })
168
+ });
169
+ if (!response.ok) {
170
+ console.log("\u26A0\uFE0F Refresh token failed, performing full authentication");
171
+ this.tokenInfo = void 0;
172
+ await this.authenticate();
173
+ return;
174
+ }
175
+ const data = await response.json();
176
+ const now = /* @__PURE__ */ new Date();
177
+ const expiresAt = new Date(now.getTime() + data.expires_in * 1e3);
178
+ this.tokenInfo = {
179
+ token: data.access_token,
180
+ refreshToken: this.tokenInfo.refreshToken,
181
+ // Keep the same refresh token
182
+ issuedAt: now,
183
+ expiresAt
184
+ };
185
+ console.log("\u2705 Token refreshed successfully");
186
+ } catch (error) {
187
+ console.error("\u274C Error refreshing token:", error);
188
+ this.tokenInfo = void 0;
189
+ await this.authenticate();
190
+ }
120
191
  }
121
192
  /**
122
193
  * Clears the current token
@@ -138,9 +209,10 @@ var AuthManager = class {
138
209
  */
139
210
  setTokenInfo(response) {
140
211
  const now = /* @__PURE__ */ new Date();
141
- const expiresAt = new Date(now.getTime() + response.expiresIn * 1e3);
212
+ const expiresAt = new Date(now.getTime() + response.expires_in * 1e3);
142
213
  this.tokenInfo = {
143
- token: response.token,
214
+ token: response.access_token,
215
+ refreshToken: response.refresh_token,
144
216
  issuedAt: now,
145
217
  expiresAt
146
218
  };
@@ -171,8 +243,9 @@ var WebSocketManager = class {
171
243
  console.log("\u26A0\uFE0F WebSocket reconnection in progress");
172
244
  return;
173
245
  }
174
- const wsUrl = this.buildWebSocketUrl();
175
246
  const token = await this.authManager.ensureAuthenticated();
247
+ const baseWsUrl = this.buildWebSocketUrl();
248
+ const wsUrl = `${baseWsUrl}${baseWsUrl.includes("?") ? "&" : "?"}token=${encodeURIComponent(token)}`;
176
249
  return new Promise((resolve, reject) => {
177
250
  try {
178
251
  const WS = this.getWebSocketConstructor();
@@ -182,11 +255,6 @@ var WebSocketManager = class {
182
255
  this.isConnected = true;
183
256
  this.isReconnecting = false;
184
257
  this.reconnectAttempts = 0;
185
- this.send({
186
- type: "auth",
187
- payload: { token },
188
- timestamp: Date.now()
189
- });
190
258
  this.emit("connected", { timestamp: Date.now() });
191
259
  if (this.subscribedChannels.length > 0) {
192
260
  console.log("\u{1F504} Resubscribing to channels:", this.subscribedChannels);
@@ -229,24 +297,22 @@ var WebSocketManager = class {
229
297
  });
230
298
  }
231
299
  /**
232
- * Subscribes to channels (symbols, events, etc.)
233
- * TODO: Adjust message format according to real WebSocket API
300
+ * Subscribes to WebSocket channels
234
301
  */
235
302
  subscribe(channels) {
236
303
  if (!this.isConnected) {
237
304
  throw new WebSocketError("WebSocket not connected. Call connect() first.");
238
305
  }
239
306
  this.subscribedChannels = [.../* @__PURE__ */ new Set([...this.subscribedChannels, ...channels])];
240
- this.send({
241
- type: "subscribe",
242
- payload: { channels },
243
- timestamp: Date.now()
244
- });
307
+ const message = {
308
+ action: "subscribe",
309
+ channels
310
+ };
311
+ this.sendAction(message);
245
312
  console.log("\u{1F4E1} Subscribed to channels:", channels);
246
313
  }
247
314
  /**
248
- * Unsubscribes from channels
249
- * TODO: Adjust message format according to real WebSocket API
315
+ * Unsubscribes from WebSocket channels
250
316
  */
251
317
  unsubscribe(channels) {
252
318
  if (!this.isConnected) {
@@ -256,11 +322,11 @@ var WebSocketManager = class {
256
322
  this.subscribedChannels = this.subscribedChannels.filter(
257
323
  (ch) => !channels.includes(ch)
258
324
  );
259
- this.send({
260
- type: "unsubscribe",
261
- payload: { channels },
262
- timestamp: Date.now()
263
- });
325
+ const message = {
326
+ action: "unsubscribe",
327
+ channels
328
+ };
329
+ this.sendAction(message);
264
330
  console.log("\u{1F4E1} Unsubscribed from channels:", channels);
265
331
  }
266
332
  /**
@@ -317,7 +383,7 @@ var WebSocketManager = class {
317
383
  try {
318
384
  const url = new URL(this.config.server);
319
385
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
320
- const wsPath = this.config.websocket?.path || "/ws";
386
+ const wsPath = this.config.websocket?.path || "/api/v1/ws";
321
387
  url.pathname = wsPath;
322
388
  return url.toString();
323
389
  } catch (error) {
@@ -340,11 +406,11 @@ var WebSocketManager = class {
340
406
  }
341
407
  }
342
408
  /**
343
- * Sends a message through WebSocket
409
+ * Sends an action message through WebSocket
344
410
  */
345
- send(message) {
411
+ sendAction(message) {
346
412
  if (!this.ws || !this.isConnected) {
347
- console.warn("\u26A0\uFE0F WebSocket not ready, message not sent:", message.type);
413
+ console.warn("\u26A0\uFE0F WebSocket not ready, message not sent:", message.action);
348
414
  return;
349
415
  }
350
416
  try {
@@ -355,21 +421,99 @@ var WebSocketManager = class {
355
421
  }
356
422
  }
357
423
  /**
358
- * Handles incoming WebSocket messages
359
- * TODO: Adjust message handling according to real WebSocket API
424
+ * Sends a ping message (for heartbeat)
360
425
  */
361
- handleMessage(data) {
362
- if (data.type === "authenticated") {
363
- console.log("\u2705 WebSocket authenticated");
364
- this.emit("authenticated", data.payload || {});
426
+ sendPing() {
427
+ if (!this.ws || !this.isConnected) {
365
428
  return;
366
429
  }
367
- if (data.type === "pong") {
368
- return;
430
+ try {
431
+ this.ws.send(JSON.stringify({ action: "ping" }));
432
+ } catch (error) {
433
+ console.error("\u274C Failed to send ping:", error);
369
434
  }
370
- const eventType = data.type;
371
- this.emit(eventType, data.payload || data);
372
- this.emit("*", data);
435
+ }
436
+ /**
437
+ * Handles incoming WebSocket messages
438
+ */
439
+ handleMessage(message) {
440
+ switch (message.op) {
441
+ case "connected":
442
+ this.handleConnected(message);
443
+ break;
444
+ case "subscription_success":
445
+ this.handleSubscriptionSuccess(message);
446
+ break;
447
+ case "unsubscription_success":
448
+ this.handleUnsubscriptionSuccess(message);
449
+ break;
450
+ case "push":
451
+ this.handlePushEvent(message);
452
+ break;
453
+ case "error":
454
+ this.handleError(message);
455
+ break;
456
+ default:
457
+ console.warn(message.op);
458
+ }
459
+ this.emit("*", message);
460
+ }
461
+ /**
462
+ * Handles connected message from server
463
+ */
464
+ handleConnected(message) {
465
+ console.log("\u2705 WebSocket connected:", message.data.message);
466
+ console.log(" Available channels:", message.data.available_channels);
467
+ this.emit("connected", message.data);
468
+ }
469
+ /**
470
+ * Handles subscription success
471
+ */
472
+ handleSubscriptionSuccess(message) {
473
+ console.log("\u2705 Subscription successful:", message.data.subscribed);
474
+ this.emit("subscription_success", message.data);
475
+ }
476
+ /**
477
+ * Handles unsubscription success
478
+ */
479
+ handleUnsubscriptionSuccess(message) {
480
+ console.log("\u2705 Unsubscription successful:", message.data.unsubscribed);
481
+ this.emit("unsubscription_success", message.data);
482
+ }
483
+ /**
484
+ * Handles push events (actual data events)
485
+ */
486
+ handlePushEvent(message) {
487
+ const eventData = message.data;
488
+ const eventType = eventData.type;
489
+ switch (eventType) {
490
+ case "position.opened":
491
+ this.emit("position:opened", eventData);
492
+ break;
493
+ case "position.closed":
494
+ case "position.partially_closed":
495
+ this.emit("position:closed", eventData);
496
+ break;
497
+ case "position.updated":
498
+ this.emit("position:updated", eventData);
499
+ break;
500
+ default:
501
+ if (message.topic?.startsWith("balance:")) {
502
+ this.emit("balance:updated", eventData);
503
+ } else {
504
+ console.warn("Unknown event type:", eventType);
505
+ }
506
+ }
507
+ }
508
+ /**
509
+ * Handles error messages from server
510
+ */
511
+ handleError(message) {
512
+ console.error("\u274C WebSocket error:", message.data.message);
513
+ if (message.data.error) {
514
+ console.error(" Error details:", message.data.error);
515
+ }
516
+ this.emit("error", message.data);
373
517
  }
374
518
  /**
375
519
  * Emits an event to all registered listeners
@@ -420,10 +564,7 @@ var WebSocketManager = class {
420
564
  const interval = this.config.websocket?.heartbeatInterval || 3e4;
421
565
  this.heartbeatTimer = setInterval(() => {
422
566
  if (this.isConnected) {
423
- this.send({
424
- type: "ping",
425
- timestamp: Date.now()
426
- });
567
+ this.sendPing();
427
568
  }
428
569
  }, interval);
429
570
  }
@@ -440,28 +581,59 @@ var WebSocketManager = class {
440
581
 
441
582
  // src/api/http-client.ts
442
583
  var HttpClient = class {
443
- constructor(baseUrl) {
584
+ constructor(baseUrl, authManager) {
444
585
  this.baseUrl = baseUrl;
586
+ this.authManager = authManager;
445
587
  }
446
588
  /**
447
589
  * Makes an HTTP request
448
590
  */
449
- async request(endpoint, options = {}) {
591
+ async request(endpoint, options = {}, retryOn401 = true) {
450
592
  const url = `${this.baseUrl}${endpoint}`;
451
593
  try {
594
+ const headers = {
595
+ "Content-Type": "application/json",
596
+ ...options.headers || {}
597
+ };
598
+ if (this.authManager) {
599
+ const token = await this.authManager.ensureAuthenticated();
600
+ headers["Authorization"] = `Bearer ${token}`;
601
+ }
452
602
  const response = await fetch(url, {
453
603
  ...options,
454
- headers: {
455
- "Content-Type": "application/json",
456
- ...options.headers
457
- }
604
+ headers
458
605
  });
606
+ if (response.status === 401 && this.authManager && retryOn401) {
607
+ console.log("\u26A0\uFE0F Received 401, attempting to refresh token...");
608
+ await this.authManager.refreshToken();
609
+ return this.request(endpoint, options, false);
610
+ }
459
611
  if (!response.ok) {
460
612
  const errorText = await response.text();
613
+ let errorMessage = `HTTP ${response.status}: ${errorText || response.statusText}`;
614
+ let errorCode = "HTTP_ERROR";
615
+ let validationErrors;
616
+ let errorResponse;
617
+ try {
618
+ const errorJson = JSON.parse(errorText);
619
+ errorResponse = errorJson;
620
+ if (errorJson.message) {
621
+ errorMessage = errorJson.message;
622
+ }
623
+ if (errorJson.status_code) {
624
+ errorCode = errorJson.status_code;
625
+ }
626
+ if (errorJson.errors && Array.isArray(errorJson.errors)) {
627
+ validationErrors = errorJson.errors;
628
+ }
629
+ } catch {
630
+ }
461
631
  throw new PipsendError(
462
- `HTTP ${response.status}: ${errorText || response.statusText}`,
463
- "HTTP_ERROR",
464
- response.status
632
+ errorMessage,
633
+ errorCode,
634
+ response.status,
635
+ validationErrors,
636
+ errorResponse
465
637
  );
466
638
  }
467
639
  const text = await response.text();
@@ -525,6 +697,12 @@ var AccountsAPI = class {
525
697
  constructor(http) {
526
698
  this.http = http;
527
699
  }
700
+ /**
701
+ * Create a new trading account
702
+ */
703
+ async create(request) {
704
+ return this.http.post("/api/v1/accounts", request);
705
+ }
528
706
  /**
529
707
  * List all accounts with optional filters and pagination
530
708
  */
@@ -537,6 +715,12 @@ var AccountsAPI = class {
537
715
  async getLogins() {
538
716
  return this.http.get("/api/v1/accounts/logins");
539
717
  }
718
+ /**
719
+ * Get account status/metrics (balance, equity, credit, margin)
720
+ */
721
+ async getStatus(login) {
722
+ return this.http.get(`/api/v1/accounts/${login}/status`);
723
+ }
540
724
  /**
541
725
  * Get account statistics with optional filters
542
726
  */
@@ -567,6 +751,20 @@ var AccountsAPI = class {
567
751
  async unarchive(login) {
568
752
  return this.http.put(`/api/v1/accounts/${login}/unarchive`);
569
753
  }
754
+ /**
755
+ * Update account information (partial update)
756
+ * All fields are optional, only send the ones you want to update
757
+ */
758
+ async update(login, request) {
759
+ return this.http.put(`/api/v1/accounts/${login}`, request);
760
+ }
761
+ /**
762
+ * Adjust balance or credit for an account
763
+ * The adjustment is relative: amount is added or subtracted from current value
764
+ */
765
+ async balance(login, request) {
766
+ return this.http.put(`/api/v1/accounts/${login}/adjust`, request);
767
+ }
570
768
  };
571
769
 
572
770
  // src/api/trading-groups.ts
@@ -863,9 +1061,9 @@ var PipsendClient = class {
863
1061
  return this.wsManager?.isWebSocketConnected() ?? false;
864
1062
  },
865
1063
  /**
866
- * Subscribes to symbols or channels
1064
+ * Subscribes to WebSocket channels
867
1065
  * @example
868
- * client.stream.subscribe(['EURUSD', 'GBPUSD']);
1066
+ * client.stream.subscribe(['positions:new', 'positions:closed']);
869
1067
  */
870
1068
  subscribe: (channels) => {
871
1069
  if (!this.wsManager) {
@@ -874,7 +1072,7 @@ var PipsendClient = class {
874
1072
  this.wsManager.subscribe(channels);
875
1073
  },
876
1074
  /**
877
- * Unsubscribes from symbols or channels
1075
+ * Unsubscribes from WebSocket channels
878
1076
  */
879
1077
  unsubscribe: (channels) => {
880
1078
  if (!this.wsManager) {
@@ -889,40 +1087,81 @@ var PipsendClient = class {
889
1087
  return this.wsManager?.getSubscribedChannels() ?? [];
890
1088
  },
891
1089
  /**
892
- * Listens to real-time price updates
1090
+ * Listens to position opened events
1091
+ */
1092
+ onPositionOpened: (callback) => {
1093
+ if (!this.wsManager) {
1094
+ throw new WebSocketError("WebSocket not enabled");
1095
+ }
1096
+ this.wsManager.on("position:opened", callback);
1097
+ },
1098
+ /**
1099
+ * Listens to position closed events (includes partial closes)
1100
+ */
1101
+ onPositionClosed: (callback) => {
1102
+ if (!this.wsManager) {
1103
+ throw new WebSocketError("WebSocket not enabled");
1104
+ }
1105
+ this.wsManager.on("position:closed", callback);
1106
+ },
1107
+ /**
1108
+ * Listens to position updated events (throttled to max 1 per 2 seconds)
1109
+ */
1110
+ onPositionUpdated: (callback) => {
1111
+ if (!this.wsManager) {
1112
+ throw new WebSocketError("WebSocket not enabled");
1113
+ }
1114
+ this.wsManager.on("position:updated", callback);
1115
+ },
1116
+ /**
1117
+ * Listens to balance updated events
1118
+ */
1119
+ onBalanceUpdated: (callback) => {
1120
+ if (!this.wsManager) {
1121
+ throw new WebSocketError("WebSocket not enabled");
1122
+ }
1123
+ this.wsManager.on("balance:updated", callback);
1124
+ },
1125
+ // Legacy methods (deprecated but kept for compatibility)
1126
+ /**
1127
+ * @deprecated Use onPositionUpdated instead
893
1128
  */
894
1129
  onPriceUpdate: (callback) => {
1130
+ console.warn("\u26A0\uFE0F onPriceUpdate is deprecated. Use onPositionUpdated instead.");
895
1131
  if (!this.wsManager) {
896
1132
  throw new WebSocketError("WebSocket not enabled");
897
1133
  }
898
- this.wsManager.on("price", callback);
1134
+ this.wsManager.on("position:updated", callback);
899
1135
  },
900
1136
  /**
901
- * Listens to real-time order updates
1137
+ * @deprecated Use onPositionOpened/onPositionClosed instead
902
1138
  */
903
1139
  onOrderUpdate: (callback) => {
1140
+ console.warn("\u26A0\uFE0F onOrderUpdate is deprecated. Use onPositionOpened/onPositionClosed instead.");
904
1141
  if (!this.wsManager) {
905
1142
  throw new WebSocketError("WebSocket not enabled");
906
1143
  }
907
- this.wsManager.on("order", callback);
1144
+ this.wsManager.on("position:opened", callback);
908
1145
  },
909
1146
  /**
910
- * Listens to real-time position updates
1147
+ * @deprecated Use onPositionUpdated instead
911
1148
  */
912
1149
  onPositionUpdate: (callback) => {
1150
+ console.warn("\u26A0\uFE0F onPositionUpdate is deprecated. Use onPositionUpdated instead.");
913
1151
  if (!this.wsManager) {
914
1152
  throw new WebSocketError("WebSocket not enabled");
915
1153
  }
916
- this.wsManager.on("position", callback);
1154
+ this.wsManager.on("position:updated", callback);
917
1155
  },
918
1156
  /**
919
- * Listens to real-time balance updates
1157
+ * @deprecated Use onBalanceUpdated instead
920
1158
  */
921
1159
  onBalanceUpdate: (callback) => {
1160
+ console.warn("\u26A0\uFE0F onBalanceUpdate is deprecated. Use onBalanceUpdated instead.");
922
1161
  if (!this.wsManager) {
923
1162
  throw new WebSocketError("WebSocket not enabled");
924
1163
  }
925
- this.wsManager.on("balance", callback);
1164
+ this.wsManager.on("balance:updated", callback);
926
1165
  },
927
1166
  /**
928
1167
  * Listens to connection events
@@ -966,7 +1205,7 @@ var PipsendClient = class {
966
1205
  timezone: config.timezone ?? "UTC"
967
1206
  };
968
1207
  this.authManager = new AuthManager(this.config);
969
- this.http = new HttpClient(this.config.server);
1208
+ this.http = new HttpClient(this.config.server, this.authManager);
970
1209
  this.accounts = new AccountsAPI(this.http);
971
1210
  this.tradingGroups = new TradingGroupsAPI(this.http);
972
1211
  this.marketData = new MarketDataAPI(this.http);