@lifi/widget 4.7.1 → 4.8.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/dist/esm/components/AmountInput/AmountInput.style.d.ts.map +1 -1
- package/dist/esm/components/Card/Card.d.ts.map +1 -1
- package/dist/esm/components/Card/CardIconButton.d.ts.map +1 -1
- package/dist/esm/components/ContractComponent/ItemPrice.d.ts.map +1 -1
- package/dist/esm/components/ListItem/ListItemButton.d.ts.map +1 -1
- package/dist/esm/components/ListItemButton.d.ts.map +1 -1
- package/dist/esm/components/Token/Token.d.ts.map +1 -1
- package/dist/esm/components/TokenList/types.d.ts.map +1 -1
- package/dist/esm/config/version.d.ts +1 -1
- package/dist/esm/config/version.js +1 -1
- package/dist/esm/config/version.js.map +1 -1
- package/dist/esm/hooks/useAvailableChains.d.ts.map +1 -1
- package/dist/esm/hooks/useAvailableChains.js +21 -38
- package/dist/esm/hooks/useAvailableChains.js.map +1 -1
- package/dist/esm/hooks/useFromTokenSufficiency.js +3 -3
- package/dist/esm/hooks/useFromTokenSufficiency.js.map +1 -1
- package/dist/esm/hooks/useGasRecommendation.js +3 -3
- package/dist/esm/hooks/useGasRecommendation.js.map +1 -1
- package/dist/esm/hooks/useGasSufficiency.js +3 -3
- package/dist/esm/hooks/useGasSufficiency.js.map +1 -1
- package/dist/esm/hooks/useIsBatchingSupported.js +2 -2
- package/dist/esm/hooks/useIsBatchingSupported.js.map +1 -1
- package/dist/esm/hooks/useRouteExecution.js +6 -6
- package/dist/esm/hooks/useRouteExecution.js.map +1 -1
- package/dist/esm/hooks/useRoutes.js +7 -7
- package/dist/esm/hooks/useRoutes.js.map +1 -1
- package/dist/esm/hooks/useTokenBalance.js +4 -4
- package/dist/esm/hooks/useTokenBalance.js.map +1 -1
- package/dist/esm/hooks/useTokenBalancesQueries.js +3 -3
- package/dist/esm/hooks/useTokenBalancesQueries.js.map +1 -1
- package/dist/esm/hooks/useTokenSearch.js +3 -3
- package/dist/esm/hooks/useTokenSearch.js.map +1 -1
- package/dist/esm/hooks/useTokens.js +14 -14
- package/dist/esm/hooks/useTokens.js.map +1 -1
- package/dist/esm/hooks/useTools.d.ts.map +1 -1
- package/dist/esm/hooks/useTools.js +26 -27
- package/dist/esm/hooks/useTools.js.map +1 -1
- package/dist/esm/hooks/useTransactionDetails.js +2 -2
- package/dist/esm/hooks/useTransactionDetails.js.map +1 -1
- package/dist/esm/hooks/useTransactionHistory.js +3 -3
- package/dist/esm/hooks/useTransactionHistory.js.map +1 -1
- package/dist/esm/index.d.ts +5 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/pages/RoutesPage/RoutesPage.style.d.ts.map +1 -1
- package/dist/esm/providers/I18nProvider/types.d.ts.map +1 -1
- package/dist/esm/providers/WidgetProvider/WidgetProvider.d.ts.map +1 -1
- package/dist/esm/providers/WidgetProvider/WidgetProvider.js +9 -1
- package/dist/esm/providers/WidgetProvider/WidgetProvider.js.map +1 -1
- package/dist/esm/providers/WidgetProvider/types.d.ts +2 -0
- package/dist/esm/providers/WidgetProvider/types.d.ts.map +1 -1
- package/dist/esm/queries/getChains.d.ts +38 -0
- package/dist/esm/queries/getChains.d.ts.map +1 -0
- package/dist/esm/queries/getChains.js +51 -0
- package/dist/esm/queries/getChains.js.map +1 -0
- package/dist/esm/queries/getTools.d.ts +26 -0
- package/dist/esm/queries/getTools.d.ts.map +1 -0
- package/dist/esm/queries/getTools.js +30 -0
- package/dist/esm/queries/getTools.js.map +1 -0
- package/dist/esm/queries/index.d.ts +4 -0
- package/dist/esm/queries/index.js +3 -0
- package/dist/esm/queries/types.d.ts +25 -0
- package/dist/esm/queries/types.d.ts.map +1 -0
- package/dist/esm/shared.d.ts +2 -1
- package/dist/esm/shared.js +2 -1
- package/dist/esm/stores/StoreProvider.d.ts.map +1 -1
- package/dist/esm/stores/StoreProvider.js +16 -17
- package/dist/esm/stores/StoreProvider.js.map +1 -1
- package/dist/esm/stores/header/useHeaderStore.d.ts +1 -1
- package/dist/esm/stores/header/useHeaderStore.d.ts.map +1 -1
- package/dist/esm/stores/header/useHeaderStore.js.map +1 -1
- package/dist/esm/themes/createTheme.d.ts.map +1 -1
- package/dist/esm/types/events.d.ts.map +1 -1
- package/dist/esm/types/token.d.ts.map +1 -1
- package/dist/esm/types/widget.d.ts +30 -0
- package/dist/esm/types/widget.d.ts.map +1 -1
- package/dist/esm/types/widget.js.map +1 -1
- package/dist/esm/utils/queries.js.map +1 -1
- package/dist/esm/utils/scopeKeys.d.ts +6 -0
- package/dist/esm/utils/scopeKeys.d.ts.map +1 -0
- package/dist/esm/utils/scopeKeys.js +8 -0
- package/dist/esm/utils/scopeKeys.js.map +1 -0
- package/package.json +12 -4
- package/src/config/version.ts +1 -1
- package/src/hooks/useAvailableChains.test.tsx +170 -0
- package/src/hooks/useAvailableChains.ts +29 -47
- package/src/hooks/useFromTokenSufficiency.ts +2 -2
- package/src/hooks/useGasRecommendation.ts +2 -2
- package/src/hooks/useGasSufficiency.ts +2 -2
- package/src/hooks/useIsBatchingSupported.ts +2 -2
- package/src/hooks/useRouteExecution.ts +5 -5
- package/src/hooks/useRoutes.ts +6 -6
- package/src/hooks/useTokenBalance.ts +4 -4
- package/src/hooks/useTokenBalancesQueries.ts +3 -3
- package/src/hooks/useTokenSearch.ts +7 -3
- package/src/hooks/useTokens.ts +13 -14
- package/src/hooks/useTools.sharedEntry.test.tsx +155 -0
- package/src/hooks/useTools.ts +42 -37
- package/src/hooks/useTransactionDetails.ts +3 -3
- package/src/hooks/useTransactionHistory.ts +2 -2
- package/src/index.ts +2 -1
- package/src/providers/WidgetProvider/WidgetProvider.test.tsx +73 -0
- package/src/providers/WidgetProvider/WidgetProvider.tsx +13 -0
- package/src/providers/WidgetProvider/types.ts +2 -0
- package/src/queries/getChains.ts +93 -0
- package/src/queries/getTools.ts +62 -0
- package/src/queries/index.ts +30 -0
- package/src/queries/sharedCache.test.tsx +223 -0
- package/src/queries/types.ts +38 -0
- package/src/shared.ts +1 -0
- package/src/stores/StoreProvider.test.tsx +107 -0
- package/src/stores/StoreProvider.tsx +8 -6
- package/src/stores/header/useHeaderStore.tsx +3 -4
- package/src/types/widget.ts +30 -0
- package/src/utils/queries.ts +1 -0
- package/src/utils/scopeKeys.ts +18 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SDKClient, ToolsResponse } from '@lifi/sdk'
|
|
2
|
+
import { getTools } from '@lifi/sdk'
|
|
3
|
+
import type {
|
|
4
|
+
FactoryQueryOptions,
|
|
5
|
+
QueryParameter,
|
|
6
|
+
ScopeKeyParameter,
|
|
7
|
+
} from './types.js'
|
|
8
|
+
|
|
9
|
+
export type GetToolsQueryFnData = ToolsResponse
|
|
10
|
+
|
|
11
|
+
export type GetToolsQueryKeyParameters = {
|
|
12
|
+
/** API base the client talks to. Different bases return different tools. */
|
|
13
|
+
apiUrl: string
|
|
14
|
+
} & ScopeKeyParameter
|
|
15
|
+
|
|
16
|
+
export type GetToolsQueryKey = readonly [
|
|
17
|
+
'tools',
|
|
18
|
+
{ apiUrl: string; scopeKey?: string },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
/** The key alone — to read, set or invalidate the entry. */
|
|
22
|
+
export function getToolsQueryKey(
|
|
23
|
+
parameters: GetToolsQueryKeyParameters
|
|
24
|
+
): GetToolsQueryKey {
|
|
25
|
+
return [
|
|
26
|
+
'tools',
|
|
27
|
+
{
|
|
28
|
+
apiUrl: parameters.apiUrl,
|
|
29
|
+
...(parameters.scopeKey === undefined
|
|
30
|
+
? {}
|
|
31
|
+
: { scopeKey: parameters.scopeKey }),
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type GetToolsOptions<selectData = GetToolsQueryFnData> =
|
|
37
|
+
ScopeKeyParameter &
|
|
38
|
+
QueryParameter<GetToolsQueryFnData, selectData, GetToolsQueryKey>
|
|
39
|
+
|
|
40
|
+
export type GetToolsQueryOptions<selectData = GetToolsQueryFnData> =
|
|
41
|
+
FactoryQueryOptions<GetToolsQueryFnData, selectData, GetToolsQueryKey>
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Query options for the bridges and exchanges of one API base.
|
|
45
|
+
*
|
|
46
|
+
* The key is `['tools', { apiUrl, scopeKey }]`. The cache holds the raw,
|
|
47
|
+
* unfiltered response so every caller can share it; narrow it per caller with
|
|
48
|
+
* `query.select`, never in the `queryFn`.
|
|
49
|
+
*/
|
|
50
|
+
export function getToolsQueryOptions<selectData = GetToolsQueryFnData>(
|
|
51
|
+
client: SDKClient,
|
|
52
|
+
options: GetToolsOptions<selectData> = {}
|
|
53
|
+
): GetToolsQueryOptions<selectData> {
|
|
54
|
+
return {
|
|
55
|
+
...options.query,
|
|
56
|
+
queryKey: getToolsQueryKey({
|
|
57
|
+
apiUrl: client.config.apiUrl,
|
|
58
|
+
scopeKey: options.scopeKey,
|
|
59
|
+
}),
|
|
60
|
+
queryFn: ({ signal }) => getTools(client, undefined, { signal }),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query option factories for LI.FI server data, shared by the widget and its
|
|
3
|
+
* host. Published as `@lifi/widget/queries`; it depends only on
|
|
4
|
+
* `@lifi/sdk` and on `@tanstack/react-query` types, so importing it does not
|
|
5
|
+
* pull in the widget itself.
|
|
6
|
+
*
|
|
7
|
+
* Keys follow wagmi's `@wagmi/core/query` convention: `[name, parameters]`,
|
|
8
|
+
* where the parameters hold everything that changes the response plus an
|
|
9
|
+
* optional `scopeKey`.
|
|
10
|
+
*/
|
|
11
|
+
export {
|
|
12
|
+
type GetChainsOptions,
|
|
13
|
+
type GetChainsQueryFnData,
|
|
14
|
+
type GetChainsQueryKey,
|
|
15
|
+
type GetChainsQueryKeyParameters,
|
|
16
|
+
type GetChainsQueryOptions,
|
|
17
|
+
getChainsQueryKey,
|
|
18
|
+
getChainsQueryOptions,
|
|
19
|
+
supportedChainTypes,
|
|
20
|
+
} from './getChains.js'
|
|
21
|
+
export {
|
|
22
|
+
type GetToolsOptions,
|
|
23
|
+
type GetToolsQueryFnData,
|
|
24
|
+
type GetToolsQueryKey,
|
|
25
|
+
type GetToolsQueryKeyParameters,
|
|
26
|
+
type GetToolsQueryOptions,
|
|
27
|
+
getToolsQueryKey,
|
|
28
|
+
getToolsQueryOptions,
|
|
29
|
+
} from './getTools.js'
|
|
30
|
+
export type { QueryParameter, ScopeKeyParameter } from './types.js'
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/** @vitest-environment happy-dom */
|
|
2
|
+
|
|
3
|
+
import type * as LifiSdk from '@lifi/sdk'
|
|
4
|
+
import type { SDKClient, ToolsResponse } from '@lifi/sdk'
|
|
5
|
+
import { ChainType } from '@lifi/sdk'
|
|
6
|
+
import {
|
|
7
|
+
QueryClient,
|
|
8
|
+
QueryClientProvider,
|
|
9
|
+
useQuery,
|
|
10
|
+
} from '@tanstack/react-query'
|
|
11
|
+
import type { ReactNode } from 'react'
|
|
12
|
+
import { act } from 'react'
|
|
13
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A host app and the widget that both build
|
|
18
|
+
* their options from these factories, inside one QueryClient, send each
|
|
19
|
+
* request once — whether they mount together or seconds apart.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const mocks = vi.hoisted(() => ({
|
|
23
|
+
getChains: vi.fn(),
|
|
24
|
+
getTools: vi.fn(),
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
vi.mock('@lifi/sdk', async (importOriginal) => ({
|
|
28
|
+
...(await importOriginal<typeof LifiSdk>()),
|
|
29
|
+
getChains: mocks.getChains,
|
|
30
|
+
getTools: mocks.getTools,
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
import { getChainsQueryOptions } from './getChains.js'
|
|
34
|
+
import { getToolsQueryOptions } from './getTools.js'
|
|
35
|
+
|
|
36
|
+
const V1 = 'https://api.example/v1'
|
|
37
|
+
const OTHER_BASE = 'https://api-b.example/v1'
|
|
38
|
+
const clientFor = (apiUrl: string) =>
|
|
39
|
+
({ config: { apiUrl } }) as unknown as SDKClient
|
|
40
|
+
|
|
41
|
+
const TOOLS = {
|
|
42
|
+
bridges: [{ key: 'across' }, { key: 'stargate' }],
|
|
43
|
+
exchanges: [{ key: '1inch' }],
|
|
44
|
+
} as unknown as ToolsResponse
|
|
45
|
+
|
|
46
|
+
/** A host app: all chain types, its own refresh cadence. */
|
|
47
|
+
const Host = ({
|
|
48
|
+
client,
|
|
49
|
+
scopeKey,
|
|
50
|
+
}: {
|
|
51
|
+
client: SDKClient
|
|
52
|
+
scopeKey: string
|
|
53
|
+
}) => {
|
|
54
|
+
useQuery(
|
|
55
|
+
getChainsQueryOptions(client, {
|
|
56
|
+
scopeKey,
|
|
57
|
+
query: { staleTime: 300_000 },
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
useQuery(
|
|
61
|
+
getToolsQueryOptions(client, { scopeKey, query: { staleTime: 300_000 } })
|
|
62
|
+
)
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** What the widget does: chain types in another order, a filtered tools view. */
|
|
67
|
+
const Widget = ({
|
|
68
|
+
client,
|
|
69
|
+
scopeKey,
|
|
70
|
+
}: {
|
|
71
|
+
client: SDKClient
|
|
72
|
+
scopeKey: string
|
|
73
|
+
}) => {
|
|
74
|
+
useQuery(
|
|
75
|
+
getChainsQueryOptions(client, {
|
|
76
|
+
chainTypes: [
|
|
77
|
+
ChainType.STL,
|
|
78
|
+
ChainType.TVM,
|
|
79
|
+
ChainType.MVM,
|
|
80
|
+
ChainType.UTXO,
|
|
81
|
+
ChainType.SVM,
|
|
82
|
+
ChainType.EVM,
|
|
83
|
+
],
|
|
84
|
+
scopeKey,
|
|
85
|
+
query: { staleTime: 300_000 },
|
|
86
|
+
})
|
|
87
|
+
)
|
|
88
|
+
useQuery(
|
|
89
|
+
getToolsQueryOptions(client, {
|
|
90
|
+
scopeKey,
|
|
91
|
+
query: {
|
|
92
|
+
staleTime: 180_000,
|
|
93
|
+
select: (tools) => ({ ...tools, bridges: tools.bridges.slice(0, 1) }),
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
)
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let root: Root
|
|
101
|
+
let queryClient: QueryClient
|
|
102
|
+
|
|
103
|
+
const render = async (node: ReactNode) => {
|
|
104
|
+
await act(async () => {
|
|
105
|
+
root.render(
|
|
106
|
+
<QueryClientProvider client={queryClient}>{node}</QueryClientProvider>
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const settle = () =>
|
|
112
|
+
vi.waitFor(() => {
|
|
113
|
+
expect(queryClient.isFetching()).toBe(0)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('shared chains and tools cache', () => {
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
vi.clearAllMocks()
|
|
119
|
+
mocks.getChains.mockResolvedValue([{ id: 1 }])
|
|
120
|
+
mocks.getTools.mockResolvedValue(TOOLS)
|
|
121
|
+
queryClient = new QueryClient({
|
|
122
|
+
defaultOptions: { queries: { retry: false } },
|
|
123
|
+
})
|
|
124
|
+
const container = document.createElement('div')
|
|
125
|
+
document.body.appendChild(container)
|
|
126
|
+
root = createRoot(container)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
afterEach(async () => {
|
|
130
|
+
await act(async () => root.unmount())
|
|
131
|
+
queryClient.clear()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('fetches once when the host and the widget mount together', async () => {
|
|
135
|
+
const client = clientFor(V1)
|
|
136
|
+
|
|
137
|
+
await render(
|
|
138
|
+
<>
|
|
139
|
+
<Host client={client} scopeKey="host" />
|
|
140
|
+
<Widget client={client} scopeKey="host" />
|
|
141
|
+
</>
|
|
142
|
+
)
|
|
143
|
+
await settle()
|
|
144
|
+
|
|
145
|
+
expect(mocks.getChains).toHaveBeenCalledTimes(1)
|
|
146
|
+
expect(mocks.getTools).toHaveBeenCalledTimes(1)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// A widget often mounts seconds after the host app. A later observer with a
|
|
150
|
+
// staleTime reads the entry instead of refetching it.
|
|
151
|
+
it('fetches once when the widget mounts after the host has settled', async () => {
|
|
152
|
+
const client = clientFor(V1)
|
|
153
|
+
|
|
154
|
+
await render(<Host client={client} scopeKey="host" />)
|
|
155
|
+
await settle()
|
|
156
|
+
await render(
|
|
157
|
+
<>
|
|
158
|
+
<Host client={client} scopeKey="host" />
|
|
159
|
+
<Widget client={client} scopeKey="host" />
|
|
160
|
+
</>
|
|
161
|
+
)
|
|
162
|
+
await settle()
|
|
163
|
+
|
|
164
|
+
expect(mocks.getChains).toHaveBeenCalledTimes(1)
|
|
165
|
+
expect(mocks.getTools).toHaveBeenCalledTimes(1)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('also shares when the host and widget hold different client objects on the same base', async () => {
|
|
169
|
+
await render(
|
|
170
|
+
<>
|
|
171
|
+
<Host client={clientFor(V1)} scopeKey="host" />
|
|
172
|
+
<Widget client={clientFor(V1)} scopeKey="host" />
|
|
173
|
+
</>
|
|
174
|
+
)
|
|
175
|
+
await settle()
|
|
176
|
+
|
|
177
|
+
expect(mocks.getChains).toHaveBeenCalledTimes(1)
|
|
178
|
+
expect(mocks.getTools).toHaveBeenCalledTimes(1)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('keeps the raw tools response in the cache under a filtering select', async () => {
|
|
182
|
+
const client = clientFor(V1)
|
|
183
|
+
|
|
184
|
+
await render(<Widget client={client} scopeKey="host" />)
|
|
185
|
+
await settle()
|
|
186
|
+
|
|
187
|
+
const cached = queryClient.getQueryData<ToolsResponse>(
|
|
188
|
+
getToolsQueryOptions(client, { scopeKey: 'host' }).queryKey
|
|
189
|
+
)
|
|
190
|
+
expect(cached?.bridges).toHaveLength(2)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// A partner's own queries and another integration's widget stay apart.
|
|
194
|
+
it('does not share across scopes', async () => {
|
|
195
|
+
const client = clientFor(V1)
|
|
196
|
+
|
|
197
|
+
await render(
|
|
198
|
+
<>
|
|
199
|
+
<Host client={client} scopeKey="host" />
|
|
200
|
+
<Widget client={client} scopeKey="partner" />
|
|
201
|
+
</>
|
|
202
|
+
)
|
|
203
|
+
await settle()
|
|
204
|
+
|
|
205
|
+
expect(mocks.getChains).toHaveBeenCalledTimes(2)
|
|
206
|
+
expect(mocks.getTools).toHaveBeenCalledTimes(2)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// Different API bases can return different chains and tools, so they must
|
|
210
|
+
// never share an entry.
|
|
211
|
+
it('does not share across API bases', async () => {
|
|
212
|
+
await render(
|
|
213
|
+
<>
|
|
214
|
+
<Host client={clientFor(V1)} scopeKey="host" />
|
|
215
|
+
<Widget client={clientFor(OTHER_BASE)} scopeKey="host" />
|
|
216
|
+
</>
|
|
217
|
+
)
|
|
218
|
+
await settle()
|
|
219
|
+
|
|
220
|
+
expect(mocks.getChains).toHaveBeenCalledTimes(2)
|
|
221
|
+
expect(mocks.getTools).toHaveBeenCalledTimes(2)
|
|
222
|
+
})
|
|
223
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
QueryFunction,
|
|
3
|
+
QueryKey,
|
|
4
|
+
UseQueryOptions,
|
|
5
|
+
} from '@tanstack/react-query'
|
|
6
|
+
|
|
7
|
+
export type ScopeKeyParameter = {
|
|
8
|
+
/**
|
|
9
|
+
* Scopes the entry inside a shared `QueryClient`, like wagmi's `scopeKey`.
|
|
10
|
+
* Callers that pass the same value share the cached data; a widget passes
|
|
11
|
+
* its `queryScopeKey`.
|
|
12
|
+
*/
|
|
13
|
+
scopeKey?: string | undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Behaviour the caller owns — `staleTime`, `refetchInterval`, `select`,
|
|
18
|
+
* `enabled`, … It is spread into the options and never becomes part of the
|
|
19
|
+
* key, so callers with different behaviour still share one entry.
|
|
20
|
+
*/
|
|
21
|
+
export type QueryParameter<queryFnData, data, queryKey extends QueryKey> = {
|
|
22
|
+
query?:
|
|
23
|
+
| Omit<
|
|
24
|
+
UseQueryOptions<queryFnData, Error, data, queryKey>,
|
|
25
|
+
'queryKey' | 'queryFn'
|
|
26
|
+
>
|
|
27
|
+
| undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Options ready for `useQuery`, `fetchQuery` or `prefetchQuery`. */
|
|
31
|
+
export type FactoryQueryOptions<
|
|
32
|
+
queryFnData,
|
|
33
|
+
data,
|
|
34
|
+
queryKey extends QueryKey,
|
|
35
|
+
> = UseQueryOptions<queryFnData, Error, data, queryKey> & {
|
|
36
|
+
queryKey: queryKey
|
|
37
|
+
queryFn: QueryFunction<queryFnData, queryKey>
|
|
38
|
+
}
|
package/src/shared.ts
CHANGED
|
@@ -188,5 +188,6 @@ export {
|
|
|
188
188
|
export { getPriceImpact } from './utils/getPriceImpact.js'
|
|
189
189
|
export { fitInputText } from './utils/input.js'
|
|
190
190
|
export { navigationRoutes } from './utils/navigationRoutes.js'
|
|
191
|
+
export { resolveStorageScopeKey } from './utils/scopeKeys.js'
|
|
191
192
|
export { isHttpUrl, openInNewTab } from './utils/url.js'
|
|
192
193
|
export { shortenAddress } from './utils/wallet.js'
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** @vitest-environment happy-dom */
|
|
2
|
+
|
|
3
|
+
import { act } from 'react'
|
|
4
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import type { WidgetConfig } from '../types/widget.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The persisted stores are named from the resolved storage scope. An
|
|
10
|
+
* integration that only set the deprecated keyPrefix must keep the store
|
|
11
|
+
* names it always had, or its users lose their saved state.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { recorded, recorder } = vi.hoisted(() => {
|
|
15
|
+
const recorded = { namePrefix: {} as Record<string, string | undefined> }
|
|
16
|
+
const recorder = (name: string) => ({
|
|
17
|
+
[name]: ({
|
|
18
|
+
children,
|
|
19
|
+
namePrefix,
|
|
20
|
+
}: {
|
|
21
|
+
children?: unknown
|
|
22
|
+
namePrefix?: string
|
|
23
|
+
}) => {
|
|
24
|
+
recorded.namePrefix[name] = namePrefix
|
|
25
|
+
return children
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
return { recorded, recorder }
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
vi.mock('./bookmarks/BookmarkStore.js', () => recorder('BookmarkStoreProvider'))
|
|
32
|
+
vi.mock('./chains/ChainOrderStore.js', () =>
|
|
33
|
+
recorder('ChainOrderStoreProvider')
|
|
34
|
+
)
|
|
35
|
+
vi.mock('./form/FormStore.js', () => recorder('FormStoreProvider'))
|
|
36
|
+
vi.mock('./header/useHeaderStore.js', () => recorder('HeaderStoreProvider'))
|
|
37
|
+
vi.mock('./navigationTabs/useNavigationTabsStore.js', () =>
|
|
38
|
+
recorder('NavigationTabsStoreProvider')
|
|
39
|
+
)
|
|
40
|
+
vi.mock('./pinnedTokens/PinnedTokensStore.js', () =>
|
|
41
|
+
recorder('PinnedTokensStoreProvider')
|
|
42
|
+
)
|
|
43
|
+
vi.mock('./recentTokens/RecentTokensStore.js', () =>
|
|
44
|
+
recorder('RecentTokensStoreProvider')
|
|
45
|
+
)
|
|
46
|
+
vi.mock('./routes/RouteExecutionStore.js', () =>
|
|
47
|
+
recorder('RouteExecutionStoreProvider')
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
import { StoreProvider } from './StoreProvider.js'
|
|
51
|
+
|
|
52
|
+
const PERSISTED = [
|
|
53
|
+
'BookmarkStoreProvider',
|
|
54
|
+
'PinnedTokensStoreProvider',
|
|
55
|
+
'RecentTokensStoreProvider',
|
|
56
|
+
'ChainOrderStoreProvider',
|
|
57
|
+
'RouteExecutionStoreProvider',
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
let root: Root
|
|
61
|
+
|
|
62
|
+
const render = async (config: WidgetConfig) => {
|
|
63
|
+
await act(async () => {
|
|
64
|
+
root.render(<StoreProvider config={config}>{null}</StoreProvider>)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('StoreProvider storage scope', () => {
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
recorded.namePrefix = {}
|
|
71
|
+
const container = document.createElement('div')
|
|
72
|
+
document.body.appendChild(container)
|
|
73
|
+
root = createRoot(container)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
afterEach(async () => {
|
|
77
|
+
await act(async () => root.unmount())
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('names the persisted stores from keyPrefix when only that is set', async () => {
|
|
81
|
+
await render({ integrator: 'test', keyPrefix: 'legacy' })
|
|
82
|
+
|
|
83
|
+
for (const store of PERSISTED) {
|
|
84
|
+
expect(recorded.namePrefix[store]).toBe('legacy')
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('names the persisted stores from storageScopeKey over keyPrefix', async () => {
|
|
89
|
+
await render({
|
|
90
|
+
integrator: 'test',
|
|
91
|
+
keyPrefix: 'legacy',
|
|
92
|
+
storageScopeKey: 'store',
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
for (const store of PERSISTED) {
|
|
96
|
+
expect(recorded.namePrefix[store]).toBe('store')
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('keeps the historical names when nothing is set', async () => {
|
|
101
|
+
await render({ integrator: 'test' })
|
|
102
|
+
|
|
103
|
+
for (const store of PERSISTED) {
|
|
104
|
+
expect(recorded.namePrefix[store]).toBeUndefined()
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PropsWithChildren } from 'react'
|
|
2
2
|
import type { WidgetConfigProps } from '../types/widget.js'
|
|
3
|
+
import { resolveStorageScopeKey } from '../utils/scopeKeys.js'
|
|
3
4
|
import { BookmarkStoreProvider } from './bookmarks/BookmarkStore.js'
|
|
4
5
|
import { ChainOrderStoreProvider } from './chains/ChainOrderStore.js'
|
|
5
6
|
import { FormStoreProvider } from './form/FormStore.js'
|
|
@@ -14,15 +15,16 @@ export const StoreProvider: React.FC<PropsWithChildren<WidgetConfigProps>> = ({
|
|
|
14
15
|
config,
|
|
15
16
|
formRef,
|
|
16
17
|
}) => {
|
|
18
|
+
const storageScopeKey = resolveStorageScopeKey(config)
|
|
17
19
|
return (
|
|
18
20
|
<NavigationTabsStoreProvider config={config}>
|
|
19
|
-
<HeaderStoreProvider
|
|
20
|
-
<BookmarkStoreProvider namePrefix={
|
|
21
|
-
<PinnedTokensStoreProvider namePrefix={
|
|
22
|
-
<RecentTokensStoreProvider namePrefix={
|
|
21
|
+
<HeaderStoreProvider>
|
|
22
|
+
<BookmarkStoreProvider namePrefix={storageScopeKey}>
|
|
23
|
+
<PinnedTokensStoreProvider namePrefix={storageScopeKey}>
|
|
24
|
+
<RecentTokensStoreProvider namePrefix={storageScopeKey}>
|
|
23
25
|
<FormStoreProvider formRef={formRef}>
|
|
24
|
-
<ChainOrderStoreProvider namePrefix={
|
|
25
|
-
<RouteExecutionStoreProvider namePrefix={
|
|
26
|
+
<ChainOrderStoreProvider namePrefix={storageScopeKey}>
|
|
27
|
+
<RouteExecutionStoreProvider namePrefix={storageScopeKey}>
|
|
26
28
|
{children}
|
|
27
29
|
</RouteExecutionStoreProvider>
|
|
28
30
|
</ChainOrderStoreProvider>
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
import type { PropsWithChildren } from 'react'
|
|
1
2
|
import { createContext, use, useRef } from 'react'
|
|
2
3
|
import { create } from 'zustand'
|
|
3
4
|
import { useShallow } from 'zustand/shallow'
|
|
4
|
-
import type { PersistStoreProviderProps } from '../types.js'
|
|
5
5
|
import type { HeaderState, HeaderStore } from './types.js'
|
|
6
6
|
|
|
7
7
|
const HeaderStoreContext = createContext<HeaderStore | null>(null)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}: PersistStoreProviderProps): any {
|
|
9
|
+
// Not persisted, so it takes no storage scope.
|
|
10
|
+
export function HeaderStoreProvider({ children }: PropsWithChildren): any {
|
|
12
11
|
const storeRef = useRef<HeaderStore>(null)
|
|
13
12
|
if (!storeRef.current) {
|
|
14
13
|
storeRef.current = createHeaderStore()
|
package/src/types/widget.ts
CHANGED
|
@@ -410,7 +410,37 @@ export interface WidgetConfig {
|
|
|
410
410
|
sdkConfig?: WidgetSDKConfig
|
|
411
411
|
|
|
412
412
|
buildUrl?: boolean
|
|
413
|
+
/**
|
|
414
|
+
* @deprecated Use `storageScopeKey` for persisted state and `queryScopeKey`
|
|
415
|
+
* for the query cache. Still honoured as the fallback for both.
|
|
416
|
+
*/
|
|
413
417
|
keyPrefix?: string
|
|
418
|
+
/**
|
|
419
|
+
* Namespaces the widget's persisted state in `localStorage`: bookmarks,
|
|
420
|
+
* pinned tokens, recent tokens, chain order and route history, as
|
|
421
|
+
* `keyPrefix` does. Use a different value per widget instance that should
|
|
422
|
+
* keep its own state. Settings keep one shared name.
|
|
423
|
+
*
|
|
424
|
+
* It also scopes the widget's other queries (routes, tokens, balances, …),
|
|
425
|
+
* so instances with different values keep their data apart.
|
|
426
|
+
*
|
|
427
|
+
* Falls back to `keyPrefix`, then to the historical `li.fi-*` names.
|
|
428
|
+
*/
|
|
429
|
+
storageScopeKey?: string
|
|
430
|
+
/**
|
|
431
|
+
* Scopes the widget's chains and tools queries inside the host's react-query
|
|
432
|
+
* `QueryClient`. Widget instances - and a host app using the
|
|
433
|
+
* `@lifi/widget/queries` factories - that pass the same value
|
|
434
|
+
* share those entries. The other widget queries are scoped by
|
|
435
|
+
* `storageScopeKey`.
|
|
436
|
+
*
|
|
437
|
+
* The keys start with a plain `'chains'` / `'tools'`, as in wagmi, so a host
|
|
438
|
+
* that matches on those roots (for example `setQueriesData({ queryKey:
|
|
439
|
+
* ['chains'] })`) also reaches the widget's entries.
|
|
440
|
+
*
|
|
441
|
+
* Falls back to `keyPrefix`, then to `'li.fi'`.
|
|
442
|
+
*/
|
|
443
|
+
queryScopeKey?: string
|
|
414
444
|
|
|
415
445
|
bridges?: AllowDeny<string>
|
|
416
446
|
exchanges?: AllowDeny<string>
|
package/src/utils/queries.ts
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WidgetConfig } from '../types/widget.js'
|
|
2
|
+
|
|
3
|
+
// Non-empty, so an unconfigured widget never shares a host's unscoped entries.
|
|
4
|
+
export const defaultQueryScopeKey = 'li.fi'
|
|
5
|
+
|
|
6
|
+
type ScopeKeyConfig = Pick<
|
|
7
|
+
WidgetConfig,
|
|
8
|
+
'storageScopeKey' | 'queryScopeKey' | 'keyPrefix'
|
|
9
|
+
>
|
|
10
|
+
|
|
11
|
+
// `undefined` keeps the historical `li.fi-*` store names.
|
|
12
|
+
export const resolveStorageScopeKey = (
|
|
13
|
+
config: Partial<ScopeKeyConfig> | undefined
|
|
14
|
+
): string | undefined => config?.storageScopeKey ?? config?.keyPrefix
|
|
15
|
+
|
|
16
|
+
export const resolveQueryScopeKey = (
|
|
17
|
+
config: Partial<ScopeKeyConfig> | undefined
|
|
18
|
+
): string => config?.queryScopeKey ?? config?.keyPrefix ?? defaultQueryScopeKey
|