@lifi/sdk 4.8.1 → 4.9.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 (68) hide show
  1. package/CHANGELOG.md +32 -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 +27 -6
  16. package/dist/cjs/client/createClient.js.map +1 -1
  17. package/dist/cjs/index.d.ts +2 -2
  18. package/dist/cjs/index.js +2 -2
  19. package/dist/cjs/types/core.d.ts +56 -1
  20. package/dist/cjs/utils/checkPackageUpdates.js +1 -1
  21. package/dist/cjs/utils/convertQuoteToRoute.js +1 -1
  22. package/dist/cjs/utils/request.js +2 -2
  23. package/dist/cjs/utils/withDedupe.d.ts +15 -2
  24. package/dist/cjs/utils/withDedupe.js +70 -7
  25. package/dist/cjs/utils/withDedupe.js.map +1 -1
  26. package/dist/cjs/version.d.ts +1 -1
  27. package/dist/cjs/version.js +1 -1
  28. package/dist/cjs/version.js.map +1 -1
  29. package/dist/esm/actions/getChains.d.ts.map +1 -1
  30. package/dist/esm/actions/getChains.js +7 -1
  31. package/dist/esm/actions/getChains.js.map +1 -1
  32. package/dist/esm/actions/getContractCallsQuote.js +1 -1
  33. package/dist/esm/actions/getGasRecommendation.js +1 -1
  34. package/dist/esm/actions/getQuote.js +1 -1
  35. package/dist/esm/actions/getRelayedTransactionStatus.js +1 -1
  36. package/dist/esm/actions/getRelayerQuote.js +1 -1
  37. package/dist/esm/actions/getRoutes.js +1 -1
  38. package/dist/esm/actions/getStatus.js +1 -1
  39. package/dist/esm/actions/getToken.js +1 -1
  40. package/dist/esm/actions/getTokens.d.ts.map +1 -1
  41. package/dist/esm/actions/getTokens.js +7 -1
  42. package/dist/esm/actions/getTokens.js.map +1 -1
  43. package/dist/esm/actions/relayTransaction.js +1 -1
  44. package/dist/esm/client/createClient.d.ts.map +1 -1
  45. package/dist/esm/client/createClient.js +27 -6
  46. package/dist/esm/client/createClient.js.map +1 -1
  47. package/dist/esm/index.d.ts +2 -2
  48. package/dist/esm/index.js +2 -2
  49. package/dist/esm/types/core.d.ts +56 -1
  50. package/dist/esm/types/core.d.ts.map +1 -1
  51. package/dist/esm/utils/checkPackageUpdates.js +1 -1
  52. package/dist/esm/utils/convertQuoteToRoute.js +1 -1
  53. package/dist/esm/utils/request.js +2 -2
  54. package/dist/esm/utils/withDedupe.d.ts +15 -2
  55. package/dist/esm/utils/withDedupe.d.ts.map +1 -1
  56. package/dist/esm/utils/withDedupe.js +70 -7
  57. package/dist/esm/utils/withDedupe.js.map +1 -1
  58. package/dist/esm/version.d.ts +1 -1
  59. package/dist/esm/version.js +1 -1
  60. package/dist/esm/version.js.map +1 -1
  61. package/package.json +1 -1
  62. package/src/actions/getChains.ts +12 -4
  63. package/src/actions/getTokens.ts +12 -4
  64. package/src/client/createClient.ts +47 -3
  65. package/src/index.ts +2 -0
  66. package/src/types/core.ts +59 -1
  67. package/src/utils/withDedupe.ts +127 -9
  68. package/src/version.ts +1 -1
@@ -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>(
@@ -87,7 +120,18 @@ export function createClient(options: SDKConfig): SDKClient {
87
120
  ) => TClient & TExtensions {
88
121
  return (extendFn) => {
89
122
  const extensions = extendFn(base)
90
- const extended = { ...base, ...extensions } as TClient & typeof extensions
123
+ // Copy descriptors rather than spreading. `config` and `providers` are
124
+ // accessors over closure variables, and `setProviders` reassigns
125
+ // `_providers` — so a spread would freeze the extension on the values
126
+ // those getters happened to return at extend time, leaving it with a
127
+ // provider list that never updates.
128
+ const extended = Object.defineProperties(
129
+ {},
130
+ {
131
+ ...Object.getOwnPropertyDescriptors(base),
132
+ ...Object.getOwnPropertyDescriptors(extensions),
133
+ }
134
+ ) as TClient & typeof extensions
91
135
 
92
136
  // Preserve the extend function for further extensions
93
137
  return Object.assign(extended, {
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,
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 {
@@ -20,26 +20,144 @@ export class LruMap<value = unknown> extends Map<string, value> {
20
20
  }
21
21
  }
22
22
 
23
+ type Caller = {
24
+ resolve: (value: any) => void
25
+ reject: (error: unknown) => void
26
+ }
27
+
28
+ type InFlight = {
29
+ promise: Promise<any>
30
+ /** Absent when the first caller had no signal: then nothing can abort it. */
31
+ controller?: AbortController | undefined
32
+ /** Callers with a signal that are still waiting, once the first one joins. */
33
+ callers?: Set<Caller> | undefined
34
+ /** A caller without a signal joined, so the request must run to the end. */
35
+ pinned: boolean
36
+ }
37
+
23
38
  /** @internal */
24
- const promiseCache = /*#__PURE__*/ new LruMap<Promise<any>>(8192)
39
+ const promiseCache = /*#__PURE__*/ new LruMap<InFlight>(8192)
25
40
 
26
41
  type WithDedupeOptions = {
27
42
  enabled?: boolean | undefined
28
43
  id?: string | undefined
44
+ /** Lets this caller leave a shared request without aborting it for others. */
45
+ signal?: AbortSignal | undefined
29
46
  }
30
47
 
31
- /** Deduplicates in-flight promises. */
48
+ /**
49
+ * Deduplicates in-flight promises.
50
+ *
51
+ * When the first caller passes a signal, `fn` receives the signal to hand on
52
+ * to its request. A caller that aborts leaves the shared request at once; the
53
+ * request itself is aborted only when every caller has aborted. The next
54
+ * caller then starts a new request, so `fn` should pass the signal on: a run
55
+ * that ignores it keeps going alongside the new one.
56
+ */
57
+ export function withDedupe<T>(
58
+ fn: (signal?: AbortSignal) => Promise<T>,
59
+ options: WithDedupeOptions
60
+ ): Promise<T>
32
61
  export function withDedupe<T>(
33
62
  fn: () => Promise<T>,
34
- { enabled = true, id }: WithDedupeOptions
63
+ options: WithDedupeOptions & { signal?: undefined }
64
+ ): Promise<T>
65
+ export function withDedupe<T>(
66
+ fn: (signal?: AbortSignal) => Promise<T>,
67
+ { enabled = true, id, signal }: WithDedupeOptions
35
68
  ): Promise<T> {
36
69
  if (!enabled || !id) {
37
- return fn()
70
+ return fn(signal)
71
+ }
72
+ if (signal?.aborted) {
73
+ return Promise.reject(abortReason(signal))
74
+ }
75
+ let inFlight = promiseCache.get(id)
76
+ if (!inFlight) {
77
+ const controller = signal ? new AbortController() : undefined
78
+ const promise = fn(controller?.signal).finally(() => evict(id, promise))
79
+ inFlight = { promise, controller, pinned: false }
80
+ promiseCache.set(id, inFlight)
81
+ }
82
+ return join<T>(id, inFlight, signal)
83
+ }
84
+
85
+ function join<T>(
86
+ id: string,
87
+ inFlight: InFlight,
88
+ signal: AbortSignal | undefined
89
+ ): Promise<T> {
90
+ if (!signal) {
91
+ inFlight.pinned = true
92
+ return inFlight.promise
38
93
  }
39
- if (promiseCache.get(id)) {
40
- return promiseCache.get(id)!
94
+ const callers = inFlight.callers ?? subscribe(inFlight)
95
+ return new Promise<T>((resolve, reject) => {
96
+ // Stop listening before settling, so a later abort cannot reach the request.
97
+ const caller: Caller = {
98
+ resolve: (value) => {
99
+ signal.removeEventListener('abort', leave)
100
+ resolve(value)
101
+ },
102
+ reject: (error) => {
103
+ signal.removeEventListener('abort', leave)
104
+ reject(error)
105
+ },
106
+ }
107
+ const leave = () => {
108
+ // `leave` can run without the event, so the listener is not always gone.
109
+ signal.removeEventListener('abort', leave)
110
+ callers.delete(caller)
111
+ const reason = abortReason(signal)
112
+ reject(reason)
113
+ if (!inFlight.pinned && callers.size === 0) {
114
+ // A caller arriving now must start a new request, not join this one.
115
+ evict(id, inFlight.promise)
116
+ inFlight.controller?.abort(reason)
117
+ }
118
+ }
119
+ callers.add(caller)
120
+ signal.addEventListener('abort', leave, { once: true })
121
+ // `fn` runs before this listener exists and may have aborted the signal.
122
+ if (signal.aborted) {
123
+ leave()
124
+ }
125
+ })
126
+ }
127
+
128
+ /**
129
+ * Hands the result to the callers with a signal, through one reaction per
130
+ * request, so a caller that leaves holds nothing here. It is added only when
131
+ * the first such caller joins: a request shared only by callers without a
132
+ * signal gets no handler of ours, and a failure they all ignore is still
133
+ * reported as an unhandled rejection.
134
+ */
135
+ function subscribe(inFlight: InFlight): Set<Caller> {
136
+ const callers = new Set<Caller>()
137
+ inFlight.callers = callers
138
+ inFlight.promise.then(
139
+ (value) => {
140
+ for (const caller of callers) {
141
+ caller.resolve(value)
142
+ }
143
+ },
144
+ (error) => {
145
+ for (const caller of callers) {
146
+ caller.reject(error)
147
+ }
148
+ }
149
+ )
150
+ return callers
151
+ }
152
+
153
+ /** Drops the entry for `id` only while it still holds this request. */
154
+ function evict(id: string, promise: Promise<unknown>): void {
155
+ if (promiseCache.get(id)?.promise === promise) {
156
+ promiseCache.delete(id)
41
157
  }
42
- const promise = fn().finally(() => promiseCache.delete(id))
43
- promiseCache.set(id, promise)
44
- return promise
45
158
  }
159
+
160
+ /** Older runtimes and polyfills can abort a signal without a `reason`. */
161
+ const abortReason = (signal: AbortSignal): unknown =>
162
+ signal.reason ??
163
+ Object.assign(new Error('This operation was aborted'), { name: 'AbortError' })
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export const name = '@lifi/sdk'
2
- export const version = '4.8.1'
2
+ export const version = '4.9.0'