@lifi/widget 4.5.0 → 4.6.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 (199) hide show
  1. package/dist/esm/AppDrawer.js +2 -1
  2. package/dist/esm/AppDrawer.js.map +1 -1
  3. package/dist/esm/components/AmountInput/AmountInput.js +13 -7
  4. package/dist/esm/components/AmountInput/AmountInput.js.map +1 -1
  5. package/dist/esm/components/AmountInput/AmountInputEndAdornment.js +2 -13
  6. package/dist/esm/components/AmountInput/AmountInputEndAdornment.js.map +1 -1
  7. package/dist/esm/components/AmountInputCard/PercentageChips.js +2 -13
  8. package/dist/esm/components/AmountInputCard/PercentageChips.js.map +1 -1
  9. package/dist/esm/components/AmountInputCard/PercentageChips.style.js +1 -1
  10. package/dist/esm/components/AmountInputCard/PercentageChips.style.js.map +1 -1
  11. package/dist/esm/components/AmountInputCard/SendAmountCard.js +13 -6
  12. package/dist/esm/components/AmountInputCard/SendAmountCard.js.map +1 -1
  13. package/dist/esm/components/Chains/AllChainsAvatar.js +7 -1
  14. package/dist/esm/components/Chains/AllChainsAvatar.js.map +1 -1
  15. package/dist/esm/components/Dialog/Dialog.js +2 -1
  16. package/dist/esm/components/Dialog/Dialog.js.map +1 -1
  17. package/dist/esm/components/Expansion/Expansion.js +4 -3
  18. package/dist/esm/components/Expansion/Expansion.js.map +1 -1
  19. package/dist/esm/components/Header/SettingsButton.js +2 -1
  20. package/dist/esm/components/Header/SettingsButton.js.map +1 -1
  21. package/dist/esm/components/IconCircle/IconCircle.js +2 -1
  22. package/dist/esm/components/IconCircle/IconCircle.js.map +1 -1
  23. package/dist/esm/components/Messages/UnsupportedToAddressMessage.js +27 -0
  24. package/dist/esm/components/Messages/UnsupportedToAddressMessage.js.map +1 -0
  25. package/dist/esm/components/Messages/WarningMessages.d.ts.map +1 -1
  26. package/dist/esm/components/Messages/WarningMessages.js +2 -0
  27. package/dist/esm/components/Messages/WarningMessages.js.map +1 -1
  28. package/dist/esm/components/Messages/useMessageQueue.js +6 -1
  29. package/dist/esm/components/Messages/useMessageQueue.js.map +1 -1
  30. package/dist/esm/components/RouteCard/RouteDetails.js +2 -1
  31. package/dist/esm/components/RouteCard/RouteDetails.js.map +1 -1
  32. package/dist/esm/components/Routes/LimitOrderRoutes.js +2 -2
  33. package/dist/esm/components/Routes/LimitOrderRoutes.js.map +1 -1
  34. package/dist/esm/components/Routes/RoutesContent.js +2 -2
  35. package/dist/esm/components/Routes/RoutesContent.js.map +1 -1
  36. package/dist/esm/components/SendToWallet/SendToWalletExpandButton.js +2 -2
  37. package/dist/esm/components/SendToWallet/SendToWalletExpandButton.js.map +1 -1
  38. package/dist/esm/components/Skeleton/WidgetSkeleton.js +2 -1
  39. package/dist/esm/components/Skeleton/WidgetSkeleton.js.map +1 -1
  40. package/dist/esm/components/Step/StepActions.js +18 -16
  41. package/dist/esm/components/Step/StepActions.js.map +1 -1
  42. package/dist/esm/components/StepActions/StepActionRow.js +2 -1
  43. package/dist/esm/components/StepActions/StepActionRow.js.map +1 -1
  44. package/dist/esm/components/Switch.js +1 -1
  45. package/dist/esm/components/Switch.js.map +1 -1
  46. package/dist/esm/components/Tabs/Tabs.style.js +4 -2
  47. package/dist/esm/components/Tabs/Tabs.style.js.map +1 -1
  48. package/dist/esm/components/TokenList/TokenListItem.js +46 -8
  49. package/dist/esm/components/TokenList/TokenListItem.js.map +1 -1
  50. package/dist/esm/components/TokenList/VirtualizedTokenList.js +22 -9
  51. package/dist/esm/components/TokenList/VirtualizedTokenList.js.map +1 -1
  52. package/dist/esm/components/TokenRate/TokenRate.js +2 -1
  53. package/dist/esm/components/TokenRate/TokenRate.js.map +1 -1
  54. package/dist/esm/components/TransactionCard/ActiveTransactionCard.js +22 -21
  55. package/dist/esm/components/TransactionCard/ActiveTransactionCard.js.map +1 -1
  56. package/dist/esm/config/version.d.ts +1 -1
  57. package/dist/esm/config/version.js +1 -1
  58. package/dist/esm/config/version.js.map +1 -1
  59. package/dist/esm/hooks/timer/time.js +2 -2
  60. package/dist/esm/hooks/timer/time.js.map +1 -1
  61. package/dist/esm/hooks/useAvailableChains.d.ts.map +1 -1
  62. package/dist/esm/hooks/useAvailableChains.js +2 -1
  63. package/dist/esm/hooks/useAvailableChains.js.map +1 -1
  64. package/dist/esm/hooks/useExplorer.d.ts.map +1 -1
  65. package/dist/esm/hooks/useExplorer.js +4 -0
  66. package/dist/esm/hooks/useExplorer.js.map +1 -1
  67. package/dist/esm/hooks/useFlaggedTokenGuard.js +28 -0
  68. package/dist/esm/hooks/useFlaggedTokenGuard.js.map +1 -0
  69. package/dist/esm/hooks/useGasSufficiency.js +3 -1
  70. package/dist/esm/hooks/useGasSufficiency.js.map +1 -1
  71. package/dist/esm/hooks/useMaxSendAmount.js +15 -0
  72. package/dist/esm/hooks/useMaxSendAmount.js.map +1 -0
  73. package/dist/esm/hooks/usePacedRouteExecution.js +3 -2
  74. package/dist/esm/hooks/usePacedRouteExecution.js.map +1 -1
  75. package/dist/esm/hooks/useRouteExecutionMessage.js +3 -2
  76. package/dist/esm/hooks/useRouteExecutionMessage.js.map +1 -1
  77. package/dist/esm/hooks/useRoutes.d.ts.map +1 -1
  78. package/dist/esm/hooks/useRoutes.js +3 -1
  79. package/dist/esm/hooks/useRoutes.js.map +1 -1
  80. package/dist/esm/hooks/useToAddressRequirements.d.ts +2 -0
  81. package/dist/esm/hooks/useToAddressRequirements.d.ts.map +1 -1
  82. package/dist/esm/hooks/useToAddressRequirements.js +7 -1
  83. package/dist/esm/hooks/useToAddressRequirements.js.map +1 -1
  84. package/dist/esm/hooks/useTokenBalances.d.ts.map +1 -1
  85. package/dist/esm/hooks/useTokenBalances.js +7 -2
  86. package/dist/esm/hooks/useTokenBalances.js.map +1 -1
  87. package/dist/esm/hooks/useTokenBalancesQueries.js +29 -28
  88. package/dist/esm/hooks/useTokenBalancesQueries.js.map +1 -1
  89. package/dist/esm/hooks/useTokenSearch.js +3 -5
  90. package/dist/esm/hooks/useTokenSearch.js.map +1 -1
  91. package/dist/esm/hooks/useTokens.js +14 -10
  92. package/dist/esm/hooks/useTokens.js.map +1 -1
  93. package/dist/esm/hooks/useWideVariant.js +2 -1
  94. package/dist/esm/hooks/useWideVariant.js.map +1 -1
  95. package/dist/esm/i18n/bn.json +3 -1
  96. package/dist/esm/i18n/de.json +3 -1
  97. package/dist/esm/i18n/en.json +12 -3
  98. package/dist/esm/i18n/es.json +3 -1
  99. package/dist/esm/i18n/fr.json +3 -1
  100. package/dist/esm/i18n/hi.json +3 -1
  101. package/dist/esm/i18n/id.json +3 -1
  102. package/dist/esm/i18n/it.json +3 -1
  103. package/dist/esm/i18n/ja.json +3 -1
  104. package/dist/esm/i18n/ko.json +3 -1
  105. package/dist/esm/i18n/pl.json +3 -1
  106. package/dist/esm/i18n/pt.json +3 -1
  107. package/dist/esm/i18n/th.json +3 -1
  108. package/dist/esm/i18n/tr.json +3 -1
  109. package/dist/esm/i18n/uk.json +3 -1
  110. package/dist/esm/i18n/vi.json +3 -1
  111. package/dist/esm/i18n/zh.json +3 -1
  112. package/dist/esm/pages/MainPage/MainPage.js +2 -1
  113. package/dist/esm/pages/MainPage/MainPage.js.map +1 -1
  114. package/dist/esm/pages/MainPage/ReviewButton.js +2 -2
  115. package/dist/esm/pages/MainPage/ReviewButton.js.map +1 -1
  116. package/dist/esm/pages/RoutesPage/RoutesPage.js +2 -2
  117. package/dist/esm/pages/RoutesPage/RoutesPage.js.map +1 -1
  118. package/dist/esm/pages/SelectChainPage/SelectChainPage.js +13 -12
  119. package/dist/esm/pages/SelectChainPage/SelectChainPage.js.map +1 -1
  120. package/dist/esm/pages/SelectEnabledToolsPage.js +8 -5
  121. package/dist/esm/pages/SelectEnabledToolsPage.js.map +1 -1
  122. package/dist/esm/pages/SelectTokenPage/SelectTokenPage.js +2 -1
  123. package/dist/esm/pages/SelectTokenPage/SelectTokenPage.js.map +1 -1
  124. package/dist/esm/pages/SettingsPage/SmallBalanceFilterSettings.js +2 -1
  125. package/dist/esm/pages/SettingsPage/SmallBalanceFilterSettings.js.map +1 -1
  126. package/dist/esm/pages/TransactionDetailsPage/ContactSupportButton.js +2 -1
  127. package/dist/esm/pages/TransactionDetailsPage/ContactSupportButton.js.map +1 -1
  128. package/dist/esm/pages/TransactionDetailsPage/TransactionDetailsPage.js +2 -1
  129. package/dist/esm/pages/TransactionDetailsPage/TransactionDetailsPage.js.map +1 -1
  130. package/dist/esm/pages/TransactionPage/StartTransactionButton.js +4 -3
  131. package/dist/esm/pages/TransactionPage/StartTransactionButton.js.map +1 -1
  132. package/dist/esm/pages/TransactionPage/TokenVerificationBottomSheet.js +64 -0
  133. package/dist/esm/pages/TransactionPage/TokenVerificationBottomSheet.js.map +1 -0
  134. package/dist/esm/pages/TransactionPage/TransactionExecutionContent.js +19 -16
  135. package/dist/esm/pages/TransactionPage/TransactionExecutionContent.js.map +1 -1
  136. package/dist/esm/pages/TransactionPage/TransactionFailedButtons.js +3 -1
  137. package/dist/esm/pages/TransactionPage/TransactionFailedButtons.js.map +1 -1
  138. package/dist/esm/pages/TransactionPage/TransactionReview.js +24 -2
  139. package/dist/esm/pages/TransactionPage/TransactionReview.js.map +1 -1
  140. package/dist/esm/providers/I18nProvider/I18nProvider.js +37 -36
  141. package/dist/esm/providers/I18nProvider/I18nProvider.js.map +1 -1
  142. package/dist/esm/providers/ThemeProvider/ThemeProvider.js +2 -1
  143. package/dist/esm/providers/ThemeProvider/ThemeProvider.js.map +1 -1
  144. package/dist/esm/providers/WalletProvider/WalletProvider.js +10 -9
  145. package/dist/esm/providers/WalletProvider/WalletProvider.js.map +1 -1
  146. package/dist/esm/providers/WalletProvider/useExternalWalletProvider.js +7 -3
  147. package/dist/esm/providers/WalletProvider/useExternalWalletProvider.js.map +1 -1
  148. package/dist/esm/stores/routes/useRouteExecutionIndicator.js +4 -3
  149. package/dist/esm/stores/routes/useRouteExecutionIndicator.js.map +1 -1
  150. package/dist/esm/types/token.d.ts +4 -1
  151. package/dist/esm/types/token.d.ts.map +1 -1
  152. package/dist/esm/utils/compactNumberFormatter.js +1 -3
  153. package/dist/esm/utils/compactNumberFormatter.js.map +1 -1
  154. package/dist/esm/utils/customReceiver.js +7 -0
  155. package/dist/esm/utils/customReceiver.js.map +1 -0
  156. package/dist/esm/utils/gas.js +24 -0
  157. package/dist/esm/utils/gas.js.map +1 -0
  158. package/dist/esm/utils/getErrorMessage.js +0 -1
  159. package/dist/esm/utils/getErrorMessage.js.map +1 -1
  160. package/dist/esm/utils/limitOrder.js +1 -1
  161. package/dist/esm/utils/limitOrder.js.map +1 -1
  162. package/dist/esm/utils/token.js +21 -4
  163. package/dist/esm/utils/token.js.map +1 -1
  164. package/dist/esm/utils/tokenList.js +38 -21
  165. package/dist/esm/utils/tokenList.js.map +1 -1
  166. package/package.json +15 -15
  167. package/src/components/AmountInput/AmountInputEndAdornment.tsx +2 -21
  168. package/src/components/AmountInputCard/PercentageChips.tsx +2 -18
  169. package/src/components/Chains/AllChainsAvatar.tsx +7 -1
  170. package/src/components/Messages/UnsupportedToAddressMessage.tsx +24 -0
  171. package/src/components/Messages/WarningMessages.tsx +3 -0
  172. package/src/components/Messages/useMessageQueue.ts +9 -0
  173. package/src/components/Routes/LimitOrderRoutes.tsx +5 -2
  174. package/src/components/Routes/RoutesContent.tsx +5 -2
  175. package/src/components/SendToWallet/SendToWalletExpandButton.tsx +2 -1
  176. package/src/components/TokenList/TokenListItem.tsx +68 -9
  177. package/src/components/TokenList/VirtualizedTokenList.tsx +33 -9
  178. package/src/components/TokenList/types.ts +4 -0
  179. package/src/config/version.ts +1 -1
  180. package/src/hooks/useAvailableChains.ts +1 -0
  181. package/src/hooks/useExplorer.ts +2 -0
  182. package/src/hooks/useFlaggedTokenGuard.ts +33 -0
  183. package/src/hooks/useGasSufficiency.ts +9 -1
  184. package/src/hooks/useMaxSendAmount.ts +21 -0
  185. package/src/hooks/useRoutes.ts +7 -0
  186. package/src/hooks/useToAddressRequirements.ts +17 -1
  187. package/src/hooks/useTokenBalances.ts +18 -2
  188. package/src/hooks/useTokenSearch.ts +1 -5
  189. package/src/hooks/useTokens.ts +28 -13
  190. package/src/pages/MainPage/ReviewButton.tsx +6 -2
  191. package/src/pages/RoutesPage/RoutesPage.tsx +5 -2
  192. package/src/pages/TransactionPage/TokenVerificationBottomSheet.tsx +93 -0
  193. package/src/pages/TransactionPage/TransactionReview.tsx +28 -1
  194. package/src/providers/WalletProvider/useExternalWalletProvider.ts +9 -1
  195. package/src/types/token.ts +6 -3
  196. package/src/utils/customReceiver.ts +7 -0
  197. package/src/utils/gas.ts +41 -0
  198. package/src/utils/token.ts +53 -9
  199. package/src/utils/tokenList.ts +28 -0
@@ -1,7 +1,8 @@
1
1
  import type { StaticToken } from '@lifi/sdk'
2
2
  import { ChainType } from '@lifi/sdk'
3
3
  import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'
4
- import WarningRounded from '@mui/icons-material/WarningRounded'
4
+ import ReportRoundedIcon from '@mui/icons-material/ReportRounded'
5
+ import VerifiedIcon from '@mui/icons-material/Verified'
5
6
  import {
6
7
  Avatar,
7
8
  Box,
@@ -18,6 +19,7 @@ import { memo, useMemo, useRef, useState } from 'react'
18
19
  import { useTranslation } from 'react-i18next'
19
20
  import { useLongPress } from '../../hooks/useLongPress.js'
20
21
  import { formatTokenAmount, formatTokenPrice } from '../../utils/format.js'
22
+ import { getTokenVerificationProvider } from '../../utils/token.js'
21
23
  import { shortenAddress } from '../../utils/wallet.js'
22
24
  import { TokenAvatar } from '../Avatar/TokenAvatar.js'
23
25
  import { ListItemButton } from '../ListItem/ListItemButton.js'
@@ -36,6 +38,7 @@ export const TokenListItem: React.FC<TokenListItemProps> = memo(
36
38
  start,
37
39
  token,
38
40
  chain,
41
+ chainName,
39
42
  isBalanceLoading,
40
43
  startAdornment,
41
44
  endAdornment,
@@ -56,6 +59,7 @@ export const TokenListItem: React.FC<TokenListItemProps> = memo(
56
59
  isBalanceLoading={isBalanceLoading}
57
60
  onClick={onClick}
58
61
  chain={chain}
62
+ chainName={chainName}
59
63
  selected={selected}
60
64
  onShowTokenDetails={onShowTokenDetails}
61
65
  />
@@ -121,6 +125,7 @@ const TokenListItemButton: React.FC<TokenListItemButtonProps> = memo(
121
125
  onClick,
122
126
  token,
123
127
  chain,
128
+ chainName,
124
129
  isBalanceLoading,
125
130
  selected,
126
131
  onShowTokenDetails,
@@ -166,6 +171,64 @@ const TokenListItemButton: React.FC<TokenListItemButtonProps> = memo(
166
171
  onShowTokenDetails(token.address, withoutContractAddress, token.chainId)
167
172
  )
168
173
 
174
+ // The native token is badged ahead of any verdict: the provider screens
175
+ // the native-address convention and calls it a scam on some chains.
176
+ // A token the main list or the integrator vouches for needs no warning;
177
+ // anything else without a clean verdict keeps the amber one.
178
+ let verificationBadge:
179
+ | { Icon: typeof VerifiedIcon; color: string; title: string }
180
+ | undefined
181
+ if (token.native) {
182
+ verificationBadge = {
183
+ Icon: VerifiedIcon,
184
+ color: 'info.main',
185
+ title: chainName
186
+ ? t('tooltip.tokenNativeOnChain', {
187
+ tokenSymbol: token.symbol,
188
+ chainName,
189
+ })
190
+ : t('tooltip.tokenNative', { tokenSymbol: token.symbol }),
191
+ }
192
+ } else if (token.verificationStatus === 'flagged') {
193
+ verificationBadge = {
194
+ Icon: ReportRoundedIcon,
195
+ color: 'error.main',
196
+ title: t('warning.message.tokenFlagged', {
197
+ tokenSymbol: token.symbol,
198
+ }),
199
+ }
200
+ } else if (token.verificationStatus === 'verified') {
201
+ const provider = getTokenVerificationProvider(token)
202
+ verificationBadge = {
203
+ Icon: VerifiedIcon,
204
+ color: 'success.main',
205
+ title: provider
206
+ ? t('tooltip.tokenVerifiedByProvider', {
207
+ tokenSymbol: token.symbol,
208
+ provider,
209
+ })
210
+ : t('tooltip.tokenVerified', { tokenSymbol: token.symbol }),
211
+ }
212
+ } else if (
213
+ token.verificationStatus === 'unverified' &&
214
+ !token.listed &&
215
+ !token.verified
216
+ ) {
217
+ verificationBadge = {
218
+ Icon: ReportRoundedIcon,
219
+ color: 'warning.main',
220
+ title: t('warning.message.tokenUnverifiedByProvider', {
221
+ tokenSymbol: token.symbol,
222
+ }),
223
+ }
224
+ } else if (!token.verificationStatus && !token.listed && !token.verified) {
225
+ verificationBadge = {
226
+ Icon: ReportRoundedIcon,
227
+ color: 'warning.main',
228
+ title: t('warning.message.tokenUnverified'),
229
+ }
230
+ }
231
+
169
232
  return (
170
233
  <ListItemButton
171
234
  onClick={handleClick}
@@ -195,17 +258,13 @@ const TokenListItemButton: React.FC<TokenListItemButtonProps> = memo(
195
258
  primary={
196
259
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
197
260
  {token.symbol}
198
- {!token.verified && (
199
- <Tooltip
200
- title={t('warning.message.unverifiedToken')}
201
- placement="top"
202
- arrow
203
- >
204
- <WarningRounded
261
+ {verificationBadge && (
262
+ <Tooltip title={verificationBadge.title} placement="top" arrow>
263
+ <verificationBadge.Icon
205
264
  sx={{
206
265
  display: 'flex',
207
266
  fontSize: 16,
208
- color: 'warning.main',
267
+ color: verificationBadge.color,
209
268
  cursor: 'help',
210
269
  }}
211
270
  />
@@ -60,6 +60,24 @@ export const VirtualizedTokenList: FC<VirtualizedTokenListProps> = ({
60
60
  [tokens]
61
61
  )
62
62
 
63
+ // The hoisted native token sits above every category, so the row after it
64
+ // opens the first one. Only a row the hoist could have moved counts.
65
+ const nativeTokenHoisted = useMemo(() => {
66
+ const first = tokens[0]
67
+ return (
68
+ !!first?.native &&
69
+ !first.pinned &&
70
+ !first.featured &&
71
+ !first.popular &&
72
+ !first.verified
73
+ )
74
+ }, [tokens])
75
+
76
+ const isListStartIndex = useCallback(
77
+ (index: number) => index === 0 || (index === 1 && nativeTokenHoisted),
78
+ [nativeTokenHoisted]
79
+ )
80
+
63
81
  const estimateSize = useCallback(
64
82
  (index: number) => {
65
83
  const currentToken = tokens[index]
@@ -67,7 +85,7 @@ export const VirtualizedTokenList: FC<VirtualizedTokenListProps> = ({
67
85
  let size = tokenItemHeight
68
86
 
69
87
  // Pinned tokens (always shown, even in all networks mode)
70
- if (currentToken.pinned && index === 0) {
88
+ if (currentToken.pinned && isListStartIndex(index)) {
71
89
  size += 24
72
90
  }
73
91
  if (previousToken?.pinned && !currentToken.pinned) {
@@ -78,13 +96,19 @@ export const VirtualizedTokenList: FC<VirtualizedTokenListProps> = ({
78
96
  return size
79
97
  }
80
98
 
81
- if (currentToken.featured && !currentToken.pinned && index === 0) {
99
+ if (
100
+ currentToken.featured &&
101
+ !currentToken.pinned &&
102
+ isListStartIndex(index)
103
+ ) {
82
104
  size += 24
83
105
  }
84
106
 
85
- // Category transition (excluding pinned tokens)
107
+ // Category transition (excluding pinned tokens). The hoisted native row
108
+ // sits above every band, so the row after it ends nothing.
86
109
  const isNotPinned = !currentToken.pinned && !previousToken?.pinned
87
110
  if (
111
+ !isListStartIndex(index) &&
88
112
  isNotPinned &&
89
113
  ((previousToken?.amount && !currentToken.amount) ||
90
114
  (previousToken?.featured && !currentToken.featured) ||
@@ -95,7 +119,7 @@ export const VirtualizedTokenList: FC<VirtualizedTokenListProps> = ({
95
119
 
96
120
  return size
97
121
  },
98
- [tokens, showCategories]
122
+ [tokens, showCategories, isListStartIndex]
99
123
  )
100
124
 
101
125
  const virtualizerConfig = useMemo(
@@ -142,8 +166,9 @@ export const VirtualizedTokenList: FC<VirtualizedTokenListProps> = ({
142
166
  const previousToken: TokenAmount | undefined = tokens[item.index - 1]
143
167
  const chain = chainsSet?.get(currentToken.chainId)
144
168
 
169
+ const isListStart = isListStartIndex(item.index)
145
170
  const isNotPinned = !currentToken.pinned
146
- const isFirstPinnedToken = currentToken.pinned && item.index === 0
171
+ const isFirstPinnedToken = currentToken.pinned && isListStart
147
172
  const isTransitionFromPinned = previousToken?.pinned && isNotPinned
148
173
 
149
174
  // Category transitions (excluding pinned)
@@ -168,7 +193,7 @@ export const VirtualizedTokenList: FC<VirtualizedTokenListProps> = ({
168
193
 
169
194
  if (
170
195
  (isTransitionFromPinned && currentToken.featured) ||
171
- (currentToken.featured && isNotPinned && item.index === 0)
196
+ (currentToken.featured && isNotPinned && isListStart)
172
197
  ) {
173
198
  return t('main.featuredTokens')
174
199
  }
@@ -211,6 +236,7 @@ export const VirtualizedTokenList: FC<VirtualizedTokenListProps> = ({
211
236
  start={item.start}
212
237
  token={currentToken}
213
238
  chain={isAllNetworks ? chain : undefined}
239
+ chainName={chain?.name}
214
240
  selected={isSelected}
215
241
  onShowTokenDetails={onShowTokenDetails}
216
242
  isBalanceLoading={isBalanceLoading}
@@ -224,9 +250,7 @@ export const VirtualizedTokenList: FC<VirtualizedTokenListProps> = ({
224
250
  px: 1.5,
225
251
  pt:
226
252
  isFirstPinnedToken ||
227
- (currentToken.featured &&
228
- isNotPinned &&
229
- item.index === 0)
253
+ (currentToken.featured && isNotPinned && isListStart)
230
254
  ? 0
231
255
  : 1,
232
256
  pb: 1,
@@ -35,6 +35,8 @@ export interface TokenListItemProps extends TokenListItemBaseProps {
35
35
  chainId: number
36
36
  ) => void
37
37
  chain?: ExtendedChain
38
+ /** Resolved for every row, unlike `chain`, which the list passes only in all-networks mode. */
39
+ chainName?: string
38
40
  isBalanceLoading?: boolean
39
41
  startAdornment?: React.ReactNode
40
42
  endAdornment?: React.ReactNode
@@ -50,6 +52,8 @@ export interface TokenListItemButtonProps {
50
52
  onClick?(tokenAddress: string, chainId?: number): void
51
53
  token: TokenAmount
52
54
  chain?: ExtendedChain
55
+ /** Resolved for every row, unlike `chain`, which the list passes only in all-networks mode. */
56
+ chainName?: string
53
57
  isBalanceLoading?: boolean
54
58
  selected?: boolean
55
59
  }
@@ -1,2 +1,2 @@
1
1
  export const name = '@lifi/widget'
2
- export const version = '4.5.0'
2
+ export const version = '4.6.0'
@@ -19,6 +19,7 @@ const supportedChainTypes = [
19
19
  ChainType.UTXO,
20
20
  ChainType.MVM,
21
21
  ChainType.TVM,
22
+ ChainType.STL,
22
23
  ]
23
24
 
24
25
  export const useAvailableChains = (
@@ -13,6 +13,8 @@ const explorerPathOverrides: Partial<
13
13
  [ChainId.SUI]: { txPath: 'txblock', addressPath: 'coin' },
14
14
  [ChainId.LTR]: { txPath: 'logs', addressPath: 'accounts' },
15
15
  [ChainType.TVM]: { txPath: '#/transaction', addressPath: '#/address' },
16
+ // StellarExpert and steexp both route accounts at /account, never /address.
17
+ [ChainType.STL]: { txPath: 'tx', addressPath: 'account' },
16
18
  }
17
19
 
18
20
  type TransactionLinkProps = {
@@ -0,0 +1,33 @@
1
+ import type { RouteExtended, Token } from '@lifi/sdk'
2
+ import type { RefObject } from 'react'
3
+ import { useMemo, useRef } from 'react'
4
+ import type { BottomSheetBase } from '../components/BottomSheet/types.js'
5
+ import { getNativeTokenAddresses } from '../utils/token.js'
6
+ import { useAvailableChains } from './useAvailableChains.js'
7
+
8
+ /**
9
+ * Route tokens the screening provider flagged, and the sheet that warns about
10
+ * them before the transaction starts. A native token is exempt: the provider
11
+ * screens the native-address convention and calls it a scam on some chains.
12
+ */
13
+ export const useFlaggedTokenGuard = (
14
+ route: RouteExtended
15
+ ): {
16
+ flaggedTokens: Token[]
17
+ flaggedTokenSheetRef: RefObject<BottomSheetBase | null>
18
+ } => {
19
+ const { chains } = useAvailableChains()
20
+ const flaggedTokenSheetRef = useRef<BottomSheetBase>(null)
21
+
22
+ const flaggedTokens = useMemo(() => {
23
+ const nativeTokenAddresses = getNativeTokenAddresses(chains)
24
+ return [route.fromToken, route.toToken].filter(
25
+ (token) =>
26
+ token.verificationStatus === 'flagged' &&
27
+ nativeTokenAddresses.get(token.chainId)?.toLowerCase() !==
28
+ token.address.toLowerCase()
29
+ )
30
+ }, [chains, route.fromToken, route.toToken])
31
+
32
+ return { flaggedTokens, flaggedTokenSheetRef }
33
+ }
@@ -5,6 +5,7 @@ import { useQuery } from '@tanstack/react-query'
5
5
  import { useMemo } from 'react'
6
6
  import { useSDKClient } from '../providers/SDKClientProvider.js'
7
7
  import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'
8
+ import { getSelfFundedGasAmount } from '../utils/gas.js'
8
9
  import { getQueryKey } from '../utils/queries.js'
9
10
  import { useAvailableChains } from './useAvailableChains.js'
10
11
  import { useIsContractAddress } from './useIsContractAddress.js'
@@ -108,7 +109,14 @@ export const useGasSufficiency = (
108
109
  amount + BigInt(Number(gasCost.amount).toFixed(0)),
109
110
  0n
110
111
  )
111
- if (gasCostAmount > 0n) {
112
+ // A previous step can deliver this step's gas token (XLM bridged to
113
+ // Stellar), so the user does not have to hold it up front
114
+ const selfFundedGasAmount = getSelfFundedGasAmount(
115
+ route,
116
+ step,
117
+ getChainById(token.chainId)?.chainType
118
+ )
119
+ if (gasCostAmount > 0n && gasCostAmount > selfFundedGasAmount) {
112
120
  groupedGasCosts[token.chainId] = {
113
121
  gasAmount: groupedGasCosts[token.chainId]
114
122
  ? groupedGasCosts[token.chainId].gasAmount + gasCostAmount
@@ -0,0 +1,21 @@
1
+ import { useGasRecommendation } from './useGasRecommendation.js'
2
+ import { useTokenAddressBalance } from './useTokenAddressBalance.js'
3
+
4
+ // Largest amount of the token the wallet can send: for the native token that is
5
+ // the balance less the gas the chain recommends keeping back.
6
+ export const useMaxSendAmount = (
7
+ chainId?: number,
8
+ tokenAddress?: string
9
+ ): bigint => {
10
+ const { token, chain } = useTokenAddressBalance(chainId, tokenAddress)
11
+ const { data } = useGasRecommendation(chainId)
12
+
13
+ if (!token?.amount) {
14
+ return 0n
15
+ }
16
+ if (chain?.nativeToken.address !== tokenAddress) {
17
+ return token.amount
18
+ }
19
+ const reserved = BigInt(data?.recommended?.amount ?? 0)
20
+ return token.amount > reserved ? token.amount - reserved : token.amount
21
+ }
@@ -29,6 +29,7 @@ import { defaultSlippage } from '../stores/settings/createSettingsStore.js'
29
29
  import { useSettings } from '../stores/settings/useSettings.js'
30
30
  import { WidgetEvent } from '../types/events.js'
31
31
  import type { TokensByChain } from '../types/token.js'
32
+ import { isCustomReceiverUnsupported } from '../utils/customReceiver.js'
32
33
  import { getQueryKey } from '../utils/queries.js'
33
34
  import { updateTokenInCache } from '../utils/token.js'
34
35
  import { useChain } from './useChain.js'
@@ -177,6 +178,11 @@ export const useRoutes = ({
177
178
  ? hasToAddressAndChainTypeSatisfied
178
179
  : true
179
180
 
181
+ // Stellar settles to the signer, so a set receiver makes the route unfulfillable.
182
+ const customReceiverBlocked =
183
+ isCustomReceiverUnsupported(fromChain?.chainType, toChain?.chainType) &&
184
+ Boolean(toAddress)
185
+
180
186
  // toAddress might be an empty string, but we need to pass undefined if there is no value
181
187
  const toWalletAddress = toAddress || undefined
182
188
 
@@ -197,6 +203,7 @@ export const useRoutes = ({
197
203
  !Number.isNaN(slippage) &&
198
204
  hasAmount &&
199
205
  isToAddressSatisfied &&
206
+ !customReceiverBlocked &&
200
207
  contractCallQuoteEnabled &&
201
208
  !isBatchingSupportedLoading
202
209
 
@@ -4,12 +4,15 @@ import { useEthereumContext } from '@lifi/widget-provider'
4
4
  import { useChain } from '../hooks/useChain.js'
5
5
  import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'
6
6
  import { useFieldValues } from '../stores/form/useFieldValues.js'
7
+ import { isCustomReceiverUnsupported } from '../utils/customReceiver.js'
7
8
  import { useIsContractAddress } from './useIsContractAddress.js'
8
9
 
9
10
  export const useToAddressRequirements = (
10
11
  route?: RouteExtended
11
12
  ): {
12
13
  requiredToAddress: boolean
14
+ unsupportedToAddress: boolean
15
+ unsupportedReceiverBlocking: boolean
13
16
  requiredToChainType: ChainType | undefined
14
17
  accountNotDeployedAtDestination: boolean
15
18
  accountDeployedAtDestination: boolean
@@ -68,11 +71,22 @@ export const useToAddressRequirements = (
68
71
  fromChainId !== toChainId &&
69
72
  !fromContractCodeHasDelegationIndicator
70
73
 
74
+ const unsupportedToAddress = isCustomReceiverUnsupported(
75
+ fromChain?.chainType,
76
+ toChain?.chainType
77
+ )
78
+
79
+ // Stellar routes settle to the signer, so a required receiver is never honoured.
80
+ const unsupportedReceiverBlocking = Boolean(
81
+ unsupportedToAddress && (toAddress || requiredUI?.toAddress)
82
+ )
83
+
71
84
  const requiredToAddress = Boolean(
72
85
  (isDifferentChainType ||
73
86
  isCrossChainContractAddress ||
74
87
  requiredUI?.toAddress) &&
75
- !hiddenUI?.toAddress
88
+ !hiddenUI?.toAddress &&
89
+ !unsupportedToAddress
76
90
  )
77
91
 
78
92
  const accountNotDeployedAtDestination = Boolean(
@@ -91,6 +105,8 @@ export const useToAddressRequirements = (
91
105
 
92
106
  return {
93
107
  requiredToAddress,
108
+ unsupportedToAddress,
109
+ unsupportedReceiverBlocking,
94
110
  requiredToChainType: toChain?.chainType,
95
111
  accountNotDeployedAtDestination,
96
112
  accountDeployedAtDestination,
@@ -4,7 +4,11 @@ import type { FormType } from '../stores/form/types.js'
4
4
  import { useSettings } from '../stores/settings/useSettings.js'
5
5
  import type { TokenAmount } from '../types/token.js'
6
6
  import { formatTokenPrice } from '../utils/format.js'
7
- import { isSearchMatch, processTokenList } from '../utils/tokenList.js'
7
+ import {
8
+ hoistNativeToken,
9
+ isSearchMatch,
10
+ processTokenList,
11
+ } from '../utils/tokenList.js'
8
12
  import { useAccountsBalancesData } from './useAccountsBalancesData.js'
9
13
  import { useDisplayedTokens } from './useDisplayedTokens.js'
10
14
  import { useTokenBalancesQueries } from './useTokenBalancesQueries.js'
@@ -115,7 +119,7 @@ export const useTokenBalances = (
115
119
  ])
116
120
 
117
121
  const { processedTokens, withCategories, withPinnedTokens } = useMemo(() => {
118
- return processTokenList(
122
+ const result = processTokenList(
119
123
  isBalanceLoading,
120
124
  isAllNetworks || !!search,
121
125
  configTokens,
@@ -124,6 +128,18 @@ export const useTokenBalances = (
124
128
  displayedTokensWithBalances,
125
129
  isPinnedToken
126
130
  )
131
+ // A search keeps its results ranked by match, and "All networks" has no
132
+ // single native token to lead with.
133
+ if (isAllNetworks || search) {
134
+ return result
135
+ }
136
+ return {
137
+ ...result,
138
+ processedTokens: hoistNativeToken(
139
+ result.processedTokens,
140
+ selectedChainId
141
+ ),
142
+ }
127
143
  }, [
128
144
  isBalanceLoading,
129
145
  isAllNetworks,
@@ -74,13 +74,9 @@ export const useTokenSearch = (
74
74
  ) {
75
75
  return data
76
76
  }
77
- // Mark token from search as unverified
78
77
  return {
79
78
  ...data,
80
- [chainId as number]: [
81
- ...(chainTokens ?? []),
82
- { ...token, verified: false },
83
- ],
79
+ [chainId as number]: [...(chainTokens ?? []), token],
84
80
  }
85
81
  }
86
82
  )
@@ -17,8 +17,10 @@ import { isItemAllowed } from '../utils/item.js'
17
17
  import { getQueryKey } from '../utils/queries.js'
18
18
  import {
19
19
  filterAllowedTokens,
20
+ getNativeTokenAddresses,
20
21
  mergeVerifiedWithSearchTokens,
21
22
  } from '../utils/token.js'
23
+ import { useAvailableChains } from './useAvailableChains.js'
22
24
 
23
25
  const refetchInterval = 300_000
24
26
 
@@ -49,6 +51,7 @@ export const useTokens = (
49
51
  ChainType.UTXO,
50
52
  ChainType.MVM,
51
53
  ChainType.TVM,
54
+ ChainType.STL,
52
55
  ].filter((chainType) => isItemAllowed(chainType, chainsConfig?.types))
53
56
 
54
57
  const tokensResponse: TokensExtendedResponse = await getTokens(
@@ -63,11 +66,11 @@ export const useTokens = (
63
66
  { signal }
64
67
  )
65
68
 
66
- // Mark all tokens as verified
69
+ // `listed` tells a main-list token from a searched one
67
70
  const tokens: TokensByChain = Object.fromEntries(
68
71
  Object.entries(tokensResponse.tokens).map(([chainId, tokens]) => [
69
72
  chainId,
70
- tokens.map((token) => ({ ...token, verified: true })),
73
+ tokens.map((token) => ({ ...token, listed: true })),
71
74
  ])
72
75
  )
73
76
 
@@ -92,6 +95,7 @@ export const useTokens = (
92
95
  ChainType.UTXO,
93
96
  ChainType.MVM,
94
97
  ChainType.TVM,
98
+ ChainType.STL,
95
99
  ].filter((chainType) => isItemAllowed(chainType, chainsConfig?.types))
96
100
 
97
101
  const tokensResponse: TokensExtendedResponse = await getTokens(
@@ -130,26 +134,37 @@ export const useTokens = (
130
134
  }
131
135
  }
132
136
 
133
- // Mark all search tokens as unverified
134
- const tokens: TokensByChain = Object.fromEntries(
135
- Object.entries(tokensResponse.tokens).map(([chainId, tokens]) => [
136
- chainId,
137
- tokens.map((token) => ({ ...token, verified: false })),
138
- ])
139
- )
140
-
141
- return tokens
137
+ return tokensResponse.tokens as TokensByChain
142
138
  },
143
139
  enabled: !!search,
144
140
  refetchInterval,
145
141
  staleTime: refetchInterval,
146
142
  })
147
143
 
144
+ const { chains } = useAvailableChains()
145
+ const nativeTokenAddresses = useMemo(
146
+ () => getNativeTokenAddresses(chains),
147
+ [chains]
148
+ )
149
+
148
150
  // Merge tokens at read time - single place where caches are combined
149
151
  const allTokens = useMemo(() => {
150
152
  const merged = mergeVerifiedWithSearchTokens(verifiedTokens, searchTokens)
151
- return filterAllowedTokens(merged, configTokens, chainsConfig, formType)
152
- }, [verifiedTokens, searchTokens, configTokens, chainsConfig, formType])
153
+ return filterAllowedTokens(
154
+ merged,
155
+ configTokens,
156
+ chainsConfig,
157
+ formType,
158
+ nativeTokenAddresses
159
+ )
160
+ }, [
161
+ verifiedTokens,
162
+ searchTokens,
163
+ configTokens,
164
+ chainsConfig,
165
+ formType,
166
+ nativeTokenAddresses,
167
+ ])
153
168
 
154
169
  return {
155
170
  allTokens,
@@ -16,7 +16,8 @@ export const ReviewButton: React.FC = () => {
16
16
  const emitter = useWidgetEvents()
17
17
  const { mode, modeOptions } = useWidgetConfig()
18
18
  const splitState = useSplitMode()
19
- const { toAddress, requiredToAddress } = useToAddressRequirements()
19
+ const { toAddress, requiredToAddress, unsupportedReceiverBlocking } =
20
+ useToAddressRequirements()
20
21
  const { routes, setReviewableRoute } = useRoutes()
21
22
  const [selectedRouteId] = useFieldValues('selectedRouteId')
22
23
 
@@ -85,7 +86,10 @@ export const ReviewButton: React.FC = () => {
85
86
  <BaseTransactionButton
86
87
  text={getButtonText()}
87
88
  onClick={handleClick}
88
- disabled={currentRoute && requiredToAddress && !toAddress}
89
+ disabled={
90
+ currentRoute &&
91
+ (unsupportedReceiverBlocking || (requiredToAddress && !toAddress))
92
+ }
89
93
  route={currentRoute}
90
94
  sx={{ flex: 1 }}
91
95
  />
@@ -36,7 +36,8 @@ export const RoutesPage = (): JSX.Element => {
36
36
  } = useRoutes()
37
37
  const { account } = useAccount({ chainType: fromChain?.chainType })
38
38
  const [toAddress] = useFieldValues('toAddress')
39
- const { requiredToAddress } = useToAddressRequirements()
39
+ const { requiredToAddress, unsupportedReceiverBlocking } =
40
+ useToAddressRequirements()
40
41
 
41
42
  const headerAction = useMemo(
42
43
  () => (
@@ -72,7 +73,9 @@ export const RoutesPage = (): JSX.Element => {
72
73
 
73
74
  const routeNotFound = !routes?.length && !isLoading && !isFetching
74
75
 
75
- const toAddressUnsatisfied = routes?.[0] && requiredToAddress && !toAddress
76
+ const toAddressUnsatisfied =
77
+ routes?.[0] &&
78
+ (unsupportedReceiverBlocking || (requiredToAddress && !toAddress))
76
79
 
77
80
  const allowInteraction = account.isConnected && !toAddressUnsatisfied
78
81