@n1xyz/nord-ts 0.1.7 → 0.1.8

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 (84) hide show
  1. package/dist/actions.js +39 -82
  2. package/dist/bundle.js +79181 -0
  3. package/dist/client/Nord.d.ts +2 -2
  4. package/dist/client/Nord.js +46 -78
  5. package/dist/client/NordAdmin.d.ts +2 -2
  6. package/dist/client/NordAdmin.js +57 -89
  7. package/dist/client/NordUser.js +118 -147
  8. package/dist/const.js +5 -8
  9. package/dist/error.js +7 -5
  10. package/dist/gen/nord_pb.js +88 -92
  11. package/dist/gen/openapi.d.ts +5 -6
  12. package/dist/gen/openapi.js +1 -2
  13. package/dist/index.js +10 -49
  14. package/dist/types.d.ts +1 -0
  15. package/dist/types.js +21 -60
  16. package/dist/utils.js +38 -86
  17. package/dist/websocket/NordWebSocketClient.js +12 -17
  18. package/dist/websocket/Subscriber.js +6 -7
  19. package/dist/websocket/events.js +1 -2
  20. package/dist/websocket/index.js +10 -15
  21. package/package.json +2 -3
  22. package/dist/api/client.d.ts +0 -14
  23. package/dist/api/client.js +0 -45
  24. package/dist/bridge/client.d.ts +0 -151
  25. package/dist/bridge/client.js +0 -434
  26. package/dist/bridge/const.d.ts +0 -23
  27. package/dist/bridge/const.js +0 -47
  28. package/dist/bridge/index.d.ts +0 -4
  29. package/dist/bridge/index.js +0 -23
  30. package/dist/bridge/types.d.ts +0 -120
  31. package/dist/bridge/types.js +0 -18
  32. package/dist/bridge/utils.d.ts +0 -64
  33. package/dist/bridge/utils.js +0 -131
  34. package/dist/gen/common.d.ts +0 -68
  35. package/dist/gen/common.js +0 -215
  36. package/dist/gen/nord.d.ts +0 -882
  37. package/dist/gen/nord.js +0 -6520
  38. package/dist/idl/bridge.d.ts +0 -569
  39. package/dist/idl/bridge.js +0 -8
  40. package/dist/idl/bridge.json +0 -1506
  41. package/dist/idl/index.d.ts +0 -607
  42. package/dist/idl/index.js +0 -8
  43. package/dist/nord/api/actions.d.ts +0 -126
  44. package/dist/nord/api/actions.js +0 -397
  45. package/dist/nord/api/core.d.ts +0 -16
  46. package/dist/nord/api/core.js +0 -81
  47. package/dist/nord/api/market.d.ts +0 -36
  48. package/dist/nord/api/market.js +0 -96
  49. package/dist/nord/api/metrics.d.ts +0 -67
  50. package/dist/nord/api/metrics.js +0 -229
  51. package/dist/nord/api/queries.d.ts +0 -46
  52. package/dist/nord/api/queries.js +0 -109
  53. package/dist/nord/api/triggers.d.ts +0 -7
  54. package/dist/nord/api/triggers.js +0 -38
  55. package/dist/nord/client/Nord.d.ts +0 -396
  56. package/dist/nord/client/Nord.js +0 -747
  57. package/dist/nord/client/NordAdmin.d.ts +0 -259
  58. package/dist/nord/client/NordAdmin.js +0 -395
  59. package/dist/nord/client/NordClient.d.ts +0 -33
  60. package/dist/nord/client/NordClient.js +0 -45
  61. package/dist/nord/client/NordUser.d.ts +0 -362
  62. package/dist/nord/client/NordUser.js +0 -781
  63. package/dist/nord/index.d.ts +0 -11
  64. package/dist/nord/index.js +0 -36
  65. package/dist/nord/models/Subscriber.d.ts +0 -37
  66. package/dist/nord/models/Subscriber.js +0 -25
  67. package/dist/nord/utils/NordError.d.ts +0 -35
  68. package/dist/nord/utils/NordError.js +0 -49
  69. package/src/actions.ts +0 -333
  70. package/src/client/Nord.ts +0 -934
  71. package/src/client/NordAdmin.ts +0 -484
  72. package/src/client/NordUser.ts +0 -1122
  73. package/src/const.ts +0 -34
  74. package/src/error.ts +0 -76
  75. package/src/gen/.gitkeep +0 -0
  76. package/src/gen/nord_pb.ts +0 -5053
  77. package/src/gen/openapi.ts +0 -2904
  78. package/src/index.ts +0 -11
  79. package/src/types.ts +0 -327
  80. package/src/utils.ts +0 -266
  81. package/src/websocket/NordWebSocketClient.ts +0 -316
  82. package/src/websocket/Subscriber.ts +0 -56
  83. package/src/websocket/events.ts +0 -31
  84. package/src/websocket/index.ts +0 -105
@@ -1,1122 +0,0 @@
1
- import {
2
- ASSOCIATED_TOKEN_PROGRAM_ID,
3
- getAssociatedTokenAddress,
4
- TOKEN_PROGRAM_ID,
5
- TOKEN_2022_PROGRAM_ID,
6
- } from "@solana/spl-token";
7
- import { PublicKey, Transaction, SendOptions } from "@solana/web3.js";
8
- import Decimal from "decimal.js";
9
- import * as ed from "@noble/ed25519";
10
- import { floatToScaledBigIntLossy } from "@n1xyz/proton";
11
- import {
12
- FillMode,
13
- Side,
14
- SPLTokenInfo,
15
- QuoteSize,
16
- TriggerKind,
17
- fillModeToProtoFillMode,
18
- } from "../types";
19
- import * as proto from "../gen/nord_pb";
20
- import {
21
- BigIntValue,
22
- checkedFetch,
23
- assert,
24
- findMarket,
25
- findToken,
26
- optExpect,
27
- keypairFromPrivateKey,
28
- toScaledU64,
29
- } from "../utils";
30
- import { create } from "@bufbuild/protobuf";
31
- import {
32
- createSession,
33
- revokeSession,
34
- atomic,
35
- expectReceiptKind,
36
- createAction,
37
- sendAction,
38
- AtomicSubaction,
39
- } from "../actions";
40
- import { NordError } from "../error";
41
- import { Nord } from "./Nord";
42
-
43
- /**
44
- * Parameters for individual atomic subactions (user-friendly version)
45
- */
46
- export interface UserAtomicSubaction {
47
- /** The type of action to perform. */
48
- kind: "place" | "cancel";
49
-
50
- /** The market ID to place the order in. */
51
- marketId?: number;
52
-
53
- /** The order ID to cancel. */
54
- orderId?: BigIntValue;
55
-
56
- /** Order side (bid or ask) */
57
- side?: Side;
58
-
59
- /** Fill mode (limit, market, etc.) */
60
- fillMode?: FillMode;
61
-
62
- /** Whether the order is reduce-only. */
63
- isReduceOnly?: boolean;
64
-
65
- /** The size of the order. */
66
- size?: Decimal.Value;
67
-
68
- /** Order price */
69
- price?: Decimal.Value;
70
-
71
- /** Quote size object (for market-style placement) */
72
- quoteSize?: QuoteSize;
73
-
74
- /** The client order ID of the order. */
75
- clientOrderId?: BigIntValue;
76
- }
77
-
78
- /**
79
- * User class for interacting with the Nord protocol
80
- */
81
- export class NordUser {
82
- public readonly nord: Nord;
83
- public readonly sessionSignFn: (message: Uint8Array) => Promise<Uint8Array>;
84
- public readonly transactionSignFn: (tx: Transaction) => Promise<Transaction>;
85
- public sessionId?: bigint;
86
- public sessionPubKey: PublicKey;
87
- public publicKey: PublicKey;
88
- public lastTs = 0;
89
-
90
- private nonce = 0;
91
-
92
- /** User balances by token symbol */
93
- public balances: {
94
- [key: string]: { accountId: number; balance: number; symbol: string }[];
95
- } = {};
96
-
97
- /** User positions by account ID */
98
- public positions: {
99
- [key: string]: {
100
- marketId: number;
101
- openOrders: number;
102
- perp?: {
103
- baseSize: number;
104
- price: number;
105
- updatedFundingRateIndex: number;
106
- fundingPaymentPnl: number;
107
- sizePricePnl: number;
108
- isLong: boolean;
109
- };
110
- actionId: number;
111
- }[];
112
- } = {};
113
-
114
- /** User margins by account ID */
115
- public margins: {
116
- [key: string]: {
117
- omf: number;
118
- mf: number;
119
- imf: number;
120
- cmf: number;
121
- mmf: number;
122
- pon: number;
123
- pn: number;
124
- bankruptcy: boolean;
125
- };
126
- } = {};
127
-
128
- /** User's account IDs */
129
- public accountIds?: number[];
130
-
131
- /** SPL token information */
132
- public splTokenInfos: SPLTokenInfo[] = [];
133
-
134
- /**
135
- * Create a new NordUser instance
136
- *
137
- * @param nord - Nord client instance
138
- * @param sessionSignFn - Function to sign messages with the user's session key
139
- * @param transactionSignFn - Function to sign transactions with the user's wallet (optional)
140
- * @param sessionId - Existing session identifier
141
- * @param sessionPubKey - Session public key
142
- * @param publicKey - Wallet public key
143
- * @throws {NordError} If required parameters are missing
144
- */
145
- constructor({
146
- nord,
147
- sessionSignFn,
148
- transactionSignFn,
149
- sessionId,
150
- sessionPubKey,
151
- publicKey,
152
- }: Readonly<{
153
- nord: Nord;
154
- sessionSignFn: (message: Uint8Array) => Promise<Uint8Array>;
155
- transactionSignFn: (tx: Transaction) => Promise<Transaction>;
156
- sessionId?: bigint;
157
- sessionPubKey: Uint8Array;
158
- publicKey: PublicKey;
159
- }>) {
160
- this.nord = nord;
161
- this.sessionSignFn = sessionSignFn;
162
- this.transactionSignFn = transactionSignFn;
163
- this.sessionId = sessionId;
164
- this.sessionPubKey = new PublicKey(sessionPubKey);
165
- this.publicKey = publicKey;
166
-
167
- // Convert tokens from info endpoint to SPLTokenInfo
168
- if (this.nord.tokens && this.nord.tokens.length > 0) {
169
- this.splTokenInfos = this.nord.tokens.map((token) => ({
170
- mint: token.mintAddr, // Use mintAddr as mint
171
- precision: token.decimals,
172
- tokenId: token.tokenId,
173
- name: token.symbol,
174
- }));
175
- }
176
- }
177
-
178
- /**
179
- * Create a NordUser from a private key
180
- *
181
- * @param nord - Nord instance
182
- * @param privateKey - Private key as string or Uint8Array
183
- * @returns NordUser instance
184
- * @throws {NordError} If the private key is invalid
185
- */
186
- static fromPrivateKey(nord: Nord, privateKey: string | Uint8Array): NordUser {
187
- try {
188
- const keypair = keypairFromPrivateKey(privateKey);
189
- const publicKey = keypair.publicKey;
190
-
191
- const sessionSignFn = async (
192
- message: Uint8Array,
193
- ): Promise<Uint8Array> => {
194
- // Use ed25519 to sign the message
195
- return ed.sign(message, keypair.secretKey.slice(0, 32));
196
- };
197
-
198
- const transactionSignFn = async (
199
- tx: Transaction,
200
- ): Promise<Transaction> => {
201
- tx.sign(keypair);
202
- return tx;
203
- };
204
-
205
- return new NordUser({
206
- nord,
207
- sessionSignFn,
208
- transactionSignFn,
209
- publicKey,
210
- sessionPubKey: publicKey.toBytes(),
211
- });
212
- } catch (error) {
213
- throw new NordError("Failed to create NordUser from private key", {
214
- cause: error,
215
- });
216
- }
217
- }
218
-
219
- /**
220
- * Get the associated token account for a token mint
221
- *
222
- * @param mint - Token mint address
223
- * @returns Associated token account address
224
- * @throws {NordError} If required parameters are missing or operation fails
225
- */
226
- async getAssociatedTokenAccount(mint: PublicKey): Promise<PublicKey> {
227
- try {
228
- // Get the token program ID from the mint account
229
- const mintAccount = await this.nord.solanaConnection.getAccountInfo(mint);
230
- if (!mintAccount) {
231
- throw new NordError("Mint account not found");
232
- }
233
- const tokenProgramId = mintAccount.owner;
234
-
235
- // Validate that the mint is owned by a supported SPL token program
236
- if (
237
- !tokenProgramId.equals(TOKEN_PROGRAM_ID) &&
238
- !tokenProgramId.equals(TOKEN_2022_PROGRAM_ID)
239
- ) {
240
- throw new NordError(
241
- "Mint Account is not owned by a supported SPL token program",
242
- );
243
- }
244
-
245
- const associatedTokenAddress = await getAssociatedTokenAddress(
246
- mint,
247
- this.publicKey,
248
- false,
249
- tokenProgramId,
250
- ASSOCIATED_TOKEN_PROGRAM_ID,
251
- );
252
- return associatedTokenAddress;
253
- } catch (error) {
254
- throw new NordError("Failed to get associated token account", {
255
- cause: error,
256
- });
257
- }
258
- }
259
-
260
- /**
261
- * Deposit SPL tokens to the app
262
- *
263
- * @param amount - Amount to deposit
264
- * @param tokenId - Token ID
265
- * @param recipient - Recipient address; defaults to the user's address
266
- * @returns Transaction signature
267
- * @deprecated Use deposit instead
268
- * @throws {NordError} If required parameters are missing or operation fails
269
- */
270
- async depositSpl(
271
- amount: number,
272
- tokenId: number,
273
- recipient?: PublicKey,
274
- ): Promise<string> {
275
- return this.deposit({ amount, tokenId, recipient });
276
- }
277
-
278
- /**
279
- * Deposit SPL tokens to the app
280
- *
281
- * @param amount - Amount to deposit
282
- * @param tokenId - Token ID
283
- * @param recipient - Recipient address; defaults to the user's address
284
- * @param sendOptions - Send options for .sendTransaction
285
- * @returns Transaction signature
286
- * @throws {NordError} If required parameters are missing or operation fails
287
- */
288
- async deposit({
289
- amount,
290
- tokenId,
291
- recipient,
292
- sendOptions,
293
- }: Readonly<{
294
- amount: number;
295
- tokenId: number;
296
- recipient?: PublicKey;
297
- sendOptions?: SendOptions;
298
- }>): Promise<string> {
299
- try {
300
- // Find the token info
301
- const tokenInfo = this.splTokenInfos.find((t) => t.tokenId === tokenId);
302
- if (!tokenInfo) {
303
- throw new NordError(`Token with ID ${tokenId} not found`);
304
- }
305
-
306
- const mint = new PublicKey(tokenInfo.mint);
307
- const fromAccount = await this.getAssociatedTokenAccount(mint);
308
- const payer = this.publicKey;
309
-
310
- const { ix, extraSigner } = await this.nord.protonClient.buildDepositIx({
311
- payer,
312
- recipient: recipient ?? payer,
313
- quantAmount: floatToScaledBigIntLossy(amount, tokenInfo.precision),
314
- mint,
315
- sourceTokenAccount: fromAccount,
316
- });
317
-
318
- const { blockhash } =
319
- await this.nord.solanaConnection.getLatestBlockhash();
320
- const tx = new Transaction();
321
-
322
- tx.add(ix);
323
- tx.recentBlockhash = blockhash;
324
- tx.feePayer = payer;
325
-
326
- const signedTx = await this.transactionSignFn(tx);
327
- signedTx.partialSign(extraSigner);
328
-
329
- const signature = await this.nord.solanaConnection.sendRawTransaction(
330
- signedTx.serialize(),
331
- sendOptions,
332
- );
333
-
334
- return signature;
335
- } catch (error) {
336
- throw new NordError(
337
- `Failed to deposit ${amount} of token ID ${tokenId}`,
338
- { cause: error },
339
- );
340
- }
341
- }
342
-
343
- /**
344
- * Get a new nonce for actions
345
- *
346
- * @returns Nonce as number
347
- */
348
- getNonce(): number {
349
- return ++this.nonce;
350
- }
351
-
352
- private async submitSessionAction(
353
- kind: proto.Action["kind"],
354
- ): Promise<proto.Receipt> {
355
- return this.submitSignedAction(kind, async (message) => {
356
- const signature = await this.sessionSignFn(message);
357
- const signed = new Uint8Array(message.length + signature.length);
358
- signed.set(message);
359
- signed.set(signature, message.length);
360
- return signed;
361
- });
362
- }
363
-
364
- /**
365
- * Update account IDs for this user
366
- *
367
- * @throws {NordError} If the operation fails
368
- */
369
- async updateAccountId(): Promise<void> {
370
- try {
371
- if (!this.publicKey) {
372
- throw new NordError("Public key is required to update account ID");
373
- }
374
-
375
- const resp = await this.nord.getUser({
376
- pubkey: this.publicKey.toBase58(),
377
- });
378
-
379
- if (!resp) {
380
- throw new NordError(`User ${this.publicKey.toBase58()} not found`);
381
- }
382
-
383
- this.accountIds = resp.accountIds;
384
- } catch (error) {
385
- throw new NordError("Failed to update account ID", { cause: error });
386
- }
387
- }
388
-
389
- /**
390
- * Fetch user information including balances and orders
391
- *
392
- * @throws {NordError} If the operation fails
393
- */
394
- async fetchInfo(): Promise<void> {
395
- type OpenOrder = {
396
- orderId: number;
397
- marketId: number;
398
- side: "ask" | "bid";
399
- size: number;
400
- price: number;
401
- originalOrderSize: number;
402
- clientOrderId?: number | null;
403
- };
404
-
405
- type Balance = {
406
- tokenId: number;
407
- token: string;
408
- amount: number;
409
- };
410
-
411
- type Position = {
412
- marketId: number;
413
- openOrders: number;
414
- perp?: {
415
- baseSize: number;
416
- price: number;
417
- updatedFundingRateIndex: number;
418
- fundingPaymentPnl: number;
419
- sizePricePnl: number;
420
- isLong: boolean;
421
- };
422
- actionId: number;
423
- };
424
-
425
- type Margins = {
426
- omf: number;
427
- mf: number;
428
- imf: number;
429
- cmf: number;
430
- mmf: number;
431
- pon: number;
432
- pn: number;
433
- bankruptcy: boolean;
434
- };
435
-
436
- type Account = {
437
- updateId: number;
438
- orders: OpenOrder[];
439
- positions: Position[];
440
- balances: Balance[];
441
- margins: Margins;
442
- };
443
-
444
- if (this.accountIds !== undefined) {
445
- const accountsData: (Account & { accountId: number })[] =
446
- await Promise.all(
447
- this.accountIds.map(async (accountId) => {
448
- const response = await checkedFetch(
449
- `${this.nord.webServerUrl}/account/${accountId}`,
450
- );
451
- const accountData = (await response.json()) as Account;
452
- // Ensure we have the correct accountId
453
- return {
454
- ...accountData,
455
- accountId,
456
- };
457
- }),
458
- );
459
-
460
- for (const accountData of accountsData) {
461
- // Process balances
462
- this.balances[accountData.accountId] = [];
463
- for (const balance of accountData.balances) {
464
- this.balances[accountData.accountId].push({
465
- accountId: accountData.accountId,
466
- balance: balance.amount,
467
- symbol: balance.token,
468
- });
469
- }
470
-
471
- // Process positions
472
- this.positions[accountData.accountId] = accountData.positions;
473
-
474
- // Process margins
475
- this.margins[accountData.accountId] = accountData.margins;
476
- }
477
- }
478
- }
479
-
480
- /**
481
- * Refresh the user's session
482
- *
483
- * @throws {NordError} If the operation fails
484
- */
485
- async refreshSession(): Promise<void> {
486
- const result = await createSession(
487
- this.nord.httpClient,
488
- this.transactionSignFn,
489
- await this.nord.getTimestamp(),
490
- this.getNonce(),
491
- {
492
- userPubkey: this.publicKey,
493
- sessionPubkey: this.sessionPubKey,
494
- },
495
- );
496
- this.sessionId = result.sessionId;
497
- }
498
- /**
499
- * Revoke a session
500
- *
501
- * @param sessionId - Session ID to revoke
502
- * @throws {NordError} If the operation fails
503
- */
504
- async revokeSession(sessionId: BigIntValue): Promise<void> {
505
- try {
506
- await revokeSession(
507
- this.nord.httpClient,
508
- this.transactionSignFn,
509
- await this.nord.getTimestamp(),
510
- this.getNonce(),
511
- {
512
- userPubkey: this.publicKey,
513
- sessionId,
514
- },
515
- );
516
- } catch (error) {
517
- throw new NordError(`Failed to revoke session ${sessionId}`, {
518
- cause: error,
519
- });
520
- }
521
- }
522
-
523
- /**
524
- * Checks if the session is valid
525
- * @private
526
- * @throws {NordError} If the session is not valid
527
- */
528
- private checkSessionValidity(): void {
529
- if (this.sessionId === undefined || this.sessionId === BigInt(0)) {
530
- throw new NordError(
531
- "Invalid or empty session ID. Please create or refresh your session.",
532
- );
533
- }
534
- }
535
-
536
- /**
537
- * Withdraw tokens from the exchange
538
- *
539
- * @param tokenId - Token ID to withdraw
540
- * @param amount - Amount to withdraw
541
- * @throws {NordError} If the operation fails
542
- */
543
- async withdraw({
544
- amount,
545
- tokenId,
546
- }: Readonly<{
547
- tokenId: number;
548
- amount: number;
549
- }>): Promise<{ actionId: bigint }> {
550
- try {
551
- this.checkSessionValidity();
552
- const token = findToken(this.nord.tokens, tokenId);
553
- const scaledAmount = toScaledU64(amount, token.decimals);
554
- if (scaledAmount <= 0n) {
555
- throw new NordError("Withdraw amount must be positive");
556
- }
557
- const receipt = await this.submitSessionAction({
558
- case: "withdraw",
559
- value: create(proto.Action_WithdrawSchema, {
560
- sessionId: BigInt(optExpect(this.sessionId, "No session")),
561
- tokenId,
562
- amount: scaledAmount,
563
- }),
564
- });
565
- expectReceiptKind(receipt, "withdrawResult", "withdraw");
566
- return { actionId: receipt.actionId };
567
- } catch (error) {
568
- throw new NordError(
569
- `Failed to withdraw ${amount} of token ID ${tokenId}`,
570
- { cause: error },
571
- );
572
- }
573
- }
574
-
575
- /**
576
- * Place an order on the exchange
577
- *
578
- * @param marketId - Target market identifier
579
- * @param side - Order side
580
- * @param fillMode - Fill mode (limit, market, etc.)
581
- * @param isReduceOnly - Reduce-only flag
582
- * @param size - Base size to place
583
- * @param price - Limit price
584
- * @param quoteSize - Quote-sized order representation
585
- * @param accountId - Account executing the order
586
- * @param clientOrderId - Optional client-specified identifier
587
- * @returns Object containing actionId, orderId (if posted), fills, and clientOrderId
588
- * @throws {NordError} If the operation fails
589
- */
590
- async placeOrder({
591
- marketId,
592
- side,
593
- fillMode,
594
- isReduceOnly,
595
- size,
596
- price,
597
- quoteSize,
598
- accountId,
599
- clientOrderId,
600
- }: Readonly<{
601
- marketId: number;
602
- side: Side;
603
- fillMode: FillMode;
604
- isReduceOnly: boolean;
605
- size?: Decimal.Value;
606
- price?: Decimal.Value;
607
- quoteSize?: QuoteSize;
608
- accountId?: number;
609
- clientOrderId?: BigIntValue;
610
- }>): Promise<{
611
- actionId: bigint;
612
- orderId?: bigint;
613
- fills: proto.Receipt_Trade[];
614
- }> {
615
- try {
616
- this.checkSessionValidity();
617
- const market = findMarket(this.nord.markets, marketId);
618
- if (!market) {
619
- throw new NordError(`Market with ID ${marketId} not found`);
620
- }
621
- const sessionId = optExpect(this.sessionId, "No session");
622
- const scaledPrice = toScaledU64(price ?? 0, market.priceDecimals);
623
- const scaledSize = toScaledU64(size ?? 0, market.sizeDecimals);
624
- const scaledQuote = quoteSize
625
- ? quoteSize.toWire(market.priceDecimals, market.sizeDecimals)
626
- : undefined;
627
- assert(
628
- scaledPrice > 0n || scaledSize > 0n || scaledQuote !== undefined,
629
- "OrderLimit must include at least one of: size, price, or quoteSize",
630
- );
631
-
632
- const receipt = await this.submitSessionAction({
633
- case: "placeOrder",
634
- value: create(proto.Action_PlaceOrderSchema, {
635
- sessionId: BigInt(sessionId),
636
- senderAccountId: accountId,
637
- marketId,
638
- side: side === Side.Bid ? proto.Side.BID : proto.Side.ASK,
639
- fillMode: fillModeToProtoFillMode(fillMode),
640
- isReduceOnly,
641
- price: scaledPrice,
642
- size: scaledSize,
643
- quoteSize:
644
- scaledQuote === undefined
645
- ? undefined
646
- : create(proto.QuoteSizeSchema, {
647
- size: scaledQuote.size,
648
- price: scaledQuote.price,
649
- }),
650
- clientOrderId:
651
- clientOrderId === undefined ? undefined : BigInt(clientOrderId),
652
- }),
653
- });
654
- expectReceiptKind(receipt, "placeOrderResult", "place order");
655
- const result = receipt.kind.value;
656
- return {
657
- actionId: receipt.actionId,
658
- orderId: result.posted?.orderId,
659
- fills: result.fills,
660
- };
661
- } catch (error) {
662
- throw new NordError("Failed to place order", { cause: error });
663
- }
664
- }
665
-
666
- /**
667
- * Cancel an order
668
- *
669
- * @param orderId - Order ID to cancel
670
- * @param providedAccountId - Account ID that placed the order
671
- * @returns Object containing actionId, cancelled orderId, and accountId
672
- * @throws {NordError} If the operation fails
673
- */
674
- async cancelOrder(
675
- orderId: BigIntValue,
676
- providedAccountId?: number,
677
- ): Promise<{
678
- actionId: bigint;
679
- orderId: bigint;
680
- accountId: number;
681
- }> {
682
- const accountId =
683
- providedAccountId != null ? providedAccountId : this.accountIds?.[0];
684
- try {
685
- this.checkSessionValidity();
686
- const receipt = await this.submitSessionAction({
687
- case: "cancelOrderById",
688
- value: create(proto.Action_CancelOrderByIdSchema, {
689
- orderId: BigInt(orderId),
690
- sessionId: BigInt(optExpect(this.sessionId, "No session")),
691
- senderAccountId: accountId,
692
- }),
693
- });
694
- expectReceiptKind(receipt, "cancelOrderResult", "cancel order");
695
- return {
696
- actionId: receipt.actionId,
697
- orderId: receipt.kind.value.orderId,
698
- accountId: receipt.kind.value.accountId,
699
- };
700
- } catch (error) {
701
- throw new NordError(`Failed to cancel order ${orderId}`, {
702
- cause: error,
703
- });
704
- }
705
- }
706
-
707
- /**
708
- * Add a trigger for the current session
709
- *
710
- * @param marketId - Market to watch
711
- * @param side - Order side for the trigger
712
- * @param kind - Stop-loss or take-profit trigger type
713
- * @param triggerPrice - Price that activates the trigger
714
- * @param limitPrice - Limit price placed once the trigger fires
715
- * @param accountId - Account executing the trigger
716
- * @returns Object containing the actionId of the submitted trigger
717
- * @throws {NordError} If the operation fails
718
- */
719
- async addTrigger({
720
- marketId,
721
- side,
722
- kind,
723
- triggerPrice,
724
- limitPrice,
725
- accountId,
726
- }: Readonly<{
727
- marketId: number;
728
- side: Side;
729
- kind: TriggerKind;
730
- triggerPrice: Decimal.Value;
731
- limitPrice?: Decimal.Value;
732
- accountId?: number;
733
- }>): Promise<{ actionId: bigint }> {
734
- try {
735
- this.checkSessionValidity();
736
- const market = findMarket(this.nord.markets, marketId);
737
- if (!market) {
738
- throw new NordError(`Market with ID ${marketId} not found`);
739
- }
740
- const scaledTriggerPrice = toScaledU64(
741
- triggerPrice,
742
- market.priceDecimals,
743
- );
744
- assert(scaledTriggerPrice > 0n, "Trigger price must be positive");
745
- const scaledLimitPrice =
746
- limitPrice === undefined
747
- ? undefined
748
- : toScaledU64(limitPrice, market.priceDecimals);
749
- if (scaledLimitPrice !== undefined) {
750
- assert(scaledLimitPrice > 0n, "Limit price must be positive");
751
- }
752
- const key = create(proto.TriggerKeySchema, {
753
- kind:
754
- kind === TriggerKind.StopLoss
755
- ? proto.TriggerKind.STOP_LOSS
756
- : proto.TriggerKind.TAKE_PROFIT,
757
- side: side === Side.Bid ? proto.Side.BID : proto.Side.ASK,
758
- });
759
- const prices = create(proto.Action_TriggerPricesSchema, {
760
- triggerPrice: scaledTriggerPrice,
761
- limitPrice: scaledLimitPrice,
762
- });
763
- const receipt = await this.submitSessionAction({
764
- case: "addTrigger",
765
- value: create(proto.Action_AddTriggerSchema, {
766
- sessionId: BigInt(optExpect(this.sessionId, "No session")),
767
- marketId,
768
- key,
769
- prices,
770
- accountId,
771
- }),
772
- });
773
- expectReceiptKind(receipt, "triggerAdded", "add trigger");
774
- return { actionId: receipt.actionId };
775
- } catch (error) {
776
- throw new NordError("Failed to add trigger", { cause: error });
777
- }
778
- }
779
-
780
- /**
781
- * Remove a trigger for the current session
782
- *
783
- * @param marketId - Market the trigger belongs to
784
- * @param side - Order side for the trigger
785
- * @param kind - Stop-loss or take-profit trigger type
786
- * @param accountId - Account executing the trigger
787
- * @returns Object containing the actionId of the removal action
788
- * @throws {NordError} If the operation fails
789
- */
790
- async removeTrigger({
791
- marketId,
792
- side,
793
- kind,
794
- accountId,
795
- }: Readonly<{
796
- marketId: number;
797
- side: Side;
798
- kind: TriggerKind;
799
- accountId?: number;
800
- }>): Promise<{ actionId: bigint }> {
801
- try {
802
- this.checkSessionValidity();
803
- const market = findMarket(this.nord.markets, marketId);
804
- if (!market) {
805
- throw new NordError(`Market with ID ${marketId} not found`);
806
- }
807
- const key = create(proto.TriggerKeySchema, {
808
- kind:
809
- kind === TriggerKind.StopLoss
810
- ? proto.TriggerKind.STOP_LOSS
811
- : proto.TriggerKind.TAKE_PROFIT,
812
- side: side === Side.Bid ? proto.Side.BID : proto.Side.ASK,
813
- });
814
- const receipt = await this.submitSessionAction({
815
- case: "removeTrigger",
816
- value: create(proto.Action_RemoveTriggerSchema, {
817
- sessionId: BigInt(optExpect(this.sessionId, "No session")),
818
- marketId,
819
- key,
820
- accountId,
821
- }),
822
- });
823
- expectReceiptKind(receipt, "triggerRemoved", "remove trigger");
824
- return { actionId: receipt.actionId };
825
- } catch (error) {
826
- throw new NordError("Failed to remove trigger", { cause: error });
827
- }
828
- }
829
-
830
- /**
831
- * Transfer tokens to another account
832
- *
833
- * @param tokenId - Token identifier to move
834
- * @param amount - Amount to transfer
835
- * @param fromAccountId - Source account id
836
- * @param toAccountId - Destination account id
837
- * @throws {NordError} If the operation fails
838
- */
839
- async transferToAccount({
840
- tokenId,
841
- amount,
842
- fromAccountId,
843
- toAccountId,
844
- }: Readonly<{
845
- tokenId: number;
846
- amount: Decimal.Value;
847
- fromAccountId?: number;
848
- toAccountId?: number;
849
- }>): Promise<{
850
- actionId: bigint;
851
- newAccountId?: number;
852
- }> {
853
- try {
854
- this.checkSessionValidity();
855
- const token = findToken(this.nord.tokens, tokenId);
856
-
857
- const scaledAmount = toScaledU64(amount, token.decimals);
858
- if (scaledAmount <= 0n) {
859
- throw new NordError("Transfer amount must be positive");
860
- }
861
-
862
- const receipt = await this.submitSessionAction({
863
- case: "transfer",
864
- value: create(proto.Action_TransferSchema, {
865
- sessionId: BigInt(optExpect(this.sessionId, "No session")),
866
- fromAccountId: optExpect(fromAccountId, "No source account"),
867
- toAccountId: optExpect(toAccountId, "No target account"),
868
- tokenId,
869
- amount: scaledAmount,
870
- }),
871
- });
872
- expectReceiptKind(receipt, "transferred", "transfer tokens");
873
- if (receipt.kind.value.accountCreated) {
874
- assert(
875
- receipt.kind.value.toUserAccount !== undefined,
876
- `toAccount must be defined on new account on ${receipt.kind.value}`,
877
- );
878
- return {
879
- actionId: receipt.actionId,
880
- newAccountId: receipt.kind.value.toUserAccount,
881
- };
882
- } else {
883
- return { actionId: receipt.actionId };
884
- }
885
- } catch (error) {
886
- throw new NordError("Failed to transfer tokens", { cause: error });
887
- }
888
- }
889
-
890
- /**
891
- * Execute up to four place/cancel operations atomically.
892
- * Per Market:
893
- * 1. cancels can only be in the start (one cannot predict future order ids)
894
- * 2. intermediate trades can trade only
895
- * 3. placements go last
896
- *
897
- * Across Markets, order action can be any
898
- *
899
- * @param userActions array of user-friendly subactions
900
- * @param providedAccountId optional account performing the action (defaults to first account)
901
- */
902
- async atomic(
903
- userActions: UserAtomicSubaction[],
904
- providedAccountId?: number,
905
- ): Promise<{
906
- actionId: bigint;
907
- results: proto.Receipt_AtomicSubactionResultKind[];
908
- }> {
909
- try {
910
- this.checkSessionValidity();
911
-
912
- const accountId =
913
- providedAccountId != null ? providedAccountId : this.accountIds?.[0];
914
-
915
- if (accountId == null) {
916
- throw new NordError(
917
- "Account ID is undefined. Make sure to call updateAccountId() before atomic operations.",
918
- );
919
- }
920
-
921
- const apiActions: AtomicSubaction[] = userActions.map((act) => {
922
- if (act.kind === "place") {
923
- const market = findMarket(this.nord.markets, act.marketId!);
924
- if (!market) {
925
- throw new NordError(`Market ${act.marketId} not found`);
926
- }
927
- return {
928
- kind: "place",
929
- marketId: act.marketId,
930
- side: act.side,
931
- fillMode: act.fillMode,
932
- isReduceOnly: act.isReduceOnly,
933
- sizeDecimals: market.sizeDecimals,
934
- priceDecimals: market.priceDecimals,
935
- size: act.size,
936
- price: act.price,
937
- quoteSize: act.quoteSize,
938
- clientOrderId: act.clientOrderId,
939
- } as AtomicSubaction;
940
- }
941
- return {
942
- kind: "cancel",
943
- orderId: act.orderId,
944
- } as AtomicSubaction;
945
- });
946
-
947
- const result = await atomic(
948
- this.nord.httpClient,
949
- this.sessionSignFn,
950
- await this.nord.getTimestamp(),
951
- this.getNonce(),
952
- {
953
- sessionId: optExpect(this.sessionId, "No session"),
954
- accountId: accountId,
955
- actions: apiActions,
956
- },
957
- );
958
- return result;
959
- } catch (error) {
960
- throw new NordError("Atomic operation failed", { cause: error });
961
- }
962
- }
963
-
964
- /**
965
- * Helper function to retry a promise with exponential backoff
966
- *
967
- * @param fn - Function to retry
968
- * @param maxRetries - Maximum number of retries
969
- * @param initialDelay - Initial delay in milliseconds
970
- * @returns Promise result
971
- */
972
- private async retryWithBackoff<T>(
973
- fn: () => Promise<T>,
974
- maxRetries: number = 3,
975
- initialDelay: number = 500,
976
- ): Promise<T> {
977
- let retries = 0;
978
- let delay = initialDelay;
979
-
980
- while (true) {
981
- try {
982
- return await fn();
983
- } catch (error) {
984
- if (retries >= maxRetries) {
985
- throw error;
986
- }
987
-
988
- // Check if error is rate limiting related
989
- const isRateLimitError =
990
- error instanceof Error &&
991
- (error.message.includes("rate limit") ||
992
- error.message.includes("429") ||
993
- error.message.includes("too many requests"));
994
-
995
- if (!isRateLimitError) {
996
- throw error;
997
- }
998
-
999
- retries++;
1000
- await new Promise((resolve) => setTimeout(resolve, delay));
1001
- delay *= 2; // Exponential backoff
1002
- }
1003
- }
1004
- }
1005
-
1006
- /**
1007
- * Get user's token balances on Solana chain using mintAddr
1008
- *
1009
- * @param options - Optional parameters
1010
- * @param options.includeZeroBalances - Whether to include tokens with zero balance (default: true)
1011
- * @param options.includeTokenAccounts - Whether to include token account addresses in the result (default: false)
1012
- * @param options.maxConcurrent - Maximum number of concurrent requests (default: 5)
1013
- * @param options.maxRetries - Maximum number of retries for rate-limited requests (default: 3)
1014
- * @returns Object with token balances and optional token account addresses
1015
- * @throws {NordError} If required parameters are missing or operation fails
1016
- */
1017
- async getSolanaBalances(
1018
- options: {
1019
- includeZeroBalances?: boolean;
1020
- includeTokenAccounts?: boolean;
1021
- maxConcurrent?: number;
1022
- maxRetries?: number;
1023
- } = {},
1024
- ): Promise<{
1025
- balances: { [symbol: string]: number };
1026
- tokenAccounts?: { [symbol: string]: string };
1027
- }> {
1028
- const {
1029
- includeZeroBalances = true,
1030
- includeTokenAccounts = false,
1031
- maxConcurrent = 5,
1032
- maxRetries = 3,
1033
- } = options;
1034
-
1035
- const balances: { [symbol: string]: number } = {};
1036
- const tokenAccounts: { [symbol: string]: string } = {};
1037
-
1038
- try {
1039
- // Get SOL balance (native token)
1040
- const solBalance = await this.retryWithBackoff(
1041
- () => this.nord.solanaConnection.getBalance(this.publicKey),
1042
- maxRetries,
1043
- );
1044
- balances["SOL"] = solBalance / 1e9; // Convert lamports to SOL
1045
- if (includeTokenAccounts) {
1046
- tokenAccounts["SOL"] = this.publicKey.toString();
1047
- }
1048
-
1049
- // Get SPL token balances using mintAddr from Nord tokens
1050
- if (this.nord.tokens && this.nord.tokens.length > 0) {
1051
- const tokens = this.nord.tokens.filter((token) => !!token.mintAddr);
1052
-
1053
- // Process tokens in batches to avoid rate limiting
1054
- for (let i = 0; i < tokens.length; i += maxConcurrent) {
1055
- const batch = tokens.slice(i, i + maxConcurrent);
1056
-
1057
- // Process batch in parallel
1058
- const batchPromises = batch.map(async (token) => {
1059
- try {
1060
- const mint = new PublicKey(token.mintAddr);
1061
- const associatedTokenAddress = await this.retryWithBackoff(
1062
- () => getAssociatedTokenAddress(mint, this.publicKey),
1063
- maxRetries,
1064
- );
1065
-
1066
- if (includeTokenAccounts) {
1067
- tokenAccounts[token.symbol] = associatedTokenAddress.toString();
1068
- }
1069
-
1070
- try {
1071
- const tokenBalance = await this.retryWithBackoff(
1072
- () =>
1073
- this.nord.solanaConnection.getTokenAccountBalance(
1074
- associatedTokenAddress,
1075
- ),
1076
- maxRetries,
1077
- );
1078
- const balance = Number(tokenBalance.value.uiAmount);
1079
-
1080
- if (balance > 0 || includeZeroBalances) {
1081
- balances[token.symbol] = balance;
1082
- }
1083
- } catch {
1084
- // Token account might not exist yet, set balance to 0
1085
- if (includeZeroBalances) {
1086
- balances[token.symbol] = 0;
1087
- }
1088
- }
1089
- } catch (error) {
1090
- console.error(
1091
- `Error getting balance for token ${token.symbol}:`,
1092
- error,
1093
- );
1094
- if (includeZeroBalances) {
1095
- balances[token.symbol] = 0;
1096
- }
1097
- }
1098
- });
1099
-
1100
- // Wait for current batch to complete before processing next batch
1101
- await Promise.all(batchPromises);
1102
- }
1103
- }
1104
-
1105
- return includeTokenAccounts ? { balances, tokenAccounts } : { balances };
1106
- } catch (error) {
1107
- throw new NordError("Failed to get Solana token balances", {
1108
- cause: error,
1109
- });
1110
- }
1111
- }
1112
-
1113
- protected async submitSignedAction(
1114
- kind: proto.Action["kind"],
1115
- makeSignedMessage: (message: Uint8Array) => Promise<Uint8Array>,
1116
- ): Promise<proto.Receipt> {
1117
- const nonce = this.getNonce();
1118
- const currentTimestamp = await this.nord.getTimestamp();
1119
- const action = createAction(currentTimestamp, nonce, kind);
1120
- return sendAction(this.nord.httpClient, makeSignedMessage, action);
1121
- }
1122
- }