From 5180526154371c5378c38224592894933d1cc313 Mon Sep 17 00:00:00 2001
From: Eugene Chybisov <18644653+chybisov@users.noreply.github.com>
Date: Tue, 30 Jul 2024 12:23:04 +0200
Subject: [PATCH] feat: improve the display of estimated duration and fees
(#278)
---
.../ActiveTransactionItem.tsx | 2 +-
.../src/components/Card/CardIconButton.tsx | 5 +-
.../src/components/FeeBreakdownTooltip.tsx | 14 +--
.../widget/src/components/IconTypography.ts | 4 +-
.../RouteCard/RouteCardEssentials.tsx | 34 +++---
.../Step/DestinationWalletAddress.tsx | 6 +-
packages/widget/src/components/Step/Step.tsx | 2 +-
.../src/components/Step/StepProcess.style.tsx | 9 --
.../src/components/Step/StepProcess.tsx | 12 +-
.../widget/src/components/Step/StepTimer.tsx | 109 +++++++++++++-----
.../StepActions/StepActions.style.tsx | 8 --
.../src/components/TransactionDetails.tsx | 33 ++----
packages/widget/src/hooks/timer/useTimer.ts | 6 +-
packages/widget/src/i18n/en.json | 3 +-
.../TransactionDetailsPage.tsx | 11 +-
.../TransactionPage/TokenValueBottomSheet.tsx | 51 +++++---
.../pages/TransactionPage/TransactionPage.tsx | 37 ++++--
.../widget/src/pages/TransactionPage/utils.ts | 31 +++--
packages/widget/src/stores/routes/types.ts | 12 +-
packages/widget/src/types/events.ts | 9 +-
packages/widget/src/utils/converters.ts | 2 +-
packages/widget/src/utils/fees.ts | 24 ++++
22 files changed, 254 insertions(+), 170 deletions(-)
delete mode 100644 packages/widget/src/components/Step/StepProcess.style.tsx
diff --git a/packages/widget/src/components/ActiveTransactions/ActiveTransactionItem.tsx b/packages/widget/src/components/ActiveTransactions/ActiveTransactionItem.tsx
index ac9811c92..6f0d73e73 100644
--- a/packages/widget/src/components/ActiveTransactions/ActiveTransactionItem.tsx
+++ b/packages/widget/src/components/ActiveTransactions/ActiveTransactionItem.tsx
@@ -41,7 +41,7 @@ export const ActiveTransactionItem: React.FC<{
return ;
default:
return (
-
+
);
diff --git a/packages/widget/src/components/Card/CardIconButton.tsx b/packages/widget/src/components/Card/CardIconButton.tsx
index 8dcb6d19d..5b08ccfd0 100644
--- a/packages/widget/src/components/Card/CardIconButton.tsx
+++ b/packages/widget/src/components/Card/CardIconButton.tsx
@@ -1,7 +1,10 @@
+import type { IconButtonProps, LinkProps } from '@mui/material';
import { IconButton as MuiIconButton, styled } from '@mui/material';
import { getContrastAlphaColor } from '../../utils/colors.js';
-export const CardIconButton = styled(MuiIconButton)(({ theme }) => {
+export const CardIconButton = styled(MuiIconButton)<
+ IconButtonProps & Pick
+>(({ theme }) => {
return {
padding: theme.spacing(0.5),
backgroundColor: getContrastAlphaColor(theme, 0.04),
diff --git a/packages/widget/src/components/FeeBreakdownTooltip.tsx b/packages/widget/src/components/FeeBreakdownTooltip.tsx
index 982f632be..d2bca8dce 100644
--- a/packages/widget/src/components/FeeBreakdownTooltip.tsx
+++ b/packages/widget/src/components/FeeBreakdownTooltip.tsx
@@ -1,42 +1,36 @@
-import type { Route } from '@lifi/sdk';
import { Box, Tooltip, Typography } from '@mui/material';
import type { TFunction } from 'i18next';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import { formatUnits } from 'viem';
import type { FeesBreakdown } from '../utils/fees.js';
-import { getFeeCostsBreakdown, getGasCostsBreakdown } from '../utils/fees.js';
export interface FeeBreakdownTooltipProps {
- route: Route;
gasCosts?: FeesBreakdown[];
feeCosts?: FeesBreakdown[];
children: ReactElement;
}
export const FeeBreakdownTooltip: React.FC = ({
- route,
gasCosts,
feeCosts,
children,
}) => {
const { t } = useTranslation();
- const _gasCosts = gasCosts ?? getGasCostsBreakdown(route);
- const _feeCosts = feeCosts ?? getFeeCostsBreakdown(route, false);
return (
- {_gasCosts.length ? (
+ {gasCosts?.length ? (
{t('main.fees.network')}
- {getFeeBreakdownTypography(_gasCosts, t)}
+ {getFeeBreakdownTypography(gasCosts, t)}
) : null}
- {_feeCosts.length ? (
+ {feeCosts?.length ? (
{t('main.fees.provider')}
- {getFeeBreakdownTypography(_feeCosts, t)}
+ {getFeeBreakdownTypography(feeCosts, t)}
) : null}
diff --git a/packages/widget/src/components/IconTypography.ts b/packages/widget/src/components/IconTypography.ts
index 9454e244d..3c7cda363 100644
--- a/packages/widget/src/components/IconTypography.ts
+++ b/packages/widget/src/components/IconTypography.ts
@@ -1,6 +1,6 @@
-import { Typography, alpha, styled } from '@mui/material';
+import { alpha, Box, styled } from '@mui/material';
-export const IconTypography = styled(Typography)(({ theme }) => ({
+export const IconTypography = styled(Box)(({ theme }) => ({
color:
theme.palette.mode === 'light'
? alpha(theme.palette.common.black, 0.32)
diff --git a/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx b/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx
index d09f6273b..e2120b27d 100644
--- a/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx
+++ b/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx
@@ -1,7 +1,7 @@
import { AccessTimeFilled, LocalGasStationRounded } from '@mui/icons-material';
import { Box, Tooltip, Typography } from '@mui/material';
import { useTranslation } from 'react-i18next';
-import { getFeeCostsBreakdown } from '../../utils/fees.js';
+import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees.js';
import { FeeBreakdownTooltip } from '../FeeBreakdownTooltip.js';
import { IconTypography } from '../IconTypography.js';
import { TokenRate } from '../TokenRate/TokenRate.js';
@@ -11,15 +11,15 @@ export const RouteCardEssentials: React.FC = ({
route,
}) => {
const { t, i18n } = useTranslation();
- const executionTimeMinutes = Math.ceil(
- route.steps
- .map((step) => step.estimate.executionDuration)
- .reduce((duration, x) => duration + x, 0) / 60,
+ const executionTimeSeconds = Math.floor(
+ route.steps.reduce(
+ (duration, step) => duration + step.estimate.executionDuration,
+ 0,
+ ),
);
- const gasCostUSD = parseFloat(route.gasCostUSD || '0');
- const feeCosts = getFeeCostsBreakdown(route, false);
- const fees =
- gasCostUSD + feeCosts.reduce((sum, feeCost) => sum + feeCost.amountUSD, 0);
+ const executionTimeMinutes = Math.floor(executionTimeSeconds / 60);
+ const { gasCosts, feeCosts, combinedFeesUSD } =
+ getAccumulatedFeeCostsBreakdown(route);
return (
= ({
>
-
+
@@ -38,11 +38,12 @@ export const RouteCardEssentials: React.FC = ({
{t(`format.currency`, {
- value: fees,
+ value: combinedFeesUSD,
})}
@@ -55,12 +56,15 @@ export const RouteCardEssentials: React.FC = ({
- {executionTimeMinutes.toLocaleString(i18n.language, {
+ {(executionTimeSeconds < 60
+ ? executionTimeSeconds
+ : executionTimeMinutes
+ ).toLocaleString(i18n.language, {
style: 'unit',
- unit: 'minute',
+ unit: executionTimeSeconds < 60 ? 'second' : 'minute',
unitDisplay: 'narrow',
})}
diff --git a/packages/widget/src/components/Step/DestinationWalletAddress.tsx b/packages/widget/src/components/Step/DestinationWalletAddress.tsx
index 437cf3735..01d4bf799 100644
--- a/packages/widget/src/components/Step/DestinationWalletAddress.tsx
+++ b/packages/widget/src/components/Step/DestinationWalletAddress.tsx
@@ -2,8 +2,8 @@ import type { LiFiStepExtended } from '@lifi/sdk';
import { LinkRounded, Wallet } from '@mui/icons-material';
import { Box, Link, Typography } from '@mui/material';
import { useTranslation } from 'react-i18next';
+import { CardIconButton } from '../Card/CardIconButton.js';
import { CircularIcon } from './CircularProgress.style.js';
-import { LinkButton } from './StepProcess.style.js';
export const DestinationWalletAddress: React.FC<{
step: LiFiStepExtended;
@@ -38,7 +38,7 @@ export const DestinationWalletAddress: React.FC<{
address: toAddress,
})}
-
-
+
);
diff --git a/packages/widget/src/components/Step/Step.tsx b/packages/widget/src/components/Step/Step.tsx
index 8d3d5b673..1a47ede30 100644
--- a/packages/widget/src/components/Step/Step.tsx
+++ b/packages/widget/src/components/Step/Step.tsx
@@ -73,7 +73,7 @@ export const Step: React.FC<{
}}
>
{getCardTitle()}
-
+
diff --git a/packages/widget/src/components/Step/StepProcess.style.tsx b/packages/widget/src/components/Step/StepProcess.style.tsx
deleted file mode 100644
index 5c0c4d431..000000000
--- a/packages/widget/src/components/Step/StepProcess.style.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import type { IconButtonProps, LinkProps } from '@mui/material';
-import { styled } from '@mui/material';
-import { CardIconButton } from '../Card/CardIconButton.js';
-
-export const LinkButton = styled(CardIconButton)(
- ({ theme }) => ({
- padding: theme.spacing(0.5),
- }),
-);
diff --git a/packages/widget/src/components/Step/StepProcess.tsx b/packages/widget/src/components/Step/StepProcess.tsx
index 2f478cf87..5ba24648a 100644
--- a/packages/widget/src/components/Step/StepProcess.tsx
+++ b/packages/widget/src/components/Step/StepProcess.tsx
@@ -1,9 +1,9 @@
import type { LiFiStep, Process } from '@lifi/sdk';
-import { LinkRounded } from '@mui/icons-material';
+import { OpenInNewRounded } from '@mui/icons-material';
import { Box, Link, Typography } from '@mui/material';
import { useProcessMessage } from '../../hooks/useProcessMessage.js';
+import { CardIconButton } from '../Card/CardIconButton.js';
import { CircularProgress } from './CircularProgress.js';
-import { LinkButton } from './StepProcess.style.js';
export const StepProcess: React.FC<{
step: LiFiStep;
@@ -28,15 +28,15 @@ export const StepProcess: React.FC<{
{title}
{process.txLink ? (
-
-
-
+
+
) : null}
{message ? (
diff --git a/packages/widget/src/components/Step/StepTimer.tsx b/packages/widget/src/components/Step/StepTimer.tsx
index 8f40e6bd4..4562be851 100644
--- a/packages/widget/src/components/Step/StepTimer.tsx
+++ b/packages/widget/src/components/Step/StepTimer.tsx
@@ -1,11 +1,22 @@
import type { LiFiStepExtended } from '@lifi/sdk';
+import { AccessTimeFilled } from '@mui/icons-material';
+import { Box, Tooltip } from '@mui/material';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useTimer } from '../../hooks/timer/useTimer.js';
+import { IconTypography } from '../IconTypography.js';
+
+const getExecutionProcess = (step: LiFiStepExtended) =>
+ step.execution?.process.findLast(
+ (process) =>
+ process.type === 'SWAP' ||
+ process.type === 'CROSS_CHAIN' ||
+ process.type === 'RECEIVING_CHAIN',
+ );
const getExpiryTimestamp = (step: LiFiStepExtended) =>
new Date(
- (step.execution?.process[0]?.startedAt ?? Date.now()) +
+ (getExecutionProcess(step)?.startedAt ?? Date.now()) +
step.estimate.executionDuration * 1000,
);
@@ -15,8 +26,12 @@ export const StepTimer: React.FC<{
}> = ({ step, hideInProgress }) => {
const { t, i18n } = useTranslation();
const [isExpired, setExpired] = useState(false);
- const [isExecutionStarted, setExecutionStarted] = useState(!!step.execution);
- const [expiryTimestamp] = useState(() => getExpiryTimestamp(step));
+ const [isExecutionStarted, setExecutionStarted] = useState(
+ () => !!getExecutionProcess(step),
+ );
+ const [expiryTimestamp, setExpiryTimestamp] = useState(() =>
+ getExpiryTimestamp(step),
+ );
const { seconds, minutes, isRunning, pause, resume, restart } = useTimer({
autoStart: false,
expiryTimestamp,
@@ -24,41 +39,59 @@ export const StepTimer: React.FC<{
});
useEffect(() => {
- if (isExpired || !step.execution) {
+ const executionProcess = getExecutionProcess(step);
+ if (!executionProcess) {
+ return;
+ }
+ const shouldRestart = executionProcess.status === 'FAILED';
+ const shouldPause = executionProcess.status === 'ACTION_REQUIRED';
+ const shouldStart =
+ executionProcess.status === 'STARTED' ||
+ executionProcess.status === 'PENDING';
+ const shouldResume = executionProcess.status === 'PENDING';
+ if (isExecutionStarted && shouldRestart) {
+ setExecutionStarted(false);
+ setExpired(false);
return;
}
- if (!isExecutionStarted) {
+ if (isExecutionStarted && isExpired) {
+ return;
+ }
+ if (!isExecutionStarted && shouldStart) {
+ const expiryTimestamp = getExpiryTimestamp(step);
setExecutionStarted(true);
- restart(getExpiryTimestamp(step));
+ setExpired(false);
+ setExpiryTimestamp(expiryTimestamp);
+ restart(expiryTimestamp);
+ return;
}
- const shouldBePaused = step.execution.process.some(
- (process) =>
- process.status === 'ACTION_REQUIRED' || process.status === 'FAILED',
- );
- if (isRunning && shouldBePaused) {
+ if (isRunning && shouldPause) {
pause();
- } else if (!isRunning && !shouldBePaused) {
+ } else if (!isRunning && shouldResume) {
resume();
}
- }, [
- expiryTimestamp,
- isExecutionStarted,
- isExpired,
- isRunning,
- pause,
- restart,
- resume,
- step,
- ]);
+ }, [isExecutionStarted, isExpired, isRunning, pause, restart, resume, step]);
if (!isExecutionStarted) {
- return Math.ceil(step.estimate.executionDuration / 60).toLocaleString(
- i18n.language,
- {
- style: 'unit',
- unit: 'minute',
- unitDisplay: 'narrow',
- },
+ const showSeconds = step.estimate.executionDuration < 60;
+ const duration = showSeconds
+ ? Math.floor(step.estimate.executionDuration)
+ : Math.floor(step.estimate.executionDuration / 60);
+ return (
+
+
+
+
+
+
+ {duration.toLocaleString(i18n.language, {
+ style: 'unit',
+ unit: showSeconds ? 'second' : 'minute',
+ unitDisplay: 'narrow',
+ })}
+
+
+
);
}
@@ -72,7 +105,19 @@ export const StepTimer: React.FC<{
return null;
}
- return isTimerExpired
- ? t('main.inProgress')
- : `${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`;
+ return isTimerExpired ? (
+ t('main.inProgress')
+ ) : (
+
+
+
+
+
+ {`${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`}
+
+
+ );
};
diff --git a/packages/widget/src/components/StepActions/StepActions.style.tsx b/packages/widget/src/components/StepActions/StepActions.style.tsx
index b8979b927..ffcb5d71b 100644
--- a/packages/widget/src/components/StepActions/StepActions.style.tsx
+++ b/packages/widget/src/components/StepActions/StepActions.style.tsx
@@ -71,11 +71,3 @@ export const StepAvatar = styled(AvatarMasked)(({ theme }) => ({
color: theme.palette.text.primary,
backgroundColor: 'transparent',
}));
-
-export const IconTypography = styled(Typography)(({ theme }) => ({
- color:
- theme.palette.mode === 'light'
- ? alpha(theme.palette.common.black, 0.32)
- : alpha(theme.palette.common.white, 0.4),
- lineHeight: 0,
-}));
diff --git a/packages/widget/src/components/TransactionDetails.tsx b/packages/widget/src/components/TransactionDetails.tsx
index 1e86183a1..b98410b20 100644
--- a/packages/widget/src/components/TransactionDetails.tsx
+++ b/packages/widget/src/components/TransactionDetails.tsx
@@ -9,7 +9,7 @@ import { Box, Collapse, Tooltip, Typography } from '@mui/material';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { isRouteDone } from '../stores/routes/utils.js';
-import { getFeeCostsBreakdown, getGasCostsBreakdown } from '../utils/fees.js';
+import { getAccumulatedFeeCostsBreakdown } from '../utils/fees.js';
import {
convertToSubscriptFormat,
formatTokenAmount,
@@ -35,18 +35,8 @@ export const TransactionDetails: React.FC = ({
const toggleCard = () => {
setCardExpanded((cardExpanded) => !cardExpanded);
};
-
- const gasCosts = getGasCostsBreakdown(route);
- const feeCosts = getFeeCostsBreakdown(route, false);
- const gasCostUSD = gasCosts.reduce(
- (sum, gasCost) => sum + gasCost.amountUSD,
- 0,
- );
- const feeCostUSD = feeCosts.reduce(
- (sum, feeCost) => sum + feeCost.amountUSD,
- 0,
- );
- const fees = gasCostUSD + feeCostUSD;
+ const { gasCosts, feeCosts, gasCostUSD, feeCostUSD, combinedFeesUSD } =
+ getAccumulatedFeeCostsBreakdown(route);
const fromTokenAmount = formatTokenAmount(
BigInt(route.fromAmount),
@@ -80,11 +70,7 @@ export const TransactionDetails: React.FC = ({
-
+
= ({
color="text.primary"
fontWeight="600"
lineHeight={1.429}
+ data-value={combinedFeesUSD}
>
- {t(`format.currency`, { value: fees })}
+ {t(`format.currency`, { value: combinedFeesUSD })}
@@ -118,7 +105,7 @@ export const TransactionDetails: React.FC = ({
{t('main.fees.network')}
-
+
{t(`format.currency`, {
value: gasCostUSD,
@@ -129,11 +116,7 @@ export const TransactionDetails: React.FC = ({
{feeCosts.length ? (
{t('main.fees.provider')}
-
+
{t(`format.currency`, {
value: feeCostUSD,
diff --git a/packages/widget/src/hooks/timer/useTimer.ts b/packages/widget/src/hooks/timer/useTimer.ts
index 95eb48a73..05ba7dd4c 100644
--- a/packages/widget/src/hooks/timer/useTimer.ts
+++ b/packages/widget/src/hooks/timer/useTimer.ts
@@ -23,10 +23,12 @@ export function useTimer({
autoStart = true,
}: UseTimerProps) {
const [expiryTimestamp, setExpiryTimestamp] = useState(expiry);
- const [seconds, setSeconds] = useState(getSecondsFromExpiry(expiryTimestamp));
+ const [seconds, setSeconds] = useState(() =>
+ getSecondsFromExpiry(expiryTimestamp),
+ );
const [isRunning, setIsRunning] = useState(autoStart);
const [didStart, setDidStart] = useState(autoStart);
- const [delay, setDelay] = useState(
+ const [delay, setDelay] = useState(() =>
getDelayFromExpiryTimestamp(expiryTimestamp, DEFAULT_DELAY),
);
diff --git a/packages/widget/src/i18n/en.json b/packages/widget/src/i18n/en.json
index 8bf311d6b..43773ea6a 100644
--- a/packages/widget/src/i18n/en.json
+++ b/packages/widget/src/i18n/en.json
@@ -169,7 +169,7 @@
},
"tooltip": {
"deselectAll": "Deselect all",
- "estimatedTime": "Estimated execution time in minutes.",
+ "estimatedTime": "Estimated time to complete the swap or bridge transaction, excluding chain switching and token approval.",
"minReceived": "The estimated minimum amount may change until the swapping/bridging transaction is signed. For 2-step transfers, this applies until the second step transaction is signed.",
"notFound": {
"text": "We couldn't find this page.",
@@ -196,7 +196,6 @@
"provider": "Provider fee"
},
"from": "From",
- "gasCost": "Gas cost",
"inProgress": "in progress",
"maxSlippage": "Max. slippage",
"minReceived": "Min. received",
diff --git a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx
index 710569cf9..798ed233a 100644
--- a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx
+++ b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx
@@ -1,10 +1,11 @@
import type { FullStatusData } from '@lifi/sdk';
import { ContentCopyRounded } from '@mui/icons-material';
-import { Box, IconButton, Typography } from '@mui/material';
+import { Box, Typography } from '@mui/material';
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { Card } from '../../components/Card/Card.js';
+import { CardIconButton } from '../../components/Card/CardIconButton.js';
import { CardTitle } from '../../components/Card/CardTitle.js';
import { ContractComponent } from '../../components/ContractComponent/ContractComponent.js';
import { PageContainer } from '../../components/PageContainer.js';
@@ -124,10 +125,10 @@ export const TransactionDetailsPage: React.FC = () => {
}}
>
{t('main.supportId')}
-
-
-
-
+
+
+
+
= ({
const { t } = useTranslation();
const ref = useRef();
useSetContentHeight(ref);
+ const { gasCosts, feeCosts, gasCostUSD, feeCostUSD } =
+ getAccumulatedFeeCostsBreakdown(route);
+ const fromAmountUSD = parseFloat(route.fromAmountUSD);
+ const toAmountUSD = parseFloat(route.toAmountUSD);
return (
@@ -62,11 +68,23 @@ const TokenValueBottomSheetContent: React.FC = ({
- {t('main.gasCost')}
-
- {t('format.currency', { value: route.gasCostUSD })}
-
+ {t('main.fees.network')}
+
+
+ {t('format.currency', { value: gasCostUSD })}
+
+
+ {feeCostUSD ? (
+
+ {t('main.fees.provider')}
+
+
+ {t('format.currency', { value: feeCostUSD })}
+
+
+
+ ) : null}
{t('main.receiving')}
@@ -75,7 +93,15 @@ const TokenValueBottomSheetContent: React.FC = ({
{t('main.valueLoss')}
- {calcValueLoss(route)}
+
+ {calculateValueLossPercentage(
+ fromAmountUSD,
+ toAmountUSD,
+ gasCostUSD,
+ feeCostUSD,
+ )}
+ %
+
);
};
-
-export const getTokenValueLossThreshold = (route?: Route) => {
- if (!route) {
- return false;
- }
- const fromAmountUSD = Number(route.fromAmountUSD || 0);
- const toAmountUSD = Number(route.toAmountUSD || 0);
- const gasCostUSD = Number(route.gasCostUSD || 0);
- if (!fromAmountUSD && !toAmountUSD) {
- return false;
- }
- return toAmountUSD / (fromAmountUSD + gasCostUSD) < 0.9;
-};
diff --git a/packages/widget/src/pages/TransactionPage/TransactionPage.tsx b/packages/widget/src/pages/TransactionPage/TransactionPage.tsx
index 81a6eb947..98bf11d6f 100644
--- a/packages/widget/src/pages/TransactionPage/TransactionPage.tsx
+++ b/packages/widget/src/pages/TransactionPage/TransactionPage.tsx
@@ -18,16 +18,17 @@ import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.j
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 type { ExchangeRateBottomSheetBase } from './ExchangeRateBottomSheet.js';
import { ExchangeRateBottomSheet } from './ExchangeRateBottomSheet.js';
import { RouteTracker } from './RouteTracker.js';
import { StartTransactionButton } from './StartTransactionButton.js';
import { StatusBottomSheet } from './StatusBottomSheet.js';
+import { TokenValueBottomSheet } from './TokenValueBottomSheet.js';
import {
- TokenValueBottomSheet,
+ calculateValueLossPercentage,
getTokenValueLossThreshold,
-} from './TokenValueBottomSheet.js';
-import { calcValueLoss } from './utils.js';
+} from './utils.js';
export const TransactionPage: React.FC = () => {
const { t } = useTranslation();
@@ -99,15 +100,22 @@ export const TransactionPage: React.FC = () => {
return null;
}
- const tokenValueLossThresholdExceeded = getTokenValueLossThreshold(route);
-
const handleExecuteRoute = () => {
if (tokenValueBottomSheetRef.current?.isOpen()) {
+ const { gasCostUSD, feeCostUSD } = getAccumulatedFeeCostsBreakdown(route);
+ const fromAmountUSD = parseFloat(route.fromAmountUSD);
+ const toAmountUSD = parseFloat(route.toAmountUSD);
emitter.emit(WidgetEvent.RouteHighValueLoss, {
- fromAmountUsd: route.fromAmountUSD,
- gasCostUSD: route.gasCostUSD,
- toAmountUSD: route.toAmountUSD,
- valueLoss: calcValueLoss(route),
+ fromAmountUSD,
+ toAmountUSD,
+ gasCostUSD,
+ feeCostUSD,
+ valueLoss: calculateValueLossPercentage(
+ fromAmountUSD,
+ toAmountUSD,
+ gasCostUSD,
+ feeCostUSD,
+ ),
});
}
tokenValueBottomSheetRef.current?.close();
@@ -121,6 +129,15 @@ export const TransactionPage: React.FC = () => {
const handleStartClick = async () => {
if (status === RouteExecutionStatus.Idle) {
+ const { gasCostUSD, feeCostUSD } = getAccumulatedFeeCostsBreakdown(route);
+ const fromAmountUSD = parseFloat(route.fromAmountUSD);
+ const toAmountUSD = parseFloat(route.toAmountUSD);
+ const tokenValueLossThresholdExceeded = getTokenValueLossThreshold(
+ fromAmountUSD,
+ toAmountUSD,
+ gasCostUSD,
+ feeCostUSD,
+ );
if (tokenValueLossThresholdExceeded && subvariant !== 'custom') {
tokenValueBottomSheetRef.current?.open();
} else {
@@ -197,7 +214,7 @@ export const TransactionPage: React.FC = () => {
>
) : null}
{status ? : null}
- {tokenValueLossThresholdExceeded && subvariant !== 'custom' ? (
+ {subvariant !== 'custom' ? (
{
+ return parseFloat(
+ (
+ (toAmountUSD / (fromAmountUSD + gasCostUSD + feeCostUSD) - 1) *
+ 100
+ ).toFixed(2),
+ );
+};
-export const calcValueLoss = (route: Route) => {
- return `${(
- (Number(route.toAmountUSD || 0) /
- (Number(route.fromAmountUSD || 0) + Number(route.gasCostUSD || 0)) -
- 1) *
- 100
- ).toFixed(2)}%`;
+export const getTokenValueLossThreshold = (
+ fromAmountUSD: number,
+ toAmountUSD: number,
+ gasCostUSD: number,
+ feeCostUSD: number,
+) => {
+ if (!fromAmountUSD || !toAmountUSD) {
+ return false;
+ }
+ return toAmountUSD / (fromAmountUSD + gasCostUSD + feeCostUSD) < 0.9;
};
diff --git a/packages/widget/src/stores/routes/types.ts b/packages/widget/src/stores/routes/types.ts
index 386445092..7fc8f9b27 100644
--- a/packages/widget/src/stores/routes/types.ts
+++ b/packages/widget/src/stores/routes/types.ts
@@ -15,10 +15,10 @@ export interface RouteExecutionState {
}
export enum RouteExecutionStatus {
- Idle = 0,
- Pending = 1 << 0,
- Done = 1 << 1,
- Failed = 1 << 2,
- Partial = 1 << 3,
- Refunded = 1 << 4,
+ Idle = 1 << 0,
+ Pending = 1 << 1,
+ Done = 1 << 2,
+ Failed = 1 << 3,
+ Partial = 1 << 4,
+ Refunded = 1 << 5,
}
diff --git a/packages/widget/src/types/events.ts b/packages/widget/src/types/events.ts
index 6db5997d1..60f75e388 100644
--- a/packages/widget/src/types/events.ts
+++ b/packages/widget/src/types/events.ts
@@ -37,10 +37,11 @@ export interface ContactSupport {
}
export interface RouteHighValueLossUpdate {
- fromAmountUsd: string;
- gasCostUSD?: string;
- toAmountUSD: string;
- valueLoss: string;
+ fromAmountUSD: number;
+ toAmountUSD: number;
+ gasCostUSD?: number;
+ feeCostUSD?: number;
+ valueLoss: number;
}
export interface RouteExecutionUpdate {
diff --git a/packages/widget/src/utils/converters.ts b/packages/widget/src/utils/converters.ts
index 52f9842f6..4358fa263 100644
--- a/packages/widget/src/utils/converters.ts
+++ b/packages/widget/src/utils/converters.ts
@@ -167,7 +167,7 @@ export const buildRouteFromTxHistory = (
toAmountMin: receiving.amount ?? '',
toAmount: receiving.amount ?? '',
toAmountUSD: receiving.amountUSD ?? '',
- executionDuration: 30,
+ executionDuration: 0,
},
includedSteps: [
{
diff --git a/packages/widget/src/utils/fees.ts b/packages/widget/src/utils/fees.ts
index b4be7086d..5b968e194 100644
--- a/packages/widget/src/utils/fees.ts
+++ b/packages/widget/src/utils/fees.ts
@@ -7,6 +7,30 @@ export interface FeesBreakdown {
token: Token;
}
+export const getAccumulatedFeeCostsBreakdown = (
+ route: RouteExtended,
+ included: boolean = false,
+) => {
+ const gasCosts = getGasCostsBreakdown(route);
+ const feeCosts = getFeeCostsBreakdown(route, included);
+ const gasCostUSD = gasCosts.reduce(
+ (sum, gasCost) => sum + gasCost.amountUSD,
+ 0,
+ );
+ const feeCostUSD = feeCosts.reduce(
+ (sum, feeCost) => sum + feeCost.amountUSD,
+ 0,
+ );
+ const combinedFeesUSD = gasCostUSD + feeCostUSD;
+ return {
+ gasCosts,
+ feeCosts,
+ gasCostUSD,
+ feeCostUSD,
+ combinedFeesUSD,
+ };
+};
+
export const getGasCostsBreakdown = (route: RouteExtended): FeesBreakdown[] => {
return Array.from(
route.steps