@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.
- 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 +15 -1
- package/dist/cjs/ws/client.test.js +86 -0
- 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 +1 -0
- package/dist/ws/client.js +16 -2
- package/dist/ws/client.test.js +86 -0
- package/dist/ws/discovery.js +2 -0
- package/dist/ws/types.d.ts +12 -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/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
|
@@ -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
|
-
|
|
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,
|
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
|
+
}
|
|
@@ -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
|
+
});
|
package/dist/events.test.js
CHANGED
|
@@ -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
|
},
|
package/dist/idl/hash.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const ACTA_IDL_SHA256 = "
|
|
1
|
+
export declare const ACTA_IDL_SHA256 = "704677f7071ecbe98f442fb62468d882329b906950a707fc47aad0f38683636f";
|
package/dist/idl/hash.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const ACTA_IDL_SHA256 = "
|
|
1
|
+
export const ACTA_IDL_SHA256 = "704677f7071ecbe98f442fb62468d882329b906950a707fc47aad0f38683636f";
|
package/dist/ws/client.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 ?? []);
|
package/dist/ws/client.test.js
CHANGED
|
@@ -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,
|
package/dist/ws/discovery.js
CHANGED
|
@@ -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();
|
package/dist/ws/types.d.ts
CHANGED
|
@@ -469,13 +469,20 @@ export type MarketDescriptorInfo = {
|
|
|
469
469
|
quote_oracle_pda: PubkeyBase58;
|
|
470
470
|
underlying_decimals: number;
|
|
471
471
|
quote_decimals: number;
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
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
|
|
715
|
-
quote_symbol
|
|
721
|
+
underlying_symbol: string;
|
|
722
|
+
quote_symbol: string;
|
|
716
723
|
stats?: MarketStats;
|
|
717
724
|
};
|
|
718
725
|
export type MarketStats = {
|
package/dist/ws/wirePolicy.d.ts
CHANGED
|
@@ -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;
|
package/dist/ws/wirePolicy.js
CHANGED
|
@@ -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
|
+
});
|