@limitless-exchange/sdk 0.0.1

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.
package/dist/index.mjs ADDED
@@ -0,0 +1,2485 @@
1
+ // src/types/logger.ts
2
+ var NoOpLogger = class {
3
+ debug() {
4
+ }
5
+ info() {
6
+ }
7
+ warn() {
8
+ }
9
+ error() {
10
+ }
11
+ };
12
+ var ConsoleLogger = class {
13
+ constructor(level = "info") {
14
+ this.level = level;
15
+ }
16
+ shouldLog(messageLevel) {
17
+ const levels = ["debug", "info", "warn", "error"];
18
+ return levels.indexOf(messageLevel) >= levels.indexOf(this.level);
19
+ }
20
+ debug(message, meta) {
21
+ if (this.shouldLog("debug")) {
22
+ console.debug("[Limitless SDK]", message, meta || "");
23
+ }
24
+ }
25
+ info(message, meta) {
26
+ if (this.shouldLog("info")) {
27
+ console.info("[Limitless SDK]", message, meta || "");
28
+ }
29
+ }
30
+ warn(message, meta) {
31
+ if (this.shouldLog("warn")) {
32
+ console.warn("[Limitless SDK]", message, meta || "");
33
+ }
34
+ }
35
+ error(message, error, meta) {
36
+ if (this.shouldLog("error")) {
37
+ const errorMsg = error ? error.message : "";
38
+ console.error("[Limitless SDK]", message, errorMsg ? `- ${errorMsg}` : "", meta || "");
39
+ }
40
+ }
41
+ };
42
+
43
+ // src/types/orders.ts
44
+ var Side = /* @__PURE__ */ ((Side2) => {
45
+ Side2[Side2["BUY"] = 0] = "BUY";
46
+ Side2[Side2["SELL"] = 1] = "SELL";
47
+ return Side2;
48
+ })(Side || {});
49
+ var OrderType = /* @__PURE__ */ ((OrderType3) => {
50
+ OrderType3["FOK"] = "FOK";
51
+ OrderType3["GTC"] = "GTC";
52
+ return OrderType3;
53
+ })(OrderType || {});
54
+ var MarketType = /* @__PURE__ */ ((MarketType3) => {
55
+ MarketType3["CLOB"] = "CLOB";
56
+ MarketType3["NEGRISK"] = "NEGRISK";
57
+ return MarketType3;
58
+ })(MarketType || {});
59
+ var SignatureType = /* @__PURE__ */ ((SignatureType2) => {
60
+ SignatureType2[SignatureType2["EOA"] = 0] = "EOA";
61
+ SignatureType2[SignatureType2["POLY_PROXY"] = 1] = "POLY_PROXY";
62
+ SignatureType2[SignatureType2["POLY_GNOSIS_SAFE"] = 2] = "POLY_GNOSIS_SAFE";
63
+ return SignatureType2;
64
+ })(SignatureType || {});
65
+
66
+ // src/types/websocket.ts
67
+ var WebSocketState = /* @__PURE__ */ ((WebSocketState2) => {
68
+ WebSocketState2["DISCONNECTED"] = "disconnected";
69
+ WebSocketState2["CONNECTING"] = "connecting";
70
+ WebSocketState2["CONNECTED"] = "connected";
71
+ WebSocketState2["RECONNECTING"] = "reconnecting";
72
+ WebSocketState2["ERROR"] = "error";
73
+ return WebSocketState2;
74
+ })(WebSocketState || {});
75
+
76
+ // src/auth/signer.ts
77
+ import { ethers } from "ethers";
78
+ var MessageSigner = class {
79
+ /**
80
+ * Creates a new message signer instance.
81
+ *
82
+ * @param wallet - Ethers wallet instance for signing
83
+ */
84
+ constructor(wallet) {
85
+ this.wallet = wallet;
86
+ }
87
+ /**
88
+ * Creates authentication headers for API requests.
89
+ *
90
+ * @param signingMessage - Message to sign from the API
91
+ * @returns Promise resolving to signature headers
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * const signer = new MessageSigner(wallet);
96
+ * const headers = await signer.createAuthHeaders(message);
97
+ * ```
98
+ */
99
+ async createAuthHeaders(signingMessage) {
100
+ const hexMessage = this.stringToHex(signingMessage);
101
+ const signature = await this.wallet.signMessage(signingMessage);
102
+ const address = this.wallet.address;
103
+ const recoveredAddress = ethers.verifyMessage(signingMessage, signature);
104
+ if (address.toLowerCase() !== recoveredAddress.toLowerCase()) {
105
+ throw new Error("Signature verification failed: address mismatch");
106
+ }
107
+ return {
108
+ "x-account": address,
109
+ "x-signing-message": hexMessage,
110
+ "x-signature": signature
111
+ };
112
+ }
113
+ /**
114
+ * Signs EIP-712 typed data.
115
+ *
116
+ * @param domain - EIP-712 domain
117
+ * @param types - EIP-712 types
118
+ * @param value - Value to sign
119
+ * @returns Promise resolving to signature string
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * const signature = await signer.signTypedData(domain, types, order);
124
+ * ```
125
+ */
126
+ async signTypedData(domain, types, value) {
127
+ return await this.wallet.signTypedData(domain, types, value);
128
+ }
129
+ /**
130
+ * Gets the wallet address.
131
+ *
132
+ * @returns Ethereum address
133
+ */
134
+ getAddress() {
135
+ return this.wallet.address;
136
+ }
137
+ /**
138
+ * Converts a string to hex format.
139
+ *
140
+ * @param text - String to convert
141
+ * @returns Hex string with 0x prefix
142
+ * @internal
143
+ */
144
+ stringToHex(text) {
145
+ return ethers.hexlify(ethers.toUtf8Bytes(text));
146
+ }
147
+ };
148
+
149
+ // src/auth/authenticator.ts
150
+ var Authenticator = class {
151
+ /**
152
+ * Creates a new authenticator instance.
153
+ *
154
+ * @param httpClient - HTTP client for API requests
155
+ * @param signer - Message signer for wallet operations
156
+ * @param logger - Optional logger for debugging and monitoring (default: no logging)
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * // Without logging (default)
161
+ * const authenticator = new Authenticator(httpClient, signer);
162
+ *
163
+ * // With logging
164
+ * import { ConsoleLogger } from '@limitless-exchange/sdk';
165
+ * const logger = new ConsoleLogger('debug');
166
+ * const authenticator = new Authenticator(httpClient, signer, logger);
167
+ * ```
168
+ */
169
+ constructor(httpClient, signer, logger) {
170
+ this.httpClient = httpClient;
171
+ this.signer = signer;
172
+ this.logger = logger || new NoOpLogger();
173
+ }
174
+ /**
175
+ * Gets a signing message from the API.
176
+ *
177
+ * @returns Promise resolving to signing message string
178
+ * @throws Error if API request fails
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * const message = await authenticator.getSigningMessage();
183
+ * console.log(message);
184
+ * ```
185
+ */
186
+ async getSigningMessage() {
187
+ this.logger.debug("Requesting signing message from API");
188
+ const message = await this.httpClient.get("/auth/signing-message");
189
+ this.logger.debug("Received signing message", { length: message.length });
190
+ return message;
191
+ }
192
+ /**
193
+ * Authenticates with the API and obtains session cookie.
194
+ *
195
+ * @param options - Login options including client type and smart wallet
196
+ * @returns Promise resolving to authentication result
197
+ * @throws Error if authentication fails or smart wallet is required but not provided
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * // EOA authentication
202
+ * const result = await authenticator.authenticate({ client: 'eoa' });
203
+ *
204
+ * // ETHERSPOT with smart wallet
205
+ * const result = await authenticator.authenticate({
206
+ * client: 'etherspot',
207
+ * smartWallet: '0x...'
208
+ * });
209
+ * ```
210
+ */
211
+ async authenticate(options = {}) {
212
+ const client = options.client || "eoa";
213
+ this.logger.info("Starting authentication", {
214
+ client,
215
+ hasSmartWallet: !!options.smartWallet
216
+ });
217
+ if (client === "etherspot" && !options.smartWallet) {
218
+ this.logger.error("Smart wallet address required for ETHERSPOT client");
219
+ throw new Error("Smart wallet address is required for ETHERSPOT client");
220
+ }
221
+ try {
222
+ const signingMessage = await this.getSigningMessage();
223
+ this.logger.debug("Creating signature headers");
224
+ const headers = await this.signer.createAuthHeaders(signingMessage);
225
+ this.logger.debug("Sending authentication request", { client });
226
+ const response = await this.httpClient.postWithResponse(
227
+ "/auth/login",
228
+ { client, smartWallet: options.smartWallet },
229
+ {
230
+ headers,
231
+ validateStatus: (status) => status < 500
232
+ }
233
+ );
234
+ this.logger.debug("Extracting session cookie from response");
235
+ const cookies = this.httpClient.extractCookies(response);
236
+ const sessionCookie = cookies["limitless_session"];
237
+ if (!sessionCookie) {
238
+ this.logger.error("Session cookie not found in response headers");
239
+ throw new Error("Failed to obtain session cookie from response");
240
+ }
241
+ this.httpClient.setSessionCookie(sessionCookie);
242
+ this.logger.info("Authentication successful", {
243
+ account: response.data.account,
244
+ client: response.data.client
245
+ });
246
+ return {
247
+ sessionCookie,
248
+ profile: response.data
249
+ };
250
+ } catch (error) {
251
+ this.logger.error("Authentication failed", error, {
252
+ client
253
+ });
254
+ throw error;
255
+ }
256
+ }
257
+ /**
258
+ * Verifies the current authentication status.
259
+ *
260
+ * @param sessionCookie - Session cookie to verify
261
+ * @returns Promise resolving to user's Ethereum address
262
+ * @throws Error if session is invalid
263
+ *
264
+ * @example
265
+ * ```typescript
266
+ * const address = await authenticator.verifyAuth(sessionCookie);
267
+ * console.log(`Authenticated as: ${address}`);
268
+ * ```
269
+ */
270
+ async verifyAuth(sessionCookie) {
271
+ this.logger.debug("Verifying authentication session");
272
+ const originalCookie = this.httpClient["sessionCookie"];
273
+ this.httpClient.setSessionCookie(sessionCookie);
274
+ try {
275
+ const address = await this.httpClient.get("/auth/verify-auth");
276
+ this.logger.info("Session verified", { address });
277
+ return address;
278
+ } catch (error) {
279
+ this.logger.error("Session verification failed", error);
280
+ throw error;
281
+ } finally {
282
+ if (originalCookie) {
283
+ this.httpClient.setSessionCookie(originalCookie);
284
+ } else {
285
+ this.httpClient.clearSessionCookie();
286
+ }
287
+ }
288
+ }
289
+ /**
290
+ * Logs out and clears the session.
291
+ *
292
+ * @param sessionCookie - Session cookie to invalidate
293
+ * @throws Error if logout request fails
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * await authenticator.logout(sessionCookie);
298
+ * console.log('Logged out successfully');
299
+ * ```
300
+ */
301
+ async logout(sessionCookie) {
302
+ this.logger.debug("Logging out session");
303
+ const originalCookie = this.httpClient["sessionCookie"];
304
+ this.httpClient.setSessionCookie(sessionCookie);
305
+ try {
306
+ await this.httpClient.post("/auth/logout", {});
307
+ this.logger.info("Logout successful");
308
+ } catch (error) {
309
+ this.logger.error("Logout failed", error);
310
+ throw error;
311
+ } finally {
312
+ if (originalCookie) {
313
+ this.httpClient.setSessionCookie(originalCookie);
314
+ } else {
315
+ this.httpClient.clearSessionCookie();
316
+ }
317
+ }
318
+ }
319
+ };
320
+
321
+ // src/api/errors.ts
322
+ var APIError = class _APIError extends Error {
323
+ /**
324
+ * Creates a new API error.
325
+ *
326
+ * @param message - Human-readable error message
327
+ * @param status - HTTP status code
328
+ * @param data - Raw API response data
329
+ * @param url - Request URL
330
+ * @param method - Request method
331
+ */
332
+ constructor(message, status, data, url, method) {
333
+ super(message);
334
+ this.name = "APIError";
335
+ this.status = status;
336
+ this.data = data;
337
+ this.url = url;
338
+ this.method = method;
339
+ if (Error.captureStackTrace) {
340
+ Error.captureStackTrace(this, _APIError);
341
+ }
342
+ }
343
+ /**
344
+ * Checks if this error is an authentication/authorization error.
345
+ *
346
+ * @returns True if the error is due to expired or invalid authentication
347
+ *
348
+ * @example
349
+ * ```typescript
350
+ * try {
351
+ * await portfolioFetcher.getPositions();
352
+ * } catch (error) {
353
+ * if (error instanceof APIError && error.isAuthError()) {
354
+ * // Re-authenticate and retry
355
+ * await authenticator.authenticate({ client: 'eoa' });
356
+ * await portfolioFetcher.getPositions();
357
+ * }
358
+ * }
359
+ * ```
360
+ */
361
+ isAuthError() {
362
+ return this.status === 401 || this.status === 403;
363
+ }
364
+ };
365
+
366
+ // src/auth/authenticated-client.ts
367
+ var AuthenticatedClient = class {
368
+ /**
369
+ * Creates a new authenticated client with auto-retry capability.
370
+ *
371
+ * @param config - Configuration for authenticated client
372
+ */
373
+ constructor(config) {
374
+ this.httpClient = config.httpClient;
375
+ this.authenticator = config.authenticator;
376
+ this.client = config.client;
377
+ this.smartWallet = config.smartWallet;
378
+ this.logger = config.logger || new NoOpLogger();
379
+ this.maxRetries = config.maxRetries ?? 1;
380
+ }
381
+ /**
382
+ * Executes a function with automatic retry on authentication errors.
383
+ *
384
+ * @param fn - Function to execute with auth retry
385
+ * @returns Promise resolving to the function result
386
+ * @throws Error if max retries exceeded or non-auth error occurs
387
+ *
388
+ * @example
389
+ * ```typescript
390
+ * // Automatic retry on 401/403
391
+ * const positions = await authClient.withRetry(() =>
392
+ * portfolioFetcher.getPositions()
393
+ * );
394
+ *
395
+ * // Works with any async operation
396
+ * const order = await authClient.withRetry(() =>
397
+ * orderClient.createOrder({ ... })
398
+ * );
399
+ * ```
400
+ */
401
+ async withRetry(fn) {
402
+ let attempts = 0;
403
+ while (attempts <= this.maxRetries) {
404
+ try {
405
+ return await fn();
406
+ } catch (error) {
407
+ if (error instanceof APIError && error.isAuthError() && attempts < this.maxRetries) {
408
+ this.logger.info("Authentication expired, re-authenticating...", {
409
+ attempt: attempts + 1,
410
+ maxRetries: this.maxRetries
411
+ });
412
+ await this.reauthenticate();
413
+ attempts++;
414
+ continue;
415
+ }
416
+ throw error;
417
+ }
418
+ }
419
+ throw new Error("Unexpected error: exceeded max retries");
420
+ }
421
+ /**
422
+ * Re-authenticates with the API.
423
+ *
424
+ * @internal
425
+ */
426
+ async reauthenticate() {
427
+ this.logger.debug("Re-authenticating with API");
428
+ await this.authenticator.authenticate({
429
+ client: this.client,
430
+ smartWallet: this.smartWallet
431
+ });
432
+ this.logger.info("Re-authentication successful");
433
+ }
434
+ };
435
+
436
+ // src/api/http.ts
437
+ import axios from "axios";
438
+
439
+ // src/utils/constants.ts
440
+ var DEFAULT_API_URL = "https://api.limitless.exchange";
441
+ var DEFAULT_WS_URL = "wss://ws.limitless.exchange";
442
+ var DEFAULT_CHAIN_ID = 8453;
443
+ var BASE_SEPOLIA_CHAIN_ID = 84532;
444
+ var SIGNING_MESSAGE_TEMPLATE = "Welcome to Limitless.exchange! Please sign this message to verify your identity.\n\nNonce: {NONCE}";
445
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
446
+ var CONTRACT_ADDRESSES = {
447
+ // Base mainnet (chainId: 8453)
448
+ 8453: {
449
+ CLOB: "0xa4409D988CA2218d956BeEFD3874100F444f0DC3",
450
+ NEGRISK: "0x5a38afc17F7E97ad8d6C547ddb837E40B4aEDfC6"
451
+ },
452
+ // Base Sepolia testnet (chainId: 84532)
453
+ 84532: {
454
+ CLOB: "0x...",
455
+ // Add testnet addresses when available
456
+ NEGRISK: "0x..."
457
+ }
458
+ };
459
+ function getContractAddress(marketType, chainId = DEFAULT_CHAIN_ID) {
460
+ const addresses = CONTRACT_ADDRESSES[chainId];
461
+ if (!addresses) {
462
+ throw new Error(
463
+ `No contract addresses configured for chainId ${chainId}. Supported chains: ${Object.keys(CONTRACT_ADDRESSES).join(", ")}`
464
+ );
465
+ }
466
+ return addresses[marketType];
467
+ }
468
+
469
+ // src/api/http.ts
470
+ var HttpClient = class {
471
+ /**
472
+ * Creates a new HTTP client instance.
473
+ *
474
+ * @param config - Configuration options for the HTTP client
475
+ */
476
+ constructor(config = {}) {
477
+ this.sessionCookie = config.sessionCookie;
478
+ this.logger = config.logger || new NoOpLogger();
479
+ this.client = axios.create({
480
+ baseURL: config.baseURL || DEFAULT_API_URL,
481
+ timeout: config.timeout || 3e4,
482
+ headers: {
483
+ "Content-Type": "application/json",
484
+ Accept: "application/json"
485
+ }
486
+ });
487
+ this.setupInterceptors();
488
+ }
489
+ /**
490
+ * Sets up request and response interceptors.
491
+ * @internal
492
+ */
493
+ setupInterceptors() {
494
+ this.client.interceptors.request.use(
495
+ (config) => {
496
+ if (this.sessionCookie) {
497
+ config.headers["Cookie"] = `limitless_session=${this.sessionCookie}`;
498
+ }
499
+ const fullUrl = `${config.baseURL || ""}${config.url || ""}`;
500
+ const method = config.method?.toUpperCase() || "GET";
501
+ this.logger.debug(`\u2192 ${method} ${fullUrl}`, {
502
+ headers: config.headers,
503
+ body: config.data
504
+ });
505
+ return config;
506
+ },
507
+ (error) => Promise.reject(error)
508
+ );
509
+ this.client.interceptors.response.use(
510
+ (response) => {
511
+ const method = response.config.method?.toUpperCase() || "GET";
512
+ const url = response.config.url || "";
513
+ this.logger.debug(`\u2713 ${response.status} ${method} ${url}`, {
514
+ data: response.data
515
+ });
516
+ return response;
517
+ },
518
+ (error) => {
519
+ if (error.response) {
520
+ const status = error.response.status;
521
+ const data = error.response.data;
522
+ const url = error.config?.url;
523
+ const method = error.config?.method?.toUpperCase();
524
+ let message = error.message;
525
+ if (data) {
526
+ this.logger.debug(`\u2717 ${status} ${method} ${url}`, {
527
+ error: data
528
+ });
529
+ if (typeof data === "object") {
530
+ if (Array.isArray(data.message)) {
531
+ const messages = data.message.map((err) => {
532
+ const details = Object.entries(err).filter(([_key, val]) => val !== "" && val !== null && val !== void 0).map(([key, val]) => `${key}: ${val}`).join(", ");
533
+ return details || JSON.stringify(err);
534
+ }).filter((msg) => msg.trim() !== "").join(" | ");
535
+ message = messages || data.error || JSON.stringify(data);
536
+ } else {
537
+ message = data.message || data.error || data.msg || data.errors && JSON.stringify(data.errors) || JSON.stringify(data);
538
+ }
539
+ } else {
540
+ message = String(data);
541
+ }
542
+ }
543
+ throw new APIError(message, status, data, url, method);
544
+ } else if (error.request) {
545
+ throw new Error("No response received from API");
546
+ } else {
547
+ throw new Error(`Request failed: ${error.message}`);
548
+ }
549
+ }
550
+ );
551
+ }
552
+ /**
553
+ * Sets the session cookie for authenticated requests.
554
+ *
555
+ * @param cookie - Session cookie value
556
+ */
557
+ setSessionCookie(cookie) {
558
+ this.sessionCookie = cookie;
559
+ }
560
+ /**
561
+ * Clears the session cookie.
562
+ */
563
+ clearSessionCookie() {
564
+ this.sessionCookie = void 0;
565
+ }
566
+ /**
567
+ * Performs a GET request.
568
+ *
569
+ * @param url - Request URL
570
+ * @param config - Additional request configuration
571
+ * @returns Promise resolving to the response data
572
+ */
573
+ async get(url, config) {
574
+ const response = await this.client.get(url, config);
575
+ return response.data;
576
+ }
577
+ /**
578
+ * Performs a POST request.
579
+ *
580
+ * @param url - Request URL
581
+ * @param data - Request body data
582
+ * @param config - Additional request configuration
583
+ * @returns Promise resolving to the response data
584
+ */
585
+ async post(url, data, config) {
586
+ const response = await this.client.post(url, data, config);
587
+ return response.data;
588
+ }
589
+ /**
590
+ * Performs a POST request and returns the full response object.
591
+ * Useful when you need access to response headers (e.g., for cookie extraction).
592
+ *
593
+ * @param url - Request URL
594
+ * @param data - Request body data
595
+ * @param config - Additional request configuration
596
+ * @returns Promise resolving to the full AxiosResponse object
597
+ * @internal
598
+ */
599
+ async postWithResponse(url, data, config) {
600
+ return await this.client.post(url, data, config);
601
+ }
602
+ /**
603
+ * Performs a DELETE request.
604
+ *
605
+ * @remarks
606
+ * DELETE requests typically don't have a body, so we remove the Content-Type header
607
+ * to avoid "Body cannot be empty" errors from the API.
608
+ *
609
+ * @param url - Request URL
610
+ * @param config - Additional request configuration
611
+ * @returns Promise resolving to the response data
612
+ */
613
+ async delete(url, config) {
614
+ const deleteConfig = {
615
+ ...config,
616
+ headers: {
617
+ ...config?.headers,
618
+ "Content-Type": void 0
619
+ }
620
+ };
621
+ const response = await this.client.delete(url, deleteConfig);
622
+ return response.data;
623
+ }
624
+ /**
625
+ * Extracts cookies from response headers.
626
+ *
627
+ * @param response - Axios response object
628
+ * @returns Object containing parsed cookies
629
+ * @internal
630
+ */
631
+ extractCookies(response) {
632
+ const setCookie = response.headers["set-cookie"];
633
+ if (!setCookie) return {};
634
+ const cookies = {};
635
+ const cookieStrings = Array.isArray(setCookie) ? setCookie : [setCookie];
636
+ for (const cookieString of cookieStrings) {
637
+ const parts = cookieString.split(";")[0].split("=");
638
+ if (parts.length === 2) {
639
+ cookies[parts[0].trim()] = parts[1].trim();
640
+ }
641
+ }
642
+ return cookies;
643
+ }
644
+ };
645
+
646
+ // src/api/retry.ts
647
+ var RetryConfig = class {
648
+ /**
649
+ * Creates a new retry configuration.
650
+ *
651
+ * @param options - Configuration options
652
+ */
653
+ constructor(options = {}) {
654
+ this.statusCodes = new Set(options.statusCodes || [429, 500, 502, 503, 504]);
655
+ this.maxRetries = options.maxRetries ?? 3;
656
+ this.delays = options.delays;
657
+ this.exponentialBase = options.exponentialBase ?? 2;
658
+ this.maxDelay = options.maxDelay ?? 60;
659
+ this.onRetry = options.onRetry;
660
+ }
661
+ /**
662
+ * Calculates delay for a given retry attempt.
663
+ *
664
+ * @param attempt - Retry attempt number (0-based)
665
+ * @returns Delay in seconds
666
+ */
667
+ getDelay(attempt) {
668
+ if (this.delays) {
669
+ return this.delays[Math.min(attempt, this.delays.length - 1)];
670
+ } else {
671
+ return Math.min(Math.pow(this.exponentialBase, attempt), this.maxDelay);
672
+ }
673
+ }
674
+ };
675
+ function sleep(seconds) {
676
+ return new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
677
+ }
678
+ function retryOnErrors(options = {}) {
679
+ const config = new RetryConfig(options);
680
+ return function(_target, _propertyKey, descriptor) {
681
+ const originalMethod = descriptor.value;
682
+ descriptor.value = async function(...args) {
683
+ let lastError;
684
+ try {
685
+ return await originalMethod.apply(this, args);
686
+ } catch (error) {
687
+ if (error instanceof APIError && config.statusCodes.has(error.status)) {
688
+ lastError = error;
689
+ } else {
690
+ throw error;
691
+ }
692
+ }
693
+ for (let attempt = 0; attempt < config.maxRetries; attempt++) {
694
+ try {
695
+ const delay = config.getDelay(attempt);
696
+ if (config.onRetry && lastError) {
697
+ config.onRetry(attempt, lastError, delay);
698
+ }
699
+ await sleep(delay);
700
+ return await originalMethod.apply(this, args);
701
+ } catch (error) {
702
+ if (error instanceof APIError && config.statusCodes.has(error.status)) {
703
+ lastError = error;
704
+ } else {
705
+ throw error;
706
+ }
707
+ }
708
+ }
709
+ throw lastError;
710
+ };
711
+ return descriptor;
712
+ };
713
+ }
714
+ async function withRetry(fn, options = {}, logger = new NoOpLogger()) {
715
+ const config = new RetryConfig(options);
716
+ let lastError;
717
+ try {
718
+ return await fn();
719
+ } catch (error) {
720
+ if (error instanceof APIError && config.statusCodes.has(error.status)) {
721
+ lastError = error;
722
+ logger.warn("API error, starting retries", { status: error.status });
723
+ } else {
724
+ throw error;
725
+ }
726
+ }
727
+ for (let attempt = 0; attempt < config.maxRetries; attempt++) {
728
+ try {
729
+ const delay = config.getDelay(attempt);
730
+ if (config.onRetry && lastError) {
731
+ config.onRetry(attempt, lastError, delay);
732
+ }
733
+ logger.info("Retrying operation", { attempt: attempt + 1, delay });
734
+ await sleep(delay);
735
+ return await fn();
736
+ } catch (error) {
737
+ if (error instanceof APIError && config.statusCodes.has(error.status)) {
738
+ lastError = error;
739
+ logger.warn("Retry failed", { attempt: attempt + 1, status: error.status });
740
+ } else {
741
+ throw error;
742
+ }
743
+ }
744
+ }
745
+ logger.error("All retries exhausted");
746
+ throw lastError;
747
+ }
748
+ var RetryableClient = class {
749
+ /**
750
+ * Creates a new retryable client wrapper.
751
+ *
752
+ * @param httpClient - HTTP client to wrap
753
+ * @param retryConfig - Retry configuration
754
+ * @param logger - Optional logger
755
+ */
756
+ constructor(httpClient, retryConfig = new RetryConfig(), logger = new NoOpLogger()) {
757
+ this.httpClient = httpClient;
758
+ this.retryConfig = retryConfig;
759
+ this.logger = logger;
760
+ }
761
+ /**
762
+ * Performs a GET request with retry logic.
763
+ *
764
+ * @param url - Request URL
765
+ * @param config - Additional request configuration
766
+ * @returns Promise resolving to the response data
767
+ */
768
+ async get(url, config) {
769
+ return withRetry(
770
+ async () => this.httpClient.get(url, config),
771
+ {
772
+ statusCodes: Array.from(this.retryConfig.statusCodes),
773
+ maxRetries: this.retryConfig.maxRetries,
774
+ delays: this.retryConfig.delays,
775
+ exponentialBase: this.retryConfig.exponentialBase,
776
+ maxDelay: this.retryConfig.maxDelay,
777
+ onRetry: this.retryConfig.onRetry
778
+ },
779
+ this.logger
780
+ );
781
+ }
782
+ /**
783
+ * Performs a POST request with retry logic.
784
+ *
785
+ * @param url - Request URL
786
+ * @param data - Request body data
787
+ * @param config - Additional request configuration
788
+ * @returns Promise resolving to the response data
789
+ */
790
+ async post(url, data, config) {
791
+ return withRetry(
792
+ async () => this.httpClient.post(url, data, config),
793
+ {
794
+ statusCodes: Array.from(this.retryConfig.statusCodes),
795
+ maxRetries: this.retryConfig.maxRetries,
796
+ delays: this.retryConfig.delays,
797
+ exponentialBase: this.retryConfig.exponentialBase,
798
+ maxDelay: this.retryConfig.maxDelay,
799
+ onRetry: this.retryConfig.onRetry
800
+ },
801
+ this.logger
802
+ );
803
+ }
804
+ /**
805
+ * Performs a DELETE request with retry logic.
806
+ *
807
+ * @param url - Request URL
808
+ * @param config - Additional request configuration
809
+ * @returns Promise resolving to the response data
810
+ */
811
+ async delete(url, config) {
812
+ return withRetry(
813
+ async () => this.httpClient.delete(url, config),
814
+ {
815
+ statusCodes: Array.from(this.retryConfig.statusCodes),
816
+ maxRetries: this.retryConfig.maxRetries,
817
+ delays: this.retryConfig.delays,
818
+ exponentialBase: this.retryConfig.exponentialBase,
819
+ maxDelay: this.retryConfig.maxDelay,
820
+ onRetry: this.retryConfig.onRetry
821
+ },
822
+ this.logger
823
+ );
824
+ }
825
+ };
826
+
827
+ // src/orders/builder.ts
828
+ import { ethers as ethers2 } from "ethers";
829
+ var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
830
+ var DEFAULT_PRICE_TICK = 1e-3;
831
+ var OrderBuilder = class {
832
+ /**
833
+ * Creates a new order builder instance.
834
+ *
835
+ * @param makerAddress - Ethereum address of the order maker
836
+ * @param feeRateBps - Fee rate in basis points (e.g., 100 = 1%)
837
+ * @param priceTick - Price tick size (default: 0.001 for 3 decimals)
838
+ *
839
+ * @example
840
+ * ```typescript
841
+ * const builder = new OrderBuilder(
842
+ * '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
843
+ * 300, // 3% fee
844
+ * 0.001 // 3 decimal price precision
845
+ * );
846
+ * ```
847
+ */
848
+ constructor(makerAddress, feeRateBps, priceTick = DEFAULT_PRICE_TICK) {
849
+ this.makerAddress = makerAddress;
850
+ this.feeRateBps = feeRateBps;
851
+ this.priceTick = priceTick;
852
+ }
853
+ /**
854
+ * Builds an unsigned order payload.
855
+ *
856
+ * @param args - Order arguments (FOK or GTC)
857
+ * @returns Unsigned order ready for signing
858
+ *
859
+ * @throws Error if validation fails or tick alignment fails
860
+ *
861
+ * @example
862
+ * ```typescript
863
+ * // FOK order (market order)
864
+ * const fokOrder = builder.buildOrder({
865
+ * tokenId: '123456',
866
+ * makerAmount: 50, // 50 USDC to spend
867
+ * side: Side.BUY,
868
+ * marketType: MarketType.CLOB
869
+ * });
870
+ *
871
+ * // GTC order (price + size)
872
+ * const gtcOrder = builder.buildOrder({
873
+ * tokenId: '123456',
874
+ * price: 0.38,
875
+ * size: 22.123, // Will be rounded to tick-aligned: 22.123 shares
876
+ * side: Side.BUY,
877
+ * marketType: MarketType.CLOB
878
+ * });
879
+ * ```
880
+ */
881
+ buildOrder(args) {
882
+ this.validateOrderArgs(args);
883
+ const { makerAmount, takerAmount, price } = this.isFOKOrder(args) ? this.calculateFOKAmounts(args.makerAmount) : this.calculateGTCAmountsTickAligned(args.price, args.size, args.side);
884
+ const order = {
885
+ salt: this.generateSalt(),
886
+ maker: this.makerAddress,
887
+ signer: this.makerAddress,
888
+ taker: args.taker || ZERO_ADDRESS2,
889
+ tokenId: args.tokenId,
890
+ makerAmount,
891
+ takerAmount,
892
+ expiration: args.expiration || "0",
893
+ nonce: args.nonce || 0,
894
+ feeRateBps: this.feeRateBps,
895
+ side: args.side,
896
+ signatureType: 0 /* EOA */
897
+ };
898
+ if (price !== void 0) {
899
+ order.price = price;
900
+ }
901
+ return order;
902
+ }
903
+ /**
904
+ * Type guard to check if order arguments are for FOK order.
905
+ *
906
+ * @param args - Order arguments
907
+ * @returns True if FOK order arguments
908
+ *
909
+ * @internal
910
+ */
911
+ isFOKOrder(args) {
912
+ return "amount" in args;
913
+ }
914
+ /**
915
+ * Generates a unique salt using timestamp + nano-offset pattern.
916
+ *
917
+ * @remarks
918
+ * This follows the reference implementation pattern:
919
+ * salt = timestamp * 1000 + nanoOffset + 24h
920
+ *
921
+ * This ensures uniqueness even when creating orders rapidly.
922
+ *
923
+ * @returns Unique salt value
924
+ *
925
+ * @internal
926
+ */
927
+ generateSalt() {
928
+ const timestamp = Date.now();
929
+ const nanoOffset = Math.floor(performance.now() * 1e3) % 1e6;
930
+ const oneDayMs = 1e3 * 60 * 60 * 24;
931
+ return timestamp * 1e3 + nanoOffset + oneDayMs;
932
+ }
933
+ /**
934
+ * Parses decimal string to scaled BigInt.
935
+ *
936
+ * @param value - Decimal string (e.g., "0.38")
937
+ * @param scale - Scale factor (e.g., 1_000_000n for 6 decimals)
938
+ * @returns Scaled BigInt value
939
+ *
940
+ * @internal
941
+ */
942
+ parseDecToInt(value, scale) {
943
+ const s = value.trim();
944
+ const [intPart, fracPart = ""] = s.split(".");
945
+ const decimals = scale.toString().length - 1;
946
+ const frac = (fracPart + "0".repeat(decimals)).slice(0, decimals);
947
+ const sign = intPart.startsWith("-") ? -1n : 1n;
948
+ const intPartAbs = intPart.replace("-", "");
949
+ return sign * (BigInt(intPartAbs || "0") * scale + BigInt(frac || "0"));
950
+ }
951
+ /**
952
+ * Ceiling division for BigInt.
953
+ *
954
+ * @param numerator - Numerator
955
+ * @param denominator - Denominator
956
+ * @returns Ceiling of numerator / denominator
957
+ *
958
+ * @internal
959
+ */
960
+ divCeil(numerator, denominator) {
961
+ if (denominator === 0n) {
962
+ throw new Error("Division by zero");
963
+ }
964
+ return (numerator + denominator - 1n) / denominator;
965
+ }
966
+ /**
967
+ * Calculates maker and taker amounts for GTC orders with tick alignment validation.
968
+ *
969
+ * @remarks
970
+ * Validates and calculates amounts to ensure:
971
+ * 1. Price aligns to tick size (e.g., 0.001 for 3 decimals)
972
+ * 2. Size produces takerAmount divisible by sharesStep
973
+ * 3. No auto-rounding - fails fast if values are invalid
974
+ * 4. Transparent error messages guide users to valid values
975
+ *
976
+ * **Algorithm**:
977
+ * - sharesStep = priceScale / tickInt (e.g., 1000 for 0.001 tick)
978
+ * - Validates shares are divisible by sharesStep
979
+ * - Calculates collateral from shares × price (ceil for BUY, floor for SELL)
980
+ * - Assigns maker/taker based on side:
981
+ * - BUY: maker = collateral, taker = shares
982
+ * - SELL: maker = shares, taker = collateral
983
+ * - Throws clear error if size is not tick-aligned
984
+ *
985
+ * @param price - Price per share (0.0 to 1.0, max 3 decimals)
986
+ * @param size - Number of shares (must be tick-aligned)
987
+ * @param side - Order side (BUY or SELL)
988
+ * @returns Object with validated makerAmount, takerAmount, and price
989
+ *
990
+ * @throws Error if price or size not tick-aligned
991
+ *
992
+ * @internal
993
+ */
994
+ calculateGTCAmountsTickAligned(price, size, side) {
995
+ const sharesScale = 1000000n;
996
+ const collateralScale = 1000000n;
997
+ const priceScale = 1000000n;
998
+ const shares = this.parseDecToInt(size.toString(), sharesScale);
999
+ const priceInt = this.parseDecToInt(price.toString(), priceScale);
1000
+ const tickInt = this.parseDecToInt(this.priceTick.toString(), priceScale);
1001
+ if (tickInt <= 0n) {
1002
+ throw new Error(`Invalid priceTick: ${this.priceTick}`);
1003
+ }
1004
+ if (priceInt <= 0n) {
1005
+ throw new Error(`Invalid price: ${price}`);
1006
+ }
1007
+ if (priceInt % tickInt !== 0n) {
1008
+ throw new Error(
1009
+ `Price ${price} is not tick-aligned. Must be multiple of ${this.priceTick} (e.g., 0.380, 0.381, etc.)`
1010
+ );
1011
+ }
1012
+ const sharesStep = priceScale / tickInt;
1013
+ if (shares % sharesStep !== 0n) {
1014
+ const validSizeDown = Number(shares / sharesStep * sharesStep) / 1e6;
1015
+ const validSizeUp = Number(this.divCeil(shares, sharesStep) * sharesStep) / 1e6;
1016
+ throw new Error(
1017
+ `Invalid size: ${size}. Size must produce contracts divisible by ${sharesStep} (sharesStep). Try ${validSizeDown} (rounded down) or ${validSizeUp} (rounded up) instead.`
1018
+ );
1019
+ }
1020
+ const numerator = shares * priceInt * collateralScale;
1021
+ const denominator = sharesScale * priceScale;
1022
+ const collateral = side === 0 /* BUY */ ? this.divCeil(numerator, denominator) : numerator / denominator;
1023
+ let makerAmount;
1024
+ let takerAmount;
1025
+ if (side === 0 /* BUY */) {
1026
+ makerAmount = collateral;
1027
+ takerAmount = shares;
1028
+ } else {
1029
+ makerAmount = shares;
1030
+ takerAmount = collateral;
1031
+ }
1032
+ return {
1033
+ makerAmount: Number(makerAmount),
1034
+ takerAmount: Number(takerAmount),
1035
+ price
1036
+ };
1037
+ }
1038
+ /**
1039
+ * Calculates maker and taker amounts for FOK (market) orders.
1040
+ *
1041
+ * @remarks
1042
+ * FOK orders use makerAmount for both BUY and SELL:
1043
+ * - BUY: makerAmount = USDC amount to spend (e.g., 50 = $50 USDC)
1044
+ * - SELL: makerAmount = number of shares to sell (e.g., 18.64 shares)
1045
+ *
1046
+ * takerAmount is always 1 (constant for FOK orders)
1047
+ *
1048
+ * @param makerAmount - Amount in human-readable format (max 6 decimals)
1049
+ * @returns Object with makerAmount (scaled), takerAmount (always 1), and undefined price
1050
+ *
1051
+ * @internal
1052
+ */
1053
+ calculateFOKAmounts(makerAmount) {
1054
+ const DECIMALS = 6;
1055
+ const amountStr = makerAmount.toString();
1056
+ const decimalIndex = amountStr.indexOf(".");
1057
+ if (decimalIndex !== -1) {
1058
+ const decimalPlaces = amountStr.length - decimalIndex - 1;
1059
+ if (decimalPlaces > DECIMALS) {
1060
+ throw new Error(
1061
+ `Invalid makerAmount: ${makerAmount}. Can have max ${DECIMALS} decimal places. Try ${makerAmount.toFixed(DECIMALS)} instead.`
1062
+ );
1063
+ }
1064
+ }
1065
+ const amountFormatted = makerAmount.toFixed(DECIMALS);
1066
+ const amountScaled = ethers2.parseUnits(amountFormatted, DECIMALS);
1067
+ const collateralAmount = Number(amountScaled);
1068
+ return {
1069
+ makerAmount: collateralAmount,
1070
+ takerAmount: 1,
1071
+ price: void 0
1072
+ };
1073
+ }
1074
+ /**
1075
+ * Validates order arguments.
1076
+ *
1077
+ * @param args - Order arguments to validate
1078
+ * @throws Error if validation fails
1079
+ *
1080
+ * @internal
1081
+ */
1082
+ validateOrderArgs(args) {
1083
+ if (!args.tokenId || args.tokenId === "0") {
1084
+ throw new Error("Invalid tokenId: tokenId is required.");
1085
+ }
1086
+ if (args.taker && !ethers2.isAddress(args.taker)) {
1087
+ throw new Error(`Invalid taker address: ${args.taker}`);
1088
+ }
1089
+ if (this.isFOKOrder(args)) {
1090
+ if (!args.makerAmount) {
1091
+ throw new Error("FOK orders require makerAmount");
1092
+ }
1093
+ if (args.makerAmount <= 0) {
1094
+ throw new Error(`Invalid makerAmount: ${args.makerAmount}. Maker amount must be positive.`);
1095
+ }
1096
+ } else {
1097
+ if (args.price < 0 || args.price > 1) {
1098
+ throw new Error(`Invalid price: ${args.price}. Price must be between 0 and 1.`);
1099
+ }
1100
+ if (args.size <= 0) {
1101
+ throw new Error(`Invalid size: ${args.size}. Size must be positive.`);
1102
+ }
1103
+ const priceStr = args.price.toString();
1104
+ const decimalIndex = priceStr.indexOf(".");
1105
+ if (decimalIndex !== -1) {
1106
+ const decimalPlaces = priceStr.length - decimalIndex - 1;
1107
+ if (decimalPlaces > 3) {
1108
+ throw new Error(
1109
+ `Invalid price: ${args.price}. Price must have max 3 decimal places (e.g., 0.380, 0.001).`
1110
+ );
1111
+ }
1112
+ }
1113
+ }
1114
+ }
1115
+ };
1116
+
1117
+ // src/orders/signer.ts
1118
+ var OrderSigner = class {
1119
+ /**
1120
+ * Creates a new order signer instance.
1121
+ *
1122
+ * @param wallet - Ethers wallet for signing
1123
+ * @param logger - Optional logger for debugging (default: no logging)
1124
+ *
1125
+ * @example
1126
+ * ```typescript
1127
+ * import { ethers } from 'ethers';
1128
+ * import { OrderSigner } from '@limitless-exchange/sdk';
1129
+ *
1130
+ * const wallet = new ethers.Wallet(privateKey);
1131
+ * const signer = new OrderSigner(wallet);
1132
+ * ```
1133
+ */
1134
+ constructor(wallet, logger) {
1135
+ this.wallet = wallet;
1136
+ this.logger = logger || new NoOpLogger();
1137
+ }
1138
+ /**
1139
+ * Signs an order with EIP-712.
1140
+ *
1141
+ * @param order - Unsigned order to sign
1142
+ * @param config - Signing configuration (chainId, contract address, market type)
1143
+ * @returns Promise resolving to EIP-712 signature
1144
+ *
1145
+ * @throws Error if wallet address doesn't match order signer
1146
+ * @throws Error if signing fails
1147
+ *
1148
+ * @example
1149
+ * ```typescript
1150
+ * const signature = await signer.signOrder(unsignedOrder, {
1151
+ * chainId: 8453,
1152
+ * contractAddress: '0x...',
1153
+ * marketType: MarketType.CLOB
1154
+ * });
1155
+ * ```
1156
+ */
1157
+ async signOrder(order, config) {
1158
+ this.logger.debug("Signing order with EIP-712", {
1159
+ tokenId: order.tokenId,
1160
+ side: order.side,
1161
+ marketType: config.marketType
1162
+ });
1163
+ const walletAddress = await this.wallet.getAddress();
1164
+ if (walletAddress.toLowerCase() !== order.signer.toLowerCase()) {
1165
+ const error = `Wallet address mismatch! Signing with: ${walletAddress}, but order signer is: ${order.signer}`;
1166
+ this.logger.error(error);
1167
+ throw new Error(error);
1168
+ }
1169
+ const domain = this.getDomain(config);
1170
+ this.logger.debug("EIP-712 Domain", domain);
1171
+ const types = this.getTypes();
1172
+ const orderValue = {
1173
+ salt: order.salt,
1174
+ maker: order.maker,
1175
+ signer: order.signer,
1176
+ taker: order.taker,
1177
+ tokenId: order.tokenId,
1178
+ makerAmount: order.makerAmount,
1179
+ takerAmount: order.takerAmount,
1180
+ expiration: order.expiration,
1181
+ nonce: order.nonce,
1182
+ feeRateBps: order.feeRateBps,
1183
+ side: order.side,
1184
+ signatureType: order.signatureType
1185
+ };
1186
+ this.logger.debug("EIP-712 Order Value", orderValue);
1187
+ console.log("[OrderSigner] Full signing payload:", JSON.stringify({
1188
+ domain,
1189
+ types: this.getTypes(),
1190
+ value: orderValue
1191
+ }, null, 2));
1192
+ try {
1193
+ const signature = await this.wallet.signTypedData(domain, types, orderValue);
1194
+ this.logger.info("Successfully generated EIP-712 signature", {
1195
+ signature: signature.slice(0, 10) + "..."
1196
+ });
1197
+ return signature;
1198
+ } catch (error) {
1199
+ this.logger.error("Failed to sign order", error);
1200
+ throw error;
1201
+ }
1202
+ }
1203
+ /**
1204
+ * Gets the EIP-712 domain for signing.
1205
+ *
1206
+ * @param config - Signing configuration
1207
+ * @returns EIP-712 domain object
1208
+ *
1209
+ * @internal
1210
+ */
1211
+ getDomain(config) {
1212
+ return {
1213
+ name: "Limitless CTF Exchange",
1214
+ version: "1",
1215
+ chainId: config.chainId,
1216
+ verifyingContract: config.contractAddress
1217
+ };
1218
+ }
1219
+ /**
1220
+ * Gets the EIP-712 type definitions.
1221
+ *
1222
+ * @remarks
1223
+ * This matches the order structure expected by the Limitless Exchange
1224
+ * smart contracts.
1225
+ *
1226
+ * @returns EIP-712 types definition
1227
+ *
1228
+ * @internal
1229
+ */
1230
+ getTypes() {
1231
+ return {
1232
+ Order: [
1233
+ { name: "salt", type: "uint256" },
1234
+ { name: "maker", type: "address" },
1235
+ { name: "signer", type: "address" },
1236
+ { name: "taker", type: "address" },
1237
+ { name: "tokenId", type: "uint256" },
1238
+ { name: "makerAmount", type: "uint256" },
1239
+ { name: "takerAmount", type: "uint256" },
1240
+ { name: "expiration", type: "uint256" },
1241
+ { name: "nonce", type: "uint256" },
1242
+ { name: "feeRateBps", type: "uint256" },
1243
+ { name: "side", type: "uint8" },
1244
+ { name: "signatureType", type: "uint8" }
1245
+ ]
1246
+ };
1247
+ }
1248
+ };
1249
+
1250
+ // src/orders/validator.ts
1251
+ import { ethers as ethers3 } from "ethers";
1252
+ var ValidationError = class extends Error {
1253
+ constructor(message) {
1254
+ super(message);
1255
+ this.name = "ValidationError";
1256
+ }
1257
+ };
1258
+ function isFOKOrder(args) {
1259
+ return "amount" in args;
1260
+ }
1261
+ function validateOrderArgs(args) {
1262
+ if (!args.tokenId) {
1263
+ throw new ValidationError("TokenId is required");
1264
+ }
1265
+ if (args.tokenId === "0") {
1266
+ throw new ValidationError("TokenId cannot be zero");
1267
+ }
1268
+ if (!/^\d+$/.test(args.tokenId)) {
1269
+ throw new ValidationError(`Invalid tokenId format: ${args.tokenId}`);
1270
+ }
1271
+ if (args.taker && !ethers3.isAddress(args.taker)) {
1272
+ throw new ValidationError(`Invalid taker address: ${args.taker}`);
1273
+ }
1274
+ if (args.expiration !== void 0) {
1275
+ if (!/^\d+$/.test(args.expiration)) {
1276
+ throw new ValidationError(`Invalid expiration format: ${args.expiration}`);
1277
+ }
1278
+ }
1279
+ if (args.nonce !== void 0) {
1280
+ if (!Number.isInteger(args.nonce) || args.nonce < 0) {
1281
+ throw new ValidationError(`Invalid nonce: ${args.nonce}`);
1282
+ }
1283
+ }
1284
+ if (isFOKOrder(args)) {
1285
+ if (typeof args.makerAmount !== "number" || isNaN(args.makerAmount)) {
1286
+ throw new ValidationError("Amount must be a valid number");
1287
+ }
1288
+ if (args.makerAmount <= 0) {
1289
+ throw new ValidationError(`Amount must be positive, got: ${args.makerAmount}`);
1290
+ }
1291
+ const amountStr = args.makerAmount.toString();
1292
+ const decimalIndex = amountStr.indexOf(".");
1293
+ if (decimalIndex !== -1) {
1294
+ const decimalPlaces = amountStr.length - decimalIndex - 1;
1295
+ if (decimalPlaces > 2) {
1296
+ throw new ValidationError(
1297
+ `Amount must have max 2 decimal places, got: ${args.makerAmount} (${decimalPlaces} decimals)`
1298
+ );
1299
+ }
1300
+ }
1301
+ } else {
1302
+ if (typeof args.price !== "number" || isNaN(args.price)) {
1303
+ throw new ValidationError("Price must be a valid number");
1304
+ }
1305
+ if (args.price < 0 || args.price > 1) {
1306
+ throw new ValidationError(`Price must be between 0 and 1, got: ${args.price}`);
1307
+ }
1308
+ if (typeof args.size !== "number" || isNaN(args.size)) {
1309
+ throw new ValidationError("Size must be a valid number");
1310
+ }
1311
+ if (args.size <= 0) {
1312
+ throw new ValidationError(`Size must be positive, got: ${args.size}`);
1313
+ }
1314
+ }
1315
+ }
1316
+ function validateUnsignedOrder(order) {
1317
+ if (!ethers3.isAddress(order.maker)) {
1318
+ throw new ValidationError(`Invalid maker address: ${order.maker}`);
1319
+ }
1320
+ if (!ethers3.isAddress(order.signer)) {
1321
+ throw new ValidationError(`Invalid signer address: ${order.signer}`);
1322
+ }
1323
+ if (!ethers3.isAddress(order.taker)) {
1324
+ throw new ValidationError(`Invalid taker address: ${order.taker}`);
1325
+ }
1326
+ if (!order.makerAmount || order.makerAmount === 0) {
1327
+ throw new ValidationError("MakerAmount must be greater than zero");
1328
+ }
1329
+ if (!order.takerAmount || order.takerAmount === 0) {
1330
+ throw new ValidationError("TakerAmount must be greater than zero");
1331
+ }
1332
+ if (typeof order.makerAmount !== "number" || order.makerAmount <= 0) {
1333
+ throw new ValidationError(`Invalid makerAmount: ${order.makerAmount}`);
1334
+ }
1335
+ if (typeof order.takerAmount !== "number" || order.takerAmount <= 0) {
1336
+ throw new ValidationError(`Invalid takerAmount: ${order.takerAmount}`);
1337
+ }
1338
+ if (!/^\d+$/.test(order.tokenId)) {
1339
+ throw new ValidationError(`Invalid tokenId format: ${order.tokenId}`);
1340
+ }
1341
+ if (!/^\d+$/.test(order.expiration)) {
1342
+ throw new ValidationError(`Invalid expiration format: ${order.expiration}`);
1343
+ }
1344
+ if (!Number.isInteger(order.salt) || order.salt <= 0) {
1345
+ throw new ValidationError(`Invalid salt: ${order.salt}`);
1346
+ }
1347
+ if (!Number.isInteger(order.nonce) || order.nonce < 0) {
1348
+ throw new ValidationError(`Invalid nonce: ${order.nonce}`);
1349
+ }
1350
+ if (!Number.isInteger(order.feeRateBps) || order.feeRateBps < 0) {
1351
+ throw new ValidationError(`Invalid feeRateBps: ${order.feeRateBps}`);
1352
+ }
1353
+ if (order.side !== 0 && order.side !== 1) {
1354
+ throw new ValidationError(`Invalid side: ${order.side}. Must be 0 (BUY) or 1 (SELL)`);
1355
+ }
1356
+ if (!Number.isInteger(order.signatureType) || order.signatureType < 0) {
1357
+ throw new ValidationError(`Invalid signatureType: ${order.signatureType}`);
1358
+ }
1359
+ if (order.price !== void 0) {
1360
+ if (typeof order.price !== "number" || isNaN(order.price)) {
1361
+ throw new ValidationError("Price must be a valid number");
1362
+ }
1363
+ if (order.price < 0 || order.price > 1) {
1364
+ throw new ValidationError(`Price must be between 0 and 1, got: ${order.price}`);
1365
+ }
1366
+ }
1367
+ }
1368
+ function validateSignedOrder(order) {
1369
+ validateUnsignedOrder(order);
1370
+ if (!order.signature) {
1371
+ throw new ValidationError("Signature is required");
1372
+ }
1373
+ if (!order.signature.startsWith("0x")) {
1374
+ throw new ValidationError("Signature must start with 0x");
1375
+ }
1376
+ if (order.signature.length !== 132) {
1377
+ throw new ValidationError(
1378
+ `Invalid signature length: ${order.signature.length}. Expected 132 characters.`
1379
+ );
1380
+ }
1381
+ if (!/^0x[0-9a-fA-F]{130}$/.test(order.signature)) {
1382
+ throw new ValidationError("Signature must be valid hex string");
1383
+ }
1384
+ }
1385
+
1386
+ // src/orders/client.ts
1387
+ var OrderClient = class {
1388
+ /**
1389
+ * Creates a new order client instance.
1390
+ *
1391
+ * @param config - Order client configuration
1392
+ *
1393
+ * @throws Error if neither marketType nor signingConfig is provided
1394
+ */
1395
+ constructor(config) {
1396
+ this.httpClient = config.httpClient;
1397
+ this.logger = config.logger || new NoOpLogger();
1398
+ this.ownerId = config.userData.userId;
1399
+ const feeRateBps = config.userData.feeRateBps;
1400
+ this.orderBuilder = new OrderBuilder(config.wallet.address, feeRateBps, 1e-3);
1401
+ this.orderSigner = new OrderSigner(config.wallet, this.logger);
1402
+ if (config.signingConfig) {
1403
+ this.signingConfig = config.signingConfig;
1404
+ } else if (config.marketType) {
1405
+ const chainId = parseInt(process.env.CHAIN_ID || "8453");
1406
+ const contractAddress = config.marketType === "NEGRISK" /* NEGRISK */ ? process.env.NEGRISK_CONTRACT_ADDRESS || getContractAddress("NEGRISK", chainId) : process.env.CLOB_CONTRACT_ADDRESS || getContractAddress("CLOB", chainId);
1407
+ this.signingConfig = {
1408
+ chainId,
1409
+ contractAddress,
1410
+ marketType: config.marketType
1411
+ };
1412
+ this.logger.info("Auto-configured signing", {
1413
+ chainId,
1414
+ contractAddress,
1415
+ marketType: config.marketType
1416
+ });
1417
+ } else {
1418
+ const chainId = parseInt(process.env.CHAIN_ID || "8453");
1419
+ const contractAddress = process.env.CLOB_CONTRACT_ADDRESS || getContractAddress("CLOB", chainId);
1420
+ this.signingConfig = {
1421
+ chainId,
1422
+ contractAddress,
1423
+ marketType: "CLOB" /* CLOB */
1424
+ };
1425
+ this.logger.debug("Using default CLOB configuration", {
1426
+ chainId,
1427
+ contractAddress
1428
+ });
1429
+ }
1430
+ }
1431
+ /**
1432
+ * Creates and submits a new order.
1433
+ *
1434
+ * @remarks
1435
+ * This method handles the complete order creation flow:
1436
+ * 1. Build unsigned order
1437
+ * 2. Sign with EIP-712
1438
+ * 3. Submit to API
1439
+ *
1440
+ * @param params - Order parameters
1441
+ * @returns Promise resolving to order response
1442
+ *
1443
+ * @throws Error if order creation fails
1444
+ *
1445
+ * @example
1446
+ * ```typescript
1447
+ * const order = await orderClient.createOrder({
1448
+ * tokenId: '123456',
1449
+ * price: 0.65,
1450
+ * size: 100,
1451
+ * side: Side.BUY,
1452
+ * orderType: OrderType.GTC,
1453
+ * marketSlug: 'market-slug'
1454
+ * });
1455
+ *
1456
+ * console.log(`Order created: ${order.order.id}`);
1457
+ * ```
1458
+ */
1459
+ async createOrder(params) {
1460
+ this.logger.info("Creating order", {
1461
+ side: params.side,
1462
+ orderType: params.orderType,
1463
+ marketSlug: params.marketSlug
1464
+ });
1465
+ const unsignedOrder = this.orderBuilder.buildOrder(params);
1466
+ this.logger.debug("Built unsigned order", {
1467
+ salt: unsignedOrder.salt,
1468
+ makerAmount: unsignedOrder.makerAmount,
1469
+ takerAmount: unsignedOrder.takerAmount
1470
+ });
1471
+ const signature = await this.orderSigner.signOrder(
1472
+ unsignedOrder,
1473
+ this.signingConfig
1474
+ );
1475
+ const payload = {
1476
+ order: {
1477
+ ...unsignedOrder,
1478
+ signature
1479
+ },
1480
+ orderType: params.orderType,
1481
+ marketSlug: params.marketSlug,
1482
+ ownerId: this.ownerId
1483
+ };
1484
+ this.logger.debug("Submitting order to API");
1485
+ console.log("[OrderClient] Full API request payload:", JSON.stringify(payload, null, 2));
1486
+ const apiResponse = await this.httpClient.post(
1487
+ "/orders",
1488
+ payload
1489
+ );
1490
+ this.logger.info("Order created successfully", {
1491
+ orderId: apiResponse.order.id
1492
+ });
1493
+ return this.transformOrderResponse(apiResponse);
1494
+ }
1495
+ /**
1496
+ * Transforms raw API response to clean OrderResponse DTO.
1497
+ *
1498
+ * @param apiResponse - Raw API response with nested objects
1499
+ * @returns Clean OrderResponse with only essential fields
1500
+ *
1501
+ * @internal
1502
+ */
1503
+ transformOrderResponse(apiResponse) {
1504
+ const cleanOrder = {
1505
+ order: {
1506
+ id: apiResponse.order.id,
1507
+ createdAt: apiResponse.order.createdAt,
1508
+ makerAmount: apiResponse.order.makerAmount,
1509
+ takerAmount: apiResponse.order.takerAmount,
1510
+ expiration: apiResponse.order.expiration,
1511
+ signatureType: apiResponse.order.signatureType,
1512
+ salt: apiResponse.order.salt,
1513
+ maker: apiResponse.order.maker,
1514
+ signer: apiResponse.order.signer,
1515
+ taker: apiResponse.order.taker,
1516
+ tokenId: apiResponse.order.tokenId,
1517
+ side: apiResponse.order.side,
1518
+ feeRateBps: apiResponse.order.feeRateBps,
1519
+ nonce: apiResponse.order.nonce,
1520
+ signature: apiResponse.order.signature,
1521
+ orderType: apiResponse.order.orderType,
1522
+ price: apiResponse.order.price,
1523
+ marketId: apiResponse.order.marketId
1524
+ }
1525
+ };
1526
+ if (apiResponse.makerMatches && apiResponse.makerMatches.length > 0) {
1527
+ cleanOrder.makerMatches = apiResponse.makerMatches.map((match) => ({
1528
+ id: match.id,
1529
+ createdAt: match.createdAt,
1530
+ matchedSize: match.matchedSize,
1531
+ orderId: match.orderId
1532
+ }));
1533
+ }
1534
+ return cleanOrder;
1535
+ }
1536
+ /**
1537
+ * Cancels an existing order by ID.
1538
+ *
1539
+ * @param orderId - Order ID to cancel
1540
+ * @returns Promise resolving to cancellation message
1541
+ *
1542
+ * @throws Error if cancellation fails
1543
+ *
1544
+ * @example
1545
+ * ```typescript
1546
+ * const result = await orderClient.cancel('order-id-123');
1547
+ * console.log(result.message); // "Order canceled successfully"
1548
+ * ```
1549
+ */
1550
+ async cancel(orderId) {
1551
+ this.logger.info("Cancelling order", { orderId });
1552
+ const response = await this.httpClient.delete(
1553
+ `/orders/${orderId}`
1554
+ );
1555
+ this.logger.info("Order cancellation response", {
1556
+ orderId,
1557
+ message: response.message
1558
+ });
1559
+ return response;
1560
+ }
1561
+ /**
1562
+ * Cancels all orders for a specific market.
1563
+ *
1564
+ * @param marketSlug - Market slug to cancel all orders for
1565
+ * @returns Promise resolving to cancellation message
1566
+ *
1567
+ * @throws Error if cancellation fails
1568
+ *
1569
+ * @example
1570
+ * ```typescript
1571
+ * const result = await orderClient.cancelAll('market-slug-123');
1572
+ * console.log(result.message); // "Orders canceled successfully"
1573
+ * ```
1574
+ */
1575
+ async cancelAll(marketSlug) {
1576
+ this.logger.info("Cancelling all orders for market", { marketSlug });
1577
+ const response = await this.httpClient.delete(
1578
+ `/orders/all/${marketSlug}`
1579
+ );
1580
+ this.logger.info("All orders cancellation response", {
1581
+ marketSlug,
1582
+ message: response.message
1583
+ });
1584
+ return response;
1585
+ }
1586
+ /**
1587
+ * @deprecated Use `cancel()` instead
1588
+ */
1589
+ async cancelOrder(orderId) {
1590
+ await this.cancel(orderId);
1591
+ }
1592
+ /**
1593
+ * Gets an order by ID.
1594
+ *
1595
+ * @param orderId - Order ID to fetch
1596
+ * @returns Promise resolving to order details
1597
+ *
1598
+ * @throws Error if order not found
1599
+ *
1600
+ * @example
1601
+ * ```typescript
1602
+ * const order = await orderClient.getOrder('order-id-123');
1603
+ * console.log(order.order.side);
1604
+ * ```
1605
+ */
1606
+ async getOrder(orderId) {
1607
+ this.logger.debug("Fetching order", { orderId });
1608
+ const response = await this.httpClient.get(
1609
+ `/orders/${orderId}`
1610
+ );
1611
+ return response;
1612
+ }
1613
+ /**
1614
+ * Builds an unsigned order without submitting.
1615
+ *
1616
+ * @remarks
1617
+ * Useful for advanced use cases where you need the unsigned order
1618
+ * before signing and submission.
1619
+ *
1620
+ * @param params - Order parameters
1621
+ * @returns Unsigned order
1622
+ *
1623
+ * @example
1624
+ * ```typescript
1625
+ * const unsignedOrder = orderClient.buildUnsignedOrder({
1626
+ * tokenId: '123456',
1627
+ * price: 0.65,
1628
+ * size: 100,
1629
+ * side: Side.BUY
1630
+ * });
1631
+ * ```
1632
+ */
1633
+ buildUnsignedOrder(params) {
1634
+ return this.orderBuilder.buildOrder(params);
1635
+ }
1636
+ /**
1637
+ * Signs an unsigned order without submitting.
1638
+ *
1639
+ * @remarks
1640
+ * Useful for advanced use cases where you need to inspect
1641
+ * the signature before submission.
1642
+ *
1643
+ * @param order - Unsigned order to sign
1644
+ * @returns Promise resolving to signature
1645
+ *
1646
+ * @example
1647
+ * ```typescript
1648
+ * const signature = await orderClient.signOrder(unsignedOrder);
1649
+ * ```
1650
+ */
1651
+ async signOrder(order) {
1652
+ return await this.orderSigner.signOrder(order, this.signingConfig);
1653
+ }
1654
+ };
1655
+
1656
+ // src/markets/fetcher.ts
1657
+ var MarketFetcher = class {
1658
+ /**
1659
+ * Creates a new market fetcher instance.
1660
+ *
1661
+ * @param httpClient - HTTP client for API requests
1662
+ * @param logger - Optional logger for debugging (default: no logging)
1663
+ *
1664
+ * @example
1665
+ * ```typescript
1666
+ * const fetcher = new MarketFetcher(httpClient);
1667
+ * ```
1668
+ */
1669
+ constructor(httpClient, logger) {
1670
+ this.httpClient = httpClient;
1671
+ this.logger = logger || new NoOpLogger();
1672
+ }
1673
+ /**
1674
+ * Gets active markets with query parameters and pagination support.
1675
+ *
1676
+ * @param params - Query parameters for filtering and pagination
1677
+ * @returns Promise resolving to active markets response
1678
+ * @throws Error if API request fails
1679
+ *
1680
+ * @example
1681
+ * ```typescript
1682
+ * // Get 8 markets sorted by LP rewards
1683
+ * const response = await fetcher.getActiveMarkets({
1684
+ * limit: 8,
1685
+ * sortBy: 'lp_rewards'
1686
+ * });
1687
+ * console.log(`Found ${response.data.length} of ${response.totalMarketsCount} markets`);
1688
+ *
1689
+ * // Get page 2
1690
+ * const page2 = await fetcher.getActiveMarkets({
1691
+ * limit: 8,
1692
+ * page: 2,
1693
+ * sortBy: 'ending_soon'
1694
+ * });
1695
+ * ```
1696
+ */
1697
+ async getActiveMarkets(params) {
1698
+ const queryParams = new URLSearchParams();
1699
+ if (params?.limit !== void 0) {
1700
+ queryParams.append("limit", params.limit.toString());
1701
+ }
1702
+ if (params?.page !== void 0) {
1703
+ queryParams.append("page", params.page.toString());
1704
+ }
1705
+ if (params?.sortBy) {
1706
+ queryParams.append("sortBy", params.sortBy);
1707
+ }
1708
+ const queryString = queryParams.toString();
1709
+ const endpoint = `/markets/active${queryString ? `?${queryString}` : ""}`;
1710
+ this.logger.debug("Fetching active markets", { params });
1711
+ try {
1712
+ const response = await this.httpClient.get(endpoint);
1713
+ this.logger.info("Active markets fetched successfully", {
1714
+ count: response.data.length,
1715
+ total: response.totalMarketsCount,
1716
+ sortBy: params?.sortBy,
1717
+ page: params?.page
1718
+ });
1719
+ return response;
1720
+ } catch (error) {
1721
+ this.logger.error("Failed to fetch active markets", error, { params });
1722
+ throw error;
1723
+ }
1724
+ }
1725
+ /**
1726
+ * Gets a single market by slug.
1727
+ *
1728
+ * @param slug - Market slug identifier
1729
+ * @returns Promise resolving to market details
1730
+ * @throws Error if API request fails or market not found
1731
+ *
1732
+ * @example
1733
+ * ```typescript
1734
+ * const market = await fetcher.getMarket('bitcoin-price-2024');
1735
+ * console.log(`Market: ${market.title}`);
1736
+ * ```
1737
+ */
1738
+ async getMarket(slug) {
1739
+ this.logger.debug("Fetching market", { slug });
1740
+ try {
1741
+ const market = await this.httpClient.get(`/markets/${slug}`);
1742
+ this.logger.info("Market fetched successfully", {
1743
+ slug,
1744
+ title: market.title
1745
+ });
1746
+ return market;
1747
+ } catch (error) {
1748
+ this.logger.error("Failed to fetch market", error, { slug });
1749
+ throw error;
1750
+ }
1751
+ }
1752
+ /**
1753
+ * Gets the orderbook for a CLOB market.
1754
+ *
1755
+ * @param slug - Market slug identifier
1756
+ * @returns Promise resolving to orderbook data
1757
+ * @throws Error if API request fails
1758
+ *
1759
+ * @example
1760
+ * ```typescript
1761
+ * const orderbook = await fetcher.getOrderBook('bitcoin-price-2024');
1762
+ * console.log(`Bids: ${orderbook.bids.length}, Asks: ${orderbook.asks.length}`);
1763
+ * ```
1764
+ */
1765
+ async getOrderBook(slug) {
1766
+ this.logger.debug("Fetching orderbook", { slug });
1767
+ try {
1768
+ const orderbook = await this.httpClient.get(
1769
+ `/markets/${slug}/orderbook`
1770
+ );
1771
+ this.logger.info("Orderbook fetched successfully", {
1772
+ slug,
1773
+ bids: orderbook.bids.length,
1774
+ asks: orderbook.asks.length,
1775
+ tokenId: orderbook.tokenId
1776
+ });
1777
+ return orderbook;
1778
+ } catch (error) {
1779
+ this.logger.error("Failed to fetch orderbook", error, { slug });
1780
+ throw error;
1781
+ }
1782
+ }
1783
+ /**
1784
+ * Gets the current price for a token.
1785
+ *
1786
+ * @param tokenId - Token ID
1787
+ * @returns Promise resolving to price information
1788
+ * @throws Error if API request fails
1789
+ *
1790
+ * @example
1791
+ * ```typescript
1792
+ * const price = await fetcher.getPrice('123456');
1793
+ * console.log(`Current price: ${price.price}`);
1794
+ * ```
1795
+ */
1796
+ async getPrice(tokenId) {
1797
+ this.logger.debug("Fetching price", { tokenId });
1798
+ try {
1799
+ const price = await this.httpClient.get(`/prices/${tokenId}`);
1800
+ this.logger.info("Price fetched successfully", {
1801
+ tokenId,
1802
+ price: price.price
1803
+ });
1804
+ return price;
1805
+ } catch (error) {
1806
+ this.logger.error("Failed to fetch price", error, { tokenId });
1807
+ throw error;
1808
+ }
1809
+ }
1810
+ };
1811
+
1812
+ // src/portfolio/fetcher.ts
1813
+ var PortfolioFetcher = class {
1814
+ /**
1815
+ * Creates a new portfolio fetcher instance.
1816
+ *
1817
+ * @param httpClient - Authenticated HTTP client for API requests
1818
+ * @param logger - Optional logger for debugging (default: no logging)
1819
+ *
1820
+ * @example
1821
+ * ```typescript
1822
+ * // Create authenticated client
1823
+ * const httpClient = new HttpClient({ baseURL: API_URL });
1824
+ * await authenticator.authenticate({ client: 'eoa' });
1825
+ *
1826
+ * // Create portfolio fetcher
1827
+ * const portfolioFetcher = new PortfolioFetcher(httpClient);
1828
+ * ```
1829
+ */
1830
+ constructor(httpClient, logger) {
1831
+ this.httpClient = httpClient;
1832
+ this.logger = logger || new NoOpLogger();
1833
+ }
1834
+ /**
1835
+ * Gets raw portfolio positions response from API.
1836
+ *
1837
+ * @returns Promise resolving to portfolio positions response with CLOB and AMM positions
1838
+ * @throws Error if API request fails or user is not authenticated
1839
+ *
1840
+ * @example
1841
+ * ```typescript
1842
+ * const response = await portfolioFetcher.getPositions();
1843
+ * console.log(`CLOB positions: ${response.clob.length}`);
1844
+ * console.log(`AMM positions: ${response.amm.length}`);
1845
+ * console.log(`Total points: ${response.accumulativePoints}`);
1846
+ * ```
1847
+ */
1848
+ async getPositions() {
1849
+ this.logger.debug("Fetching user positions");
1850
+ try {
1851
+ const response = await this.httpClient.get(
1852
+ "/portfolio/positions"
1853
+ );
1854
+ this.logger.info("Positions fetched successfully", {
1855
+ clobCount: response.clob?.length || 0,
1856
+ ammCount: response.amm?.length || 0
1857
+ });
1858
+ return response;
1859
+ } catch (error) {
1860
+ this.logger.error("Failed to fetch positions", error);
1861
+ throw error;
1862
+ }
1863
+ }
1864
+ /**
1865
+ * Gets CLOB positions only.
1866
+ *
1867
+ * @returns Promise resolving to array of CLOB positions
1868
+ * @throws Error if API request fails
1869
+ *
1870
+ * @example
1871
+ * ```typescript
1872
+ * const clobPositions = await portfolioFetcher.getCLOBPositions();
1873
+ * clobPositions.forEach(pos => {
1874
+ * console.log(`${pos.market.title}: YES ${pos.positions.yes.unrealizedPnl} P&L`);
1875
+ * });
1876
+ * ```
1877
+ */
1878
+ async getCLOBPositions() {
1879
+ const response = await this.getPositions();
1880
+ return response.clob || [];
1881
+ }
1882
+ /**
1883
+ * Gets AMM positions only.
1884
+ *
1885
+ * @returns Promise resolving to array of AMM positions
1886
+ * @throws Error if API request fails
1887
+ *
1888
+ * @example
1889
+ * ```typescript
1890
+ * const ammPositions = await portfolioFetcher.getAMMPositions();
1891
+ * ammPositions.forEach(pos => {
1892
+ * console.log(`${pos.market.title}: ${pos.unrealizedPnl} P&L`);
1893
+ * });
1894
+ * ```
1895
+ */
1896
+ async getAMMPositions() {
1897
+ const response = await this.getPositions();
1898
+ return response.amm || [];
1899
+ }
1900
+ /**
1901
+ * Flattens positions into a unified format for easier consumption.
1902
+ *
1903
+ * @remarks
1904
+ * Converts CLOB positions (which have YES/NO sides) and AMM positions
1905
+ * into a unified Position array. Only includes positions with non-zero values.
1906
+ *
1907
+ * @returns Promise resolving to array of flattened positions
1908
+ * @throws Error if API request fails
1909
+ *
1910
+ * @example
1911
+ * ```typescript
1912
+ * const positions = await portfolioFetcher.getFlattenedPositions();
1913
+ * positions.forEach(pos => {
1914
+ * const pnlPercent = (pos.unrealizedPnl / pos.costBasis) * 100;
1915
+ * console.log(`${pos.market.title} (${pos.side}): ${pnlPercent.toFixed(2)}% P&L`);
1916
+ * });
1917
+ * ```
1918
+ */
1919
+ async getFlattenedPositions() {
1920
+ const response = await this.getPositions();
1921
+ const positions = [];
1922
+ for (const clobPos of response.clob || []) {
1923
+ const yesCost = parseFloat(clobPos.positions.yes.cost);
1924
+ const yesValue = parseFloat(clobPos.positions.yes.marketValue);
1925
+ if (yesCost > 0 || yesValue > 0) {
1926
+ positions.push({
1927
+ type: "CLOB",
1928
+ market: clobPos.market,
1929
+ side: "YES",
1930
+ costBasis: yesCost,
1931
+ marketValue: yesValue,
1932
+ unrealizedPnl: parseFloat(clobPos.positions.yes.unrealizedPnl),
1933
+ realizedPnl: parseFloat(clobPos.positions.yes.realisedPnl),
1934
+ currentPrice: clobPos.latestTrade?.latestYesPrice ?? 0,
1935
+ avgPrice: yesCost > 0 ? parseFloat(clobPos.positions.yes.fillPrice) / 1e6 : 0,
1936
+ tokenBalance: parseFloat(clobPos.tokensBalance.yes)
1937
+ });
1938
+ }
1939
+ const noCost = parseFloat(clobPos.positions.no.cost);
1940
+ const noValue = parseFloat(clobPos.positions.no.marketValue);
1941
+ if (noCost > 0 || noValue > 0) {
1942
+ positions.push({
1943
+ type: "CLOB",
1944
+ market: clobPos.market,
1945
+ side: "NO",
1946
+ costBasis: noCost,
1947
+ marketValue: noValue,
1948
+ unrealizedPnl: parseFloat(clobPos.positions.no.unrealizedPnl),
1949
+ realizedPnl: parseFloat(clobPos.positions.no.realisedPnl),
1950
+ currentPrice: clobPos.latestTrade?.latestNoPrice ?? 0,
1951
+ avgPrice: noCost > 0 ? parseFloat(clobPos.positions.no.fillPrice) / 1e6 : 0,
1952
+ tokenBalance: parseFloat(clobPos.tokensBalance.no)
1953
+ });
1954
+ }
1955
+ }
1956
+ for (const ammPos of response.amm || []) {
1957
+ const cost = parseFloat(ammPos.totalBuysCost);
1958
+ const value = parseFloat(ammPos.collateralAmount);
1959
+ if (cost > 0 || value > 0) {
1960
+ positions.push({
1961
+ type: "AMM",
1962
+ market: ammPos.market,
1963
+ side: ammPos.outcomeIndex === 0 ? "YES" : "NO",
1964
+ costBasis: cost,
1965
+ marketValue: value,
1966
+ unrealizedPnl: parseFloat(ammPos.unrealizedPnl),
1967
+ realizedPnl: parseFloat(ammPos.realizedPnl),
1968
+ currentPrice: ammPos.latestTrade ? parseFloat(ammPos.latestTrade.outcomeTokenPrice) : 0,
1969
+ avgPrice: parseFloat(ammPos.averageFillPrice),
1970
+ tokenBalance: parseFloat(ammPos.outcomeTokenAmount)
1971
+ });
1972
+ }
1973
+ }
1974
+ this.logger.debug("Flattened positions", { count: positions.length });
1975
+ return positions;
1976
+ }
1977
+ /**
1978
+ * Calculates portfolio summary statistics from raw API response.
1979
+ *
1980
+ * @param response - Portfolio positions response from API
1981
+ * @returns Portfolio summary with totals and statistics
1982
+ *
1983
+ * @example
1984
+ * ```typescript
1985
+ * const response = await portfolioFetcher.getPositions();
1986
+ * const summary = portfolioFetcher.calculateSummary(response);
1987
+ *
1988
+ * console.log(`Total Portfolio Value: $${(summary.totalValue / 1e6).toFixed(2)}`);
1989
+ * console.log(`Total P&L: ${summary.totalUnrealizedPnlPercent.toFixed(2)}%`);
1990
+ * console.log(`CLOB Positions: ${summary.breakdown.clob.positions}`);
1991
+ * console.log(`AMM Positions: ${summary.breakdown.amm.positions}`);
1992
+ * ```
1993
+ */
1994
+ calculateSummary(response) {
1995
+ this.logger.debug("Calculating portfolio summary", {
1996
+ clobCount: response.clob?.length || 0,
1997
+ ammCount: response.amm?.length || 0
1998
+ });
1999
+ let totalValue = 0;
2000
+ let totalCostBasis = 0;
2001
+ let totalUnrealizedPnl = 0;
2002
+ let totalRealizedPnl = 0;
2003
+ let clobPositions = 0;
2004
+ let clobValue = 0;
2005
+ let clobPnl = 0;
2006
+ let ammPositions = 0;
2007
+ let ammValue = 0;
2008
+ let ammPnl = 0;
2009
+ for (const clobPos of response.clob || []) {
2010
+ const yesCost = parseFloat(clobPos.positions.yes.cost);
2011
+ const yesValue = parseFloat(clobPos.positions.yes.marketValue);
2012
+ const yesUnrealizedPnl = parseFloat(clobPos.positions.yes.unrealizedPnl);
2013
+ const yesRealizedPnl = parseFloat(clobPos.positions.yes.realisedPnl);
2014
+ if (yesCost > 0 || yesValue > 0) {
2015
+ clobPositions++;
2016
+ totalCostBasis += yesCost;
2017
+ totalValue += yesValue;
2018
+ totalUnrealizedPnl += yesUnrealizedPnl;
2019
+ totalRealizedPnl += yesRealizedPnl;
2020
+ clobValue += yesValue;
2021
+ clobPnl += yesUnrealizedPnl;
2022
+ }
2023
+ const noCost = parseFloat(clobPos.positions.no.cost);
2024
+ const noValue = parseFloat(clobPos.positions.no.marketValue);
2025
+ const noUnrealizedPnl = parseFloat(clobPos.positions.no.unrealizedPnl);
2026
+ const noRealizedPnl = parseFloat(clobPos.positions.no.realisedPnl);
2027
+ if (noCost > 0 || noValue > 0) {
2028
+ clobPositions++;
2029
+ totalCostBasis += noCost;
2030
+ totalValue += noValue;
2031
+ totalUnrealizedPnl += noUnrealizedPnl;
2032
+ totalRealizedPnl += noRealizedPnl;
2033
+ clobValue += noValue;
2034
+ clobPnl += noUnrealizedPnl;
2035
+ }
2036
+ }
2037
+ for (const ammPos of response.amm || []) {
2038
+ const cost = parseFloat(ammPos.totalBuysCost);
2039
+ const value = parseFloat(ammPos.collateralAmount);
2040
+ const unrealizedPnl = parseFloat(ammPos.unrealizedPnl);
2041
+ const realizedPnl = parseFloat(ammPos.realizedPnl);
2042
+ if (cost > 0 || value > 0) {
2043
+ ammPositions++;
2044
+ totalCostBasis += cost;
2045
+ totalValue += value;
2046
+ totalUnrealizedPnl += unrealizedPnl;
2047
+ totalRealizedPnl += realizedPnl;
2048
+ ammValue += value;
2049
+ ammPnl += unrealizedPnl;
2050
+ }
2051
+ }
2052
+ const totalUnrealizedPnlPercent = totalCostBasis > 0 ? totalUnrealizedPnl / totalCostBasis * 100 : 0;
2053
+ const uniqueMarkets = /* @__PURE__ */ new Set();
2054
+ for (const pos of response.clob || []) {
2055
+ uniqueMarkets.add(pos.market.id);
2056
+ }
2057
+ for (const pos of response.amm || []) {
2058
+ uniqueMarkets.add(pos.market.id);
2059
+ }
2060
+ const summary = {
2061
+ totalValue,
2062
+ totalCostBasis,
2063
+ totalUnrealizedPnl,
2064
+ totalRealizedPnl,
2065
+ totalUnrealizedPnlPercent,
2066
+ positionCount: clobPositions + ammPositions,
2067
+ marketCount: uniqueMarkets.size,
2068
+ breakdown: {
2069
+ clob: {
2070
+ positions: clobPositions,
2071
+ value: clobValue,
2072
+ pnl: clobPnl
2073
+ },
2074
+ amm: {
2075
+ positions: ammPositions,
2076
+ value: ammValue,
2077
+ pnl: ammPnl
2078
+ }
2079
+ }
2080
+ };
2081
+ this.logger.debug("Portfolio summary calculated", summary);
2082
+ return summary;
2083
+ }
2084
+ /**
2085
+ * Gets positions and calculates summary in a single call.
2086
+ *
2087
+ * @returns Promise resolving to response and summary
2088
+ * @throws Error if API request fails or user is not authenticated
2089
+ *
2090
+ * @example
2091
+ * ```typescript
2092
+ * const { response, summary } = await portfolioFetcher.getPortfolio();
2093
+ *
2094
+ * console.log('Portfolio Summary:');
2095
+ * console.log(` Total Value: $${(summary.totalValue / 1e6).toFixed(2)}`);
2096
+ * console.log(` Total P&L: $${(summary.totalUnrealizedPnl / 1e6).toFixed(2)}`);
2097
+ * console.log(` P&L %: ${summary.totalUnrealizedPnlPercent.toFixed(2)}%`);
2098
+ * console.log(`\nCLOB Positions: ${response.clob.length}`);
2099
+ * console.log(`AMM Positions: ${response.amm.length}`);
2100
+ * ```
2101
+ */
2102
+ async getPortfolio() {
2103
+ this.logger.debug("Fetching portfolio with summary");
2104
+ const response = await this.getPositions();
2105
+ const summary = this.calculateSummary(response);
2106
+ this.logger.info("Portfolio fetched with summary", {
2107
+ positionCount: summary.positionCount,
2108
+ totalValueUSDC: summary.totalValue / 1e6,
2109
+ pnlPercent: summary.totalUnrealizedPnlPercent
2110
+ });
2111
+ return { response, summary };
2112
+ }
2113
+ };
2114
+
2115
+ // src/websocket/client.ts
2116
+ import { io } from "socket.io-client";
2117
+ var WebSocketClient = class {
2118
+ /**
2119
+ * Creates a new WebSocket client.
2120
+ *
2121
+ * @param config - WebSocket configuration
2122
+ * @param logger - Optional logger for debugging
2123
+ */
2124
+ constructor(config = {}, logger) {
2125
+ this.socket = null;
2126
+ this.state = "disconnected" /* DISCONNECTED */;
2127
+ this.reconnectAttempts = 0;
2128
+ this.subscriptions = /* @__PURE__ */ new Map();
2129
+ this.pendingListeners = [];
2130
+ this.config = {
2131
+ url: config.url || DEFAULT_WS_URL,
2132
+ sessionCookie: config.sessionCookie || "",
2133
+ autoReconnect: config.autoReconnect ?? true,
2134
+ reconnectDelay: config.reconnectDelay || 1e3,
2135
+ maxReconnectAttempts: config.maxReconnectAttempts || Infinity,
2136
+ timeout: config.timeout || 1e4
2137
+ };
2138
+ this.logger = logger || new NoOpLogger();
2139
+ }
2140
+ /**
2141
+ * Gets current connection state.
2142
+ *
2143
+ * @returns Current WebSocket state
2144
+ */
2145
+ getState() {
2146
+ return this.state;
2147
+ }
2148
+ /**
2149
+ * Checks if client is connected.
2150
+ *
2151
+ * @returns True if connected
2152
+ */
2153
+ isConnected() {
2154
+ return this.state === "connected" /* CONNECTED */ && this.socket?.connected === true;
2155
+ }
2156
+ /**
2157
+ * Sets the session cookie for authentication.
2158
+ *
2159
+ * @param sessionCookie - Session cookie value
2160
+ */
2161
+ setSessionCookie(sessionCookie) {
2162
+ this.config.sessionCookie = sessionCookie;
2163
+ if (this.socket?.connected) {
2164
+ this.logger.info("Session cookie updated, reconnecting...");
2165
+ this.disconnect();
2166
+ this.connect();
2167
+ }
2168
+ }
2169
+ /**
2170
+ * Connects to the WebSocket server.
2171
+ *
2172
+ * @returns Promise that resolves when connected
2173
+ * @throws Error if connection fails
2174
+ *
2175
+ * @example
2176
+ * ```typescript
2177
+ * await wsClient.connect();
2178
+ * console.log('Connected!');
2179
+ * ```
2180
+ */
2181
+ async connect() {
2182
+ if (this.socket?.connected) {
2183
+ this.logger.info("Already connected");
2184
+ return;
2185
+ }
2186
+ this.logger.info("Connecting to WebSocket", { url: this.config.url });
2187
+ this.state = "connecting" /* CONNECTING */;
2188
+ return new Promise((resolve, reject) => {
2189
+ const timeout = setTimeout(() => {
2190
+ reject(new Error(`Connection timeout after ${this.config.timeout}ms`));
2191
+ }, this.config.timeout);
2192
+ const wsUrl = this.config.url.endsWith("/markets") ? this.config.url : `${this.config.url}/markets`;
2193
+ this.socket = io(wsUrl, {
2194
+ transports: ["websocket"],
2195
+ // Use WebSocket transport only
2196
+ extraHeaders: {
2197
+ cookie: `limitless_session=${this.config.sessionCookie}`
2198
+ },
2199
+ reconnection: this.config.autoReconnect,
2200
+ reconnectionDelay: this.config.reconnectDelay,
2201
+ reconnectionAttempts: this.config.maxReconnectAttempts,
2202
+ timeout: this.config.timeout
2203
+ });
2204
+ this.attachPendingListeners();
2205
+ this.setupEventHandlers();
2206
+ this.socket.once("connect", () => {
2207
+ clearTimeout(timeout);
2208
+ this.state = "connected" /* CONNECTED */;
2209
+ this.reconnectAttempts = 0;
2210
+ this.logger.info("WebSocket connected");
2211
+ this.resubscribeAll();
2212
+ resolve();
2213
+ });
2214
+ this.socket.once("connect_error", (error) => {
2215
+ clearTimeout(timeout);
2216
+ this.state = "error" /* ERROR */;
2217
+ this.logger.error("WebSocket connection error", error);
2218
+ reject(error);
2219
+ });
2220
+ });
2221
+ }
2222
+ /**
2223
+ * Disconnects from the WebSocket server.
2224
+ *
2225
+ * @example
2226
+ * ```typescript
2227
+ * wsClient.disconnect();
2228
+ * ```
2229
+ */
2230
+ disconnect() {
2231
+ if (!this.socket) {
2232
+ return;
2233
+ }
2234
+ this.logger.info("Disconnecting from WebSocket");
2235
+ this.socket.disconnect();
2236
+ this.socket = null;
2237
+ this.state = "disconnected" /* DISCONNECTED */;
2238
+ this.subscriptions.clear();
2239
+ }
2240
+ /**
2241
+ * Subscribes to a channel.
2242
+ *
2243
+ * @param channel - Channel to subscribe to
2244
+ * @param options - Subscription options
2245
+ * @returns Promise that resolves when subscribed
2246
+ * @throws Error if not connected
2247
+ *
2248
+ * @example
2249
+ * ```typescript
2250
+ * // Subscribe to orderbook for a specific market
2251
+ * await wsClient.subscribe('orderbook', { marketSlug: 'market-123' });
2252
+ *
2253
+ * // Subscribe to all trades
2254
+ * await wsClient.subscribe('trades');
2255
+ *
2256
+ * // Subscribe to your orders
2257
+ * await wsClient.subscribe('orders');
2258
+ * ```
2259
+ */
2260
+ async subscribe(channel, options = {}) {
2261
+ if (!this.isConnected()) {
2262
+ throw new Error("WebSocket not connected. Call connect() first.");
2263
+ }
2264
+ const subscriptionKey = this.getSubscriptionKey(channel, options);
2265
+ this.subscriptions.set(subscriptionKey, options);
2266
+ this.logger.info("Subscribing to channel", { channel, options });
2267
+ return new Promise((resolve, reject) => {
2268
+ this.socket.emit(channel, options, (response) => {
2269
+ if (response?.error) {
2270
+ this.logger.error("Subscription failed", response.error);
2271
+ this.subscriptions.delete(subscriptionKey);
2272
+ reject(new Error(response.error));
2273
+ } else {
2274
+ this.logger.info("Subscribed successfully", { channel, options });
2275
+ resolve();
2276
+ }
2277
+ });
2278
+ });
2279
+ }
2280
+ /**
2281
+ * Unsubscribes from a channel.
2282
+ *
2283
+ * @param channel - Channel to unsubscribe from
2284
+ * @param options - Subscription options (must match subscribe call)
2285
+ * @returns Promise that resolves when unsubscribed
2286
+ *
2287
+ * @example
2288
+ * ```typescript
2289
+ * await wsClient.unsubscribe('orderbook', { marketSlug: 'market-123' });
2290
+ * ```
2291
+ */
2292
+ async unsubscribe(channel, options = {}) {
2293
+ if (!this.isConnected()) {
2294
+ throw new Error("WebSocket not connected");
2295
+ }
2296
+ const subscriptionKey = this.getSubscriptionKey(channel, options);
2297
+ this.subscriptions.delete(subscriptionKey);
2298
+ this.logger.info("Unsubscribing from channel", { channel, options });
2299
+ return new Promise((resolve, reject) => {
2300
+ this.socket.emit("unsubscribe", { channel, ...options }, (response) => {
2301
+ if (response?.error) {
2302
+ this.logger.error("Unsubscribe failed", response.error);
2303
+ reject(new Error(response.error));
2304
+ } else {
2305
+ this.logger.info("Unsubscribed successfully", { channel, options });
2306
+ resolve();
2307
+ }
2308
+ });
2309
+ });
2310
+ }
2311
+ /**
2312
+ * Registers an event listener.
2313
+ *
2314
+ * @param event - Event name
2315
+ * @param handler - Event handler
2316
+ * @returns This client for chaining
2317
+ *
2318
+ * @example
2319
+ * ```typescript
2320
+ * wsClient
2321
+ * .on('orderbook', (data) => console.log('Orderbook:', data))
2322
+ * .on('trade', (data) => console.log('Trade:', data))
2323
+ * .on('error', (error) => console.error('Error:', error));
2324
+ * ```
2325
+ */
2326
+ on(event, handler) {
2327
+ if (!this.socket) {
2328
+ this.pendingListeners.push({ event, handler });
2329
+ return this;
2330
+ }
2331
+ this.socket.on(event, handler);
2332
+ return this;
2333
+ }
2334
+ /**
2335
+ * Registers a one-time event listener.
2336
+ *
2337
+ * @param event - Event name
2338
+ * @param handler - Event handler
2339
+ * @returns This client for chaining
2340
+ */
2341
+ once(event, handler) {
2342
+ if (!this.socket) {
2343
+ throw new Error("WebSocket not initialized. Call connect() first.");
2344
+ }
2345
+ this.socket.once(event, handler);
2346
+ return this;
2347
+ }
2348
+ /**
2349
+ * Removes an event listener.
2350
+ *
2351
+ * @param event - Event name
2352
+ * @param handler - Event handler to remove
2353
+ * @returns This client for chaining
2354
+ */
2355
+ off(event, handler) {
2356
+ if (!this.socket) {
2357
+ return this;
2358
+ }
2359
+ this.socket.off(event, handler);
2360
+ return this;
2361
+ }
2362
+ /**
2363
+ * Attach any pending event listeners that were added before connect().
2364
+ * @internal
2365
+ */
2366
+ attachPendingListeners() {
2367
+ if (!this.socket || this.pendingListeners.length === 0) {
2368
+ return;
2369
+ }
2370
+ for (const { event, handler } of this.pendingListeners) {
2371
+ this.socket.on(event, handler);
2372
+ }
2373
+ this.pendingListeners = [];
2374
+ }
2375
+ /**
2376
+ * Setup internal event handlers for connection management.
2377
+ * @internal
2378
+ */
2379
+ setupEventHandlers() {
2380
+ if (!this.socket) {
2381
+ return;
2382
+ }
2383
+ this.socket.on("connect", () => {
2384
+ this.state = "connected" /* CONNECTED */;
2385
+ this.reconnectAttempts = 0;
2386
+ this.logger.info("WebSocket connected");
2387
+ });
2388
+ this.socket.on("disconnect", (reason) => {
2389
+ this.state = "disconnected" /* DISCONNECTED */;
2390
+ this.logger.info("WebSocket disconnected", { reason });
2391
+ });
2392
+ this.socket.on("error", (error) => {
2393
+ this.state = "error" /* ERROR */;
2394
+ this.logger.error("WebSocket error", error);
2395
+ });
2396
+ this.socket.io.on("reconnect_attempt", (attempt) => {
2397
+ this.state = "reconnecting" /* RECONNECTING */;
2398
+ this.reconnectAttempts = attempt;
2399
+ this.logger.info("Reconnecting...", { attempt });
2400
+ });
2401
+ this.socket.io.on("reconnect", (attempt) => {
2402
+ this.state = "connected" /* CONNECTED */;
2403
+ this.logger.info("Reconnected", { attempts: attempt });
2404
+ this.resubscribeAll();
2405
+ });
2406
+ this.socket.io.on("reconnect_error", (error) => {
2407
+ this.logger.error("Reconnection error", error);
2408
+ });
2409
+ this.socket.io.on("reconnect_failed", () => {
2410
+ this.state = "error" /* ERROR */;
2411
+ this.logger.error("Reconnection failed");
2412
+ });
2413
+ }
2414
+ /**
2415
+ * Re-subscribes to all previous subscriptions after reconnection.
2416
+ * @internal
2417
+ */
2418
+ async resubscribeAll() {
2419
+ if (this.subscriptions.size === 0) {
2420
+ return;
2421
+ }
2422
+ this.logger.info("Re-subscribing to channels", {
2423
+ count: this.subscriptions.size
2424
+ });
2425
+ for (const [key, options] of this.subscriptions.entries()) {
2426
+ const channel = this.getChannelFromKey(key);
2427
+ try {
2428
+ await this.subscribe(channel, options);
2429
+ } catch (error) {
2430
+ this.logger.error("Failed to re-subscribe", error, { channel, options });
2431
+ }
2432
+ }
2433
+ }
2434
+ /**
2435
+ * Creates a unique subscription key.
2436
+ * @internal
2437
+ */
2438
+ getSubscriptionKey(channel, options) {
2439
+ return `${channel}:${options.marketSlug || "global"}`;
2440
+ }
2441
+ /**
2442
+ * Extracts channel from subscription key.
2443
+ * @internal
2444
+ */
2445
+ getChannelFromKey(key) {
2446
+ return key.split(":")[0];
2447
+ }
2448
+ };
2449
+ export {
2450
+ APIError,
2451
+ AuthenticatedClient,
2452
+ Authenticator,
2453
+ BASE_SEPOLIA_CHAIN_ID,
2454
+ CONTRACT_ADDRESSES,
2455
+ ConsoleLogger,
2456
+ DEFAULT_API_URL,
2457
+ DEFAULT_CHAIN_ID,
2458
+ DEFAULT_WS_URL,
2459
+ HttpClient,
2460
+ MarketFetcher,
2461
+ MarketType,
2462
+ MessageSigner,
2463
+ NoOpLogger,
2464
+ OrderBuilder,
2465
+ OrderClient,
2466
+ OrderSigner,
2467
+ OrderType,
2468
+ PortfolioFetcher,
2469
+ RetryConfig,
2470
+ RetryableClient,
2471
+ SIGNING_MESSAGE_TEMPLATE,
2472
+ Side,
2473
+ SignatureType,
2474
+ ValidationError,
2475
+ WebSocketClient,
2476
+ WebSocketState,
2477
+ ZERO_ADDRESS,
2478
+ getContractAddress,
2479
+ retryOnErrors,
2480
+ validateOrderArgs,
2481
+ validateSignedOrder,
2482
+ validateUnsignedOrder,
2483
+ withRetry
2484
+ };
2485
+ //# sourceMappingURL=index.mjs.map