@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.
- 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/useExplorer.d.ts +1 -1
- package/dist/esm/hooks/useExplorer.d.ts.map +1 -1
- package/dist/esm/hooks/useExplorer.js +7 -3
- package/dist/esm/hooks/useExplorer.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/pages/SendToWallet/BookmarksPage.js +2 -1
- package/dist/esm/pages/SendToWallet/BookmarksPage.js.map +1 -1
- package/dist/esm/pages/SendToWallet/ConnectedWalletsPage.js +2 -1
- package/dist/esm/pages/SendToWallet/ConnectedWalletsPage.js.map +1 -1
- package/dist/esm/pages/SendToWallet/RecentWalletsPage.js +2 -1
- package/dist/esm/pages/SendToWallet/RecentWalletsPage.js.map +1 -1
- package/dist/esm/pages/SendToWallet/SendToConfiguredWalletPage.js +2 -1
- package/dist/esm/pages/SendToWallet/SendToConfiguredWalletPage.js.map +1 -1
- package/dist/esm/pages/TransactionDetailsPage/TransactionDetailsPage.js +2 -1
- package/dist/esm/pages/TransactionDetailsPage/TransactionDetailsPage.js.map +1 -1
- package/dist/esm/pages/TransactionDetailsPage/TransferIdCard.js +2 -1
- package/dist/esm/pages/TransactionDetailsPage/TransferIdCard.js.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 +3 -1
- package/dist/esm/shared.js +3 -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/dist/esm/utils/url.d.ts +5 -0
- package/dist/esm/utils/url.d.ts.map +1 -0
- package/dist/esm/utils/url.js +17 -0
- package/dist/esm/utils/url.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/useExplorer.test.tsx +106 -0
- package/src/hooks/useExplorer.ts +11 -5
- 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/pages/SendToWallet/BookmarksPage.tsx +3 -3
- package/src/pages/SendToWallet/ConnectedWalletsPage.tsx +3 -3
- package/src/pages/SendToWallet/RecentWalletsPage.tsx +3 -3
- package/src/pages/SendToWallet/SendToConfiguredWalletPage.tsx +3 -3
- package/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx +5 -5
- package/src/pages/TransactionDetailsPage/TransferIdCard.tsx +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 +2 -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
- 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 {
|
|
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 {
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
//
|
|
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 &&
|
|
86
|
-
|
|
67
|
+
if (data && dataUpdatedAt) {
|
|
68
|
+
client.setChains(data)
|
|
87
69
|
}
|
|
88
|
-
}, [data,
|
|
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
|
+
})
|
package/src/hooks/useExplorer.ts
CHANGED
|
@@ -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: (
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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 {
|
|
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',
|
|
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 {
|
|
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',
|
|
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 {
|
|
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',
|
|
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 {
|
|
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',
|
|
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 {
|
|
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',
|
|
87
|
+
getQueryKey('token-balances', storageScopeKey),
|
|
88
88
|
clonedUpdatedRoute.fromAddress,
|
|
89
89
|
clonedUpdatedRoute.fromChainId,
|
|
90
90
|
],
|
|
91
91
|
[
|
|
92
|
-
getQueryKey('token-balances',
|
|
92
|
+
getQueryKey('token-balances', storageScopeKey),
|
|
93
93
|
clonedUpdatedRoute.toAddress,
|
|
94
94
|
clonedUpdatedRoute.toChainId,
|
|
95
95
|
],
|
|
96
|
-
[getQueryKey('transaction-history',
|
|
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',
|
|
136
|
+
queryKey: [getQueryKey('routes', storageScopeKey)],
|
|
137
137
|
exact: false,
|
|
138
138
|
})
|
|
139
139
|
return executeRoute(sdkClient, routeExecution.route, {
|
package/src/hooks/useRoutes.ts
CHANGED
|
@@ -82,7 +82,7 @@ export const useRoutes = ({
|
|
|
82
82
|
exchanges,
|
|
83
83
|
feeConfig,
|
|
84
84
|
useRelayerRoutes,
|
|
85
|
-
|
|
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',
|
|
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
|
-
|
|
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',
|
|
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',
|
|
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',
|
|
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 {
|
|
26
|
+
const { storageScopeKey } = useWidgetConfig()
|
|
27
27
|
const sdkClient = useSDKClient()
|
|
28
28
|
|
|
29
29
|
const tokenBalanceQueryKey = useMemo(
|
|
30
30
|
() =>
|
|
31
31
|
[
|
|
32
|
-
getQueryKey('token-balance',
|
|
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,
|
|
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',
|
|
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 {
|
|
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',
|
|
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,
|
|
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,
|
|
21
|
+
const { tokens: configTokens, storageScopeKey } = useWidgetConfig()
|
|
22
22
|
const sdkClient = useSDKClient()
|
|
23
23
|
|
|
24
24
|
const { data, isLoading } = useQuery({
|
|
25
|
-
queryKey: [
|
|
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',
|
|
59
|
+
{ queryKey: [getQueryKey('tokens', storageScopeKey)] },
|
|
56
60
|
(data) => {
|
|
57
61
|
if (!data) {
|
|
58
62
|
return data
|