@0xmonaco/core 1.0.65 → 1.0.66

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.
@@ -75,6 +75,16 @@ export class PositionsAPIImpl extends BaseAPI {
75
75
  expiresAt: request.stopLoss.expiresAt,
76
76
  }
77
77
  : undefined,
78
+ trailingStop: request.trailingStop
79
+ ? {
80
+ trailBps: request.trailingStop.trailBps,
81
+ activationPrice: request.trailingStop.activationPrice,
82
+ quantity: request.trailingStop.quantity,
83
+ closePosition: request.trailingStop.closePosition,
84
+ slippageToleranceBps: request.trailingStop.slippageToleranceBps,
85
+ expiresAt: request.trailingStop.expiresAt,
86
+ }
87
+ : undefined,
78
88
  oco: request.oco,
79
89
  }),
80
90
  });
@@ -21,7 +21,7 @@
21
21
  * );
22
22
  * ```
23
23
  */
24
- import type { BatchCancelOrdersResponse, BatchCreateOrderParams, BatchCreateOrdersResponse, BatchReplaceOrderParams, BatchReplaceOrdersResponse, CancelConditionalOrderResponse, CancelOrderResponse, CancelTwapOrderResponse, ConditionalOrder, CreateOrderResponse, CreateTwapOrderOptions, CreateTwapOrderResponse, GetOrderResponse, GetPaginatedOrdersParams, GetPaginatedOrdersResponse, GetTwapOrderTcaResponse, ListConditionalOrdersParams, ListConditionalOrdersResponse, ListTwapOrdersParams, ListTwapOrdersResponse, MarginMode, OrderSide, ParentTpSlLegParams, PositionSide, ReplaceOrderResponse, SelfTradePreventionMode, TimeInForce, TradingAPI, TradingMode, TwapOrder } from "@0xmonaco/types";
24
+ import type { BatchCancelOrdersResponse, BatchCreateOrderParams, BatchCreateOrdersResponse, BatchReplaceOrderParams, BatchReplaceOrdersResponse, CancelConditionalOrderResponse, CancelOrderResponse, CancelTwapOrderResponse, ConditionalOrder, CreateOrderResponse, CreateTwapOrderOptions, CreateTwapOrderResponse, GetOrderResponse, GetPaginatedOrdersParams, GetPaginatedOrdersResponse, GetTwapOrderTcaResponse, ListConditionalOrdersParams, ListConditionalOrdersResponse, ListTwapOrdersParams, ListTwapOrdersResponse, MarginMode, OrderSide, ParentTpSlLegParams, ParentTrailingStopLegParams, PositionSide, ReplaceOrderResponse, SelfTradePreventionMode, TimeInForce, TradingAPI, TradingMode, TwapOrder } from "@0xmonaco/types";
25
25
  import { BaseAPI } from "../base";
26
26
  export declare class TradingAPIImpl extends BaseAPI implements TradingAPI {
27
27
  /**
@@ -94,6 +94,8 @@ export declare class TradingAPIImpl extends BaseAPI implements TradingAPI {
94
94
  idempotencyKey?: string;
95
95
  takeProfit?: ParentTpSlLegParams;
96
96
  stopLoss?: ParentTpSlLegParams;
97
+ /** Trailing stop that arms when this order fully fills; margin entry orders only. */
98
+ trailingStop?: ParentTrailingStopLegParams;
97
99
  }): Promise<CreateOrderResponse>;
98
100
  /**
99
101
  * Places a market order for immediate execution.
@@ -152,6 +154,8 @@ export declare class TradingAPIImpl extends BaseAPI implements TradingAPI {
152
154
  selfTradePreventionMode?: SelfTradePreventionMode;
153
155
  takeProfit?: ParentTpSlLegParams;
154
156
  stopLoss?: ParentTpSlLegParams;
157
+ /** Trailing stop that arms when this order fully fills; margin entry orders only. */
158
+ trailingStop?: ParentTrailingStopLegParams;
155
159
  clientOrderId?: string;
156
160
  /** Generated once per call when omitted. Supply the same key to retry a logical order within 24 hours. */
157
161
  idempotencyKey?: string;
@@ -41,6 +41,19 @@ function parentTpSlLegToRequest(leg) {
41
41
  closePosition: leg.closePosition,
42
42
  };
43
43
  }
44
+ function parentTrailingStopLegToRequest(leg) {
45
+ if (!leg)
46
+ return undefined;
47
+ return {
48
+ trailBps: leg.trailBps,
49
+ activationPrice: leg.activationPrice,
50
+ // Same sizing contract as the TP/SL legs above — see the note there.
51
+ quantity: leg.quantity,
52
+ closePosition: leg.closePosition,
53
+ slippageToleranceBps: leg.slippageToleranceBps,
54
+ expiresAt: leg.expiresAt,
55
+ };
56
+ }
44
57
  export class TradingAPIImpl extends BaseAPI {
45
58
  /**
46
59
  * Places a limit order on the order book.
@@ -125,6 +138,7 @@ export class TradingAPIImpl extends BaseAPI {
125
138
  idempotencyKey: options?.idempotencyKey === undefined ? crypto.randomUUID() : options.idempotencyKey,
126
139
  takeProfit: parentTpSlLegToRequest(options?.takeProfit),
127
140
  stopLoss: parentTpSlLegToRequest(options?.stopLoss),
141
+ trailingStop: parentTrailingStopLegToRequest(options?.trailingStop),
128
142
  };
129
143
  return await this.makeAuthenticatedRequest(perpRoutes.orders.create(), {
130
144
  method: "POST",
@@ -201,6 +215,7 @@ export class TradingAPIImpl extends BaseAPI {
201
215
  selfTradePreventionMode: options?.selfTradePreventionMode,
202
216
  takeProfit: parentTpSlLegToRequest(options?.takeProfit),
203
217
  stopLoss: parentTpSlLegToRequest(options?.stopLoss),
218
+ trailingStop: parentTrailingStopLegToRequest(options?.trailingStop),
204
219
  clientOrderId: options?.clientOrderId,
205
220
  idempotencyKey: options?.idempotencyKey === undefined ? crypto.randomUUID() : options.idempotencyKey,
206
221
  };
@@ -1,5 +1,5 @@
1
1
  import type { MonacoWebSocketConfig, SessionCredentials } from "@0xmonaco/types";
2
- export type { MessageHandler, MonacoWebSocket, ResyncHandler, StatusHandler, WebSocketResyncInfo } from "@0xmonaco/types";
2
+ export type { MessageHandler, MonacoWebSocket, ReauthenticationRequiredHandler, ResyncHandler, StatusHandler, WebSocketReauthenticationInfo, WebSocketResyncInfo, } from "@0xmonaco/types";
3
3
  /**
4
4
  * Options for {@link createMonacoWebSocket}. The reconnect/resync knobs and
5
5
  * callbacks are shared with the main SDK config via {@link MonacoWebSocketConfig}
@@ -155,6 +155,13 @@ function parseConditionalOrderEventData(raw) {
155
155
  cancelledAt: readOptionalString(data, "cancelledAt"),
156
156
  expiresAt: readOptionalString(data, "expiresAt"),
157
157
  failureReason: readOptionalString(data, "failureReason"),
158
+ // Trailing-stop fields (0XM-2827), explicit `null` on TP/SL rows. A live
159
+ // `ratcheted` frame carries the moved `triggerPrice` together with the
160
+ // `watermarkPrice` it was derived from; none of these is validated.
161
+ trailBps: readOptionalNumber(data, "trailBps"),
162
+ activationPrice: readOptionalString(data, "activationPrice"),
163
+ watermarkPrice: readOptionalString(data, "watermarkPrice"),
164
+ trailArmedAt: readOptionalString(data, "trailArmedAt"),
158
165
  // Free-form on the wire (the server field is a plain string), so it is
159
166
  // passed through untouched rather than matched against a value list.
160
167
  reason: readString(data, "reason"),
@@ -683,6 +690,15 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
683
690
  // Close code of the connection loss the next successful reopen recovers
684
691
  // from; null when no resync is owed (first connect, clean disconnect).
685
692
  let pendingResyncCode = null;
693
+ // Authentication-ending Error frame received on the current socket, tagged
694
+ // with the session it rejected. The server answers handshakes in the order
695
+ // they were sent, so each Authenticated / AUTH_FAILED reply belongs to the
696
+ // oldest pending handshake, and SESSION_INVALID revokes the session the
697
+ // server last confirmed. All of it resets per socket so a frame from one
698
+ // connection never decides how a later connection's close is read.
699
+ let lastAuthError = null;
700
+ let pendingAuthSessions = [];
701
+ let confirmedSession;
686
702
  // Handler storage: channel -> set of handlers (supports multiple subscribers per channel)
687
703
  const handlers = new Map();
688
704
  // Subscribe-time Snapshot frame handlers, keyed by the RAW subscribed
@@ -732,6 +748,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
732
748
  return;
733
749
  const timestamp = Date.now();
734
750
  const signature = signMessage(hexToBytes(session.privateKey), composeWsSigningString(session.publicKey, timestamp));
751
+ pendingAuthSessions.push(session);
735
752
  ws.send(JSON.stringify({
736
753
  type: "Authenticate",
737
754
  public_key: session.publicKey,
@@ -831,8 +848,8 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
831
848
  };
832
849
  const routeMessage = (msg) => {
833
850
  if (msg.type === "Error") {
834
- // Connection-scoped, not channel-scoped: an Error frame matches no
835
- // subscription, so without this it reached no consumer at all and a
851
+ // An Error frame matches no subscription even when it names a channel,
852
+ // so without this it reached no consumer at all and a
836
853
  // failed subscribe / invalidated session / unreadable subscribe-time
837
854
  // snapshot was silent. `code` is passed through whatever its value —
838
855
  // a code added server-side after this SDK shipped must still reach the
@@ -841,10 +858,31 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
841
858
  // and synthesizing the missing half would send the app down a branch on
842
859
  // data the server never sent.
843
860
  if (typeof msg.code === "string" && typeof msg.message === "string") {
844
- options.onError?.({ code: msg.code, message: msg.message });
861
+ // The server sends the reason a connection is ending as an Error
862
+ // frame just before the close; the close event decides from it.
863
+ // Recorded before any application code runs, so a throwing onError
864
+ // cannot make the close look transient.
865
+ if (msg.code === "AUTH_FAILED") {
866
+ lastAuthError = { code: msg.code, message: msg.message, session: pendingAuthSessions.shift() };
867
+ }
868
+ else if (msg.code === "SESSION_INVALID") {
869
+ lastAuthError = { code: msg.code, message: msg.message, session: confirmedSession ?? pendingAuthSessions[0] };
870
+ }
871
+ // A per-channel failure names the channel it concerns; the key is
872
+ // left off entirely otherwise, so a connection-wide failure reaches
873
+ // the app exactly as the server sent it. A non-string channel is
874
+ // dropped rather than the frame: the code and message are still
875
+ // actionable without it. (The frame is untyped JSON, so the declared
876
+ // `string` type of `channel` is not trusted here.)
877
+ const channel = msg.channel;
878
+ options.onError?.(typeof channel === "string" ? { code: msg.code, message: msg.message, channel } : { code: msg.code, message: msg.message });
845
879
  }
846
880
  return;
847
881
  }
882
+ if (msg.type === "Authenticated") {
883
+ confirmedSession = pendingAuthSessions.shift();
884
+ return;
885
+ }
848
886
  if (!msg.channel)
849
887
  return;
850
888
  if (msg.type === "Snapshot") {
@@ -876,6 +914,9 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
876
914
  try {
877
915
  const socket = new WebSocket(getUrl());
878
916
  ws = socket;
917
+ lastAuthError = null;
918
+ pendingAuthSessions = [];
919
+ confirmedSession = undefined;
879
920
  const timeout = setTimeout(() => {
880
921
  if (ws !== socket) {
881
922
  resolve();
@@ -947,6 +988,56 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
947
988
  }
948
989
  ws = null;
949
990
  stopHeartbeat();
991
+ // Terminal authentication loss: the server revoked the session
992
+ // (SESSION_INVALID, then 1008) or rejected this connection's
993
+ // handshake (AUTH_FAILED, then a close). Reconnecting would replay
994
+ // the same rejected credentials into the same rejection forever, so
995
+ // drop them, skip the reconnect, and let the app supply a fresh
996
+ // session. Handlers stay registered so that session resubscribes
997
+ // them. A 1008 without SESSION_INVALID (the message-rate cap) is a
998
+ // back-off-and-retry and falls through to the reconnect below, as
999
+ // does a close after the app already replaced the rejected session
1000
+ // (e.g. re-authenticating from onError): the fresh session is what
1001
+ // the reconnect uses.
1002
+ const authError = lastAuthError;
1003
+ const authLoss = authError !== null && authError.session !== undefined && (authError.code === "AUTH_FAILED" || event.code === 1008);
1004
+ const terminalAuthLoss = authLoss && session === authError.session;
1005
+ if (terminalAuthLoss) {
1006
+ session = undefined;
1007
+ // The reopen with a fresh session still owes the app a resync —
1008
+ // nothing was delivered while the session was dead — whether or
1009
+ // not automatic reconnect is on, since that reopen is driven by
1010
+ // the new session, not by the reconnect loop.
1011
+ if (pendingResyncCode === null) {
1012
+ pendingResyncCode = event.code;
1013
+ }
1014
+ try {
1015
+ options.onStatusChange?.("disconnected");
1016
+ }
1017
+ finally {
1018
+ // Recovery must not hinge on the status observer not throwing.
1019
+ options.onReauthenticationRequired?.({ code: authError.code, message: authError.message, closeCode: event.code });
1020
+ }
1021
+ return;
1022
+ }
1023
+ // The app already handed over a replacement session on this socket.
1024
+ // Had it arrived after the close, setSessionKeypair would have
1025
+ // connected with it; honour that even with automatic reconnect off
1026
+ // (with it on, the reconnect below uses the new session).
1027
+ if (authLoss && session !== undefined && !autoReconnect) {
1028
+ if (pendingResyncCode === null) {
1029
+ pendingResyncCode = event.code;
1030
+ }
1031
+ try {
1032
+ options.onStatusChange?.("disconnected");
1033
+ }
1034
+ finally {
1035
+ connect().catch((err) => {
1036
+ console.warn("WebSocket: Failed to connect with the replacement session:", err);
1037
+ });
1038
+ }
1039
+ return;
1040
+ }
950
1041
  // Reconnect on abnormal close. 1013 is the server's slow-client
951
1042
  // shed: the stream has a gap, so the reopen owes the app a resync.
952
1043
  // Keep the first owed code (upgrading to 1013 if it arrives later):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xmonaco/core",
3
- "version": "1.0.65",
3
+ "version": "1.0.66",
4
4
  "type": "module",
5
5
  "homepage": "https://docs.0xmonaco.com/sdk/typescript",
6
6
  "main": "./dist/index.js",
@@ -19,8 +19,8 @@
19
19
  "viem": "^2.45.2"
20
20
  },
21
21
  "dependencies": {
22
- "@0xmonaco/contracts": "1.0.65",
23
- "@0xmonaco/types": "1.0.65",
22
+ "@0xmonaco/contracts": "1.0.66",
23
+ "@0xmonaco/types": "1.0.66",
24
24
  "@noble/curves": "^1.9.1",
25
25
  "@noble/hashes": "^2.4.0",
26
26
  "http-status-codes": "^2.3.0"