@acta-markets/ts-sdk 0.0.5-beta → 0.0.7-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
@@ -8,10 +8,10 @@ Generated by Codama.
8
8
 
9
9
  ## Package
10
10
 
11
- Published package: **`@acta-markets/ts-sdk-v1@0.0.9-beta`** (ESM-first, CJS fallback)
11
+ Published package: **`@acta-markets/ts-sdk@0.0.6-beta`** (ESM-first, CJS fallback)
12
12
 
13
13
  ```bash
14
- yarn add @acta-markets/ts-sdk-v1@0.0.9-beta
14
+ yarn add @acta-markets/ts-sdk@0.0.6-beta
15
15
  ```
16
16
 
17
17
  ## Tooling
@@ -38,7 +38,7 @@ All instructions/codecs are in `src/generated/instructions` (Codama @solana/kit
38
38
  You can instantiate WS-only, chain-only, or both:
39
39
 
40
40
  ```ts
41
- import { ActaClient } from "@acta-markets/ts-sdk-v1";
41
+ import { ActaClient } from "@acta-markets/ts-sdk";
42
42
 
43
43
  // WS-only
44
44
  const c1 = new ActaClient({ ws: { url: "wss://...", role: "taker" } });
@@ -52,8 +52,8 @@ const c2 = new ActaClient({ chain: { rpc, rpcSubscriptions, payer } });
52
52
  ### 1) Connect to WS anonymously, then authenticate on “Connect wallet”
53
53
 
54
54
  ```ts
55
- import { ActaClient } from "@acta-markets/ts-sdk-v1";
56
- import { WalletAuthProvider } from "@acta-markets/ts-sdk-v1/ws";
55
+ import { ActaClient } from "@acta-markets/ts-sdk";
56
+ import { WalletAuthProvider } from "@acta-markets/ts-sdk/ws";
57
57
 
58
58
  const client = new ActaClient({ ws: { url: "wss://localhost:8080", role: "taker" } });
59
59
  const auth = new WalletAuthProvider({
@@ -125,7 +125,7 @@ await client.ws!.acceptQuote(rfqId, makerPubkeyBase58, orderIdHex32);
125
125
  client.ws!.on("sponsoredTxToSign", async (orderIdHex, txBase64, signatureDeadline) => {
126
126
  // If your wallet exposes `signTransaction` (Phantom/Privy via adapters), you can use it.
127
127
  // Alternatively (lighter): sign the **message bytes** and fill signature slot 1.
128
- const signedTxBase64 = await import("@acta-markets/ts-sdk-v1/ws").then(({ signSponsoredTxBase64 }) =>
128
+ const signedTxBase64 = await import("@acta-markets/ts-sdk/ws").then(({ signSponsoredTxBase64 }) =>
129
129
  signSponsoredTxBase64({
130
130
  txBase64,
131
131
  taker: { signMessage: (msg: Uint8Array) => wallet.signMessage(msg) },
@@ -184,7 +184,7 @@ Then annualize using time to expiry \(dt = expiry\_ts - now\):
184
184
  TS SDK helper (WS-only, no on-chain RPC needed):
185
185
 
186
186
  ```ts
187
- import { ws } from "@acta-markets/ts-sdk-v1";
187
+ import { ws } from "@acta-markets/ts-sdk";
188
188
 
189
189
  const secondsToExpiry = rfq.market.expiry_ts - Math.floor(Date.now() / 1000);
190
190
 
@@ -207,7 +207,7 @@ Note:
207
207
  Build the minimal instruction set for `open_position` (maker ed25519 verify + open ix):
208
208
 
209
209
  ```ts
210
- import { chain } from "@acta-markets/ts-sdk-v1";
210
+ import { chain } from "@acta-markets/ts-sdk";
211
211
 
212
212
  const built = await chain.flows.buildOpenPositionFlowIxs({
213
213
  underlyingMint,
@@ -237,7 +237,7 @@ By default the SDK targets the mainnet deployment program id exported from `src/
237
237
  For tests/devnet you can override it at runtime:
238
238
 
239
239
  ```ts
240
- import { setActaProgramId, clearActaProgramId } from "@acta-markets/ts-sdk-v1";
240
+ import { setActaProgramId, clearActaProgramId } from "@acta-markets/ts-sdk";
241
241
  // setActaProgramId("...devnet program id..." as Address<string>);
242
242
  // clearActaProgramId();
243
243
  ```
@@ -249,14 +249,14 @@ On-chain expects an **Ed25519 verify instruction** (maker) immediately followed
249
249
  ```ts
250
250
  import { randomBytes } from "crypto";
251
251
  import { Address } from "@solana/addresses";
252
- import { PositionType } from "@acta-markets/ts-sdk-v1";
252
+ import { PositionType } from "@acta-markets/ts-sdk";
253
253
  import {
254
254
  computeOrderId,
255
255
  orderSignatureInstruction,
256
256
  findMarketPda,
257
257
  findPositionPda,
258
258
  buildOpenPositionIx,
259
- } from "@acta-markets/ts-sdk-v1/chain";
259
+ } from "@acta-markets/ts-sdk/chain";
260
260
 
261
261
  const nonce = Buffer.from(randomBytes(8)).readBigUInt64LE(0);
262
262
  const orderId = computeOrderId({
@@ -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 ?? []);
@@ -559,9 +573,13 @@ class ActaWsClient extends TypedEventEmitter {
559
573
  const rfq = this.state.activeRfqs.get(message.data.rfq_id);
560
574
  if (rfq) {
561
575
  const nextCount = rfq.quotes_count + 1;
576
+ // Use net_price for best_price tracking to stay consistent with
577
+ // server-side best_price (also net). Fall back to gross price when
578
+ // fee config is unavailable (net_price absent).
579
+ const quotePrice = message.data.net_price ?? message.data.price;
562
580
  const nextBest = rfq.best_price === null
563
- ? message.data.price
564
- : Math.max(rfq.best_price, message.data.price);
581
+ ? quotePrice
582
+ : Math.max(rfq.best_price, quotePrice);
565
583
  this.state.activeRfqs.set(message.data.rfq_id, {
566
584
  ...rfq,
567
585
  quotes_count: nextCount,
@@ -575,7 +593,8 @@ class ActaWsClient extends TypedEventEmitter {
575
593
  {
576
594
  const rfq = this.state.activeRfqs.get(message.data.rfq_id);
577
595
  if (rfq) {
578
- const prices = message.data.quotes.map((q) => q.price);
596
+ // Use net_price for consistency with server-side best_price (also net).
597
+ const prices = message.data.quotes.map((q) => q.net_price ?? q.price);
579
598
  const nextBest = prices.length === 0 ? null : Math.max(...prices);
580
599
  this.state.activeRfqs.set(message.data.rfq_id, {
581
600
  ...rfq,
@@ -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 ?? []);
@@ -556,9 +570,13 @@ export class ActaWsClient extends TypedEventEmitter {
556
570
  const rfq = this.state.activeRfqs.get(message.data.rfq_id);
557
571
  if (rfq) {
558
572
  const nextCount = rfq.quotes_count + 1;
573
+ // Use net_price for best_price tracking to stay consistent with
574
+ // server-side best_price (also net). Fall back to gross price when
575
+ // fee config is unavailable (net_price absent).
576
+ const quotePrice = message.data.net_price ?? message.data.price;
559
577
  const nextBest = rfq.best_price === null
560
- ? message.data.price
561
- : Math.max(rfq.best_price, message.data.price);
578
+ ? quotePrice
579
+ : Math.max(rfq.best_price, quotePrice);
562
580
  this.state.activeRfqs.set(message.data.rfq_id, {
563
581
  ...rfq,
564
582
  quotes_count: nextCount,
@@ -572,7 +590,8 @@ export class ActaWsClient extends TypedEventEmitter {
572
590
  {
573
591
  const rfq = this.state.activeRfqs.get(message.data.rfq_id);
574
592
  if (rfq) {
575
- const prices = message.data.quotes.map((q) => q.price);
593
+ // Use net_price for consistency with server-side best_price (also net).
594
+ const prices = message.data.quotes.map((q) => q.net_price ?? q.price);
576
595
  const nextBest = prices.length === 0 ? null : Math.max(...prices);
577
596
  this.state.activeRfqs.set(message.data.rfq_id, {
578
597
  ...rfq,
@@ -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 = {
@@ -783,12 +790,20 @@ export type QuoteReceivedMessage = {
783
790
  rfq_id: UuidString;
784
791
  strike: WsU64;
785
792
  maker: Address<string>;
793
+ /** Gross price from maker. Hash-bound via order_id — use for AcceptQuote / order verification. */
786
794
  price: WsU64;
787
795
  valid_until: WsU64;
788
796
  /** u64 nonce used in the orderId preimage. */
789
797
  nonce: WsU64;
790
798
  /** Order ID hex string (64 chars). */
791
799
  order_id: OrderIdHex32;
800
+ /**
801
+ * Net price after protocol fee deduction (display only).
802
+ * `net_price = price * (10_000 - fee_bps) / 10_000`
803
+ * Present when the server has loaded the on-chain fee config.
804
+ * Use this for UI display; use `price` for all order operations.
805
+ */
806
+ net_price?: WsU64 | null;
792
807
  };
793
808
  export type QuotesUpdateMessage = {
794
809
  rfq_id: UuidString;
@@ -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.7-beta",
4
4
  "description": "TypeScript SDK for Acta Protocol",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",