@n1xyz/nord-ts 0.0.18-8121ed05.0 → 0.0.19

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 (77) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/.local/qa.ts +77 -0
  3. package/.local/test-atomic.ts +112 -0
  4. package/check.sh +4 -0
  5. package/default.nix +47 -0
  6. package/package.json +20 -27
  7. package/src/index.ts +0 -16
  8. package/src/nord/api/actions.ts +131 -9
  9. package/src/nord/api/core.ts +0 -71
  10. package/src/nord/client/Nord.ts +142 -76
  11. package/src/nord/client/NordUser.ts +171 -50
  12. package/src/nord/index.ts +0 -2
  13. package/src/nord/models/Subscriber.ts +2 -2
  14. package/src/types.ts +55 -216
  15. package/src/utils.ts +6 -42
  16. package/src/websocket/NordWebSocketClient.ts +23 -15
  17. package/src/websocket/index.ts +1 -1
  18. package/tests/utils.spec.ts +1 -34
  19. package/dist/bridge/client.d.ts +0 -151
  20. package/dist/bridge/client.js +0 -434
  21. package/dist/bridge/const.d.ts +0 -23
  22. package/dist/bridge/const.js +0 -47
  23. package/dist/bridge/index.d.ts +0 -4
  24. package/dist/bridge/index.js +0 -23
  25. package/dist/bridge/types.d.ts +0 -120
  26. package/dist/bridge/types.js +0 -18
  27. package/dist/bridge/utils.d.ts +0 -64
  28. package/dist/bridge/utils.js +0 -131
  29. package/dist/const.d.ts +0 -8
  30. package/dist/const.js +0 -30
  31. package/dist/gen/common.d.ts +0 -68
  32. package/dist/gen/common.js +0 -215
  33. package/dist/gen/nord.d.ts +0 -853
  34. package/dist/gen/nord.js +0 -6368
  35. package/dist/idl/bridge.d.ts +0 -569
  36. package/dist/idl/bridge.js +0 -8
  37. package/dist/idl/bridge.json +0 -1506
  38. package/dist/idl/index.d.ts +0 -607
  39. package/dist/idl/index.js +0 -8
  40. package/dist/index.d.ts +0 -6
  41. package/dist/index.js +0 -30
  42. package/dist/nord/api/actions.d.ts +0 -106
  43. package/dist/nord/api/actions.js +0 -256
  44. package/dist/nord/api/core.d.ts +0 -49
  45. package/dist/nord/api/core.js +0 -164
  46. package/dist/nord/api/market.d.ts +0 -36
  47. package/dist/nord/api/market.js +0 -96
  48. package/dist/nord/api/metrics.d.ts +0 -67
  49. package/dist/nord/api/metrics.js +0 -229
  50. package/dist/nord/api/queries.d.ts +0 -46
  51. package/dist/nord/api/queries.js +0 -109
  52. package/dist/nord/client/Nord.d.ts +0 -284
  53. package/dist/nord/client/Nord.js +0 -491
  54. package/dist/nord/client/NordUser.d.ts +0 -287
  55. package/dist/nord/client/NordUser.js +0 -595
  56. package/dist/nord/index.d.ts +0 -9
  57. package/dist/nord/index.js +0 -33
  58. package/dist/nord/models/Subscriber.d.ts +0 -37
  59. package/dist/nord/models/Subscriber.js +0 -25
  60. package/dist/nord/utils/NordError.d.ts +0 -35
  61. package/dist/nord/utils/NordError.js +0 -49
  62. package/dist/types.d.ts +0 -407
  63. package/dist/types.js +0 -103
  64. package/dist/utils.d.ts +0 -116
  65. package/dist/utils.js +0 -271
  66. package/dist/websocket/NordWebSocketClient.d.ts +0 -68
  67. package/dist/websocket/NordWebSocketClient.js +0 -338
  68. package/dist/websocket/events.d.ts +0 -19
  69. package/dist/websocket/events.js +0 -2
  70. package/dist/websocket/index.d.ts +0 -2
  71. package/dist/websocket/index.js +0 -5
  72. package/jest.config.ts +0 -9
  73. package/nodemon.json +0 -4
  74. package/protoc-generate.sh +0 -23
  75. package/src/idl/bridge.json +0 -1506
  76. package/src/nord/api/market.ts +0 -122
  77. package/src/nord/api/queries.ts +0 -135
@@ -1,35 +1,32 @@
1
1
  import { EventEmitter } from "events";
2
+ import { PublicKey, Connection } from "@solana/web3.js";
3
+ import createClient, { Client, FetchOptions } from "openapi-fetch";
2
4
  import {
5
+ Info,
3
6
  Account,
4
- ActionQuery,
5
7
  ActionResponse,
6
- ActionsResponse,
7
8
  AggregateMetrics,
8
- Info,
9
9
  Market,
10
- MarketsStatsResponse,
11
10
  NordConfig,
12
11
  OrderbookQuery,
13
12
  OrderbookResponse,
14
13
  PeakTpsPeriodUnit,
15
- RollmanActionResponse,
16
- RollmanActionsResponse,
17
14
  SubscriptionPattern,
18
15
  Token,
19
- TradesQuery,
20
16
  TradesResponse,
21
- UserAccountIdsQuery,
22
- UserAccountIdsResponse,
17
+ User,
18
+ MarketStats,
23
19
  } from "../../types";
24
20
  import { ProtonClient } from "@n1xyz/proton";
21
+ import * as proto from "../../gen/nord";
22
+ // import { base64 } from "@scure/base";
25
23
  import { NordWebSocketClient } from "../../websocket/index";
26
24
  import * as core from "../api/core";
27
- import * as market from "../api/market";
28
25
  import * as metrics from "../api/metrics";
29
- import * as queries from "../api/queries";
26
+ import * as utils from "../../utils";
30
27
  import { OrderbookSubscription, TradeSubscription } from "../models/Subscriber";
31
28
  import { NordError } from "../utils/NordError";
32
- import { PublicKey, Connection } from "@solana/web3.js";
29
+ import type { paths } from "../../gen/openapi.ts";
33
30
 
34
31
  /**
35
32
  * User subscription interface
@@ -72,9 +69,12 @@ export class Nord {
72
69
  /** Map of symbol to market_id */
73
70
  private symbolToMarketId: Map<string, number> = new Map();
74
71
 
75
- /** Proton client for bridge and indexd operations */
72
+ /** Proton client for bridge and hansel operations */
76
73
  public protonClient: ProtonClient;
77
74
 
75
+ /** HTTP client for Nord operations */
76
+ private httpClient: Client<paths>;
77
+
78
78
  /**
79
79
  * Create a new Nord client
80
80
  *
@@ -99,6 +99,7 @@ export class Nord {
99
99
  this.bridgeVk = bridgeVk;
100
100
  this.solanaUrl = solanaUrl;
101
101
  this.protonClient = protonClient;
102
+ this.httpClient = createClient<paths>({ baseUrl: webServerUrl });
102
103
  }
103
104
 
104
105
  /**
@@ -162,6 +163,22 @@ export class Nord {
162
163
  return core.initWebSocketClient(this.webServerUrl, subscriptions);
163
164
  }
164
165
 
166
+ private async GET<P extends keyof paths & string>(
167
+ path: P,
168
+ options: FetchOptions<paths[P]["get"]>,
169
+ ) {
170
+ const r = await this.httpClient.GET(path, options);
171
+ if (r.error) {
172
+ throw new NordError(`failed to GET ${path}`, { cause: r.error });
173
+ }
174
+ if (r.data === undefined) {
175
+ // this should never happen, but the type checker seems unhappy.
176
+ // if we catch this we'll need to debug accordingly.
177
+ throw new NordError("internal assertion violation", { cause: r });
178
+ }
179
+ return r.data;
180
+ }
181
+
165
182
  /**
166
183
  * Get the current timestamp from the Nord server
167
184
  *
@@ -169,7 +186,7 @@ export class Nord {
169
186
  * @throws {NordError} If the request fails
170
187
  */
171
188
  async getTimestamp(): Promise<bigint> {
172
- return core.getTimestamp(this.webServerUrl);
189
+ return BigInt(await this.GET("/timestamp", {}));
173
190
  }
174
191
 
175
192
  /**
@@ -179,7 +196,7 @@ export class Nord {
179
196
  * @throws {NordError} If the request fails
180
197
  */
181
198
  async getActionNonce(): Promise<number> {
182
- return core.getLastEventNonce(this.webServerUrl);
199
+ return await this.GET("/event/last-acked-nonce", {});
183
200
  }
184
201
 
185
202
  /**
@@ -189,7 +206,7 @@ export class Nord {
189
206
  */
190
207
  async fetchNordInfo(): Promise<void> {
191
208
  try {
192
- const info = await core.getInfo(this.webServerUrl);
209
+ const info = await this.GET("/info", {});
193
210
  this.markets = info.markets;
194
211
  this.tokens = info.tokens;
195
212
 
@@ -246,16 +263,6 @@ export class Nord {
246
263
  await this.fetchNordInfo();
247
264
  }
248
265
 
249
- /**
250
- * Get market statistics
251
- *
252
- * @returns Market statistics response
253
- * @throws {NordError} If the request fails
254
- */
255
- public async marketsStats(): Promise<MarketsStatsResponse> {
256
- return market.marketsStats(this.webServerUrl);
257
- }
258
-
259
266
  /**
260
267
  * Query a specific action
261
268
  *
@@ -263,8 +270,19 @@ export class Nord {
263
270
  * @returns Action response
264
271
  * @throws {NordError} If the request fails
265
272
  */
266
- async queryAction(query: ActionQuery): Promise<ActionResponse> {
267
- return queries.queryAction(this.webServerUrl, query);
273
+ async queryAction({
274
+ action_id,
275
+ }: {
276
+ action_id: number;
277
+ }): Promise<ActionResponse | null> {
278
+ return (
279
+ (
280
+ await this.queryRecentActions({
281
+ from: action_id,
282
+ to: action_id,
283
+ })
284
+ )[0] ?? null
285
+ );
268
286
  }
269
287
 
270
288
  /**
@@ -275,8 +293,23 @@ export class Nord {
275
293
  * @returns Actions response
276
294
  * @throws {NordError} If the request fails
277
295
  */
278
- async queryRecentActions(from: number, to: number): Promise<ActionsResponse> {
279
- return queries.queryRecentActions(this.webServerUrl, from, to);
296
+ async queryRecentActions(query: {
297
+ from: number;
298
+ to: number;
299
+ }): Promise<ActionResponse[]> {
300
+ const xs = await this.GET("/action", {
301
+ params: {
302
+ query,
303
+ },
304
+ });
305
+ return xs.map((x) => ({
306
+ actionId: x.actionId,
307
+ action: utils.decodeLengthDelimited(
308
+ Buffer.from(x.payload, "base64"),
309
+ proto.Action,
310
+ ),
311
+ physicalExecTime: new Date(x.physicalTime * 1000),
312
+ }));
280
313
  }
281
314
 
282
315
  /**
@@ -286,7 +319,7 @@ export class Nord {
286
319
  * @throws {NordError} If the request fails
287
320
  */
288
321
  async getLastActionId(): Promise<number> {
289
- return queries.getLastActionId(this.webServerUrl);
322
+ return await this.GET("/action/last-executed-id", {});
290
323
  }
291
324
 
292
325
  /**
@@ -351,28 +384,6 @@ export class Nord {
351
384
  return metrics.getTotalTransactions(this.webServerUrl);
352
385
  }
353
386
 
354
- /**
355
- * Query an action from Rollman
356
- *
357
- * @param query - Action query parameters
358
- * @returns Rollman action response
359
- * @throws {NordError} If the request fails
360
- */
361
- async actionQueryRollman(query: ActionQuery): Promise<RollmanActionResponse> {
362
- return queries.actionQueryRollman(this.webServerUrl, query);
363
- }
364
-
365
- /**
366
- * Query actions from Rollman
367
- *
368
- * @param last_n - Number of recent actions to query
369
- * @returns Rollman actions response
370
- * @throws {NordError} If the request fails
371
- */
372
- async actionsQueryRollman(last_n: number): Promise<RollmanActionsResponse> {
373
- return queries.actionsQueryRollman(this.webServerUrl, last_n);
374
- }
375
-
376
387
  /**
377
388
  * Query Prometheus metrics
378
389
  *
@@ -511,8 +522,38 @@ export class Nord {
511
522
  * @returns Trades response
512
523
  * @throws {NordError} If the request fails
513
524
  */
514
- public async getTrades(query: TradesQuery): Promise<TradesResponse> {
515
- return market.getTrades(this.webServerUrl, query);
525
+ public async getTrades(
526
+ query: Readonly<{
527
+ marketId?: number;
528
+ takerId?: number;
529
+ makerId?: number;
530
+ takerSide?: "bid" | "ask";
531
+ pageSize?: number;
532
+ sinceRcf3339?: string;
533
+ untilRfc3339?: string;
534
+ pageId?: string;
535
+ }>,
536
+ ): Promise<TradesResponse> {
537
+ if (query.sinceRcf3339 && !utils.isRfc3339(query.sinceRcf3339)) {
538
+ throw new NordError(`Invalid RFC3339 timestamp: ${query.sinceRcf3339}`);
539
+ }
540
+ if (query.untilRfc3339 && !utils.isRfc3339(query.untilRfc3339)) {
541
+ throw new NordError(`Invalid RFC3339 timestamp: ${query.untilRfc3339}`);
542
+ }
543
+ return await this.GET("/trades", {
544
+ params: {
545
+ query: {
546
+ takerId: query.takerId,
547
+ makerId: query.makerId,
548
+ marketId: query.marketId,
549
+ pageSize: query.pageSize,
550
+ takerSide: query.takerSide,
551
+ since: query.sinceRcf3339,
552
+ until: query.untilRfc3339,
553
+ startInclusive: query.pageId,
554
+ },
555
+ },
556
+ });
516
557
  }
517
558
 
518
559
  /**
@@ -522,10 +563,18 @@ export class Nord {
522
563
  * @returns User account IDs response
523
564
  * @throws {NordError} If the request fails
524
565
  */
525
- public async getUserAccountIds(
526
- query: UserAccountIdsQuery,
527
- ): Promise<UserAccountIdsResponse> {
528
- return market.getUserAccountIds(this.webServerUrl, query);
566
+ public async getUser(query: {
567
+ pubkey: string | PublicKey;
568
+ }): Promise<User | null> {
569
+ const r = await this.httpClient.GET("/user/{pubkey}", {
570
+ params: {
571
+ path: { pubkey: query.pubkey.toString() },
572
+ },
573
+ });
574
+ if (r.response.status === 404) {
575
+ return null;
576
+ }
577
+ return r.data!;
529
578
  }
530
579
 
531
580
  /**
@@ -539,21 +588,31 @@ export class Nord {
539
588
  */
540
589
  public async getOrderbook(query: OrderbookQuery): Promise<OrderbookResponse> {
541
590
  // If only symbol is provided, convert it to market_id
591
+ let marketId: number;
542
592
  if (query.symbol && query.market_id === undefined) {
543
593
  // If the map is empty, try to fetch market information first
544
594
  if (this.symbolToMarketId.size === 0) {
545
595
  await this.fetchNordInfo();
546
596
  }
547
597
 
548
- const marketId = this.symbolToMarketId.get(query.symbol);
549
- if (marketId === undefined) {
598
+ const id = this.symbolToMarketId.get(query.symbol);
599
+ if (id === undefined) {
550
600
  throw new NordError(`Unknown market symbol: ${query.symbol}`);
551
601
  }
552
-
553
- query = { market_id: marketId };
602
+ marketId = id;
603
+ } else if (query.market_id !== undefined) {
604
+ marketId = query.market_id;
605
+ } else {
606
+ throw new NordError(
607
+ "Either symbol or market_id must be provided for orderbook query",
608
+ );
554
609
  }
555
610
 
556
- return market.getOrderbook(this.webServerUrl, query);
611
+ return await this.GET("/market/{market_id}/orderbook", {
612
+ params: {
613
+ path: { market_id: marketId },
614
+ },
615
+ });
557
616
  }
558
617
 
559
618
  /**
@@ -563,7 +622,7 @@ export class Nord {
563
622
  * @throws {NordError} If the request fails
564
623
  */
565
624
  public async getInfo(): Promise<Info> {
566
- return core.getInfo(this.webServerUrl);
625
+ return await this.GET("/info", {});
567
626
  }
568
627
 
569
628
  /**
@@ -574,17 +633,28 @@ export class Nord {
574
633
  * @throws {NordError} If the request fails
575
634
  */
576
635
  public async getAccount(accountId: number): Promise<Account> {
577
- return core.getAccount(this.webServerUrl, accountId);
636
+ return await this.GET("/account/{account_id}", {
637
+ params: {
638
+ path: { account_id: accountId },
639
+ },
640
+ });
578
641
  }
579
642
 
580
643
  /**
581
644
  * Get market statistics (alias for marketsStats for backward compatibility)
582
645
  *
583
- * @deprecated Use marketsStats instead
584
646
  * @returns Market statistics response
585
647
  */
586
- public async getMarketStats(): Promise<MarketsStatsResponse> {
587
- return this.marketsStats();
648
+ public async getMarketStats({
649
+ marketId,
650
+ }: {
651
+ marketId: number;
652
+ }): Promise<MarketStats> {
653
+ return await this.GET("/market/{market_id}/stats", {
654
+ params: {
655
+ path: { market_id: marketId },
656
+ },
657
+ });
588
658
  }
589
659
 
590
660
  /**
@@ -592,13 +662,9 @@ export class Nord {
592
662
  *
593
663
  * @param address - The public key address to check
594
664
  * @returns True if the account exists, false otherwise
665
+ * @deprecated use getUser instead
595
666
  */
596
- public async accountExists(address: string): Promise<boolean> {
597
- try {
598
- await market.getUserAccountIds(this.webServerUrl, { pubkey: address });
599
- return true;
600
- } catch {
601
- return false;
602
- }
667
+ public async accountExists(pubkey: string | PublicKey): Promise<boolean> {
668
+ return !!(await this.getUser({ pubkey }));
603
669
  }
604
670
  }
@@ -9,8 +9,9 @@ import Decimal from "decimal.js";
9
9
  import * as ed from "@noble/ed25519";
10
10
  import { sha512 } from "@noble/hashes/sha512";
11
11
  ed.etc.sha512Sync = sha512;
12
- import { SPLTokenInfo, DepositSplParams, floatToBn } from "@n1xyz/proton";
13
- import { FillMode, Order, Side } from "../../types";
12
+ import { floatToScaledBigIntLossy } from "@n1xyz/proton";
13
+ import { FillMode, Order, Side, SPLTokenInfo } from "../../types";
14
+ import * as proto from "../../gen/nord";
14
15
  import {
15
16
  BigIntValue,
16
17
  checkedFetch,
@@ -26,6 +27,8 @@ import {
26
27
  revokeSession,
27
28
  transfer,
28
29
  withdraw,
30
+ atomic as atomicAction,
31
+ AtomicSubaction as ApiAtomicSubaction,
29
32
  } from "../api/actions";
30
33
  import { NordError } from "../utils/NordError";
31
34
  import { Nord } from "./Nord";
@@ -47,7 +50,7 @@ export interface NordUserParams {
47
50
  sessionSignFn: (message: Uint8Array) => Promise<Uint8Array>;
48
51
 
49
52
  /** Function to sign transactions with the user's wallet (optional) */
50
- transactionSignFn: (transaction: any) => Promise<any>;
53
+ transactionSignFn: <T extends Transaction>(tx: T) => Promise<T>;
51
54
 
52
55
  /** Solana connection (optional) */
53
56
  connection?: Connection;
@@ -111,6 +114,41 @@ export interface TransferParams {
111
114
  toAccountId: number;
112
115
  }
113
116
 
117
+ /**
118
+ * Parameters for individual atomic subactions (user-friendly version)
119
+ */
120
+ export interface UserAtomicSubaction {
121
+ /** The type of action to perform. */
122
+ kind: "place" | "cancel";
123
+
124
+ /** The market ID to place the order in. */
125
+ marketId?: number;
126
+
127
+ /** The order ID to cancel. */
128
+ orderId?: BigIntValue;
129
+
130
+ /** Order side (bid or ask) */
131
+ side?: Side;
132
+
133
+ /** Fill mode (limit, market, etc.) */
134
+ fillMode?: FillMode;
135
+
136
+ /** Whether the order is reduce-only. */
137
+ isReduceOnly?: boolean;
138
+
139
+ /** The size of the order. */
140
+ size?: Decimal.Value;
141
+
142
+ /** Order price */
143
+ price?: Decimal.Value;
144
+
145
+ /** Quote size (for market orders) */
146
+ quoteSize?: Decimal.Value;
147
+
148
+ /** The client order ID of the order. */
149
+ clientOrderId?: BigIntValue;
150
+ }
151
+
114
152
  /**
115
153
  * User class for interacting with the Nord protocol
116
154
  */
@@ -130,16 +168,15 @@ export class NordUser {
130
168
  public readonly sessionSignFn: (message: Uint8Array) => Promise<Uint8Array>;
131
169
 
132
170
  /** Function to sign transactions with the user's wallet */
133
- public readonly transactionSignFn: (transaction: any) => Promise<any>;
171
+ public readonly transactionSignFn: <T extends Transaction>(
172
+ tx: T,
173
+ ) => Promise<T>;
134
174
 
135
175
  /** User balances by token symbol */
136
176
  public balances: {
137
177
  [key: string]: { accountId: number; balance: number; symbol: string }[];
138
178
  } = {};
139
179
 
140
- /** User orders by market symbol */
141
- public orders: { [key: string]: Order[] } = {};
142
-
143
180
  /** User positions by account ID */
144
181
  public positions: {
145
182
  [key: string]: {
@@ -275,7 +312,6 @@ export class NordUser {
275
312
 
276
313
  // Copy other properties
277
314
  cloned.balances = { ...this.balances };
278
- cloned.orders = { ...this.orders };
279
315
  cloned.positions = { ...this.positions };
280
316
  cloned.margins = { ...this.margins };
281
317
  cloned.accountIds = this.accountIds ? [...this.accountIds] : undefined;
@@ -416,10 +452,37 @@ export class NordUser {
416
452
  *
417
453
  * @param amount - Amount to deposit
418
454
  * @param tokenId - Token ID
455
+ * @param recipient - Recipient address; defaults to the user's address
419
456
  * @returns Transaction signature
457
+ * @deprecated Use deposit instead
420
458
  * @throws {NordError} If required parameters are missing or operation fails
421
459
  */
422
- async depositSpl(amount: number, tokenId: number): Promise<string> {
460
+ async depositSpl(
461
+ amount: number,
462
+ tokenId: number,
463
+ recipient?: PublicKey,
464
+ ): Promise<string> {
465
+ return this.deposit({ amount, tokenId, recipient });
466
+ }
467
+
468
+ /**
469
+ * Deposit SPL tokens to the bridge
470
+ *
471
+ * @param amount - Amount to deposit
472
+ * @param tokenId - Token ID
473
+ * @param recipient - Recipient address; defaults to the user's address
474
+ * @returns Transaction signature
475
+ * @throws {NordError} If required parameters are missing or operation fails
476
+ */
477
+ async deposit({
478
+ amount,
479
+ tokenId,
480
+ recipient,
481
+ }: Readonly<{
482
+ amount: number;
483
+ tokenId: number;
484
+ recipient?: PublicKey;
485
+ }>): Promise<string> {
423
486
  try {
424
487
  // Find the token info
425
488
  const tokenInfo = this.splTokenInfos.find((t) => t.tokenId === tokenId);
@@ -428,35 +491,28 @@ export class NordUser {
428
491
  }
429
492
 
430
493
  const mint = new PublicKey(tokenInfo.mint);
431
- // Get the user's token account
432
494
  const fromAccount = await this.getAssociatedTokenAccount(mint);
495
+ const payer = this.getSolanaPublicKey();
433
496
 
434
- // Convert amount to BN with proper decimals
435
- const amountBN = floatToBn(amount, tokenInfo.precision);
436
-
437
- // Create deposit parameters
438
- const depositParams: DepositSplParams = {
439
- amount: amountBN,
497
+ const { ix, extraSigner } = await this.nord.protonClient.buildDepositIx({
498
+ payer,
499
+ recipient: recipient ?? payer,
500
+ quantAmount: floatToScaledBigIntLossy(amount, tokenInfo.precision),
440
501
  mint,
441
- fromAccount,
442
- };
443
-
444
- // Build the deposit transaction using proton client
445
- const depositTx = await this.nord.protonClient.buildDepositTx(
446
- depositParams,
447
- this.getSolanaPublicKey(),
448
- this.connection,
449
- );
502
+ sourceTokenAccount: fromAccount,
503
+ });
450
504
 
451
- const { blockhash } = await this.connection.getLatestBlockhash();
505
+ const { blockhash } =
506
+ await this.connection.getLatestBlockhash("confirmed");
507
+ const tx = new Transaction();
452
508
 
453
- depositTx.recentBlockhash = blockhash;
454
- depositTx.feePayer = this.getSolanaPublicKey();
509
+ tx.add(ix);
510
+ tx.recentBlockhash = blockhash;
511
+ tx.feePayer = payer;
455
512
 
456
- const signedTx = await this.transactionSignFn(depositTx);
513
+ const signedTx = await this.transactionSignFn(tx);
514
+ signedTx.partialSign(extraSigner);
457
515
 
458
- // TODO: should use `VersionedTransaction` and remove any for `transactionSignFn`,
459
- // this is incredibly annoying to debug and i could've saved 30 mins.
460
516
  const signature = await this.connection.sendRawTransaction(
461
517
  signedTx.serialize(),
462
518
  );
@@ -490,10 +546,14 @@ export class NordUser {
490
546
  throw new NordError("Public key is required to update account ID");
491
547
  }
492
548
 
493
- const resp = await this.nord.getUserAccountIds({
549
+ const resp = await this.nord.getUser({
494
550
  pubkey: this.publicKey.toBase58(),
495
551
  });
496
552
 
553
+ if (!resp) {
554
+ throw new NordError(`User ${this.publicKey.toBase58()} not found`);
555
+ }
556
+
497
557
  this.accountIds = resp.accountIds;
498
558
  } catch (error) {
499
559
  throw new NordError("Failed to update account ID", { cause: error });
@@ -582,19 +642,6 @@ export class NordUser {
582
642
  });
583
643
  }
584
644
 
585
- // Process orders
586
- this.orders[accountData.accountId] = accountData.orders.map(
587
- (order: OpenOrder) => {
588
- return {
589
- orderId: order.orderId,
590
- isLong: order.side === "bid",
591
- size: order.size,
592
- price: order.price,
593
- marketId: order.marketId,
594
- };
595
- },
596
- );
597
-
598
645
  // Process positions
599
646
  this.positions[accountData.accountId] = accountData.positions;
600
647
 
@@ -610,7 +657,6 @@ export class NordUser {
610
657
  * @throws {NordError} If the operation fails
611
658
  */
612
659
  async refreshSession(): Promise<void> {
613
- console.log(this.publicKey);
614
660
  this.sessionId = await createSession(
615
661
  this.nord.webServerUrl,
616
662
  this.walletSignFn,
@@ -666,10 +712,13 @@ export class NordUser {
666
712
  * @param amount - Amount to withdraw
667
713
  * @throws {NordError} If the operation fails
668
714
  */
669
- async withdraw(
670
- tokenId: number,
671
- amount: number,
672
- ): Promise<{ actionId: bigint }> {
715
+ async withdraw({
716
+ amount,
717
+ tokenId,
718
+ }: Readonly<{
719
+ tokenId: number;
720
+ amount: number;
721
+ }>): Promise<{ actionId: bigint }> {
673
722
  try {
674
723
  this.checkSessionValidity();
675
724
  const { actionId } = await withdraw(
@@ -796,6 +845,78 @@ export class NordUser {
796
845
  }
797
846
  }
798
847
 
848
+ /**
849
+ * Execute up to four place/cancel operations atomically.
850
+ * Per Market:
851
+ * 1. cancels can only be in the start (one cannot predict future order ids)
852
+ * 2. intermediate trades can trade only
853
+ * 3. placements go last
854
+ *
855
+ * Across Markets, order action can be any
856
+ *
857
+ * @param userActions array of user-friendly subactions
858
+ * @param providedAccountId optional account performing the action (defaults to first account)
859
+ */
860
+ async atomic(
861
+ userActions: UserAtomicSubaction[],
862
+ providedAccountId?: number,
863
+ ): Promise<proto.Receipt_AtomicResult> {
864
+ try {
865
+ this.checkSessionValidity();
866
+
867
+ const accountId =
868
+ providedAccountId != null ? providedAccountId : this.accountIds?.[0];
869
+
870
+ if (accountId == null) {
871
+ throw new NordError(
872
+ "Account ID is undefined. Make sure to call updateAccountId() before atomic operations.",
873
+ );
874
+ }
875
+
876
+ const apiActions: ApiAtomicSubaction[] = userActions.map((act) => {
877
+ if (act.kind === "place") {
878
+ const market = findMarket(this.nord.markets, act.marketId!);
879
+ if (!market) {
880
+ throw new NordError(`Market ${act.marketId} not found`);
881
+ }
882
+ return {
883
+ kind: "place",
884
+ marketId: act.marketId,
885
+ side: act.side,
886
+ fillMode: act.fillMode,
887
+ isReduceOnly: act.isReduceOnly,
888
+ sizeDecimals: market.sizeDecimals,
889
+ priceDecimals: market.priceDecimals,
890
+ size: act.size,
891
+ price: act.price,
892
+ quoteSizeSize: act.quoteSize, // treated as quote size; we pass only size component
893
+ quoteSizePrice: undefined,
894
+ clientOrderId: act.clientOrderId,
895
+ } as ApiAtomicSubaction;
896
+ }
897
+ return {
898
+ kind: "cancel",
899
+ orderId: act.orderId,
900
+ } as ApiAtomicSubaction;
901
+ });
902
+
903
+ const result = await atomicAction(
904
+ this.nord.webServerUrl,
905
+ this.sessionSignFn,
906
+ await this.nord.getTimestamp(),
907
+ this.getNonce(),
908
+ {
909
+ sessionId: optExpect(this.sessionId, "No session"),
910
+ accountId: accountId,
911
+ actions: apiActions,
912
+ },
913
+ );
914
+ return result;
915
+ } catch (error) {
916
+ throw new NordError("Atomic operation failed", { cause: error });
917
+ }
918
+ }
919
+
799
920
  /**
800
921
  * Helper function to retry a promise with exponential backoff
801
922
  *