@lifi/widget 4.7.0 → 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 (144) 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/useExplorer.d.ts +1 -1
  16. package/dist/esm/hooks/useExplorer.d.ts.map +1 -1
  17. package/dist/esm/hooks/useExplorer.js +7 -3
  18. package/dist/esm/hooks/useExplorer.js.map +1 -1
  19. package/dist/esm/hooks/useFromTokenSufficiency.js +3 -3
  20. package/dist/esm/hooks/useFromTokenSufficiency.js.map +1 -1
  21. package/dist/esm/hooks/useGasRecommendation.js +3 -3
  22. package/dist/esm/hooks/useGasRecommendation.js.map +1 -1
  23. package/dist/esm/hooks/useGasSufficiency.js +3 -3
  24. package/dist/esm/hooks/useGasSufficiency.js.map +1 -1
  25. package/dist/esm/hooks/useIsBatchingSupported.js +2 -2
  26. package/dist/esm/hooks/useIsBatchingSupported.js.map +1 -1
  27. package/dist/esm/hooks/useRouteExecution.js +6 -6
  28. package/dist/esm/hooks/useRouteExecution.js.map +1 -1
  29. package/dist/esm/hooks/useRoutes.js +7 -7
  30. package/dist/esm/hooks/useRoutes.js.map +1 -1
  31. package/dist/esm/hooks/useTokenBalance.js +4 -4
  32. package/dist/esm/hooks/useTokenBalance.js.map +1 -1
  33. package/dist/esm/hooks/useTokenBalancesQueries.js +3 -3
  34. package/dist/esm/hooks/useTokenBalancesQueries.js.map +1 -1
  35. package/dist/esm/hooks/useTokenSearch.js +3 -3
  36. package/dist/esm/hooks/useTokenSearch.js.map +1 -1
  37. package/dist/esm/hooks/useTokens.js +14 -14
  38. package/dist/esm/hooks/useTokens.js.map +1 -1
  39. package/dist/esm/hooks/useTools.d.ts.map +1 -1
  40. package/dist/esm/hooks/useTools.js +26 -27
  41. package/dist/esm/hooks/useTools.js.map +1 -1
  42. package/dist/esm/hooks/useTransactionDetails.js +2 -2
  43. package/dist/esm/hooks/useTransactionDetails.js.map +1 -1
  44. package/dist/esm/hooks/useTransactionHistory.js +3 -3
  45. package/dist/esm/hooks/useTransactionHistory.js.map +1 -1
  46. package/dist/esm/index.d.ts +5 -1
  47. package/dist/esm/index.js +3 -1
  48. package/dist/esm/pages/RoutesPage/RoutesPage.style.d.ts.map +1 -1
  49. package/dist/esm/pages/SendToWallet/BookmarksPage.js +2 -1
  50. package/dist/esm/pages/SendToWallet/BookmarksPage.js.map +1 -1
  51. package/dist/esm/pages/SendToWallet/ConnectedWalletsPage.js +2 -1
  52. package/dist/esm/pages/SendToWallet/ConnectedWalletsPage.js.map +1 -1
  53. package/dist/esm/pages/SendToWallet/RecentWalletsPage.js +2 -1
  54. package/dist/esm/pages/SendToWallet/RecentWalletsPage.js.map +1 -1
  55. package/dist/esm/pages/SendToWallet/SendToConfiguredWalletPage.js +2 -1
  56. package/dist/esm/pages/SendToWallet/SendToConfiguredWalletPage.js.map +1 -1
  57. package/dist/esm/pages/TransactionDetailsPage/TransactionDetailsPage.js +2 -1
  58. package/dist/esm/pages/TransactionDetailsPage/TransactionDetailsPage.js.map +1 -1
  59. package/dist/esm/pages/TransactionDetailsPage/TransferIdCard.js +2 -1
  60. package/dist/esm/pages/TransactionDetailsPage/TransferIdCard.js.map +1 -1
  61. package/dist/esm/providers/I18nProvider/types.d.ts.map +1 -1
  62. package/dist/esm/providers/WidgetProvider/WidgetProvider.d.ts.map +1 -1
  63. package/dist/esm/providers/WidgetProvider/WidgetProvider.js +9 -1
  64. package/dist/esm/providers/WidgetProvider/WidgetProvider.js.map +1 -1
  65. package/dist/esm/providers/WidgetProvider/types.d.ts +2 -0
  66. package/dist/esm/providers/WidgetProvider/types.d.ts.map +1 -1
  67. package/dist/esm/queries/getChains.d.ts +38 -0
  68. package/dist/esm/queries/getChains.d.ts.map +1 -0
  69. package/dist/esm/queries/getChains.js +51 -0
  70. package/dist/esm/queries/getChains.js.map +1 -0
  71. package/dist/esm/queries/getTools.d.ts +26 -0
  72. package/dist/esm/queries/getTools.d.ts.map +1 -0
  73. package/dist/esm/queries/getTools.js +30 -0
  74. package/dist/esm/queries/getTools.js.map +1 -0
  75. package/dist/esm/queries/index.d.ts +4 -0
  76. package/dist/esm/queries/index.js +3 -0
  77. package/dist/esm/queries/types.d.ts +25 -0
  78. package/dist/esm/queries/types.d.ts.map +1 -0
  79. package/dist/esm/shared.d.ts +3 -1
  80. package/dist/esm/shared.js +3 -1
  81. package/dist/esm/stores/StoreProvider.d.ts.map +1 -1
  82. package/dist/esm/stores/StoreProvider.js +16 -17
  83. package/dist/esm/stores/StoreProvider.js.map +1 -1
  84. package/dist/esm/stores/header/useHeaderStore.d.ts +1 -1
  85. package/dist/esm/stores/header/useHeaderStore.d.ts.map +1 -1
  86. package/dist/esm/stores/header/useHeaderStore.js.map +1 -1
  87. package/dist/esm/themes/createTheme.d.ts.map +1 -1
  88. package/dist/esm/types/events.d.ts.map +1 -1
  89. package/dist/esm/types/token.d.ts.map +1 -1
  90. package/dist/esm/types/widget.d.ts +30 -0
  91. package/dist/esm/types/widget.d.ts.map +1 -1
  92. package/dist/esm/types/widget.js.map +1 -1
  93. package/dist/esm/utils/queries.js.map +1 -1
  94. package/dist/esm/utils/scopeKeys.d.ts +6 -0
  95. package/dist/esm/utils/scopeKeys.d.ts.map +1 -0
  96. package/dist/esm/utils/scopeKeys.js +8 -0
  97. package/dist/esm/utils/scopeKeys.js.map +1 -0
  98. package/dist/esm/utils/url.d.ts +5 -0
  99. package/dist/esm/utils/url.d.ts.map +1 -0
  100. package/dist/esm/utils/url.js +17 -0
  101. package/dist/esm/utils/url.js.map +1 -0
  102. package/package.json +12 -4
  103. package/src/config/version.ts +1 -1
  104. package/src/hooks/useAvailableChains.test.tsx +170 -0
  105. package/src/hooks/useAvailableChains.ts +29 -47
  106. package/src/hooks/useExplorer.test.tsx +106 -0
  107. package/src/hooks/useExplorer.ts +11 -5
  108. package/src/hooks/useFromTokenSufficiency.ts +2 -2
  109. package/src/hooks/useGasRecommendation.ts +2 -2
  110. package/src/hooks/useGasSufficiency.ts +2 -2
  111. package/src/hooks/useIsBatchingSupported.ts +2 -2
  112. package/src/hooks/useRouteExecution.ts +5 -5
  113. package/src/hooks/useRoutes.ts +6 -6
  114. package/src/hooks/useTokenBalance.ts +4 -4
  115. package/src/hooks/useTokenBalancesQueries.ts +3 -3
  116. package/src/hooks/useTokenSearch.ts +7 -3
  117. package/src/hooks/useTokens.ts +13 -14
  118. package/src/hooks/useTools.sharedEntry.test.tsx +155 -0
  119. package/src/hooks/useTools.ts +42 -37
  120. package/src/hooks/useTransactionDetails.ts +3 -3
  121. package/src/hooks/useTransactionHistory.ts +2 -2
  122. package/src/index.ts +2 -1
  123. package/src/pages/SendToWallet/BookmarksPage.tsx +3 -3
  124. package/src/pages/SendToWallet/ConnectedWalletsPage.tsx +3 -3
  125. package/src/pages/SendToWallet/RecentWalletsPage.tsx +3 -3
  126. package/src/pages/SendToWallet/SendToConfiguredWalletPage.tsx +3 -3
  127. package/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx +5 -5
  128. package/src/pages/TransactionDetailsPage/TransferIdCard.tsx +2 -1
  129. package/src/providers/WidgetProvider/WidgetProvider.test.tsx +73 -0
  130. package/src/providers/WidgetProvider/WidgetProvider.tsx +13 -0
  131. package/src/providers/WidgetProvider/types.ts +2 -0
  132. package/src/queries/getChains.ts +93 -0
  133. package/src/queries/getTools.ts +62 -0
  134. package/src/queries/index.ts +30 -0
  135. package/src/queries/sharedCache.test.tsx +223 -0
  136. package/src/queries/types.ts +38 -0
  137. package/src/shared.ts +2 -0
  138. package/src/stores/StoreProvider.test.tsx +107 -0
  139. package/src/stores/StoreProvider.tsx +8 -6
  140. package/src/stores/header/useHeaderStore.tsx +3 -4
  141. package/src/types/widget.ts +30 -0
  142. package/src/utils/queries.ts +1 -0
  143. package/src/utils/scopeKeys.ts +18 -0
  144. package/src/utils/url.ts +17 -0
@@ -0,0 +1,170 @@
1
+ /** @vitest-environment happy-dom */
2
+
3
+ import type * as LifiSdk from '@lifi/sdk'
4
+ import { ChainType, type ExtendedChain, type SDKClient } 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 chains entry is shared with the host app through
13
+ * `getChainsQueryOptions`, keyed by the client's API base, the requested chain
14
+ * types and the widget's `queryScopeKey`. The widget's own SDK client still
15
+ * gets the chains, whoever filled the entry.
16
+ */
17
+
18
+ const API_URL = 'https://api.example/v1'
19
+
20
+ const mocks = vi.hoisted(() => ({
21
+ getChains: vi.fn(),
22
+ setChains: vi.fn(),
23
+ }))
24
+
25
+ vi.mock('@lifi/sdk', async (importOriginal) => ({
26
+ ...(await importOriginal<typeof LifiSdk>()),
27
+ getChains: mocks.getChains,
28
+ }))
29
+
30
+ let widgetConfig: Partial<WidgetConfig> = {}
31
+
32
+ vi.mock('../providers/WidgetProvider/WidgetProvider.js', () => ({
33
+ useWidgetConfig: () => widgetConfig,
34
+ }))
35
+
36
+ const client = {
37
+ config: { apiUrl: API_URL },
38
+ setChains: mocks.setChains,
39
+ } as unknown as SDKClient
40
+ vi.mock('../providers/SDKClientProvider.js', () => ({
41
+ useSDKClient: () => client,
42
+ }))
43
+
44
+ import { getChainsQueryKey } from '../queries/getChains.js'
45
+ import { useAvailableChains } from './useAvailableChains.js'
46
+
47
+ const CHAINS = [{ id: 1, name: 'Ethereum' }] as ExtendedChain[]
48
+
49
+ let root: Root
50
+ let queryClient: QueryClient
51
+ let externalConfig: WidgetConfig | undefined
52
+
53
+ const Probe = () => {
54
+ useAvailableChains(undefined, externalConfig)
55
+ return null
56
+ }
57
+
58
+ const render = async () => {
59
+ await act(async () => {
60
+ root.render(
61
+ <QueryClientProvider client={queryClient}>
62
+ <Probe />
63
+ </QueryClientProvider>
64
+ )
65
+ })
66
+ }
67
+
68
+ const evmKey = getChainsQueryKey({
69
+ apiUrl: API_URL,
70
+ chainTypes: [ChainType.EVM],
71
+ scopeKey: 'host',
72
+ })
73
+
74
+ describe('useAvailableChains', () => {
75
+ beforeEach(() => {
76
+ vi.clearAllMocks()
77
+ mocks.getChains.mockResolvedValue(CHAINS)
78
+ widgetConfig = {
79
+ queryScopeKey: 'host',
80
+ chains: { types: { allow: [ChainType.EVM] } },
81
+ }
82
+ externalConfig = undefined
83
+ queryClient = new QueryClient({
84
+ defaultOptions: { queries: { retry: false } },
85
+ })
86
+ const container = document.createElement('div')
87
+ document.body.appendChild(container)
88
+ root = createRoot(container)
89
+ })
90
+
91
+ afterEach(async () => {
92
+ await act(async () => root.unmount())
93
+ queryClient.clear()
94
+ })
95
+
96
+ it('fetches the allowed chain types under the widget scope and hands them to the client', async () => {
97
+ await render()
98
+
99
+ await vi.waitFor(() => {
100
+ expect(mocks.setChains).toHaveBeenCalledWith(CHAINS)
101
+ })
102
+ expect(mocks.getChains).toHaveBeenCalledWith(client, {
103
+ chainTypes: [ChainType.EVM],
104
+ })
105
+ expect(queryClient.getQueryData(evmKey)).toBe(CHAINS)
106
+ })
107
+
108
+ it('reads an entry the host filled, and still hands the chains to the client', async () => {
109
+ queryClient.setQueryData(evmKey, CHAINS)
110
+
111
+ await render()
112
+
113
+ await vi.waitFor(() => {
114
+ expect(mocks.setChains).toHaveBeenCalledWith(CHAINS)
115
+ })
116
+ expect(mocks.getChains).not.toHaveBeenCalled()
117
+ })
118
+
119
+ it('hands the chains to the client again after a refetch with equal data', async () => {
120
+ // Structural sharing keeps the data reference on an equal refetch. The
121
+ // client's own chain storage expires, so every fetch must refresh it.
122
+ await render()
123
+ await vi.waitFor(() => {
124
+ expect(mocks.setChains).toHaveBeenCalledTimes(1)
125
+ })
126
+
127
+ await act(async () => {
128
+ await queryClient.refetchQueries({ queryKey: evmKey })
129
+ })
130
+
131
+ await vi.waitFor(() => {
132
+ expect(mocks.setChains).toHaveBeenCalledTimes(2)
133
+ })
134
+ })
135
+
136
+ it('refreshes the chains every five minutes', async () => {
137
+ await render()
138
+ await vi.waitFor(() => {
139
+ expect(queryClient.getQueryData(evmKey)).toBe(CHAINS)
140
+ })
141
+
142
+ const [observer] = queryClient
143
+ .getQueryCache()
144
+ .find({ queryKey: evmKey })!.observers
145
+ expect(observer.options.refetchInterval).toBe(300_000)
146
+ expect(observer.options.staleTime).toBe(300_000)
147
+ })
148
+
149
+ it('uses the API base and scope of an external widget config', async () => {
150
+ externalConfig = {
151
+ integrator: 'partner',
152
+ keyPrefix: 'partner',
153
+ sdkConfig: { apiUrl: 'https://partner.example/v1' },
154
+ chains: { types: { allow: [ChainType.SVM] } },
155
+ }
156
+
157
+ await render()
158
+
159
+ const partnerKey = getChainsQueryKey({
160
+ apiUrl: 'https://partner.example/v1',
161
+ chainTypes: [ChainType.SVM],
162
+ scopeKey: 'partner',
163
+ })
164
+ await vi.waitFor(() => {
165
+ expect(queryClient.getQueryData(partnerKey)).toBe(CHAINS)
166
+ })
167
+ // The external config's own client, not the widget's.
168
+ expect(mocks.getChains.mock.calls[0][0]).not.toBe(client)
169
+ })
170
+ })
@@ -1,26 +1,23 @@
1
- import type { ExtendedChain } from '@lifi/sdk'
2
- import { ChainType, createClient, getChains } from '@lifi/sdk'
1
+ import type { ChainType, ExtendedChain } from '@lifi/sdk'
2
+ import { createClient } from '@lifi/sdk'
3
3
  import { useQuery } from '@tanstack/react-query'
4
4
  import { useCallback, useEffect, useMemo } from 'react'
5
5
  import { useSDKClient } from '../providers/SDKClientProvider.js'
6
6
  import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'
7
+ import {
8
+ getChainsQueryOptions,
9
+ supportedChainTypes,
10
+ } from '../queries/getChains.js'
7
11
  import type { WidgetConfig } from '../types/widget.js'
8
12
  import { getConfigItemSets, isItemAllowedForSets } from '../utils/item.js'
9
- import { getQueryKey } from '../utils/queries.js'
13
+ import { resolveQueryScopeKey } from '../utils/scopeKeys.js'
10
14
 
11
15
  type GetChainById = (
12
16
  chainId?: number,
13
17
  chains?: ExtendedChain[]
14
18
  ) => ExtendedChain | undefined
15
19
 
16
- const supportedChainTypes = [
17
- ChainType.EVM,
18
- ChainType.SVM,
19
- ChainType.UTXO,
20
- ChainType.MVM,
21
- ChainType.TVM,
22
- ChainType.STL,
23
- ]
20
+ const refetchInterval = 300_000
24
21
 
25
22
  export const useAvailableChains = (
26
23
  chainTypes?: ChainType[],
@@ -30,7 +27,7 @@ export const useAvailableChains = (
30
27
  getChainById: GetChainById
31
28
  isLoading: boolean
32
29
  } => {
33
- const { chains: internalChains, keyPrefix: internalKeyPrefix } =
30
+ const { chains: internalChains, queryScopeKey: internalQueryScopeKey } =
34
31
  useWidgetConfig()
35
32
  const internalClient = useSDKClient()
36
33
 
@@ -46,46 +43,31 @@ export const useAvailableChains = (
46
43
  }, [widgetConfig])
47
44
 
48
45
  // Overwrite widget config if passed as param
49
- const keyPrefix = widgetConfig?.keyPrefix ?? internalKeyPrefix
46
+ const client = externalClient ?? internalClient
50
47
  const chains = widgetConfig?.chains ?? internalChains
48
+ const queryScopeKey = widgetConfig
49
+ ? resolveQueryScopeKey(widgetConfig)
50
+ : internalQueryScopeKey
51
51
 
52
- const { data, isLoading } = useQuery({
53
- queryKey: [
54
- getQueryKey('chains', keyPrefix),
55
- chains?.types,
56
- chains?.allow,
57
- chains?.deny,
58
- chains?.from,
59
- chains?.to,
60
- !!externalClient,
61
- ] as const,
62
- queryFn: async ({ queryKey: [, chainTypesConfig] }) => {
63
- const chainsConfigSets = getConfigItemSets(
64
- chainTypesConfig,
65
- (chains) => new Set(chains)
66
- )
67
- const chainTypesRequest = supportedChainTypes.filter((chainType) =>
68
- isItemAllowedForSets(chainType, chainsConfigSets)
69
- )
70
- const client = externalClient ?? internalClient
71
- const availableChains = await getChains(client, {
72
- chainTypes: chainTypes || chainTypesRequest,
73
- })
74
- client.setChains(availableChains)
75
- return availableChains
76
- },
77
- refetchInterval: 300_000,
78
- staleTime: 300_000,
79
- })
52
+ const typeSets = getConfigItemSets(chains?.types, (types) => new Set(types))
53
+ const requestedTypes =
54
+ chainTypes ??
55
+ supportedChainTypes.filter((type) => isItemAllowedForSets(type, typeSets))
56
+
57
+ const { data, dataUpdatedAt, isLoading } = useQuery(
58
+ getChainsQueryOptions(client, {
59
+ chainTypes: requestedTypes,
60
+ scopeKey: queryScopeKey,
61
+ query: { refetchInterval, staleTime: refetchInterval },
62
+ })
63
+ )
80
64
 
81
- // Ensure the current internal client always has chains, even when:
82
- // - the query result came from cache (external call populated it first)
83
- // - the client was recreated due to config changes (stale closure in queryFn)
65
+ // A host may fill the entry; key on dataUpdatedAt, as a refetch keeps data.
84
66
  useEffect(() => {
85
- if (data && !externalClient) {
86
- internalClient.setChains(data)
67
+ if (data && dataUpdatedAt) {
68
+ client.setChains(data)
87
69
  }
88
- }, [data, externalClient, internalClient])
70
+ }, [data, dataUpdatedAt, client])
89
71
 
90
72
  const getChainById: GetChainById = useCallback(
91
73
  (chainId?: number, chains: ExtendedChain[] | undefined = data) => {
@@ -0,0 +1,106 @@
1
+ /** @vitest-environment happy-dom */
2
+
3
+ import type { ExtendedChain } from '@lifi/sdk'
4
+ import { act } from 'react'
5
+ import { createRoot, type Root } from 'react-dom/client'
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7
+ import type { WidgetConfig } from '../types/widget.js'
8
+
9
+ const txHash = `0x${'ab'.repeat(32)}`
10
+
11
+ let explorerUrls: WidgetConfig['explorerUrls']
12
+ let blockExplorerUrls: string[] = []
13
+
14
+ const getChainById = (id: number) =>
15
+ ({
16
+ id,
17
+ chainType: 'EVM',
18
+ metamask: { blockExplorerUrls },
19
+ }) as unknown as ExtendedChain
20
+
21
+ vi.mock('../providers/WidgetProvider/WidgetProvider.js', () => ({
22
+ useWidgetConfig: () => ({ explorerUrls }),
23
+ }))
24
+
25
+ vi.mock('./useAvailableChains.js', () => ({
26
+ useAvailableChains: () => ({ getChainById }),
27
+ }))
28
+
29
+ const { useExplorer } = await import('./useExplorer.js')
30
+
31
+ let explorer: ReturnType<typeof useExplorer>
32
+
33
+ const ExplorerHandle = (): null => {
34
+ explorer = useExplorer()
35
+ return null
36
+ }
37
+
38
+ let root: Root
39
+
40
+ beforeEach(() => {
41
+ explorerUrls = undefined
42
+ blockExplorerUrls = ['https://etherscan.io/']
43
+ root = createRoot(document.createElement('div'))
44
+ })
45
+
46
+ afterEach(() => {
47
+ act(() => {
48
+ root.unmount()
49
+ })
50
+ })
51
+
52
+ const render = () => {
53
+ act(() => {
54
+ root.render(<ExplorerHandle />)
55
+ })
56
+ }
57
+
58
+ describe('useExplorer', () => {
59
+ it('builds links from an https explorer base', () => {
60
+ render()
61
+ expect(explorer.getAddressLink('0xabc', 1)).toBe(
62
+ 'https://etherscan.io/address/0xabc'
63
+ )
64
+ expect(explorer.getTransactionLink({ txHash, chain: 1 })).toBe(
65
+ `https://etherscan.io/tx/${txHash}`
66
+ )
67
+ })
68
+
69
+ it('returns no link when chain metadata carries a javascript: explorer base', () => {
70
+ blockExplorerUrls = ['javascript:alert(document.domain)//']
71
+ render()
72
+ expect(explorer.getAddressLink('0xabc', 1)).toBeUndefined()
73
+ expect(explorer.getTransactionLink({ txHash, chain: 1 })).toBeUndefined()
74
+ })
75
+
76
+ it('returns no link when the integrator config carries a javascript: explorer base', () => {
77
+ explorerUrls = {
78
+ 1: ['javascript:alert(document.domain)'],
79
+ internal: ['javascript:alert(document.domain)'],
80
+ }
81
+ render()
82
+ expect(explorer.getAddressLink('0xabc', 1)).toBeUndefined()
83
+ expect(explorer.getTransactionLink({ txHash })).toBeUndefined()
84
+ })
85
+
86
+ it('falls back to an https txLink when the hash link is rejected', () => {
87
+ blockExplorerUrls = ['javascript:alert(document.domain)//']
88
+ render()
89
+ expect(
90
+ explorer.getTransactionLink({
91
+ txHash,
92
+ txLink: 'https://scan.li.fi/tx/0xabc',
93
+ chain: 1,
94
+ })
95
+ ).toBe('https://scan.li.fi/tx/0xabc')
96
+ })
97
+
98
+ it('rejects a javascript: txLink fallback', () => {
99
+ render()
100
+ expect(
101
+ explorer.getTransactionLink({
102
+ txLink: 'javascript:alert(document.domain)',
103
+ })
104
+ ).toBeUndefined()
105
+ })
106
+ })
@@ -4,6 +4,7 @@ import { useCallback } from 'react'
4
4
  import { internalExplorerUrl } from '../config/constants.js'
5
5
  import { useAvailableChains } from '../hooks/useAvailableChains.js'
6
6
  import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'
7
+ import { isHttpUrl } from '../utils/url.js'
7
8
 
8
9
  const sanitiseBaseUrl = (baseUrl: string) => baseUrl.trim().replace(/\/+$/, '')
9
10
 
@@ -25,7 +26,10 @@ type TransactionLinkProps = {
25
26
 
26
27
  export const useExplorer = (): {
27
28
  getTransactionLink: (props: TransactionLinkProps) => string | undefined
28
- getAddressLink: (address: string, chain?: Chain | number) => string
29
+ getAddressLink: (
30
+ address: string,
31
+ chain?: Chain | number
32
+ ) => string | undefined
29
33
  } => {
30
34
  const { explorerUrls } = useWidgetConfig()
31
35
  const { getChainById } = useAvailableChains()
@@ -82,11 +86,12 @@ export const useExplorer = (): {
82
86
  config.resolvedChain?.chainType !== ChainType.EVM ||
83
87
  config.hasOverride ||
84
88
  isHex(txHash, { strict: true })
85
- if (validForEvm) {
86
- return `${config.url}/${config.txPath}/${txHash}`
89
+ const link = `${config.url}/${config.txPath}/${txHash}`
90
+ if (validForEvm && isHttpUrl(link)) {
91
+ return link
87
92
  }
88
93
  }
89
- return txLink
94
+ return isHttpUrl(txLink) ? txLink : undefined
90
95
  },
91
96
  [getExplorerConfig]
92
97
  )
@@ -94,7 +99,8 @@ export const useExplorer = (): {
94
99
  const getAddressLink = useCallback(
95
100
  (address: string, chain?: Chain | number) => {
96
101
  const config = getExplorerConfig(chain)
97
- return `${config.url}/${config.addressPath}/${address}`
102
+ const link = `${config.url}/${config.addressPath}/${address}`
103
+ return isHttpUrl(link) ? link : undefined
98
104
  },
99
105
  [getExplorerConfig]
100
106
  )
@@ -22,7 +22,7 @@ export const useFromTokenSufficiency = (
22
22
  'fromToken',
23
23
  'fromAmount'
24
24
  )
25
- const { keyPrefix } = useWidgetConfig()
25
+ const { storageScopeKey } = useWidgetConfig()
26
26
  const sdkClient = useSDKClient()
27
27
 
28
28
  let chainId = fromChainId
@@ -42,7 +42,7 @@ export const useFromTokenSufficiency = (
42
42
 
43
43
  const { data: insufficientFromToken, isLoading } = useQuery({
44
44
  queryKey: [
45
- getQueryKey('from-token-sufficiency-check', keyPrefix),
45
+ getQueryKey('from-token-sufficiency-check', storageScopeKey),
46
46
  account.address,
47
47
  chainId,
48
48
  tokenAddress,
@@ -14,7 +14,7 @@ export const useGasRecommendation = (
14
14
  fromToken?: string
15
15
  ): UseQueryResult<Awaited<ReturnType<typeof getGasRecommendation>> | null> => {
16
16
  const { chains } = useAvailableChains()
17
- const { keyPrefix, hiddenUI } = useWidgetConfig()
17
+ const { storageScopeKey, hiddenUI } = useWidgetConfig()
18
18
  const sdkClient = useSDKClient()
19
19
 
20
20
  const checkRecommendationLiFuel =
@@ -28,7 +28,7 @@ export const useGasRecommendation = (
28
28
 
29
29
  return useQuery({
30
30
  queryKey: [
31
- getQueryKey('gas-recommendation', keyPrefix),
31
+ getQueryKey('gas-recommendation', storageScopeKey),
32
32
  toChainId,
33
33
  fromChain,
34
34
  fromToken,
@@ -32,7 +32,7 @@ export const useGasSufficiency = (
32
32
  const { account: EVMAccount, accounts } = useAccount({
33
33
  chainType: ChainType.EVM,
34
34
  })
35
- const { keyPrefix, hiddenUI } = useWidgetConfig()
35
+ const { storageScopeKey, hiddenUI } = useWidgetConfig()
36
36
  const sdkClient = useSDKClient()
37
37
 
38
38
  const { relevantAccounts, relevantAccountsQueryKey } = useMemo(() => {
@@ -67,7 +67,7 @@ export const useGasSufficiency = (
67
67
 
68
68
  const { data: insufficientGas, isLoading } = useQuery<GasSufficiency[]>({
69
69
  queryKey: [
70
- getQueryKey('gas-sufficiency-check', keyPrefix),
70
+ getQueryKey('gas-sufficiency-check', storageScopeKey),
71
71
  relevantAccountsQueryKey,
72
72
  route?.id,
73
73
  isContractAddress,
@@ -13,7 +13,7 @@ export function useIsBatchingSupported(
13
13
  isBatchingSupported: boolean | undefined
14
14
  isBatchingSupportedLoading: boolean
15
15
  } {
16
- const { keyPrefix } = useWidgetConfig()
16
+ const { storageScopeKey } = useWidgetConfig()
17
17
  const sdkClient = useSDKClient()
18
18
  const { isBatchingSupported } = useEthereumContext()
19
19
 
@@ -22,7 +22,7 @@ export function useIsBatchingSupported(
22
22
  )
23
23
  const { data, isLoading } = useQuery({
24
24
  queryKey: [
25
- getQueryKey('isBatchingSupported', keyPrefix),
25
+ getQueryKey('isBatchingSupported', storageScopeKey),
26
26
  chain?.id,
27
27
  address,
28
28
  ],
@@ -43,7 +43,7 @@ export const useRouteExecution = ({
43
43
  const queryClient = useQueryClient()
44
44
  const { account } = useAccount()
45
45
  const resumedAfterMount = useRef(false)
46
- const { keyPrefix } = useWidgetConfig()
46
+ const { storageScopeKey } = useWidgetConfig()
47
47
  const sdkClient = useSDKClient()
48
48
  const emitter = useWidgetEvents()
49
49
  const routeExecutionStoreContext = useRouteExecutionStoreContext()
@@ -84,16 +84,16 @@ export const useRouteExecution = ({
84
84
  if (executionCompleted || executionFailed) {
85
85
  const invalidateKeys = [
86
86
  [
87
- getQueryKey('token-balances', keyPrefix),
87
+ getQueryKey('token-balances', storageScopeKey),
88
88
  clonedUpdatedRoute.fromAddress,
89
89
  clonedUpdatedRoute.fromChainId,
90
90
  ],
91
91
  [
92
- getQueryKey('token-balances', keyPrefix),
92
+ getQueryKey('token-balances', storageScopeKey),
93
93
  clonedUpdatedRoute.toAddress,
94
94
  clonedUpdatedRoute.toChainId,
95
95
  ],
96
- [getQueryKey('transaction-history', keyPrefix)],
96
+ [getQueryKey('transaction-history', storageScopeKey)],
97
97
  ]
98
98
  for (const key of invalidateKeys) {
99
99
  queryClient.invalidateQueries(
@@ -133,7 +133,7 @@ export const useRouteExecution = ({
133
133
  throw new Error('Execution route not found.')
134
134
  }
135
135
  queryClient.removeQueries({
136
- queryKey: [getQueryKey('routes', keyPrefix)],
136
+ queryKey: [getQueryKey('routes', storageScopeKey)],
137
137
  exact: false,
138
138
  })
139
139
  return executeRoute(sdkClient, routeExecution.route, {
@@ -82,7 +82,7 @@ export const useRoutes = ({
82
82
  exchanges,
83
83
  feeConfig,
84
84
  useRelayerRoutes,
85
- keyPrefix,
85
+ storageScopeKey,
86
86
  } = useWidgetConfig()
87
87
  const sdkClient = useSDKClient()
88
88
  const setExecutableRoute = useSetExecutableRoute()
@@ -214,7 +214,7 @@ export const useRoutes = ({
214
214
  const queryKey = useMemo(
215
215
  () =>
216
216
  [
217
- getQueryKey('routes', keyPrefix),
217
+ getQueryKey('routes', storageScopeKey),
218
218
  effectiveFromAddress,
219
219
  fromChain?.id as number,
220
220
  fromToken?.address as string,
@@ -244,7 +244,7 @@ export const useRoutes = ({
244
244
  observableRoute?.id,
245
245
  ] as const,
246
246
  [
247
- keyPrefix,
247
+ storageScopeKey,
248
248
  effectiveFromAddress,
249
249
  fromChain?.id,
250
250
  fromToken?.address,
@@ -550,14 +550,14 @@ export const useRoutes = ({
550
550
  ;[fromToken, toToken].forEach((token) => {
551
551
  // Update main tokens cache (verified)
552
552
  queryClient.setQueriesData<TokensByChain>(
553
- { queryKey: [getQueryKey('tokens', keyPrefix)] },
553
+ { queryKey: [getQueryKey('tokens', storageScopeKey)] },
554
554
  (data) => updateTokenInCache(data, token)
555
555
  )
556
556
 
557
557
  // Update search tokens cache (unverified) - matches any search query
558
558
  queryClient.setQueriesData<TokensByChain>(
559
559
  {
560
- queryKey: [getQueryKey('tokens-search', keyPrefix)],
560
+ queryKey: [getQueryKey('tokens-search', storageScopeKey)],
561
561
  exact: false,
562
562
  },
563
563
  (data) => updateTokenInCache(data, token)
@@ -566,7 +566,7 @@ export const useRoutes = ({
566
566
  queryClient.setQueriesData<Token[]>(
567
567
  {
568
568
  queryKey: [
569
- getQueryKey('token-balances', keyPrefix),
569
+ getQueryKey('token-balances', storageScopeKey),
570
570
  fromAddress,
571
571
  token.chainId,
572
572
  ],
@@ -23,18 +23,18 @@ export const useTokenBalance = (
23
23
  getTokenBalancesWithRetry: typeof getTokenBalancesWithRetry
24
24
  } => {
25
25
  const queryClient = useQueryClient()
26
- const { keyPrefix } = useWidgetConfig()
26
+ const { storageScopeKey } = useWidgetConfig()
27
27
  const sdkClient = useSDKClient()
28
28
 
29
29
  const tokenBalanceQueryKey = useMemo(
30
30
  () =>
31
31
  [
32
- getQueryKey('token-balance', keyPrefix),
32
+ getQueryKey('token-balance', storageScopeKey),
33
33
  accountAddress,
34
34
  token?.chainId,
35
35
  token?.address,
36
36
  ] as const,
37
- [token?.address, token?.chainId, accountAddress, keyPrefix]
37
+ [token?.address, token?.chainId, accountAddress, storageScopeKey]
38
38
  )
39
39
 
40
40
  const { data, isLoading, refetch } = useQuery({
@@ -67,7 +67,7 @@ export const useTokenBalance = (
67
67
  queryClient.setQueriesData<TokenAmount[]>(
68
68
  {
69
69
  queryKey: [
70
- getQueryKey('token-balances', keyPrefix),
70
+ getQueryKey('token-balances', storageScopeKey),
71
71
  accountAddress,
72
72
  tokenChainId,
73
73
  ],
@@ -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