@n1xyz/nord-ts 0.1.3 → 0.1.5

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.
@@ -0,0 +1,441 @@
1
+ import { create } from "@bufbuild/protobuf";
2
+ import { PublicKey } from "@solana/web3.js";
3
+ import * as proto from "../../gen/nord_pb";
4
+ import { decodeHex } from "../../utils";
5
+ import { createAction, sendAction, expectReceiptKind } from "../api/actions";
6
+ import { NordError } from "../utils/NordError";
7
+ import { Nord } from "./Nord";
8
+ import { FeeTierConfig } from "../../gen/nord_pb";
9
+
10
+ /**
11
+ * Parameters required to register a new token via the admin API.
12
+ */
13
+ export interface CreateTokenParams {
14
+ tokenDecimals: number;
15
+ weightBps: number;
16
+ viewSymbol: string;
17
+ oracleSymbol: string;
18
+ mintAddr: PublicKey;
19
+ }
20
+
21
+ /**
22
+ * Parameters used when creating a new market.
23
+ */
24
+ export interface CreateMarketParams {
25
+ sizeDecimals: number;
26
+ priceDecimals: number;
27
+ imfBps: number;
28
+ cmfBps: number;
29
+ mmfBps: number;
30
+ marketType: proto.MarketType;
31
+ viewSymbol: string;
32
+ oracleSymbol: string;
33
+ baseTokenId: number;
34
+ }
35
+
36
+ /**
37
+ * Configuration for updating the Wormhole guardian set on the oracle.
38
+ */
39
+ export interface PythSetWormholeGuardiansParams {
40
+ guardianSetIndex: number;
41
+ addresses: string[];
42
+ }
43
+
44
+ /**
45
+ * Parameters required to link an oracle symbol to a Pyth price feed.
46
+ */
47
+ export interface PythSetSymbolFeedParams {
48
+ oracleSymbol: string;
49
+ priceFeedId: string;
50
+ }
51
+
52
+ /**
53
+ * Identifies a market that should be frozen.
54
+ */
55
+ export interface FreezeMarketParams {
56
+ marketId: number;
57
+ }
58
+
59
+ /**
60
+ * Identifies a market that should be unfrozen.
61
+ */
62
+ export interface UnfreezeMarketParams {
63
+ marketId: number;
64
+ }
65
+
66
+ /**
67
+ * Parameters for adding a new fee tier.
68
+ */
69
+ export interface AddFeeTierParams {
70
+ config: FeeTierConfig;
71
+ }
72
+
73
+ /**
74
+ * Parameters for updating an existing fee tier.
75
+ */
76
+ export interface UpdateFeeTierParams {
77
+ tierId: number;
78
+ config: FeeTierConfig;
79
+ }
80
+
81
+ /**
82
+ * Administrative client capable of submitting privileged configuration actions.
83
+ */
84
+ export class NordAdmin {
85
+ private readonly nord: Nord;
86
+ private readonly signFn: (x: Uint8Array) => Promise<Uint8Array>;
87
+
88
+ private constructor({
89
+ nord,
90
+ signFn,
91
+ }: {
92
+ nord: Nord;
93
+ signFn: (x: Uint8Array) => Promise<Uint8Array>;
94
+ }) {
95
+ this.nord = nord;
96
+ this.signFn = signFn;
97
+ }
98
+
99
+ /** Create a new admin client.
100
+ *
101
+ * @param nord - Nord instance
102
+ * @param signFn - Function to sign messages with the admin's wallet.
103
+ *
104
+ * `signFn` must sign the _hex-encoded_ message, not the raw message itself, for
105
+ * the purpose of being compatible with Solana wallets.
106
+ *
107
+ * In practice, you will do something along the lines of:
108
+ *
109
+ * ```typescript
110
+ * (x) => wallet.signMessage(new TextEncoder().encode(x.toHex()));
111
+ * ```
112
+ *
113
+ * For a software signing key, this might look more like:
114
+ *
115
+ * ```typescript
116
+ * (x) => nacl.sign.detached(new TextEncoder().encode(x.toHex()), sk);
117
+ * ``
118
+ *
119
+ * where `nacl` is the tweetnacl library.
120
+ */
121
+ public static new({
122
+ nord,
123
+ signFn,
124
+ }: Readonly<{
125
+ nord: Nord;
126
+ signFn: (m: Uint8Array) => Promise<Uint8Array>;
127
+ }>): NordAdmin {
128
+ return new NordAdmin({
129
+ nord,
130
+ signFn,
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Submit an action and append the admin signature before sending it to Nord.
136
+ *
137
+ * @param kind - Action payload describing the admin request
138
+ * @throws {NordError} If signing or submission fails
139
+ */
140
+ private async submitAction(
141
+ kind: proto.Action["kind"],
142
+ ): Promise<proto.Receipt> {
143
+ const timestamp = await this.nord.getTimestamp();
144
+ const action = createAction(timestamp, 0, kind);
145
+ return sendAction(
146
+ this.nord.webServerUrl,
147
+ async (xs: Uint8Array) => {
148
+ const signature = await this.signFn(xs);
149
+ if (signature.length !== 64) {
150
+ throw new NordError("invalid signature length; must be 64 bytes");
151
+ }
152
+ return Uint8Array.from([...xs, ...signature]);
153
+ },
154
+ action,
155
+ );
156
+ }
157
+
158
+ /**
159
+ * Register a new token that can be listed on Nord.
160
+ *
161
+ * @param params - Token configuration values
162
+ * @returns Action identifier and resulting token metadata
163
+ * @throws {NordError} If the action submission fails
164
+ */
165
+ async createToken({
166
+ tokenDecimals,
167
+ weightBps,
168
+ viewSymbol,
169
+ oracleSymbol,
170
+ mintAddr,
171
+ }: CreateTokenParams): Promise<
172
+ { actionId: bigint } & proto.Receipt_InsertTokenResult
173
+ > {
174
+ const receipt = await this.submitAction({
175
+ case: "createToken",
176
+ value: create(proto.Action_CreateTokenSchema, {
177
+ tokenDecimals,
178
+ weightBps,
179
+ viewSymbol,
180
+ oracleSymbol,
181
+ solAddr: mintAddr.toBytes(),
182
+ }),
183
+ });
184
+ expectReceiptKind(receipt, "insertTokenResult", "create token");
185
+ return { actionId: receipt.actionId, ...receipt.kind.value };
186
+ }
187
+
188
+ /**
189
+ * Open a new market with the provided trading parameters.
190
+ *
191
+ * @param params - Market configuration to apply
192
+ * @returns Action identifier and resulting market metadata
193
+ * @throws {NordError} If the action submission fails
194
+ */
195
+ async createMarket(
196
+ params: CreateMarketParams,
197
+ ): Promise<{ actionId: bigint } & proto.Receipt_InsertMarketResult> {
198
+ const receipt = await this.submitAction({
199
+ case: "createMarket",
200
+ value: create(proto.Action_CreateMarketSchema, {
201
+ sizeDecimals: params.sizeDecimals,
202
+ priceDecimals: params.priceDecimals,
203
+ imfBps: params.imfBps,
204
+ cmfBps: params.cmfBps,
205
+ mmfBps: params.mmfBps,
206
+ marketType: params.marketType,
207
+ viewSymbol: params.viewSymbol,
208
+ oracleSymbol: params.oracleSymbol,
209
+ baseTokenId: params.baseTokenId,
210
+ }),
211
+ });
212
+ expectReceiptKind(receipt, "insertMarketResult", "create market");
213
+ return { actionId: receipt.actionId, ...receipt.kind.value };
214
+ }
215
+
216
+ /**
217
+ * Update the Pyth guardian set used for verifying Wormhole messages.
218
+ *
219
+ * Each address must decode from a 20-byte hex string (with or without a
220
+ * leading `0x` prefix). The engine validates the supplied guardian set index
221
+ * before applying the update.
222
+ *
223
+ * @param params - Guardian set index and guardian addresses
224
+ * @returns Action identifier and guardian update receipt
225
+ * @throws {NordError} If the action submission fails
226
+ */
227
+ async pythSetWormholeGuardians(
228
+ params: PythSetWormholeGuardiansParams,
229
+ ): Promise<{ actionId: bigint } & proto.Receipt_UpdateGuardianSetResult> {
230
+ const addresses = params.addresses.map((address) => {
231
+ try {
232
+ const decoded = decodeHex(address);
233
+ if (decoded.length !== 20) {
234
+ throw new Error("guardian address must be 20 bytes");
235
+ }
236
+ return decoded;
237
+ } catch (e) {
238
+ throw new NordError(
239
+ "invalid guardian address; must be a 20 byte hex address",
240
+ { cause: e },
241
+ );
242
+ }
243
+ });
244
+
245
+ const receipt = await this.submitAction({
246
+ case: "pythSetWormholeGuardians",
247
+ value: create(proto.Action_PythSetWormholeGuardiansSchema, {
248
+ guardianSetIndex: params.guardianSetIndex,
249
+ addresses,
250
+ }),
251
+ });
252
+ expectReceiptKind(
253
+ receipt,
254
+ "updateGuardianSetResult",
255
+ "update wormhole guardians",
256
+ );
257
+ return { actionId: receipt.actionId, ...receipt.kind.value };
258
+ }
259
+
260
+ /**
261
+ * Link an oracle symbol to a specific Pyth price feed.
262
+ *
263
+ * The price feed identifier must decode to 32 bytes (with or without a
264
+ * leading `0x` prefix). Use this call to create or update the mapping used
265
+ * by the oracle integration.
266
+ *
267
+ * @param params - Oracle symbol and price feed identifier
268
+ * @returns Action identifier and symbol feed receipt
269
+ * @throws {NordError} If the action submission fails
270
+ */
271
+ async pythSetSymbolFeed(
272
+ params: PythSetSymbolFeedParams,
273
+ ): Promise<{ actionId: bigint } & proto.Receipt_OracleSymbolFeedResult> {
274
+ let priceFeedId: Uint8Array;
275
+ try {
276
+ priceFeedId = decodeHex(params.priceFeedId);
277
+ if (priceFeedId.length !== 32) {
278
+ throw new Error("price feed id must be 32 bytes");
279
+ }
280
+ } catch (e) {
281
+ throw new NordError("invalid price feed id; must be a 32 byte hex id", {
282
+ cause: e,
283
+ });
284
+ }
285
+
286
+ const receipt = await this.submitAction({
287
+ case: "pythSetSymbolFeed",
288
+ value: create(proto.Action_PythSetSymbolFeedSchema, {
289
+ oracleSymbol: params.oracleSymbol,
290
+ priceFeedId,
291
+ }),
292
+ });
293
+ expectReceiptKind(receipt, "oracleSymbolFeedResult", "set symbol feed");
294
+ return { actionId: receipt.actionId, ...receipt.kind.value };
295
+ }
296
+
297
+ /**
298
+ * Pause all trading activity on the exchange.
299
+ *
300
+ * @returns Action identifier confirming the pause
301
+ * @throws {NordError} If the action submission fails
302
+ */
303
+ async pause(): Promise<{ actionId: bigint }> {
304
+ const receipt = await this.submitAction({
305
+ case: "pause",
306
+ value: create(proto.Action_PauseSchema, {}),
307
+ });
308
+ expectReceiptKind(receipt, "paused", "pause");
309
+ return { actionId: receipt.actionId };
310
+ }
311
+
312
+ /**
313
+ * Resume trading activity after a pause.
314
+ *
315
+ * @returns Action identifier confirming the unpause
316
+ * @throws {NordError} If the action submission fails
317
+ */
318
+ async unpause(): Promise<{ actionId: bigint }> {
319
+ const receipt = await this.submitAction({
320
+ case: "unpause",
321
+ value: create(proto.Action_UnpauseSchema, {}),
322
+ });
323
+ expectReceiptKind(receipt, "unpaused", "unpause");
324
+ return { actionId: receipt.actionId };
325
+ }
326
+
327
+ /**
328
+ * Freeze an individual market, preventing new trades and orders.
329
+ *
330
+ * @param params - Target market identifier
331
+ * @returns Action identifier and freeze receipt
332
+ * @throws {NordError} If the action submission fails
333
+ */
334
+ async freezeMarket(
335
+ params: FreezeMarketParams,
336
+ ): Promise<{ actionId: bigint } & proto.Receipt_MarketFreezeUpdated> {
337
+ const receipt = await this.submitAction({
338
+ case: "freezeMarket",
339
+ value: create(proto.Action_FreezeMarketSchema, {
340
+ marketId: params.marketId,
341
+ }),
342
+ });
343
+ expectReceiptKind(receipt, "marketFreezeUpdated", "freeze market");
344
+ return { actionId: receipt.actionId, ...receipt.kind.value };
345
+ }
346
+
347
+ /**
348
+ * Unfreeze a market that was previously halted.
349
+ *
350
+ * @param params - Target market identifier
351
+ * @returns Action identifier and freeze receipt
352
+ * @throws {NordError} If the action submission fails
353
+ */
354
+ async unfreezeMarket(
355
+ params: UnfreezeMarketParams,
356
+ ): Promise<{ actionId: bigint } & proto.Receipt_MarketFreezeUpdated> {
357
+ const receipt = await this.submitAction({
358
+ case: "unfreezeMarket",
359
+ value: create(proto.Action_UnfreezeMarketSchema, {
360
+ marketId: params.marketId,
361
+ }),
362
+ });
363
+ expectReceiptKind(receipt, "marketFreezeUpdated", "unfreeze market");
364
+ return { actionId: receipt.actionId, ...receipt.kind.value };
365
+ }
366
+
367
+ /**
368
+ * Append a new fee tier to the account bracket configuration.
369
+ *
370
+ * - The engine supports at most 16 tiers (ids 0–15). Tier 0 is reserved for
371
+ * the default Nord fees; use `updateFeeTier` if you need to change it.
372
+ * - The first appended tier receives id 1, and subsequent tiers increment the id.
373
+ *
374
+ * @param params - Fee tier configuration to insert
375
+ * @returns Action identifier and fee tier addition receipt
376
+ * @throws {NordError} If the action submission fails or the new tier exceeds the maximum range (0-15).
377
+ */
378
+ async addFeeTier(
379
+ params: AddFeeTierParams,
380
+ ): Promise<{ actionId: bigint } & proto.Receipt_FeeTierAdded> {
381
+ const receipt = await this.submitAction({
382
+ case: "addFeeTier",
383
+ value: create(proto.Action_AddFeeTierSchema, {
384
+ config: create(proto.FeeTierConfigSchema, params.config),
385
+ }),
386
+ });
387
+ expectReceiptKind(receipt, "feeTierAdded", "add fee tier");
388
+ return { actionId: receipt.actionId, ...receipt.kind.value };
389
+ }
390
+
391
+ /**
392
+ * Update an existing fee tier with new maker/taker rates.
393
+ *
394
+ * Tier identifiers must already exist; attempting to update a missing tier
395
+ * causes the action to fail.
396
+ *
397
+ * @param params - Fee tier identifier and updated configuration
398
+ * @returns Action identifier and fee tier update receipt
399
+ * @throws {NordError} If the action submission fails or the tier ID exceeds the configured range.
400
+ */
401
+ async updateFeeTier(
402
+ params: UpdateFeeTierParams,
403
+ ): Promise<{ actionId: bigint } & proto.Receipt_FeeTierUpdated> {
404
+ const receipt = await this.submitAction({
405
+ case: "updateFeeTier",
406
+ value: create(proto.Action_UpdateFeeTierSchema, {
407
+ id: params.tierId,
408
+ config: create(proto.FeeTierConfigSchema, params.config),
409
+ }),
410
+ });
411
+ expectReceiptKind(receipt, "feeTierUpdated", "update fee tier");
412
+ return { actionId: receipt.actionId, ...receipt.kind.value };
413
+ }
414
+
415
+ /**
416
+ * Assign a fee tier to one or more accounts.
417
+ *
418
+ * The tier id must be within the configured range (0–15). Every account starts
419
+ * on tier 0; assigning it to another tier requires that tier to exist already.
420
+ * Invalid account ids or tier ids cause the action to fail.
421
+ *
422
+ * @param accounts - Account IDs to update
423
+ * @param tierId - Target fee tier identifier
424
+ * @returns Action identifier and accounts-tier receipt
425
+ * @throws {NordError} If the tier id exceeds the configured range or an account id is invalid.
426
+ */
427
+ async updateAccountsTier(
428
+ accounts: number[],
429
+ tierId: number,
430
+ ): Promise<{ actionId: bigint } & proto.Receipt_AccountsTierUpdated> {
431
+ const receipt = await this.submitAction({
432
+ case: "updateAccountsTier",
433
+ value: create(proto.Action_UpdateAccountsTierSchema, {
434
+ accounts,
435
+ tierId,
436
+ }),
437
+ });
438
+ expectReceiptKind(receipt, "accountsTierUpdated", "update accounts tier");
439
+ return { actionId: receipt.actionId, ...receipt.kind.value };
440
+ }
441
+ }
@@ -0,0 +1,79 @@
1
+ import { Connection, PublicKey } from "@solana/web3.js";
2
+ import type { Transaction } from "@solana/web3.js";
3
+ import * as proto from "../../gen/nord_pb";
4
+ import { createAction, sendAction } from "../api/actions";
5
+ import { Nord } from "./Nord";
6
+
7
+ export interface NordClientParams {
8
+ nord: Nord;
9
+ address: PublicKey;
10
+ walletSignFn: (message: Uint8Array | string) => Promise<Uint8Array>;
11
+ sessionSignFn: (message: Uint8Array) => Promise<Uint8Array>;
12
+ transactionSignFn: <T extends Transaction>(tx: T) => Promise<T>;
13
+ connection?: Connection;
14
+ sessionId?: bigint;
15
+ sessionPubKey: Uint8Array;
16
+ publicKey: PublicKey;
17
+ }
18
+
19
+ export abstract class NordClient {
20
+ public readonly nord: Nord;
21
+ public readonly address: PublicKey;
22
+ public readonly walletSignFn: (
23
+ message: Uint8Array | string,
24
+ ) => Promise<Uint8Array>;
25
+ public readonly sessionSignFn: (message: Uint8Array) => Promise<Uint8Array>;
26
+ public readonly transactionSignFn: <T extends Transaction>(
27
+ tx: T,
28
+ ) => Promise<T>;
29
+ public connection: Connection;
30
+ public sessionId?: bigint;
31
+ public sessionPubKey: Uint8Array;
32
+ public publicKey: PublicKey;
33
+ public lastTs = 0;
34
+
35
+ protected actionNonce = 0;
36
+
37
+ protected constructor(params: NordClientParams) {
38
+ this.nord = params.nord;
39
+ this.address = params.address;
40
+ this.walletSignFn = params.walletSignFn;
41
+ this.sessionSignFn = params.sessionSignFn;
42
+ this.transactionSignFn = params.transactionSignFn;
43
+ this.connection =
44
+ params.connection ??
45
+ new Connection(params.nord.solanaUrl, {
46
+ commitment: "confirmed",
47
+ });
48
+ this.sessionId = params.sessionId;
49
+ this.sessionPubKey = new Uint8Array(params.sessionPubKey);
50
+ this.publicKey = params.publicKey;
51
+ }
52
+
53
+ protected async submitSignedAction(
54
+ kind: proto.Action["kind"],
55
+ makeSignedMessage: (message: Uint8Array) => Promise<Uint8Array>,
56
+ ): Promise<proto.Receipt> {
57
+ const nonce = this.nextActionNonce();
58
+ const currentTimestamp = await this.nord.getTimestamp();
59
+ const action = createAction(currentTimestamp, nonce, kind);
60
+ return sendAction(this.nord.webServerUrl, makeSignedMessage, action);
61
+ }
62
+
63
+ protected nextActionNonce(): number {
64
+ return ++this.actionNonce;
65
+ }
66
+
67
+ protected cloneClientState(target: NordClient): void {
68
+ target.connection = this.connection;
69
+ target.sessionId = this.sessionId;
70
+ target.sessionPubKey = new Uint8Array(this.sessionPubKey);
71
+ target.publicKey = this.publicKey;
72
+ target.lastTs = this.lastTs;
73
+ target.actionNonce = this.actionNonce;
74
+ }
75
+
76
+ getSolanaPublicKey(): PublicKey {
77
+ return this.address;
78
+ }
79
+ }