@lifi/sdk 4.8.2 → 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.
- package/CHANGELOG.md +14 -0
- package/dist/cjs/actions/getChains.js +7 -1
- package/dist/cjs/actions/getChains.js.map +1 -1
- package/dist/cjs/actions/getContractCallsQuote.js +1 -1
- package/dist/cjs/actions/getGasRecommendation.js +1 -1
- package/dist/cjs/actions/getQuote.js +1 -1
- package/dist/cjs/actions/getRelayedTransactionStatus.js +1 -1
- package/dist/cjs/actions/getRelayerQuote.js +1 -1
- package/dist/cjs/actions/getRoutes.js +1 -1
- package/dist/cjs/actions/getStatus.js +1 -1
- package/dist/cjs/actions/getToken.js +1 -1
- package/dist/cjs/actions/getTokens.js +7 -1
- package/dist/cjs/actions/getTokens.js.map +1 -1
- package/dist/cjs/actions/relayTransaction.js +1 -1
- package/dist/cjs/client/createClient.js +23 -2
- package/dist/cjs/client/createClient.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +2 -2
- package/dist/cjs/types/core.d.ts +56 -1
- package/dist/cjs/utils/checkPackageUpdates.js +1 -1
- package/dist/cjs/utils/convertQuoteToRoute.js +1 -1
- package/dist/cjs/utils/request.js +2 -2
- package/dist/cjs/utils/withDedupe.d.ts +15 -2
- package/dist/cjs/utils/withDedupe.js +70 -7
- package/dist/cjs/utils/withDedupe.js.map +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/cjs/version.js.map +1 -1
- package/dist/esm/actions/getChains.d.ts.map +1 -1
- package/dist/esm/actions/getChains.js +7 -1
- package/dist/esm/actions/getChains.js.map +1 -1
- package/dist/esm/actions/getContractCallsQuote.js +1 -1
- package/dist/esm/actions/getGasRecommendation.js +1 -1
- package/dist/esm/actions/getQuote.js +1 -1
- package/dist/esm/actions/getRelayedTransactionStatus.js +1 -1
- package/dist/esm/actions/getRelayerQuote.js +1 -1
- package/dist/esm/actions/getRoutes.js +1 -1
- package/dist/esm/actions/getStatus.js +1 -1
- package/dist/esm/actions/getToken.js +1 -1
- package/dist/esm/actions/getTokens.d.ts.map +1 -1
- package/dist/esm/actions/getTokens.js +7 -1
- package/dist/esm/actions/getTokens.js.map +1 -1
- package/dist/esm/actions/relayTransaction.js +1 -1
- package/dist/esm/client/createClient.d.ts.map +1 -1
- package/dist/esm/client/createClient.js +23 -2
- package/dist/esm/client/createClient.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +2 -2
- package/dist/esm/types/core.d.ts +56 -1
- package/dist/esm/types/core.d.ts.map +1 -1
- package/dist/esm/utils/checkPackageUpdates.js +1 -1
- package/dist/esm/utils/convertQuoteToRoute.js +1 -1
- package/dist/esm/utils/request.js +2 -2
- package/dist/esm/utils/withDedupe.d.ts +15 -2
- package/dist/esm/utils/withDedupe.d.ts.map +1 -1
- package/dist/esm/utils/withDedupe.js +70 -7
- package/dist/esm/utils/withDedupe.js.map +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/package.json +1 -1
- package/src/actions/getChains.ts +12 -4
- package/src/actions/getTokens.ts +12 -4
- package/src/client/createClient.ts +35 -2
- package/src/index.ts +2 -0
- package/src/types/core.ts +59 -1
- package/src/utils/withDedupe.ts +127 -9
- 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
|
-
|
|
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>(
|
package/src/index.ts
CHANGED
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
|
|
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 {
|
package/src/utils/withDedupe.ts
CHANGED
|
@@ -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<
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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.
|
|
2
|
+
export const version = '4.9.0'
|