@limitless-exchange/sdk 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,3 +1,8 @@
1
+ // src/types/api-tokens.ts
2
+ var ScopeTrading = "trading";
3
+ var ScopeAccountCreation = "account_creation";
4
+ var ScopeDelegatedSigning = "delegated_signing";
5
+
1
6
  // src/types/logger.ts
2
7
  var NoOpLogger = class {
3
8
  debug() {
@@ -155,6 +160,40 @@ function getContractAddress(contractType, chainId = DEFAULT_CHAIN_ID) {
155
160
  return address;
156
161
  }
157
162
 
163
+ // src/utils/sdk-tracking.ts
164
+ var SDK_ID = "lmts-sdk-ts";
165
+ function isNodeRuntime() {
166
+ return typeof process !== "undefined" && !!process.versions?.node;
167
+ }
168
+ function resolveSdkVersion() {
169
+ if ("1.0.4") {
170
+ return "1.0.4";
171
+ }
172
+ return "0.0.0";
173
+ }
174
+ function resolveRuntimeToken() {
175
+ if (isNodeRuntime()) {
176
+ return `node/${process.versions.node}`;
177
+ }
178
+ return "runtime/unknown";
179
+ }
180
+ function buildSdkTrackingHeaders() {
181
+ const sdkVersion = resolveSdkVersion();
182
+ const headers = {
183
+ "x-sdk-version": `${SDK_ID}/${sdkVersion}`
184
+ };
185
+ if (isNodeRuntime()) {
186
+ headers["user-agent"] = `${SDK_ID}/${sdkVersion} (${resolveRuntimeToken()})`;
187
+ }
188
+ return headers;
189
+ }
190
+ function buildWebSocketTrackingHeaders() {
191
+ if (!isNodeRuntime()) {
192
+ return {};
193
+ }
194
+ return buildSdkTrackingHeaders();
195
+ }
196
+
158
197
  // src/api/errors.ts
159
198
  var APIError = class _APIError extends Error {
160
199
  /**
@@ -227,6 +266,19 @@ var ValidationError = class _ValidationError extends APIError {
227
266
  }
228
267
  };
229
268
 
269
+ // src/api/hmac.ts
270
+ import { createHmac } from "crypto";
271
+ function buildHMACMessage(timestamp, method, path, body) {
272
+ return `${timestamp}
273
+ ${method.toUpperCase()}
274
+ ${path}
275
+ ${body}`;
276
+ }
277
+ function computeHMACSignature(secret, timestamp, method, path, body) {
278
+ const key = Buffer.from(secret, "base64");
279
+ return createHmac("sha256", key).update(buildHMACMessage(timestamp, method, path, body)).digest("base64");
280
+ }
281
+
230
282
  // src/api/http.ts
231
283
  var HttpClient = class {
232
284
  /**
@@ -236,10 +288,14 @@ var HttpClient = class {
236
288
  */
237
289
  constructor(config = {}) {
238
290
  this.apiKey = config.apiKey || process.env.LIMITLESS_API_KEY;
291
+ this.hmacCredentials = config.hmacCredentials ? {
292
+ tokenId: config.hmacCredentials.tokenId,
293
+ secret: config.hmacCredentials.secret
294
+ } : void 0;
239
295
  this.logger = config.logger || new NoOpLogger();
240
- if (!this.apiKey) {
296
+ if (!this.apiKey && !this.hmacCredentials) {
241
297
  this.logger.warn(
242
- "API key not set. Authenticated endpoints will fail. Set LIMITLESS_API_KEY environment variable or pass apiKey parameter."
298
+ "Authentication not set. Authenticated endpoints will fail. Set LIMITLESS_API_KEY environment variable, pass apiKey, or configure hmacCredentials."
243
299
  );
244
300
  }
245
301
  const keepAlive = config.keepAlive !== false;
@@ -266,6 +322,7 @@ var HttpClient = class {
266
322
  headers: {
267
323
  "Content-Type": "application/json",
268
324
  Accept: "application/json",
325
+ ...buildSdkTrackingHeaders(),
269
326
  ...config.additionalHeaders
270
327
  }
271
328
  });
@@ -283,21 +340,42 @@ var HttpClient = class {
283
340
  */
284
341
  setupInterceptors() {
285
342
  this.client.interceptors.request.use(
286
- (config) => {
287
- if (this.apiKey) {
288
- config.headers["X-API-Key"] = this.apiKey;
343
+ (rawConfig) => {
344
+ const config = rawConfig;
345
+ const headers = config.headers || (config.headers = {});
346
+ const identityToken = config.identityToken;
347
+ delete headers["X-API-Key"];
348
+ delete headers["lmts-api-key"];
349
+ delete headers["lmts-timestamp"];
350
+ delete headers["lmts-signature"];
351
+ delete headers.identity;
352
+ if (identityToken) {
353
+ headers.identity = `Bearer ${identityToken}`;
354
+ } else if (this.hmacCredentials) {
355
+ const requestPath = this.getRequestPath(config);
356
+ const requestBody = this.getRequestBodyForSignature(config.data);
357
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
358
+ const signature = computeHMACSignature(
359
+ this.hmacCredentials.secret,
360
+ timestamp,
361
+ config.method || "GET",
362
+ requestPath,
363
+ requestBody
364
+ );
365
+ headers["lmts-api-key"] = this.hmacCredentials.tokenId;
366
+ headers["lmts-timestamp"] = timestamp;
367
+ headers["lmts-signature"] = signature;
368
+ } else if (this.apiKey) {
369
+ headers["X-API-Key"] = this.apiKey;
289
370
  }
290
371
  const fullUrl = `${config.baseURL || ""}${config.url || ""}`;
291
372
  const method = config.method?.toUpperCase() || "GET";
292
- const logHeaders = { ...config.headers };
293
- if (logHeaders["X-API-Key"]) {
294
- logHeaders["X-API-Key"] = "***";
295
- }
373
+ const logHeaders = this.maskSensitiveHeaders(headers);
296
374
  this.logger.debug(`\u2192 ${method} ${fullUrl}`, {
297
375
  headers: logHeaders,
298
376
  body: config.data
299
377
  });
300
- return config;
378
+ return rawConfig;
301
379
  },
302
380
  (error) => Promise.reject(error)
303
381
  );
@@ -352,6 +430,42 @@ var HttpClient = class {
352
430
  }
353
431
  );
354
432
  }
433
+ /**
434
+ * Extracts a human-readable error message from API response payload.
435
+ * @internal
436
+ */
437
+ extractErrorMessage(data, fallback) {
438
+ if (!data) {
439
+ return fallback;
440
+ }
441
+ if (typeof data === "object") {
442
+ if (Array.isArray(data.message)) {
443
+ const messages = data.message.map((err) => {
444
+ const details = Object.entries(err || {}).filter(([_key, val]) => val !== "" && val !== null && val !== void 0).map(([key, val]) => `${key}: ${val}`).join(", ");
445
+ return details || JSON.stringify(err);
446
+ }).filter((msg) => msg.trim() !== "").join(" | ");
447
+ return messages || data.error || JSON.stringify(data);
448
+ }
449
+ return data.message || data.error || data.msg || data.errors && JSON.stringify(data.errors) || JSON.stringify(data);
450
+ }
451
+ return String(data);
452
+ }
453
+ /**
454
+ * Creates a typed API error class from status code.
455
+ * @internal
456
+ */
457
+ createTypedApiError(status, message, data, url, method) {
458
+ if (status === 429) {
459
+ return new RateLimitError(message, status, data, url, method);
460
+ }
461
+ if (status === 401 || status === 403) {
462
+ return new AuthenticationError(message, status, data, url, method);
463
+ }
464
+ if (status === 400) {
465
+ return new ValidationError(message, status, data, url, method);
466
+ }
467
+ return new APIError(message, status, data, url, method);
468
+ }
355
469
  /**
356
470
  * Sets the API key for authenticated requests.
357
471
  *
@@ -360,12 +474,86 @@ var HttpClient = class {
360
474
  setApiKey(apiKey) {
361
475
  this.apiKey = apiKey;
362
476
  }
477
+ /**
478
+ * Returns the configured API key, if any.
479
+ */
480
+ getApiKey() {
481
+ return this.apiKey;
482
+ }
363
483
  /**
364
484
  * Clears the API key.
365
485
  */
366
486
  clearApiKey() {
367
487
  this.apiKey = void 0;
368
488
  }
489
+ /**
490
+ * Sets HMAC credentials for scoped API-token authentication.
491
+ */
492
+ setHMACCredentials(credentials) {
493
+ this.hmacCredentials = {
494
+ tokenId: credentials.tokenId,
495
+ secret: credentials.secret
496
+ };
497
+ }
498
+ /**
499
+ * Clears HMAC credentials.
500
+ */
501
+ clearHMACCredentials() {
502
+ this.hmacCredentials = void 0;
503
+ }
504
+ /**
505
+ * Returns a copy of the configured HMAC credentials, if any.
506
+ */
507
+ getHMACCredentials() {
508
+ if (!this.hmacCredentials) {
509
+ return void 0;
510
+ }
511
+ return {
512
+ tokenId: this.hmacCredentials.tokenId,
513
+ secret: this.hmacCredentials.secret
514
+ };
515
+ }
516
+ /**
517
+ * Returns the logger attached to this HTTP client.
518
+ */
519
+ getLogger() {
520
+ return this.logger;
521
+ }
522
+ /**
523
+ * Returns true when cookie/header-based auth is configured on the underlying client.
524
+ * This is primarily used for custom authenticated flows that don't use API keys or HMAC.
525
+ */
526
+ hasConfiguredHeaderAuth() {
527
+ const defaultHeaders = this.client.defaults.headers;
528
+ const candidates = [defaultHeaders?.common, defaultHeaders];
529
+ for (const headers of candidates) {
530
+ if (!headers) {
531
+ continue;
532
+ }
533
+ const authValues = [
534
+ headers.Cookie,
535
+ headers.cookie,
536
+ headers.Authorization,
537
+ headers.authorization,
538
+ headers.identity
539
+ ];
540
+ if (authValues.some((value) => typeof value === "string" && value.trim() !== "")) {
541
+ return true;
542
+ }
543
+ }
544
+ return false;
545
+ }
546
+ /**
547
+ * Ensures the client has some authenticated transport configured.
548
+ */
549
+ requireAuth(operation) {
550
+ if (this.apiKey || this.hmacCredentials || this.hasConfiguredHeaderAuth()) {
551
+ return;
552
+ }
553
+ throw new Error(
554
+ `Authentication is required for ${operation}; pass apiKey, hmacCredentials, cookie/auth headers, or set LIMITLESS_API_KEY.`
555
+ );
556
+ }
369
557
  /**
370
558
  * Performs a GET request.
371
559
  *
@@ -377,6 +565,38 @@ var HttpClient = class {
377
565
  const response = await this.client.get(url, config);
378
566
  return response.data;
379
567
  }
568
+ /**
569
+ * Performs a GET request with identity-token authentication.
570
+ */
571
+ async getWithIdentity(url, identityToken, config) {
572
+ const response = await this.client.get(url, {
573
+ ...config,
574
+ identityToken
575
+ });
576
+ return response.data;
577
+ }
578
+ /**
579
+ * Performs a GET request and returns raw response metadata.
580
+ *
581
+ * @remarks
582
+ * Use this when callers need access to status code or headers (e.g. redirect `Location`).
583
+ *
584
+ * @param url - Request URL
585
+ * @param config - Additional request configuration
586
+ * @returns Promise resolving to status, headers, and response data
587
+ */
588
+ async getRaw(url, config) {
589
+ const response = await this.client.get(url, config);
590
+ if (response.status >= 400) {
591
+ const message = this.extractErrorMessage(response.data, `Request failed with status ${response.status}`);
592
+ throw this.createTypedApiError(response.status, message, response.data, url, "GET");
593
+ }
594
+ return {
595
+ status: response.status,
596
+ headers: response.headers,
597
+ data: response.data
598
+ };
599
+ }
380
600
  /**
381
601
  * Performs a POST request.
382
602
  *
@@ -389,6 +609,36 @@ var HttpClient = class {
389
609
  const response = await this.client.post(url, data, config);
390
610
  return response.data;
391
611
  }
612
+ /**
613
+ * Performs a POST request with identity-token authentication.
614
+ */
615
+ async postWithIdentity(url, identityToken, data, config) {
616
+ const response = await this.client.post(url, data, {
617
+ ...config,
618
+ identityToken
619
+ });
620
+ return response.data;
621
+ }
622
+ /**
623
+ * Performs a POST request with additional per-request headers.
624
+ */
625
+ async postWithHeaders(url, data, headers, config) {
626
+ const response = await this.client.post(url, data, {
627
+ ...config,
628
+ headers: {
629
+ ...config?.headers || {},
630
+ ...headers || {}
631
+ }
632
+ });
633
+ return response.data;
634
+ }
635
+ /**
636
+ * Performs a PATCH request.
637
+ */
638
+ async patch(url, data, config) {
639
+ const response = await this.client.patch(url, data, config);
640
+ return response.data;
641
+ }
392
642
  /**
393
643
  * Performs a DELETE request.
394
644
  *
@@ -411,6 +661,28 @@ var HttpClient = class {
411
661
  const response = await this.client.delete(url, deleteConfig);
412
662
  return response.data;
413
663
  }
664
+ getRequestPath(config) {
665
+ const resolved = new URL(config.url || "", config.baseURL || this.client.defaults.baseURL || DEFAULT_API_URL);
666
+ return `${resolved.pathname}${resolved.search}`;
667
+ }
668
+ getRequestBodyForSignature(data) {
669
+ if (data === void 0 || data === null || data === "") {
670
+ return "";
671
+ }
672
+ if (typeof data === "string") {
673
+ return data;
674
+ }
675
+ return JSON.stringify(data);
676
+ }
677
+ maskSensitiveHeaders(headers) {
678
+ const masked = { ...headers };
679
+ for (const key of ["X-API-Key", "lmts-api-key", "lmts-timestamp", "lmts-signature", "identity"]) {
680
+ if (masked[key] !== void 0) {
681
+ masked[key] = key === "identity" ? "Bearer ***" : "***";
682
+ }
683
+ }
684
+ return masked;
685
+ }
414
686
  };
415
687
 
416
688
  // src/api/retry.ts
@@ -594,6 +866,75 @@ var RetryableClient = class {
594
866
  }
595
867
  };
596
868
 
869
+ // src/api-tokens/service.ts
870
+ var ApiTokenService = class {
871
+ constructor(httpClient, logger) {
872
+ this.httpClient = httpClient;
873
+ this.logger = logger || new NoOpLogger();
874
+ }
875
+ async deriveToken(identityToken, input) {
876
+ if (!identityToken) {
877
+ throw new Error("Identity token is required for deriveToken");
878
+ }
879
+ this.logger.debug("Deriving API token", { scopes: input.scopes, label: input.label });
880
+ return this.httpClient.postWithIdentity("/auth/api-tokens/derive", identityToken, input);
881
+ }
882
+ async listTokens() {
883
+ this.httpClient.requireAuth("listTokens");
884
+ return this.httpClient.get("/auth/api-tokens");
885
+ }
886
+ async getCapabilities(identityToken) {
887
+ if (!identityToken) {
888
+ throw new Error("Identity token is required for getCapabilities");
889
+ }
890
+ return this.httpClient.getWithIdentity("/auth/api-tokens/capabilities", identityToken);
891
+ }
892
+ async revokeToken(tokenId) {
893
+ this.httpClient.requireAuth("revokeToken");
894
+ const response = await this.httpClient.delete(`/auth/api-tokens/${encodeURIComponent(tokenId)}`);
895
+ return response.message;
896
+ }
897
+ };
898
+
899
+ // src/partner-accounts/service.ts
900
+ var _PartnerAccountService = class _PartnerAccountService {
901
+ constructor(httpClient, logger) {
902
+ this.httpClient = httpClient;
903
+ this.logger = logger || new NoOpLogger();
904
+ }
905
+ async createAccount(input, eoaHeaders) {
906
+ this.httpClient.requireAuth("createPartnerAccount");
907
+ const serverWalletMode = input.createServerWallet === true;
908
+ if (!serverWalletMode && !eoaHeaders) {
909
+ throw new Error("EOA headers are required when createServerWallet is not true");
910
+ }
911
+ if (input.displayName && input.displayName.length > _PartnerAccountService.DISPLAY_NAME_MAX_LENGTH) {
912
+ throw new Error(
913
+ `displayName must be at most ${_PartnerAccountService.DISPLAY_NAME_MAX_LENGTH} characters`
914
+ );
915
+ }
916
+ this.logger.debug("Creating partner account", {
917
+ displayName: input.displayName,
918
+ createServerWallet: input.createServerWallet
919
+ });
920
+ const payload = {
921
+ displayName: input.displayName,
922
+ createServerWallet: input.createServerWallet
923
+ };
924
+ return this.httpClient.postWithHeaders(
925
+ "/profiles/partner-accounts",
926
+ payload,
927
+ eoaHeaders ? {
928
+ "x-account": eoaHeaders.account,
929
+ "x-signing-message": eoaHeaders.signingMessage,
930
+ "x-signature": eoaHeaders.signature
931
+ } : void 0
932
+ );
933
+ }
934
+ };
935
+ _PartnerAccountService.DISPLAY_NAME_MAX_LENGTH = 44;
936
+ var PartnerAccountService = _PartnerAccountService;
937
+
597
938
  // src/orders/builder.ts
598
939
  import { ethers } from "ethers";
599
940
  var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
@@ -882,6 +1223,104 @@ var OrderBuilder = class {
882
1223
  }
883
1224
  };
884
1225
 
1226
+ // src/delegated-orders/service.ts
1227
+ var DEFAULT_DELEGATED_FEE_RATE_BPS = 300;
1228
+ var DelegatedOrderService = class {
1229
+ constructor(httpClient, logger) {
1230
+ this.httpClient = httpClient;
1231
+ this.logger = logger || new NoOpLogger();
1232
+ }
1233
+ async createOrder(params) {
1234
+ this.httpClient.requireAuth("createDelegatedOrder");
1235
+ if (!Number.isInteger(params.onBehalfOf) || params.onBehalfOf <= 0) {
1236
+ throw new Error("onBehalfOf must be a positive integer");
1237
+ }
1238
+ const feeRateBps = params.feeRateBps && params.feeRateBps > 0 ? params.feeRateBps : DEFAULT_DELEGATED_FEE_RATE_BPS;
1239
+ const builder = new OrderBuilder(ZERO_ADDRESS, feeRateBps);
1240
+ const unsignedOrder = builder.buildOrder(params.args);
1241
+ const payload = {
1242
+ order: {
1243
+ salt: unsignedOrder.salt,
1244
+ maker: unsignedOrder.maker,
1245
+ signer: unsignedOrder.signer,
1246
+ taker: unsignedOrder.taker,
1247
+ tokenId: unsignedOrder.tokenId,
1248
+ makerAmount: unsignedOrder.makerAmount,
1249
+ takerAmount: unsignedOrder.takerAmount,
1250
+ expiration: unsignedOrder.expiration,
1251
+ nonce: unsignedOrder.nonce,
1252
+ feeRateBps: unsignedOrder.feeRateBps,
1253
+ side: unsignedOrder.side,
1254
+ signatureType: 0 /* EOA */,
1255
+ ...unsignedOrder.price !== void 0 ? { price: unsignedOrder.price } : {}
1256
+ },
1257
+ orderType: params.orderType,
1258
+ marketSlug: params.marketSlug,
1259
+ ownerId: params.onBehalfOf,
1260
+ onBehalfOf: params.onBehalfOf
1261
+ };
1262
+ this.logger.debug("Creating delegated order", {
1263
+ marketSlug: params.marketSlug,
1264
+ onBehalfOf: params.onBehalfOf,
1265
+ feeRateBps
1266
+ });
1267
+ return this.httpClient.post("/orders", payload);
1268
+ }
1269
+ async cancel(orderId) {
1270
+ this.httpClient.requireAuth("cancelDelegatedOrder");
1271
+ const response = await this.httpClient.delete(`/orders/${encodeURIComponent(orderId)}`);
1272
+ return response.message;
1273
+ }
1274
+ async cancelOnBehalfOf(orderId, onBehalfOf) {
1275
+ this.httpClient.requireAuth("cancelDelegatedOrder");
1276
+ if (!Number.isInteger(onBehalfOf) || onBehalfOf <= 0) {
1277
+ throw new Error("onBehalfOf must be a positive integer");
1278
+ }
1279
+ const response = await this.httpClient.delete(
1280
+ `/orders/${encodeURIComponent(orderId)}?onBehalfOf=${onBehalfOf}`
1281
+ );
1282
+ return response.message;
1283
+ }
1284
+ async cancelAll(marketSlug) {
1285
+ this.httpClient.requireAuth("cancelAllDelegatedOrders");
1286
+ const response = await this.httpClient.delete(`/orders/all/${encodeURIComponent(marketSlug)}`);
1287
+ return response.message;
1288
+ }
1289
+ async cancelAllOnBehalfOf(marketSlug, onBehalfOf) {
1290
+ this.httpClient.requireAuth("cancelAllDelegatedOrders");
1291
+ if (!Number.isInteger(onBehalfOf) || onBehalfOf <= 0) {
1292
+ throw new Error("onBehalfOf must be a positive integer");
1293
+ }
1294
+ const response = await this.httpClient.delete(
1295
+ `/orders/all/${encodeURIComponent(marketSlug)}?onBehalfOf=${onBehalfOf}`
1296
+ );
1297
+ return response.message;
1298
+ }
1299
+ };
1300
+
1301
+ // src/utils/number-flex.ts
1302
+ function toFiniteNumber(value) {
1303
+ if (typeof value === "number") {
1304
+ return Number.isFinite(value) ? value : void 0;
1305
+ }
1306
+ if (typeof value === "string") {
1307
+ const trimmed = value.trim();
1308
+ if (trimmed.length === 0) {
1309
+ return void 0;
1310
+ }
1311
+ const parsed = Number(trimmed);
1312
+ return Number.isFinite(parsed) ? parsed : void 0;
1313
+ }
1314
+ return void 0;
1315
+ }
1316
+ function toFiniteInteger(value) {
1317
+ const parsed = toFiniteNumber(value);
1318
+ if (parsed === void 0) {
1319
+ return void 0;
1320
+ }
1321
+ return Number.isSafeInteger(parsed) ? parsed : void 0;
1322
+ }
1323
+
885
1324
  // src/orders/signer.ts
886
1325
  var OrderSigner = class {
887
1326
  /**
@@ -1023,7 +1462,7 @@ var OrderValidationError = class extends Error {
1023
1462
  }
1024
1463
  };
1025
1464
  function isFOKOrder(args) {
1026
- return "amount" in args;
1465
+ return "makerAmount" in args;
1027
1466
  }
1028
1467
  function validateOrderArgs(args) {
1029
1468
  if (!args.tokenId) {
@@ -1545,9 +1984,14 @@ var OrderClient = class {
1545
1984
  });
1546
1985
  const portfolioFetcher = new PortfolioFetcher(this.httpClient);
1547
1986
  const profile = await portfolioFetcher.getProfile(this.wallet.address);
1987
+ const userId = toFiniteInteger(profile.id);
1988
+ const feeRateBps = toFiniteInteger(profile.rank?.feeRateBps) ?? 300;
1989
+ if (userId === void 0) {
1990
+ throw new Error(`Invalid user profile id: ${profile.id}`);
1991
+ }
1548
1992
  this.cachedUserData = {
1549
- userId: profile.id,
1550
- feeRateBps: profile.rank?.feeRateBps || 300
1993
+ userId,
1994
+ feeRateBps
1551
1995
  };
1552
1996
  this.orderBuilder = new OrderBuilder(
1553
1997
  this.wallet.address,
@@ -1659,26 +2103,27 @@ var OrderClient = class {
1659
2103
  * @internal
1660
2104
  */
1661
2105
  transformOrderResponse(apiResponse) {
2106
+ const order = apiResponse.order;
1662
2107
  const cleanOrder = {
1663
2108
  order: {
1664
- id: apiResponse.order.id,
1665
- createdAt: apiResponse.order.createdAt,
1666
- makerAmount: apiResponse.order.makerAmount,
1667
- takerAmount: apiResponse.order.takerAmount,
1668
- expiration: apiResponse.order.expiration,
1669
- signatureType: apiResponse.order.signatureType,
1670
- salt: apiResponse.order.salt,
1671
- maker: apiResponse.order.maker,
1672
- signer: apiResponse.order.signer,
1673
- taker: apiResponse.order.taker,
1674
- tokenId: apiResponse.order.tokenId,
1675
- side: apiResponse.order.side,
1676
- feeRateBps: apiResponse.order.feeRateBps,
1677
- nonce: apiResponse.order.nonce,
1678
- signature: apiResponse.order.signature,
1679
- orderType: apiResponse.order.orderType,
1680
- price: apiResponse.order.price,
1681
- marketId: apiResponse.order.marketId
2109
+ id: order.id,
2110
+ createdAt: order.createdAt,
2111
+ makerAmount: toFiniteNumber(order.makerAmount) ?? order.makerAmount,
2112
+ takerAmount: toFiniteNumber(order.takerAmount) ?? order.takerAmount,
2113
+ expiration: order.expiration,
2114
+ signatureType: order.signatureType,
2115
+ salt: toFiniteInteger(order.salt) ?? order.salt,
2116
+ maker: order.maker,
2117
+ signer: order.signer,
2118
+ taker: order.taker,
2119
+ tokenId: order.tokenId,
2120
+ side: order.side,
2121
+ feeRateBps: order.feeRateBps,
2122
+ nonce: order.nonce,
2123
+ signature: order.signature,
2124
+ orderType: order.orderType,
2125
+ price: order.price === void 0 || order.price === null ? order.price : toFiniteNumber(order.price) ?? order.price,
2126
+ marketId: order.marketId
1682
2127
  }
1683
2128
  };
1684
2129
  if (apiResponse.makerMatches && apiResponse.makerMatches.length > 0) {
@@ -1811,6 +2256,174 @@ var OrderClient = class {
1811
2256
  }
1812
2257
  };
1813
2258
 
2259
+ // src/market-pages/fetcher.ts
2260
+ var MAX_REDIRECT_DEPTH = 3;
2261
+ var MarketPageFetcher = class {
2262
+ /**
2263
+ * Creates a new market-pages fetcher.
2264
+ *
2265
+ * @param httpClient - HTTP client for API calls
2266
+ * @param logger - Optional logger
2267
+ */
2268
+ constructor(httpClient, logger) {
2269
+ this.httpClient = httpClient;
2270
+ this.logger = logger || new NoOpLogger();
2271
+ }
2272
+ /**
2273
+ * Gets the navigation tree.
2274
+ */
2275
+ async getNavigation() {
2276
+ this.logger.debug("Fetching navigation tree");
2277
+ return this.httpClient.get("/navigation");
2278
+ }
2279
+ /**
2280
+ * Resolves a market page by path.
2281
+ *
2282
+ * @remarks
2283
+ * Handles 301 redirects manually by re-requesting `/market-pages/by-path` with the
2284
+ * redirected path value from `Location` header.
2285
+ */
2286
+ async getMarketPageByPath(path) {
2287
+ return this.getMarketPageByPathInternal(path, 0);
2288
+ }
2289
+ async getMarketPageByPathInternal(path, depth) {
2290
+ const query = new URLSearchParams({ path }).toString();
2291
+ const endpoint = `/market-pages/by-path?${query}`;
2292
+ const requestConfig = {
2293
+ maxRedirects: 0,
2294
+ validateStatus: (status) => status === 200 || status === 301
2295
+ };
2296
+ this.logger.debug("Resolving market page by path", { path, depth });
2297
+ const response = await this.httpClient.getRaw(endpoint, requestConfig);
2298
+ if (response.status === 200) {
2299
+ return response.data;
2300
+ }
2301
+ if (response.status !== 301) {
2302
+ throw new Error(`Unexpected response status: ${response.status}`);
2303
+ }
2304
+ if (depth >= MAX_REDIRECT_DEPTH) {
2305
+ throw new Error(
2306
+ `Too many redirects while resolving market page path '${path}' (max ${MAX_REDIRECT_DEPTH})`
2307
+ );
2308
+ }
2309
+ const locationHeader = response.headers?.location;
2310
+ const location = Array.isArray(locationHeader) ? locationHeader[0] : locationHeader;
2311
+ if (!location || typeof location !== "string") {
2312
+ throw new Error("Redirect response missing valid Location header");
2313
+ }
2314
+ const redirectedPath = this.extractRedirectPath(location);
2315
+ this.logger.info("Following market page redirect", {
2316
+ from: path,
2317
+ to: redirectedPath,
2318
+ depth: depth + 1
2319
+ });
2320
+ return this.getMarketPageByPathInternal(redirectedPath, depth + 1);
2321
+ }
2322
+ extractRedirectPath(location) {
2323
+ const directByPathPrefix = "/market-pages/by-path";
2324
+ if (location.startsWith(directByPathPrefix)) {
2325
+ const url = new URL(location, "https://api.limitless.exchange");
2326
+ const path = url.searchParams.get("path");
2327
+ if (!path) {
2328
+ throw new Error("Redirect location '/market-pages/by-path' is missing required 'path' query parameter");
2329
+ }
2330
+ return path;
2331
+ }
2332
+ if (/^https?:\/\//i.test(location)) {
2333
+ const url = new URL(location);
2334
+ if (url.pathname === directByPathPrefix) {
2335
+ const path = url.searchParams.get("path");
2336
+ if (!path) {
2337
+ throw new Error("Redirect location '/market-pages/by-path' is missing required 'path' query parameter");
2338
+ }
2339
+ return path;
2340
+ }
2341
+ return url.pathname || "/";
2342
+ }
2343
+ return location;
2344
+ }
2345
+ /**
2346
+ * Gets markets for a market page with optional filtering and pagination.
2347
+ */
2348
+ async getMarkets(pageId, params = {}) {
2349
+ if (params.cursor !== void 0 && params.page !== void 0) {
2350
+ throw new Error("Parameters `cursor` and `page` are mutually exclusive");
2351
+ }
2352
+ const query = new URLSearchParams();
2353
+ if (params.page !== void 0) {
2354
+ query.append("page", String(params.page));
2355
+ }
2356
+ if (params.limit !== void 0) {
2357
+ query.append("limit", String(params.limit));
2358
+ }
2359
+ if (params.sort) {
2360
+ query.append("sort", params.sort);
2361
+ }
2362
+ if (params.cursor !== void 0) {
2363
+ query.append("cursor", params.cursor);
2364
+ }
2365
+ if (params.filters) {
2366
+ for (const [key, value] of Object.entries(params.filters)) {
2367
+ if (Array.isArray(value)) {
2368
+ for (const item of value) {
2369
+ query.append(key, this.stringifyFilterValue(item));
2370
+ }
2371
+ } else {
2372
+ query.append(key, this.stringifyFilterValue(value));
2373
+ }
2374
+ }
2375
+ }
2376
+ const queryString = query.toString();
2377
+ const endpoint = `/market-pages/${pageId}/markets${queryString ? `?${queryString}` : ""}`;
2378
+ this.logger.debug("Fetching market-page markets", { pageId, params });
2379
+ const response = await this.httpClient.get(endpoint);
2380
+ const markets = (response.data || []).map((marketData) => new Market(marketData, this.httpClient));
2381
+ if (response.pagination) {
2382
+ return {
2383
+ data: markets,
2384
+ pagination: response.pagination
2385
+ };
2386
+ }
2387
+ if (response.cursor) {
2388
+ return {
2389
+ data: markets,
2390
+ cursor: response.cursor
2391
+ };
2392
+ }
2393
+ throw new Error("Invalid market-page response: expected `pagination` or `cursor` metadata");
2394
+ }
2395
+ /**
2396
+ * Lists all property keys with options.
2397
+ */
2398
+ async getPropertyKeys() {
2399
+ return this.httpClient.get("/property-keys");
2400
+ }
2401
+ /**
2402
+ * Gets a single property key by ID.
2403
+ */
2404
+ async getPropertyKey(id) {
2405
+ return this.httpClient.get(`/property-keys/${id}`);
2406
+ }
2407
+ /**
2408
+ * Lists options for a property key, optionally filtered by parent option ID.
2409
+ */
2410
+ async getPropertyOptions(keyId, parentId) {
2411
+ const query = new URLSearchParams();
2412
+ if (parentId) {
2413
+ query.append("parentId", parentId);
2414
+ }
2415
+ const queryString = query.toString();
2416
+ const endpoint = `/property-keys/${keyId}/options${queryString ? `?${queryString}` : ""}`;
2417
+ return this.httpClient.get(endpoint);
2418
+ }
2419
+ stringifyFilterValue(value) {
2420
+ if (typeof value === "boolean") {
2421
+ return value ? "true" : "false";
2422
+ }
2423
+ return String(value);
2424
+ }
2425
+ };
2426
+
1814
2427
  // src/websocket/client.ts
1815
2428
  import { io } from "socket.io-client";
1816
2429
  var WebSocketClient = class {
@@ -1829,6 +2442,7 @@ var WebSocketClient = class {
1829
2442
  this.config = {
1830
2443
  url: config.url || DEFAULT_WS_URL,
1831
2444
  apiKey: config.apiKey || process.env.LIMITLESS_API_KEY || "",
2445
+ hmacCredentials: config.hmacCredentials,
1832
2446
  autoReconnect: config.autoReconnect ?? true,
1833
2447
  reconnectDelay: config.reconnectDelay || 1e3,
1834
2448
  maxReconnectAttempts: config.maxReconnectAttempts || Infinity,
@@ -1868,6 +2482,32 @@ var WebSocketClient = class {
1868
2482
  this.reconnectWithNewAuth();
1869
2483
  }
1870
2484
  }
2485
+ /**
2486
+ * Sets HMAC credentials for authenticated subscriptions.
2487
+ *
2488
+ * @remarks
2489
+ * When configured alongside `apiKey`, this client uses HMAC headers for authenticated subscriptions.
2490
+ */
2491
+ setHMACCredentials(hmacCredentials) {
2492
+ this.config.hmacCredentials = {
2493
+ tokenId: hmacCredentials.tokenId,
2494
+ secret: hmacCredentials.secret
2495
+ };
2496
+ if (this.socket?.connected) {
2497
+ this.logger.info("HMAC credentials updated, reconnecting...");
2498
+ this.reconnectWithNewAuth();
2499
+ }
2500
+ }
2501
+ /**
2502
+ * Clears HMAC credentials.
2503
+ */
2504
+ clearHMACCredentials() {
2505
+ this.config.hmacCredentials = void 0;
2506
+ if (this.socket?.connected) {
2507
+ this.logger.info("HMAC credentials cleared, reconnecting...");
2508
+ this.reconnectWithNewAuth();
2509
+ }
2510
+ }
1871
2511
  /**
1872
2512
  * Reconnects with new authentication credentials.
1873
2513
  * @internal
@@ -1913,10 +2553,29 @@ var WebSocketClient = class {
1913
2553
  // Add jitter to prevent thundering herd
1914
2554
  timeout: this.config.timeout
1915
2555
  };
1916
- if (this.config.apiKey) {
2556
+ const extraHeaders = buildWebSocketTrackingHeaders();
2557
+ if (this.config.hmacCredentials) {
2558
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2559
+ const signature = computeHMACSignature(
2560
+ this.config.hmacCredentials.secret,
2561
+ timestamp,
2562
+ "GET",
2563
+ "/socket.io/?EIO=4&transport=websocket",
2564
+ ""
2565
+ );
2566
+ socketOptions.extraHeaders = {
2567
+ ...extraHeaders,
2568
+ "lmts-api-key": this.config.hmacCredentials.tokenId,
2569
+ "lmts-timestamp": timestamp,
2570
+ "lmts-signature": signature
2571
+ };
2572
+ } else if (this.config.apiKey) {
1917
2573
  socketOptions.extraHeaders = {
2574
+ ...extraHeaders,
1918
2575
  "X-API-Key": this.config.apiKey
1919
2576
  };
2577
+ } else if (Object.keys(extraHeaders).length > 0) {
2578
+ socketOptions.extraHeaders = extraHeaders;
1920
2579
  }
1921
2580
  this.socket = io(wsUrl + "/markets", socketOptions);
1922
2581
  this.attachPendingListeners();
@@ -1985,9 +2644,9 @@ var WebSocketClient = class {
1985
2644
  "subscribe_positions",
1986
2645
  "subscribe_transactions"
1987
2646
  ];
1988
- if (authenticatedChannels.includes(channel) && !this.config.apiKey) {
2647
+ if (authenticatedChannels.includes(channel) && !this.config.apiKey && !this.config.hmacCredentials) {
1989
2648
  throw new Error(
1990
- `API key is required for '${channel}' subscription. Please provide an API key in the constructor or set LIMITLESS_API_KEY environment variable. You can generate an API key at https://limitless.exchange`
2649
+ `Authentication is required for '${channel}' subscription. Please provide either apiKey or hmacCredentials when creating the WebSocket client.`
1991
2650
  );
1992
2651
  }
1993
2652
  const subscriptionKey = this.getSubscriptionKey(channel, options);
@@ -2181,37 +2840,105 @@ var WebSocketClient = class {
2181
2840
  return key.split(":")[0];
2182
2841
  }
2183
2842
  };
2843
+
2844
+ // src/client.ts
2845
+ import { ethers as ethers3 } from "ethers";
2846
+ var Client = class _Client {
2847
+ constructor(config = {}) {
2848
+ this.http = new HttpClient(config);
2849
+ const logger = this.http.getLogger?.() || new NoOpLogger();
2850
+ this.markets = new MarketFetcher(this.http, logger);
2851
+ this.portfolio = new PortfolioFetcher(this.http, logger);
2852
+ this.pages = new MarketPageFetcher(this.http, logger);
2853
+ this.apiTokens = new ApiTokenService(this.http, logger);
2854
+ this.partnerAccounts = new PartnerAccountService(this.http, logger);
2855
+ this.delegatedOrders = new DelegatedOrderService(this.http, logger);
2856
+ }
2857
+ /**
2858
+ * Creates a root client around an existing shared HTTP client.
2859
+ */
2860
+ static fromHttpClient(httpClient) {
2861
+ const client = Object.create(_Client.prototype);
2862
+ const logger = httpClient.getLogger?.() || new NoOpLogger();
2863
+ client.http = httpClient;
2864
+ client.markets = new MarketFetcher(httpClient, logger);
2865
+ client.portfolio = new PortfolioFetcher(httpClient, logger);
2866
+ client.pages = new MarketPageFetcher(httpClient, logger);
2867
+ client.apiTokens = new ApiTokenService(httpClient, logger);
2868
+ client.partnerAccounts = new PartnerAccountService(httpClient, logger);
2869
+ client.delegatedOrders = new DelegatedOrderService(httpClient, logger);
2870
+ return client;
2871
+ }
2872
+ /**
2873
+ * Creates a regular EIP-712 order client reusing the shared transport and market cache.
2874
+ */
2875
+ newOrderClient(walletOrPrivateKey, config = {}) {
2876
+ const wallet = typeof walletOrPrivateKey === "string" ? new ethers3.Wallet(walletOrPrivateKey) : walletOrPrivateKey;
2877
+ return new OrderClient({
2878
+ httpClient: this.http,
2879
+ wallet,
2880
+ marketFetcher: this.markets,
2881
+ logger: this.http.getLogger(),
2882
+ ...config
2883
+ });
2884
+ }
2885
+ /**
2886
+ * Creates a WebSocket client reusing shared auth where possible.
2887
+ */
2888
+ newWebSocketClient(config = {}) {
2889
+ return new WebSocketClient(
2890
+ {
2891
+ apiKey: config.apiKey || this.http.getApiKey(),
2892
+ hmacCredentials: config.hmacCredentials || this.http.getHMACCredentials(),
2893
+ ...config
2894
+ },
2895
+ this.http.getLogger()
2896
+ );
2897
+ }
2898
+ };
2184
2899
  export {
2185
2900
  APIError,
2901
+ ApiTokenService,
2186
2902
  AuthenticationError,
2187
2903
  BASE_SEPOLIA_CHAIN_ID,
2188
2904
  CONTRACT_ADDRESSES,
2905
+ Client,
2189
2906
  ConsoleLogger,
2190
2907
  DEFAULT_API_URL,
2191
2908
  DEFAULT_CHAIN_ID,
2192
2909
  DEFAULT_WS_URL,
2910
+ DelegatedOrderService,
2193
2911
  HttpClient,
2194
2912
  Market,
2195
2913
  MarketFetcher,
2914
+ MarketPageFetcher,
2196
2915
  NoOpLogger,
2197
2916
  OrderBuilder,
2198
2917
  OrderClient,
2199
2918
  OrderSigner,
2200
2919
  OrderType,
2201
2920
  OrderValidationError,
2921
+ PartnerAccountService,
2202
2922
  PortfolioFetcher,
2203
2923
  RateLimitError,
2204
2924
  RetryConfig,
2205
2925
  RetryableClient,
2206
2926
  SIGNING_MESSAGE_TEMPLATE,
2927
+ ScopeAccountCreation,
2928
+ ScopeDelegatedSigning,
2929
+ ScopeTrading,
2207
2930
  Side,
2208
2931
  SignatureType,
2209
2932
  ValidationError,
2210
2933
  WebSocketClient,
2211
2934
  WebSocketState,
2212
2935
  ZERO_ADDRESS,
2936
+ buildHMACMessage,
2937
+ computeHMACSignature,
2213
2938
  getContractAddress,
2214
2939
  retryOnErrors,
2940
+ toFiniteInteger,
2941
+ toFiniteNumber,
2215
2942
  validateOrderArgs,
2216
2943
  validateSignedOrder,
2217
2944
  validateUnsignedOrder,