@n1xyz/nord-ts 0.1.6 → 0.1.7

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.
Files changed (39) hide show
  1. package/dist/actions.d.ts +57 -0
  2. package/dist/actions.js +229 -0
  3. package/dist/client/Nord.d.ts +379 -0
  4. package/dist/client/Nord.js +718 -0
  5. package/dist/client/NordAdmin.d.ts +225 -0
  6. package/dist/client/NordAdmin.js +394 -0
  7. package/dist/client/NordUser.d.ts +350 -0
  8. package/dist/client/NordUser.js +743 -0
  9. package/dist/error.d.ts +35 -0
  10. package/dist/error.js +49 -0
  11. package/dist/gen/openapi.d.ts +40 -0
  12. package/dist/index.d.ts +6 -1
  13. package/dist/index.js +29 -1
  14. package/dist/nord/client/NordAdmin.js +2 -0
  15. package/dist/types.d.ts +4 -50
  16. package/dist/types.js +1 -24
  17. package/dist/utils.d.ts +8 -11
  18. package/dist/utils.js +54 -41
  19. package/dist/websocket/Subscriber.d.ts +37 -0
  20. package/dist/websocket/Subscriber.js +25 -0
  21. package/dist/websocket/index.d.ts +19 -2
  22. package/dist/websocket/index.js +82 -2
  23. package/package.json +1 -1
  24. package/src/actions.ts +333 -0
  25. package/src/{nord/client → client}/Nord.ts +207 -210
  26. package/src/{nord/client → client}/NordAdmin.ts +123 -153
  27. package/src/{nord/client → client}/NordUser.ts +216 -305
  28. package/src/gen/openapi.ts +40 -0
  29. package/src/index.ts +7 -1
  30. package/src/types.ts +4 -54
  31. package/src/utils.ts +44 -47
  32. package/src/{nord/models → websocket}/Subscriber.ts +2 -2
  33. package/src/websocket/index.ts +105 -2
  34. package/src/nord/api/actions.ts +0 -648
  35. package/src/nord/api/core.ts +0 -96
  36. package/src/nord/api/metrics.ts +0 -269
  37. package/src/nord/client/NordClient.ts +0 -79
  38. package/src/nord/index.ts +0 -25
  39. /package/src/{nord/utils/NordError.ts → error.ts} +0 -0
@@ -1,5 +1,85 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.NordWebSocketClient = void 0;
4
- var NordWebSocketClient_1 = require("./NordWebSocketClient");
3
+ exports.Subscriber = exports.NordWebSocketClient = void 0;
4
+ exports.initWebSocketClient = initWebSocketClient;
5
+ const NordWebSocketClient_1 = require("./NordWebSocketClient");
5
6
  Object.defineProperty(exports, "NordWebSocketClient", { enumerable: true, get: function () { return NordWebSocketClient_1.NordWebSocketClient; } });
7
+ const error_1 = require("../error");
8
+ const Subscriber_1 = require("./Subscriber");
9
+ Object.defineProperty(exports, "Subscriber", { enumerable: true, get: function () { return Subscriber_1.Subscriber; } });
10
+ /**
11
+ * Initialize a WebSocket client for Nord
12
+ *
13
+ * Connects to the Nord WebSocket endpoint with support for multiple subscription types:
14
+ * - trades@SYMBOL - For trade updates
15
+ * - deltas@SYMBOL - For orderbook delta updates
16
+ * - account@ACCOUNT_ID - For user-specific updates
17
+ *
18
+ * @param webServerUrl - Base URL for the Nord web server
19
+ * @param subscriptions - Array of subscriptions (e.g., ["trades@BTCUSDC", "deltas@BTCUSDC", "account@42"])
20
+ * @returns WebSocket client
21
+ * @throws {NordError} If initialization fails or invalid subscription is provided
22
+ */
23
+ function initWebSocketClient(webServerUrl, subscriptions) {
24
+ try {
25
+ // Determine URL and subscriptions based on parameters
26
+ let wsUrl = webServerUrl.replace(/^http/, "ws") + `/ws`;
27
+ // Validate subscriptions parameter
28
+ if (typeof subscriptions === "string") {
29
+ // Legacy mode - handle endpoint string
30
+ if (subscriptions === "trades" ||
31
+ subscriptions === "delta" ||
32
+ subscriptions === "account") {
33
+ wsUrl += `/${subscriptions}`;
34
+ }
35
+ else {
36
+ throw new error_1.NordError(`Invalid endpoint: ${subscriptions}. Must be "trades", "deltas", or "account".`);
37
+ }
38
+ }
39
+ else if (Array.isArray(subscriptions) && subscriptions.length > 0) {
40
+ // New mode - validate and combine subscriptions in URL
41
+ subscriptions.forEach(validateSubscription);
42
+ wsUrl += `/${subscriptions.join("&")}`;
43
+ }
44
+ else {
45
+ // Default to trades endpoint if no subscriptions specified
46
+ wsUrl += `/trades`;
47
+ }
48
+ console.log(`Initializing WebSocket client with URL: ${wsUrl}`);
49
+ // Create and connect the WebSocket client
50
+ const ws = new NordWebSocketClient_1.NordWebSocketClient(wsUrl);
51
+ // Add error handler
52
+ ws.on("error", (error) => {
53
+ console.error("Nord WebSocket error:", error);
54
+ });
55
+ // Add connected handler for debugging
56
+ ws.on("connected", () => {
57
+ console.log("Nord WebSocket connected successfully");
58
+ });
59
+ // Connect the WebSocket
60
+ ws.connect();
61
+ return ws;
62
+ }
63
+ catch (error) {
64
+ console.error("Failed to initialize WebSocket client:", error);
65
+ throw new error_1.NordError("Failed to initialize WebSocket client", {
66
+ cause: error,
67
+ });
68
+ }
69
+ }
70
+ /**
71
+ * Validates a subscription string follows the correct format
72
+ *
73
+ * @param subscription - The subscription to validate
74
+ * @throws {NordError} If the subscription format is invalid
75
+ */
76
+ function validateSubscription(subscription) {
77
+ const [type, param] = subscription.split("@");
78
+ if (!type || !param || !["trades", "deltas", "account"].includes(type)) {
79
+ throw new error_1.NordError(`Invalid subscription format: ${subscription}. Expected format: "trades@SYMBOL", "deltas@SYMBOL", or "account@ID"`);
80
+ }
81
+ // Additional validation for account subscriptions
82
+ if (type === "account" && isNaN(Number(param))) {
83
+ throw new error_1.NordError(`Invalid account ID in subscription: ${subscription}. Account ID must be a number.`);
84
+ }
85
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@n1xyz/nord-ts",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Typescript for Nord",
5
5
  "keywords": [],
6
6
  "author": "",
package/src/actions.ts ADDED
@@ -0,0 +1,333 @@
1
+ import Decimal from "decimal.js";
2
+ import * as proto from "./gen/nord_pb";
3
+ import { paths } from "./gen/openapi";
4
+ import { Client } from "openapi-fetch";
5
+ import { create } from "@bufbuild/protobuf";
6
+ import { FillMode, fillModeToProtoFillMode, Side, QuoteSize } from "./types";
7
+ import {
8
+ assert,
9
+ BigIntValue,
10
+ decodeLengthDelimited,
11
+ SESSION_TTL,
12
+ toScaledU64,
13
+ signUserPayload,
14
+ } from "./utils";
15
+ import { sizeDelimitedEncode } from "@bufbuild/protobuf/wire";
16
+ import { NordError } from "./error";
17
+ import { PublicKey, Transaction } from "@solana/web3.js";
18
+
19
+ type ReceiptKind = NonNullable<proto.Receipt["kind"]>;
20
+ type ExtractReceiptKind<K extends ReceiptKind["case"]> = Extract<
21
+ ReceiptKind,
22
+ { case: K }
23
+ >;
24
+
25
+ export function formatReceiptError(receipt: proto.Receipt): string {
26
+ if (receipt.kind?.case === "err") {
27
+ const err = receipt.kind.value;
28
+ return proto.Error[err] ?? err.toString();
29
+ }
30
+ return receipt.kind?.case ?? "unknown";
31
+ }
32
+
33
+ export function expectReceiptKind<K extends ReceiptKind["case"]>(
34
+ receipt: proto.Receipt,
35
+ expected: K,
36
+ action: string,
37
+ ): asserts receipt is proto.Receipt & { kind: ExtractReceiptKind<K> } {
38
+ if (receipt.kind?.case !== expected) {
39
+ const label = formatReceiptError(receipt);
40
+ throw new NordError(`Failed to ${action}: ${label}`);
41
+ }
42
+ }
43
+
44
+ async function sessionSign(
45
+ signFn: (message: Uint8Array) => Promise<Uint8Array>,
46
+ message: Uint8Array,
47
+ ): Promise<Uint8Array> {
48
+ const signature = await signFn(message);
49
+ return new Uint8Array([...message, ...signature]);
50
+ }
51
+
52
+ // Helper to create an action with common fields
53
+ export function createAction(
54
+ currentTimestamp: bigint,
55
+ nonce: number,
56
+ kind: proto.Action["kind"],
57
+ ): proto.Action {
58
+ return create(proto.ActionSchema, {
59
+ currentTimestamp,
60
+ nonce,
61
+ kind,
62
+ });
63
+ }
64
+
65
+ export async function sendAction(
66
+ client: Client<paths>,
67
+ makeSignedMessage: (message: Uint8Array) => Promise<Uint8Array>,
68
+ action: proto.Action,
69
+ ): Promise<proto.Receipt> {
70
+ const body = await prepareAction(action, makeSignedMessage);
71
+
72
+ const response = await client.POST("/action", {
73
+ params: {
74
+ header: {
75
+ "content-type": "application/octet-stream",
76
+ },
77
+ },
78
+ body: body,
79
+ // NOTE: openapi-fetch ignores headers and types/const headers in schema, and always assume all things are JSON
80
+ // to handle multi type bodies, need these overrides and later adhoc parsing
81
+ bodySerializer: (body) => body,
82
+ parseAs: "stream",
83
+ });
84
+
85
+ if (response.error) {
86
+ throw new Error(
87
+ `Failed to ${action.kind.case}, HTTP status ${JSON.stringify(response.error)}`,
88
+ );
89
+ }
90
+
91
+ const rawResp = new Uint8Array(await response.response.bytes());
92
+
93
+ const resp: proto.Receipt = decodeLengthDelimited(
94
+ rawResp,
95
+ proto.ReceiptSchema,
96
+ );
97
+
98
+ if (resp.kind?.case === "err") {
99
+ throw new Error(
100
+ `Could not execute ${action.kind.case}, reason: ${proto.Error[resp.kind.value]}`,
101
+ );
102
+ }
103
+
104
+ return resp;
105
+ }
106
+
107
+ // Given action and signature function, prepare the signed message to send to server as `body`.
108
+ // `makeSignedMessage` must include the original message and signature.
109
+ export async function prepareAction(
110
+ action: proto.Action,
111
+ makeSignedMessage: (message: Uint8Array) => Promise<Uint8Array>,
112
+ ) {
113
+ const encoded = sizeDelimitedEncode(proto.ActionSchema, action);
114
+ // NOTE(agent): keep in sync with MAX_ENCODED_ACTION_SIZE in Rust code
115
+ const MAX_ENCODED_ACTION_SIZE = 1024;
116
+ if (encoded.byteLength > MAX_ENCODED_ACTION_SIZE) {
117
+ console.warn("Encoded message:", encoded);
118
+ throw new Error(
119
+ `Encoded message size (${encoded.byteLength} bytes) is greater than max payload size (${MAX_ENCODED_ACTION_SIZE} bytes).`,
120
+ );
121
+ }
122
+ const body = await makeSignedMessage(encoded);
123
+ if (body.byteLength > MAX_ENCODED_ACTION_SIZE) {
124
+ console.warn("Encoded length:", encoded.byteLength);
125
+ throw new Error(
126
+ `Signed message size (${body.byteLength} bytes) is greater than max payload size (${MAX_ENCODED_ACTION_SIZE} bytes).`,
127
+ );
128
+ }
129
+ return body;
130
+ }
131
+
132
+ export async function createSession(
133
+ client: Client<paths>,
134
+ signTransaction: (tx: Transaction) => Promise<Transaction>,
135
+ currentTimestamp: bigint,
136
+ nonce: number,
137
+ params: {
138
+ userPubkey: PublicKey;
139
+ sessionPubkey: PublicKey;
140
+ // If not specified, set to current moment plus default session TTL
141
+ expiryTimestamp?: bigint;
142
+ },
143
+ ): Promise<{ actionId: bigint; sessionId: bigint }> {
144
+ let expiry = 0n;
145
+
146
+ if (params.expiryTimestamp !== undefined) {
147
+ expiry = params.expiryTimestamp;
148
+ assert(
149
+ expiry > currentTimestamp,
150
+ "Cannot set expiry timestamp in the past",
151
+ );
152
+ } else {
153
+ expiry = currentTimestamp + SESSION_TTL;
154
+ }
155
+
156
+ const action = createAction(currentTimestamp, nonce, {
157
+ case: "createSession",
158
+ value: create(proto.Action_CreateSessionSchema, {
159
+ userPubkey: params.userPubkey.toBytes(),
160
+ blstPubkey: params.sessionPubkey.toBytes(),
161
+ expiryTimestamp: expiry,
162
+ }),
163
+ });
164
+
165
+ const resp = await sendAction(
166
+ client,
167
+ async (payload) => {
168
+ return new Uint8Array([
169
+ ...payload,
170
+ ...(await signUserPayload({
171
+ payload,
172
+ user: params.userPubkey,
173
+ signTransaction,
174
+ })),
175
+ ]);
176
+ },
177
+ action,
178
+ );
179
+
180
+ if (resp.kind?.case === "createSessionResult") {
181
+ return {
182
+ actionId: resp.actionId,
183
+ sessionId: resp.kind.value.sessionId,
184
+ };
185
+ } else {
186
+ throw new Error(`Unexpected receipt kind ${resp.kind?.case}`);
187
+ }
188
+ }
189
+
190
+ export async function revokeSession(
191
+ client: Client<paths>,
192
+ signTransaction: (tx: Transaction) => Promise<Transaction>,
193
+ currentTimestamp: bigint,
194
+ nonce: number,
195
+ params: {
196
+ sessionId: BigIntValue;
197
+ userPubkey: PublicKey;
198
+ },
199
+ ): Promise<{ actionId: bigint }> {
200
+ const action = createAction(currentTimestamp, nonce, {
201
+ case: "revokeSession",
202
+ value: create(proto.Action_RevokeSessionSchema, {
203
+ sessionId: BigInt(params.sessionId),
204
+ }),
205
+ });
206
+
207
+ const resp = await sendAction(
208
+ client,
209
+ async (payload) => {
210
+ return new Uint8Array([
211
+ ...payload,
212
+ ...(await signUserPayload({
213
+ payload,
214
+ user: params.userPubkey,
215
+ signTransaction,
216
+ })),
217
+ ]);
218
+ },
219
+ action,
220
+ );
221
+
222
+ return { actionId: resp.actionId };
223
+ }
224
+
225
+ export type AtomicSubaction =
226
+ | {
227
+ kind: "place";
228
+ // Market and order parameters – identical semantics to placeOrder()
229
+ marketId: number;
230
+ side: Side;
231
+ fillMode: FillMode;
232
+ isReduceOnly: boolean;
233
+ // decimals for scaling
234
+ sizeDecimals: number;
235
+ priceDecimals: number;
236
+ // at least one of the three has to be specified; 0 treated as "not set"
237
+ size?: Decimal.Value;
238
+ price?: Decimal.Value;
239
+ quoteSize?: QuoteSize;
240
+ clientOrderId?: BigIntValue;
241
+ }
242
+ | {
243
+ kind: "cancel";
244
+ orderId: BigIntValue;
245
+ };
246
+
247
+ export async function atomic(
248
+ client: Client<paths>,
249
+ signFn: (message: Uint8Array) => Promise<Uint8Array>,
250
+ currentTimestamp: bigint,
251
+ nonce: number,
252
+ params: {
253
+ sessionId: BigIntValue;
254
+ accountId?: number;
255
+ actions: AtomicSubaction[];
256
+ },
257
+ ): Promise<{
258
+ actionId: bigint;
259
+ results: proto.Receipt_AtomicSubactionResultKind[];
260
+ }> {
261
+ assert(
262
+ params.actions.length > 0 && params.actions.length <= 4,
263
+ "Atomic action must contain between 1 and 4 sub-actions",
264
+ );
265
+
266
+ const subactions: proto.AtomicSubactionKind[] = params.actions.map((a) => {
267
+ if (a.kind === "place") {
268
+ const price = toScaledU64(a.price ?? 0, a.priceDecimals);
269
+ const size = toScaledU64(a.size ?? 0, a.sizeDecimals);
270
+ const scaledQuote = a.quoteSize
271
+ ? a.quoteSize.toWire(a.priceDecimals, a.sizeDecimals)
272
+ : undefined;
273
+
274
+ // Require at least one limit to be set (non-zero size, non-zero price, or quoteSize)
275
+ assert(
276
+ price > 0n || size > 0n || scaledQuote !== undefined,
277
+ "OrderLimit must include at least one of: size, price, or quoteSize",
278
+ );
279
+
280
+ const tradeOrPlace: proto.TradeOrPlace = create(
281
+ proto.TradeOrPlaceSchema,
282
+ {
283
+ marketId: a.marketId,
284
+ orderType: create(proto.OrderTypeSchema, {
285
+ side: a.side === Side.Bid ? proto.Side.BID : proto.Side.ASK,
286
+ fillMode: fillModeToProtoFillMode(a.fillMode),
287
+ isReduceOnly: a.isReduceOnly,
288
+ }),
289
+ limit: create(proto.OrderLimitSchema, {
290
+ price,
291
+ size,
292
+ quoteSize:
293
+ scaledQuote === undefined
294
+ ? undefined
295
+ : create(proto.QuoteSizeSchema, {
296
+ size: scaledQuote.size,
297
+ price: scaledQuote.price,
298
+ }),
299
+ }),
300
+ clientOrderId:
301
+ a.clientOrderId === undefined ? undefined : BigInt(a.clientOrderId),
302
+ },
303
+ );
304
+ return create(proto.AtomicSubactionKindSchema, {
305
+ inner: { case: "tradeOrPlace", value: tradeOrPlace },
306
+ });
307
+ }
308
+ return create(proto.AtomicSubactionKindSchema, {
309
+ inner: {
310
+ case: "cancelOrder",
311
+ value: create(proto.CancelOrderSchema, { orderId: BigInt(a.orderId) }),
312
+ },
313
+ });
314
+ });
315
+
316
+ const action = createAction(currentTimestamp, nonce, {
317
+ case: "atomic",
318
+ value: create(proto.AtomicSchema, {
319
+ sessionId: BigInt(params.sessionId),
320
+ accountId: params.accountId, // optional
321
+ actions: subactions,
322
+ }),
323
+ });
324
+
325
+ const resp = await sendAction(client, (m) => sessionSign(signFn, m), action);
326
+ if (resp.kind?.case === "atomic") {
327
+ return {
328
+ actionId: resp.actionId,
329
+ results: resp.kind.value.results,
330
+ };
331
+ }
332
+ throw new Error(`Unexpected receipt kind ${resp.kind?.case}`);
333
+ }