@lifi/widget 3.15.2 → 3.16.1-beta.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 (76) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/esm/components/Messages/WarningMessages.d.ts +7 -0
  3. package/dist/esm/components/Messages/{MainMessages.js → WarningMessages.js} +3 -6
  4. package/dist/esm/components/Messages/WarningMessages.js.map +1 -0
  5. package/dist/esm/components/Messages/useMessageQueue.d.ts +1 -0
  6. package/dist/esm/components/Messages/useMessageQueue.js +10 -5
  7. package/dist/esm/components/Messages/useMessageQueue.js.map +1 -1
  8. package/dist/esm/components/Routes/RoutesExpanded.js +4 -1
  9. package/dist/esm/components/Routes/RoutesExpanded.js.map +1 -1
  10. package/dist/esm/components/StepActions/StepActions.js +1 -1
  11. package/dist/esm/components/StepActions/StepActions.js.map +1 -1
  12. package/dist/esm/config/version.d.ts +1 -1
  13. package/dist/esm/config/version.js +1 -1
  14. package/dist/esm/config/version.js.map +1 -1
  15. package/dist/esm/hooks/useAddressActivity.d.ts +8 -0
  16. package/dist/esm/hooks/useAddressActivity.js +23 -0
  17. package/dist/esm/hooks/useAddressActivity.js.map +1 -0
  18. package/dist/esm/hooks/useGasSufficiency.js +1 -1
  19. package/dist/esm/hooks/useGasSufficiency.js.map +1 -1
  20. package/dist/esm/hooks/useIsCompatibleDestinationAccount.d.ts +8 -0
  21. package/dist/esm/hooks/useIsCompatibleDestinationAccount.js +37 -0
  22. package/dist/esm/hooks/useIsCompatibleDestinationAccount.js.map +1 -0
  23. package/dist/esm/hooks/useIsContractAddress.d.ts +2 -0
  24. package/dist/esm/hooks/useIsContractAddress.js +7 -2
  25. package/dist/esm/hooks/useIsContractAddress.js.map +1 -1
  26. package/dist/esm/hooks/useRoutes.js +6 -1
  27. package/dist/esm/hooks/useRoutes.js.map +1 -1
  28. package/dist/esm/hooks/useToAddressRequirements.d.ts +2 -2
  29. package/dist/esm/hooks/useToAddressRequirements.js +10 -12
  30. package/dist/esm/hooks/useToAddressRequirements.js.map +1 -1
  31. package/dist/esm/i18n/en.json +5 -2
  32. package/dist/esm/pages/MainPage/MainPage.js +2 -2
  33. package/dist/esm/pages/MainPage/MainPage.js.map +1 -1
  34. package/dist/esm/pages/MainPage/MainWarningMessages.d.ts +7 -0
  35. package/dist/esm/pages/MainPage/MainWarningMessages.js +9 -0
  36. package/dist/esm/pages/MainPage/MainWarningMessages.js.map +1 -0
  37. package/dist/esm/pages/MainPage/ReviewButton.js +14 -13
  38. package/dist/esm/pages/MainPage/ReviewButton.js.map +1 -1
  39. package/dist/esm/pages/RoutesPage/RoutesPage.js +5 -2
  40. package/dist/esm/pages/RoutesPage/RoutesPage.js.map +1 -1
  41. package/dist/esm/pages/TransactionPage/ConfirmToAddressSheet.d.ts +8 -0
  42. package/dist/esm/pages/TransactionPage/ConfirmToAddressSheet.js +38 -0
  43. package/dist/esm/pages/TransactionPage/ConfirmToAddressSheet.js.map +1 -0
  44. package/dist/esm/pages/TransactionPage/StartTransactionButton.js +3 -6
  45. package/dist/esm/pages/TransactionPage/StartTransactionButton.js.map +1 -1
  46. package/dist/esm/pages/TransactionPage/TransactionPage.js +15 -4
  47. package/dist/esm/pages/TransactionPage/TransactionPage.js.map +1 -1
  48. package/dist/esm/stores/form/URLSearchParamsBuilder.js +4 -1
  49. package/dist/esm/stores/form/URLSearchParamsBuilder.js.map +1 -1
  50. package/dist/esm/types/events.d.ts +6 -1
  51. package/dist/esm/types/events.js +1 -0
  52. package/dist/esm/types/events.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/components/Messages/{MainMessages.tsx → WarningMessages.tsx} +10 -5
  55. package/src/components/Messages/useMessageQueue.ts +16 -6
  56. package/src/components/Routes/RoutesExpanded.tsx +5 -1
  57. package/src/components/StepActions/StepActions.tsx +1 -1
  58. package/src/config/version.ts +1 -1
  59. package/src/hooks/useAddressActivity.ts +39 -0
  60. package/src/hooks/useGasSufficiency.ts +1 -1
  61. package/src/hooks/useIsCompatibleDestinationAccount.ts +58 -0
  62. package/src/hooks/useIsContractAddress.ts +11 -2
  63. package/src/hooks/useRoutes.ts +7 -1
  64. package/src/hooks/useToAddressRequirements.ts +16 -20
  65. package/src/i18n/en.json +5 -2
  66. package/src/pages/MainPage/MainPage.tsx +2 -2
  67. package/src/pages/MainPage/MainWarningMessages.tsx +17 -0
  68. package/src/pages/MainPage/ReviewButton.tsx +15 -14
  69. package/src/pages/RoutesPage/RoutesPage.tsx +6 -3
  70. package/src/pages/TransactionPage/ConfirmToAddressSheet.tsx +96 -0
  71. package/src/pages/TransactionPage/StartTransactionButton.tsx +4 -12
  72. package/src/pages/TransactionPage/TransactionPage.tsx +29 -3
  73. package/src/stores/form/URLSearchParamsBuilder.tsx +4 -1
  74. package/src/types/events.ts +5 -0
  75. package/dist/esm/components/Messages/MainMessages.d.ts +0 -2
  76. package/dist/esm/components/Messages/MainMessages.js.map +0 -1
@@ -0,0 +1,39 @@
1
+ import type { Address } from 'viem'
2
+ import { isAddress } from 'viem'
3
+ import { useTransactionCount } from 'wagmi'
4
+ import { useFieldValues } from '../stores/form/useFieldValues.js'
5
+
6
+ interface AddressActivity {
7
+ hasActivity: boolean
8
+ isLoading: boolean
9
+ isFetched: boolean
10
+ toAddress: string | undefined
11
+ }
12
+
13
+ export const useAddressActivity = (chainId?: number): AddressActivity => {
14
+ const [toAddress, toChainId] = useFieldValues('toAddress', 'toChain')
15
+
16
+ const destinationChainId = chainId ?? toChainId
17
+
18
+ const {
19
+ data: transactionCount,
20
+ isLoading,
21
+ isFetched,
22
+ error,
23
+ } = useTransactionCount({
24
+ address: toAddress as Address,
25
+ chainId: destinationChainId,
26
+ query: {
27
+ enabled: Boolean(toAddress && destinationChainId && isAddress(toAddress)),
28
+ refetchInterval: 300_000,
29
+ staleTime: 300_000,
30
+ },
31
+ })
32
+
33
+ return {
34
+ toAddress,
35
+ hasActivity: Boolean(transactionCount && transactionCount > 0),
36
+ isLoading,
37
+ isFetched: isFetched && !error,
38
+ }
39
+ }
@@ -38,7 +38,7 @@ export const useGasSufficiency = (route?: RouteExtended) => {
38
38
  // We assume that LI.Fuel protocol always refuels the destination chain
39
39
  const hasRefuelStep = route.steps
40
40
  .flatMap((step) => step.includedSteps)
41
- .some((includedStep) => includedStep.tool === 'lifuelProtocol')
41
+ .some((includedStep) => includedStep.tool === 'gasZip')
42
42
 
43
43
  const gasCosts = route.steps
44
44
  .filter((step) => !step.execution || step.execution.status !== 'DONE')
@@ -0,0 +1,58 @@
1
+ import type { RouteExtended } from '@lifi/sdk'
2
+ import { useAccount } from '@lifi/wallet-management'
3
+ import { useFieldValues } from '../stores/form/useFieldValues.js'
4
+ import { isDelegationDesignatorCode } from '../utils/eip7702.js'
5
+ import { useChain } from './useChain.js'
6
+ import { useIsContractAddress } from './useIsContractAddress.js'
7
+
8
+ export const useIsCompatibleDestinationAccount = (route?: RouteExtended) => {
9
+ const [formFromChainId, formToChainId, formToAddress] = useFieldValues(
10
+ 'fromChain',
11
+ 'toChain',
12
+ 'toAddress'
13
+ )
14
+
15
+ const fromChainId = route?.fromChainId ?? formFromChainId
16
+ const toChainId = route?.toChainId ?? formToChainId
17
+
18
+ const { chain: fromChain } = useChain(fromChainId)
19
+ const { chain: toChain } = useChain(toChainId)
20
+ const { account } = useAccount({
21
+ chainType: fromChain?.chainType,
22
+ })
23
+
24
+ const fromAddress = route?.fromAddress ?? account.address
25
+ const toAddress = route
26
+ ? route.fromAddress !== route.toAddress
27
+ ? route.toAddress
28
+ : formToAddress
29
+ : formToAddress
30
+
31
+ const {
32
+ isContractAddress: isFromContractAddress,
33
+ contractCode: fromContractCode,
34
+ isLoading: isFromContractLoading,
35
+ isFetched: isFromContractFetched,
36
+ } = useIsContractAddress(fromAddress, fromChainId, fromChain?.chainType)
37
+ const {
38
+ isContractAddress: isToContractAddress,
39
+ isLoading: isToContractLoading,
40
+ isFetched: isToContractFetched,
41
+ } = useIsContractAddress(toAddress, toChainId, toChain?.chainType)
42
+
43
+ const accountNotDeployedAtDestination =
44
+ isFromContractAddress &&
45
+ // We don't want to block transfers for EIP-7702 accounts since they are designed
46
+ // to maintain EOA-like properties while delegating execution.
47
+ !isDelegationDesignatorCode(fromContractCode) &&
48
+ !isToContractAddress &&
49
+ fromAddress?.toLowerCase() === toAddress?.toLowerCase()
50
+
51
+ return {
52
+ isCompatibleDestinationAccount: !accountNotDeployedAtDestination,
53
+ isFromContractAddress,
54
+ isToContractAddress,
55
+ isLoading: isFromContractLoading || isToContractLoading,
56
+ isFetched: isFromContractFetched && isToContractFetched,
57
+ }
58
+ }
@@ -7,7 +7,11 @@ export const useIsContractAddress = (
7
7
  chainId?: number,
8
8
  chainType?: ChainType
9
9
  ) => {
10
- const { data: contractCode } = useBytecode({
10
+ const {
11
+ data: contractCode,
12
+ isLoading,
13
+ isFetched,
14
+ } = useBytecode({
11
15
  address: address as Address,
12
16
  chainId: chainId,
13
17
  query: {
@@ -17,5 +21,10 @@ export const useIsContractAddress = (
17
21
  },
18
22
  })
19
23
 
20
- return { isContractAddress: !!contractCode, contractCode }
24
+ return {
25
+ isContractAddress: !!contractCode,
26
+ contractCode,
27
+ isLoading,
28
+ isFetched,
29
+ }
21
30
  }
@@ -13,6 +13,7 @@ import { getChainTypeFromAddress } from '../utils/chainType.js'
13
13
  import { useChain } from './useChain.js'
14
14
  import { useDebouncedWatch } from './useDebouncedWatch.js'
15
15
  import { useGasRefuel } from './useGasRefuel.js'
16
+ import { useIsCompatibleDestinationAccount } from './useIsCompatibleDestinationAccount.js'
16
17
  import { useSwapOnly } from './useSwapOnly.js'
17
18
  import { useToken } from './useToken.js'
18
19
  import { useWidgetEvents } from './useWidgetEvents.js'
@@ -76,6 +77,8 @@ export const useRoutes = ({ observableRoute }: RoutesProps = {}) => {
76
77
  const { token: toToken } = useToken(toChainId, toTokenAddress)
77
78
  const { chain: fromChain } = useChain(fromChainId)
78
79
  const { chain: toChain } = useChain(toChainId)
80
+ const { isCompatibleDestinationAccount } =
81
+ useIsCompatibleDestinationAccount(observableRoute)
79
82
  const { enabled: enabledRefuel, fromAmount: gasRecommendationFromAmount } =
80
83
  useGasRefuel()
81
84
 
@@ -107,6 +110,9 @@ export const useRoutes = ({ observableRoute }: RoutesProps = {}) => {
107
110
  exchanges?.allow?.length || exchanges?.deny?.length
108
111
  ? enabledExchanges
109
112
  : undefined
113
+ const allowSwitchChain = isCompatibleDestinationAccount
114
+ ? sdkConfig?.routeOptions?.allowSwitchChain
115
+ : false
110
116
 
111
117
  const isEnabled =
112
118
  Boolean(Number(fromChainId)) &&
@@ -138,7 +144,7 @@ export const useRoutes = ({ observableRoute }: RoutesProps = {}) => {
138
144
  allowedExchanges,
139
145
  routePriority,
140
146
  subvariant,
141
- sdkConfig?.routeOptions?.allowSwitchChain,
147
+ allowSwitchChain,
142
148
  enabledRefuel && enabledAutoRefuel,
143
149
  gasRecommendationFromAmount,
144
150
  feeConfig?.fee || fee,
@@ -1,31 +1,36 @@
1
+ import type { RouteExtended } from '@lifi/sdk'
1
2
  import { useAccount } from '@lifi/wallet-management'
2
3
  import { useChain } from '../hooks/useChain.js'
3
4
  import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'
4
5
  import { useFieldValues } from '../stores/form/useFieldValues.js'
5
6
  import { RequiredUI } from '../types/widget.js'
6
- import { isDelegationDesignatorCode } from '../utils/eip7702.js'
7
7
  import { useIsContractAddress } from './useIsContractAddress.js'
8
8
 
9
- export const useToAddressRequirements = () => {
9
+ export const useToAddressRequirements = (route?: RouteExtended) => {
10
10
  const { requiredUI } = useWidgetConfig()
11
- const [fromChainId, toChainId, toAddress] = useFieldValues(
11
+ const [formFromChainId, formToChainId, formToAddress] = useFieldValues(
12
12
  'fromChain',
13
13
  'toChain',
14
14
  'toAddress'
15
15
  )
16
+
17
+ const fromChainId = route?.fromChainId ?? formFromChainId
18
+ const toChainId = route?.toChainId ?? formToChainId
19
+ const toAddress = route
20
+ ? route.fromAddress !== route.toAddress
21
+ ? route.toAddress
22
+ : formToAddress
23
+ : formToAddress
24
+
16
25
  const { chain: fromChain } = useChain(fromChainId)
17
26
  const { chain: toChain } = useChain(toChainId)
18
27
  const { account } = useAccount({
19
28
  chainType: fromChain?.chainType,
20
29
  })
21
- const {
22
- isContractAddress: isFromContractAddress,
23
- contractCode: fromContractCode,
24
- } = useIsContractAddress(account.address, fromChainId, account.chainType)
25
- const { isContractAddress: isToContractAddress } = useIsContractAddress(
26
- toAddress,
27
- toChainId,
28
- toChain?.chainType
30
+ const { isContractAddress: isFromContractAddress } = useIsContractAddress(
31
+ account.address,
32
+ fromChainId,
33
+ account.chainType
29
34
  )
30
35
 
31
36
  const isDifferentChainType =
@@ -34,14 +39,6 @@ export const useToAddressRequirements = () => {
34
39
  const isCrossChainContractAddress =
35
40
  isFromContractAddress && fromChainId !== toChainId
36
41
 
37
- const accountNotDeployedAtDestination =
38
- isFromContractAddress &&
39
- // We don't want to block transfers for EIP-7702 accounts since they are designed
40
- // to maintain EOA-like properties while delegating execution.
41
- !isDelegationDesignatorCode(fromContractCode) &&
42
- !isToContractAddress &&
43
- account.address?.toLowerCase() === toAddress?.toLowerCase()
44
-
45
42
  const requiredToAddress =
46
43
  requiredUI?.includes(RequiredUI.ToAddress) ||
47
44
  isDifferentChainType ||
@@ -50,7 +47,6 @@ export const useToAddressRequirements = () => {
50
47
  return {
51
48
  requiredToAddress,
52
49
  requiredToChainType: toChain?.chainType,
53
- accountNotDeployedAtDestination,
54
50
  toAddress,
55
51
  }
56
52
  }
package/src/i18n/en.json CHANGED
@@ -90,7 +90,7 @@
90
90
  "emptyTokenList": "We couldn't find tokens on {{chainName}} chain or you don't have any. Please search by contract address if your token doesn't appear or choose another chain.",
91
91
  "emptyTransactionHistory": "Transaction history is only stored locally and will be deleted if you clear your browser data.",
92
92
  "routeNotFound": "Reasons for that could be: low liquidity, amount selected is too low, gas costs are too high or there are no routes for the selected combination.",
93
- "toAddressIsRequired": "Please provide the destination wallet address to which the funds will be transferred."
93
+ "toAddressIsRequired": "The destination wallet address is required to proceed with the transfer."
94
94
  },
95
95
  "title": {
96
96
  "autoRefuel": "Get {{chainName}} gas",
@@ -118,6 +118,8 @@
118
118
  "warning": {
119
119
  "message": {
120
120
  "accountNotDeployedMessage": "Smart contract account is not deployed on the destination chain. Sending funds to a non-existent contract would result in permanent loss.",
121
+ "noAddressActivity": "This address has never been used on this network. Please verify you're sending to the correct address to prevent potential loss of funds.",
122
+ "lowAddressActivity": "This address has low activity on {{chainName}} network. Please verify you're sending to the correct address and network to prevent potential loss of funds.",
121
123
  "deleteActiveTransactions": "Active transactions are only stored locally and can't be recovered if you delete them.",
122
124
  "deleteTransactionHistory": "Transaction history is only stored locally and can't be recovered if you delete it.",
123
125
  "fundsLossPrevention": "Always ensure smart contract accounts are properly set up on the destination chain and avoid direct transfers to exchanges to prevent fund loss.",
@@ -135,7 +137,8 @@
135
137
  "highValueLoss": "High value loss",
136
138
  "insufficientGas": "Insufficient gas",
137
139
  "rateChanged": "Rate changed",
138
- "resetSettings": "Reset settings?"
140
+ "resetSettings": "Reset settings?",
141
+ "lowAddressActivity": "Low Activity Address"
139
142
  }
140
143
  },
141
144
  "error": {
@@ -4,7 +4,6 @@ import { ActiveTransactions } from '../../components/ActiveTransactions/ActiveTr
4
4
  import { AmountInput } from '../../components/AmountInput/AmountInput.js'
5
5
  import { ContractComponent } from '../../components/ContractComponent/ContractComponent.js'
6
6
  import { GasRefuelMessage } from '../../components/Messages/GasRefuelMessage.js'
7
- import { MainMessages } from '../../components/Messages/MainMessages.js'
8
7
  import { PageContainer } from '../../components/PageContainer.js'
9
8
  import { PoweredBy } from '../../components/PoweredBy/PoweredBy.js'
10
9
  import { Routes } from '../../components/Routes/Routes.js'
@@ -15,6 +14,7 @@ import { useHeader } from '../../hooks/useHeader.js'
15
14
  import { useWideVariant } from '../../hooks/useWideVariant.js'
16
15
  import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js'
17
16
  import { HiddenUI } from '../../types/widget.js'
17
+ import { MainWarningMessages } from './MainWarningMessages.js'
18
18
  import { ReviewButton } from './ReviewButton.js'
19
19
 
20
20
  export const MainPage: React.FC = () => {
@@ -49,7 +49,7 @@ export const MainPage: React.FC = () => {
49
49
  {!wideVariant ? <Routes sx={marginSx} /> : null}
50
50
  <SendToWalletButton sx={marginSx} />
51
51
  <GasRefuelMessage mb={2} />
52
- <MainMessages mb={2} />
52
+ <MainWarningMessages mb={2} />
53
53
  <Box
54
54
  sx={{
55
55
  display: 'flex',
@@ -0,0 +1,17 @@
1
+ import type { Route } from '@lifi/sdk'
2
+ import type { BoxProps } from '@mui/material'
3
+ import { WarningMessages } from '../../components/Messages/WarningMessages.js'
4
+ import { useRoutes } from '../../hooks/useRoutes.js'
5
+
6
+ interface MainWarningMessagesProps extends BoxProps {
7
+ route?: Route
8
+ }
9
+
10
+ export const MainWarningMessages: React.FC<MainWarningMessagesProps> = (
11
+ props
12
+ ) => {
13
+ const { routes } = useRoutes()
14
+ const currentRoute = routes?.[0]
15
+
16
+ return <WarningMessages route={currentRoute} {...props} />
17
+ }
@@ -1,11 +1,11 @@
1
1
  import { useTranslation } from 'react-i18next'
2
2
  import { useNavigate } from 'react-router-dom'
3
3
  import { BaseTransactionButton } from '../../components/BaseTransactionButton/BaseTransactionButton.js'
4
+ import { useIsCompatibleDestinationAccount } from '../../hooks/useIsCompatibleDestinationAccount.js'
4
5
  import { useRoutes } from '../../hooks/useRoutes.js'
5
6
  import { useToAddressRequirements } from '../../hooks/useToAddressRequirements.js'
6
7
  import { useWidgetEvents } from '../../hooks/useWidgetEvents.js'
7
8
  import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js'
8
- import { useFieldValues } from '../../stores/form/useFieldValues.js'
9
9
  import { useSplitSubvariantStore } from '../../stores/settings/useSplitSubvariantStore.js'
10
10
  import { WidgetEvent } from '../../types/events.js'
11
11
  import { navigationRoutes } from '../../utils/navigationRoutes.js'
@@ -16,24 +16,25 @@ export const ReviewButton: React.FC = () => {
16
16
  const emitter = useWidgetEvents()
17
17
  const { subvariant, subvariantOptions } = useWidgetConfig()
18
18
  const splitState = useSplitSubvariantStore((state) => state.state)
19
- const [toAddress] = useFieldValues('toAddress')
20
- const { requiredToAddress, accountNotDeployedAtDestination } =
21
- useToAddressRequirements()
19
+ const { toAddress, requiredToAddress } = useToAddressRequirements()
20
+ const { isCompatibleDestinationAccount } = useIsCompatibleDestinationAccount()
22
21
  const { routes, setReviewableRoute } = useRoutes()
23
22
 
24
23
  const currentRoute = routes?.[0]
25
24
 
26
25
  const handleClick = async () => {
27
- if (currentRoute) {
28
- setReviewableRoute(currentRoute)
29
- navigate(navigationRoutes.transactionExecution, {
30
- state: { routeId: currentRoute.id },
31
- })
32
- emitter.emit(WidgetEvent.RouteSelected, {
33
- route: currentRoute,
34
- routes: routes!,
35
- })
26
+ if (!currentRoute) {
27
+ return
36
28
  }
29
+
30
+ setReviewableRoute(currentRoute)
31
+ navigate(navigationRoutes.transactionExecution, {
32
+ state: { routeId: currentRoute.id },
33
+ })
34
+ emitter.emit(WidgetEvent.RouteSelected, {
35
+ route: currentRoute,
36
+ routes: routes!,
37
+ })
37
38
  }
38
39
 
39
40
  const getButtonText = (): string => {
@@ -75,7 +76,7 @@ export const ReviewButton: React.FC = () => {
75
76
  onClick={handleClick}
76
77
  disabled={
77
78
  (currentRoute && requiredToAddress && !toAddress) ||
78
- accountNotDeployedAtDestination
79
+ !isCompatibleDestinationAccount
79
80
  }
80
81
  />
81
82
  )
@@ -8,6 +8,7 @@ import { RouteCard } from '../../components/RouteCard/RouteCard.js'
8
8
  import { RouteCardSkeleton } from '../../components/RouteCard/RouteCardSkeleton.js'
9
9
  import { RouteNotFoundCard } from '../../components/RouteCard/RouteNotFoundCard.js'
10
10
  import { useHeader } from '../../hooks/useHeader.js'
11
+ import { useIsCompatibleDestinationAccount } from '../../hooks/useIsCompatibleDestinationAccount.js'
11
12
  import { useNavigateBack } from '../../hooks/useNavigateBack.js'
12
13
  import { useRoutes } from '../../hooks/useRoutes.js'
13
14
  import { useToAddressRequirements } from '../../hooks/useToAddressRequirements.js'
@@ -18,6 +19,7 @@ import { navigationRoutes } from '../../utils/navigationRoutes.js'
18
19
  import { Stack } from './RoutesPage.style.js'
19
20
 
20
21
  export const RoutesPage: React.FC<BoxProps> = () => {
22
+ const { t } = useTranslation()
21
23
  const { navigate } = useNavigateBack()
22
24
  const emitter = useWidgetEvents()
23
25
  const {
@@ -33,8 +35,7 @@ export const RoutesPage: React.FC<BoxProps> = () => {
33
35
  const { account } = useAccount({ chainType: fromChain?.chainType })
34
36
  const [toAddress] = useFieldValues('toAddress')
35
37
  const { requiredToAddress } = useToAddressRequirements()
36
-
37
- const { t } = useTranslation()
38
+ const { isCompatibleDestinationAccount } = useIsCompatibleDestinationAccount()
38
39
 
39
40
  const headerAction = useMemo(
40
41
  () => (
@@ -65,7 +66,9 @@ export const RoutesPage: React.FC<BoxProps> = () => {
65
66
 
66
67
  const routeNotFound = !routes?.length && !isLoading && !isFetching
67
68
 
68
- const toAddressUnsatisfied = routes?.[0] && requiredToAddress && !toAddress
69
+ const toAddressUnsatisfied =
70
+ (routes?.[0] && requiredToAddress && !toAddress) ||
71
+ !isCompatibleDestinationAccount
69
72
  const allowInteraction = account.isConnected && !toAddressUnsatisfied
70
73
 
71
74
  return (
@@ -0,0 +1,96 @@
1
+ import { Wallet, WarningRounded } from '@mui/icons-material'
2
+ import { Button, Typography } from '@mui/material'
3
+ import type { MutableRefObject } from 'react'
4
+ import { forwardRef, useRef } from 'react'
5
+ import { useTranslation } from 'react-i18next'
6
+ import { BottomSheet } from '../../components/BottomSheet/BottomSheet.js'
7
+ import type { BottomSheetBase } from '../../components/BottomSheet/types.js'
8
+ import { AlertMessage } from '../../components/Messages/AlertMessage.js'
9
+ import { useChain } from '../../hooks/useChain.js'
10
+ import { useSetContentHeight } from '../../hooks/useSetContentHeight.js'
11
+ import { useWidgetEvents } from '../../hooks/useWidgetEvents.js'
12
+ import { WidgetEvent } from '../../types/events.js'
13
+ import {
14
+ IconContainer,
15
+ SendToWalletButtonRow,
16
+ SendToWalletSheetContainer,
17
+ SheetAddressContainer,
18
+ } from '../SendToWallet/SendToWalletPage.style.js'
19
+
20
+ interface ConfirmToAddressSheetProps {
21
+ onContinue: () => void
22
+ toAddress: string
23
+ toChainId: number
24
+ }
25
+
26
+ interface ConfirmToAddressSheetContentProps extends ConfirmToAddressSheetProps {
27
+ onClose: () => void
28
+ }
29
+
30
+ export const ConfirmToAddressSheet = forwardRef<
31
+ BottomSheetBase,
32
+ ConfirmToAddressSheetProps
33
+ >((props, ref) => {
34
+ const handleClose = () => {
35
+ ;(ref as MutableRefObject<BottomSheetBase>).current?.close()
36
+ }
37
+
38
+ return (
39
+ <BottomSheet ref={ref}>
40
+ <ConfirmToAddressSheetContent {...props} onClose={handleClose} />
41
+ </BottomSheet>
42
+ )
43
+ })
44
+
45
+ const ConfirmToAddressSheetContent: React.FC<
46
+ ConfirmToAddressSheetContentProps
47
+ > = ({ onContinue, onClose, toAddress, toChainId }) => {
48
+ const { t } = useTranslation()
49
+ const { chain } = useChain(toChainId)
50
+ const emitter = useWidgetEvents()
51
+ const ref = useRef<HTMLElement>(null)
52
+ useSetContentHeight(ref)
53
+
54
+ const handleContinue = () => {
55
+ emitter.emit(WidgetEvent.LowAddressActivityConfirmed, {
56
+ address: toAddress,
57
+ chainId: toChainId,
58
+ })
59
+ onClose()
60
+ onContinue()
61
+ }
62
+
63
+ return (
64
+ <SendToWalletSheetContainer ref={ref}>
65
+ <IconContainer>
66
+ <Wallet sx={{ fontSize: 40 }} />
67
+ </IconContainer>
68
+ <Typography variant="h6" sx={{ textAlign: 'center', mb: 2 }}>
69
+ {t('warning.title.lowAddressActivity')}
70
+ </Typography>
71
+ <SheetAddressContainer>
72
+ <Typography>{toAddress}</Typography>
73
+ </SheetAddressContainer>
74
+ <AlertMessage
75
+ severity="warning"
76
+ title={
77
+ <Typography variant="body2" sx={{ color: 'text.primary' }}>
78
+ {t('warning.message.lowAddressActivity', {
79
+ chainName: chain?.name,
80
+ })}
81
+ </Typography>
82
+ }
83
+ icon={<WarningRounded />}
84
+ multiline
85
+ />
86
+ <SendToWalletButtonRow>
87
+ <Button variant="text" onClick={onClose} fullWidth>
88
+ {t('button.cancel')}
89
+ </Button>
90
+ <Button variant="contained" onClick={handleContinue} fullWidth>
91
+ {t('button.continue')}
92
+ </Button>
93
+ </SendToWalletButtonRow>
94
+ </SendToWalletSheetContainer>
95
+ )
96
+ }
@@ -1,6 +1,5 @@
1
1
  import { BaseTransactionButton } from '../../components/BaseTransactionButton/BaseTransactionButton.js'
2
- import { useFromTokenSufficiency } from '../../hooks/useFromTokenSufficiency.js'
3
- import { useGasSufficiency } from '../../hooks/useGasSufficiency.js'
2
+ import { useMessageQueue } from '../../components/Messages/useMessageQueue.js'
4
3
  import type { StartTransactionButtonProps } from './types.js'
5
4
 
6
5
  export const StartTransactionButton: React.FC<StartTransactionButtonProps> = ({
@@ -9,21 +8,14 @@ export const StartTransactionButton: React.FC<StartTransactionButtonProps> = ({
9
8
  text,
10
9
  loading,
11
10
  }) => {
12
- const { insufficientGas, isLoading: isGasSufficiencyLoading } =
13
- useGasSufficiency(route)
14
- const { insufficientFromToken, isLoading: isFromTokenSufficiencyLoading } =
15
- useFromTokenSufficiency(route)
16
-
17
- const shouldDisableButton = insufficientFromToken || !!insufficientGas?.length
11
+ const { hasMessages, isLoading } = useMessageQueue(route)
18
12
 
19
13
  return (
20
14
  <BaseTransactionButton
21
15
  onClick={onClick}
22
16
  text={text}
23
- disabled={shouldDisableButton}
24
- loading={
25
- isFromTokenSufficiencyLoading || isGasSufficiencyLoading || loading
26
- }
17
+ disabled={hasMessages}
18
+ loading={isLoading || loading}
27
19
  />
28
20
  )
29
21
  }
@@ -6,10 +6,11 @@ import { useTranslation } from 'react-i18next'
6
6
  import { useLocation } from 'react-router-dom'
7
7
  import type { BottomSheetBase } from '../../components/BottomSheet/types.js'
8
8
  import { ContractComponent } from '../../components/ContractComponent/ContractComponent.js'
9
- import { GasMessage } from '../../components/Messages/GasMessage.js'
9
+ import { WarningMessages } from '../../components/Messages/WarningMessages.js'
10
10
  import { PageContainer } from '../../components/PageContainer.js'
11
11
  import { getStepList } from '../../components/Step/StepList.js'
12
12
  import { TransactionDetails } from '../../components/TransactionDetails.js'
13
+ import { useAddressActivity } from '../../hooks/useAddressActivity.js'
13
14
  import { useHeader } from '../../hooks/useHeader.js'
14
15
  import { useNavigateBack } from '../../hooks/useNavigateBack.js'
15
16
  import { useRouteExecution } from '../../hooks/useRouteExecution.js'
@@ -19,6 +20,7 @@ import { useFieldActions } from '../../stores/form/useFieldActions.js'
19
20
  import { RouteExecutionStatus } from '../../stores/routes/types.js'
20
21
  import { WidgetEvent } from '../../types/events.js'
21
22
  import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees.js'
23
+ import { ConfirmToAddressSheet } from './ConfirmToAddressSheet.js'
22
24
  import type { ExchangeRateBottomSheetBase } from './ExchangeRateBottomSheet.js'
23
25
  import { ExchangeRateBottomSheet } from './ExchangeRateBottomSheet.js'
24
26
  import { RouteTracker } from './RouteTracker.js'
@@ -44,6 +46,7 @@ export const TransactionPage: React.FC = () => {
44
46
 
45
47
  const tokenValueBottomSheetRef = useRef<BottomSheetBase>(null)
46
48
  const exchangeRateBottomSheetRef = useRef<ExchangeRateBottomSheetBase>(null)
49
+ const confirmToAddressSheetRef = useRef<BottomSheetBase>(null)
47
50
 
48
51
  const onAcceptExchangeRateUpdate = (
49
52
  resolver: (value: boolean) => void,
@@ -58,6 +61,13 @@ export const TransactionPage: React.FC = () => {
58
61
  onAcceptExchangeRateUpdate,
59
62
  })
60
63
 
64
+ const {
65
+ toAddress,
66
+ hasActivity,
67
+ isLoading: isLoadingAddressActivity,
68
+ isFetched: isActivityAddressFetched,
69
+ } = useAddressActivity(route?.toChainId)
70
+
61
71
  const getHeaderTitle = () => {
62
72
  if (subvariant === 'custom') {
63
73
  return t(`header.${subvariantOptions?.custom ?? 'checkout'}`)
@@ -127,6 +137,16 @@ export const TransactionPage: React.FC = () => {
127
137
 
128
138
  const handleStartClick = async () => {
129
139
  if (status === RouteExecutionStatus.Idle) {
140
+ if (
141
+ toAddress &&
142
+ !hasActivity &&
143
+ !isLoadingAddressActivity &&
144
+ isActivityAddressFetched
145
+ ) {
146
+ confirmToAddressSheetRef.current?.open()
147
+ return
148
+ }
149
+
130
150
  const { gasCostUSD, feeCostUSD } = getAccumulatedFeeCostsBreakdown(route)
131
151
  const fromAmountUSD = Number.parseFloat(route.fromAmountUSD)
132
152
  const toAmountUSD = Number.parseFloat(route.toAmountUSD)
@@ -187,7 +207,7 @@ export const TransactionPage: React.FC = () => {
187
207
  {status === RouteExecutionStatus.Idle ||
188
208
  status === RouteExecutionStatus.Failed ? (
189
209
  <>
190
- <GasMessage mt={2} route={route} />
210
+ <WarningMessages mt={2} route={route} />
191
211
  <Box
192
212
  sx={{
193
213
  mt: 2,
@@ -198,7 +218,7 @@ export const TransactionPage: React.FC = () => {
198
218
  text={getButtonText()}
199
219
  onClick={handleStartClick}
200
220
  route={route}
201
- loading={routeRefreshing}
221
+ loading={routeRefreshing || isLoadingAddressActivity}
202
222
  />
203
223
  {status === RouteExecutionStatus.Failed ? (
204
224
  <Tooltip
@@ -228,6 +248,12 @@ export const TransactionPage: React.FC = () => {
228
248
  />
229
249
  ) : null}
230
250
  <ExchangeRateBottomSheet ref={exchangeRateBottomSheetRef} />
251
+ <ConfirmToAddressSheet
252
+ ref={confirmToAddressSheetRef}
253
+ onContinue={handleExecuteRoute}
254
+ toAddress={toAddress!}
255
+ toChainId={route.toChainId!}
256
+ />
231
257
  </PageContainer>
232
258
  )
233
259
  }
@@ -76,7 +76,10 @@ export const URLSearchParamsBuilder = () => {
76
76
  const validationResult = await validateAddress({
77
77
  value: formValues.toAddress,
78
78
  })
79
- if (validationResult.isValid) {
79
+ // Check if the toAddress is still in the query string
80
+ // Could be modified by the user before the validation is done
81
+ const { toAddress } = getDefaultValuesFromQueryString()
82
+ if (validationResult.isValid && toAddress) {
80
83
  const bookmark = {
81
84
  address: validationResult.address,
82
85
  chainType: validationResult.chainType,
@@ -29,6 +29,7 @@ export enum WidgetEvent {
29
29
  FormFieldChanged = 'formFieldChanged',
30
30
  SettingUpdated = 'settingUpdated',
31
31
  TokenSearch = 'tokenSearch',
32
+ LowAddressActivityConfirmed = 'lowAddressActivityConfirmed',
32
33
  }
33
34
 
34
35
  export type WidgetEvents = {
@@ -50,6 +51,10 @@ export type WidgetEvents = {
50
51
  pageEntered: NavigationRouteType
51
52
  settingUpdated: SettingUpdated
52
53
  tokenSearch: TokenSearch
54
+ [WidgetEvent.LowAddressActivityConfirmed]: {
55
+ address: string
56
+ chainId: number
57
+ }
53
58
  }
54
59
 
55
60
  export type ContactSupport = {
@@ -1,2 +0,0 @@
1
- import type { BoxProps } from '@mui/material';
2
- export declare const MainMessages: React.FC<BoxProps>;