@n1xyz/nord-ts 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/actions.d.ts +57 -0
  2. package/dist/actions.js +229 -0
  3. package/dist/client/Nord.d.ts +379 -0
  4. package/dist/client/Nord.js +718 -0
  5. package/dist/client/NordAdmin.d.ts +225 -0
  6. package/dist/client/NordAdmin.js +394 -0
  7. package/dist/client/NordUser.d.ts +350 -0
  8. package/dist/client/NordUser.js +743 -0
  9. package/dist/error.d.ts +35 -0
  10. package/dist/error.js +49 -0
  11. package/dist/gen/openapi.d.ts +40 -0
  12. package/dist/index.d.ts +6 -1
  13. package/dist/index.js +29 -1
  14. package/dist/nord/client/NordAdmin.js +2 -0
  15. package/dist/types.d.ts +4 -50
  16. package/dist/types.js +1 -24
  17. package/dist/utils.d.ts +8 -11
  18. package/dist/utils.js +54 -41
  19. package/dist/websocket/Subscriber.d.ts +37 -0
  20. package/dist/websocket/Subscriber.js +25 -0
  21. package/dist/websocket/index.d.ts +19 -2
  22. package/dist/websocket/index.js +82 -2
  23. package/package.json +1 -1
  24. package/src/actions.ts +333 -0
  25. package/src/{nord/client → client}/Nord.ts +207 -210
  26. package/src/{nord/client → client}/NordAdmin.ts +123 -153
  27. package/src/{nord/client → client}/NordUser.ts +216 -305
  28. package/src/gen/openapi.ts +40 -0
  29. package/src/index.ts +7 -1
  30. package/src/types.ts +4 -54
  31. package/src/utils.ts +44 -47
  32. package/src/{nord/models → websocket}/Subscriber.ts +2 -2
  33. package/src/websocket/index.ts +105 -2
  34. package/src/nord/api/actions.ts +0 -648
  35. package/src/nord/api/core.ts +0 -96
  36. package/src/nord/api/metrics.ts +0 -269
  37. package/src/nord/client/NordClient.ts +0 -79
  38. package/src/nord/index.ts +0 -25
  39. /package/src/{nord/utils/NordError.ts → error.ts} +0 -0
@@ -0,0 +1,743 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.NordUser = void 0;
37
+ const spl_token_1 = require("@solana/spl-token");
38
+ const web3_js_1 = require("@solana/web3.js");
39
+ const ed = __importStar(require("@noble/ed25519"));
40
+ const proton_1 = require("@n1xyz/proton");
41
+ const types_1 = require("../types");
42
+ const proto = __importStar(require("../gen/nord_pb"));
43
+ const utils_1 = require("../utils");
44
+ const protobuf_1 = require("@bufbuild/protobuf");
45
+ const actions_1 = require("../actions");
46
+ const error_1 = require("../error");
47
+ /**
48
+ * User class for interacting with the Nord protocol
49
+ */
50
+ class NordUser {
51
+ /**
52
+ * Create a new NordUser instance
53
+ *
54
+ * @param nord - Nord client instance
55
+ * @param sessionSignFn - Function to sign messages with the user's session key
56
+ * @param transactionSignFn - Function to sign transactions with the user's wallet (optional)
57
+ * @param sessionId - Existing session identifier
58
+ * @param sessionPubKey - Session public key
59
+ * @param publicKey - Wallet public key
60
+ * @throws {NordError} If required parameters are missing
61
+ */
62
+ constructor({ nord, sessionSignFn, transactionSignFn, sessionId, sessionPubKey, publicKey, }) {
63
+ this.lastTs = 0;
64
+ this.nonce = 0;
65
+ /** User balances by token symbol */
66
+ this.balances = {};
67
+ /** User positions by account ID */
68
+ this.positions = {};
69
+ /** User margins by account ID */
70
+ this.margins = {};
71
+ /** SPL token information */
72
+ this.splTokenInfos = [];
73
+ this.nord = nord;
74
+ this.sessionSignFn = sessionSignFn;
75
+ this.transactionSignFn = transactionSignFn;
76
+ this.sessionId = sessionId;
77
+ this.sessionPubKey = new web3_js_1.PublicKey(sessionPubKey);
78
+ this.publicKey = publicKey;
79
+ // Convert tokens from info endpoint to SPLTokenInfo
80
+ if (this.nord.tokens && this.nord.tokens.length > 0) {
81
+ this.splTokenInfos = this.nord.tokens.map((token) => ({
82
+ mint: token.mintAddr, // Use mintAddr as mint
83
+ precision: token.decimals,
84
+ tokenId: token.tokenId,
85
+ name: token.symbol,
86
+ }));
87
+ }
88
+ }
89
+ /**
90
+ * Create a NordUser from a private key
91
+ *
92
+ * @param nord - Nord instance
93
+ * @param privateKey - Private key as string or Uint8Array
94
+ * @returns NordUser instance
95
+ * @throws {NordError} If the private key is invalid
96
+ */
97
+ static fromPrivateKey(nord, privateKey) {
98
+ try {
99
+ const keypair = (0, utils_1.keypairFromPrivateKey)(privateKey);
100
+ const publicKey = keypair.publicKey;
101
+ const sessionSignFn = async (message) => {
102
+ // Use ed25519 to sign the message
103
+ return ed.sign(message, keypair.secretKey.slice(0, 32));
104
+ };
105
+ const transactionSignFn = async (tx) => {
106
+ tx.sign(keypair);
107
+ return tx;
108
+ };
109
+ return new NordUser({
110
+ nord,
111
+ sessionSignFn,
112
+ transactionSignFn,
113
+ publicKey,
114
+ sessionPubKey: publicKey.toBytes(),
115
+ });
116
+ }
117
+ catch (error) {
118
+ throw new error_1.NordError("Failed to create NordUser from private key", {
119
+ cause: error,
120
+ });
121
+ }
122
+ }
123
+ /**
124
+ * Get the associated token account for a token mint
125
+ *
126
+ * @param mint - Token mint address
127
+ * @returns Associated token account address
128
+ * @throws {NordError} If required parameters are missing or operation fails
129
+ */
130
+ async getAssociatedTokenAccount(mint) {
131
+ try {
132
+ // Get the token program ID from the mint account
133
+ const mintAccount = await this.nord.solanaConnection.getAccountInfo(mint);
134
+ if (!mintAccount) {
135
+ throw new error_1.NordError("Mint account not found");
136
+ }
137
+ const tokenProgramId = mintAccount.owner;
138
+ // Validate that the mint is owned by a supported SPL token program
139
+ if (!tokenProgramId.equals(spl_token_1.TOKEN_PROGRAM_ID) &&
140
+ !tokenProgramId.equals(spl_token_1.TOKEN_2022_PROGRAM_ID)) {
141
+ throw new error_1.NordError("Mint Account is not owned by a supported SPL token program");
142
+ }
143
+ const associatedTokenAddress = await (0, spl_token_1.getAssociatedTokenAddress)(mint, this.publicKey, false, tokenProgramId, spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID);
144
+ return associatedTokenAddress;
145
+ }
146
+ catch (error) {
147
+ throw new error_1.NordError("Failed to get associated token account", {
148
+ cause: error,
149
+ });
150
+ }
151
+ }
152
+ /**
153
+ * Deposit SPL tokens to the app
154
+ *
155
+ * @param amount - Amount to deposit
156
+ * @param tokenId - Token ID
157
+ * @param recipient - Recipient address; defaults to the user's address
158
+ * @returns Transaction signature
159
+ * @deprecated Use deposit instead
160
+ * @throws {NordError} If required parameters are missing or operation fails
161
+ */
162
+ async depositSpl(amount, tokenId, recipient) {
163
+ return this.deposit({ amount, tokenId, recipient });
164
+ }
165
+ /**
166
+ * Deposit SPL tokens to the app
167
+ *
168
+ * @param amount - Amount to deposit
169
+ * @param tokenId - Token ID
170
+ * @param recipient - Recipient address; defaults to the user's address
171
+ * @param sendOptions - Send options for .sendTransaction
172
+ * @returns Transaction signature
173
+ * @throws {NordError} If required parameters are missing or operation fails
174
+ */
175
+ async deposit({ amount, tokenId, recipient, sendOptions, }) {
176
+ try {
177
+ // Find the token info
178
+ const tokenInfo = this.splTokenInfos.find((t) => t.tokenId === tokenId);
179
+ if (!tokenInfo) {
180
+ throw new error_1.NordError(`Token with ID ${tokenId} not found`);
181
+ }
182
+ const mint = new web3_js_1.PublicKey(tokenInfo.mint);
183
+ const fromAccount = await this.getAssociatedTokenAccount(mint);
184
+ const payer = this.publicKey;
185
+ const { ix, extraSigner } = await this.nord.protonClient.buildDepositIx({
186
+ payer,
187
+ recipient: recipient ?? payer,
188
+ quantAmount: (0, proton_1.floatToScaledBigIntLossy)(amount, tokenInfo.precision),
189
+ mint,
190
+ sourceTokenAccount: fromAccount,
191
+ });
192
+ const { blockhash } = await this.nord.solanaConnection.getLatestBlockhash();
193
+ const tx = new web3_js_1.Transaction();
194
+ tx.add(ix);
195
+ tx.recentBlockhash = blockhash;
196
+ tx.feePayer = payer;
197
+ const signedTx = await this.transactionSignFn(tx);
198
+ signedTx.partialSign(extraSigner);
199
+ const signature = await this.nord.solanaConnection.sendRawTransaction(signedTx.serialize(), sendOptions);
200
+ return signature;
201
+ }
202
+ catch (error) {
203
+ throw new error_1.NordError(`Failed to deposit ${amount} of token ID ${tokenId}`, { cause: error });
204
+ }
205
+ }
206
+ /**
207
+ * Get a new nonce for actions
208
+ *
209
+ * @returns Nonce as number
210
+ */
211
+ getNonce() {
212
+ return ++this.nonce;
213
+ }
214
+ async submitSessionAction(kind) {
215
+ return this.submitSignedAction(kind, async (message) => {
216
+ const signature = await this.sessionSignFn(message);
217
+ const signed = new Uint8Array(message.length + signature.length);
218
+ signed.set(message);
219
+ signed.set(signature, message.length);
220
+ return signed;
221
+ });
222
+ }
223
+ /**
224
+ * Update account IDs for this user
225
+ *
226
+ * @throws {NordError} If the operation fails
227
+ */
228
+ async updateAccountId() {
229
+ try {
230
+ if (!this.publicKey) {
231
+ throw new error_1.NordError("Public key is required to update account ID");
232
+ }
233
+ const resp = await this.nord.getUser({
234
+ pubkey: this.publicKey.toBase58(),
235
+ });
236
+ if (!resp) {
237
+ throw new error_1.NordError(`User ${this.publicKey.toBase58()} not found`);
238
+ }
239
+ this.accountIds = resp.accountIds;
240
+ }
241
+ catch (error) {
242
+ throw new error_1.NordError("Failed to update account ID", { cause: error });
243
+ }
244
+ }
245
+ /**
246
+ * Fetch user information including balances and orders
247
+ *
248
+ * @throws {NordError} If the operation fails
249
+ */
250
+ async fetchInfo() {
251
+ if (this.accountIds !== undefined) {
252
+ const accountsData = await Promise.all(this.accountIds.map(async (accountId) => {
253
+ const response = await (0, utils_1.checkedFetch)(`${this.nord.webServerUrl}/account/${accountId}`);
254
+ const accountData = (await response.json());
255
+ // Ensure we have the correct accountId
256
+ return {
257
+ ...accountData,
258
+ accountId,
259
+ };
260
+ }));
261
+ for (const accountData of accountsData) {
262
+ // Process balances
263
+ this.balances[accountData.accountId] = [];
264
+ for (const balance of accountData.balances) {
265
+ this.balances[accountData.accountId].push({
266
+ accountId: accountData.accountId,
267
+ balance: balance.amount,
268
+ symbol: balance.token,
269
+ });
270
+ }
271
+ // Process positions
272
+ this.positions[accountData.accountId] = accountData.positions;
273
+ // Process margins
274
+ this.margins[accountData.accountId] = accountData.margins;
275
+ }
276
+ }
277
+ }
278
+ /**
279
+ * Refresh the user's session
280
+ *
281
+ * @throws {NordError} If the operation fails
282
+ */
283
+ async refreshSession() {
284
+ const result = await (0, actions_1.createSession)(this.nord.httpClient, this.transactionSignFn, await this.nord.getTimestamp(), this.getNonce(), {
285
+ userPubkey: this.publicKey,
286
+ sessionPubkey: this.sessionPubKey,
287
+ });
288
+ this.sessionId = result.sessionId;
289
+ }
290
+ /**
291
+ * Revoke a session
292
+ *
293
+ * @param sessionId - Session ID to revoke
294
+ * @throws {NordError} If the operation fails
295
+ */
296
+ async revokeSession(sessionId) {
297
+ try {
298
+ await (0, actions_1.revokeSession)(this.nord.httpClient, this.transactionSignFn, await this.nord.getTimestamp(), this.getNonce(), {
299
+ userPubkey: this.publicKey,
300
+ sessionId,
301
+ });
302
+ }
303
+ catch (error) {
304
+ throw new error_1.NordError(`Failed to revoke session ${sessionId}`, {
305
+ cause: error,
306
+ });
307
+ }
308
+ }
309
+ /**
310
+ * Checks if the session is valid
311
+ * @private
312
+ * @throws {NordError} If the session is not valid
313
+ */
314
+ checkSessionValidity() {
315
+ if (this.sessionId === undefined || this.sessionId === BigInt(0)) {
316
+ throw new error_1.NordError("Invalid or empty session ID. Please create or refresh your session.");
317
+ }
318
+ }
319
+ /**
320
+ * Withdraw tokens from the exchange
321
+ *
322
+ * @param tokenId - Token ID to withdraw
323
+ * @param amount - Amount to withdraw
324
+ * @throws {NordError} If the operation fails
325
+ */
326
+ async withdraw({ amount, tokenId, }) {
327
+ try {
328
+ this.checkSessionValidity();
329
+ const token = (0, utils_1.findToken)(this.nord.tokens, tokenId);
330
+ const scaledAmount = (0, utils_1.toScaledU64)(amount, token.decimals);
331
+ if (scaledAmount <= 0n) {
332
+ throw new error_1.NordError("Withdraw amount must be positive");
333
+ }
334
+ const receipt = await this.submitSessionAction({
335
+ case: "withdraw",
336
+ value: (0, protobuf_1.create)(proto.Action_WithdrawSchema, {
337
+ sessionId: BigInt((0, utils_1.optExpect)(this.sessionId, "No session")),
338
+ tokenId,
339
+ amount: scaledAmount,
340
+ }),
341
+ });
342
+ (0, actions_1.expectReceiptKind)(receipt, "withdrawResult", "withdraw");
343
+ return { actionId: receipt.actionId };
344
+ }
345
+ catch (error) {
346
+ throw new error_1.NordError(`Failed to withdraw ${amount} of token ID ${tokenId}`, { cause: error });
347
+ }
348
+ }
349
+ /**
350
+ * Place an order on the exchange
351
+ *
352
+ * @param marketId - Target market identifier
353
+ * @param side - Order side
354
+ * @param fillMode - Fill mode (limit, market, etc.)
355
+ * @param isReduceOnly - Reduce-only flag
356
+ * @param size - Base size to place
357
+ * @param price - Limit price
358
+ * @param quoteSize - Quote-sized order representation
359
+ * @param accountId - Account executing the order
360
+ * @param clientOrderId - Optional client-specified identifier
361
+ * @returns Object containing actionId, orderId (if posted), fills, and clientOrderId
362
+ * @throws {NordError} If the operation fails
363
+ */
364
+ async placeOrder({ marketId, side, fillMode, isReduceOnly, size, price, quoteSize, accountId, clientOrderId, }) {
365
+ try {
366
+ this.checkSessionValidity();
367
+ const market = (0, utils_1.findMarket)(this.nord.markets, marketId);
368
+ if (!market) {
369
+ throw new error_1.NordError(`Market with ID ${marketId} not found`);
370
+ }
371
+ const sessionId = (0, utils_1.optExpect)(this.sessionId, "No session");
372
+ const scaledPrice = (0, utils_1.toScaledU64)(price ?? 0, market.priceDecimals);
373
+ const scaledSize = (0, utils_1.toScaledU64)(size ?? 0, market.sizeDecimals);
374
+ const scaledQuote = quoteSize
375
+ ? quoteSize.toWire(market.priceDecimals, market.sizeDecimals)
376
+ : undefined;
377
+ (0, utils_1.assert)(scaledPrice > 0n || scaledSize > 0n || scaledQuote !== undefined, "OrderLimit must include at least one of: size, price, or quoteSize");
378
+ const receipt = await this.submitSessionAction({
379
+ case: "placeOrder",
380
+ value: (0, protobuf_1.create)(proto.Action_PlaceOrderSchema, {
381
+ sessionId: BigInt(sessionId),
382
+ senderAccountId: accountId,
383
+ marketId,
384
+ side: side === types_1.Side.Bid ? proto.Side.BID : proto.Side.ASK,
385
+ fillMode: (0, types_1.fillModeToProtoFillMode)(fillMode),
386
+ isReduceOnly,
387
+ price: scaledPrice,
388
+ size: scaledSize,
389
+ quoteSize: scaledQuote === undefined
390
+ ? undefined
391
+ : (0, protobuf_1.create)(proto.QuoteSizeSchema, {
392
+ size: scaledQuote.size,
393
+ price: scaledQuote.price,
394
+ }),
395
+ clientOrderId: clientOrderId === undefined ? undefined : BigInt(clientOrderId),
396
+ }),
397
+ });
398
+ (0, actions_1.expectReceiptKind)(receipt, "placeOrderResult", "place order");
399
+ const result = receipt.kind.value;
400
+ return {
401
+ actionId: receipt.actionId,
402
+ orderId: result.posted?.orderId,
403
+ fills: result.fills,
404
+ };
405
+ }
406
+ catch (error) {
407
+ throw new error_1.NordError("Failed to place order", { cause: error });
408
+ }
409
+ }
410
+ /**
411
+ * Cancel an order
412
+ *
413
+ * @param orderId - Order ID to cancel
414
+ * @param providedAccountId - Account ID that placed the order
415
+ * @returns Object containing actionId, cancelled orderId, and accountId
416
+ * @throws {NordError} If the operation fails
417
+ */
418
+ async cancelOrder(orderId, providedAccountId) {
419
+ const accountId = providedAccountId != null ? providedAccountId : this.accountIds?.[0];
420
+ try {
421
+ this.checkSessionValidity();
422
+ const receipt = await this.submitSessionAction({
423
+ case: "cancelOrderById",
424
+ value: (0, protobuf_1.create)(proto.Action_CancelOrderByIdSchema, {
425
+ orderId: BigInt(orderId),
426
+ sessionId: BigInt((0, utils_1.optExpect)(this.sessionId, "No session")),
427
+ senderAccountId: accountId,
428
+ }),
429
+ });
430
+ (0, actions_1.expectReceiptKind)(receipt, "cancelOrderResult", "cancel order");
431
+ return {
432
+ actionId: receipt.actionId,
433
+ orderId: receipt.kind.value.orderId,
434
+ accountId: receipt.kind.value.accountId,
435
+ };
436
+ }
437
+ catch (error) {
438
+ throw new error_1.NordError(`Failed to cancel order ${orderId}`, {
439
+ cause: error,
440
+ });
441
+ }
442
+ }
443
+ /**
444
+ * Add a trigger for the current session
445
+ *
446
+ * @param marketId - Market to watch
447
+ * @param side - Order side for the trigger
448
+ * @param kind - Stop-loss or take-profit trigger type
449
+ * @param triggerPrice - Price that activates the trigger
450
+ * @param limitPrice - Limit price placed once the trigger fires
451
+ * @param accountId - Account executing the trigger
452
+ * @returns Object containing the actionId of the submitted trigger
453
+ * @throws {NordError} If the operation fails
454
+ */
455
+ async addTrigger({ marketId, side, kind, triggerPrice, limitPrice, accountId, }) {
456
+ try {
457
+ this.checkSessionValidity();
458
+ const market = (0, utils_1.findMarket)(this.nord.markets, marketId);
459
+ if (!market) {
460
+ throw new error_1.NordError(`Market with ID ${marketId} not found`);
461
+ }
462
+ const scaledTriggerPrice = (0, utils_1.toScaledU64)(triggerPrice, market.priceDecimals);
463
+ (0, utils_1.assert)(scaledTriggerPrice > 0n, "Trigger price must be positive");
464
+ const scaledLimitPrice = limitPrice === undefined
465
+ ? undefined
466
+ : (0, utils_1.toScaledU64)(limitPrice, market.priceDecimals);
467
+ if (scaledLimitPrice !== undefined) {
468
+ (0, utils_1.assert)(scaledLimitPrice > 0n, "Limit price must be positive");
469
+ }
470
+ const key = (0, protobuf_1.create)(proto.TriggerKeySchema, {
471
+ kind: kind === types_1.TriggerKind.StopLoss
472
+ ? proto.TriggerKind.STOP_LOSS
473
+ : proto.TriggerKind.TAKE_PROFIT,
474
+ side: side === types_1.Side.Bid ? proto.Side.BID : proto.Side.ASK,
475
+ });
476
+ const prices = (0, protobuf_1.create)(proto.Action_TriggerPricesSchema, {
477
+ triggerPrice: scaledTriggerPrice,
478
+ limitPrice: scaledLimitPrice,
479
+ });
480
+ const receipt = await this.submitSessionAction({
481
+ case: "addTrigger",
482
+ value: (0, protobuf_1.create)(proto.Action_AddTriggerSchema, {
483
+ sessionId: BigInt((0, utils_1.optExpect)(this.sessionId, "No session")),
484
+ marketId,
485
+ key,
486
+ prices,
487
+ accountId,
488
+ }),
489
+ });
490
+ (0, actions_1.expectReceiptKind)(receipt, "triggerAdded", "add trigger");
491
+ return { actionId: receipt.actionId };
492
+ }
493
+ catch (error) {
494
+ throw new error_1.NordError("Failed to add trigger", { cause: error });
495
+ }
496
+ }
497
+ /**
498
+ * Remove a trigger for the current session
499
+ *
500
+ * @param marketId - Market the trigger belongs to
501
+ * @param side - Order side for the trigger
502
+ * @param kind - Stop-loss or take-profit trigger type
503
+ * @param accountId - Account executing the trigger
504
+ * @returns Object containing the actionId of the removal action
505
+ * @throws {NordError} If the operation fails
506
+ */
507
+ async removeTrigger({ marketId, side, kind, accountId, }) {
508
+ try {
509
+ this.checkSessionValidity();
510
+ const market = (0, utils_1.findMarket)(this.nord.markets, marketId);
511
+ if (!market) {
512
+ throw new error_1.NordError(`Market with ID ${marketId} not found`);
513
+ }
514
+ const key = (0, protobuf_1.create)(proto.TriggerKeySchema, {
515
+ kind: kind === types_1.TriggerKind.StopLoss
516
+ ? proto.TriggerKind.STOP_LOSS
517
+ : proto.TriggerKind.TAKE_PROFIT,
518
+ side: side === types_1.Side.Bid ? proto.Side.BID : proto.Side.ASK,
519
+ });
520
+ const receipt = await this.submitSessionAction({
521
+ case: "removeTrigger",
522
+ value: (0, protobuf_1.create)(proto.Action_RemoveTriggerSchema, {
523
+ sessionId: BigInt((0, utils_1.optExpect)(this.sessionId, "No session")),
524
+ marketId,
525
+ key,
526
+ accountId,
527
+ }),
528
+ });
529
+ (0, actions_1.expectReceiptKind)(receipt, "triggerRemoved", "remove trigger");
530
+ return { actionId: receipt.actionId };
531
+ }
532
+ catch (error) {
533
+ throw new error_1.NordError("Failed to remove trigger", { cause: error });
534
+ }
535
+ }
536
+ /**
537
+ * Transfer tokens to another account
538
+ *
539
+ * @param tokenId - Token identifier to move
540
+ * @param amount - Amount to transfer
541
+ * @param fromAccountId - Source account id
542
+ * @param toAccountId - Destination account id
543
+ * @throws {NordError} If the operation fails
544
+ */
545
+ async transferToAccount({ tokenId, amount, fromAccountId, toAccountId, }) {
546
+ try {
547
+ this.checkSessionValidity();
548
+ const token = (0, utils_1.findToken)(this.nord.tokens, tokenId);
549
+ const scaledAmount = (0, utils_1.toScaledU64)(amount, token.decimals);
550
+ if (scaledAmount <= 0n) {
551
+ throw new error_1.NordError("Transfer amount must be positive");
552
+ }
553
+ const receipt = await this.submitSessionAction({
554
+ case: "transfer",
555
+ value: (0, protobuf_1.create)(proto.Action_TransferSchema, {
556
+ sessionId: BigInt((0, utils_1.optExpect)(this.sessionId, "No session")),
557
+ fromAccountId: (0, utils_1.optExpect)(fromAccountId, "No source account"),
558
+ toAccountId: (0, utils_1.optExpect)(toAccountId, "No target account"),
559
+ tokenId,
560
+ amount: scaledAmount,
561
+ }),
562
+ });
563
+ (0, actions_1.expectReceiptKind)(receipt, "transferred", "transfer tokens");
564
+ if (receipt.kind.value.accountCreated) {
565
+ (0, utils_1.assert)(receipt.kind.value.toUserAccount !== undefined, `toAccount must be defined on new account on ${receipt.kind.value}`);
566
+ return {
567
+ actionId: receipt.actionId,
568
+ newAccountId: receipt.kind.value.toUserAccount,
569
+ };
570
+ }
571
+ else {
572
+ return { actionId: receipt.actionId };
573
+ }
574
+ }
575
+ catch (error) {
576
+ throw new error_1.NordError("Failed to transfer tokens", { cause: error });
577
+ }
578
+ }
579
+ /**
580
+ * Execute up to four place/cancel operations atomically.
581
+ * Per Market:
582
+ * 1. cancels can only be in the start (one cannot predict future order ids)
583
+ * 2. intermediate trades can trade only
584
+ * 3. placements go last
585
+ *
586
+ * Across Markets, order action can be any
587
+ *
588
+ * @param userActions array of user-friendly subactions
589
+ * @param providedAccountId optional account performing the action (defaults to first account)
590
+ */
591
+ async atomic(userActions, providedAccountId) {
592
+ try {
593
+ this.checkSessionValidity();
594
+ const accountId = providedAccountId != null ? providedAccountId : this.accountIds?.[0];
595
+ if (accountId == null) {
596
+ throw new error_1.NordError("Account ID is undefined. Make sure to call updateAccountId() before atomic operations.");
597
+ }
598
+ const apiActions = userActions.map((act) => {
599
+ if (act.kind === "place") {
600
+ const market = (0, utils_1.findMarket)(this.nord.markets, act.marketId);
601
+ if (!market) {
602
+ throw new error_1.NordError(`Market ${act.marketId} not found`);
603
+ }
604
+ return {
605
+ kind: "place",
606
+ marketId: act.marketId,
607
+ side: act.side,
608
+ fillMode: act.fillMode,
609
+ isReduceOnly: act.isReduceOnly,
610
+ sizeDecimals: market.sizeDecimals,
611
+ priceDecimals: market.priceDecimals,
612
+ size: act.size,
613
+ price: act.price,
614
+ quoteSize: act.quoteSize,
615
+ clientOrderId: act.clientOrderId,
616
+ };
617
+ }
618
+ return {
619
+ kind: "cancel",
620
+ orderId: act.orderId,
621
+ };
622
+ });
623
+ const result = await (0, actions_1.atomic)(this.nord.httpClient, this.sessionSignFn, await this.nord.getTimestamp(), this.getNonce(), {
624
+ sessionId: (0, utils_1.optExpect)(this.sessionId, "No session"),
625
+ accountId: accountId,
626
+ actions: apiActions,
627
+ });
628
+ return result;
629
+ }
630
+ catch (error) {
631
+ throw new error_1.NordError("Atomic operation failed", { cause: error });
632
+ }
633
+ }
634
+ /**
635
+ * Helper function to retry a promise with exponential backoff
636
+ *
637
+ * @param fn - Function to retry
638
+ * @param maxRetries - Maximum number of retries
639
+ * @param initialDelay - Initial delay in milliseconds
640
+ * @returns Promise result
641
+ */
642
+ async retryWithBackoff(fn, maxRetries = 3, initialDelay = 500) {
643
+ let retries = 0;
644
+ let delay = initialDelay;
645
+ while (true) {
646
+ try {
647
+ return await fn();
648
+ }
649
+ catch (error) {
650
+ if (retries >= maxRetries) {
651
+ throw error;
652
+ }
653
+ // Check if error is rate limiting related
654
+ const isRateLimitError = error instanceof Error &&
655
+ (error.message.includes("rate limit") ||
656
+ error.message.includes("429") ||
657
+ error.message.includes("too many requests"));
658
+ if (!isRateLimitError) {
659
+ throw error;
660
+ }
661
+ retries++;
662
+ await new Promise((resolve) => setTimeout(resolve, delay));
663
+ delay *= 2; // Exponential backoff
664
+ }
665
+ }
666
+ }
667
+ /**
668
+ * Get user's token balances on Solana chain using mintAddr
669
+ *
670
+ * @param options - Optional parameters
671
+ * @param options.includeZeroBalances - Whether to include tokens with zero balance (default: true)
672
+ * @param options.includeTokenAccounts - Whether to include token account addresses in the result (default: false)
673
+ * @param options.maxConcurrent - Maximum number of concurrent requests (default: 5)
674
+ * @param options.maxRetries - Maximum number of retries for rate-limited requests (default: 3)
675
+ * @returns Object with token balances and optional token account addresses
676
+ * @throws {NordError} If required parameters are missing or operation fails
677
+ */
678
+ async getSolanaBalances(options = {}) {
679
+ const { includeZeroBalances = true, includeTokenAccounts = false, maxConcurrent = 5, maxRetries = 3, } = options;
680
+ const balances = {};
681
+ const tokenAccounts = {};
682
+ try {
683
+ // Get SOL balance (native token)
684
+ const solBalance = await this.retryWithBackoff(() => this.nord.solanaConnection.getBalance(this.publicKey), maxRetries);
685
+ balances["SOL"] = solBalance / 1e9; // Convert lamports to SOL
686
+ if (includeTokenAccounts) {
687
+ tokenAccounts["SOL"] = this.publicKey.toString();
688
+ }
689
+ // Get SPL token balances using mintAddr from Nord tokens
690
+ if (this.nord.tokens && this.nord.tokens.length > 0) {
691
+ const tokens = this.nord.tokens.filter((token) => !!token.mintAddr);
692
+ // Process tokens in batches to avoid rate limiting
693
+ for (let i = 0; i < tokens.length; i += maxConcurrent) {
694
+ const batch = tokens.slice(i, i + maxConcurrent);
695
+ // Process batch in parallel
696
+ const batchPromises = batch.map(async (token) => {
697
+ try {
698
+ const mint = new web3_js_1.PublicKey(token.mintAddr);
699
+ const associatedTokenAddress = await this.retryWithBackoff(() => (0, spl_token_1.getAssociatedTokenAddress)(mint, this.publicKey), maxRetries);
700
+ if (includeTokenAccounts) {
701
+ tokenAccounts[token.symbol] = associatedTokenAddress.toString();
702
+ }
703
+ try {
704
+ const tokenBalance = await this.retryWithBackoff(() => this.nord.solanaConnection.getTokenAccountBalance(associatedTokenAddress), maxRetries);
705
+ const balance = Number(tokenBalance.value.uiAmount);
706
+ if (balance > 0 || includeZeroBalances) {
707
+ balances[token.symbol] = balance;
708
+ }
709
+ }
710
+ catch {
711
+ // Token account might not exist yet, set balance to 0
712
+ if (includeZeroBalances) {
713
+ balances[token.symbol] = 0;
714
+ }
715
+ }
716
+ }
717
+ catch (error) {
718
+ console.error(`Error getting balance for token ${token.symbol}:`, error);
719
+ if (includeZeroBalances) {
720
+ balances[token.symbol] = 0;
721
+ }
722
+ }
723
+ });
724
+ // Wait for current batch to complete before processing next batch
725
+ await Promise.all(batchPromises);
726
+ }
727
+ }
728
+ return includeTokenAccounts ? { balances, tokenAccounts } : { balances };
729
+ }
730
+ catch (error) {
731
+ throw new error_1.NordError("Failed to get Solana token balances", {
732
+ cause: error,
733
+ });
734
+ }
735
+ }
736
+ async submitSignedAction(kind, makeSignedMessage) {
737
+ const nonce = this.getNonce();
738
+ const currentTimestamp = await this.nord.getTimestamp();
739
+ const action = (0, actions_1.createAction)(currentTimestamp, nonce, kind);
740
+ return (0, actions_1.sendAction)(this.nord.httpClient, makeSignedMessage, action);
741
+ }
742
+ }
743
+ exports.NordUser = NordUser;