@lifi/widget 3.8.2 → 3.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 (76) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/esm/components/Header/WalletHeader.js +9 -3
  3. package/dist/esm/components/Header/WalletHeader.js.map +1 -1
  4. package/dist/esm/components/ReverseTokensButton/ReverseTokensButton.js +16 -1
  5. package/dist/esm/components/ReverseTokensButton/ReverseTokensButton.js.map +1 -1
  6. package/dist/esm/components/TokenList/TokenList.js +6 -4
  7. package/dist/esm/components/TokenList/TokenList.js.map +1 -1
  8. package/dist/esm/components/TokenList/TokenListItem.js +1 -1
  9. package/dist/esm/components/TokenList/TokenListItem.js.map +1 -1
  10. package/dist/esm/components/TokenList/types.d.ts +2 -2
  11. package/dist/esm/components/TokenList/useTokenSelect.d.ts +4 -0
  12. package/dist/esm/components/TokenList/useTokenSelect.js +19 -10
  13. package/dist/esm/components/TokenList/useTokenSelect.js.map +1 -1
  14. package/dist/esm/config/version.d.ts +1 -1
  15. package/dist/esm/config/version.js +1 -1
  16. package/dist/esm/hooks/useAvailableChains.d.ts +2 -1
  17. package/dist/esm/hooks/useAvailableChains.js.map +1 -1
  18. package/dist/esm/hooks/useChain.d.ts +1 -0
  19. package/dist/esm/hooks/useChain.js +1 -0
  20. package/dist/esm/hooks/useChain.js.map +1 -1
  21. package/dist/esm/hooks/useChains.d.ts +1 -1
  22. package/dist/esm/hooks/useGasSufficiency.d.ts +1 -1
  23. package/dist/esm/hooks/useGasSufficiency.js +2 -12
  24. package/dist/esm/hooks/useGasSufficiency.js.map +1 -1
  25. package/dist/esm/hooks/useIsContractAddress.d.ts +2 -0
  26. package/dist/esm/hooks/useIsContractAddress.js +16 -0
  27. package/dist/esm/hooks/useIsContractAddress.js.map +1 -0
  28. package/dist/esm/hooks/useToAddressAutoPopulate.d.ts +12 -0
  29. package/dist/esm/hooks/useToAddressAutoPopulate.js +68 -0
  30. package/dist/esm/hooks/useToAddressAutoPopulate.js.map +1 -0
  31. package/dist/esm/hooks/useToAddressRequirements.d.ts +1 -1
  32. package/dist/esm/hooks/useToAddressRequirements.js +9 -1
  33. package/dist/esm/hooks/useToAddressRequirements.js.map +1 -1
  34. package/dist/esm/hooks/useToAddressReset.js +15 -8
  35. package/dist/esm/hooks/useToAddressReset.js.map +1 -1
  36. package/dist/esm/pages/SendToWallet/BookmarksPage.js +1 -0
  37. package/dist/esm/pages/SendToWallet/BookmarksPage.js.map +1 -1
  38. package/dist/esm/pages/SendToWallet/ConfirmAddressSheet.js +1 -0
  39. package/dist/esm/pages/SendToWallet/ConfirmAddressSheet.js.map +1 -1
  40. package/dist/esm/pages/SendToWallet/ConnectedWalletsPage.js +1 -0
  41. package/dist/esm/pages/SendToWallet/ConnectedWalletsPage.js.map +1 -1
  42. package/dist/esm/pages/SendToWallet/RecentWalletsPage.js +1 -0
  43. package/dist/esm/pages/SendToWallet/RecentWalletsPage.js.map +1 -1
  44. package/dist/esm/pages/SendToWallet/SendToConfiguredWalletPage.js +4 -1
  45. package/dist/esm/pages/SendToWallet/SendToConfiguredWalletPage.js.map +1 -1
  46. package/dist/esm/stores/form/createFormStore.js +1 -0
  47. package/dist/esm/stores/form/createFormStore.js.map +1 -1
  48. package/dist/esm/stores/form/types.d.ts +1 -0
  49. package/dist/esm/stores/form/types.js.map +1 -1
  50. package/dist/esm/stores/form/useFieldActions.d.ts +1 -0
  51. package/dist/esm/stores/form/useFieldActions.js +1 -0
  52. package/dist/esm/stores/form/useFieldActions.js.map +1 -1
  53. package/package.json +8 -8
  54. package/src/components/Header/WalletHeader.tsx +14 -5
  55. package/src/components/ReverseTokensButton/ReverseTokensButton.tsx +25 -6
  56. package/src/components/TokenList/TokenList.tsx +7 -4
  57. package/src/components/TokenList/TokenListItem.tsx +1 -1
  58. package/src/components/TokenList/types.ts +2 -2
  59. package/src/components/TokenList/useTokenSelect.ts +39 -16
  60. package/src/config/version.ts +1 -1
  61. package/src/hooks/useAvailableChains.ts +6 -1
  62. package/src/hooks/useChain.ts +1 -0
  63. package/src/hooks/useGasSufficiency.ts +7 -19
  64. package/src/hooks/useIsContractAddress.ts +21 -0
  65. package/src/hooks/useToAddressAutoPopulate.ts +95 -0
  66. package/src/hooks/useToAddressRequirements.ts +13 -2
  67. package/src/hooks/useToAddressReset.ts +15 -8
  68. package/src/pages/SendToWallet/BookmarksPage.tsx +1 -0
  69. package/src/pages/SendToWallet/ConfirmAddressSheet.tsx +1 -0
  70. package/src/pages/SendToWallet/ConnectedWalletsPage.tsx +1 -0
  71. package/src/pages/SendToWallet/RecentWalletsPage.tsx +1 -0
  72. package/src/pages/SendToWallet/SendToConfiguredWalletPage.tsx +4 -1
  73. package/src/stores/form/createFormStore.ts +2 -0
  74. package/src/stores/form/types.ts +1 -0
  75. package/src/stores/form/useFieldActions.ts +1 -0
  76. package/src/stores/header/useHeaderStore.tsx +1 -1
@@ -5,6 +5,11 @@ import { useCallback } from 'react'
5
5
  import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'
6
6
  import { isItemAllowed } from '../utils/item.js'
7
7
 
8
+ export type GetChainById = (
9
+ chainId?: number,
10
+ chains?: ExtendedChain[]
11
+ ) => ExtendedChain | undefined
12
+
8
13
  const supportedChainTypes = [ChainType.EVM, ChainType.SVM, ChainType.UTXO]
9
14
 
10
15
  export const useAvailableChains = (chainTypes?: ChainType[]) => {
@@ -35,7 +40,7 @@ export const useAvailableChains = (chainTypes?: ChainType[]) => {
35
40
  staleTime: 300_000,
36
41
  })
37
42
 
38
- const getChainById = useCallback(
43
+ const getChainById: GetChainById = useCallback(
39
44
  (chainId?: number, chains: ExtendedChain[] | undefined = data) => {
40
45
  if (!chainId) {
41
46
  return
@@ -9,5 +9,6 @@ export const useChain = (chainId?: number) => {
9
9
  return {
10
10
  chain,
11
11
  isLoading,
12
+ getChainById,
12
13
  }
13
14
  }
@@ -1,14 +1,8 @@
1
- import {
2
- ChainType,
3
- type EVMChain,
4
- type RouteExtended,
5
- type Token,
6
- } from '@lifi/sdk'
1
+ import type { EVMChain, RouteExtended, Token } from '@lifi/sdk'
7
2
  import { useAccount } from '@lifi/wallet-management'
8
3
  import { useQuery } from '@tanstack/react-query'
9
- import type { Address } from 'viem'
10
- import { type Connector, useBytecode } from 'wagmi'
11
4
  import { useAvailableChains } from './useAvailableChains.js'
5
+ import { useIsContractAddress } from './useIsContractAddress.js'
12
6
  import { getTokenBalancesWithRetry } from './useTokenBalance.js'
13
7
 
14
8
  export interface GasSufficiency {
@@ -27,18 +21,12 @@ export const useGasSufficiency = (route?: RouteExtended) => {
27
21
  const { account } = useAccount({
28
22
  chainType: getChainById(route?.fromChainId)?.chainType,
29
23
  })
30
- const { data: contractCode } = useBytecode({
31
- address:
32
- account.chainType === ChainType.EVM
33
- ? (account.address as Address)
34
- : undefined,
35
- query: {
36
- refetchInterval: 300_000,
37
- staleTime: 300_000,
38
- },
39
- })
40
24
 
41
- const isContractAddress = !!contractCode
25
+ const isContractAddress = useIsContractAddress(
26
+ account.address,
27
+ route?.fromChainId,
28
+ account.chainType
29
+ )
42
30
 
43
31
  const { data: insufficientGas, isLoading } = useQuery({
44
32
  queryKey: ['gas-sufficiency-check', account.address, route?.id],
@@ -0,0 +1,21 @@
1
+ import { ChainType } from '@lifi/sdk'
2
+ import type { Address } from 'viem'
3
+ import { useBytecode } from 'wagmi'
4
+
5
+ export const useIsContractAddress = (
6
+ address?: string,
7
+ chainId?: number,
8
+ chainType?: ChainType
9
+ ) => {
10
+ const { data: contractCode } = useBytecode({
11
+ address: address as Address,
12
+ chainId: chainId,
13
+ query: {
14
+ refetchInterval: 300_000,
15
+ staleTime: 300_000,
16
+ enabled: Boolean(chainType === ChainType.EVM && chainId),
17
+ },
18
+ })
19
+ const isContractAddress = !!contractCode
20
+ return isContractAddress
21
+ }
@@ -0,0 +1,95 @@
1
+ import { useAccount } from '@lifi/wallet-management'
2
+ import { useCallback } from 'react'
3
+ import { useBookmarkActions } from '../stores/bookmarks/useBookmarkActions.js'
4
+ import type { FormType } from '../stores/form/types.js'
5
+ import { useFieldActions } from '../stores/form/useFieldActions.js'
6
+ import { useSendToWalletActions } from '../stores/settings/useSendToWalletStore.js'
7
+ import { getChainTypeFromAddress } from '../utils/chainType.js'
8
+ import { useAvailableChains } from './useAvailableChains.js'
9
+
10
+ export type UpdateToAddressArgs = {
11
+ formType: FormType
12
+ selectedToAddress?: string
13
+ selectedChainId?: number
14
+ selectedOppositeTokenAddress?: string
15
+ selectedOppositeChainId?: number
16
+ }
17
+
18
+ /**
19
+ * Automatically populates toAddress field if bridging across ecosystems and compatible wallet is connected
20
+ */
21
+ export const useToAddressAutoPopulate = () => {
22
+ const { setFieldValue } = useFieldActions()
23
+ const { setSendToWallet } = useSendToWalletActions()
24
+ const { setSelectedBookmark } = useBookmarkActions()
25
+ const { getChainById } = useAvailableChains()
26
+ const { accounts } = useAccount()
27
+
28
+ return useCallback(
29
+ ({
30
+ formType,
31
+ selectedToAddress,
32
+ selectedChainId,
33
+ selectedOppositeTokenAddress,
34
+ selectedOppositeChainId,
35
+ }: UpdateToAddressArgs) => {
36
+ if (
37
+ !selectedOppositeTokenAddress ||
38
+ !selectedOppositeChainId ||
39
+ !selectedChainId ||
40
+ !accounts?.length
41
+ ) {
42
+ return
43
+ }
44
+ const selectedChain = getChainById?.(selectedChainId)
45
+ const selectedOppositeChain = getChainById?.(selectedOppositeChainId)
46
+ // Proceed if both chains are defined and of different ecosystem types (indicating cross-ecosystem bridging)
47
+ if (
48
+ !selectedChain ||
49
+ !selectedOppositeChain ||
50
+ selectedChain.chainType === selectedOppositeChain.chainType
51
+ ) {
52
+ return
53
+ }
54
+ // Identify the destination chain type based on the bridge direction ('from' or 'to')
55
+ const destinationChainType =
56
+ formType === 'from'
57
+ ? selectedOppositeChain.chainType
58
+ : selectedChain.chainType
59
+ // If toAddress is already selected, verify that it matches the destination chain type
60
+ if (selectedToAddress) {
61
+ const selectedToAddressChainType =
62
+ getChainTypeFromAddress(selectedToAddress)
63
+ if (destinationChainType === selectedToAddressChainType) {
64
+ return
65
+ }
66
+ }
67
+ // Find connected account compatible with the destination chain type
68
+ const destinationAccount = accounts?.find(
69
+ (account) => account.chainType === destinationChainType
70
+ )
71
+ // If a compatible destination account is found, set toAddress as if selecting it from the "Send to Wallet" connected wallets page
72
+ if (destinationAccount?.address) {
73
+ setFieldValue('toAddress', destinationAccount.address, {
74
+ isDirty: false,
75
+ isTouched: true,
76
+ })
77
+ setSelectedBookmark({
78
+ name: destinationAccount.connector?.name,
79
+ address: destinationAccount.address,
80
+ chainType: destinationAccount.chainType,
81
+ isConnectedAccount: true,
82
+ })
83
+ setSendToWallet(true)
84
+ return destinationAccount.address
85
+ }
86
+ },
87
+ [
88
+ accounts,
89
+ getChainById,
90
+ setFieldValue,
91
+ setSelectedBookmark,
92
+ setSendToWallet,
93
+ ]
94
+ )
95
+ }
@@ -1,20 +1,31 @@
1
+ import { useAccount } from '@lifi/wallet-management'
1
2
  import { useChain } from '../hooks/useChain.js'
2
3
  import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'
3
4
  import { useFieldValues } from '../stores/form/useFieldValues.js'
4
5
  import { RequiredUI } from '../types/widget.js'
6
+ import { useIsContractAddress } from './useIsContractAddress.js'
5
7
 
6
8
  export const useToAddressRequirements = () => {
7
9
  const { requiredUI } = useWidgetConfig()
8
10
  const [fromChainId, toChainId] = useFieldValues('fromChain', 'toChain')
9
-
10
11
  const { chain: fromChain } = useChain(fromChainId)
11
12
  const { chain: toChain } = useChain(toChainId)
13
+ const { account } = useAccount({
14
+ chainType: fromChain?.chainType,
15
+ })
16
+ const isFromContractAddress = useIsContractAddress(
17
+ account.address,
18
+ fromChainId,
19
+ account.chainType
20
+ )
12
21
 
13
22
  const isDifferentChainType =
14
23
  fromChain && toChain && fromChain.chainType !== toChain.chainType
15
24
 
16
25
  const requiredToAddress =
17
- requiredUI?.includes(RequiredUI.ToAddress) || isDifferentChainType
26
+ requiredUI?.includes(RequiredUI.ToAddress) ||
27
+ isDifferentChainType ||
28
+ isFromContractAddress
18
29
 
19
30
  return {
20
31
  requiredToAddress,
@@ -3,13 +3,15 @@ import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'
3
3
  import { useBookmarkActions } from '../stores/bookmarks/useBookmarkActions.js'
4
4
  import { useBookmarks } from '../stores/bookmarks/useBookmarks.js'
5
5
  import { useFieldActions } from '../stores/form/useFieldActions.js'
6
+ import { useSendToWalletActions } from '../stores/settings/useSendToWalletStore.js'
6
7
  import { RequiredUI } from '../types/widget.js'
7
8
 
8
9
  export const useToAddressReset = () => {
9
10
  const { requiredUI } = useWidgetConfig()
10
- const { setFieldValue } = useFieldActions()
11
+ const { setFieldValue, isDirty } = useFieldActions()
11
12
  const { selectedBookmark } = useBookmarks()
12
13
  const { setSelectedBookmark } = useBookmarkActions()
14
+ const { setSendToWallet } = useSendToWalletActions()
13
15
 
14
16
  const tryResetToAddress = (toChain: ExtendedChain) => {
15
17
  const requiredToAddress = requiredUI?.includes(RequiredUI.ToAddress)
@@ -20,15 +22,20 @@ export const useToAddressReset = () => {
20
22
  const shouldResetToAddress =
21
23
  !requiredToAddress && !bookmarkSatisfiesToChainType
22
24
 
23
- // toAddress field is required (always visible) when bridging between
24
- // two ecosystems (fromChain and toChain have different chain types).
25
- // We clean up toAddress on every chain change if toAddress is not required.
26
- // This is used when we switch between different chain ecosystems (chain types) and
27
- // prevents cases when after we switch the chain from one type to another "Send to wallet" field hides,
28
- // but it keeps toAddress value set for the previous chain pair.
25
+ // The toAddress field is required and always visible when bridging between
26
+ // different ecosystems (fromChain and toChain have different chain types).
27
+ // We reset toAddress on each chain change if it's no longer required, ensuring that
28
+ // switching chain types doesn't leave a previously set toAddress value when
29
+ // the "Send to Wallet" field is hidden.
29
30
  if (shouldResetToAddress) {
30
- setFieldValue('toAddress', '')
31
+ setFieldValue('toAddress', '', { isTouched: true })
31
32
  setSelectedBookmark()
33
+ // If toAddress was auto-filled (e.g., when making cross-ecosystem bridging and compatible destination wallet was connected)
34
+ // and not manually edited by the user, we need to hide "Send to Wallet".
35
+ const isToAddressDirty = isDirty('toAddress')
36
+ if (!isToAddressDirty) {
37
+ setSendToWallet(false)
38
+ }
32
39
  }
33
40
  }
34
41
 
@@ -58,6 +58,7 @@ export const BookmarksPage = () => {
58
58
  const handleBookmarkSelected = (bookmark: Bookmark) => {
59
59
  setFieldValue('toAddress', bookmark.address, {
60
60
  isTouched: true,
61
+ isDirty: true,
61
62
  })
62
63
  setSelectedBookmark(bookmark)
63
64
  setSendToWallet(true)
@@ -40,6 +40,7 @@ export const ConfirmAddressSheet = forwardRef<
40
40
  if (validatedBookmark) {
41
41
  setFieldValue('toAddress', validatedBookmark.address, {
42
42
  isTouched: true,
43
+ isDirty: true,
43
44
  })
44
45
  onConfirm?.(validatedBookmark)
45
46
  setSendToWallet(true)
@@ -47,6 +47,7 @@ export const ConnectedWalletsPage = () => {
47
47
  const handleWalletSelected = (account: Account) => {
48
48
  setFieldValue('toAddress', account.address!, {
49
49
  isTouched: true,
50
+ isDirty: true,
50
51
  })
51
52
  setSelectedBookmark({
52
53
  name: account.connector?.name,
@@ -60,6 +60,7 @@ export const RecentWalletsPage = () => {
60
60
  addRecentWallet(recentWallet)
61
61
  setFieldValue('toAddress', recentWallet.address, {
62
62
  isTouched: true,
63
+ isDirty: true,
63
64
  })
64
65
  setSelectedBookmark(recentWallet)
65
66
  setSendToWallet(true)
@@ -43,7 +43,10 @@ export const SendToConfiguredWalletPage = () => {
43
43
 
44
44
  const handleCuratedSelected = (toAddress: ToAddress) => {
45
45
  setSelectedBookmark(toAddress)
46
- setFieldValue('toAddress', toAddress.address, { isTouched: true })
46
+ setFieldValue('toAddress', toAddress.address, {
47
+ isTouched: true,
48
+ isDirty: true,
49
+ })
47
50
  navigateBack()
48
51
  }
49
52
 
@@ -102,6 +102,8 @@ export const createFormStore = (defaultValues?: DefaultValues) =>
102
102
  },
103
103
  isTouched: (fieldName: FormFieldNames) =>
104
104
  !!get().userValues[fieldName]?.isTouched,
105
+ isDirty: (fieldName: FormFieldNames) =>
106
+ !!get().userValues[fieldName]?.isDirty,
105
107
  setAsTouched: (fieldName: FormFieldNames) => {
106
108
  const userValues = {
107
109
  ...get().userValues,
@@ -70,6 +70,7 @@ export interface FormActions {
70
70
  setDefaultValues: (formValues: DefaultValues) => void
71
71
  setUserAndDefaultValues: (formValues: Partial<DefaultValues>) => void
72
72
  isTouched: (fieldName: FormFieldNames) => boolean
73
+ isDirty: (fieldName: FormFieldNames) => boolean
73
74
  setAsTouched: (fieldName: FormFieldNames) => void
74
75
  resetField: (fieldName: FormFieldNames, resetOptions?: ResetOptions) => void
75
76
  setFieldValue: (
@@ -18,6 +18,7 @@ export const useFieldActions = () => {
18
18
  (store) => ({
19
19
  getFieldValues: store.getFieldValues,
20
20
  isTouched: store.isTouched,
21
+ isDirty: store.isDirty,
21
22
  resetField: store.resetField,
22
23
  setAsTouched: store.setAsTouched,
23
24
  setDefaultValues: store.setDefaultValues,
@@ -1,7 +1,7 @@
1
1
  import { createContext, useContext, useRef } from 'react'
2
2
  import { shallow } from 'zustand/shallow'
3
3
  import { createWithEqualityFn } from 'zustand/traditional'
4
- import type { PersistStoreProps, PersistStoreProviderProps } from '../types.js'
4
+ import type { PersistStoreProviderProps } from '../types.js'
5
5
  import type { HeaderState, HeaderStore } from './types.js'
6
6
 
7
7
  export const HeaderStoreContext = createContext<HeaderStore | null>(null)