@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
@@ -2,49 +2,82 @@
2
2
  * Multiplies a string representation of a number by a given exponent of base 10 (10exponent).
3
3
  */
4
4
  export function parseUnits(value: string, decimals: number): bigint {
5
- if (!/^(-?)([0-9]*)\.?([0-9]*)$/.test(value)) {
5
+ if (!Number.isInteger(decimals) || decimals < 0) {
6
+ throw new Error(
7
+ `\`decimals\` must be a non-negative integer. Got \`${decimals}\`.`
8
+ )
9
+ }
10
+
11
+ // An input with no digit is 0n: the widget parses an empty amount field.
12
+ if (/^-?\.?$/.test(value)) {
13
+ return 0n
14
+ }
15
+
16
+ if (!/^-?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$/.test(value)) {
6
17
  throw new Error(`Number \`${value}\` is not a valid decimal number.`)
7
18
  }
8
19
 
9
- let [integer, fraction = '0'] = value.split('.')
20
+ let [integer = '', fraction = '0'] = value.split('.')
10
21
 
11
22
  const negative = integer.startsWith('-')
12
23
  if (negative) {
13
24
  integer = integer.slice(1)
14
25
  }
26
+ if (integer === '') {
27
+ integer = '0'
28
+ }
15
29
 
16
30
  // trim trailing zeros.
17
31
  fraction = fraction.replace(/(0+)$/, '')
18
32
 
19
33
  // round off if the fraction is larger than the number of decimals.
20
34
  if (decimals === 0) {
21
- if (Math.round(Number(`.${fraction}`)) === 1) {
35
+ // Round half away from zero by the first fractional digit.
36
+ if (fraction.length > 0 && Number.parseInt(fraction[0], 10) >= 5) {
22
37
  integer = `${BigInt(integer) + 1n}`
23
38
  }
24
39
  fraction = ''
25
40
  } else if (fraction.length > decimals) {
26
- const [left, unit, right] = [
27
- fraction.slice(0, decimals - 1),
28
- fraction.slice(decimals - 1, decimals),
29
- fraction.slice(decimals),
30
- ]
31
-
32
- const rounded = Math.round(Number(`${unit}.${right}`))
33
- if (rounded > 9) {
34
- fraction = `${BigInt(left) + BigInt(1)}0`.padStart(left.length + 1, '0')
35
- } else {
36
- fraction = `${left}${rounded}`
37
- }
41
+ const left = fraction.slice(0, decimals)
42
+ const roundDigit = Number.parseInt(
43
+ fraction.slice(decimals, decimals + 1),
44
+ 10
45
+ )
38
46
 
39
- if (fraction.length > decimals) {
40
- fraction = fraction.slice(1)
41
- integer = `${BigInt(integer) + 1n}`
47
+ if (roundDigit >= 5) {
48
+ // Carry in decimal space, not through a JS number: a float loses
49
+ // precision on long fractions and can round the wrong way.
50
+ const carried = carry(left)
51
+ if (carried.length > decimals) {
52
+ // The carry overflowed into the integer part.
53
+ fraction = carried.slice(1)
54
+ integer = `${BigInt(integer) + 1n}`
55
+ } else {
56
+ fraction = carried
57
+ }
58
+ } else {
59
+ fraction = left
42
60
  }
43
-
44
- fraction = fraction.slice(0, decimals)
45
61
  } else {
46
62
  fraction = fraction.padEnd(decimals, '0')
47
63
  }
48
64
 
49
65
  return BigInt(`${negative ? '-' : ''}${integer}${fraction}`)
50
66
  }
67
+
68
+ /**
69
+ * Adds 1 to a digit string. The result has the same length, unless the carry
70
+ * passes the most significant digit: then it is one digit longer.
71
+ */
72
+ function carry(digits: string): string {
73
+ const out = digits.split('')
74
+ for (let i = out.length - 1; i >= 0; i--) {
75
+ const digit = Number.parseInt(out[i], 10) + 1
76
+ if (digit < 10) {
77
+ out[i] = String(digit)
78
+ return out.join('')
79
+ }
80
+ out[i] = '0'
81
+ }
82
+ return `1${out.join('')}`
83
+ }
@@ -1,5 +1,29 @@
1
- export function sleep(ms: number): Promise<null> {
2
- return new Promise((resolve) => {
3
- setTimeout(() => resolve(null), ms)
1
+ import { getAbortError } from './abort.js'
2
+
3
+ /**
4
+ * Resolves with `null` after `ms`. With a `signal`, it rejects with the abort
5
+ * reason as soon as the signal aborts, and clears its timer.
6
+ */
7
+ export function sleep(
8
+ ms: number,
9
+ { signal }: { signal?: AbortSignal | undefined } = {}
10
+ ): Promise<null> {
11
+ return new Promise((resolve, reject) => {
12
+ if (signal?.aborted) {
13
+ reject(getAbortError(signal))
14
+ return
15
+ }
16
+
17
+ const cleanup = (): void => signal?.removeEventListener('abort', onAbort)
18
+ const timer = setTimeout(() => {
19
+ cleanup()
20
+ resolve(null)
21
+ }, ms)
22
+ const onAbort = (): void => {
23
+ clearTimeout(timer)
24
+ cleanup()
25
+ reject(getAbortError(signal!))
26
+ }
27
+ signal?.addEventListener('abort', onAbort, { once: true })
4
28
  })
5
29
  }
@@ -1,13 +1,13 @@
1
1
  import { sleep } from './sleep.js'
2
2
 
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
  export const waitForResult = async <T>(
13
13
  fn: () => Promise<T | undefined>,
@@ -1,45 +1,139 @@
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
1
+ import { getAbortError } from './abort.js'
2
+ import { LruMap } from './lru.js'
8
3
 
9
- constructor(size: number) {
10
- super()
11
- this.maxSize = size
12
- }
4
+ type Caller = {
5
+ resolve: (value: any) => void
6
+ reject: (error: unknown) => void
7
+ }
13
8
 
14
- override set(key: string, value: value): this {
15
- super.set(key, value)
16
- if (this.maxSize && this.size > this.maxSize) {
17
- this.delete(this.keys().next().value!)
18
- }
19
- return this
20
- }
9
+ type InFlight = {
10
+ promise: Promise<any>
11
+ /** Absent when the first caller had no signal: then nothing can abort it. */
12
+ controller?: AbortController | undefined
13
+ /** Callers with a signal that are still waiting, once the first one joins. */
14
+ callers?: Set<Caller> | undefined
15
+ /** A caller without a signal joined, so the request must run to the end. */
16
+ pinned: boolean
21
17
  }
22
18
 
23
19
  /** @internal */
24
- const promiseCache = /*#__PURE__*/ new LruMap<Promise<any>>(8192)
20
+ const promiseCache = /*#__PURE__*/ new LruMap<InFlight>(8192)
25
21
 
26
22
  type WithDedupeOptions = {
27
23
  enabled?: boolean | undefined
28
24
  id?: string | undefined
25
+ /** Lets this caller leave a shared request without aborting it for others. */
26
+ signal?: AbortSignal | undefined
29
27
  }
30
28
 
31
- /** Deduplicates in-flight promises. */
29
+ /**
30
+ * Deduplicates in-flight promises.
31
+ *
32
+ * When the first caller passes a signal, `fn` receives the signal to hand on
33
+ * to its request. A caller that aborts leaves the shared request at once; the
34
+ * request itself is aborted only when every caller has aborted. The next
35
+ * caller then starts a new request, so `fn` should pass the signal on: a run
36
+ * that ignores it keeps going alongside the new one.
37
+ */
38
+ export function withDedupe<T>(
39
+ fn: (signal?: AbortSignal) => Promise<T>,
40
+ options: WithDedupeOptions
41
+ ): Promise<T>
32
42
  export function withDedupe<T>(
33
43
  fn: () => Promise<T>,
34
- { enabled = true, id }: WithDedupeOptions
44
+ options: WithDedupeOptions & { signal?: undefined }
45
+ ): Promise<T>
46
+ export function withDedupe<T>(
47
+ fn: (signal?: AbortSignal) => Promise<T>,
48
+ { enabled = true, id, signal }: WithDedupeOptions
35
49
  ): Promise<T> {
36
50
  if (!enabled || !id) {
37
- return fn()
51
+ return fn(signal)
52
+ }
53
+ if (signal?.aborted) {
54
+ return Promise.reject(getAbortError(signal))
55
+ }
56
+ let inFlight = promiseCache.get(id)
57
+ if (!inFlight) {
58
+ const controller = signal ? new AbortController() : undefined
59
+ const promise = fn(controller?.signal).finally(() => evict(id, promise))
60
+ inFlight = { promise, controller, pinned: false }
61
+ promiseCache.set(id, inFlight)
38
62
  }
39
- if (promiseCache.get(id)) {
40
- return promiseCache.get(id)!
63
+ return join<T>(id, inFlight, signal)
64
+ }
65
+
66
+ function join<T>(
67
+ id: string,
68
+ inFlight: InFlight,
69
+ signal: AbortSignal | undefined
70
+ ): Promise<T> {
71
+ if (!signal) {
72
+ inFlight.pinned = true
73
+ return inFlight.promise
74
+ }
75
+ const callers = inFlight.callers ?? subscribe(inFlight)
76
+ return new Promise<T>((resolve, reject) => {
77
+ // Stop listening before settling, so a later abort cannot reach the request.
78
+ const caller: Caller = {
79
+ resolve: (value) => {
80
+ signal.removeEventListener('abort', leave)
81
+ resolve(value)
82
+ },
83
+ reject: (error) => {
84
+ signal.removeEventListener('abort', leave)
85
+ reject(error)
86
+ },
87
+ }
88
+ const leave = () => {
89
+ // `leave` can run without the event, so the listener is not always gone.
90
+ signal.removeEventListener('abort', leave)
91
+ callers.delete(caller)
92
+ const reason = getAbortError(signal)
93
+ reject(reason)
94
+ if (!inFlight.pinned && callers.size === 0) {
95
+ // A caller arriving now must start a new request, not join this one.
96
+ evict(id, inFlight.promise)
97
+ inFlight.controller?.abort(reason)
98
+ }
99
+ }
100
+ callers.add(caller)
101
+ signal.addEventListener('abort', leave, { once: true })
102
+ // `fn` runs before this listener exists and may have aborted the signal.
103
+ if (signal.aborted) {
104
+ leave()
105
+ }
106
+ })
107
+ }
108
+
109
+ /**
110
+ * Hands the result to the callers with a signal, through one reaction per
111
+ * request, so a caller that leaves holds nothing here. It is added only when
112
+ * the first such caller joins: a request shared only by callers without a
113
+ * signal gets no handler of ours, and a failure they all ignore is still
114
+ * reported as an unhandled rejection.
115
+ */
116
+ function subscribe(inFlight: InFlight): Set<Caller> {
117
+ const callers = new Set<Caller>()
118
+ inFlight.callers = callers
119
+ inFlight.promise.then(
120
+ (value) => {
121
+ for (const caller of callers) {
122
+ caller.resolve(value)
123
+ }
124
+ },
125
+ (error) => {
126
+ for (const caller of callers) {
127
+ caller.reject(error)
128
+ }
129
+ }
130
+ )
131
+ return callers
132
+ }
133
+
134
+ /** Drops the entry for `id` only while it still holds this request. */
135
+ function evict(id: string, promise: Promise<unknown>): void {
136
+ if (promiseCache.get(id)?.promise === promise) {
137
+ promiseCache.delete(id)
41
138
  }
42
- const promise = fn().finally(() => promiseCache.delete(id))
43
- promiseCache.set(id, promise)
44
- return promise
45
139
  }
@@ -1,3 +1,5 @@
1
+ import { isAbortError } from './abort.js'
2
+
1
3
  /**
2
4
  * Wraps a function in a timeout.
3
5
  * Based on viem's withTimeout implementation.
@@ -25,8 +27,8 @@ export function withTimeout<T>(
25
27
  return new Promise((resolve, reject) => {
26
28
  ;(async () => {
27
29
  let timeoutId!: NodeJS.Timeout
30
+ const controller = new AbortController()
28
31
  try {
29
- const controller = new AbortController()
30
32
  if (timeout > 0) {
31
33
  timeoutId = setTimeout(() => {
32
34
  if (signal) {
@@ -38,8 +40,11 @@ export function withTimeout<T>(
38
40
  }
39
41
  resolve(await fn({ signal: controller?.signal || null }))
40
42
  } catch (err) {
41
- if ((err as Error)?.name === 'AbortError') {
43
+ // Only an abort from our own timeout is a timeout. Any other
44
+ // AbortError belongs to the caller and passes through.
45
+ if (controller.signal.aborted && isAbortError(err)) {
42
46
  reject(errorInstance)
47
+ return
43
48
  }
44
49
  reject(err)
45
50
  } finally {
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export const name = '@lifi/sdk'
2
- export const version = '4.8.2'
2
+ export const version = '4.10.0'