@acta-markets/ts-sdk 0.0.4-beta → 0.0.5-beta

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
@@ -88,11 +88,14 @@ client.ws!.on("positions", (positions) => {
88
88
  console.log("first expiry", positions[0]?.expiry_ts);
89
89
  });
90
90
 
91
- client.ws!.getMarkets();
91
+ const marketsRequestId = client.ws!.getMarkets();
92
+ console.log("GetMarkets request_id:", marketsRequestId);
92
93
  // positions require auth:
93
94
  // client.ws!.getPositions();
94
95
  ```
95
96
 
97
+ Most WS read/query methods now return the generated `request_id` so UI code can correlate responses (`data.request_id`) without building custom request id plumbing.
98
+
96
99
  ### RFQ → accept quote → sign sponsored tx (wallet UX)
97
100
 
98
101
  ```ts
@@ -73,6 +73,21 @@ function getCloseInfo(ev) {
73
73
  reason: typeof rec.reason === "string" ? rec.reason : undefined,
74
74
  };
75
75
  }
76
+ function generateRequestId() {
77
+ const cryptoApi = globalThis.crypto;
78
+ if (cryptoApi?.randomUUID) {
79
+ return cryptoApi.randomUUID();
80
+ }
81
+ if (cryptoApi?.getRandomValues) {
82
+ const bytes = cryptoApi.getRandomValues(new Uint8Array(16));
83
+ // RFC4122 v4 bits: version=0100, variant=10xx.
84
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
85
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
86
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
87
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
88
+ }
89
+ return `req-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
90
+ }
76
91
  class ActaWsClient extends TypedEventEmitter {
77
92
  ws = null;
78
93
  options;
@@ -82,6 +97,7 @@ class ActaWsClient extends TypedEventEmitter {
82
97
  pendingResumeSessionId = null;
83
98
  connectionState = "disconnected";
84
99
  sessionId = null;
100
+ lastAuthSessionId = null;
85
101
  helloSent = false;
86
102
  welcomeReceived = false;
87
103
  pendingMessages = [];
@@ -124,6 +140,7 @@ class ActaWsClient extends TypedEventEmitter {
124
140
  this.authProvider = null;
125
141
  this.authRequested = false;
126
142
  this.pendingResumeSessionId = null;
143
+ this.lastAuthSessionId = null;
127
144
  this.shouldReconnect = this.options.autoReconnect;
128
145
  this.doConnect();
129
146
  }
@@ -223,47 +240,89 @@ class ActaWsClient extends TypedEventEmitter {
223
240
  }
224
241
  getPositions() {
225
242
  this.ensureAuthenticated();
226
- this.send({ type: "GetPositions" });
243
+ const requestId = this.nextRequestId();
244
+ this.send({
245
+ type: "GetPositions",
246
+ data: { request_id: requestId },
247
+ });
248
+ return requestId;
227
249
  }
228
250
  getMarkets() {
229
- this.send({ type: "GetMarkets" });
251
+ const requestId = this.nextRequestId();
252
+ this.send({
253
+ type: "GetMarkets",
254
+ data: { request_id: requestId },
255
+ });
256
+ return requestId;
230
257
  }
231
258
  getMarketDescriptors(args) {
259
+ const requestId = this.nextRequestId();
232
260
  const data = {
261
+ request_id: requestId,
233
262
  active_only: args?.active_only ?? true,
234
263
  };
235
264
  this.send({ type: "GetMarketDescriptors", data });
265
+ return requestId;
236
266
  }
237
267
  getExpiries(args) {
268
+ const requestId = this.nextRequestId();
238
269
  this.send({
239
270
  type: "GetExpiries",
240
271
  data: {
272
+ request_id: requestId,
241
273
  underlying_mint: args?.underlying_mint,
242
274
  quote_mint: args?.quote_mint,
243
275
  is_put: args?.is_put ?? null,
244
276
  },
245
277
  });
278
+ return requestId;
246
279
  }
247
280
  getTokens(args) {
281
+ const requestId = this.nextRequestId();
248
282
  const data = {
283
+ request_id: requestId,
249
284
  active_only: args?.active_only ?? true,
250
285
  };
251
286
  this.send({ type: "GetTokens", data });
287
+ return requestId;
252
288
  }
253
289
  getMyActiveRfqs() {
254
290
  this.ensureAuthenticated();
255
- this.send({ type: "GetMyActiveRfqs" });
291
+ const requestId = this.nextRequestId();
292
+ this.send({
293
+ type: "GetMyActiveRfqs",
294
+ data: { request_id: requestId },
295
+ });
296
+ return requestId;
256
297
  }
257
298
  getActiveRfqs() {
258
- this.send({ type: "GetActiveRfqs" });
299
+ const requestId = this.nextRequestId();
300
+ this.send({
301
+ type: "GetActiveRfqs",
302
+ data: { request_id: requestId },
303
+ });
304
+ return requestId;
305
+ }
306
+ logout() {
307
+ this.send({ type: "Logout" });
259
308
  }
260
309
  getOrderStatus(orderIdHex) {
261
310
  this.ensureAuthenticated();
262
- this.send({ type: "GetOrderStatus", data: { order_id: orderIdHex } });
311
+ const requestId = this.nextRequestId();
312
+ this.send({
313
+ type: "GetOrderStatus",
314
+ data: { request_id: requestId, order_id: orderIdHex },
315
+ });
316
+ return requestId;
263
317
  }
264
318
  cancelRfq(rfqId) {
265
319
  this.ensureAuthenticated();
266
- this.send({ type: "CancelRfq", data: { rfq_id: rfqId } });
320
+ const requestId = this.nextRequestId();
321
+ this.send({
322
+ type: "CancelRfq",
323
+ data: { rfq_id: rfqId, request_id: requestId },
324
+ });
325
+ return requestId;
267
326
  }
268
327
  submitQuote(quote) {
269
328
  this.ensureAuthenticated();
@@ -293,7 +352,12 @@ class ActaWsClient extends TypedEventEmitter {
293
352
  }
294
353
  cancelQuote(rfqId) {
295
354
  this.ensureAuthenticated();
296
- this.send({ type: "CancelQuote", data: { rfq_id: rfqId } });
355
+ const requestId = this.nextRequestId();
356
+ this.send({
357
+ type: "CancelQuote",
358
+ data: { rfq_id: rfqId, request_id: requestId },
359
+ });
360
+ return requestId;
297
361
  }
298
362
  subscribe(channels, markets) {
299
363
  this.ensureAuthenticated();
@@ -418,6 +482,14 @@ class ActaWsClient extends TypedEventEmitter {
418
482
  case "AuthError":
419
483
  this.handleAuthError(message.data.reason, message.data.message);
420
484
  break;
485
+ case "LogoutSuccess":
486
+ this.sessionId = null;
487
+ this.pendingResumeSessionId = null;
488
+ this.lastAuthSessionId = null;
489
+ this.startAuthSent = false;
490
+ this.setConnectionState("connecting");
491
+ this.emit("logoutSuccess");
492
+ break;
421
493
  case "Snapshot":
422
494
  this.handleSnapshot(message.data);
423
495
  break;
@@ -587,10 +659,12 @@ class ActaWsClient extends TypedEventEmitter {
587
659
  }
588
660
  /** Taker-only: request current indicative prices for a market + position_type. */
589
661
  getIndicativePrices(req) {
662
+ const requestId = this.nextRequestId();
590
663
  this.send({
591
664
  type: "GetIndicativePrices",
592
- data: req,
665
+ data: { ...req, request_id: requestId },
593
666
  });
667
+ return requestId;
594
668
  }
595
669
  /** Maker-only: respond to an indicative request (unsigned, non-binding). */
596
670
  sendIndicativePricesResponse(resp) {
@@ -619,8 +693,9 @@ class ActaWsClient extends TypedEventEmitter {
619
693
  }
620
694
  }
621
695
  async beginAuthHandshake() {
622
- if (this.pendingResumeSessionId) {
623
- this.sendResumeAuth(this.pendingResumeSessionId);
696
+ const resumeSessionId = this.pendingResumeSessionId ?? this.lastAuthSessionId;
697
+ if (resumeSessionId) {
698
+ this.sendResumeAuth(resumeSessionId);
624
699
  return;
625
700
  }
626
701
  await this.sendStartAuth();
@@ -657,6 +732,9 @@ class ActaWsClient extends TypedEventEmitter {
657
732
  handleAuthSuccess(sessionId, expiresAt) {
658
733
  this.sessionId = sessionId;
659
734
  this.pendingResumeSessionId = null;
735
+ if (expiresAt !== null) {
736
+ this.lastAuthSessionId = sessionId;
737
+ }
660
738
  this.setConnectionState("authenticated");
661
739
  this.emit("authenticated", sessionId, expiresAt);
662
740
  if (this.subscribedChannels.size > 0) {
@@ -668,12 +746,13 @@ class ActaWsClient extends TypedEventEmitter {
668
746
  }
669
747
  }
670
748
  handleAuthError(reason, message) {
671
- this.emit("authError", reason);
749
+ this.emit("authError", reason, message);
672
750
  if (reason === "session_expired" &&
673
751
  this.authRequested &&
674
752
  this.authProvider &&
675
- this.pendingResumeSessionId) {
753
+ (this.pendingResumeSessionId || this.lastAuthSessionId)) {
676
754
  this.pendingResumeSessionId = null;
755
+ this.lastAuthSessionId = null;
677
756
  this.startAuthSent = false;
678
757
  void this.sendStartAuth().catch((err) => {
679
758
  this.emit("error", err);
@@ -789,6 +868,9 @@ class ActaWsClient extends TypedEventEmitter {
789
868
  this.log("Cannot send, WebSocket not open");
790
869
  }
791
870
  }
871
+ nextRequestId() {
872
+ return generateRequestId();
873
+ }
792
874
  ensureAuthenticated() {
793
875
  if (this.connectionState !== "authenticated") {
794
876
  throw new Error("Client is not authenticated");
@@ -883,7 +965,6 @@ class ActaWsClient extends TypedEventEmitter {
883
965
  this.helloSent = false;
884
966
  this.welcomeReceived = false;
885
967
  this.startAuthSent = false;
886
- this.pendingResumeSessionId = null;
887
968
  this.pendingMessages = [];
888
969
  this.setConnectionState("disconnected");
889
970
  }
@@ -169,13 +169,123 @@ describe("ActaWsClient", () => {
169
169
  });
170
170
  it("emits authError reason on AuthError", () => {
171
171
  const { client } = makeHarness();
172
- const reasons = [];
173
- client.on("authError", (reason) => reasons.push(reason));
172
+ const errors = [];
173
+ client.on("authError", (reason, message) => errors.push({ reason, message }));
174
174
  client.handleMessage({
175
175
  type: "AuthError",
176
176
  data: { reason: "invalid_signature", message: "bad signature bytes" },
177
177
  });
178
- expect(reasons).toEqual(["invalid_signature"]);
178
+ expect(errors).toEqual([
179
+ { reason: "invalid_signature", message: "bad signature bytes" },
180
+ ]);
181
+ });
182
+ it("uses last auth session for resume-first reconnect", async () => {
183
+ const { client, socket } = makeHarness();
184
+ const auth = makeAuthProvider("WalletPubkey", "WalletSignature");
185
+ client.connectAndAuthenticate(auth);
186
+ let ws = socket();
187
+ ws.triggerOpen();
188
+ ws.triggerMessage(WELCOME_MESSAGE);
189
+ await flushMicrotasks();
190
+ ws.triggerMessage({
191
+ type: "AuthSuccess",
192
+ data: { session_id: "persisted-session", expires_at: 1_710_086_400 },
193
+ });
194
+ ws.triggerClose(1006, "network_drop");
195
+ client.connectAndAuthenticate(auth);
196
+ ws = socket();
197
+ ws.triggerOpen();
198
+ ws.triggerMessage(WELCOME_MESSAGE);
199
+ await flushMicrotasks();
200
+ const sentTypes = ws.sent.map((payload) => parseClientMessage(payload).type);
201
+ expect(sentTypes).toEqual(["Hello", "ResumeAuth"]);
202
+ const resumeAuth = parseClientMessage(ws.sent[1]);
203
+ expect(resumeAuth.type).toBe("ResumeAuth");
204
+ if (resumeAuth.type === "ResumeAuth") {
205
+ expect(resumeAuth.data.session_id).toBe("persisted-session");
206
+ }
207
+ });
208
+ it("sends Logout and emits logoutSuccess", () => {
209
+ const { client, socket } = makeHarness();
210
+ const events = [];
211
+ client.on("logoutSuccess", () => events.push("logoutSuccess"));
212
+ client.connectAnonymous();
213
+ const ws = socket();
214
+ ws.triggerOpen();
215
+ ws.triggerMessage(WELCOME_MESSAGE);
216
+ client.logout();
217
+ const sent = parseClientMessage(ws.sent[1]);
218
+ expect(sent.type).toBe("Logout");
219
+ ws.triggerMessage({ type: "LogoutSuccess", data: {} });
220
+ expect(events).toEqual(["logoutSuccess"]);
221
+ });
222
+ it("adds request_id to public request messages", () => {
223
+ const { client, socket } = makeHarness();
224
+ client.connectAnonymous();
225
+ const ws = socket();
226
+ ws.triggerOpen();
227
+ ws.triggerMessage(WELCOME_MESSAGE);
228
+ const getMarketsId = client.getMarkets();
229
+ const getMarketDescriptorsId = client.getMarketDescriptors();
230
+ const getTokensId = client.getTokens();
231
+ const getExpiriesId = client.getExpiries();
232
+ const getActiveRfqsId = client.getActiveRfqs();
233
+ const getIndicativePricesId = client.getIndicativePrices({
234
+ market: "market-1",
235
+ position_type: "covered_call",
236
+ });
237
+ const sent = ws.sent.slice(1).map(parseClientMessage);
238
+ const withRequestId = sent.filter((msg) => msg.type !== "Logout");
239
+ for (const msg of withRequestId) {
240
+ if ("data" in msg) {
241
+ const data = msg.data;
242
+ expect(typeof data.request_id).toBe("string");
243
+ expect(data.request_id.length).toBeGreaterThan(0);
244
+ }
245
+ }
246
+ expect(sent[0].data.request_id).toBe(getMarketsId);
247
+ expect(sent[1].data.request_id).toBe(getMarketDescriptorsId);
248
+ expect(sent[2].data.request_id).toBe(getTokensId);
249
+ expect(sent[3].data.request_id).toBe(getExpiriesId);
250
+ expect(sent[4].data.request_id).toBe(getActiveRfqsId);
251
+ expect(sent[5].data.request_id).toBe(getIndicativePricesId);
252
+ });
253
+ it("adds request_id to authenticated request messages", () => {
254
+ const { client, socket } = makeHarness();
255
+ client.connectAnonymous();
256
+ const ws = socket();
257
+ ws.triggerOpen();
258
+ ws.triggerMessage(WELCOME_MESSAGE);
259
+ client.handleMessage({
260
+ type: "AuthSuccess",
261
+ data: { session_id: "session-id", expires_at: 1_710_086_400 },
262
+ });
263
+ const getPositionsId = client.getPositions();
264
+ const getMyActiveRfqsId = client.getMyActiveRfqs();
265
+ const getOrderStatusId = client.getOrderStatus("11".repeat(32));
266
+ const cancelRfqId = client.cancelRfq("rfq-1");
267
+ const cancelQuoteId = client.cancelQuote("rfq-2");
268
+ const sent = ws.sent.slice(1).map(parseClientMessage);
269
+ const authRequests = sent.filter((msg) => [
270
+ "GetPositions",
271
+ "GetMyActiveRfqs",
272
+ "GetOrderStatus",
273
+ "CancelRfq",
274
+ "CancelQuote",
275
+ ].includes(msg.type));
276
+ expect(authRequests).toHaveLength(5);
277
+ for (const msg of authRequests) {
278
+ if ("data" in msg) {
279
+ const data = msg.data;
280
+ expect(typeof data.request_id).toBe("string");
281
+ expect(data.request_id.length).toBeGreaterThan(0);
282
+ }
283
+ }
284
+ expect(authRequests[0].data.request_id).toBe(getPositionsId);
285
+ expect(authRequests[1].data.request_id).toBe(getMyActiveRfqsId);
286
+ expect(authRequests[2].data.request_id).toBe(getOrderStatusId);
287
+ expect(authRequests[3].data.request_id).toBe(cancelRfqId);
288
+ expect(authRequests[4].data.request_id).toBe(cancelQuoteId);
179
289
  });
180
290
  it("drop_oldest policy keeps the latest queued messages", () => {
181
291
  const { client, socket } = makeHarness({
@@ -2,7 +2,7 @@
2
2
  import type { AuthProvider } from "./auth";
3
3
  import type { SignerLike } from "../chain/orders";
4
4
  import type { Address } from "@solana/addresses";
5
- import type { ActiveRfqInfo, ChainEventMessage, GlobalStats, MarketDescriptorInfo, MarketInfo, MyActiveRfqInfo, MyActiveRfqsMessage, OrderStatusMessage, PositionInfo, QuoteAcknowledgedMessage, QuoteBestStatusMessage, QuoteCancelledMessage, QuoteMessage, QuoteRefreshRequestedMessage, QuoteOutbidMessage, QuoteReceivedMessage, QuoteSelectedMessage, QuotesUpdateMessage, RfqBroadcastMessage, RfqClosedMessage, RfqCreatedMessage, RfqRequestMessage, RfqAvailableAgainMessage, QuoteExpiredMessage, QuoteFilledMessage, IndicativePricesMessage, IndicativePricesRequestMessage, IndicativePricesResponseMessage, GetIndicativePricesMessage, ServerMessage, SnapshotMessage, StatsDelta, SubscriptionsMessage, TokenInfo, TradeInfo, UuidString, VersionMismatchMessage, WelcomeMessage, WsChannel } from "./types";
5
+ import type { ActiveRfqInfo, ChainEventMessage, GlobalStats, MarketDescriptorInfo, MarketInfo, MyActiveRfqInfo, MyActiveRfqsMessage, OrderStatusMessage, PositionInfo, QuoteAcknowledgedMessage, QuoteBestStatusMessage, QuoteCancelledMessage, QuoteMessage, QuoteRefreshRequestedMessage, QuoteOutbidMessage, QuoteReceivedMessage, QuoteSelectedMessage, QuotesUpdateMessage, RfqBroadcastMessage, RfqClosedMessage, RfqCreatedMessage, RfqRequestMessage, RfqAvailableAgainMessage, QuoteExpiredMessage, QuoteFilledMessage, IndicativePricesMessage, IndicativePricesRequestMessage, IndicativePricesResponseMessage, GetIndicativePricesMessage, RequestId, ServerMessage, SnapshotMessage, StatsDelta, SubscriptionsMessage, TokenInfo, TradeInfo, UuidString, VersionMismatchMessage, WelcomeMessage, WsChannel } from "./types";
6
6
  export type ConnectionState = "disconnected" | "connecting" | "authenticating" | "authenticated" | "error";
7
7
  export type ClientRole = "taker" | "maker";
8
8
  export type PendingMessagesOverflowPolicy = "drop_oldest" | "drop_newest" | "throw";
@@ -57,7 +57,8 @@ export type ActaWsClientEvents = {
57
57
  welcome: (msg: WelcomeMessage) => void;
58
58
  versionMismatch: (msg: VersionMismatchMessage) => void;
59
59
  authenticated: (sessionId: string, expiresAt: number | null) => void;
60
- authError: (reason: string) => void;
60
+ authError: (reason: string, message?: string) => void;
61
+ logoutSuccess: () => void;
61
62
  disconnected: (code: number, reason: string) => void;
62
63
  error: (error: Error) => void;
63
64
  stateChange: (state: ConnectionState) => void;
@@ -151,6 +152,7 @@ export declare class ActaWsClient extends TypedEventEmitter<ActaWsClientEvents>
151
152
  private pendingResumeSessionId;
152
153
  private connectionState;
153
154
  private sessionId;
155
+ private lastAuthSessionId;
154
156
  private helloSent;
155
157
  private welcomeReceived;
156
158
  private pendingMessages;
@@ -195,23 +197,24 @@ export declare class ActaWsClient extends TypedEventEmitter<ActaWsClientEvents>
195
197
  orderIdHex: string;
196
198
  txBase64: string;
197
199
  }): Promise<void>;
198
- getPositions(): void;
199
- getMarkets(): void;
200
+ getPositions(): RequestId;
201
+ getMarkets(): RequestId;
200
202
  getMarketDescriptors(args?: {
201
203
  active_only?: boolean;
202
- }): void;
204
+ }): RequestId;
203
205
  getExpiries(args?: {
204
206
  underlying_mint?: Address<string>;
205
207
  quote_mint?: Address<string>;
206
208
  is_put?: boolean | null;
207
- }): void;
209
+ }): RequestId;
208
210
  getTokens(args?: {
209
211
  active_only?: boolean;
210
- }): void;
211
- getMyActiveRfqs(): void;
212
- getActiveRfqs(): void;
213
- getOrderStatus(orderIdHex: string): void;
214
- cancelRfq(rfqId: string): void;
212
+ }): RequestId;
213
+ getMyActiveRfqs(): RequestId;
214
+ getActiveRfqs(): RequestId;
215
+ logout(): void;
216
+ getOrderStatus(orderIdHex: string): RequestId;
217
+ cancelRfq(rfqId: string): RequestId;
215
218
  submitQuote(quote: QuoteMessage): void;
216
219
  /**
217
220
  * Convenience: sign 32-byte `orderId` and send `Quote`.
@@ -228,7 +231,7 @@ export declare class ActaWsClient extends TypedEventEmitter<ActaWsClientEvents>
228
231
  orderId: Uint8Array;
229
232
  makerSigner: SignerLike;
230
233
  }): Promise<void>;
231
- cancelQuote(rfqId: string): void;
234
+ cancelQuote(rfqId: string): RequestId;
232
235
  subscribe(channels: WsChannel[], markets?: string[]): void;
233
236
  unsubscribe(channels: WsChannel[]): void;
234
237
  ping(): void;
@@ -236,7 +239,7 @@ export declare class ActaWsClient extends TypedEventEmitter<ActaWsClientEvents>
236
239
  private doConnect;
237
240
  private handleMessage;
238
241
  /** Taker-only: request current indicative prices for a market + position_type. */
239
- getIndicativePrices(req: GetIndicativePricesMessage): void;
242
+ getIndicativePrices(req: Omit<GetIndicativePricesMessage, "request_id">): RequestId;
240
243
  /** Maker-only: respond to an indicative request (unsigned, non-binding). */
241
244
  sendIndicativePricesResponse(resp: IndicativePricesResponseMessage): void;
242
245
  private handleAuthRequest;
@@ -257,6 +260,7 @@ export declare class ActaWsClient extends TypedEventEmitter<ActaWsClientEvents>
257
260
  private handlePositionUpdated;
258
261
  private handleChainEvent;
259
262
  private send;
263
+ private nextRequestId;
260
264
  private ensureAuthenticated;
261
265
  private setConnectionState;
262
266
  private startPingInterval;
package/dist/ws/client.js CHANGED
@@ -70,6 +70,21 @@ function getCloseInfo(ev) {
70
70
  reason: typeof rec.reason === "string" ? rec.reason : undefined,
71
71
  };
72
72
  }
73
+ function generateRequestId() {
74
+ const cryptoApi = globalThis.crypto;
75
+ if (cryptoApi?.randomUUID) {
76
+ return cryptoApi.randomUUID();
77
+ }
78
+ if (cryptoApi?.getRandomValues) {
79
+ const bytes = cryptoApi.getRandomValues(new Uint8Array(16));
80
+ // RFC4122 v4 bits: version=0100, variant=10xx.
81
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
82
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
83
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
84
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
85
+ }
86
+ return `req-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
87
+ }
73
88
  export class ActaWsClient extends TypedEventEmitter {
74
89
  ws = null;
75
90
  options;
@@ -79,6 +94,7 @@ export class ActaWsClient extends TypedEventEmitter {
79
94
  pendingResumeSessionId = null;
80
95
  connectionState = "disconnected";
81
96
  sessionId = null;
97
+ lastAuthSessionId = null;
82
98
  helloSent = false;
83
99
  welcomeReceived = false;
84
100
  pendingMessages = [];
@@ -121,6 +137,7 @@ export class ActaWsClient extends TypedEventEmitter {
121
137
  this.authProvider = null;
122
138
  this.authRequested = false;
123
139
  this.pendingResumeSessionId = null;
140
+ this.lastAuthSessionId = null;
124
141
  this.shouldReconnect = this.options.autoReconnect;
125
142
  this.doConnect();
126
143
  }
@@ -220,47 +237,89 @@ export class ActaWsClient extends TypedEventEmitter {
220
237
  }
221
238
  getPositions() {
222
239
  this.ensureAuthenticated();
223
- this.send({ type: "GetPositions" });
240
+ const requestId = this.nextRequestId();
241
+ this.send({
242
+ type: "GetPositions",
243
+ data: { request_id: requestId },
244
+ });
245
+ return requestId;
224
246
  }
225
247
  getMarkets() {
226
- this.send({ type: "GetMarkets" });
248
+ const requestId = this.nextRequestId();
249
+ this.send({
250
+ type: "GetMarkets",
251
+ data: { request_id: requestId },
252
+ });
253
+ return requestId;
227
254
  }
228
255
  getMarketDescriptors(args) {
256
+ const requestId = this.nextRequestId();
229
257
  const data = {
258
+ request_id: requestId,
230
259
  active_only: args?.active_only ?? true,
231
260
  };
232
261
  this.send({ type: "GetMarketDescriptors", data });
262
+ return requestId;
233
263
  }
234
264
  getExpiries(args) {
265
+ const requestId = this.nextRequestId();
235
266
  this.send({
236
267
  type: "GetExpiries",
237
268
  data: {
269
+ request_id: requestId,
238
270
  underlying_mint: args?.underlying_mint,
239
271
  quote_mint: args?.quote_mint,
240
272
  is_put: args?.is_put ?? null,
241
273
  },
242
274
  });
275
+ return requestId;
243
276
  }
244
277
  getTokens(args) {
278
+ const requestId = this.nextRequestId();
245
279
  const data = {
280
+ request_id: requestId,
246
281
  active_only: args?.active_only ?? true,
247
282
  };
248
283
  this.send({ type: "GetTokens", data });
284
+ return requestId;
249
285
  }
250
286
  getMyActiveRfqs() {
251
287
  this.ensureAuthenticated();
252
- this.send({ type: "GetMyActiveRfqs" });
288
+ const requestId = this.nextRequestId();
289
+ this.send({
290
+ type: "GetMyActiveRfqs",
291
+ data: { request_id: requestId },
292
+ });
293
+ return requestId;
253
294
  }
254
295
  getActiveRfqs() {
255
- this.send({ type: "GetActiveRfqs" });
296
+ const requestId = this.nextRequestId();
297
+ this.send({
298
+ type: "GetActiveRfqs",
299
+ data: { request_id: requestId },
300
+ });
301
+ return requestId;
302
+ }
303
+ logout() {
304
+ this.send({ type: "Logout" });
256
305
  }
257
306
  getOrderStatus(orderIdHex) {
258
307
  this.ensureAuthenticated();
259
- this.send({ type: "GetOrderStatus", data: { order_id: orderIdHex } });
308
+ const requestId = this.nextRequestId();
309
+ this.send({
310
+ type: "GetOrderStatus",
311
+ data: { request_id: requestId, order_id: orderIdHex },
312
+ });
313
+ return requestId;
260
314
  }
261
315
  cancelRfq(rfqId) {
262
316
  this.ensureAuthenticated();
263
- this.send({ type: "CancelRfq", data: { rfq_id: rfqId } });
317
+ const requestId = this.nextRequestId();
318
+ this.send({
319
+ type: "CancelRfq",
320
+ data: { rfq_id: rfqId, request_id: requestId },
321
+ });
322
+ return requestId;
264
323
  }
265
324
  submitQuote(quote) {
266
325
  this.ensureAuthenticated();
@@ -290,7 +349,12 @@ export class ActaWsClient extends TypedEventEmitter {
290
349
  }
291
350
  cancelQuote(rfqId) {
292
351
  this.ensureAuthenticated();
293
- this.send({ type: "CancelQuote", data: { rfq_id: rfqId } });
352
+ const requestId = this.nextRequestId();
353
+ this.send({
354
+ type: "CancelQuote",
355
+ data: { rfq_id: rfqId, request_id: requestId },
356
+ });
357
+ return requestId;
294
358
  }
295
359
  subscribe(channels, markets) {
296
360
  this.ensureAuthenticated();
@@ -415,6 +479,14 @@ export class ActaWsClient extends TypedEventEmitter {
415
479
  case "AuthError":
416
480
  this.handleAuthError(message.data.reason, message.data.message);
417
481
  break;
482
+ case "LogoutSuccess":
483
+ this.sessionId = null;
484
+ this.pendingResumeSessionId = null;
485
+ this.lastAuthSessionId = null;
486
+ this.startAuthSent = false;
487
+ this.setConnectionState("connecting");
488
+ this.emit("logoutSuccess");
489
+ break;
418
490
  case "Snapshot":
419
491
  this.handleSnapshot(message.data);
420
492
  break;
@@ -584,10 +656,12 @@ export class ActaWsClient extends TypedEventEmitter {
584
656
  }
585
657
  /** Taker-only: request current indicative prices for a market + position_type. */
586
658
  getIndicativePrices(req) {
659
+ const requestId = this.nextRequestId();
587
660
  this.send({
588
661
  type: "GetIndicativePrices",
589
- data: req,
662
+ data: { ...req, request_id: requestId },
590
663
  });
664
+ return requestId;
591
665
  }
592
666
  /** Maker-only: respond to an indicative request (unsigned, non-binding). */
593
667
  sendIndicativePricesResponse(resp) {
@@ -616,8 +690,9 @@ export class ActaWsClient extends TypedEventEmitter {
616
690
  }
617
691
  }
618
692
  async beginAuthHandshake() {
619
- if (this.pendingResumeSessionId) {
620
- this.sendResumeAuth(this.pendingResumeSessionId);
693
+ const resumeSessionId = this.pendingResumeSessionId ?? this.lastAuthSessionId;
694
+ if (resumeSessionId) {
695
+ this.sendResumeAuth(resumeSessionId);
621
696
  return;
622
697
  }
623
698
  await this.sendStartAuth();
@@ -654,6 +729,9 @@ export class ActaWsClient extends TypedEventEmitter {
654
729
  handleAuthSuccess(sessionId, expiresAt) {
655
730
  this.sessionId = sessionId;
656
731
  this.pendingResumeSessionId = null;
732
+ if (expiresAt !== null) {
733
+ this.lastAuthSessionId = sessionId;
734
+ }
657
735
  this.setConnectionState("authenticated");
658
736
  this.emit("authenticated", sessionId, expiresAt);
659
737
  if (this.subscribedChannels.size > 0) {
@@ -665,12 +743,13 @@ export class ActaWsClient extends TypedEventEmitter {
665
743
  }
666
744
  }
667
745
  handleAuthError(reason, message) {
668
- this.emit("authError", reason);
746
+ this.emit("authError", reason, message);
669
747
  if (reason === "session_expired" &&
670
748
  this.authRequested &&
671
749
  this.authProvider &&
672
- this.pendingResumeSessionId) {
750
+ (this.pendingResumeSessionId || this.lastAuthSessionId)) {
673
751
  this.pendingResumeSessionId = null;
752
+ this.lastAuthSessionId = null;
674
753
  this.startAuthSent = false;
675
754
  void this.sendStartAuth().catch((err) => {
676
755
  this.emit("error", err);
@@ -786,6 +865,9 @@ export class ActaWsClient extends TypedEventEmitter {
786
865
  this.log("Cannot send, WebSocket not open");
787
866
  }
788
867
  }
868
+ nextRequestId() {
869
+ return generateRequestId();
870
+ }
789
871
  ensureAuthenticated() {
790
872
  if (this.connectionState !== "authenticated") {
791
873
  throw new Error("Client is not authenticated");
@@ -880,7 +962,6 @@ export class ActaWsClient extends TypedEventEmitter {
880
962
  this.helloSent = false;
881
963
  this.welcomeReceived = false;
882
964
  this.startAuthSent = false;
883
- this.pendingResumeSessionId = null;
884
965
  this.pendingMessages = [];
885
966
  this.setConnectionState("disconnected");
886
967
  }
@@ -167,13 +167,123 @@ describe("ActaWsClient", () => {
167
167
  });
168
168
  it("emits authError reason on AuthError", () => {
169
169
  const { client } = makeHarness();
170
- const reasons = [];
171
- client.on("authError", (reason) => reasons.push(reason));
170
+ const errors = [];
171
+ client.on("authError", (reason, message) => errors.push({ reason, message }));
172
172
  client.handleMessage({
173
173
  type: "AuthError",
174
174
  data: { reason: "invalid_signature", message: "bad signature bytes" },
175
175
  });
176
- expect(reasons).toEqual(["invalid_signature"]);
176
+ expect(errors).toEqual([
177
+ { reason: "invalid_signature", message: "bad signature bytes" },
178
+ ]);
179
+ });
180
+ it("uses last auth session for resume-first reconnect", async () => {
181
+ const { client, socket } = makeHarness();
182
+ const auth = makeAuthProvider("WalletPubkey", "WalletSignature");
183
+ client.connectAndAuthenticate(auth);
184
+ let ws = socket();
185
+ ws.triggerOpen();
186
+ ws.triggerMessage(WELCOME_MESSAGE);
187
+ await flushMicrotasks();
188
+ ws.triggerMessage({
189
+ type: "AuthSuccess",
190
+ data: { session_id: "persisted-session", expires_at: 1_710_086_400 },
191
+ });
192
+ ws.triggerClose(1006, "network_drop");
193
+ client.connectAndAuthenticate(auth);
194
+ ws = socket();
195
+ ws.triggerOpen();
196
+ ws.triggerMessage(WELCOME_MESSAGE);
197
+ await flushMicrotasks();
198
+ const sentTypes = ws.sent.map((payload) => parseClientMessage(payload).type);
199
+ expect(sentTypes).toEqual(["Hello", "ResumeAuth"]);
200
+ const resumeAuth = parseClientMessage(ws.sent[1]);
201
+ expect(resumeAuth.type).toBe("ResumeAuth");
202
+ if (resumeAuth.type === "ResumeAuth") {
203
+ expect(resumeAuth.data.session_id).toBe("persisted-session");
204
+ }
205
+ });
206
+ it("sends Logout and emits logoutSuccess", () => {
207
+ const { client, socket } = makeHarness();
208
+ const events = [];
209
+ client.on("logoutSuccess", () => events.push("logoutSuccess"));
210
+ client.connectAnonymous();
211
+ const ws = socket();
212
+ ws.triggerOpen();
213
+ ws.triggerMessage(WELCOME_MESSAGE);
214
+ client.logout();
215
+ const sent = parseClientMessage(ws.sent[1]);
216
+ expect(sent.type).toBe("Logout");
217
+ ws.triggerMessage({ type: "LogoutSuccess", data: {} });
218
+ expect(events).toEqual(["logoutSuccess"]);
219
+ });
220
+ it("adds request_id to public request messages", () => {
221
+ const { client, socket } = makeHarness();
222
+ client.connectAnonymous();
223
+ const ws = socket();
224
+ ws.triggerOpen();
225
+ ws.triggerMessage(WELCOME_MESSAGE);
226
+ const getMarketsId = client.getMarkets();
227
+ const getMarketDescriptorsId = client.getMarketDescriptors();
228
+ const getTokensId = client.getTokens();
229
+ const getExpiriesId = client.getExpiries();
230
+ const getActiveRfqsId = client.getActiveRfqs();
231
+ const getIndicativePricesId = client.getIndicativePrices({
232
+ market: "market-1",
233
+ position_type: "covered_call",
234
+ });
235
+ const sent = ws.sent.slice(1).map(parseClientMessage);
236
+ const withRequestId = sent.filter((msg) => msg.type !== "Logout");
237
+ for (const msg of withRequestId) {
238
+ if ("data" in msg) {
239
+ const data = msg.data;
240
+ expect(typeof data.request_id).toBe("string");
241
+ expect(data.request_id.length).toBeGreaterThan(0);
242
+ }
243
+ }
244
+ expect(sent[0].data.request_id).toBe(getMarketsId);
245
+ expect(sent[1].data.request_id).toBe(getMarketDescriptorsId);
246
+ expect(sent[2].data.request_id).toBe(getTokensId);
247
+ expect(sent[3].data.request_id).toBe(getExpiriesId);
248
+ expect(sent[4].data.request_id).toBe(getActiveRfqsId);
249
+ expect(sent[5].data.request_id).toBe(getIndicativePricesId);
250
+ });
251
+ it("adds request_id to authenticated request messages", () => {
252
+ const { client, socket } = makeHarness();
253
+ client.connectAnonymous();
254
+ const ws = socket();
255
+ ws.triggerOpen();
256
+ ws.triggerMessage(WELCOME_MESSAGE);
257
+ client.handleMessage({
258
+ type: "AuthSuccess",
259
+ data: { session_id: "session-id", expires_at: 1_710_086_400 },
260
+ });
261
+ const getPositionsId = client.getPositions();
262
+ const getMyActiveRfqsId = client.getMyActiveRfqs();
263
+ const getOrderStatusId = client.getOrderStatus("11".repeat(32));
264
+ const cancelRfqId = client.cancelRfq("rfq-1");
265
+ const cancelQuoteId = client.cancelQuote("rfq-2");
266
+ const sent = ws.sent.slice(1).map(parseClientMessage);
267
+ const authRequests = sent.filter((msg) => [
268
+ "GetPositions",
269
+ "GetMyActiveRfqs",
270
+ "GetOrderStatus",
271
+ "CancelRfq",
272
+ "CancelQuote",
273
+ ].includes(msg.type));
274
+ expect(authRequests).toHaveLength(5);
275
+ for (const msg of authRequests) {
276
+ if ("data" in msg) {
277
+ const data = msg.data;
278
+ expect(typeof data.request_id).toBe("string");
279
+ expect(data.request_id.length).toBeGreaterThan(0);
280
+ }
281
+ }
282
+ expect(authRequests[0].data.request_id).toBe(getPositionsId);
283
+ expect(authRequests[1].data.request_id).toBe(getMyActiveRfqsId);
284
+ expect(authRequests[2].data.request_id).toBe(getOrderStatusId);
285
+ expect(authRequests[3].data.request_id).toBe(cancelRfqId);
286
+ expect(authRequests[4].data.request_id).toBe(cancelQuoteId);
177
287
  });
178
288
  it("drop_oldest policy keeps the latest queued messages", () => {
179
289
  const { client, socket } = makeHarness({
@@ -83,6 +83,8 @@ export type ClientMessage = {
83
83
  data: {
84
84
  session_id: string;
85
85
  };
86
+ } | {
87
+ type: "Logout";
86
88
  } | {
87
89
  type: "AuthChallenge";
88
90
  data: AuthChallengeData;
@@ -93,6 +95,7 @@ export type ClientMessage = {
93
95
  type: "CancelQuote";
94
96
  data: {
95
97
  rfq_id: UuidString;
98
+ request_id: RequestId;
96
99
  };
97
100
  } | {
98
101
  type: "IndicativePricesResponse";
@@ -113,14 +116,17 @@ export type ClientMessage = {
113
116
  type: "CancelRfq";
114
117
  data: {
115
118
  rfq_id: UuidString;
119
+ request_id: RequestId;
116
120
  };
117
121
  } | {
118
122
  type: "GetIndicativePrices";
119
123
  data: GetIndicativePricesMessage;
120
124
  } | {
121
125
  type: "GetPositions";
126
+ data: GetPositionsMessage;
122
127
  } | {
123
128
  type: "GetMarkets";
129
+ data: GetMarketsMessage;
124
130
  } | {
125
131
  type: "GetMarketDescriptors";
126
132
  data: GetMarketDescriptorsMessage;
@@ -132,11 +138,13 @@ export type ClientMessage = {
132
138
  data: GetTokensMessage;
133
139
  } | {
134
140
  type: "GetMyActiveRfqs";
141
+ data: GetMyActiveRfqsMessage;
135
142
  } | {
136
143
  type: "GetOrderStatus";
137
144
  data: GetOrderStatusMessage;
138
145
  } | {
139
146
  type: "GetActiveRfqs";
147
+ data: GetActiveRfqsMessage;
140
148
  } | {
141
149
  type: "GetMakerPositions";
142
150
  data: GetMakerPositionsMessage;
@@ -148,8 +156,10 @@ export type ClientMessage = {
148
156
  data: GetMarketsForMakerMessage;
149
157
  } | {
150
158
  type: "GetMakerBalances";
159
+ data: GetMakerBalancesMessage;
151
160
  } | {
152
161
  type: "GetSubscriptions";
162
+ data: GetSubscriptionsMessage;
153
163
  } | {
154
164
  type: "CancelAllQuotes";
155
165
  data: CancelAllQuotesMessage;
@@ -168,29 +178,54 @@ export type ClientMessage = {
168
178
  };
169
179
  };
170
180
  export type GetMarketDescriptorsMessage = {
181
+ request_id: RequestId;
171
182
  active_only?: boolean;
172
183
  };
173
184
  export type GetTokensMessage = {
185
+ request_id: RequestId;
174
186
  active_only?: boolean;
175
187
  };
176
188
  export type GetExpiriesMessage = {
189
+ request_id: RequestId;
177
190
  underlying_mint?: Address<string>;
178
191
  quote_mint?: Address<string>;
179
192
  is_put?: boolean | null;
180
193
  };
194
+ export type GetPositionsMessage = {
195
+ request_id: RequestId;
196
+ };
197
+ export type GetMarketsMessage = {
198
+ request_id: RequestId;
199
+ };
200
+ export type GetMyActiveRfqsMessage = {
201
+ request_id: RequestId;
202
+ };
203
+ export type GetActiveRfqsMessage = {
204
+ request_id: RequestId;
205
+ };
206
+ export type GetMakerBalancesMessage = {
207
+ request_id: RequestId;
208
+ };
209
+ export type GetSubscriptionsMessage = {
210
+ request_id: RequestId;
211
+ };
181
212
  export type GetOrderStatusMessage = {
213
+ request_id: RequestId;
182
214
  order_id: OrderIdHex32;
183
215
  };
184
216
  export type GetMakerPositionsMessage = {
217
+ request_id: RequestId;
185
218
  market?: string;
186
219
  underlying_mint?: string;
187
220
  status?: string[];
188
221
  min_expiry_ts?: WsU64;
189
222
  };
190
223
  export type GetMyQuotesMessage = {
224
+ request_id: RequestId;
191
225
  active_only?: boolean;
192
226
  };
193
227
  export type GetMarketsForMakerMessage = {
228
+ request_id: RequestId;
194
229
  underlying_mints?: string[];
195
230
  quote_mints?: string[];
196
231
  min_expiry_ts?: WsU64;
@@ -199,6 +234,7 @@ export type GetMarketsForMakerMessage = {
199
234
  include_stats?: boolean;
200
235
  };
201
236
  export type CancelAllQuotesMessage = {
237
+ request_id: RequestId;
202
238
  market?: string;
203
239
  };
204
240
  export type AuthChallengeData = {
@@ -268,6 +304,9 @@ export type ServerMessage = {
268
304
  reason: ErrorMessage;
269
305
  message?: string;
270
306
  };
307
+ } | {
308
+ type: "LogoutSuccess";
309
+ data: Record<string, never>;
271
310
  } | {
272
311
  type: "RfqCreated";
273
312
  data: RfqCreatedMessage;
@@ -319,6 +358,7 @@ export type ServerMessage = {
319
358
  } | {
320
359
  type: "ActiveRfqs";
321
360
  data: {
361
+ request_id: RequestId;
322
362
  rfqs: ActiveRfqInfo[];
323
363
  };
324
364
  } | {
@@ -625,11 +665,14 @@ export type QuoteExpiredMessage = {
625
665
  reason: QuoteExpiredReason;
626
666
  };
627
667
  export type MakerPositionsMessage = {
668
+ request_id: RequestId;
628
669
  positions: MakerPositionInfo[];
629
670
  };
630
671
  export type MakerPositionInfo = {
631
672
  pda: string;
632
673
  market: string;
674
+ underlying_mint: string;
675
+ quote_mint: string;
633
676
  position_type: PositionType;
634
677
  status: PositionStatus;
635
678
  strike: WsU64;
@@ -642,6 +685,7 @@ export type MakerPositionInfo = {
642
685
  expiry_ts: WsU64;
643
686
  };
644
687
  export type MyQuotesMessage = {
688
+ request_id: RequestId;
645
689
  quotes: MakerQuoteInfo[];
646
690
  };
647
691
  export type MakerQuoteInfo = {
@@ -657,6 +701,7 @@ export type MakerQuoteInfo = {
657
701
  created_at: WsU64;
658
702
  };
659
703
  export type MakerMarketsMessage = {
704
+ request_id: RequestId;
660
705
  markets: MakerMarketInfo[];
661
706
  };
662
707
  export type MakerMarketInfo = {
@@ -675,6 +720,7 @@ export type MarketStats = {
675
720
  trades_24h: WsU32;
676
721
  };
677
722
  export type MakerBalancesMessage = {
723
+ request_id: RequestId;
678
724
  balances_by_mint: Record<string, MakerMintBalance>;
679
725
  };
680
726
  export type MakerMintBalance = {
@@ -683,6 +729,7 @@ export type MakerMintBalance = {
683
729
  available: WsU64;
684
730
  };
685
731
  export type SubscriptionsMessage = {
732
+ request_id: RequestId;
686
733
  channels: WsChannel[];
687
734
  markets?: string[];
688
735
  };
@@ -702,10 +749,12 @@ export type IndicativePricesResponseMessage = {
702
749
  }>;
703
750
  };
704
751
  export type GetIndicativePricesMessage = {
752
+ request_id: RequestId;
705
753
  market: Address<string>;
706
754
  position_type: PositionType;
707
755
  };
708
756
  export type IndicativePricesMessage = {
757
+ request_id: RequestId;
709
758
  market: Address<string>;
710
759
  position_type: PositionType;
711
760
  updated_at: WsU64;
@@ -746,18 +795,23 @@ export type QuotesUpdateMessage = {
746
795
  quotes: QuoteReceivedMessage[];
747
796
  };
748
797
  export type PositionsMessage = {
798
+ request_id: RequestId;
749
799
  positions: PositionInfo[];
750
800
  };
751
801
  export type MarketsMessage = {
802
+ request_id: RequestId;
752
803
  markets: MarketInfo[];
753
804
  };
754
805
  export type MarketDescriptorsMessage = {
806
+ request_id: RequestId;
755
807
  markets: MarketDescriptorInfo[];
756
808
  };
757
809
  export type ExpiriesMessage = {
810
+ request_id: RequestId;
758
811
  expiries_ts: WsU64[];
759
812
  };
760
813
  export type TokensMessage = {
814
+ request_id: RequestId;
761
815
  underlyings: TokenInfo[];
762
816
  quotes_by_underlying: Record<Address<string>, TokenInfo[]>;
763
817
  };
@@ -769,9 +823,11 @@ export type SnapshotMessage = {
769
823
  markets: MarketInfo[];
770
824
  };
771
825
  export type MyActiveRfqsMessage = {
826
+ request_id: RequestId;
772
827
  rfqs: MyActiveRfqInfo[];
773
828
  };
774
829
  export type OrderStatusMessage = {
830
+ request_id: RequestId;
775
831
  order_id: OrderIdHex32;
776
832
  status: OrderStatusValue;
777
833
  rfq_id?: UuidString | null;
@@ -817,6 +873,8 @@ export type RfqOrderOption = {
817
873
  export type PositionInfo = {
818
874
  pda: string;
819
875
  market: string;
876
+ underlying_mint: string;
877
+ quote_mint: string;
820
878
  position_type: PositionType;
821
879
  status: PositionStatus;
822
880
  strike: WsU64;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acta-markets/ts-sdk",
3
- "version": "0.0.4-beta",
3
+ "version": "0.0.5-beta",
4
4
  "description": "TypeScript SDK for Acta Protocol",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",