Skip to content

Commit

Permalink
feat: add low activity wallet warning
Browse files Browse the repository at this point in the history
  • Loading branch information
chybisov committed Feb 1, 2025
1 parent 2d74453 commit fb69217
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 14 deletions.
39 changes: 39 additions & 0 deletions packages/widget/src/hooks/useAddressActivity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Address } from 'viem'
import { isAddress } from 'viem'
import { useTransactionCount } from 'wagmi'
import { useFieldValues } from '../stores/form/useFieldValues.js'

interface AddressActivity {
hasActivity: boolean
isLoading: boolean
isFetched: boolean
toAddress: string | undefined
}

export const useAddressActivity = (chainId?: number): AddressActivity => {
const [toAddress, toChainId] = useFieldValues('toAddress', 'toChain')

const destinationChainId = chainId ?? toChainId

const {
data: transactionCount,
isLoading,
isFetched,
error,
} = useTransactionCount({
address: toAddress as Address,
chainId: destinationChainId,
query: {
enabled: Boolean(toAddress && destinationChainId && isAddress(toAddress)),
refetchInterval: 300_000,
staleTime: 300_000,
},
})

return {
toAddress,
hasActivity: Boolean(transactionCount && transactionCount > 0),
isLoading,
isFetched: isFetched && !error,
}
}
5 changes: 4 additions & 1 deletion packages/widget/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@
"warning": {
"message": {
"accountNotDeployedMessage": "Smart contract account is not deployed on the destination chain. Sending funds to a non-existent contract would result in permanent loss.",
"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.",
"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.",
"deleteActiveTransactions": "Active transactions are only stored locally and can't be recovered if you delete them.",
"deleteTransactionHistory": "Transaction history is only stored locally and can't be recovered if you delete it.",
"fundsLossPrevention": "Always ensure smart contract accounts are properly set up on the destination chain and avoid direct transfers to exchanges to prevent fund loss.",
Expand All @@ -135,7 +137,8 @@
"highValueLoss": "High value loss",
"insufficientGas": "Insufficient gas",
"rateChanged": "Rate changed",
"resetSettings": "Reset settings?"
"resetSettings": "Reset settings?",
"lowAddressActivity": "Low Activity Address"
}
},
"error": {
Expand Down
24 changes: 12 additions & 12 deletions packages/widget/src/pages/MainPage/ReviewButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useRoutes } from '../../hooks/useRoutes.js'
import { useToAddressRequirements } from '../../hooks/useToAddressRequirements.js'
import { useWidgetEvents } from '../../hooks/useWidgetEvents.js'
import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js'
import { useFieldValues } from '../../stores/form/useFieldValues.js'
import { useSplitSubvariantStore } from '../../stores/settings/useSplitSubvariantStore.js'
import { WidgetEvent } from '../../types/events.js'
import { navigationRoutes } from '../../utils/navigationRoutes.js'
Expand All @@ -16,24 +15,25 @@ export const ReviewButton: React.FC = () => {
const emitter = useWidgetEvents()
const { subvariant, subvariantOptions } = useWidgetConfig()
const splitState = useSplitSubvariantStore((state) => state.state)
const [toAddress] = useFieldValues('toAddress')
const { requiredToAddress, accountNotDeployedAtDestination } =
const { toAddress, requiredToAddress, accountNotDeployedAtDestination } =
useToAddressRequirements()
const { routes, setReviewableRoute } = useRoutes()

const currentRoute = routes?.[0]

const handleClick = async () => {
if (currentRoute) {
setReviewableRoute(currentRoute)
navigate(navigationRoutes.transactionExecution, {
state: { routeId: currentRoute.id },
})
emitter.emit(WidgetEvent.RouteSelected, {
route: currentRoute,
routes: routes!,
})
if (!currentRoute) {
return
}

setReviewableRoute(currentRoute)
navigate(navigationRoutes.transactionExecution, {
state: { routeId: currentRoute.id },
})
emitter.emit(WidgetEvent.RouteSelected, {
route: currentRoute,
routes: routes!,
})
}

const getButtonText = (): string => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Wallet, WarningRounded } from '@mui/icons-material'
import { Button, Typography } from '@mui/material'
import type { MutableRefObject } from 'react'
import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
import { BottomSheet } from '../../components/BottomSheet/BottomSheet.js'
import type { BottomSheetBase } from '../../components/BottomSheet/types.js'
import { AlertMessage } from '../../components/Messages/AlertMessage.js'
import { useChain } from '../../hooks/useChain.js'
import { useWidgetEvents } from '../../hooks/useWidgetEvents.js'
import { WidgetEvent } from '../../types/events.js'
import {
IconContainer,
SendToWalletButtonRow,
SendToWalletSheetContainer,
SheetAddressContainer,
} from '../SendToWallet/SendToWalletPage.style.js'

interface ConfirmToAddressSheetProps {
onContinue: () => void
toAddress: string
toChainId: number
}

interface ConfirmToAddressSheetContentProps extends ConfirmToAddressSheetProps {
onClose: () => void
}

export const ConfirmToAddressSheet = forwardRef<
BottomSheetBase,
ConfirmToAddressSheetProps
>((props, ref) => {
const handleClose = () => {
;(ref as MutableRefObject<BottomSheetBase>).current?.close()
}

return (
<BottomSheet ref={ref}>
<ConfirmToAddressSheetContent {...props} onClose={handleClose} />
</BottomSheet>
)
})

const ConfirmToAddressSheetContent: React.FC<
ConfirmToAddressSheetContentProps
> = ({ onContinue, onClose, toAddress, toChainId }) => {
const { t } = useTranslation()
const { chain } = useChain(toChainId)
const emitter = useWidgetEvents()

const handleContinue = () => {
emitter.emit(WidgetEvent.LowAddressActivityConfirmed, {
address: toAddress,
chainId: toChainId,
})
onClose()
onContinue()
}

return (
<SendToWalletSheetContainer>
<IconContainer>
<Wallet sx={{ fontSize: 40 }} />
</IconContainer>
<Typography variant="h6" sx={{ textAlign: 'center', mb: 2 }}>
{t('warning.title.lowAddressActivity')}
</Typography>
<SheetAddressContainer>
<Typography>{toAddress}</Typography>
</SheetAddressContainer>
<AlertMessage
severity="warning"
title={
<Typography variant="body2" sx={{ color: 'text.primary' }}>
{t('warning.message.lowAddressActivity', {
chainName: chain?.name,
})}
</Typography>
}
icon={<WarningRounded />}
multiline
/>
<SendToWalletButtonRow>
<Button variant="text" onClick={onClose} fullWidth>
{t('button.cancel')}
</Button>
<Button variant="contained" onClick={handleContinue} fullWidth>
{t('button.continue')}
</Button>
</SendToWalletButtonRow>
</SendToWalletSheetContainer>
)
}
28 changes: 27 additions & 1 deletion packages/widget/src/pages/TransactionPage/TransactionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { GasMessage } from '../../components/Messages/GasMessage.js'
import { PageContainer } from '../../components/PageContainer.js'
import { getStepList } from '../../components/Step/StepList.js'
import { TransactionDetails } from '../../components/TransactionDetails.js'
import { useAddressActivity } from '../../hooks/useAddressActivity.js'
import { useHeader } from '../../hooks/useHeader.js'
import { useNavigateBack } from '../../hooks/useNavigateBack.js'
import { useRouteExecution } from '../../hooks/useRouteExecution.js'
Expand All @@ -19,6 +20,7 @@ import { useFieldActions } from '../../stores/form/useFieldActions.js'
import { RouteExecutionStatus } from '../../stores/routes/types.js'
import { WidgetEvent } from '../../types/events.js'
import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees.js'
import { ConfirmToAddressSheet } from './ConfirmToAddressSheet.js'
import type { ExchangeRateBottomSheetBase } from './ExchangeRateBottomSheet.js'
import { ExchangeRateBottomSheet } from './ExchangeRateBottomSheet.js'
import { RouteTracker } from './RouteTracker.js'
Expand All @@ -44,6 +46,7 @@ export const TransactionPage: React.FC = () => {

const tokenValueBottomSheetRef = useRef<BottomSheetBase>(null)
const exchangeRateBottomSheetRef = useRef<ExchangeRateBottomSheetBase>(null)
const confirmToAddressSheetRef = useRef<BottomSheetBase>(null)

const onAcceptExchangeRateUpdate = (
resolver: (value: boolean) => void,
Expand All @@ -58,6 +61,13 @@ export const TransactionPage: React.FC = () => {
onAcceptExchangeRateUpdate,
})

const {
toAddress,
hasActivity,
isLoading: isLoadingAddressActivity,
isFetched: isActivityAddressFetched,
} = useAddressActivity(route?.toChainId)

const getHeaderTitle = () => {
if (subvariant === 'custom') {
return t(`header.${subvariantOptions?.custom ?? 'checkout'}`)
Expand Down Expand Up @@ -127,6 +137,16 @@ export const TransactionPage: React.FC = () => {

const handleStartClick = async () => {
if (status === RouteExecutionStatus.Idle) {
if (
toAddress &&
!hasActivity &&
!isLoadingAddressActivity &&
isActivityAddressFetched
) {
confirmToAddressSheetRef.current?.open()
return
}

const { gasCostUSD, feeCostUSD } = getAccumulatedFeeCostsBreakdown(route)
const fromAmountUSD = Number.parseFloat(route.fromAmountUSD)
const toAmountUSD = Number.parseFloat(route.toAmountUSD)
Expand Down Expand Up @@ -198,7 +218,7 @@ export const TransactionPage: React.FC = () => {
text={getButtonText()}
onClick={handleStartClick}
route={route}
loading={routeRefreshing}
loading={routeRefreshing || isLoadingAddressActivity}
/>
{status === RouteExecutionStatus.Failed ? (
<Tooltip
Expand Down Expand Up @@ -228,6 +248,12 @@ export const TransactionPage: React.FC = () => {
/>
) : null}
<ExchangeRateBottomSheet ref={exchangeRateBottomSheetRef} />
<ConfirmToAddressSheet
ref={confirmToAddressSheetRef}
onContinue={handleExecuteRoute}
toAddress={toAddress!}
toChainId={route.toChainId!}
/>
</PageContainer>
)
}
5 changes: 5 additions & 0 deletions packages/widget/src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export enum WidgetEvent {
FormFieldChanged = 'formFieldChanged',
SettingUpdated = 'settingUpdated',
TokenSearch = 'tokenSearch',
LowAddressActivityConfirmed = 'lowAddressActivityConfirmed',
}

export type WidgetEvents = {
Expand All @@ -50,6 +51,10 @@ export type WidgetEvents = {
pageEntered: NavigationRouteType
settingUpdated: SettingUpdated
tokenSearch: TokenSearch
[WidgetEvent.LowAddressActivityConfirmed]: {
address: string
chainId: number
}
}

export type ContactSupport = {
Expand Down

0 comments on commit fb69217

Please sign in to comment.