@acta-markets/ts-sdk 0.0.4-beta → 0.0.6-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 +4 -1
- package/dist/cjs/events.test.js +6 -0
- package/dist/cjs/generated/types/actaEvent.js +6 -0
- package/dist/cjs/idl/acta_contract.json +17 -0
- package/dist/cjs/idl/hash.js +1 -1
- package/dist/cjs/ws/client.js +109 -14
- package/dist/cjs/ws/client.test.js +199 -3
- package/dist/cjs/ws/discovery.js +2 -0
- package/dist/cjs/ws/wirePolicy.js +16 -0
- package/dist/cjs/ws/wirePolicy.test.js +22 -0
- package/dist/events.test.js +6 -0
- package/dist/generated/types/actaEvent.d.ts +6 -0
- package/dist/generated/types/actaEvent.js +6 -0
- package/dist/idl/acta_contract.json +17 -0
- package/dist/idl/hash.d.ts +1 -1
- package/dist/idl/hash.js +1 -1
- package/dist/ws/client.d.ts +18 -13
- package/dist/ws/client.js +110 -15
- package/dist/ws/client.test.js +199 -3
- package/dist/ws/discovery.js +2 -0
- package/dist/ws/types.d.ts +70 -5
- package/dist/ws/wirePolicy.d.ts +6 -0
- package/dist/ws/wirePolicy.js +15 -0
- package/dist/ws/wirePolicy.test.d.ts +1 -0
- package/dist/ws/wirePolicy.test.js +20 -0
- package/package.json +1 -1
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
|
package/dist/cjs/events.test.js
CHANGED
|
@@ -184,6 +184,8 @@ describe("events parsing", () => {
|
|
|
184
184
|
Buffer.from(pk(3)),
|
|
185
185
|
u64(777),
|
|
186
186
|
Buffer.from([1]), // is_put
|
|
187
|
+
Buffer.from([6]), // underlying_decimals
|
|
188
|
+
Buffer.from([9]), // quote_decimals
|
|
187
189
|
]));
|
|
188
190
|
const d = (0, events_1.decodeActaEventLine)(line);
|
|
189
191
|
expect(d.kind).toBe(events_1.EventKind.CreateMarket);
|
|
@@ -194,6 +196,8 @@ describe("events parsing", () => {
|
|
|
194
196
|
expect(d.event.quoteMint).toBe(addr(3));
|
|
195
197
|
expect(d.event.expiryTs).toBe(777n);
|
|
196
198
|
expect(d.event.isPut).toBe(1);
|
|
199
|
+
expect(d.event.underlyingDecimals).toBe(6);
|
|
200
|
+
expect(d.event.quoteDecimals).toBe(9);
|
|
197
201
|
}
|
|
198
202
|
}
|
|
199
203
|
// 9: FinalizeMarket
|
|
@@ -215,6 +219,7 @@ describe("events parsing", () => {
|
|
|
215
219
|
u64(222),
|
|
216
220
|
Buffer.from([1]),
|
|
217
221
|
Buffer.from(pk(3)),
|
|
222
|
+
Buffer.from(pk(9)),
|
|
218
223
|
]));
|
|
219
224
|
const d = (0, events_1.decodeActaEventLine)(line);
|
|
220
225
|
expect(d.kind).toBe(events_1.EventKind.CreateOracle);
|
|
@@ -225,6 +230,7 @@ describe("events parsing", () => {
|
|
|
225
230
|
expect(d.event.expiryTs).toBe(222n);
|
|
226
231
|
expect(d.event.oracleType).toBe(1);
|
|
227
232
|
expect(d.event.authority).toBe(addr(3));
|
|
233
|
+
expect(Buffer.from(d.event.feedId)).toEqual(Buffer.from(pk(9)));
|
|
228
234
|
}
|
|
229
235
|
}
|
|
230
236
|
// 15: UpdateOraclePrice
|
|
@@ -89,6 +89,8 @@ function getActaEventEncoder() {
|
|
|
89
89
|
["quoteMint", (0, kit_1.getAddressEncoder)()],
|
|
90
90
|
["expiryTs", (0, kit_1.getU64Encoder)()],
|
|
91
91
|
["isPut", (0, kit_1.getU8Encoder)()],
|
|
92
|
+
["underlyingDecimals", (0, kit_1.getU8Encoder)()],
|
|
93
|
+
["quoteDecimals", (0, kit_1.getU8Encoder)()],
|
|
92
94
|
]),
|
|
93
95
|
],
|
|
94
96
|
[
|
|
@@ -142,6 +144,7 @@ function getActaEventEncoder() {
|
|
|
142
144
|
["expiryTs", (0, kit_1.getU64Encoder)()],
|
|
143
145
|
["oracleType", (0, kit_1.getU8Encoder)()],
|
|
144
146
|
["authority", (0, kit_1.getAddressEncoder)()],
|
|
147
|
+
["feedId", (0, kit_1.fixEncoderSize)((0, kit_1.getBytesEncoder)(), 32)],
|
|
145
148
|
]),
|
|
146
149
|
],
|
|
147
150
|
[
|
|
@@ -247,6 +250,8 @@ function getActaEventDecoder() {
|
|
|
247
250
|
["quoteMint", (0, kit_1.getAddressDecoder)()],
|
|
248
251
|
["expiryTs", (0, kit_1.getU64Decoder)()],
|
|
249
252
|
["isPut", (0, kit_1.getU8Decoder)()],
|
|
253
|
+
["underlyingDecimals", (0, kit_1.getU8Decoder)()],
|
|
254
|
+
["quoteDecimals", (0, kit_1.getU8Decoder)()],
|
|
250
255
|
]),
|
|
251
256
|
],
|
|
252
257
|
[
|
|
@@ -300,6 +305,7 @@ function getActaEventDecoder() {
|
|
|
300
305
|
["expiryTs", (0, kit_1.getU64Decoder)()],
|
|
301
306
|
["oracleType", (0, kit_1.getU8Decoder)()],
|
|
302
307
|
["authority", (0, kit_1.getAddressDecoder)()],
|
|
308
|
+
["feedId", (0, kit_1.fixDecoderSize)((0, kit_1.getBytesDecoder)(), 32)],
|
|
303
309
|
]),
|
|
304
310
|
],
|
|
305
311
|
[
|
|
@@ -1883,6 +1883,14 @@
|
|
|
1883
1883
|
{
|
|
1884
1884
|
"name": "is_put",
|
|
1885
1885
|
"type": "u8"
|
|
1886
|
+
},
|
|
1887
|
+
{
|
|
1888
|
+
"name": "underlying_decimals",
|
|
1889
|
+
"type": "u8"
|
|
1890
|
+
},
|
|
1891
|
+
{
|
|
1892
|
+
"name": "quote_decimals",
|
|
1893
|
+
"type": "u8"
|
|
1886
1894
|
}
|
|
1887
1895
|
]
|
|
1888
1896
|
},
|
|
@@ -2030,6 +2038,15 @@
|
|
|
2030
2038
|
{
|
|
2031
2039
|
"name": "authority",
|
|
2032
2040
|
"type": "publicKey"
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
"name": "feed_id",
|
|
2044
|
+
"type": {
|
|
2045
|
+
"array": [
|
|
2046
|
+
"u8",
|
|
2047
|
+
32
|
|
2048
|
+
]
|
|
2049
|
+
}
|
|
2033
2050
|
}
|
|
2034
2051
|
]
|
|
2035
2052
|
},
|
package/dist/cjs/idl/hash.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ACTA_IDL_SHA256 = void 0;
|
|
4
|
-
exports.ACTA_IDL_SHA256 = "
|
|
4
|
+
exports.ACTA_IDL_SHA256 = "704677f7071ecbe98f442fb62468d882329b906950a707fc47aad0f38683636f";
|
package/dist/cjs/ws/client.js
CHANGED
|
@@ -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 = [];
|
|
@@ -92,6 +108,7 @@ class ActaWsClient extends TypedEventEmitter {
|
|
|
92
108
|
subscribedChannels = new Set(["rfqs"]);
|
|
93
109
|
subscribedMarkets = new Set();
|
|
94
110
|
hasMarketScope = false;
|
|
111
|
+
marketDescriptorsByMarket = new Map();
|
|
95
112
|
state = {
|
|
96
113
|
stats: null,
|
|
97
114
|
activeRfqs: new Map(),
|
|
@@ -124,6 +141,7 @@ class ActaWsClient extends TypedEventEmitter {
|
|
|
124
141
|
this.authProvider = null;
|
|
125
142
|
this.authRequested = false;
|
|
126
143
|
this.pendingResumeSessionId = null;
|
|
144
|
+
this.lastAuthSessionId = null;
|
|
127
145
|
this.shouldReconnect = this.options.autoReconnect;
|
|
128
146
|
this.doConnect();
|
|
129
147
|
}
|
|
@@ -179,6 +197,13 @@ class ActaWsClient extends TypedEventEmitter {
|
|
|
179
197
|
}
|
|
180
198
|
async createRfq(request) {
|
|
181
199
|
this.ensureAuthenticated();
|
|
200
|
+
(0, wirePolicy_1.assertWsU64Safe)(request.strike, "strike");
|
|
201
|
+
(0, wirePolicy_1.assertWsU64Safe)(request.quantity, "quantity");
|
|
202
|
+
const marketDescriptor = this.marketDescriptorsByMarket.get(request.market);
|
|
203
|
+
if (!marketDescriptor) {
|
|
204
|
+
throw new Error(`Missing market descriptor for market=${request.market}; call getMarketDescriptors() before createRfq().`);
|
|
205
|
+
}
|
|
206
|
+
(0, wirePolicy_1.validateQuantityBySizeRule)(request.quantity, marketDescriptor.size_rule);
|
|
182
207
|
this.send({
|
|
183
208
|
type: "RfqRequest",
|
|
184
209
|
data: {
|
|
@@ -223,47 +248,89 @@ class ActaWsClient extends TypedEventEmitter {
|
|
|
223
248
|
}
|
|
224
249
|
getPositions() {
|
|
225
250
|
this.ensureAuthenticated();
|
|
226
|
-
this.
|
|
251
|
+
const requestId = this.nextRequestId();
|
|
252
|
+
this.send({
|
|
253
|
+
type: "GetPositions",
|
|
254
|
+
data: { request_id: requestId },
|
|
255
|
+
});
|
|
256
|
+
return requestId;
|
|
227
257
|
}
|
|
228
258
|
getMarkets() {
|
|
229
|
-
this.
|
|
259
|
+
const requestId = this.nextRequestId();
|
|
260
|
+
this.send({
|
|
261
|
+
type: "GetMarkets",
|
|
262
|
+
data: { request_id: requestId },
|
|
263
|
+
});
|
|
264
|
+
return requestId;
|
|
230
265
|
}
|
|
231
266
|
getMarketDescriptors(args) {
|
|
267
|
+
const requestId = this.nextRequestId();
|
|
232
268
|
const data = {
|
|
269
|
+
request_id: requestId,
|
|
233
270
|
active_only: args?.active_only ?? true,
|
|
234
271
|
};
|
|
235
272
|
this.send({ type: "GetMarketDescriptors", data });
|
|
273
|
+
return requestId;
|
|
236
274
|
}
|
|
237
275
|
getExpiries(args) {
|
|
276
|
+
const requestId = this.nextRequestId();
|
|
238
277
|
this.send({
|
|
239
278
|
type: "GetExpiries",
|
|
240
279
|
data: {
|
|
280
|
+
request_id: requestId,
|
|
241
281
|
underlying_mint: args?.underlying_mint,
|
|
242
282
|
quote_mint: args?.quote_mint,
|
|
243
283
|
is_put: args?.is_put ?? null,
|
|
244
284
|
},
|
|
245
285
|
});
|
|
286
|
+
return requestId;
|
|
246
287
|
}
|
|
247
288
|
getTokens(args) {
|
|
289
|
+
const requestId = this.nextRequestId();
|
|
248
290
|
const data = {
|
|
291
|
+
request_id: requestId,
|
|
249
292
|
active_only: args?.active_only ?? true,
|
|
250
293
|
};
|
|
251
294
|
this.send({ type: "GetTokens", data });
|
|
295
|
+
return requestId;
|
|
252
296
|
}
|
|
253
297
|
getMyActiveRfqs() {
|
|
254
298
|
this.ensureAuthenticated();
|
|
255
|
-
this.
|
|
299
|
+
const requestId = this.nextRequestId();
|
|
300
|
+
this.send({
|
|
301
|
+
type: "GetMyActiveRfqs",
|
|
302
|
+
data: { request_id: requestId },
|
|
303
|
+
});
|
|
304
|
+
return requestId;
|
|
256
305
|
}
|
|
257
306
|
getActiveRfqs() {
|
|
258
|
-
this.
|
|
307
|
+
const requestId = this.nextRequestId();
|
|
308
|
+
this.send({
|
|
309
|
+
type: "GetActiveRfqs",
|
|
310
|
+
data: { request_id: requestId },
|
|
311
|
+
});
|
|
312
|
+
return requestId;
|
|
313
|
+
}
|
|
314
|
+
logout() {
|
|
315
|
+
this.send({ type: "Logout" });
|
|
259
316
|
}
|
|
260
317
|
getOrderStatus(orderIdHex) {
|
|
261
318
|
this.ensureAuthenticated();
|
|
262
|
-
this.
|
|
319
|
+
const requestId = this.nextRequestId();
|
|
320
|
+
this.send({
|
|
321
|
+
type: "GetOrderStatus",
|
|
322
|
+
data: { request_id: requestId, order_id: orderIdHex },
|
|
323
|
+
});
|
|
324
|
+
return requestId;
|
|
263
325
|
}
|
|
264
326
|
cancelRfq(rfqId) {
|
|
265
327
|
this.ensureAuthenticated();
|
|
266
|
-
this.
|
|
328
|
+
const requestId = this.nextRequestId();
|
|
329
|
+
this.send({
|
|
330
|
+
type: "CancelRfq",
|
|
331
|
+
data: { rfq_id: rfqId, request_id: requestId },
|
|
332
|
+
});
|
|
333
|
+
return requestId;
|
|
267
334
|
}
|
|
268
335
|
submitQuote(quote) {
|
|
269
336
|
this.ensureAuthenticated();
|
|
@@ -293,7 +360,12 @@ class ActaWsClient extends TypedEventEmitter {
|
|
|
293
360
|
}
|
|
294
361
|
cancelQuote(rfqId) {
|
|
295
362
|
this.ensureAuthenticated();
|
|
296
|
-
this.
|
|
363
|
+
const requestId = this.nextRequestId();
|
|
364
|
+
this.send({
|
|
365
|
+
type: "CancelQuote",
|
|
366
|
+
data: { rfq_id: rfqId, request_id: requestId },
|
|
367
|
+
});
|
|
368
|
+
return requestId;
|
|
297
369
|
}
|
|
298
370
|
subscribe(channels, markets) {
|
|
299
371
|
this.ensureAuthenticated();
|
|
@@ -418,6 +490,14 @@ class ActaWsClient extends TypedEventEmitter {
|
|
|
418
490
|
case "AuthError":
|
|
419
491
|
this.handleAuthError(message.data.reason, message.data.message);
|
|
420
492
|
break;
|
|
493
|
+
case "LogoutSuccess":
|
|
494
|
+
this.sessionId = null;
|
|
495
|
+
this.pendingResumeSessionId = null;
|
|
496
|
+
this.lastAuthSessionId = null;
|
|
497
|
+
this.startAuthSent = false;
|
|
498
|
+
this.setConnectionState("connecting");
|
|
499
|
+
this.emit("logoutSuccess");
|
|
500
|
+
break;
|
|
421
501
|
case "Snapshot":
|
|
422
502
|
this.handleSnapshot(message.data);
|
|
423
503
|
break;
|
|
@@ -428,7 +508,13 @@ class ActaWsClient extends TypedEventEmitter {
|
|
|
428
508
|
this.handleMarkets(message.data.markets ?? []);
|
|
429
509
|
break;
|
|
430
510
|
case "MarketDescriptors":
|
|
431
|
-
|
|
511
|
+
{
|
|
512
|
+
const marketDescriptors = message.data.markets ?? [];
|
|
513
|
+
for (const marketDescriptor of marketDescriptors) {
|
|
514
|
+
this.marketDescriptorsByMarket.set(marketDescriptor.market.market_pda, marketDescriptor);
|
|
515
|
+
}
|
|
516
|
+
this.emit("marketDescriptors", marketDescriptors);
|
|
517
|
+
}
|
|
432
518
|
break;
|
|
433
519
|
case "Expiries":
|
|
434
520
|
this.emit("expiries", message.data.expiries_ts ?? []);
|
|
@@ -587,10 +673,12 @@ class ActaWsClient extends TypedEventEmitter {
|
|
|
587
673
|
}
|
|
588
674
|
/** Taker-only: request current indicative prices for a market + position_type. */
|
|
589
675
|
getIndicativePrices(req) {
|
|
676
|
+
const requestId = this.nextRequestId();
|
|
590
677
|
this.send({
|
|
591
678
|
type: "GetIndicativePrices",
|
|
592
|
-
data: req,
|
|
679
|
+
data: { ...req, request_id: requestId },
|
|
593
680
|
});
|
|
681
|
+
return requestId;
|
|
594
682
|
}
|
|
595
683
|
/** Maker-only: respond to an indicative request (unsigned, non-binding). */
|
|
596
684
|
sendIndicativePricesResponse(resp) {
|
|
@@ -619,8 +707,9 @@ class ActaWsClient extends TypedEventEmitter {
|
|
|
619
707
|
}
|
|
620
708
|
}
|
|
621
709
|
async beginAuthHandshake() {
|
|
622
|
-
|
|
623
|
-
|
|
710
|
+
const resumeSessionId = this.pendingResumeSessionId ?? this.lastAuthSessionId;
|
|
711
|
+
if (resumeSessionId) {
|
|
712
|
+
this.sendResumeAuth(resumeSessionId);
|
|
624
713
|
return;
|
|
625
714
|
}
|
|
626
715
|
await this.sendStartAuth();
|
|
@@ -657,6 +746,9 @@ class ActaWsClient extends TypedEventEmitter {
|
|
|
657
746
|
handleAuthSuccess(sessionId, expiresAt) {
|
|
658
747
|
this.sessionId = sessionId;
|
|
659
748
|
this.pendingResumeSessionId = null;
|
|
749
|
+
if (expiresAt !== null) {
|
|
750
|
+
this.lastAuthSessionId = sessionId;
|
|
751
|
+
}
|
|
660
752
|
this.setConnectionState("authenticated");
|
|
661
753
|
this.emit("authenticated", sessionId, expiresAt);
|
|
662
754
|
if (this.subscribedChannels.size > 0) {
|
|
@@ -668,12 +760,13 @@ class ActaWsClient extends TypedEventEmitter {
|
|
|
668
760
|
}
|
|
669
761
|
}
|
|
670
762
|
handleAuthError(reason, message) {
|
|
671
|
-
this.emit("authError", reason);
|
|
763
|
+
this.emit("authError", reason, message);
|
|
672
764
|
if (reason === "session_expired" &&
|
|
673
765
|
this.authRequested &&
|
|
674
766
|
this.authProvider &&
|
|
675
|
-
this.pendingResumeSessionId) {
|
|
767
|
+
(this.pendingResumeSessionId || this.lastAuthSessionId)) {
|
|
676
768
|
this.pendingResumeSessionId = null;
|
|
769
|
+
this.lastAuthSessionId = null;
|
|
677
770
|
this.startAuthSent = false;
|
|
678
771
|
void this.sendStartAuth().catch((err) => {
|
|
679
772
|
this.emit("error", err);
|
|
@@ -789,6 +882,9 @@ class ActaWsClient extends TypedEventEmitter {
|
|
|
789
882
|
this.log("Cannot send, WebSocket not open");
|
|
790
883
|
}
|
|
791
884
|
}
|
|
885
|
+
nextRequestId() {
|
|
886
|
+
return generateRequestId();
|
|
887
|
+
}
|
|
792
888
|
ensureAuthenticated() {
|
|
793
889
|
if (this.connectionState !== "authenticated") {
|
|
794
890
|
throw new Error("Client is not authenticated");
|
|
@@ -883,7 +979,6 @@ class ActaWsClient extends TypedEventEmitter {
|
|
|
883
979
|
this.helloSent = false;
|
|
884
980
|
this.welcomeReceived = false;
|
|
885
981
|
this.startAuthSent = false;
|
|
886
|
-
this.pendingResumeSessionId = null;
|
|
887
982
|
this.pendingMessages = [];
|
|
888
983
|
this.setConnectionState("disconnected");
|
|
889
984
|
}
|
|
@@ -13,6 +13,7 @@ const WELCOME_MESSAGE = {
|
|
|
13
13
|
enabled_features: [],
|
|
14
14
|
},
|
|
15
15
|
};
|
|
16
|
+
const createdClients = [];
|
|
16
17
|
class MockWebSocket {
|
|
17
18
|
url;
|
|
18
19
|
readyState = WS_CONNECTING;
|
|
@@ -67,6 +68,7 @@ function makeHarness(overrides = {}) {
|
|
|
67
68
|
return currentSocket;
|
|
68
69
|
},
|
|
69
70
|
});
|
|
71
|
+
createdClients.push(client);
|
|
70
72
|
return {
|
|
71
73
|
client,
|
|
72
74
|
socket: () => {
|
|
@@ -83,6 +85,9 @@ async function flushMicrotasks() {
|
|
|
83
85
|
}
|
|
84
86
|
describe("ActaWsClient", () => {
|
|
85
87
|
afterEach(() => {
|
|
88
|
+
for (const client of createdClients.splice(0, createdClients.length)) {
|
|
89
|
+
client.disconnect();
|
|
90
|
+
}
|
|
86
91
|
jest.restoreAllMocks();
|
|
87
92
|
jest.useRealTimers();
|
|
88
93
|
});
|
|
@@ -169,13 +174,204 @@ describe("ActaWsClient", () => {
|
|
|
169
174
|
});
|
|
170
175
|
it("emits authError reason on AuthError", () => {
|
|
171
176
|
const { client } = makeHarness();
|
|
172
|
-
const
|
|
173
|
-
client.on("authError", (reason) =>
|
|
177
|
+
const errors = [];
|
|
178
|
+
client.on("authError", (reason, message) => errors.push({ reason, message }));
|
|
174
179
|
client.handleMessage({
|
|
175
180
|
type: "AuthError",
|
|
176
181
|
data: { reason: "invalid_signature", message: "bad signature bytes" },
|
|
177
182
|
});
|
|
178
|
-
expect(
|
|
183
|
+
expect(errors).toEqual([
|
|
184
|
+
{ reason: "invalid_signature", message: "bad signature bytes" },
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
it("uses last auth session for resume-first reconnect", async () => {
|
|
188
|
+
const { client, socket } = makeHarness();
|
|
189
|
+
const auth = makeAuthProvider("WalletPubkey", "WalletSignature");
|
|
190
|
+
client.connectAndAuthenticate(auth);
|
|
191
|
+
let ws = socket();
|
|
192
|
+
ws.triggerOpen();
|
|
193
|
+
ws.triggerMessage(WELCOME_MESSAGE);
|
|
194
|
+
await flushMicrotasks();
|
|
195
|
+
ws.triggerMessage({
|
|
196
|
+
type: "AuthSuccess",
|
|
197
|
+
data: { session_id: "persisted-session", expires_at: 1_710_086_400 },
|
|
198
|
+
});
|
|
199
|
+
ws.triggerClose(1006, "network_drop");
|
|
200
|
+
client.connectAndAuthenticate(auth);
|
|
201
|
+
ws = socket();
|
|
202
|
+
ws.triggerOpen();
|
|
203
|
+
ws.triggerMessage(WELCOME_MESSAGE);
|
|
204
|
+
await flushMicrotasks();
|
|
205
|
+
const sentTypes = ws.sent.map((payload) => parseClientMessage(payload).type);
|
|
206
|
+
expect(sentTypes).toEqual(["Hello", "ResumeAuth"]);
|
|
207
|
+
const resumeAuth = parseClientMessage(ws.sent[1]);
|
|
208
|
+
expect(resumeAuth.type).toBe("ResumeAuth");
|
|
209
|
+
if (resumeAuth.type === "ResumeAuth") {
|
|
210
|
+
expect(resumeAuth.data.session_id).toBe("persisted-session");
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
it("sends Logout and emits logoutSuccess", () => {
|
|
214
|
+
const { client, socket } = makeHarness();
|
|
215
|
+
const events = [];
|
|
216
|
+
client.on("logoutSuccess", () => events.push("logoutSuccess"));
|
|
217
|
+
client.connectAnonymous();
|
|
218
|
+
const ws = socket();
|
|
219
|
+
ws.triggerOpen();
|
|
220
|
+
ws.triggerMessage(WELCOME_MESSAGE);
|
|
221
|
+
client.logout();
|
|
222
|
+
const sent = parseClientMessage(ws.sent[1]);
|
|
223
|
+
expect(sent.type).toBe("Logout");
|
|
224
|
+
ws.triggerMessage({ type: "LogoutSuccess", data: {} });
|
|
225
|
+
expect(events).toEqual(["logoutSuccess"]);
|
|
226
|
+
});
|
|
227
|
+
it("adds request_id to public request messages", () => {
|
|
228
|
+
const { client, socket } = makeHarness();
|
|
229
|
+
client.connectAnonymous();
|
|
230
|
+
const ws = socket();
|
|
231
|
+
ws.triggerOpen();
|
|
232
|
+
ws.triggerMessage(WELCOME_MESSAGE);
|
|
233
|
+
const getMarketsId = client.getMarkets();
|
|
234
|
+
const getMarketDescriptorsId = client.getMarketDescriptors();
|
|
235
|
+
const getTokensId = client.getTokens();
|
|
236
|
+
const getExpiriesId = client.getExpiries();
|
|
237
|
+
const getActiveRfqsId = client.getActiveRfqs();
|
|
238
|
+
const getIndicativePricesId = client.getIndicativePrices({
|
|
239
|
+
market: "market-1",
|
|
240
|
+
position_type: "covered_call",
|
|
241
|
+
});
|
|
242
|
+
const sent = ws.sent.slice(1).map(parseClientMessage);
|
|
243
|
+
const withRequestId = sent.filter((msg) => msg.type !== "Logout");
|
|
244
|
+
for (const msg of withRequestId) {
|
|
245
|
+
if ("data" in msg) {
|
|
246
|
+
const data = msg.data;
|
|
247
|
+
expect(typeof data.request_id).toBe("string");
|
|
248
|
+
expect(data.request_id.length).toBeGreaterThan(0);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
expect(sent[0].data.request_id).toBe(getMarketsId);
|
|
252
|
+
expect(sent[1].data.request_id).toBe(getMarketDescriptorsId);
|
|
253
|
+
expect(sent[2].data.request_id).toBe(getTokensId);
|
|
254
|
+
expect(sent[3].data.request_id).toBe(getExpiriesId);
|
|
255
|
+
expect(sent[4].data.request_id).toBe(getActiveRfqsId);
|
|
256
|
+
expect(sent[5].data.request_id).toBe(getIndicativePricesId);
|
|
257
|
+
});
|
|
258
|
+
it("adds request_id to authenticated request messages", () => {
|
|
259
|
+
const { client, socket } = makeHarness();
|
|
260
|
+
client.connectAnonymous();
|
|
261
|
+
const ws = socket();
|
|
262
|
+
ws.triggerOpen();
|
|
263
|
+
ws.triggerMessage(WELCOME_MESSAGE);
|
|
264
|
+
client.handleMessage({
|
|
265
|
+
type: "AuthSuccess",
|
|
266
|
+
data: { session_id: "session-id", expires_at: 1_710_086_400 },
|
|
267
|
+
});
|
|
268
|
+
const getPositionsId = client.getPositions();
|
|
269
|
+
const getMyActiveRfqsId = client.getMyActiveRfqs();
|
|
270
|
+
const getOrderStatusId = client.getOrderStatus("11".repeat(32));
|
|
271
|
+
const cancelRfqId = client.cancelRfq("rfq-1");
|
|
272
|
+
const cancelQuoteId = client.cancelQuote("rfq-2");
|
|
273
|
+
const sent = ws.sent.slice(1).map(parseClientMessage);
|
|
274
|
+
const authRequests = sent.filter((msg) => [
|
|
275
|
+
"GetPositions",
|
|
276
|
+
"GetMyActiveRfqs",
|
|
277
|
+
"GetOrderStatus",
|
|
278
|
+
"CancelRfq",
|
|
279
|
+
"CancelQuote",
|
|
280
|
+
].includes(msg.type));
|
|
281
|
+
expect(authRequests).toHaveLength(5);
|
|
282
|
+
for (const msg of authRequests) {
|
|
283
|
+
if ("data" in msg) {
|
|
284
|
+
const data = msg.data;
|
|
285
|
+
expect(typeof data.request_id).toBe("string");
|
|
286
|
+
expect(data.request_id.length).toBeGreaterThan(0);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
expect(authRequests[0].data.request_id).toBe(getPositionsId);
|
|
290
|
+
expect(authRequests[1].data.request_id).toBe(getMyActiveRfqsId);
|
|
291
|
+
expect(authRequests[2].data.request_id).toBe(getOrderStatusId);
|
|
292
|
+
expect(authRequests[3].data.request_id).toBe(cancelRfqId);
|
|
293
|
+
expect(authRequests[4].data.request_id).toBe(cancelQuoteId);
|
|
294
|
+
});
|
|
295
|
+
it("createRfq rejects when market descriptor is missing", async () => {
|
|
296
|
+
const { client, socket } = makeHarness();
|
|
297
|
+
client.connectAnonymous();
|
|
298
|
+
const ws = socket();
|
|
299
|
+
ws.triggerOpen();
|
|
300
|
+
ws.triggerMessage(WELCOME_MESSAGE);
|
|
301
|
+
client.handleMessage({
|
|
302
|
+
type: "AuthSuccess",
|
|
303
|
+
data: { session_id: "session-id", expires_at: 1_710_086_400 },
|
|
304
|
+
});
|
|
305
|
+
await expect(client.createRfq({
|
|
306
|
+
market: "market-1",
|
|
307
|
+
position_type: "covered_call",
|
|
308
|
+
strike: 100_000_000_000,
|
|
309
|
+
quantity: 2_000_000_000,
|
|
310
|
+
})).rejects.toThrow("Missing market descriptor");
|
|
311
|
+
const sentTypes = ws.sent.map((payload) => parseClientMessage(payload).type);
|
|
312
|
+
expect(sentTypes).not.toContain("RfqRequest");
|
|
313
|
+
});
|
|
314
|
+
it("createRfq validates quantity against market size rule", async () => {
|
|
315
|
+
const { client, socket } = makeHarness();
|
|
316
|
+
client.connectAnonymous();
|
|
317
|
+
const ws = socket();
|
|
318
|
+
ws.triggerOpen();
|
|
319
|
+
ws.triggerMessage(WELCOME_MESSAGE);
|
|
320
|
+
client.handleMessage({
|
|
321
|
+
type: "AuthSuccess",
|
|
322
|
+
data: { session_id: "session-id", expires_at: 1_710_086_400 },
|
|
323
|
+
});
|
|
324
|
+
client.handleMessage({
|
|
325
|
+
type: "MarketDescriptors",
|
|
326
|
+
data: {
|
|
327
|
+
request_id: "req-1",
|
|
328
|
+
markets: [
|
|
329
|
+
{
|
|
330
|
+
market: {
|
|
331
|
+
chain_id: 1,
|
|
332
|
+
program_id: "program-1",
|
|
333
|
+
market_pda: "market-1",
|
|
334
|
+
underlying_mint: "So11111111111111111111111111111111111111112",
|
|
335
|
+
quote_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
336
|
+
expiry_ts: 1_800_000_000,
|
|
337
|
+
is_put: false,
|
|
338
|
+
collateral_mint: "So11111111111111111111111111111111111111112",
|
|
339
|
+
settlement_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
340
|
+
},
|
|
341
|
+
underlying_oracle_pda: "oracle-underlying",
|
|
342
|
+
quote_oracle_pda: "oracle-quote",
|
|
343
|
+
underlying_decimals: 9,
|
|
344
|
+
quote_decimals: 6,
|
|
345
|
+
size_rule: {
|
|
346
|
+
min_size: 2_000_000_000,
|
|
347
|
+
max_size: 10_000_000_000,
|
|
348
|
+
step: 1_000_000_000,
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
await expect(client.createRfq({
|
|
355
|
+
market: "market-1",
|
|
356
|
+
position_type: "covered_call",
|
|
357
|
+
strike: 100_000_000_000,
|
|
358
|
+
quantity: 2_500_000_000,
|
|
359
|
+
})).rejects.toThrow("(quantity - min_size) % step == 0");
|
|
360
|
+
await client.createRfq({
|
|
361
|
+
market: "market-1",
|
|
362
|
+
position_type: "covered_call",
|
|
363
|
+
strike: 100_000_000_000,
|
|
364
|
+
quantity: 3_000_000_000,
|
|
365
|
+
});
|
|
366
|
+
const sentMessages = ws.sent.map(parseClientMessage);
|
|
367
|
+
const rfqRequest = sentMessages
|
|
368
|
+
.filter((message) => message.type === "RfqRequest")
|
|
369
|
+
.at(-1);
|
|
370
|
+
expect(rfqRequest?.type).toBe("RfqRequest");
|
|
371
|
+
if (rfqRequest?.type === "RfqRequest") {
|
|
372
|
+
expect(rfqRequest.data.market).toBe("market-1");
|
|
373
|
+
expect(rfqRequest.data.quantity).toBe(3_000_000_000);
|
|
374
|
+
}
|
|
179
375
|
});
|
|
180
376
|
it("drop_oldest policy keeps the latest queued messages", () => {
|
|
181
377
|
const { client, socket } = makeHarness({
|
package/dist/cjs/ws/discovery.js
CHANGED
|
@@ -12,11 +12,13 @@ function deriveTokenLists(markets) {
|
|
|
12
12
|
underlyings.set(uMint, {
|
|
13
13
|
mint: m.market.underlying_mint,
|
|
14
14
|
decimals: m.underlying_decimals,
|
|
15
|
+
size_rule: m.size_rule,
|
|
15
16
|
symbol: m.underlying_symbol,
|
|
16
17
|
});
|
|
17
18
|
const q = {
|
|
18
19
|
mint: m.market.quote_mint,
|
|
19
20
|
decimals: m.quote_decimals,
|
|
21
|
+
size_rule: m.size_rule,
|
|
20
22
|
symbol: m.quote_symbol,
|
|
21
23
|
};
|
|
22
24
|
const inner = quotesByUnderlying.get(uMint) ?? new Map();
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
exports.assertWsU64Safe = assertWsU64Safe;
|
|
14
14
|
exports.assertWsI64Safe = assertWsI64Safe;
|
|
15
|
+
exports.validateQuantityBySizeRule = validateQuantityBySizeRule;
|
|
15
16
|
function assertWsU64Safe(value, name) {
|
|
16
17
|
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
17
18
|
throw new Error(`${name} must be an integer number (u64 on wire)`);
|
|
@@ -32,3 +33,18 @@ function assertWsI64Safe(value, name) {
|
|
|
32
33
|
throw new Error(`${name} exceeds JS safe integer range; WS wire requires safe integers`);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
36
|
+
function validateQuantityBySizeRule(quantity, rule, quantityName = "quantity") {
|
|
37
|
+
assertWsU64Safe(quantity, quantityName);
|
|
38
|
+
assertWsU64Safe(rule.min_size, "size_rule.min_size");
|
|
39
|
+
assertWsU64Safe(rule.max_size, "size_rule.max_size");
|
|
40
|
+
assertWsU64Safe(rule.step, "size_rule.step");
|
|
41
|
+
if (rule.max_size < rule.min_size) {
|
|
42
|
+
throw new Error(`invalid size_rule: max_size (${rule.max_size}) must be >= min_size (${rule.min_size})`);
|
|
43
|
+
}
|
|
44
|
+
if (quantity < rule.min_size || quantity > rule.max_size) {
|
|
45
|
+
throw new Error(`${quantityName} must be within [${rule.min_size}, ${rule.max_size}], got ${quantity}`);
|
|
46
|
+
}
|
|
47
|
+
if ((quantity - rule.min_size) % rule.step !== 0) {
|
|
48
|
+
throw new Error(`${quantityName} must satisfy (quantity - min_size) % step == 0 (quantity=${quantity}, min_size=${rule.min_size}, step=${rule.step})`);
|
|
49
|
+
}
|
|
50
|
+
}
|