@acta-markets/ts-sdk 0.0.5-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.
@@ -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
  },
@@ -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 = "1466fbb647b4c33af315eefbb61e5e572ce14f22f16d050ac2894f8e017aaf66";
4
+ exports.ACTA_IDL_SHA256 = "704677f7071ecbe98f442fb62468d882329b906950a707fc47aad0f38683636f";
@@ -108,6 +108,7 @@ class ActaWsClient extends TypedEventEmitter {
108
108
  subscribedChannels = new Set(["rfqs"]);
109
109
  subscribedMarkets = new Set();
110
110
  hasMarketScope = false;
111
+ marketDescriptorsByMarket = new Map();
111
112
  state = {
112
113
  stats: null,
113
114
  activeRfqs: new Map(),
@@ -196,6 +197,13 @@ class ActaWsClient extends TypedEventEmitter {
196
197
  }
197
198
  async createRfq(request) {
198
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);
199
207
  this.send({
200
208
  type: "RfqRequest",
201
209
  data: {
@@ -500,7 +508,13 @@ class ActaWsClient extends TypedEventEmitter {
500
508
  this.handleMarkets(message.data.markets ?? []);
501
509
  break;
502
510
  case "MarketDescriptors":
503
- this.emit("marketDescriptors", message.data.markets ?? []);
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
+ }
504
518
  break;
505
519
  case "Expiries":
506
520
  this.emit("expiries", message.data.expiries_ts ?? []);
@@ -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
  });
@@ -287,6 +292,87 @@ describe("ActaWsClient", () => {
287
292
  expect(authRequests[3].data.request_id).toBe(cancelRfqId);
288
293
  expect(authRequests[4].data.request_id).toBe(cancelQuoteId);
289
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
+ }
375
+ });
290
376
  it("drop_oldest policy keeps the latest queued messages", () => {
291
377
  const { client, socket } = makeHarness({
292
378
  maxPendingMessages: 2,
@@ -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
+ }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const wirePolicy_1 = require("./wirePolicy");
4
+ describe("validateQuantityBySizeRule", () => {
5
+ const rule = {
6
+ min_size: 2_000_000_000,
7
+ max_size: 10_000_000_000,
8
+ step: 1_000_000_000,
9
+ };
10
+ it("accepts a valid quantity", () => {
11
+ expect(() => (0, wirePolicy_1.validateQuantityBySizeRule)(6_000_000_000, rule)).not.toThrow();
12
+ });
13
+ it("rejects quantity below min", () => {
14
+ expect(() => (0, wirePolicy_1.validateQuantityBySizeRule)(1_000_000_000, rule)).toThrow("must be within");
15
+ });
16
+ it("rejects quantity above max", () => {
17
+ expect(() => (0, wirePolicy_1.validateQuantityBySizeRule)(11_000_000_000, rule)).toThrow("must be within");
18
+ });
19
+ it("rejects quantity with invalid step", () => {
20
+ expect(() => (0, wirePolicy_1.validateQuantityBySizeRule)(2_500_000_000, rule)).toThrow("(quantity - min_size) % step == 0");
21
+ });
22
+ });
@@ -182,6 +182,8 @@ describe("events parsing", () => {
182
182
  Buffer.from(pk(3)),
183
183
  u64(777),
184
184
  Buffer.from([1]), // is_put
185
+ Buffer.from([6]), // underlying_decimals
186
+ Buffer.from([9]), // quote_decimals
185
187
  ]));
186
188
  const d = decodeActaEventLine(line);
187
189
  expect(d.kind).toBe(EventKind.CreateMarket);
@@ -192,6 +194,8 @@ describe("events parsing", () => {
192
194
  expect(d.event.quoteMint).toBe(addr(3));
193
195
  expect(d.event.expiryTs).toBe(777n);
194
196
  expect(d.event.isPut).toBe(1);
197
+ expect(d.event.underlyingDecimals).toBe(6);
198
+ expect(d.event.quoteDecimals).toBe(9);
195
199
  }
196
200
  }
197
201
  // 9: FinalizeMarket
@@ -213,6 +217,7 @@ describe("events parsing", () => {
213
217
  u64(222),
214
218
  Buffer.from([1]),
215
219
  Buffer.from(pk(3)),
220
+ Buffer.from(pk(9)),
216
221
  ]));
217
222
  const d = decodeActaEventLine(line);
218
223
  expect(d.kind).toBe(EventKind.CreateOracle);
@@ -223,6 +228,7 @@ describe("events parsing", () => {
223
228
  expect(d.event.expiryTs).toBe(222n);
224
229
  expect(d.event.oracleType).toBe(1);
225
230
  expect(d.event.authority).toBe(addr(3));
231
+ expect(Buffer.from(d.event.feedId)).toEqual(Buffer.from(pk(9)));
226
232
  }
227
233
  }
228
234
  // 15: UpdateOraclePrice
@@ -55,6 +55,8 @@ export type ActaEvent = {
55
55
  quoteMint: Address;
56
56
  expiryTs: bigint;
57
57
  isPut: number;
58
+ underlyingDecimals: number;
59
+ quoteDecimals: number;
58
60
  } | {
59
61
  __kind: "FinalizeMarket";
60
62
  marketPda: Address;
@@ -95,6 +97,7 @@ export type ActaEvent = {
95
97
  expiryTs: bigint;
96
98
  oracleType: number;
97
99
  authority: Address;
100
+ feedId: ReadonlyUint8Array;
98
101
  } | {
99
102
  __kind: "UpdateOraclePrice";
100
103
  oraclePda: Address;
@@ -163,6 +166,8 @@ export type ActaEventArgs = {
163
166
  quoteMint: Address;
164
167
  expiryTs: number | bigint;
165
168
  isPut: number;
169
+ underlyingDecimals: number;
170
+ quoteDecimals: number;
166
171
  } | {
167
172
  __kind: "FinalizeMarket";
168
173
  marketPda: Address;
@@ -203,6 +208,7 @@ export type ActaEventArgs = {
203
208
  expiryTs: number | bigint;
204
209
  oracleType: number;
205
210
  authority: Address;
211
+ feedId: ReadonlyUint8Array;
206
212
  } | {
207
213
  __kind: "UpdateOraclePrice";
208
214
  oraclePda: Address;
@@ -82,6 +82,8 @@ export function getActaEventEncoder() {
82
82
  ["quoteMint", getAddressEncoder()],
83
83
  ["expiryTs", getU64Encoder()],
84
84
  ["isPut", getU8Encoder()],
85
+ ["underlyingDecimals", getU8Encoder()],
86
+ ["quoteDecimals", getU8Encoder()],
85
87
  ]),
86
88
  ],
87
89
  [
@@ -135,6 +137,7 @@ export function getActaEventEncoder() {
135
137
  ["expiryTs", getU64Encoder()],
136
138
  ["oracleType", getU8Encoder()],
137
139
  ["authority", getAddressEncoder()],
140
+ ["feedId", fixEncoderSize(getBytesEncoder(), 32)],
138
141
  ]),
139
142
  ],
140
143
  [
@@ -240,6 +243,8 @@ export function getActaEventDecoder() {
240
243
  ["quoteMint", getAddressDecoder()],
241
244
  ["expiryTs", getU64Decoder()],
242
245
  ["isPut", getU8Decoder()],
246
+ ["underlyingDecimals", getU8Decoder()],
247
+ ["quoteDecimals", getU8Decoder()],
243
248
  ]),
244
249
  ],
245
250
  [
@@ -293,6 +298,7 @@ export function getActaEventDecoder() {
293
298
  ["expiryTs", getU64Decoder()],
294
299
  ["oracleType", getU8Decoder()],
295
300
  ["authority", getAddressDecoder()],
301
+ ["feedId", fixDecoderSize(getBytesDecoder(), 32)],
296
302
  ]),
297
303
  ],
298
304
  [
@@ -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
  },
@@ -1 +1 @@
1
- export declare const ACTA_IDL_SHA256 = "1466fbb647b4c33af315eefbb61e5e572ce14f22f16d050ac2894f8e017aaf66";
1
+ export declare const ACTA_IDL_SHA256 = "704677f7071ecbe98f442fb62468d882329b906950a707fc47aad0f38683636f";
package/dist/idl/hash.js CHANGED
@@ -1 +1 @@
1
- export const ACTA_IDL_SHA256 = "1466fbb647b4c33af315eefbb61e5e572ce14f22f16d050ac2894f8e017aaf66";
1
+ export const ACTA_IDL_SHA256 = "704677f7071ecbe98f442fb62468d882329b906950a707fc47aad0f38683636f";
@@ -163,6 +163,7 @@ export declare class ActaWsClient extends TypedEventEmitter<ActaWsClientEvents>
163
163
  private subscribedChannels;
164
164
  private subscribedMarkets;
165
165
  private hasMarketScope;
166
+ private marketDescriptorsByMarket;
166
167
  readonly state: ClientState;
167
168
  constructor(options: ActaWsClientOptions);
168
169
  connectAnonymous(): void;
package/dist/ws/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Acta WebSocket client (rfq-server). */
2
2
  import { buildSignedQuoteMessage, buildAcceptQuoteMessage } from "./flows";
3
- import { assertWsU64Safe } from "./wirePolicy";
3
+ import { assertWsU64Safe, validateQuantityBySizeRule } from "./wirePolicy";
4
4
  function formatServerError(error) {
5
5
  if (error.type === "generic") {
6
6
  return `${error.data.code}: ${error.data.message}`;
@@ -105,6 +105,7 @@ export class ActaWsClient extends TypedEventEmitter {
105
105
  subscribedChannels = new Set(["rfqs"]);
106
106
  subscribedMarkets = new Set();
107
107
  hasMarketScope = false;
108
+ marketDescriptorsByMarket = new Map();
108
109
  state = {
109
110
  stats: null,
110
111
  activeRfqs: new Map(),
@@ -193,6 +194,13 @@ export class ActaWsClient extends TypedEventEmitter {
193
194
  }
194
195
  async createRfq(request) {
195
196
  this.ensureAuthenticated();
197
+ assertWsU64Safe(request.strike, "strike");
198
+ assertWsU64Safe(request.quantity, "quantity");
199
+ const marketDescriptor = this.marketDescriptorsByMarket.get(request.market);
200
+ if (!marketDescriptor) {
201
+ throw new Error(`Missing market descriptor for market=${request.market}; call getMarketDescriptors() before createRfq().`);
202
+ }
203
+ validateQuantityBySizeRule(request.quantity, marketDescriptor.size_rule);
196
204
  this.send({
197
205
  type: "RfqRequest",
198
206
  data: {
@@ -497,7 +505,13 @@ export class ActaWsClient extends TypedEventEmitter {
497
505
  this.handleMarkets(message.data.markets ?? []);
498
506
  break;
499
507
  case "MarketDescriptors":
500
- this.emit("marketDescriptors", message.data.markets ?? []);
508
+ {
509
+ const marketDescriptors = message.data.markets ?? [];
510
+ for (const marketDescriptor of marketDescriptors) {
511
+ this.marketDescriptorsByMarket.set(marketDescriptor.market.market_pda, marketDescriptor);
512
+ }
513
+ this.emit("marketDescriptors", marketDescriptors);
514
+ }
501
515
  break;
502
516
  case "Expiries":
503
517
  this.emit("expiries", message.data.expiries_ts ?? []);
@@ -11,6 +11,7 @@ const WELCOME_MESSAGE = {
11
11
  enabled_features: [],
12
12
  },
13
13
  };
14
+ const createdClients = [];
14
15
  class MockWebSocket {
15
16
  url;
16
17
  readyState = WS_CONNECTING;
@@ -65,6 +66,7 @@ function makeHarness(overrides = {}) {
65
66
  return currentSocket;
66
67
  },
67
68
  });
69
+ createdClients.push(client);
68
70
  return {
69
71
  client,
70
72
  socket: () => {
@@ -81,6 +83,9 @@ async function flushMicrotasks() {
81
83
  }
82
84
  describe("ActaWsClient", () => {
83
85
  afterEach(() => {
86
+ for (const client of createdClients.splice(0, createdClients.length)) {
87
+ client.disconnect();
88
+ }
84
89
  jest.restoreAllMocks();
85
90
  jest.useRealTimers();
86
91
  });
@@ -285,6 +290,87 @@ describe("ActaWsClient", () => {
285
290
  expect(authRequests[3].data.request_id).toBe(cancelRfqId);
286
291
  expect(authRequests[4].data.request_id).toBe(cancelQuoteId);
287
292
  });
293
+ it("createRfq rejects when market descriptor is missing", async () => {
294
+ const { client, socket } = makeHarness();
295
+ client.connectAnonymous();
296
+ const ws = socket();
297
+ ws.triggerOpen();
298
+ ws.triggerMessage(WELCOME_MESSAGE);
299
+ client.handleMessage({
300
+ type: "AuthSuccess",
301
+ data: { session_id: "session-id", expires_at: 1_710_086_400 },
302
+ });
303
+ await expect(client.createRfq({
304
+ market: "market-1",
305
+ position_type: "covered_call",
306
+ strike: 100_000_000_000,
307
+ quantity: 2_000_000_000,
308
+ })).rejects.toThrow("Missing market descriptor");
309
+ const sentTypes = ws.sent.map((payload) => parseClientMessage(payload).type);
310
+ expect(sentTypes).not.toContain("RfqRequest");
311
+ });
312
+ it("createRfq validates quantity against market size rule", async () => {
313
+ const { client, socket } = makeHarness();
314
+ client.connectAnonymous();
315
+ const ws = socket();
316
+ ws.triggerOpen();
317
+ ws.triggerMessage(WELCOME_MESSAGE);
318
+ client.handleMessage({
319
+ type: "AuthSuccess",
320
+ data: { session_id: "session-id", expires_at: 1_710_086_400 },
321
+ });
322
+ client.handleMessage({
323
+ type: "MarketDescriptors",
324
+ data: {
325
+ request_id: "req-1",
326
+ markets: [
327
+ {
328
+ market: {
329
+ chain_id: 1,
330
+ program_id: "program-1",
331
+ market_pda: "market-1",
332
+ underlying_mint: "So11111111111111111111111111111111111111112",
333
+ quote_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
334
+ expiry_ts: 1_800_000_000,
335
+ is_put: false,
336
+ collateral_mint: "So11111111111111111111111111111111111111112",
337
+ settlement_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
338
+ },
339
+ underlying_oracle_pda: "oracle-underlying",
340
+ quote_oracle_pda: "oracle-quote",
341
+ underlying_decimals: 9,
342
+ quote_decimals: 6,
343
+ size_rule: {
344
+ min_size: 2_000_000_000,
345
+ max_size: 10_000_000_000,
346
+ step: 1_000_000_000,
347
+ },
348
+ },
349
+ ],
350
+ },
351
+ });
352
+ await expect(client.createRfq({
353
+ market: "market-1",
354
+ position_type: "covered_call",
355
+ strike: 100_000_000_000,
356
+ quantity: 2_500_000_000,
357
+ })).rejects.toThrow("(quantity - min_size) % step == 0");
358
+ await client.createRfq({
359
+ market: "market-1",
360
+ position_type: "covered_call",
361
+ strike: 100_000_000_000,
362
+ quantity: 3_000_000_000,
363
+ });
364
+ const sentMessages = ws.sent.map(parseClientMessage);
365
+ const rfqRequest = sentMessages
366
+ .filter((message) => message.type === "RfqRequest")
367
+ .at(-1);
368
+ expect(rfqRequest?.type).toBe("RfqRequest");
369
+ if (rfqRequest?.type === "RfqRequest") {
370
+ expect(rfqRequest.data.market).toBe("market-1");
371
+ expect(rfqRequest.data.quantity).toBe(3_000_000_000);
372
+ }
373
+ });
288
374
  it("drop_oldest policy keeps the latest queued messages", () => {
289
375
  const { client, socket } = makeHarness({
290
376
  maxPendingMessages: 2,
@@ -8,11 +8,13 @@ export function deriveTokenLists(markets) {
8
8
  underlyings.set(uMint, {
9
9
  mint: m.market.underlying_mint,
10
10
  decimals: m.underlying_decimals,
11
+ size_rule: m.size_rule,
11
12
  symbol: m.underlying_symbol,
12
13
  });
13
14
  const q = {
14
15
  mint: m.market.quote_mint,
15
16
  decimals: m.quote_decimals,
17
+ size_rule: m.size_rule,
16
18
  symbol: m.quote_symbol,
17
19
  };
18
20
  const inner = quotesByUnderlying.get(uMint) ?? new Map();
@@ -469,13 +469,20 @@ export type MarketDescriptorInfo = {
469
469
  quote_oracle_pda: PubkeyBase58;
470
470
  underlying_decimals: number;
471
471
  quote_decimals: number;
472
- underlying_symbol?: string;
473
- quote_symbol?: string;
472
+ size_rule: PositionSizeRule;
473
+ underlying_symbol: string;
474
+ quote_symbol: string;
475
+ };
476
+ export type PositionSizeRule = {
477
+ min_size: WsU64;
478
+ max_size: WsU64;
479
+ step: WsU64;
474
480
  };
475
481
  export type TokenInfo = {
476
482
  mint: Address<string>;
477
483
  decimals: number;
478
- symbol?: string;
484
+ size_rule: PositionSizeRule;
485
+ symbol: string;
479
486
  };
480
487
  export type RfqCreatedMessage = {
481
488
  rfq_id: UuidString;
@@ -711,8 +718,8 @@ export type MakerMarketInfo = {
711
718
  expiry_ts: WsU64;
712
719
  is_put: boolean;
713
720
  is_finalized: boolean;
714
- underlying_symbol?: string;
715
- quote_symbol?: string;
721
+ underlying_symbol: string;
722
+ quote_symbol: string;
716
723
  stats?: MarketStats;
717
724
  };
718
725
  export type MarketStats = {
@@ -10,3 +10,9 @@
10
10
  */
11
11
  export declare function assertWsU64Safe(value: number, name: string): void;
12
12
  export declare function assertWsI64Safe(value: number, name: string): void;
13
+ export type QuantitySizeRuleLike = {
14
+ min_size: number;
15
+ max_size: number;
16
+ step: number;
17
+ };
18
+ export declare function validateQuantityBySizeRule(quantity: number, rule: QuantitySizeRuleLike, quantityName?: string): void;
@@ -28,3 +28,18 @@ export function assertWsI64Safe(value, name) {
28
28
  throw new Error(`${name} exceeds JS safe integer range; WS wire requires safe integers`);
29
29
  }
30
30
  }
31
+ export function validateQuantityBySizeRule(quantity, rule, quantityName = "quantity") {
32
+ assertWsU64Safe(quantity, quantityName);
33
+ assertWsU64Safe(rule.min_size, "size_rule.min_size");
34
+ assertWsU64Safe(rule.max_size, "size_rule.max_size");
35
+ assertWsU64Safe(rule.step, "size_rule.step");
36
+ if (rule.max_size < rule.min_size) {
37
+ throw new Error(`invalid size_rule: max_size (${rule.max_size}) must be >= min_size (${rule.min_size})`);
38
+ }
39
+ if (quantity < rule.min_size || quantity > rule.max_size) {
40
+ throw new Error(`${quantityName} must be within [${rule.min_size}, ${rule.max_size}], got ${quantity}`);
41
+ }
42
+ if ((quantity - rule.min_size) % rule.step !== 0) {
43
+ throw new Error(`${quantityName} must satisfy (quantity - min_size) % step == 0 (quantity=${quantity}, min_size=${rule.min_size}, step=${rule.step})`);
44
+ }
45
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import { validateQuantityBySizeRule } from "./wirePolicy";
2
+ describe("validateQuantityBySizeRule", () => {
3
+ const rule = {
4
+ min_size: 2_000_000_000,
5
+ max_size: 10_000_000_000,
6
+ step: 1_000_000_000,
7
+ };
8
+ it("accepts a valid quantity", () => {
9
+ expect(() => validateQuantityBySizeRule(6_000_000_000, rule)).not.toThrow();
10
+ });
11
+ it("rejects quantity below min", () => {
12
+ expect(() => validateQuantityBySizeRule(1_000_000_000, rule)).toThrow("must be within");
13
+ });
14
+ it("rejects quantity above max", () => {
15
+ expect(() => validateQuantityBySizeRule(11_000_000_000, rule)).toThrow("must be within");
16
+ });
17
+ it("rejects quantity with invalid step", () => {
18
+ expect(() => validateQuantityBySizeRule(2_500_000_000, rule)).toThrow("(quantity - min_size) % step == 0");
19
+ });
20
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acta-markets/ts-sdk",
3
- "version": "0.0.5-beta",
3
+ "version": "0.0.6-beta",
4
4
  "description": "TypeScript SDK for Acta Protocol",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",