@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 +11 -11
- 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 +23 -4
- 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 +24 -5
- package/dist/ws/client.test.js +86 -0
- package/dist/ws/discovery.js +2 -0
- package/dist/ws/types.d.ts +20 -5
- package/dist/ws/wirePolicy.d.ts +6 -0
- package/dist/ws/wirePolicy.js +15 -0
- package/dist/ws/wirePolicy.test.d.ts +1 -0
- package/dist/ws/wirePolicy.test.js +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ Generated by Codama.
|
|
|
8
8
|
|
|
9
9
|
## Package
|
|
10
10
|
|
|
11
|
-
Published package: **`@acta-markets/ts-sdk
|
|
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
|
|
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
|
|
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
|
|
56
|
-
import { WalletAuthProvider } from "@acta-markets/ts-sdk
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
259
|
+
} from "@acta-markets/ts-sdk/chain";
|
|
260
260
|
|
|
261
261
|
const nonce = Buffer.from(randomBytes(8)).readBigUInt64LE(0);
|
|
262
262
|
const orderId = computeOrderId({
|
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 ?? []);
|
|
@@ -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
|
-
?
|
|
564
|
-
: Math.max(rfq.best_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
|
-
|
|
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,
|
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 ?? []);
|
|
@@ -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
|
-
?
|
|
561
|
-
: Math.max(rfq.best_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
|
-
|
|
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,
|
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 = {
|
|
@@ -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;
|
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
|
+
});
|