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