@limitless-exchange/sdk 1.0.1 → 1.0.3

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
@@ -352,6 +352,42 @@ var HttpClient = class {
352
352
  }
353
353
  );
354
354
  }
355
+ /**
356
+ * Extracts a human-readable error message from API response payload.
357
+ * @internal
358
+ */
359
+ extractErrorMessage(data, fallback) {
360
+ if (!data) {
361
+ return fallback;
362
+ }
363
+ if (typeof data === "object") {
364
+ if (Array.isArray(data.message)) {
365
+ const messages = data.message.map((err) => {
366
+ const details = Object.entries(err || {}).filter(([_key, val]) => val !== "" && val !== null && val !== void 0).map(([key, val]) => `${key}: ${val}`).join(", ");
367
+ return details || JSON.stringify(err);
368
+ }).filter((msg) => msg.trim() !== "").join(" | ");
369
+ return messages || data.error || JSON.stringify(data);
370
+ }
371
+ return data.message || data.error || data.msg || data.errors && JSON.stringify(data.errors) || JSON.stringify(data);
372
+ }
373
+ return String(data);
374
+ }
375
+ /**
376
+ * Creates a typed API error class from status code.
377
+ * @internal
378
+ */
379
+ createTypedApiError(status, message, data, url, method) {
380
+ if (status === 429) {
381
+ return new RateLimitError(message, status, data, url, method);
382
+ }
383
+ if (status === 401 || status === 403) {
384
+ return new AuthenticationError(message, status, data, url, method);
385
+ }
386
+ if (status === 400) {
387
+ return new ValidationError(message, status, data, url, method);
388
+ }
389
+ return new APIError(message, status, data, url, method);
390
+ }
355
391
  /**
356
392
  * Sets the API key for authenticated requests.
357
393
  *
@@ -377,6 +413,28 @@ var HttpClient = class {
377
413
  const response = await this.client.get(url, config);
378
414
  return response.data;
379
415
  }
416
+ /**
417
+ * Performs a GET request and returns raw response metadata.
418
+ *
419
+ * @remarks
420
+ * Use this when callers need access to status code or headers (e.g. redirect `Location`).
421
+ *
422
+ * @param url - Request URL
423
+ * @param config - Additional request configuration
424
+ * @returns Promise resolving to status, headers, and response data
425
+ */
426
+ async getRaw(url, config) {
427
+ const response = await this.client.get(url, config);
428
+ if (response.status >= 400) {
429
+ const message = this.extractErrorMessage(response.data, `Request failed with status ${response.status}`);
430
+ throw this.createTypedApiError(response.status, message, response.data, url, "GET");
431
+ }
432
+ return {
433
+ status: response.status,
434
+ headers: response.headers,
435
+ data: response.data
436
+ };
437
+ }
380
438
  /**
381
439
  * Performs a POST request.
382
440
  *
@@ -1023,7 +1081,7 @@ var OrderValidationError = class extends Error {
1023
1081
  }
1024
1082
  };
1025
1083
  function isFOKOrder(args) {
1026
- return "amount" in args;
1084
+ return "makerAmount" in args;
1027
1085
  }
1028
1086
  function validateOrderArgs(args) {
1029
1087
  if (!args.tokenId) {
@@ -1811,6 +1869,174 @@ var OrderClient = class {
1811
1869
  }
1812
1870
  };
1813
1871
 
1872
+ // src/market-pages/fetcher.ts
1873
+ var MAX_REDIRECT_DEPTH = 3;
1874
+ var MarketPageFetcher = class {
1875
+ /**
1876
+ * Creates a new market-pages fetcher.
1877
+ *
1878
+ * @param httpClient - HTTP client for API calls
1879
+ * @param logger - Optional logger
1880
+ */
1881
+ constructor(httpClient, logger) {
1882
+ this.httpClient = httpClient;
1883
+ this.logger = logger || new NoOpLogger();
1884
+ }
1885
+ /**
1886
+ * Gets the navigation tree.
1887
+ */
1888
+ async getNavigation() {
1889
+ this.logger.debug("Fetching navigation tree");
1890
+ return this.httpClient.get("/navigation");
1891
+ }
1892
+ /**
1893
+ * Resolves a market page by path.
1894
+ *
1895
+ * @remarks
1896
+ * Handles 301 redirects manually by re-requesting `/market-pages/by-path` with the
1897
+ * redirected path value from `Location` header.
1898
+ */
1899
+ async getMarketPageByPath(path) {
1900
+ return this.getMarketPageByPathInternal(path, 0);
1901
+ }
1902
+ async getMarketPageByPathInternal(path, depth) {
1903
+ const query = new URLSearchParams({ path }).toString();
1904
+ const endpoint = `/market-pages/by-path?${query}`;
1905
+ const requestConfig = {
1906
+ maxRedirects: 0,
1907
+ validateStatus: (status) => status === 200 || status === 301
1908
+ };
1909
+ this.logger.debug("Resolving market page by path", { path, depth });
1910
+ const response = await this.httpClient.getRaw(endpoint, requestConfig);
1911
+ if (response.status === 200) {
1912
+ return response.data;
1913
+ }
1914
+ if (response.status !== 301) {
1915
+ throw new Error(`Unexpected response status: ${response.status}`);
1916
+ }
1917
+ if (depth >= MAX_REDIRECT_DEPTH) {
1918
+ throw new Error(
1919
+ `Too many redirects while resolving market page path '${path}' (max ${MAX_REDIRECT_DEPTH})`
1920
+ );
1921
+ }
1922
+ const locationHeader = response.headers?.location;
1923
+ const location = Array.isArray(locationHeader) ? locationHeader[0] : locationHeader;
1924
+ if (!location || typeof location !== "string") {
1925
+ throw new Error("Redirect response missing valid Location header");
1926
+ }
1927
+ const redirectedPath = this.extractRedirectPath(location);
1928
+ this.logger.info("Following market page redirect", {
1929
+ from: path,
1930
+ to: redirectedPath,
1931
+ depth: depth + 1
1932
+ });
1933
+ return this.getMarketPageByPathInternal(redirectedPath, depth + 1);
1934
+ }
1935
+ extractRedirectPath(location) {
1936
+ const directByPathPrefix = "/market-pages/by-path";
1937
+ if (location.startsWith(directByPathPrefix)) {
1938
+ const url = new URL(location, "https://api.limitless.exchange");
1939
+ const path = url.searchParams.get("path");
1940
+ if (!path) {
1941
+ throw new Error("Redirect location '/market-pages/by-path' is missing required 'path' query parameter");
1942
+ }
1943
+ return path;
1944
+ }
1945
+ if (/^https?:\/\//i.test(location)) {
1946
+ const url = new URL(location);
1947
+ if (url.pathname === directByPathPrefix) {
1948
+ const path = url.searchParams.get("path");
1949
+ if (!path) {
1950
+ throw new Error("Redirect location '/market-pages/by-path' is missing required 'path' query parameter");
1951
+ }
1952
+ return path;
1953
+ }
1954
+ return url.pathname || "/";
1955
+ }
1956
+ return location;
1957
+ }
1958
+ /**
1959
+ * Gets markets for a market page with optional filtering and pagination.
1960
+ */
1961
+ async getMarkets(pageId, params = {}) {
1962
+ if (params.cursor !== void 0 && params.page !== void 0) {
1963
+ throw new Error("Parameters `cursor` and `page` are mutually exclusive");
1964
+ }
1965
+ const query = new URLSearchParams();
1966
+ if (params.page !== void 0) {
1967
+ query.append("page", String(params.page));
1968
+ }
1969
+ if (params.limit !== void 0) {
1970
+ query.append("limit", String(params.limit));
1971
+ }
1972
+ if (params.sort) {
1973
+ query.append("sort", params.sort);
1974
+ }
1975
+ if (params.cursor !== void 0) {
1976
+ query.append("cursor", params.cursor);
1977
+ }
1978
+ if (params.filters) {
1979
+ for (const [key, value] of Object.entries(params.filters)) {
1980
+ if (Array.isArray(value)) {
1981
+ for (const item of value) {
1982
+ query.append(key, this.stringifyFilterValue(item));
1983
+ }
1984
+ } else {
1985
+ query.append(key, this.stringifyFilterValue(value));
1986
+ }
1987
+ }
1988
+ }
1989
+ const queryString = query.toString();
1990
+ const endpoint = `/market-pages/${pageId}/markets${queryString ? `?${queryString}` : ""}`;
1991
+ this.logger.debug("Fetching market-page markets", { pageId, params });
1992
+ const response = await this.httpClient.get(endpoint);
1993
+ const markets = (response.data || []).map((marketData) => new Market(marketData, this.httpClient));
1994
+ if (response.pagination) {
1995
+ return {
1996
+ data: markets,
1997
+ pagination: response.pagination
1998
+ };
1999
+ }
2000
+ if (response.cursor) {
2001
+ return {
2002
+ data: markets,
2003
+ cursor: response.cursor
2004
+ };
2005
+ }
2006
+ throw new Error("Invalid market-page response: expected `pagination` or `cursor` metadata");
2007
+ }
2008
+ /**
2009
+ * Lists all property keys with options.
2010
+ */
2011
+ async getPropertyKeys() {
2012
+ return this.httpClient.get("/property-keys");
2013
+ }
2014
+ /**
2015
+ * Gets a single property key by ID.
2016
+ */
2017
+ async getPropertyKey(id) {
2018
+ return this.httpClient.get(`/property-keys/${id}`);
2019
+ }
2020
+ /**
2021
+ * Lists options for a property key, optionally filtered by parent option ID.
2022
+ */
2023
+ async getPropertyOptions(keyId, parentId) {
2024
+ const query = new URLSearchParams();
2025
+ if (parentId) {
2026
+ query.append("parentId", parentId);
2027
+ }
2028
+ const queryString = query.toString();
2029
+ const endpoint = `/property-keys/${keyId}/options${queryString ? `?${queryString}` : ""}`;
2030
+ return this.httpClient.get(endpoint);
2031
+ }
2032
+ stringifyFilterValue(value) {
2033
+ if (typeof value === "boolean") {
2034
+ return value ? "true" : "false";
2035
+ }
2036
+ return String(value);
2037
+ }
2038
+ };
2039
+
1814
2040
  // src/websocket/client.ts
1815
2041
  import { io } from "socket.io-client";
1816
2042
  var WebSocketClient = class {
@@ -2193,6 +2419,7 @@ export {
2193
2419
  HttpClient,
2194
2420
  Market,
2195
2421
  MarketFetcher,
2422
+ MarketPageFetcher,
2196
2423
  NoOpLogger,
2197
2424
  OrderBuilder,
2198
2425
  OrderClient,