@lifi/widget 4.7.1 → 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/dist/esm/components/AmountInput/AmountInput.style.d.ts.map +1 -1
  2. package/dist/esm/components/AmountInputCard/AmountInputCardPair.js +5 -2
  3. package/dist/esm/components/AmountInputCard/AmountInputCardPair.js.map +1 -1
  4. package/dist/esm/components/Card/Card.d.ts.map +1 -1
  5. package/dist/esm/components/Card/CardIconButton.d.ts.map +1 -1
  6. package/dist/esm/components/ContractComponent/ItemPrice.d.ts.map +1 -1
  7. package/dist/esm/components/ListItem/ListItemButton.d.ts.map +1 -1
  8. package/dist/esm/components/ListItemButton.d.ts.map +1 -1
  9. package/dist/esm/components/SelectChainAndToken.js +5 -2
  10. package/dist/esm/components/SelectChainAndToken.js.map +1 -1
  11. package/dist/esm/components/SendToWallet/SendToWalletButton.js +5 -4
  12. package/dist/esm/components/SendToWallet/SendToWalletButton.js.map +1 -1
  13. package/dist/esm/components/Token/Token.d.ts.map +1 -1
  14. package/dist/esm/components/TokenList/types.d.ts.map +1 -1
  15. package/dist/esm/components/TokenList/useTokenSelect.d.ts.map +1 -1
  16. package/dist/esm/components/TokenList/useTokenSelect.js +10 -0
  17. package/dist/esm/components/TokenList/useTokenSelect.js.map +1 -1
  18. package/dist/esm/config/constants.js.map +1 -1
  19. package/dist/esm/config/version.d.ts +1 -1
  20. package/dist/esm/config/version.js +1 -1
  21. package/dist/esm/config/version.js.map +1 -1
  22. package/dist/esm/hooks/useAccountsBalancesData.js +5 -2
  23. package/dist/esm/hooks/useAccountsBalancesData.js.map +1 -1
  24. package/dist/esm/hooks/useAddressValidation.d.ts +3 -2
  25. package/dist/esm/hooks/useAddressValidation.d.ts.map +1 -1
  26. package/dist/esm/hooks/useAddressValidation.js +48 -21
  27. package/dist/esm/hooks/useAddressValidation.js.map +1 -1
  28. package/dist/esm/hooks/useAvailableChains.d.ts.map +1 -1
  29. package/dist/esm/hooks/useAvailableChains.js +21 -38
  30. package/dist/esm/hooks/useAvailableChains.js.map +1 -1
  31. package/dist/esm/hooks/useFromTokenSufficiency.js +3 -3
  32. package/dist/esm/hooks/useFromTokenSufficiency.js.map +1 -1
  33. package/dist/esm/hooks/useGasRecommendation.js +3 -3
  34. package/dist/esm/hooks/useGasRecommendation.js.map +1 -1
  35. package/dist/esm/hooks/useGasRefuel.js +4 -1
  36. package/dist/esm/hooks/useGasRefuel.js.map +1 -1
  37. package/dist/esm/hooks/useGasSufficiency.js +3 -3
  38. package/dist/esm/hooks/useGasSufficiency.js.map +1 -1
  39. package/dist/esm/hooks/useIsBatchingSupported.js +2 -2
  40. package/dist/esm/hooks/useIsBatchingSupported.js.map +1 -1
  41. package/dist/esm/hooks/useRouteExecution.js +6 -6
  42. package/dist/esm/hooks/useRouteExecution.js.map +1 -1
  43. package/dist/esm/hooks/useRoutes.d.ts.map +1 -1
  44. package/dist/esm/hooks/useRoutes.js +18 -13
  45. package/dist/esm/hooks/useRoutes.js.map +1 -1
  46. package/dist/esm/hooks/useToAddressAutoPopulate.js +6 -8
  47. package/dist/esm/hooks/useToAddressAutoPopulate.js.map +1 -1
  48. package/dist/esm/hooks/useToAddressRequirements.d.ts +3 -2
  49. package/dist/esm/hooks/useToAddressRequirements.d.ts.map +1 -1
  50. package/dist/esm/hooks/useToAddressRequirements.js +8 -7
  51. package/dist/esm/hooks/useToAddressRequirements.js.map +1 -1
  52. package/dist/esm/hooks/useToAddressReset.js +6 -3
  53. package/dist/esm/hooks/useToAddressReset.js.map +1 -1
  54. package/dist/esm/hooks/useTokenBalance.js +6 -5
  55. package/dist/esm/hooks/useTokenBalance.js.map +1 -1
  56. package/dist/esm/hooks/useTokenBalancesQueries.js +3 -3
  57. package/dist/esm/hooks/useTokenBalancesQueries.js.map +1 -1
  58. package/dist/esm/hooks/useTokenSearch.js +3 -3
  59. package/dist/esm/hooks/useTokenSearch.js.map +1 -1
  60. package/dist/esm/hooks/useTokens.js +15 -15
  61. package/dist/esm/hooks/useTokens.js.map +1 -1
  62. package/dist/esm/hooks/useTools.d.ts.map +1 -1
  63. package/dist/esm/hooks/useTools.js +26 -27
  64. package/dist/esm/hooks/useTools.js.map +1 -1
  65. package/dist/esm/hooks/useTransactionDetails.js +2 -2
  66. package/dist/esm/hooks/useTransactionDetails.js.map +1 -1
  67. package/dist/esm/hooks/useTransactionHistory.js +3 -3
  68. package/dist/esm/hooks/useTransactionHistory.js.map +1 -1
  69. package/dist/esm/i18n/bn.json +2 -1
  70. package/dist/esm/i18n/de.json +2 -1
  71. package/dist/esm/i18n/en.json +2 -1
  72. package/dist/esm/i18n/es.json +2 -1
  73. package/dist/esm/i18n/fr.json +2 -1
  74. package/dist/esm/i18n/hi.json +2 -1
  75. package/dist/esm/i18n/id.json +2 -1
  76. package/dist/esm/i18n/it.json +2 -1
  77. package/dist/esm/i18n/ja.json +2 -1
  78. package/dist/esm/i18n/ko.json +2 -1
  79. package/dist/esm/i18n/pl.json +2 -1
  80. package/dist/esm/i18n/pt.json +2 -1
  81. package/dist/esm/i18n/th.json +2 -1
  82. package/dist/esm/i18n/tr.json +2 -1
  83. package/dist/esm/i18n/uk.json +2 -1
  84. package/dist/esm/i18n/vi.json +2 -1
  85. package/dist/esm/i18n/zh.json +2 -1
  86. package/dist/esm/index.d.ts +5 -1
  87. package/dist/esm/index.js +3 -1
  88. package/dist/esm/pages/RoutesPage/RoutesPage.style.d.ts.map +1 -1
  89. package/dist/esm/pages/SendToWallet/BookmarkAddressSheet.js +7 -4
  90. package/dist/esm/pages/SendToWallet/BookmarkAddressSheet.js.map +1 -1
  91. package/dist/esm/pages/SendToWallet/BookmarksPage.js +35 -31
  92. package/dist/esm/pages/SendToWallet/BookmarksPage.js.map +1 -1
  93. package/dist/esm/pages/SendToWallet/ConnectedWalletsPage.js +4 -3
  94. package/dist/esm/pages/SendToWallet/ConnectedWalletsPage.js.map +1 -1
  95. package/dist/esm/pages/SendToWallet/RecentWalletsPage.js +43 -39
  96. package/dist/esm/pages/SendToWallet/RecentWalletsPage.js.map +1 -1
  97. package/dist/esm/pages/SendToWallet/SendToConfiguredWalletPage.js +32 -26
  98. package/dist/esm/pages/SendToWallet/SendToConfiguredWalletPage.js.map +1 -1
  99. package/dist/esm/pages/SendToWallet/SendToWalletPage.js +5 -10
  100. package/dist/esm/pages/SendToWallet/SendToWalletPage.js.map +1 -1
  101. package/dist/esm/providers/I18nProvider/types.d.ts.map +1 -1
  102. package/dist/esm/providers/WalletProvider/WalletProvider.d.ts.map +1 -1
  103. package/dist/esm/providers/WalletProvider/WalletProvider.js +14 -2
  104. package/dist/esm/providers/WalletProvider/WalletProvider.js.map +1 -1
  105. package/dist/esm/providers/WidgetProvider/WidgetProvider.d.ts.map +1 -1
  106. package/dist/esm/providers/WidgetProvider/WidgetProvider.js +15 -11
  107. package/dist/esm/providers/WidgetProvider/WidgetProvider.js.map +1 -1
  108. package/dist/esm/providers/WidgetProvider/types.d.ts +2 -0
  109. package/dist/esm/providers/WidgetProvider/types.d.ts.map +1 -1
  110. package/dist/esm/queries/getChains.d.ts +38 -0
  111. package/dist/esm/queries/getChains.d.ts.map +1 -0
  112. package/dist/esm/queries/getChains.js +51 -0
  113. package/dist/esm/queries/getChains.js.map +1 -0
  114. package/dist/esm/queries/getTools.d.ts +26 -0
  115. package/dist/esm/queries/getTools.d.ts.map +1 -0
  116. package/dist/esm/queries/getTools.js +30 -0
  117. package/dist/esm/queries/getTools.js.map +1 -0
  118. package/dist/esm/queries/index.d.ts +4 -0
  119. package/dist/esm/queries/index.js +3 -0
  120. package/dist/esm/queries/types.d.ts +25 -0
  121. package/dist/esm/queries/types.d.ts.map +1 -0
  122. package/dist/esm/shared.d.ts +2 -1
  123. package/dist/esm/shared.js +2 -1
  124. package/dist/esm/stores/StoreProvider.d.ts.map +1 -1
  125. package/dist/esm/stores/StoreProvider.js +16 -17
  126. package/dist/esm/stores/StoreProvider.js.map +1 -1
  127. package/dist/esm/stores/bookmarks/types.d.ts +3 -0
  128. package/dist/esm/stores/bookmarks/types.d.ts.map +1 -1
  129. package/dist/esm/stores/chains/ChainOrderStore.d.ts.map +1 -1
  130. package/dist/esm/stores/chains/ChainOrderStore.js +4 -1
  131. package/dist/esm/stores/chains/ChainOrderStore.js.map +1 -1
  132. package/dist/esm/stores/form/FormStore.js +3 -2
  133. package/dist/esm/stores/form/FormStore.js.map +1 -1
  134. package/dist/esm/stores/form/URLSearchParamsBuilder.js +7 -3
  135. package/dist/esm/stores/form/URLSearchParamsBuilder.js.map +1 -1
  136. package/dist/esm/stores/form/getDefaultValuesFromQueryString.js +4 -1
  137. package/dist/esm/stores/form/getDefaultValuesFromQueryString.js.map +1 -1
  138. package/dist/esm/stores/header/useHeaderStore.d.ts +1 -1
  139. package/dist/esm/stores/header/useHeaderStore.d.ts.map +1 -1
  140. package/dist/esm/stores/header/useHeaderStore.js.map +1 -1
  141. package/dist/esm/themes/createTheme.d.ts.map +1 -1
  142. package/dist/esm/types/events.d.ts.map +1 -1
  143. package/dist/esm/types/token.d.ts.map +1 -1
  144. package/dist/esm/types/widget.d.ts +30 -0
  145. package/dist/esm/types/widget.d.ts.map +1 -1
  146. package/dist/esm/types/widget.js.map +1 -1
  147. package/dist/esm/utils/chainType.js +32 -1
  148. package/dist/esm/utils/chainType.js.map +1 -1
  149. package/dist/esm/utils/customReceiver.js +6 -1
  150. package/dist/esm/utils/customReceiver.js.map +1 -1
  151. package/dist/esm/utils/queries.js.map +1 -1
  152. package/dist/esm/utils/scopeKeys.d.ts +6 -0
  153. package/dist/esm/utils/scopeKeys.d.ts.map +1 -0
  154. package/dist/esm/utils/scopeKeys.js +8 -0
  155. package/dist/esm/utils/scopeKeys.js.map +1 -0
  156. package/package.json +15 -7
  157. package/src/components/AmountInputCard/AmountInputCardPair.tsx +6 -2
  158. package/src/components/SelectChainAndToken.tsx +5 -1
  159. package/src/components/SendToWallet/SendToWalletButton.tsx +13 -8
  160. package/src/components/TokenList/useTokenSelect.ts +15 -0
  161. package/src/config/constants.ts +3 -0
  162. package/src/config/version.ts +1 -1
  163. package/src/hooks/useAccountsBalancesData.ts +6 -2
  164. package/src/hooks/useAddressValidation.ts +87 -32
  165. package/src/hooks/useAvailableChains.test.tsx +170 -0
  166. package/src/hooks/useAvailableChains.ts +29 -47
  167. package/src/hooks/useFromTokenSufficiency.ts +2 -2
  168. package/src/hooks/useGasRecommendation.ts +2 -2
  169. package/src/hooks/useGasRefuel.ts +11 -1
  170. package/src/hooks/useGasSufficiency.ts +2 -2
  171. package/src/hooks/useIsBatchingSupported.ts +2 -2
  172. package/src/hooks/useRouteExecution.ts +5 -5
  173. package/src/hooks/useRoutes.ts +28 -23
  174. package/src/hooks/useToAddressAutoPopulate.ts +13 -17
  175. package/src/hooks/useToAddressRequirements.ts +19 -4
  176. package/src/hooks/useToAddressReset.ts +13 -4
  177. package/src/hooks/useTokenBalance.ts +10 -5
  178. package/src/hooks/useTokenBalancesQueries.ts +3 -3
  179. package/src/hooks/useTokenSearch.ts +7 -3
  180. package/src/hooks/useTokens.ts +13 -14
  181. package/src/hooks/useTools.sharedEntry.test.tsx +155 -0
  182. package/src/hooks/useTools.ts +42 -37
  183. package/src/hooks/useTransactionDetails.ts +3 -3
  184. package/src/hooks/useTransactionHistory.ts +2 -2
  185. package/src/index.ts +2 -1
  186. package/src/pages/SendToWallet/BookmarkAddressSheet.tsx +5 -1
  187. package/src/pages/SendToWallet/BookmarksPage.tsx +42 -52
  188. package/src/pages/SendToWallet/ConnectedWalletsPage.tsx +4 -14
  189. package/src/pages/SendToWallet/RecentWalletsPage.tsx +59 -66
  190. package/src/pages/SendToWallet/SendToConfiguredWalletPage.tsx +45 -50
  191. package/src/pages/SendToWallet/SendToWalletPage.tsx +4 -16
  192. package/src/providers/WalletProvider/WalletProvider.tsx +28 -3
  193. package/src/providers/WidgetProvider/WidgetProvider.test.tsx +73 -0
  194. package/src/providers/WidgetProvider/WidgetProvider.tsx +28 -14
  195. package/src/providers/WidgetProvider/types.ts +2 -0
  196. package/src/queries/getChains.ts +93 -0
  197. package/src/queries/getTools.ts +62 -0
  198. package/src/queries/index.ts +30 -0
  199. package/src/queries/sharedCache.test.tsx +223 -0
  200. package/src/queries/types.ts +38 -0
  201. package/src/shared.ts +1 -0
  202. package/src/stores/StoreProvider.test.tsx +107 -0
  203. package/src/stores/StoreProvider.tsx +8 -6
  204. package/src/stores/bookmarks/types.ts +3 -0
  205. package/src/stores/chains/ChainOrderStore.tsx +4 -1
  206. package/src/stores/form/FormStore.tsx +2 -0
  207. package/src/stores/form/URLSearchParamsBuilder.tsx +4 -2
  208. package/src/stores/form/getDefaultValuesFromQueryString.ts +19 -0
  209. package/src/stores/header/useHeaderStore.tsx +3 -4
  210. package/src/types/widget.ts +30 -0
  211. package/src/utils/chainType.ts +73 -1
  212. package/src/utils/customReceiver.ts +36 -0
  213. package/src/utils/queries.ts +1 -0
  214. package/src/utils/scopeKeys.ts +18 -0
@@ -12,13 +12,13 @@ import type { BottomSheetBase } from '../../components/BottomSheet/types.js'
12
12
  import { ButtonTertiary } from '../../components/ButtonTertiary.js'
13
13
  import { CardButton } from '../../components/Card/CardButton.js'
14
14
  import { AccountDeployedMessage } from '../../components/Messages/AccountDeployedMessage.js'
15
+ import { addressInputMaxLength } from '../../config/constants.js'
15
16
  import {
16
17
  AddressType,
17
18
  useAddressValidation,
18
19
  } from '../../hooks/useAddressValidation.js'
19
20
  import { useChain } from '../../hooks/useChain.js'
20
21
  import { useHeader } from '../../hooks/useHeader.js'
21
- import { useToAddressRequirements } from '../../hooks/useToAddressRequirements.js'
22
22
  import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js'
23
23
  import type { Bookmark } from '../../stores/bookmarks/types.js'
24
24
  import { useBookmarkActions } from '../../stores/bookmarks/useBookmarkActions.js'
@@ -57,7 +57,6 @@ export const SendToWalletPage: React.FC = () => {
57
57
  const [validatedWallet, setValidatedWallet] = useState<Bookmark>()
58
58
  const [errorMessage, setErrorMessage] = useState('')
59
59
  const { validateAddress, isValidating } = useAddressValidation()
60
- const { requiredToChainType } = useToAddressRequirements()
61
60
  const [toChainId] = useFieldValues('toChain')
62
61
  const { chain: toChain } = useChain(toChainId)
63
62
  const [isDoneButtonLoading, setIsDoneButtonLoading] = useState(false)
@@ -91,7 +90,6 @@ export const SendToWalletPage: React.FC = () => {
91
90
  setIsDoneButtonLoading(true)
92
91
  const validationResult = await validateAddress({
93
92
  value: inputAddressValue,
94
- chainType: requiredToChainType,
95
93
  chain: toChain,
96
94
  })
97
95
  setIsDoneButtonLoading(false)
@@ -100,18 +98,6 @@ export const SendToWalletPage: React.FC = () => {
100
98
  return
101
99
  }
102
100
 
103
- if (
104
- requiredToChainType &&
105
- requiredToChainType !== validationResult.chainType
106
- ) {
107
- setErrorMessage(
108
- t('error.title.walletChainTypeInvalid', {
109
- chainName: toChain?.name,
110
- })
111
- )
112
- return
113
- }
114
-
115
101
  setValidatedWallet({
116
102
  name:
117
103
  validationResult.addressType === AddressType.NameService
@@ -119,6 +105,7 @@ export const SendToWalletPage: React.FC = () => {
119
105
  : undefined,
120
106
  address: validationResult.address,
121
107
  chainType: validationResult.chainType,
108
+ chainId: validationResult.chainId,
122
109
  })
123
110
  confirmAddressSheetRef.current?.open()
124
111
  }
@@ -155,6 +142,7 @@ export const SendToWalletPage: React.FC = () => {
155
142
  : undefined,
156
143
  address: validationResult.address,
157
144
  chainType: validationResult.chainType,
145
+ chainId: validationResult.chainId,
158
146
  })
159
147
  bookmarkAddressSheetRef.current?.open()
160
148
  } else {
@@ -213,7 +201,7 @@ export const SendToWalletPage: React.FC = () => {
213
201
  placeholder={placeholder}
214
202
  aria-label={placeholder}
215
203
  maxRows={2}
216
- inputProps={{ maxLength: 128 }}
204
+ inputProps={{ maxLength: addressInputMaxLength }}
217
205
  multiline
218
206
  />
219
207
  {errorMessage ? (
@@ -1,5 +1,8 @@
1
1
  import { WalletManagementProviders } from '@lifi/wallet-management'
2
- import type { WidgetProviderProps } from '@lifi/widget-provider'
2
+ import {
3
+ useSDKProviders,
4
+ type WidgetProviderProps,
5
+ } from '@lifi/widget-provider'
3
6
  import {
4
7
  type FC,
5
8
  type JSX,
@@ -10,8 +13,15 @@ import {
10
13
  import { useTranslation } from 'react-i18next'
11
14
  import { useAvailableChains } from '../../hooks/useAvailableChains.js'
12
15
  import { useInitializeSDKProviders } from '../../hooks/useInitializeSDKProviders.js'
16
+ import {
17
+ withoutUnservedToChain,
18
+ withServedDestinationChains,
19
+ } from '../../utils/chainType.js'
13
20
  import { getConfigItemSets, isItemAllowedForSets } from '../../utils/item.js'
14
- import { useWidgetConfig } from '../WidgetProvider/WidgetProvider.js'
21
+ import {
22
+ useWidgetConfig,
23
+ WidgetContext,
24
+ } from '../WidgetProvider/WidgetProvider.js'
15
25
  import { useExternalWalletProvider } from './useExternalWalletProvider.js'
16
26
 
17
27
  interface WalletProviderProps extends PropsWithChildren {
@@ -78,5 +88,20 @@ export const WalletProvider = ({
78
88
 
79
89
  const SDKProviderInitializer: FC<PropsWithChildren> = ({ children }) => {
80
90
  useInitializeSDKProviders()
81
- return children
91
+ const config = useWidgetConfig()
92
+ const sdkProviders = useSDKProviders()
93
+ // Only below the provider components is it known which chains are served.
94
+ // Keyed on `config.chains`: the config object is new on every render.
95
+ const chains = useMemo(
96
+ () => withServedDestinationChains(config.chains, sdkProviders),
97
+ [config.chains, sdkProviders]
98
+ )
99
+ const value = useMemo(
100
+ () =>
101
+ withoutUnservedToChain(
102
+ chains === config.chains ? config : { ...config, chains }
103
+ ),
104
+ [config, chains]
105
+ )
106
+ return <WidgetContext value={value}>{children}</WidgetContext>
82
107
  }
@@ -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,21 @@
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
+ isDestinationOnlyChain,
6
+ withDestinationOnlyChains,
7
+ } from '../../utils/chainType.js'
8
+ import {
9
+ defaultQueryScopeKey,
10
+ resolveQueryScopeKey,
11
+ resolveStorageScopeKey,
12
+ } from '../../utils/scopeKeys.js'
4
13
  import type { WidgetContextProps, WidgetProviderProps } from './types.js'
5
14
 
6
15
  const initialContext: WidgetContextProps = {
7
16
  elementId: '',
8
17
  integrator: '',
18
+ queryScopeKey: defaultQueryScopeKey,
9
19
  }
10
20
 
11
21
  export const WidgetContext: Context<WidgetContextProps> =
@@ -23,26 +33,30 @@ export const WidgetProvider: React.FC<
23
33
  throw new Error('Required property "integrator" is missing.')
24
34
  }
25
35
 
36
+ // Keyed on the integrator's `chains`: the config object is new on every render.
37
+ const chains = useMemo(
38
+ () => withDestinationOnlyChains(widgetConfig.chains),
39
+ [widgetConfig.chains]
40
+ )
41
+
26
42
  const value = useMemo((): WidgetContextProps => {
43
+ // Omitted, not undefined: an own undefined key resets the form field.
44
+ const { fromChain, fromToken, ...config } = widgetConfig
45
+ const value = {
46
+ ...(isDestinationOnlyChain(fromChain) ? config : widgetConfig),
47
+ chains,
48
+ // Resolved once, so nothing below reads the deprecated keyPrefix.
49
+ storageScopeKey: resolveStorageScopeKey(widgetConfig),
50
+ queryScopeKey: resolveQueryScopeKey(widgetConfig),
51
+ elementId,
52
+ } as WidgetContextProps
27
53
  try {
28
- // Create widget configuration object
29
- const value = {
30
- ...widgetConfig,
31
- elementId,
32
- } as WidgetContextProps
33
-
34
54
  // Set default settings for widget settings store
35
55
  setDefaultSettings(value)
36
-
37
- return value
38
56
  } catch (e) {
39
57
  console.warn(e)
40
- return {
41
- ...widgetConfig,
42
- elementId,
43
- integrator: widgetConfig.integrator,
44
- }
45
58
  }
46
- }, [elementId, widgetConfig, setDefaultSettings])
59
+ return value
60
+ }, [elementId, widgetConfig, chains, setDefaultSettings])
47
61
  return <WidgetContext value={value}>{children}</WidgetContext>
48
62
  }
@@ -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
+ }
@@ -0,0 +1,62 @@
1
+ import type { SDKClient, ToolsResponse } from '@lifi/sdk'
2
+ import { getTools } from '@lifi/sdk'
3
+ import type {
4
+ FactoryQueryOptions,
5
+ QueryParameter,
6
+ ScopeKeyParameter,
7
+ } from './types.js'
8
+
9
+ export type GetToolsQueryFnData = ToolsResponse
10
+
11
+ export type GetToolsQueryKeyParameters = {
12
+ /** API base the client talks to. Different bases return different tools. */
13
+ apiUrl: string
14
+ } & ScopeKeyParameter
15
+
16
+ export type GetToolsQueryKey = readonly [
17
+ 'tools',
18
+ { apiUrl: string; scopeKey?: string },
19
+ ]
20
+
21
+ /** The key alone — to read, set or invalidate the entry. */
22
+ export function getToolsQueryKey(
23
+ parameters: GetToolsQueryKeyParameters
24
+ ): GetToolsQueryKey {
25
+ return [
26
+ 'tools',
27
+ {
28
+ apiUrl: parameters.apiUrl,
29
+ ...(parameters.scopeKey === undefined
30
+ ? {}
31
+ : { scopeKey: parameters.scopeKey }),
32
+ },
33
+ ]
34
+ }
35
+
36
+ export type GetToolsOptions<selectData = GetToolsQueryFnData> =
37
+ ScopeKeyParameter &
38
+ QueryParameter<GetToolsQueryFnData, selectData, GetToolsQueryKey>
39
+
40
+ export type GetToolsQueryOptions<selectData = GetToolsQueryFnData> =
41
+ FactoryQueryOptions<GetToolsQueryFnData, selectData, GetToolsQueryKey>
42
+
43
+ /**
44
+ * Query options for the bridges and exchanges of one API base.
45
+ *
46
+ * The key is `['tools', { apiUrl, scopeKey }]`. The cache holds the raw,
47
+ * unfiltered response so every caller can share it; narrow it per caller with
48
+ * `query.select`, never in the `queryFn`.
49
+ */
50
+ export function getToolsQueryOptions<selectData = GetToolsQueryFnData>(
51
+ client: SDKClient,
52
+ options: GetToolsOptions<selectData> = {}
53
+ ): GetToolsQueryOptions<selectData> {
54
+ return {
55
+ ...options.query,
56
+ queryKey: getToolsQueryKey({
57
+ apiUrl: client.config.apiUrl,
58
+ scopeKey: options.scopeKey,
59
+ }),
60
+ queryFn: ({ signal }) => getTools(client, undefined, { signal }),
61
+ }
62
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Query option factories for LI.FI server data, shared by the widget and its
3
+ * host. Published as `@lifi/widget/queries`; it depends only on
4
+ * `@lifi/sdk` and on `@tanstack/react-query` types, so importing it does not
5
+ * pull in the widget itself.
6
+ *
7
+ * Keys follow wagmi's `@wagmi/core/query` convention: `[name, parameters]`,
8
+ * where the parameters hold everything that changes the response plus an
9
+ * optional `scopeKey`.
10
+ */
11
+ export {
12
+ type GetChainsOptions,
13
+ type GetChainsQueryFnData,
14
+ type GetChainsQueryKey,
15
+ type GetChainsQueryKeyParameters,
16
+ type GetChainsQueryOptions,
17
+ getChainsQueryKey,
18
+ getChainsQueryOptions,
19
+ supportedChainTypes,
20
+ } from './getChains.js'
21
+ export {
22
+ type GetToolsOptions,
23
+ type GetToolsQueryFnData,
24
+ type GetToolsQueryKey,
25
+ type GetToolsQueryKeyParameters,
26
+ type GetToolsQueryOptions,
27
+ getToolsQueryKey,
28
+ getToolsQueryOptions,
29
+ } from './getTools.js'
30
+ export type { QueryParameter, ScopeKeyParameter } from './types.js'
@@ -0,0 +1,223 @@
1
+ /** @vitest-environment happy-dom */
2
+
3
+ import type * as LifiSdk from '@lifi/sdk'
4
+ import type { SDKClient, ToolsResponse } from '@lifi/sdk'
5
+ import { ChainType } from '@lifi/sdk'
6
+ import {
7
+ QueryClient,
8
+ QueryClientProvider,
9
+ useQuery,
10
+ } from '@tanstack/react-query'
11
+ import type { ReactNode } from 'react'
12
+ import { act } from 'react'
13
+ import { createRoot, type Root } from 'react-dom/client'
14
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
15
+
16
+ /**
17
+ * A host app and the widget that both build
18
+ * their options from these factories, inside one QueryClient, send each
19
+ * request once — whether they mount together or seconds apart.
20
+ */
21
+
22
+ const mocks = vi.hoisted(() => ({
23
+ getChains: vi.fn(),
24
+ getTools: vi.fn(),
25
+ }))
26
+
27
+ vi.mock('@lifi/sdk', async (importOriginal) => ({
28
+ ...(await importOriginal<typeof LifiSdk>()),
29
+ getChains: mocks.getChains,
30
+ getTools: mocks.getTools,
31
+ }))
32
+
33
+ import { getChainsQueryOptions } from './getChains.js'
34
+ import { getToolsQueryOptions } from './getTools.js'
35
+
36
+ const V1 = 'https://api.example/v1'
37
+ const OTHER_BASE = 'https://api-b.example/v1'
38
+ const clientFor = (apiUrl: string) =>
39
+ ({ config: { apiUrl } }) as unknown as SDKClient
40
+
41
+ const TOOLS = {
42
+ bridges: [{ key: 'across' }, { key: 'stargate' }],
43
+ exchanges: [{ key: '1inch' }],
44
+ } as unknown as ToolsResponse
45
+
46
+ /** A host app: all chain types, its own refresh cadence. */
47
+ const Host = ({
48
+ client,
49
+ scopeKey,
50
+ }: {
51
+ client: SDKClient
52
+ scopeKey: string
53
+ }) => {
54
+ useQuery(
55
+ getChainsQueryOptions(client, {
56
+ scopeKey,
57
+ query: { staleTime: 300_000 },
58
+ })
59
+ )
60
+ useQuery(
61
+ getToolsQueryOptions(client, { scopeKey, query: { staleTime: 300_000 } })
62
+ )
63
+ return null
64
+ }
65
+
66
+ /** What the widget does: chain types in another order, a filtered tools view. */
67
+ const Widget = ({
68
+ client,
69
+ scopeKey,
70
+ }: {
71
+ client: SDKClient
72
+ scopeKey: string
73
+ }) => {
74
+ useQuery(
75
+ getChainsQueryOptions(client, {
76
+ chainTypes: [
77
+ ChainType.STL,
78
+ ChainType.TVM,
79
+ ChainType.MVM,
80
+ ChainType.UTXO,
81
+ ChainType.SVM,
82
+ ChainType.EVM,
83
+ ],
84
+ scopeKey,
85
+ query: { staleTime: 300_000 },
86
+ })
87
+ )
88
+ useQuery(
89
+ getToolsQueryOptions(client, {
90
+ scopeKey,
91
+ query: {
92
+ staleTime: 180_000,
93
+ select: (tools) => ({ ...tools, bridges: tools.bridges.slice(0, 1) }),
94
+ },
95
+ })
96
+ )
97
+ return null
98
+ }
99
+
100
+ let root: Root
101
+ let queryClient: QueryClient
102
+
103
+ const render = async (node: ReactNode) => {
104
+ await act(async () => {
105
+ root.render(
106
+ <QueryClientProvider client={queryClient}>{node}</QueryClientProvider>
107
+ )
108
+ })
109
+ }
110
+
111
+ const settle = () =>
112
+ vi.waitFor(() => {
113
+ expect(queryClient.isFetching()).toBe(0)
114
+ })
115
+
116
+ describe('shared chains and tools cache', () => {
117
+ beforeEach(() => {
118
+ vi.clearAllMocks()
119
+ mocks.getChains.mockResolvedValue([{ id: 1 }])
120
+ mocks.getTools.mockResolvedValue(TOOLS)
121
+ queryClient = new QueryClient({
122
+ defaultOptions: { queries: { retry: false } },
123
+ })
124
+ const container = document.createElement('div')
125
+ document.body.appendChild(container)
126
+ root = createRoot(container)
127
+ })
128
+
129
+ afterEach(async () => {
130
+ await act(async () => root.unmount())
131
+ queryClient.clear()
132
+ })
133
+
134
+ it('fetches once when the host and the widget mount together', async () => {
135
+ const client = clientFor(V1)
136
+
137
+ await render(
138
+ <>
139
+ <Host client={client} scopeKey="host" />
140
+ <Widget client={client} scopeKey="host" />
141
+ </>
142
+ )
143
+ await settle()
144
+
145
+ expect(mocks.getChains).toHaveBeenCalledTimes(1)
146
+ expect(mocks.getTools).toHaveBeenCalledTimes(1)
147
+ })
148
+
149
+ // A widget often mounts seconds after the host app. A later observer with a
150
+ // staleTime reads the entry instead of refetching it.
151
+ it('fetches once when the widget mounts after the host has settled', async () => {
152
+ const client = clientFor(V1)
153
+
154
+ await render(<Host client={client} scopeKey="host" />)
155
+ await settle()
156
+ await render(
157
+ <>
158
+ <Host client={client} scopeKey="host" />
159
+ <Widget client={client} scopeKey="host" />
160
+ </>
161
+ )
162
+ await settle()
163
+
164
+ expect(mocks.getChains).toHaveBeenCalledTimes(1)
165
+ expect(mocks.getTools).toHaveBeenCalledTimes(1)
166
+ })
167
+
168
+ it('also shares when the host and widget hold different client objects on the same base', async () => {
169
+ await render(
170
+ <>
171
+ <Host client={clientFor(V1)} scopeKey="host" />
172
+ <Widget client={clientFor(V1)} scopeKey="host" />
173
+ </>
174
+ )
175
+ await settle()
176
+
177
+ expect(mocks.getChains).toHaveBeenCalledTimes(1)
178
+ expect(mocks.getTools).toHaveBeenCalledTimes(1)
179
+ })
180
+
181
+ it('keeps the raw tools response in the cache under a filtering select', async () => {
182
+ const client = clientFor(V1)
183
+
184
+ await render(<Widget client={client} scopeKey="host" />)
185
+ await settle()
186
+
187
+ const cached = queryClient.getQueryData<ToolsResponse>(
188
+ getToolsQueryOptions(client, { scopeKey: 'host' }).queryKey
189
+ )
190
+ expect(cached?.bridges).toHaveLength(2)
191
+ })
192
+
193
+ // A partner's own queries and another integration's widget stay apart.
194
+ it('does not share across scopes', async () => {
195
+ const client = clientFor(V1)
196
+
197
+ await render(
198
+ <>
199
+ <Host client={client} scopeKey="host" />
200
+ <Widget client={client} scopeKey="partner" />
201
+ </>
202
+ )
203
+ await settle()
204
+
205
+ expect(mocks.getChains).toHaveBeenCalledTimes(2)
206
+ expect(mocks.getTools).toHaveBeenCalledTimes(2)
207
+ })
208
+
209
+ // Different API bases can return different chains and tools, so they must
210
+ // never share an entry.
211
+ it('does not share across API bases', async () => {
212
+ await render(
213
+ <>
214
+ <Host client={clientFor(V1)} scopeKey="host" />
215
+ <Widget client={clientFor(OTHER_BASE)} scopeKey="host" />
216
+ </>
217
+ )
218
+ await settle()
219
+
220
+ expect(mocks.getChains).toHaveBeenCalledTimes(2)
221
+ expect(mocks.getTools).toHaveBeenCalledTimes(2)
222
+ })
223
+ })