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