@lifi/sdk 4.8.2 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/cjs/actions/getChains.js +7 -1
  3. package/dist/cjs/actions/getChains.js.map +1 -1
  4. package/dist/cjs/actions/getContractCallsQuote.js +1 -1
  5. package/dist/cjs/actions/getGasRecommendation.js +1 -1
  6. package/dist/cjs/actions/getQuote.js +1 -1
  7. package/dist/cjs/actions/getRelayedTransactionStatus.js +1 -1
  8. package/dist/cjs/actions/getRelayerQuote.js +1 -1
  9. package/dist/cjs/actions/getRoutes.js +1 -1
  10. package/dist/cjs/actions/getStatus.js +1 -1
  11. package/dist/cjs/actions/getToken.js +1 -1
  12. package/dist/cjs/actions/getTokens.js +7 -1
  13. package/dist/cjs/actions/getTokens.js.map +1 -1
  14. package/dist/cjs/actions/relayTransaction.js +1 -1
  15. package/dist/cjs/client/createClient.js +23 -2
  16. package/dist/cjs/client/createClient.js.map +1 -1
  17. package/dist/cjs/core/tasks/helpers/checkBalance.js +6 -3
  18. package/dist/cjs/core/tasks/helpers/checkBalance.js.map +1 -1
  19. package/dist/cjs/index.d.ts +4 -3
  20. package/dist/cjs/index.js +4 -3
  21. package/dist/cjs/types/core.d.ts +56 -1
  22. package/dist/cjs/utils/abort.d.ts +6 -0
  23. package/dist/cjs/utils/abort.js +10 -0
  24. package/dist/cjs/utils/abort.js.map +1 -0
  25. package/dist/cjs/utils/checkPackageUpdates.js +1 -1
  26. package/dist/cjs/utils/convertQuoteToRoute.js +1 -1
  27. package/dist/cjs/utils/formatUnits.js +1 -0
  28. package/dist/cjs/utils/formatUnits.js.map +1 -1
  29. package/dist/cjs/utils/lru.d.ts +14 -0
  30. package/dist/cjs/utils/lru.js +35 -0
  31. package/dist/cjs/utils/lru.js.map +1 -0
  32. package/dist/cjs/utils/parseUnits.js +30 -16
  33. package/dist/cjs/utils/parseUnits.js.map +1 -1
  34. package/dist/cjs/utils/request.js +2 -2
  35. package/dist/cjs/utils/sleep.d.ts +7 -1
  36. package/dist/cjs/utils/sleep.js +22 -3
  37. package/dist/cjs/utils/sleep.js.map +1 -1
  38. package/dist/cjs/utils/waitForResult.d.ts +4 -4
  39. package/dist/cjs/utils/waitForResult.js +4 -4
  40. package/dist/cjs/utils/waitForResult.js.map +1 -1
  41. package/dist/cjs/utils/withDedupe.d.ts +15 -12
  42. package/dist/cjs/utils/withDedupe.js +69 -25
  43. package/dist/cjs/utils/withDedupe.js.map +1 -1
  44. package/dist/cjs/utils/withTimeout.js +6 -2
  45. package/dist/cjs/utils/withTimeout.js.map +1 -1
  46. package/dist/cjs/version.d.ts +1 -1
  47. package/dist/cjs/version.js +1 -1
  48. package/dist/cjs/version.js.map +1 -1
  49. package/dist/esm/actions/getChains.d.ts.map +1 -1
  50. package/dist/esm/actions/getChains.js +7 -1
  51. package/dist/esm/actions/getChains.js.map +1 -1
  52. package/dist/esm/actions/getContractCallsQuote.js +1 -1
  53. package/dist/esm/actions/getGasRecommendation.js +1 -1
  54. package/dist/esm/actions/getQuote.js +1 -1
  55. package/dist/esm/actions/getRelayedTransactionStatus.js +1 -1
  56. package/dist/esm/actions/getRelayerQuote.js +1 -1
  57. package/dist/esm/actions/getRoutes.js +1 -1
  58. package/dist/esm/actions/getStatus.js +1 -1
  59. package/dist/esm/actions/getToken.js +1 -1
  60. package/dist/esm/actions/getTokens.d.ts.map +1 -1
  61. package/dist/esm/actions/getTokens.js +7 -1
  62. package/dist/esm/actions/getTokens.js.map +1 -1
  63. package/dist/esm/actions/relayTransaction.js +1 -1
  64. package/dist/esm/client/createClient.d.ts.map +1 -1
  65. package/dist/esm/client/createClient.js +23 -2
  66. package/dist/esm/client/createClient.js.map +1 -1
  67. package/dist/esm/core/tasks/helpers/checkBalance.js +6 -3
  68. package/dist/esm/core/tasks/helpers/checkBalance.js.map +1 -1
  69. package/dist/esm/index.d.ts +4 -3
  70. package/dist/esm/index.js +4 -3
  71. package/dist/esm/types/core.d.ts +56 -1
  72. package/dist/esm/types/core.d.ts.map +1 -1
  73. package/dist/esm/utils/abort.d.ts +6 -0
  74. package/dist/esm/utils/abort.d.ts.map +1 -0
  75. package/dist/esm/utils/abort.js +8 -0
  76. package/dist/esm/utils/abort.js.map +1 -0
  77. package/dist/esm/utils/checkPackageUpdates.js +1 -1
  78. package/dist/esm/utils/convertQuoteToRoute.js +1 -1
  79. package/dist/esm/utils/formatUnits.js +1 -0
  80. package/dist/esm/utils/formatUnits.js.map +1 -1
  81. package/dist/esm/utils/lru.d.ts +14 -0
  82. package/dist/esm/utils/lru.d.ts.map +1 -0
  83. package/dist/esm/utils/lru.js +34 -0
  84. package/dist/esm/utils/lru.js.map +1 -0
  85. package/dist/esm/utils/parseUnits.js +30 -16
  86. package/dist/esm/utils/parseUnits.js.map +1 -1
  87. package/dist/esm/utils/request.js +2 -2
  88. package/dist/esm/utils/sleep.d.ts +7 -1
  89. package/dist/esm/utils/sleep.d.ts.map +1 -1
  90. package/dist/esm/utils/sleep.js +22 -3
  91. package/dist/esm/utils/sleep.js.map +1 -1
  92. package/dist/esm/utils/waitForResult.d.ts +4 -4
  93. package/dist/esm/utils/waitForResult.js +4 -4
  94. package/dist/esm/utils/waitForResult.js.map +1 -1
  95. package/dist/esm/utils/withDedupe.d.ts +15 -12
  96. package/dist/esm/utils/withDedupe.d.ts.map +1 -1
  97. package/dist/esm/utils/withDedupe.js +71 -25
  98. package/dist/esm/utils/withDedupe.js.map +1 -1
  99. package/dist/esm/utils/withTimeout.d.ts.map +1 -1
  100. package/dist/esm/utils/withTimeout.js +6 -2
  101. package/dist/esm/utils/withTimeout.js.map +1 -1
  102. package/dist/esm/version.d.ts +1 -1
  103. package/dist/esm/version.js +1 -1
  104. package/dist/esm/version.js.map +1 -1
  105. package/package.json +2 -2
  106. package/src/actions/getChains.ts +12 -4
  107. package/src/actions/getTokens.ts +12 -4
  108. package/src/client/createClient.ts +35 -2
  109. package/src/core/tasks/helpers/checkBalance.ts +11 -3
  110. package/src/index.ts +4 -1
  111. package/src/types/core.ts +59 -1
  112. package/src/utils/abort.ts +10 -0
  113. package/src/utils/formatUnits.ts +6 -0
  114. package/src/utils/lru.ts +38 -0
  115. package/src/utils/parseUnits.ts +53 -20
  116. package/src/utils/sleep.ts +27 -3
  117. package/src/utils/waitForResult.ts +4 -4
  118. package/src/utils/withDedupe.ts +121 -27
  119. package/src/utils/withTimeout.ts +7 -2
  120. package/src/version.ts +1 -1
@@ -1,12 +1,12 @@
1
1
  //#region src/utils/waitForResult.d.ts
2
2
  /**
3
- * Repeatedly calls a given asynchronous function until it resolves with a value
4
- * @param fn The function that should be repeated
3
+ * Repeatedly calls a given asynchronous function until it resolves with a truthy value
4
+ * @param fn The function that should be repeated. A falsy result means "no result yet" and polls again, with no limit on the number of polls
5
5
  * @param interval The timeout in milliseconds between retries, or a function that receives the current poll count and returns the interval. Defaults to 5000
6
- * @param maxRetries Maximum number of retries before throwing an error, defaults to 3
6
+ * @param maxRetries Maximum number of calls that throw before the error is rethrown, defaults to 3
7
7
  * @param shouldRetry Optional predicate to determine if an error should trigger a retry
8
8
  * @returns The result of the fn function
9
- * @throws Error if maximum retries is reached, if function keeps returning undefined, or if shouldRetry returns false
9
+ * @throws The error of fn if maximum retries is reached, or if shouldRetry returns false
10
10
  */
11
11
  export declare const waitForResult: <T>(fn: () => Promise<T | undefined>, interval?: number | ((poll: number) => number), maxRetries?: number, shouldRetry?: (count: number, error: unknown) => boolean) => Promise<T>;
12
12
  //#endregion
@@ -1,13 +1,13 @@
1
1
  import { sleep } from "./sleep.js";
2
2
  //#region src/utils/waitForResult.ts
3
3
  /**
4
- * Repeatedly calls a given asynchronous function until it resolves with a value
5
- * @param fn The function that should be repeated
4
+ * Repeatedly calls a given asynchronous function until it resolves with a truthy value
5
+ * @param fn The function that should be repeated. A falsy result means "no result yet" and polls again, with no limit on the number of polls
6
6
  * @param interval The timeout in milliseconds between retries, or a function that receives the current poll count and returns the interval. Defaults to 5000
7
- * @param maxRetries Maximum number of retries before throwing an error, defaults to 3
7
+ * @param maxRetries Maximum number of calls that throw before the error is rethrown, defaults to 3
8
8
  * @param shouldRetry Optional predicate to determine if an error should trigger a retry
9
9
  * @returns The result of the fn function
10
- * @throws Error if maximum retries is reached, if function keeps returning undefined, or if shouldRetry returns false
10
+ * @throws The error of fn if maximum retries is reached, or if shouldRetry returns false
11
11
  */
12
12
  const waitForResult = async (fn, interval = 5e3, maxRetries = 3, shouldRetry = () => true) => {
13
13
  let result;
@@ -1 +1 @@
1
- {"version":3,"file":"waitForResult.js","names":[],"sources":["../../../src/utils/waitForResult.ts"],"sourcesContent":["import { sleep } from './sleep.js'\n\n/**\n * Repeatedly calls a given asynchronous function until it resolves with a value\n * @param fn The function that should be repeated\n * @param interval The timeout in milliseconds between retries, or a function that receives the current poll count and returns the interval. Defaults to 5000\n * @param maxRetries Maximum number of retries before throwing an error, defaults to 3\n * @param shouldRetry Optional predicate to determine if an error should trigger a retry\n * @returns The result of the fn function\n * @throws Error if maximum retries is reached, if function keeps returning undefined, or if shouldRetry returns false\n */\nexport const waitForResult = async <T>(\n fn: () => Promise<T | undefined>,\n interval: number | ((poll: number) => number) = 5000,\n maxRetries = 3,\n shouldRetry: (count: number, error: unknown) => boolean = () => true\n): Promise<T> => {\n let result: T | undefined\n let attempts = 0\n let polls = 0\n\n const getInterval = typeof interval === 'function' ? interval : () => interval\n\n while (!result) {\n try {\n result = await fn()\n if (!result) {\n await sleep(getInterval(polls))\n polls++\n }\n } catch (error) {\n if (!shouldRetry(attempts, error)) {\n throw error\n }\n attempts++\n if (attempts === maxRetries) {\n throw error\n }\n await sleep(getInterval(polls))\n polls++\n }\n }\n\n return result\n}\n"],"mappings":";;;;;;;;;;;AAWA,MAAa,gBAAgB,OAC3B,IACA,WAAgD,KAChD,aAAa,GACb,oBAAgE,SACjD;CACf,IAAI;CACJ,IAAI,WAAW;CACf,IAAI,QAAQ;CAEZ,MAAM,cAAc,OAAO,aAAa,aAAa,iBAAiB;CAEtE,OAAO,CAAC,QACN,IAAI;EACF,SAAS,MAAM,GAAG;EAClB,IAAI,CAAC,QAAQ;GACX,MAAM,MAAM,YAAY,KAAK,CAAC;GAC9B;EACF;CACF,SAAS,OAAO;EACd,IAAI,CAAC,YAAY,UAAU,KAAK,GAC9B,MAAM;EAER;EACA,IAAI,aAAa,YACf,MAAM;EAER,MAAM,MAAM,YAAY,KAAK,CAAC;EAC9B;CACF;CAGF,OAAO;AACT"}
1
+ {"version":3,"file":"waitForResult.js","names":[],"sources":["../../../src/utils/waitForResult.ts"],"sourcesContent":["import { sleep } from './sleep.js'\n\n/**\n * Repeatedly calls a given asynchronous function until it resolves with a truthy value\n * @param fn The function that should be repeated. A falsy result means \"no result yet\" and polls again, with no limit on the number of polls\n * @param interval The timeout in milliseconds between retries, or a function that receives the current poll count and returns the interval. Defaults to 5000\n * @param maxRetries Maximum number of calls that throw before the error is rethrown, defaults to 3\n * @param shouldRetry Optional predicate to determine if an error should trigger a retry\n * @returns The result of the fn function\n * @throws The error of fn if maximum retries is reached, or if shouldRetry returns false\n */\nexport const waitForResult = async <T>(\n fn: () => Promise<T | undefined>,\n interval: number | ((poll: number) => number) = 5000,\n maxRetries = 3,\n shouldRetry: (count: number, error: unknown) => boolean = () => true\n): Promise<T> => {\n let result: T | undefined\n let attempts = 0\n let polls = 0\n\n const getInterval = typeof interval === 'function' ? interval : () => interval\n\n while (!result) {\n try {\n result = await fn()\n if (!result) {\n await sleep(getInterval(polls))\n polls++\n }\n } catch (error) {\n if (!shouldRetry(attempts, error)) {\n throw error\n }\n attempts++\n if (attempts === maxRetries) {\n throw error\n }\n await sleep(getInterval(polls))\n polls++\n }\n }\n\n return result\n}\n"],"mappings":";;;;;;;;;;;AAWA,MAAa,gBAAgB,OAC3B,IACA,WAAgD,KAChD,aAAa,GACb,oBAAgE,SACjD;CACf,IAAI;CACJ,IAAI,WAAW;CACf,IAAI,QAAQ;CAEZ,MAAM,cAAc,OAAO,aAAa,aAAa,iBAAiB;CAEtE,OAAO,CAAC,QACN,IAAI;EACF,SAAS,MAAM,GAAG;EAClB,IAAI,CAAC,QAAQ;GACX,MAAM,MAAM,YAAY,KAAK,CAAC;GAC9B;EACF;CACF,SAAS,OAAO;EACd,IAAI,CAAC,YAAY,UAAU,KAAK,GAC9B,MAAM;EAER;EACA,IAAI,aAAa,YACf,MAAM;EAER,MAAM,MAAM,YAAY,KAAK,CAAC;EAC9B;CACF;CAGF,OAAO;AACT"}
@@ -1,19 +1,22 @@
1
1
  //#region src/utils/withDedupe.d.ts
2
- /**
3
- * Map with a LRU (Least recently used) policy.
4
- *
5
- * https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU
6
- */
7
- export declare class LruMap<value = unknown> extends Map<string, value> {
8
- maxSize: number;
9
- constructor(size: number);
10
- override set(key: string, value: value): this;
11
- }
12
2
  type WithDedupeOptions = {
13
3
  enabled?: boolean | undefined;
14
4
  id?: string | undefined;
5
+ /** Lets this caller leave a shared request without aborting it for others. */
6
+ signal?: AbortSignal | undefined;
15
7
  };
16
- /** Deduplicates in-flight promises. */
17
- export declare function withDedupe<T>(fn: () => Promise<T>, { enabled, id }: WithDedupeOptions): Promise<T>;
8
+ /**
9
+ * Deduplicates in-flight promises.
10
+ *
11
+ * When the first caller passes a signal, `fn` receives the signal to hand on
12
+ * to its request. A caller that aborts leaves the shared request at once; the
13
+ * request itself is aborted only when every caller has aborted. The next
14
+ * caller then starts a new request, so `fn` should pass the signal on: a run
15
+ * that ignores it keeps going alongside the new one.
16
+ */
17
+ export declare function withDedupe<T>(fn: (signal?: AbortSignal) => Promise<T>, options: WithDedupeOptions): Promise<T>;
18
+ export declare function withDedupe<T>(fn: () => Promise<T>, options: WithDedupeOptions & {
19
+ signal?: undefined;
20
+ }): Promise<T>;
18
21
  //#endregion
19
22
  //# sourceMappingURL=withDedupe.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"withDedupe.d.ts","names":[],"sources":["../../../src/utils/withDedupe.ts"],"mappings":";;;;;;qBAKa,OAAO,yBAAyB,YAAY;EACvD;EAEA,YAAY;WAKH,IAAI,aAAa,OAAO;;KAY9B;EACH;EACA;;;wBAIc,WAAW,GACzB,UAAU,QAAQ,MAChB,SAAgB,MAAM,oBACvB,QAAQ"}
1
+ {"version":3,"file":"withDedupe.d.ts","names":[],"sources":["../../../src/utils/withDedupe.ts"],"mappings":";KAqBK;EACH;EACA;;EAEA,SAAS;;;;;;;;;;;wBAYK,WAAW,GACzB,KAAK,SAAS,gBAAgB,QAAQ,IACtC,SAAS,oBACR,QAAQ;wBACK,WAAW,GACzB,UAAU,QAAQ,IAClB,SAAS;EAAsB;IAC9B,QAAQ"}
@@ -1,32 +1,78 @@
1
+ import { getAbortError } from "./abort.js";
2
+ import { LruMap } from "./lru.js";
1
3
  //#region src/utils/withDedupe.ts
2
- /**
3
- * Map with a LRU (Least recently used) policy.
4
- *
5
- * https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU
6
- */
7
- var LruMap = class extends Map {
8
- maxSize;
9
- constructor(size) {
10
- super();
11
- this.maxSize = size;
12
- }
13
- set(key, value) {
14
- super.set(key, value);
15
- if (this.maxSize && this.size > this.maxSize) this.delete(this.keys().next().value);
16
- return this;
17
- }
18
- };
19
4
  /** @internal */
20
5
  const promiseCache = /*#__PURE__*/ new LruMap(8192);
21
- /** Deduplicates in-flight promises. */
22
- function withDedupe(fn, { enabled = true, id }) {
23
- if (!enabled || !id) return fn();
24
- if (promiseCache.get(id)) return promiseCache.get(id);
25
- const promise = fn().finally(() => promiseCache.delete(id));
26
- promiseCache.set(id, promise);
27
- return promise;
6
+ function withDedupe(fn, { enabled = true, id, signal }) {
7
+ if (!enabled || !id) return fn(signal);
8
+ if (signal?.aborted) return Promise.reject(getAbortError(signal));
9
+ let inFlight = promiseCache.get(id);
10
+ if (!inFlight) {
11
+ const controller = signal ? new AbortController() : void 0;
12
+ const promise = fn(controller?.signal).finally(() => evict(id, promise));
13
+ inFlight = {
14
+ promise,
15
+ controller,
16
+ pinned: false
17
+ };
18
+ promiseCache.set(id, inFlight);
19
+ }
20
+ return join(id, inFlight, signal);
21
+ }
22
+ function join(id, inFlight, signal) {
23
+ if (!signal) {
24
+ inFlight.pinned = true;
25
+ return inFlight.promise;
26
+ }
27
+ const callers = inFlight.callers ?? subscribe(inFlight);
28
+ return new Promise((resolve, reject) => {
29
+ const caller = {
30
+ resolve: (value) => {
31
+ signal.removeEventListener("abort", leave);
32
+ resolve(value);
33
+ },
34
+ reject: (error) => {
35
+ signal.removeEventListener("abort", leave);
36
+ reject(error);
37
+ }
38
+ };
39
+ const leave = () => {
40
+ signal.removeEventListener("abort", leave);
41
+ callers.delete(caller);
42
+ const reason = getAbortError(signal);
43
+ reject(reason);
44
+ if (!inFlight.pinned && callers.size === 0) {
45
+ evict(id, inFlight.promise);
46
+ inFlight.controller?.abort(reason);
47
+ }
48
+ };
49
+ callers.add(caller);
50
+ signal.addEventListener("abort", leave, { once: true });
51
+ if (signal.aborted) leave();
52
+ });
53
+ }
54
+ /**
55
+ * Hands the result to the callers with a signal, through one reaction per
56
+ * request, so a caller that leaves holds nothing here. It is added only when
57
+ * the first such caller joins: a request shared only by callers without a
58
+ * signal gets no handler of ours, and a failure they all ignore is still
59
+ * reported as an unhandled rejection.
60
+ */
61
+ function subscribe(inFlight) {
62
+ const callers = /* @__PURE__ */ new Set();
63
+ inFlight.callers = callers;
64
+ inFlight.promise.then((value) => {
65
+ for (const caller of callers) caller.resolve(value);
66
+ }, (error) => {
67
+ for (const caller of callers) caller.reject(error);
68
+ });
69
+ return callers;
70
+ }
71
+ /** Drops the entry for `id` only while it still holds this request. */
72
+ function evict(id, promise) {
73
+ if (promiseCache.get(id)?.promise === promise) promiseCache.delete(id);
28
74
  }
29
75
  //#endregion
30
- export { LruMap, withDedupe };
76
+ export { withDedupe };
31
77
 
32
78
  //# sourceMappingURL=withDedupe.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"withDedupe.js","names":[],"sources":["../../../src/utils/withDedupe.ts"],"sourcesContent":["/**\n * Map with a LRU (Least recently used) policy.\n *\n * https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU\n */\nexport class LruMap<value = unknown> extends Map<string, value> {\n maxSize: number\n\n constructor(size: number) {\n super()\n this.maxSize = size\n }\n\n override set(key: string, value: value): this {\n super.set(key, value)\n if (this.maxSize && this.size > this.maxSize) {\n this.delete(this.keys().next().value!)\n }\n return this\n }\n}\n\n/** @internal */\nconst promiseCache = /*#__PURE__*/ new LruMap<Promise<any>>(8192)\n\ntype WithDedupeOptions = {\n enabled?: boolean | undefined\n id?: string | undefined\n}\n\n/** Deduplicates in-flight promises. */\nexport function withDedupe<T>(\n fn: () => Promise<T>,\n { enabled = true, id }: WithDedupeOptions\n): Promise<T> {\n if (!enabled || !id) {\n return fn()\n }\n if (promiseCache.get(id)) {\n return promiseCache.get(id)!\n }\n const promise = fn().finally(() => promiseCache.delete(id))\n promiseCache.set(id, promise)\n return promise\n}\n"],"mappings":";;;;;;AAKA,IAAa,SAAb,cAA6C,IAAmB;CAC9D;CAEA,YAAY,MAAc;EACxB,MAAM;EACN,KAAK,UAAU;CACjB;CAEA,IAAa,KAAa,OAAoB;EAC5C,MAAM,IAAI,KAAK,KAAK;EACpB,IAAI,KAAK,WAAW,KAAK,OAAO,KAAK,SACnC,KAAK,OAAO,KAAK,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,KAAM;EAEvC,OAAO;CACT;AACF;;AAGA,MAAM,6BAA6B,IAAI,OAAqB,IAAI;;AAQhE,SAAgB,WACd,IACA,EAAE,UAAU,MAAM,MACN;CACZ,IAAI,CAAC,WAAW,CAAC,IACf,OAAO,GAAG;CAEZ,IAAI,aAAa,IAAI,EAAE,GACrB,OAAO,aAAa,IAAI,EAAE;CAE5B,MAAM,UAAU,GAAG,CAAC,CAAC,cAAc,aAAa,OAAO,EAAE,CAAC;CAC1D,aAAa,IAAI,IAAI,OAAO;CAC5B,OAAO;AACT"}
1
+ {"version":3,"file":"withDedupe.js","names":[],"sources":["../../../src/utils/withDedupe.ts"],"sourcesContent":["import { getAbortError } from './abort.js'\nimport { LruMap } from './lru.js'\n\ntype Caller = {\n resolve: (value: any) => void\n reject: (error: unknown) => void\n}\n\ntype InFlight = {\n promise: Promise<any>\n /** Absent when the first caller had no signal: then nothing can abort it. */\n controller?: AbortController | undefined\n /** Callers with a signal that are still waiting, once the first one joins. */\n callers?: Set<Caller> | undefined\n /** A caller without a signal joined, so the request must run to the end. */\n pinned: boolean\n}\n\n/** @internal */\nconst promiseCache = /*#__PURE__*/ new LruMap<InFlight>(8192)\n\ntype WithDedupeOptions = {\n enabled?: boolean | undefined\n id?: string | undefined\n /** Lets this caller leave a shared request without aborting it for others. */\n signal?: AbortSignal | undefined\n}\n\n/**\n * Deduplicates in-flight promises.\n *\n * When the first caller passes a signal, `fn` receives the signal to hand on\n * to its request. A caller that aborts leaves the shared request at once; the\n * request itself is aborted only when every caller has aborted. The next\n * caller then starts a new request, so `fn` should pass the signal on: a run\n * that ignores it keeps going alongside the new one.\n */\nexport function withDedupe<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: WithDedupeOptions\n): Promise<T>\nexport function withDedupe<T>(\n fn: () => Promise<T>,\n options: WithDedupeOptions & { signal?: undefined }\n): Promise<T>\nexport function withDedupe<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n { enabled = true, id, signal }: WithDedupeOptions\n): Promise<T> {\n if (!enabled || !id) {\n return fn(signal)\n }\n if (signal?.aborted) {\n return Promise.reject(getAbortError(signal))\n }\n let inFlight = promiseCache.get(id)\n if (!inFlight) {\n const controller = signal ? new AbortController() : undefined\n const promise = fn(controller?.signal).finally(() => evict(id, promise))\n inFlight = { promise, controller, pinned: false }\n promiseCache.set(id, inFlight)\n }\n return join<T>(id, inFlight, signal)\n}\n\nfunction join<T>(\n id: string,\n inFlight: InFlight,\n signal: AbortSignal | undefined\n): Promise<T> {\n if (!signal) {\n inFlight.pinned = true\n return inFlight.promise\n }\n const callers = inFlight.callers ?? subscribe(inFlight)\n return new Promise<T>((resolve, reject) => {\n // Stop listening before settling, so a later abort cannot reach the request.\n const caller: Caller = {\n resolve: (value) => {\n signal.removeEventListener('abort', leave)\n resolve(value)\n },\n reject: (error) => {\n signal.removeEventListener('abort', leave)\n reject(error)\n },\n }\n const leave = () => {\n // `leave` can run without the event, so the listener is not always gone.\n signal.removeEventListener('abort', leave)\n callers.delete(caller)\n const reason = getAbortError(signal)\n reject(reason)\n if (!inFlight.pinned && callers.size === 0) {\n // A caller arriving now must start a new request, not join this one.\n evict(id, inFlight.promise)\n inFlight.controller?.abort(reason)\n }\n }\n callers.add(caller)\n signal.addEventListener('abort', leave, { once: true })\n // `fn` runs before this listener exists and may have aborted the signal.\n if (signal.aborted) {\n leave()\n }\n })\n}\n\n/**\n * Hands the result to the callers with a signal, through one reaction per\n * request, so a caller that leaves holds nothing here. It is added only when\n * the first such caller joins: a request shared only by callers without a\n * signal gets no handler of ours, and a failure they all ignore is still\n * reported as an unhandled rejection.\n */\nfunction subscribe(inFlight: InFlight): Set<Caller> {\n const callers = new Set<Caller>()\n inFlight.callers = callers\n inFlight.promise.then(\n (value) => {\n for (const caller of callers) {\n caller.resolve(value)\n }\n },\n (error) => {\n for (const caller of callers) {\n caller.reject(error)\n }\n }\n )\n return callers\n}\n\n/** Drops the entry for `id` only while it still holds this request. */\nfunction evict(id: string, promise: Promise<unknown>): void {\n if (promiseCache.get(id)?.promise === promise) {\n promiseCache.delete(id)\n }\n}\n"],"mappings":";;;;AAmBA,MAAM,6BAA6B,IAAI,OAAiB,IAAI;AA0B5D,SAAgB,WACd,IACA,EAAE,UAAU,MAAM,IAAI,UACV;CACZ,IAAI,CAAC,WAAW,CAAC,IACf,OAAO,GAAG,MAAM;CAElB,IAAI,QAAQ,SACV,OAAO,QAAQ,OAAO,cAAc,MAAM,CAAC;CAE7C,IAAI,WAAW,aAAa,IAAI,EAAE;CAClC,IAAI,CAAC,UAAU;EACb,MAAM,aAAa,SAAS,IAAI,gBAAgB,IAAI,KAAA;EACpD,MAAM,UAAU,GAAG,YAAY,MAAM,CAAC,CAAC,cAAc,MAAM,IAAI,OAAO,CAAC;EACvE,WAAW;GAAE;GAAS;GAAY,QAAQ;EAAM;EAChD,aAAa,IAAI,IAAI,QAAQ;CAC/B;CACA,OAAO,KAAQ,IAAI,UAAU,MAAM;AACrC;AAEA,SAAS,KACP,IACA,UACA,QACY;CACZ,IAAI,CAAC,QAAQ;EACX,SAAS,SAAS;EAClB,OAAO,SAAS;CAClB;CACA,MAAM,UAAU,SAAS,WAAW,UAAU,QAAQ;CACtD,OAAO,IAAI,SAAY,SAAS,WAAW;EAEzC,MAAM,SAAiB;GACrB,UAAU,UAAU;IAClB,OAAO,oBAAoB,SAAS,KAAK;IACzC,QAAQ,KAAK;GACf;GACA,SAAS,UAAU;IACjB,OAAO,oBAAoB,SAAS,KAAK;IACzC,OAAO,KAAK;GACd;EACF;EACA,MAAM,cAAc;GAElB,OAAO,oBAAoB,SAAS,KAAK;GACzC,QAAQ,OAAO,MAAM;GACrB,MAAM,SAAS,cAAc,MAAM;GACnC,OAAO,MAAM;GACb,IAAI,CAAC,SAAS,UAAU,QAAQ,SAAS,GAAG;IAE1C,MAAM,IAAI,SAAS,OAAO;IAC1B,SAAS,YAAY,MAAM,MAAM;GACnC;EACF;EACA,QAAQ,IAAI,MAAM;EAClB,OAAO,iBAAiB,SAAS,OAAO,EAAE,MAAM,KAAK,CAAC;EAEtD,IAAI,OAAO,SACT,MAAM;CAEV,CAAC;AACH;;;;;;;;AASA,SAAS,UAAU,UAAiC;CAClD,MAAM,0BAAU,IAAI,IAAY;CAChC,SAAS,UAAU;CACnB,SAAS,QAAQ,MACd,UAAU;EACT,KAAK,MAAM,UAAU,SACnB,OAAO,QAAQ,KAAK;CAExB,IACC,UAAU;EACT,KAAK,MAAM,UAAU,SACnB,OAAO,OAAO,KAAK;CAEvB,CACF;CACA,OAAO;AACT;;AAGA,SAAS,MAAM,IAAY,SAAiC;CAC1D,IAAI,aAAa,IAAI,EAAE,CAAC,EAAE,YAAY,SACpC,aAAa,OAAO,EAAE;AAE1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"withTimeout.d.ts","names":[],"sources":["../../../src/utils/withTimeout.ts"],"mappings":";;;;;;;;;;wBASgB,YAAY,GAC1B,OAAO;EAAY,QAAQ;MAAuC,QAAQ,MAExE,eACA,SACA;EAGA,gBAAgB;EAEhB;EAEA;IAED,QAAQ"}
1
+ {"version":3,"file":"withTimeout.d.ts","names":[],"sources":["../../../src/utils/withTimeout.ts"],"mappings":";;;;;;;;;;wBAWgB,YAAY,GAC1B,OAAO;EAAY,QAAQ;MAAuC,QAAQ,MAExE,eACA,SACA;EAGA,gBAAgB;EAEhB;EAEA;IAED,QAAQ"}
@@ -1,3 +1,4 @@
1
+ import { isAbortError } from "./abort.js";
1
2
  //#region src/utils/withTimeout.ts
2
3
  /**
3
4
  * Wraps a function in a timeout.
@@ -12,15 +13,18 @@ function withTimeout(fn, { errorInstance = /* @__PURE__ */ new Error("Timed out
12
13
  return new Promise((resolve, reject) => {
13
14
  (async () => {
14
15
  let timeoutId;
16
+ const controller = new AbortController();
15
17
  try {
16
- const controller = new AbortController();
17
18
  if (timeout > 0) timeoutId = setTimeout(() => {
18
19
  if (signal) controller.abort();
19
20
  else reject(errorInstance);
20
21
  }, timeout);
21
22
  resolve(await fn({ signal: controller?.signal || null }));
22
23
  } catch (err) {
23
- if (err?.name === "AbortError") reject(errorInstance);
24
+ if (controller.signal.aborted && isAbortError(err)) {
25
+ reject(errorInstance);
26
+ return;
27
+ }
24
28
  reject(err);
25
29
  } finally {
26
30
  clearTimeout(timeoutId);
@@ -1 +1 @@
1
- {"version":3,"file":"withTimeout.js","names":[],"sources":["../../../src/utils/withTimeout.ts"],"sourcesContent":["/**\n * Wraps a function in a timeout.\n * Based on viem's withTimeout implementation.\n * @param fn - The function to wrap.\n * @param timeout - The timeout in milliseconds.\n * @param errorInstance - The error instance to throw when the timeout is reached.\n * @param signal - Whether or not the timeout should use an abort signal.\n * @returns The result of the function.\n */\nexport function withTimeout<T>(\n fn: ({ signal }: { signal: AbortController['signal'] | null }) => Promise<T>,\n {\n errorInstance = new Error('Timed out after waiting for too long.'),\n timeout,\n signal,\n }: {\n // The error instance to throw when the timeout is reached.\n errorInstance?: Error | undefined\n // The timeout (in ms).\n timeout: number\n // Whether or not the timeout should use an abort signal.\n signal?: boolean | undefined\n }\n): Promise<T> {\n return new Promise((resolve, reject) => {\n ;(async () => {\n let timeoutId!: NodeJS.Timeout\n try {\n const controller = new AbortController()\n if (timeout > 0) {\n timeoutId = setTimeout(() => {\n if (signal) {\n controller.abort()\n } else {\n reject(errorInstance)\n }\n }, timeout) as NodeJS.Timeout // need to cast because bun globals.d.ts overrides @types/node\n }\n resolve(await fn({ signal: controller?.signal || null }))\n } catch (err) {\n if ((err as Error)?.name === 'AbortError') {\n reject(errorInstance)\n }\n reject(err)\n } finally {\n clearTimeout(timeoutId)\n }\n })()\n })\n}\n"],"mappings":";;;;;;;;;;AASA,SAAgB,YACd,IACA,EACE,gCAAgB,IAAI,MAAM,uCAAuC,GACjE,SACA,UASU;CACZ,OAAO,IAAI,SAAS,SAAS,WAAW;EACrC,CAAC,YAAY;GACZ,IAAI;GACJ,IAAI;IACF,MAAM,aAAa,IAAI,gBAAgB;IACvC,IAAI,UAAU,GACZ,YAAY,iBAAiB;KAC3B,IAAI,QACF,WAAW,MAAM;UAEjB,OAAO,aAAa;IAExB,GAAG,OAAO;IAEZ,QAAQ,MAAM,GAAG,EAAE,QAAQ,YAAY,UAAU,KAAK,CAAC,CAAC;GAC1D,SAAS,KAAK;IACZ,IAAK,KAAe,SAAS,cAC3B,OAAO,aAAa;IAEtB,OAAO,GAAG;GACZ,UAAU;IACR,aAAa,SAAS;GACxB;EACF,EAAA,CAAG;CACL,CAAC;AACH"}
1
+ {"version":3,"file":"withTimeout.js","names":[],"sources":["../../../src/utils/withTimeout.ts"],"sourcesContent":["import { isAbortError } from './abort.js'\n\n/**\n * Wraps a function in a timeout.\n * Based on viem's withTimeout implementation.\n * @param fn - The function to wrap.\n * @param timeout - The timeout in milliseconds.\n * @param errorInstance - The error instance to throw when the timeout is reached.\n * @param signal - Whether or not the timeout should use an abort signal.\n * @returns The result of the function.\n */\nexport function withTimeout<T>(\n fn: ({ signal }: { signal: AbortController['signal'] | null }) => Promise<T>,\n {\n errorInstance = new Error('Timed out after waiting for too long.'),\n timeout,\n signal,\n }: {\n // The error instance to throw when the timeout is reached.\n errorInstance?: Error | undefined\n // The timeout (in ms).\n timeout: number\n // Whether or not the timeout should use an abort signal.\n signal?: boolean | undefined\n }\n): Promise<T> {\n return new Promise((resolve, reject) => {\n ;(async () => {\n let timeoutId!: NodeJS.Timeout\n const controller = new AbortController()\n try {\n if (timeout > 0) {\n timeoutId = setTimeout(() => {\n if (signal) {\n controller.abort()\n } else {\n reject(errorInstance)\n }\n }, timeout) as NodeJS.Timeout // need to cast because bun globals.d.ts overrides @types/node\n }\n resolve(await fn({ signal: controller?.signal || null }))\n } catch (err) {\n // Only an abort from our own timeout is a timeout. Any other\n // AbortError belongs to the caller and passes through.\n if (controller.signal.aborted && isAbortError(err)) {\n reject(errorInstance)\n return\n }\n reject(err)\n } finally {\n clearTimeout(timeoutId)\n }\n })()\n })\n}\n"],"mappings":";;;;;;;;;;;AAWA,SAAgB,YACd,IACA,EACE,gCAAgB,IAAI,MAAM,uCAAuC,GACjE,SACA,UASU;CACZ,OAAO,IAAI,SAAS,SAAS,WAAW;EACrC,CAAC,YAAY;GACZ,IAAI;GACJ,MAAM,aAAa,IAAI,gBAAgB;GACvC,IAAI;IACF,IAAI,UAAU,GACZ,YAAY,iBAAiB;KAC3B,IAAI,QACF,WAAW,MAAM;UAEjB,OAAO,aAAa;IAExB,GAAG,OAAO;IAEZ,QAAQ,MAAM,GAAG,EAAE,QAAQ,YAAY,UAAU,KAAK,CAAC,CAAC;GAC1D,SAAS,KAAK;IAGZ,IAAI,WAAW,OAAO,WAAW,aAAa,GAAG,GAAG;KAClD,OAAO,aAAa;KACpB;IACF;IACA,OAAO,GAAG;GACZ,UAAU;IACR,aAAa,SAAS;GACxB;EACF,EAAA,CAAG;CACL,CAAC;AACH"}
@@ -1,5 +1,5 @@
1
1
  //#region src/version.d.ts
2
2
  export declare const name = "@lifi/sdk";
3
- export declare const version = "4.8.2";
3
+ export declare const version = "4.10.0";
4
4
  //#endregion
5
5
  //# sourceMappingURL=version.d.ts.map
@@ -1,6 +1,6 @@
1
1
  //#region src/version.ts
2
2
  const name = "@lifi/sdk";
3
- const version = "4.8.2";
3
+ const version = "4.10.0";
4
4
  //#endregion
5
5
  export { name, version };
6
6
 
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","names":[],"sources":["../../src/version.ts"],"sourcesContent":["export const name = '@lifi/sdk'\nexport const version = '4.8.2'\n"],"mappings":";AAAA,MAAa,OAAO;AACpB,MAAa,UAAU"}
1
+ {"version":3,"file":"version.js","names":[],"sources":["../../src/version.ts"],"sourcesContent":["export const name = '@lifi/sdk'\nexport const version = '4.10.0'\n"],"mappings":";AAAA,MAAa,OAAO;AACpB,MAAa,UAAU"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifi/sdk",
3
- "version": "4.8.2",
3
+ "version": "4.10.0",
4
4
  "description": "LI.FI SDK for Any-to-Any Cross-Chain-Swap",
5
5
  "homepage": "https://github.com/lifinance/sdk",
6
6
  "bugs": {
@@ -28,7 +28,7 @@
28
28
  "./package.json": "./package.json"
29
29
  },
30
30
  "dependencies": {
31
- "@lifi/types": "^18.10.0"
31
+ "@lifi/types": "^18.12.1"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"
@@ -4,6 +4,7 @@ import type {
4
4
  ExtendedChain,
5
5
  RequestOptions,
6
6
  } from '@lifi/types'
7
+ import { SDKError } from '../errors/SDKError.js'
7
8
  import type { SDKBaseConfig, SDKClient } from '../types/core.js'
8
9
  import { request } from '../utils/request.js'
9
10
  import { withDedupe } from '../utils/withDedupe.js'
@@ -41,15 +42,22 @@ export const _getChains = async (
41
42
  params as Record<string, string>
42
43
  ).toString()
43
44
  const response = await withDedupe(
44
- () =>
45
+ (signal) =>
45
46
  request<ChainsResponse>(
46
47
  config,
47
48
  `${config.apiUrl}/chains?${urlSearchParams}`,
48
49
  {
49
- signal: options?.signal,
50
+ signal,
50
51
  }
51
52
  ),
52
- { id: `${getChains.name}.${config.apiUrl}.${urlSearchParams}` }
53
- )
53
+ {
54
+ id: `${getChains.name}.${config.apiUrl}.${urlSearchParams}`,
55
+ signal: options?.signal,
56
+ }
57
+ ).catch((error) => {
58
+ // A caller that aborts leaves before the request settles; keep its error
59
+ // shaped like one from `request`.
60
+ throw error instanceof SDKError ? error : new SDKError(error)
61
+ })
54
62
  return response.chains
55
63
  }
@@ -4,6 +4,7 @@ import type {
4
4
  TokensRequest,
5
5
  TokensResponse,
6
6
  } from '@lifi/types'
7
+ import { SDKError } from '../errors/SDKError.js'
7
8
  import type { SDKClient } from '../types/core.js'
8
9
  import { request } from '../utils/request.js'
9
10
  import { withDedupe } from '../utils/withDedupe.js'
@@ -43,15 +44,22 @@ export async function getTokens(
43
44
  ).toString()
44
45
  const _isExtended = params?.extended === true
45
46
  const response = await withDedupe(
46
- () =>
47
+ (signal) =>
47
48
  request<
48
49
  typeof _isExtended extends true
49
50
  ? TokensExtendedResponse
50
51
  : TokensResponse
51
52
  >(client.config, `${client.config.apiUrl}/tokens?${urlSearchParams}`, {
52
- signal: options?.signal,
53
+ signal,
53
54
  }),
54
- { id: `${getTokens.name}.${client.config.apiUrl}.${urlSearchParams}` }
55
- )
55
+ {
56
+ id: `${getTokens.name}.${client.config.apiUrl}.${urlSearchParams}`,
57
+ signal: options?.signal,
58
+ }
59
+ ).catch((error) => {
60
+ // A caller that aborts leaves before the request settles; keep its error
61
+ // shaped like one from `request`.
62
+ throw error instanceof SDKError ? error : new SDKError(error)
63
+ })
56
64
  return response
57
65
  }
@@ -1,5 +1,6 @@
1
1
  import type { ChainId, ChainType, ExtendedChain } from '@lifi/types'
2
2
  import type {
3
+ RPCUrls,
3
4
  SDKBaseConfig,
4
5
  SDKClient,
5
6
  SDKConfig,
@@ -20,12 +21,38 @@ export function createClient(options: SDKConfig): SDKClient {
20
21
  checkPackageUpdates(name, version)
21
22
  }
22
23
 
23
- const { providers, ...configOptions } = options
24
+ const { providers, rpcUrls, ...configOptions } = options
25
+
26
+ // Role entries split once, here: the config keeps plain read lists, so
27
+ // everything that reads `config.rpcUrls` sees `string[]` as before.
28
+ const readRpcUrls: RPCUrls = {}
29
+ const writeRpcUrls: RPCUrls = {}
30
+ const bundleRpcUrls: RPCUrls = {}
31
+ let hasRoles = false
32
+ for (const key in rpcUrls) {
33
+ const chainId = Number(key) as ChainId
34
+ const entry = rpcUrls[chainId]
35
+ if (Array.isArray(entry)) {
36
+ readRpcUrls[chainId] = entry
37
+ } else if (entry) {
38
+ hasRoles = true
39
+ if (entry.read) {
40
+ readRpcUrls[chainId] = entry.read
41
+ }
42
+ if (entry.write?.length) {
43
+ writeRpcUrls[chainId] = entry.write
44
+ }
45
+ if (entry.bundle?.length) {
46
+ bundleRpcUrls[chainId] = entry.bundle
47
+ }
48
+ }
49
+ }
24
50
 
25
51
  const _config: SDKBaseConfig = {
26
52
  ...configOptions,
27
53
  apiUrl: configOptions?.apiUrl ?? 'https://li.quest/v1',
28
- rpcUrls: configOptions?.rpcUrls ?? {},
54
+ // Plain lists only: the caller's own object, exactly as before.
55
+ rpcUrls: hasRoles ? readRpcUrls : ((rpcUrls ?? {}) as RPCUrls),
29
56
  debug: configOptions?.debug ?? false,
30
57
  preloadChains: configOptions?.preloadChains ?? true,
31
58
  integrator: configOptions?.integrator ?? 'lifi-sdk',
@@ -78,6 +105,12 @@ export function createClient(options: SDKConfig): SDKClient {
78
105
  }
79
106
  return chainRpcUrls
80
107
  },
108
+ async getWriteRpcUrlsByChainId(chainId: ChainId) {
109
+ return writeRpcUrls[chainId] ?? []
110
+ },
111
+ async getBundleRpcUrlsByChainId(chainId: ChainId) {
112
+ return bundleRpcUrls[chainId] ?? []
113
+ },
81
114
  }
82
115
 
83
116
  function extend<TClient extends SDKClient>(
@@ -118,6 +118,11 @@ export const checkBalance = async (
118
118
  Math.floor((1 - slippage) * Number(SLIPPAGE_PRECISION))
119
119
  )
120
120
 
121
+ // Aborted once `withTimeout` settles. A timeout rejects at once, but the
122
+ // loop keeps running; the signal stops it before it can change `step`.
123
+ const controller = new AbortController()
124
+ const { signal } = controller
125
+
121
126
  await withTimeout(
122
127
  async () => {
123
128
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
@@ -133,9 +138,12 @@ export const checkBalance = async (
133
138
  error as Error
134
139
  )
135
140
  }
136
- await sleep(BACKOFF_BASE_MS * 2 ** attempt)
141
+ await sleep(BACKOFF_BASE_MS * 2 ** attempt, { signal })
137
142
  continue
138
143
  }
144
+ if (signal.aborted) {
145
+ return
146
+ }
139
147
 
140
148
  const balanceByAddress = new Map(
141
149
  balances.map((b) => [b.address.toLowerCase(), b.amount] as const)
@@ -207,12 +215,12 @@ export const checkBalance = async (
207
215
  )
208
216
  }
209
217
 
210
- await sleep(BACKOFF_BASE_MS * 2 ** attempt)
218
+ await sleep(BACKOFF_BASE_MS * 2 ** attempt, { signal })
211
219
  }
212
220
  },
213
221
  {
214
222
  timeout: OVERALL_TIMEOUT_MS,
215
223
  errorInstance: new BalanceError('Could not read wallet balance.'),
216
224
  }
217
- )
225
+ ).finally(() => controller.abort())
218
226
  }
package/src/index.ts CHANGED
@@ -97,6 +97,8 @@ export type {
97
97
  RouteExecutionDictionary,
98
98
  RouteExtended,
99
99
  RPCUrls,
100
+ RPCUrlsByRole,
101
+ RPCUrlsConfig,
100
102
  SDKBaseConfig,
101
103
  SDKClient,
102
104
  SDKConfig,
@@ -121,9 +123,10 @@ export { convertQuoteToRoute } from './utils/convertQuoteToRoute.js'
121
123
  export { fetchTxErrorDetails } from './utils/fetchTxErrorDetails.js'
122
124
  export { formatUnits } from './utils/formatUnits.js'
123
125
  export { isHex } from './utils/isHex.js'
126
+ export { LruMap } from './utils/lru.js'
124
127
  export { parseUnits } from './utils/parseUnits.js'
125
128
  export { sleep } from './utils/sleep.js'
126
129
  export { waitForResult } from './utils/waitForResult.js'
127
- export { LruMap, withDedupe } from './utils/withDedupe.js'
130
+ export { withDedupe } from './utils/withDedupe.js'
128
131
  export { withTimeout } from './utils/withTimeout.js'
129
132
  export { name, version } from './version.js'
package/src/types/core.ts CHANGED
@@ -38,11 +38,55 @@ export interface SDKBaseConfig {
38
38
  storage?: SDKStorage
39
39
  }
40
40
 
41
- export interface SDKConfig extends Partial<Omit<SDKBaseConfig, 'integrator'>> {
41
+ export interface SDKConfig
42
+ extends Partial<Omit<SDKBaseConfig, 'integrator' | 'rpcUrls'>> {
42
43
  integrator: string
44
+ /**
45
+ * Per chain, one list for reads and sends, or lists by role. The client
46
+ * keeps the read lists in `config.rpcUrls` and serves the write and bundle
47
+ * lists through `getWriteRpcUrlsByChainId` and `getBundleRpcUrlsByChainId`.
48
+ */
49
+ rpcUrls?: RPCUrlsConfig
43
50
  providers?: SDKProvider[]
44
51
  }
45
52
 
53
+ /**
54
+ * RPC URLs for one chain, split by what they are used for.
55
+ */
56
+ export interface RPCUrlsByRole {
57
+ /**
58
+ * Reads: balances, simulation and confirmation. Unset, the chain's own RPC
59
+ * URLs serve reads, as for a chain with no `rpcUrls` entry.
60
+ */
61
+ read?: string[]
62
+ /**
63
+ * Sends. Unset or empty, the read URLs send as well. While `write` is set,
64
+ * nothing is sent through the read URLs: when `bundle` is unset, the write
65
+ * URLs that support bundles also submit bundles, and with none of them, a
66
+ * route that needs a bundle fails.
67
+ *
68
+ * Only `@lifi/sdk-provider-solana` uses this today; other providers ignore
69
+ * it.
70
+ */
71
+ write?: string[]
72
+ /**
73
+ * Bundle submissions (Jito `sendBundle`). Unset, empty, or with no URL that
74
+ * supports bundles, the write URLs that do submit them. While `bundle` or
75
+ * `write` is set, bundles never go to the read URLs.
76
+ *
77
+ * Only `@lifi/sdk-provider-solana` uses this today; other providers ignore
78
+ * it.
79
+ */
80
+ bundle?: string[]
81
+ }
82
+
83
+ /**
84
+ * The `rpcUrls` option of `createClient`: per chain, either one list for reads
85
+ * and sends, or lists by role.
86
+ */
87
+ export type RPCUrlsConfig = Partial<Record<ChainId, string[] | RPCUrlsByRole>>
88
+
89
+ /** RPC URL lists per chain. `client.config.rpcUrls` holds the read lists. */
46
90
  export type RPCUrls = Partial<Record<ChainId, string[]>>
47
91
 
48
92
  export interface SDKProvider {
@@ -78,6 +122,20 @@ export interface SDKClient {
78
122
  getChainById(chainId: ChainId): Promise<ExtendedChain>
79
123
  getRpcUrls(): Promise<RPCUrls>
80
124
  getRpcUrlsByChainId(chainId: ChainId): Promise<string[]>
125
+ /**
126
+ * The chain's dedicated write RPC URLs (`rpcUrls[chainId].write`). Empty
127
+ * when the chain has none.
128
+ *
129
+ * Optional so clients from other SDK versions, and hand-written ones, still
130
+ * satisfy `SDKClient`. Providers treat a missing method as no write list.
131
+ */
132
+ getWriteRpcUrlsByChainId?(chainId: ChainId): Promise<string[]>
133
+ /**
134
+ * The chain's dedicated bundle RPC URLs (`rpcUrls[chainId].bundle`). Empty
135
+ * when the chain has none. Optional for the same reason as
136
+ * `getWriteRpcUrlsByChainId`.
137
+ */
138
+ getBundleRpcUrlsByChainId?(chainId: ChainId): Promise<string[]>
81
139
  }
82
140
 
83
141
  export interface StepExecutorOptions {
@@ -0,0 +1,10 @@
1
+ /** Older runtimes and polyfills can abort a signal without a `reason`. */
2
+ export const getAbortError = (signal: AbortSignal): unknown =>
3
+ signal.reason ??
4
+ Object.assign(new Error('This operation was aborted'), { name: 'AbortError' })
5
+
6
+ export const isAbortError = (error: unknown): boolean =>
7
+ typeof error === 'object' &&
8
+ error !== null &&
9
+ 'name' in error &&
10
+ error.name === 'AbortError'
@@ -2,6 +2,12 @@
2
2
  * Divides a number by a given exponent of base 10 (10exponent), and formats it into a string representation of the number.
3
3
  */
4
4
  export function formatUnits(value: bigint, decimals: number): string {
5
+ if (!Number.isInteger(decimals) || decimals < 0) {
6
+ throw new Error(
7
+ `\`decimals\` must be a non-negative integer. Got \`${decimals}\`.`
8
+ )
9
+ }
10
+
5
11
  let display = value.toString()
6
12
 
7
13
  const negative = display.startsWith('-')
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Map with a LRU (Least recently used) policy.
3
+ *
4
+ * https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU
5
+ */
6
+ export class LruMap<value = unknown> extends Map<string, value> {
7
+ maxSize: number
8
+
9
+ constructor(size: number) {
10
+ super()
11
+ this.maxSize = size
12
+ }
13
+
14
+ override get(key: string): value | undefined {
15
+ const value = super.get(key)
16
+ if (super.has(key)) {
17
+ super.delete(key)
18
+ super.set(key, value as value)
19
+ }
20
+ return value
21
+ }
22
+
23
+ override set(key: string, value: value): this {
24
+ if (super.has(key)) {
25
+ super.delete(key)
26
+ }
27
+ super.set(key, value)
28
+ if (this.maxSize && this.size > this.maxSize) {
29
+ // `super.keys()`, not `this.keys()`: on iOS 18 the subclass iterator
30
+ // can yield `undefined`, and the map then grows without limit.
31
+ const firstKey = super.keys().next().value
32
+ if (firstKey !== undefined) {
33
+ super.delete(firstKey)
34
+ }
35
+ }
36
+ return this
37
+ }
38
+ }