@n1xyz/nord-ts 0.3.2 → 0.3.4

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