@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.
Files changed (115) hide show
  1. package/dist/esm/components/AmountInput/AmountInput.style.d.ts.map +1 -1
  2. package/dist/esm/components/Card/Card.d.ts.map +1 -1
  3. package/dist/esm/components/Card/CardIconButton.d.ts.map +1 -1
  4. package/dist/esm/components/ContractComponent/ItemPrice.d.ts.map +1 -1
  5. package/dist/esm/components/ListItem/ListItemButton.d.ts.map +1 -1
  6. package/dist/esm/components/ListItemButton.d.ts.map +1 -1
  7. package/dist/esm/components/Token/Token.d.ts.map +1 -1
  8. package/dist/esm/components/TokenList/types.d.ts.map +1 -1
  9. package/dist/esm/config/version.d.ts +1 -1
  10. package/dist/esm/config/version.js +1 -1
  11. package/dist/esm/config/version.js.map +1 -1
  12. package/dist/esm/hooks/useAvailableChains.d.ts.map +1 -1
  13. package/dist/esm/hooks/useAvailableChains.js +21 -38
  14. package/dist/esm/hooks/useAvailableChains.js.map +1 -1
  15. package/dist/esm/hooks/useFromTokenSufficiency.js +3 -3
  16. package/dist/esm/hooks/useFromTokenSufficiency.js.map +1 -1
  17. package/dist/esm/hooks/useGasRecommendation.js +3 -3
  18. package/dist/esm/hooks/useGasRecommendation.js.map +1 -1
  19. package/dist/esm/hooks/useGasSufficiency.js +3 -3
  20. package/dist/esm/hooks/useGasSufficiency.js.map +1 -1
  21. package/dist/esm/hooks/useIsBatchingSupported.js +2 -2
  22. package/dist/esm/hooks/useIsBatchingSupported.js.map +1 -1
  23. package/dist/esm/hooks/useRouteExecution.js +6 -6
  24. package/dist/esm/hooks/useRouteExecution.js.map +1 -1
  25. package/dist/esm/hooks/useRoutes.js +7 -7
  26. package/dist/esm/hooks/useRoutes.js.map +1 -1
  27. package/dist/esm/hooks/useTokenBalance.js +4 -4
  28. package/dist/esm/hooks/useTokenBalance.js.map +1 -1
  29. package/dist/esm/hooks/useTokenBalancesQueries.js +3 -3
  30. package/dist/esm/hooks/useTokenBalancesQueries.js.map +1 -1
  31. package/dist/esm/hooks/useTokenSearch.js +3 -3
  32. package/dist/esm/hooks/useTokenSearch.js.map +1 -1
  33. package/dist/esm/hooks/useTokens.js +14 -14
  34. package/dist/esm/hooks/useTokens.js.map +1 -1
  35. package/dist/esm/hooks/useTools.d.ts.map +1 -1
  36. package/dist/esm/hooks/useTools.js +26 -27
  37. package/dist/esm/hooks/useTools.js.map +1 -1
  38. package/dist/esm/hooks/useTransactionDetails.js +2 -2
  39. package/dist/esm/hooks/useTransactionDetails.js.map +1 -1
  40. package/dist/esm/hooks/useTransactionHistory.js +3 -3
  41. package/dist/esm/hooks/useTransactionHistory.js.map +1 -1
  42. package/dist/esm/index.d.ts +5 -1
  43. package/dist/esm/index.js +3 -1
  44. package/dist/esm/pages/RoutesPage/RoutesPage.style.d.ts.map +1 -1
  45. package/dist/esm/providers/I18nProvider/types.d.ts.map +1 -1
  46. package/dist/esm/providers/WidgetProvider/WidgetProvider.d.ts.map +1 -1
  47. package/dist/esm/providers/WidgetProvider/WidgetProvider.js +9 -1
  48. package/dist/esm/providers/WidgetProvider/WidgetProvider.js.map +1 -1
  49. package/dist/esm/providers/WidgetProvider/types.d.ts +2 -0
  50. package/dist/esm/providers/WidgetProvider/types.d.ts.map +1 -1
  51. package/dist/esm/queries/getChains.d.ts +38 -0
  52. package/dist/esm/queries/getChains.d.ts.map +1 -0
  53. package/dist/esm/queries/getChains.js +51 -0
  54. package/dist/esm/queries/getChains.js.map +1 -0
  55. package/dist/esm/queries/getTools.d.ts +26 -0
  56. package/dist/esm/queries/getTools.d.ts.map +1 -0
  57. package/dist/esm/queries/getTools.js +30 -0
  58. package/dist/esm/queries/getTools.js.map +1 -0
  59. package/dist/esm/queries/index.d.ts +4 -0
  60. package/dist/esm/queries/index.js +3 -0
  61. package/dist/esm/queries/types.d.ts +25 -0
  62. package/dist/esm/queries/types.d.ts.map +1 -0
  63. package/dist/esm/shared.d.ts +2 -1
  64. package/dist/esm/shared.js +2 -1
  65. package/dist/esm/stores/StoreProvider.d.ts.map +1 -1
  66. package/dist/esm/stores/StoreProvider.js +16 -17
  67. package/dist/esm/stores/StoreProvider.js.map +1 -1
  68. package/dist/esm/stores/header/useHeaderStore.d.ts +1 -1
  69. package/dist/esm/stores/header/useHeaderStore.d.ts.map +1 -1
  70. package/dist/esm/stores/header/useHeaderStore.js.map +1 -1
  71. package/dist/esm/themes/createTheme.d.ts.map +1 -1
  72. package/dist/esm/types/events.d.ts.map +1 -1
  73. package/dist/esm/types/token.d.ts.map +1 -1
  74. package/dist/esm/types/widget.d.ts +30 -0
  75. package/dist/esm/types/widget.d.ts.map +1 -1
  76. package/dist/esm/types/widget.js.map +1 -1
  77. package/dist/esm/utils/queries.js.map +1 -1
  78. package/dist/esm/utils/scopeKeys.d.ts +6 -0
  79. package/dist/esm/utils/scopeKeys.d.ts.map +1 -0
  80. package/dist/esm/utils/scopeKeys.js +8 -0
  81. package/dist/esm/utils/scopeKeys.js.map +1 -0
  82. package/package.json +12 -4
  83. package/src/config/version.ts +1 -1
  84. package/src/hooks/useAvailableChains.test.tsx +170 -0
  85. package/src/hooks/useAvailableChains.ts +29 -47
  86. package/src/hooks/useFromTokenSufficiency.ts +2 -2
  87. package/src/hooks/useGasRecommendation.ts +2 -2
  88. package/src/hooks/useGasSufficiency.ts +2 -2
  89. package/src/hooks/useIsBatchingSupported.ts +2 -2
  90. package/src/hooks/useRouteExecution.ts +5 -5
  91. package/src/hooks/useRoutes.ts +6 -6
  92. package/src/hooks/useTokenBalance.ts +4 -4
  93. package/src/hooks/useTokenBalancesQueries.ts +3 -3
  94. package/src/hooks/useTokenSearch.ts +7 -3
  95. package/src/hooks/useTokens.ts +13 -14
  96. package/src/hooks/useTools.sharedEntry.test.tsx +155 -0
  97. package/src/hooks/useTools.ts +42 -37
  98. package/src/hooks/useTransactionDetails.ts +3 -3
  99. package/src/hooks/useTransactionHistory.ts +2 -2
  100. package/src/index.ts +2 -1
  101. package/src/providers/WidgetProvider/WidgetProvider.test.tsx +73 -0
  102. package/src/providers/WidgetProvider/WidgetProvider.tsx +13 -0
  103. package/src/providers/WidgetProvider/types.ts +2 -0
  104. package/src/queries/getChains.ts +93 -0
  105. package/src/queries/getTools.ts +62 -0
  106. package/src/queries/index.ts +30 -0
  107. package/src/queries/sharedCache.test.tsx +223 -0
  108. package/src/queries/types.ts +38 -0
  109. package/src/shared.ts +1 -0
  110. package/src/stores/StoreProvider.test.tsx +107 -0
  111. package/src/stores/StoreProvider.tsx +8 -6
  112. package/src/stores/header/useHeaderStore.tsx +3 -4
  113. package/src/types/widget.ts +30 -0
  114. package/src/utils/queries.ts +1 -0
  115. package/src/utils/scopeKeys.ts +18 -0
@@ -20,7 +20,7 @@ export const useTokenBalancesQueries = (
20
20
  isLoading: boolean
21
21
  isError: boolean
22
22
  } => {
23
- const { keyPrefix } = useWidgetConfig()
23
+ const { storageScopeKey } = useWidgetConfig()
24
24
  const sdkClient = useSDKClient()
25
25
  const firstLoadStartRef = useRef<number | null>(null)
26
26
 
@@ -34,7 +34,7 @@ export const useTokenBalancesQueries = (
34
34
  const chainId = Number(chainIdStr)
35
35
  return {
36
36
  queryKey: [
37
- getQueryKey('token-balances', keyPrefix),
37
+ getQueryKey('token-balances', storageScopeKey),
38
38
  accountAddress,
39
39
  chainId,
40
40
  tokens.length,
@@ -55,7 +55,7 @@ export const useTokenBalancesQueries = (
55
55
  }
56
56
  })
57
57
  )
58
- }, [accountsWithTokens, isBalanceLoadingEnabled, keyPrefix, sdkClient])
58
+ }, [accountsWithTokens, isBalanceLoadingEnabled, storageScopeKey, sdkClient])
59
59
 
60
60
  const result = useQueries({
61
61
  queries: queryConfig,
@@ -18,11 +18,15 @@ export const useTokenSearch = (
18
18
  isLoading: boolean
19
19
  } => {
20
20
  const queryClient = useQueryClient()
21
- const { tokens: configTokens, keyPrefix } = useWidgetConfig()
21
+ const { tokens: configTokens, storageScopeKey } = useWidgetConfig()
22
22
  const sdkClient = useSDKClient()
23
23
 
24
24
  const { data, isLoading } = useQuery({
25
- queryKey: [getQueryKey('token-search', keyPrefix), chainId, tokenQuery],
25
+ queryKey: [
26
+ getQueryKey('token-search', storageScopeKey),
27
+ chainId,
28
+ tokenQuery,
29
+ ],
26
30
  queryFn: async ({ queryKey: [, chainId, tokenQuery], signal }) => {
27
31
  const token = await getToken(
28
32
  sdkClient,
@@ -52,7 +56,7 @@ export const useTokenSearch = (
52
56
 
53
57
  // Add token to main tokens cache
54
58
  queryClient.setQueriesData<TokensByChain>(
55
- { queryKey: [getQueryKey('tokens', keyPrefix)] },
59
+ { queryKey: [getQueryKey('tokens', storageScopeKey)] },
56
60
  (data) => {
57
61
  if (!data) {
58
62
  return data
@@ -25,6 +25,14 @@ import { useAvailableChains } from './useAvailableChains.js'
25
25
 
26
26
  const refetchInterval = 300_000
27
27
 
28
+ // The main list and the search send the same list parameters.
29
+ const tokenListParams = {
30
+ orderBy: 'volumeUSD24H',
31
+ extended: true,
32
+ limit: 1100,
33
+ minPriceUSD: 0.000001,
34
+ } as const
35
+
28
36
  export const useTokens = (
29
37
  formType?: FormType,
30
38
  search?: string,
@@ -39,7 +47,7 @@ export const useTokens = (
39
47
  const {
40
48
  tokens: configTokens,
41
49
  chains: chainsConfig,
42
- keyPrefix,
50
+ storageScopeKey,
43
51
  } = useWidgetConfig()
44
52
  const sdkClient = useSDKClient()
45
53
  const { getChainTypeFromTokenAddress } = useChainTypeFromAddress()
@@ -55,7 +63,7 @@ export const useTokens = (
55
63
 
56
64
  // Main tokens cache - verified tokens from API
57
65
  const { data: verifiedTokens, isLoading } = useQuery({
58
- queryKey: [getQueryKey('tokens', keyPrefix)],
66
+ queryKey: [getQueryKey('tokens', storageScopeKey)],
59
67
  queryFn: async ({ signal }) => {
60
68
  const chainTypes = [
61
69
  ChainType.EVM,
@@ -68,13 +76,7 @@ export const useTokens = (
68
76
 
69
77
  const tokensResponse: TokensExtendedResponse = await getTokens(
70
78
  sdkClient,
71
- {
72
- chainTypes,
73
- orderBy: 'volumeUSD24H',
74
- extended: true,
75
- limit: 1000,
76
- minPriceUSD: 0.000001,
77
- },
79
+ { ...tokenListParams, chainTypes },
78
80
  { signal }
79
81
  )
80
82
 
@@ -95,7 +97,7 @@ export const useTokens = (
95
97
  // Search tokens cache - unverified tokens from search
96
98
  const { data: searchTokens, isLoading: isSearchLoading } = useQuery({
97
99
  queryKey: [
98
- getQueryKey('tokens-search', keyPrefix),
100
+ getQueryKey('tokens-search', storageScopeKey),
99
101
  trimmedSearch,
100
102
  chainId,
101
103
  isAddressSearch,
@@ -114,12 +116,9 @@ export const useTokens = (
114
116
  const tokensResponse: TokensExtendedResponse = await getTokens(
115
117
  sdkClient,
116
118
  {
119
+ ...tokenListParams,
117
120
  chainTypes,
118
- orderBy: 'volumeUSD24H',
119
- extended: true,
120
121
  search: searchQuery,
121
- limit: 1000,
122
- minPriceUSD: 0.000001,
123
122
  },
124
123
  { signal }
125
124
  )
@@ -0,0 +1,155 @@
1
+ /** @vitest-environment happy-dom */
2
+
3
+ import type * as LifiSdk from '@lifi/sdk'
4
+ import type { SDKClient, ToolsResponse } from '@lifi/sdk'
5
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
6
+ import { act } from 'react'
7
+ import { createRoot, type Root } from 'react-dom/client'
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
9
+ import type { WidgetConfig } from '../types/widget.js'
10
+
11
+ /**
12
+ * The tools entry is shared with the host app through
13
+ * `getToolsQueryOptions`, keyed by the client's API base and the widget's
14
+ * `queryScopeKey`. When the host fetched first, the widget must read that entry
15
+ * — no request of its own — and still seed its settings store from the
16
+ * filtered view.
17
+ */
18
+
19
+ const API_URL = 'https://api.example/v1'
20
+
21
+ const mocks = vi.hoisted(() => ({
22
+ getTools: vi.fn(),
23
+ initializeTools: vi.fn(),
24
+ }))
25
+
26
+ vi.mock('@lifi/sdk', async (importOriginal) => ({
27
+ ...(await importOriginal<typeof LifiSdk>()),
28
+ getTools: mocks.getTools,
29
+ }))
30
+
31
+ let widgetConfig: Partial<WidgetConfig> = {}
32
+
33
+ vi.mock('../providers/WidgetProvider/WidgetProvider.js', () => ({
34
+ useWidgetConfig: () => widgetConfig,
35
+ }))
36
+
37
+ const client = { config: { apiUrl: API_URL } } as unknown as SDKClient
38
+ vi.mock('../providers/SDKClientProvider.js', () => ({
39
+ useSDKClient: () => client,
40
+ }))
41
+
42
+ const settingsStore = {
43
+ getState: () => ({ initializeTools: mocks.initializeTools }),
44
+ }
45
+ vi.mock('../stores/settings/SettingsStore.js', () => ({
46
+ useSettingsStoreContext: () => settingsStore,
47
+ }))
48
+
49
+ import { getToolsQueryKey } from '../queries/getTools.js'
50
+ import { useTools } from './useTools.js'
51
+
52
+ const TOOLS = {
53
+ bridges: [{ key: 'across' }, { key: 'stargate' }, { key: 'hop' }],
54
+ exchanges: [{ key: '1inch' }, { key: 'paraswap' }],
55
+ } as unknown as ToolsResponse
56
+
57
+ const Probe = () => {
58
+ useTools()
59
+ return null
60
+ }
61
+
62
+ let root: Root
63
+ let queryClient: QueryClient
64
+
65
+ const render = async () => {
66
+ await act(async () => {
67
+ root.render(
68
+ <QueryClientProvider client={queryClient}>
69
+ <Probe />
70
+ </QueryClientProvider>
71
+ )
72
+ })
73
+ }
74
+
75
+ describe('useTools with a shared tools entry', () => {
76
+ beforeEach(() => {
77
+ vi.clearAllMocks()
78
+ mocks.getTools.mockResolvedValue(TOOLS)
79
+ widgetConfig = {
80
+ integrator: 'test',
81
+ queryScopeKey: 'host',
82
+ bridges: { allow: ['across', 'hop'] },
83
+ exchanges: { deny: ['paraswap'] },
84
+ }
85
+ queryClient = new QueryClient({
86
+ defaultOptions: { queries: { retry: false } },
87
+ })
88
+ const container = document.createElement('div')
89
+ document.body.appendChild(container)
90
+ root = createRoot(container)
91
+ })
92
+
93
+ afterEach(async () => {
94
+ await act(async () => root.unmount())
95
+ queryClient.clear()
96
+ })
97
+
98
+ it('initialises the settings store when the host already fetched the tools', async () => {
99
+ // The host app fetched first and filled the shared entry.
100
+ queryClient.setQueryData(
101
+ getToolsQueryKey({ apiUrl: API_URL, scopeKey: 'host' }),
102
+ TOOLS
103
+ )
104
+
105
+ await render()
106
+
107
+ await vi.waitFor(() => {
108
+ expect(mocks.initializeTools).toHaveBeenCalledWith('Bridges', [
109
+ 'across',
110
+ 'hop',
111
+ ])
112
+ })
113
+ expect(mocks.initializeTools).toHaveBeenCalledWith('Exchanges', ['1inch'])
114
+ // The widget read the host's entry instead of sending its own request.
115
+ expect(mocks.getTools).not.toHaveBeenCalled()
116
+ })
117
+
118
+ it('initialises the settings store when the widget fetched the tools itself', async () => {
119
+ await render()
120
+
121
+ await vi.waitFor(() => {
122
+ expect(mocks.initializeTools).toHaveBeenCalledWith('Bridges', [
123
+ 'across',
124
+ 'hop',
125
+ ])
126
+ })
127
+ expect(mocks.getTools).toHaveBeenCalledTimes(1)
128
+ })
129
+
130
+ it('initialises with the filtered view while the cache keeps the raw list', async () => {
131
+ await render()
132
+
133
+ await vi.waitFor(() => {
134
+ expect(mocks.initializeTools).toHaveBeenCalled()
135
+ })
136
+ const cached = queryClient.getQueryData<ToolsResponse>(
137
+ getToolsQueryKey({ apiUrl: API_URL, scopeKey: 'host' })
138
+ )
139
+ expect(cached?.bridges).toHaveLength(3)
140
+ expect(cached?.exchanges).toHaveLength(2)
141
+ })
142
+
143
+ it('refreshes the tools every three minutes', async () => {
144
+ await render()
145
+ await vi.waitFor(() => {
146
+ expect(mocks.initializeTools).toHaveBeenCalled()
147
+ })
148
+
149
+ const [observer] = queryClient.getQueryCache().find({
150
+ queryKey: getToolsQueryKey({ apiUrl: API_URL, scopeKey: 'host' }),
151
+ })!.observers
152
+ expect(observer.options.refetchInterval).toBe(180_000)
153
+ expect(observer.options.staleTime).toBe(180_000)
154
+ })
155
+ })
@@ -1,56 +1,61 @@
1
- import { getTools, type ToolsResponse } from '@lifi/sdk'
1
+ import type { ToolsResponse } from '@lifi/sdk'
2
2
  import { useQuery } from '@tanstack/react-query'
3
+ import { useCallback, useEffect } from 'react'
3
4
  import { useSDKClient } from '../providers/SDKClientProvider.js'
4
5
  import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'
6
+ import { getToolsQueryOptions } from '../queries/getTools.js'
5
7
  import { useSettingsStoreContext } from '../stores/settings/SettingsStore.js'
6
8
  import { getConfigItemSets, isItemAllowedForSets } from '../utils/item.js'
7
- import { getQueryKey } from '../utils/queries.js'
9
+
10
+ const refetchInterval = 180_000
8
11
 
9
12
  export const useTools = (): { tools: ToolsResponse | undefined } => {
10
- const { bridges, exchanges, keyPrefix } = useWidgetConfig()
13
+ const { bridges, exchanges, queryScopeKey } = useWidgetConfig()
11
14
  const settingsStore = useSettingsStoreContext()
12
15
  const sdkClient = useSDKClient()
13
16
 
14
- const { data } = useQuery({
15
- queryKey: [
16
- getQueryKey('tools', keyPrefix),
17
- bridges?.allow,
18
- bridges?.deny,
19
- exchanges?.allow,
20
- exchanges?.deny,
21
- ],
22
- queryFn: async (): Promise<ToolsResponse> => {
23
- const tools = await getTools(sdkClient)
24
- const bridgesConfigSets = getConfigItemSets(
25
- bridges,
26
- (bridges) => new Set(bridges)
27
- )
28
- const exchangesConfigSets = getConfigItemSets(
29
- exchanges,
30
- (exchanges) => new Set(exchanges)
31
- )
32
- const result = {
17
+ const selectAllowedTools = useCallback(
18
+ (tools: ToolsResponse): ToolsResponse => {
19
+ const bridgeSets = getConfigItemSets(bridges, (keys) => new Set(keys))
20
+ const exchangeSets = getConfigItemSets(exchanges, (keys) => new Set(keys))
21
+ return {
33
22
  bridges: tools.bridges.filter((bridge) =>
34
- isItemAllowedForSets(bridge.key, bridgesConfigSets)
23
+ isItemAllowedForSets(bridge.key, bridgeSets)
35
24
  ),
36
25
  exchanges: tools.exchanges.filter((exchange) =>
37
- isItemAllowedForSets(exchange.key, exchangesConfigSets)
26
+ isItemAllowedForSets(exchange.key, exchangeSets)
38
27
  ),
39
28
  }
40
- const { initializeTools } = settingsStore.getState()
41
- initializeTools(
42
- 'Bridges',
43
- result.bridges.map((bridge) => bridge.key)
44
- )
45
- initializeTools(
46
- 'Exchanges',
47
- result.exchanges.map((exchange) => exchange.key)
48
- )
49
- return result
50
29
  },
51
- refetchInterval: 180_000,
52
- staleTime: 180_000,
53
- })
30
+ [bridges, exchanges]
31
+ )
32
+
33
+ const { data } = useQuery(
34
+ getToolsQueryOptions(sdkClient, {
35
+ scopeKey: queryScopeKey,
36
+ query: {
37
+ refetchInterval,
38
+ staleTime: refetchInterval,
39
+ select: selectAllowedTools,
40
+ },
41
+ })
42
+ )
43
+
44
+ // A host may fill the shared entry, so seed from the data, not in a queryFn.
45
+ useEffect(() => {
46
+ if (!data) {
47
+ return
48
+ }
49
+ const { initializeTools } = settingsStore.getState()
50
+ initializeTools(
51
+ 'Bridges',
52
+ data.bridges.map((bridge) => bridge.key)
53
+ )
54
+ initializeTools(
55
+ 'Exchanges',
56
+ data.exchanges.map((exchange) => exchange.key)
57
+ )
58
+ }, [data, settingsStore])
54
59
 
55
60
  return { tools: data }
56
61
  }
@@ -19,12 +19,12 @@ export const useTransactionDetails = (
19
19
  } => {
20
20
  const { account, accounts } = useAccount()
21
21
  const queryClient = useQueryClient()
22
- const { keyPrefix } = useWidgetConfig()
22
+ const { storageScopeKey } = useWidgetConfig()
23
23
  const sdkClient = useSDKClient()
24
24
 
25
25
  const transactionHistoryQueryKey = useMemo(
26
- () => getQueryKey('transaction-history', keyPrefix),
27
- [keyPrefix]
26
+ () => getQueryKey('transaction-history', storageScopeKey),
27
+ [storageScopeKey]
28
28
  )
29
29
 
30
30
  const { data, isLoading } = useQuery({
@@ -22,14 +22,14 @@ export const useTransactionHistory = (): {
22
22
  } => {
23
23
  const store = useRouteExecutionStoreContext()
24
24
  const { accounts } = useAccount()
25
- const { keyPrefix } = useWidgetConfig()
25
+ const { storageScopeKey } = useWidgetConfig()
26
26
  const sdkClient = useSDKClient()
27
27
  const { tools } = useTools()
28
28
 
29
29
  const { data: routeExecutions, isLoading } = useQueries({
30
30
  queries: accounts.map((account) => ({
31
31
  queryKey: [
32
- getQueryKey('transaction-history', keyPrefix),
32
+ getQueryKey('transaction-history', storageScopeKey),
33
33
  account.address,
34
34
  ],
35
35
  queryFn: (async ({ queryKey: [, accountAddress], signal }) => {
package/src/index.ts CHANGED
@@ -8,13 +8,14 @@ export * from './components/ContractComponent/NFT/NFTBase.js'
8
8
  export * from './components/ContractComponent/NFT/types.js'
9
9
  export * from './components/Skeleton/WidgetSkeleton.js'
10
10
  export { defaultMaxHeight } from './config/constants.js'
11
- export * from './config/version.js'
11
+ export { name, version } from './config/version.js'
12
12
  export { useWidgetChains } from './hooks/useWidgetChains.js'
13
13
  export {
14
14
  useWidgetEvents,
15
15
  type WidgetEventEmitter,
16
16
  widgetEvents,
17
17
  } from './hooks/useWidgetEvents.js'
18
+ export * from './queries/index.js'
18
19
  export * from './stores/bookmarks/types.js'
19
20
  export * from './stores/form/types.js'
20
21
  export { useFieldActions } from './stores/form/useFieldActions.js'
@@ -0,0 +1,73 @@
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
+ import type { WidgetContextProps } from './types.js'
8
+
9
+ const settingsActions = { setDefaultSettings: vi.fn() }
10
+ vi.mock('../../stores/settings/useSettingsActions.js', () => ({
11
+ useSettingsActions: () => settingsActions,
12
+ }))
13
+
14
+ import { useWidgetConfig, WidgetProvider } from './WidgetProvider.js'
15
+
16
+ let root: Root
17
+ let seen: WidgetContextProps | undefined
18
+
19
+ const Probe = () => {
20
+ seen = useWidgetConfig()
21
+ return null
22
+ }
23
+
24
+ const render = async (config: WidgetConfig) => {
25
+ await act(async () => {
26
+ root.render(
27
+ <WidgetProvider config={config}>
28
+ <Probe />
29
+ </WidgetProvider>
30
+ )
31
+ })
32
+ }
33
+
34
+ describe('WidgetProvider scope keys', () => {
35
+ beforeEach(() => {
36
+ seen = undefined
37
+ const container = document.createElement('div')
38
+ document.body.appendChild(container)
39
+ root = createRoot(container)
40
+ })
41
+
42
+ afterEach(async () => {
43
+ await act(async () => root.unmount())
44
+ })
45
+
46
+ // An integration that only ever set the deprecated keyPrefix keeps its
47
+ // isolation: everything below reads the resolved keys, never keyPrefix.
48
+ it('resolves both scopes from keyPrefix alone', async () => {
49
+ await render({ integrator: 'test', keyPrefix: 'legacy' })
50
+
51
+ expect(seen?.storageScopeKey).toBe('legacy')
52
+ expect(seen?.queryScopeKey).toBe('legacy')
53
+ })
54
+
55
+ it('prefers the explicit scopes over keyPrefix', async () => {
56
+ await render({
57
+ integrator: 'test',
58
+ keyPrefix: 'legacy',
59
+ storageScopeKey: 'store',
60
+ queryScopeKey: 'query',
61
+ })
62
+
63
+ expect(seen?.storageScopeKey).toBe('store')
64
+ expect(seen?.queryScopeKey).toBe('query')
65
+ })
66
+
67
+ it('keeps the historical storage names and the default query scope when nothing is set', async () => {
68
+ await render({ integrator: 'test' })
69
+
70
+ expect(seen?.storageScopeKey).toBeUndefined()
71
+ expect(seen?.queryScopeKey).toBe('li.fi')
72
+ })
73
+ })
@@ -1,11 +1,17 @@
1
1
  import type { Context } from 'react'
2
2
  import { createContext, use, useId, useMemo } from 'react'
3
3
  import { useSettingsActions } from '../../stores/settings/useSettingsActions.js'
4
+ import {
5
+ defaultQueryScopeKey,
6
+ resolveQueryScopeKey,
7
+ resolveStorageScopeKey,
8
+ } from '../../utils/scopeKeys.js'
4
9
  import type { WidgetContextProps, WidgetProviderProps } from './types.js'
5
10
 
6
11
  const initialContext: WidgetContextProps = {
7
12
  elementId: '',
8
13
  integrator: '',
14
+ queryScopeKey: defaultQueryScopeKey,
9
15
  }
10
16
 
11
17
  export const WidgetContext: Context<WidgetContextProps> =
@@ -24,10 +30,16 @@ export const WidgetProvider: React.FC<
24
30
  }
25
31
 
26
32
  const value = useMemo((): WidgetContextProps => {
33
+ // Resolved once, so nothing below reads the deprecated keyPrefix.
34
+ const scopeKeys = {
35
+ storageScopeKey: resolveStorageScopeKey(widgetConfig),
36
+ queryScopeKey: resolveQueryScopeKey(widgetConfig),
37
+ }
27
38
  try {
28
39
  // Create widget configuration object
29
40
  const value = {
30
41
  ...widgetConfig,
42
+ ...scopeKeys,
31
43
  elementId,
32
44
  } as WidgetContextProps
33
45
 
@@ -39,6 +51,7 @@ export const WidgetProvider: React.FC<
39
51
  console.warn(e)
40
52
  return {
41
53
  ...widgetConfig,
54
+ ...scopeKeys,
42
55
  elementId,
43
56
  integrator: widgetConfig.integrator,
44
57
  }
@@ -2,6 +2,8 @@ import type { WidgetConfig } from '../../types/widget.js'
2
2
 
3
3
  export type WidgetContextProps = WidgetConfig & {
4
4
  elementId: string
5
+ /** Always resolved by `WidgetProvider` — see `resolveQueryScopeKey`. */
6
+ queryScopeKey: string
5
7
  }
6
8
 
7
9
  export interface WidgetProviderProps {
@@ -0,0 +1,93 @@
1
+ import type { ExtendedChain, SDKClient } from '@lifi/sdk'
2
+ import { ChainType, getChains } from '@lifi/sdk'
3
+ import type {
4
+ FactoryQueryOptions,
5
+ QueryParameter,
6
+ ScopeKeyParameter,
7
+ } from './types.js'
8
+
9
+ /** Every chain type the widget supports, in the order keys are built in. */
10
+ export const supportedChainTypes: readonly ChainType[] = [
11
+ ChainType.EVM,
12
+ ChainType.SVM,
13
+ ChainType.UTXO,
14
+ ChainType.MVM,
15
+ ChainType.TVM,
16
+ ChainType.STL,
17
+ ]
18
+
19
+ export type GetChainsQueryFnData = ExtendedChain[]
20
+
21
+ export type GetChainsQueryKeyParameters = {
22
+ /** API base the client talks to. Different bases return different chains. */
23
+ apiUrl: string
24
+ chainTypes: readonly ChainType[]
25
+ } & ScopeKeyParameter
26
+
27
+ export type GetChainsQueryKey = readonly [
28
+ 'chains',
29
+ { apiUrl: string; chainTypes: ChainType[]; scopeKey?: string },
30
+ ]
31
+
32
+ const rank = (type: ChainType): number => {
33
+ const index = supportedChainTypes.indexOf(type)
34
+ return index === -1 ? supportedChainTypes.length : index
35
+ }
36
+
37
+ /** The key alone — to read, set or invalidate the entry. */
38
+ export function getChainsQueryKey(
39
+ parameters: GetChainsQueryKeyParameters
40
+ ): GetChainsQueryKey {
41
+ // Array order is part of the query hash, so sort into one canonical order.
42
+ const chainTypes = [...new Set(parameters.chainTypes)].sort(
43
+ (a, b) => rank(a) - rank(b) || a.localeCompare(b)
44
+ )
45
+ return [
46
+ 'chains',
47
+ {
48
+ apiUrl: parameters.apiUrl,
49
+ chainTypes,
50
+ ...(parameters.scopeKey === undefined
51
+ ? {}
52
+ : { scopeKey: parameters.scopeKey }),
53
+ },
54
+ ]
55
+ }
56
+
57
+ export type GetChainsOptions<selectData = GetChainsQueryFnData> = {
58
+ /** Chain types to request, in any order. Defaults to every supported type. */
59
+ chainTypes?: readonly ChainType[] | undefined
60
+ } & ScopeKeyParameter &
61
+ QueryParameter<GetChainsQueryFnData, selectData, GetChainsQueryKey>
62
+
63
+ export type GetChainsQueryOptions<selectData = GetChainsQueryFnData> =
64
+ FactoryQueryOptions<GetChainsQueryFnData, selectData, GetChainsQueryKey>
65
+
66
+ /**
67
+ * Query options for the chain list of one API base.
68
+ *
69
+ * The key is `['chains', { apiUrl, chainTypes, scopeKey }]`: the API base and
70
+ * the requested types decide the response, the scope isolates integrations.
71
+ * The widget and a host app that build their options here, with the same
72
+ * scope, therefore share one cache entry and one request.
73
+ *
74
+ * The query has no side effects. A caller that needs the chains in its SDK
75
+ * client's storage calls `client.setChains` on the data — the entry may have
76
+ * been filled by another caller, so a `queryFn` cannot be relied on to run.
77
+ */
78
+ export function getChainsQueryOptions<selectData = GetChainsQueryFnData>(
79
+ client: SDKClient,
80
+ options: GetChainsOptions<selectData> = {}
81
+ ): GetChainsQueryOptions<selectData> {
82
+ return {
83
+ ...options.query,
84
+ queryKey: getChainsQueryKey({
85
+ apiUrl: client.config.apiUrl,
86
+ chainTypes: options.chainTypes ?? supportedChainTypes,
87
+ scopeKey: options.scopeKey,
88
+ }),
89
+ // No signal: the data still fills the cache if all observers leave.
90
+ queryFn: ({ queryKey: [, { chainTypes }] }) =>
91
+ getChains(client, { chainTypes }),
92
+ }
93
+ }