@blackcube/xgate-sdk 0.25.0 → 0.25.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/README.md CHANGED
@@ -5,6 +5,46 @@ un catalogue, des bougies, des prix, un portefeuille et du trading — quelle qu
5
5
 
6
6
  > **SDK communautaire / non officiel.** Non affilié aux exchanges couverts. Usage à vos risques.
7
7
 
8
+ ## ⚠️ Si ça ne passe pas par XGate, arrête-toi
9
+
10
+ **XGate est la seule porte vers les venues.** Importer un `@blackcube/*-sdk` directement dans une
11
+ application, c'est contourner — et un contournement ne se voit pas : le code compile, les tests
12
+ passent, et la divergence n'apparaît qu'en production, sur la venue que personne n'a exercée.
13
+
14
+ Tu t'apprêtes à écrire quelque chose qui ne passe pas par XGate ? **Il n'y a que deux cas, et
15
+ aucun des deux ne consiste à contourner.**
16
+
17
+ **1. Tu n'as pas trouvé — c'est le cas le plus fréquent.** La méthode existe sous un autre nom. Le
18
+ vocabulaire d'XGate n'est celui d'aucune venue : il est unifié.
19
+
20
+ | tu cherches | c'est | et pas |
21
+ |---|---|---|
22
+ | passer un ordre | `openWithProtection` | `place` — qui n'existe pas ici |
23
+ | fermer | `closePosition` | un ordre marché `reduceOnly` à la main |
24
+ | modifier un stop | `moveStop` | annuler puis reposer |
25
+ | le levier | `setLeverage` | `updateLeverage` |
26
+ | l'état du compte | `accountInfo` | `getAccountInfo` |
27
+ | l'historique des ordres | `orderHistory` | `getHistory` |
28
+ | le funding payé | `fundingHistory` | `getFundingHistory` |
29
+ | les exécutions en direct | `WsTradesService.subscribe` | `subscribeUserTrades` |
30
+ | N marchés en temps réel | `CandlesStreamRegistry.of()` | N × `WsCandlesService` |
31
+
32
+ **Vérifie avant de conclure** : `grep -rn "public " node_modules/@blackcube/xgate-sdk/dist/index.d.ts`
33
+ ou la table des services plus bas. Ce README documente **35 méthodes publiques sur 35** — si tu ne
34
+ trouves pas, cherche encore.
35
+
36
+ **2. C'est un vrai trou.** Alors **dis-le, et discutes-en** — ne le comble pas dans ton coin.
37
+ Certaines absences sont des **décisions** : `place`, `placeProtection`, `cancelProtection`,
38
+ `subscribeAllCandles`, `subscribePrices` n'existent pas *exprès*, et les raisons sont écrites plus
39
+ bas. Les recréer côté application, c'est réintroduire ce qui a été retiré après incident.
40
+
41
+ D'autres absences sont réelles — `getOrderBook`, `edit`, `getExchangeInfo`, `withdraw`,
42
+ `subscribeOrderBook`, `subscribeBbo`. Elles se comblent **dans XGate**, où elles profitent à tous
43
+ les appelants et sont exercées contre les venues, jamais dans l'application.
44
+
45
+ > **Le réflexe qui trahit le contournement** : tu es sur le point d'écrire `import { Hyperliquid }
46
+ > from '@blackcube/hyperliquid-sdk'` dans du code applicatif. Arrête-toi là.
47
+
8
48
  ## Installation
9
49
 
10
50
  ```bash
package/dist/index.cjs CHANGED
@@ -97,6 +97,24 @@ function toCandle(wire, xex, interval, quote) {
97
97
  };
98
98
  }
99
99
 
100
+ // src/helpers/socket-error.ts
101
+ function lisible(error) {
102
+ if (error instanceof Error) {
103
+ return error.message;
104
+ }
105
+ const event = error;
106
+ if (event !== null && typeof event === "object") {
107
+ const message = event.message ?? event.error?.message;
108
+ if (typeof message === "string" && message !== "") {
109
+ return message;
110
+ }
111
+ if (typeof event.type === "string" && event.type !== "") {
112
+ return `\xE9v\xE9nement \xAB ${event.type} \xBB`;
113
+ }
114
+ }
115
+ return String(error);
116
+ }
117
+
100
118
  // src/helpers/ws-limits.ts
101
119
  var WS_SUBSCRIPTION_LIMITS = {
102
120
  aster: 200,
@@ -266,20 +284,36 @@ var CandlesStreamService = class {
266
284
  unsubscribes = [];
267
285
  /** Le plafond de souscriptions documenté par la venue, ou `null` si personne ne l'a publié. */
268
286
  maxSubscriptions;
287
+ /**
288
+ * Notifié quand le flux meurt — après que les souscriptions ont été coupées.
289
+ *
290
+ * **Ce crochet a remplacé un `throw`.** Lever depuis un rappel de socket ne remonte à aucun
291
+ * appelant : l'exception sort en `unhandledRejection` et **tue le process entier**, ce qui est
292
+ * arrivé le 2026-08-13 sur Blips par un autre chemin. « Impossible à ignorer » et « fatal pour
293
+ * tout le monde » ne sont pas la même chose : la première est une garantie, la seconde une panne.
294
+ *
295
+ * Le contrat reste franc — le flux est coupé, il ne repartira pas tout seul, et le silence est
296
+ * journalisé en `.error` de toute façon. Mais c'est à l'appelant de décider ce qu'il en fait :
297
+ * rouvrir, basculer de venue, ou s'arrêter.
298
+ */
299
+ onFailure = null;
269
300
  /**
270
301
  * Ouvre le flux sur une LISTE de marchés, sur une seule socket.
271
302
  *
272
- * **UNE ERREUR DE FLUX COUPE TOUT, PUIS LÈVE.** Elle n'arrive pas à la souscription : mesuré le
303
+ * **UNE ERREUR DE FLUX COUPE TOUT, PUIS NOTIFIE.** Elle n'arrive pas à la souscription : mesuré le
273
304
  * 2026-08-07, la venue accepte un symbole inconnu sans broncher, puis ferme la connexion en le
274
305
  * découvrant. Le SDK reconnecte, elle referme — cinq cycles en douze secondes, et sur une socket
275
306
  * partagée **tous** les marchés valides cessent de recevoir, en silence.
276
307
  *
277
308
  * On refuse cette boucle : à la première erreur, les souscriptions sont coupées, l'incident est
278
- * journalisé en `error`, et l'exception part. Un flux à moitié mort qui se tait coûte plus cher
279
- * qu'un arrêt franc — on ne découvre le premier qu'en constatant l'absence de données.
309
+ * journalisé en `error`, et {@link onFailure} est notifié. Un flux à moitié mort qui se tait
310
+ * coûte plus cher qu'un arrêt franc — on ne découvre le premier qu'en constatant l'absence de
311
+ * données.
280
312
  *
281
- * ⚠️ L'exception naît dans un rappel de socket : elle ne remonte pas à l'appelant de `start()`,
282
- * elle sort en erreur non capturée. C'est délibéré — elle doit être impossible à ignorer.
313
+ * ⚠️ **On ne lève pas depuis le rappel de socket.** Une exception née là ne remonte à aucun
314
+ * appelant de `start()` : elle sort en `unhandledRejection` et emporte le process entier — pas
315
+ * seulement le flux fautif, mais les crons et tout ce qui tournait à côté. C'est {@link onFailure}
316
+ * qui porte l'information, et l'appelant qui décide.
283
317
  *
284
318
  * **binance et bybit ne remontent pas encore leurs erreurs** : leur client gère la fermeture en
285
319
  * interne sans l'exposer. Sur ces deux venues, la boucle silencieuse reste possible.
@@ -291,7 +325,7 @@ var CandlesStreamService = class {
291
325
  const raison = lisible(error);
292
326
  this.logger.error(`${this.xex} : erreur de flux \u2014 ${raison}. Souscriptions coup\xE9es.`);
293
327
  this.stop();
294
- throw new Error(`stream(${this.xex}) : flux interrompu \u2014 ${raison}`);
328
+ this.onFailure?.(new Error(`stream(${this.xex}) : flux interrompu \u2014 ${raison}`));
295
329
  };
296
330
  }
297
331
  for (const symbolXex of symbolsXex) {
@@ -374,22 +408,6 @@ exports.CandlesStreamRegistry = class CandlesStreamRegistry {
374
408
  exports.CandlesStreamRegistry = __decorateClass([
375
409
  common.Injectable()
376
410
  ], exports.CandlesStreamRegistry);
377
- function lisible(error) {
378
- if (error instanceof Error) {
379
- return error.message;
380
- }
381
- const event = error;
382
- if (event !== null && typeof event === "object") {
383
- const message = event.message ?? event.error?.message;
384
- if (typeof message === "string" && message !== "") {
385
- return message;
386
- }
387
- if (typeof event.type === "string" && event.type !== "") {
388
- return `\xE9v\xE9nement \xAB ${event.type} \xBB`;
389
- }
390
- }
391
- return String(error);
392
- }
393
411
 
394
412
  // src/enums/timeframe.enums.ts
395
413
  var Timeframe = /* @__PURE__ */ ((Timeframe2) => {
@@ -1821,6 +1839,8 @@ exports.WalletService = __decorateClass([
1821
1839
  ], exports.WalletService);
1822
1840
  exports.WsCandlesService = class WsCandlesService {
1823
1841
  logger = new common.Logger(exports.WsCandlesService.name);
1842
+ /** Un client temps réel par venue — voir {@link wireOf}. La clé EST la venue. */
1843
+ wires = /* @__PURE__ */ new Map();
1824
1844
  /**
1825
1845
  * Souscrit aux bougies d'un marché chez une venue. Rend la fonction de désabonnement.
1826
1846
  *
@@ -1836,9 +1856,12 @@ exports.WsCandlesService = class WsCandlesService {
1836
1856
  subscribe(xex, query, handler) {
1837
1857
  const quote = this.quoteOf(query.symbol);
1838
1858
  this.logger.log(`souscription bougies ${query.symbol} ${query.interval} sur ${xex}`);
1839
- return createPublicXex(xex).ws().subscribeCandles({ name: query.symbol, interval: query.interval }, (wire) => {
1840
- handler(toCandle(wire, xex, query.interval, quote));
1841
- });
1859
+ return this.wireOf(xex).subscribeCandles(
1860
+ { name: query.symbol, interval: query.interval },
1861
+ (wire) => {
1862
+ handler(toCandle(wire, xex, query.interval, quote));
1863
+ }
1864
+ );
1842
1865
  }
1843
1866
  /**
1844
1867
  * Souscrit au MÊME marché chez plusieurs venues, avec un seul handler.
@@ -1865,6 +1888,35 @@ exports.WsCandlesService = class WsCandlesService {
1865
1888
  }
1866
1889
  };
1867
1890
  }
1891
+ /**
1892
+ * Le client temps réel d'une venue — **créé une seule fois, donc UNE SOCKET PAR VENUE**.
1893
+ *
1894
+ * C'est la règle que `CandlesStreamService` énonce déjà et que ce service violait : chaque
1895
+ * `subscribe()` appelait `createPublicXex(xex)`, qui construit une façade NEUVE. Or la façade
1896
+ * mémoïse son client temps réel (`unifiedWs()`), et c'est lui qui porte la socket : une façade
1897
+ * par souscription, c'est une socket par souscription.
1898
+ *
1899
+ * Mesuré le 2026-08-13 sur Blips : 1 104 souscriptions ont ouvert **1 104 sockets** sur sept
1900
+ * venues, quand hyperliquid n'en accepte que 10 par IP. Les descripteurs s'épuisent, les venues
1901
+ * refusent, et chaque connexion refusée rejetait une promesse que personne n'attendait — le
1902
+ * process entier mourait deux minutes après l'ouverture des flux. Ici : **sept sockets**.
1903
+ *
1904
+ * Rien à fermer explicitement : le ref-count vit dans les SDK (`refs += 1` / `release()`), donc
1905
+ * la socket se ferme d'elle-même au dernier désabonnement de la venue, et se rouvre au suivant.
1906
+ */
1907
+ wireOf(xex) {
1908
+ let wire = this.wires.get(xex);
1909
+ if (wire === void 0) {
1910
+ wire = createPublicXex(xex).ws();
1911
+ if ("onError" in wire) {
1912
+ wire.onError = (error) => {
1913
+ this.logger.error(`${xex} : erreur de flux \u2014 ${lisible(error)}`);
1914
+ };
1915
+ }
1916
+ this.wires.set(xex, wire);
1917
+ }
1918
+ return wire;
1919
+ }
1868
1920
  /** Voir `CandlesService.quoteOf` : la cotation se déduit du symbole natif, faute d'être publiée. */
1869
1921
  quoteOf(symbolXex) {
1870
1922
  const parts = symbolXex.split("-");