diff --git a/.eslintignore b/.eslintignore
index 5ba9f5090f..e6a481dac4 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -20,3 +20,5 @@ src/locales/
*.md
*.log
*.lock
+
+src/components/transactions/Swap/backup/**/*.*
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000000..51945768ce
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,6 @@
+# Uncomment for CoW Preview Releases
+
+# @cowprotocol:registry=https://npm.pkg.github.com
+# always-auth=true
+# # registry=https://registry.npmjs.org/
+# //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
diff --git a/package.json b/package.json
index 228768e51b..38bd3686fd 100644
--- a/package.json
+++ b/package.json
@@ -35,9 +35,11 @@
"@aave/math-utils": "1.36.1",
"@aave/react": "0.6.1",
"@amplitude/analytics-browser": "^2.13.0",
+ "@cowprotocol/sdk-app-data": "4.1.6",
+ "@cowprotocol/cow-sdk": "7.1.1",
+ "@cowprotocol/sdk-flash-loans": "1.5.2",
+ "@cowprotocol/sdk-viem-adapter": "0.2.0",
"@bgd-labs/aave-address-book": "^4.36.0",
- "@cowprotocol/app-data": "^3.1.0",
- "@cowprotocol/cow-sdk": "6.3.3",
"@emotion/cache": "11.10.3",
"@emotion/react": "11.10.4",
"@emotion/server": "latest",
@@ -157,4 +159,4 @@
"budgetPercentIncreaseRed": 20,
"showDetails": true
}
-}
+}
\ No newline at end of file
diff --git a/pages/_app.page.tsx b/pages/_app.page.tsx
index d1882d0e8f..0ab60dbcc4 100644
--- a/pages/_app.page.tsx
+++ b/pages/_app.page.tsx
@@ -16,10 +16,10 @@ import { AddressBlocked } from 'src/components/AddressBlocked';
import { Meta } from 'src/components/Meta';
import { TransactionEventHandler } from 'src/components/TransactionEventHandler';
import { GasStationProvider } from 'src/components/transactions/GasStation/GasStationProvider';
-import { CowOrderToast } from 'src/components/transactions/Switch/cowprotocol/CowOrderToast';
+import { CowOrderToast } from 'src/components/transactions/Swap/modals/result/CowOrderToast';
import { AppDataProvider } from 'src/hooks/app-data-provider/useAppDataProvider';
-import { CowOrderToastProvider } from 'src/hooks/useCowOrderToast';
import { ModalContextProvider } from 'src/hooks/useModal';
+import { SwapOrdersTrackingProvider } from 'src/hooks/useSwapOrdersTracking';
import { Web3ContextProvider } from 'src/libs/web3-data-provider/Web3Provider';
import { useRootStore } from 'src/store/root';
import { SharedDependenciesProvider } from 'src/ui-config/SharedDependenciesProvider';
@@ -31,16 +31,22 @@ import createEmotionCache from '../src/createEmotionCache';
import { AppGlobalStyles } from '../src/layouts/AppGlobalStyles';
import { LanguageProvider } from '../src/libs/LanguageProvider';
-const SwitchModal = dynamic(() =>
- import('src/components/transactions/Switch/SwitchModal').then((module) => module.SwitchModal)
+const SwapModal = dynamic(() =>
+ import('src/components/transactions/Swap/modals/SwapModal').then((module) => module.SwapModal)
);
const CollateralSwapModal = dynamic(() =>
- import('src/components/transactions/Switch/CollateralSwap/CollateralSwapModal').then(
+ import('src/components/transactions/Swap/modals/CollateralSwapModal').then(
(module) => module.CollateralSwapModal
)
);
+const DebtSwapModal = dynamic(() =>
+ import('src/components/transactions/Swap/modals/DebtSwapModal').then(
+ (module) => module.DebtSwapModal
+ )
+);
+
const BridgeModal = dynamic(() =>
import('src/components/transactions/Bridge/BridgeModal').then((module) => module.BridgeModal)
);
@@ -53,16 +59,6 @@ const ClaimRewardsModal = dynamic(() =>
(module) => module.ClaimRewardsModal
)
);
-const CollateralChangeModal = dynamic(() =>
- import('src/components/transactions/CollateralChange/CollateralChangeModal').then(
- (module) => module.CollateralChangeModal
- )
-);
-const DebtSwitchModal = dynamic(() =>
- import('src/components/transactions/DebtSwitch/DebtSwitchModal').then(
- (module) => module.DebtSwitchModal
- )
-);
const EmodeModal = dynamic(() =>
import('src/components/transactions/Emode/EmodeModal').then((module) => module.EmodeModal)
);
@@ -160,7 +156,7 @@ export default function MyApp(props: MyAppProps) {
-
+
@@ -170,24 +166,25 @@ export default function MyApp(props: MyAppProps) {
-
-
-
-
-
+
+ {/* Swap Modals */}
+
+
+
+
-
+
diff --git a/src/components/MarketSwitcher.tsx b/src/components/MarketSwitcher.tsx
index 3dc1eb5844..01ace4f7a4 100644
--- a/src/components/MarketSwitcher.tsx
+++ b/src/components/MarketSwitcher.tsx
@@ -78,7 +78,13 @@ type MarketLogoProps = {
export const MarketLogo = ({ size, logo, testChainName, sx }: MarketLogoProps) => {
return (
-
+
{testChainName && (
diff --git a/src/components/StyledToggleButton.tsx b/src/components/StyledToggleButton.tsx
index 8786f83bfd..ec0128d8b6 100644
--- a/src/components/StyledToggleButton.tsx
+++ b/src/components/StyledToggleButton.tsx
@@ -37,18 +37,31 @@ const CustomTxModalToggleButton = styled(ToggleButton)(({ the
color: theme.palette.text.muted,
borderRadius: '4px',
+ // Selected (active) state
'&.Mui-selected, &.Mui-selected:hover': {
border: `1px solid ${theme.palette.other.standardInputLine}`,
backgroundColor: '#FFFFFF',
borderRadius: '4px !important',
- },
-
- '&.Mui-selected, &.Mui-disabled': {
+ color: theme.palette.background.header,
zIndex: 100,
height: '100%',
display: 'flex',
justifyContent: 'center',
+ },
+
+ // Disabled but NOT selected: keep readable text with slight fade
+ '&.Mui-disabled:not(.Mui-selected)': {
+ color: theme.palette.text.secondary,
+ opacity: 0.55,
+ },
+
+ // Disabled + selected: preserve the selected look
+ '&.Mui-disabled.Mui-selected': {
+ border: `1px solid ${theme.palette.other.standardInputLine}`,
+ backgroundColor: '#FFFFFF',
+ borderRadius: '4px !important',
color: theme.palette.background.header,
+ opacity: 1,
},
})) as typeof ToggleButton;
diff --git a/src/components/infoTooltips/EstimatedCostsForLimitSwap.tsx b/src/components/infoTooltips/EstimatedCostsForLimitSwap.tsx
new file mode 100644
index 0000000000..787e467ec8
--- /dev/null
+++ b/src/components/infoTooltips/EstimatedCostsForLimitSwap.tsx
@@ -0,0 +1,15 @@
+import { Trans } from '@lingui/macro';
+
+import { TextWithTooltip } from '../TextWithTooltip';
+
+export const EstimatedCostsForLimitSwapTooltip = () => {
+ return (
+ Estimated Costs & Fees}>
+
+ These are the estimated costs associated with your limit swap, including costs and fees.
+ Consider these costs when setting your order amounts to help optimize execution and maximize
+ your chances of filling the order.
+
+
+ );
+};
diff --git a/src/components/infoTooltips/ExecutionFeeTooltip.tsx b/src/components/infoTooltips/ExecutionFeeTooltip.tsx
new file mode 100644
index 0000000000..796e4d9dbd
--- /dev/null
+++ b/src/components/infoTooltips/ExecutionFeeTooltip.tsx
@@ -0,0 +1,11 @@
+import { Trans } from '@lingui/macro';
+
+import { TextWithTooltip } from '../TextWithTooltip';
+
+export const ExecutionFeeTooltip = () => {
+ return (
+ Execution fee}>
+ This is the fee for executing position changes, set by governance.
+
+ );
+};
diff --git a/src/components/transactions/CancelCowOrder/CancelAdapterOrderActions.tsx b/src/components/transactions/CancelCowOrder/CancelAdapterOrderActions.tsx
new file mode 100644
index 0000000000..66e610ac7b
--- /dev/null
+++ b/src/components/transactions/CancelCowOrder/CancelAdapterOrderActions.tsx
@@ -0,0 +1,119 @@
+import { OrderStatus, SupportedChainId } from '@cowprotocol/cow-sdk';
+import { Trans } from '@lingui/macro';
+import { useQueryClient } from '@tanstack/react-query';
+import { Interface } from 'ethers/lib/utils';
+import { useIsWrongNetwork } from 'src/hooks/useIsWrongNetwork';
+import { useModalContext } from 'src/hooks/useModal';
+import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
+import {
+ ActionName,
+ CowSwapSubset,
+ isCowSwapSubset,
+ SwapActionFields,
+ TransactionHistoryItem,
+} from 'src/modules/history/types';
+import { useRootStore } from 'src/store/root';
+import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
+import { updateCowOrderStatus } from 'src/utils/swapAdapterHistory';
+
+import { ADAPTER_FACTORY } from '../Swap/constants/cow.constants';
+import { TxActionsWrapper } from '../TxActionsWrapper';
+
+interface CancelAdapterOrderActionsProps {
+ cowOrder: TransactionHistoryItem<
+ | SwapActionFields[ActionName.DebtSwap]
+ | SwapActionFields[ActionName.RepayWithCollateral]
+ | SwapActionFields[ActionName.CollateralSwap]
+ >;
+ blocked: boolean;
+}
+
+// ABI for cancelInstance function
+const ADAPTER_ABI = ['function cancelInstance(address instance) external'];
+
+export const CancelAdapterOrderActions = ({
+ cowOrder,
+ blocked,
+}: CancelAdapterOrderActionsProps) => {
+ const { isWrongNetwork } = useIsWrongNetwork(cowOrder.chainId);
+ const { mainTxState, loadingTxns, setMainTxState, setTxError } = useModalContext();
+ const { sendTx } = useWeb3Context();
+ const queryClient = useQueryClient();
+ const account = useRootStore((state) => state.account);
+
+ const action = async () => {
+ try {
+ setMainTxState({ ...mainTxState, loading: true });
+
+ // Type guard to ensure we have a CowSwapSubset with adapter fields
+ if (!isCowSwapSubset(cowOrder)) {
+ throw new Error('Order is not a CoW swap order');
+ }
+
+ // At this point TypeScript knows cowOrder is CowSwapSubset, but we need to assert it has adapter fields
+ const cowSwapOrder = cowOrder as CowSwapSubset;
+
+ if (!cowSwapOrder.adapterInstanceAddress) {
+ throw new Error('Adapter instance address not found');
+ }
+
+ const adapterInterface = new Interface(ADAPTER_ABI);
+
+ const factoryAddress = ADAPTER_FACTORY[cowOrder.chainId as SupportedChainId];
+
+ if (!factoryAddress) {
+ throw new Error('Factory address not found for this chain');
+ }
+
+ const data = adapterInterface.encodeFunctionData('cancelInstance', [
+ cowSwapOrder.adapterInstanceAddress,
+ ]);
+
+ const txResponse = await sendTx({
+ to: factoryAddress,
+ data,
+ chainId: cowOrder.chainId,
+ });
+
+ await txResponse.wait(1);
+
+ // Update order status to cancelled in local storage
+ if (account && cowSwapOrder.orderId) {
+ updateCowOrderStatus(
+ cowOrder.chainId,
+ account,
+ cowSwapOrder.orderId,
+ OrderStatus.CANCELLED
+ );
+ }
+
+ queryClient.invalidateQueries({ queryKey: 'transactionHistory' });
+ setMainTxState({
+ ...mainTxState,
+ loading: false,
+ success: true,
+ txHash: txResponse.hash,
+ });
+ } catch (error) {
+ const parsedError = getErrorTextFromError(error, TxAction.MAIN_ACTION, false);
+ setTxError(parsedError);
+ setMainTxState({
+ txHash: undefined,
+ loading: false,
+ });
+ }
+ };
+
+ return (
+ Cancel order}
+ actionInProgressText={Cancelling order...}
+ blocked={blocked}
+ mainTxState={mainTxState}
+ requiresApproval={false}
+ preparingTransactions={loadingTxns}
+ />
+ );
+};
diff --git a/src/components/transactions/CancelCowOrder/CancelCowOrderActions.tsx b/src/components/transactions/CancelCowOrder/CancelCowOrderActions.tsx
index 1bf4897988..abc1a19c42 100644
--- a/src/components/transactions/CancelCowOrder/CancelCowOrderActions.tsx
+++ b/src/components/transactions/CancelCowOrder/CancelCowOrderActions.tsx
@@ -1,17 +1,21 @@
-import { OrderBookApi, OrderSigningUtils } from '@cowprotocol/cow-sdk';
+import { AdapterContext, OrderBookApi, OrderSigningUtils, OrderStatus } from '@cowprotocol/cow-sdk';
import { Trans } from '@lingui/macro';
import { useQueryClient } from '@tanstack/react-query';
import { useIsWrongNetwork } from 'src/hooks/useIsWrongNetwork';
import { useModalContext } from 'src/hooks/useModal';
-import { getEthersProvider } from 'src/libs/web3-data-provider/adapters/EthersAdapter';
-import { ActionFields, TransactionHistoryItem } from 'src/modules/history/types';
+import { ActionName, SwapActionFields, TransactionHistoryItem } from 'src/modules/history/types';
+import { useRootStore } from 'src/store/root';
import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
import { wagmiConfig } from 'src/ui-config/wagmiConfig';
+import { updateCowOrderStatus } from 'src/utils/swapAdapterHistory';
+import { getWalletClient } from 'wagmi/actions';
+import { COW_ENV, getCowAdapter } from '../Swap/helpers/cow';
import { TxActionsWrapper } from '../TxActionsWrapper';
+// TODO: check with cow if we can cancel adapters orders
interface CancelCowOrderActionsProps {
- cowOrder: TransactionHistoryItem;
+ cowOrder: TransactionHistoryItem;
blocked: boolean;
}
@@ -19,23 +23,36 @@ export const CancelCowOrderActions = ({ cowOrder, blocked }: CancelCowOrderActio
const { isWrongNetwork } = useIsWrongNetwork(cowOrder.chainId);
const { mainTxState, loadingTxns, setMainTxState, setTxError } = useModalContext();
const queryClient = useQueryClient();
+ const account = useRootStore((state) => state.account);
const action = async () => {
try {
setMainTxState({ ...mainTxState, loading: true });
- const provider = getEthersProvider(wagmiConfig, { chainId: cowOrder.chainId });
- const signer = (await provider).getSigner();
- const orderBookApi = new OrderBookApi({ chainId: cowOrder.chainId });
+
+ const adapter = await getCowAdapter(cowOrder.chainId);
+ AdapterContext.getInstance().setAdapter(adapter);
+ const orderBookApi = new OrderBookApi({ chainId: cowOrder.chainId, env: COW_ENV });
+ const walletClient = await getWalletClient(wagmiConfig, { chainId: cowOrder.chainId });
+
+ if (!walletClient || !walletClient.account) {
+ throw new Error('Wallet not connected for signing');
+ }
const { signature, signingScheme } = await OrderSigningUtils.signOrderCancellation(
cowOrder.id,
cowOrder.chainId,
- signer
+ walletClient
);
await orderBookApi.sendSignedOrderCancellations({
orderUids: [cowOrder.id],
signature,
signingScheme,
});
+
+ // Update order status to cancelled in local storage
+ if (account && cowOrder.id) {
+ updateCowOrderStatus(cowOrder.chainId, account, cowOrder.id, OrderStatus.CANCELLED);
+ }
+
queryClient.invalidateQueries({ queryKey: 'transactionHistory' });
setTimeout(() => {
setMainTxState({
diff --git a/src/components/transactions/CancelCowOrder/CancelCowOrderModal.tsx b/src/components/transactions/CancelCowOrder/CancelCowOrderModal.tsx
index e71769310e..55486b6c2f 100644
--- a/src/components/transactions/CancelCowOrder/CancelCowOrderModal.tsx
+++ b/src/components/transactions/CancelCowOrder/CancelCowOrderModal.tsx
@@ -1,13 +1,19 @@
import { BasicModal } from 'src/components/primitives/BasicModal';
import { ModalContextType, ModalType, useModalContext } from 'src/hooks/useModal';
-import { ActionFields, TransactionHistoryItem } from 'src/modules/history/types';
+import { ActionName, SwapActionFields, TransactionHistoryItem } from 'src/modules/history/types';
import { TxModalTitle } from '../FlowCommons/TxModalTitle';
import { CancelCowOrderModalContent } from './CancelCowOrderModalContent';
export const CancelCowOrderModal = () => {
const { type, close, args } = useModalContext() as ModalContextType<{
- cowOrder: TransactionHistoryItem;
+ cowOrder: TransactionHistoryItem<
+ | SwapActionFields[ActionName.Swap]
+ | SwapActionFields[ActionName.CollateralSwap]
+ | SwapActionFields[ActionName.DebtSwap]
+ | SwapActionFields[ActionName.RepayWithCollateral]
+ | SwapActionFields[ActionName.WithdrawAndSwap]
+ >;
}>;
return (
diff --git a/src/components/transactions/CancelCowOrder/CancelCowOrderModalContent.tsx b/src/components/transactions/CancelCowOrder/CancelCowOrderModalContent.tsx
index 3fd17ba106..f4447b068e 100644
--- a/src/components/transactions/CancelCowOrder/CancelCowOrderModalContent.tsx
+++ b/src/components/transactions/CancelCowOrder/CancelCowOrderModalContent.tsx
@@ -3,7 +3,12 @@ import { Typography } from '@mui/material';
import { useIsWrongNetwork } from 'src/hooks/useIsWrongNetwork';
import { useModalContext } from 'src/hooks/useModal';
import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
-import { ActionFields, TransactionHistoryItem } from 'src/modules/history/types';
+import {
+ ActionName,
+ isCowSwapSubset,
+ SwapActionFields,
+ TransactionHistoryItem,
+} from 'src/modules/history/types';
import { getNetworkConfig } from 'src/utils/marketsAndNetworksConfig';
import { formatUnits } from 'viem';
@@ -11,10 +16,17 @@ import { BaseSuccessView } from '../FlowCommons/BaseSuccess';
import { GasEstimationError } from '../FlowCommons/GasEstimationError';
import { DetailsNumberLine, DetailsTextLine, TxModalDetails } from '../FlowCommons/TxModalDetails';
import { ChangeNetworkWarning } from '../Warnings/ChangeNetworkWarning';
+import { CancelAdapterOrderActions } from './CancelAdapterOrderActions';
import { CancelCowOrderActions } from './CancelCowOrderActions';
interface CancelCowOrderModalContentProps {
- cowOrder: TransactionHistoryItem;
+ cowOrder: TransactionHistoryItem<
+ | SwapActionFields[ActionName.Swap]
+ | SwapActionFields[ActionName.CollateralSwap]
+ | SwapActionFields[ActionName.DebtSwap]
+ | SwapActionFields[ActionName.RepayWithCollateral]
+ | SwapActionFields[ActionName.WithdrawAndSwap]
+ >;
}
export const CancelCowOrderModalContent = ({ cowOrder }: CancelCowOrderModalContentProps) => {
@@ -26,8 +38,11 @@ export const CancelCowOrderModalContent = ({ cowOrder }: CancelCowOrderModalCont
const showNetworkWarning = isWrongNetwork && !readOnlyMode;
if (mainTxState.success) {
+ // Show explorer link if txHash exists (adapter cancellations have txHash)
+ const hasTxHash = !!mainTxState.txHash;
+
return (
-
+
Cancellation submited
@@ -39,6 +54,23 @@ export const CancelCowOrderModalContent = ({ cowOrder }: CancelCowOrderModalCont
<>
{showNetworkWarning && }
+
+ Cancel order
+
+
+ {isCowSwapSubset(cowOrder) && cowOrder.usedAdapter ? (
+
+ This will cancel the order via an on-chain transaction. Note that the order will not
+ be marked as cancelled in the CoW Protocol system, but will remain open and expire
+ naturally. Keep in mind that a solver may already have filled your order.
+
+ ) : (
+
+ This is an off-chain operation. Keep in mind that a solver may already have filled
+ your order.
+
+ )}
+
{txError && }
-
+ {isCowSwapSubset(cowOrder) && cowOrder.usedAdapter && cowOrder.adapterInstanceAddress ? (
+
+ }
+ blocked={false}
+ />
+ ) : (
+ }
+ blocked={false}
+ />
+ )}
>
);
};
diff --git a/src/components/transactions/CollateralChange/CollateralChangeActions.tsx b/src/components/transactions/CollateralChange/CollateralChangeActions.tsx
deleted file mode 100644
index ff819b4c54..0000000000
--- a/src/components/transactions/CollateralChange/CollateralChangeActions.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { ProtocolAction } from '@aave/contract-helpers';
-import { Trans } from '@lingui/macro';
-import { useTransactionHandler } from 'src/helpers/useTransactionHandler';
-import { ComputedReserveData } from 'src/hooks/app-data-provider/useAppDataProvider';
-import { useRootStore } from 'src/store/root';
-
-import { TxActionsWrapper } from '../TxActionsWrapper';
-
-export type CollateralChangeActionsProps = {
- poolReserve: ComputedReserveData;
- isWrongNetwork: boolean;
- usageAsCollateral: boolean;
- blocked: boolean;
- symbol: string;
-};
-
-export const CollateralChangeActions = ({
- poolReserve,
- isWrongNetwork,
- usageAsCollateral,
- blocked,
- symbol,
-}: CollateralChangeActionsProps) => {
- const setUsageAsCollateral = useRootStore((state) => state.setUsageAsCollateral);
-
- const { action, loadingTxns, mainTxState, requiresApproval } = useTransactionHandler({
- tryPermit: false,
- protocolAction: ProtocolAction.setUsageAsCollateral,
- eventTxInfo: {
- assetName: poolReserve.name,
- asset: poolReserve.underlyingAsset,
- previousState: (!usageAsCollateral).toString(),
- newState: usageAsCollateral.toString(),
- },
-
- handleGetTxns: async () => {
- return setUsageAsCollateral({
- reserve: poolReserve.underlyingAsset,
- usageAsCollateral,
- });
- },
- skip: blocked,
- });
-
- return (
- Enable {symbol} as collateral
- ) : (
- Disable {symbol} as collateral
- )
- }
- actionInProgressText={Pending...}
- handleAction={action}
- />
- );
-};
diff --git a/src/components/transactions/CollateralChange/CollateralChangeModal.tsx b/src/components/transactions/CollateralChange/CollateralChangeModal.tsx
deleted file mode 100644
index ed044938e8..0000000000
--- a/src/components/transactions/CollateralChange/CollateralChangeModal.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Trans } from '@lingui/macro';
-import React from 'react';
-import { UserAuthenticated } from 'src/components/UserAuthenticated';
-import { ModalContextType, ModalType, useModalContext } from 'src/hooks/useModal';
-
-import { BasicModal } from '../../primitives/BasicModal';
-import { ModalWrapper } from '../FlowCommons/ModalWrapper';
-import { CollateralChangeModalContent } from './CollateralChangeModalContent';
-
-export const CollateralChangeModal = () => {
- const { type, close, args } = useModalContext() as ModalContextType<{
- underlyingAsset: string;
- }>;
- return (
-
- Review tx} underlyingAsset={args.underlyingAsset}>
- {(params) => (
-
- {(user) => }
-
- )}
-
-
- );
-};
diff --git a/src/components/transactions/CollateralChange/CollateralChangeModalContent.tsx b/src/components/transactions/CollateralChange/CollateralChangeModalContent.tsx
deleted file mode 100644
index de11544c94..0000000000
--- a/src/components/transactions/CollateralChange/CollateralChangeModalContent.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-import { calculateHealthFactorFromBalancesBigUnits, valueToBigNumber } from '@aave/math-utils';
-import { Trans } from '@lingui/macro';
-import { Typography } from '@mui/material';
-import { useEffect, useState } from 'react';
-import { Warning } from 'src/components/primitives/Warning';
-import { ExtendedFormattedUser } from 'src/hooks/app-data-provider/useAppDataProvider';
-import { useAssetCaps } from 'src/hooks/useAssetCaps';
-import { useModalContext } from 'src/hooks/useModal';
-import { useZeroLTVBlockingWithdraw } from 'src/hooks/useZeroLTVBlockingWithdraw';
-
-import { GasEstimationError } from '../FlowCommons/GasEstimationError';
-import { ModalWrapperProps } from '../FlowCommons/ModalWrapper';
-import { TxSuccessView } from '../FlowCommons/Success';
-import { DetailsHFLine, DetailsNumberLine, TxModalDetails } from '../FlowCommons/TxModalDetails';
-import { IsolationModeWarning } from '../Warnings/IsolationModeWarning';
-import { CollateralChangeActions } from './CollateralChangeActions';
-
-export type CollateralChangeModalContentProps = {
- underlyingAsset: string;
-};
-
-export enum ErrorType {
- DO_NOT_HAVE_SUPPLIES_IN_THIS_CURRENCY,
- CAN_NOT_USE_THIS_CURRENCY_AS_COLLATERAL,
- CAN_NOT_SWITCH_USAGE_AS_COLLATERAL_MODE,
- ZERO_LTV_WITHDRAW_BLOCKED,
-}
-
-export const CollateralChangeModalContent = ({
- poolReserve,
- userReserve,
- isWrongNetwork,
- symbol,
- user,
-}: ModalWrapperProps & { user: ExtendedFormattedUser }) => {
- const { gasLimit, mainTxState: collateralChangeTxState, txError } = useModalContext();
- const { debtCeiling } = useAssetCaps();
-
- const [collateralEnabled, setCollateralEnabled] = useState(
- userReserve.usageAsCollateralEnabledOnUser
- );
-
- // Health factor calculations
- const usageAsCollateralModeAfterSwitch = !userReserve.usageAsCollateralEnabledOnUser;
- const currenttotalCollateralMarketReferenceCurrency = valueToBigNumber(
- user.totalCollateralMarketReferenceCurrency
- );
-
- // Messages
- const showEnableIsolationModeMsg = !poolReserve.isIsolated && usageAsCollateralModeAfterSwitch;
- const showDisableIsolationModeMsg = !poolReserve.isIsolated && !usageAsCollateralModeAfterSwitch;
- const showEnterIsolationModeMsg = poolReserve.isIsolated && usageAsCollateralModeAfterSwitch;
- const showExitIsolationModeMsg = poolReserve.isIsolated && !usageAsCollateralModeAfterSwitch;
-
- const totalCollateralAfterSwitchETH = currenttotalCollateralMarketReferenceCurrency[
- usageAsCollateralModeAfterSwitch ? 'plus' : 'minus'
- ](userReserve.underlyingBalanceMarketReferenceCurrency);
-
- const healthFactorAfterSwitch = calculateHealthFactorFromBalancesBigUnits({
- collateralBalanceMarketReferenceCurrency: totalCollateralAfterSwitchETH,
- borrowBalanceMarketReferenceCurrency: user.totalBorrowsMarketReferenceCurrency,
- currentLiquidationThreshold: user.currentLiquidationThreshold,
- });
-
- const assetsBlockingWithdraw = useZeroLTVBlockingWithdraw();
-
- // error handling
- let blockingError: ErrorType | undefined = undefined;
- if (assetsBlockingWithdraw.length > 0 && !assetsBlockingWithdraw.includes(poolReserve.symbol)) {
- blockingError = ErrorType.ZERO_LTV_WITHDRAW_BLOCKED;
- } else if (valueToBigNumber(userReserve.underlyingBalance).eq(0)) {
- blockingError = ErrorType.DO_NOT_HAVE_SUPPLIES_IN_THIS_CURRENCY;
- } else if (
- (!userReserve.usageAsCollateralEnabledOnUser &&
- poolReserve.reserveLiquidationThreshold === '0') ||
- poolReserve.reserveLiquidationThreshold === '0'
- ) {
- blockingError = ErrorType.CAN_NOT_USE_THIS_CURRENCY_AS_COLLATERAL;
- } else if (
- userReserve.usageAsCollateralEnabledOnUser &&
- user.totalBorrowsMarketReferenceCurrency !== '0' &&
- healthFactorAfterSwitch.lte('1')
- ) {
- blockingError = ErrorType.CAN_NOT_SWITCH_USAGE_AS_COLLATERAL_MODE;
- }
-
- // error render handling
- const BlockingError: React.FC = () => {
- switch (blockingError) {
- case ErrorType.DO_NOT_HAVE_SUPPLIES_IN_THIS_CURRENCY:
- return You do not have supplies in this currency;
- case ErrorType.CAN_NOT_USE_THIS_CURRENCY_AS_COLLATERAL:
- return You can not use this currency as collateral;
- case ErrorType.CAN_NOT_SWITCH_USAGE_AS_COLLATERAL_MODE:
- return (
-
- You can not switch usage as collateral mode for this currency, because it will cause
- collateral call
-
- );
- case ErrorType.ZERO_LTV_WITHDRAW_BLOCKED:
- return (
-
- Assets with zero LTV ({assetsBlockingWithdraw.join(', ')}) must be withdrawn or disabled
- as collateral to perform this action
-
- );
- default:
- return null;
- }
- };
-
- // Effect to handle changes in collateral mode after switch as polling is fetching reserve state different after successful tx
- useEffect(() => {
- if (collateralChangeTxState.success) {
- setCollateralEnabled(usageAsCollateralModeAfterSwitch);
- }
- }, [collateralChangeTxState.success, collateralEnabled]);
-
- if (collateralChangeTxState.success)
- return ;
-
- return (
- <>
- {showEnableIsolationModeMsg && (
-
-
- Enabling this asset as collateral increases your borrowing power and Health Factor.
- However, it can get liquidated if your health factor drops below 1.
-
-
- )}
-
- {showDisableIsolationModeMsg && (
-
-
- Disabling this asset as collateral affects your borrowing power and Health Factor.
-
-
- )}
-
- {showEnterIsolationModeMsg && }
-
- {showExitIsolationModeMsg && (
-
- You will exit isolation mode and other tokens can now be used as collateral
-
- )}
-
- {poolReserve.isIsolated && debtCeiling.determineWarningDisplay({ debtCeiling })}
-
-
- Supply balance}
- value={userReserve.underlyingBalance}
- />
-
-
-
- {blockingError !== undefined && (
-
-
-
- )}
-
- {txError && }
-
-
- >
- );
-};
diff --git a/src/components/transactions/DebtSwitch/DebtSwitchActions.tsx b/src/components/transactions/DebtSwitch/DebtSwitchActions.tsx
deleted file mode 100644
index d8a4cc8941..0000000000
--- a/src/components/transactions/DebtSwitch/DebtSwitchActions.tsx
+++ /dev/null
@@ -1,306 +0,0 @@
-import {
- ApproveDelegationType,
- gasLimitRecommendations,
- ProtocolAction,
-} from '@aave/contract-helpers';
-import { valueToBigNumber } from '@aave/math-utils';
-import { SignatureLike } from '@ethersproject/bytes';
-import { Trans } from '@lingui/macro';
-import { BoxProps } from '@mui/material';
-import { useQueryClient } from '@tanstack/react-query';
-import { parseUnits } from 'ethers/lib/utils';
-import { useCallback, useEffect, useState } from 'react';
-import { MOCK_SIGNED_HASH } from 'src/helpers/useTransactionHandler';
-import { ComputedReserveData } from 'src/hooks/app-data-provider/useAppDataProvider';
-import { calculateSignedAmount, SwapTransactionParams } from 'src/hooks/paraswap/common';
-import { useModalContext } from 'src/hooks/useModal';
-import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
-import { useRootStore } from 'src/store/root';
-import { ApprovalMethod } from 'src/store/walletSlice';
-import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
-import { queryKeysFactory } from 'src/ui-config/queries';
-import { useShallow } from 'zustand/shallow';
-
-import { TxActionsWrapper } from '../TxActionsWrapper';
-import { APPROVE_DELEGATION_GAS_LIMIT, checkRequiresApproval } from '../utils';
-
-interface DebtSwitchBaseProps extends BoxProps {
- amountToSwap: string;
- amountToReceive: string;
- poolReserve: ComputedReserveData;
- targetReserve: ComputedReserveData;
- isWrongNetwork: boolean;
- customGasPrice?: string;
- symbol?: string;
- blocked?: boolean;
- isMaxSelected: boolean;
- loading?: boolean;
- signatureParams?: SignedParams;
-}
-
-export interface DebtSwitchActionProps extends DebtSwitchBaseProps {
- augustus: string;
- txCalldata: string;
-}
-
-interface SignedParams {
- signature: SignatureLike;
- deadline: string;
- amount: string;
-}
-
-export const DebtSwitchActions = ({
- amountToSwap,
- amountToReceive,
- isWrongNetwork,
- sx,
- poolReserve,
- targetReserve,
- isMaxSelected,
- loading,
- blocked,
- buildTxFn,
-}: DebtSwitchBaseProps & { buildTxFn: () => Promise }) => {
- const [
- getCreditDelegationApprovedAmount,
- currentMarketData,
- generateApproveDelegation,
- estimateGasLimit,
- addTransaction,
- debtSwitch,
- walletApprovalMethodPreference,
- generateCreditDelegationSignatureRequest,
- ] = useRootStore(
- useShallow((state) => [
- state.getCreditDelegationApprovedAmount,
- state.currentMarketData,
- state.generateApproveDelegation,
- state.estimateGasLimit,
- state.addTransaction,
- state.debtSwitch,
- state.walletApprovalMethodPreference,
- state.generateCreditDelegationSignatureRequest,
- ])
- );
- const {
- approvalTxState,
- mainTxState,
- loadingTxns,
- setMainTxState,
- setTxError,
- setGasLimit,
- setLoadingTxns,
- setApprovalTxState,
- } = useModalContext();
- const { sendTx, signTxData } = useWeb3Context();
- const queryClient = useQueryClient();
- const [requiresApproval, setRequiresApproval] = useState(false);
- const [approvedAmount, setApprovedAmount] = useState();
- const [useSignature, setUseSignature] = useState(false);
- const [signatureParams, setSignatureParams] = useState();
-
- const approvalWithSignatureAvailable = currentMarketData.v3;
-
- useEffect(() => {
- const preferSignature = walletApprovalMethodPreference === ApprovalMethod.PERMIT;
- setUseSignature(preferSignature);
- }, [walletApprovalMethodPreference]);
-
- const approval = async () => {
- try {
- if (requiresApproval && approvedAmount) {
- const approveDelegationAmount = calculateSignedAmount(
- amountToReceive,
- targetReserve.decimals,
- 0.25
- );
- if (useSignature && approvalWithSignatureAvailable) {
- const deadline = Math.floor(Date.now() / 1000 + 3600).toString();
- const signatureRequest = await generateCreditDelegationSignatureRequest({
- underlyingAsset: targetReserve.variableDebtTokenAddress,
- deadline,
- amount: approveDelegationAmount,
- spender: currentMarketData.addresses.DEBT_SWITCH_ADAPTER ?? '',
- });
- const response = await signTxData(signatureRequest);
- setSignatureParams({ signature: response, deadline, amount: approveDelegationAmount });
- setApprovalTxState({
- txHash: MOCK_SIGNED_HASH,
- loading: false,
- success: true,
- });
- } else {
- let approveDelegationTxData = generateApproveDelegation({
- debtTokenAddress: targetReserve.variableDebtTokenAddress,
- delegatee: currentMarketData.addresses.DEBT_SWITCH_ADAPTER ?? '',
- amount: approveDelegationAmount,
- });
- setApprovalTxState({ ...approvalTxState, loading: true });
- approveDelegationTxData = await estimateGasLimit(approveDelegationTxData);
- const response = await sendTx(approveDelegationTxData);
- await response.wait(1);
- setApprovalTxState({
- txHash: response.hash,
- loading: false,
- success: true,
- });
- addTransaction(response.hash, {
- action: ProtocolAction.approval,
- txState: 'success',
- asset: targetReserve.variableDebtTokenAddress,
- amount: approveDelegationAmount,
- assetName: 'varDebt' + targetReserve.name,
- spender: currentMarketData.addresses.DEBT_SWITCH_ADAPTER,
- });
- setTxError(undefined);
- fetchApprovedAmount(true);
- }
- }
- } catch (error) {
- const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
- setTxError(parsedError);
- if (!approvalTxState.success) {
- setApprovalTxState({
- txHash: undefined,
- loading: false,
- });
- }
- }
- };
- const action = async () => {
- try {
- setMainTxState({ ...mainTxState, loading: true });
- const route = await buildTxFn();
- let debtSwitchTxData = debtSwitch({
- poolReserve,
- targetReserve,
- amountToReceive: parseUnits(amountToReceive, targetReserve.decimals).toString(),
- amountToSwap: parseUnits(amountToSwap, poolReserve.decimals).toString(),
- isMaxSelected,
- txCalldata: route.swapCallData,
- augustus: route.augustus,
- signatureParams,
- isWrongNetwork,
- });
- debtSwitchTxData = await estimateGasLimit(debtSwitchTxData);
- const response = await sendTx(debtSwitchTxData);
- await response.wait(1);
- setMainTxState({
- txHash: response.hash,
- loading: false,
- success: true,
- });
- addTransaction(response.hash, {
- action: 'debtSwitch',
- txState: 'success',
- previousState: `${route.outputAmount} variable ${poolReserve.symbol}`,
- newState: `${route.inputAmount} variable ${targetReserve.symbol}`,
- amountUsd: valueToBigNumber(parseUnits(amountToSwap, poolReserve.decimals).toString())
- .multipliedBy(poolReserve.priceInUSD)
- .toString(),
- outAmountUsd: valueToBigNumber(
- parseUnits(amountToReceive, targetReserve.decimals).toString()
- )
- .multipliedBy(targetReserve.priceInUSD)
- .toString(),
- });
-
- queryClient.invalidateQueries({ queryKey: queryKeysFactory.pool });
- queryClient.invalidateQueries({ queryKey: queryKeysFactory.gho });
- } catch (error) {
- const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
- setTxError(parsedError);
- setMainTxState({
- txHash: undefined,
- loading: false,
- });
- }
- };
-
- // callback to fetch approved credit delegation amount and determine execution path on dependency updates
- const fetchApprovedAmount = useCallback(
- async (forceApprovalCheck?: boolean) => {
- // Check approved amount on-chain on first load or if an action triggers a re-check such as an approveDelegation being confirmed
- let approval = approvedAmount;
- if (approval === undefined || forceApprovalCheck) {
- setLoadingTxns(true);
- approval = await getCreditDelegationApprovedAmount({
- debtTokenAddress: targetReserve.variableDebtTokenAddress,
- delegatee: currentMarketData.addresses.DEBT_SWITCH_ADAPTER ?? '',
- });
- setApprovedAmount(approval);
- } else {
- setRequiresApproval(false);
- setApprovalTxState({});
- }
-
- if (approval) {
- const fetchedRequiresApproval = checkRequiresApproval({
- approvedAmount: approval.amount,
- amount: amountToReceive,
- signedAmount: '0',
- });
- setRequiresApproval(fetchedRequiresApproval);
- if (fetchedRequiresApproval) setApprovalTxState({});
- }
-
- setLoadingTxns(false);
- },
- [
- approvedAmount,
- setLoadingTxns,
- getCreditDelegationApprovedAmount,
- targetReserve.variableDebtTokenAddress,
- currentMarketData.addresses.DEBT_SWITCH_ADAPTER,
- setApprovalTxState,
- amountToReceive,
- ]
- );
-
- // Run on first load and when the target reserve changes
- useEffect(() => {
- if (amountToSwap === '0') return;
-
- if (!approvedAmount) {
- fetchApprovedAmount();
- } else if (approvedAmount.debtTokenAddress !== targetReserve.variableDebtTokenAddress) {
- fetchApprovedAmount(true);
- }
- }, [amountToSwap, approvedAmount, fetchApprovedAmount, targetReserve.variableDebtTokenAddress]);
-
- // Update gas estimation
- useEffect(() => {
- let switchGasLimit = 0;
- switchGasLimit = Number(gasLimitRecommendations[ProtocolAction.borrow].recommended);
- if (requiresApproval && !approvalTxState.success) {
- switchGasLimit += Number(APPROVE_DELEGATION_GAS_LIMIT);
- }
- setGasLimit(switchGasLimit.toString());
- }, [requiresApproval, approvalTxState, setGasLimit]);
-
- return (
- approval()}
- requiresApproval={requiresApproval}
- actionText={Swap}
- actionInProgressText={Swapping}
- sx={sx}
- fetchingData={loading}
- errorParams={{
- loading: false,
- disabled: blocked || !approvalTxState?.success,
- content: Swap,
- handleClick: action,
- }}
- blocked={blocked}
- tryPermit={approvalWithSignatureAvailable}
- />
- );
-};
diff --git a/src/components/transactions/DebtSwitch/DebtSwitchModal.tsx b/src/components/transactions/DebtSwitch/DebtSwitchModal.tsx
deleted file mode 100644
index c1a44b58a5..0000000000
--- a/src/components/transactions/DebtSwitch/DebtSwitchModal.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Trans } from '@lingui/macro';
-import React from 'react';
-import { BasicModal } from 'src/components/primitives/BasicModal';
-import { UserAuthenticated } from 'src/components/UserAuthenticated';
-import { ModalContextType, ModalType, useModalContext } from 'src/hooks/useModal';
-
-import { ModalWrapper } from '../FlowCommons/ModalWrapper';
-import { DebtSwitchModalContent } from './DebtSwitchModalContent';
-
-export const DebtSwitchModal = () => {
- const { type, close, args } = useModalContext() as ModalContextType<{
- underlyingAsset: string;
- }>;
- return (
-
- Swap borrow position}
- underlyingAsset={args.underlyingAsset}
- hideTitleSymbol
- >
- {(params) => (
-
- {(user) => }
-
- )}
-
-
- );
-};
diff --git a/src/components/transactions/DebtSwitch/DebtSwitchModalContent.tsx b/src/components/transactions/DebtSwitch/DebtSwitchModalContent.tsx
deleted file mode 100644
index 6e657a903b..0000000000
--- a/src/components/transactions/DebtSwitch/DebtSwitchModalContent.tsx
+++ /dev/null
@@ -1,374 +0,0 @@
-import { valueToBigNumber } from '@aave/math-utils';
-import { MaxUint256 } from '@ethersproject/constants';
-import { ArrowDownIcon } from '@heroicons/react/outline';
-import { ArrowNarrowRightIcon } from '@heroicons/react/solid';
-import { Trans } from '@lingui/macro';
-import { Box, ListItemText, ListSubheader, Stack, SvgIcon, Typography } from '@mui/material';
-import { BigNumber } from 'bignumber.js';
-import React, { useRef, useState } from 'react';
-import { PriceImpactTooltip } from 'src/components/infoTooltips/PriceImpactTooltip';
-import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
-import { TokenIcon } from 'src/components/primitives/TokenIcon';
-import { Warning } from 'src/components/primitives/Warning';
-import { Asset, AssetInput } from 'src/components/transactions/AssetInput';
-import { TxModalDetails } from 'src/components/transactions/FlowCommons/TxModalDetails';
-import { maxInputAmountWithSlippage } from 'src/hooks/paraswap/common';
-import { useDebtSwitch } from 'src/hooks/paraswap/useDebtSwitch';
-import { useModalContext } from 'src/hooks/useModal';
-import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
-import { ListSlippageButton } from 'src/modules/dashboard/lists/SlippageList';
-import { useRootStore } from 'src/store/root';
-import { assetCanBeBorrowedByUser } from 'src/utils/getMaxAmountAvailableToBorrow';
-
-import {
- ComputedUserReserveData,
- ExtendedFormattedUser,
- useAppDataContext,
-} from '../../../hooks/app-data-provider/useAppDataProvider';
-import { ModalWrapperProps } from '../FlowCommons/ModalWrapper';
-import { TxSuccessView } from '../FlowCommons/Success';
-import { ParaswapErrorDisplay } from '../Warnings/ParaswapErrorDisplay';
-import { DebtSwitchActions } from './DebtSwitchActions';
-import { DebtSwitchModalDetails } from './DebtSwitchModalDetails';
-
-export type SupplyProps = {
- underlyingAsset: string;
-};
-
-export interface GhoRange {
- qualifiesForDiscount: boolean;
- userBorrowApyAfterMaxSwitch: number;
- ghoApyRange?: [number, number];
- userDiscountTokenBalance: number;
- inputAmount: number;
- targetAmount: number;
- userCurrentBorrowApy: number;
- ghoVariableBorrowApy: number;
- userGhoAvailableToBorrowAtDiscount: number;
- ghoBorrowAPYWithMaxDiscount: number;
- userCurrentBorrowBalance: number;
-}
-
-interface SwitchTargetAsset extends Asset {
- variableApy: string;
-}
-
-enum ErrorType {
- INSUFFICIENT_LIQUIDITY,
-}
-
-export const DebtSwitchModalContent = ({
- poolReserve,
- userReserve,
- isWrongNetwork,
- user,
-}: ModalWrapperProps & { user: ExtendedFormattedUser }) => {
- const { reserves } = useAppDataContext();
- const currentChainId = useRootStore((store) => store.currentChainId);
- const currentNetworkConfig = useRootStore((store) => store.currentNetworkConfig);
- const { currentAccount } = useWeb3Context();
- const { gasLimit, mainTxState, txError, setTxError } = useModalContext();
-
- let switchTargets = reserves
- .filter(
- (r) =>
- r.underlyingAsset !== poolReserve.underlyingAsset &&
- r.availableLiquidity !== '0' &&
- assetCanBeBorrowedByUser(r, user)
- )
- .map((reserve) => ({
- address: reserve.underlyingAsset,
- symbol: reserve.symbol,
- iconSymbol: reserve.iconSymbol,
- variableApy: reserve.variableBorrowAPY,
- priceInUsd: reserve.priceInUSD,
- decimals: reserve.decimals,
- }));
-
- switchTargets = [
- ...switchTargets.filter((r) => r.symbol === 'GHO'),
- ...switchTargets.filter((r) => r.symbol !== 'GHO'),
- ];
-
- // states
- const [_amount, setAmount] = useState('');
- const amountRef = useRef('');
- const [targetReserve, setTargetReserve] = useState(switchTargets[0]);
- const [maxSlippage, setMaxSlippage] = useState('0.1');
-
- const switchTarget = user.userReservesData.find(
- (r) => r.underlyingAsset === targetReserve.address
- ) as ComputedUserReserveData;
-
- const maxAmountToSwitch = userReserve.variableBorrows;
-
- const isMaxSelected = _amount === '-1';
- const amount = isMaxSelected ? maxAmountToSwitch : _amount;
-
- const {
- inputAmount,
- outputAmount,
- outputAmountUSD,
- error,
- loading: routeLoading,
- buildTxFn,
- } = useDebtSwitch({
- chainId: currentNetworkConfig.underlyingChainId || currentChainId,
- userAddress: currentAccount,
- swapOut: { ...poolReserve, amount: amountRef.current },
- swapIn: { ...switchTarget.reserve, amount: '0' },
- max: isMaxSelected,
- skip: mainTxState.loading || false,
- maxSlippage: Number(maxSlippage),
- });
-
- const loadingSkeleton = routeLoading && outputAmountUSD === '0';
-
- const handleChange = (value: string) => {
- const maxSelected = value === '-1';
- amountRef.current = maxSelected ? maxAmountToSwitch : value;
- setAmount(value);
- setTxError(undefined);
- };
-
- let availableBorrowCap = valueToBigNumber(MaxUint256.toString());
- let availableLiquidity: string | number = '0';
- availableBorrowCap =
- switchTarget.reserve.borrowCap === '0'
- ? valueToBigNumber(MaxUint256.toString())
- : valueToBigNumber(Number(switchTarget.reserve.borrowCap)).minus(
- valueToBigNumber(switchTarget.reserve.totalDebt)
- );
- availableLiquidity = switchTarget.reserve.formattedAvailableLiquidity;
-
- const availableLiquidityOfTargetReserve = BigNumber.max(
- BigNumber.min(availableLiquidity, availableBorrowCap),
- 0
- );
-
- const poolReserveAmountUSD = Number(amount) * Number(poolReserve.priceInUSD);
- const targetReserveAmountUSD = Number(inputAmount) * Number(targetReserve.priceInUsd);
-
- const priceImpactDifference: number = targetReserveAmountUSD - poolReserveAmountUSD;
- const insufficientCollateral =
- Number(user.availableBorrowsUSD) === 0 ||
- priceImpactDifference > Number(user.availableBorrowsUSD);
-
- let blockingError: ErrorType | undefined = undefined;
- if (BigNumber(inputAmount).gt(availableLiquidityOfTargetReserve)) {
- blockingError = ErrorType.INSUFFICIENT_LIQUIDITY;
- }
-
- const BlockingError: React.FC = () => {
- switch (blockingError) {
- case ErrorType.INSUFFICIENT_LIQUIDITY:
- return (
-
- There is not enough liquidity for the target asset to perform the switch. Try lowering
- the amount.
-
- );
- default:
- return null;
- }
- };
-
- const maxAmountToReceiveWithSlippage = maxInputAmountWithSlippage(
- inputAmount,
- maxSlippage,
- targetReserve.decimals || 18
- );
-
- if (mainTxState.success)
- return (
-
-
- You've successfully swapped borrow position.
-
-
-
-
- {poolReserve.symbol}
-
-
-
-
-
- {switchTarget.reserve.symbol}
-
-
- }
- />
- );
-
- return (
- <>
- Borrowed asset amount}
- balanceText={
-
- Borrow balance
-
- }
- isMaxSelected={isMaxSelected}
- />
-
-
-
-
-
- {/** For debt switch, targetAmountUSD (input) > poolReserveAmountUSD (output) means that more is being borrowed to cover the current borrow balance as exactOut, so this should be treated as positive impact */}
-
-
-
- value={inputAmount}
- onSelect={setTargetReserve}
- usdValue={targetReserveAmountUSD.toString()}
- symbol={targetReserve.symbol}
- assets={switchTargets}
- inputTitle={Swap to}
- balanceText={Supply balance}
- disableInput
- loading={loadingSkeleton}
- selectOptionHeader={}
- selectOption={(asset) => }
- />
- {error && !loadingSkeleton && (
-
- {error}
-
- )}
- {!error && blockingError !== undefined && (
-
-
-
- )}
-
- {
- setTxError(undefined);
- setMaxSlippage(newMaxSlippage);
- }}
- slippageTooltipHeader={
-
- Maximum amount received
-
-
-
-
-
-
-
- }
- />
- }
- >
-
-
-
- {txError && }
-
- {insufficientCollateral && (
-
-
-
- Insufficient collateral to cover new borrow position. Wallet must have borrowing power
- remaining to perform debt switch.
-
-
-
- )}
-
-
- >
- );
-};
-
-const SelectOptionListHeader = () => {
- return (
- ({ borderBottom: `1px solid ${theme.palette.divider}`, mt: -1 })}>
-
-
- Select an asset
-
-
- Borrow APY
-
-
-
- );
-};
-
-const SwitchTargetSelectOption = ({ asset }: { asset: SwitchTargetAsset }) => {
- return (
- <>
-
- {asset.symbol}
-
-
-
- Variable rate
-
-
- >
- );
-};
diff --git a/src/components/transactions/DebtSwitch/DebtSwitchModalDetails.tsx b/src/components/transactions/DebtSwitch/DebtSwitchModalDetails.tsx
deleted file mode 100644
index d0e537aa70..0000000000
--- a/src/components/transactions/DebtSwitch/DebtSwitchModalDetails.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-import { valueToBigNumber } from '@aave/math-utils';
-import { ArrowNarrowRightIcon } from '@heroicons/react/solid';
-import { Trans } from '@lingui/macro';
-import { Box, Skeleton, SvgIcon } from '@mui/material';
-import React from 'react';
-import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
-import { Row } from 'src/components/primitives/Row';
-import { TokenIcon } from 'src/components/primitives/TokenIcon';
-import { DetailsIncentivesLine } from 'src/components/transactions/FlowCommons/TxModalDetails';
-
-import { ComputedUserReserveData } from '../../../hooks/app-data-provider/useAppDataProvider';
-
-export type DebtSwitchModalDetailsProps = {
- switchSource: ComputedUserReserveData;
- switchTarget: ComputedUserReserveData;
- toAmount: string;
- fromAmount: string;
- loading: boolean;
- sourceBalance: string;
- sourceBorrowAPY: string;
- targetBorrowAPY: string;
-};
-const ArrowRightIcon = (
-
-
-
-);
-
-export const DebtSwitchModalDetails = ({
- switchSource,
- switchTarget,
- toAmount,
- fromAmount,
- loading,
- sourceBalance,
- sourceBorrowAPY,
- targetBorrowAPY,
-}: DebtSwitchModalDetailsProps) => {
- const sourceAmountAfterSwap = valueToBigNumber(sourceBalance).minus(valueToBigNumber(fromAmount));
-
- const targetAmountAfterSwap = valueToBigNumber(switchTarget.variableBorrows).plus(
- valueToBigNumber(toAmount)
- );
-
- const skeleton: JSX.Element = (
- <>
-
-
- >
- );
-
- return (
- <>
- Borrow apy} captionVariant="description" mb={4}>
-
- {loading ? (
-
- ) : (
- <>
-
- {ArrowRightIcon}
-
- >
- )}
-
-
-
-
-
- Borrow balance after switch}
- captionVariant="description"
- mb={4}
- align="flex-start"
- >
-
-
- {loading ? (
- skeleton
- ) : (
- <>
-
-
-
-
-
- >
- )}
-
-
-
- {loading ? (
- skeleton
- ) : (
- <>
-
-
-
-
-
- >
- )}
-
-
-
- >
- );
-};
diff --git a/src/components/transactions/FlowCommons/BaseSuccess.tsx b/src/components/transactions/FlowCommons/BaseSuccess.tsx
index 689b6f8d78..a045d8249b 100644
--- a/src/components/transactions/FlowCommons/BaseSuccess.tsx
+++ b/src/components/transactions/FlowCommons/BaseSuccess.tsx
@@ -95,7 +95,7 @@ export const BaseSuccessView = ({
onClick={handleClose}
variant="contained"
size="large"
- sx={{ minHeight: '50px', mb: '30px' }}
+ sx={{ minHeight: '50px', mb: '0px' }}
data-cy="closeButton"
>
Ok, Close
diff --git a/src/components/transactions/FlowCommons/PermitNonceInfo.tsx b/src/components/transactions/FlowCommons/PermitNonceInfo.tsx
new file mode 100644
index 0000000000..358680be90
--- /dev/null
+++ b/src/components/transactions/FlowCommons/PermitNonceInfo.tsx
@@ -0,0 +1,32 @@
+import { Trans } from '@lingui/macro';
+import { Tooltip, Typography } from '@mui/material';
+
+export const PermitNonceInfo = () => {
+ return (
+
+ There is an active order for the same sell asset (avoid nonce reuse).
+ >
+ }
+ placement="left"
+ arrow
+ >
+
+ Approval by signature not available
+
+
+ );
+};
diff --git a/src/components/transactions/FlowCommons/RightHelperText.tsx b/src/components/transactions/FlowCommons/RightHelperText.tsx
index e401d04e65..64518dab3a 100644
--- a/src/components/transactions/FlowCommons/RightHelperText.tsx
+++ b/src/components/transactions/FlowCommons/RightHelperText.tsx
@@ -9,9 +9,12 @@ import { useRootStore } from 'src/store/root';
import { ApprovalMethod } from 'src/store/walletSlice';
import { useShallow } from 'zustand/shallow';
+import { PermitNonceInfo } from './PermitNonceInfo';
+
export type RightHelperTextProps = {
approvalHash?: string;
tryPermit?: boolean;
+ permitInUse?: boolean;
};
const ExtLinkIcon = () => (
@@ -20,7 +23,11 @@ const ExtLinkIcon = () => (
);
-export const RightHelperText = ({ approvalHash, tryPermit }: RightHelperTextProps) => {
+export const RightHelperText = ({
+ approvalHash,
+ tryPermit,
+ permitInUse = false,
+}: RightHelperTextProps) => {
const [
account,
walletApprovalMethodPreference,
@@ -61,6 +68,13 @@ export const RightHelperText = ({ approvalHash, tryPermit }: RightHelperTextProp
/>
);
+ // When permit use is disabled by the flow, inform the user why
+ if (!tryPermit && permitInUse)
+ return (
+
+
+
+ );
if (approvalHash && !usingPermit)
return (
Promise }) => {
- const [paraswapRepayWithCollateral, currentMarketData] = useRootStore(
- useShallow((state) => [state.paraswapRepayWithCollateral, state.currentMarketData])
- );
-
- const { approval, action, loadingTxns, approvalTxState, mainTxState, requiresApproval } =
- useParaSwapTransactionHandler({
- protocolAction: ProtocolAction.repayCollateral,
- handleGetTxns: async (signature, deadline) => {
- const route = await buildTxFn();
- return paraswapRepayWithCollateral({
- repayAllDebt,
- repayAmount,
- rateMode,
- repayWithAmount,
- fromAssetData,
- poolReserve,
- isWrongNetwork,
- symbol,
- useFlashLoan,
- blocked,
- swapCallData: route.swapCallData,
- augustus: route.augustus,
- signature,
- deadline,
- signedAmount: calculateSignedAmount(repayWithAmount, fromAssetData.decimals),
- });
- },
- handleGetApprovalTxns: async () => {
- return paraswapRepayWithCollateral({
- repayAllDebt,
- repayAmount,
- rateMode,
- repayWithAmount,
- fromAssetData,
- poolReserve,
- isWrongNetwork,
- symbol,
- useFlashLoan: false,
- blocked,
- swapCallData: '0x',
- augustus: API_ETH_MOCK_ADDRESS,
- });
- },
- gasLimitRecommendation: gasLimitRecommendations[ProtocolAction.repayCollateral].limit,
- skip: loading || !repayAmount || parseFloat(repayAmount) === 0 || blocked,
- spender: currentMarketData.addresses.REPAY_WITH_COLLATERAL_ADAPTER ?? '',
- deps: [fromAssetData.symbol, repayWithAmount],
- });
-
- return (
-
- approval({
- amount: calculateSignedAmount(repayWithAmount, fromAssetData.decimals),
- underlyingAsset: fromAssetData.aTokenAddress,
- })
- }
- actionText={Repay {symbol}}
- actionInProgressText={Repaying {symbol}}
- fetchingData={loading}
- errorParams={{
- loading: false,
- disabled: blocked,
- content: Repay {symbol},
- handleClick: action,
- }}
- tryPermit
- />
- );
-};
diff --git a/src/components/transactions/Repay/CollateralRepayModalContent.tsx b/src/components/transactions/Repay/CollateralRepayModalContent.tsx
deleted file mode 100644
index 2fc80232e3..0000000000
--- a/src/components/transactions/Repay/CollateralRepayModalContent.tsx
+++ /dev/null
@@ -1,399 +0,0 @@
-import { InterestRate } from '@aave/contract-helpers';
-import { valueToBigNumber } from '@aave/math-utils';
-import { ArrowDownIcon } from '@heroicons/react/outline';
-import { Trans } from '@lingui/macro';
-import { Box, Stack, SvgIcon, Typography } from '@mui/material';
-import { BigNumber } from 'bignumber.js';
-import { useRef, useState } from 'react';
-import { PriceImpactTooltip } from 'src/components/infoTooltips/PriceImpactTooltip';
-import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
-import { TokenIcon } from 'src/components/primitives/TokenIcon';
-import {
- ComputedReserveData,
- ExtendedFormattedUser,
- useAppDataContext,
-} from 'src/hooks/app-data-provider/useAppDataProvider';
-import {
- maxInputAmountWithSlippage,
- minimumReceivedAfterSlippage,
- SwapVariant,
-} from 'src/hooks/paraswap/common';
-import { useCollateralRepaySwap } from 'src/hooks/paraswap/useCollateralRepaySwap';
-import { useModalContext } from 'src/hooks/useModal';
-import { useZeroLTVBlockingWithdraw } from 'src/hooks/useZeroLTVBlockingWithdraw';
-import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
-import { ListSlippageButton } from 'src/modules/dashboard/lists/SlippageList';
-import { useRootStore } from 'src/store/root';
-import { calculateHFAfterRepay } from 'src/utils/hfUtils';
-import { useShallow } from 'zustand/shallow';
-
-import { Asset, AssetInput } from '../AssetInput';
-import { ModalWrapperProps } from '../FlowCommons/ModalWrapper';
-import { TxSuccessView } from '../FlowCommons/Success';
-import {
- DetailsHFLine,
- DetailsNumberLineWithSub,
- TxModalDetails,
-} from '../FlowCommons/TxModalDetails';
-import { ErrorType, useFlashloan } from '../utils';
-import { ParaswapErrorDisplay } from '../Warnings/ParaswapErrorDisplay';
-import { CollateralRepayActions } from './CollateralRepayActions';
-
-export function CollateralRepayModalContent({
- poolReserve,
- symbol,
- debtType,
- userReserve,
- isWrongNetwork,
- user,
-}: ModalWrapperProps & { debtType: InterestRate; user: ExtendedFormattedUser }) {
- const { reserves, userReserves } = useAppDataContext();
- const { gasLimit, txError, mainTxState } = useModalContext();
- const [currentChainId, currentNetworkConfig] = useRootStore(
- useShallow((store) => [store.currentChainId, store.currentNetworkConfig])
- );
- const { currentAccount } = useWeb3Context();
-
- // List of tokens eligble to repay with, ordered by USD value
- const repayTokens = user.userReservesData
- .filter(
- (userReserve) =>
- userReserve.underlyingBalance !== '0' &&
- userReserve.underlyingAsset !== poolReserve.underlyingAsset &&
- userReserve.reserve.symbol !== 'stETH'
- )
- .map((userReserve) => ({
- address: userReserve.underlyingAsset,
- balance: userReserve.underlyingBalance,
- balanceUSD: userReserve.underlyingBalanceUSD,
- symbol: userReserve.reserve.symbol,
- iconSymbol: userReserve.reserve.iconSymbol,
- decimals: userReserve.reserve.decimals,
- }))
- .sort((a, b) => Number(b.balanceUSD) - Number(a.balanceUSD));
- const [tokenToRepayWith, setTokenToRepayWith] = useState(repayTokens[0]);
- const tokenToRepayWithBalance = tokenToRepayWith.balance || '0';
-
- // const [swapVariant, setSwapVariant] = useState('exactOut');
- const swapVariant: SwapVariant = 'exactOut';
-
- const [amount, setAmount] = useState('');
- const [maxSlippage, setMaxSlippage] = useState('0.5');
-
- const amountRef = useRef('');
-
- const collateralReserveData = reserves.find(
- (reserve) => reserve.underlyingAsset === tokenToRepayWith.address
- ) as ComputedReserveData;
-
- const debt = userReserve?.variableBorrows || '0';
-
- let safeAmountToRepayAll = valueToBigNumber(debt);
- // Add in the approximate interest accrued over the next 30 minutes
- safeAmountToRepayAll = safeAmountToRepayAll.plus(
- safeAmountToRepayAll.multipliedBy(poolReserve.variableBorrowAPY).dividedBy(360 * 24 * 2)
- );
-
- const isMaxSelected = amount === '-1';
- const repayAmount = isMaxSelected ? safeAmountToRepayAll.toString() : amount;
- const repayAmountUsdValue = valueToBigNumber(repayAmount)
- .multipliedBy(poolReserve.priceInUSD)
- .toString();
-
- // The slippage is factored into the collateral amount because when we swap for 'exactOut', positive slippage is applied on the collateral amount.
- const collateralAmountRequiredToCoverDebt = safeAmountToRepayAll
- .multipliedBy(poolReserve.priceInUSD)
- .multipliedBy(100 + Number(maxSlippage))
- .dividedBy(100)
- .dividedBy(collateralReserveData.priceInUSD);
-
- const swapIn = { ...collateralReserveData, amount: tokenToRepayWithBalance };
- const swapOut = { ...poolReserve, amount: amountRef.current };
- // if (swapVariant === 'exactIn') {
- // swapIn.amount = tokenToRepayWithBalance;
- // swapOut.amount = '0';
- // }
-
- const repayAllDebt =
- isMaxSelected &&
- valueToBigNumber(tokenToRepayWithBalance).gte(collateralAmountRequiredToCoverDebt);
-
- const {
- inputAmountUSD,
- inputAmount,
- outputAmount,
- outputAmountUSD,
- loading: routeLoading,
- error,
- buildTxFn,
- } = useCollateralRepaySwap({
- chainId: currentNetworkConfig.underlyingChainId || currentChainId,
- userAddress: currentAccount,
- swapVariant: swapVariant,
- swapIn,
- swapOut,
- max: repayAllDebt,
- skip: mainTxState.loading || false,
- maxSlippage: Number(maxSlippage),
- });
-
- const loadingSkeleton = routeLoading && inputAmountUSD === '0';
-
- const handleRepayAmountChange = (value: string) => {
- const maxSelected = value === '-1';
- amountRef.current = maxSelected ? safeAmountToRepayAll.toString(10) : value;
- setAmount(value);
-
- // if (
- // maxSelected &&
- // valueToBigNumber(tokenToRepayWithBalance).lt(collateralAmountRequiredToCoverDebt)
- // ) {
- // // The selected collateral amount is not enough to pay the full debt. We'll try to do a swap using the exact amount of collateral.
- // // The amount won't be known until we fetch the swap data, so we'll clear it out. Once the swap data is fetched, we'll set the amount.
- // amountRef.current = '';
- // setAmount('');
- // setSwapVariant('exactIn');
- // } else {
- // amountRef.current = maxSelected ? safeAmountToRepayAll.toString(10) : value;
- // setAmount(value);
- // setSwapVariant('exactOut');
- // }
- };
-
- // for v3 we need hf after withdraw collateral, because when removing collateral to repay
- // debt, hf could go under 1 then it would fail. If that is the case then we need
- // to use flashloan path
- const repayWithUserReserve = userReserves.find(
- (userReserve) => userReserve.underlyingAsset === tokenToRepayWith.address
- );
- const { hfAfterSwap, hfEffectOfFromAmount } = calculateHFAfterRepay({
- amountToReceiveAfterSwap: outputAmount,
- amountToSwap: inputAmount,
- fromAssetData: collateralReserveData,
- user,
- toAssetData: poolReserve,
- repayWithUserReserve,
- debt,
- });
-
- // If the selected collateral asset is frozen, a flashloan must be used. When a flashloan isn't used,
- // the remaining amount after the swap is deposited into the pool, which will fail for frozen assets.
- const shouldUseFlashloan =
- useFlashloan(user.healthFactor, hfEffectOfFromAmount.toString()) ||
- collateralReserveData?.isFrozen;
-
- // we need to get the min as minimumReceived can be greater than debt as we are swapping
- // a safe amount to repay all. When this happens amountAfterRepay would be < 0 and
- // this would show as certain amount left to repay when we are actually repaying all debt
- const amountAfterRepay = valueToBigNumber(debt).minus(BigNumber.min(outputAmount, debt));
- const displayAmountAfterRepayInUsd = amountAfterRepay.multipliedBy(poolReserve.priceInUSD);
- const collateralAmountAfterRepay = tokenToRepayWithBalance
- ? valueToBigNumber(tokenToRepayWithBalance).minus(inputAmount)
- : valueToBigNumber('0');
- const collateralAmountAfterRepayUSD = collateralAmountAfterRepay.multipliedBy(
- collateralReserveData.priceInUSD
- );
-
- const exactOutputAmount = repayAmount; // swapVariant === 'exactIn' ? outputAmount : repayAmount;
- const exactOutputUsd = repayAmountUsdValue; // swapVariant === 'exactIn' ? outputAmountUSD : repayAmountUsdValue;
-
- const assetsBlockingWithdraw = useZeroLTVBlockingWithdraw();
-
- let blockingError: ErrorType | undefined = undefined;
-
- if (
- assetsBlockingWithdraw.length > 0 &&
- !assetsBlockingWithdraw.includes(tokenToRepayWith.symbol)
- ) {
- blockingError = ErrorType.ZERO_LTV_WITHDRAW_BLOCKED;
- } else if (valueToBigNumber(tokenToRepayWithBalance).lt(inputAmount)) {
- blockingError = ErrorType.NOT_ENOUGH_COLLATERAL_TO_REPAY_WITH;
- } else if (shouldUseFlashloan && !collateralReserveData.flashLoanEnabled) {
- blockingError = ErrorType.FLASH_LOAN_NOT_AVAILABLE;
- }
-
- const BlockingError: React.FC = () => {
- switch (blockingError) {
- case ErrorType.NOT_ENOUGH_COLLATERAL_TO_REPAY_WITH:
- return Not enough collateral to repay this amount of debt with;
- case ErrorType.ZERO_LTV_WITHDRAW_BLOCKED:
- return (
-
- Assets with zero LTV ({assetsBlockingWithdraw.join(', ')}) must be withdrawn or disabled
- as collateral to perform this action
-
- );
- case ErrorType.FLASH_LOAN_NOT_AVAILABLE:
- return (
-
- Due to health factor impact, a flashloan is required to perform this transaction, but
- Aave Governance has disabled flashloan availability for this asset. Try lowering the
- amount or supplying additional collateral.
-
- );
- default:
- return null;
- }
- };
-
- const inputAmountWithSlippage = maxInputAmountWithSlippage(
- inputAmount,
- maxSlippage,
- tokenToRepayWith.decimals || 18
- );
-
- const outputAmountWithSlippage = minimumReceivedAfterSlippage(
- outputAmount,
- maxSlippage,
- poolReserve.decimals
- );
-
- if (mainTxState.success)
- return (
- Repaid}
- amount={swapVariant === 'exactOut' ? outputAmount : outputAmountWithSlippage}
- symbol={poolReserve.symbol}
- />
- );
-
- return (
- <>
- Expected amount to repay}
- balanceText={Borrow balance}
- />
-
-
-
-
-
-
-
- Collateral to repay with}
- balanceText={Borrow balance}
- maxValue={tokenToRepayWithBalance}
- loading={loadingSkeleton}
- disableInput
- />
- {error && !loadingSkeleton && (
-
- {error}
-
- )}
- {blockingError !== undefined && (
-
-
-
- )}
-
-
- {false ? (
- <>
- Minimum amount of debt to be repaid
-
-
-
-
-
-
- >
- ) : (
- <>
- Maximum collateral amount to use
-
-
-
-
-
-
- >
- )}
-
- }
- />
- }
- >
-
- Borrow balance after repay}
- futureValue={amountAfterRepay.toString()}
- futureValueUSD={displayAmountAfterRepayInUsd.toString()}
- symbol={symbol}
- tokenIcon={poolReserve.iconSymbol}
- loading={loadingSkeleton}
- hideSymbolSuffix
- />
- Collateral balance after repay}
- futureValue={collateralAmountAfterRepay.toString()}
- futureValueUSD={collateralAmountAfterRepayUSD.toString()}
- symbol={tokenToRepayWith.symbol}
- tokenIcon={tokenToRepayWith.iconSymbol}
- loading={loadingSkeleton}
- hideSymbolSuffix
- />
-
-
- {txError && }
-
-
- >
- );
-}
diff --git a/src/components/transactions/Repay/RepayModal.tsx b/src/components/transactions/Repay/RepayModal.tsx
index 972f30addd..0ae907beed 100644
--- a/src/components/transactions/Repay/RepayModal.tsx
+++ b/src/components/transactions/Repay/RepayModal.tsx
@@ -9,7 +9,7 @@ import { isFeatureEnabled } from 'src/utils/marketsAndNetworksConfig';
import { BasicModal } from '../../primitives/BasicModal';
import { ModalWrapper } from '../FlowCommons/ModalWrapper';
-import { CollateralRepayModalContent } from './CollateralRepayModalContent';
+import { RepayWithCollateralModalContent } from '../Swap/modals/request/RepayWithCollateralModalContent';
import { RepayModalContent } from './RepayModalContent';
import { RepayType, RepayTypeSelector } from './RepayTypeSelector';
@@ -53,10 +53,10 @@ export const RepayModal = () => {
)}
{repayType === RepayType.BALANCE && }
{repayType === RepayType.COLLATERAL && (
-
)}
>
diff --git a/src/components/transactions/Swap/README.md b/src/components/transactions/Swap/README.md
new file mode 100644
index 0000000000..ee50ebc025
--- /dev/null
+++ b/src/components/transactions/Swap/README.md
@@ -0,0 +1,89 @@
+## Swap module
+
+
+
+### Goals
+- **Consistent UI/UX** across all swap-related modals
+- **Shared logic, no duplication** via a common base content and shared helpers
+- **Multi‑provider** architecture (CoW Protocol, ParaSwap; easy to extend)
+- **Composable and customizable** components
+
+### High‑level flow
+1. A top‑level modal (`modals/*Modal.tsx`) renders a corresponding content component in `modals/request/*ModalContent.tsx`.
+2. Each `*ModalContent` composes `BaseSwapModalContent`, which wires data, inputs, warnings, details, and actions.
+3. Provider selection is automatic via `helpers/shared/provider.helpers.ts` (`getSwitchProvider`).
+4. Quotes are fetched with `hooks/useSwapQuote` and refreshed periodically; flash‑loan and health‑factor logic is handled by `hooks/useFlowSelector`.
+5. The UI is composed from shared inputs, pre/post warnings, per‑flow details, and provider‑specific actions.
+
+### Directory overview
+- `modals/`
+ - Entry points displayed to users: `SwapModal.tsx`, `CollateralSwapModal.tsx`, `DebtSwapModal.tsx`, etc.
+ - `modals/request/` contains `*ModalContent.tsx` for each flow and a `BaseSwapModalContent` used by all.
+ - `modals/result/` shows success/receipt views.
+
+- `inputs/`
+ - `SwapInputs.tsx` orchestrates order inputs; `MarketOrderInputs.tsx` and `LimitOrderInputs.tsx` are the two modes.
+ - `inputs/shared/` and `inputs/primitives/` host reusable input UI.
+
+- `warnings/`
+ - Pre‑ and post‑input warnings: `SwapPreInputWarnings.tsx`, `SwapPostInputWarnings.tsx`.
+ - Flow‑ and rule‑specific implementations live under `warnings/preInputs/` and `warnings/postInputs/`.
+
+- `details/`
+ - Transaction overview blocks: `SwapDetails.tsx`, plus flow variants like `CollateralSwapDetails.tsx`, `DebtSwapDetails.tsx`, `RepayWithCollateralDetails.tsx`, `WithdrawAndSwapDetails.tsx`.
+ - Provider‑specific cost breakdowns, e.g. `CowCostsDetails.tsx`.
+
+- `actions/`
+ - The submit/CTA area and transaction execution.
+ - Flow containers: `SwapActions`, `CollateralSwapActions`, `DebtSwapActions`, `RepayWithCollateralActions`, `WithdrawAndSwapActions`.
+ - Provider adapters implement the same surface per flow, e.g. `SwapActionsViaCoW.tsx`, `SwapActionsViaParaswap.tsx`, and their flow equivalents under each subfolder.
+ - `approval/useSwapTokenApproval.ts` handles allowance flows when needed.
+
+- `hooks/`
+ - `useSwapQuote` retrieves and normalizes quotes (CoW/ParaSwap) and writes into `SwapState`.
+ - `useFlowSelector` computes health‑factor effects and decides when to use flash‑loans.
+ - Other utilities: `useSwapOrderAmounts`, `useSwapGasEstimation`, `useSlippageSelector`, `useMaxNativeAmount`, `useProtocolReserves`, `useUserContext`.
+
+- `helpers/`
+ - `shared/` contains provider‑agnostic logic (provider selection, formatting, misc) and `gasEstimation.helpers.ts`.
+ - `cow/` and `paraswap/` contain provider integrations (rates, order helpers, misc).
+
+- `errors/`
+ - UI components and mapping/helpers for error presentation; organized by provider and shared concerns.
+
+- `constants/`
+ - Provider and feature flags: `cow.constants.ts`, `paraswap.constants.ts`, `limitOrders.constants.ts`, `shared.constants.ts`.
+
+- `types/`
+ - Shared domain types: params, state, tokens, quotes, and re‑exports.
+
+- `shared/`
+ - Small UI pieces reused across modals (e.g., `OrderTypeSelector`, `SwapModalTitle`).
+
+- `analytics/`
+ - Analytics constants and hooks to track swap interactions.
+
+- `backup/`
+ - Legacy/previous implementation kept for reference during the migration to the new modular structure.
+
+### Extending to a new provider
+1. Add provider constants in `constants/` and integration helpers under `helpers//`.
+2. Plug the provider into `helpers/shared/provider.helpers.ts` so it can be selected.
+3. Implement `*ActionsVia.tsx` for each supported flow under `actions/`.
+4. Optionally add provider‑specific details/warnings and wire them in the `*Details.tsx`/warnings where appropriate.
+
+### Data model
+- `types/state.types.ts` defines the authoritative `SwapState` used across the module.
+- `types/params.types.ts` carries immutable parameters from the modal entry.
+- Quotes unify to a `quote.types.ts` shape so the UI remains provider‑agnostic.
+
+### Notes
+- `useSwapQuote` refreshes quotes every 30s by default, paused during action execution.
+- Some flows invert the quote route (e.g., `DebtSwap`, `RepayWithCollateral`); this is encapsulated in `useSwapQuote` and consumers stay agnostic.
+
+### Core types (documented)
+- State: see `types/state.types.ts` (`SwapState`, `TokensSwapState`, `ProtocolSwapState`).
+- Params: see `types/params.types.ts` (`SwapParams`).
+- Tokens and quotes: see `types/tokens.types.ts`, `types/quote.types.ts`, and shared enums in `types/shared.types.ts`.
+
+
diff --git a/src/components/transactions/Swap/actions/ActionsBlocked.tsx b/src/components/transactions/Swap/actions/ActionsBlocked.tsx
new file mode 100644
index 0000000000..83ac6dd4fc
--- /dev/null
+++ b/src/components/transactions/Swap/actions/ActionsBlocked.tsx
@@ -0,0 +1,26 @@
+import { Trans } from '@lingui/macro';
+import { Button } from '@mui/material';
+
+import { SwapState } from '../types';
+
+type blockType = 'errors' | 'generic';
+const stateToBlockType = (state: SwapState): blockType => {
+ if (state.error) return 'errors';
+ return 'generic';
+};
+
+export const ActionsBlocked: React.FC<{ state: SwapState }> = ({ state }) => {
+ const blockType = stateToBlockType(state);
+
+ return (
+
+ );
+};
diff --git a/src/components/transactions/Swap/actions/ActionsSkeleton.tsx b/src/components/transactions/Swap/actions/ActionsSkeleton.tsx
new file mode 100644
index 0000000000..a1193552c7
--- /dev/null
+++ b/src/components/transactions/Swap/actions/ActionsSkeleton.tsx
@@ -0,0 +1,69 @@
+import { Trans } from '@lingui/macro';
+import { Button, CircularProgress } from '@mui/material';
+import React, { useEffect, useRef, useState } from 'react';
+
+import { SwapState } from '../types';
+
+export type LoadingType = 'quote' | 'actions' | 'other';
+const stateToLoadingType = (state: SwapState): LoadingType => {
+ if (state.ratesLoading) return 'quote';
+ if (state.actionsLoading) return 'actions';
+ return 'other';
+};
+
+export const ActionsLoading: React.FC<{ state: SwapState }> = ({ state }) => {
+ const loadingType = stateToLoadingType(state);
+ const [quoteTimeElapsed, setQuoteTimeElapsed] = useState(false);
+ const timerRef = useRef(null);
+
+ // Timer logic for updating the loading text after 2 seconds when loadingType is 'quote'
+ // Trick to change quote loading trick to make it feel more smooth
+ useEffect(() => {
+ if (loadingType === 'quote') {
+ setQuoteTimeElapsed(false);
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+ timerRef.current = setTimeout(() => {
+ setQuoteTimeElapsed(true);
+ }, 2000);
+ } else {
+ // In case the loading type changes, clear timer and reset state
+ setQuoteTimeElapsed(false);
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ timerRef.current = null;
+ }
+ }
+ // Cleanup on unmount
+ return () => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ timerRef.current = null;
+ }
+ };
+ }, [loadingType]);
+
+ return (
+
+ );
+};
diff --git a/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActions.tsx b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActions.tsx
new file mode 100644
index 0000000000..82ae9ad3e6
--- /dev/null
+++ b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActions.tsx
@@ -0,0 +1,65 @@
+import { Dispatch } from 'react';
+
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import { ProtocolSwapParams, ProtocolSwapState, SwapProvider, SwapState } from '../../types';
+import { SwapActionsViaCoW } from '../SwapActions/SwapActionsViaCoW';
+import { SwapActionsViaParaswap } from '../SwapActions/SwapActionsViaParaswap';
+import { CollateralSwapActionsViaCowAdapters } from './CollateralSwapActionsViaCoWAdapters';
+import { CollateralSwapActionsViaParaswapAdapters } from './CollateralSwapActionsViaParaswapAdapters';
+
+export const CollateralSwapActions = ({
+ params,
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: ProtocolSwapParams;
+ state: ProtocolSwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ switch (state.provider) {
+ case SwapProvider.COW_PROTOCOL:
+ if (state.useFlashloan) {
+ return (
+
+ );
+ } else {
+ // Essentially traditional aTokens swap
+ return (
+
+ );
+ }
+ case SwapProvider.PARASWAP:
+ if (state.useFlashloan) {
+ return (
+
+ );
+ } else {
+ // Essentially traditional aTokens swap
+ return (
+
+ );
+ }
+ }
+};
diff --git a/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx
new file mode 100644
index 0000000000..9fe42097da
--- /dev/null
+++ b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx
@@ -0,0 +1,352 @@
+import { normalize } from '@aave/math-utils';
+import { getOrderToSign, LimitTradeParameters, OrderKind, OrderStatus } from '@cowprotocol/cow-sdk';
+import { AaveFlashLoanType, HASH_ZERO } from '@cowprotocol/sdk-flash-loans';
+import { Trans } from '@lingui/macro';
+import { Dispatch, useEffect, useMemo, useState } from 'react';
+import { TxActionsWrapper } from 'src/components/transactions/TxActionsWrapper';
+import { calculateSignedAmount } from 'src/hooks/paraswap/common';
+import { useModalContext } from 'src/hooks/useModal';
+import { useSwapOrdersTracking } from 'src/hooks/useSwapOrdersTracking';
+import { useRootStore } from 'src/store/root';
+import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
+import { saveCowOrderToUserHistory } from 'src/utils/swapAdapterHistory';
+import { useShallow } from 'zustand/react/shallow';
+
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import { COW_PARTNER_FEE, FLASH_LOAN_FEE_BPS } from '../../constants/cow.constants';
+import { APP_CODE_PER_SWAP_TYPE } from '../../constants/shared.constants';
+import {
+ addOrderTypeToAppData,
+ getCowFlashLoanSdk,
+ getCowTradingSdkByChainIdAndAppCode,
+} from '../../helpers/cow';
+import { calculateInstanceAddress } from '../../helpers/cow/adapters.helpers';
+import { useSwapGasEstimation } from '../../hooks/useSwapGasEstimation';
+import {
+ areActionsBlocked,
+ ExpiryToSecondsMap,
+ isCowProtocolRates,
+ OrderType,
+ SwapParams,
+ SwapState,
+} from '../../types';
+import { useSwapTokenApproval } from '../approval/useSwapTokenApproval';
+
+/**
+ * Collateral swap via CoW Protocol Flashloan Adapters.
+ *
+ * Flow summary:
+ * 1) Approve collateral aToken (permit supported) to the CoW flashloan adapter
+ * 2) Compute flashloan fee and sell amount to sign
+ * 3) Create a LIMIT order relative to the UI: collateral -> debt asset
+ * 4) Post order with adapter-provided swap settings; adapter orchestrates the swap
+ */
+export const CollateralSwapActionsViaCowAdapters = ({
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ const [user] = useRootStore(useShallow((state) => [state.account]));
+
+ const {
+ mainTxState,
+ loadingTxns,
+ approvalTxState,
+ setMainTxState,
+ setTxError,
+ setApprovalTxState,
+ } = useModalContext();
+
+ const [precalculatedInstanceAddress, setPrecalculatedInstanceAddress] = useState<
+ string | undefined
+ >();
+
+ const validTo = useMemo(
+ () => Math.floor(Date.now() / 1000) + ExpiryToSecondsMap[state.expiry],
+ [state.expiry]
+ );
+
+ // Pre-compute instance address
+ useEffect(() => {
+ calculateInstanceAddress({
+ user,
+ validTo,
+ type: AaveFlashLoanType.CollateralSwap,
+ state,
+ })
+ .catch((error) => {
+ console.error('calculateInstanceAddress error', error);
+ setTxError(getErrorTextFromError(error, TxAction.MAIN_ACTION, true));
+ setMainTxState({
+ txHash: undefined,
+ loading: false,
+ success: false,
+ });
+ })
+ .then((address) => {
+ if (address) setPrecalculatedInstanceAddress(address);
+ });
+ }, [
+ user,
+ validTo,
+ state.sellAmountBigInt,
+ state.buyAmountBigInt,
+ state.sellAmountToken,
+ state.buyAmountToken,
+ state.processedSide,
+ state.slippage,
+ state.orderType,
+ state.chainId,
+ ]);
+
+ // Approval is aToken ERC20 Approval
+ const amountToApprove = useMemo(() => {
+ if (!state.sellAmountFormatted || !state.sellAmountToken) return '0';
+ return calculateSignedAmount(state.sellAmountFormatted, state.sellAmountToken.decimals);
+ }, [state.sellAmountFormatted, state.sellAmountToken]);
+
+ const { hasActiveOrderForSellToken, trackSwapOrderProgress } = useSwapOrdersTracking();
+ const sellAssetAddress =
+ state.sellAmountToken?.underlyingAddress || state.sourceToken.addressToSwap;
+ const disablePermitDueToActiveOrder = hasActiveOrderForSellToken(state.chainId, sellAssetAddress);
+
+ const {
+ requiresApproval,
+ approval,
+ tryPermit,
+ signatureParams,
+ loadingPermitData,
+ approvedAddress,
+ } = useSwapTokenApproval({
+ chainId: state.chainId,
+ token: state.sourceToken.addressToSwap,
+ symbol: state.sourceToken.symbol,
+ amount: normalize(amountToApprove.toString(), state.sellAmountToken?.decimals ?? 18),
+ decimals: state.sourceToken.decimals,
+ spender: precalculatedInstanceAddress,
+ setState,
+ allowPermit: !disablePermitDueToActiveOrder, // CoW Adapters do support permit but avoid nonce reuse
+ trackingHandlers,
+ swapType: state.swapType,
+ });
+
+ // Use centralized gas estimation
+ useSwapGasEstimation({
+ state,
+ setState,
+ requiresApproval,
+ requiresApprovalReset: state.requiresApprovalReset,
+ approvalTxState,
+ });
+
+ const action = async () => {
+ setMainTxState({
+ txHash: undefined,
+ loading: true,
+ });
+ setState({
+ actionsLoading: false,
+ });
+
+ try {
+ if (
+ !state.sellAmountBigInt ||
+ !state.sellAmountToken ||
+ !state.buyAmountBigInt ||
+ !state.buyAmountToken
+ )
+ return;
+
+ const tradingSdk = await getCowTradingSdkByChainIdAndAppCode(
+ state.chainId,
+ APP_CODE_PER_SWAP_TYPE[state.swapType]
+ );
+ const flashLoanSdk = await getCowFlashLoanSdk(state.chainId);
+
+ const collateralPermit = signatureParams
+ ? {
+ amount: signatureParams?.amount,
+ deadline: Number(signatureParams?.deadline),
+ v: signatureParams?.splitedSignature.v,
+ r: signatureParams?.splitedSignature.r,
+ s: signatureParams?.splitedSignature.s,
+ }
+ : undefined;
+
+ const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
+ flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
+ sellAmount: state.sellAmountBigInt,
+ });
+
+ const limitOrder: LimitTradeParameters = {
+ sellToken: state.sellAmountToken.underlyingAddress,
+ sellTokenDecimals: state.sellAmountToken.decimals,
+ buyToken: state.buyAmountToken.underlyingAddress,
+ buyTokenDecimals: state.buyAmountToken.decimals,
+ sellAmount: sellAmountToSign.toString(),
+ quoteId: isCowProtocolRates(state.swapRate) ? state.swapRate?.quoteId : undefined,
+ buyAmount: state.buyAmountBigInt.toString(),
+ kind: state.processedSide === 'buy' ? OrderKind.BUY : OrderKind.SELL,
+ validTo,
+ slippageBps: state.orderType == OrderType.MARKET ? Number(state.slippage) * 100 : undefined,
+ partnerFee: COW_PARTNER_FEE(state.sellAmountToken.symbol, state.buyAmountToken.symbol),
+ };
+
+ const orderToSign = getOrderToSign(
+ {
+ chainId: state.chainId,
+ from: user,
+ networkCostsAmount: '0',
+ isEthFlow: false,
+ applyCostsSlippageAndFees: false,
+ },
+ limitOrder,
+ HASH_ZERO
+ );
+
+ const orderPostParams = await flashLoanSdk.getOrderPostingSettings(
+ AaveFlashLoanType.CollateralSwap,
+ {
+ chainId: state.chainId,
+ validTo,
+ owner: user as `0x${string}`,
+ flashLoanFeeAmount,
+ },
+ {
+ sellAmount: state.sellAmountBigInt,
+ buyAmount: state.buyAmountBigInt,
+ orderToSign,
+ collateralPermit,
+ }
+ );
+
+ orderPostParams.swapSettings.appData = addOrderTypeToAppData(
+ state.orderType,
+ orderPostParams.swapSettings.appData
+ );
+
+ // Safe-check in case any param changed between approval and order posting
+ const instanceAddress = orderPostParams.instanceAddress;
+ if (instanceAddress !== approvedAddress) {
+ console.error(
+ 'Some parameters changed between approval and order posting: instanceAddress !== approvedAddress, asking for a new approval',
+ instanceAddress,
+ approvedAddress
+ );
+ // Force re-approve
+ setPrecalculatedInstanceAddress(instanceAddress);
+ setApprovalTxState({
+ txHash: undefined,
+ loading: false,
+ success: false,
+ });
+ setMainTxState({ txHash: undefined, loading: false, success: false });
+
+ return;
+ }
+
+ const result = await tradingSdk.postLimitOrder(limitOrder, orderPostParams.swapSettings);
+
+ trackingHandlers.trackSwap();
+ setMainTxState({
+ loading: false,
+ success: true,
+ txHash: result.orderId,
+ });
+ // Save to local history and start tracking status
+ saveCowOrderToUserHistory({
+ protocol: 'cow',
+ orderId: result.orderId,
+ status: OrderStatus.OPEN,
+ swapType: state.swapType,
+ chainId: state.chainId,
+ account: user,
+ timestamp: new Date().toISOString(),
+ srcToken: {
+ address: state.sellAmountToken.underlyingAddress,
+ symbol: state.sellAmountToken.symbol,
+ name: state.sellAmountToken.symbol,
+ decimals: state.sellAmountToken.decimals,
+ },
+ destToken: {
+ address: state.buyAmountToken.underlyingAddress,
+ symbol: state.buyAmountToken.symbol,
+ name: state.buyAmountToken.symbol,
+ decimals: state.buyAmountToken.decimals,
+ },
+ adapterInstanceAddress: instanceAddress,
+ usedAdapter: true, // CollateralSwap via adapters always uses adapter (flashloan)
+ srcAmount: state.sellAmountBigInt.toString(),
+ destAmount: state.buyAmountBigInt.toString(),
+ });
+ trackSwapOrderProgress(result.orderId, state.chainId);
+ setState({
+ actionsLoading: false,
+ });
+ } catch (error) {
+ console.error('CollateralSwapActionsViaCoWAdapters error', error);
+ setTxError(getErrorTextFromError(error, TxAction.MAIN_ACTION, true));
+ setMainTxState({
+ txHash: undefined,
+ loading: false,
+ success: false,
+ });
+ setState({
+ actionsLoading: false,
+ });
+ }
+ };
+
+ return (
+ Checking approval
+ ) : (
+ Swap {state.sourceToken.symbol} collateral
+ )
+ }
+ actionInProgressText={
+ approvalTxState.loading ? (
+ Checking approval
+ ) : (
+ Swapping {state.sourceToken.symbol} collateral
+ )
+ }
+ errorParams={{
+ loading: false,
+ disabled:
+ areActionsBlocked(state) ||
+ approvalTxState.loading ||
+ (!approvalTxState.success && requiresApproval),
+ content: approvalTxState.loading ? (
+ Checking approval
+ ) : (
+ Swap {state.sourceToken.symbol} collateral
+ ),
+ handleClick: action,
+ }}
+ fetchingData={state.actionsLoading || loadingPermitData}
+ blocked={areActionsBlocked(state) || !precalculatedInstanceAddress}
+ tryPermit={tryPermit}
+ permitInUse={disablePermitDueToActiveOrder}
+ />
+ );
+};
diff --git a/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaParaswapAdapters.tsx b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaParaswapAdapters.tsx
new file mode 100644
index 0000000000..d532555929
--- /dev/null
+++ b/src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaParaswapAdapters.tsx
@@ -0,0 +1,309 @@
+import { normalize } from '@aave/math-utils';
+import { OrderStatus } from '@cowprotocol/cow-sdk';
+import { Trans } from '@lingui/macro';
+import { BigNumber, PopulatedTransaction } from 'ethers';
+import { Dispatch, useEffect, useMemo } from 'react';
+import { TxActionsWrapper } from 'src/components/transactions/TxActionsWrapper';
+import { calculateSignedAmount, ExactInSwapper, ExactOutSwapper } from 'src/hooks/paraswap/common';
+import { useModalContext } from 'src/hooks/useModal';
+import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
+import { useRootStore } from 'src/store/root';
+import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
+import { saveParaswapTxToUserHistory } from 'src/utils/swapAdapterHistory';
+import { useShallow } from 'zustand/shallow';
+
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import { useSwapGasEstimation } from '../../hooks/useSwapGasEstimation';
+import {
+ areActionsBlocked,
+ isParaswapRates,
+ isProtocolSwapState,
+ SwapParams,
+ SwapState,
+} from '../../types';
+import { useSwapTokenApproval } from '../approval/useSwapTokenApproval';
+// import { normalizeBN } from '@aave/math-utils';
+
+export const CollateralSwapActionsViaParaswapAdapters = ({
+ params,
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ const { setTxError, setMainTxState, approvalTxState } = useModalContext();
+ const { addTransaction, estimateGasLimit } = useRootStore();
+ const { sendTx } = useWeb3Context();
+ const [swapCollateral, currentMarketData] = useRootStore(
+ useShallow((state) => [state.swapCollateral, state.currentMarketData])
+ );
+
+ // Approval is aToken ERC20 Approval
+ const amountToApprove = useMemo(() => {
+ if (!state.sellAmountFormatted || !state.sellAmountToken) return '0';
+ return calculateSignedAmount(state.sellAmountFormatted, state.sellAmountToken.decimals);
+ }, [state.sellAmountFormatted, state.sellAmountToken]);
+
+ const {
+ requiresApproval,
+ signatureParams,
+ approval,
+ tryPermit,
+ approvedAmount,
+ loadingPermitData,
+ } = useSwapTokenApproval({
+ chainId: state.chainId,
+ token: state.sourceToken.addressToSwap, // aToken
+ symbol: state.sourceToken.symbol,
+ amount: normalize(amountToApprove.toString(), state.sourceToken?.decimals ?? 18),
+ decimals: state.sourceToken.decimals,
+ spender: currentMarketData.addresses.SWAP_COLLATERAL_ADAPTER,
+ setState,
+ trackingHandlers,
+ swapType: state.swapType,
+ });
+
+ // Use centralized gas estimation
+ useSwapGasEstimation({
+ state,
+ setState,
+ requiresApproval,
+ requiresApprovalReset: state.requiresApprovalReset,
+ approvalTxState,
+ });
+
+ const action = async () => {
+ if (!state.swapRate || !isParaswapRates(state.swapRate))
+ throw new Error('Route required to build transaction');
+
+ setMainTxState({
+ txHash: undefined,
+ loading: true,
+ });
+ const isMaxSelected = state.isMaxSelected;
+ const optimalRateData = state.swapRate.optimalRateData;
+
+ // 1. Prepare internal swap call data
+ let swapCallData = '';
+ let augustus = '';
+ if (state.side === 'sell') {
+ const swapper = ExactInSwapper(state.chainId);
+
+ const result = await swapper.getTransactionParams(
+ state.sourceToken.underlyingAddress,
+ state.sourceToken.decimals,
+ state.destinationToken.underlyingAddress,
+ state.destinationToken.decimals,
+ state.user,
+ optimalRateData,
+ Number(state.slippage)
+ );
+ swapCallData = result.swapCallData;
+ augustus = result.augustus;
+ } else {
+ const swapper = ExactOutSwapper(state.chainId);
+
+ const result = await swapper.getTransactionParams(
+ state.destinationToken.underlyingAddress,
+ state.destinationToken.decimals,
+ state.sourceToken.underlyingAddress,
+ state.sourceToken.decimals,
+ state.user,
+ optimalRateData,
+ Number(state.slippage)
+ );
+ swapCallData = result.swapCallData;
+ augustus = result.augustus;
+ }
+
+ if (!isProtocolSwapState(state)) throw new Error('State is not a protocol swap state');
+
+ const signedAmount = approvedAmount;
+ const amountToSwap = state.inputAmount;
+ const amountToReceive = state.buyAmountFormatted || '0';
+
+ let response;
+ try {
+ // 2. Prepare Tx
+ const txs = await swapCollateral({
+ amountToSwap: amountToSwap,
+ amountToReceive: amountToReceive,
+ poolReserve: state.sourceReserve.reserve,
+ targetReserve: state.destinationReserve.reserve,
+ isWrongNetwork: state.isWrongNetwork,
+ symbol: state.sourceToken.symbol,
+ blocked: areActionsBlocked(state),
+ isMaxSelected: isMaxSelected,
+ useFlashLoan: true,
+ swapCallData: swapCallData,
+ augustus: augustus,
+ signature: signatureParams?.splitedSignature,
+ deadline: signatureParams?.deadline,
+ signedAmount,
+ });
+
+ const actionTx = txs.find((tx) => ['DLP_ACTION'].includes(tx.txType));
+ if (!actionTx) throw new Error('Action tx not found');
+ const tx = await actionTx.tx();
+ const populatedTx: PopulatedTransaction = {
+ to: tx.to,
+ from: tx.from,
+ data: tx.data,
+ gasLimit: tx.gasLimit,
+ gasPrice: tx.gasPrice,
+ nonce: tx.nonce,
+ chainId: tx.chainId,
+ value: tx.value ? BigNumber.from(tx.value) : undefined,
+ };
+
+ // 3. Estimate gas limit and send tx
+ const txWithGasEstimation = await estimateGasLimit(populatedTx, state.chainId);
+ response = await sendTx(txWithGasEstimation);
+ await response.wait(1);
+ try {
+ saveParaswapTxToUserHistory({
+ protocol: 'paraswap',
+ txHash: response.hash,
+ swapType: params.swapType,
+ chainId: state.chainId,
+ account: state.user,
+ timestamp: new Date().toISOString(),
+ status: OrderStatus.FULFILLED,
+ srcToken: {
+ address: state.sourceToken.addressToSwap,
+ symbol: state.sourceToken.symbol,
+ name: state.sourceToken.symbol,
+ decimals: state.sourceToken.decimals,
+ },
+ destToken: {
+ address: state.destinationToken.addressToSwap,
+ symbol: state.destinationToken.symbol,
+ name: state.destinationToken.symbol,
+ decimals: state.destinationToken.decimals,
+ },
+ srcAmount: state.sellAmountBigInt?.toString() ?? '0',
+ destAmount: state.buyAmountBigInt?.toString() ?? '0',
+ });
+ } catch {}
+ addTransaction(
+ response.hash,
+ {
+ txState: 'success',
+ },
+ {
+ chainId: state.chainId,
+ }
+ );
+ trackingHandlers.trackSwap();
+ params.invalidateAppState();
+ setMainTxState({
+ txHash: response.hash,
+ loading: false,
+ success: true,
+ });
+ } catch (error) {
+ const parsedError = getErrorTextFromError(error, TxAction.MAIN_ACTION, false);
+
+ // Check if this is a gas estimation error (from estimateGasLimit call)
+ // Gas estimation errors typically occur when estimateGasLimit fails
+ const errorMessage = parsedError.rawError?.message?.toLowerCase() || '';
+ const isGasEstimationError =
+ errorMessage.includes('gas') ||
+ errorMessage.includes('estimation') ||
+ (errorMessage.includes('execution reverted') && errorMessage.includes('estimation'));
+
+ // For gas estimation errors in Paraswap actions, show as warning instead of blocking error
+ if (isGasEstimationError) {
+ setState({
+ actionsLoading: false,
+ warnings: [
+ {
+ message:
+ 'Gas estimation error: The swap could not be estimated. Try increasing slippage or changing the amount.',
+ },
+ ],
+ error: undefined, // Clear any existing errors
+ });
+ } else {
+ // For other errors, handle normally
+ setTxError(parsedError);
+ setState({
+ actionsLoading: false,
+ error: {
+ rawError: parsedError.rawError,
+ message: `Error: ${parsedError.error} on ${parsedError.txAction}`,
+ actionBlocked: parsedError.actionBlocked,
+ },
+ });
+ }
+
+ setMainTxState({
+ loading: false,
+ });
+
+ const reason = error instanceof Error ? error.message : 'Swap failed';
+ trackingHandlers.trackSwapFailed(reason);
+ }
+ };
+
+ useEffect(() => {
+ if (state.mainTxState.success) {
+ trackingHandlers.trackSwap();
+ params.invalidateAppState();
+
+ addTransaction(
+ state.mainTxState.txHash || '',
+ {
+ txState: 'success',
+ },
+ {
+ chainId: state.chainId,
+ }
+ );
+
+ setMainTxState({
+ txHash: state.mainTxState.txHash || '',
+ loading: false,
+ success: true,
+ });
+ }
+ }, [state.mainTxState.success]);
+
+ return (
+ Checking approval
+ ) : (
+ Swap {state.sourceToken.symbol} collateral
+ )
+ }
+ actionInProgressText={Swapping {state.sourceToken.symbol} collateral}
+ fetchingData={state.actionsLoading || loadingPermitData}
+ errorParams={{
+ loading: false,
+ disabled: areActionsBlocked(state),
+ content: Swap {state.sourceToken.symbol} collateral,
+ handleClick: action,
+ }}
+ tryPermit={tryPermit}
+ />
+ );
+};
diff --git a/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActions.tsx b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActions.tsx
new file mode 100644
index 0000000000..20f90d8b90
--- /dev/null
+++ b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActions.tsx
@@ -0,0 +1,41 @@
+import { Dispatch } from 'react';
+
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import { ProtocolSwapParams, ProtocolSwapState, SwapProvider, SwapState } from '../../types';
+import { DebtSwapActionsViaCoW } from './DebtSwapActionsViaCoW';
+import { DebtSwapActionsViaParaswap } from './DebtSwapActionsViaParaswap';
+
+export const DebtSwapActions = ({
+ params,
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: ProtocolSwapParams;
+ state: ProtocolSwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ switch (state.provider) {
+ case SwapProvider.COW_PROTOCOL:
+ return (
+
+ );
+ case SwapProvider.PARASWAP:
+ return (
+
+ );
+ default:
+ return null;
+ }
+};
diff --git a/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx
new file mode 100644
index 0000000000..e4afce7d23
--- /dev/null
+++ b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaCoW.tsx
@@ -0,0 +1,368 @@
+import { normalize, valueToBigNumber } from '@aave/math-utils';
+import { getOrderToSign, LimitTradeParameters, OrderKind, OrderStatus } from '@cowprotocol/cow-sdk';
+import { AaveFlashLoanType, HASH_ZERO } from '@cowprotocol/sdk-flash-loans';
+import { Trans } from '@lingui/macro';
+import { Dispatch, useEffect, useMemo, useState } from 'react';
+import { TxActionsWrapper } from 'src/components/transactions/TxActionsWrapper';
+import { calculateSignedAmount } from 'src/hooks/paraswap/common';
+import { useModalContext } from 'src/hooks/useModal';
+import { useSwapOrdersTracking } from 'src/hooks/useSwapOrdersTracking';
+import { useRootStore } from 'src/store/root';
+import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
+import { saveCowOrderToUserHistory } from 'src/utils/swapAdapterHistory';
+import { zeroAddress } from 'viem';
+import { useShallow } from 'zustand/react/shallow';
+
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import {
+ COW_PARTNER_FEE,
+ DUST_PROTECTION_MULTIPLIER,
+ FLASH_LOAN_FEE_BPS,
+} from '../../constants/cow.constants';
+import { APP_CODE_PER_SWAP_TYPE } from '../../constants/shared.constants';
+import {
+ addOrderTypeToAppData,
+ getCowFlashLoanSdk,
+ getCowTradingSdkByChainIdAndAppCode,
+} from '../../helpers/cow';
+import { calculateInstanceAddress } from '../../helpers/cow/adapters.helpers';
+import { useSwapGasEstimation } from '../../hooks/useSwapGasEstimation';
+import {
+ areActionsBlocked,
+ ExpiryToSecondsMap,
+ isCowProtocolRates,
+ isProtocolSwapState,
+ OrderType,
+ SwapParams,
+ SwapState,
+} from '../../types';
+import { useSwapTokenApproval } from '../approval/useSwapTokenApproval';
+
+/**
+ * Debt swap via CoW Protocol Flashloan Adapters.
+ *
+ * Flow summary:
+ * 1) Approve delegation on the destination variable debt token (permit supported)
+ * 2) Compute flashloan fee and sell amount; we temporarily borrow to close existing debt
+ * 3) Create a LIMIT order INVERTED relative to the UI: new debt asset -> old debt asset
+ * 4) Post order with adapter swap settings; adapter executes the repay + reborrow atomically
+ */
+export const DebtSwapActionsViaCoW = ({
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ const [user] = useRootStore(useShallow((state) => [state.account]));
+
+ const {
+ mainTxState,
+ loadingTxns,
+ approvalTxState,
+ setMainTxState,
+ setTxError,
+ setApprovalTxState,
+ } = useModalContext();
+
+ const [precalculatedInstanceAddress, setPrecalculatedInstanceAddress] = useState<
+ string | undefined
+ >();
+
+ const validTo = useMemo(
+ () => Math.floor(Date.now() / 1000) + ExpiryToSecondsMap[state.expiry],
+ [state.expiry]
+ );
+
+ // Pre-compute instance address
+ useEffect(() => {
+ calculateInstanceAddress({
+ user,
+ validTo,
+ type: AaveFlashLoanType.DebtSwap,
+ state,
+ })
+ .catch((error) => {
+ console.error('calculateInstanceAddress error', error);
+ setTxError(getErrorTextFromError(error, TxAction.MAIN_ACTION, true));
+ setMainTxState({
+ txHash: undefined,
+ loading: false,
+ success: false,
+ });
+ })
+ .then((address) => {
+ if (address) setPrecalculatedInstanceAddress(address);
+ });
+ }, [
+ user,
+ validTo,
+ state.sellAmountBigInt,
+ state.buyAmountBigInt,
+ state.sellAmountToken,
+ state.buyAmountToken,
+ state.processedSide,
+ state.slippage,
+ state.orderType,
+ state.chainId,
+ APP_CODE_PER_SWAP_TYPE[state.swapType],
+ ]);
+
+ const amountToApprove = useMemo(() => {
+ if (!state.sellAmountFormatted || !state.sellAmountToken) return '0';
+ return calculateSignedAmount(state.sellAmountFormatted, state.sellAmountToken.decimals);
+ }, [state.sellAmountFormatted, state.sellAmountToken]);
+
+ const { hasActiveOrderForSellToken, trackSwapOrderProgress } = useSwapOrdersTracking();
+ const sellAssetAddress =
+ state.sellAmountToken?.underlyingAddress || state.sourceToken.addressToSwap;
+ const disablePermitDueToActiveOrder = hasActiveOrderForSellToken(state.chainId, sellAssetAddress);
+
+ // Approval is to the destination token via delegation Approval
+ const {
+ requiresApproval,
+ approval,
+ tryPermit,
+ signatureParams,
+ loadingPermitData,
+ approvedAddress,
+ } = useSwapTokenApproval({
+ chainId: state.chainId,
+ token: isProtocolSwapState(state)
+ ? state.destinationReserve.reserve.variableDebtTokenAddress
+ : zeroAddress,
+ symbol: state.destinationToken.symbol,
+ amount: normalize(amountToApprove, state.sellAmountToken?.decimals ?? 18),
+ decimals: state.destinationToken.decimals,
+ spender: precalculatedInstanceAddress,
+ setState,
+ allowPermit: !disablePermitDueToActiveOrder, // avoid nonce reuse if active order present
+ type: 'delegation', // Debt swap uses delegation
+ trackingHandlers,
+ swapType: state.swapType,
+ });
+
+ // Use centralized gas estimation
+ useSwapGasEstimation({
+ state,
+ setState,
+ requiresApproval,
+ requiresApprovalReset: state.requiresApprovalReset,
+ approvalTxState,
+ });
+
+ const action = async () => {
+ setMainTxState({
+ txHash: undefined,
+ loading: true,
+ });
+ setState({
+ actionsLoading: false,
+ });
+
+ try {
+ if (
+ !state.sellAmountBigInt ||
+ !state.sellAmountToken ||
+ !state.buyAmountBigInt ||
+ !state.buyAmountToken
+ )
+ return;
+
+ const tradingSdk = await getCowTradingSdkByChainIdAndAppCode(
+ state.chainId,
+ APP_CODE_PER_SWAP_TYPE[state.swapType]
+ );
+ const flashLoanSdk = await getCowFlashLoanSdk(state.chainId);
+
+ const buyAmountWithMarginForDustProtection = valueToBigNumber(
+ state.buyAmountBigInt.toString()
+ )
+ .multipliedBy(DUST_PROTECTION_MULTIPLIER)
+ .toFixed(0);
+
+ const delegationPermit = signatureParams
+ ? {
+ amount: signatureParams?.amount,
+ deadline: Number(signatureParams?.deadline),
+ v: signatureParams?.splitedSignature.v,
+ r: signatureParams?.splitedSignature.r,
+ s: signatureParams?.splitedSignature.s,
+ }
+ : undefined;
+
+ const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
+ flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
+ sellAmount: state.sellAmountBigInt,
+ });
+
+ // On Debt Swap, the side is inverted for the swap
+ const limitOrder: LimitTradeParameters = {
+ sellToken: state.sellAmountToken.underlyingAddress,
+ sellTokenDecimals: state.sellAmountToken.decimals,
+ buyToken: state.buyAmountToken.underlyingAddress,
+ buyTokenDecimals: state.buyAmountToken.decimals,
+ sellAmount: sellAmountToSign.toString(),
+ buyAmount: buyAmountWithMarginForDustProtection.toString(),
+ kind: state.processedSide === 'buy' ? OrderKind.BUY : OrderKind.SELL,
+ quoteId: isCowProtocolRates(state.swapRate) ? state.swapRate?.quoteId : undefined,
+ validTo,
+ slippageBps: state.orderType == OrderType.MARKET ? Number(state.slippage) * 100 : undefined,
+ partnerFee: COW_PARTNER_FEE(state.sellAmountToken.symbol, state.buyAmountToken.symbol),
+ };
+
+ const orderToSign = getOrderToSign(
+ {
+ chainId: state.chainId,
+ from: user,
+ networkCostsAmount: '0',
+ isEthFlow: false,
+ applyCostsSlippageAndFees: false,
+ },
+ limitOrder,
+ HASH_ZERO
+ );
+
+ const orderPostParams = await flashLoanSdk.getOrderPostingSettings(
+ AaveFlashLoanType.DebtSwap,
+ {
+ chainId: state.chainId,
+ validTo,
+ owner: user as `0x${string}`,
+ flashLoanFeeAmount,
+ },
+ {
+ sellAmount: state.sellAmountBigInt,
+ buyAmount: BigInt(buyAmountWithMarginForDustProtection),
+ orderToSign,
+ collateralPermit: delegationPermit,
+ }
+ );
+
+ // Safe-check in case any param changed between approval and order posting
+ const instanceAddress = orderPostParams.instanceAddress;
+ if (instanceAddress !== approvedAddress) {
+ console.error(
+ 'Some parameters changed between approval and order posting: instanceAddress !== approvedAddress, asking for a new approval',
+ instanceAddress,
+ approvedAddress
+ );
+ // Force re-approve
+ setPrecalculatedInstanceAddress(instanceAddress);
+ setApprovalTxState({
+ txHash: undefined,
+ loading: false,
+ success: false,
+ });
+ setMainTxState({ txHash: undefined, loading: false, success: false });
+
+ return;
+ }
+
+ orderPostParams.swapSettings.appData = addOrderTypeToAppData(
+ state.orderType,
+ orderPostParams.swapSettings.appData
+ );
+ const result = await tradingSdk.postLimitOrder(limitOrder, orderPostParams.swapSettings);
+
+ trackingHandlers.trackSwap();
+ setMainTxState({
+ loading: false,
+ success: true,
+ txHash: result.orderId,
+ });
+ // Save to local history and start tracking status
+ saveCowOrderToUserHistory({
+ protocol: 'cow',
+ orderId: result.orderId,
+ status: OrderStatus.OPEN,
+ swapType: state.swapType,
+ chainId: state.chainId,
+ account: user,
+ timestamp: new Date().toISOString(),
+ srcToken: {
+ address: state.sellAmountToken.underlyingAddress,
+ symbol: state.sellAmountToken.symbol,
+ name: state.sellAmountToken.symbol,
+ decimals: state.sellAmountToken.decimals,
+ },
+ destToken: {
+ address: state.buyAmountToken.underlyingAddress,
+ symbol: state.buyAmountToken.symbol,
+ name: state.buyAmountToken.symbol,
+ decimals: state.buyAmountToken.decimals,
+ },
+ adapterInstanceAddress: instanceAddress,
+ usedAdapter: true, // DebtSwap always uses adapter
+ srcAmount: state.sellAmountBigInt.toString(),
+ destAmount: state.buyAmountBigInt.toString(),
+ });
+ trackSwapOrderProgress(result.orderId, state.chainId);
+ setState({
+ actionsLoading: false,
+ });
+ } catch (error) {
+ console.error('DebtSwapActionsViaCoW error', error);
+ setTxError(getErrorTextFromError(error, TxAction.MAIN_ACTION, true));
+ setMainTxState({
+ txHash: undefined,
+ loading: false,
+ success: false,
+ });
+ setState({
+ actionsLoading: false,
+ });
+ }
+ };
+
+ return (
+ Checking approval
+ ) : (
+ Swap {state.sourceToken.symbol} debt
+ )
+ }
+ actionInProgressText={
+ approvalTxState.loading ? (
+ Checking approval
+ ) : (
+ Swapping {state.sourceToken.symbol} debt
+ )
+ }
+ errorParams={{
+ loading: false,
+ disabled:
+ areActionsBlocked(state) ||
+ approvalTxState.loading ||
+ (!approvalTxState.success && requiresApproval),
+ content: approvalTxState.loading ? (
+ Checking approval
+ ) : (
+ Swap {state.sourceToken.symbol} debt
+ ),
+ handleClick: action,
+ }}
+ fetchingData={state.actionsLoading || loadingPermitData}
+ blocked={areActionsBlocked(state) || !precalculatedInstanceAddress}
+ tryPermit={tryPermit}
+ permitInUse={disablePermitDueToActiveOrder}
+ />
+ );
+};
diff --git a/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaParaswap.tsx b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaParaswap.tsx
new file mode 100644
index 0000000000..951fab2e26
--- /dev/null
+++ b/src/components/transactions/Swap/actions/DebtSwap/DebtSwapActionsViaParaswap.tsx
@@ -0,0 +1,260 @@
+import { valueToBigNumber } from '@aave/math-utils';
+import { OrderStatus } from '@cowprotocol/cow-sdk';
+import { Trans } from '@lingui/macro';
+import { parseUnits } from 'ethers/lib/utils';
+import { Dispatch } from 'react';
+import { TxActionsWrapper } from 'src/components/transactions/TxActionsWrapper';
+import { maxInputAmountWithSlippage } from 'src/hooks/paraswap/common';
+import { useModalContext } from 'src/hooks/useModal';
+import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
+import { useRootStore } from 'src/store/root';
+import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
+import { saveParaswapTxToUserHistory } from 'src/utils/swapAdapterHistory';
+import { useShallow } from 'zustand/shallow';
+
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import { getTransactionParams } from '../../helpers/paraswap';
+import { useSwapGasEstimation } from '../../hooks/useSwapGasEstimation';
+import {
+ areActionsBlocked,
+ isParaswapRates,
+ ProtocolSwapParams,
+ ProtocolSwapState,
+ SwapState,
+} from '../../types';
+import { useSwapTokenApproval } from '../approval/useSwapTokenApproval';
+
+/**
+ * Debt swap via ParaSwap Adapter.
+ *
+ * Flow summary:
+ * 1) Approve delegation on the destination variable debt token to the adapter
+ * 2) Build a ParaSwap route INVERTED relative to the UI: new debt asset -> old debt asset
+ * - Inversion is required because we're acquiring new debt to repay old debt
+ * 3) Call the Debt Switch adapter with swap calldata and permit/delegation signature
+ */
+export const DebtSwapActionsViaParaswap = ({
+ state,
+ params,
+ setState,
+ trackingHandlers,
+}: {
+ params: ProtocolSwapParams;
+ state: ProtocolSwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ const [currentMarketData, estimateGasLimit, addTransaction, debtSwitch] = useRootStore(
+ useShallow((state) => [
+ state.currentMarketData,
+ state.estimateGasLimit,
+ state.addTransaction,
+ state.debtSwitch,
+ ])
+ );
+ const { approvalTxState, mainTxState, loadingTxns, setMainTxState, setTxError } =
+ useModalContext();
+ const { sendTx } = useWeb3Context();
+
+ // TODO: CHECK LIMIT ORDERS BUY ORDERS
+
+ const amountToSwap = maxInputAmountWithSlippage(
+ state.buyAmountFormatted ?? '0',
+ (Number(state.slippage) * 100).toString(),
+ state.buyAmountToken?.decimals || 18
+ );
+
+ const maxNewDebtAmountToReceiveWithSlippage = maxInputAmountWithSlippage(
+ state.sellAmountFormatted ?? '0',
+ (Number(state.slippage) * 100).toString(),
+ state.sellAmountToken?.decimals || 18
+ );
+
+ const { requiresApproval, approval, tryPermit, signatureParams, loadingPermitData } =
+ useSwapTokenApproval({
+ chainId: state.chainId,
+ token: state.destinationReserve.reserve.variableDebtTokenAddress,
+ symbol: state.destinationReserve.reserve.symbol,
+ amount: maxNewDebtAmountToReceiveWithSlippage,
+ decimals: state.destinationReserve.reserve.decimals,
+ spender: currentMarketData.addresses.DEBT_SWITCH_ADAPTER,
+ setState,
+ allowPermit: currentMarketData.v3,
+ margin: 0.25,
+ type: 'delegation',
+ trackingHandlers,
+ swapType: state.swapType,
+ });
+
+ // Use centralized gas estimation
+ useSwapGasEstimation({
+ state,
+ setState,
+ requiresApproval,
+ requiresApprovalReset: state.requiresApprovalReset,
+ approvalTxState,
+ });
+
+ const action = async () => {
+ try {
+ setMainTxState({ ...mainTxState, loading: true });
+
+ if (!state.swapRate || !isParaswapRates(state.swapRate)) {
+ throw new Error('No swap rate found');
+ }
+
+ if (!signatureParams) {
+ throw new Error('Signature params not found');
+ }
+
+ const inferredKind = state.swapRate.optimalRateData.side === 'SELL' ? 'sell' : 'buy';
+
+ // CallData for ParaswapRoute, which is inversed to the actual swap (dest -> src)
+ const { swapCallData, augustus } = await getTransactionParams(
+ inferredKind,
+ state.chainId,
+ state.destinationToken.addressToSwap,
+ state.destinationToken.decimals,
+ state.sourceToken.addressToSwap,
+ state.sourceToken.decimals,
+ state.user,
+ state.swapRate.optimalRateData,
+ Number(state.slippage)
+ );
+
+ const amountToReceiveForDebtSwitch = state.buyAmountBigInt?.toString() ?? '0';
+ const amountToSwapForDebtSwitch = state.sellAmountBigInt?.toString() ?? '0';
+
+ let debtSwitchTxData = debtSwitch({
+ poolReserve: state.sourceReserve.reserve,
+ targetReserve: state.destinationReserve.reserve,
+ amountToReceive: amountToSwapForDebtSwitch,
+ amountToSwap: amountToReceiveForDebtSwitch,
+ isMaxSelected: state.isMaxSelected,
+ txCalldata: swapCallData,
+ augustus: augustus,
+ signatureParams: {
+ signature: signatureParams.signature,
+ deadline: signatureParams.deadline,
+ amount: signatureParams.amount,
+ },
+ isWrongNetwork: state.isWrongNetwork,
+ });
+
+ debtSwitchTxData = await estimateGasLimit(debtSwitchTxData);
+ const response = await sendTx(debtSwitchTxData);
+ await response.wait(1);
+ try {
+ saveParaswapTxToUserHistory({
+ protocol: 'paraswap',
+ txHash: response.hash,
+ swapType: state.swapType,
+ chainId: state.chainId,
+ account: state.user,
+ timestamp: new Date().toISOString(),
+ status: OrderStatus.FULFILLED,
+ srcToken: {
+ address: state.sourceToken.addressToSwap,
+ symbol: state.sourceToken.symbol,
+ name: state.sourceToken.symbol,
+ decimals: state.sourceToken.decimals,
+ },
+ destToken: {
+ address: state.destinationToken.addressToSwap,
+ symbol: state.destinationToken.symbol,
+ name: state.destinationToken.symbol,
+ decimals: state.destinationToken.decimals,
+ },
+ srcAmount: state.buyAmountBigInt?.toString() ?? '0',
+ destAmount: state.sellAmountBigInt?.toString() ?? '0',
+ });
+ } catch {}
+ setMainTxState({
+ txHash: response.hash,
+ loading: false,
+ success: true,
+ });
+ addTransaction(response.hash, {
+ action: 'debtSwitch',
+ txState: 'success',
+ previousState: `${state.buyAmountFormatted} variable ${state.sourceReserve.reserve.symbol}`,
+ newState: `${state.inputAmount} variable ${state.destinationReserve.reserve.symbol}`,
+ amountUsd: valueToBigNumber(
+ parseUnits(amountToSwap, state.sourceReserve.reserve.decimals).toString()
+ )
+ .multipliedBy(state.sourceReserve.reserve.priceInUSD)
+ .toString(),
+ outAmountUsd: valueToBigNumber(
+ parseUnits(
+ maxNewDebtAmountToReceiveWithSlippage,
+ state.destinationReserve.reserve.decimals
+ ).toString()
+ )
+ .multipliedBy(state.destinationReserve.reserve.priceInUSD)
+ .toString(),
+ });
+
+ params.invalidateAppState();
+ trackingHandlers.trackSwap();
+ } catch (error) {
+ console.error('error', error);
+ const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
+
+ // For gas estimation errors in Paraswap actions, show as warning instead of blocking error
+ if (parsedError.txAction === TxAction.GAS_ESTIMATION) {
+ setState({
+ actionsLoading: false,
+ warnings: [
+ {
+ message:
+ 'Gas estimation error: The swap could not be estimated. Try increasing slippage or changing the amount.',
+ },
+ ],
+ error: undefined, // Clear any existing errors
+ });
+ } else {
+ // For other errors, handle normally
+ setTxError(parsedError);
+ setState({
+ actionsLoading: false,
+ });
+ }
+
+ setMainTxState({
+ txHash: undefined,
+ loading: false,
+ });
+
+ const reason = error instanceof Error ? error.message : 'Swap failed';
+ trackingHandlers.trackSwapFailed(reason);
+ }
+ };
+
+ return (
+ Swap}
+ actionInProgressText={Swapping}
+ fetchingData={state.ratesLoading || loadingPermitData}
+ errorParams={{
+ loading: false,
+ disabled: areActionsBlocked(state) || !approvalTxState?.success,
+ content: Swap,
+ handleClick: action,
+ }}
+ blocked={areActionsBlocked(state)}
+ tryPermit={tryPermit}
+ />
+ );
+};
diff --git a/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActions.tsx b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActions.tsx
new file mode 100644
index 0000000000..5db63b5e8f
--- /dev/null
+++ b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActions.tsx
@@ -0,0 +1,41 @@
+import { Dispatch } from 'react';
+
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import { ProtocolSwapParams, ProtocolSwapState, SwapProvider, SwapState } from '../../types';
+import { RepayWithCollateralActionsViaCoW } from './RepayWithCollateralActionsViaCoW';
+import { RepayWithCollateralActionsViaParaswap } from './RepayWithCollateralActionsViaParaswap';
+
+export const RepayWithCollateralActions = ({
+ params,
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: ProtocolSwapParams;
+ state: ProtocolSwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ switch (state.provider) {
+ case SwapProvider.COW_PROTOCOL:
+ return (
+
+ );
+ case SwapProvider.PARASWAP:
+ return (
+
+ );
+ default:
+ return null;
+ }
+};
diff --git a/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx
new file mode 100644
index 0000000000..d6560d0008
--- /dev/null
+++ b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaCoW.tsx
@@ -0,0 +1,373 @@
+import { normalize, valueToBigNumber } from '@aave/math-utils';
+import { getOrderToSign, LimitTradeParameters, OrderKind, OrderStatus } from '@cowprotocol/cow-sdk';
+import { AaveFlashLoanType, HASH_ZERO } from '@cowprotocol/sdk-flash-loans';
+import { Trans } from '@lingui/macro';
+import { Dispatch, useEffect, useMemo, useState } from 'react';
+import { TxActionsWrapper } from 'src/components/transactions/TxActionsWrapper';
+import { calculateSignedAmount } from 'src/hooks/paraswap/common';
+import { useModalContext } from 'src/hooks/useModal';
+import { useSwapOrdersTracking } from 'src/hooks/useSwapOrdersTracking';
+import { useRootStore } from 'src/store/root';
+import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
+import { saveCowOrderToUserHistory } from 'src/utils/swapAdapterHistory';
+import { useShallow } from 'zustand/react/shallow';
+
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import {
+ COW_PARTNER_FEE,
+ DUST_PROTECTION_MULTIPLIER,
+ FLASH_LOAN_FEE_BPS,
+} from '../../constants/cow.constants';
+import { APP_CODE_PER_SWAP_TYPE } from '../../constants/shared.constants';
+import {
+ addOrderTypeToAppData,
+ getCowFlashLoanSdk,
+ getCowTradingSdkByChainIdAndAppCode,
+} from '../../helpers/cow';
+import { calculateInstanceAddress } from '../../helpers/cow/adapters.helpers';
+import { useSwapGasEstimation } from '../../hooks/useSwapGasEstimation';
+import {
+ areActionsBlocked,
+ ExpiryToSecondsMap,
+ isCowProtocolRates,
+ OrderType,
+ SwapParams,
+ SwapState,
+} from '../../types';
+import { useSwapTokenApproval } from '../approval/useSwapTokenApproval';
+
+/**
+ * Repay-with-collateral via CoW Protocol Flashloan Adapters.
+ *
+ * Flow summary:
+ * 1) Approve collateral aToken (permit supported) to the CoW flashloan adapter
+ * 2) Compute flashloan fee and sell amount to sign
+ * 3) Create a LIMIT order INVERTED relative to the UI: collateral -> debt asset
+ * - The order kind depends on processed side; inversion is required because
+ * we swap the available collateral to acquire the debt asset to repay
+ * 4) Post order with adapter-provided swap settings; adapter orchestrates repay
+ */
+export const RepayWithCollateralActionsViaCoW = ({
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ const [user] = useRootStore(useShallow((state) => [state.account]));
+
+ const {
+ mainTxState,
+ loadingTxns,
+ approvalTxState,
+ setMainTxState,
+ setTxError,
+ setApprovalTxState,
+ } = useModalContext();
+
+ const [precalculatedInstanceAddress, setPrecalculatedInstanceAddress] = useState<
+ string | undefined
+ >();
+
+ const validTo = useMemo(
+ () => Math.floor(Date.now() / 1000) + ExpiryToSecondsMap[state.expiry],
+ [state.expiry]
+ );
+
+ // Pre-compute instance address
+ useEffect(() => {
+ calculateInstanceAddress({
+ user,
+ validTo,
+ type: AaveFlashLoanType.RepayCollateral,
+ state,
+ })
+ .catch((error) => {
+ console.error('calculateInstanceAddress error', error);
+ setTxError(getErrorTextFromError(error, TxAction.MAIN_ACTION, true));
+ setMainTxState({
+ txHash: undefined,
+ loading: false,
+ success: false,
+ });
+ })
+ .then((address) => {
+ if (address) setPrecalculatedInstanceAddress(address);
+ });
+ }, [
+ user,
+ validTo,
+ state.sellAmountBigInt,
+ state.buyAmountBigInt,
+ state.sellAmountToken,
+ state.buyAmountToken,
+ state.processedSide,
+ state.slippage,
+ state.orderType,
+ state.chainId,
+ APP_CODE_PER_SWAP_TYPE[state.swapType],
+ ]);
+
+ // Approval is aToken ERC20 Approval
+ const amountToApprove = useMemo(() => {
+ if (!state.sellAmountFormatted || !state.sellAmountToken) return '0';
+ return calculateSignedAmount(state.sellAmountFormatted, state.sellAmountToken.decimals);
+ }, [state.sellAmountFormatted, state.sellAmountToken]);
+
+ const { hasActiveOrderForSellToken, trackSwapOrderProgress } = useSwapOrdersTracking();
+ const sellAssetAddress =
+ state.sellAmountToken?.underlyingAddress || state.sourceToken.addressToSwap;
+ const disablePermitDueToActiveOrder = hasActiveOrderForSellToken(state.chainId, sellAssetAddress);
+
+ // Approval is aToken ERC20 Approval
+ const {
+ requiresApproval,
+ approval,
+ tryPermit,
+ signatureParams,
+ loadingPermitData,
+ approvedAddress,
+ } = useSwapTokenApproval({
+ chainId: state.chainId,
+ token: state.destinationToken.addressToSwap, // aToken to repay with
+ symbol: state.destinationToken.symbol,
+ amount: normalize(amountToApprove.toString(), state.sellAmountToken?.decimals ?? 18),
+ decimals: state.destinationToken.decimals,
+ spender: precalculatedInstanceAddress,
+ setState,
+ allowPermit: !disablePermitDueToActiveOrder, // avoid nonce reuse if active order present
+ trackingHandlers,
+ swapType: state.swapType,
+ });
+
+ // Use centralized gas estimation
+ useSwapGasEstimation({
+ state,
+ setState,
+ requiresApproval,
+ requiresApprovalReset: state.requiresApprovalReset,
+ approvalTxState,
+ });
+
+ const action = async () => {
+ setMainTxState({
+ txHash: undefined,
+ loading: true,
+ });
+ setState({
+ actionsLoading: false,
+ });
+
+ try {
+ if (
+ !state.sellAmountBigInt ||
+ !state.sellAmountToken ||
+ !state.buyAmountBigInt ||
+ !state.buyAmountToken
+ )
+ return;
+
+ const tradingSdk = await getCowTradingSdkByChainIdAndAppCode(
+ state.chainId,
+ APP_CODE_PER_SWAP_TYPE[state.swapType]
+ );
+ const flashLoanSdk = await getCowFlashLoanSdk(state.chainId);
+
+ const buyAmountWithMarginForDustProtection = valueToBigNumber(
+ state.buyAmountBigInt.toString()
+ )
+ .multipliedBy(DUST_PROTECTION_MULTIPLIER)
+ .toFixed(0);
+
+ const collateralPermit = signatureParams
+ ? {
+ amount: signatureParams?.amount,
+ deadline: Number(signatureParams?.deadline),
+ v: signatureParams?.splitedSignature.v,
+ r: signatureParams?.splitedSignature.r,
+ s: signatureParams?.splitedSignature.s,
+ }
+ : undefined;
+
+ const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
+ flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
+ sellAmount: state.sellAmountBigInt,
+ });
+
+ // In Repay With Collateral, the order is inverted, we need to sell the collateral to repay with and do a BUY order to the repay amount
+ const limitOrder: LimitTradeParameters = {
+ sellToken: state.sellAmountToken.underlyingAddress,
+ sellTokenDecimals: state.sellAmountToken.decimals,
+ buyToken: state.buyAmountToken.underlyingAddress,
+ buyTokenDecimals: state.buyAmountToken.decimals,
+ sellAmount: sellAmountToSign.toString(),
+ buyAmount: buyAmountWithMarginForDustProtection.toString(),
+ kind: state.processedSide === 'buy' ? OrderKind.BUY : OrderKind.SELL,
+ quoteId: isCowProtocolRates(state.swapRate) ? state.swapRate?.quoteId : undefined,
+ validTo,
+ slippageBps: state.orderType == OrderType.MARKET ? Number(state.slippage) * 100 : undefined,
+ partnerFee: COW_PARTNER_FEE(state.sellAmountToken.symbol, state.buyAmountToken.symbol),
+ };
+
+ const orderToSign = getOrderToSign(
+ {
+ chainId: state.chainId,
+ from: user,
+ networkCostsAmount: '0',
+ isEthFlow: false,
+ applyCostsSlippageAndFees: false,
+ },
+ limitOrder,
+ HASH_ZERO
+ );
+
+ const orderPostParams = await flashLoanSdk.getOrderPostingSettings(
+ AaveFlashLoanType.RepayCollateral,
+ {
+ chainId: state.chainId,
+ validTo,
+ owner: user as `0x${string}`,
+ flashLoanFeeAmount,
+ },
+ {
+ sellAmount: state.sellAmountBigInt,
+ buyAmount: BigInt(buyAmountWithMarginForDustProtection),
+ orderToSign,
+ collateralPermit,
+ }
+ );
+
+ orderPostParams.swapSettings.appData = addOrderTypeToAppData(
+ state.orderType,
+ orderPostParams.swapSettings.appData
+ );
+
+ // Safe-check in case any param changed between approval and order posting
+ const instanceAddress = orderPostParams.instanceAddress;
+ if (instanceAddress !== approvedAddress) {
+ console.error(
+ 'Some parameters changed between approval and order posting: instanceAddress !== approvedAddress, asking for a new approval',
+ instanceAddress,
+ approvedAddress
+ );
+ // Force re-approve
+ setPrecalculatedInstanceAddress(instanceAddress);
+ setApprovalTxState({
+ txHash: undefined,
+ loading: false,
+ success: false,
+ });
+ setMainTxState({ txHash: undefined, loading: false, success: false });
+
+ return;
+ }
+
+ const result = await tradingSdk.postLimitOrder(limitOrder, orderPostParams.swapSettings);
+
+ trackingHandlers.trackSwap();
+ setMainTxState({
+ loading: false,
+ success: true,
+ txHash: result.orderId,
+ });
+ // Save to local history and start tracking status
+ saveCowOrderToUserHistory({
+ protocol: 'cow',
+ orderId: result.orderId,
+ status: OrderStatus.OPEN,
+ swapType: state.swapType,
+ chainId: state.chainId,
+ account: user,
+ timestamp: new Date().toISOString(),
+ srcToken: {
+ address: state.sellAmountToken.underlyingAddress,
+ symbol: state.sellAmountToken.symbol,
+ name: state.sellAmountToken.symbol,
+ decimals: state.sellAmountToken.decimals,
+ },
+ destToken: {
+ address: state.buyAmountToken.underlyingAddress,
+ symbol: state.buyAmountToken.symbol,
+ name: state.buyAmountToken.symbol,
+ decimals: state.buyAmountToken.decimals,
+ },
+ adapterInstanceAddress: instanceAddress,
+ usedAdapter: true, // RepayWithCollateral always uses adapter
+ srcAmount: state.sellAmountBigInt.toString(),
+ destAmount: state.buyAmountBigInt.toString(),
+ });
+ trackSwapOrderProgress(result.orderId, state.chainId);
+ setState({
+ actionsLoading: false,
+ });
+ } catch (error) {
+ console.error('RepayWithCollateralActionsViaCoW error', error);
+ setTxError(getErrorTextFromError(error, TxAction.MAIN_ACTION, true)); // TODO: Fix cannot copy error
+ setMainTxState({
+ txHash: undefined,
+ loading: false,
+ success: false,
+ });
+ setState({
+ actionsLoading: false,
+ });
+ }
+ };
+
+ return (
+ Checking approval
+ ) : (
+
+ Repay {state.sourceToken.symbol} with {state.destinationToken.symbol}
+
+ )
+ }
+ actionInProgressText={
+ approvalTxState.loading ? (
+ Checking approval
+ ) : (
+
+ Repaying {state.sourceToken.symbol} with {state.destinationToken.symbol}
+
+ )
+ }
+ errorParams={{
+ loading: false,
+ disabled:
+ areActionsBlocked(state) ||
+ approvalTxState.loading ||
+ (!approvalTxState.success && requiresApproval),
+ content: approvalTxState.loading ? (
+ Checking approval
+ ) : (
+
+ Repay {state.sourceToken.symbol} with {state.destinationToken.symbol}
+
+ ),
+ handleClick: action,
+ }}
+ fetchingData={state.actionsLoading || loadingPermitData}
+ blocked={areActionsBlocked(state) || !precalculatedInstanceAddress}
+ tryPermit={tryPermit}
+ permitInUse={disablePermitDueToActiveOrder}
+ />
+ );
+};
diff --git a/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaParaswap.tsx b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaParaswap.tsx
new file mode 100644
index 0000000000..3f0d8bce67
--- /dev/null
+++ b/src/components/transactions/Swap/actions/RepayWithCollateral/RepayWithCollateralActionsViaParaswap.tsx
@@ -0,0 +1,296 @@
+import { normalize, normalizeBN, valueToBigNumber } from '@aave/math-utils';
+import { OrderStatus } from '@cowprotocol/cow-sdk';
+import { Trans } from '@lingui/macro';
+import { BigNumber, PopulatedTransaction } from 'ethers';
+import { Dispatch } from 'react';
+import { calculateSignedAmount } from 'src/hooks/paraswap/common';
+import { useModalContext } from 'src/hooks/useModal';
+import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
+import { useRootStore } from 'src/store/root';
+import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
+import { saveParaswapTxToUserHistory } from 'src/utils/swapAdapterHistory';
+import { useShallow } from 'zustand/shallow';
+
+import { TxActionsWrapper } from '../../../TxActionsWrapper';
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import { getTransactionParams } from '../../helpers/paraswap';
+import { useSwapGasEstimation } from '../../hooks/useSwapGasEstimation';
+import {
+ areActionsBlocked,
+ isParaswapRates,
+ ProtocolSwapParams,
+ ProtocolSwapState,
+ SwapState,
+} from '../../types';
+import { useSwapTokenApproval } from '../approval/useSwapTokenApproval';
+
+/**
+ * Repay-with-collateral via ParaSwap Adapter.
+ *
+ * Flow summary:
+ * 1) Approve aToken (or use permit) to the RepayWithCollateral adapter
+ * 2) Build a ParaSwap route INVERTED relative to the UI: collateral aToken -> debt token
+ * - We invert because the protocol action consumes collateral to acquire the debt asset
+ * 3) Compute repay amounts with slippage; detect `repayAllDebt` when balance covers max with margin
+ * 4) Call adapter with swap calldata + optional permit to execute repay and residual handling
+ */
+export const RepayWithCollateralActionsViaParaswap = ({
+ params,
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: ProtocolSwapParams;
+ state: ProtocolSwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ const { setTxError, setMainTxState, approvalTxState } = useModalContext();
+ const { sendTx } = useWeb3Context();
+ const [paraswapRepayWithCollateral, currentMarketData, estimateGasLimit] = useRootStore(
+ useShallow((state) => [
+ state.paraswapRepayWithCollateral,
+ state.currentMarketData,
+ state.estimateGasLimit,
+ ])
+ );
+
+ const toRepaySelectedAmountFormatted = state.inputAmount;
+ const collateralToRepayWithAmountFormatted = state.outputAmount;
+ const collateralToRepayAmountToApprove = normalize(
+ calculateSignedAmount(
+ normalizeBN(
+ collateralToRepayWithAmountFormatted,
+ -state.destinationToken.decimals
+ ).toString(),
+ 0
+ // Adds margin to account future incremental so better ux
+ ),
+ state.destinationToken.decimals
+ );
+
+ // Approval is aToken ERC20 Approval
+ const {
+ requiresApproval,
+ signatureParams,
+ approval,
+ tryPermit,
+ approvedAmount,
+ loadingPermitData,
+ } = useSwapTokenApproval({
+ chainId: state.chainId,
+ token: state.destinationToken.addressToSwap, // aToken
+ symbol: state.destinationToken.symbol,
+ decimals: state.destinationToken.decimals,
+ amount: collateralToRepayAmountToApprove.toString(),
+ spender: currentMarketData.addresses.REPAY_WITH_COLLATERAL_ADAPTER,
+ setState,
+ trackingHandlers,
+ swapType: state.swapType,
+ });
+
+ // Use centralized gas estimation
+ useSwapGasEstimation({
+ state,
+ setState,
+ requiresApproval,
+ requiresApprovalReset: state.requiresApprovalReset,
+ approvalTxState,
+ });
+
+ const action = async () => {
+ if (!state.swapRate || !isParaswapRates(state.swapRate))
+ throw new Error('Route required to build transaction');
+
+ setMainTxState({
+ txHash: undefined,
+ loading: true,
+ });
+
+ try {
+ const tokenToRepayWithBalance = state.destinationToken.balance || '0';
+ let safeAmountToRepayAll = valueToBigNumber(state.sourceReserve.variableBorrows || '0');
+ // Add in the approximate interest accrued over the next 30 minutes
+ safeAmountToRepayAll = safeAmountToRepayAll.plus(
+ safeAmountToRepayAll
+ .multipliedBy(state.sourceReserve.reserve.variableBorrowAPY)
+ .dividedBy(360 * 24 * 2)
+ );
+
+ let repayAmount, repayWithAmount;
+ if (state.side === 'sell') {
+ // If sell order i want to repay exactly the input amount
+ repayAmount = state.isMaxSelected
+ ? safeAmountToRepayAll.toFixed(state.sourceToken.decimals)
+ : toRepaySelectedAmountFormatted;
+
+ // Account slippage to make sure we have enough collateral to repay with
+ repayWithAmount = valueToBigNumber(state.outputAmount || '0')
+ .multipliedBy(1 + Number(state.slippage) / 100)
+ .toFixed(state.destinationToken.decimals);
+ } else {
+ // If buy order i want use exactly the collateral to repay with amount
+ repayWithAmount = state.outputAmount;
+
+ // Account slippage to make sure we pay as much debt as possible
+ repayAmount = valueToBigNumber(state.inputAmount || '0')
+ .dividedBy(1 + Number(state.slippage) / 100)
+ .toFixed(state.sourceToken.decimals);
+ }
+
+ // The slippage is factored into the collateral amount because when we swap for 'exactOut', positive slippage is applied on the collateral amount.
+ const collateralAmountRequiredToCoverDebt = safeAmountToRepayAll
+ .multipliedBy(state.sourceReserve.reserve.priceInUSD)
+ .multipliedBy(100 + Number(state.slippage))
+ .dividedBy(100)
+ .dividedBy(state.destinationReserve.reserve.priceInUSD);
+
+ const repayAllDebt =
+ state.isMaxSelected &&
+ valueToBigNumber(tokenToRepayWithBalance).gte(collateralAmountRequiredToCoverDebt);
+
+ const invertedSide = state.side === 'sell' ? 'buy' : 'sell';
+
+ // Prepare Swap (inversed, from the collateral asset to the debt to repay asset)
+ const { swapCallData, augustus } = await getTransactionParams(
+ invertedSide,
+ state.chainId,
+ state.destinationToken.underlyingAddress,
+ state.destinationToken.decimals,
+ state.sourceToken.underlyingAddress,
+ state.sourceToken.decimals,
+ state.user,
+ state.swapRate.optimalRateData,
+ Number(state.slippage)
+ );
+
+ const txs = await paraswapRepayWithCollateral({
+ repayAllDebt,
+ repayAmount,
+ rateMode: params.interestMode,
+ repayWithAmount,
+
+ fromAssetData: state.destinationReserve.reserve,
+ poolReserve: state.sourceReserve.reserve,
+
+ symbol: state.sourceReserve.reserve.symbol,
+ isWrongNetwork: state.isWrongNetwork,
+ useFlashLoan: state.useFlashloan || false,
+ blocked: areActionsBlocked(state),
+ swapCallData,
+ augustus,
+ signature: signatureParams?.splitedSignature,
+ deadline: signatureParams?.deadline,
+ signedAmount: approvedAmount,
+ });
+
+ const actionTx = txs.find((tx) => ['DLP_ACTION'].includes(tx.txType));
+ if (!actionTx) throw new Error('Action tx not found');
+ const tx = await actionTx.tx();
+ const populatedTx: PopulatedTransaction = {
+ to: tx.to,
+ from: tx.from,
+ data: tx.data,
+ gasLimit: tx.gasLimit,
+ gasPrice: tx.gasPrice,
+ nonce: tx.nonce,
+ chainId: tx.chainId,
+ value: tx.value ? BigNumber.from(tx.value) : undefined,
+ };
+
+ const txWithGasEstimation = await estimateGasLimit(populatedTx, state.chainId);
+ const response = await sendTx(txWithGasEstimation);
+ await response.wait(1);
+ try {
+ saveParaswapTxToUserHistory({
+ protocol: 'paraswap',
+ txHash: response.hash,
+ swapType: state.swapType,
+ chainId: state.chainId,
+ status: OrderStatus.FULFILLED,
+ account: state.user,
+ timestamp: new Date().toISOString(),
+ srcToken: {
+ address: state.sourceToken.addressToSwap,
+ symbol: state.sourceToken.symbol,
+ name: state.sourceToken.symbol,
+ decimals: state.sourceToken.decimals,
+ },
+ destToken: {
+ address: state.destinationToken.addressToSwap,
+ symbol: state.destinationToken.symbol,
+ name: state.destinationToken.symbol,
+ decimals: state.destinationToken.decimals,
+ },
+ srcAmount: state.sellAmountBigInt?.toString() ?? '0',
+ destAmount: state.buyAmountBigInt?.toString() ?? '0',
+ });
+ } catch {}
+
+ trackingHandlers.trackSwap();
+ params.invalidateAppState();
+ setMainTxState({
+ txHash: response.hash,
+ loading: false,
+ success: true,
+ });
+ } catch (error) {
+ const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
+
+ // For gas estimation errors in Paraswap actions, show as warning instead of blocking error
+ if (parsedError.txAction === TxAction.GAS_ESTIMATION) {
+ setState({
+ actionsLoading: false,
+ warnings: [
+ {
+ message:
+ 'Gas estimation error: The swap could not be estimated. Try increasing slippage or changing the amount.',
+ },
+ ],
+ error: undefined, // Clear any existing errors
+ });
+ } else {
+ // For other errors, handle normally
+ setTxError(parsedError);
+ setState({
+ actionsLoading: false,
+ });
+ }
+
+ setMainTxState({
+ loading: false,
+ });
+
+ const reason = error instanceof Error ? error.message : undefined;
+ trackingHandlers.trackSwapFailed(reason);
+ }
+ };
+
+ return (
+ Repay {state.sourceReserve.reserve.symbol}}
+ actionInProgressText={Repaying {state.sourceReserve.reserve.symbol}}
+ fetchingData={state.ratesLoading || loadingPermitData}
+ errorParams={{
+ loading: false,
+ disabled: areActionsBlocked(state),
+ content: Repay {state.sourceReserve.reserve.symbol},
+ handleClick: action,
+ }}
+ tryPermit={tryPermit}
+ />
+ );
+};
diff --git a/src/components/transactions/Swap/actions/SwapActions/SwapActionsViaCoW.tsx b/src/components/transactions/Swap/actions/SwapActions/SwapActionsViaCoW.tsx
new file mode 100644
index 0000000000..f6743a4abe
--- /dev/null
+++ b/src/components/transactions/Swap/actions/SwapActions/SwapActionsViaCoW.tsx
@@ -0,0 +1,423 @@
+import {
+ calculateUniqueOrderId,
+ COW_PROTOCOL_VAULT_RELAYER_ADDRESS,
+ OrderKind,
+ SupportedChainId,
+} from '@cowprotocol/cow-sdk';
+import { Trans } from '@lingui/macro';
+import { BigNumber } from 'ethers';
+import stringify from 'json-stringify-deterministic';
+import { Dispatch, useMemo } from 'react';
+import { TxActionsWrapper } from 'src/components/transactions/TxActionsWrapper';
+import { isSmartContractWallet } from 'src/helpers/provider';
+import { useModalContext } from 'src/hooks/useModal';
+import { useSwapOrdersTracking } from 'src/hooks/useSwapOrdersTracking';
+import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
+import { getEthersProvider } from 'src/libs/web3-data-provider/adapters/EthersAdapter';
+import { useRootStore } from 'src/store/root';
+import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
+import { wagmiConfig } from 'src/ui-config/wagmiConfig';
+import { useShallow } from 'zustand/shallow';
+
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import { COW_APP_DATA } from '../../constants/cow.constants';
+import { APP_CODE_PER_SWAP_TYPE } from '../../constants/shared.constants';
+import {
+ getPreSignTransaction,
+ getUnsignerOrder,
+ isNativeToken,
+ populateEthFlowTx,
+ sendOrder,
+ uploadAppData,
+} from '../../helpers/cow';
+import { useSwapGasEstimation } from '../../hooks/useSwapGasEstimation';
+import {
+ areActionsBlocked,
+ ExpiryToSecondsMap,
+ isCowProtocolRates,
+ OrderType,
+ SwapParams,
+ SwapState,
+ TokenType,
+} from '../../types';
+import { useSwapTokenApproval } from '../approval/useSwapTokenApproval';
+
+/**
+ * Asset swap via CoW Protocol (Limit/Market orders).
+ *
+ * Process:
+ * 1) Ensure token approval (with permit when possible) for the CoW Relayer. Handles smart contract wallets and native token flows.
+ * 2) For tokens requiring approval, attempts onchain approval and reacts to possible failures or pending states.
+ * 3) For ERC-20s supporting permit, attempts signature path unless already approved.
+ * 4) Handles both normal EOA users and smart contract wallets (e.g. Gnosis Safe) with pre-sign or off-chain signatures as needed.
+ * 5) Posts order to the CoW API (off-chain) or submits on-chain pre-sign transaction, depending on user/wallet.
+ * 6) Tracks tx/analytics and updates transaction state accurately for UI.
+ *
+ * Automatically accounts for amount normalization, possible token decimal mismatches,
+ * error states in approvals or post order flow, and UI feedback for each path.
+ */
+export const SwapActionsViaCoW = ({
+ params,
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ const [user, estimateGasLimit, addTransaction] = useRootStore(
+ useShallow((state) => [state.account, state.estimateGasLimit, state.addTransaction])
+ );
+
+ const { mainTxState, loadingTxns, setMainTxState, setTxError, approvalTxState } =
+ useModalContext();
+
+ const { hasActiveOrderForSellToken } = useSwapOrdersTracking();
+
+ const disablePermitDueToActiveOrder = hasActiveOrderForSellToken(
+ state.chainId,
+ state.sourceToken.addressToSwap
+ );
+
+ const {
+ requiresApproval,
+ requiresApprovalReset,
+ approval,
+ tryPermit,
+ signatureParams,
+ loadingPermitData,
+ } = useSwapTokenApproval({
+ chainId: state.chainId,
+ token: state.sourceToken.addressToSwap,
+ symbol: state.sourceToken.symbol,
+ amount: state.sellAmountFormatted ?? '0',
+ decimals: state.sourceToken.decimals,
+ spender: isCowProtocolRates(state.swapRate)
+ ? COW_PROTOCOL_VAULT_RELAYER_ADDRESS[state.chainId as SupportedChainId]
+ : undefined,
+ setState,
+ allowPermit: !disablePermitDueToActiveOrder,
+ trackingHandlers,
+ swapType: state.swapType,
+ });
+
+ // Use centralized gas estimation
+ useSwapGasEstimation({
+ state,
+ setState,
+ requiresApproval,
+ requiresApprovalReset,
+ approvalTxState,
+ });
+
+ const validTo = useMemo(
+ () => Math.floor(Date.now() / 1000) + ExpiryToSecondsMap[state.expiry],
+ [state.expiry]
+ );
+
+ const { sendTx } = useWeb3Context();
+
+ const slippageInPercent = state.slippage;
+
+ const sellAmountAccountingCosts = state.sellAmountBigInt;
+ const buyAmountAccountingCosts = state.buyAmountBigInt;
+
+ const action = async () => {
+ if (!sellAmountAccountingCosts || !buyAmountAccountingCosts) {
+ return;
+ }
+
+ if (state.orderType === OrderType.LIMIT) {
+ if (state.sourceToken.tokenType === TokenType.NATIVE) {
+ // Disallow native as sell token in ALL limit orders (would require eth-flow and locked funds)
+ setTxError(
+ getErrorTextFromError(
+ new Error(
+ 'Native sell token is not supported in limit orders. Please use the wrapped token.'
+ ),
+ TxAction.MAIN_ACTION,
+ true
+ )
+ );
+ setState({ actionsLoading: false });
+ setMainTxState({ txHash: undefined, loading: false });
+ return;
+ }
+ }
+
+ setMainTxState({ ...mainTxState, loading: true });
+ if (isCowProtocolRates(state.swapRate)) {
+ if (state.useFlashloan) {
+ setTxError(
+ getErrorTextFromError(new Error('Please use flashloan'), TxAction.MAIN_ACTION, true)
+ );
+ setState({
+ actionsLoading: false,
+ });
+ setMainTxState({
+ txHash: undefined,
+ loading: false,
+ });
+ return;
+ }
+
+ try {
+ const provider = await getEthersProvider(wagmiConfig, { chainId: state.chainId });
+ const slippageBps =
+ state.orderType === OrderType.LIMIT ? 0 : Math.round(Number(slippageInPercent) * 100); // percent to bps
+ const smartSlippage = state.swapRate.suggestedSlippage == Number(slippageInPercent);
+ const appCode = APP_CODE_PER_SWAP_TYPE[params.swapType];
+
+ // If srcToken is native, we need to use the eth-flow instead of the orderbook
+ if (isNativeToken(state.sourceToken.addressToSwap)) {
+ const ethFlowTx = await populateEthFlowTx(
+ sellAmountAccountingCosts.toString(),
+ buyAmountAccountingCosts.toString(),
+ state.destinationToken.addressToSwap,
+ user,
+ validTo,
+ state.sourceToken.symbol,
+ state.destinationToken.symbol,
+ slippageBps,
+ smartSlippage,
+ appCode,
+ state.orderType,
+ state.swapRate.quoteId
+ );
+ const txWithGasEstimation = await estimateGasLimit(ethFlowTx, state.chainId);
+ let response;
+ try {
+ response = await sendTx(txWithGasEstimation);
+ addTransaction(
+ response.hash,
+ {
+ txState: 'success',
+ },
+ {
+ chainId: state.chainId,
+ }
+ );
+
+ setMainTxState({
+ loading: false,
+ success: true,
+ });
+
+ const unsignerOrder = await getUnsignerOrder({
+ sellAmount: sellAmountAccountingCosts.toString(),
+ buyAmount: buyAmountAccountingCosts.toString(),
+ dstToken: state.destinationToken.addressToSwap,
+ user,
+ chainId: state.chainId,
+ tokenFromSymbol: state.sourceToken.symbol,
+ tokenToSymbol: state.destinationToken.symbol,
+ slippageBps,
+ smartSlippage,
+ appCode,
+ orderType: state.orderType,
+ validTo,
+ });
+ const calculatedOrderId = await calculateUniqueOrderId(state.chainId, unsignerOrder);
+
+ await uploadAppData(
+ calculatedOrderId,
+ stringify(
+ COW_APP_DATA(
+ state.sourceToken.symbol,
+ state.destinationToken.symbol,
+ slippageBps,
+ smartSlippage,
+ state.orderType,
+ APP_CODE_PER_SWAP_TYPE[params.swapType]
+ )
+ ),
+ state.chainId
+ );
+
+ // CoW takes some time to index the order for 'eth-flow' orders
+ setTimeout(() => {
+ setMainTxState({
+ loading: false,
+ success: true,
+ txHash: calculatedOrderId,
+ });
+ }, 1000 * 30); // 30 seconds - if we set less than 30 seconds, the order is not indexed yet and CoW explorer will not find the order
+ } catch (error) {
+ setTxError(getErrorTextFromError(error, TxAction.MAIN_ACTION, false));
+ setMainTxState({
+ txHash: response?.hash,
+ loading: false,
+ });
+ setState({
+ actionsLoading: false,
+ });
+ if (response?.hash) {
+ addTransaction(
+ response?.hash,
+ {
+ txState: 'failed',
+ },
+ { chainId: state.chainId }
+ );
+ }
+ }
+ } else {
+ let orderId;
+ try {
+ if (await isSmartContractWallet(user, provider)) {
+ const preSignTransaction = await getPreSignTransaction({
+ provider,
+ validTo,
+ tokenDest: state.destinationToken.addressToSwap,
+ chainId: state.chainId,
+ user,
+ sellAmount: sellAmountAccountingCosts.toString(),
+ buyAmount: buyAmountAccountingCosts.toString(),
+ tokenSrc: state.sourceToken.addressToSwap,
+ tokenSrcDecimals: state.sourceToken.decimals,
+ tokenDestDecimals: state.destinationToken.decimals,
+ slippageBps,
+ smartSlippage,
+ inputSymbol: state.sourceToken.symbol,
+ outputSymbol: state.destinationToken.symbol,
+ quote: state.swapRate?.order,
+ appCode,
+ orderBookQuote: state.swapRate?.orderBookQuote,
+ orderType: state.orderType,
+ kind:
+ state.orderType === OrderType.MARKET
+ ? OrderKind.SELL
+ : state.side === 'buy'
+ ? OrderKind.BUY
+ : OrderKind.SELL,
+ signatureParams, // TODO: Test permit for smart contract wallets?
+ estimateGasLimit,
+ });
+
+ const response = await sendTx({
+ data: preSignTransaction.data,
+ to: preSignTransaction.to,
+ value: BigNumber.from(preSignTransaction.value),
+ gasLimit: BigNumber.from(preSignTransaction.gasLimit),
+ });
+
+ addTransaction(
+ response.hash,
+ {
+ txState: 'success',
+ },
+ {
+ chainId: state.chainId,
+ }
+ );
+
+ setMainTxState({
+ loading: false,
+ success: true,
+ txHash: preSignTransaction.orderId,
+ });
+ } else {
+ orderId = await sendOrder({
+ validTo,
+ tokenSrc: state.sourceToken.addressToSwap,
+ tokenSrcDecimals: state.sourceToken.decimals,
+ tokenDest: state.destinationToken.addressToSwap,
+ tokenDestDecimals: state.destinationToken.decimals,
+ quote: state.swapRate?.order,
+ sellAmount: sellAmountAccountingCosts.toString(),
+ buyAmount: buyAmountAccountingCosts.toString(),
+ slippageBps,
+ smartSlippage,
+ orderType: state.orderType,
+ kind:
+ state.orderType === OrderType.MARKET
+ ? OrderKind.SELL
+ : state.side === 'buy'
+ ? OrderKind.BUY
+ : OrderKind.SELL,
+ chainId: state.chainId,
+ user,
+ provider,
+ inputSymbol: state.sourceToken.symbol,
+ outputSymbol: state.destinationToken.symbol,
+ appCode,
+ orderBookQuote: state.swapRate?.orderBookQuote,
+ signatureParams,
+ estimateGasLimit,
+ });
+ setMainTxState({
+ loading: false,
+ success: true,
+ txHash: orderId ?? undefined,
+ });
+ }
+ } catch (error) {
+ console.error('SwapActionsViaCoW error', error);
+ const parsedError = getErrorTextFromError(error, TxAction.MAIN_ACTION, false);
+ setTxError(parsedError);
+ setMainTxState({
+ success: false,
+ loading: false,
+ });
+ setState({
+ actionsLoading: false,
+ });
+ }
+ }
+ } catch (error) {
+ console.error(error);
+ const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
+ setTxError(parsedError);
+ setMainTxState({
+ txHash: undefined,
+ loading: false,
+ success: false,
+ });
+ setState({
+ actionsLoading: false,
+ });
+ }
+ } else {
+ setTxError(
+ getErrorTextFromError(new Error('No sell rates found'), TxAction.MAIN_ACTION, true)
+ );
+ setState({
+ actionsLoading: false,
+ });
+ }
+
+ trackingHandlers.trackSwap();
+ };
+
+ return (
+ approval()}
+ requiresApproval={!areActionsBlocked(state) && requiresApproval}
+ actionText={Swap}
+ actionInProgressText={Swapping}
+ errorParams={{
+ loading: false,
+ disabled: areActionsBlocked(state) || (!approvalTxState.success && requiresApproval),
+ content: Swap,
+ handleClick: action,
+ }}
+ fetchingData={state.actionsLoading || loadingPermitData}
+ blocked={areActionsBlocked(state)}
+ tryPermit={tryPermit}
+ permitInUse={disablePermitDueToActiveOrder}
+ />
+ );
+};
diff --git a/src/components/transactions/Swap/actions/SwapActions/SwapActionsViaParaswap.tsx b/src/components/transactions/Swap/actions/SwapActions/SwapActionsViaParaswap.tsx
new file mode 100644
index 0000000000..f218f1f583
--- /dev/null
+++ b/src/components/transactions/Swap/actions/SwapActions/SwapActionsViaParaswap.tsx
@@ -0,0 +1,243 @@
+import { OrderStatus } from '@cowprotocol/cow-sdk';
+import { Trans } from '@lingui/macro';
+import { Dispatch } from 'react';
+import { TxActionsWrapper } from 'src/components/transactions/TxActionsWrapper';
+import { useParaswapSellTxParams } from 'src/hooks/paraswap/useParaswapRates';
+import { useModalContext } from 'src/hooks/useModal';
+import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
+import { useRootStore } from 'src/store/root';
+import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
+import { useShallow } from 'zustand/shallow';
+
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import { APP_CODE_PER_SWAP_TYPE } from '../../constants/shared.constants';
+import { useSwapGasEstimation } from '../../hooks/useSwapGasEstimation';
+import { areActionsBlocked, isParaswapRates, SwapParams, SwapState } from '../../types';
+import { useSwapTokenApproval } from '../approval/useSwapTokenApproval';
+
+/**
+ * Simple asset swap via ParaSwap Adapter (non-position flow).
+ * Prepares approval if needed and executes the route returned by useSwapQuote.
+ */
+export const SwapActionsViaParaswap = ({
+ params,
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ const [user, estimateGasLimit, addTransaction] = useRootStore(
+ useShallow((state) => [state.account, state.estimateGasLimit, state.addTransaction])
+ );
+
+ const { mainTxState, loadingTxns, setMainTxState, setTxError, approvalTxState } =
+ useModalContext();
+
+ const { sendTx } = useWeb3Context();
+ const { mutateAsync: fetchParaswapTxParams } = useParaswapSellTxParams(state.chainId);
+
+ const slippageInPercent = (Number(state.slippage) * 100).toString();
+
+ const {
+ requiresApproval,
+ requiresApprovalReset,
+ signatureParams,
+ approval,
+ tryPermit,
+ loadingPermitData,
+ } = useSwapTokenApproval({
+ chainId: state.chainId,
+ token: state.sourceToken.addressToSwap,
+ symbol: state.sourceToken.symbol,
+ amount: state.inputAmount,
+ decimals: state.sourceToken.decimals,
+ spender: isParaswapRates(state.swapRate)
+ ? state?.swapRate?.optimalRateData?.tokenTransferProxy
+ : undefined,
+ setState,
+ trackingHandlers,
+ swapType: state.swapType,
+ });
+
+ // Use centralized gas estimation
+ useSwapGasEstimation({
+ state,
+ setState,
+ requiresApproval,
+ requiresApprovalReset,
+ approvalTxState: { success: approvalTxState.success || false },
+ });
+
+ const action = async () => {
+ setMainTxState({ ...mainTxState, loading: true });
+ if (isParaswapRates(state.swapRate)) {
+ try {
+ const appCode = APP_CODE_PER_SWAP_TYPE[params.swapType];
+
+ // Normal switch using paraswap
+ const tx = await fetchParaswapTxParams({
+ srcToken: state.sourceToken.addressToSwap,
+ srcDecimals: state.swapRate.srcDecimals,
+ destDecimals: state.swapRate.destDecimals,
+ destToken: state.destinationToken.addressToSwap,
+ route: state.swapRate.optimalRateData,
+ user,
+ maxSlippage: Number(slippageInPercent),
+ permit: signatureParams && signatureParams.signature,
+ deadline: signatureParams && signatureParams.deadline,
+ partner: appCode,
+ });
+ tx.chainId = state.chainId;
+ const txWithGasEstimation = await estimateGasLimit(tx, state.chainId);
+ const response = await sendTx(txWithGasEstimation);
+ try {
+ await response.wait(1);
+ // Save Paraswap tx locally for history
+ try {
+ const { saveParaswapTxToUserHistory: addParaswapTx } = await import(
+ 'src/utils/swapAdapterHistory'
+ );
+ addParaswapTx({
+ protocol: 'paraswap',
+ txHash: response.hash,
+ swapType: params.swapType,
+ chainId: state.chainId,
+ account: user,
+ timestamp: new Date().toISOString(),
+ status: OrderStatus.FULFILLED,
+ srcToken: {
+ address: state.sourceToken.addressToSwap,
+ symbol: state.sourceToken.symbol,
+ name: state.sourceToken.symbol,
+ decimals: state.sourceToken.decimals,
+ },
+ destToken: {
+ address: state.destinationToken.addressToSwap,
+ symbol: state.destinationToken.symbol,
+ name: state.destinationToken.symbol,
+ decimals: state.destinationToken.decimals,
+ },
+ srcAmount: state.sellAmountBigInt?.toString() ?? '0',
+ destAmount: state.buyAmountBigInt?.toString() ?? '0',
+ });
+ // ParaSwap is atomic onchain; no toast tracking required
+ } catch {}
+ addTransaction(
+ response.hash,
+ {
+ txState: 'success',
+ },
+ {
+ chainId: state.chainId,
+ }
+ );
+ setMainTxState({
+ txHash: response.hash,
+ loading: false,
+ success: true,
+ });
+
+ params.invalidateAppState();
+ trackingHandlers.trackSwap();
+ } catch (error) {
+ // This is for transaction waiting errors, not gas estimation, so handle normally
+ const parsedError = getErrorTextFromError(error, TxAction.MAIN_ACTION, false);
+ setTxError(parsedError);
+ setMainTxState({
+ txHash: response.hash,
+ loading: false,
+ });
+ setState({
+ actionsLoading: false,
+ });
+ addTransaction(
+ response.hash,
+ {
+ txState: 'failed',
+ },
+ {
+ chainId: state.chainId,
+ }
+ );
+ }
+ } catch (error) {
+ const parsedError = getErrorTextFromError(error, TxAction.MAIN_ACTION, false);
+
+ // Check if this is a gas estimation error (from estimateGasLimit call)
+ // Gas estimation errors typically occur when estimateGasLimit fails
+ const errorMessage = parsedError.rawError?.message?.toLowerCase() || '';
+ const isGasEstimationError =
+ errorMessage.includes('gas') ||
+ errorMessage.includes('estimation') ||
+ (errorMessage.includes('execution reverted') && errorMessage.includes('estimation'));
+
+ // For gas estimation errors in Paraswap actions, show as warning instead of blocking error
+ if (isGasEstimationError) {
+ setState({
+ actionsLoading: false,
+ warnings: [
+ {
+ message:
+ 'Gas estimation error: The swap could not be estimated. Try increasing slippage or changing the amount.',
+ },
+ ],
+ error: undefined, // Clear any existing errors
+ });
+ } else {
+ // For other errors, handle normally
+ setTxError(parsedError);
+ setState({
+ actionsLoading: false,
+ });
+ }
+
+ setMainTxState({
+ txHash: undefined,
+ loading: false,
+ });
+
+ const reason = error instanceof Error ? error.message : 'Swap failed';
+ trackingHandlers.trackSwapFailed(reason);
+ }
+ } else {
+ setTxError(
+ getErrorTextFromError(new Error('No sell rates found'), TxAction.MAIN_ACTION, true)
+ );
+ setState({
+ actionsLoading: false,
+ });
+ }
+ };
+
+ return (
+ approval()}
+ requiresApproval={!areActionsBlocked(state) && requiresApproval}
+ actionText={Swap}
+ actionInProgressText={Swapping}
+ errorParams={{
+ loading: false,
+ disabled: areActionsBlocked(state) || (!approvalTxState.success && requiresApproval),
+ content: Swap,
+ handleClick: action,
+ }}
+ fetchingData={state.actionsLoading || loadingPermitData}
+ blocked={areActionsBlocked(state)}
+ tryPermit={tryPermit}
+ />
+ );
+};
diff --git a/src/components/transactions/Swap/actions/SwapActions/index.tsx b/src/components/transactions/Swap/actions/SwapActions/index.tsx
new file mode 100644
index 0000000000..bb327a39a0
--- /dev/null
+++ b/src/components/transactions/Swap/actions/SwapActions/index.tsx
@@ -0,0 +1,41 @@
+import { Dispatch } from 'react';
+
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import { SwapParams, SwapProvider, SwapState } from '../../types';
+import { SwapActionsViaCoW } from './SwapActionsViaCoW';
+import { SwapActionsViaParaswap } from './SwapActionsViaParaswap';
+
+export const SwapActions = ({
+ params,
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ switch (state.provider) {
+ case SwapProvider.COW_PROTOCOL:
+ return (
+
+ );
+ case SwapProvider.PARASWAP:
+ return (
+
+ );
+ default:
+ return null;
+ }
+};
diff --git a/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActions.tsx b/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActions.tsx
new file mode 100644
index 0000000000..5824f28dd4
--- /dev/null
+++ b/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActions.tsx
@@ -0,0 +1,41 @@
+import { Dispatch } from 'react';
+
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import { ProtocolSwapParams, ProtocolSwapState, SwapProvider, SwapState } from '../../types';
+import { WithdrawAndSwapActionsViaCoW } from './WithdrawAndSwapActionsViaCoW';
+import { WithdrawAndSwapActionsViaParaswap } from './WithdrawAndSwapActionsViaParaswap';
+
+export const WithdrawAndSwapActions = ({
+ params,
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: ProtocolSwapParams;
+ state: ProtocolSwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ switch (state.provider) {
+ case SwapProvider.COW_PROTOCOL:
+ return (
+
+ );
+ case SwapProvider.PARASWAP:
+ return (
+
+ );
+ default:
+ return null;
+ }
+};
diff --git a/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActionsViaCoW.tsx b/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActionsViaCoW.tsx
new file mode 100644
index 0000000000..ebc1fe8518
--- /dev/null
+++ b/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActionsViaCoW.tsx
@@ -0,0 +1,28 @@
+import { Dispatch } from 'react';
+
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import { ProtocolSwapParams, ProtocolSwapState, SwapState } from '../../types';
+import { SwapActionsViaCoW } from '../SwapActions/SwapActionsViaCoW';
+
+export const WithdrawAndSwapActionsViaCoW = ({
+ params,
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: ProtocolSwapParams;
+ state: ProtocolSwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ // Essentially an aToken to token swap without a flashloan
+
+ return (
+
+ );
+};
diff --git a/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActionsViaParaswap.tsx b/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActionsViaParaswap.tsx
new file mode 100644
index 0000000000..04ace480a6
--- /dev/null
+++ b/src/components/transactions/Swap/actions/WithdrawAndSwap/WithdrawAndSwapActionsViaParaswap.tsx
@@ -0,0 +1,236 @@
+import { normalize } from '@aave/math-utils';
+import { OrderStatus } from '@cowprotocol/cow-sdk';
+import { Trans } from '@lingui/macro';
+import { Dispatch, useEffect, useMemo } from 'react';
+import { TxActionsWrapper } from 'src/components/transactions/TxActionsWrapper';
+import { calculateSignedAmount } from 'src/hooks/paraswap/common';
+import { useModalContext } from 'src/hooks/useModal';
+import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
+import { useRootStore } from 'src/store/root';
+import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
+import { saveParaswapTxToUserHistory } from 'src/utils/swapAdapterHistory';
+import { useShallow } from 'zustand/shallow';
+
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import { getTransactionParams } from '../../helpers/paraswap';
+import { useSwapGasEstimation } from '../../hooks/useSwapGasEstimation';
+import {
+ areActionsBlocked,
+ isParaswapRates,
+ ProtocolSwapParams,
+ ProtocolSwapState,
+ SwapState,
+} from '../../types';
+import { useSwapTokenApproval } from '../approval/useSwapTokenApproval';
+
+export const WithdrawAndSwapActionsViaParaswap = ({
+ state,
+ setState,
+ params,
+ trackingHandlers,
+}: {
+ params: ProtocolSwapParams;
+ state: ProtocolSwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ const [withdrawAndSwitch, currentMarketData, estimateGasLimit, addTransaction] = useRootStore(
+ useShallow((state) => [
+ state.withdrawAndSwitch,
+ state.currentMarketData,
+ state.estimateGasLimit,
+ state.addTransaction,
+ ])
+ );
+
+ const { approvalTxState, mainTxState, setMainTxState, setTxError } = useModalContext();
+
+ const { sendTx } = useWeb3Context();
+
+ // Approval is aToken ERC20 Approval
+ const amountToApprove = useMemo(() => {
+ if (!state.sellAmountFormatted || !state.sellAmountToken) return '0';
+ return calculateSignedAmount(state.sellAmountFormatted, state.sellAmountToken.decimals);
+ }, [state.sellAmountFormatted, state.sellAmountToken]);
+
+ const { requiresApproval, signatureParams, approval, tryPermit, loadingPermitData } =
+ useSwapTokenApproval({
+ chainId: state.chainId,
+ token: state.sourceToken.addressToSwap, // aToken
+ symbol: state.sourceToken.symbol,
+ amount: normalize(amountToApprove.toString(), state.sourceToken?.decimals ?? 18),
+ decimals: state.sourceToken.decimals,
+ spender: currentMarketData.addresses.WITHDRAW_SWITCH_ADAPTER,
+ setState,
+ trackingHandlers,
+ swapType: state.swapType,
+ });
+
+ // Use centralized gas estimation
+ useSwapGasEstimation({
+ state,
+ setState,
+ requiresApproval,
+ requiresApprovalReset: state.requiresApprovalReset,
+ approvalTxState,
+ });
+
+ const action = async () => {
+ if (!state.swapRate || !isParaswapRates(state.swapRate)) {
+ console.error('No swap rate found');
+ return;
+ }
+
+ try {
+ setMainTxState({ ...mainTxState, loading: true });
+ const { swapCallData, augustus } = await getTransactionParams(
+ state.side,
+ state.chainId,
+ state.sourceToken.underlyingAddress,
+ state.sourceToken.decimals,
+ state.destinationToken.underlyingAddress,
+ state.destinationToken.decimals,
+ state.user,
+ state.swapRate.optimalRateData,
+ Number(state.slippage)
+ );
+
+ const tx = withdrawAndSwitch({
+ poolReserve: state.sourceReserve.reserve,
+ targetReserve: state.destinationReserve.reserve,
+ isMaxSelected: state.isMaxSelected,
+ amountToSwap: state.sellAmountBigInt?.toString() ?? '0',
+ amountToReceive: state.buyAmountBigInt?.toString() ?? '0',
+ augustus: augustus,
+ txCalldata: swapCallData,
+ signatureParams: {
+ signature: signatureParams?.plain ?? '',
+ deadline: signatureParams?.deadline ?? '',
+ amount: signatureParams?.amount ?? '',
+ },
+ });
+
+ const txDataWithGasEstimation = await estimateGasLimit(tx);
+ const response = await sendTx(txDataWithGasEstimation);
+ await response.wait(1);
+
+ trackingHandlers.trackSwap();
+ params.invalidateAppState();
+ saveParaswapTxToUserHistory({
+ protocol: 'paraswap',
+ txHash: response.hash,
+ swapType: state.swapType,
+ chainId: state.chainId,
+ account: state.user,
+ timestamp: new Date().toISOString(),
+ status: OrderStatus.FULFILLED,
+ srcToken: {
+ address: state.sourceToken.underlyingAddress,
+ symbol: state.sourceToken.symbol,
+ name: state.sourceToken.symbol,
+ decimals: state.sourceToken.decimals,
+ },
+ destToken: {
+ address: state.destinationToken.underlyingAddress,
+ symbol: state.destinationToken.symbol,
+ name: state.destinationToken.symbol,
+ decimals: state.destinationToken.decimals,
+ },
+ srcAmount: state.sellAmountBigInt?.toString() ?? '0',
+ destAmount: state.buyAmountBigInt?.toString() ?? '0',
+ });
+ addTransaction(
+ response.hash,
+ {
+ txState: 'success',
+ },
+ {
+ chainId: state.chainId,
+ }
+ );
+
+ setMainTxState({
+ txHash: response.hash,
+ loading: false,
+ success: true,
+ });
+ } catch (error) {
+ const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
+
+ // For gas estimation errors in Paraswap actions, show as warning instead of blocking error
+ if (parsedError.txAction === TxAction.GAS_ESTIMATION) {
+ setState({
+ actionsLoading: false,
+ warnings: [
+ {
+ message:
+ 'Gas estimation error: The swap could not be estimated. Try increasing slippage or changing the amount.',
+ },
+ ],
+ error: undefined, // Clear any existing errors
+ });
+ } else {
+ // For other errors, handle normally
+ setTxError(parsedError);
+ setState({
+ actionsLoading: false,
+ });
+ }
+
+ setMainTxState({
+ txHash: undefined,
+ loading: false,
+ });
+ const reason = error instanceof Error ? error.message : undefined;
+ trackingHandlers.trackSwapFailed(reason);
+ }
+ };
+
+ useEffect(() => {
+ if (state.mainTxState.success) {
+ trackingHandlers.trackSwap();
+ params.invalidateAppState();
+
+ addTransaction(
+ state.mainTxState.txHash || '',
+ {
+ txState: 'success',
+ },
+ {
+ chainId: state.chainId,
+ }
+ );
+
+ setMainTxState({
+ txHash: state.mainTxState.txHash || '',
+ loading: false,
+ success: true,
+ });
+ }
+ }, [state.mainTxState.success]);
+
+ return (
+ Withdraw and Swap}
+ actionInProgressText={Withdrawing and Swapping}
+ errorParams={{
+ loading: false,
+ disabled: areActionsBlocked(state) || !approvalTxState?.success,
+ content: Withdraw and Swap,
+ handleClick: action,
+ }}
+ fetchingData={state.actionsLoading || loadingPermitData}
+ blocked={areActionsBlocked(state)}
+ tryPermit={tryPermit}
+ />
+ );
+};
diff --git a/src/components/transactions/Swap/actions/approval/useSwapTokenApproval.ts b/src/components/transactions/Swap/actions/approval/useSwapTokenApproval.ts
new file mode 100644
index 0000000000..f982fef3af
--- /dev/null
+++ b/src/components/transactions/Swap/actions/approval/useSwapTokenApproval.ts
@@ -0,0 +1,455 @@
+import { ERC20Service } from '@aave/contract-helpers';
+import { normalizeBN, valueToBigNumber } from '@aave/math-utils';
+import { ethers } from 'ethers';
+import { defaultAbiCoder, splitSignature } from 'ethers/lib/utils';
+import { Dispatch, useEffect, useMemo, useRef, useState } from 'react';
+import { MOCK_SIGNED_HASH } from 'src/helpers/useTransactionHandler';
+import { calculateSignedAmount } from 'src/hooks/paraswap/common';
+import { useModalContext } from 'src/hooks/useModal';
+import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
+import { useRootStore } from 'src/store/root';
+import { ApprovalMethod } from 'src/store/walletSlice';
+import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
+import { isPermitSupportedWithFallback } from 'src/ui-config/permitConfig';
+import { getProvider } from 'src/utils/marketsAndNetworksConfig';
+import { needsUSDTApprovalReset } from 'src/utils/usdtHelpers';
+import { useShallow } from 'zustand/shallow';
+
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
+import { isNativeToken } from '../../helpers/cow';
+import { SwapState, SwapType } from '../../types';
+
+export type SwapTokenApprovalParams = {
+ chainId: number;
+ token: string;
+ decimals: number;
+ symbol: string;
+ amount: string;
+ spender?: string;
+ setState: Dispatch>;
+ allowPermit?: boolean;
+ margin?: number;
+ type?: 'approval' | 'delegation';
+ trackingHandlers?: TrackAnalyticsHandlers;
+ swapType: SwapType;
+};
+
+export type SignatureLike = {
+ r: string;
+ s: string;
+ _vs: string;
+ recoveryParam: number;
+ v: number;
+};
+export interface SignedParams {
+ plain: string;
+ signature: string;
+ splitedSignature: SignatureLike;
+ deadline: string;
+ amount: string;
+ approvedToken: string;
+}
+
+/**
+ * Custom React hook to handle token approval flow for swaps.
+ *
+ * Handles both “traditional” ERC-20 approvals and permit signatures, depending on token and chain support.
+ * - Determines if approval or approval reset is required for a given token, amount, and spender.
+ * - Exposes functions and state for triggering approvals, permits, and tracking their status.
+ * - Integrates with the modal and global stores for transaction state management.
+ * - Handles token-specific quirks (e.g., USDT approval reset) and margin calculations for edge cases.
+ *
+ * @param {object} params - Hook parameters.
+ * @param {number} params.chainId - Current chain ID.
+ * @param {string} params.token - Address of the token to approve.
+ * @param {string} params.symbol - Symbol of the token.
+ * @param {string} params.amount - Amount, as string formatter like '1.234567890', for which approval is requested.
+ * @param {number} params.decimals - Token decimals.
+ * @param {string} [params.spender] - Spender address, smart contract requiring approval.
+ * @param {Dispatch>} params.setState - State setter for updating SwapState.
+ * @param {boolean} [params.allowPermit=true] - Whether to allow permit signature flow if supported.
+ * @param {number} [params.margin=0] - Optional margin for approval checks (in token units).
+ * @param {"approval"|"delegation"} [params.type="approval"] - Approval type; "approval" for typical ERC-20, "delegation" for credit delegation.
+ *
+ * @returns {{
+ * requiresApproval: boolean; // Whether an approval transaction is needed.
+ * requiresApprovalReset: boolean; // Whether an approval "reset" to 0 is needed before the actual approval (e.g. for USDT).
+ * approval: () => Promise; // Function to trigger the approval transaction.
+ * tryPermit: () => Promise; // Function to attempt permit signature flow, if available.
+ * signatureParams?: SignedParams; // Details/signature object if permit is ready.
+ * }}
+ */
+export const useSwapTokenApproval = ({
+ chainId,
+ token,
+ symbol,
+ amount,
+ decimals,
+ spender,
+ setState,
+ allowPermit = true,
+ margin = 0,
+ type = 'approval',
+ trackingHandlers,
+ swapType,
+}: SwapTokenApprovalParams) => {
+ const [approvedAmount, setApprovedAmount] = useState();
+ const [approvedAddress, setApprovedAddress] = useState();
+ const [requiresApprovalReset, setRequiresApprovalReset] = useState(false);
+ const [signatureParams, setSignatureParams] = useState();
+ // Keep track of last fetched approval key (token:spender) to avoid duplicate calls for same pair
+ const lastFetchedApprovalKeyRef = useRef();
+
+ const { approvalTxState, setLoadingTxns, setTxError, setApprovalTxState } = useModalContext();
+ const { sendTx, signTxData } = useWeb3Context();
+ const [loadingPermitData, setLoadingPermitData] = useState(true);
+
+ const [
+ user,
+ generateApproval,
+ estimateGasLimit,
+ walletApprovalMethodPreference,
+ generateSignatureRequest,
+ getCreditDelegationApprovedAmount,
+ generateApproveDelegation,
+ generateCreditDelegationSignatureRequest,
+ ] = useRootStore(
+ useShallow((state) => [
+ state.account,
+ state.generateApproval,
+ state.estimateGasLimit,
+ state.walletApprovalMethodPreference,
+ state.generateSignatureRequest,
+ state.getCreditDelegationApprovedAmount,
+ state.generateApproveDelegation,
+ state.generateCreditDelegationSignatureRequest,
+ state.currentMarketData,
+ ])
+ );
+
+ const requiresApproval = useMemo(() => {
+ if (isNativeToken(token)) {
+ return false;
+ }
+
+ if (approvedAmount === undefined) {
+ return true;
+ }
+
+ if (approvedAmount === '-1' || amount === '0') {
+ return false;
+ }
+
+ return valueToBigNumber(approvedAmount).isLessThan(valueToBigNumber(amount));
+ }, [approvedAmount, amount, signatureParams, decimals]);
+
+ // Clear status if amount changes
+ useEffect(() => {
+ if (signatureParams || approvalTxState.success) {
+ setSignatureParams(undefined);
+ setApprovedAmount(undefined);
+ setApprovedAddress(undefined);
+ setApprovalTxState({
+ txHash: undefined,
+ loading: false,
+ success: false,
+ });
+ }
+ }, [amount]);
+
+ // Reset approval-related state when token/spender context changes to ensure fresh checks
+ useEffect(() => {
+ setSignatureParams(undefined);
+ setApprovedAmount(undefined);
+ lastFetchedApprovalKeyRef.current = undefined;
+ setApprovedAddress(undefined);
+ setApprovalTxState({ txHash: undefined, loading: false, success: false });
+ }, [token, spender, chainId, type]);
+
+ // Warning for USDT on Ethereum approval reset
+ useEffect(() => {
+ const amountToApprove = calculateSignedAmount(normalizeBN(amount, -decimals).toString(), 0);
+ const currentApproved = calculateSignedAmount(approvedAmount?.toString() || '0', decimals, 0);
+
+ let needsApprovalReset = false;
+ if (
+ needsUSDTApprovalReset(symbol, chainId, currentApproved, amountToApprove) &&
+ swapType == SwapType.Swap
+ ) {
+ needsApprovalReset = true;
+ setRequiresApprovalReset(true);
+ } else {
+ needsApprovalReset = false;
+ }
+
+ setRequiresApprovalReset(needsApprovalReset);
+ setState({ requiresApprovalReset: needsApprovalReset });
+ }, [symbol, chainId, approvedAmount, amount]);
+
+ const fetchApprovedAmountFromContract = async () => {
+ if (!spender || signatureParams) {
+ return;
+ }
+ setApprovalTxState({
+ txHash: undefined,
+ loading: false,
+ success: false,
+ });
+ setLoadingTxns(true);
+
+ const rpc = getProvider(chainId);
+ let approvedTargetAmount: string;
+ if (type === 'delegation') {
+ const creditDelegationApprovedAmount = await getCreditDelegationApprovedAmount({
+ debtTokenAddress: token,
+ delegatee: spender ?? '',
+ });
+ approvedTargetAmount = creditDelegationApprovedAmount.amount;
+ } else {
+ const erc20Service = new ERC20Service(rpc);
+ const erc20ApprovedAmount = await erc20Service.approvedAmount({
+ user,
+ token,
+ spender,
+ });
+ approvedTargetAmount = erc20ApprovedAmount.toString();
+ }
+
+ setApprovedAmount(approvedTargetAmount.toString());
+ setApprovedAddress(spender);
+ setLoadingTxns(false);
+ setState({
+ actionsLoading: false,
+ });
+ };
+
+ useEffect(() => {
+ if (!spender) return;
+ if (signatureParams) return; // skip after permit path
+ if (approvalTxState.loading || approvalTxState.success) return;
+
+ const approvalKey = `${token.toLowerCase()}:${spender.toLowerCase()}`;
+ if (lastFetchedApprovalKeyRef.current === approvalKey) return; // prevent duplicate fetches for same token/spender
+
+ lastFetchedApprovalKeyRef.current = approvalKey;
+ fetchApprovedAmountFromContract();
+ }, [token, spender, signatureParams, approvalTxState.loading, approvalTxState.success]);
+
+ const [permitSupported, setPermitSupported] = useState(undefined);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ setLoadingPermitData(true);
+ const rpc = getProvider(chainId);
+ const supported = await isPermitSupportedWithFallback(chainId, token, rpc);
+ if (!cancelled) setPermitSupported(supported);
+ setLoadingPermitData(false);
+ } catch {
+ if (!cancelled) setPermitSupported(false);
+ setLoadingPermitData(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [chainId, token]);
+
+ const tryPermit = allowPermit && permitSupported === true;
+ const usePermit = tryPermit && walletApprovalMethodPreference === ApprovalMethod.PERMIT;
+
+ const approval = async () => {
+ if (!spender) {
+ return;
+ }
+
+ const amountToApprove = calculateSignedAmount(
+ normalizeBN(amount, -decimals).toString(),
+ 0,
+ margin
+ );
+
+ // If requires approval reset, reset the approval first
+ if (requiresApprovalReset) {
+ try {
+ // Create direct ERC20 approval transaction for reset to 0 as ERC20Service requires positive amount
+ const abi = new ethers.utils.Interface([
+ 'function approve(address spender, uint256 amount)',
+ ]);
+ const encodedData = abi.encodeFunctionData('approve', [spender, '0']);
+ const resetTx = {
+ data: encodedData,
+ to: token,
+ };
+ const resetTxWithGasEstimation = await estimateGasLimit(resetTx, chainId);
+ setApprovalTxState({ ...approvalTxState, loading: true });
+ const resetResponse = await sendTx(resetTxWithGasEstimation);
+ await resetResponse.wait(1);
+ setState({ requiresApprovalReset: false });
+ } catch (error) {
+ const parsedError = getErrorTextFromError(error, TxAction.APPROVAL, false);
+ setTxError(parsedError);
+ setApprovalTxState({
+ txHash: undefined,
+ loading: false,
+ });
+ setState({
+ actionsLoading: false,
+ });
+ }
+ fetchApprovedAmountFromContract().then(() => {
+ setApprovalTxState({
+ loading: false,
+ success: false,
+ });
+ setState({
+ actionsLoading: false,
+ });
+ });
+
+ return; // Button will be updated to approve
+ }
+
+ const approvalData = {
+ spender,
+ user,
+ token,
+ amount: amountToApprove,
+ };
+
+ if (usePermit) {
+ // Permit approval
+ try {
+ const deadline = Math.floor(Date.now() / 1000 + 3600).toString();
+ let signatureRequest: string;
+ if (type === 'delegation') {
+ signatureRequest = await generateCreditDelegationSignatureRequest({
+ underlyingAsset: token,
+ deadline,
+ amount: amountToApprove.toString(),
+ spender,
+ });
+ } else {
+ signatureRequest = await generateSignatureRequest(
+ {
+ ...approvalData,
+ deadline,
+ },
+ { chainId: chainId }
+ );
+ }
+ setApprovalTxState({ ...approvalTxState, loading: true });
+ const response = await signTxData(signatureRequest);
+ const splitedSignature = splitSignature(response);
+ const encodedSignature =
+ type === 'delegation'
+ ? response.toString()
+ : defaultAbiCoder.encode(
+ ['address', 'address', 'uint256', 'uint256', 'uint8', 'bytes32', 'bytes32'],
+ [
+ approvalData.user,
+ approvalData.spender,
+ approvalData.amount,
+ deadline,
+ splitedSignature.v,
+ splitedSignature.r,
+ splitedSignature.s,
+ ]
+ );
+ const newSignatureParams = {
+ plain: response.toString(),
+ signature: encodedSignature,
+ splitedSignature,
+ deadline,
+ amount: approvalData.amount,
+ approvedToken: approvalData.spender,
+ };
+ setSignatureParams(newSignatureParams);
+ setState({
+ actionsLoading: false,
+ });
+
+ setApprovedAmount(amountToApprove.toString());
+ setApprovedAddress(spender);
+ setTxError(undefined);
+ setApprovalTxState({
+ txHash: MOCK_SIGNED_HASH,
+ loading: false,
+ success: true,
+ });
+ } catch (error) {
+ const parsedError = getErrorTextFromError(error, TxAction.APPROVAL, false);
+ setTxError(parsedError);
+ setApprovalTxState({
+ txHash: undefined,
+ loading: false,
+ });
+ setState({
+ actionsLoading: false,
+ });
+ }
+ } else {
+ // Direct ERC20 approval transaction
+ try {
+ let tx;
+ if (type === 'delegation') {
+ tx = generateApproveDelegation({
+ debtTokenAddress: token,
+ delegatee: spender ?? '',
+ amount: amountToApprove.toString(),
+ });
+ } else {
+ tx = generateApproval(approvalData, {
+ chainId: chainId,
+ amount: amountToApprove,
+ });
+ }
+ const txWithGasEstimation = await estimateGasLimit(tx, chainId);
+ setApprovalTxState({ loading: true });
+ const response = await sendTx(txWithGasEstimation);
+ await response.wait(1);
+ fetchApprovedAmountFromContract().then(() => {
+ setApprovalTxState({
+ txHash: response.hash,
+ loading: false,
+ success: true,
+ });
+ setTxError(undefined);
+ setState({
+ actionsLoading: false,
+ });
+ });
+ } catch (error) {
+ const parsedError = getErrorTextFromError(error, TxAction.APPROVAL, false);
+ setTxError(parsedError);
+ setApprovalTxState({
+ txHash: undefined,
+ loading: false,
+ });
+ setState({
+ actionsLoading: false,
+ });
+ }
+ }
+
+ // Stop loading quotes
+ setState({
+ quoteRefreshPaused: true,
+ quoteTimerPausedAt: Date.now(),
+ });
+
+ trackingHandlers?.trackApproval(amountToApprove.toString(), usePermit);
+ };
+
+ return {
+ requiresApproval,
+ requiresApprovalReset,
+ loadingPermitData,
+ signatureParams,
+ approval,
+ tryPermit,
+ approvedAmount,
+ approvedAddress,
+ };
+};
diff --git a/src/components/transactions/Swap/actions/index.ts b/src/components/transactions/Swap/actions/index.ts
new file mode 100644
index 0000000000..b676ca90a4
--- /dev/null
+++ b/src/components/transactions/Swap/actions/index.ts
@@ -0,0 +1,80 @@
+import React, { Dispatch } from 'react';
+
+import { TrackAnalyticsHandlers } from '../analytics/useTrackAnalytics';
+import {
+ isProtocolSwapParams,
+ isProtocolSwapState,
+ isTokensSwapParams,
+ isTokensSwapState,
+ SwapParams,
+ SwapState,
+ SwapType,
+} from '../types';
+import { ActionsBlocked } from './ActionsBlocked';
+import { ActionsLoading } from './ActionsSkeleton';
+import { CollateralSwapActions } from './CollateralSwap/CollateralSwapActions';
+import { DebtSwapActions } from './DebtSwap/DebtSwapActions';
+import { RepayWithCollateralActions } from './RepayWithCollateral/RepayWithCollateralActions';
+import { SwapActions } from './SwapActions';
+import { WithdrawAndSwapActions } from './WithdrawAndSwap/WithdrawAndSwapActions';
+
+/**
+ * Decides which action component to render for the current swap type.
+ * Shows skeleton/blocked states based on `SwapState` and guards against
+ * invalid combinations of params/state.
+ */
+export const BaseSwapActions = ({
+ params,
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ if (state.ratesLoading || state.actionsLoading || !state.isSwapFlowSelected) {
+ return React.createElement(ActionsLoading, { state });
+ }
+
+ if (state.error?.actionBlocked || !state.swapRate) {
+ return React.createElement(ActionsBlocked, { state });
+ }
+
+ if (params.swapType === SwapType.Swap && isTokensSwapParams(params) && isTokensSwapState(state)) {
+ return React.createElement(SwapActions, { params, state, setState, trackingHandlers });
+ } else if (isProtocolSwapParams(params) && isProtocolSwapState(state)) {
+ switch (params.swapType) {
+ case SwapType.CollateralSwap:
+ return React.createElement(CollateralSwapActions, {
+ params,
+ state,
+ setState,
+ trackingHandlers,
+ });
+ case SwapType.DebtSwap:
+ return React.createElement(DebtSwapActions, { params, state, setState, trackingHandlers });
+ case SwapType.RepayWithCollateral:
+ return React.createElement(RepayWithCollateralActions, {
+ params,
+ state,
+ setState,
+ trackingHandlers,
+ });
+ case SwapType.WithdrawAndSwap:
+ return React.createElement(WithdrawAndSwapActions, {
+ params,
+ state,
+ setState,
+ trackingHandlers,
+ });
+ default:
+ console.error(`Unsupported swap type`);
+ return null;
+ }
+ } else {
+ console.error(`Invalid swap params or state in actions`);
+ return null;
+ }
+};
diff --git a/src/components/transactions/Swap/analytics/constants.ts b/src/components/transactions/Swap/analytics/constants.ts
new file mode 100644
index 0000000000..6de0ad76bf
--- /dev/null
+++ b/src/components/transactions/Swap/analytics/constants.ts
@@ -0,0 +1,43 @@
+// Re-export the SWAP enum from the events file, for clarity within the analytics folder
+export { SWAP } from 'src/utils/events';
+
+export enum SwapInputChanges {
+ /// The user has changed the input amount
+ INPUT_AMOUNT = 'INPUT_AMOUNT',
+
+ /// The user has changed the output amount
+ OUTPUT_AMOUNT = 'OUTPUT_AMOUNT',
+
+ /// The user has changed the rate
+ RATE_CHANGE = 'RATE_CHANGE',
+
+ /// The user has switched the reserves
+ SWITCH_RESERVES = 'SWITCH_RESERVES',
+
+ /// The user has changed the slippage
+ SLIPPAGE = 'SLIPPAGE',
+
+ /// The user has changed the network
+ NETWORK = 'NETWORK',
+
+ /// The user has changed the input token
+ INPUT_TOKEN = 'INPUT_TOKEN',
+
+ /// The user has added a custom token
+ ADD_CUSTOM_TOKEN = 'ADD_CUSTOM_TOKEN',
+
+ /// The user has changed the output token
+ OUTPUT_TOKEN = 'OUTPUT_TOKEN',
+
+ /// The user has changed the order type
+ ORDER_TYPE = 'ORDER_TYPE',
+
+ /// The user has changed the expiry
+ EXPIRY = 'EXPIRY',
+
+ /// The user has changed the gas limit
+ GAS_LIMIT = 'GAS_LIMIT',
+
+ /// The user approved high price impact warning
+ HIGH_PRICE_IMPACT_CONFIRM = 'HIGH_PRICE_IMPACT_CONFIRM',
+}
diff --git a/src/components/transactions/Swap/analytics/state.helpers.ts b/src/components/transactions/Swap/analytics/state.helpers.ts
new file mode 100644
index 0000000000..1479685163
--- /dev/null
+++ b/src/components/transactions/Swap/analytics/state.helpers.ts
@@ -0,0 +1,137 @@
+import { TrackEventProperties } from 'src/store/analyticsSlice';
+
+import { isCowProtocolRates, SwapError, SwapQuoteType, SwapState } from '../types';
+import { SwapInputChanges } from './constants';
+
+export const swapStateToAnalyticsEventParams = (state: SwapState): TrackEventProperties => {
+ return {
+ // UI inputs info
+ chainId: state.chainId,
+ inputSymbol: state.sourceToken.symbol,
+ outputSymbol: state.destinationToken.symbol,
+ inputAmount: state.inputAmount,
+ inputAmountUSD: state.swapRate?.srcSpotUSD,
+ outputAmount: state.outputAmount,
+ outputAmountUSD: state.swapRate?.destSpotUSD,
+ slippage: state.slippage,
+
+ // Swap Order info
+ sellAmountFormatted: state.sellAmountFormatted,
+ sellAmountBigInt: state.sellAmountBigInt?.toString() ?? '',
+ sellAmountToken: state.sellAmountToken?.symbol ?? '',
+ buyAmountFormatted: state.buyAmountFormatted,
+ buyAmountBigInt: state.buyAmountBigInt?.toString() ?? '',
+ buyAmountToken: state.buyAmountToken?.symbol ?? '',
+ isInvertedSwap: state.isInvertedSwap,
+
+ // Swap context info
+ provider: state.provider,
+ expiry: state.expiry,
+ orderType: state.orderType,
+ gasLimit: state.gasLimit,
+ shouldUseFlashloan: state.useFlashloan,
+ useFlashloan: state.useFlashloan,
+ swapType: state.swapType,
+ txHash: state.mainTxState.txHash,
+ isMaxSelected: state.isMaxSelected,
+ pair: `${state.sourceToken.symbol}-${state.destinationToken.symbol}`,
+ side: state.side,
+ userIsSmartContractWallet: state.userIsSmartContractWallet,
+ userIsSafeWallet: state.userIsSafeWallet,
+ };
+};
+
+export const swapErrorToAnalyticsEventParams = (error: SwapError): TrackEventProperties => {
+ return {
+ errorMessage: error.message,
+ isActionBlocked: error.actionBlocked,
+ stage: error.stage,
+ };
+};
+
+export const swapQuoteToAnalyticsEventParams = (
+ state: SwapState,
+ swapQuote: SwapQuoteType
+): TrackEventProperties => {
+ return {
+ ...swapStateToAnalyticsEventParams(state),
+
+ quoteProvider: swapQuote.provider,
+ quoteSrcAmount: swapQuote.srcSpotAmount,
+ quoteSrcUSD: swapQuote.srcSpotUSD,
+ quoteDestAmount: swapQuote.destSpotAmount,
+ quoteDestUSD: swapQuote.destSpotUSD,
+ quoteSuggestedSlippage: swapQuote.suggestedSlippage,
+ ...(isCowProtocolRates(swapQuote)
+ ? {
+ quoteQuoteId: swapQuote.quoteId,
+ }
+ : {
+ // any?
+ }),
+ };
+};
+
+export const swapInputChangeToAnalyticsEventParams = (
+ state: SwapState,
+ fieldChange: SwapInputChanges,
+ newValue: string
+): TrackEventProperties => {
+ return {
+ ...swapStateToAnalyticsEventParams(state),
+ fieldChange,
+ newValue,
+ };
+};
+
+export const swapTrackApprovalToAnalyticsEventParams = (
+ state: SwapState,
+ approvalAmount: string,
+ viaPermit: boolean
+): TrackEventProperties => {
+ return {
+ ...swapStateToAnalyticsEventParams(state),
+ approvalAmount,
+ viaPermit,
+ };
+};
+
+export const swapTrackSwapToAnalyticsEventParams = (state: SwapState): TrackEventProperties => {
+ return {
+ ...swapStateToAnalyticsEventParams(state),
+ };
+};
+
+export const swapTrackSwapFilledToAnalyticsEventParams = (
+ state: SwapState,
+ executedSellAmount: string,
+ executedBuyAmount: string
+): TrackEventProperties => {
+ return {
+ ...swapStateToAnalyticsEventParams(state),
+ executedSellAmount,
+ executedSellAmountUSD: state.swapRate?.srcSpotUSD,
+ executedBuyAmount,
+ executedBuyAmountUSD: state.swapRate?.destSpotUSD,
+ };
+};
+
+export const swapTrackSwapFailedToAnalyticsEventParams = (
+ state: SwapState,
+ reason?: string
+): TrackEventProperties => {
+ return {
+ ...swapStateToAnalyticsEventParams(state),
+ ...(reason
+ ? { errorReason: String(reason).slice(0, 160) }
+ : state.error?.message
+ ? { errorReason: String(state.error.message).slice(0, 160) }
+ : {}),
+ };
+};
+
+export const swapUserDeniedToAnalyticsEventParams = (state: SwapState): TrackEventProperties => {
+ return {
+ ...swapStateToAnalyticsEventParams(state),
+ };
+};
diff --git a/src/components/transactions/Swap/analytics/useTrackAnalytics.ts b/src/components/transactions/Swap/analytics/useTrackAnalytics.ts
new file mode 100644
index 0000000000..487b641180
--- /dev/null
+++ b/src/components/transactions/Swap/analytics/useTrackAnalytics.ts
@@ -0,0 +1,63 @@
+import { useRootStore } from 'src/store/root';
+
+import { SwapError, SwapQuoteType, SwapState } from '../types';
+import { SWAP, SwapInputChanges } from './constants';
+import {
+ swapErrorToAnalyticsEventParams,
+ swapInputChangeToAnalyticsEventParams,
+ swapQuoteToAnalyticsEventParams,
+ swapTrackApprovalToAnalyticsEventParams,
+ swapTrackSwapFailedToAnalyticsEventParams,
+ swapTrackSwapFilledToAnalyticsEventParams,
+ swapTrackSwapToAnalyticsEventParams,
+ swapUserDeniedToAnalyticsEventParams,
+} from './state.helpers';
+
+export type TrackAnalyticsHandlers = {
+ trackSwapQuote: (isAutoRefreshed: boolean, swapQuote: SwapQuoteType) => void;
+ trackSwapError: (error: SwapError) => void;
+ trackUserDenied: () => void;
+ trackInputChange: (fieldChange: SwapInputChanges, newValue: string) => void;
+ trackApproval: (approvalAmount: string, viaPermit: boolean) => void;
+ trackSwap: () => void;
+ trackSwapFilled: (executedSellAmount: string, executedBuyAmount: string) => void;
+ trackSwapFailed: (reason?: string) => void;
+};
+
+/*
+ This hook handles all analytics for the swap component.
+ We track all the user journey through the swap component, including quote, input changes, errors, warnings, actions, etc.
+*/
+export const useHandleAnalytics = ({ state }: { state: SwapState }) => {
+ const trackEvent = useRootStore((store) => store.trackEvent);
+
+ return {
+ trackSwapQuote: (isAutoRefreshed: boolean, swapQuote: SwapQuoteType) =>
+ trackEvent(
+ isAutoRefreshed ? SWAP.QUOTE_REFRESHED : SWAP.QUOTE,
+ swapQuoteToAnalyticsEventParams(state, swapQuote)
+ ),
+ trackSwapError: (error: SwapError) =>
+ trackEvent(SWAP.ERROR, swapErrorToAnalyticsEventParams(error)),
+ trackUserDenied: () =>
+ trackEvent(SWAP.USER_DENIED, swapUserDeniedToAnalyticsEventParams(state)),
+ trackInputChange: (fieldChange: SwapInputChanges, newValue: string) =>
+ trackEvent(
+ SWAP.INPUT_CHANGES,
+ swapInputChangeToAnalyticsEventParams(state, fieldChange, newValue)
+ ),
+ trackApproval: (approvalAmount: string, viaPermit: boolean) =>
+ trackEvent(
+ SWAP.APPROVAL,
+ swapTrackApprovalToAnalyticsEventParams(state, approvalAmount, viaPermit)
+ ),
+ trackSwap: () => trackEvent(SWAP.SWAP, swapTrackSwapToAnalyticsEventParams(state)),
+ trackSwapFilled: (executedSellAmount: string, executedBuyAmount: string) =>
+ trackEvent(
+ SWAP.SWAP_FILLED,
+ swapTrackSwapFilledToAnalyticsEventParams(state, executedSellAmount, executedBuyAmount)
+ ),
+ trackSwapFailed: (reason?: string) =>
+ trackEvent(SWAP.SWAP_FAILED, swapTrackSwapFailedToAnalyticsEventParams(state, reason)),
+ };
+};
diff --git a/src/components/transactions/Swap/constants/cow.constants.ts b/src/components/transactions/Swap/constants/cow.constants.ts
new file mode 100644
index 0000000000..e79bb28f11
--- /dev/null
+++ b/src/components/transactions/Swap/constants/cow.constants.ts
@@ -0,0 +1,203 @@
+import { CowEnv, OrderClass, SupportedChainId } from '@cowprotocol/cow-sdk';
+import { AaveFlashLoanType } from '@cowprotocol/sdk-flash-loans';
+
+import { getAssetGroup } from '../helpers/shared/assetCorrelation.helpers';
+import { OrderType, SwapType } from '../types';
+
+export const HOOK_ADAPTER_PER_TYPE: Record> = {
+ [AaveFlashLoanType.CollateralSwap]: {
+ [SupportedChainId.MAINNET]: '0x029d584E847373B6373b01dfaD1a0C9BfB916382',
+ [SupportedChainId.GNOSIS_CHAIN]: '0x029d584E847373B6373b01dfaD1a0C9BfB916382',
+ [SupportedChainId.ARBITRUM_ONE]: '0x029d584E847373B6373b01dfaD1a0C9BfB916382',
+ [SupportedChainId.AVALANCHE]: '0x029d584E847373B6373b01dfaD1a0C9BfB916382',
+ [SupportedChainId.BNB]: '0x029d584E847373B6373b01dfaD1a0C9BfB916382',
+ [SupportedChainId.POLYGON]: '0x029d584E847373B6373b01dfaD1a0C9BfB916382',
+ [SupportedChainId.BASE]: '0x029d584E847373B6373b01dfaD1a0C9BfB916382',
+ [SupportedChainId.SEPOLIA]: '',
+ [SupportedChainId.LENS]: '',
+ [SupportedChainId.LINEA]: '',
+ [SupportedChainId.PLASMA]: '',
+ },
+ [AaveFlashLoanType.DebtSwap]: {
+ [SupportedChainId.MAINNET]: '0x73e7aF13Ef172F13d8FEfEbfD90C7A6530096344',
+ [SupportedChainId.GNOSIS_CHAIN]: '0x73e7aF13Ef172F13d8FEfEbfD90C7A6530096344',
+ [SupportedChainId.ARBITRUM_ONE]: '0x73e7aF13Ef172F13d8FEfEbfD90C7A6530096344',
+ [SupportedChainId.AVALANCHE]: '0x73e7aF13Ef172F13d8FEfEbfD90C7A6530096344',
+ [SupportedChainId.BNB]: '0x73e7aF13Ef172F13d8FEfEbfD90C7A6530096344',
+ [SupportedChainId.POLYGON]: '0x73e7aF13Ef172F13d8FEfEbfD90C7A6530096344',
+ [SupportedChainId.BASE]: '0x73e7aF13Ef172F13d8FEfEbfD90C7A6530096344',
+ [SupportedChainId.SEPOLIA]: '',
+ [SupportedChainId.LENS]: '',
+ [SupportedChainId.LINEA]: '',
+ [SupportedChainId.PLASMA]: '',
+ },
+ [AaveFlashLoanType.RepayCollateral]: {
+ [SupportedChainId.MAINNET]: '0xAc27F3f86e78B14721d07C4f9CE999285f9AAa06',
+ [SupportedChainId.GNOSIS_CHAIN]: '0xAc27F3f86e78B14721d07C4f9CE999285f9AAa06',
+ [SupportedChainId.ARBITRUM_ONE]: '0xAc27F3f86e78B14721d07C4f9CE999285f9AAa06',
+ [SupportedChainId.AVALANCHE]: '0xAc27F3f86e78B14721d07C4f9CE999285f9AAa06',
+ [SupportedChainId.BNB]: '0xAc27F3f86e78B14721d07C4f9CE999285f9AAa06',
+ [SupportedChainId.POLYGON]: '0xAc27F3f86e78B14721d07C4f9CE999285f9AAa06',
+ [SupportedChainId.BASE]: '0xAc27F3f86e78B14721d07C4f9CE999285f9AAa06',
+ [SupportedChainId.SEPOLIA]: '',
+ [SupportedChainId.LENS]: '',
+ [SupportedChainId.LINEA]: '',
+ [SupportedChainId.PLASMA]: '',
+ },
+};
+
+export const ADAPTER_FACTORY: Record = {
+ [SupportedChainId.MAINNET]: '0xdeCC46a4b09162F5369c5C80383AAa9159bCf192',
+ [SupportedChainId.GNOSIS_CHAIN]: '0xdeCC46a4b09162F5369c5C80383AAa9159bCf192',
+ [SupportedChainId.ARBITRUM_ONE]: '0xdeCC46a4b09162F5369c5C80383AAa9159bCf192',
+ [SupportedChainId.AVALANCHE]: '0xdeCC46a4b09162F5369c5C80383AAa9159bCf192',
+ [SupportedChainId.BNB]: '0xdeCC46a4b09162F5369c5C80383AAa9159bCf192',
+ [SupportedChainId.POLYGON]: '0xdeCC46a4b09162F5369c5C80383AAa9159bCf192',
+ [SupportedChainId.BASE]: '0xdeCC46a4b09162F5369c5C80383AAa9159bCf192',
+ [SupportedChainId.LENS]: '',
+ [SupportedChainId.LINEA]: '',
+ [SupportedChainId.PLASMA]: '',
+ [SupportedChainId.SEPOLIA]: '',
+};
+
+export const DUST_PROTECTION_MULTIPLIER = 1.001;
+
+export const COW_UNSUPPORTED_ASSETS: Partial<
+ Record>>
+> = {
+ // // For adapters we start supporting only base
+ // [SwapType.DebtSwap]: {
+ // [SupportedChainId.ARBITRUM_ONE]: 'ALL',
+ // [SupportedChainId.AVALANCHE]: 'ALL',
+ // [SupportedChainId.BNB]: 'ALL',
+ // [SupportedChainId.GNOSIS_CHAIN]: 'ALL',
+ // [SupportedChainId.MAINNET]: 'ALL',
+ // [SupportedChainId.POLYGON]: 'ALL',
+ // [SupportedChainId.SEPOLIA]: 'ALL',
+ // // Base is supported
+ // },
+ // [SwapType.CollateralSwap]: {
+ // [SupportedChainId.ARBITRUM_ONE]: 'ALL',
+ // [SupportedChainId.AVALANCHE]: 'ALL',
+ // [SupportedChainId.BNB]: 'ALL',
+ // [SupportedChainId.GNOSIS_CHAIN]: 'ALL',
+ // [SupportedChainId.MAINNET]: 'ALL',
+ // [SupportedChainId.POLYGON]: 'ALL',
+ // [SupportedChainId.SEPOLIA]: 'ALL',
+ // // Base is supported
+ // },
+ // [SwapType.RepayWithCollateral]: {
+ // [SupportedChainId.ARBITRUM_ONE]: 'ALL',
+ // [SupportedChainId.AVALANCHE]: 'ALL',
+ // [SupportedChainId.BNB]: 'ALL',
+ // [SupportedChainId.GNOSIS_CHAIN]: 'ALL',
+ // [SupportedChainId.MAINNET]: 'ALL',
+ // [SupportedChainId.POLYGON]: 'ALL',
+ // [SupportedChainId.SEPOLIA]: 'ALL',
+ // // Base is supported
+ // },
+ // // Specific assets that are not supported for certain chains across all swap types
+ // ['ALL']: {
+ // [SupportedChainId.POLYGON]: [
+ // '0x8eb270e296023e9d92081fdf967ddd7878724424'.toLowerCase(), // aPOLGHST not supported
+ // '0x38d693ce1df5aadf7bc62595a37d667ad57922e5'.toLowerCase(), // aPolEURS not supported
+ // '0xea1132120ddcdda2f119e99fa7a27a0d036f7ac9'.toLowerCase(), // aPolSTMATIC not supported
+ // '0x6533afac2e7bccb20dca161449a13a32d391fb00'.toLowerCase(), // aPolJEUR not supported
+ // '0x513c7e3a9c69ca3e22550ef58ac1c0088e918fff'.toLowerCase(), // aPolCRV not supported
+ // '0xebe517846d0f36eced99c735cbf6131e1feb775d'.toLowerCase(), // aPolMIMATIC not supported
+ // '0xc45a479877e1e9dfe9fcd4056c699575a1045daa'.toLowerCase(), // aPolSUSHI not supported
+ // '0x8437d7c167dfb82ed4cb79cd44b7a32a1dd95c77'.toLowerCase(), // aPolAGEUR not supported
+ // '0x724dc807b04555b71ed48a6896b6f41593b8c637'.toLowerCase(), // aPolDPI not supported
+ // '0x8ffdf2de812095b1d19cb146e4c004587c0a0692'.toLowerCase(), // aPolBAL not supported
+ // ],
+ // [SupportedChainId.AVALANCHE]: [
+ // '0x8eb270e296023e9d92081fdf967ddd7878724424'.toLowerCase(), // AVaMAI not supported
+ // '0x078f358208685046a11c85e8ad32895ded33a249'.toLowerCase(), // aVaWBTC not supported
+ // '0xc45a479877e1e9dfe9fcd4056c699575a1045daa'.toLowerCase(), // aVaFRAX not supported
+ // ],
+ // [SupportedChainId.GNOSIS_CHAIN]: [
+ // '0xedbc7449a9b594ca4e053d9737ec5dc4cbccbfb2'.toLowerCase(), // EURe USD Price not supported
+ // ],
+ // [SupportedChainId.ARBITRUM_ONE]: [
+ // '0x62fC96b27a510cF4977B59FF952Dc32378Cc221d'.toLowerCase(), // atBTC does not have good solver liquidity
+ // ],
+ // [SupportedChainId.BASE]: [
+ // '0x90072A4aA69B5Eb74984Ab823EFC5f91e90b3a72'.toLowerCase(), // alBTC does not have good solver liquidity
+ // ],
+ // [SupportedChainId.MAINNET]: [
+ // '0x00907f9921424583e7ffBfEdf84F92B7B2Be4977'.toLowerCase(), // aGHO not supported
+ // '0x18eFE565A5373f430e2F809b97De30335B3ad96A'.toLowerCase(), // aGHO not supported
+ // ],
+ // [SupportedChainId.SEPOLIA]: [
+ // '0xd190eF37dB51Bb955A680fF1A85763CC72d083D4'.toLowerCase(), // aGHO not supported
+ // ],
+ // },
+};
+
+export const CoWProtocolSupportedNetworks = [
+ SupportedChainId.MAINNET,
+ SupportedChainId.GNOSIS_CHAIN,
+ SupportedChainId.ARBITRUM_ONE,
+ SupportedChainId.BASE,
+ SupportedChainId.SEPOLIA,
+ SupportedChainId.AVALANCHE,
+ SupportedChainId.POLYGON,
+ SupportedChainId.BNB,
+] as const;
+
+export const isChainIdSupportedByCoWProtocol = (chainId: number): chainId is SupportedChainId => {
+ return CoWProtocolSupportedNetworks.includes(chainId);
+};
+
+export const COW_EVM_RECIPIENT = '0xC542C2F197c4939154017c802B0583C596438380';
+// export const COW_LENS_RECIPIENT = '0xce4eB8a1f6Bd0e0B9282102DC056B11E9D83b7CA';
+export const COW_PROTOCOL_ETH_FLOW_ADDRESS = '0xbA3cB449bD2B4ADddBc894D8697F5170800EAdeC';
+export const COW_PROTOCOL_ETH_FLOW_ADDRESS_STAGING = '0x04501b9b1D52e67f6862d157E00D13419D2D6E95';
+
+export const COW_PROTOCOL_ETH_FLOW_ADDRESS_BY_ENV = (env: CowEnv) => {
+ return env === 'staging' ? COW_PROTOCOL_ETH_FLOW_ADDRESS_STAGING : COW_PROTOCOL_ETH_FLOW_ADDRESS;
+};
+
+export const COW_CREATE_ORDER_ABI =
+ 'function createOrder((address,address,uint256,uint256,bytes32,uint256,uint32,bool,int64)) returns (bytes32)';
+
+export const COW_PARTNER_FEE = (tokenFromSymbol: string, tokenToSymbol: string) => ({
+ volumeBps: getAssetGroup(tokenFromSymbol) == getAssetGroup(tokenToSymbol) ? 15 : 25,
+ recipient: COW_EVM_RECIPIENT,
+});
+
+export const FLASH_LOAN_FEE_BPS = 5;
+export const VALID_TO_HALF_HOUR = Math.floor(Date.now() / 1000) + 60 * 30; // 30 minutes
+
+export const COW_APP_DATA = (
+ tokenFromSymbol: string,
+ tokenToSymbol: string,
+ slippageBips: number,
+ smartSlippage: boolean,
+ orderType: OrderType,
+ appCode: string,
+ hooks?: Record
+) => ({
+ appCode: appCode,
+ version: '1.4.0',
+ metadata: {
+ orderClass: {
+ orderClass: orderType === OrderType.LIMIT ? OrderClass.LIMIT : OrderClass.MARKET,
+ }, // for CoW Swap UI & Analytics
+ ...(orderType === OrderType.MARKET
+ ? { quote: { slippageBips, smartSlippage } }
+ : // Slippage is not used in limit orders
+ {}),
+ partnerFee: COW_PARTNER_FEE(tokenFromSymbol, tokenToSymbol),
+ hooks,
+ },
+});
+
+// TODO: Optimize CoW Values
+export const COW_PROTOCOL_GAS_LIMITS: Record = {
+ [SwapType.Swap]: 1000000, // only eth-flow and smart contract wallets
+ [SwapType.CollateralSwap]: 1000000, // only if non-flashloan
+ [SwapType.DebtSwap]: 0,
+ [SwapType.RepayWithCollateral]: 0,
+ [SwapType.WithdrawAndSwap]: 0,
+};
diff --git a/src/components/transactions/Swap/constants/limitOrders.constants.ts b/src/components/transactions/Swap/constants/limitOrders.constants.ts
new file mode 100644
index 0000000000..5e6f6d09aa
--- /dev/null
+++ b/src/components/transactions/Swap/constants/limitOrders.constants.ts
@@ -0,0 +1,26 @@
+const ONE_MINUTE_IN_SECONDS = 60;
+const ONE_HOUR_IN_SECONDS = 3600;
+const ONE_DAY_IN_SECONDS = 86400;
+const ONE_MONTH_IN_SECONDS = 2592000;
+
+export enum Expiry {
+ TEN_MINUTES = '10 minutes',
+ HALF_HOUR = 'Half hour',
+ ONE_HOUR = 'One hour',
+ ONE_DAY = 'One day',
+ ONE_WEEK = 'One week',
+ ONE_MONTH = 'One month',
+ THREE_MONTHS = 'Three months',
+ ONE_YEAR = 'One year',
+}
+
+export const ExpiryToSecondsMap = {
+ [Expiry.TEN_MINUTES]: ONE_MINUTE_IN_SECONDS * 10,
+ [Expiry.HALF_HOUR]: ONE_HOUR_IN_SECONDS / 2,
+ [Expiry.ONE_HOUR]: ONE_HOUR_IN_SECONDS,
+ [Expiry.ONE_DAY]: ONE_DAY_IN_SECONDS,
+ [Expiry.ONE_WEEK]: 7 * ONE_DAY_IN_SECONDS,
+ [Expiry.ONE_MONTH]: ONE_MONTH_IN_SECONDS,
+ [Expiry.THREE_MONTHS]: 3 * ONE_MONTH_IN_SECONDS,
+ [Expiry.ONE_YEAR]: 12 * ONE_MONTH_IN_SECONDS,
+};
diff --git a/src/components/transactions/Swap/constants/paraswap.constants.ts b/src/components/transactions/Swap/constants/paraswap.constants.ts
new file mode 100644
index 0000000000..78bbd165b2
--- /dev/null
+++ b/src/components/transactions/Swap/constants/paraswap.constants.ts
@@ -0,0 +1,27 @@
+import { ChainId } from '@aave/contract-helpers';
+
+import { SwapType } from '../types';
+
+export const ParaswapSupportedNetworks = [
+ ChainId.mainnet,
+ ChainId.polygon,
+ ChainId.avalanche,
+ ChainId.sepolia,
+ ChainId.base,
+ ChainId.arbitrum_one,
+ ChainId.optimism,
+ ChainId.xdai,
+ ChainId.bnb,
+ ChainId.sonic,
+];
+
+export const PARASWAP_FLASH_LOAN_FEE_BPS = 5;
+
+// TODO: Optimize Paraswap Values
+export const PARASWAP_GAS_LIMITS: Record = {
+ [SwapType.Swap]: 1000000,
+ [SwapType.CollateralSwap]: 1000000,
+ [SwapType.DebtSwap]: 400000,
+ [SwapType.RepayWithCollateral]: 700000,
+ [SwapType.WithdrawAndSwap]: 1000000,
+};
diff --git a/src/components/transactions/Swap/constants/shared.constants.ts b/src/components/transactions/Swap/constants/shared.constants.ts
new file mode 100644
index 0000000000..3bf8a2aa45
--- /dev/null
+++ b/src/components/transactions/Swap/constants/shared.constants.ts
@@ -0,0 +1,25 @@
+import { SwapType } from '../types';
+
+export const SAFETY_MODULE_TOKENS = [
+ 'stkgho',
+ 'stkaave',
+ 'stkaavewstethbptv2',
+ 'stkbptv2',
+ 'stkbpt',
+ 'stkabpt',
+];
+
+export const LIQUIDATION_SAFETY_THRESHOLD = 1.05;
+export const LIQUIDATION_DANGER_THRESHOLD = 1.01;
+export const SESSION_STORAGE_EXPIRY_MS = 15 * 60 * 1000;
+
+// TODO: Do we want one per swap type to analyze analytics?
+export const APP_CODE_PER_SWAP_TYPE: Record = {
+ [SwapType.Swap]: 'aave-v3-interface-widget',
+ [SwapType.CollateralSwap]: 'aave-v3-interface-collateral-swap',
+ [SwapType.DebtSwap]: 'aave-v3-interface-debt-swap',
+ [SwapType.RepayWithCollateral]: 'aave-v3-interface-repay-with-collateral',
+ [SwapType.WithdrawAndSwap]: 'aave-v3-interface-withdraw-and-swap',
+};
+
+export const APP_CODE_VALUES = Object.values(APP_CODE_PER_SWAP_TYPE);
diff --git a/src/components/transactions/Swap/details/CollateralSwapDetails.tsx b/src/components/transactions/Swap/details/CollateralSwapDetails.tsx
new file mode 100644
index 0000000000..320ad90153
--- /dev/null
+++ b/src/components/transactions/Swap/details/CollateralSwapDetails.tsx
@@ -0,0 +1,313 @@
+import { valueToBigNumber } from '@aave/math-utils';
+import { ArrowNarrowRightIcon } from '@heroicons/react/outline';
+import { Trans } from '@lingui/macro';
+import { Box, Skeleton, SvgIcon, Typography } from '@mui/material';
+import React from 'react';
+import { DarkTooltip } from 'src/components/infoTooltips/DarkTooltip';
+import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
+import { Row } from 'src/components/primitives/Row';
+import { TokenIcon } from 'src/components/primitives/TokenIcon';
+import { CollateralType } from 'src/helpers/types';
+import {
+ ComputedReserveData,
+ ComputedUserReserveData,
+ useAppDataContext,
+} from 'src/hooks/app-data-provider/useAppDataProvider';
+import { getDebtCeilingData } from 'src/hooks/useAssetCaps';
+import { calculateHFAfterSwap } from 'src/utils/hfUtils';
+
+import {
+ CollateralState,
+ DetailsHFLine,
+ DetailsIncentivesLine,
+ DetailsNumberLine,
+ TxModalDetails,
+} from '../../FlowCommons/TxModalDetails';
+import { getAssetCollateralType } from '../../utils';
+import { SwapParams, SwapProvider, SwapState } from '../types';
+import { CowCostsDetails } from './CowCostsDetails';
+import { ParaswapCostsDetails } from './ParaswapCostsDetails';
+
+export const ColalteralSwapDetails = ({ state }: { params: SwapParams; state: SwapState }) => {
+ const { user, reserves } = useAppDataContext();
+
+ if (!state.swapRate || !user) {
+ return null;
+ }
+
+ // Map selected tokens to reserves and user reserves
+ const poolReserve = reserves.find(
+ (r) => r.underlyingAsset.toLowerCase() === state.sourceToken.underlyingAddress.toLowerCase()
+ ) as ComputedReserveData | undefined;
+ const targetReserve = reserves.find(
+ (r) =>
+ r.underlyingAsset.toLowerCase() === state.destinationToken.underlyingAddress.toLowerCase()
+ ) as ComputedReserveData | undefined;
+
+ if (!poolReserve || !targetReserve || !user) {
+ console.error(
+ 'Pool reserve or target reserve or user not found',
+ state.sourceToken.underlyingAddress,
+ state.destinationToken.underlyingAddress
+ );
+ return null;
+ }
+
+ const userReserve = user.userReservesData.find(
+ (ur) => ur.underlyingAsset.toLowerCase() === poolReserve.underlyingAsset.toLowerCase()
+ ) as ComputedUserReserveData | undefined;
+ const userTargetReserve = user.userReservesData.find(
+ (ur) => ur.underlyingAsset.toLowerCase() === targetReserve.underlyingAsset.toLowerCase()
+ ) as ComputedUserReserveData | undefined;
+
+ if (!userReserve || !userTargetReserve) {
+ return null;
+ }
+
+ // Show HF only when there are borrows and source reserve is collateralizable
+ const showHealthFactor =
+ user.totalBorrowsMarketReferenceCurrency !== '0' &&
+ poolReserve.reserveLiquidationThreshold !== '0';
+
+ const fromAmount = state.sellAmountFormatted ?? '0';
+ const toAmount = state.buyAmountFormatted ?? '0';
+
+ // Compute collateral types
+ const { debtCeilingReached: sourceDebtCeiling } = getDebtCeilingData(targetReserve);
+ const swapSourceCollateralType: CollateralType = getAssetCollateralType(
+ userReserve,
+ user.totalCollateralUSD,
+ user.isInIsolationMode,
+ sourceDebtCeiling
+ );
+ const { debtCeilingReached: targetDebtCeiling } = getDebtCeilingData(targetReserve);
+ const swapTargetCollateralType: CollateralType = getAssetCollateralType(
+ userTargetReserve,
+ user.totalCollateralUSD,
+ user.isInIsolationMode,
+ targetDebtCeiling
+ );
+
+ // Health factor after swap using slippage-adjusted output amount
+ const { hfAfterSwap } = calculateHFAfterSwap({
+ fromAmount,
+ fromAssetData: poolReserve,
+ fromAssetUserData: userReserve,
+ user,
+ toAmountAfterSlippage: valueToBigNumber(toAmount || '0'),
+ toAssetData: targetReserve,
+ fromAssetType: 'collateral',
+ toAssetType: 'collateral',
+ });
+
+ const sourceAmountAfterSwap = valueToBigNumber(userReserve.underlyingBalance).minus(
+ valueToBigNumber(fromAmount)
+ );
+
+ const targetAmountAfterSwap = valueToBigNumber(userTargetReserve.underlyingBalance).plus(
+ valueToBigNumber(toAmount || '0')
+ );
+
+ const skeleton: JSX.Element = (
+ <>
+
+
+ >
+ );
+
+ const showBalance = true;
+
+ return (
+
+ {state.provider === SwapProvider.COW_PROTOCOL && }
+ {state.provider === SwapProvider.PARASWAP && }
+
+ {swapSourceCollateralType !== swapTargetCollateralType && (
+ Collateralization} captionVariant="description" mb={4}>
+
+ {state.ratesLoading ? (
+
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ )}
+ {hfAfterSwap && (
+
+ )}
+ Supply apy}
+ value={userReserve.reserve.supplyAPY}
+ futureValue={userTargetReserve.reserve.supplyAPY}
+ percent
+ loading={state.ratesLoading}
+ />
+
+ Liquidation threshold}
+ value={userReserve.reserve.formattedReserveLiquidationThreshold}
+ futureValue={userTargetReserve.reserve.formattedReserveLiquidationThreshold}
+ percent
+ visibleDecimals={0}
+ loading={state.ratesLoading}
+ />
+
+ {showBalance && (
+ Supply balance after switch}
+ captionVariant="description"
+ mb={4}
+ align="flex-start"
+ >
+
+
+ {state.ratesLoading ? (
+ skeleton
+ ) : (
+ <>
+
+
+
+ {sourceAmountAfterSwap.toString()} {userReserve.reserve.symbol}
+
+ }
+ arrow
+ placement="top"
+ enterTouchDelay={100}
+ leaveTouchDelay={500}
+ >
+
+
+
+
+
+
+ >
+ )}
+
+
+
+ {state.ratesLoading ? (
+ skeleton
+ ) : (
+ <>
+
+
+
+ {targetAmountAfterSwap.toString()} {userTargetReserve.reserve.symbol}
+
+ }
+ arrow
+ placement="top"
+ enterTouchDelay={100}
+ leaveTouchDelay={500}
+ >
+
+
+
+
+
+
+ >
+ )}
+
+
+
+ )}
+
+ );
+};
diff --git a/src/components/transactions/Swap/details/CowCostsDetails.tsx b/src/components/transactions/Swap/details/CowCostsDetails.tsx
new file mode 100644
index 0000000000..4b4e35c827
--- /dev/null
+++ b/src/components/transactions/Swap/details/CowCostsDetails.tsx
@@ -0,0 +1,256 @@
+import { normalize, valueToBigNumber } from '@aave/math-utils';
+import { Trans } from '@lingui/macro';
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import { Accordion, AccordionDetails, AccordionSummary, Box } from '@mui/material';
+import { useState } from 'react';
+import { EstimatedCostsForLimitSwapTooltip } from 'src/components/infoTooltips/EstimatedCostsForLimitSwap';
+import { ExecutionFeeTooltip } from 'src/components/infoTooltips/ExecutionFeeTooltip';
+import { NetworkCostTooltip } from 'src/components/infoTooltips/NetworkCostTooltip';
+import { SwapFeeTooltip } from 'src/components/infoTooltips/SwapFeeTooltip';
+import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
+import { Row } from 'src/components/primitives/Row';
+import { ExternalTokenIcon } from 'src/components/primitives/TokenIcon';
+
+import { calculateFlashLoanAmounts } from '../helpers/cow/adapters.helpers';
+import { isCowProtocolRates, OrderType, SwapState } from '../types';
+
+export const CowCostsDetails = ({ state }: { state: SwapState }) => {
+ const [costBreakdownExpanded, setCostBreakdownExpanded] = useState(false);
+
+ if (!state.swapRate || !isCowProtocolRates(state.swapRate)) return null;
+
+ // Prefer unified values exported on state; fall back to raw swapRate if missing
+ const networkFeeFormatted = state.networkFeeAmountInSellFormatted || '0';
+
+ const networkFeeUsd =
+ Number(networkFeeFormatted) *
+ (!state.isInvertedSwap ? state.swapRate.srcTokenPriceUsd : state.swapRate.destTokenPriceUsd);
+ const networkFeeToken = !state.isInvertedSwap ? state.sourceToken : state.destinationToken;
+
+ // If using flash-loan via CoW we need to account for the flash-loan fee
+ const flashloanFeeFormatted = normalize(
+ calculateFlashLoanAmounts(state).flashLoanFeeAmount.toString(),
+ state.sellAmountToken?.decimals ?? 18
+ );
+ const flashLoanFeeTokenPriceUnitUsd = valueToBigNumber(state.sellAmountUSD ?? '0')
+ .dividedBy(valueToBigNumber(state.sellAmountFormatted ?? '0'))
+ .toNumber();
+ const flashloanFeeUsd = Number(flashloanFeeFormatted) * flashLoanFeeTokenPriceUnitUsd;
+ const flashloanFeeToken = state.sellAmountToken;
+
+ if (!state.buyAmountToken || !state.sellAmountToken) return null;
+
+ // Partner fee is applied to the surplus token:
+ // - For sell orders: fee in buy token (destinationToken), deducted from buy amount
+ // - For buy orders: fee in sell token (sourceToken), added to sell amount
+ // For Debt and Repay with collateral, the swap is inverted to our UI
+ const invertedSide = state.processedSide;
+ let partnerFeeFormatted: string,
+ partnerFeeUsd: number,
+ partnerFeeToken: typeof state.buyAmountToken | typeof state.sellAmountToken;
+ if (invertedSide === 'buy') {
+ // Fee in destination token (buy token)
+ partnerFeeFormatted = state.partnerFeeAmountFormatted ?? '0';
+ const partnerFeeAmountPriceUnitUsd =
+ state.sellAmountFormatted == '0'
+ ? 0
+ : valueToBigNumber(state.sellAmountUSD ?? '0')
+ .dividedBy(valueToBigNumber(state.sellAmountFormatted ?? '0'))
+ .toNumber();
+ partnerFeeUsd = Number(partnerFeeFormatted) * partnerFeeAmountPriceUnitUsd;
+ partnerFeeToken = state.sellAmountToken;
+ } else {
+ // Fee in source token (sell token)
+ partnerFeeFormatted = state.partnerFeeAmountFormatted || '0';
+
+ const partnerFeeAmountPriceUnitUsd =
+ state.buyAmountFormatted == '0'
+ ? 0
+ : valueToBigNumber(state.buyAmountUSD ?? '0')
+ .dividedBy(valueToBigNumber(state.buyAmountFormatted ?? '0'))
+ .toNumber();
+
+ partnerFeeUsd = Number(partnerFeeFormatted) * partnerFeeAmountPriceUnitUsd;
+ partnerFeeToken = state.buyAmountToken;
+ }
+
+ const totalCostsInUsd = networkFeeUsd + partnerFeeUsd + (flashloanFeeUsd ?? 0); // + costs.slippageInUsd;
+
+ return (
+ {
+ setCostBreakdownExpanded(expanded);
+ }}
+ >
+ }
+ sx={{
+ margin: 0,
+ padding: 0,
+ minHeight: '24px',
+ maxHeight: '24px',
+ height: '24px',
+ '&.Mui-expanded': {
+ minHeight: '24px',
+ maxHeight: '24px',
+ height: '24px',
+ },
+ '.MuiAccordionSummary-content': {
+ margin: 0,
+ alignItems: !costBreakdownExpanded ? 'center' : undefined,
+ display: !costBreakdownExpanded ? 'flex' : undefined,
+ },
+ '& .MuiAccordionSummary-content.Mui-expanded': {
+ margin: 0,
+ },
+ }}
+ >
+
+ ) : (
+ Costs & Fees
+ )
+ }
+ captionVariant="description"
+ align="flex-start"
+ width="100%"
+ minHeight="24px"
+ maxHeight="24px"
+ sx={{
+ margin: 0,
+ display: 'flex',
+ alignItems: !costBreakdownExpanded ? 'center' : undefined, // center only if not expanded
+ }}
+ >
+ {!costBreakdownExpanded && (
+
+ )}
+
+
+
+
}
+ captionVariant="caption"
+ align="flex-start"
+ >
+
+
+
+
+
+
+
+
+ {!!(flashloanFeeFormatted && flashloanFeeToken && flashloanFeeUsd) && (
+
}
+ captionVariant="caption"
+ align="flex-start"
+ >
+
+
+
+
+
+
+
+
+ )}
+
} captionVariant="caption" align="flex-start">
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/transactions/Swap/details/DebtSwapDetails.tsx b/src/components/transactions/Swap/details/DebtSwapDetails.tsx
new file mode 100644
index 0000000000..f014bfee75
--- /dev/null
+++ b/src/components/transactions/Swap/details/DebtSwapDetails.tsx
@@ -0,0 +1,201 @@
+import { valueToBigNumber } from '@aave/math-utils';
+import { ArrowNarrowRightIcon } from '@heroicons/react/solid';
+import { Trans } from '@lingui/macro';
+import { Box, Skeleton, SvgIcon, Typography } from '@mui/material';
+import React from 'react';
+import { DarkTooltip } from 'src/components/infoTooltips/DarkTooltip';
+import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
+import { Row } from 'src/components/primitives/Row';
+import { TokenIcon } from 'src/components/primitives/TokenIcon';
+import {
+ DetailsIncentivesLine,
+ TxModalDetails,
+} from 'src/components/transactions/FlowCommons/TxModalDetails';
+
+import { ProtocolSwapParams, ProtocolSwapState, SwapProvider } from '../types';
+import { CowCostsDetails } from './CowCostsDetails';
+import { ParaswapCostsDetails } from './ParaswapCostsDetails';
+
+export const DebtSwapDetails = ({
+ state,
+}: {
+ params: ProtocolSwapParams;
+ state: ProtocolSwapState;
+}) => {
+ const sourceAmountAfterSwap = valueToBigNumber(state.sourceReserve.variableBorrows).minus(
+ valueToBigNumber(state.buyAmountFormatted ?? '0')
+ );
+ const targetAmountAfterSwap = valueToBigNumber(state.destinationReserve.variableBorrows).plus(
+ valueToBigNumber(state.sellAmountFormatted ?? '0')
+ );
+
+ const sourceAmountAfterSwapUSD = sourceAmountAfterSwap.multipliedBy(
+ valueToBigNumber(state.sourceReserve.reserve.priceInUSD)
+ );
+ const targetAmountAfterSwapUSD = targetAmountAfterSwap.multipliedBy(
+ valueToBigNumber(state.destinationReserve.reserve.priceInUSD)
+ );
+
+ const skeleton: JSX.Element = (
+ <>
+
+
+ >
+ );
+
+ return (
+
+ {state.provider === SwapProvider.COW_PROTOCOL && }
+ {state.provider === SwapProvider.PARASWAP && }
+
+ Borrow apy} captionVariant="description" mb={4}>
+
+ {state.ratesLoading ? (
+
+ ) : (
+ <>
+
+ {ArrowRightIcon}
+
+ >
+ )}
+
+
+
+
+
+ Borrow balance after switch}
+ captionVariant="description"
+ mb={4}
+ align="flex-start"
+ >
+
+
+ {state.ratesLoading ? (
+ skeleton
+ ) : (
+ <>
+
+
+
+ {sourceAmountAfterSwap.toString()} {state.sourceReserve.reserve.symbol}
+
+ }
+ arrow
+ placement="top"
+ enterTouchDelay={100}
+ leaveTouchDelay={500}
+ >
+
+
+
+
+
+
+ >
+ )}
+
+
+
+ {state.ratesLoading ? (
+ skeleton
+ ) : (
+ <>
+
+
+
+ {targetAmountAfterSwap.toString()} {state.destinationReserve.reserve.symbol}
+
+ }
+ arrow
+ placement="top"
+ enterTouchDelay={100}
+ leaveTouchDelay={500}
+ >
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+ );
+};
+
+const ArrowRightIcon = (
+
+
+
+);
diff --git a/src/components/transactions/Swap/details/DetailsSkeleton.tsx b/src/components/transactions/Swap/details/DetailsSkeleton.tsx
new file mode 100644
index 0000000000..357e437ec4
--- /dev/null
+++ b/src/components/transactions/Swap/details/DetailsSkeleton.tsx
@@ -0,0 +1,37 @@
+import { Box, Skeleton } from '@mui/material';
+
+import { TxModalDetails } from '../../FlowCommons/TxModalDetails';
+import { SwapState } from '../types';
+
+export const DetailsSkeleton: React.FC<{ state: SwapState }> = ({
+ state,
+}: {
+ state: SwapState;
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/transactions/Swap/details/ParaswapCostsDetails.tsx b/src/components/transactions/Swap/details/ParaswapCostsDetails.tsx
new file mode 100644
index 0000000000..ae0f632b10
--- /dev/null
+++ b/src/components/transactions/Swap/details/ParaswapCostsDetails.tsx
@@ -0,0 +1,64 @@
+import { valueToBigNumber } from '@aave/math-utils';
+import { Box } from '@mui/material';
+import { ExecutionFeeTooltip } from 'src/components/infoTooltips/ExecutionFeeTooltip';
+import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
+import { Row } from 'src/components/primitives/Row';
+import { ExternalTokenIcon } from 'src/components/primitives/TokenIcon';
+
+import { calculateParaswapFlashLoanFee } from '../helpers/paraswap/flashloan.helpers';
+import { isParaswapRates, SwapState } from '../types';
+
+export const ParaswapCostsDetails = ({ state }: { state: SwapState }) => {
+ if (!state.swapRate || !isParaswapRates(state.swapRate)) return null;
+
+ // Calculate flashloan fee if using flashloan
+ const { flashLoanFeeFormatted } = calculateParaswapFlashLoanFee(state);
+
+ // Calculate flashloan fee in USD
+ const flashLoanFeeUsd =
+ state.sellAmountUSD && state.sellAmountFormatted && Number(flashLoanFeeFormatted) > 0
+ ? valueToBigNumber(state.sellAmountUSD)
+ .dividedBy(valueToBigNumber(state.sellAmountFormatted))
+ .multipliedBy(valueToBigNumber(flashLoanFeeFormatted))
+ .toNumber()
+ : 0;
+
+ const flashloanFeeToken = state.sellAmountToken;
+
+ // Only show if there's a flashloan fee
+ if (!flashLoanFeeFormatted || Number(flashLoanFeeFormatted) === 0 || !flashloanFeeToken) {
+ return null;
+ }
+
+ return (
+
} captionVariant="description" align="flex-start" mb={4}>
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/transactions/Swap/details/RepayWithCollateralDetails.tsx b/src/components/transactions/Swap/details/RepayWithCollateralDetails.tsx
new file mode 100644
index 0000000000..2ae9f1245a
--- /dev/null
+++ b/src/components/transactions/Swap/details/RepayWithCollateralDetails.tsx
@@ -0,0 +1,103 @@
+import { valueToBigNumber } from '@aave/math-utils';
+import { Trans } from '@lingui/macro';
+import { useMemo } from 'react';
+import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider';
+import { calculateHFAfterRepay } from 'src/utils/hfUtils';
+
+import {
+ DetailsHFLine,
+ DetailsNumberLineWithSub,
+ TxModalDetails,
+} from '../../FlowCommons/TxModalDetails';
+import { ProtocolSwapParams, ProtocolSwapState, SwapProvider } from '../types';
+import { CowCostsDetails } from './CowCostsDetails';
+import { ParaswapCostsDetails } from './ParaswapCostsDetails';
+
+export const RepayWithCollateralDetails = ({
+ state,
+}: {
+ params: ProtocolSwapParams;
+ state: ProtocolSwapState;
+}) => {
+ const { user } = useAppDataContext();
+
+ const currentDebt = state.sourceReserve.variableBorrows;
+
+ // // If the selected collateral asset is frozen, a flashloan must be used. When a flashloan isn't used,
+ // // the remaining amount after the swap is deposited into the pool, which will fail for frozen assets.
+ // const shouldUseFlashloan =
+ // useFlashloan(user.healthFactor, hfEffectOfFromAmount.toString()) ||
+ // state.destinationReserve.reserve.isFrozen;
+
+ // we need to get the min as minimumReceived can be greater than debt as we are swapping
+ // a safe amount to repay all. When this happens amountAfterRepay would be < 0 and
+ // this would show as certain amount left to repay when we are actually repaying all debt
+ const tokenToRepayWithBalance = state.destinationReserve.underlyingBalance;
+ const debtAmountAfterRepay = useMemo(() => {
+ if (!state.buyAmountFormatted || !currentDebt) return valueToBigNumber('0');
+
+ return valueToBigNumber(currentDebt).minus(
+ valueToBigNumber(state.buyAmountFormatted) < valueToBigNumber(currentDebt)
+ ? valueToBigNumber(state.buyAmountFormatted)
+ : valueToBigNumber(currentDebt)
+ );
+ }, [currentDebt, state.buyAmountFormatted]);
+
+ if (!user || !state.buyAmountFormatted) {
+ return null;
+ }
+
+ const { hfAfterSwap } = calculateHFAfterRepay({
+ amountToReceiveAfterSwap: state.buyAmountFormatted,
+ amountToSwap: state.sellAmountFormatted ?? '0',
+ fromAssetData: state.destinationReserve.reserve, // used as collateral
+ user,
+ toAssetData: state.sourceReserve.reserve,
+ repayWithUserReserve: state.destinationReserve,
+ debt: currentDebt,
+ });
+
+ const displayAmountAfterRepayInUsd = debtAmountAfterRepay.multipliedBy(
+ state.sourceReserve.reserve.priceInUSD
+ );
+ const rawCollateralAmountAfterRepay = tokenToRepayWithBalance
+ ? valueToBigNumber(tokenToRepayWithBalance).minus(state.sellAmountFormatted ?? '0')
+ : valueToBigNumber('0');
+ const collateralAmountAfterRepay = rawCollateralAmountAfterRepay.isNegative()
+ ? valueToBigNumber('0')
+ : rawCollateralAmountAfterRepay;
+ const collateralAmountAfterRepayUSD = collateralAmountAfterRepay.multipliedBy(
+ state.destinationReserve.reserve.priceInUSD
+ );
+
+ return (
+
+ {state.provider === SwapProvider.COW_PROTOCOL && }
+ {state.provider === SwapProvider.PARASWAP && }
+
+
+ Borrow balance after repay}
+ futureValue={debtAmountAfterRepay.toString()}
+ futureValueUSD={displayAmountAfterRepayInUsd.toString()}
+ symbol={state.sourceReserve.reserve.symbol}
+ tokenIcon={state.sourceReserve.reserve.iconSymbol}
+ loading={state.ratesLoading}
+ hideSymbolSuffix
+ />
+ Collateral balance after repay}
+ futureValue={collateralAmountAfterRepay.toString()}
+ futureValueUSD={collateralAmountAfterRepayUSD.toString()}
+ symbol={state.destinationReserve.reserve.symbol}
+ tokenIcon={state.destinationReserve.reserve.iconSymbol}
+ loading={state.ratesLoading}
+ hideSymbolSuffix
+ />
+
+ );
+};
diff --git a/src/components/transactions/Swap/details/SwapDetails.tsx b/src/components/transactions/Swap/details/SwapDetails.tsx
new file mode 100644
index 0000000000..f5e41c6cc5
--- /dev/null
+++ b/src/components/transactions/Swap/details/SwapDetails.tsx
@@ -0,0 +1,250 @@
+import { valueToBigNumber } from '@aave/math-utils';
+import { Trans } from '@lingui/macro';
+import { Box, Typography } from '@mui/material';
+import { DarkTooltip } from 'src/components/infoTooltips/DarkTooltip';
+import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
+import { Row } from 'src/components/primitives/Row';
+import { ExternalTokenIcon } from 'src/components/primitives/TokenIcon';
+
+import { TxModalDetails } from '../../FlowCommons/TxModalDetails';
+import { SwappableToken, SwapParams, SwapProvider, SwapState } from '../types';
+import { CowCostsDetails } from './CowCostsDetails';
+
+export const SwapDetails = ({ params, state }: { params: SwapParams; state: SwapState }) => {
+ if (
+ !state.swapRate ||
+ !state.sellAmountToken ||
+ !state.buyAmountToken ||
+ !state.sellAmountUSD ||
+ !state.buyAmountUSD ||
+ !state.sellAmountFormatted ||
+ !state.buyAmountFormatted
+ )
+ return null;
+
+ return (
+
+
+
+ );
+};
+
+export const SwapModalTxDetails = ({
+ provider,
+ buyToken,
+ buyAmount,
+ buyAmountUSD,
+ sellAmountUSD,
+ safeSlippage,
+ customReceivedTitle,
+ sellToken,
+ state,
+}: {
+ provider: SwapProvider;
+ safeSlippage: number;
+ customReceivedTitle?: React.ReactNode;
+ sellToken: SwappableToken;
+ buyToken: SwappableToken;
+ sellAmount: string;
+ buyAmount: string;
+ buyAmountUSD: string;
+ sellAmountUSD: string;
+ state: SwapState;
+}) => {
+ return provider === SwapProvider.COW_PROTOCOL ? (
+
+ ) : (
+
+ );
+};
+
+export const IntentTxDetails = ({
+ state,
+ buyToken,
+ customReceivedTitle,
+ sellAmountUSD,
+ buyAmount,
+ buyAmountUSD,
+}: {
+ state: SwapState;
+ buyToken: SwappableToken;
+ sellToken: SwappableToken;
+ safeSlippage: number;
+ customReceivedTitle?: React.ReactNode;
+ sellAmountUSD: string;
+ buyAmount: string;
+ buyAmountUSD: string;
+}) => {
+ const receivingInUsd = valueToBigNumber(buyAmountUSD);
+ const sendingInUsd = valueToBigNumber(sellAmountUSD);
+
+ const priceImpact = (1 - receivingInUsd.dividedBy(sendingInUsd).toNumber()) * 100;
+
+ return (
+ <>
+ {state.provider === SwapProvider.COW_PROTOCOL && }
+
+ {`Minimum ${buyToken.symbol} received`}}
+ captionVariant="description"
+ align="flex-start"
+ >
+
+
+
+
+ {buyAmount} {buyToken.symbol}
+
+ }
+ arrow
+ placement="top"
+ enterTouchDelay={100}
+ leaveTouchDelay={500}
+ >
+
+
+
+
+
+
+
+ {priceImpact && priceImpact > 0 && priceImpact < 100 && (
+ 10 ? 'error' : priceImpact > 5 ? 'warning' : 'text.secondary'}
+ >
+ (-{priceImpact.toFixed(priceImpact > 3 ? 0 : priceImpact > 1 ? 1 : 2)}%)
+
+ )}
+
+
+
+ >
+ );
+};
+
+const MarketOrderTxDetails = ({
+ buyToken,
+ customReceivedTitle,
+ buyAmount,
+ buyAmountUSD,
+}: {
+ buyToken: SwappableToken;
+ safeSlippage: number;
+ customReceivedTitle?: React.ReactNode;
+ buyAmount: string;
+ buyAmountUSD: string;
+}) => {
+ return (
+ <>
+ {`Minimum ${buyToken.symbol} received`}}
+ captionVariant="description"
+ align="flex-start"
+ >
+
+
+
+
+ {buyAmount} {buyToken.symbol}
+
+ }
+ arrow
+ placement="top"
+ enterTouchDelay={100}
+ leaveTouchDelay={500}
+ >
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/src/components/transactions/Swap/details/WithdrawAndSwapDetails.tsx b/src/components/transactions/Swap/details/WithdrawAndSwapDetails.tsx
new file mode 100644
index 0000000000..7c0914081e
--- /dev/null
+++ b/src/components/transactions/Swap/details/WithdrawAndSwapDetails.tsx
@@ -0,0 +1,71 @@
+import { valueToBigNumber } from '@aave/math-utils';
+import { Trans } from '@lingui/macro';
+import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider';
+import { useRootStore } from 'src/store/root';
+import { calculateHFAfterWithdraw } from 'src/utils/hfUtils';
+import { useShallow } from 'zustand/shallow';
+
+import { DetailsHFLine, DetailsNumberLine, TxModalDetails } from '../../FlowCommons/TxModalDetails';
+import { ProtocolSwapParams, ProtocolSwapState } from '../types';
+import { SwapModalTxDetails } from './SwapDetails';
+
+export const WithdrawAndSwapDetails = ({
+ state,
+ params,
+}: {
+ params: ProtocolSwapParams;
+ state: ProtocolSwapState;
+}) => {
+ const { user } = useAppDataContext();
+ const { currentNetworkConfig } = useRootStore(
+ useShallow((store) => ({ currentNetworkConfig: store.currentNetworkConfig }))
+ );
+
+ const underlyingBalance = valueToBigNumber(state.sourceReserve.underlyingBalance);
+ const withdrawAmount = state.inputAmount;
+ const poolReserve = state.sourceReserve.reserve;
+
+ if (
+ !user ||
+ !state.buyAmountFormatted ||
+ !state.buyAmountUSD ||
+ !state.sellAmountFormatted ||
+ !state.sellAmountUSD
+ )
+ return null;
+ const healthFactorAfterWithdraw = calculateHFAfterWithdraw({
+ user,
+ userReserve: state.sourceReserve,
+ poolReserve,
+ withdrawAmount,
+ });
+
+ return (
+
+
+ Remaining supply}
+ value={underlyingBalance.minus(withdrawAmount || '0').toString(10)}
+ symbol={
+ poolReserve.isWrappedBaseAsset ? currentNetworkConfig.baseAssetSymbol : poolReserve.symbol
+ }
+ />
+
+
+ );
+};
diff --git a/src/components/transactions/Swap/details/index.ts b/src/components/transactions/Swap/details/index.ts
new file mode 100644
index 0000000000..1b109e157e
--- /dev/null
+++ b/src/components/transactions/Swap/details/index.ts
@@ -0,0 +1,52 @@
+import React from 'react';
+
+import {
+ isProtocolSwapParams,
+ isProtocolSwapState,
+ isTokensSwapParams,
+ isTokensSwapState,
+ SwapParams,
+ SwapState,
+ SwapType,
+} from '../types';
+import { ColalteralSwapDetails } from './CollateralSwapDetails';
+import { DebtSwapDetails } from './DebtSwapDetails';
+import { DetailsSkeleton } from './DetailsSkeleton';
+import { RepayWithCollateralDetails } from './RepayWithCollateralDetails';
+import { SwapDetails } from './SwapDetails';
+import { WithdrawAndSwapDetails } from './WithdrawAndSwapDetails';
+
+/**
+ * Decides which details component to show given the swap type.
+ * Renders a skeleton while rates load and hides the section if no quote is present.
+ */
+export const BaseSwapDetails = ({ params, state }: { params: SwapParams; state: SwapState }) => {
+ if (state.ratesLoading) {
+ return React.createElement(DetailsSkeleton, { state });
+ }
+
+ if (!state.swapRate) {
+ return null;
+ }
+
+ if (params.swapType === SwapType.Swap && isTokensSwapParams(params) && isTokensSwapState(state)) {
+ return React.createElement(SwapDetails, { params, state });
+ } else if (isProtocolSwapParams(params) && isProtocolSwapState(state)) {
+ switch (params.swapType) {
+ case SwapType.CollateralSwap:
+ return React.createElement(ColalteralSwapDetails, { params, state });
+ case SwapType.DebtSwap:
+ return React.createElement(DebtSwapDetails, { params, state });
+ case SwapType.RepayWithCollateral:
+ return React.createElement(RepayWithCollateralDetails, { params, state });
+ case SwapType.WithdrawAndSwap:
+ return React.createElement(WithdrawAndSwapDetails, { params, state });
+ default:
+ console.error(`Unsupported swap type`);
+ return null;
+ }
+ } else {
+ console.error(`Invalid swap params or state in details`);
+ return null;
+ }
+};
diff --git a/src/components/transactions/Swap/docs/.gitkeep b/src/components/transactions/Swap/docs/.gitkeep
new file mode 100644
index 0000000000..d7e5d7c40d
--- /dev/null
+++ b/src/components/transactions/Swap/docs/.gitkeep
@@ -0,0 +1,3 @@
+# Placeholder to keep docs directory in git. Add `swap-modal-architecture.png` here.
+
+
diff --git a/src/components/transactions/Swap/docs/swap-modal-architecture.png b/src/components/transactions/Swap/docs/swap-modal-architecture.png
new file mode 100644
index 0000000000..99e71f34db
Binary files /dev/null and b/src/components/transactions/Swap/docs/swap-modal-architecture.png differ
diff --git a/src/components/transactions/Swap/errors/SwapErrors.tsx b/src/components/transactions/Swap/errors/SwapErrors.tsx
new file mode 100644
index 0000000000..f90f0478c6
--- /dev/null
+++ b/src/components/transactions/Swap/errors/SwapErrors.tsx
@@ -0,0 +1,150 @@
+import React, { Dispatch, useEffect } from 'react';
+
+import { useModalContext } from '../../../../hooks/useModal';
+import { TrackAnalyticsHandlers } from '../analytics/useTrackAnalytics';
+import { SwapError, SwapParams, SwapState } from '../types';
+import { isProtocolSwapState } from '../types/state.types';
+import { errorToConsole } from './shared/console.helpers';
+import {
+ FlashLoanDisabledBlockingGuard,
+ hasFlashLoanDisabled,
+} from './shared/FlashLoanDisabledBlockingGuard';
+import { GasEstimationError } from './shared/GasEstimationError';
+import { GenericError } from './shared/GenericError';
+import {
+ hasInsufficientBalance,
+ InsufficientBalanceGuard,
+} from './shared/InsufficientBalanceGuard';
+import {
+ hasInsufficientLiquidity,
+ InsufficientLiquidityBlockingGuard,
+} from './shared/InsufficientLiquidityBlockingGuard';
+import { ProviderError } from './shared/ProviderError';
+import { hasSupplyCapBlocking, SupplyCapBlockingGuard } from './shared/SupplyCapBlockingGuard';
+import { hasUserDenied, UserDenied } from './shared/UserDenied';
+import { hasZeroLTVBlocking, ZeroLTVBlockingGuard } from './shared/ZeroLTVBlockingGuard';
+
+export const SwapErrors = ({
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ const { txError } = useModalContext();
+
+ useEffect(() => {
+ if (txError) {
+ const swapError: SwapError = {
+ rawError: txError.rawError,
+ message: `Error: ${txError.error} on ${txError.txAction}`,
+ actionBlocked: txError.actionBlocked || txError.blocking,
+ };
+
+ setState({
+ error: swapError,
+ });
+ trackingHandlers.trackSwapError(swapError);
+
+ // Human readable error for user to share with support team
+ // Avoid wrapping in console.error to prevent dev overlay "undefined Error" noise
+ errorToConsole(state, {
+ rawError: txError.rawError,
+ message: `Error: ${txError.error} on ${txError.txAction}`,
+ actionBlocked: txError.actionBlocked || txError.blocking,
+ });
+ }
+ }, [txError]);
+
+ // Track user denied
+ useEffect(() => {
+ if (state.error && hasUserDenied(state.error)) {
+ trackingHandlers.trackUserDenied();
+ }
+ }, [state.error]);
+
+ if (hasInsufficientBalance(state)) {
+ return (
+
+ );
+ }
+
+ if (hasZeroLTVBlocking(state, [])) {
+ return (
+
+ );
+ }
+
+ if (hasFlashLoanDisabled(state) && isProtocolSwapState(state)) {
+ return (
+
+ );
+ }
+
+ if (isProtocolSwapState(state) && hasSupplyCapBlocking(state)) {
+ return (
+
+ );
+ }
+
+ if (isProtocolSwapState(state) && hasInsufficientLiquidity(state)) {
+ return (
+
+ );
+ }
+
+ if (!state.error) {
+ return null;
+ }
+
+ if (hasUserDenied(state.error)) {
+ return ;
+ }
+
+ const provider = state.provider;
+ if (!provider) {
+ return (
+
+ );
+ }
+
+ const providerError = React.createElement(ProviderError, {
+ error: state.error,
+ state,
+ provider,
+ sx: { mb: !state.isSwapFlowSelected ? 0 : 4 },
+ key: `provider-error`,
+ });
+
+ if (providerError) {
+ return providerError;
+ }
+
+ return ;
+};
diff --git a/src/components/transactions/Switch/cowprotocol/cowprotocol.errors.ts b/src/components/transactions/Swap/errors/cow/quote.helpers.ts
similarity index 82%
rename from src/components/transactions/Switch/cowprotocol/cowprotocol.errors.ts
rename to src/components/transactions/Swap/errors/cow/quote.helpers.ts
index 287c45882b..9077d5283a 100644
--- a/src/components/transactions/Switch/cowprotocol/cowprotocol.errors.ts
+++ b/src/components/transactions/Swap/errors/cow/quote.helpers.ts
@@ -1,10 +1,10 @@
-const MESSAGE_MAP: { [key: string]: string } = {
+export const MESSAGE_MAP: { [key: string]: string } = {
NoLiquidity: 'No liquidity found for the given amount and asset pair.',
NoRoutesFound: 'No routes found with enough liquidity.',
SellAmountDoesNotCoverFee: 'Sell amount is too small to cover the fee.',
};
-const MESSAGE_REGEX_MAP: Array<{ regex: RegExp; message: string }> = [
+export const MESSAGE_REGEX_MAP: Array<{ regex: RegExp; message: string }> = [
{
regex: /^Source and destination tokens cannot be the same$/,
message: 'Source and destination tokens cannot be the same',
diff --git a/src/components/transactions/Swap/errors/paraswap/quote.helpers.ts b/src/components/transactions/Swap/errors/paraswap/quote.helpers.ts
new file mode 100644
index 0000000000..537e4a21ea
--- /dev/null
+++ b/src/components/transactions/Swap/errors/paraswap/quote.helpers.ts
@@ -0,0 +1,27 @@
+export const MESSAGE_MAP: { [key: string]: string } = {
+ ESTIMATED_LOSS_GREATER_THAN_MAX_IMPACT:
+ 'Price impact too high. Please try a different amount or asset pair.',
+ // not sure why this error-code is not upper-cased
+ 'No routes found with enough liquidity': 'No routes found with enough liquidity.',
+};
+
+export const MESSAGE_REGEX_MAP: Array<{ regex: RegExp; message: string }> = [
+ {
+ regex: /^Amount \d+ is too small to proceed$/,
+ message: 'Amount is too small. Please try larger amount.',
+ },
+];
+
+/**
+ * Converts Paraswap error message to message for displaying in interface
+ * @param message Paraswap error message
+ * @returns Message for displaying in interface
+ */
+export function convertParaswapErrorMessage(message: string): string | undefined {
+ if (message in MESSAGE_MAP) {
+ return MESSAGE_MAP[message];
+ }
+
+ const newMessage = MESSAGE_REGEX_MAP.find((mapping) => mapping.regex.test(message))?.message;
+ return newMessage;
+}
diff --git a/src/components/transactions/Swap/errors/shared/BalanceLowerThanInput.tsx b/src/components/transactions/Swap/errors/shared/BalanceLowerThanInput.tsx
new file mode 100644
index 0000000000..63224ece6f
--- /dev/null
+++ b/src/components/transactions/Swap/errors/shared/BalanceLowerThanInput.tsx
@@ -0,0 +1,18 @@
+import { Trans } from '@lingui/macro';
+import { SxProps, Typography } from '@mui/material';
+import { Warning } from 'src/components/primitives/Warning';
+
+import { SwapType } from '../../types/shared.types';
+
+export const BalanceLowerThanInput = ({ sx, swapType }: { sx?: SxProps; swapType: SwapType }) => {
+ return (
+
+
+
+ Your {swapType === SwapType.RepayWithCollateral ? 'collateral' : ''} balance is lower than
+ the selected amount.
+
+
+
+ );
+};
diff --git a/src/components/transactions/Swap/errors/shared/FlashLoanDisabledBlockingError.tsx b/src/components/transactions/Swap/errors/shared/FlashLoanDisabledBlockingError.tsx
new file mode 100644
index 0000000000..9764a06b74
--- /dev/null
+++ b/src/components/transactions/Swap/errors/shared/FlashLoanDisabledBlockingError.tsx
@@ -0,0 +1,13 @@
+import { Trans } from '@lingui/macro';
+import { SxProps, Typography } from '@mui/material';
+import { Warning } from 'src/components/primitives/Warning';
+
+export const FlashLoanDisabledBlockingError = ({ sx }: { sx?: SxProps }) => {
+ return (
+
+
+ Position swaps are disabled for this asset due to security reasons.
+
+
+ );
+};
diff --git a/src/components/transactions/Swap/errors/shared/FlashLoanDisabledBlockingGuard.tsx b/src/components/transactions/Swap/errors/shared/FlashLoanDisabledBlockingGuard.tsx
new file mode 100644
index 0000000000..c201669c11
--- /dev/null
+++ b/src/components/transactions/Swap/errors/shared/FlashLoanDisabledBlockingGuard.tsx
@@ -0,0 +1,83 @@
+import { SxProps } from '@mui/material';
+import { Dispatch, useEffect } from 'react';
+
+import { ActionsBlockedReason, SwapError, SwapProvider, SwapState } from '../../types';
+import { isProtocolSwapState, ProtocolSwapState } from '../../types/state.types';
+import { FlashLoanDisabledBlockingError } from './FlashLoanDisabledBlockingError';
+
+export const hasFlashLoanDisabled = (state: SwapState): boolean => {
+ if (!isProtocolSwapState(state)) {
+ return false;
+ }
+
+ // Check if provider is Paraswap, using flashloan, and sourceReserve exists
+ if (
+ state.provider === SwapProvider.PARASWAP &&
+ state.useFlashloan === true &&
+ state.sourceReserve?.reserve &&
+ !state.sourceReserve.reserve.flashLoanEnabled
+ ) {
+ return true;
+ }
+
+ return false;
+};
+
+export const FlashLoanDisabledBlockingGuard = ({
+ state,
+ setState,
+ sx,
+ isSwapFlowSelected,
+}: {
+ state: ProtocolSwapState;
+ setState: Dispatch>;
+ sx?: SxProps;
+ isSwapFlowSelected: boolean;
+}) => {
+ useEffect(() => {
+ const isBlocking = hasFlashLoanDisabled(state);
+
+ if (isBlocking) {
+ const isAlreadyBlockingError =
+ state.error?.rawError instanceof Error &&
+ state.error.rawError.message === 'FlashLoanDisabledError';
+
+ if (!isAlreadyBlockingError) {
+ const blockingError: SwapError = {
+ rawError: new Error('FlashLoanDisabledError'),
+ message: 'Position Swaps disabled for this asset',
+ actionBlocked: true,
+ };
+ setState({
+ error: blockingError,
+ actionsBlocked: {
+ [ActionsBlockedReason.FLASH_LOAN_DISABLED]: true,
+ },
+ });
+ }
+ } else {
+ const isBlockingError =
+ state.error?.rawError instanceof Error &&
+ state.error.rawError.message === 'FlashLoanDisabledError';
+ if (isBlockingError) {
+ setState({
+ error: undefined,
+ actionsBlocked: {
+ [ActionsBlockedReason.FLASH_LOAN_DISABLED]: undefined,
+ },
+ });
+ }
+ }
+ }, [
+ state.provider,
+ state.useFlashloan,
+ state.sourceReserve?.reserve?.flashLoanEnabled,
+ state.error,
+ ]);
+
+ if (hasFlashLoanDisabled(state)) {
+ return ;
+ }
+
+ return null;
+};
diff --git a/src/components/transactions/Swap/errors/shared/GasEstimationError.tsx b/src/components/transactions/Swap/errors/shared/GasEstimationError.tsx
new file mode 100644
index 0000000000..b2c87cbc75
--- /dev/null
+++ b/src/components/transactions/Swap/errors/shared/GasEstimationError.tsx
@@ -0,0 +1,43 @@
+import { Trans } from '@lingui/macro';
+import { Box, Typography } from '@mui/material';
+import { Warning } from 'src/components/primitives/Warning';
+import { TxAction, TxErrorType } from 'src/ui-config/errorMapping';
+
+import { GasEstimationError as GasEstimationErrorComponent } from '../../../FlowCommons/GasEstimationError';
+
+interface ErrorProps {
+ error?: Error;
+ isLimitOrder?: boolean;
+}
+
+export const GasEstimationError: React.FC = ({ error, isLimitOrder }) => {
+ if (!error) {
+ return null;
+ }
+
+ const txErrorType: TxErrorType = {
+ blocking: false,
+ actionBlocked: false,
+ rawError: error,
+ error: Gas estimation error,
+ txAction: TxAction.GAS_ESTIMATION,
+ };
+
+ return (
+
+
+
+
+
+ {' '}
+ {isLimitOrder ? (
+ Tip: Try increasing slippage or reduce input amount
+ ) : (
+ Tip: Try improving your order parameters
+ )}
+
+
+
+
+ );
+};
diff --git a/src/components/transactions/Swap/errors/shared/GenericError.tsx b/src/components/transactions/Swap/errors/shared/GenericError.tsx
new file mode 100644
index 0000000000..89fd15b888
--- /dev/null
+++ b/src/components/transactions/Swap/errors/shared/GenericError.tsx
@@ -0,0 +1,48 @@
+import { Trans } from '@lingui/macro';
+import { ContentCopy } from '@mui/icons-material';
+import { IconButton, SxProps, Tooltip, Typography } from '@mui/material';
+import React, { useState } from 'react';
+import { Warning } from 'src/components/primitives/Warning';
+
+interface GenericErrorProps {
+ sx?: SxProps;
+ message: string;
+ copyText?: string;
+}
+
+export const GenericError = ({ sx, message, copyText }: GenericErrorProps) => {
+ const [copyTooltip, setCopyTooltip] = useState<'Copy' | 'Copied!'>('Copy');
+
+ const handleCopy = async () => {
+ if (copyText) {
+ try {
+ await navigator.clipboard.writeText(copyText);
+ setCopyTooltip('Copied!');
+ setTimeout(() => setCopyTooltip('Copy'), 1200);
+ } catch (e) {
+ setCopyTooltip('Copy');
+ setTimeout(() => setCopyTooltip('Copy'), 1200);
+ }
+ }
+ };
+
+ return (
+
+
+ {message}
+ {copyText ? (
+
+
+
+
+
+ ) : null}
+
+
+ );
+};
diff --git a/src/components/transactions/Swap/errors/shared/InsufficientBalanceGuard.tsx b/src/components/transactions/Swap/errors/shared/InsufficientBalanceGuard.tsx
new file mode 100644
index 0000000000..3de76d3383
--- /dev/null
+++ b/src/components/transactions/Swap/errors/shared/InsufficientBalanceGuard.tsx
@@ -0,0 +1,92 @@
+import { valueToBigNumber } from '@aave/math-utils';
+import { SxProps } from '@mui/material';
+import React, { Dispatch, useEffect } from 'react';
+
+import { ActionsBlockedReason, SwapError, SwapState, SwapType } from '../../types';
+import { BalanceLowerThanInput } from './BalanceLowerThanInput';
+
+export const hasInsufficientBalance = (state: SwapState) => {
+ // Determine which token pays and which amount to compare.
+ // - Default: sell side pays.
+ // - Inverted flows (e.g., RepayWithCollateral) use destination token.
+ // - DebtSwap is special: the buy side pays (repaying with the bought debt token).
+ const paysOnBuySide = state.swapType === SwapType.DebtSwap;
+ const payingToken = paysOnBuySide ? state.buyAmountToken : state.sellAmountToken;
+
+ const requiredAmount = paysOnBuySide ? state.buyAmountFormatted : state.sellAmountFormatted;
+
+ return valueToBigNumber(requiredAmount || 0).isGreaterThan(
+ valueToBigNumber(payingToken?.balance || 0)
+ );
+};
+
+export const InsufficientBalanceGuard = ({
+ state,
+ setState,
+ sx,
+ isSwapFlowSelected,
+}: {
+ state: SwapState;
+ setState: Dispatch>;
+ sx?: SxProps;
+ isSwapFlowSelected: boolean;
+}) => {
+ useEffect(() => {
+ const insufficient = hasInsufficientBalance(state);
+
+ if (insufficient) {
+ const isAlreadyBalanceError =
+ state.error?.rawError instanceof Error &&
+ state.error.rawError.message === 'BalanceLowerThanInput';
+
+ if (!isAlreadyBalanceError) {
+ const balanceError: SwapError = {
+ rawError: new Error('BalanceLowerThanInput'),
+ message: 'Your balance is lower than the selected amount.',
+ actionBlocked: true,
+ };
+ setState({
+ error: balanceError,
+ actionsBlocked: {
+ [ActionsBlockedReason.INSUFFICIENT_BALANCE]: true,
+ },
+ });
+ }
+ } else {
+ const isBalanceError =
+ state.error?.rawError instanceof Error &&
+ state.error.rawError.message === 'BalanceLowerThanInput';
+
+ if (isBalanceError) {
+ setState({
+ error: undefined,
+ actionsBlocked: {
+ [ActionsBlockedReason.INSUFFICIENT_BALANCE]: undefined,
+ },
+ });
+ }
+ }
+ }, [
+ state.debouncedInputAmount,
+ state.debouncedOutputAmount,
+ state.sourceToken.balance,
+ state.destinationToken.balance,
+ state.sellAmountFormatted,
+ state.isInvertedSwap,
+ state.side,
+ state.swapType,
+ state.buyAmountFormatted,
+ state.sellAmountFormatted,
+ ]);
+
+ if (hasInsufficientBalance(state)) {
+ return (
+
+ );
+ }
+
+ return null;
+};
diff --git a/src/components/transactions/Swap/errors/shared/InsufficientLiquidityBlockingError.tsx b/src/components/transactions/Swap/errors/shared/InsufficientLiquidityBlockingError.tsx
new file mode 100644
index 0000000000..2569c85027
--- /dev/null
+++ b/src/components/transactions/Swap/errors/shared/InsufficientLiquidityBlockingError.tsx
@@ -0,0 +1,21 @@
+import { Trans } from '@lingui/macro';
+import { SxProps, Typography } from '@mui/material';
+import { Warning } from 'src/components/primitives/Warning';
+
+export const InsufficientLiquidityBlockingError = ({
+ symbol,
+ sx,
+}: {
+ symbol: string;
+ sx?: SxProps;
+}) => {
+ return (
+
+
+
+ There is not enough liquidity in {symbol} to complete this swap. Try lowering the amount.
+
+
+
+ );
+};
diff --git a/src/components/transactions/Swap/errors/shared/InsufficientLiquidityBlockingGuard.tsx b/src/components/transactions/Swap/errors/shared/InsufficientLiquidityBlockingGuard.tsx
new file mode 100644
index 0000000000..775f95f817
--- /dev/null
+++ b/src/components/transactions/Swap/errors/shared/InsufficientLiquidityBlockingGuard.tsx
@@ -0,0 +1,96 @@
+import { valueToBigNumber } from '@aave/math-utils';
+import { SxProps } from '@mui/material';
+import { BigNumber } from 'bignumber.js';
+import { ethers } from 'ethers';
+import { Dispatch, useEffect } from 'react';
+
+import { ActionsBlockedReason, ProtocolSwapState, SwapError, SwapState } from '../../types';
+import { isProtocolSwapState } from '../../types/state.types';
+import { InsufficientLiquidityBlockingError } from './InsufficientLiquidityBlockingError';
+
+export const hasInsufficientLiquidity = (state: SwapState) => {
+ if (!isProtocolSwapState(state)) return false;
+ const reserve = state.isInvertedSwap
+ ? state.sourceReserve?.reserve
+ : state.destinationReserve?.reserve;
+ const buyAmount = state.buyAmountFormatted;
+ if (!reserve || !buyAmount) return false;
+
+ const availableBorrowCap =
+ reserve.borrowCap === '0'
+ ? valueToBigNumber(ethers.constants.MaxUint256.toString())
+ : valueToBigNumber(reserve.borrowCap).minus(valueToBigNumber(reserve.totalDebt));
+ const availableLiquidity = BigNumber.max(
+ BigNumber.min(valueToBigNumber(reserve.formattedAvailableLiquidity), availableBorrowCap),
+ 0
+ );
+
+ return valueToBigNumber(buyAmount).gt(availableLiquidity);
+};
+
+export const InsufficientLiquidityBlockingGuard = ({
+ state,
+ setState,
+ sx,
+ isSwapFlowSelected,
+}: {
+ state: ProtocolSwapState;
+ setState: Dispatch>;
+ sx?: SxProps;
+ isSwapFlowSelected: boolean;
+}) => {
+ useEffect(() => {
+ const isBlocking = hasInsufficientLiquidity(state);
+
+ if (isBlocking) {
+ const isAlreadyBlockingError =
+ state.error?.rawError instanceof Error &&
+ state.error.rawError.message === 'InsufficientLiquidityError';
+
+ if (!isAlreadyBlockingError) {
+ const blockingError: SwapError = {
+ rawError: new Error('InsufficientLiquidityError'),
+ message: 'Not enough liquidity in target asset to complete the swap.',
+ actionBlocked: true,
+ };
+ setState({
+ error: blockingError,
+ actionsBlocked: {
+ [ActionsBlockedReason.INSUFFICIENT_LIQUIDITY]: true,
+ },
+ });
+ }
+ } else {
+ const isBlockingError =
+ state.error?.rawError instanceof Error &&
+ state.error.rawError.message === 'InsufficientLiquidityError';
+ if (isBlockingError) {
+ setState({
+ error: undefined,
+ actionsBlocked: {
+ [ActionsBlockedReason.INSUFFICIENT_LIQUIDITY]: undefined,
+ },
+ });
+ }
+ }
+ }, [
+ state.buyAmountFormatted,
+ state.destinationReserve?.reserve?.formattedAvailableLiquidity,
+ state.sourceReserve?.reserve?.formattedAvailableLiquidity,
+ state.isInvertedSwap,
+ ]);
+
+ if (hasInsufficientLiquidity(state)) {
+ const symbol = state.isInvertedSwap
+ ? state.sourceReserve?.reserve?.symbol
+ : state.destinationReserve?.reserve?.symbol;
+ return (
+
+ );
+ }
+
+ return null;
+};
diff --git a/src/components/transactions/Swap/errors/shared/ProviderError.tsx b/src/components/transactions/Swap/errors/shared/ProviderError.tsx
new file mode 100644
index 0000000000..83d31fa712
--- /dev/null
+++ b/src/components/transactions/Swap/errors/shared/ProviderError.tsx
@@ -0,0 +1,47 @@
+import { SxProps, Typography } from '@mui/material';
+import { Warning } from 'src/components/primitives/Warning';
+
+import { SwapError, SwapProvider, SwapState } from '../../types';
+import { convertCowProtocolErrorMessage } from '../cow/quote.helpers';
+import { convertParaswapErrorMessage } from '../paraswap/quote.helpers';
+import { errorToConsoleString } from '../shared/console.helpers';
+import { GenericError } from './GenericError';
+
+interface QuoteErrorProps {
+ error: SwapError;
+ provider: SwapProvider;
+ sx?: SxProps;
+ state: SwapState;
+}
+
+export const ProviderError = ({ error, sx, provider, state }: QuoteErrorProps) => {
+ let customErrorMessage;
+
+ switch (provider) {
+ case SwapProvider.PARASWAP:
+ customErrorMessage = convertParaswapErrorMessage(error.message);
+ break;
+ case SwapProvider.COW_PROTOCOL:
+ customErrorMessage = convertCowProtocolErrorMessage(error.message);
+ break;
+ default:
+ console.error('No provider error mapping found for', provider, error);
+ break;
+ }
+
+ if (!customErrorMessage) {
+ const errorToCopy = errorToConsoleString(state, error);
+ return (
+
+ );
+ }
+
+ return (
+
+ {customErrorMessage}
+
+ );
+};
diff --git a/src/components/transactions/Swap/errors/shared/SupplyCapBlockingError.tsx b/src/components/transactions/Swap/errors/shared/SupplyCapBlockingError.tsx
new file mode 100644
index 0000000000..f4b2f778d1
--- /dev/null
+++ b/src/components/transactions/Swap/errors/shared/SupplyCapBlockingError.tsx
@@ -0,0 +1,15 @@
+import { Trans } from '@lingui/macro';
+import { SxProps, Typography } from '@mui/material';
+import { Warning } from 'src/components/primitives/Warning';
+
+export const SupplyCapBlockingError = ({ symbol, sx }: { symbol: string; sx?: SxProps }) => {
+ return (
+
+
+
+ Supply cap reached for {symbol}. Reduce the amount or choose a different asset.
+
+
+
+ );
+};
diff --git a/src/components/transactions/Swap/errors/shared/SupplyCapBlockingGuard.tsx b/src/components/transactions/Swap/errors/shared/SupplyCapBlockingGuard.tsx
new file mode 100644
index 0000000000..236919dc10
--- /dev/null
+++ b/src/components/transactions/Swap/errors/shared/SupplyCapBlockingGuard.tsx
@@ -0,0 +1,89 @@
+import { valueToBigNumber } from '@aave/math-utils';
+import { SxProps } from '@mui/material';
+import { Dispatch, useEffect } from 'react';
+
+import { ActionsBlockedReason, ProtocolSwapState, SwapError, SwapState } from '../../types';
+import { isProtocolSwapState } from '../../types/state.types';
+import { SupplyCapBlockingError } from './SupplyCapBlockingError';
+
+export const hasSupplyCapBlocking = (state: SwapState) => {
+ if (!isProtocolSwapState(state)) return false;
+ const reserve = state.isInvertedSwap
+ ? state.sourceReserve?.reserve
+ : state.destinationReserve?.reserve;
+ const buyAmount = state.buyAmountFormatted;
+ if (!reserve || !buyAmount) return false;
+
+ if (reserve.supplyCap === '0') return false;
+
+ const remainingCap = valueToBigNumber(reserve.supplyCap).minus(
+ valueToBigNumber(reserve.totalLiquidity)
+ );
+
+ // If remaining cap is exhausted or the intended buy exceeds remaining, block
+ return remainingCap.lte(0) || valueToBigNumber(buyAmount).gt(remainingCap);
+};
+
+export const SupplyCapBlockingGuard = ({
+ state,
+ setState,
+ sx,
+ isSwapFlowSelected,
+}: {
+ state: ProtocolSwapState;
+ setState: Dispatch>;
+ sx?: SxProps;
+ isSwapFlowSelected: boolean;
+}) => {
+ useEffect(() => {
+ const isBlocking = hasSupplyCapBlocking(state);
+
+ if (isBlocking) {
+ const isAlreadyBlockingError =
+ state.error?.rawError instanceof Error &&
+ state.error.rawError.message === 'SupplyCapBlockingError';
+
+ if (!isAlreadyBlockingError) {
+ const blockingError: SwapError = {
+ rawError: new Error('SupplyCapBlockingError'),
+ message: 'Supply cap reached for target asset.',
+ actionBlocked: true,
+ };
+ setState({
+ error: blockingError,
+ actionsBlocked: {
+ [ActionsBlockedReason.SUPPLY_CAP_BLOCKING]: true,
+ },
+ });
+ }
+ } else {
+ const isBlockingError =
+ state.error?.rawError instanceof Error &&
+ state.error.rawError.message === 'SupplyCapBlockingError';
+ if (isBlockingError) {
+ setState({
+ error: undefined,
+ actionsBlocked: {
+ [ActionsBlockedReason.SUPPLY_CAP_BLOCKING]: undefined,
+ },
+ });
+ }
+ }
+ }, [
+ state.buyAmountFormatted,
+ state.destinationReserve?.reserve?.totalLiquidity,
+ state.sourceReserve?.reserve?.totalLiquidity,
+ state.isInvertedSwap,
+ ]);
+
+ if (hasSupplyCapBlocking(state)) {
+ const symbol = state.isInvertedSwap
+ ? state.sourceReserve?.reserve?.symbol
+ : state.destinationReserve?.reserve?.symbol;
+ return (
+
+ );
+ }
+
+ return null;
+};
diff --git a/src/components/transactions/Swap/errors/shared/UserDenied.tsx b/src/components/transactions/Swap/errors/shared/UserDenied.tsx
new file mode 100644
index 0000000000..e5b362c943
--- /dev/null
+++ b/src/components/transactions/Swap/errors/shared/UserDenied.tsx
@@ -0,0 +1,95 @@
+import { Trans } from '@lingui/macro';
+import { Box, CircularProgress, Typography } from '@mui/material';
+import React, { Dispatch, useEffect, useState } from 'react';
+import { Warning } from 'src/components/primitives/Warning';
+
+import { SwapError, SwapState } from '../../types';
+
+const USER_DENIED_MESSAGES = [
+ 'user denied message signature',
+ 'user denied message',
+ 'user denied transaction signature',
+ 'user denied transaction',
+ 'user denied the request',
+ 'user denied request',
+ 'user rejected the request',
+ 'user rejected request',
+ 'user rejected the transaction',
+ 'user rejected transaction',
+ 'you cancelled the transaction',
+];
+
+export const hasUserDenied = (txError: SwapError) => {
+ return USER_DENIED_MESSAGES.some((message) =>
+ txError.rawError.message.toLowerCase().includes(message.toLowerCase())
+ );
+};
+
+export const UserDenied = ({
+ state,
+ setState,
+}: {
+ state: SwapState;
+ setState: Dispatch>;
+}) => {
+ // Show info message for 10 seconds with progress circle at the end, then remove
+ const [visible, setVisible] = useState(true);
+ const [progress, setProgress] = useState(0);
+
+ useEffect(() => {
+ // Progress increments every 50ms (100 * 50ms = 5s to reach 100)
+ const interval = setInterval(() => {
+ setProgress((prevProgress) => {
+ if (prevProgress >= 100) {
+ clearInterval(interval);
+
+ if (state.actionsLoading) {
+ setState({
+ actionsLoading: false,
+ });
+ }
+
+ // Hide after short delay to allow circle to show full (e.g. 250ms)
+ setTimeout(() => {
+ setVisible(false);
+ setProgress(0);
+ setState({
+ error: undefined,
+ });
+ }, 100);
+
+ return 100;
+ }
+ return prevProgress + 1;
+ });
+ }, 50);
+
+ return () => {
+ clearInterval(interval);
+ };
+ }, []);
+
+ return (
+
+
+ }
+ >
+
+ User denied the operation.
+
+
+
+ );
+};
diff --git a/src/components/transactions/Swap/errors/shared/ZeroLTVBlockingError.tsx b/src/components/transactions/Swap/errors/shared/ZeroLTVBlockingError.tsx
new file mode 100644
index 0000000000..426d0ae562
--- /dev/null
+++ b/src/components/transactions/Swap/errors/shared/ZeroLTVBlockingError.tsx
@@ -0,0 +1,16 @@
+import { Trans } from '@lingui/macro';
+import { SxProps, Typography } from '@mui/material';
+import { Warning } from 'src/components/primitives/Warning';
+
+export const ZeroLTVBlockingError = ({ sx }: { sx?: SxProps }) => {
+ return (
+
+
+
+ You have assets with zero LTV that are blocking this operation. Please disable them as
+ collateral first.
+
+
+
+ );
+};
diff --git a/src/components/transactions/Swap/errors/shared/ZeroLTVBlockingGuard.tsx b/src/components/transactions/Swap/errors/shared/ZeroLTVBlockingGuard.tsx
new file mode 100644
index 0000000000..ab07d55f23
--- /dev/null
+++ b/src/components/transactions/Swap/errors/shared/ZeroLTVBlockingGuard.tsx
@@ -0,0 +1,67 @@
+import { SxProps } from '@mui/material';
+import React, { Dispatch, useEffect } from 'react';
+import { useZeroLTVBlockingWithdraw } from 'src/hooks/useZeroLTVBlockingWithdraw';
+
+import { ActionsBlockedReason, SwapError, SwapState } from '../../types';
+import { ZeroLTVBlockingError } from './ZeroLTVBlockingError';
+
+export const hasZeroLTVBlocking = (state: SwapState, blockingAssets: string[]) => {
+ return blockingAssets.length > 0 && !blockingAssets.includes(state.sourceToken.symbol);
+};
+
+export const ZeroLTVBlockingGuard = ({
+ state,
+ setState,
+ sx,
+ isSwapFlowSelected,
+}: {
+ state: SwapState;
+ setState: Dispatch>;
+ sx?: SxProps;
+ isSwapFlowSelected: boolean;
+}) => {
+ const assetsBlockingWithdraw = useZeroLTVBlockingWithdraw();
+
+ useEffect(() => {
+ const isBlocking = hasZeroLTVBlocking(state, assetsBlockingWithdraw);
+
+ if (isBlocking) {
+ const isAlreadyBlockingError =
+ state.error?.rawError instanceof Error &&
+ state.error.rawError.message === 'ZeroLTVBlockingError';
+
+ if (!isAlreadyBlockingError) {
+ const blockingError: SwapError = {
+ rawError: new Error('ZeroLTVBlockingError'),
+ message:
+ 'You have assets with zero LTV that are blocking this operation. Please disable them as collateral first.',
+ actionBlocked: true,
+ };
+ setState({
+ error: blockingError,
+ actionsBlocked: {
+ [ActionsBlockedReason.ZERO_LTV_BLOCKING]: true,
+ },
+ });
+ }
+ } else {
+ const isBlockingError =
+ state.error?.rawError instanceof Error &&
+ state.error.rawError.message === 'ZeroLTVBlockingError';
+ if (isBlockingError) {
+ setState({
+ error: undefined,
+ actionsBlocked: {
+ [ActionsBlockedReason.ZERO_LTV_BLOCKING]: undefined,
+ },
+ });
+ }
+ }
+ }, [assetsBlockingWithdraw, state.sourceToken.symbol]);
+
+ if (hasZeroLTVBlocking(state, assetsBlockingWithdraw)) {
+ return ;
+ }
+
+ return null;
+};
diff --git a/src/components/transactions/Swap/errors/shared/console.helpers.ts b/src/components/transactions/Swap/errors/shared/console.helpers.ts
new file mode 100644
index 0000000000..3962a2bf90
--- /dev/null
+++ b/src/components/transactions/Swap/errors/shared/console.helpers.ts
@@ -0,0 +1,63 @@
+import { SwapError, SwapState } from '../../types';
+
+function serializeError(raw: unknown) {
+ if (!raw) return undefined;
+ try {
+ const err = raw as Record;
+ const base: Record = {
+ name: err?.name,
+ message: err?.message,
+ stack: err?.stack,
+ };
+ const props: Record = {};
+ try {
+ for (const key of Object.getOwnPropertyNames(err)) {
+ if (!(key in base)) props[key] = err[key];
+ }
+ } catch (_) {
+ // ignore
+ }
+ return { ...base, ...props };
+ } catch (_) {
+ return { message: String(raw) };
+ }
+}
+
+function buildErrorPayload(state: SwapState, error: SwapError) {
+ return {
+ timestamp: new Date().toISOString(),
+ chainId: state.chainId,
+ provider: state.provider,
+ useFlashloan: state.useFlashloan ?? false,
+ side: state.side,
+ orderType: state.orderType,
+ swapType: state.swapType,
+ slippage: state.slippage,
+ input: {
+ token: state.sourceToken.symbol,
+ amount: state.inputAmount,
+ usd: state.swapRate?.srcSpotUSD,
+ },
+ output: {
+ token: state.destinationToken.symbol,
+ amount: state.outputAmount,
+ usd: state.swapRate?.destSpotUSD,
+ },
+ error: {
+ message: error.message,
+ actionBlocked: error.actionBlocked,
+ stage: error.stage,
+ raw: serializeError(error.rawError),
+ },
+ };
+}
+
+export const errorToConsoleString = (state: SwapState, error: SwapError): string => {
+ const payload = buildErrorPayload(state, error);
+ return JSON.stringify(payload, null, 2);
+};
+
+export const errorToConsole = (state: SwapState, error: SwapError) => {
+ const pretty = errorToConsoleString(state, error);
+ console.error('Aave Swap Error\n' + pretty);
+};
diff --git a/src/components/transactions/Swap/helpers/cow/adapters.helpers.ts b/src/components/transactions/Swap/helpers/cow/adapters.helpers.ts
new file mode 100644
index 0000000000..d6e73ede08
--- /dev/null
+++ b/src/components/transactions/Swap/helpers/cow/adapters.helpers.ts
@@ -0,0 +1,304 @@
+import { valueToBigNumber } from '@aave/math-utils';
+import {
+ AppDataParams,
+ getOrderToSign,
+ LimitTradeParameters,
+ OrderKind,
+ OrderSigningUtils,
+ SupportedChainId,
+} from '@cowprotocol/cow-sdk';
+import {
+ AaveCollateralSwapSdk,
+ AaveFlashLoanType,
+ EncodedOrder,
+ FlashLoanHookAmounts,
+ HASH_ZERO,
+} from '@cowprotocol/sdk-flash-loans';
+
+import {
+ COW_PARTNER_FEE,
+ DUST_PROTECTION_MULTIPLIER,
+ FLASH_LOAN_FEE_BPS,
+} from '../../constants/cow.constants';
+import { isCowProtocolRates, OrderType, SwapProvider, SwapState, SwapType } from '../../types';
+import { getCowFlashLoanSdk } from './env.helpers';
+
+export const calculateInstanceAddress = async ({
+ user,
+ validTo,
+ type,
+ state,
+}: {
+ user: string;
+ validTo: number;
+ type: AaveFlashLoanType;
+ state: SwapState;
+}) => {
+ if (!user) return;
+ if (
+ !state.sellAmountBigInt ||
+ !state.buyAmountBigInt ||
+ !state.sellAmountToken ||
+ !state.buyAmountToken
+ )
+ return;
+
+ const flashLoanSdk = await getCowFlashLoanSdk(state.chainId);
+ const {
+ sellAmount,
+ buyAmountWithMarginForDustProtection,
+ buyAmount,
+ sellToken,
+ buyToken,
+ quoteId,
+ side,
+ slippageBps,
+ partnerFee,
+ } = {
+ sellAmount: state.sellAmountBigInt,
+ // @note: We wont have dust for borrow side, but we may have dust in collateral swaps
+ buyAmountWithMarginForDustProtection:
+ state.swapType !== SwapType.CollateralSwap
+ ? valueToBigNumber(state.buyAmountBigInt.toString())
+ .multipliedBy(DUST_PROTECTION_MULTIPLIER)
+ .toFixed(0)
+ : state.buyAmountBigInt,
+ sellToken: state.sellAmountToken,
+ buyAmount: state.buyAmountBigInt,
+ buyToken: state.buyAmountToken,
+ quoteId: isCowProtocolRates(state.swapRate) ? state.swapRate?.quoteId : undefined,
+ side: state.processedSide,
+ slippageBps: state.orderType == OrderType.MARKET ? Number(state.slippage) * 100 : undefined,
+ partnerFee: COW_PARTNER_FEE(state.sellAmountToken.symbol, state.buyAmountToken.symbol),
+ };
+
+ const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
+ flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
+ sellAmount: sellAmount,
+ });
+
+ const limitOrder: LimitTradeParameters = {
+ sellToken: sellToken.underlyingAddress,
+ sellTokenDecimals: sellToken.decimals,
+ buyToken: buyToken.underlyingAddress,
+ buyTokenDecimals: buyToken.decimals,
+ sellAmount: sellAmountToSign.toString(),
+ buyAmount: buyAmount.toString(),
+ kind: side === 'buy' ? OrderKind.BUY : OrderKind.SELL,
+ quoteId,
+ validTo,
+ slippageBps,
+ partnerFee,
+ };
+
+ const orderToSign = getOrderToSign(
+ {
+ chainId: state.chainId,
+ from: user,
+ networkCostsAmount: '0',
+ isEthFlow: false,
+ applyCostsSlippageAndFees: false,
+ },
+ limitOrder,
+ HASH_ZERO
+ );
+
+ const encodedOrder: EncodedOrder = {
+ ...OrderSigningUtils.encodeUnsignedOrder(orderToSign),
+ appData: HASH_ZERO,
+ validTo,
+ };
+
+ const hookAmounts: FlashLoanHookAmounts = {
+ flashLoanAmount: sellAmount.toString(),
+ flashLoanFeeAmount: flashLoanFeeAmount.toString(),
+ sellAssetAmount: sellAmount.toString(),
+ buyAssetAmount: buyAmountWithMarginForDustProtection.toString(),
+ };
+
+ return await flashLoanSdk.getExpectedInstanceAddress(
+ type,
+ state.chainId,
+ user as `0x${string}`,
+ hookAmounts,
+ encodedOrder
+ );
+};
+
+export const calculateFlashLoanAmounts = (
+ state: SwapState
+): {
+ flashLoanFeeAmount: bigint;
+ finalSellAmount: bigint;
+} => {
+ const flashLoanSdk = new AaveCollateralSwapSdk();
+ const sellAmount = state.sellAmountBigInt;
+
+ if (!sellAmount)
+ return {
+ flashLoanFeeAmount: BigInt(0),
+ finalSellAmount: BigInt(0),
+ };
+
+ if (state.swapType === SwapType.Swap || state.provider !== SwapProvider.COW_PROTOCOL) {
+ return {
+ flashLoanFeeAmount: BigInt(0),
+ finalSellAmount: sellAmount,
+ };
+ }
+
+ const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
+ sellAmount: sellAmount,
+ flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
+ });
+
+ return {
+ flashLoanFeeAmount,
+ finalSellAmount: sellAmountToSign,
+ };
+};
+
+/**
+ * This helper function is used to get the app data for a quote from the CowSwap API when adapters are used
+ * The goal is to let the solvers know that a flash loan is being used so the quote contemplates the higher gas costs
+ * and therefore the quote is more precise and more chances of being executed
+ * It's important to send the hooks and flashloan hint but not the exact amounts that will be used in the final
+ */
+export const getAppDataForQuote = async ({}: // user,
+// type,
+// amount,
+// chainId,
+// srcToken,
+// srcDecimals,
+// destToken,
+// destDecimals,
+{
+ user: string;
+ type: SwapType;
+ amount: string;
+ chainId: SupportedChainId;
+ srcToken: string;
+ srcDecimals: number;
+ destToken: string;
+ destDecimals: number;
+}): Promise => {
+ return undefined;
+
+ // NOTE: This function is prepared to add solver hooks for accurate network cost estimation,
+ // but such estimations are not currently supported so solvers are absorbing some costs.
+ // Disabled for now; enable when proper support becomes available.
+
+ // if (type === SwapType.Swap || type === SwapType.WithdrawAndSwap) {
+ // return undefined; // no flashloan needed - plain swap
+ // }
+
+ // const factory =
+ // AAVE_ADAPTER_FACTORY[chainId].length > 0 ? AAVE_ADAPTER_FACTORY[chainId] : API_ETH_MOCK_ADDRESS;
+ // const pool =
+ // AAVE_POOL_ADDRESS[chainId].length > 0 ? AAVE_POOL_ADDRESS[chainId] : API_ETH_MOCK_ADDRESS;
+ // const AAVE_SWAP_TYPE_TO_COW_TYPE: Partial> = {
+ // [SwapType.CollateralSwap]: AaveFlashLoanType.CollateralSwap,
+ // [SwapType.DebtSwap]: AaveFlashLoanType.DebtSwap,
+ // [SwapType.RepayWithCollateral]: AaveFlashLoanType.RepayCollateral,
+ // } as const;
+ // const dappId =
+ // AAVE_DAPP_ID_PER_TYPE[AAVE_SWAP_TYPE_TO_COW_TYPE[type] ?? AaveFlashLoanType.CollateralSwap];
+
+ // // const flashLoanSdk = new AaveCollateralSwapSdk();
+ // // const { flashLoanFeeAmount, sellAmountToSign } = flashLoanSdk.calculateFlashLoanAmounts({
+ // // sellAmount: BigInt(normalize(amount, -srcDecimals)),
+ // // flashLoanFeeBps: FLASH_LOAN_FEE_BPS,
+ // // });
+
+ // // let cowType: AaveFlashLoanType;
+ // // if (type === SwapType.CollateralSwap) {
+ // // cowType = AaveFlashLoanType.CollateralSwap;
+ // // } else if (type === SwapType.DebtSwap) {
+ // // cowType = AaveFlashLoanType.DebtSwap;
+ // // } else if(type === SwapType.RepayWithCollateral) {
+ // // cowType = AaveFlashLoanType.RepayCollateral;
+ // // } else {
+ // // throw new Error('Invalid swap type');
+ // // }
+
+ // // const hookAmounts: FlashLoanHookAmounts = {
+ // // flashLoanAmount: amount,
+ // // flashLoanFeeAmount: flashLoanFeeAmount.toString(),
+ // // sellAssetAmount: sellAmountToSign.toString(),
+ // // buyAssetAmount: amount,
+ // // }
+
+ // const flashloan: FlashLoanHint = {
+ // amount, // this is actually in UNDERLYING but aave tokens are 1:1
+ // receiver: factory,
+ // liquidityProvider: pool,
+ // protocolAdapter: factory,
+ // token: srcToken,
+ // };
+
+ // // const limitOrder: LimitTradeParameters = {
+ // // kind: OrderKind.SELL,
+ // // sellToken: srcToken,
+ // // sellTokenDecimals: srcDecimals,
+ // // buyToken: destToken,
+ // // buyTokenDecimals: destDecimals,
+ // // sellAmount: normalize(amount, -srcDecimals).toString(),
+ // // buyAmount: amount,
+ // // }
+
+ // // const orderToSign = getOrderToSign(
+ // // {
+ // // chainId,
+ // // from: user,
+ // // networkCostsAmount: '0',
+ // // isEthFlow: false,
+ // // applyCostsSlippageAndFees: false,
+ // // },
+ // // limitOrder,
+ // // HASH_ZERO
+ // // );
+
+ // // const encodedOrder: EncodedOrder = {
+ // // ...OrderSigningUtils.encodeUnsignedOrder(orderToSign),
+ // // appData: HASH_ZERO,
+ // // }
+
+ // // const hooks = await getOrderHooks(
+ // // cowType,
+ // // chainId,
+ // // user as `0x${string}`,
+ // // zeroAddress,
+ // // hookAmounts,
+ // // {
+ // // ...encodedOrder,
+ // // receiver: zeroAddress,
+ // // },
+ // // );
+
+ // // TODO: send proper calldatas when available so solvers can properly simulate
+ // const hooks = {
+ // pre: [
+ // {
+ // target: factory,
+ // callData: '0x',
+ // gasLimit: 160k DEFAULT_HOOK_GAS_LIMIT.pre.toString(),
+ // dappId,
+ // },
+ // ],
+ // post: [
+ // {
+ // target: 0x,
+ // callData: '0x',
+ // gasLimit: 160k DEFAULT_HOOK_GAS_LIMIT.post.toString(),
+ // dappId,
+ // },
+ // ],
+ // };
+
+ // return {
+ // metadata: {
+ // flashloan,
+ // hooks,
+ // },
+ // };
+};
diff --git a/src/components/transactions/Swap/helpers/cow/env.helpers.ts b/src/components/transactions/Swap/helpers/cow/env.helpers.ts
new file mode 100644
index 0000000000..f17ed809b9
--- /dev/null
+++ b/src/components/transactions/Swap/helpers/cow/env.helpers.ts
@@ -0,0 +1,55 @@
+import { CowEnv, setGlobalAdapter, TradingSdk } from '@cowprotocol/cow-sdk';
+import { AaveCollateralSwapSdk } from '@cowprotocol/sdk-flash-loans';
+import { ViemAdapter } from '@cowprotocol/sdk-viem-adapter';
+import { wagmiConfig } from 'src/ui-config/wagmiConfig';
+import { getPublicClient, getWalletClient } from 'wagmi/actions';
+
+import { ADAPTER_FACTORY, HOOK_ADAPTER_PER_TYPE } from '../../constants/cow.constants';
+import { APP_CODE_PER_SWAP_TYPE } from '../../constants/shared.constants';
+import { SwapState } from '../../types';
+import { COW_ENV } from './orders.helpers';
+
+export const getCowTradingSdk = async (state: SwapState, env: CowEnv = 'prod') => {
+ return getCowTradingSdkByChainIdAndAppCode(
+ state.chainId,
+ APP_CODE_PER_SWAP_TYPE[state.swapType],
+ env
+ );
+};
+
+export const getCowTradingSdkByChainIdAndAppCode = async (
+ chainId: number,
+ appCode: string,
+ env: CowEnv = COW_ENV
+) => {
+ const adapter = await getCowAdapter(chainId);
+ return new TradingSdk(
+ {
+ chainId,
+ appCode,
+ env,
+ signer: adapter.signer,
+ },
+ {},
+ adapter
+ );
+};
+
+export const getCowFlashLoanSdk = async (chainId: number) => {
+ setGlobalAdapter(await getCowAdapter(chainId));
+ return new AaveCollateralSwapSdk({
+ hookAdapterPerType: HOOK_ADAPTER_PER_TYPE,
+ aaveAdapterFactory: ADAPTER_FACTORY,
+ });
+};
+
+export const getCowAdapter = async (chainId: number) => {
+ const walletClient = await getWalletClient(wagmiConfig, { chainId });
+ const publicClient = getPublicClient(wagmiConfig, { chainId });
+
+ if (!publicClient || !walletClient) {
+ throw new Error('Wallet not connected');
+ }
+
+ return new ViemAdapter({ provider: publicClient, walletClient });
+};
diff --git a/src/components/transactions/Swap/helpers/cow/index.ts b/src/components/transactions/Swap/helpers/cow/index.ts
new file mode 100644
index 0000000000..494c0a7fb1
--- /dev/null
+++ b/src/components/transactions/Swap/helpers/cow/index.ts
@@ -0,0 +1,4 @@
+export * from './adapters.helpers';
+export * from './env.helpers';
+export * from './orders.helpers';
+export * from './rates.helpers';
diff --git a/src/components/transactions/Swap/helpers/cow/orders.helpers.ts b/src/components/transactions/Swap/helpers/cow/orders.helpers.ts
new file mode 100644
index 0000000000..4d271d702a
--- /dev/null
+++ b/src/components/transactions/Swap/helpers/cow/orders.helpers.ts
@@ -0,0 +1,541 @@
+import { API_ETH_MOCK_ADDRESS } from '@aave/contract-helpers';
+import {
+ BuyTokenDestination,
+ CowEnv,
+ OrderBookApi,
+ OrderClass,
+ OrderKind,
+ OrderParameters,
+ OrderStatus,
+ QuoteAndPost,
+ SellTokenSource,
+ SigningScheme,
+ SlippageToleranceRequest,
+ SlippageToleranceResponse,
+ SupportedChainId,
+ WRAPPED_NATIVE_CURRENCIES,
+} from '@cowprotocol/cow-sdk';
+import { AnyAppDataDocVersion, AppDataParams, MetadataApi } from '@cowprotocol/sdk-app-data';
+import { JsonRpcProvider } from '@ethersproject/providers';
+import { BigNumber, ethers, PopulatedTransaction } from 'ethers';
+import { isSmartContractWallet } from 'src/helpers/provider';
+
+import { SignedParams } from '../../actions/approval/useSwapTokenApproval';
+import {
+ COW_APP_DATA,
+ COW_CREATE_ORDER_ABI,
+ COW_PROTOCOL_ETH_FLOW_ADDRESS_BY_ENV,
+ isChainIdSupportedByCoWProtocol,
+} from '../../constants/cow.constants';
+import { OrderType } from '../../types';
+import { getCowTradingSdkByChainIdAndAppCode } from './env.helpers';
+
+export const COW_ENV: CowEnv = 'prod';
+
+const EIP_2612_PERMIT_ABI = [
+ {
+ constant: false,
+ inputs: [
+ { name: 'owner', type: 'address' },
+ { name: 'spender', type: 'address' },
+ { name: 'value', type: 'uint256' },
+ { name: 'deadline', type: 'uint256' },
+ { name: 'v', type: 'uint8' },
+ { name: 'r', type: 'bytes32' },
+ { name: 's', type: 'bytes32' },
+ ],
+ name: 'permit',
+ outputs: [],
+ payable: false,
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+];
+
+export type CowProtocolActionParams = {
+ orderType: OrderType;
+ quote?: OrderParameters;
+ provider: JsonRpcProvider;
+ chainId: number;
+ user: string;
+ sellAmount: string;
+ buyAmount: string;
+ tokenDest: string;
+ tokenSrc: string;
+ tokenSrcDecimals: number;
+ tokenDestDecimals: number;
+ inputSymbol: string;
+ outputSymbol: string;
+ slippageBps: number;
+ smartSlippage: boolean;
+ appCode: string;
+ kind: OrderKind;
+ orderBookQuote?: QuoteAndPost;
+ signatureParams?: SignedParams;
+ estimateGasLimit?: (tx: PopulatedTransaction, chainId?: number) => Promise;
+ validTo: number;
+};
+
+export const getPreSignTransaction = async ({
+ provider,
+ chainId,
+ user,
+ slippageBps,
+ smartSlippage,
+ inputSymbol,
+ outputSymbol,
+ appCode,
+ orderType,
+ sellAmount,
+ buyAmount,
+ tokenSrc,
+ tokenDest,
+ tokenSrcDecimals,
+ tokenDestDecimals,
+ kind,
+ validTo,
+}: CowProtocolActionParams) => {
+ if (!isChainIdSupportedByCoWProtocol(chainId)) {
+ throw new Error('Chain not supported.');
+ }
+
+ const tradingSdk = await getCowTradingSdkByChainIdAndAppCode(chainId, appCode);
+ const isSmartContract = await isSmartContractWallet(user, provider);
+ if (!isSmartContract) {
+ throw new Error('Only smart contract wallets should use presign.');
+ }
+
+ const orderResult = await tradingSdk.postLimitOrder(
+ {
+ sellAmount,
+ buyAmount,
+ kind: kind == OrderKind.SELL ? OrderKind.SELL : OrderKind.BUY,
+ sellToken: tokenSrc,
+ buyToken: tokenDest,
+ sellTokenDecimals: tokenSrcDecimals,
+ buyTokenDecimals: tokenDestDecimals,
+ validTo,
+ owner: user as `0x${string}`,
+ env: COW_ENV,
+ },
+ {
+ appData: COW_APP_DATA(
+ inputSymbol,
+ outputSymbol,
+ slippageBps,
+ smartSlippage,
+ orderType,
+ appCode
+ ),
+ additionalParams: {
+ signingScheme: SigningScheme.PRESIGN,
+ },
+ }
+ );
+
+ const preSignTransaction = await tradingSdk.getPreSignTransaction({
+ orderUid: orderResult.orderId,
+ signer: provider?.getSigner(),
+ });
+
+ return {
+ ...preSignTransaction,
+ orderId: orderResult.orderId,
+ };
+};
+
+// Only for EOA wallets
+export const sendOrder = async ({
+ provider,
+ chainId,
+ user,
+ slippageBps,
+ inputSymbol,
+ outputSymbol,
+ smartSlippage,
+ appCode,
+ orderType,
+ sellAmount,
+ buyAmount,
+ tokenSrc,
+ tokenDest,
+ tokenSrcDecimals,
+ tokenDestDecimals,
+ kind,
+ signatureParams,
+ estimateGasLimit,
+ validTo,
+}: CowProtocolActionParams) => {
+ const signer = provider?.getSigner();
+
+ if (!isChainIdSupportedByCoWProtocol(chainId)) {
+ throw new Error('Chain not supported.');
+ }
+
+ if (!signer) {
+ throw new Error('No signer found in provider');
+ }
+
+ const isSmartContract = await isSmartContractWallet(user, provider);
+ if (isSmartContract) {
+ throw new Error('Smart contract wallets should use presign.');
+ }
+
+ const permitHook =
+ signatureParams && estimateGasLimit
+ ? await getPermitHook({ tokenAddress: tokenSrc, signatureParams, estimateGasLimit, chainId })
+ : undefined;
+
+ const hooks = permitHook
+ ? {
+ pre: [permitHook],
+ }
+ : undefined;
+
+ const appData = COW_APP_DATA(
+ inputSymbol,
+ outputSymbol,
+ slippageBps,
+ smartSlippage,
+ orderType,
+ appCode,
+ hooks
+ );
+
+ const tradingSdk = await getCowTradingSdkByChainIdAndAppCode(chainId, appCode);
+
+ return tradingSdk
+ .postLimitOrder(
+ {
+ sellAmount,
+ buyAmount,
+ kind: kind == OrderKind.SELL ? OrderKind.SELL : OrderKind.BUY,
+ sellToken: tokenSrc,
+ buyToken: tokenDest,
+ sellTokenDecimals: tokenSrcDecimals,
+ buyTokenDecimals: tokenDestDecimals,
+ validTo,
+ owner: user as `0x${string}`,
+ env: COW_ENV,
+ },
+ {
+ appData,
+ additionalParams: {
+ applyCostsSlippageAndFees: false,
+ },
+ }
+ )
+ .then((orderResult) => orderResult.orderId);
+};
+
+export const getOrderStatus = async (orderId: string, chainId: number) => {
+ const orderBookApi = new OrderBookApi({ chainId: chainId, env: COW_ENV });
+ const status = await orderBookApi.getOrderCompetitionStatus(orderId, {
+ chainId,
+ });
+ return status.type;
+};
+
+export const getOrder = async (orderId: string, chainId: number) => {
+ const orderBookApi = new OrderBookApi({ chainId, env: COW_ENV });
+ const order = await orderBookApi.getOrder(orderId, {
+ chainId,
+ });
+ return order;
+};
+
+export const getOrders = async (chainId: number, account: string) => {
+ const orderBookApi = new OrderBookApi({ chainId, env: COW_ENV });
+ const orders = await orderBookApi.getOrders({
+ owner: account,
+ });
+
+ return orders;
+};
+
+export const isOrderLoading = (status: OrderStatus) => {
+ return status === OrderStatus.OPEN || status === OrderStatus.PRESIGNATURE_PENDING;
+};
+
+export const isOrderFilled = (status: OrderStatus) => {
+ return status === OrderStatus.FULFILLED;
+};
+
+export const isOrderExpired = (status: OrderStatus) => {
+ return status === OrderStatus.EXPIRED;
+};
+
+export const isOrderCancelled = (status: OrderStatus) => {
+ return status === OrderStatus.CANCELLED;
+};
+
+export const isNativeToken = (token?: string) => {
+ return token?.toLowerCase() === API_ETH_MOCK_ADDRESS.toLowerCase();
+};
+
+// TODO: make object param
+export const getUnsignerOrder = async ({
+ sellAmount,
+ buyAmount,
+ dstToken,
+ user,
+ chainId,
+ tokenFromSymbol,
+ tokenToSymbol,
+ slippageBps,
+ smartSlippage,
+ appCode,
+ orderType,
+ validTo,
+ srcToken,
+ receiver,
+}: {
+ sellAmount: string;
+ buyAmount: string;
+ dstToken: string;
+ user: string;
+ chainId: number;
+ tokenFromSymbol: string;
+ tokenToSymbol: string;
+ slippageBps: number;
+ smartSlippage: boolean;
+ appCode: string;
+ orderType: OrderType;
+ validTo: number;
+ srcToken?: string;
+ receiver?: string;
+}) => {
+ const metadataApi = new MetadataApi();
+ const { appDataHex } = await metadataApi.getAppDataInfo(
+ COW_APP_DATA(tokenFromSymbol, tokenToSymbol, slippageBps, smartSlippage, orderType, appCode)
+ );
+
+ return {
+ buyToken: dstToken,
+ receiver: receiver || user,
+ sellAmount,
+ buyAmount,
+ appData: appDataHex,
+ feeAmount: '0',
+ validTo: validTo,
+ partiallyFillable: false,
+ kind: OrderKind.SELL,
+ sellToken: srcToken
+ ? srcToken.toLowerCase()
+ : WRAPPED_NATIVE_CURRENCIES[chainId as SupportedChainId].address.toLowerCase(),
+ buyTokenBalance: BuyTokenDestination.ERC20,
+ sellTokenBalance: SellTokenSource.ERC20,
+ };
+};
+
+export const hashAppData = async (appData: AnyAppDataDocVersion) => {
+ const metadataApi = new MetadataApi();
+ const { appDataHex } = await metadataApi.getAppDataInfo(appData);
+ return appDataHex;
+};
+
+export const populateEthFlowTx = async (
+ sellAmount: string,
+ buyAmount: string,
+ dstToken: string,
+ user: string,
+ validTo: number,
+ tokenFromSymbol: string,
+ tokenToSymbol: string,
+ slippageBps: number,
+ smartSlippage: boolean,
+ appCode: string,
+ orderType: OrderType,
+ quoteId?: number
+): Promise => {
+ const appDataHex = await hashAppData(
+ COW_APP_DATA(tokenFromSymbol, tokenToSymbol, slippageBps, smartSlippage, orderType, appCode)
+ );
+
+ const orderData = {
+ buyToken: dstToken,
+ receiver: user,
+ sellAmount,
+ buyAmount,
+ appData: appDataHex,
+ feeAmount: '0',
+ validTo,
+ partiallyFillable: false,
+ quoteId: quoteId || 0,
+ };
+
+ const value = BigNumber.from(sellAmount);
+
+ // Create the contract interface
+ const iface = new ethers.utils.Interface([COW_CREATE_ORDER_ABI]);
+
+ // Encode the function call
+ const data = iface.encodeFunctionData('createOrder', [
+ [
+ orderData.buyToken,
+ orderData.receiver,
+ orderData.sellAmount,
+ orderData.buyAmount,
+ orderData.appData,
+ orderData.feeAmount,
+ orderData.validTo,
+ orderData.partiallyFillable,
+ orderData.quoteId,
+ ],
+ ]);
+
+ return {
+ to: COW_PROTOCOL_ETH_FLOW_ADDRESS_BY_ENV(COW_ENV),
+ value,
+ data,
+ };
+};
+
+export const getRecommendedSlippage = (srcUSD: string) => {
+ try {
+ if (Number(srcUSD) <= 0) {
+ return Number(0.5);
+ }
+
+ if (Number(srcUSD) <= 1) {
+ return Number(5.0);
+ } else if (Number(srcUSD) <= 5) {
+ return Number(2.5);
+ } else if (Number(srcUSD) <= 10) {
+ return Number(1.5);
+ } else {
+ return Number(0.5);
+ }
+ } catch (e) {
+ return Number(0.5);
+ }
+};
+
+export const uploadAppData = async (orderId: string, appDataHex: string, chainId: number) => {
+ const orderBookApi = new OrderBookApi({ chainId, env: COW_ENV });
+
+ return orderBookApi.uploadAppData(orderId, appDataHex);
+};
+
+export const generateCoWExplorerLink = (chainId: SupportedChainId, orderId?: string) => {
+ if (!orderId) {
+ return undefined;
+ }
+
+ const base = 'https://explorer.cow.fi';
+ switch (chainId) {
+ case SupportedChainId.MAINNET:
+ return `${base}/orders/${orderId}`;
+ case SupportedChainId.GNOSIS_CHAIN:
+ return `${base}/gc/orders/${orderId}`;
+ case SupportedChainId.BASE:
+ return `${base}/base/orders/${orderId}`;
+ case SupportedChainId.ARBITRUM_ONE:
+ return `${base}/arb1/orders/${orderId}`;
+ case SupportedChainId.SEPOLIA:
+ return `${base}/sepolia/orders/${orderId}`;
+ case SupportedChainId.AVALANCHE:
+ return `${base}/avax/orders/${orderId}`;
+ case SupportedChainId.POLYGON:
+ return `${base}/pol/orders/${orderId}`;
+ case SupportedChainId.BNB:
+ return `${base}/bnb/orders/${orderId}`;
+ default:
+ throw new Error('Define explorer link for chainId: ' + chainId);
+ }
+};
+
+export const adjustedBps = (sdkFeeBps: number) => {
+ const f = sdkFeeBps / 10000;
+ const effective = f / (1 + f);
+ return effective * 10000;
+};
+
+export const getPermitHook = async ({
+ tokenAddress,
+ signatureParams,
+ estimateGasLimit,
+ chainId,
+}: {
+ tokenAddress: string;
+ signatureParams: SignedParams;
+ estimateGasLimit: (tx: PopulatedTransaction, chainId?: number) => Promise;
+ chainId: number;
+}) => {
+ // Decode the owner from the stored encoded signature payload if needed
+ const [owner] = ethers.utils.defaultAbiCoder.decode(
+ ['address', 'address', 'uint256', 'uint256', 'uint8', 'bytes32', 'bytes32'],
+ signatureParams.signature
+ );
+
+ const iface = new ethers.utils.Interface(EIP_2612_PERMIT_ABI);
+ const { v, r, s } = signatureParams.splitedSignature;
+ const spender = signatureParams.approvedToken; // Vault Relayer / adapter address
+ const value = signatureParams.amount;
+ const deadline = signatureParams.deadline;
+
+ const callData = iface.encodeFunctionData('permit', [owner, spender, value, deadline, v, r, s]);
+
+ const PERMIT_HOOK_DAPP_ID = 'cow.fi';
+ const gasLimit = '80000';
+
+ const tx: PopulatedTransaction = {
+ to: tokenAddress,
+ data: callData,
+ gasLimit: BigNumber.from(gasLimit),
+ };
+
+ const txWithGasEstimation = await estimateGasLimit(tx, chainId);
+
+ return {
+ target: txWithGasEstimation.to,
+ callData: txWithGasEstimation.data,
+ gasLimit: txWithGasEstimation.gasLimit?.toString(),
+ dappId: PERMIT_HOOK_DAPP_ID,
+ };
+};
+
+// This function is used to get the slippage suggestion for a token pair on the respective chain based on the pair volatility.
+export const getSlippageSuggestion = async (
+ request: SlippageToleranceRequest
+): Promise => {
+ const { sellToken, buyToken } = request;
+
+ try {
+ if (request.chainId && sellToken && buyToken) {
+ const chainSlug = request.chainId; // e.g., 42161 for Arbitrum
+ const sell = sellToken.toLowerCase();
+ const buy = buyToken.toLowerCase();
+ const url = `https://bff.cow.fi/${chainSlug}/markets/${sell}-${buy}/slippageTolerance`;
+
+ const res = await fetch(url);
+
+ if (res.ok) {
+ const result = await res.json();
+ // The endpoint returns { slippageBps: number }
+ // This is expected by the CoW SDK within the Slippage logic.
+ return result;
+ }
+ }
+ } catch (e) {
+ console.error('Error fetching slippage suggestion:', e);
+ return { slippageBps: 0 };
+ }
+
+ return { slippageBps: 0 };
+};
+
+export const addOrderTypeToAppData = (
+ orderType: OrderType,
+ appData?: AppDataParams
+): AppDataParams => {
+ return {
+ ...appData,
+ metadata: {
+ ...appData?.metadata,
+ orderClass: {
+ orderClass: orderType === OrderType.LIMIT ? OrderClass.LIMIT : OrderClass.MARKET,
+ },
+ },
+ };
+};
diff --git a/src/components/transactions/Swap/helpers/cow/rates.helpers.ts b/src/components/transactions/Swap/helpers/cow/rates.helpers.ts
new file mode 100644
index 0000000000..695ba80609
--- /dev/null
+++ b/src/components/transactions/Swap/helpers/cow/rates.helpers.ts
@@ -0,0 +1,275 @@
+import { ChainId } from '@aave/contract-helpers';
+import { OrderKind, QuoteAndPost, WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/cow-sdk';
+import { BigNumber } from 'bignumber.js';
+import { getEthersProvider } from 'src/libs/web3-data-provider/adapters/EthersAdapter';
+import { CoWProtocolPricesService } from 'src/services/CoWProtocolPricesService';
+import { FamilyPricesService } from 'src/services/FamilyPricesService';
+import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
+import { wagmiConfig } from 'src/ui-config/wagmiConfig';
+import { getNetworkConfig } from 'src/utils/marketsAndNetworksConfig';
+
+import { COW_PARTNER_FEE, isChainIdSupportedByCoWProtocol } from '../../constants/cow.constants';
+import { isNativeToken } from '../../helpers/cow';
+import { CowProtocolRatesType, ProviderRatesParams, SwapProvider } from '../../types';
+import { getAppDataForQuote } from './adapters.helpers';
+import { getCowTradingSdkByChainIdAndAppCode } from './env.helpers';
+import { getSlippageSuggestion } from './orders.helpers';
+
+export const getTokenUsdPrice = async (
+ chainId: number,
+ tokenAddress: string,
+ isTokenCustom: boolean,
+ isMainnet: boolean
+) => {
+ const cowProtocolPricesService = new CoWProtocolPricesService();
+ const familyPricesService = new FamilyPricesService();
+
+ try {
+ let price;
+
+ if (!isTokenCustom && isMainnet) {
+ price = await familyPricesService.getTokenUsdPrice(chainId, tokenAddress);
+ }
+
+ if (price) {
+ return price;
+ }
+
+ return await cowProtocolPricesService.getTokenUsdPrice(chainId, tokenAddress);
+ } catch (familyError) {
+ console.error(familyError);
+ return undefined;
+ }
+};
+
+export async function getCowProtocolSellRates({
+ chainId,
+ amount,
+ srcToken,
+ srcDecimals,
+ destToken,
+ destDecimals,
+ user,
+ swapType,
+ inputSymbol,
+ outputSymbol,
+ isInputTokenCustom,
+ isOutputTokenCustom,
+ appCode,
+ setError,
+ side = 'sell',
+ invertedQuoteRoute = false,
+}: ProviderRatesParams): Promise {
+ const tradingSdk = await getCowTradingSdkByChainIdAndAppCode(chainId, appCode);
+ let orderBookQuote: QuoteAndPost | undefined;
+ let srcTokenPriceUsd: string | undefined;
+ let destTokenPriceUsd: string | undefined;
+ try {
+ if (!isChainIdSupportedByCoWProtocol(chainId)) {
+ throw new Error('Chain not supported by CowProtocol');
+ }
+
+ // If srcToken is native, we need to use the wrapped token for the quote
+ let srcTokenWrapped = srcToken;
+ if (isNativeToken(srcToken)) {
+ srcTokenWrapped = WRAPPED_NATIVE_CURRENCIES[chainId].address;
+ }
+
+ let destTokenWrapped = destToken;
+ if (isNativeToken(destToken)) {
+ destTokenWrapped = WRAPPED_NATIVE_CURRENCIES[chainId].address;
+ }
+
+ const provider = await getEthersProvider(wagmiConfig, { chainId });
+ const signer = provider?.getSigner();
+ const isMainnet =
+ !getNetworkConfig(chainId as unknown as ChainId).isTestnet &&
+ !getNetworkConfig(chainId as unknown as ChainId).isFork;
+
+ if (!inputSymbol || !outputSymbol) {
+ throw new Error('No input or output symbol provided');
+ }
+
+ [orderBookQuote, srcTokenPriceUsd, destTokenPriceUsd] = await Promise.all([
+ tradingSdk
+ .getQuote(
+ {
+ owner: user as `0x${string}`,
+ kind: side === 'buy' ? OrderKind.BUY : OrderKind.SELL,
+ amount,
+ sellToken: srcTokenWrapped,
+ sellTokenDecimals: srcDecimals,
+ buyToken: destTokenWrapped,
+ buyTokenDecimals: destDecimals,
+ signer,
+ appCode: appCode,
+ partnerFee: COW_PARTNER_FEE(inputSymbol, outputSymbol),
+ },
+ {
+ // Price Quality is set to OPTIMAL by default
+ appData: await getAppDataForQuote({
+ user,
+ type: swapType,
+ chainId,
+ amount,
+ srcToken,
+ srcDecimals,
+ destToken,
+ destDecimals,
+ }),
+ getSlippageSuggestion,
+ }
+ )
+ .catch((cowError) => {
+ console.error(cowError);
+ throw new Error(cowError?.body?.errorType);
+ }),
+ getTokenUsdPrice(chainId, srcTokenWrapped, isInputTokenCustom ?? false, isMainnet),
+ getTokenUsdPrice(chainId, destTokenWrapped, isOutputTokenCustom ?? false, isMainnet),
+ ]);
+
+ if (!srcTokenPriceUsd || !destTokenPriceUsd) {
+ console.error('No price found for token');
+ const error = getErrorTextFromError(
+ new Error('No price found for token, please try another token'),
+ TxAction.MAIN_ACTION,
+ true
+ );
+ setError?.(error);
+ console.error(error);
+ throw new Error('No price found for token, please try another token');
+ }
+ } catch (error) {
+ console.error('generate error', error);
+ setError?.({
+ error,
+ blocking: true,
+ actionBlocked: true,
+ rawError: error,
+ txAction: TxAction.MAIN_ACTION,
+ });
+
+ throw error;
+ }
+
+ if (!orderBookQuote.quoteResults.suggestedSlippageBps) {
+ console.error('No suggested slippage found');
+ const error = getErrorTextFromError(
+ new Error('No suggested slippage found'),
+ TxAction.MAIN_ACTION,
+ true
+ );
+ setError?.(error);
+ console.error(error);
+ throw new Error('No suggested slippage found');
+ }
+
+ if (!orderBookQuote.quoteResults.amountsAndCosts.afterPartnerFees.buyAmount) {
+ console.error('No buy amount found');
+ const error = getErrorTextFromError(
+ new Error('No buy amount found'),
+ TxAction.MAIN_ACTION,
+ true
+ );
+ setError?.(error);
+ console.error(error);
+ throw new Error('No buy amount found');
+ }
+
+ let suggestedSlippage = (orderBookQuote.quoteResults.suggestedSlippageBps ?? 100) / 100; // E.g. 100 bps -> 1% 100 / 100 = 1
+
+ if (isNativeToken(srcToken)) {
+ // Recommended by CoW for potential reimbursments
+ if (chainId == 1 && suggestedSlippage < 2) {
+ suggestedSlippage = 2;
+ } else if (chainId != 1 && suggestedSlippage < 0.5) {
+ suggestedSlippage = 0.5;
+ }
+ }
+
+ if (invertedQuoteRoute) {
+ // Calculate Amounts
+ const srcSpotAmount =
+ side === 'sell'
+ ? orderBookQuote.quoteResults.amountsAndCosts.beforeNetworkCosts.buyAmount.toString()
+ : orderBookQuote.quoteResults.amountsAndCosts.afterNetworkCosts.buyAmount.toString();
+ const srcSpotUSD = BigNumber(destTokenPriceUsd)
+ .multipliedBy(BigNumber(srcSpotAmount).dividedBy(10 ** destDecimals))
+ .toString();
+ const destSpotAmount =
+ side === 'sell'
+ ? orderBookQuote.quoteResults.amountsAndCosts.beforeNetworkCosts.sellAmount.toString()
+ : orderBookQuote.quoteResults.amountsAndCosts.afterNetworkCosts.sellAmount.toString();
+ const destSpotUSD = BigNumber(srcTokenPriceUsd)
+ .multipliedBy(BigNumber(destSpotAmount).dividedBy(10 ** srcDecimals))
+ .toString();
+ const afterFeesAmount =
+ orderBookQuote.quoteResults.amountsAndCosts.afterPartnerFees.sellAmount.toString();
+ const afterFeesUSD = BigNumber(srcTokenPriceUsd)
+ .multipliedBy(BigNumber(afterFeesAmount).dividedBy(10 ** srcDecimals))
+ .toString();
+
+ return {
+ srcToken: destToken,
+ srcSpotUSD,
+ srcSpotAmount: srcSpotAmount,
+ srcDecimals: destDecimals,
+ destToken: srcToken,
+ destSpotAmount,
+ destSpotUSD,
+ afterFeesUSD,
+ afterFeesAmount,
+ destDecimals: srcDecimals,
+ orderBookQuote,
+ provider: SwapProvider.COW_PROTOCOL,
+ order: orderBookQuote.quoteResults.orderToSign,
+ quoteId: orderBookQuote.quoteResults.quoteResponse.id,
+ suggestedSlippage,
+ amountAndCosts: orderBookQuote.quoteResults.amountsAndCosts,
+ srcTokenPriceUsd: Number(destTokenPriceUsd),
+ destTokenPriceUsd: Number(srcTokenPriceUsd),
+ };
+ } else {
+ // Calculate Amounts
+ const srcSpotAmount =
+ orderBookQuote.quoteResults.orderToSign.kind === OrderKind.SELL
+ ? orderBookQuote.quoteResults.amountsAndCosts.afterNetworkCosts.sellAmount.toString()
+ : orderBookQuote.quoteResults.amountsAndCosts.beforeNetworkCosts.sellAmount.toString();
+ const srcSpotUSD = BigNumber(srcTokenPriceUsd)
+ .multipliedBy(BigNumber(srcSpotAmount).dividedBy(10 ** srcDecimals))
+ .toString();
+ const destSpotAmount =
+ orderBookQuote.quoteResults.orderToSign.kind === OrderKind.SELL
+ ? orderBookQuote.quoteResults.amountsAndCosts.beforeNetworkCosts.buyAmount.toString()
+ : orderBookQuote.quoteResults.amountsAndCosts.afterNetworkCosts.buyAmount.toString();
+ const destSpotUSD = BigNumber(destTokenPriceUsd)
+ .multipliedBy(BigNumber(destSpotAmount).dividedBy(10 ** destDecimals))
+ .toString();
+ const afterFeesAmount =
+ orderBookQuote.quoteResults.amountsAndCosts.afterPartnerFees.buyAmount.toString();
+ const afterFeesUSD = BigNumber(destTokenPriceUsd)
+ .multipliedBy(BigNumber(afterFeesAmount).dividedBy(10 ** destDecimals))
+ .toString();
+
+ return {
+ srcToken,
+ srcSpotUSD,
+ srcSpotAmount,
+ srcDecimals,
+ destToken,
+ destSpotAmount,
+ destSpotUSD,
+ afterFeesUSD,
+ afterFeesAmount,
+ destDecimals,
+ orderBookQuote,
+ provider: SwapProvider.COW_PROTOCOL,
+ order: orderBookQuote.quoteResults.orderToSign,
+ quoteId: orderBookQuote.quoteResults.quoteResponse.id,
+ suggestedSlippage,
+ amountAndCosts: orderBookQuote.quoteResults.amountsAndCosts,
+ srcTokenPriceUsd: Number(srcTokenPriceUsd),
+ destTokenPriceUsd: Number(destTokenPriceUsd),
+ };
+ }
+}
diff --git a/src/components/transactions/Swap/helpers/gasEstimation.helpers.ts b/src/components/transactions/Swap/helpers/gasEstimation.helpers.ts
new file mode 100644
index 0000000000..d347b67066
--- /dev/null
+++ b/src/components/transactions/Swap/helpers/gasEstimation.helpers.ts
@@ -0,0 +1,151 @@
+import { APPROVAL_GAS_LIMIT } from 'src/components/transactions/utils';
+import { TxStateType } from 'src/hooks/useModal';
+
+import { COW_PROTOCOL_GAS_LIMITS } from '../constants/cow.constants';
+import { PARASWAP_GAS_LIMITS } from '../constants/paraswap.constants';
+import { SwapProvider, SwapType, TokenType } from '../types';
+
+// Helper function to check if token is native
+const isNativeToken = (address: string): boolean => {
+ return (
+ address === '0x0000000000000000000000000000000000000000' ||
+ address === '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
+ );
+};
+
+export interface GasEstimationParams {
+ swapType: SwapType;
+ provider: SwapProvider;
+ sourceToken: { addressToSwap: string; tokenType: TokenType };
+ userIsSmartContractWallet: boolean;
+ requiresApproval: boolean;
+ requiresApprovalReset: boolean;
+ approvalTxState: TxStateType;
+ useFlashloan: boolean;
+ usePermit: boolean;
+}
+
+export interface GasEstimationResult {
+ gasLimit: string;
+ showGasStation: boolean;
+ breakdown: {
+ baseGas: number;
+ approvalGas: number;
+ resetApprovalGas: number;
+ total: number;
+ };
+}
+
+/**
+ * Centralized gas estimation logic for all swap types and providers
+ */
+export const estimateSwapGas = (params: GasEstimationParams): GasEstimationResult => {
+ const {
+ swapType,
+ provider,
+ sourceToken,
+ userIsSmartContractWallet,
+ requiresApproval,
+ requiresApprovalReset,
+ approvalTxState,
+ useFlashloan,
+ usePermit,
+ } = params;
+
+ let baseGas = 0;
+ let approvalGas = 0;
+ let resetApprovalGas = 0;
+ let showGasStation = false;
+ // Drastically reduced version of base gas estimation
+ if (provider === SwapProvider.PARASWAP) {
+ baseGas = PARASWAP_GAS_LIMITS[swapType] ?? 0;
+ showGasStation = true;
+ } else if (provider === SwapProvider.COW_PROTOCOL) {
+ const isEthNativeSwap = isNativeToken(sourceToken.addressToSwap);
+ if (
+ (swapType === SwapType.Swap && (isEthNativeSwap || userIsSmartContractWallet)) ||
+ (swapType === SwapType.CollateralSwap && !useFlashloan)
+ ) {
+ baseGas = COW_PROTOCOL_GAS_LIMITS[swapType] ?? 0;
+ showGasStation = true;
+ } else {
+ baseGas = 0;
+ showGasStation = false;
+ }
+ } else {
+ baseGas = 0;
+ showGasStation = false;
+ }
+
+ // Add approval gas if needed
+ if (requiresApproval && !approvalTxState.success && !usePermit) {
+ approvalGas = Number(APPROVAL_GAS_LIMIT);
+ showGasStation = true;
+ }
+
+ // Add reset approval gas if needed
+ if (requiresApprovalReset && !usePermit) {
+ resetApprovalGas = Number(APPROVAL_GAS_LIMIT);
+ showGasStation = true;
+ }
+
+ const total = baseGas + approvalGas + resetApprovalGas;
+
+ return {
+ gasLimit: total.toString(),
+ showGasStation,
+ breakdown: {
+ baseGas,
+ approvalGas,
+ resetApprovalGas,
+ total,
+ },
+ };
+};
+
+/**
+ * Determines if a swap requires gas based on provider and token types
+ */
+export const shouldShowGasStation = (
+ provider: SwapProvider,
+ sourceToken: { addressToSwap: string; tokenType: TokenType },
+ userIsSmartContractWallet: boolean,
+ requiresApproval: boolean
+): boolean => {
+ // Always show gas station for Paraswap
+ if (provider === SwapProvider.PARASWAP) {
+ return true;
+ }
+
+ // For CoW Protocol, only show gas station for ETH-native swaps or smart contract wallets
+ if (provider === SwapProvider.COW_PROTOCOL) {
+ const isEthNativeSwap = isNativeToken(sourceToken.addressToSwap);
+ return isEthNativeSwap || userIsSmartContractWallet || requiresApproval;
+ }
+
+ // For other providers, show gas station if approval is required
+ return requiresApproval;
+};
+
+/**
+ * Gets gas estimation for native token swaps (ETH, MATIC, etc.)
+ */
+export const getNativeTokenGasEstimation = (
+ chainId: number,
+ tokenType: TokenType
+): { gasRequired: string; showWarning: boolean } => {
+ // Different gas requirements for different chains
+ const gasRequirements = {
+ 1: '0.01', // Ethereum mainnet
+ 137: '0.001', // Polygon
+ 42161: '0.001', // Arbitrum
+ 10: '0.001', // Optimism
+ 56: '0.001', // BSC
+ 43114: '0.001', // Avalanche
+ };
+
+ const gasRequired = gasRequirements[chainId as keyof typeof gasRequirements] || '0.001';
+ const showWarning = tokenType === TokenType.NATIVE;
+
+ return { gasRequired, showWarning };
+};
diff --git a/src/components/transactions/Swap/helpers/paraswap/flashloan.helpers.ts b/src/components/transactions/Swap/helpers/paraswap/flashloan.helpers.ts
new file mode 100644
index 0000000000..1bb122358c
--- /dev/null
+++ b/src/components/transactions/Swap/helpers/paraswap/flashloan.helpers.ts
@@ -0,0 +1,46 @@
+import { valueToBigNumber } from '@aave/math-utils';
+
+import { PARASWAP_FLASH_LOAN_FEE_BPS } from '../../constants/paraswap.constants';
+import { SwapProvider, SwapState, SwapType } from '../../types';
+
+/**
+ * Calculate flashloan fee amount for Paraswap adapter swaps.
+ * The fee is 0.05% (5 bps) of the flashloan amount, which is the sell amount.
+ *
+ * @param state - Swap state
+ * @returns Object containing flashloan fee amount in bigint and formatted string
+ */
+export const calculateParaswapFlashLoanFee = (
+ state: SwapState
+): {
+ flashLoanFeeAmount: bigint;
+ flashLoanFeeFormatted: string;
+} => {
+ // Only calculate fee for protocol swaps using Paraswap with flashloan
+ if (
+ state.swapType === SwapType.Swap ||
+ state.provider !== SwapProvider.PARASWAP ||
+ !state.useFlashloan ||
+ !state.sellAmountBigInt
+ ) {
+ return {
+ flashLoanFeeAmount: BigInt(0),
+ flashLoanFeeFormatted: '0',
+ };
+ }
+
+ // Calculate fee: flashloan amount * fee bps / 10000
+ // The flashloan amount is the sell amount (collateral being swapped)
+ const flashLoanFeeAmount =
+ (state.sellAmountBigInt * BigInt(PARASWAP_FLASH_LOAN_FEE_BPS)) / BigInt(10000);
+
+ // Format the fee amount
+ const flashLoanFeeFormatted = valueToBigNumber(flashLoanFeeAmount.toString())
+ .dividedBy(valueToBigNumber(10).pow(state.sellAmountToken?.decimals ?? 18))
+ .toString();
+
+ return {
+ flashLoanFeeAmount,
+ flashLoanFeeFormatted,
+ };
+};
diff --git a/src/components/transactions/Swap/helpers/paraswap/index.ts b/src/components/transactions/Swap/helpers/paraswap/index.ts
new file mode 100644
index 0000000000..3b4e920c50
--- /dev/null
+++ b/src/components/transactions/Swap/helpers/paraswap/index.ts
@@ -0,0 +1,3 @@
+export * from './misc.helpers';
+export * from './order.helpers';
+export * from './rates.helpers';
diff --git a/src/components/transactions/Swap/helpers/paraswap/misc.helpers.ts b/src/components/transactions/Swap/helpers/paraswap/misc.helpers.ts
new file mode 100644
index 0000000000..b7a2cb7793
--- /dev/null
+++ b/src/components/transactions/Swap/helpers/paraswap/misc.helpers.ts
@@ -0,0 +1,23 @@
+import { SwapType } from '../../types/shared.types';
+import { getAssetGroup } from '../shared/assetCorrelation.helpers';
+
+export const getParaswapSlippage = (
+ inputSymbol: string,
+ outputSymbol: string,
+ swapType: SwapType
+): string => {
+ const inputGroup = getAssetGroup(inputSymbol);
+ const outputGroup = getAssetGroup(outputSymbol);
+
+ const baseSlippage = inputGroup === outputGroup ? '0.10' : '0.20';
+
+ if (swapType === SwapType.DebtSwap) {
+ return (Number(baseSlippage) * 2).toString();
+ }
+
+ if (swapType === SwapType.RepayWithCollateral) {
+ return (Number(baseSlippage) * 5).toString();
+ }
+
+ return baseSlippage;
+};
diff --git a/src/components/transactions/Swap/helpers/paraswap/order.helpers.ts b/src/components/transactions/Swap/helpers/paraswap/order.helpers.ts
new file mode 100644
index 0000000000..25e3fa0b4e
--- /dev/null
+++ b/src/components/transactions/Swap/helpers/paraswap/order.helpers.ts
@@ -0,0 +1,80 @@
+import { SignatureLike } from '@ethersproject/bytes';
+import { BoxProps } from '@mui/material';
+import { OptimalRate, TransactionParams } from '@paraswap/sdk';
+import { ComputedReserveData } from 'src/hooks/app-data-provider/useAppDataProvider';
+import { getParaswap } from 'src/hooks/paraswap/common';
+
+import { SwapKind } from '../../types';
+
+export const getTransactionParams = async (
+ kind: SwapKind,
+ chainId: number,
+ srcToken: string,
+ srcDecimals: number,
+ destToken: string,
+ destDecimals: number,
+ user: string,
+ route: OptimalRate,
+ maxSlippage: number
+) => {
+ const { paraswap, feeTarget } = getParaswap(chainId);
+
+ try {
+ const params = await paraswap.buildTx(
+ {
+ srcToken,
+ destToken,
+ ...(kind === 'buy' ? { destAmount: route.destAmount } : { srcAmount: route.srcAmount }),
+ slippage: maxSlippage * 100,
+ priceRoute: route,
+ userAddress: user,
+ partnerAddress: feeTarget,
+ srcDecimals,
+ destDecimals,
+ isDirectFeeTransfer: true,
+ takeSurplus: true,
+ },
+ { ignoreChecks: true }
+ );
+
+ return {
+ swapCallData: (params as TransactionParams).data,
+ augustus: (params as TransactionParams).to,
+ };
+ } catch (e) {
+ console.error(e, {
+ srcToken,
+ destToken,
+ ...(kind === 'buy' ? { destAmount: route.destAmount } : { srcAmount: route.srcAmount }),
+ priceRoute: route,
+ userAddress: user,
+ partnerAddress: feeTarget,
+ takeSurplus: true,
+ slippage: maxSlippage * 100,
+ srcDecimals,
+ });
+ throw new Error('Error building transaction parameters');
+ }
+};
+
+export interface SwapBaseProps extends BoxProps {
+ amountToSwap: string;
+ amountToReceive: string;
+ poolReserve: ComputedReserveData;
+ targetReserve: ComputedReserveData;
+ isWrongNetwork: boolean;
+ customGasPrice?: string;
+ symbol: string;
+ blocked: boolean;
+ isMaxSelected: boolean;
+ useFlashLoan: boolean;
+ loading?: boolean;
+ signature?: SignatureLike;
+ deadline?: string;
+ signedAmount?: string;
+}
+
+export interface SwapActionProps extends SwapBaseProps {
+ swapCallData: string;
+ augustus: string;
+}
diff --git a/src/components/transactions/Swap/helpers/paraswap/rates.helpers.ts b/src/components/transactions/Swap/helpers/paraswap/rates.helpers.ts
new file mode 100644
index 0000000000..251c952130
--- /dev/null
+++ b/src/components/transactions/Swap/helpers/paraswap/rates.helpers.ts
@@ -0,0 +1,98 @@
+import { valueToBigNumber } from '@aave/math-utils';
+import { OptimalRate, SwapSide } from '@paraswap/sdk';
+import { constants } from 'ethers';
+
+import { getParaswap } from '../../../../../hooks/paraswap/common';
+import { ParaswapRatesType, ProviderRatesParams, SwapProvider } from '../../types';
+
+export async function getParaswapSellRates({
+ chainId,
+ amount,
+ srcToken,
+ srcDecimals,
+ destToken,
+ destDecimals,
+ user,
+ side = 'sell',
+ options = {},
+ invertedQuoteRoute = false,
+}: ProviderRatesParams): Promise {
+ const { paraswap } = getParaswap(chainId);
+ return paraswap
+ .getRate({
+ amount,
+ srcToken,
+ srcDecimals,
+ destToken,
+ destDecimals,
+ userAddress: user ? user : constants.AddressZero,
+ side: side === 'buy' ? SwapSide.BUY : SwapSide.SELL,
+ options: {
+ ...options,
+ includeContractMethods: [
+ // side === "buy" ? ContractMethod.swapExactAmountIn : ContractMethod.swapExactAmountOut,
+ ],
+ excludeDEXS: [
+ 'ParaSwapPool',
+ 'ParaSwapLimitOrders',
+ 'SwaapV2',
+ 'Hashflow',
+ 'Dexalot',
+ 'Bebop',
+ ],
+ },
+ })
+ .then((paraSwapResponse: OptimalRate) => {
+ if (invertedQuoteRoute) {
+ return {
+ srcToken: destToken,
+ srcSpotUSD: paraSwapResponse.destUSD,
+ srcSpotAmount: paraSwapResponse.destAmount,
+ srcTokenPriceUsd: Number(
+ valueToBigNumber(paraSwapResponse.destUSD)
+ .dividedBy(paraSwapResponse.destAmount)
+ .toString()
+ ),
+ srcDecimals: destDecimals,
+ destToken: srcToken,
+ destSpotUSD: paraSwapResponse.srcUSD,
+ destSpotAmount: paraSwapResponse.srcAmount,
+ destTokenPriceUsd: Number(
+ valueToBigNumber(paraSwapResponse.srcUSD)
+ .dividedBy(paraSwapResponse.srcAmount)
+ .toString()
+ ),
+ afterFeesUSD: paraSwapResponse.srcUSD,
+ afterFeesAmount: paraSwapResponse.srcAmount,
+ destDecimals: srcDecimals,
+ provider: SwapProvider.PARASWAP,
+ optimalRateData: paraSwapResponse,
+ };
+ } else {
+ return {
+ srcToken,
+ srcSpotUSD: paraSwapResponse.srcUSD,
+ srcSpotAmount: paraSwapResponse.srcAmount,
+ srcTokenPriceUsd: Number(
+ valueToBigNumber(paraSwapResponse.srcUSD)
+ .dividedBy(paraSwapResponse.srcAmount)
+ .toString()
+ ),
+ srcDecimals,
+ destToken,
+ destSpotUSD: paraSwapResponse.destUSD,
+ destSpotAmount: paraSwapResponse.destAmount,
+ destTokenPriceUsd: Number(
+ valueToBigNumber(paraSwapResponse.destUSD)
+ .dividedBy(paraSwapResponse.destAmount)
+ .toString()
+ ),
+ afterFeesUSD: paraSwapResponse.destUSD,
+ afterFeesAmount: paraSwapResponse.destAmount,
+ destDecimals,
+ provider: SwapProvider.PARASWAP,
+ optimalRateData: paraSwapResponse,
+ };
+ }
+ });
+}
diff --git a/src/components/transactions/Switch/assetCorrelation.helpers.ts b/src/components/transactions/Swap/helpers/shared/assetCorrelation.helpers.ts
similarity index 100%
rename from src/components/transactions/Switch/assetCorrelation.helpers.ts
rename to src/components/transactions/Swap/helpers/shared/assetCorrelation.helpers.ts
diff --git a/src/components/transactions/Swap/helpers/shared/index.ts b/src/components/transactions/Swap/helpers/shared/index.ts
new file mode 100644
index 0000000000..6e1c7b38f7
--- /dev/null
+++ b/src/components/transactions/Swap/helpers/shared/index.ts
@@ -0,0 +1,5 @@
+export * from './assetCorrelation.helpers';
+export * from './invalidation.helpers';
+export * from './misc.helpers';
+export * from './provider.helpers';
+export * from './slippage.helpers';
diff --git a/src/components/transactions/Swap/helpers/shared/invalidation.helpers.ts b/src/components/transactions/Swap/helpers/shared/invalidation.helpers.ts
new file mode 100644
index 0000000000..406018bf13
--- /dev/null
+++ b/src/components/transactions/Swap/helpers/shared/invalidation.helpers.ts
@@ -0,0 +1,89 @@
+import { QueryClient } from '@tanstack/react-query';
+import { findByChainId, MarketDataType } from 'src/ui-config/marketsConfig';
+import { queryKeysFactory } from 'src/ui-config/queries';
+
+import { SwapType } from '../../types';
+
+export const invalidateAppStateForSwap = ({
+ swapType,
+ chainId,
+ account,
+ queryClient,
+}: {
+ swapType: SwapType;
+ chainId: number;
+ account: string;
+ queryClient: QueryClient;
+}) => {
+ const marketDataType = findByChainId(chainId);
+
+ if (!marketDataType) {
+ return;
+ }
+
+ switch (swapType) {
+ case SwapType.Swap:
+ invalidateUserBalances({ account, queryClient, marketDataType });
+ invalidateTransactionHistory({ account, queryClient, marketDataType });
+ break;
+ case SwapType.CollateralSwap:
+ invalidateUserPoolBalances({ account, queryClient, marketDataType });
+ invalidateTransactionHistory({ account, queryClient, marketDataType });
+ break;
+ case SwapType.DebtSwap:
+ invalidateUserPoolBalances({ account, queryClient, marketDataType });
+ invalidateTransactionHistory({ account, queryClient, marketDataType });
+ break;
+ case SwapType.RepayWithCollateral:
+ invalidateUserPoolBalances({ account, queryClient, marketDataType });
+ invalidateTransactionHistory({ account, queryClient, marketDataType });
+ break;
+ case SwapType.WithdrawAndSwap:
+ invalidateUserBalances({ account, queryClient, marketDataType });
+ invalidateUserPoolBalances({ account, queryClient, marketDataType });
+ invalidateTransactionHistory({ account, queryClient, marketDataType });
+ break;
+ }
+};
+
+const invalidateUserBalances = ({
+ account,
+ queryClient,
+ marketDataType,
+}: {
+ account: string;
+ queryClient: QueryClient;
+ marketDataType: MarketDataType;
+}) => {
+ queryClient.invalidateQueries({
+ queryKey: queryKeysFactory.poolTokens(account, marketDataType),
+ });
+};
+
+const invalidateUserPoolBalances = ({
+ account,
+ queryClient,
+ marketDataType,
+}: {
+ account: string;
+ queryClient: QueryClient;
+ marketDataType: MarketDataType;
+}) => {
+ queryClient.invalidateQueries({
+ queryKey: queryKeysFactory.userPoolReservesDataHumanized(account, marketDataType),
+ });
+};
+
+const invalidateTransactionHistory = ({
+ account,
+ queryClient,
+ marketDataType,
+}: {
+ account: string;
+ queryClient: QueryClient;
+ marketDataType: MarketDataType;
+}) => {
+ queryClient.invalidateQueries({
+ queryKey: queryKeysFactory.transactionHistory(account, marketDataType),
+ });
+};
diff --git a/src/components/transactions/Switch/common.ts b/src/components/transactions/Swap/helpers/shared/misc.helpers.ts
similarity index 89%
rename from src/components/transactions/Switch/common.ts
rename to src/components/transactions/Swap/helpers/shared/misc.helpers.ts
index 2f173d7ab2..45631d7535 100644
--- a/src/components/transactions/Switch/common.ts
+++ b/src/components/transactions/Swap/helpers/shared/misc.helpers.ts
@@ -15,9 +15,11 @@ export const supportedNetworksConfig: SupportedNetworkWithChainId[] = getSupport
chainId,
})
);
+
+// TODO: join and make sure at least one provider supports it
export const supportedNetworksWithEnabledMarket = supportedNetworksConfig.filter((elem) =>
Object.values(marketsData).find(
- (market) => market.chainId === elem.chainId && market.enabledFeatures?.switch
+ (market) => market.chainId === elem.chainId && market.enabledFeatures?.switch // TODO: change to swap
)
);
diff --git a/src/components/transactions/Swap/helpers/shared/provider.helpers.ts b/src/components/transactions/Swap/helpers/shared/provider.helpers.ts
new file mode 100644
index 0000000000..181c5ab2d9
--- /dev/null
+++ b/src/components/transactions/Swap/helpers/shared/provider.helpers.ts
@@ -0,0 +1,76 @@
+import {
+ COW_UNSUPPORTED_ASSETS,
+ isChainIdSupportedByCoWProtocol,
+} from '../../constants/cow.constants';
+import { SwapProvider, SwapType } from '../../types';
+
+/**
+ * Returns whether CoW Protocol can handle the given pair/swapType on the chain.
+ * Checks chain support and a per-flow unsupported assets list.
+ */
+export const isSwapSupportedByCowProtocol = (
+ chainId: number,
+ assetFrom: string,
+ assetTo: string,
+ swapType: SwapType,
+ useFlashloan: boolean
+) => {
+ if (!isChainIdSupportedByCoWProtocol(chainId)) return false;
+
+ let swapTypeToUse = swapType;
+ if (useFlashloan == false && swapType === SwapType.CollateralSwap) {
+ swapTypeToUse = SwapType.Swap;
+ }
+
+ // Helper to normalize values that can be string[] or 'ALL' to always be an array
+ const normalizeToArray = (value: string[] | 'ALL' | undefined): string[] => {
+ if (!value) return [];
+ if (value === 'ALL') return ['ALL'];
+ return value;
+ };
+
+ const unsupportedAssetsPerChainAndModalType = [
+ ...normalizeToArray(COW_UNSUPPORTED_ASSETS['ALL']?.[chainId]),
+ ...normalizeToArray(COW_UNSUPPORTED_ASSETS[swapTypeToUse]?.[chainId]),
+ ];
+
+ if (unsupportedAssetsPerChainAndModalType.length === 0) return true; // No unsupported assets for this chain and modal type
+
+ if (unsupportedAssetsPerChainAndModalType.includes('ALL')) return false; // All assets are unsupported
+
+ if (
+ unsupportedAssetsPerChainAndModalType.includes(assetFrom.toLowerCase()) ||
+ unsupportedAssetsPerChainAndModalType.includes(assetTo.toLowerCase())
+ )
+ return false;
+
+ return true;
+};
+
+/**
+ * Picks the provider for the current swap based on chain, assets and flow.
+ *
+ * Notes:
+ * - CoW is preferred when supported; fallback to ParaSwap
+ */
+export const getSwitchProvider = ({
+ chainId,
+ assetFrom,
+ assetTo,
+ shouldUseFlashloan,
+ swapType,
+}: {
+ chainId: number;
+ assetFrom: string;
+ assetTo: string;
+ shouldUseFlashloan?: boolean;
+ swapType: SwapType;
+}): SwapProvider | undefined => {
+ if (
+ isSwapSupportedByCowProtocol(chainId, assetFrom, assetTo, swapType, shouldUseFlashloan ?? false)
+ ) {
+ return SwapProvider.COW_PROTOCOL;
+ }
+
+ return SwapProvider.PARASWAP;
+};
diff --git a/src/components/transactions/Switch/validation.helpers.ts b/src/components/transactions/Swap/helpers/shared/slippage.helpers.ts
similarity index 96%
rename from src/components/transactions/Switch/validation.helpers.ts
rename to src/components/transactions/Swap/helpers/shared/slippage.helpers.ts
index fa3e5d3d21..d0c9d3f6df 100644
--- a/src/components/transactions/Switch/validation.helpers.ts
+++ b/src/components/transactions/Swap/helpers/shared/slippage.helpers.ts
@@ -1,4 +1,4 @@
-import { SwitchProvider } from './switch.types';
+import { SwapProvider } from '../../types';
export enum ValidationSeverity {
ERROR = 'error',
@@ -14,7 +14,7 @@ export const validateSlippage = (
slippage: string,
chainId: number,
isNativeToken = false,
- provider?: SwitchProvider
+ provider?: SwapProvider
): ValidationData | undefined => {
try {
const numberSlippage = Number(slippage);
diff --git a/src/components/transactions/Swap/hooks/useFlowSelector.ts b/src/components/transactions/Swap/hooks/useFlowSelector.ts
new file mode 100644
index 0000000000..0330d32c5f
--- /dev/null
+++ b/src/components/transactions/Swap/hooks/useFlowSelector.ts
@@ -0,0 +1,228 @@
+import { ComputedUserReserve, valueToBigNumber } from '@aave/math-utils';
+import { Dispatch, useEffect } from 'react';
+import {
+ ComputedReserveData,
+ ExtendedFormattedUser,
+ useAppDataContext,
+} from 'src/hooks/app-data-provider/useAppDataProvider';
+import { calculateHFAfterSwap, CalculateHFAfterSwapProps } from 'src/utils/hfUtils';
+
+import {
+ LIQUIDATION_DANGER_THRESHOLD,
+ LIQUIDATION_SAFETY_THRESHOLD,
+} from '../constants/shared.constants';
+import {
+ ActionsBlockedReason,
+ isProtocolSwapState,
+ SwapParams,
+ SwapProvider,
+ SwapState,
+ SwapType,
+} from '../types';
+
+/**
+ * React hook that decides the execution flow (simple vs flashloan) and
+ * computes health-factor effects for protocol-aware swaps.
+ *
+ * Writes derived flags into SwapState: isHFLow, isLiquidatable, useFlashloan,
+ * and marks the flow as selected once enough context is present.
+ */
+export const useFlowSelector = ({
+ params,
+ state,
+ setState,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+}) => {
+ const { user: extendedUser, reserves } = useAppDataContext();
+ const requiresInvertedQuote = state.isInvertedSwap;
+
+ useEffect(() => {
+ if (params.swapType === SwapType.Swap) {
+ // For non positions swaps, set isSwapFlowSelected to true
+ setState({ isSwapFlowSelected: true });
+ } else {
+ return healthFactorSensibleSwapFlowSelector({
+ params,
+ state,
+ setState,
+ extendedUser,
+ reserves,
+ requiresInvertedQuote,
+ });
+ }
+ }, [
+ params.swapType,
+ state.sourceToken,
+ state.destinationToken,
+ state.inputAmount,
+ state.outputAmount,
+ state.sellAmountFormatted,
+ state.buyAmountFormatted,
+ extendedUser,
+ reserves,
+ state.swapRate,
+ ]);
+};
+
+/**
+ * Pure helper that computes HF and determines whether to force flashloan.
+ */
+export const healthFactorSensibleSwapFlowSelector = ({
+ state,
+ setState,
+ extendedUser,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+ extendedUser: ExtendedFormattedUser | undefined;
+ reserves: ComputedReserveData[];
+ requiresInvertedQuote: boolean;
+}) => {
+ const fromAssetUserReserve = extendedUser?.userReservesData.find(
+ (ur) => ur.underlyingAsset.toLowerCase() === state.sourceToken?.underlyingAddress.toLowerCase()
+ );
+ const toAssetUserReserve = extendedUser?.userReservesData.find(
+ (ur) =>
+ ur.underlyingAsset.toLowerCase() === state.destinationToken?.underlyingAddress.toLowerCase()
+ );
+
+ if (
+ !fromAssetUserReserve ||
+ !toAssetUserReserve ||
+ !extendedUser ||
+ !state.swapRate ||
+ !state.sellAmountFormatted ||
+ !state.buyAmountFormatted
+ )
+ return;
+
+ if (!isProtocolSwapState(state)) {
+ return;
+ }
+
+ // Compute HF effect of withdrawing inputAmount (copied from SwitchModalTxDetails)
+ const calculateHfEffectOfFromAmount = () => {
+ try {
+ if (!state.swapRate) return { hfEffectOfFromAmount: '0', hfAfterSwap: undefined };
+
+ const params = getHFAfterSwapParamsFromSwapType(
+ state,
+ fromAssetUserReserve,
+ toAssetUserReserve,
+ extendedUser
+ );
+
+ if (!params) return { hfEffectOfFromAmount: '0', hfAfterSwap: undefined };
+
+ const { hfEffectOfFromAmount, hfAfterSwap } = calculateHFAfterSwap(params);
+
+ return {
+ hfEffectOfFromAmount: hfEffectOfFromAmount.toString(),
+ hfAfterSwap: hfAfterSwap.toString(),
+ };
+ } catch {
+ return { hfEffectOfFromAmount: '0', hfAfterSwap: undefined };
+ }
+ };
+
+ const { hfEffectOfFromAmount, hfAfterSwap } = calculateHfEffectOfFromAmount();
+
+ const isHFLow = () => {
+ if (!hfAfterSwap) return false;
+
+ const hfNumber = valueToBigNumber(hfAfterSwap);
+
+ if (hfNumber.lt(0)) return false;
+
+ return hfNumber.lt(LIQUIDATION_SAFETY_THRESHOLD) && hfNumber.gte(LIQUIDATION_DANGER_THRESHOLD);
+ };
+
+ const isLiquidatable =
+ hfAfterSwap && hfAfterSwap !== '-1'
+ ? valueToBigNumber(hfAfterSwap).lt(LIQUIDATION_DANGER_THRESHOLD)
+ : false;
+
+ const forceFlashloanFlow =
+ state.provider === SwapProvider.COW_PROTOCOL &&
+ (state.swapType === SwapType.RepayWithCollateral || state.swapType === SwapType.DebtSwap);
+ const useFlashloan =
+ forceFlashloanFlow ||
+ (extendedUser?.healthFactor !== '-1' &&
+ valueToBigNumber(extendedUser?.healthFactor || 0)
+ .minus(valueToBigNumber(hfEffectOfFromAmount || 0))
+ .lt(LIQUIDATION_SAFETY_THRESHOLD));
+
+ if (!state.ratesLoading && !!state.provider) {
+ setState({
+ isHFLow: isHFLow(),
+ isLiquidatable,
+ hfAfterSwap: Number(hfAfterSwap || '0'),
+ useFlashloan,
+ isSwapFlowSelected: true,
+ actionsBlocked: {
+ [ActionsBlockedReason.IS_LIQUIDATABLE]: isLiquidatable ? true : undefined,
+ },
+ });
+ }
+};
+
+const getHFAfterSwapParamsFromSwapType = (
+ state: SwapState,
+ fromAssetUserReserve: ComputedUserReserve,
+ toAssetUserReserve: ComputedUserReserve,
+ user: ExtendedFormattedUser
+): CalculateHFAfterSwapProps | undefined => {
+ if (!state.sellAmountFormatted || !state.buyAmountFormatted) return undefined;
+ switch (state.swapType) {
+ case SwapType.CollateralSwap:
+ return {
+ fromAmount: state.sellAmountFormatted.toString(),
+ toAmountAfterSlippage: state.buyAmountFormatted.toString(),
+ fromAssetData: state.sourceReserve.reserve,
+ toAssetData: state.destinationReserve.reserve,
+ fromAssetUserData: fromAssetUserReserve,
+ fromAssetType: 'collateral',
+ toAssetType: 'collateral',
+ user,
+ };
+ case SwapType.DebtSwap:
+ return {
+ fromAmount: state.sellAmountFormatted.toString(),
+ toAmountAfterSlippage: state.buyAmountFormatted.toString(),
+ fromAssetData: state.destinationReserve.reserve,
+ toAssetData: state.sourceReserve.reserve,
+ fromAssetUserData: toAssetUserReserve,
+ user,
+ fromAssetType: 'debt',
+ toAssetType: 'debt',
+ };
+ case SwapType.RepayWithCollateral:
+ return {
+ fromAmount: state.sellAmountFormatted.toString(),
+ toAmountAfterSlippage: state.buyAmountFormatted.toString(),
+ fromAssetData: state.destinationReserve.reserve,
+ toAssetData: state.sourceReserve.reserve,
+ fromAssetUserData: toAssetUserReserve,
+ user,
+ fromAssetType: 'collateral',
+ toAssetType: 'debt',
+ };
+ case SwapType.WithdrawAndSwap:
+ return {
+ fromAmount: state.sellAmountFormatted.toString(),
+ toAmountAfterSlippage: state.buyAmountFormatted.toString(),
+ fromAssetData: state.sourceReserve.reserve,
+ toAssetData: state.destinationReserve.reserve,
+ fromAssetUserData: fromAssetUserReserve,
+ fromAssetType: 'collateral',
+ toAssetType: 'none',
+ user,
+ };
+ default:
+ return undefined;
+ }
+};
diff --git a/src/components/transactions/Swap/hooks/useMaxNativeAmount.ts b/src/components/transactions/Swap/hooks/useMaxNativeAmount.ts
new file mode 100644
index 0000000000..2a78639c7e
--- /dev/null
+++ b/src/components/transactions/Swap/hooks/useMaxNativeAmount.ts
@@ -0,0 +1,45 @@
+import { normalize } from '@aave/math-utils';
+import { parseUnits } from 'ethers/lib/utils';
+import { Dispatch, useEffect } from 'react';
+
+import { SwapParams, SwapState, SwapType, TokenType } from '../types';
+
+/**
+ * Computes the max selectable amount for native-asset sells, leaving gas headroom.
+ * Applies only to simple token swaps for EOAs; SCWs/Safe and protocol flows ignore it.
+ */
+export const useMaxNativeAmount = ({
+ params,
+ state,
+ setState,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+}) => {
+ // Eth-Flow requires to leave some assets for gas
+ const nativeDecimals = 18;
+ const gasRequiredForEthFlow =
+ state.chainId === 1 ? parseUnits('0.01', nativeDecimals) : parseUnits('0.0001', nativeDecimals); // TODO: Ask for better value coming from the SDK
+
+ const requiredAssetsLeftForGas =
+ state.sourceToken.tokenType === TokenType.NATIVE &&
+ !state.userIsSmartContractWallet &&
+ params.swapType === SwapType.Swap
+ ? gasRequiredForEthFlow
+ : undefined;
+
+ const maxAmount = (() => {
+ const balance = parseUnits(state.sourceToken.balance, nativeDecimals);
+ if (!requiredAssetsLeftForGas) return balance;
+ return balance.gt(requiredAssetsLeftForGas) ? balance.sub(requiredAssetsLeftForGas) : balance;
+ })();
+
+ const maxAmountFormatted = maxAmount
+ ? normalize(maxAmount.toString(), nativeDecimals).toString()
+ : undefined;
+
+ useEffect(() => {
+ setState({ forcedMaxValue: maxAmountFormatted });
+ }, [maxAmountFormatted]);
+};
diff --git a/src/components/transactions/Swap/hooks/useProtocolReserves.ts b/src/components/transactions/Swap/hooks/useProtocolReserves.ts
new file mode 100644
index 0000000000..d01337f21f
--- /dev/null
+++ b/src/components/transactions/Swap/hooks/useProtocolReserves.ts
@@ -0,0 +1,45 @@
+import { Dispatch, useEffect } from 'react';
+import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider';
+
+import { isProtocolSwapParams, SwapParams, SwapState } from '../types';
+
+/**
+ * Resolves `sourceReserve` and `destinationReserve` from the connected user's data
+ * for protocol-aware flows. Keeps state in sync with token selection changes.
+ */
+export const useProtocolReserves = ({
+ state,
+ params,
+ setState,
+}: {
+ state: SwapState;
+ params: SwapParams;
+ setState: Dispatch>;
+}) => {
+ const { user } = useAppDataContext();
+
+ const userReserves = user?.userReservesData;
+
+ useEffect(() => {
+ if (state.sourceToken && isProtocolSwapParams(params)) {
+ const reserve = userReserves?.find(
+ (r) => r.underlyingAsset.toLowerCase() === state.sourceToken.underlyingAddress.toLowerCase()
+ );
+ if (reserve) {
+ setState({ sourceReserve: reserve });
+ }
+ }
+ }, [state.sourceToken, userReserves]);
+
+ useEffect(() => {
+ if (state.destinationToken && isProtocolSwapParams(params)) {
+ const reserve = userReserves?.find(
+ (r) =>
+ r.underlyingAsset.toLowerCase() === state.destinationToken.underlyingAddress.toLowerCase()
+ );
+ if (reserve) {
+ setState({ destinationReserve: reserve });
+ }
+ }
+ }, [state.destinationToken, userReserves]);
+};
diff --git a/src/components/transactions/Swap/hooks/useSlippageSelector.ts b/src/components/transactions/Swap/hooks/useSlippageSelector.ts
new file mode 100644
index 0000000000..b4265bed24
--- /dev/null
+++ b/src/components/transactions/Swap/hooks/useSlippageSelector.ts
@@ -0,0 +1,78 @@
+import { Dispatch, useEffect, useRef } from 'react';
+
+import { validateSlippage, ValidationSeverity } from '../helpers/shared/slippage.helpers';
+import { isCowProtocolRates, OrderType, SwapParams, SwapState, TokenType } from '../types';
+
+/**
+/**
+ * Hook responsibilities:
+ * - Synchronizes the slippage value in state with the selected order type (MARKET or LIMIT), restoring previous market slippage as needed.
+ * - Tracks the last non-zero market slippage to allow restoration when toggling between order types.
+ * - Triggers slippage warnings for the user if their input value is below the provider's suggested minimum for CoW Protocol swaps.
+ * - Validates the slippage value and updates related state/validation UI.
+ * - Keeps UI/validation in sync with both user input and provider hints or requirements.
+ */
+export const useSlippageSelector = ({
+ state,
+ setState,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+}) => {
+ // Track last non-zero market slippage to restore when switching back from LIMIT
+ const lastMarketSlippageRef = useRef(null);
+
+ // Keep slippage aligned with order type globally
+ useEffect(() => {
+ if (state.orderType === OrderType.LIMIT) {
+ // Remember current market slippage if non-zero before forcing to 0 for limit
+ if (state.slippage && Number(state.slippage) !== 0) {
+ lastMarketSlippageRef.current = state.slippage;
+ }
+ if (state.slippage !== '0') {
+ setState({ slippage: '0' });
+ }
+ } else if (state.orderType === OrderType.MARKET) {
+ // Restore to suggested slippage if available, otherwise last known market slippage, else default 0.10%
+ const target = lastMarketSlippageRef.current || state.autoSlippage || '0.10';
+ if (state.slippage !== target) {
+ setState({ slippage: target });
+ }
+ }
+ }, [state.orderType]);
+
+ useEffect(() => {
+ // Debounce to avoid race condition
+ const timeout = setTimeout(() => {
+ setState({
+ showSlippageWarning:
+ isCowProtocolRates(state.swapRate) &&
+ Number(state.slippage) < state.swapRate?.suggestedSlippage,
+ });
+ }, 500);
+
+ return () => clearTimeout(timeout);
+ }, [state.slippage]);
+
+ useEffect(() => {
+ if (!state.swapRate) return;
+
+ const slippageValidation = validateSlippage(
+ state.slippage,
+ state.chainId,
+ state.sourceToken.tokenType === TokenType.NATIVE,
+ state.provider
+ );
+
+ const safeSlippage =
+ slippageValidation && slippageValidation.severity === ValidationSeverity.ERROR
+ ? 0
+ : Number(state.slippage) / 100;
+
+ setState({
+ slippageValidation,
+ safeSlippage,
+ });
+ }, [state.slippage, state.swapRate]);
+};
diff --git a/src/components/transactions/Swap/hooks/useSwapGasEstimation.ts b/src/components/transactions/Swap/hooks/useSwapGasEstimation.ts
new file mode 100644
index 0000000000..627eb940b8
--- /dev/null
+++ b/src/components/transactions/Swap/hooks/useSwapGasEstimation.ts
@@ -0,0 +1,86 @@
+import { Dispatch, useEffect, useMemo, useRef } from 'react';
+import { TxStateType } from 'src/hooks/useModal';
+import { useRootStore } from 'src/store/root';
+import { ApprovalMethod } from 'src/store/walletSlice';
+import { useShallow } from 'zustand/react/shallow';
+
+import { estimateSwapGas, GasEstimationParams } from '../helpers/gasEstimation.helpers';
+import { SwapState, TokenType } from '../types';
+
+/**
+ * Centralized gas estimation for swap actions.
+ *
+ * Normalizes inputs required by provider/flow specific estimators and writes
+ * only when values change to avoid render loops.
+ */
+export const useSwapGasEstimation = ({
+ state,
+ setState,
+ requiresApproval,
+ requiresApprovalReset,
+ approvalTxState,
+}: {
+ state: SwapState;
+ setState: Dispatch>;
+ requiresApproval: boolean;
+ requiresApprovalReset: boolean;
+ approvalTxState: TxStateType;
+}) => {
+ const walletApprovalMethodPreference = useRootStore(
+ useShallow((store) => store.walletApprovalMethodPreference)
+ );
+ const usePermit = walletApprovalMethodPreference === ApprovalMethod.PERMIT;
+
+ // Memoize gas estimation parameters to prevent unnecessary recalculations
+ const gasEstimationParams: GasEstimationParams = useMemo(
+ () => ({
+ swapType: state.swapType,
+ provider: state.provider,
+ sourceToken: {
+ addressToSwap: state.sourceToken.addressToSwap,
+ tokenType: state.sourceToken.tokenType || TokenType.ERC20,
+ },
+ userIsSmartContractWallet: state.userIsSmartContractWallet,
+ requiresApproval,
+ requiresApprovalReset,
+ approvalTxState,
+ useFlashloan: state.useFlashloan ?? false,
+ usePermit,
+ }),
+ [
+ state.swapType,
+ state.provider,
+ state.sourceToken.addressToSwap,
+ state.sourceToken.tokenType,
+ state.userIsSmartContractWallet,
+ requiresApproval,
+ requiresApprovalReset,
+ approvalTxState.success,
+ state.useFlashloan,
+ usePermit,
+ ]
+ );
+
+ // Memoize gas estimation result
+ const gasEstimation = useMemo(() => estimateSwapGas(gasEstimationParams), [gasEstimationParams]);
+
+ // Use ref to track previous values and prevent unnecessary updates
+ const previousGasEstimation = useRef<{ gasLimit: string; showGasStation: boolean } | null>(null);
+
+ useEffect(() => {
+ const currentGasEstimation = {
+ gasLimit: gasEstimation.gasLimit,
+ showGasStation: gasEstimation.showGasStation,
+ };
+
+ // Only update if the values have actually changed
+ if (
+ !previousGasEstimation.current ||
+ previousGasEstimation.current.gasLimit !== currentGasEstimation.gasLimit ||
+ previousGasEstimation.current.showGasStation !== currentGasEstimation.showGasStation
+ ) {
+ setState(currentGasEstimation);
+ previousGasEstimation.current = currentGasEstimation;
+ }
+ }, [gasEstimation.gasLimit, gasEstimation.showGasStation, setState]);
+};
diff --git a/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts b/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts
new file mode 100644
index 0000000000..4696e77cc2
--- /dev/null
+++ b/src/components/transactions/Swap/hooks/useSwapOrderAmounts.ts
@@ -0,0 +1,327 @@
+import { normalize, normalizeBN, valueToBigNumber } from '@aave/math-utils';
+import { OrderKind } from '@cowprotocol/cow-sdk';
+import { Dispatch, useEffect } from 'react';
+
+import { COW_PARTNER_FEE } from '../constants/cow.constants';
+import {
+ isCowProtocolRates,
+ OrderType,
+ SwapParams,
+ SwapProvider,
+ SwapState,
+ SwapType,
+} from '../types';
+import { swapTypesThatRequiresInvertedQuote } from './useSwapQuote';
+
+const marketOrderKindPerSwapType: Record = {
+ [SwapType.Swap]: OrderKind.SELL,
+ [SwapType.CollateralSwap]: OrderKind.SELL,
+ [SwapType.DebtSwap]: OrderKind.BUY,
+ [SwapType.RepayWithCollateral]: OrderKind.BUY,
+ [SwapType.WithdrawAndSwap]: OrderKind.SELL,
+};
+
+/**
+ * Computes normalized sell/buy amounts used to build transactions.
+ *
+ * Responsibilities:
+ * - Applies partner fee and user slippage depending on order side and type
+ * - Handles flows that require inverted quoting (DebtSwap, RepayWithCollateral)
+ * by swapping token roles: UI(source,destination) -> swap order request(sell,buy)
+ * - Derives bigint amounts and USD values for details and execution
+ * - Chooses the correct OrderKind for market orders per swap type
+ */
+export const useSwapOrderAmounts = ({
+ state,
+ setState,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+}) => {
+ useEffect(() => {
+ if (
+ !state.swapRate?.afterFeesAmount ||
+ state.outputAmount == '' ||
+ state.outputAmount == 'NaN' ||
+ state.inputAmount == '' ||
+ state.inputAmount == 'NaN' ||
+ (state.orderType === OrderType.MARKET && state.slippage == undefined)
+ )
+ return;
+
+ // On some swaps, the order is inverted, the required swap behind the operation is from our second input to our first.
+ // e.g. repay with collateral, we have input Repay and output Available collateral, the required swap is from Available collateral to Repay.
+ // So we need to invert the order of the tokens and the amounts.
+ const isInvertedSwap = swapTypesThatRequiresInvertedQuote.includes(state.swapType);
+ const processedSide = isInvertedSwap ? (state.side === 'sell' ? 'buy' : 'sell') : state.side;
+ // The default order kind for market order is not always SELL, it depends on the swap type
+ // e.g. for collateral swap, the default order kind is SELL, for debt swap, the default order kind is BUY
+ const marketOrderKind = marketOrderKindPerSwapType[state.swapType];
+
+ let buyAmountFormatted,
+ sellAmountFormatted,
+ buyAmountToken,
+ sellAmountToken,
+ buyTokenPriceUsd,
+ sellTokenPriceUsd;
+ // Track costs to expose them in state (unified across details views)
+ let networkFeeAmountInSellFormatted = '0';
+ let networkFeeAmountInBuyFormatted = '0';
+ const partnetFeeBps =
+ state.provider === SwapProvider.COW_PROTOCOL
+ ? COW_PARTNER_FEE(state.sourceToken.symbol, state.destinationToken.symbol).volumeBps
+ : 0;
+ const partnerFeeAmount =
+ state.side === 'sell'
+ ? valueToBigNumber(state.outputAmount).multipliedBy(partnetFeeBps).dividedBy(10000)
+ : valueToBigNumber(state.inputAmount).multipliedBy(partnetFeeBps).dividedBy(10000);
+ // const partnerFeeToken = state.side === 'sell' ? state.destinationToken : state.sourceToken;
+
+ if (!isInvertedSwap) {
+ // on classic swaps, minimum is calculated from the output token and sent amount is from the input token
+ sellAmountToken = state.sourceToken;
+ buyAmountToken = state.destinationToken;
+ sellTokenPriceUsd = valueToBigNumber(state.inputAmountUSD)
+ .dividedBy(valueToBigNumber(state.inputAmount))
+ .toNumber();
+ buyTokenPriceUsd = valueToBigNumber(state.outputAmountUSD)
+ .dividedBy(valueToBigNumber(state.outputAmount))
+ .toNumber();
+
+ let networkFeeAmountFormattedInSell = isCowProtocolRates(state.swapRate)
+ ? normalize(
+ state.swapRate.amountAndCosts.costs.networkFee.amountInSellCurrency.toString(),
+ sellAmountToken.decimals
+ )
+ : '0';
+ let networkFeeAmountFormattedInBuy = isCowProtocolRates(state.swapRate)
+ ? normalize(
+ state.swapRate.amountAndCosts.costs.networkFee.amountInBuyCurrency.toString(),
+ buyAmountToken.decimals
+ )
+ : '0';
+
+ // Trick waiting for CoW solvers precise hook simulation - TODO: remove once it's solved on CoW's BFF
+ if (
+ state.swapType === SwapType.RepayWithCollateral ||
+ state.swapType === SwapType.DebtSwap ||
+ state.swapType === SwapType.CollateralSwap
+ ) {
+ networkFeeAmountFormattedInSell = valueToBigNumber(networkFeeAmountFormattedInSell)
+ .multipliedBy(3)
+ .toFixed();
+ networkFeeAmountFormattedInBuy = valueToBigNumber(networkFeeAmountFormattedInBuy)
+ .multipliedBy(3)
+ .toFixed();
+ }
+ networkFeeAmountInSellFormatted = networkFeeAmountFormattedInSell;
+ networkFeeAmountInBuyFormatted = networkFeeAmountFormattedInBuy;
+
+ if (state.orderType === OrderType.MARKET) {
+ // On a classic sell market order, we send the input amount and receive the amount after partner fees and slippage
+
+ if (marketOrderKind === OrderKind.SELL) {
+ sellAmountFormatted = state.inputAmount;
+
+ const outputAmountAfterNetworkFees = valueToBigNumber(state.outputAmount).minus(
+ networkFeeAmountFormattedInBuy
+ );
+ const outputAmountAfterPartnerFees = valueToBigNumber(outputAmountAfterNetworkFees).minus(
+ partnerFeeAmount
+ );
+ const outputAmountAfterSlippage = valueToBigNumber(
+ outputAmountAfterPartnerFees
+ ).multipliedBy(1 - Number(state.slippage) / 100);
+ buyAmountFormatted = outputAmountAfterSlippage.toFixed();
+ } else {
+ // TODO: check if this is correct
+ buyAmountFormatted = state.inputAmount;
+
+ const sellAmountAfterNetworkFees = valueToBigNumber(state.outputAmount).plus(
+ networkFeeAmountFormattedInSell
+ );
+ const sellAmountAfterPartnerFees = valueToBigNumber(sellAmountAfterNetworkFees).plus(
+ partnerFeeAmount
+ );
+ const sellAmountAfterSlippage = valueToBigNumber(sellAmountAfterPartnerFees).multipliedBy(
+ 1 + Number(state.slippage) / 100
+ );
+ sellAmountFormatted = sellAmountAfterSlippage.toFixed();
+ }
+ } else if (state.orderType === OrderType.LIMIT) {
+ if (state.side === 'sell') {
+ // on a sell limit order, we send the input amount and receive the amount after partner fees (no slippage applied)
+ sellAmountFormatted = state.inputAmount;
+
+ // Do not apply network costs on limit orders
+ buyAmountFormatted = valueToBigNumber(state.outputAmount)
+ .minus(partnerFeeAmount)
+ .toFixed();
+ } else {
+ // on a buy limit order, we receive exactly the output amount and send the input amount after partner fees (no slippage applied)
+ // Do not apply network costs on limit orders
+ sellAmountFormatted = valueToBigNumber(state.inputAmount)
+ .plus(partnerFeeAmount)
+ .toFixed();
+
+ buyAmountFormatted = state.outputAmount;
+ }
+ }
+ } else {
+ // if the swap is inverted (from the UI perspective, e.g. in a repay with collateral our second input is the sell token),
+ // the minimum received is from the input token and sent is from the output token
+ sellAmountToken = state.destinationToken;
+ buyAmountToken = state.sourceToken;
+ sellTokenPriceUsd = valueToBigNumber(state.outputAmountUSD)
+ .dividedBy(valueToBigNumber(state.outputAmount))
+ .toNumber();
+ buyTokenPriceUsd = valueToBigNumber(state.inputAmountUSD)
+ .dividedBy(valueToBigNumber(state.inputAmount))
+ .toNumber();
+
+ let networkFeeAmountFormattedInSell = isCowProtocolRates(state.swapRate)
+ ? normalize(
+ state.swapRate.amountAndCosts.costs.networkFee.amountInSellCurrency.toString(),
+ sellAmountToken.decimals
+ )
+ : '0';
+ let networkFeeAmountFormattedInBuy = isCowProtocolRates(state.swapRate)
+ ? normalize(
+ state.swapRate.amountAndCosts.costs.networkFee.amountInBuyCurrency.toString(),
+ buyAmountToken.decimals
+ )
+ : '0';
+
+ // console.debug('networkFeeAmountFormattedInSell', networkFeeAmountFormattedInSell);
+ // console.debug('networkFeeAmountFormattedInBuy', networkFeeAmountFormattedInBuy);
+
+ // Trick waiting for CoW solvers precise hook simulation - TODO: remove once it's solved on CoW's BFF
+ if (
+ state.swapType === SwapType.RepayWithCollateral ||
+ state.swapType === SwapType.DebtSwap ||
+ state.swapType === SwapType.CollateralSwap
+ ) {
+ networkFeeAmountFormattedInSell = valueToBigNumber(networkFeeAmountFormattedInSell)
+ .multipliedBy(3)
+ .toFixed();
+ networkFeeAmountFormattedInBuy = valueToBigNumber(networkFeeAmountFormattedInBuy)
+ .multipliedBy(3)
+ .toFixed();
+ }
+
+ // console.debug('networkFeeAmountFormattedInSell after trick', networkFeeAmountFormattedInSell);
+ // console.debug('networkFeeAmountFormattedInBuy after trick', networkFeeAmountFormattedInBuy);
+ networkFeeAmountInSellFormatted = networkFeeAmountFormattedInSell;
+ networkFeeAmountInBuyFormatted = networkFeeAmountFormattedInBuy;
+
+ if (state.orderType === OrderType.MARKET) {
+ // on a classic inverted sell market order, we send the output amount and receive the input amount after partner fees and slippage
+ if (marketOrderKind === OrderKind.SELL) {
+ sellAmountFormatted = state.outputAmount;
+
+ const inputAmountAfterNetworkFees = valueToBigNumber(state.inputAmount).minus(
+ networkFeeAmountFormattedInBuy
+ );
+ const inputAmountAfterPartnerFees = valueToBigNumber(inputAmountAfterNetworkFees)
+ .minus(partnerFeeAmount)
+ .toFixed();
+ const inputAmountAfterSlippage = valueToBigNumber(inputAmountAfterPartnerFees)
+ .multipliedBy(1 + Number(state.slippage) / 100)
+ .toFixed();
+ buyAmountFormatted = inputAmountAfterSlippage;
+ } else {
+ buyAmountFormatted = state.inputAmount;
+
+ const sellAmountAfterNetworkFees = valueToBigNumber(state.outputAmount).plus(
+ networkFeeAmountFormattedInSell
+ );
+ const sellAmountAfterPartnerFees = valueToBigNumber(sellAmountAfterNetworkFees).plus(
+ partnerFeeAmount
+ );
+ const sellAmountAfterSlippage = valueToBigNumber(sellAmountAfterPartnerFees).multipliedBy(
+ 1 + Number(state.slippage) / 100
+ );
+ sellAmountFormatted = sellAmountAfterSlippage.toFixed();
+ }
+ } else {
+ if (processedSide === 'buy') {
+ // on an inverted buy limit order, we buy the input amount and sell the output amount after partner fees (no slippage applied)
+ buyAmountFormatted = state.inputAmount;
+
+ // Do not apply network costs on limit orders
+ sellAmountFormatted = valueToBigNumber(state.outputAmount)
+ .plus(partnerFeeAmount)
+ .toFixed();
+ } else {
+ // on an inverted sell limit order, we sell the output amount and buy the input amount after partner fees (no slippage applied)
+ sellAmountFormatted = state.outputAmount;
+
+ // Do not apply network costs on limit orders
+ buyAmountFormatted = valueToBigNumber(state.inputAmount)
+ .minus(partnerFeeAmount)
+ .toFixed();
+ }
+ }
+ }
+
+ if (
+ buyAmountFormatted == undefined ||
+ sellAmountFormatted == undefined ||
+ sellAmountToken == undefined ||
+ buyAmountToken == undefined ||
+ sellTokenPriceUsd == undefined ||
+ buyTokenPriceUsd == undefined
+ )
+ return;
+
+ // Avoid negative amounts
+ sellAmountFormatted = valueToBigNumber(sellAmountFormatted ?? '0').lt(0)
+ ? '0'
+ : sellAmountFormatted;
+ buyAmountFormatted = valueToBigNumber(buyAmountFormatted ?? '0').lt(0)
+ ? '0'
+ : buyAmountFormatted;
+
+ const sellAmountUSD = valueToBigNumber(sellAmountFormatted)
+ .multipliedBy(sellTokenPriceUsd)
+ .toFixed();
+ const buyAmountUSD = valueToBigNumber(buyAmountFormatted)
+ .multipliedBy(buyTokenPriceUsd)
+ .toFixed();
+
+ const sellAmountBigInt = BigInt(
+ normalizeBN(sellAmountFormatted, -sellAmountToken.decimals).toFixed(0)
+ );
+
+ const buyAmountBigInt = BigInt(
+ normalizeBN(buyAmountFormatted, -buyAmountToken.decimals).toFixed(0)
+ );
+
+ setState({
+ buyAmountFormatted,
+ buyAmountUSD,
+ sellAmountFormatted,
+ sellAmountUSD,
+ sellAmountToken,
+ buyAmountToken,
+ isInvertedSwap,
+ sellAmountBigInt,
+ buyAmountBigInt,
+ processedSide,
+ networkFeeAmountInSellFormatted,
+ networkFeeAmountInBuyFormatted,
+ partnerFeeAmountFormatted: partnerFeeAmount.toFixed(),
+ partnerFeeBps: partnetFeeBps,
+ });
+ }, [
+ state.inputAmount,
+ state.outputAmount,
+ state.slippage,
+ state.sourceToken,
+ state.destinationToken,
+ state.side,
+ state.swapType,
+ state.orderType,
+ ]);
+};
diff --git a/src/components/transactions/Swap/hooks/useSwapQuote.ts b/src/components/transactions/Swap/hooks/useSwapQuote.ts
new file mode 100644
index 0000000000..bcc5bf66e2
--- /dev/null
+++ b/src/components/transactions/Swap/hooks/useSwapQuote.ts
@@ -0,0 +1,462 @@
+import { normalizeBN } from '@aave/math-utils';
+import { useQuery } from '@tanstack/react-query';
+import { Dispatch, useEffect, useMemo } from 'react';
+import { useModalContext } from 'src/hooks/useModal';
+import { isTxErrorType, TxErrorType } from 'src/ui-config/errorMapping';
+import { queryKeysFactory } from 'src/ui-config/queries';
+
+import { TrackAnalyticsHandlers } from '../analytics/useTrackAnalytics';
+import { APP_CODE_PER_SWAP_TYPE } from '../constants/shared.constants';
+import { hasFlashLoanDisabled } from '../errors/shared/FlashLoanDisabledBlockingGuard';
+import { hasInsufficientBalance } from '../errors/shared/InsufficientBalanceGuard';
+import { getCowProtocolSellRates } from '../helpers/cow';
+import { getParaswapSellRates, getParaswapSlippage } from '../helpers/paraswap';
+import { getSwitchProvider } from '../helpers/shared/provider.helpers';
+import {
+ SwapParams,
+ SwapProvider,
+ SwapQuoteType as SwapQuoteType,
+ SwapState,
+ SwapType,
+ TokenType,
+} from '../types';
+
+interface TokenSelectionParams {
+ srcToken: string;
+ destToken: string;
+ srcDecimals: number;
+ destDecimals: number;
+ inputSymbol: string;
+ outputSymbol: string;
+ isInputTokenCustom: boolean;
+ isOutputTokenCustom: boolean;
+ side: 'buy' | 'sell';
+}
+
+export const swapTypesThatRequiresInvertedQuote: SwapType[] = [
+ SwapType.DebtSwap,
+ SwapType.RepayWithCollateral,
+];
+
+const getTokenSelectionForQuote = (
+ invertedQuoteRoute: boolean,
+ state: SwapState
+): TokenSelectionParams => {
+ // Note: Consider the quote an approximation, we prefer underlying address for better support while aTokens value should always match
+ const srcTokenObj = invertedQuoteRoute ? state.destinationToken : state.sourceToken;
+ const srcToken =
+ state.useFlashloan == false &&
+ state.provider === SwapProvider.PARASWAP &&
+ state.swapType !== SwapType.WithdrawAndSwap &&
+ state.swapType !== SwapType.RepayWithCollateral
+ ? srcTokenObj.addressToSwap
+ : srcTokenObj.underlyingAddress;
+ const destTokenObj = invertedQuoteRoute ? state.sourceToken : state.destinationToken;
+ const destToken =
+ state.useFlashloan == false &&
+ state.provider === SwapProvider.PARASWAP &&
+ state.swapType !== SwapType.WithdrawAndSwap &&
+ state.swapType !== SwapType.RepayWithCollateral
+ ? destTokenObj.addressToSwap
+ : destTokenObj.underlyingAddress;
+
+ const srcDecimals = invertedQuoteRoute
+ ? state.destinationToken.decimals
+ : state.sourceToken.decimals;
+ const destDecimals = invertedQuoteRoute
+ ? state.sourceToken.decimals
+ : state.destinationToken.decimals;
+ const inputSymbol = invertedQuoteRoute ? state.destinationToken.symbol : state.sourceToken.symbol;
+ const outputSymbol = invertedQuoteRoute
+ ? state.sourceToken.symbol
+ : state.destinationToken.symbol;
+ const isInputTokenCustom = invertedQuoteRoute
+ ? state.destinationToken.tokenType === TokenType.USER_CUSTOM
+ : state.sourceToken.tokenType === TokenType.USER_CUSTOM;
+ const isOutputTokenCustom = invertedQuoteRoute
+ ? state.sourceToken.tokenType === TokenType.USER_CUSTOM
+ : state.destinationToken.tokenType === TokenType.USER_CUSTOM;
+ const side = invertedQuoteRoute ? (state.side === 'buy' ? 'sell' : 'buy') : state.side;
+
+ return {
+ srcToken,
+ destToken,
+ srcDecimals,
+ destDecimals,
+ inputSymbol,
+ outputSymbol,
+ isInputTokenCustom,
+ isOutputTokenCustom,
+ side,
+ };
+};
+
+export const QUOTE_REFETCH_INTERVAL = 30000; // 30 seconds
+
+/**
+ * React hook that orchestrates quoting logic across providers.
+ *
+ * - Selects provider via getSwitchProvider
+ * - Builds provider-agnostic params from SwapState/SwapParams
+ * - Periodically refetches quotes and writes normalized values into SwapState
+ */
+export const useSwapQuote = ({
+ params,
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+ trackingHandlers?: TrackAnalyticsHandlers;
+}) => {
+ // Once transaction succeeds, lock the provider to prevent recalculation
+ // (useFlashloan or other dependencies might change after invalidateAppState)
+ const provider = useMemo(() => {
+ // If transaction already succeeded, use the existing provider from state
+ if (state.mainTxState.success && state.provider !== SwapProvider.NONE) {
+ return state.provider;
+ }
+ // Otherwise, calculate provider based on current state
+ return getSwitchProvider({
+ chainId: state.chainId,
+ assetFrom: state.sourceToken.addressToSwap,
+ assetTo: state.destinationToken.addressToSwap,
+ swapType: params.swapType,
+ shouldUseFlashloan: state.useFlashloan,
+ });
+ }, [
+ state.mainTxState.success,
+ state.provider,
+ state.chainId,
+ state.sourceToken.addressToSwap,
+ state.destinationToken.addressToSwap,
+ params.swapType,
+ state.useFlashloan,
+ ]);
+
+ const requiresQuoteInverted = useMemo(
+ () => swapTypesThatRequiresInvertedQuote.includes(params.swapType),
+ [provider, params.swapType]
+ );
+
+ const {
+ data: swapQuote,
+ isLoading: ratesLoading,
+ error: ratesError,
+ } = useMultiProviderSwapQuoteQuery({
+ provider: provider ?? SwapProvider.NONE,
+ params,
+ state,
+ setState,
+ requiresQuoteInverted,
+ });
+
+ const quoteToState = (quote: SwapQuoteType | null | undefined) => {
+ if (!quote) return;
+
+ const nextInputAmount = normalizeBN(quote.srcSpotAmount, quote.srcDecimals).toFixed();
+ const nextOutputAmount = normalizeBN(quote.destSpotAmount, quote.destDecimals).toFixed();
+ const nextInputAmountUSD = quote.srcSpotUSD;
+ const nextOutputAmountUSD = quote.destSpotUSD;
+
+ // Skip update if nothing changed to avoid re-render loops
+ if (
+ state.provider == quote.provider &&
+ state.swapRate?.srcSpotAmount == quote.srcSpotAmount &&
+ state.swapRate?.destSpotAmount == quote.destSpotAmount &&
+ state.inputAmount == nextInputAmount &&
+ state.outputAmount == nextOutputAmount &&
+ state.inputAmountUSD == nextInputAmountUSD &&
+ state.outputAmountUSD == nextOutputAmountUSD
+ ) {
+ return;
+ }
+
+ let slippage = state.slippage;
+ let autoSlippage = state.autoSlippage;
+ if (quote.provider === 'cowprotocol' && quote?.suggestedSlippage !== undefined) {
+ slippage = quote.suggestedSlippage.toString();
+ autoSlippage = quote.suggestedSlippage.toString();
+ } else if (quote.provider === 'paraswap') {
+ const paraswapSlippage = getParaswapSlippage(
+ state.sourceToken.symbol || '',
+ state.destinationToken.symbol || '',
+ state.swapType
+ );
+ slippage = paraswapSlippage;
+ autoSlippage = paraswapSlippage;
+ }
+
+ return {
+ swapRate: quote,
+ inputAmount: nextInputAmount,
+ outputAmount: nextOutputAmount,
+ inputAmountUSD: nextInputAmountUSD,
+ outputAmountUSD: nextOutputAmountUSD,
+ slippage,
+ autoSlippage,
+ };
+ };
+
+ useEffect(() => {
+ if (provider) {
+ setState({
+ provider,
+ swapRate: undefined, // Clear the old swap rate to force new quote
+ autoSlippage: '', // Clear suggested slippage until a new quote arrives
+ quoteRefreshPaused: false, // Ensure quotes can be fetched
+ });
+ }
+ }, [provider]);
+
+ useEffect(() => {
+ if (ratesLoading != state.ratesLoading) {
+ setState({ ratesLoading: ratesLoading });
+ }
+ }, [ratesLoading]);
+
+ useEffect(() => {
+ if (ratesError) {
+ setState({
+ error: { rawError: ratesError, message: ratesError.message, actionBlocked: true },
+ ratesLoading: false,
+ swapRate: undefined,
+ });
+ }
+ }, [ratesError]);
+
+ useEffect(() => {
+ if (swapQuote) {
+ const isAutoRefreshed = Boolean(state.quoteLastUpdatedAt);
+ trackingHandlers?.trackSwapQuote(isAutoRefreshed, swapQuote);
+
+ setState({
+ provider: swapQuote.provider,
+ ...quoteToState(swapQuote),
+ quoteLastUpdatedAt: Date.now(),
+ // Reset pause bookkeeping on new quote
+ quoteTimerPausedAt: null,
+ quoteTimerPausedAccumMs: 0,
+
+ error: undefined,
+ actionsBlocked: {},
+ warnings: [],
+ actionsLoading: false,
+ });
+ }
+ }, [swapQuote]);
+
+ // Pause/resume timer bookkeeping when actions are loading
+ useEffect(() => {
+ if (state.actionsLoading) {
+ if (!state.quoteTimerPausedAt) {
+ setState({ quoteTimerPausedAt: Date.now() });
+ }
+ } else {
+ if (state.quoteTimerPausedAt) {
+ const pausedDelta = Date.now() - state.quoteTimerPausedAt;
+ setState({
+ quoteTimerPausedAt: null,
+ quoteTimerPausedAccumMs: (state.quoteTimerPausedAccumMs || 0) + pausedDelta,
+ });
+ }
+ }
+ }, [state.actionsLoading]);
+};
+
+/**
+ * Low-level function used by useSwapQuote to query the selected provider.
+ * Converts state into provider params and returns a normalized `SwapQuoteType`.
+ */
+const useMultiProviderSwapQuoteQuery = ({
+ params,
+ state,
+ setState,
+ provider,
+ requiresQuoteInverted,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+ provider: SwapProvider;
+ requiresQuoteInverted: boolean;
+}) => {
+ const { approvalTxState } = useModalContext();
+
+ // Amount to quote depends on side (sell uses input amount, buy uses output amount)
+ const amount = useMemo(() => {
+ if (state.side === 'sell') {
+ return normalizeBN(state.debouncedInputAmount, -1 * state.sourceToken.decimals).toFixed(0);
+ } else {
+ return normalizeBN(state.debouncedOutputAmount, -1 * state.destinationToken.decimals).toFixed(
+ 0
+ );
+ }
+ }, [
+ state.debouncedInputAmount,
+ state.debouncedOutputAmount,
+ requiresQuoteInverted,
+ state.side,
+ state.sourceToken.decimals,
+ state.destinationToken.decimals,
+ ]);
+
+ const appCode = APP_CODE_PER_SWAP_TYPE[params.swapType];
+
+ const {
+ srcToken,
+ destToken,
+ srcDecimals,
+ destDecimals,
+ inputSymbol,
+ outputSymbol,
+ isInputTokenCustom,
+ isOutputTokenCustom,
+ side,
+ } = useMemo(
+ () => getTokenSelectionForQuote(requiresQuoteInverted, state),
+ [
+ state.provider,
+ state.sourceToken,
+ state.destinationToken,
+ state.side,
+ requiresQuoteInverted,
+ state.useFlashloan,
+ ]
+ );
+
+ return useQuery({
+ queryFn: async () => {
+ if (!provider) {
+ setState({
+ error: {
+ rawError: new Error('No swap provider found in the selected chain for this pair'),
+ message: 'No swap provider found in the selected chain for this pair',
+ actionBlocked: true,
+ },
+ });
+ return null;
+ }
+
+ if (state.sourceToken.addressToSwap === state.destinationToken.addressToSwap) {
+ setState({
+ error: {
+ rawError: new Error('Source and destination tokens cannot be the same'),
+ message: 'Source and destination tokens cannot be the same',
+ actionBlocked: true,
+ },
+ });
+ return null;
+ }
+
+ const setError = (error: Error | TxErrorType) => {
+ setState({
+ error: {
+ rawError: isTxErrorType(error) ? error.rawError : error,
+ message: isTxErrorType(error) ? 'Error in Swap Quote' : error.message,
+ actionBlocked: true,
+ },
+ });
+ };
+
+ switch (provider) {
+ case SwapProvider.COW_PROTOCOL:
+ return await getCowProtocolSellRates({
+ swapType: state.swapType,
+ chainId: state.chainId,
+ amount,
+ srcToken,
+ destToken,
+ user: state.user,
+ srcDecimals,
+ destDecimals,
+ inputSymbol,
+ outputSymbol,
+ isInputTokenCustom,
+ isOutputTokenCustom,
+ appCode,
+ setError,
+ side,
+ invertedQuoteRoute: requiresQuoteInverted,
+ });
+ case SwapProvider.PARASWAP:
+ return await getParaswapSellRates({
+ swapType: state.swapType,
+ chainId: state.chainId,
+ amount,
+ srcToken,
+ destToken,
+ user: state.user,
+ srcDecimals,
+ destDecimals,
+ side,
+ appCode,
+ options: {
+ partner: appCode,
+ },
+ invertedQuoteRoute: requiresQuoteInverted,
+ });
+ default:
+ // Error
+ setError(new Error('No swap provider found in the selected chain for this pair'));
+ return null;
+ }
+ },
+ queryKey: queryKeysFactory.swapQuote(
+ state.chainId,
+ provider,
+ amount,
+ requiresQuoteInverted,
+ srcToken,
+ destToken,
+ state.user
+ ),
+ enabled: (() => {
+ // Allow fetch when user has entered a positive amount, even if normalization rounded to '0'
+ const hasPositiveUserAmount =
+ state.side === 'sell'
+ ? Number(state.debouncedInputAmount || '0') > 0
+ : Number(state.debouncedOutputAmount || '0') > 0;
+
+ // Basic pre-blockers to avoid provider requests
+ const isSameTokenPair =
+ state.sourceToken.addressToSwap === state.destinationToken.addressToSwap;
+ const isFlashloanDisabled = hasFlashLoanDisabled(state);
+
+ return (
+ hasPositiveUserAmount &&
+ !isSameTokenPair &&
+ !isFlashloanDisabled &&
+ !state.mainTxState.success &&
+ !state.mainTxState.txHash && // Don't fetch quotes once transaction is sent
+ !state.mainTxState.loading && // Don't fetch quotes while transaction is processing
+ !approvalTxState?.loading && // Don't fetch quotes while approval is processing
+ !approvalTxState?.success && // Don't fetch quotes while approval is successful
+ provider !== SwapProvider.NONE &&
+ !state.quoteRefreshPaused &&
+ !state.isWrongNetwork
+ );
+ })(),
+ retry: 0,
+ throwOnError: false,
+ refetchOnWindowFocus: (query) => (query.state.error ? false : true),
+ refetchInterval: (() => {
+ const isInsufficientBalance = hasInsufficientBalance(state);
+ const isFlashloanDisabled = hasFlashLoanDisabled(state);
+
+ return !state.actionsLoading &&
+ !state.quoteRefreshPaused &&
+ !state.mainTxState.success &&
+ !state.mainTxState.txHash &&
+ !state.mainTxState.loading &&
+ !approvalTxState?.loading &&
+ !approvalTxState?.success &&
+ !isInsufficientBalance &&
+ !isFlashloanDisabled
+ ? QUOTE_REFETCH_INTERVAL
+ : false;
+ })(),
+ });
+};
diff --git a/src/components/transactions/Swap/hooks/useUserContext.ts b/src/components/transactions/Swap/hooks/useUserContext.ts
new file mode 100644
index 0000000000..25ba020976
--- /dev/null
+++ b/src/components/transactions/Swap/hooks/useUserContext.ts
@@ -0,0 +1,31 @@
+import { Dispatch, useEffect } from 'react';
+import { isSafeWallet, isSmartContractWallet } from 'src/helpers/provider';
+import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
+import { getEthersProvider } from 'src/libs/web3-data-provider/adapters/EthersAdapter';
+import { useRootStore } from 'src/store/root';
+import { wagmiConfig } from 'src/ui-config/wagmiConfig';
+
+import { SwapState } from '../types';
+
+export const useUserContext = ({ setState }: { setState: Dispatch> }) => {
+ const user = useRootStore((store) => store.account);
+ const { chainId: connectedChainId } = useWeb3Context();
+
+ useEffect(() => {
+ try {
+ if (user && connectedChainId) {
+ setState({ user });
+ getEthersProvider(wagmiConfig, { chainId: connectedChainId }).then((provider) => {
+ Promise.all([isSmartContractWallet(user, provider), isSafeWallet(user, provider)]).then(
+ ([isSmartContract, isSafe]) => {
+ setState({ userIsSmartContractWallet: isSmartContract });
+ setState({ userIsSafeWallet: isSafe });
+ }
+ );
+ });
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }, [user, connectedChainId]);
+};
diff --git a/src/components/transactions/Swap/inputs/LimitOrderInputs.tsx b/src/components/transactions/Swap/inputs/LimitOrderInputs.tsx
new file mode 100644
index 0000000000..dc5ddf9beb
--- /dev/null
+++ b/src/components/transactions/Swap/inputs/LimitOrderInputs.tsx
@@ -0,0 +1,281 @@
+import { ArrowDownIcon, SwitchVerticalIcon } from '@heroicons/react/outline';
+import { Box, IconButton, SvgIcon, Typography } from '@mui/material';
+import { Dispatch } from 'react';
+
+import { SwapInputChanges } from '../analytics/constants';
+import { TrackAnalyticsHandlers } from '../analytics/useTrackAnalytics';
+import { QUOTE_REFETCH_INTERVAL } from '../hooks/useSwapQuote';
+import { Expiry, OrderType, SwapParams, SwapProvider, SwapState } from '../types';
+import { SwitchAssetInput } from './primitives/SwapAssetInput';
+import { ExpirySelector } from './shared/ExpirySelector';
+import { NetworkSelector } from './shared/NetworkSelector';
+import { PriceInput } from './shared/PriceInput';
+import { QuoteProgressRing } from './shared/QuoteProgressRing';
+import { SwapInputState } from './SwapInputs';
+
+export type SwapInputsCustomProps = {
+ canSwitchTokens: boolean;
+};
+
+export const LimitOrderInputs = ({
+ params,
+ state,
+ swapState,
+ setState,
+ customProps,
+ trackingHandlers,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ swapState: SwapInputState;
+ setState: Dispatch>;
+ customProps?: SwapInputsCustomProps;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ // Prioritize Limit Order specific input/output titles
+ let inputInputTitle;
+ let outputInputTitle;
+
+ if (state.orderType === OrderType.LIMIT) {
+ if (!inputInputTitle) {
+ inputInputTitle =
+ state.processedSide === 'sell' ? params.inputInputTitleSell : params.inputInputTitleBuy;
+ }
+ if (!outputInputTitle) {
+ outputInputTitle =
+ state.processedSide === 'buy' ? params.outputInputTitleBuy : params.outputInputTitleSell;
+ }
+ }
+ // Fallback to global input/output titles
+ if (!inputInputTitle) {
+ inputInputTitle = params.inputInputTitle;
+ }
+ if (!outputInputTitle) {
+ outputInputTitle = params.outputInputTitle;
+ }
+
+ return (
+ <>
+
+ {(inputInputTitle || swapState.showNetworkSelector) && (
+
+ {inputInputTitle && (
+
+ {inputInputTitle}
+
+ )}
+ {swapState.showNetworkSelector && (
+
+ )}
+
+ )}
+
+ {
+ setState({ expiry });
+ trackingHandlers.trackInputChange(SwapInputChanges.EXPIRY, expiry.toString());
+ }}
+ />
+
+
+
+ {
+ setState({
+ inputAmount: '',
+ debouncedInputAmount: '',
+ inputAmountUSD: '',
+ quoteRefreshPaused: true,
+ quoteLastUpdatedAt: undefined,
+ });
+ if (state.outputAmount === '') {
+ // Both reset to listen quotes
+ setState({
+ swapRate: undefined,
+ quoteRefreshPaused: false,
+ quoteLastUpdatedAt: undefined,
+ });
+ }
+ }}
+ usdValue={state.inputAmountUSD || '0'}
+ onSelect={swapState.handleSelectedInputToken}
+ selectedAsset={state.sourceToken}
+ forcedMaxValue={state.forcedMaxValue}
+ allowCustomTokens={params.allowCustomTokens}
+ swapType={params.swapType}
+ side="input"
+ />
+
+ {params.showSwitchInputAndOutputAssetsButton ? (
+
+
+
+
+
+
+ {!state.quoteRefreshPaused && (
+
+ )}
+
+ ) : (
+ !outputInputTitle && (
+
+
+
+
+
+
+ {!state.quoteRefreshPaused && (
+
+ )}
+
+ )
+ )}
+
+ {
+ swapState.handleOutputChange(value);
+ }}
+ onClear={() => {
+ setState({
+ outputAmount: '',
+ debouncedOutputAmount: '',
+ outputAmountUSD: '',
+ quoteRefreshPaused: true,
+ quoteLastUpdatedAt: undefined,
+ });
+ if (state.inputAmount === '') {
+ // Both reset to listen quotes
+ setState({
+ swapRate: undefined,
+ quoteRefreshPaused: false,
+ quoteLastUpdatedAt: undefined,
+ });
+ }
+ }}
+ onSelect={swapState.handleSelectedOutputToken}
+ disableInput={false}
+ selectedAsset={state.destinationToken}
+ showBalance={false}
+ allowCustomTokens={params.allowCustomTokens}
+ swapType={params.swapType}
+ side="output"
+ />
+
+
+
+ >
+ );
+};
diff --git a/src/components/transactions/Swap/inputs/MarketOrderInputs.tsx b/src/components/transactions/Swap/inputs/MarketOrderInputs.tsx
new file mode 100644
index 0000000000..241fd3a85a
--- /dev/null
+++ b/src/components/transactions/Swap/inputs/MarketOrderInputs.tsx
@@ -0,0 +1,220 @@
+import { ArrowDownIcon, SwitchVerticalIcon } from '@heroicons/react/outline';
+import { Box, IconButton, SvgIcon, Typography } from '@mui/material';
+import { Dispatch } from 'react';
+
+import { QUOTE_REFETCH_INTERVAL } from '../hooks/useSwapQuote';
+import { isCowProtocolRates, SwapParams, SwapProvider, SwapState } from '../types';
+import { SwitchAssetInput } from './primitives/SwapAssetInput';
+import { NetworkSelector } from './shared/NetworkSelector';
+import { QuoteProgressRing } from './shared/QuoteProgressRing';
+import { SwitchRates } from './shared/SwitchRates';
+import { SwitchSlippageSelector } from './shared/SwitchSlippageSelector';
+import { SwapInputState } from './SwapInputs';
+
+export type SwapInputsCustomProps = {
+ canSwitchTokens: boolean;
+};
+
+export const MarketOrderInputs = ({
+ params,
+ state,
+ swapState,
+ setState,
+ customProps,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ swapState: SwapInputState;
+ setState: Dispatch>;
+ customProps?: SwapInputsCustomProps;
+}) => {
+ return (
+ <>
+
+ {(params.inputInputTitle || swapState.showNetworkSelector) && (
+
+ {params.inputInputTitle && (
+
+ {params.inputInputTitle}
+
+ )}
+ {swapState.showNetworkSelector && (
+
+ )}
+
+ )}
+
+
+
+
+
+
+ setState({
+ inputAmount: '',
+ debouncedInputAmount: '',
+ inputAmountUSD: '',
+ outputAmount: '',
+ debouncedOutputAmount: '',
+ outputAmountUSD: '',
+ swapRate: undefined,
+ ratesLoading: false,
+ error: undefined,
+ warnings: [],
+ quoteRefreshPaused: true,
+ quoteLastUpdatedAt: undefined,
+ autoSlippage: '',
+ })
+ }
+ usdValue={state.inputAmountUSD.toString() || '0'}
+ onSelect={swapState.handleSelectedInputToken}
+ selectedAsset={state.sourceToken}
+ forcedMaxValue={state.forcedMaxValue}
+ allowCustomTokens={params.allowCustomTokens}
+ swapType={params.swapType}
+ side="input"
+ />
+
+ {params.showSwitchInputAndOutputAssetsButton ? (
+
+
+
+
+
+
+ {!state.quoteRefreshPaused && (
+
+ )}
+
+ ) : (
+ !params.outputInputTitle && (
+
+
+
+
+
+
+ {!state.quoteRefreshPaused && (
+
+ )}
+
+ )
+ )}
+
+
+
+
+ {state.swapRate && state.isSwapFlowSelected && (
+ <>
+
+ >
+ )}
+ >
+ );
+};
diff --git a/src/components/transactions/Swap/inputs/SwapInputs.tsx b/src/components/transactions/Swap/inputs/SwapInputs.tsx
new file mode 100644
index 0000000000..b64f8d1f8d
--- /dev/null
+++ b/src/components/transactions/Swap/inputs/SwapInputs.tsx
@@ -0,0 +1,722 @@
+import { BigNumberValue, valueToBigNumber } from '@aave/math-utils';
+import { WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/cow-sdk';
+import { useQueryClient } from '@tanstack/react-query';
+import { Dispatch, useEffect, useMemo } from 'react';
+import { useRootStore } from 'src/store/root';
+import { queryKeysFactory } from 'src/ui-config/queries';
+
+import { SwapInputChanges } from '../analytics/constants';
+import { TrackAnalyticsHandlers } from '../analytics/useTrackAnalytics';
+import { SESSION_STORAGE_EXPIRY_MS } from '../constants/shared.constants';
+import {
+ OrderType,
+ SwappableToken,
+ swappableTokenToTokenInfo,
+ SwapParams,
+ SwapState,
+ SwapType,
+ TokenType,
+} from '../types';
+import { LimitOrderInputs } from './LimitOrderInputs';
+import { MarketOrderInputs } from './MarketOrderInputs';
+
+export type SwapInputState = {
+ handleSelectedInputToken: (token: SwappableToken) => void;
+ handleSelectedOutputToken: (token: SwappableToken) => void;
+ handleSelectedNetworkChange: (value: number) => void;
+ setSlippage: (value: string) => void;
+ showNetworkSelector: boolean;
+ inputAssets: SwappableToken[];
+ outputAssets: SwappableToken[];
+ handleInputChange: (value: string) => void;
+ handleOutputChange: (value: string) => void;
+ handleRateChange: (rateFromAsset: SwappableToken, newRate: BigNumberValue) => void;
+ onSwitchReserves: () => void;
+};
+
+/**
+ * Input surface for both market and limit orders.
+ *
+ * Responsibilities:
+ * - Manage input/output amount edits, max selection, and switching tokens
+ * - Pause automatic quote refresh when user makes manual price/amount edits
+ * - Persist last token selection per swap type + chain in sessionStorage (with expiry)
+ * - Filter token lists to avoid wrapping paths and native token pitfalls for SCWs
+ */
+export const SwapInputs = ({
+ params,
+ state,
+ setState,
+ trackingHandlers,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ const resetErrorsAndWarnings = () => {
+ setState({
+ error: undefined,
+ warnings: [],
+ actionsBlocked: {},
+ actionsLoading: false,
+ });
+ };
+
+ const handleInputChange = (value: string) => {
+ resetErrorsAndWarnings();
+
+ // Calculate USD per token unit if possible
+ const usdPerToken = state.swapRate?.srcTokenPriceUsd;
+
+ const computeUSD = (amt: string) =>
+ usdPerToken ? valueToBigNumber(amt).multipliedBy(usdPerToken).toString(10) : '';
+
+ if (state.orderType === OrderType.LIMIT && state.swapRate) {
+ // Manual edit should pause quote refresh
+ setState({ quoteRefreshPaused: true });
+ }
+
+ if (value === '-1') {
+ const maxAmount = state.sourceToken.balance;
+ setState({
+ ...(state.orderType === OrderType.LIMIT && state.swapRate
+ ? {
+ quoteRefreshPaused: true,
+ quoteLastUpdatedAt: undefined,
+ quoteTimerPausedAt: undefined,
+ quoteTimerPausedAccumMs: undefined,
+ }
+ : {}),
+ inputAmount: maxAmount,
+ inputAmountUSD: computeUSD(maxAmount),
+ isMaxSelected: true,
+ side: 'sell',
+ });
+ } else {
+ setState({
+ ...(state.orderType === OrderType.LIMIT && state.swapRate
+ ? {
+ quoteRefreshPaused: true,
+ quoteLastUpdatedAt: undefined,
+ quoteTimerPausedAt: undefined,
+ quoteTimerPausedAccumMs: undefined,
+ }
+ : {}),
+ inputAmount: value,
+ inputAmountUSD: computeUSD(value),
+ isMaxSelected: value === state.forcedMaxValue,
+ side: 'sell',
+ });
+ }
+
+ trackingHandlers.trackInputChange(SwapInputChanges.INPUT_AMOUNT, value);
+ resetErrorsAndWarnings();
+ };
+
+ const handleOutputChange = (value: string) => {
+ // Calculate USD per token unit if possible, same as in handleInputChange
+ const usdPerToken = state.swapRate?.destTokenPriceUsd;
+
+ const computeUSD = (amt: string) =>
+ usdPerToken ? valueToBigNumber(amt).multipliedBy(usdPerToken).toString(10) : '';
+
+ if (state.swapRate) {
+ // Block quote refreshs if user is changing the output amount after getting quotes
+ setState({ quoteRefreshPaused: true });
+ }
+
+ if (value === '-1') {
+ const maxAmount = state.destinationToken.balance;
+ setState({
+ ...(state.orderType === OrderType.LIMIT && state.swapRate
+ ? {
+ quoteRefreshPaused: true,
+ quoteLastUpdatedAt: undefined,
+ quoteTimerPausedAt: undefined,
+ quoteTimerPausedAccumMs: undefined,
+ }
+ : {}),
+ outputAmount: maxAmount,
+ outputAmountUSD: computeUSD(maxAmount),
+ isMaxSelected: true,
+ side: 'buy',
+ });
+ } else {
+ setState({
+ ...(state.orderType === OrderType.LIMIT && state.swapRate
+ ? {
+ quoteRefreshPaused: true,
+ quoteLastUpdatedAt: undefined,
+ quoteTimerPausedAt: undefined,
+ quoteTimerPausedAccumMs: undefined,
+ }
+ : {}),
+ outputAmount: value,
+ outputAmountUSD: computeUSD(value),
+ isMaxSelected: false,
+ side: 'buy',
+ });
+ }
+
+ trackingHandlers.trackInputChange(SwapInputChanges.OUTPUT_AMOUNT, value);
+ resetErrorsAndWarnings();
+ };
+
+ const handleRateChange = (rateFromAsset: SwappableToken, newRate: BigNumberValue) => {
+ if (newRate.toString() === '0') return;
+
+ // User changed custom price, pause quote refresh in limit orders
+ setState({ quoteRefreshPaused: true });
+
+ // Normalize the rate to always be dest per 1 source, then recompute output deterministically
+ const isBaseSource = rateFromAsset.addressToSwap === state.sourceToken.addressToSwap;
+ const rateDestPerSrc = isBaseSource
+ ? valueToBigNumber(newRate)
+ : valueToBigNumber(1).dividedBy(newRate);
+ const newOutputAmount = valueToBigNumber(state.inputAmount || '0')
+ .multipliedBy(rateDestPerSrc)
+ .toString();
+
+ setState({
+ quoteRefreshPaused: true,
+ quoteLastUpdatedAt: undefined,
+ quoteTimerPausedAt: undefined,
+ quoteTimerPausedAccumMs: undefined,
+ outputAmount: newOutputAmount,
+ isMaxSelected: false,
+ side: 'sell',
+ });
+
+ trackingHandlers.trackInputChange(SwapInputChanges.RATE_CHANGE, newRate.toString());
+ resetErrorsAndWarnings();
+ };
+
+ const onSwitchReserves = () => {
+ const fromToken = state.sourceToken;
+ const toToken = state.destinationToken;
+
+ setState({
+ quoteRefreshPaused: false,
+ quoteLastUpdatedAt: undefined,
+ quoteTimerPausedAt: undefined,
+ quoteTimerPausedAccumMs: undefined,
+ sourceToken: toToken,
+ destinationToken: fromToken,
+ inputAmount: '',
+ debouncedInputAmount: '',
+ outputAmount: '',
+ debouncedOutputAmount: '',
+ inputAmountUSD: '',
+ outputAmountUSD: '',
+ ratesLoading: false,
+ swapRate: undefined,
+ actionsLoading: false,
+ slippage: '0.1',
+ autoSlippage: '',
+ actionsBlocked: {},
+ error: undefined,
+ warnings: [],
+ });
+
+ trackingHandlers.trackInputChange(SwapInputChanges.SWITCH_RESERVES, 'switched');
+ resetErrorsAndWarnings();
+ resetSwap('both');
+ };
+
+ const queryClient = useQueryClient();
+ const user = useRootStore((store) => store.account);
+
+ const addNewToken = async (token: SwappableToken) => {
+ queryClient.setQueryData(
+ queryKeysFactory.tokensBalance(
+ state.sourceTokens.concat(state.destinationTokens).map(swappableTokenToTokenInfo) ?? [],
+ state.chainId,
+ user
+ ),
+ (oldData) => {
+ if (oldData)
+ return [...oldData, token].sort((a, b) => Number(b.balance) - Number(a.balance));
+ return [token];
+ }
+ );
+ const customTokens = localStorage.getItem('customTokens');
+ const newTokenInfo: SwappableToken = {
+ addressToSwap: token.addressToSwap,
+ addressForUsdPrice: token.addressForUsdPrice,
+ underlyingAddress: token.underlyingAddress,
+ name: token.name,
+ symbol: token.symbol,
+ decimals: token.decimals,
+ chainId: token.chainId,
+ balance: token.balance,
+ logoURI: token.logoURI,
+ tokenType: TokenType.USER_CUSTOM,
+ };
+ if (customTokens) {
+ const parsedCustomTokens: SwappableToken[] = JSON.parse(customTokens);
+ parsedCustomTokens.push(newTokenInfo);
+ localStorage.setItem('customTokens', JSON.stringify(parsedCustomTokens));
+ } else {
+ localStorage.setItem('customTokens', JSON.stringify([newTokenInfo]));
+ }
+ trackingHandlers.trackInputChange(SwapInputChanges.ADD_CUSTOM_TOKEN, token.symbol);
+ };
+
+ const handleSelectedInputToken = (token: SwappableToken) => {
+ if (!state.sourceTokens?.find((t) => t.addressToSwap === token.addressToSwap)) {
+ addNewToken(token).then(() => {
+ setState({
+ sourceToken: token,
+ inputAmount: '',
+ debouncedInputAmount: '',
+ inputAmountUSD: '',
+ quoteRefreshPaused: false,
+ quoteTimerPausedAt: undefined,
+ quoteTimerPausedAccumMs: undefined,
+ autoSlippage: '',
+ error: undefined,
+ warnings: [],
+ actionsBlocked: {},
+ actionsLoading: false,
+ });
+ saveTokenSelection(token, state.destinationToken);
+ saveRecentToken('input', token);
+ resetErrorsAndWarnings();
+ });
+ } else {
+ setState({
+ sourceToken: token,
+ inputAmount: '',
+ debouncedInputAmount: '',
+ inputAmountUSD: '',
+ quoteRefreshPaused: false,
+ quoteTimerPausedAt: undefined,
+ quoteTimerPausedAccumMs: undefined,
+ autoSlippage: '',
+ error: undefined,
+ warnings: [],
+ actionsBlocked: {},
+ actionsLoading: false,
+ });
+ saveTokenSelection(token, state.destinationToken);
+ saveRecentToken('input', token);
+ resetErrorsAndWarnings();
+ }
+ trackingHandlers.trackInputChange(SwapInputChanges.INPUT_TOKEN, token.symbol);
+ };
+
+ // Persist selected tokens in session storage to retain them on modal close/open but differentiating by modalType
+ const getStorageKey = (swapType: SwapType, chainId: number) => {
+ // if (SwapType.CollateralSwap === swapType) {
+ // return `aave_switch_tokens_${swapType}_${chainId}_${state.sourceToken?.addressToSwap?.toLowerCase()}`;
+ // } else {
+ return `aave_switch_tokens_${swapType}_${chainId}`;
+ // }
+ };
+
+ const handleSelectedOutputToken = (token: SwappableToken) => {
+ if (!state.destinationTokens?.find((t) => t.addressToSwap === token.addressToSwap)) {
+ addNewToken(token).then(() => {
+ setState({
+ destinationToken: token,
+ outputAmount: '',
+ debouncedOutputAmount: '',
+ outputAmountUSD: '',
+ quoteRefreshPaused: false,
+ quoteTimerPausedAt: undefined,
+ quoteTimerPausedAccumMs: undefined,
+ autoSlippage: '',
+ error: undefined,
+ warnings: [],
+ actionsBlocked: {},
+ actionsLoading: false,
+ });
+ saveTokenSelection(state.sourceToken, token);
+ saveRecentToken('output', token);
+ resetErrorsAndWarnings();
+ });
+ } else {
+ setState({
+ destinationToken: token,
+ outputAmount: '',
+ debouncedOutputAmount: '',
+ outputAmountUSD: '',
+ quoteRefreshPaused: false,
+ quoteTimerPausedAt: undefined,
+ quoteTimerPausedAccumMs: undefined,
+ autoSlippage: '',
+ error: undefined,
+ warnings: [],
+ actionsBlocked: {},
+ actionsLoading: false,
+ });
+ saveTokenSelection(state.sourceToken, token);
+ saveRecentToken('output', token);
+ resetErrorsAndWarnings();
+ }
+ trackingHandlers.trackInputChange(SwapInputChanges.OUTPUT_TOKEN, token.symbol);
+ };
+
+ const saveTokenSelection = (inputToken: SwappableToken, outputToken: SwappableToken) => {
+ try {
+ sessionStorage.setItem(
+ getStorageKey(params.swapType, state.chainId),
+ JSON.stringify({
+ inputToken: params.forcedInputToken ? null : inputToken,
+ outputToken: params.forcedOutputToken ? null : outputToken,
+ timestamp: Date.now(),
+ })
+ );
+ } catch (e) {
+ console.error('Error saving token selection', e);
+ }
+ };
+
+ const getRecentStorageKey = (swapType: SwapType, chainId: number, side: 'input' | 'output') =>
+ `aave_recent_tokens_${swapType}_${chainId}_${side}`;
+
+ const saveRecentToken = (side: 'input' | 'output', token: SwappableToken) => {
+ try {
+ const key = getRecentStorageKey(params.swapType, state.chainId, side);
+ const raw = localStorage.getItem(key);
+ const list: string[] = raw ? JSON.parse(raw) : [];
+ const addr = token.addressToSwap.toLowerCase();
+ const next = [addr, ...list.filter((a) => a.toLowerCase() !== addr)];
+ localStorage.setItem(key, JSON.stringify(next.slice(0, 8)));
+ } catch (e) {
+ // ignore storage errors
+ }
+ };
+
+ const loadTokenSelection = () => {
+ try {
+ const savedTokenSelection = sessionStorage.getItem(
+ getStorageKey(params.swapType, state.chainId)
+ );
+ if (!savedTokenSelection) return null;
+
+ const parsedTokenSelection = JSON.parse(savedTokenSelection);
+ if (
+ parsedTokenSelection.timestamp &&
+ Date.now() - parsedTokenSelection.timestamp > SESSION_STORAGE_EXPIRY_MS
+ ) {
+ sessionStorage.removeItem(getStorageKey(params.swapType, state.chainId));
+ return null;
+ }
+ return parsedTokenSelection;
+ } catch (e) {
+ return null;
+ }
+ };
+
+ // TODO: Can we simplify?
+ const { defaultInputToken: fallbackInputToken, defaultOutputToken: fallbackOutputToken } =
+ useMemo(() => {
+ let auxInputToken = params.forcedInputToken || params.suggestedDefaultInputToken;
+ let auxOutputToken = params.forcedOutputToken || params.suggestedDefaultOutputToken;
+
+ const fromList = params.sourceTokens;
+ const toList = params.destinationTokens;
+
+ if (!auxInputToken) {
+ auxInputToken = fromList.find(
+ (token) =>
+ (token.balance !== '0' || token.tokenType === TokenType.NATIVE) &&
+ token.symbol !== 'GHO'
+ );
+ }
+
+ if (!auxOutputToken) {
+ auxOutputToken = toList.find((token) => token.symbol === 'GHO');
+ }
+
+ return {
+ defaultInputToken: auxInputToken ?? fromList[0],
+ defaultOutputToken: auxOutputToken ?? toList[1],
+ };
+ }, [params.sourceTokens, params.destinationTokens]);
+
+ // Helper to check if two tokens are the same (by addressToSwap, underlyingAddress, or symbol)
+ const areTokensEqual = (
+ token1: SwappableToken | undefined,
+ token2: SwappableToken | undefined
+ ): boolean => {
+ if (!token1 || !token2) return false;
+ return (
+ token1.addressToSwap.toLowerCase() === token2.addressToSwap.toLowerCase() ||
+ token1.underlyingAddress.toLowerCase() === token2.underlyingAddress.toLowerCase() ||
+ token1.symbol === token2.symbol
+ );
+ };
+
+ // Update selected tokens when defaults change (e.g., after network change)
+ useEffect(() => {
+ // Guard: do not auto-adjust tokens after user interaction (amounts entered or Max selected)
+ if (state.inputAmount || state.outputAmount || state.isMaxSelected) return;
+ const saved = loadTokenSelection();
+
+ let inputToken: SwappableToken | undefined;
+ let outputToken: SwappableToken | undefined;
+
+ // Determine input token first (prioritize forced, then saved if valid, else fallback)
+ if (params.forcedInputToken) {
+ inputToken = params.forcedInputToken;
+ } else if (saved?.inputToken) {
+ // Only use saved input token if it doesn't match the intended output
+ const intendedOutput = params.forcedOutputToken || saved.outputToken || fallbackOutputToken;
+ if (!areTokensEqual(saved.inputToken, intendedOutput)) {
+ inputToken = saved.inputToken;
+ } else {
+ inputToken = fallbackInputToken;
+ }
+ } else {
+ inputToken = fallbackInputToken;
+ }
+
+ // Determine output token (prioritize forced, then saved if valid, else fallback)
+ if (params.forcedOutputToken) {
+ outputToken = params.forcedOutputToken;
+ } else if (saved?.outputToken) {
+ // Only use saved output token if it doesn't match the input token
+ if (!areTokensEqual(saved.outputToken, inputToken)) {
+ outputToken = saved.outputToken;
+ } else {
+ outputToken = fallbackOutputToken;
+ }
+ } else {
+ outputToken = fallbackOutputToken;
+ }
+
+ // Final safety check: if input and output tokens still match, reset output to fallback
+ if (areTokensEqual(inputToken, outputToken)) {
+ outputToken = fallbackOutputToken;
+ }
+
+ setState({
+ sourceToken: inputToken ?? fallbackInputToken,
+ destinationToken: outputToken ?? fallbackOutputToken,
+ });
+ }, [
+ params.forcedInputToken,
+ params.forcedOutputToken,
+ fallbackInputToken,
+ fallbackOutputToken,
+ state.chainId,
+ state.inputAmount,
+ state.outputAmount,
+ state.isMaxSelected,
+ ]);
+
+ const resetSwap = (side: 'source' | 'destination' | 'both') => {
+ setState({ error: undefined });
+ // Reset input amount when changing networks
+ if (side === 'source') {
+ setState({ inputAmount: '' });
+ setState({ debouncedInputAmount: '' });
+ } else if (side === 'destination') {
+ setState({ outputAmount: '' });
+ setState({ debouncedOutputAmount: '' });
+ } else {
+ setState({ debouncedInputAmount: '' });
+ setState({ debouncedOutputAmount: '' });
+ }
+ resetErrorsAndWarnings();
+ };
+
+ const handleSelectedNetworkChange = (value: number) => {
+ setState({ chainId: value });
+ resetSwap('both');
+
+ params.refreshTokens(value);
+ trackingHandlers.trackInputChange(SwapInputChanges.NETWORK, value.toString());
+ };
+
+ const setSlippage = (value: string) => {
+ setState({ slippage: value });
+ if (state.slippage !== state.autoSlippage) {
+ // Pause automatic quote refresh only in market orders when slippage is edited by user
+ setState({ quoteRefreshPaused: true });
+ }
+ trackingHandlers.trackInputChange(SwapInputChanges.SLIPPAGE, value);
+ };
+
+ const showNetworkSelector = params.showNetworkSelector && !!params.supportedNetworks.length;
+
+ // Debounce input and output amounts before triggering quote logic
+ useEffect(() => {
+ const t = setTimeout(() => {
+ setState({ debouncedInputAmount: state.inputAmount });
+ }, 400);
+ return () => clearTimeout(t);
+ }, [state.inputAmount]);
+
+ useEffect(() => {
+ const t = setTimeout(() => {
+ setState({ debouncedOutputAmount: state.outputAmount });
+ }, 400);
+ return () => clearTimeout(t);
+ }, [state.outputAmount]);
+
+ const filterInputAssets = (allowOwn = false, orderType: OrderType = OrderType.MARKET) =>
+ state.sourceTokens.filter(
+ (token) =>
+ (allowOwn ||
+ // Filter out tokens that match the destination token by addressToSwap OR underlyingAddress
+ // This prevents the same asset from appearing in both lists (e.g., USDT in CollateralSwap)
+ (token.addressToSwap.toLowerCase() !==
+ state.destinationToken.addressToSwap.toLowerCase() &&
+ token.underlyingAddress.toLowerCase() !==
+ state.destinationToken.underlyingAddress.toLowerCase())) &&
+ Number(token.balance) !== 0 &&
+ // Remove native when limit order, but only for classic swaps (CollateralSwap allows native limit orders)
+ !(
+ orderType === OrderType.LIMIT &&
+ token.tokenType === TokenType.NATIVE &&
+ params.swapType === SwapType.Swap
+ ) &&
+ // Remove native tokens for non-Safe smart contract wallets
+ !(
+ state.userIsSmartContractWallet &&
+ !state.userIsSafeWallet &&
+ token.tokenType === TokenType.NATIVE
+ ) &&
+ // Avoid wrapping
+ !(
+ state.destinationToken.tokenType === TokenType.NATIVE &&
+ typeof state.chainId === 'number' &&
+ token.addressToSwap.toLowerCase() ===
+ WRAPPED_NATIVE_CURRENCIES[
+ state.chainId as keyof typeof WRAPPED_NATIVE_CURRENCIES
+ ]?.address.toLowerCase()
+ ) &&
+ !(
+ state.destinationToken.addressToSwap.toLowerCase() ===
+ WRAPPED_NATIVE_CURRENCIES[
+ state.chainId as keyof typeof WRAPPED_NATIVE_CURRENCIES
+ ]?.address.toLowerCase() && token.tokenType === TokenType.NATIVE
+ )
+ );
+
+ const inputAssets = useMemo(
+ () => filterInputAssets(false, state.orderType),
+ [
+ state.sourceTokens,
+ state.destinationToken.addressToSwap,
+ state.destinationToken.underlyingAddress,
+ state.destinationToken.tokenType,
+ state.userIsSmartContractWallet,
+ state.userIsSafeWallet,
+ state.chainId,
+ state.orderType,
+ ]
+ );
+
+ const filterOutputAssets = (allowOwn = false) =>
+ state.destinationTokens.filter(
+ (token) =>
+ (allowOwn ||
+ // Filter out tokens that match the source token by addressToSwap OR underlyingAddress
+ // This prevents the same asset from appearing in both lists (e.g., USDT in CollateralSwap)
+ (token.addressToSwap.toLowerCase() !== state.sourceToken.addressToSwap.toLowerCase() &&
+ token.underlyingAddress.toLowerCase() !==
+ state.sourceToken.underlyingAddress.toLowerCase())) &&
+ // Avoid wrapping
+ !(
+ state.sourceToken.tokenType === TokenType.NATIVE &&
+ typeof state.chainId === 'number' &&
+ token.addressToSwap.toLowerCase() ===
+ WRAPPED_NATIVE_CURRENCIES[
+ state.chainId as keyof typeof WRAPPED_NATIVE_CURRENCIES
+ ]?.address.toLowerCase()
+ ) &&
+ !(
+ state.sourceToken.addressToSwap.toLowerCase() ===
+ WRAPPED_NATIVE_CURRENCIES[
+ state.chainId as keyof typeof WRAPPED_NATIVE_CURRENCIES
+ ]?.address.toLowerCase() && token.tokenType === TokenType.NATIVE
+ )
+ );
+
+ const outputAssets = useMemo(
+ () => filterOutputAssets(false),
+ [
+ state.destinationTokens,
+ state.sourceToken.addressToSwap,
+ state.sourceToken.underlyingAddress,
+ state.sourceToken.tokenType,
+ state.chainId,
+ state.orderType,
+ ]
+ );
+
+ const allowSwitchTokens = useMemo(() => {
+ const newInputAsset = state.destinationToken;
+ const newOutputAsset = state.sourceToken;
+
+ const newInputAssetExists = filterInputAssets(true, state.orderType).find(
+ (token) => token.addressToSwap.toLowerCase() === newInputAsset.addressToSwap.toLowerCase()
+ );
+ const newOutputAssetExists = filterOutputAssets(true).find(
+ (token) => token.addressToSwap.toLowerCase() === newOutputAsset.addressToSwap.toLowerCase()
+ );
+
+ return !!newInputAssetExists && !!newOutputAssetExists;
+ }, [state.sourceToken, state.destinationToken, state.orderType]);
+
+ // Hook to disable limits order based on specific assets conditions
+ useEffect(() => {
+ const inputsInLimitsOrder = filterInputAssets(true, OrderType.LIMIT);
+ const outputsInLimitsOrder = filterOutputAssets(true);
+
+ const canLimitSupportCurrentInputAsset = inputsInLimitsOrder.find(
+ (token) => token.addressToSwap.toLowerCase() === state.sourceToken.addressToSwap.toLowerCase()
+ );
+ const canLimitSupportCurrentOutputAsset = outputsInLimitsOrder.find(
+ (token) =>
+ token.addressToSwap.toLowerCase() === state.destinationToken.addressToSwap.toLowerCase()
+ );
+
+ const limitsOrderButtonBlocked =
+ !canLimitSupportCurrentInputAsset || !canLimitSupportCurrentOutputAsset;
+
+ setState({ limitsOrderButtonBlocked });
+ }, [state.sourceToken, state.destinationToken]);
+
+ const swapState: SwapInputState = {
+ handleSelectedInputToken,
+ handleSelectedOutputToken,
+ handleSelectedNetworkChange,
+ setSlippage,
+ showNetworkSelector,
+ inputAssets,
+ outputAssets,
+ handleInputChange,
+ handleOutputChange,
+ handleRateChange,
+ onSwitchReserves,
+ };
+
+ if (state.orderType === OrderType.MARKET) {
+ return (
+
+ );
+ } else if (state.orderType === OrderType.LIMIT) {
+ return (
+
+ );
+ }
+};
diff --git a/src/components/transactions/Swap/inputs/primitives/SwapAssetInput.tsx b/src/components/transactions/Swap/inputs/primitives/SwapAssetInput.tsx
new file mode 100644
index 0000000000..da26df4645
--- /dev/null
+++ b/src/components/transactions/Swap/inputs/primitives/SwapAssetInput.tsx
@@ -0,0 +1,877 @@
+import { valueToBigNumber } from '@aave/math-utils';
+import { isAddress } from '@ethersproject/address';
+import { formatUnits } from '@ethersproject/units';
+import { ExclamationIcon } from '@heroicons/react/outline';
+import { XCircleIcon } from '@heroicons/react/solid';
+import { Trans } from '@lingui/macro';
+import { ExpandMore } from '@mui/icons-material';
+import LaunchIcon from '@mui/icons-material/Launch';
+import {
+ Box,
+ Button,
+ CircularProgress,
+ IconButton,
+ InputBase,
+ MenuItem,
+ Popover,
+ SvgIcon,
+ ToggleButton,
+ ToggleButtonGroup,
+ Tooltip,
+ Typography,
+ useTheme,
+} from '@mui/material';
+import React, { useEffect, useRef, useState } from 'react';
+import NumberFormat, { NumberFormatProps } from 'react-number-format';
+import { MarketLogo } from 'src/components/MarketSwitcher';
+import { Link } from 'src/components/primitives/Link';
+import { textCenterEllipsis } from 'src/helpers/text-center-ellipsis';
+import { useRootStore } from 'src/store/root';
+import { useSharedDependencies } from 'src/ui-config/SharedDependenciesProvider';
+import { getNetworkConfig } from 'src/utils/marketsAndNetworksConfig';
+
+import { COMMON_SWAPS } from '../../../../../ui-config/TokenList';
+import { BasicModal } from '../../../../primitives/BasicModal';
+import { FormattedNumber } from '../../../../primitives/FormattedNumber';
+import { ExternalTokenIcon } from '../../../../primitives/TokenIcon';
+import { SearchInput } from '../../../../SearchInput';
+import { SwappableToken, SwapType, TokenType } from '../../types';
+
+interface CustomProps {
+ onChange: (event: { target: { name: string; value: string } }) => void;
+ name: string;
+ value: string;
+}
+
+export const NumberFormatCustom = React.forwardRef(
+ function NumberFormatCustom(props, ref) {
+ const { onChange, ...other } = props;
+
+ return (
+ {
+ if (values.value !== props.value)
+ onChange({
+ target: {
+ name: props.name,
+ value: values.value || '',
+ },
+ });
+ }}
+ thousandSeparator
+ isNumericString
+ allowNegative={false}
+ />
+ );
+ }
+);
+
+export interface AssetInputProps {
+ value: string;
+ usdValue: string;
+ chainId: number;
+ onChange?: (value: string) => void;
+ onClear?: () => void;
+ enableHover?: boolean;
+ disabled?: boolean;
+ disableInput?: boolean;
+ onSelect?: (asset: SwappableToken) => void;
+ assets: SwappableToken[];
+ maxValue?: string;
+ forcedMaxValue?: string;
+ loading?: boolean;
+ selectedAsset: SwappableToken;
+ balanceTitle?: string;
+ showBalance?: boolean;
+ allowCustomTokens?: boolean;
+ title?: string;
+ swapType: SwapType;
+ side: 'input' | 'output';
+}
+
+export const SwitchAssetInput = ({
+ value,
+ usdValue,
+ onChange,
+ onClear,
+ enableHover = false,
+ disabled,
+ disableInput,
+ onSelect,
+ assets,
+ maxValue,
+ forcedMaxValue,
+ loading = false,
+ chainId,
+ selectedAsset,
+ balanceTitle,
+ showBalance = true,
+ allowCustomTokens = true,
+ title,
+ swapType,
+ side,
+}: AssetInputProps) => {
+ const theme = useTheme();
+ const networkConfig = getNetworkConfig(chainId);
+ const networkName = networkConfig.displayName || networkConfig.name;
+ const getApyInfo = (asset: SwappableToken, swapType: SwapType, side: 'input' | 'output') => {
+ switch (swapType) {
+ case SwapType.RepayWithCollateral:
+ return side === 'input'
+ ? asset.variableBorrowAPY
+ ? { label: 'Borrow APY', value: Number(asset.variableBorrowAPY) }
+ : undefined
+ : asset.supplyAPY
+ ? { label: 'Supply APY', value: Number(asset.supplyAPY) }
+ : undefined;
+ case SwapType.WithdrawAndSwap:
+ return side === 'input' && asset.supplyAPY
+ ? { label: 'Supply APY', value: Number(asset.supplyAPY) }
+ : undefined;
+ case SwapType.CollateralSwap:
+ return asset.supplyAPY
+ ? { label: 'Supply APY', value: Number(asset.supplyAPY) }
+ : undefined;
+ case SwapType.DebtSwap:
+ return asset.variableBorrowAPY
+ ? { label: 'Borrow APY', value: Number(asset.variableBorrowAPY) }
+ : undefined;
+ case SwapType.Swap:
+ default:
+ return undefined;
+ }
+ };
+ const handleSelect = (asset: SwappableToken) => {
+ onSelect && onSelect(asset);
+ onChange && onChange('');
+ handleClose();
+ };
+
+ const { erc20Service } = useSharedDependencies();
+
+ const [openModal, setOpenModal] = useState(false);
+ const inputRef = useRef(null);
+ const [pickerHeight, setPickerHeight] = useState(undefined);
+
+ const handleClick = () => {
+ if (assets.length === 1) return;
+ setOpenModal(true);
+ };
+
+ const handleClose = () => {
+ setOpenModal(false);
+ handleCleanSearch();
+ };
+
+ // Match token picker height to the current swap modal paper height
+ useEffect(() => {
+ const paper = inputRef.current?.closest('.MuiPaper-root') as HTMLElement | null;
+ if (paper) {
+ setPickerHeight(paper.clientHeight);
+ }
+ }, [openModal]);
+
+ const [filteredAssets, setFilteredAssets] = useState(assets);
+ const [loadingNewAsset, setLoadingNewAsset] = useState(false);
+ const user = useRootStore((store) => store.account);
+
+ useEffect(() => {
+ setFilteredAssets(assets);
+ }, [assets]);
+
+ const popularAssets = assets.filter((asset) => COMMON_SWAPS.includes(asset.symbol));
+
+ const getRecentStorageKey = (swapType: SwapType, chainId: number, side: 'input' | 'output') =>
+ `aave_recent_tokens_${swapType}_${chainId}_${side}`;
+
+ const recentAddresses: string[] = (() => {
+ try {
+ const raw = localStorage.getItem(getRecentStorageKey(swapType, chainId, side));
+ return raw ? JSON.parse(raw) : [];
+ } catch {
+ return [];
+ }
+ })();
+
+ const recentAssets = recentAddresses
+ .map((addr) => assets.find((a) => a.addressToSwap.toLowerCase() === String(addr).toLowerCase()))
+ .filter(Boolean) as SwappableToken[];
+
+ const seen = new Set();
+ const mergedPopular = [...recentAssets, ...popularAssets].filter((asset) => {
+ const key = asset.addressToSwap.toLowerCase();
+ if (seen.has(key)) return false;
+ seen.add(key);
+ return true;
+ });
+
+ const popularSectionTitle = recentAssets.length > 0 ? 'Recently used & Popular' : 'Popular';
+ const handleSearchAssetChange = (value: string) => {
+ const searchQuery = value.trim().toLowerCase();
+ const matchingAssets = assets.filter(
+ (asset) =>
+ asset.symbol.toLowerCase().includes(searchQuery) ||
+ asset.name.toLowerCase().includes(searchQuery) ||
+ asset.addressToSwap.toLowerCase() === searchQuery
+ );
+ if (matchingAssets.length === 0) {
+ // If custom tokens are not allowed, do not attempt to import by address
+ if (!allowCustomTokens) {
+ setLoadingNewAsset(false);
+ setFilteredAssets([]);
+ return;
+ }
+
+ if (isAddress(value)) {
+ setLoadingNewAsset(true);
+ Promise.all([
+ erc20Service.getTokenInfo(value, chainId),
+ erc20Service.getBalance(value, user, chainId),
+ ])
+ .then(([tokenMetadata, userBalance]) => {
+ const tokenInfo = {
+ chainId: chainId,
+ balance: formatUnits(userBalance, tokenMetadata.decimals),
+ addressToSwap: tokenMetadata.address,
+ addressForUsdPrice: tokenMetadata.address,
+ underlyingAddress: tokenMetadata.address,
+ decimals: tokenMetadata.decimals,
+ symbol: tokenMetadata.symbol,
+ name: tokenMetadata.name,
+ tokenType: TokenType.USER_CUSTOM,
+ };
+ setFilteredAssets([tokenInfo]);
+ })
+ .catch(() => setFilteredAssets([]))
+ .finally(() => setLoadingNewAsset(false));
+ return;
+ }
+
+ setFilteredAssets([]);
+ } else {
+ setFilteredAssets(matchingAssets);
+ }
+ };
+
+ const handleCleanSearch = () => {
+ setFilteredAssets(assets);
+ setLoadingNewAsset(false);
+ };
+
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+ ({
+ border: `1px solid ${theme.palette.divider}`,
+ borderRadius: '6px',
+ overflow: 'hidden',
+ px: 3,
+ py: 2,
+ width: '100%',
+ ...(enableHover
+ ? {
+ transition: 'background-color 0.15s ease',
+ '&:hover': {
+ backgroundColor: 'background.surface',
+ },
+ }
+ : {}),
+ })}
+ >
+
+ {loading ? (
+
+
+
+ ) : (
+ {
+ if (!onChange) return;
+ if (Number(e.target.value) > Number(maxValue)) {
+ onChange('-1');
+ } else {
+ onChange(e.target.value);
+ }
+ }}
+ inputProps={{
+ 'aria-label': 'amount input',
+ style: {
+ width: '100%',
+ fontSize: '21px',
+ lineHeight: '28,01px',
+ padding: 0,
+ height: '28px',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ },
+ }}
+ // eslint-disable-next-line
+ inputComponent={NumberFormatCustom as any}
+ />
+ )}
+ {value !== '' && !disableInput && (
+ {
+ if (onClear) {
+ onClear();
+ } else {
+ onChange && onChange('');
+ }
+ }}
+ disabled={disabled}
+ >
+
+
+ )}
+
+
+ 3 ? 600 : 0
+ )}
+ >
+
+
+
+
+ Select token
+
+
+
+
+ {networkName}
+
+
+
+
+
+ 3 && mergedPopular.length > 0
+ ? `1px solid ${theme.palette.divider}`
+ : 'none',
+ position: 'sticky',
+ top: 0,
+ zIndex: 2,
+ mb: 3,
+ pb: 3,
+ backgroundColor: theme.palette.background.paper,
+ boxShadow: '0px 4px 6px -6px rgba(0, 0, 0, 0.1)',
+ marginTop: -3,
+ paddingTop: 3,
+ }}
+ >
+
+ {assets.length > 3 && (
+
+
+ {popularSectionTitle}
+
+ {mergedPopular.map((asset) => (
+ handleSelect(asset)}
+ >
+
+
+ {asset.symbol}
+
+
+ ))}
+
+ )}
+
+
+ {loadingNewAsset ? (
+
+
+
+ ) : filteredAssets.length > 0 ? (
+ filteredAssets.map((asset) => (
+
+ ))
+ ) : (
+
+ {allowCustomTokens ? (
+
+ No results found. You can import a custom token with a contract address
+
+ ) : (
+ No results found.
+ )}
+
+ )}
+
+
+
+
+
+
+ {loading ? (
+
+ ) : (
+
+ )}
+
+ {showBalance && selectedAsset.balance && (
+ <>
+
+ {balanceTitle || 'Balance'}
+
+
+ {!disableInput && (
+ {
+ const maxBase = forcedMaxValue || selectedAsset.balance || '0';
+ const next = valueToBigNumber(maxBase).multipliedBy(fraction).toString();
+ onChange && onChange(next);
+ }}
+ />
+ )}
+ {!disableInput && (
+
+ )}
+ >
+ )}
+
+
+
+ );
+};
+
+const PercentSelector = ({
+ disabled,
+ onSelectPercent,
+}: {
+ disabled?: boolean;
+ onSelectPercent: (fraction: number) => void;
+}) => {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const open = Boolean(anchorEl);
+
+ const handleOpen = (event: React.MouseEvent) => {
+ if (disabled) return;
+ setAnchorEl(event.currentTarget);
+ };
+ const handleClose = () => setAnchorEl(null);
+
+ const handlePick = (fraction: number) => {
+ onSelectPercent(fraction);
+ handleClose();
+ };
+
+ return (
+ <>
+
+
+
+ v && handlePick(v)}
+ >
+ {[0.25, 0.5, 0.75].map((fraction) => (
+
+
+ {Math.round(fraction * 100)}%
+
+
+ ))}
+
+
+
+ >
+ );
+};
diff --git a/src/components/transactions/Switch/ExpirySelector.tsx b/src/components/transactions/Swap/inputs/shared/ExpirySelector.tsx
similarity index 65%
rename from src/components/transactions/Switch/ExpirySelector.tsx
rename to src/components/transactions/Swap/inputs/shared/ExpirySelector.tsx
index 000cc46c15..feaef1dedc 100644
--- a/src/components/transactions/Switch/ExpirySelector.tsx
+++ b/src/components/transactions/Swap/inputs/shared/ExpirySelector.tsx
@@ -10,34 +10,28 @@ import {
Typography,
} from '@mui/material';
-const ONE_MINUTE_IN_SECONDS = 60;
-const ONE_HOUR_IN_SECONDS = 3600;
-const ONE_DAY_IN_SECONDS = 86400;
-const ONE_MONTH_IN_SECONDS = 2592000;
-
-export const Expiry: { [key: string]: number } = {
- 'Five minutes': ONE_MINUTE_IN_SECONDS * 5,
- 'Half hour': ONE_HOUR_IN_SECONDS / 2,
- 'One hour': ONE_HOUR_IN_SECONDS,
- 'One day': ONE_DAY_IN_SECONDS,
- 'One week': 7 * ONE_DAY_IN_SECONDS,
- 'One month': ONE_MONTH_IN_SECONDS,
- 'Three months': 3 * ONE_MONTH_IN_SECONDS,
- 'One year': 12 * ONE_MONTH_IN_SECONDS,
-};
+import { Expiry } from '../../types';
interface ExpirySelectorProps {
- selectedExpiry: number;
- setSelectedExpiry: (value: number) => void;
+ selectedExpiry: Expiry;
+ setSelectedExpiry: (value: Expiry) => void;
}
export const ExpirySelector = ({ selectedExpiry, setSelectedExpiry }: ExpirySelectorProps) => {
const handleChange = (event: SelectChangeEvent) => {
- setSelectedExpiry(Number(event.target.value));
+ setSelectedExpiry(event.target.value as unknown as Expiry);
};
return (
- Expires in
+
+
+ Expires in
+
+
diff --git a/src/components/transactions/Swap/inputs/shared/PriceInput.tsx b/src/components/transactions/Swap/inputs/shared/PriceInput.tsx
new file mode 100644
index 0000000000..58643859be
--- /dev/null
+++ b/src/components/transactions/Swap/inputs/shared/PriceInput.tsx
@@ -0,0 +1,420 @@
+import { BigNumberValue, valueToBigNumber } from '@aave/math-utils';
+import { ExclamationIcon, RefreshIcon } from '@heroicons/react/outline';
+import { Box, Button, CircularProgress, InputBase, SvgIcon, Typography } from '@mui/material';
+import { BigNumber } from 'bignumber.js';
+import React, { useEffect, useRef, useState } from 'react';
+import NumberFormat, { NumberFormatProps } from 'react-number-format';
+import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
+import { ExternalTokenIcon } from 'src/components/primitives/TokenIcon';
+
+import { SwappableToken, TokenType } from '../../types';
+
+interface CustomProps {
+ onChange: (event: { target: { name: string; value: string } }) => void;
+ name: string;
+ value: string;
+}
+
+export const NumberFormatCustom = React.forwardRef(
+ function NumberFormatCustom(props, ref) {
+ const { onChange, ...other } = props;
+
+ return (
+ {
+ if (values.value !== props.value)
+ onChange({
+ target: {
+ name: props.name,
+ value: values.value || '',
+ },
+ });
+ }}
+ thousandSeparator
+ // isNumericString
+ allowNegative={false}
+ />
+ );
+ }
+);
+
+export interface AssetInputProps {
+ loading?: boolean;
+ originAssetAmount: string;
+ targetAssetAmount: string;
+ originAssetAmountUSD: string;
+ targetAssetAmountUSD: string;
+ originAsset: SwappableToken;
+ targetAsset: SwappableToken;
+ disabled?: boolean;
+ handleRateChange: (rateFromAsset: SwappableToken, newRate: BigNumberValue) => void;
+}
+
+export const PriceInput = ({
+ originAssetAmount,
+ targetAssetAmount,
+ originAssetAmountUSD,
+ targetAssetAmountUSD,
+ loading = false,
+ originAsset,
+ targetAsset,
+ disabled = false,
+ handleRateChange: handleInputsAmountsChange,
+}: AssetInputProps) => {
+ const DEBOUNCE_MS = 300;
+ const inputRef = useRef(null);
+ const [fromAsset, setFromAsset] = useState(originAsset);
+ const [toAsset, setToAsset] = useState(targetAsset);
+ const [rate, setRate] = useState<{
+ nominal?: BigNumber;
+ usd?: BigNumber;
+ }>({
+ nominal: undefined,
+ usd: undefined,
+ });
+
+ const [lastMarketRate, setLastMarketRate] = useState<{
+ nominal?: BigNumber;
+ usd?: BigNumber;
+ baseSymbol?: string;
+ quoteSymbol?: string;
+ }>({
+ nominal: undefined,
+ usd: undefined,
+ baseSymbol: undefined,
+ quoteSymbol: undefined,
+ });
+ const rateDebounceRef = useRef(undefined);
+
+ const [amount, setAmount] = useState<{
+ originAmount?: BigNumber;
+ targetAmount?: BigNumber;
+ originAmountUsd?: BigNumber;
+ targetAmountUsd?: BigNumber;
+ }>({
+ originAmount: undefined,
+ targetAmount: undefined,
+ originAmountUsd: undefined,
+ targetAmountUsd: undefined,
+ });
+
+ useEffect(() => {
+ if (!originAssetAmount || !targetAssetAmount) return;
+
+ setAmount({
+ originAmount: valueToBigNumber(originAssetAmount),
+ targetAmount: valueToBigNumber(targetAssetAmount),
+ originAmountUsd: valueToBigNumber(originAssetAmountUSD),
+ targetAmountUsd: valueToBigNumber(targetAssetAmountUSD),
+ });
+ }, [originAssetAmount, targetAssetAmount, originAssetAmountUSD, targetAssetAmountUSD]);
+
+ useEffect(() => {
+ if (
+ !amount.originAmount?.gt(0) ||
+ !amount.targetAmount?.gt(0) ||
+ !amount.originAmountUsd?.gt(0) ||
+ !amount.targetAmountUsd?.gt(0)
+ )
+ return;
+
+ // Define rate in the direction currently displayed:
+ // "When 1 {fromAsset} is worth ..." so nominal shows quote units per 1 base (fromAsset).
+ const showingOriginAsBase = fromAsset.addressToSwap === originAsset.addressToSwap;
+ const nextNominal = showingOriginAsBase
+ ? amount.targetAmount && amount.originAmount
+ ? amount.targetAmount.div(amount.originAmount)
+ : undefined
+ : amount.originAmount && amount.targetAmount
+ ? amount.originAmount.div(amount.targetAmount)
+ : undefined;
+
+ const nextUsd = showingOriginAsBase
+ ? amount.targetAmountUsd && amount.originAmountUsd
+ ? amount.targetAmountUsd.div(amount.originAmountUsd)
+ : undefined
+ : amount.originAmountUsd && amount.targetAmountUsd
+ ? amount.originAmountUsd.div(amount.targetAmountUsd)
+ : undefined;
+
+ setRate({
+ nominal: nextNominal,
+ usd: nextUsd,
+ });
+
+ // Capture latest market rate from the most recent quote only once
+ if (lastMarketRate.nominal === undefined) {
+ setLastMarketRate({
+ nominal: nextNominal,
+ usd: nextUsd,
+ baseSymbol: fromAsset.symbol,
+ quoteSymbol: toAsset.symbol,
+ });
+ }
+ }, [
+ amount.originAmount,
+ amount.targetAmount,
+ amount.originAmountUsd,
+ amount.targetAmountUsd,
+ fromAsset.addressToSwap,
+ originAsset.addressToSwap,
+ ]);
+
+ useEffect(() => {
+ setFromAsset(originAsset);
+ setToAsset(targetAsset);
+ setRate({
+ nominal: undefined,
+ usd: undefined,
+ });
+ setLastMarketRate({
+ nominal: undefined,
+ usd: undefined,
+ });
+ setAmount({
+ originAmount: undefined,
+ targetAmount: undefined,
+ originAmountUsd: undefined,
+ targetAmountUsd: undefined,
+ });
+ }, [originAsset, targetAsset]);
+
+ const setNewRate = (newRate: BigNumberValue) => {
+ const nextNominal = valueToBigNumber(newRate);
+ // Prefer using existing usd-to-nominal ratio K = rate.usd / rate.nominal
+ let kRatio: BigNumber | undefined = undefined;
+ if (rate.nominal && !rate.nominal.isZero() && rate.usd) {
+ kRatio = rate.usd.div(rate.nominal);
+ } else if (
+ amount.targetAmountUsd &&
+ amount.originAmountUsd &&
+ amount.targetAmount &&
+ amount.originAmount &&
+ !amount.targetAmount.isZero() &&
+ !amount.originAmount.isZero()
+ ) {
+ // Fallback: K = (toUSD/to) / (fromUSD/from) = (toUSD * from) / (fromUSD * to)
+ kRatio = amount.targetAmountUsd
+ .times(amount.originAmount)
+ .div(amount.originAmountUsd.times(amount.targetAmount));
+ }
+
+ const nextUsd = kRatio ? nextNominal.times(kRatio) : valueToBigNumber(0);
+
+ setRate({
+ nominal: nextNominal,
+ usd: nextUsd,
+ });
+ };
+
+ // Debounced emitter to upstream handler
+ const emitRateChangeDebounced = (baseToken: SwappableToken, newRate: BigNumber) => {
+ if (rateDebounceRef.current !== undefined) {
+ window.clearTimeout(rateDebounceRef.current);
+ }
+ rateDebounceRef.current = window.setTimeout(() => {
+ handleInputsAmountsChange(baseToken, newRate);
+ setNewRate(newRate);
+ }, DEBOUNCE_MS) as unknown as number;
+ };
+
+ // Cleanup on unmount
+ useEffect(() => {
+ return () => {
+ if (rateDebounceRef.current !== undefined) {
+ window.clearTimeout(rateDebounceRef.current);
+ }
+ };
+ }, []);
+
+ const handleSwitchRateDirection = () => {
+ // Also invert the stored market reference rate so the Market button uses the correct direction
+ setLastMarketRate((prev) => {
+ const invNominal =
+ prev.nominal && !prev.nominal.isZero()
+ ? valueToBigNumber(1).div(prev.nominal)
+ : prev.nominal;
+ const invUsd = prev.usd && !prev.usd.isZero() ? valueToBigNumber(1).div(prev.usd) : prev.usd;
+ return {
+ nominal: invNominal,
+ usd: invUsd,
+ baseSymbol: prev.quoteSymbol ?? toAssetAux.symbol,
+ quoteSymbol: prev.baseSymbol ?? fromAssetAux.symbol,
+ };
+ });
+
+ setRate({
+ nominal: rate.nominal ? valueToBigNumber(1).div(rate.nominal) : undefined,
+ usd: rate.usd ? valueToBigNumber(1).div(rate.usd) : undefined,
+ });
+
+ const fromAssetAux = fromAsset;
+ const toAssetAux = toAsset;
+ setFromAsset(toAssetAux);
+ setToAsset(fromAssetAux);
+ };
+
+ return (
+ ({
+ border: `1px solid ${theme.palette.divider}`,
+ borderRadius: '6px',
+ overflow: 'hidden',
+ px: 3,
+ py: 2,
+ width: '100%',
+ transition: 'background-color 0.15s ease',
+ '&:hover': {
+ backgroundColor: 'background.surface',
+ },
+ })}
+ >
+
+ When 1 {fromAsset.symbol} is worth:
+
+
+ {loading ? (
+
+
+
+ ) : (
+ {
+ const typed = e.target.value;
+ const bn = valueToBigNumber(typed);
+ setNewRate(bn);
+ emitRateChangeDebounced(fromAsset, bn);
+ }}
+ />
+ )}
+
+
+
+
+
+ {loading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/transactions/Swap/inputs/shared/QuoteProgressRing.tsx b/src/components/transactions/Swap/inputs/shared/QuoteProgressRing.tsx
new file mode 100644
index 0000000000..cda959fde6
--- /dev/null
+++ b/src/components/transactions/Swap/inputs/shared/QuoteProgressRing.tsx
@@ -0,0 +1,78 @@
+import { Box, CircularProgress, SxProps } from '@mui/material';
+import { alpha, useTheme } from '@mui/material/styles';
+import { useEffect, useMemo, useState } from 'react';
+
+type QuoteProgressRingProps = {
+ active: boolean;
+ lastUpdatedAt: number | null;
+ intervalMs: number;
+ size?: number | string;
+ thickness?: number;
+ paused?: boolean;
+ sx?: SxProps;
+};
+
+export const QuoteProgressRing = ({
+ active,
+ lastUpdatedAt,
+ intervalMs,
+ size = 44,
+ thickness = 2,
+ paused = false,
+ sx,
+}: QuoteProgressRingProps) => {
+ const theme = useTheme();
+ const [now, setNow] = useState(Date.now());
+
+ useEffect(() => {
+ if (!active || !lastUpdatedAt || intervalMs <= 0 || paused) return;
+ const id = setInterval(() => setNow(Date.now()), 100);
+ return () => clearInterval(id);
+ }, [active, lastUpdatedAt, intervalMs, paused]);
+
+ const progress = useMemo(() => {
+ if (!active || !lastUpdatedAt || intervalMs <= 0) return 0;
+ const elapsed = Math.max(0, now - lastUpdatedAt);
+ const ratio = Math.max(0, Math.min(1, elapsed / intervalMs));
+ return ratio * 100;
+ }, [active, lastUpdatedAt, intervalMs, now]);
+
+ const ringColor = useMemo(() => {
+ // Opacity from 0.25 to 1.0 based on progress
+ const ratio = Math.max(0, Math.min(1, progress / 100));
+ const opacity = 0.25 + 0.75 * ratio;
+ return alpha(theme.palette.primary.main, opacity);
+ }, [progress, theme]);
+
+ if (!active || !lastUpdatedAt || intervalMs <= 0) return null;
+
+ return (
+
+
+
+ );
+};
diff --git a/src/components/transactions/Switch/SwitchRates.tsx b/src/components/transactions/Swap/inputs/shared/SwitchRates.tsx
similarity index 75%
rename from src/components/transactions/Switch/SwitchRates.tsx
rename to src/components/transactions/Swap/inputs/shared/SwitchRates.tsx
index c63882bd15..9de8c9fe20 100644
--- a/src/components/transactions/Switch/SwitchRates.tsx
+++ b/src/components/transactions/Swap/inputs/shared/SwitchRates.tsx
@@ -2,14 +2,14 @@ import { normalizeBN, valueToBigNumber } from '@aave/math-utils';
import { SwitchHorizontalIcon } from '@heroicons/react/outline';
import { Trans } from '@lingui/macro';
import { Box, ButtonBase, SvgIcon, Typography } from '@mui/material';
-import { useMemo, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { DarkTooltip } from 'src/components/infoTooltips/DarkTooltip';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
-import { SwitchRatesType } from './switch.types';
+import { SwapQuoteType } from '../../types';
type SwitchRatesProps = {
- rates: SwitchRatesType;
+ rates: SwapQuoteType;
srcSymbol: string;
destSymbol: string;
showPriceImpact?: boolean;
@@ -23,17 +23,28 @@ export const SwitchRates = ({
}: SwitchRatesProps) => {
const [isSwitched, setIsSwitched] = useState(false);
+ // Reset switch state when rates change
+ useEffect(() => {
+ setIsSwitched(false);
+ }, [rates.srcSpotAmount, rates.destSpotAmount, srcSymbol, destSymbol]);
+
const rate = useMemo(() => {
- const amount1 = normalizeBN(rates.srcAmount, rates.srcDecimals);
- const amount2 = normalizeBN(rates.destAmount, rates.destDecimals);
+ const amount1 = normalizeBN(rates.srcSpotAmount, rates.srcDecimals);
+ const amount2 = normalizeBN(rates.destSpotAmount, rates.destDecimals);
return isSwitched ? amount1.div(amount2) : amount2.div(amount1);
- }, [isSwitched, rates.srcAmount, rates.destAmount]);
+ }, [
+ isSwitched,
+ rates.srcSpotAmount,
+ rates.srcDecimals,
+ rates.destSpotAmount,
+ rates.destDecimals,
+ ]);
const priceImpact = useMemo(() => {
- const price1 = valueToBigNumber(rates.srcUSD);
- const price2 = valueToBigNumber(rates.destUSD);
+ const price1 = valueToBigNumber(rates.srcSpotUSD);
+ const price2 = valueToBigNumber(rates.destSpotUSD);
return price2.minus(price1).div(price1);
- }, [rates.srcUSD, rates.destUSD]);
+ }, [rates.srcSpotUSD, rates.destSpotUSD]);
return (
diff --git a/src/components/transactions/Switch/SwitchSlippageSelector.tsx b/src/components/transactions/Swap/inputs/shared/SwitchSlippageSelector.tsx
similarity index 88%
rename from src/components/transactions/Switch/SwitchSlippageSelector.tsx
rename to src/components/transactions/Swap/inputs/shared/SwitchSlippageSelector.tsx
index ab305fa0cb..30799d5610 100644
--- a/src/components/transactions/Switch/SwitchSlippageSelector.tsx
+++ b/src/components/transactions/Swap/inputs/shared/SwitchSlippageSelector.tsx
@@ -15,7 +15,7 @@ import { MouseEvent, useEffect, useState } from 'react';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
import { Warning } from 'src/components/primitives/Warning';
-import { ValidationData } from './validation.helpers';
+import { ValidationData } from '../../helpers/shared/slippage.helpers';
type SwitchSlippageSelectorProps = {
suggestedSlippage?: string;
@@ -90,14 +90,7 @@ export const SwitchSlippageSelector = ({
setSlippage(suggestedSlippage);
setPreviousSlippage(slippage);
}
- }, [
- slippage,
- suggestedSlippage,
- userHasSetCustomSlippage,
- isCustomSlippage,
- previousSlippage,
- setSlippage,
- ]);
+ }, [slippage, suggestedSlippage, userHasSetCustomSlippage, isCustomSlippage, previousSlippage]);
const handleOpen = (event: MouseEvent) => {
setAnchorEl(event.currentTarget);
@@ -130,7 +123,7 @@ export const SwitchSlippageSelector = ({
return (
-
+
{isCustomSlippage ? (
Custom slippage
) : provider === 'paraswap' ? (
@@ -238,21 +231,33 @@ export const SwitchSlippageSelector = ({
)}
-
diff --git a/src/components/transactions/Swap/modals/CollateralSwapModal.tsx b/src/components/transactions/Swap/modals/CollateralSwapModal.tsx
new file mode 100644
index 0000000000..0c22e23c07
--- /dev/null
+++ b/src/components/transactions/Swap/modals/CollateralSwapModal.tsx
@@ -0,0 +1,36 @@
+import { Trans } from '@lingui/macro';
+import { Box, Typography } from '@mui/material';
+import { BasicModal } from 'src/components/primitives/BasicModal';
+import { ConnectWalletButton } from 'src/components/WalletConnection/ConnectWalletButton';
+import { ModalContextType, ModalType, useModalContext } from 'src/hooks/useModal';
+import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
+
+import { CollateralSwapModalContent } from './request/CollateralSwapModalContent';
+
+export const CollateralSwapModal = () => {
+ const { currentAccount } = useWeb3Context();
+ const {
+ args: { underlyingAsset },
+ type,
+ close,
+ } = useModalContext() as ModalContextType<{
+ underlyingAsset: string;
+ }>;
+
+ return (
+
+ {currentAccount ? (
+ <>
+
+ >
+ ) : (
+
+
+ Please connect your wallet to swap collateral.
+
+ close()} />
+
+ )}
+
+ );
+};
diff --git a/src/components/transactions/Swap/modals/DebtSwapModal.tsx b/src/components/transactions/Swap/modals/DebtSwapModal.tsx
new file mode 100644
index 0000000000..c684129b4d
--- /dev/null
+++ b/src/components/transactions/Swap/modals/DebtSwapModal.tsx
@@ -0,0 +1,36 @@
+import { Trans } from '@lingui/macro';
+import { Box, Typography } from '@mui/material';
+import { BasicModal } from 'src/components/primitives/BasicModal';
+import { ConnectWalletButton } from 'src/components/WalletConnection/ConnectWalletButton';
+import { ModalContextType, ModalType, useModalContext } from 'src/hooks/useModal';
+import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
+
+import { DebtSwapModalContent } from './request/DebtSwapModalContent';
+
+export const DebtSwapModal = () => {
+ const {
+ type,
+ close,
+ args: { underlyingAsset },
+ } = useModalContext() as ModalContextType<{
+ underlyingAsset: string;
+ }>;
+ const { currentAccount } = useWeb3Context();
+
+ return (
+
+ {currentAccount ? (
+ <>
+
+ >
+ ) : (
+
+
+ Please connect your wallet to swap debt.
+
+ close()} />
+
+ )}
+
+ );
+};
diff --git a/src/components/transactions/Switch/SwitchModal.tsx b/src/components/transactions/Swap/modals/SwapModal.tsx
similarity index 51%
rename from src/components/transactions/Switch/SwitchModal.tsx
rename to src/components/transactions/Swap/modals/SwapModal.tsx
index aa33d0ec23..b17fe507f2 100644
--- a/src/components/transactions/Switch/SwitchModal.tsx
+++ b/src/components/transactions/Swap/modals/SwapModal.tsx
@@ -1,28 +1,25 @@
import { Trans } from '@lingui/macro';
import { Box, Typography } from '@mui/material';
-import { useState } from 'react';
import { BasicModal } from 'src/components/primitives/BasicModal';
import { ConnectWalletButton } from 'src/components/WalletConnection/ConnectWalletButton';
import { ModalType, useModalContext } from 'src/hooks/useModal';
import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
-import { TxModalTitle } from '../FlowCommons/TxModalTitle';
-import { BaseSwitchModal } from './BaseSwitchModal';
-import { SwitchLimitOrdersModalContent } from './SwitchLimitOrdersModalContent';
-import { SwitchType, SwitchTypeSelector } from './SwitchTypeSelector';
+import { SwapModalContent } from './request/SwapModalContent';
-export const SwitchModal = () => {
- const { type, close } = useModalContext();
- const [switchType, setSwitchType] = useState(SwitchType.MARKET);
+export const SwapModal = () => {
+ const {
+ type,
+ close,
+ args: { underlyingAsset, chainId },
+ } = useModalContext();
const { currentAccount } = useWeb3Context();
+
return (
-
-
+
{currentAccount ? (
<>
-
- {switchType === SwitchType.MARKET && }
- {switchType === SwitchType.LIMIT && }
+
>
) : (
diff --git a/src/components/transactions/Swap/modals/request/BaseSwapModalContent.tsx b/src/components/transactions/Swap/modals/request/BaseSwapModalContent.tsx
new file mode 100644
index 0000000000..cc91497815
--- /dev/null
+++ b/src/components/transactions/Swap/modals/request/BaseSwapModalContent.tsx
@@ -0,0 +1,236 @@
+import { CircularProgress } from '@mui/material';
+import { useEffect, useReducer } from 'react';
+import { useModalContext } from 'src/hooks/useModal';
+
+import { BaseSwapActions } from '../../actions';
+import { SwapInputChanges } from '../../analytics/constants';
+import { useHandleAnalytics } from '../../analytics/useTrackAnalytics';
+import { BaseSwapDetails } from '../../details';
+import { SwapErrors } from '../../errors/SwapErrors';
+import { useFlowSelector } from '../../hooks/useFlowSelector';
+import { useMaxNativeAmount } from '../../hooks/useMaxNativeAmount';
+import { useProtocolReserves } from '../../hooks/useProtocolReserves';
+import { useSlippageSelector } from '../../hooks/useSlippageSelector';
+import { useSwapOrderAmounts } from '../../hooks/useSwapOrderAmounts';
+import { useSwapQuote } from '../../hooks/useSwapQuote';
+import { useUserContext } from '../../hooks/useUserContext';
+import { SwapInputs } from '../../inputs/SwapInputs';
+import { OrderTypeSelector } from '../../shared/OrderTypeSelector';
+import { SwapModalTitle } from '../../shared/SwapModalTitle';
+import {
+ ActionsBlockedReason,
+ Expiry,
+ OrderType,
+ SwapDefaultParams,
+ swapDefaultState,
+ SwapParams,
+ SwapProvider,
+ SwapState,
+ swapStateFromParamsOrDefault,
+ SwapType,
+} from '../../types';
+import { SwapPostInputWarnings } from '../../warnings/SwapPostInputWarnings';
+import { SwapPreInputWarnings } from '../../warnings/SwapPreInputWarnings';
+import { SwapResultView } from '../result/SwapResultView';
+import { NoEligibleAssetsToSwap } from './NoEligibleAssetsToSwap';
+
+/**
+ * Core composition root for all Swap modals.
+ *
+ * Responsibilities:
+ * - Build immutable `params` from defaults + overrides
+ * - Initialize and own `SwapState`; expose a guarded `setState` that avoids no-op updates
+ * - Wire all domain hooks (user context, max native amount, slippage validation, flow selection (HF/flashloan), quotes, protocol reserves, processed amounts, analytics)
+ * - Gate details, warnings, errors and actions until the flow is selected (prevents flicker while HF/flashloan decision is pending for protocol flows)
+ * - Render fallback views when tokens are not available/loaded
+ */
+export const BaseSwapModalContent = ({
+ params: predefinedParams,
+}: {
+ params: Partial;
+}) => {
+ const params = {
+ ...SwapDefaultParams,
+ ...predefinedParams,
+ } as SwapParams;
+
+ // Shared core state for all the components
+ const [state, setStateBase] = useReducer(
+ (state: SwapState, action: Partial): SwapState => {
+ // Deep merge actionsBlocked to prevent overwrites when multiple warnings update simultaneously
+ const newState = { ...state, ...action } as SwapState;
+ if (action.actionsBlocked !== undefined) {
+ // Merge actionsBlocked: spread existing, then apply updates (undefined values delete keys)
+ const mergedActionsBlocked = { ...state.actionsBlocked };
+ Object.entries(action.actionsBlocked).forEach(([key, value]) => {
+ if (value === undefined || value === false) {
+ delete mergedActionsBlocked[key as ActionsBlockedReason];
+ } else {
+ mergedActionsBlocked[key as ActionsBlockedReason] = value;
+ }
+ });
+ newState.actionsBlocked = mergedActionsBlocked;
+ }
+ return newState;
+ },
+ swapStateFromParamsOrDefault(params, swapDefaultState)
+ );
+
+ // Wrapped setter that avoids re-render churn when no fields change
+ const setState = (action: Partial) => {
+ const hasChange = Object.entries(action).some(
+ ([key, value]) => !Object.is(state[key as keyof SwapState], value)
+ );
+
+ if (!hasChange) {
+ // optional: debug log
+ console.debug(
+ '%c[BaseSwapModalContent] setState skipped (no changes). Check renderings for %o',
+ 'color: #b0b0b0;',
+ action
+ );
+ return;
+ }
+
+ console.debug(
+ '%c[BaseSwapModalContent] setState called. Fields changed: %o Values: %o',
+ 'color: #b0b0b0;',
+ Object.keys(action),
+ action
+ );
+
+ setStateBase(action);
+ };
+
+ // Load specific states via hooks
+ const { mainTxState } = useModalContext();
+ useEffect(() => {
+ setState({ mainTxState });
+ }, [mainTxState]);
+ const trackingHandlers = useHandleAnalytics({ state });
+ useUserContext({ setState });
+ useMaxNativeAmount({ params, state, setState });
+ useSlippageSelector({ params, state, setState });
+ useFlowSelector({ params, state, setState });
+ useSwapQuote({ params, state, setState, trackingHandlers });
+ useProtocolReserves({ params, state, setState });
+ useSwapOrderAmounts({ params, state, setState });
+
+ // Fallback views
+ if (!state.sourceTokens.length || !state.destinationTokens.length) {
+ return ;
+ }
+
+ if (!state.sourceToken || !state.destinationToken) {
+ return ;
+ }
+
+ // Order result view
+ if (mainTxState.success) {
+ return ;
+ }
+
+ return (
+ <>
+ {params.showTitle && }
+
+ {params.allowLimitOrders &&
+ state.provider === SwapProvider.COW_PROTOCOL &&
+ (state.isSwapFlowSelected || state.swapType !== SwapType.CollateralSwap) && (
+ {
+ const switchingFromLimitToMarket =
+ state.orderType === OrderType.LIMIT && orderType === OrderType.MARKET;
+ const switchingFromMarketToLimit =
+ state.orderType === OrderType.MARKET && orderType === OrderType.LIMIT;
+
+ setState({
+ orderType,
+ actionsLoading: false,
+ ...(switchingFromLimitToMarket || switchingFromMarketToLimit
+ ? {
+ inputAmount: '',
+ debouncedInputAmount: '',
+ inputAmountUSD: '',
+ outputAmount: '',
+ debouncedOutputAmount: '',
+ outputAmountUSD: '',
+ swapRate: undefined,
+ error: undefined,
+ isLiquidatable: false,
+ warnings: [],
+ quoteRefreshPaused: false,
+ quoteLastUpdatedAt: undefined,
+ quoteTimerPausedAt: null,
+ expiry: Expiry.TEN_MINUTES,
+ quoteTimerPausedAccumMs: 0,
+ }
+ : {}),
+ });
+ trackingHandlers.trackInputChange(SwapInputChanges.ORDER_TYPE, orderType.toString());
+ }}
+ />
+ )}
+
+
+
+
+
+ {/* Show provider and validation errors early when flow is not yet selected */}
+ {!state.isSwapFlowSelected && (
+
+ )}
+
+ {/*
+ Show details, warnings, and actions only if the swap flow is selected.
+ This is particularly useful for Adapers where the swap may be via flashloan or not.
+ */}
+ {state.isSwapFlowSelected && (
+ <>
+
+
+
+
+ {/* Keep original ordering: errors appear after post-input warnings, before actions */}
+
+
+
+ >
+ )}
+ >
+ );
+};
diff --git a/src/components/transactions/Swap/modals/request/CollateralSwapModalContent.tsx b/src/components/transactions/Swap/modals/request/CollateralSwapModalContent.tsx
new file mode 100644
index 0000000000..60b194743f
--- /dev/null
+++ b/src/components/transactions/Swap/modals/request/CollateralSwapModalContent.tsx
@@ -0,0 +1,267 @@
+import { SupportedChainId, WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/cow-sdk';
+import { useQueryClient } from '@tanstack/react-query';
+import {
+ ComputedReserveData,
+ useAppDataContext,
+} from 'src/hooks/app-data-provider/useAppDataProvider';
+import { TokenInfoWithBalance } from 'src/hooks/generic/useTokensBalance';
+import { ExtendedFormattedUser } from 'src/hooks/pool/useExtendedUserSummaryAndIncentives';
+import { useRootStore } from 'src/store/root';
+import { TOKEN_LIST, TokenInfo } from 'src/ui-config/TokenList';
+import { displayGhoForMintableMarket } from 'src/utils/ghoUtilities';
+import { useShallow } from 'zustand/shallow';
+
+import { invalidateAppStateForSwap } from '../../helpers/shared';
+import { SwappableToken, SwapParams, SwapType, TokenType } from '../../types';
+import { BaseSwapModalContent } from './BaseSwapModalContent';
+
+export const CollateralSwapModalContent = ({ underlyingAsset }: { underlyingAsset: string }) => {
+ const { user, reserves } = useAppDataContext();
+ const [account, chainId, currentMarketName] = useRootStore(
+ useShallow((store) => [store.account, store.currentChainId, store.currentMarket])
+ );
+ const queryClient = useQueryClient();
+ const currentNetworkConfig = useRootStore((store) => store.currentNetworkConfig);
+ const baseTokens: TokenInfo[] = reserves.map((reserve) => {
+ return {
+ address: reserve.underlyingAsset,
+ symbol: reserve.symbol,
+ logoURI: `/icons/tokens/${reserve.iconSymbol.toLowerCase()}.svg`,
+ chainId: currentNetworkConfig.wagmiChain.id,
+ name: reserve.name,
+ decimals: reserve.decimals,
+ };
+ });
+
+ const tokensFrom = getTokensFrom(user, baseTokens, chainId);
+ const tokensTo = getTokensTo(user, reserves, baseTokens, currentMarketName, chainId);
+
+ const userSelectedInputToken = tokensFrom.find(
+ (token) => token.underlyingAddress.toLowerCase() === underlyingAsset?.toLowerCase()
+ );
+ const defaultInputToken =
+ userSelectedInputToken ?? (tokensFrom.length > 0 ? tokensFrom[0] : undefined);
+
+ const defaultOutputToken = getDefaultOutputToken(tokensTo, defaultInputToken);
+
+ const invalidateAppState = () => {
+ invalidateAppStateForSwap({
+ swapType: SwapType.CollateralSwap,
+ chainId,
+ account,
+ queryClient,
+ });
+ };
+
+ const initialSourceUserReserve = user?.userReservesData.find(
+ (userReserve) =>
+ userReserve.underlyingAsset.toLowerCase() ===
+ defaultInputToken?.underlyingAddress.toLowerCase()
+ );
+ const initialTargetUserReserve = user?.userReservesData.find(
+ (userReserve) =>
+ userReserve.underlyingAsset.toLowerCase() ===
+ defaultOutputToken?.underlyingAddress.toLowerCase()
+ );
+
+ const params: Partial = {
+ swapType: SwapType.CollateralSwap,
+ allowLimitOrders: true,
+ forcedInputToken: defaultInputToken,
+ suggestedDefaultOutputToken: defaultOutputToken,
+ invalidateAppState,
+ sourceTokens: tokensFrom,
+ destinationTokens: tokensTo,
+ showSwitchInputAndOutputAssetsButton: false,
+ chainId: currentNetworkConfig.wagmiChain.id,
+ titleTokenPostfix: 'supply',
+ sourceReserve: initialSourceUserReserve,
+ destinationReserve: initialTargetUserReserve,
+ resultScreenTokensFromTitle: 'Collateral sent',
+ resultScreenTokensToTitle: 'Collateral received',
+ resultScreenTitleItems: 'collateral',
+
+ // Note: Collateral Swap order is not inverted
+ inputInputTitleSell: 'Swap',
+ outputInputTitleSell: 'Receive at most',
+ inputInputTitleBuy: 'Swap at most',
+ outputInputTitleBuy: 'Receive',
+ };
+
+ return ;
+};
+
+const getDefaultOutputToken = (
+ tokensTo: SwappableToken[],
+ defaultInputToken: SwappableToken | undefined
+) => {
+ const tokensWithoutInputToken = tokensTo.filter(
+ (token) =>
+ // Filter out tokens that match by addressToSwap OR underlyingAddress OR symbol
+ // This prevents the same asset from appearing in both lists (triple check for robustness)
+ token.addressToSwap.toLowerCase() !== defaultInputToken?.addressToSwap.toLowerCase() &&
+ token.underlyingAddress.toLowerCase() !==
+ defaultInputToken?.underlyingAddress.toLowerCase() &&
+ token.symbol !== defaultInputToken?.symbol
+ );
+
+ // 1. Highest balance
+ const highestBalanceToken = tokensWithoutInputToken
+ .filter((token) => token.balance !== '0')
+ .sort((a, b) => Number(b.balance) - Number(a.balance));
+ if (highestBalanceToken.length > 0) {
+ return highestBalanceToken[0];
+ }
+
+ // 2. USDT or USDC or AAVE (but not the input token)
+ const usdtOrUsdcOrAaveToken = tokensWithoutInputToken.filter(
+ (token) =>
+ (token.symbol === 'USDT' || token.symbol === 'USDC' || token.symbol === 'AAVE') &&
+ token.symbol !== defaultInputToken?.symbol
+ );
+ if (usdtOrUsdcOrAaveToken.length > 0) {
+ return usdtOrUsdcOrAaveToken[0];
+ }
+
+ // 3. Other not the default input token
+ if (tokensWithoutInputToken.length > 0) {
+ return tokensWithoutInputToken[0];
+ }
+
+ return undefined;
+};
+
+const getTokensFrom = (
+ user: ExtendedFormattedUser | undefined,
+ baseTokensInfo: TokenInfo[],
+ chainId: number
+): SwappableToken[] => {
+ // Tokens From should be the supplied tokens
+ const suppliedPositions =
+ user?.userReservesData.filter((userReserve) => userReserve.underlyingBalance !== '0') || [];
+
+ return suppliedPositions
+ .map((position) => {
+ const baseToken = baseTokensInfo.find(
+ (baseToken) =>
+ baseToken.address.toLowerCase() === position.reserve.underlyingAsset.toLowerCase()
+ );
+ if (baseToken) {
+ // Prefer showing native symbol (e.g., ETH) instead of WETH when applicable, but keep underlying address
+ const wrappedNative =
+ WRAPPED_NATIVE_CURRENCIES[chainId as SupportedChainId]?.address?.toLowerCase();
+ const isWrappedNative =
+ wrappedNative && position.reserve.underlyingAsset.toLowerCase() === wrappedNative;
+ const nativeToken = isWrappedNative
+ ? TOKEN_LIST.tokens.find(
+ (t) => (t as TokenInfoWithBalance).extensions?.isNative && t.chainId === chainId
+ )
+ : undefined;
+
+ return {
+ addressToSwap: position.reserve.aTokenAddress,
+ addressForUsdPrice: position.reserve.aTokenAddress,
+ underlyingAddress: position.reserve.underlyingAsset,
+ decimals: baseToken.decimals,
+ symbol: nativeToken?.symbol ?? baseToken.symbol,
+ name: nativeToken?.name ?? baseToken.name,
+ balance: position.underlyingBalance,
+ chainId,
+ usdPrice: position.reserve.priceInUSD,
+ supplyAPY: position.reserve.supplyAPY,
+ variableBorrowAPY: position.reserve.variableBorrowAPY,
+ logoURI: nativeToken?.logoURI ?? baseToken.logoURI,
+ tokenType: nativeToken?.extensions?.isNative ? TokenType.NATIVE : TokenType.ERC20,
+ };
+ }
+ return undefined;
+ })
+ .filter((token) => token !== undefined)
+ .sort((a, b) => {
+ const aBalance = parseFloat(a?.balance ?? '0');
+ const bBalance = parseFloat(b?.balance ?? '0');
+ if (bBalance !== aBalance) {
+ return bBalance - aBalance;
+ }
+ // If balances are equal, sort by symbol alphabetically
+ const aSymbol = a?.symbol?.toLowerCase() ?? '';
+ const bSymbol = b?.symbol?.toLowerCase() ?? '';
+ if (aSymbol < bSymbol) return -1;
+ if (aSymbol > bSymbol) return 1;
+ return 0;
+ });
+};
+
+const getTokensTo = (
+ user: ExtendedFormattedUser | undefined,
+ reserves: ComputedReserveData[],
+ baseTokensInfo: TokenInfo[],
+ currentMarket: string,
+ chainId: number
+): SwappableToken[] => {
+ // Tokens To should be the potential supply tokens (so we have an aToken)
+ const tokensToSupply = reserves.filter(
+ (reserve: ComputedReserveData) =>
+ !(reserve.isFrozen || reserve.isPaused) &&
+ !displayGhoForMintableMarket({ symbol: reserve.symbol, currentMarket: currentMarket })
+ );
+
+ const suppliedPositions =
+ user?.userReservesData.filter((userReserve) => userReserve.underlyingBalance !== '0') || [];
+
+ return tokensToSupply
+ .map((reserve) => {
+ // Find the base token for this reserve
+ const baseToken = baseTokensInfo.find(
+ (baseToken) => baseToken.address.toLowerCase() === reserve.underlyingAsset.toLowerCase()
+ );
+
+ if (!baseToken) return undefined;
+
+ const currentCollateral =
+ suppliedPositions.find(
+ (position) =>
+ position.reserve.underlyingAsset.toLowerCase() === reserve.underlyingAsset.toLowerCase()
+ )?.underlyingBalance ?? '0';
+
+ // Prefer showing native symbol (e.g., ETH) instead of WETH when applicable, but keep underlying address
+ const wrappedNative =
+ WRAPPED_NATIVE_CURRENCIES[chainId as SupportedChainId]?.address?.toLowerCase();
+ const isWrappedNative =
+ wrappedNative && reserve.underlyingAsset.toLowerCase() === wrappedNative;
+ const nativeToken = isWrappedNative
+ ? TOKEN_LIST.tokens.find(
+ (t) => (t as TokenInfoWithBalance).extensions?.isNative && t.chainId === chainId
+ )
+ : undefined;
+
+ return {
+ addressToSwap: reserve.aTokenAddress,
+ addressForUsdPrice: reserve.aTokenAddress,
+ underlyingAddress: reserve.underlyingAsset,
+ decimals: baseToken.decimals,
+ symbol: nativeToken?.symbol ?? baseToken.symbol,
+ name: baseToken.name,
+ balance: currentCollateral,
+ chainId,
+ usdPrice: reserve.priceInUSD,
+ supplyAPY: reserve.supplyAPY,
+ variableBorrowAPY: reserve.variableBorrowAPY,
+ logoURI: nativeToken?.logoURI ?? baseToken.logoURI,
+ };
+ })
+ .filter((token) => token !== undefined)
+ .sort((a, b) => {
+ const aBalance = parseFloat(a?.balance ?? '0');
+ const bBalance = parseFloat(b?.balance ?? '0');
+ if (bBalance !== aBalance) {
+ return bBalance - aBalance;
+ }
+ // If balances are equal, sort by symbol alphabetically
+ const aSymbol = a?.symbol?.toLowerCase() ?? '';
+ const bSymbol = b?.symbol?.toLowerCase() ?? '';
+ if (aSymbol < bSymbol) return -1;
+ if (aSymbol > bSymbol) return 1;
+ return 0;
+ });
+};
diff --git a/src/components/transactions/Swap/modals/request/DebtSwapModalContent.tsx b/src/components/transactions/Swap/modals/request/DebtSwapModalContent.tsx
new file mode 100644
index 0000000000..74a7060b0d
--- /dev/null
+++ b/src/components/transactions/Swap/modals/request/DebtSwapModalContent.tsx
@@ -0,0 +1,240 @@
+import { API_ETH_MOCK_ADDRESS, InterestRate } from '@aave/contract-helpers';
+import { SupportedChainId, WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/cow-sdk';
+import { useQueryClient } from '@tanstack/react-query';
+import {
+ ComputedReserveData,
+ ComputedUserReserveData,
+ ExtendedFormattedUser,
+ useAppDataContext,
+} from 'src/hooks/app-data-provider/useAppDataProvider';
+import { TokenInfoWithBalance } from 'src/hooks/generic/useTokensBalance';
+import { isAssetHidden } from 'src/modules/dashboard/lists/constants';
+import { useRootStore } from 'src/store/root';
+import { CustomMarket } from 'src/ui-config/marketsConfig';
+import { NetworkConfig } from 'src/ui-config/networksConfig';
+import { fetchIconSymbolAndName } from 'src/ui-config/reservePatches';
+import { TOKEN_LIST } from 'src/ui-config/TokenList';
+import {
+ assetCanBeBorrowedByUser,
+ getMaxAmountAvailableToBorrow,
+} from 'src/utils/getMaxAmountAvailableToBorrow';
+import { useShallow } from 'zustand/shallow';
+
+import { invalidateAppStateForSwap } from '../../helpers/shared';
+import { SwappableToken, SwapParams, SwapType } from '../../types';
+import { BaseSwapModalContent } from './BaseSwapModalContent';
+
+export const DebtSwapModalContent = ({ underlyingAsset }: { underlyingAsset: string }) => {
+ const { user, reserves } = useAppDataContext();
+ const currentNetworkConfig = useRootStore((store) => store.currentNetworkConfig);
+ const [account, chainId, currentMarket] = useRootStore(
+ useShallow((store) => [store.account, store.currentChainId, store.currentMarket])
+ );
+
+ const tokensFrom = getTokensFrom(user, currentNetworkConfig.wagmiChain.id, currentNetworkConfig);
+ const tokensTo = getTokensTo(
+ user,
+ reserves,
+ currentNetworkConfig.wagmiChain.id,
+ currentMarket,
+ currentNetworkConfig
+ );
+ const defaultInputToken = getDefaultInputToken(tokensFrom, underlyingAsset);
+ const defaultOutputToken = getDefaultOutputToken(tokensTo, defaultInputToken);
+ const queryClient = useQueryClient();
+ const invalidateAppState = () => {
+ invalidateAppStateForSwap({
+ swapType: SwapType.DebtSwap,
+ chainId,
+ account,
+ queryClient,
+ });
+ };
+
+ const params: Partial = {
+ swapType: SwapType.DebtSwap,
+ allowLimitOrders: true,
+ forcedInputToken: defaultInputToken,
+ suggestedDefaultOutputToken: defaultOutputToken,
+ invalidateAppState,
+ sourceTokens: tokensFrom,
+ destinationTokens: tokensTo,
+ showSwitchInputAndOutputAssetsButton: false,
+ showOutputBalance: true,
+ outputBalanceTitle: 'Current',
+ chainId: currentNetworkConfig.wagmiChain.id,
+ titleTokenPostfix: 'debt',
+ resultScreenTokensFromTitle: 'Debt sent',
+ resultScreenTokensToTitle: 'Debt received',
+ resultScreenTitleItems: 'debt',
+
+ // Note: Debt Swap order is inverted
+ inputInputTitleSell: 'Swap at most',
+ outputInputTitleSell: 'Receive',
+ inputInputTitleBuy: 'Swap',
+ outputInputTitleBuy: 'Receive at most',
+ };
+
+ return ;
+};
+
+// Tokens from are all current open debt positions
+const getTokensFrom = (
+ user: ExtendedFormattedUser | undefined,
+ chainId: number,
+ currentNetworkConfig: NetworkConfig
+): SwappableToken[] => {
+ if (!user) return [];
+
+ const borrowPositions =
+ user?.userReservesData.reduce((acc, userReserve) => {
+ if (userReserve.variableBorrows !== '0') {
+ acc.push({
+ ...userReserve,
+ borrowRateMode: InterestRate.Variable,
+ reserve: {
+ ...userReserve.reserve,
+ ...(userReserve.reserve.isWrappedBaseAsset
+ ? fetchIconSymbolAndName({
+ symbol: currentNetworkConfig.baseAssetSymbol,
+ underlyingAsset: API_ETH_MOCK_ADDRESS.toLowerCase(),
+ })
+ : {}),
+ },
+ });
+ }
+ return acc;
+ }, [] as (ComputedUserReserveData & { borrowRateMode: InterestRate })[]) || [];
+
+ return borrowPositions
+ .map((borrowPosition) => {
+ // Find the token in the TokenList for proper logoURI and symbol if native
+ const tokenFromList = TOKEN_LIST.tokens.find(
+ (t) =>
+ t.address?.toLowerCase() === borrowPosition.reserve.underlyingAsset.toLowerCase() &&
+ t.chainId === chainId
+ );
+
+ // Prefer showing native symbol if it matches and available
+ const isWrappedNative =
+ WRAPPED_NATIVE_CURRENCIES[chainId as SupportedChainId]?.address?.toLowerCase() ===
+ borrowPosition.reserve.underlyingAsset.toLowerCase();
+ const nativeToken = isWrappedNative
+ ? TOKEN_LIST.tokens.find(
+ (t) => (t as TokenInfoWithBalance).extensions?.isNative && t.chainId === chainId
+ )
+ : undefined;
+
+ const initialSourceUserReserve = user?.userReservesData.find(
+ (userReserve) =>
+ userReserve.underlyingAsset.toLowerCase() === borrowPosition.underlyingAsset.toLowerCase()
+ );
+ const initialTargetUserReserve = user?.userReservesData.find(
+ (userReserve) =>
+ userReserve.underlyingAsset.toLowerCase() === borrowPosition.underlyingAsset.toLowerCase()
+ );
+
+ return {
+ addressToSwap: borrowPosition.underlyingAsset,
+ addressForUsdPrice: borrowPosition.underlyingAsset,
+ underlyingAddress: borrowPosition.underlyingAsset,
+ name: borrowPosition.reserve.name,
+ balance: borrowPosition.variableBorrows,
+ chainId,
+ decimals: borrowPosition.reserve.decimals,
+ symbol: nativeToken?.symbol ?? tokenFromList?.symbol ?? borrowPosition.reserve.symbol,
+ logoURI:
+ nativeToken?.logoURI ??
+ tokenFromList?.logoURI ??
+ `/icons/tokens/${borrowPosition.reserve.iconSymbol.toLowerCase()}.svg`,
+ usdPrice: borrowPosition.reserve.priceInUSD,
+ supplyAPY: borrowPosition.reserve.supplyAPY,
+ variableBorrowAPY: borrowPosition.reserve.variableBorrowAPY,
+ sourceReserve: initialSourceUserReserve,
+ destinationReserve: initialTargetUserReserve,
+ };
+ })
+ .filter((token) => token !== undefined);
+};
+
+// Tokens to are all potential borrow assets
+const getTokensTo = (
+ user: ExtendedFormattedUser | undefined,
+ reserves: ComputedReserveData[],
+ chainId: number,
+ currentMarketData: CustomMarket,
+ currentNetworkConfig: NetworkConfig
+): SwappableToken[] => {
+ if (!user) return [];
+
+ return reserves
+ .filter((reserve) => (user ? assetCanBeBorrowedByUser(reserve, user) : false))
+ .filter((reserve) => !isAssetHidden(currentMarketData, reserve.underlyingAsset))
+ .map((reserve: ComputedReserveData) => {
+ const availableBorrows = user ? Number(getMaxAmountAvailableToBorrow(reserve, user)) : 0;
+
+ const tokenFromList = TOKEN_LIST.tokens.find(
+ (t) =>
+ t.address?.toLowerCase() === reserve.underlyingAsset.toLowerCase() &&
+ t.chainId === chainId
+ );
+
+ const isWrappedNative =
+ WRAPPED_NATIVE_CURRENCIES[chainId as SupportedChainId]?.address?.toLowerCase() ===
+ reserve.underlyingAsset.toLowerCase();
+ const nativeToken = isWrappedNative
+ ? TOKEN_LIST.tokens.find(
+ (t) => (t as TokenInfoWithBalance).extensions?.isNative && t.chainId === chainId
+ )
+ : undefined;
+
+ if (!tokenFromList) return undefined;
+
+ return {
+ addressToSwap: reserve.underlyingAsset,
+ addressForUsdPrice: reserve.underlyingAsset,
+ underlyingAddress: reserve.underlyingAsset,
+ name: nativeToken?.name ?? tokenFromList?.name ?? reserve.name,
+ logoURI:
+ nativeToken?.logoURI ??
+ tokenFromList?.logoURI ??
+ `/icons/tokens/${reserve.iconSymbol.toLowerCase()}.svg`,
+ chainId,
+ decimals: reserve.decimals,
+ symbol: nativeToken?.symbol ?? tokenFromList?.symbol ?? reserve.symbol,
+ balance: availableBorrows.toString(),
+ usdPrice: reserve.priceInUSD,
+ supplyAPY: reserve.supplyAPY,
+ variableBorrowAPY: reserve.variableBorrowAPY,
+
+ ...(reserve.isWrappedBaseAsset
+ ? fetchIconSymbolAndName({
+ symbol: currentNetworkConfig.baseAssetSymbol,
+ underlyingAsset: API_ETH_MOCK_ADDRESS.toLowerCase(),
+ })
+ : {}),
+ };
+ })
+ .filter((token) => token !== undefined);
+};
+
+const getDefaultInputToken = (tokens: SwappableToken[], underlyingAsset: string) => {
+ return tokens.find(
+ (token) => token.underlyingAddress.toLowerCase() === underlyingAsset?.toLowerCase()
+ );
+};
+
+const getDefaultOutputToken = (
+ tokens: SwappableToken[],
+ defaultInputToken: SwappableToken | undefined
+) => {
+ return tokens.find(
+ (token) =>
+ // Filter out tokens that match by addressToSwap OR underlyingAddress
+ // This prevents the same asset from appearing in both lists
+ token.addressToSwap.toLowerCase() !== defaultInputToken?.addressToSwap.toLowerCase() &&
+ token.underlyingAddress.toLowerCase() !==
+ defaultInputToken?.underlyingAddress.toLowerCase() &&
+ token.symbol !== defaultInputToken?.symbol
+ );
+};
diff --git a/src/components/transactions/Swap/modals/request/NoEligibleAssetsToSwap.tsx b/src/components/transactions/Swap/modals/request/NoEligibleAssetsToSwap.tsx
new file mode 100644
index 0000000000..aac778659d
--- /dev/null
+++ b/src/components/transactions/Swap/modals/request/NoEligibleAssetsToSwap.tsx
@@ -0,0 +1,10 @@
+import { Trans } from '@lingui/macro';
+import { Typography } from '@mui/material';
+
+export const NoEligibleAssetsToSwap = () => {
+ return (
+
+ No eligible assets to swap.
+
+ );
+};
diff --git a/src/components/transactions/Swap/modals/request/RepayWithCollateralModalContent.tsx b/src/components/transactions/Swap/modals/request/RepayWithCollateralModalContent.tsx
new file mode 100644
index 0000000000..85101d3eb4
--- /dev/null
+++ b/src/components/transactions/Swap/modals/request/RepayWithCollateralModalContent.tsx
@@ -0,0 +1,246 @@
+import { API_ETH_MOCK_ADDRESS, InterestRate } from '@aave/contract-helpers';
+import { SupportedChainId, WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/cow-sdk';
+import { useQueryClient } from '@tanstack/react-query';
+import {
+ ComputedUserReserveData,
+ ExtendedFormattedUser,
+ useAppDataContext,
+} from 'src/hooks/app-data-provider/useAppDataProvider';
+import { TokenInfoWithBalance } from 'src/hooks/generic/useTokensBalance';
+import { useRootStore } from 'src/store/root';
+import { NetworkConfig } from 'src/ui-config/networksConfig';
+import { fetchIconSymbolAndName } from 'src/ui-config/reservePatches';
+import { TOKEN_LIST, TokenInfo } from 'src/ui-config/TokenList';
+import { useShallow } from 'zustand/shallow';
+
+import { invalidateAppStateForSwap } from '../../helpers/shared';
+import { SwappableToken, SwapParams, SwapType } from '../../types';
+import { BaseSwapModalContent } from './BaseSwapModalContent';
+
+export const RepayWithCollateralModalContent = ({
+ underlyingAsset,
+ debtType: interestMode,
+}: {
+ underlyingAsset: string;
+ debtType: InterestRate;
+}) => {
+ const { user, reserves } = useAppDataContext();
+ const currentNetworkConfig = useRootStore((store) => store.currentNetworkConfig);
+ const [account, chainId] = useRootStore(
+ useShallow((store) => [store.account, store.currentChainId])
+ );
+
+ const baseTokens: TokenInfo[] = reserves.map((reserve) => {
+ return {
+ address: reserve.underlyingAsset,
+ symbol: reserve.symbol,
+ logoURI: `/icons/tokens/${reserve.iconSymbol.toLowerCase()}.svg`,
+ chainId: currentNetworkConfig.wagmiChain.id,
+ name: reserve.name,
+ decimals: reserve.decimals,
+ };
+ });
+
+ const tokensFrom = getTokensFrom(user, currentNetworkConfig.wagmiChain.id, currentNetworkConfig);
+ const tokensTo = getTokensTo(user, baseTokens, currentNetworkConfig.wagmiChain.id);
+ const defaultInputToken = tokensFrom.find(
+ (token) => token.underlyingAddress.toLowerCase() === underlyingAsset?.toLowerCase()
+ );
+
+ // Collateral with highest balance, excluding the input token
+ const tokensWithoutInputToken = tokensTo.filter(
+ (token) =>
+ // Filter out tokens that match by addressToSwap OR underlyingAddress
+ // This prevents the same asset from appearing in both lists
+ token.addressToSwap.toLowerCase() !== defaultInputToken?.addressToSwap.toLowerCase() &&
+ token.underlyingAddress.toLowerCase() !== defaultInputToken?.underlyingAddress.toLowerCase()
+ );
+ const defaultOutputToken = tokensWithoutInputToken.sort(
+ (a, b) => Number(b.balance) - Number(a.balance)
+ )[0];
+
+ const queryClient = useQueryClient();
+ const invalidateAppState = () => {
+ invalidateAppStateForSwap({
+ swapType: SwapType.RepayWithCollateral,
+ chainId,
+ account,
+ queryClient,
+ });
+ };
+
+ const params: Partial = {
+ swapType: SwapType.RepayWithCollateral,
+ // allowLimitOrders: false,
+ invalidateAppState,
+ sourceTokens: tokensFrom,
+ destinationTokens: tokensTo,
+ chainId,
+ forcedInputToken: defaultInputToken,
+ suggestedDefaultOutputToken: defaultOutputToken,
+ showTitle: false,
+ showSwitchInputAndOutputAssetsButton: false,
+ titleTokenPostfix: 'with collateral',
+ inputBalanceTitle: 'Debt',
+ outputBalanceTitle: 'Collateral',
+ showOutputBalance: true,
+ inputInputTitle: 'Repay',
+ outputInputTitle: 'Using',
+ interestMode,
+ resultScreenTokensFromTitle: 'Repay',
+ resultScreenTokensToTitle: 'With',
+ resultScreenTitleItems: ' and repaid',
+ customReceivedTitle: 'Repaid',
+
+ // Note: Repay With Collateral order is inverted
+ inputInputTitleSell: 'Repay at most',
+ outputInputTitleSell: 'Using',
+ inputInputTitleBuy: 'Repay',
+ outputInputTitleBuy: 'Use at most',
+ };
+
+ return ;
+};
+
+// Tokens from are all current open debt positions
+const getTokensFrom = (
+ user: ExtendedFormattedUser | undefined,
+ chainId: number,
+ currentNetworkConfig: NetworkConfig
+): SwappableToken[] => {
+ if (!user) return [];
+
+ const borrowPositions =
+ user?.userReservesData.reduce((acc, userReserve) => {
+ if (userReserve.variableBorrows !== '0') {
+ acc.push({
+ ...userReserve,
+ borrowRateMode: InterestRate.Variable,
+ reserve: {
+ ...userReserve.reserve,
+ ...(userReserve.reserve.isWrappedBaseAsset
+ ? fetchIconSymbolAndName({
+ symbol: currentNetworkConfig.baseAssetSymbol,
+ underlyingAsset: API_ETH_MOCK_ADDRESS.toLowerCase(),
+ })
+ : {}),
+ },
+ });
+ }
+ return acc;
+ }, [] as (ComputedUserReserveData & { borrowRateMode: InterestRate })[]) || [];
+
+ return borrowPositions
+ .map((borrowPosition) => {
+ // Find the token in the TokenList for proper logoURI and symbol if native
+ const tokenFromList = TOKEN_LIST.tokens.find(
+ (t) =>
+ t.address?.toLowerCase() === borrowPosition.reserve.underlyingAsset.toLowerCase() &&
+ t.chainId === chainId
+ );
+
+ // Prefer showing native symbol if it matches and available
+ const isWrappedNative =
+ WRAPPED_NATIVE_CURRENCIES[chainId as SupportedChainId]?.address?.toLowerCase() ===
+ borrowPosition.reserve.underlyingAsset.toLowerCase();
+ const nativeToken = isWrappedNative
+ ? TOKEN_LIST.tokens.find(
+ (t) => (t as TokenInfoWithBalance).extensions?.isNative && t.chainId === chainId
+ )
+ : undefined;
+
+ const initialSourceUserReserve = user?.userReservesData.find(
+ (userReserve) =>
+ userReserve.underlyingAsset.toLowerCase() === borrowPosition.underlyingAsset.toLowerCase()
+ );
+ const initialTargetUserReserve = user?.userReservesData.find(
+ (userReserve) =>
+ userReserve.underlyingAsset.toLowerCase() === borrowPosition.underlyingAsset.toLowerCase()
+ );
+
+ return {
+ addressToSwap: borrowPosition.underlyingAsset,
+ addressForUsdPrice: borrowPosition.underlyingAsset,
+ underlyingAddress: borrowPosition.underlyingAsset,
+ name: borrowPosition.reserve.name,
+ balance: borrowPosition.variableBorrows,
+ chainId,
+ decimals: borrowPosition.reserve.decimals,
+ symbol: nativeToken?.symbol ?? tokenFromList?.symbol ?? borrowPosition.reserve.symbol,
+ logoURI:
+ nativeToken?.logoURI ??
+ tokenFromList?.logoURI ??
+ `/icons/tokens/${borrowPosition.reserve.iconSymbol.toLowerCase()}.svg`,
+ usdPrice: borrowPosition.reserve.priceInUSD,
+ supplyAPY: borrowPosition.reserve.supplyAPY,
+ variableBorrowAPY: borrowPosition.reserve.variableBorrowAPY,
+ sourceReserve: initialSourceUserReserve,
+ destinationReserve: initialTargetUserReserve,
+ };
+ })
+ .filter((token) => token !== undefined);
+};
+
+// Tokens to are all supplied tokens
+const getTokensTo = (
+ user: ExtendedFormattedUser | undefined,
+ baseTokensInfo: TokenInfo[],
+ chainId: number
+): SwappableToken[] => {
+ // Tokens From should be the supplied tokens
+ const suppliedPositions =
+ user?.userReservesData.filter((userReserve) => userReserve.underlyingBalance !== '0') || [];
+
+ return suppliedPositions
+ .map((position) => {
+ const baseToken = baseTokensInfo.find(
+ (baseToken) =>
+ baseToken.address.toLowerCase() === position.reserve.underlyingAsset.toLowerCase()
+ );
+ if (baseToken) {
+ // Prefer showing native symbol (e.g., ETH) instead of WETH when applicable, but keep underlying address
+ const wrappedNative =
+ WRAPPED_NATIVE_CURRENCIES[chainId as SupportedChainId]?.address?.toLowerCase();
+ const isWrappedNative =
+ wrappedNative && position.reserve.underlyingAsset.toLowerCase() === wrappedNative;
+ const nativeToken = isWrappedNative
+ ? TOKEN_LIST.tokens.find(
+ (t) => (t as TokenInfoWithBalance).extensions?.isNative && t.chainId === chainId
+ )
+ : undefined;
+
+ return {
+ addressToSwap: position.reserve.aTokenAddress,
+ addressForUsdPrice: position.reserve.aTokenAddress,
+ underlyingAddress: position.reserve.underlyingAsset,
+ decimals: baseToken.decimals,
+ symbol: nativeToken?.symbol ?? baseToken.symbol,
+ name: baseToken.name,
+ balance: position.underlyingBalance,
+ chainId,
+ usdPrice: position.reserve.priceInUSD,
+ supplyAPY: position.reserve.supplyAPY,
+ variableBorrowAPY: position.reserve.variableBorrowAPY,
+ logoURI:
+ nativeToken?.logoURI ??
+ baseToken.logoURI ??
+ `/icons/tokens/${position.reserve.iconSymbol.toLowerCase()}.svg`,
+ };
+ }
+ return undefined;
+ })
+ .filter((token) => token !== undefined)
+ .sort((a, b) => {
+ const aBalance = parseFloat(a?.balance ?? '0');
+ const bBalance = parseFloat(b?.balance ?? '0');
+ if (bBalance !== aBalance) {
+ return bBalance - aBalance;
+ }
+ // If balances are equal, sort by symbol alphabetically
+ const aSymbol = a?.symbol?.toLowerCase() ?? '';
+ const bSymbol = b?.symbol?.toLowerCase() ?? '';
+ if (aSymbol < bSymbol) return -1;
+ if (aSymbol > bSymbol) return 1;
+ return 0;
+ });
+};
diff --git a/src/components/transactions/Swap/modals/request/SwapModalContent.tsx b/src/components/transactions/Swap/modals/request/SwapModalContent.tsx
new file mode 100644
index 0000000000..675a270ee0
--- /dev/null
+++ b/src/components/transactions/Swap/modals/request/SwapModalContent.tsx
@@ -0,0 +1,210 @@
+import { Box, CircularProgress } from '@mui/material';
+import { useQueryClient } from '@tanstack/react-query';
+import { useMemo, useState } from 'react';
+import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider';
+import { TokenInfoWithBalance, useTokensBalance } from 'src/hooks/generic/useTokensBalance';
+import { useRootStore } from 'src/store/root';
+import { TOKEN_LIST, TokenInfo } from 'src/ui-config/TokenList';
+import { getNetworkConfig } from 'src/utils/marketsAndNetworksConfig';
+import { useShallow } from 'zustand/shallow';
+
+import {
+ invalidateAppStateForSwap,
+ supportedNetworksWithEnabledMarket,
+} from '../../helpers/shared';
+import { SwappableToken, SwapParams, SwapType, TokenType } from '../../types';
+import { BaseSwapModalContent } from './BaseSwapModalContent';
+
+export const SwapModalContent = ({
+ underlyingAsset,
+ chainId: chainIdFromSelection,
+}: {
+ underlyingAsset?: string;
+ chainId?: number;
+}) => {
+ const queryClient = useQueryClient();
+ const [account, chainIdInApp] = useRootStore(
+ useShallow((store) => [store.account, store.currentChainId])
+ );
+ const [chainId, setChainId] = useState(chainIdFromSelection ?? chainIdInApp);
+ const initialDefaultTokens = useMemo(() => getFilteredTokensForSwitch(chainId), [chainId]);
+ const reserves = useAppDataContext().reserves;
+
+ const {
+ data: initialTokens,
+ refetch: refetchInitialTokens,
+ isFetching: tokensLoading,
+ } = useTokensBalance(initialDefaultTokens, chainId, account);
+
+ const swappableTokens = initialTokens
+ ?.map((token) => {
+ const reserve = reserves.find(
+ (reserve) => reserve.underlyingAsset.toLowerCase() === token.address.toLowerCase()
+ );
+ const wrappedBaseReserve = reserves.find((r) => r.isWrappedBaseAsset);
+
+ return {
+ addressToSwap: token.address,
+ addressForUsdPrice: token.address,
+ underlyingAddress: token.address,
+ decimals: token.decimals,
+ symbol: token.symbol,
+ name: token.name,
+ balance: token.balance,
+ chainId,
+ usdPrice:
+ reserve?.priceInUSD ??
+ (token.extensions?.isNative ? wrappedBaseReserve?.priceInUSD : undefined),
+ logoURI: reserve?.iconSymbol
+ ? `/icons/tokens/${reserve.iconSymbol.toLowerCase()}.svg`
+ : token.logoURI,
+ tokenType: token.extensions?.isNative ? TokenType.NATIVE : TokenType.ERC20,
+ };
+ })
+ .filter((token) => token.balance !== '0')
+ .sort((a, b) => Number(b.balance) - Number(a.balance));
+
+ const defaultInputToken = getDefaultInputToken(swappableTokens ?? [], underlyingAsset ?? '');
+ const defaultOutputToken = getDefaultOutputToken(swappableTokens ?? []);
+
+ const invalidateAppState = () => {
+ invalidateAppStateForSwap({
+ swapType: SwapType.Swap,
+ chainId,
+ account,
+ queryClient,
+ });
+ };
+
+ const refreshTokens = (chainId: number) => {
+ setChainId(chainId);
+ refetchInitialTokens();
+ };
+
+ const params: Partial = {
+ swapType: SwapType.Swap,
+ allowLimitOrders: true,
+ suggestedDefaultInputToken: defaultInputToken,
+ suggestedDefaultOutputToken: defaultOutputToken,
+ invalidateAppState,
+ sourceTokens: swappableTokens ?? [],
+ destinationTokens: swappableTokens ?? [],
+ chainId,
+ refreshTokens,
+ supportedNetworks: supportedNetworksWithEnabledMarket,
+ showOutputBalance: true,
+ outputBalanceTitle: 'Current balance',
+ };
+
+ if (tokensLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return ;
+};
+
+export const getFilteredTokensForSwitch = (chainId: number): TokenInfoWithBalance[] => {
+ let customTokenList = TOKEN_LIST.tokens;
+ const savedCustomTokens = localStorage.getItem('customTokens');
+
+ if (savedCustomTokens) {
+ try {
+ const parsed = JSON.parse(savedCustomTokens);
+ // Validate that parsed data is an array
+ if (Array.isArray(parsed)) {
+ // Convert SwappableToken format to TokenInfo format for custom tokens
+ const convertedCustomTokens: TokenInfo[] = parsed
+ .map((token) => {
+ // Handle both SwappableToken format (with addressToSwap) and TokenInfo format (with address)
+ const address = token.addressToSwap || token.address || token.underlyingAddress;
+ if (!address) {
+ console.warn('Custom token missing address:', token);
+ return null;
+ }
+ const convertedToken: TokenInfo = {
+ chainId: token.chainId,
+ address: address,
+ name: token.name || '',
+ decimals: token.decimals || 18,
+ symbol: token.symbol || '',
+ logoURI: token.logoURI,
+ tags: token.tags,
+ extensions: token.extensions,
+ };
+ return convertedToken;
+ })
+ .filter((token): token is TokenInfo => token !== null); // Remove invalid tokens with type guard
+
+ customTokenList = customTokenList.concat(convertedCustomTokens);
+ } else {
+ console.warn('customTokens in localStorage is not an array:', parsed);
+ }
+ } catch (error) {
+ console.error('Error parsing customTokens from localStorage:', error);
+ // Continue with default token list if parsing fails
+ }
+ }
+
+ const transformedTokens = customTokenList.map((token) => {
+ return { ...token, balance: '0' };
+ });
+ const realChainId = getNetworkConfig(chainId).underlyingChainId ?? chainId;
+
+ // Remove duplicates
+ const seen = new Set();
+ return transformedTokens
+ .filter((token) => token.chainId === realChainId)
+ .filter((token) => {
+ // Handle both address and addressToSwap properties
+ const address = token.address;
+ if (!address) {
+ console.warn('Token missing address property:', token);
+ return false;
+ }
+ const key = `${token.chainId}:${address.toLowerCase()}`;
+ if (seen.has(key)) return false;
+ seen.add(key);
+ return true;
+ });
+};
+
+/// Suggested default tokens is user selection or token with highest balance
+const getDefaultInputToken = (swappableTokens: SwappableToken[], underlyingAsset: string) => {
+ const userSelectedInputToken = swappableTokens.find(
+ (token) => token.addressToSwap.toLowerCase() === underlyingAsset?.toLowerCase()
+ );
+
+ if (userSelectedInputToken) {
+ return userSelectedInputToken;
+ }
+
+ const tokensWithBalance = swappableTokens.filter((token) => token.balance !== '0');
+
+ if (tokensWithBalance.length === 0) {
+ return swappableTokens[0];
+ }
+
+ const tokenWithMaxBalance = tokensWithBalance.sort(
+ (a, b) => Number(b.balance) - Number(a.balance)
+ )[0];
+
+ return tokenWithMaxBalance;
+};
+
+/// Suggested default output token is GHO if available or second token with highest balance
+export const getDefaultOutputToken = (
+ swappableTokens: SwappableToken[],
+ underlyingAsset?: string
+) => {
+ const GHO = swappableTokens.find((token) => token.symbol === 'GHO');
+
+ const tokenWithSecondMaxBalance = swappableTokens
+ .filter((token) => token.underlyingAddress.toLowerCase() !== underlyingAsset?.toLowerCase())
+ .sort((a, b) => Number(b.balance) - Number(a.balance))[1];
+
+ return GHO ?? tokenWithSecondMaxBalance;
+};
diff --git a/src/components/transactions/Swap/modals/request/WithdrawAndSwapModalContent.tsx b/src/components/transactions/Swap/modals/request/WithdrawAndSwapModalContent.tsx
new file mode 100644
index 0000000000..531f5b709b
--- /dev/null
+++ b/src/components/transactions/Swap/modals/request/WithdrawAndSwapModalContent.tsx
@@ -0,0 +1,173 @@
+import { SupportedChainId, WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/cow-sdk';
+import { Box, CircularProgress } from '@mui/material';
+import { useQueryClient } from '@tanstack/react-query';
+import { useMemo } from 'react';
+import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider';
+import { TokenInfoWithBalance, useTokensBalance } from 'src/hooks/generic/useTokensBalance';
+import { ExtendedFormattedUser } from 'src/hooks/pool/useExtendedUserSummaryAndIncentives';
+import { useRootStore } from 'src/store/root';
+import { TOKEN_LIST, TokenInfo } from 'src/ui-config/TokenList';
+import { useShallow } from 'zustand/shallow';
+
+import { invalidateAppStateForSwap } from '../../helpers/shared';
+import { SwappableToken, SwapParams, SwapType, TokenType } from '../../types';
+import { BaseSwapModalContent } from './BaseSwapModalContent';
+import { getDefaultOutputToken, getFilteredTokensForSwitch } from './SwapModalContent';
+
+export const WithdrawAndSwapModalContent = ({ underlyingAsset }: { underlyingAsset: string }) => {
+ const { account, chainIdInApp: chainId } = useRootStore(
+ useShallow((store) => ({
+ account: store.account,
+ chainIdInApp: store.currentChainId,
+ }))
+ );
+
+ const queryClient = useQueryClient();
+ const { user } = useAppDataContext();
+
+ const initialDefaultTokens = useMemo(() => getFilteredTokensForSwitch(chainId), [chainId]);
+
+ const tokensFrom = getTokensFrom(user, initialDefaultTokens, chainId);
+
+ const reserves = useAppDataContext().reserves;
+ const { data: initialTokens, isFetching: tokensLoading } = useTokensBalance(
+ initialDefaultTokens,
+ chainId,
+ account
+ );
+
+ const swappableTokens = initialTokens
+ ?.map((token) => {
+ const reserve = reserves.find(
+ (reserve) => reserve.underlyingAsset.toLowerCase() === token.address.toLowerCase()
+ );
+ return {
+ addressToSwap: token.address,
+ addressForUsdPrice: token.address,
+ underlyingAddress: token.address,
+ decimals: token.decimals,
+ symbol: token.symbol,
+ name: token.name,
+ balance: token.balance,
+ chainId,
+ usdPrice: reserve?.priceInUSD,
+ supplyAPY: reserve?.supplyAPY,
+ variableBorrowAPY: reserve?.variableBorrowAPY,
+ logoURI: token.logoURI,
+ tokenType: token.extensions?.isNative ? TokenType.NATIVE : TokenType.ERC20,
+ };
+ })
+ .filter((token) => token.balance !== '0')
+ .sort((a, b) => Number(b.balance) - Number(a.balance));
+
+ const invalidateAppState = () => {
+ invalidateAppStateForSwap({
+ swapType: SwapType.WithdrawAndSwap,
+ chainId,
+ account,
+ queryClient,
+ });
+ };
+
+ const defaultInputToken = tokensFrom.find(
+ (token) => token.underlyingAddress.toLowerCase() === underlyingAsset?.toLowerCase()
+ );
+ const defaultOutputToken = getDefaultOutputToken(swappableTokens ?? [], underlyingAsset);
+
+ const params: Partial = {
+ swapType: SwapType.WithdrawAndSwap,
+ allowLimitOrders: true,
+ invalidateAppState,
+ sourceTokens: tokensFrom,
+ destinationTokens: swappableTokens,
+ chainId,
+ suggestedDefaultOutputToken: defaultOutputToken,
+ suggestedDefaultInputToken: defaultInputToken,
+ showTitle: false,
+ showSwitchInputAndOutputAssetsButton: false,
+ titleTokenPostfix: 'and swap',
+ inputBalanceTitle: 'Supplied',
+ outputBalanceTitle: 'Current balance',
+ showOutputBalance: true,
+ inputInputTitle: 'Withdraw',
+ outputInputTitle: 'And swap to',
+ resultScreenTitleItems: 'and withdrawn',
+ resultScreenTokensFromTitle: 'Withdrawn',
+ resultScreenTokensToTitle: 'Received',
+
+ // Note: Withdraw And Swap order is not inverted
+ inputInputTitleSell: 'Withdraw',
+ outputInputTitleSell: 'And swap to at most',
+ inputInputTitleBuy: 'Withdraw at most',
+ outputInputTitleBuy: 'And swap to',
+ };
+
+ if (tokensLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return ;
+};
+
+// Tokens from are all current open supply positions
+const getTokensFrom = (
+ user: ExtendedFormattedUser | undefined,
+ baseTokensInfo: TokenInfo[],
+ chainId: number
+): SwappableToken[] => {
+ // Tokens From should be the supplied tokens
+ const suppliedPositions =
+ user?.userReservesData.filter((userReserve) => userReserve.underlyingBalance !== '0') || [];
+
+ return suppliedPositions
+ .map((position) => {
+ const baseToken = baseTokensInfo.find(
+ (baseToken) =>
+ baseToken.address.toLowerCase() === position.reserve.underlyingAsset.toLowerCase()
+ );
+ if (baseToken) {
+ // Prefer showing native symbol (e.g., ETH) instead of WETH when applicable, but keep underlying address
+ const wrappedNative =
+ WRAPPED_NATIVE_CURRENCIES[chainId as SupportedChainId]?.address?.toLowerCase();
+ const isWrappedNative =
+ wrappedNative && position.reserve.underlyingAsset.toLowerCase() === wrappedNative;
+ const nativeToken = isWrappedNative
+ ? TOKEN_LIST.tokens.find(
+ (t) => (t as TokenInfoWithBalance).extensions?.isNative && t.chainId === chainId
+ )
+ : undefined;
+
+ return {
+ addressToSwap: position.reserve.aTokenAddress,
+ addressForUsdPrice: position.reserve.aTokenAddress,
+ underlyingAddress: position.reserve.underlyingAsset,
+ decimals: baseToken.decimals,
+ symbol: nativeToken?.symbol ?? baseToken.symbol,
+ name: baseToken.name,
+ balance: position.underlyingBalance,
+ chainId,
+ usdPrice: position.reserve.priceInUSD,
+ logoURI: nativeToken?.logoURI ?? baseToken.logoURI,
+ };
+ }
+ return undefined;
+ })
+ .filter((token) => token !== undefined)
+ .sort((a, b) => {
+ const aBalance = parseFloat(a?.balance ?? '0');
+ const bBalance = parseFloat(b?.balance ?? '0');
+ if (bBalance !== aBalance) {
+ return bBalance - aBalance;
+ }
+ // If balances are equal, sort by symbol alphabetically
+ const aSymbol = a?.symbol?.toLowerCase() ?? '';
+ const bSymbol = b?.symbol?.toLowerCase() ?? '';
+ if (aSymbol < bSymbol) return -1;
+ if (aSymbol > bSymbol) return 1;
+ return 0;
+ });
+};
diff --git a/src/components/transactions/Switch/cowprotocol/CowOrderToast.tsx b/src/components/transactions/Swap/modals/result/CowOrderToast.tsx
similarity index 100%
rename from src/components/transactions/Switch/cowprotocol/CowOrderToast.tsx
rename to src/components/transactions/Swap/modals/result/CowOrderToast.tsx
diff --git a/src/components/transactions/Switch/SwitchTxSuccessView.tsx b/src/components/transactions/Swap/modals/result/SwapResultView.tsx
similarity index 62%
rename from src/components/transactions/Switch/SwitchTxSuccessView.tsx
rename to src/components/transactions/Swap/modals/result/SwapResultView.tsx
index d83aa12d17..83d7a734ad 100644
--- a/src/components/transactions/Switch/SwitchTxSuccessView.tsx
+++ b/src/components/transactions/Swap/modals/result/SwapResultView.tsx
@@ -5,26 +5,31 @@ import { BigNumber } from 'ethers';
import { useEffect, useMemo, useRef, useState } from 'react';
import { DarkTooltip } from 'src/components/infoTooltips/DarkTooltip';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
+import { Link } from 'src/components/primitives/Link';
import { ExternalTokenIcon } from 'src/components/primitives/TokenIcon';
import { TextWithTooltip, TextWithTooltipProps } from 'src/components/TextWithTooltip';
-import { useCowOrderToast } from 'src/hooks/useCowOrderToast';
+import { useSwapOrdersTracking } from 'src/hooks/useSwapOrdersTracking';
+import { findByChainId } from 'src/ui-config/marketsConfig';
import { networkConfigs } from 'src/ui-config/networksConfig';
import { parseUnits } from 'viem';
-import { BaseCancelledView } from '../FlowCommons/BaseCancelled';
-import { BaseSuccessView } from '../FlowCommons/BaseSuccess';
-import { BaseWaitingView } from '../FlowCommons/BaseWaiting';
+import { BaseCancelledView } from '../../../FlowCommons/BaseCancelled';
+import { BaseSuccessView } from '../../../FlowCommons/BaseSuccess';
+import { BaseWaitingView } from '../../../FlowCommons/BaseWaiting';
+import { TrackAnalyticsHandlers } from '../../analytics/useTrackAnalytics';
import {
generateCoWExplorerLink,
getOrder,
isNativeToken,
isOrderCancelled,
+ isOrderExpired,
isOrderFilled,
isOrderLoading,
-} from './cowprotocol/cowprotocol.helpers';
-import { SwitchProvider } from './switch.types';
+} from '../../helpers/cow';
+import { SwapParams, SwapProvider, SwapState } from '../../types';
-export type SwitchTxSuccessViewProps = {
+export type SwapTxSuccessViewProps = {
+ isInvertedSwap: boolean;
txHash?: string;
amount: string;
symbol: string;
@@ -34,13 +39,17 @@ export type SwitchTxSuccessViewProps = {
outIconSymbol: string;
iconUri?: string;
outIconUri?: string;
- provider: SwitchProvider;
+ provider?: SwapProvider;
chainId: number;
- destDecimals: number;
- srcDecimals: number;
+ buyDecimals: number;
+ sellDecimals: number;
+ resultScreenTokensFromTitle?: string;
+ resultScreenTokensToTitle?: string;
+ resultScreenTitleItems?: string;
+ invalidateAppState: () => void;
};
-export const SwitchWithSurplusTooltip = ({
+export const SwapWithSurplusTooltip = ({
surplus,
surplusPercent,
baseAmount,
@@ -69,7 +78,44 @@ export const SwitchWithSurplusTooltip = ({
);
};
-export const SwitchTxSuccessView = ({
+export const SwapResultView = ({
+ params,
+ state,
+ trackingHandlers,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ trackingHandlers: TrackAnalyticsHandlers;
+}) => {
+ if (!state.sellAmountFormatted || !state.buyAmountFormatted) return null;
+
+ return (
+
+ );
+};
+
+export const SwapTxSuccessView = ({
+ isInvertedSwap,
txHash: txHashOrOrderId,
amount,
symbol,
@@ -81,16 +127,23 @@ export const SwitchTxSuccessView = ({
outIconUri,
provider,
chainId,
- destDecimals,
- srcDecimals,
-}: SwitchTxSuccessViewProps) => {
- const { trackOrder, setHasActiveOrders } = useCowOrderToast();
+ buyDecimals,
+ sellDecimals,
+ resultScreenTokensFromTitle,
+ resultScreenTokensToTitle,
+ resultScreenTitleItems,
+ invalidateAppState,
+ trackingHandlers,
+}: SwapTxSuccessViewProps & { trackingHandlers?: TrackAnalyticsHandlers }) => {
+ const { trackSwapOrderProgress, setHasActiveOrders } = useSwapOrdersTracking();
// Do polling each 10 seconds until the order get's filled
const [orderStatus, setOrderStatus] = useState<'succeed' | 'failed' | 'open'>('open');
const [surplus, setSurplus] = useState(undefined);
- const [inAmount, setInAmount] = useState(amount);
- const [outFinalAmount, setOutFinalAmount] = useState(outAmount);
+ const [inAmount, setInAmount] = useState(!isInvertedSwap ? amount : outAmount);
+ const [outFinalAmount, setOutFinalAmount] = useState(
+ !isInvertedSwap ? outAmount : amount
+ );
// Market for chain id
const networkConfig = networkConfigs[chainId].explorerLink;
@@ -98,12 +151,12 @@ export const SwitchTxSuccessView = ({
// Start tracking the order when the component mounts
useEffect(() => {
if (provider === 'cowprotocol' && txHashOrOrderId) {
- trackOrder(txHashOrOrderId, chainId);
+ trackSwapOrderProgress(txHashOrOrderId, chainId);
} else if (provider === 'cowprotocol' && orderStatus === 'open') {
// If the order is open, force the spinner to show, waiting for order details e.g. eth flow
setHasActiveOrders(true);
}
- }, [txHashOrOrderId, chainId, provider, trackOrder, setHasActiveOrders]);
+ }, [txHashOrOrderId, chainId, provider]);
// Poll the order status for UI updates
const interval = useRef(null);
@@ -114,20 +167,40 @@ export const SwitchTxSuccessView = ({
if (isOrderFilled(order.status)) {
setOrderStatus('succeed');
setSurplus(
- BigNumber.from(order.executedBuyAmount)
- .sub(BigNumber.from(parseUnits(outAmount, destDecimals)))
+ BigNumber.from(isInvertedSwap ? order.executedSellAmount : order.executedBuyAmount)
+ .sub(
+ BigNumber.from(
+ !isInvertedSwap
+ ? parseUnits(outAmount, buyDecimals)
+ : parseUnits(inAmount, sellDecimals)
+ )
+ )
.toBigInt()
);
- setOutFinalAmount(normalize(order.executedBuyAmount, destDecimals));
- setInAmount(normalize(order.executedSellAmount, srcDecimals));
+ setOutFinalAmount(
+ !isInvertedSwap
+ ? normalize(order.executedBuyAmount, buyDecimals)
+ : normalize(order.executedSellAmount, sellDecimals)
+ );
+ setInAmount(
+ !isInvertedSwap
+ ? normalize(order.executedSellAmount, sellDecimals)
+ : normalize(order.executedBuyAmount, buyDecimals)
+ );
if (interval.current) {
clearInterval(interval.current);
}
- } else if (isOrderCancelled(order.status)) {
+ invalidateAppState();
+ // Analytics: CoW order filled
+ trackingHandlers?.trackSwapFilled(order.executedSellAmount, order.executedBuyAmount);
+ } else if (isOrderCancelled(order.status) || isOrderExpired(order.status)) {
setOrderStatus('failed');
if (interval.current) {
clearInterval(interval.current);
}
+ invalidateAppState();
+ // Analytics: CoW order failed
+ trackingHandlers?.trackSwapFailed();
} else if (isOrderLoading(order.status)) {
setOrderStatus('open');
}
@@ -142,12 +215,12 @@ export const SwitchTxSuccessView = ({
txHashOrOrderId &&
provider === 'cowprotocol' &&
chainId &&
- destDecimals &&
+ buyDecimals &&
interval.current === null
) {
interval.current = setInterval(pollOrder, 10000);
}
- }, [txHashOrOrderId, chainId, provider, destDecimals]);
+ }, [txHashOrOrderId, chainId, provider, buyDecimals]);
const View = useMemo(() => {
if (provider === 'cowprotocol' && orderStatus === 'open') {
@@ -159,7 +232,7 @@ export const SwitchTxSuccessView = ({
}, [orderStatus, provider]);
const surplusFormatted = surplus
- ? Number(normalize(surplus.toString(), destDecimals))
+ ? Number(normalize(surplus.toString(), isInvertedSwap ? sellDecimals : buyDecimals))
: undefined;
const surplusDisplay =
surplusFormatted && surplusFormatted > 0
@@ -211,11 +284,17 @@ export const SwitchTxSuccessView = ({
) : orderStatus === 'failed' ? (
The order could't be filled.
) : (
- You've successfully swapped tokens.
+
+ You've successfully swapped{' '}
+ {resultScreenTitleItems ? resultScreenTitleItems : 'tokens'}.
+
)}
>
) : (
- You've successfully swapped tokens.
+
+ You've successfully swapped{' '}
+ {resultScreenTitleItems ? resultScreenTitleItems : 'tokens'}.
+
)}
@@ -235,8 +314,8 @@ export const SwitchTxSuccessView = ({
{provider == 'cowprotocol' &&
((orderStatus == 'open' && !isNativeToken(symbol)) || orderStatus == 'failed')
- ? 'Send'
- : 'Sent'}
+ ? `${resultScreenTokensFromTitle ?? 'Send'}`
+ : `${resultScreenTokensFromTitle ?? 'Sent'}`}
{provider == 'cowprotocol' && (orderStatus == 'open' || orderStatus == 'failed')
- ? 'Receive'
- : 'Received'}
+ ? `${resultScreenTokensToTitle ?? 'Receive'}`
+ : `${resultScreenTokensToTitle ?? 'Received'}`}
)}
+
+
+
+
+ Swap saved in your{' '}
+
+ history
+ {' '}
+ section.
+
+
+
);
};
diff --git a/src/components/transactions/Switch/SwitchTypeSelector.tsx b/src/components/transactions/Swap/shared/OrderTypeSelector.tsx
similarity index 51%
rename from src/components/transactions/Switch/SwitchTypeSelector.tsx
rename to src/components/transactions/Swap/shared/OrderTypeSelector.tsx
index f4ad320758..f3dbdb72e0 100644
--- a/src/components/transactions/Switch/SwitchTypeSelector.tsx
+++ b/src/components/transactions/Swap/shared/OrderTypeSelector.tsx
@@ -2,25 +2,18 @@ import { Trans } from '@lingui/macro';
import { Box, Typography } from '@mui/material';
import { StyledTxModalToggleButton } from 'src/components/StyledToggleButton';
import { StyledTxModalToggleGroup } from 'src/components/StyledToggleButtonGroup';
-import { useRootStore } from 'src/store/root';
-import { SWITCH_MODAL } from 'src/utils/events';
-import { useShallow } from 'zustand/shallow';
-export enum SwitchType {
- MARKET,
- LIMIT,
-}
-export function SwitchTypeSelector({
+import { OrderType } from '../types';
+
+export function OrderTypeSelector({
switchType,
setSwitchType,
+ limitsOrderButtonBlocked,
}: {
- switchType: SwitchType;
- setSwitchType: (type: SwitchType) => void;
+ switchType: OrderType;
+ setSwitchType: (type: OrderType) => void;
+ limitsOrderButtonBlocked: boolean;
}) {
- const [trackEvent] = useRootStore(
- useShallow((store) => [store.trackEvent, store.currentMarketData])
- );
-
return (
setSwitchType(value)}
>
trackEvent(SWITCH_MODAL.SWITCH_TYPE, { repayType: 'Market order' })}
+ value={OrderType.MARKET}
+ disabled={switchType === OrderType.MARKET}
>
- Market
+ Market
trackEvent(SWITCH_MODAL.SWITCH_TYPE, { repayType: 'Limit order' })}
+ value={OrderType.LIMIT}
+ disabled={switchType === OrderType.LIMIT || limitsOrderButtonBlocked}
>
Limit
diff --git a/src/components/transactions/Swap/shared/SwapModalTitle.tsx b/src/components/transactions/Swap/shared/SwapModalTitle.tsx
new file mode 100644
index 0000000000..5c0f29c453
--- /dev/null
+++ b/src/components/transactions/Swap/shared/SwapModalTitle.tsx
@@ -0,0 +1,12 @@
+import { TxModalTitle } from '../../FlowCommons/TxModalTitle';
+import { SwapParams, SwapState } from '../types';
+
+export const SwapModalTitle = ({ params, state }: { params: SwapParams; state: SwapState }) => {
+ return (
+
+ );
+};
diff --git a/src/components/transactions/Swap/types/index.ts b/src/components/transactions/Swap/types/index.ts
new file mode 100644
index 0000000000..5613256da1
--- /dev/null
+++ b/src/components/transactions/Swap/types/index.ts
@@ -0,0 +1,6 @@
+export * from '../constants/limitOrders.constants';
+export * from './params.types';
+export * from './quote.types';
+export * from './shared.types';
+export * from './state.types';
+export * from './tokens.types';
diff --git a/src/components/transactions/Swap/types/params.types.ts b/src/components/transactions/Swap/types/params.types.ts
new file mode 100644
index 0000000000..358469619e
--- /dev/null
+++ b/src/components/transactions/Swap/types/params.types.ts
@@ -0,0 +1,141 @@
+import { InterestRate } from '@aave/contract-helpers';
+import { FormattedUserReserves } from 'src/hooks/pool/useUserSummaryAndIncentives';
+
+import { SupportedNetworkWithChainId } from '../helpers/shared/misc.helpers';
+import { SwapKind, SwapType } from './shared.types';
+import { SwappableToken } from './tokens.types';
+
+/**
+ * Immutable configuration for a Swap modal/content instance.
+ *
+ * These params are set by the modal entry and do not change during the user flow.
+ * They control visual toggles, default selections and, for protocol flows, carry
+ * additional context like reserves. Anything that can be derived or that changes
+ * during the session should live in SwapState instead.
+ */
+export type TokensSwapParams = {
+ swapType: SwapType.Swap;
+ /** Whether limit orders UI is enabled for this instance. */
+ allowLimitOrders: boolean;
+ /** Chain where the swap should be performed (affects routing/providers). */
+ chainId: number;
+ /** Label for the input-token balance row. */
+ inputBalanceTitle: string;
+ /** Label for the output-token balance row. */
+ outputBalanceTitle: string;
+ /** Label for the received-amount row on the details block. */
+ customReceivedTitle: string;
+ /** Allow adding arbitrary token addresses to the token list. */
+ allowCustomTokens: boolean;
+ /** Show the control to switch input and output assets. */
+ showSwitchInputAndOutputAssetsButton: boolean;
+ /** Render network selector in the header. */
+ showNetworkSelector: boolean;
+ /** Render the modal title. */
+ showTitle: boolean;
+ /** Optional token postfix rendered in the modal title. */
+ titleTokenPostfix?: string;
+ /** Networks that the modal allows switching to. Used by the selector. */
+ supportedNetworks: SupportedNetworkWithChainId[];
+ /** Force a specific input token (disables changing it). */
+ forcedInputToken?: SwappableToken;
+ /** Force a specific output token (disables changing it). */
+ forcedOutputToken?: SwappableToken;
+ /** Suggested default input token if none is forced. */
+ suggestedDefaultInputToken?: SwappableToken;
+ /** Suggested default output token if none is forced. */
+ suggestedDefaultOutputToken?: SwappableToken;
+ /** Whether to show destination balance by default. */
+ showOutputBalance: boolean;
+ /** Candidate list for source tokens. */
+ sourceTokens: SwappableToken[];
+ /** Candidate list for destination tokens. */
+ destinationTokens: SwappableToken[];
+ /** Optional label above the input-amount field. */
+ inputInputTitle?: string;
+ /** Optional label above the output-amount field. */
+ outputInputTitle?: string;
+
+ /** Optional label above the input-amount field for the buy side. Only in Limit Orders mode.*/
+ inputInputTitleBuy?: string;
+ /** Optional label above the output-amount field for the buy side. Only in Limit Orders mode.*/
+ outputInputTitleBuy?: string;
+ /** Optional label above the input-amount field for the sell side. Only in Limit Orders mode.*/
+ inputInputTitleSell?: string;
+ /** Optional label above the output-amount field for the sell side. Only in Limit Orders mode.*/
+ outputInputTitleSell?: string;
+
+ /** Callback to invalidate/refresh app-wide state when closing/completing. */
+ invalidateAppState: () => void;
+ /** Callback to refresh tokens when the chain/network changes. */
+ refreshTokens: (chainId: number) => void;
+ /** Interest rate mode used by protocol flows (debt/collateral context). */
+ interestMode: InterestRate;
+ /** Order side selected for the UI; defaults to 'sell'. */
+ swapKind: SwapKind;
+ /** Label for the tokens from in the result screen. */
+ resultScreenTokensFromTitle?: string;
+ /** Label for the tokens to in the result screen. */
+ resultScreenTokensToTitle?: string;
+ /** Label for the title items in the result screen. */
+ resultScreenTitleItems?: string;
+};
+
+export const isProtocolSwapParams = (params: SwapParams): params is ProtocolSwapParams => {
+ return (
+ 'swapType' in params &&
+ params.swapType !== undefined &&
+ (params.swapType === SwapType.DebtSwap ||
+ params.swapType === SwapType.CollateralSwap ||
+ params.swapType === SwapType.RepayWithCollateral ||
+ params.swapType === SwapType.WithdrawAndSwap)
+ );
+};
+
+export const isTokensSwapParams = (params: SwapParams): params is TokensSwapParams => {
+ return 'swapType' in params && params.swapType === SwapType.Swap;
+};
+
+/**
+ * Extension of TokensSwapParams used by protocol-aware flows
+ * (CollateralSwap, DebtSwap, RepayWithCollateral, WithdrawAndSwap).
+ * Provides the user reserve context for the source and destination assets.
+ */
+export type ProtocolSwapParams = Omit & {
+ swapType:
+ | SwapType.DebtSwap
+ | SwapType.CollateralSwap
+ | SwapType.RepayWithCollateral
+ | SwapType.WithdrawAndSwap;
+
+ sourceReserve: FormattedUserReserves;
+ destinationReserve: FormattedUserReserves;
+};
+
+export type SwapParams = TokensSwapParams | ProtocolSwapParams;
+
+export const SwapDefaultParams: SwapParams = {
+ swapType: SwapType.Swap,
+ swapKind: 'sell',
+ allowLimitOrders: true,
+ chainId: 1,
+ inputBalanceTitle: 'Balance',
+ outputBalanceTitle: 'Balance',
+ showOutputBalance: false,
+ inputInputTitle: undefined,
+ outputInputTitle: undefined,
+ allowCustomTokens: true,
+ showSwitchInputAndOutputAssetsButton: true,
+ sourceTokens: [],
+ destinationTokens: [],
+ customReceivedTitle: 'Received',
+ showNetworkSelector: true,
+ showTitle: true,
+ supportedNetworks: [],
+
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ invalidateAppState: function () {},
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ refreshTokens: function () {},
+ interestMode: InterestRate.Variable,
+};
diff --git a/src/components/transactions/Switch/switch.types.ts b/src/components/transactions/Swap/types/quote.types.ts
similarity index 50%
rename from src/components/transactions/Switch/switch.types.ts
rename to src/components/transactions/Swap/types/quote.types.ts
index c7f895a9e5..1af26369dc 100644
--- a/src/components/transactions/Switch/switch.types.ts
+++ b/src/components/transactions/Swap/types/quote.types.ts
@@ -2,9 +2,16 @@ import { OrderParameters, QuoteAmountsAndCosts, QuoteAndPost } from '@cowprotoco
import { OptimalRate } from '@paraswap/core';
import { TxErrorType } from 'src/ui-config/errorMapping';
-export type SwitchProvider = 'cowprotocol' | 'paraswap';
+import { SwapProvider, SwapType } from './shared.types';
+/**
+ * Parameters required to fetch a quote from a provider.
+ * The module converts from SwapState into this minimal, provider-agnostic shape.
+ */
export type ProviderRatesParams = {
+ swapType: SwapType;
+ side?: 'buy' | 'sell';
+ invertedQuoteRoute?: boolean;
amount: string;
srcToken: string;
srcDecimals: number;
@@ -21,9 +28,9 @@ export type ProviderRatesParams = {
isInputTokenCustom?: boolean;
isOutputTokenCustom?: boolean;
- appCode?: string;
+ appCode: string;
- setError?: (error: TxErrorType) => void;
+ setError?: (error: Error | TxErrorType) => void;
};
export type MultiProviderRatesParams = Omit & {
@@ -33,47 +40,58 @@ export type MultiProviderRatesParams = Omit {
- return rates?.provider === 'paraswap';
+export const isParaswapRates = (rates?: SwapQuoteType): rates is ParaswapRatesType => {
+ return rates?.provider === SwapProvider.PARASWAP;
};
-export const isCowProtocolRates = (rates?: SwitchRatesType): rates is CowProtocolRatesType => {
- return rates?.provider === 'cowprotocol';
+export const isCowProtocolRates = (rates?: SwapQuoteType): rates is CowProtocolRatesType => {
+ return rates?.provider === SwapProvider.COW_PROTOCOL;
};
-export type SwitchRatesType = ParaswapRatesType | CowProtocolRatesType;
+/** Union of all provider quote types consumed by the UI. */
+export type SwapQuoteType = ParaswapRatesType | CowProtocolRatesType;
diff --git a/src/components/transactions/Swap/types/shared.types.ts b/src/components/transactions/Swap/types/shared.types.ts
new file mode 100644
index 0000000000..fa53011aaf
--- /dev/null
+++ b/src/components/transactions/Swap/types/shared.types.ts
@@ -0,0 +1,49 @@
+/** All supported swap flows. */
+export enum SwapType {
+ Swap = 'swap',
+ CollateralSwap = 'collateral_swap',
+ DebtSwap = 'debt_swap',
+ RepayWithCollateral = 'repay_with_collateral',
+ WithdrawAndSwap = 'withdraw_and_swap',
+}
+
+/** Order flavor shown in the UI. */
+export enum OrderType {
+ MARKET = 'market',
+ LIMIT = 'limit',
+}
+
+/**
+ * Side of the order:
+ * - 'sell' = user edits input amount; output is quoted
+ * - 'buy' = user edits output amount; input is quoted
+ */
+export type SwapKind = 'buy' | 'sell';
+
+/** Current execution/quote provider. */
+export enum SwapProvider {
+ COW_PROTOCOL = 'cowprotocol',
+ PARASWAP = 'paraswap',
+ NONE = 'none',
+}
+
+/** Coarse-grained UI stages used by warnings/errors tracking. */
+export type SwapStage =
+ | 'before_quote'
+ | 'after_input_change'
+ | 'before_approval'
+ | 'before_swap'
+ | 'after_swap';
+
+/** Normalized error surfaced to the UI. */
+export type SwapError = {
+ rawError: Error;
+ message: string;
+ actionBlocked: boolean;
+ stage?: SwapStage;
+};
+
+/** Non-blocking information surfaced to the user. */
+export type SwapWarning = {
+ message: string;
+};
diff --git a/src/components/transactions/Swap/types/state.types.ts b/src/components/transactions/Swap/types/state.types.ts
new file mode 100644
index 0000000000..00c77d752f
--- /dev/null
+++ b/src/components/transactions/Swap/types/state.types.ts
@@ -0,0 +1,315 @@
+import { FormattedUserReserves } from 'src/hooks/pool/useUserSummaryAndIncentives';
+import { TxStateType } from 'src/hooks/useModal';
+
+import { Expiry } from '../constants/limitOrders.constants';
+import { ValidationData } from '../helpers/shared/slippage.helpers';
+import { SwapParams } from './params.types';
+import { SwapQuoteType } from './quote.types';
+import {
+ OrderType,
+ SwapError,
+ SwapKind,
+ SwapProvider,
+ SwapType,
+ SwapWarning,
+} from './shared.types';
+import { SwappableToken, TokenType } from './tokens.types';
+
+export enum ActionsBlockedReason {
+ ZERO_LTV_BLOCKING = 'ZERO_LTV_BLOCKING',
+ SUPPLY_CAP_BLOCKING = 'SUPPLY_CAP_BLOCKING',
+ HIGH_COSTS_LIMIT_ORDER = 'HIGH_COSTS_LIMIT_ORDER',
+ HIGH_PRICE_IMPACT = 'HIGH_PRICE_IMPACT',
+ LOW_HEALTH_FACTOR = 'LOW_HEALTH_FACTOR',
+ INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE',
+ INSUFFICIENT_LIQUIDITY = 'INSUFFICIENT_LIQUIDITY',
+ FLASH_LOAN_DISABLED = 'FLASH_LOAN_DISABLED',
+ IS_LIQUIDATABLE = 'IS_LIQUIDATABLE',
+}
+
+/**
+ * Mutable UI/application state for a token-to-token swap flow.
+ *
+ * This state is updated as the user interacts (amount edits, token selection),
+ * as quotes arrive, and as actions progress. For protocol flows, see
+ * ProtocolSwapState which extends this shape with reserve context.
+ */
+export type TokensSwapState = {
+ swapType: SwapType.Swap;
+ // Order
+ /** EVM chain id where the swap executes. */
+ chainId: number;
+ /** Selected order side: 'sell' edits input, 'buy' edits output. */
+ side: SwapKind;
+ /** Market or Limit order type. */
+ orderType: OrderType;
+ /** Currently selected input token. */
+ sourceToken: SwappableToken;
+ /** Currently selected output token. */
+ destinationToken: SwappableToken;
+ /** Raw input amount in human units (string to preserve precision). */
+ inputAmount: string;
+ /** Debounced input amount used for rate fetching. */
+ debouncedInputAmount: string;
+ /** Raw output amount in human units (for buy side). */
+ outputAmount: string;
+ /** Debounced output amount used for rate fetching. */
+ debouncedOutputAmount: string;
+ /** USD value of input amount at current spot. */
+ inputAmountUSD: string;
+ /** USD value of output amount at current spot. */
+ outputAmountUSD: string;
+ /** If set, forces the max value button to use this cap. */
+ forcedMaxValue: string;
+ /** Whether the user has toggled the Max amount. */
+ isMaxSelected: boolean;
+
+ // Processed amounts for the order, based on fees, slippage, inverting requirements, etc.
+ /** Sell amount (formatter like '1.234567890') for the swap order request after costs and slippage if applicable, used to build transactions. */
+ sellAmountFormatted: string | undefined;
+ /** Sell amount (bigint) for the swap order request after costs and slippage if applicable, used to build transactions. */
+ sellAmountBigInt: bigint | undefined;
+ /** Sell amount (USD value) for the swap order request after costs and slippage if applicable, used to build transactions. */
+ sellAmountUSD: string | undefined;
+ /** Sell amount (token object) for the swap order request after costs and slippage if applicable, used to build transactions. */
+ sellAmountToken: SwappableToken | undefined;
+ /** Buy amount (formatter like '1.234567890') for the swap order request after costs and slippage if applicable, used to build transactions. */
+ buyAmountFormatted: string | undefined;
+ /** Buy amount (bigint) for the swap order request after costs and slippage if applicable, used to build transactions. */
+ buyAmountBigInt: bigint | undefined;
+ /** Buy amount (USD value) for the swap order request after costs and slippage if applicable, used to build transactions. */
+ buyAmountUSD: string | undefined;
+ /** Buy amount (token object) for the swap order request after costs and slippage if applicable, used to build transactions. */
+ buyAmountToken: SwappableToken | undefined;
+ /** Whether the quote route is inverted for this flow (e.g. repay with collateral when we need to swap from Available collateral, second input to Repay, first input). */
+ isInvertedSwap: boolean;
+ /** Side that was actually quoted after considering inversion (e.g. if the quote route is inverted, the processed side is the opposite of the side). */
+ processedSide: SwapKind;
+
+ // Costs (shared across details views)
+ /** Network fee expressed in sell currency, normalized to sell token decimals. */
+ networkFeeAmountInSellFormatted?: string;
+ /** Network fee expressed in buy currency, normalized to buy token decimals. */
+ networkFeeAmountInBuyFormatted?: string;
+ /** Partner fee amount applied to this order, normalized to the fee token units (depends on side). */
+ partnerFeeAmountFormatted?: string;
+ /** Partner fee in basis points used to compute partnerFeeAmountFormatted. */
+ partnerFeeBps?: number;
+
+ /** User-selected slippage in percentage (e.g. '0.10' -> 0.10%). */
+ slippage: string;
+ /** Safe default slippage used for warnings and guardrails. */
+ safeSlippage: number;
+ /** Provider-suggested slippage, used to auto-fill. */
+ autoSlippage: string;
+ /** Order expiry for limit orders. */
+ expiry: Expiry;
+
+ // Context
+ /** EOA address performing the swap. */
+ user: string;
+ /** True if the user is a generic smart contract wallet (SCW). */
+ userIsSmartContractWallet: boolean;
+ /** True if the user is a Safe wallet. */
+ userIsSafeWallet: boolean;
+ /** Token list for the source picker. */
+ sourceTokens: SwappableToken[];
+ /** Token list for the destination picker. */
+ destinationTokens: SwappableToken[];
+ /** Last surfaced error; when present, usually blocks actions. */
+ error: SwapError | undefined;
+ /** Non-blocking hints presented to the user. */
+ warnings: SwapWarning[];
+ /** Computed flags that disable actions until resolved. */
+ actionsBlocked: Partial>;
+
+ /** Whether the limits order button is blocked. */
+ limitsOrderButtonBlocked: boolean;
+
+ // Current
+ /** Selected swap provider (cowprotocol, paraswap, none). */
+ provider: SwapProvider;
+ /** Last received quote/rates for the current provider. */
+ swapRate?: SwapQuoteType;
+ /** True while querying quotes. */
+ ratesLoading: boolean;
+ /** True while executing approval/swap actions. */
+ actionsLoading: boolean;
+ /** Timestamp when the latest quote was received (ms since epoch). */
+ quoteLastUpdatedAt?: number | null;
+ /** Quote timer pause bookkeeping. */
+ quoteTimerPausedAt?: number | null;
+ /** Quote timer pause accum ms. */
+ quoteTimerPausedAccumMs?: number;
+ /** Whether automatic quote refresh is paused due to user edits. */
+ quoteRefreshPaused?: boolean;
+ /** Set to true once the flow (flashloan vs simple) is determined. */
+ isSwapFlowSelected: boolean;
+ /** Becomes true if the resulting HF would be below danger threshold. */
+ isLiquidatable: boolean;
+ /** Becomes true if HF would be low but not liquidatable. */
+ isHFLow: boolean;
+ /** Predicted health factor after swap, if computable. */
+ hfAfterSwap: number;
+ /** Gas limit hint computed from estimation or provider. */
+ gasLimit: string;
+ /** Whether the flow requires using flashloan (protocol flows/HF). */
+ useFlashloan: boolean | undefined;
+ /** Validation result for slippage input. */
+ slippageValidation: ValidationData | undefined;
+ /** Whether to show the gas station widget. */
+ showGasStation: boolean;
+ /** Lifecycle state of the main transaction. */
+ mainTxState: TxStateType;
+
+ // Warnings
+ /** Show a high-slippage warning. */
+ showSlippageWarning: boolean;
+ /** Force user to reset approval when switching certain tokens. */
+ requiresApprovalReset: boolean;
+ /** True if user is connected to the wrong network. */
+ isWrongNetwork: boolean;
+ showChangeNetworkWarning: boolean;
+};
+
+/**
+ * Check if any of the actions are blocked.
+ */
+export const areActionsBlocked = (state: SwapState): boolean => {
+ return Object.values(state.actionsBlocked).some((blocked) => blocked === true);
+};
+
+export const actionsBlockedReasonsAmount = (state: SwapState): number => {
+ return Object.values(state.actionsBlocked).filter((blocked) => blocked === true).length;
+};
+
+/**
+ * State for protocol-aware flows. Includes reserve context for computing
+ * HF effects, borrow/collateral semantics, and determining flashloan usage.
+ */
+export type ProtocolSwapState = Omit & {
+ swapType:
+ | SwapType.DebtSwap
+ | SwapType.CollateralSwap
+ | SwapType.RepayWithCollateral
+ | SwapType.WithdrawAndSwap;
+
+ sourceReserve: FormattedUserReserves;
+ destinationReserve: FormattedUserReserves;
+};
+
+export type SwapState = TokensSwapState | ProtocolSwapState;
+
+export const isProtocolSwapState = (state: SwapState): state is ProtocolSwapState => {
+ return (
+ ('swapType' in state && state.swapType === SwapType.DebtSwap) ||
+ state.swapType === SwapType.CollateralSwap ||
+ state.swapType === SwapType.RepayWithCollateral ||
+ state.swapType === SwapType.WithdrawAndSwap
+ );
+};
+
+export const isTokensSwapState = (state: SwapState): state is TokensSwapState => {
+ return 'swapType' in state && state.swapType === SwapType.Swap;
+};
+
+const defaultToken: SwappableToken = {
+ addressToSwap: '',
+ addressForUsdPrice: '',
+ underlyingAddress: '',
+ decimals: 18,
+ symbol: '',
+ name: '',
+ balance: '0',
+ chainId: 1,
+ logoURI: '',
+ tokenType: TokenType.NATIVE,
+};
+
+export const swapDefaultState: SwapState = {
+ swapType: SwapType.Swap,
+ provider: SwapProvider.NONE,
+ expiry: Expiry.TEN_MINUTES, // 10 minutes
+ user: '',
+ actionsLoading: false,
+ side: 'sell',
+ mainTxState: {
+ success: false,
+ txHash: undefined,
+ loading: false,
+ },
+ orderType: OrderType.MARKET,
+ chainId: 1,
+ sourceToken: defaultToken,
+ destinationToken: defaultToken,
+ inputAmount: '',
+ debouncedInputAmount: '',
+ outputAmount: '',
+ debouncedOutputAmount: '',
+ inputAmountUSD: '',
+ outputAmountUSD: '',
+ forcedMaxValue: '',
+ userIsSmartContractWallet: false,
+ userIsSafeWallet: false,
+ sourceTokens: [],
+ destinationTokens: [],
+ isMaxSelected: false,
+ error: undefined,
+ warnings: [],
+ actionsBlocked: {},
+ ratesLoading: false,
+ isSwapFlowSelected: false,
+ isLiquidatable: false,
+ isHFLow: false,
+ hfAfterSwap: 0,
+ safeSlippage: 0.005,
+ swapRate: undefined,
+
+ sellAmountFormatted: undefined,
+ sellAmountBigInt: undefined,
+ sellAmountToken: undefined,
+ buyAmountFormatted: undefined,
+ buyAmountBigInt: undefined,
+ buyAmountToken: undefined,
+ sellAmountUSD: undefined,
+ buyAmountUSD: undefined,
+ isInvertedSwap: false,
+ processedSide: 'sell',
+ networkFeeAmountInSellFormatted: '0',
+ networkFeeAmountInBuyFormatted: '0',
+ partnerFeeAmountFormatted: '0',
+ partnerFeeBps: 0,
+
+ limitsOrderButtonBlocked: false,
+ showSlippageWarning: false,
+ showChangeNetworkWarning: false,
+ quoteLastUpdatedAt: null,
+ quoteTimerPausedAt: null,
+ quoteTimerPausedAccumMs: 0,
+ quoteRefreshPaused: false,
+ slippage: '0.10',
+ autoSlippage: '',
+ gasLimit: '0',
+ useFlashloan: undefined,
+ slippageValidation: undefined,
+ showGasStation: false,
+ requiresApprovalReset: false,
+ isWrongNetwork: false,
+};
+
+export const swapStateFromParamsOrDefault = (
+ params: SwapParams,
+ defaultState: SwapState
+): SwapState => {
+ return {
+ ...defaultState,
+ ...params,
+
+ sourceToken:
+ params.forcedInputToken || params.suggestedDefaultInputToken || defaultState.sourceToken,
+ destinationToken:
+ params.forcedOutputToken ||
+ params.suggestedDefaultOutputToken ||
+ defaultState.destinationToken,
+ };
+};
diff --git a/src/components/transactions/Swap/types/tokens.types.ts b/src/components/transactions/Swap/types/tokens.types.ts
new file mode 100644
index 0000000000..5958de82e3
--- /dev/null
+++ b/src/components/transactions/Swap/types/tokens.types.ts
@@ -0,0 +1,51 @@
+import { TokenInfo } from 'src/ui-config/TokenList';
+
+/**
+ * Token classification used by the swap UI.
+ * - NATIVE: chain native asset (e.g. ETH, MATIC)
+ * - ERC20: standard ERC-20 token
+ * - USER_CUSTOM: user-provided custom token (not on curated list)
+ * - COLLATERAL / DEBT: protocol representations used in position-aware flows
+ */
+export enum TokenType {
+ NATIVE,
+ ERC20,
+ USER_CUSTOM,
+ COLLATERAL,
+ DEBT,
+}
+
+/**
+ * Minimal token shape used by the swap module.
+ * Notes:
+ * - addressToSwap is the address that providers expect for on-chain execution
+ * - addressForUsdPrice enables price feeds to diverge from the swap address
+ * - underlyingAddress is useful when swapping aTokens or debt tokens
+ */
+export type SwappableToken = {
+ addressToSwap: string;
+ addressForUsdPrice: string;
+ underlyingAddress: string;
+ decimals: number;
+ symbol: string;
+ name: string;
+ balance: string;
+ chainId: number;
+ usdPrice?: string;
+ supplyAPY?: string;
+ variableBorrowAPY?: string;
+ tokenType?: TokenType;
+ logoURI?: string;
+};
+
+export const swappableTokenToTokenInfo = (token: SwappableToken): TokenInfo => {
+ return {
+ address: token.addressToSwap,
+ symbol: token.symbol,
+ decimals: token.decimals,
+ chainId: token.chainId,
+ name: token.name,
+ logoURI: token.logoURI,
+ ...(token.tokenType === TokenType.NATIVE && { extensions: { isNative: true } }),
+ };
+};
diff --git a/src/components/transactions/Swap/warnings/SwapNetworkWarning.tsx b/src/components/transactions/Swap/warnings/SwapNetworkWarning.tsx
new file mode 100644
index 0000000000..4b9dec1a2f
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/SwapNetworkWarning.tsx
@@ -0,0 +1,28 @@
+import { useIsWrongNetwork } from 'src/hooks/useIsWrongNetwork';
+import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
+import { GENERAL } from 'src/utils/events';
+import { getNetworkConfig } from 'src/utils/marketsAndNetworksConfig';
+
+import { ChangeNetworkWarning } from '../../Warnings/ChangeNetworkWarning';
+import { SwapParams, SwapState } from '../types';
+
+export function SwapNetworkWarning({ state }: { state: SwapState; params: SwapParams }) {
+ const isWrongNetwork = useIsWrongNetwork(state.chainId);
+ const { readOnlyModeAddress } = useWeb3Context();
+
+ if (!isWrongNetwork.isWrongNetwork || readOnlyModeAddress) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/transactions/Swap/warnings/SwapPostInputWarnings.tsx b/src/components/transactions/Swap/warnings/SwapPostInputWarnings.tsx
new file mode 100644
index 0000000000..83105a49a0
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/SwapPostInputWarnings.tsx
@@ -0,0 +1,48 @@
+import { Box } from '@mui/material';
+import React, { Dispatch } from 'react';
+
+import { SwapParams, SwapState } from '../types';
+import {
+ CowAdapterApprovalInfo,
+ CustomTokenWarning,
+ GasEstimationWarning,
+ HighCostsLimitOrderWarning,
+ HighPriceImpactWarning,
+ LimitOrderAmountWarning,
+ LiquidationCriticalWarning,
+ LowHealthFactorWarning,
+ SafetyModuleSwapWarning,
+ SlippageWarning,
+ USDTResetWarning,
+} from './postInputs';
+
+export const SwapPostInputWarnings = ({
+ params,
+ state,
+ setState,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+}) => {
+ // If errors, we don't show warnings as those have priority and are action blockers
+ if (state.error) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/transactions/Swap/warnings/SwapPreInputWarnings.tsx b/src/components/transactions/Swap/warnings/SwapPreInputWarnings.tsx
new file mode 100644
index 0000000000..20fd4e2229
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/SwapPreInputWarnings.tsx
@@ -0,0 +1,22 @@
+import { SwapParams, SwapState } from '../types';
+import { CowOpenOrdersWarning } from './preInputs';
+import { NativeLimitOrderInfo } from './preInputs/NativeLimitOrderInfo';
+import { SwapNetworkWarning } from './SwapNetworkWarning';
+
+export const SwapPreInputWarnings = ({
+ params,
+ state,
+}: {
+ params: SwapParams;
+ state: SwapState;
+}) => {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
diff --git a/src/components/transactions/Swap/warnings/constants.ts b/src/components/transactions/Swap/warnings/constants.ts
new file mode 100644
index 0000000000..dcd25b1027
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/constants.ts
@@ -0,0 +1,8 @@
+export const SAFETY_MODULE_TOKENS = [
+ 'stkgho',
+ 'stkaave',
+ 'stkaavewstethbptv2',
+ 'stkbptv2',
+ 'stkbpt',
+ 'stkabpt',
+];
diff --git a/src/components/transactions/Swap/warnings/helpers.ts b/src/components/transactions/Swap/warnings/helpers.ts
new file mode 100644
index 0000000000..b3a1876e2c
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/helpers.ts
@@ -0,0 +1,32 @@
+import {
+ LIQUIDATION_DANGER_THRESHOLD,
+ LIQUIDATION_SAFETY_THRESHOLD,
+} from '../constants/shared.constants';
+
+export const valueLostPercentage = (destValueInUsd: number, srcValueInUsd: number) => {
+ if (destValueInUsd === 0) return 1;
+ if (srcValueInUsd === 0) return 0;
+
+ const receivingPercentage = destValueInUsd / srcValueInUsd;
+ const valueLostPercentage = receivingPercentage ? 1 - receivingPercentage : 0;
+ return valueLostPercentage;
+};
+
+export const shouldShowWarning = (lostValue: number, srcValueInUsd: number) => {
+ if (srcValueInUsd > 500000) return lostValue > 0.03;
+ if (srcValueInUsd > 100000) return lostValue > 0.04;
+ if (srcValueInUsd > 10000) return lostValue > 0.05;
+ if (srcValueInUsd > 1000) return lostValue > 0.07;
+
+ return lostValue > 0.05;
+};
+
+export const shouldRequireConfirmation = (lostValue: number) => {
+ return lostValue > 0.2;
+};
+
+export const shouldRequireConfirmationHFlow = (healthFactor: number) => {
+ return (
+ healthFactor < LIQUIDATION_SAFETY_THRESHOLD && healthFactor >= LIQUIDATION_DANGER_THRESHOLD
+ );
+};
diff --git a/src/components/transactions/Swap/warnings/postInputs/CowAdapterApprovalInfo.tsx b/src/components/transactions/Swap/warnings/postInputs/CowAdapterApprovalInfo.tsx
new file mode 100644
index 0000000000..c86701fc11
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/postInputs/CowAdapterApprovalInfo.tsx
@@ -0,0 +1,31 @@
+import { Trans } from '@lingui/macro';
+import { Typography } from '@mui/material';
+import { Warning } from 'src/components/primitives/Warning';
+import { useModalContext } from 'src/hooks/useModal';
+
+import { SwapState } from '../../types';
+import { SwapProvider, SwapType } from '../../types/shared.types';
+
+export function CowAdapterApprovalInfo({ state }: { state: SwapState }) {
+ const { approvalTxState } = useModalContext();
+
+ const isCow = state.provider === SwapProvider.COW_PROTOCOL;
+ const isAdapterFlow =
+ state.swapType === SwapType.CollateralSwap ||
+ state.swapType === SwapType.DebtSwap ||
+ state.swapType === SwapType.RepayWithCollateral;
+ const isFlashloan = state.useFlashloan;
+
+ if (!isCow || !isAdapterFlow || approvalTxState?.success || !isFlashloan) return null;
+
+ return (
+
+
+
+ A temporary contract will be used to execute the trade. Your wallet may show a warning for
+ approving a new or empty address.
+
+
+
+ );
+}
diff --git a/src/components/transactions/Swap/warnings/postInputs/CustomTokenWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/CustomTokenWarning.tsx
new file mode 100644
index 0000000000..65e83056f4
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/postInputs/CustomTokenWarning.tsx
@@ -0,0 +1,23 @@
+import { Typography } from '@mui/material';
+import { Warning } from 'src/components/primitives/Warning';
+
+import { SwapState, TokenType } from '../../types';
+
+export function CustomTokenWarning({ state }: { state: SwapState }) {
+ if (
+ !(
+ state.sourceToken.tokenType === TokenType.USER_CUSTOM ||
+ state.destinationToken.tokenType === TokenType.USER_CUSTOM
+ )
+ ) {
+ return null;
+ }
+
+ return (
+
+
+ You selected a custom imported token. Make sure it's the right token.
+
+
+ );
+}
diff --git a/src/components/transactions/Swap/warnings/postInputs/GasEstimationWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/GasEstimationWarning.tsx
new file mode 100644
index 0000000000..285d993a39
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/postInputs/GasEstimationWarning.tsx
@@ -0,0 +1,25 @@
+import { Trans } from '@lingui/macro';
+import { Typography } from '@mui/material';
+import { Warning } from 'src/components/primitives/Warning';
+
+import { SwapState } from '../../types';
+
+export function GasEstimationWarning({ state }: { state: SwapState }) {
+ // Check if there's a gas estimation warning in the warnings array
+ const hasGasEstimationWarning = state.warnings.some(
+ (warning) =>
+ warning.message.includes('Gas estimation') || warning.message.includes('gas estimation')
+ );
+
+ if (!hasGasEstimationWarning) return null;
+
+ return (
+
+
+
+ The swap could not be completed. Try increasing slippage or changing the amount.
+
+
+
+ );
+}
diff --git a/src/components/transactions/Swap/warnings/postInputs/HighCostsLimitOrderWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/HighCostsLimitOrderWarning.tsx
new file mode 100644
index 0000000000..91a86d91ae
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/postInputs/HighCostsLimitOrderWarning.tsx
@@ -0,0 +1,104 @@
+import { valueToBigNumber } from '@aave/math-utils';
+import { Trans } from '@lingui/macro';
+import { Typography } from '@mui/material';
+import { useEffect, useMemo } from 'react';
+import { Warning } from 'src/components/primitives/Warning';
+
+import { ActionsBlockedReason, OrderType, SwapState } from '../../types';
+
+/**
+ * Shows a warning on LIMIT orders when total costs exceed 30% of the sell amount.
+ * Additionally, disables actions across ALL swap types if total costs exceed 100%.
+ *
+ * "Total costs" = network fee + partner fee (+ flashloan fee for protocol flows) in USD.
+ */
+export function HighCostsLimitOrderWarning({
+ state,
+ setState,
+}: {
+ state: SwapState;
+ setState: (s: Partial) => void;
+}) {
+ const { costsPercentOfSell } = useMemo(() => {
+ if (!state.sellAmountFormatted || !state.sellAmountUSD) {
+ return { costsPercentOfSell: 0 };
+ }
+
+ // Price per unit in USD derived from useSwapOrderAmounts state
+ const sellAmount = valueToBigNumber(state.sellAmountFormatted || '0');
+ const sellAmountUsd = valueToBigNumber(state.sellAmountUSD || '0');
+ const buyAmount = valueToBigNumber(state.buyAmountFormatted || '0');
+ const buyAmountUsd = valueToBigNumber(state.buyAmountUSD || '0');
+ const sellPriceUnitUsd = sellAmount.isZero()
+ ? 0
+ : sellAmountUsd.dividedBy(sellAmount).toNumber();
+ const buyPriceUnitUsd = buyAmount.isZero() ? 0 : buyAmountUsd.dividedBy(buyAmount).toNumber();
+
+ // Network fee in sell token units -> USD
+ const networkFeeFormatted = state.networkFeeAmountInSellFormatted || '0';
+ const networkFeeUsd = Number(networkFeeFormatted) * sellPriceUnitUsd;
+
+ // Partner fee in "surplus" token; convert to USD using contextual unit price
+ const invertedSide = state.processedSide;
+ const partnerFeeFormatted = state.partnerFeeAmountFormatted || '0';
+ let partnerFeeUsd = 0;
+ if (invertedSide === 'buy') {
+ // Fee in destination/buy token -> use sell leg price per unit
+ partnerFeeUsd = Number(partnerFeeFormatted) * sellPriceUnitUsd;
+ } else {
+ // Fee in source/sell token -> use buy leg price per unit
+ partnerFeeUsd = Number(partnerFeeFormatted) * buyPriceUnitUsd;
+ }
+
+ const totalCostsUsd = (networkFeeUsd || 0) + (partnerFeeUsd || 0);
+ const costsPercentOfSell = sellAmountUsd.gt(0)
+ ? (totalCostsUsd / Number(sellAmountUsd.toString())) * 100
+ : 0;
+
+ return { costsPercentOfSell };
+ }, [
+ state.networkFeeAmountInSellFormatted,
+ state.sellAmountFormatted,
+ state.sellAmountUSD,
+ state.buyAmountFormatted,
+ state.buyAmountUSD,
+ state.partnerFeeAmountFormatted,
+ state.processedSide,
+ ]);
+
+ // Disable actions across ALL swap types when costs exceed 100%
+ useEffect(() => {
+ if (costsPercentOfSell >= 100) {
+ setState({
+ actionsBlocked: {
+ [ActionsBlockedReason.HIGH_COSTS_LIMIT_ORDER]: true,
+ },
+ });
+ } else {
+ setState({
+ actionsBlocked: {
+ [ActionsBlockedReason.HIGH_COSTS_LIMIT_ORDER]: undefined,
+ },
+ });
+ }
+ // Include quote timestamp to recompute when refreshed
+ }, [costsPercentOfSell, state.quoteLastUpdatedAt]);
+
+ // Show warning for LIMIT orders when > 30% and for MARKET orders when >= 100%
+ if (
+ (costsPercentOfSell <= 30 && state.orderType === OrderType.LIMIT) ||
+ (costsPercentOfSell < 100 && state.orderType === OrderType.MARKET)
+ )
+ return null;
+
+ return (
+
+
+
+ Estimated costs are {costsPercentOfSell.toFixed(2)}% of the sell amount. This order is
+ unlikely to be filled.
+
+
+
+ );
+}
diff --git a/src/components/transactions/Swap/warnings/postInputs/HighPriceImpactWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/HighPriceImpactWarning.tsx
new file mode 100644
index 0000000000..698890710b
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/postInputs/HighPriceImpactWarning.tsx
@@ -0,0 +1,107 @@
+import { Trans } from '@lingui/macro';
+import { Box, Checkbox, Typography } from '@mui/material';
+import { Dispatch, useEffect, useMemo, useState } from 'react';
+import { Warning } from 'src/components/primitives/Warning';
+
+import { SwapInputChanges } from '../../analytics/constants';
+import { useHandleAnalytics } from '../../analytics/useTrackAnalytics';
+import { ActionsBlockedReason, actionsBlockedReasonsAmount, SwapState } from '../../types';
+import { shouldRequireConfirmation, shouldShowWarning, valueLostPercentage } from '../helpers';
+
+export function HighPriceImpactWarning({
+ state,
+ setState,
+}: {
+ state: SwapState;
+ setState: Dispatch>;
+}) {
+ const trackingHandlers = useHandleAnalytics({ state });
+ const lostValue = useMemo(() => {
+ if (!state.swapRate) return 0;
+
+ return valueLostPercentage(Number(state.buyAmountUSD), Number(state.sellAmountUSD));
+ }, [state.buyAmountUSD, state.sellAmountUSD]);
+
+ const showWarning = useMemo(() => {
+ if (!state.swapRate) return false;
+ return shouldShowWarning(lostValue, Number(state.sellAmountUSD));
+ }, [state.swapRate, lostValue]);
+
+ const requireConfirmation = useMemo(() => {
+ if (!state.swapRate) return false;
+ return shouldRequireConfirmation(lostValue);
+ }, [state.swapRate, lostValue]);
+
+ const [highPriceImpactConfirmed, setHighPriceImpactConfirmed] = useState(false);
+ useEffect(() => {
+ if (requireConfirmation && !highPriceImpactConfirmed) {
+ setState({
+ actionsBlocked: {
+ [ActionsBlockedReason.HIGH_PRICE_IMPACT]: true,
+ },
+ });
+ } else {
+ setState({
+ actionsBlocked: {
+ [ActionsBlockedReason.HIGH_PRICE_IMPACT]: undefined,
+ },
+ });
+ }
+ }, [requireConfirmation, highPriceImpactConfirmed, state.quoteLastUpdatedAt]);
+
+ if (!showWarning) return null;
+
+ if (actionsBlockedReasonsAmount(state) > 1) return null;
+
+ return (
+
+
+
+ High price impact ({(lostValue * 100).toFixed(1)}%). This route may return{' '}
+ {state.isInvertedSwap ? 'more' : 'less'} due to low liquidity or small order size.
+
+
+
+ {requireConfirmation && (
+
+
+
+ I confirm the swap with a potential {(lostValue * 100).toFixed(0)}% value{' '}
+ {state.isInvertedSwap ? 'increase' : 'loss'}
+
+
+ {
+ const next = !highPriceImpactConfirmed;
+ setHighPriceImpactConfirmed(next);
+ trackingHandlers.trackInputChange(
+ SwapInputChanges.HIGH_PRICE_IMPACT_CONFIRM,
+ next ? 'confirmed' : 'unconfirmed'
+ );
+ }}
+ size="small"
+ data-cy={'high-price-impact-checkbox'}
+ />
+
+ )}
+
+ );
+}
diff --git a/src/components/transactions/Swap/warnings/postInputs/LimitOrderAmountWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/LimitOrderAmountWarning.tsx
new file mode 100644
index 0000000000..ebd524c6f1
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/postInputs/LimitOrderAmountWarning.tsx
@@ -0,0 +1,89 @@
+import { valueToBigNumber } from '@aave/math-utils';
+import { Trans } from '@lingui/macro';
+import { Typography } from '@mui/material';
+import { useMemo } from 'react';
+import { Warning } from 'src/components/primitives/Warning';
+
+import { SwapState } from '../../types';
+import { OrderType } from '../../types/shared.types';
+
+export function LimitOrderAmountWarning({ state }: { state: SwapState }) {
+ const suggestedSlippage = state.swapRate?.suggestedSlippage;
+
+ const [shouldShowWarning, isHigherDifference, differencePercentage] = useMemo(() => {
+ if (state.orderType !== OrderType.LIMIT || !state.swapRate || suggestedSlippage == null) {
+ return [false, false];
+ }
+
+ // Derive token USD unit prices from current quote (spot)
+ const sellAmountUSD = valueToBigNumber(state.sellAmountUSD || '0');
+ const buyAmountUSD = valueToBigNumber(state.buyAmountUSD || '0');
+
+ // User-defined amounts converted to USD using spot unit prices
+ const userInputUsd = sellAmountUSD;
+ const userOutputUsd = buyAmountUSD;
+
+ if (userInputUsd.isZero() || userOutputUsd.isZero()) {
+ return [false, false];
+ }
+
+ // Compute gains vs input in percentage terms
+ const userGainPct = userOutputUsd.minus(userInputUsd).dividedBy(userInputUsd).multipliedBy(100);
+
+ if (userGainPct.isLessThan(0)) {
+ return [false, false];
+ }
+
+ // Apply provider suggested slippage to output side to get a conservative reference
+ const slippageFactor = valueToBigNumber(1).minus(
+ valueToBigNumber(suggestedSlippage).dividedBy(100)
+ );
+ const recommendedInputUsd = valueToBigNumber(state.swapRate.srcSpotUSD || '0');
+ const recommendedMinOutputUsd = valueToBigNumber(
+ state.swapRate.destSpotUSD || '0'
+ ).multipliedBy(slippageFactor);
+ const recommendedGainPct = recommendedMinOutputUsd
+ .minus(recommendedInputUsd)
+ .dividedBy(recommendedInputUsd)
+ .multipliedBy(100);
+
+ // Positive difference means user is worse than recommended reference
+ const diffPct = recommendedGainPct.minus(userGainPct);
+
+ const shouldShow = diffPct.isLessThan(-5); // user's order less favorable than recommended
+ const significant = diffPct.isLessThan(-10); // >= 10% worse considered significant
+
+ return [shouldShow, significant, diffPct];
+ }, [
+ state.orderType,
+ state.swapRate?.srcTokenPriceUsd,
+ state.swapRate?.destTokenPriceUsd,
+ state.sellAmountUSD,
+ state.buyAmountUSD,
+ suggestedSlippage,
+ ]);
+
+ if (!shouldShowWarning) return null;
+
+ return (
+
+
+
+ Your order amounts are {isHigherDifference ? 'significantly ' : ''} less favorable by{' '}
+ {differencePercentage?.abs()?.toFixed(1) ?? '0'}% to the liquidity provider than
+ recommended. This order may not be executed.
+
+
+
+ );
+}
diff --git a/src/components/transactions/Swap/warnings/postInputs/LiquidationCriticalWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/LiquidationCriticalWarning.tsx
new file mode 100644
index 0000000000..b4ea0ff153
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/postInputs/LiquidationCriticalWarning.tsx
@@ -0,0 +1,36 @@
+import { Trans } from '@lingui/macro';
+import { Typography } from '@mui/material';
+import { Dispatch } from 'react';
+import { Warning } from 'src/components/primitives/Warning';
+
+import { SwapParams, SwapState } from '../../types';
+
+export function LiquidationCriticalWarning({
+ state,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+}) {
+ // TODO: move to be an error not a warning and remove isLiquidatable from state.
+ return (
+
+
+
+ Your health factor after this swap will be critically low and may result in liquidation.
+ Please choose a different asset or reduce the swap amount to stay safe.
+
+
+
+ );
+}
diff --git a/src/components/transactions/Swap/warnings/postInputs/LowHealthFactorWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/LowHealthFactorWarning.tsx
new file mode 100644
index 0000000000..55bd9cae86
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/postInputs/LowHealthFactorWarning.tsx
@@ -0,0 +1,83 @@
+import { Trans } from '@lingui/macro';
+import { Box, Checkbox, Typography } from '@mui/material';
+import { Dispatch, useEffect, useState } from 'react';
+import { Warning } from 'src/components/primitives/Warning';
+
+import { ActionsBlockedReason, SwapParams, SwapState } from '../../types';
+import { shouldRequireConfirmationHFlow } from '../helpers';
+
+export function LowHealthFactorWarning({
+ state,
+ setState,
+}: {
+ params: SwapParams;
+ state: SwapState;
+ setState: Dispatch>;
+}) {
+ const [lowHFConfirmed, setLowHFConfirmed] = useState(false);
+ const requireConfirmationHFlow = state.isHFLow
+ ? shouldRequireConfirmationHFlow(Number(state.hfAfterSwap))
+ : false;
+
+ useEffect(() => {
+ if (requireConfirmationHFlow && !lowHFConfirmed) {
+ setState({
+ actionsBlocked: {
+ [ActionsBlockedReason.LOW_HEALTH_FACTOR]: true,
+ },
+ });
+ } else {
+ setState({
+ actionsBlocked: {
+ [ActionsBlockedReason.LOW_HEALTH_FACTOR]: undefined,
+ },
+ });
+ }
+ }, [requireConfirmationHFlow, lowHFConfirmed, state.quoteLastUpdatedAt]);
+
+ if (state.isLiquidatable || !requireConfirmationHFlow) {
+ return null;
+ }
+
+ return (
+
+
+
+ Low health factor after swap. Your position will carry a higher risk of liquidation.
+
+
+ {!state.actionsBlocked[ActionsBlockedReason.LOW_HEALTH_FACTOR] && (
+
+
+ I understand the liquidation risk and want to proceed
+
+ {
+ setLowHFConfirmed(!lowHFConfirmed);
+ }}
+ size="small"
+ data-cy={'low-hf-checkbox'}
+ />
+
+ )}
+
+ );
+}
diff --git a/src/components/transactions/Swap/warnings/postInputs/SafetyModuleSwapWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/SafetyModuleSwapWarning.tsx
new file mode 100644
index 0000000000..c2f655ca4c
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/postInputs/SafetyModuleSwapWarning.tsx
@@ -0,0 +1,28 @@
+import { Trans } from '@lingui/macro';
+import { Typography } from '@mui/material';
+import { Link } from 'src/components/primitives/Link';
+import { Warning } from 'src/components/primitives/Warning';
+
+import { SwapState } from '../../types';
+import { SAFETY_MODULE_TOKENS } from '../constants';
+
+export function SafetyModuleSwapWarning({ state }: { state: SwapState }) {
+ const isSwappingSafetyModuleToken = SAFETY_MODULE_TOKENS.includes(
+ state.sourceToken.symbol.toLowerCase()
+ );
+ if (!isSwappingSafetyModuleToken) return null;
+
+ return (
+
+
+
+ For swapping safety module assets please unstake your position{' '}
+ close()}>
+ here
+
+ .
+
+
+
+ );
+}
diff --git a/src/components/transactions/Swap/warnings/postInputs/SlippageWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/SlippageWarning.tsx
new file mode 100644
index 0000000000..7131b65fc3
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/postInputs/SlippageWarning.tsx
@@ -0,0 +1,17 @@
+import { Typography } from '@mui/material';
+import { Warning } from 'src/components/primitives/Warning';
+
+import { OrderType, SwapState } from '../../types';
+
+export function SlippageWarning({ state }: { state: SwapState }) {
+ if (!state.showSlippageWarning) return null;
+ if (state.orderType === OrderType.LIMIT) return null;
+
+ return (
+
+
+ Slippage is lower than recommended. The swap may be delayed or fail.
+
+
+ );
+}
diff --git a/src/components/transactions/Swap/warnings/postInputs/USDTResetWarning.tsx b/src/components/transactions/Swap/warnings/postInputs/USDTResetWarning.tsx
new file mode 100644
index 0000000000..1c8f421169
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/postInputs/USDTResetWarning.tsx
@@ -0,0 +1,20 @@
+import { Trans } from '@lingui/macro';
+import { Typography } from '@mui/material';
+import { Warning } from 'src/components/primitives/Warning';
+
+import { SwapState } from '../../types';
+
+export function USDTResetWarning({ state }: { state: SwapState }) {
+ if (!state.requiresApprovalReset) return null;
+
+ return (
+
+
+
+ USDT on Ethereum requires approval reset before a new approval. This will require an
+ additional transaction.
+
+
+
+ );
+}
diff --git a/src/components/transactions/Swap/warnings/postInputs/index.ts b/src/components/transactions/Swap/warnings/postInputs/index.ts
new file mode 100644
index 0000000000..d79bf5530c
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/postInputs/index.ts
@@ -0,0 +1,11 @@
+export * from './CowAdapterApprovalInfo';
+export * from './CustomTokenWarning';
+export * from './GasEstimationWarning';
+export * from './HighCostsLimitOrderWarning';
+export * from './HighPriceImpactWarning';
+export * from './LimitOrderAmountWarning';
+export * from './LiquidationCriticalWarning';
+export * from './LowHealthFactorWarning';
+export * from './SafetyModuleSwapWarning';
+export * from './SlippageWarning';
+export * from './USDTResetWarning';
diff --git a/src/components/transactions/Swap/warnings/preInputs/CowOpenOrdersWarning.tsx b/src/components/transactions/Swap/warnings/preInputs/CowOpenOrdersWarning.tsx
new file mode 100644
index 0000000000..21ba7cf07c
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/preInputs/CowOpenOrdersWarning.tsx
@@ -0,0 +1,82 @@
+import { normalize } from '@aave/math-utils';
+import { OrderStatus } from '@cowprotocol/cow-sdk';
+import { Link, Typography } from '@mui/material';
+import { useEffect, useState } from 'react';
+import { Warning } from 'src/components/primitives/Warning';
+import { useSwapOrdersTracking } from 'src/hooks/useSwapOrdersTracking';
+import { useRootStore } from 'src/store/root';
+import { findByChainId } from 'src/ui-config/marketsConfig';
+
+import { getOrders } from '../../helpers/cow/orders.helpers';
+import { SwapState } from '../../types';
+
+export function CowOpenOrdersWarning({ state }: { state: SwapState }) {
+ const user = useRootStore((store) => store.account);
+ const { hasActiveOrderForSellToken } = useSwapOrdersTracking();
+ const [cowOpenOrdersTotalAmountFormatted, setCowOpenOrdersTotalAmountFormatted] = useState<
+ string | undefined
+ >(undefined);
+
+ useEffect(() => {
+ if (
+ state.provider == 'cowprotocol' &&
+ user &&
+ state.chainId &&
+ state.sourceToken &&
+ state.destinationToken
+ ) {
+ setCowOpenOrdersTotalAmountFormatted(undefined);
+
+ getOrders(state.chainId, user).then((orders) => {
+ const token = state.sellAmountToken?.addressToSwap;
+
+ if (!token) {
+ return;
+ }
+
+ const cowOpenOrdersTotalAmount = orders
+ .filter(
+ (order) =>
+ order.sellToken.toLowerCase() == token.toLowerCase() &&
+ order.status == OrderStatus.OPEN
+ )
+ .map((order) => order.sellAmount)
+ .reduce((acc, curr) => acc + Number(curr), 0);
+ if (cowOpenOrdersTotalAmount > 0) {
+ setCowOpenOrdersTotalAmountFormatted(
+ normalize(cowOpenOrdersTotalAmount, state.sourceToken.decimals).toString()
+ );
+ } else {
+ setCowOpenOrdersTotalAmountFormatted(undefined);
+ }
+ });
+ } else {
+ setCowOpenOrdersTotalAmountFormatted(undefined);
+ }
+ }, [state.sourceToken, state.destinationToken, state.provider, state.chainId, user]);
+
+ const hasActiveForToken =
+ !!state.chainId && !!state.sourceToken?.addressToSwap
+ ? hasActiveOrderForSellToken(state.chainId, state.sourceToken.addressToSwap)
+ : false;
+
+ if (!cowOpenOrdersTotalAmountFormatted && !hasActiveForToken) return null;
+
+ return (
+
+
+ {cowOpenOrdersTotalAmountFormatted ? (
+ <>
+ You have open orders for {cowOpenOrdersTotalAmountFormatted} {state.sourceToken.symbol}.{' '}
+ >
+ ) : (
+ <>You have in-progress swaps for {state.sourceToken.symbol}. >
+ )}
+
Track them in your{' '}
+
+ transaction history
+
+
+
+ );
+}
diff --git a/src/components/transactions/Swap/warnings/preInputs/NativeLimitOrderInfo.tsx b/src/components/transactions/Swap/warnings/preInputs/NativeLimitOrderInfo.tsx
new file mode 100644
index 0000000000..3cdd52ac6d
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/preInputs/NativeLimitOrderInfo.tsx
@@ -0,0 +1,25 @@
+import { Trans } from '@lingui/macro';
+import { Typography } from '@mui/material';
+import { Warning } from 'src/components/primitives/Warning';
+
+import { SwapParams, SwapProvider, SwapState, SwapType, TokenType } from '../../types';
+
+export function NativeLimitOrderInfo({ state, params }: { state: SwapState; params: SwapParams }) {
+ // Classic swaps only; show when input token is native
+ const isClassicSwap = params.swapType === SwapType.Swap;
+ const isNativeInput = state.sourceToken?.tokenType === TokenType.NATIVE;
+ const isCoWProtocol = state.provider === SwapProvider.COW_PROTOCOL;
+
+ if (!isClassicSwap || !isNativeInput || !isCoWProtocol) return null;
+
+ return (
+
+
+
+ For security reasons, limit orders are not supported for Native tokens. To place a limit
+ order, use the wrapped version.
+
+
+
+ );
+}
diff --git a/src/components/transactions/Swap/warnings/preInputs/index.ts b/src/components/transactions/Swap/warnings/preInputs/index.ts
new file mode 100644
index 0000000000..209ba1587f
--- /dev/null
+++ b/src/components/transactions/Swap/warnings/preInputs/index.ts
@@ -0,0 +1 @@
+export * from './CowOpenOrdersWarning';
diff --git a/src/components/transactions/Switch/BaseSwitchModal.tsx b/src/components/transactions/Switch/BaseSwitchModal.tsx
deleted file mode 100644
index 77ed0e8f62..0000000000
--- a/src/components/transactions/Switch/BaseSwitchModal.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import { Box, CircularProgress } from '@mui/material';
-import React, { useEffect, useMemo, useState } from 'react';
-import { supportedNetworksWithEnabledMarket } from 'src/components/transactions/Switch/common';
-import { TokenInfoWithBalance, useTokensBalance } from 'src/hooks/generic/useTokensBalance';
-import { useModalContext } from 'src/hooks/useModal';
-import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
-import { useRootStore } from 'src/store/root';
-import { TOKEN_LIST } from 'src/ui-config/TokenList';
-import { CustomMarket, getNetworkConfig, marketsData } from 'src/utils/marketsAndNetworksConfig';
-
-import { BaseSwitchModalContent, SwitchModalCustomizableProps } from './BaseSwitchModalContent';
-
-const defaultNetwork = marketsData[CustomMarket.proto_mainnet_v3];
-
-export const getFilteredTokensForSwitch = (chainId: number): TokenInfoWithBalance[] => {
- let customTokenList = TOKEN_LIST.tokens;
- const savedCustomTokens = localStorage.getItem('customTokens');
- if (savedCustomTokens) {
- customTokenList = customTokenList.concat(JSON.parse(savedCustomTokens));
- }
-
- const transformedTokens = customTokenList.map((token) => {
- return { ...token, balance: '0' };
- });
- const realChainId = getNetworkConfig(chainId).underlyingChainId ?? chainId;
-
- // Remove duplicates
- const seen = new Set();
- return transformedTokens
- .filter((token) => token.chainId === realChainId)
- .filter((token) => {
- const key = `${token.chainId}:${token.address.toLowerCase()}`;
- if (seen.has(key)) return false;
- seen.add(key);
- return true;
- });
-};
-
-export const BaseSwitchModal = ({
- modalType,
- inputBalanceTitle: balanceTitle,
- forcedDefaultInputToken,
- forcedDefaultOutputToken,
- suggestedDefaultInputToken,
- suggestedDefaultOutputToken,
- tokensFrom: forcedTokensFrom,
- tokensTo: forcedTokensTo,
- showSwitchInputAndOutputAssetsButton = true,
- forcedChainId,
-}: SwitchModalCustomizableProps) => {
- const {
- args: { chainId },
- } = useModalContext();
-
- const overallAppChainId = useRootStore((store) => store.currentChainId);
- const { chainId: connectedChainId } = useWeb3Context();
- const user = useRootStore((store) => store.account);
-
- const [selectedChainId, setSelectedChainId] = useState(() => {
- if (forcedChainId) {
- return forcedChainId;
- }
- if (supportedNetworksWithEnabledMarket.find((elem) => elem.chainId === overallAppChainId))
- return overallAppChainId;
- return defaultNetwork.chainId;
- });
-
- useEffect(() => {
- if (forcedChainId) {
- setSelectedChainId(forcedChainId);
- return;
- }
-
- // Passing chainId as prop will set default network for switch modal
- if (chainId && supportedNetworksWithEnabledMarket.find((elem) => elem.chainId === chainId)) {
- setSelectedChainId(chainId);
- } else if (
- connectedChainId &&
- supportedNetworksWithEnabledMarket.find((elem) => elem.chainId === connectedChainId)
- ) {
- const supportedFork = supportedNetworksWithEnabledMarket.find(
- (elem) => elem.underlyingChainId === connectedChainId
- );
- setSelectedChainId(supportedFork ? supportedFork.chainId : connectedChainId);
- } else if (
- supportedNetworksWithEnabledMarket.find((elem) => elem.chainId === overallAppChainId)
- ) {
- setSelectedChainId(overallAppChainId);
- } else {
- setSelectedChainId(defaultNetwork.chainId);
- }
- }, [overallAppChainId, chainId, connectedChainId, forcedChainId]);
-
- const initialDefaultTokens = useMemo(
- () => getFilteredTokensForSwitch(selectedChainId),
- [selectedChainId]
- );
-
- const {
- data: initialTokens,
- refetch: refetchInitialTokens,
- isFetching: tokensLoading,
- } = useTokensBalance(initialDefaultTokens, selectedChainId, user);
-
- if (tokensLoading) {
- return (
-
-
-
- );
- }
-
- return (
- <>
- refetchInitialTokens()}
- />
- >
- );
-};
diff --git a/src/components/transactions/Switch/BaseSwitchModalContent.tsx b/src/components/transactions/Switch/BaseSwitchModalContent.tsx
deleted file mode 100644
index 93ac3e475a..0000000000
--- a/src/components/transactions/Switch/BaseSwitchModalContent.tsx
+++ /dev/null
@@ -1,1175 +0,0 @@
-import { normalize, normalizeBN, valueToBigNumber } from '@aave/math-utils';
-import { OrderStatus, SupportedChainId, WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/cow-sdk';
-import { SwitchVerticalIcon } from '@heroicons/react/outline';
-import { Trans } from '@lingui/macro';
-import { Box, Checkbox, CircularProgress, IconButton, SvgIcon, Typography } from '@mui/material';
-import { useQueryClient } from '@tanstack/react-query';
-import { BigNumber } from 'bignumber.js';
-import { debounce } from 'lodash';
-import React, { useEffect, useMemo, useState } from 'react';
-import { BasicModal } from 'src/components/primitives/BasicModal';
-import { Link } from 'src/components/primitives/Link';
-import { Warning } from 'src/components/primitives/Warning';
-import { isSafeWallet, isSmartContractWallet } from 'src/helpers/provider';
-import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider';
-import { TokenInfoWithBalance } from 'src/hooks/generic/useTokensBalance';
-import { useMultiProviderSwitchRates } from 'src/hooks/switch/useMultiProviderSwitchRates';
-import { useIsWrongNetwork } from 'src/hooks/useIsWrongNetwork';
-import { ModalType, useModalContext } from 'src/hooks/useModal';
-import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
-import { getEthersProvider } from 'src/libs/web3-data-provider/adapters/EthersAdapter';
-import { useRootStore } from 'src/store/root';
-import { findByChainId } from 'src/ui-config/marketsConfig';
-import { queryKeysFactory } from 'src/ui-config/queries';
-import { TokenInfo } from 'src/ui-config/TokenList';
-import { wagmiConfig } from 'src/ui-config/wagmiConfig';
-import { GENERAL } from 'src/utils/events';
-import { calculateHFAfterSwap } from 'src/utils/hfUtils';
-import { getNetworkConfig } from 'src/utils/marketsAndNetworksConfig';
-import { parseUnits } from 'viem';
-
-import { TxModalTitle } from '../FlowCommons/TxModalTitle';
-import { ChangeNetworkWarning } from '../Warnings/ChangeNetworkWarning';
-import { ParaswapErrorDisplay } from '../Warnings/ParaswapErrorDisplay';
-import { SupportedNetworkWithChainId } from './common';
-import { getOrders, isNativeToken } from './cowprotocol/cowprotocol.helpers';
-import { NetworkSelector } from './NetworkSelector';
-import { getParaswapSlippage } from './slippage.helpers';
-import { isCowProtocolRates } from './switch.types';
-import { SwitchActions } from './SwitchActions';
-import { SwitchAssetInput } from './SwitchAssetInput';
-import { SwitchErrors } from './SwitchErrors';
-import { SwitchModalTxDetails } from './SwitchModalTxDetails';
-import { SwitchRates } from './SwitchRates';
-import { SwitchSlippageSelector } from './SwitchSlippageSelector';
-import { SwitchTxSuccessView } from './SwitchTxSuccessView';
-import { validateSlippage, ValidationSeverity } from './validation.helpers';
-
-const SAFETY_MODULE_TOKENS = [
- 'stkgho',
- 'stkaave',
- 'stkaavewstethbptv2',
- 'stkbptv2',
- 'stkbpt',
- 'stkabpt',
-];
-
-const LIQUIDATION_SAFETY_THRESHOLD = 1.05;
-const LIQUIDATION_DANGER_THRESHOLD = 1.01;
-const SESSION_STORAGE_EXPIRY_MS = 15 * 60 * 1000;
-
-const valueLostPercentage = (destValueInUsd: number, srcValueInUsd: number) => {
- if (destValueInUsd === 0) return 1;
- if (srcValueInUsd === 0) return 0;
-
- const receivingPercentage = destValueInUsd / srcValueInUsd;
- const valueLostPercentage = receivingPercentage ? 1 - receivingPercentage : 0;
- return valueLostPercentage;
-};
-
-const shouldShowWarning = (lostValue: number, srcValueInUsd: number) => {
- if (srcValueInUsd > 500000) return lostValue > 0.03;
- if (srcValueInUsd > 100000) return lostValue > 0.04;
- if (srcValueInUsd > 10000) return lostValue > 0.05;
- if (srcValueInUsd > 1000) return lostValue > 0.07;
-
- return lostValue > 0.05;
-};
-
-const shouldRequireConfirmation = (lostValue: number) => {
- return lostValue > 0.2;
-};
-const shouldRequireConfirmationHFlow = (healthFactor: number) => {
- return (
- healthFactor < LIQUIDATION_SAFETY_THRESHOLD && healthFactor >= LIQUIDATION_DANGER_THRESHOLD
- );
-};
-
-export interface SwitchModalCustomizableProps {
- modalType: ModalType;
- inputBalanceTitle?: string;
- outputBalanceTitle?: string;
- tokensFrom?: TokenInfoWithBalance[];
- tokensTo?: TokenInfoWithBalance[];
- forcedDefaultInputToken?: TokenInfoWithBalance;
- forcedDefaultOutputToken?: TokenInfoWithBalance;
- suggestedDefaultInputToken?: TokenInfoWithBalance;
- suggestedDefaultOutputToken?: TokenInfoWithBalance;
- showSwitchInputAndOutputAssetsButton?: boolean;
- forcedChainId?: number;
-}
-
-export const BaseSwitchModalContent = ({
- showSwitchInputAndOutputAssetsButton = true,
- showTitle = true,
- forcedDefaultInputToken,
- forcedDefaultOutputToken,
- suggestedDefaultInputToken,
- suggestedDefaultOutputToken,
- supportedNetworks,
- inputBalanceTitle,
- outputBalanceTitle,
- initialFromTokens,
- initialToTokens,
- showChangeNetworkWarning = true,
- modalType,
- selectedChainId,
- setSelectedChainId,
- refetchInitialTokens,
-}: {
- showTitle?: boolean;
- forcedChainId: number;
- showSwitchInputAndOutputAssetsButton?: boolean;
- forcedDefaultInputToken?: TokenInfoWithBalance;
- initialFromTokens: TokenInfoWithBalance[];
- initialToTokens: TokenInfoWithBalance[];
- forcedDefaultOutputToken?: TokenInfoWithBalance;
- suggestedDefaultInputToken?: TokenInfoWithBalance;
- suggestedDefaultOutputToken?: TokenInfoWithBalance;
- supportedNetworks: SupportedNetworkWithChainId[];
- showChangeNetworkWarning?: boolean;
- modalType: ModalType;
- selectedChainId: number;
- setSelectedChainId: (chainId: number) => void;
- refetchInitialTokens: () => void;
-} & SwitchModalCustomizableProps) => {
- // State
- const [inputAmount, setInputAmount] = useState('');
- const [debounceInputAmount, setDebounceInputAmount] = useState('');
- const { mainTxState: switchTxState, gasLimit, txError, setTxError, close } = useModalContext();
- const user = useRootStore((store) => store.account);
- const { readOnlyModeAddress, chainId: connectedChainId } = useWeb3Context();
- const trackEvent = useRootStore((store) => store.trackEvent);
- const [showUSDTResetWarning, setShowUSDTResetWarning] = useState(false);
- const [highPriceImpactConfirmed, setHighPriceImpactConfirmed] = useState(false);
- const [lowHFConfirmed, setLowHFConfirmed] = useState(false);
- const selectedNetworkConfig = getNetworkConfig(selectedChainId);
- const isWrongNetwork = useIsWrongNetwork(selectedChainId);
- const [isSwapFlowSelected, setIsSwapFlowSelected] = useState(false);
- const [isExecutingActions, setIsExecutingActions] = useState(false);
-
- const [userIsSmartContractWallet, setUserIsSmartContractWallet] = useState(false);
- const [userIsSafeWallet, setUserIsSafeWallet] = useState(false);
-
- useEffect(() => {
- try {
- if (user && connectedChainId) {
- getEthersProvider(wagmiConfig, { chainId: connectedChainId }).then((provider) => {
- Promise.all([isSmartContractWallet(user, provider), isSafeWallet(user, provider)]).then(
- ([isSmartContract, isSafe]) => {
- setUserIsSmartContractWallet(isSmartContract);
- setUserIsSafeWallet(isSafe);
- }
- );
- });
- }
- } catch (error) {
- console.error(error);
- }
- }, [user, connectedChainId]);
-
- const debouncedInputChange = useMemo(() => {
- return debounce((value: string) => {
- setDebounceInputAmount(value);
- }, 1500);
- }, [setDebounceInputAmount]);
-
- const handleInputChange = (value: string) => {
- setTxError(undefined);
- setHighPriceImpactConfirmed(false);
- setLowHFConfirmed(false);
- if (value === '-1') {
- // Max Selected
- setInputAmount(selectedInputToken.balance);
- debouncedInputChange(selectedInputToken.balance);
- } else {
- setInputAmount(value);
- debouncedInputChange(value);
- }
- };
-
- const handleSelectedInputToken = (token: TokenInfoWithBalance) => {
- if (!initialFromTokens?.find((t) => t.address === token.address)) {
- addNewToken(token).then(() => {
- setSelectedInputToken(token);
- saveTokenSelection(token, selectedOutputToken);
- setTxError(undefined);
- });
- } else {
- setSelectedInputToken(token);
- saveTokenSelection(token, selectedOutputToken);
- setTxError(undefined);
- }
- };
-
- const handleSelectedOutputToken = (token: TokenInfoWithBalance) => {
- if (!initialToTokens?.find((t) => t.address === token.address)) {
- addNewToken(token).then(() => {
- setSelectedOutputToken(token);
- saveTokenSelection(selectedInputToken, token);
- setTxError(undefined);
- });
- } else {
- setSelectedOutputToken(token);
- saveTokenSelection(selectedInputToken, token);
- setTxError(undefined);
- }
- };
-
- const onSwitchReserves = () => {
- const fromToken = selectedInputToken;
- const toToken = selectedOutputToken;
- const toInput = switchRates
- ? normalizeBN(switchRates.destAmount, switchRates.destDecimals).toString()
- : '0';
- setSelectedInputToken(toToken);
- setSelectedOutputToken(fromToken);
- setInputAmount(toInput);
- setDebounceInputAmount(toInput);
- setTxError(undefined);
- };
-
- const handleSelectedNetworkChange = (value: number) => {
- setTxError(undefined);
- setSelectedChainId(value);
- // Reset input amount when changing networks
- setInputAmount('');
- setDebounceInputAmount('');
- refetchInitialTokens();
- };
-
- const queryClient = useQueryClient();
- const addNewToken = async (token: TokenInfoWithBalance) => {
- queryClient.setQueryData(
- queryKeysFactory.tokensBalance(
- initialFromTokens.concat(initialToTokens) ?? [],
- selectedChainId,
- user
- ),
- (oldData) => {
- if (oldData)
- return [...oldData, token].sort((a, b) => Number(b.balance) - Number(a.balance));
- return [token];
- }
- );
- const customTokens = localStorage.getItem('customTokens');
- const newTokenInfo = {
- address: token.address,
- symbol: token.symbol,
- decimals: token.decimals,
- chainId: token.chainId,
- name: token.name,
- logoURI: token.logoURI,
- extensions: {
- isUserCustom: true,
- },
- };
- if (customTokens) {
- const parsedCustomTokens: TokenInfo[] = JSON.parse(customTokens);
- parsedCustomTokens.push(newTokenInfo);
- localStorage.setItem('customTokens', JSON.stringify(parsedCustomTokens));
- } else {
- localStorage.setItem('customTokens', JSON.stringify([newTokenInfo]));
- }
- };
-
- const { defaultInputToken, defaultOutputToken } = useMemo(() => {
- let auxInputToken = forcedDefaultInputToken || suggestedDefaultInputToken;
- let auxOutputToken = forcedDefaultOutputToken || suggestedDefaultOutputToken;
-
- const fromList = initialFromTokens;
- const toList = initialToTokens;
-
- if (!auxInputToken) {
- auxInputToken = fromList.find(
- (token) => (token.balance !== '0' || token.extensions?.isNative) && token.symbol !== 'GHO'
- );
- }
-
- if (!auxOutputToken) {
- auxOutputToken = toList.find((token) => token.symbol == 'GHO');
- }
-
- return {
- defaultInputToken: auxInputToken ?? fromList[0],
- defaultOutputToken: auxOutputToken ?? toList[1],
- };
- }, [initialFromTokens, initialToTokens]);
-
- // Persist selected tokens in session storage to retain them on modal close/open but differentiating by modalType
- const getStorageKey = (modalType: ModalType, chainId: number) => {
- if (ModalType.CollateralSwap === modalType) {
- return `aave_switch_tokens_${modalType}_${chainId}_${forcedDefaultInputToken?.aToken?.toLowerCase()}`;
- } else {
- return `aave_switch_tokens_${modalType}_${chainId}`;
- }
- };
-
- const saveTokenSelection = (
- inputToken: TokenInfoWithBalance,
- outputToken: TokenInfoWithBalance
- ) => {
- try {
- sessionStorage.setItem(
- getStorageKey(modalType, selectedChainId),
- JSON.stringify({
- inputToken: forcedDefaultInputToken ? null : inputToken,
- outputToken: forcedDefaultOutputToken ? null : outputToken,
- timestamp: Date.now(),
- })
- );
- } catch (e) {
- console.error('Error saving token selection', e);
- }
- };
-
- const loadTokenSelection = () => {
- try {
- const savedTokenSelection = sessionStorage.getItem(getStorageKey(modalType, selectedChainId));
- if (!savedTokenSelection) return null;
-
- const parsedTokenSelection = JSON.parse(savedTokenSelection);
- if (
- parsedTokenSelection.timestamp &&
- Date.now() - parsedTokenSelection.timestamp > SESSION_STORAGE_EXPIRY_MS
- ) {
- sessionStorage.removeItem(getStorageKey(modalType, selectedChainId));
- return null;
- }
- return parsedTokenSelection;
- } catch (e) {
- return null;
- }
- };
- const [selectedInputToken, setSelectedInputToken] = useState(() => {
- if (forcedDefaultInputToken) return forcedDefaultInputToken;
-
- const saved = loadTokenSelection();
- return saved?.inputToken || defaultInputToken;
- });
- const [selectedOutputToken, setSelectedOutputToken] = useState(() => {
- if (forcedDefaultOutputToken) return forcedDefaultOutputToken;
-
- const saved = loadTokenSelection();
- return saved?.outputToken || defaultOutputToken;
- });
-
- // Update selected tokens when defaults change (e.g., after network change)
- useEffect(() => {
- if (
- !forcedDefaultInputToken &&
- defaultInputToken &&
- selectedInputToken?.chainId !== selectedChainId
- ) {
- setSelectedInputToken(defaultInputToken);
- }
- if (
- !forcedDefaultOutputToken &&
- defaultOutputToken &&
- selectedOutputToken?.chainId !== selectedChainId
- ) {
- setSelectedOutputToken(defaultOutputToken);
- }
- }, [
- defaultInputToken,
- defaultOutputToken,
- selectedChainId,
- forcedDefaultInputToken,
- forcedDefaultOutputToken,
- selectedInputToken?.chainId,
- selectedOutputToken?.chainId,
- ]);
-
- // User and reserves (for HF and flashloan decision)
- const { user: extendedUser, reserves } = useAppDataContext();
- const poolReserve = useMemo(
- () =>
- reserves.find(
- (r) => r.underlyingAsset.toLowerCase() === selectedInputToken?.address.toLowerCase()
- ),
- [reserves, selectedInputToken]
- );
- const targetReserve = useMemo(
- () =>
- reserves.find(
- (r) => r.underlyingAsset.toLowerCase() === selectedOutputToken?.address.toLowerCase()
- ),
- [reserves, selectedOutputToken]
- );
- const userReserve = useMemo(
- () =>
- extendedUser?.userReservesData.find(
- (ur) => ur.underlyingAsset.toLowerCase() === selectedInputToken?.address.toLowerCase()
- ),
- [extendedUser, selectedInputToken]
- );
-
- const [shouldUseFlashloan, setShouldUseFlashloan] = useState(undefined);
-
- // Data
- const {
- data: switchRates,
- error: ratesError,
- isFetching: ratesLoading,
- } = useMultiProviderSwitchRates({
- chainId: selectedChainId,
- amount:
- debounceInputAmount === ''
- ? '0'
- : normalizeBN(debounceInputAmount, -1 * selectedInputToken.decimals).toFixed(0),
- srcUnderlyingToken: selectedInputToken?.address,
- srcAToken: selectedInputToken?.aToken,
- srcDecimals: selectedInputToken?.decimals,
- destUnderlyingToken: selectedOutputToken?.address,
- destAToken: selectedOutputToken?.aToken,
- destDecimals: selectedOutputToken?.decimals,
- inputSymbol: selectedInputToken?.symbol,
- outputSymbol: selectedOutputToken?.symbol,
- isInputTokenCustom: !!selectedInputToken?.extensions?.isUserCustom,
- isOutputTokenCustom: !!selectedOutputToken?.extensions?.isUserCustom,
- user,
- options: {
- partner: 'aave-widget',
- },
- modalType,
- isTxSuccess: switchTxState.success,
- shouldUseFlashloan,
- isExecutingActions,
- });
-
- const [slippage, setSlippage] = useState(switchRates?.provider == 'cowprotocol' ? '0.5' : '0.10');
- const [showGasStation, setShowGasStation] = useState(switchRates?.provider == 'paraswap');
-
- const slippageValidation = validateSlippage(
- slippage,
- selectedChainId,
- isNativeToken(selectedInputToken?.address),
- switchRates?.provider
- );
-
- const safeSlippage =
- slippageValidation && slippageValidation.severity === ValidationSeverity.ERROR
- ? 0
- : Number(slippage) / 100;
- // wether we use cow's suggested slippage or paraswap's correlated assets slippage default
- const autoSlippage = useMemo(() => {
- if (!switchRates) return undefined;
-
- if (switchRates.provider === 'cowprotocol') {
- return switchRates.suggestedSlippage?.toString();
- }
-
- if (switchRates.provider === 'paraswap') {
- return getParaswapSlippage(
- selectedInputToken?.symbol || '',
- selectedOutputToken?.symbol || ''
- );
- }
-
- return undefined;
- }, [
- switchRates?.provider,
- switchRates?.suggestedSlippage,
- selectedInputToken?.symbol,
- selectedOutputToken?.symbol,
- ]);
-
- useEffect(() => {
- if (ratesError) {
- console.error('tracking error', ratesError);
- trackEvent('Swap Error', {
- 'Error Message': ratesError.message,
- 'Error Name': ratesError.name,
- 'Error Stack': ratesError.stack,
- 'Input Token': selectedInputToken.symbol,
- 'Output Token': selectedOutputToken.symbol,
- 'Input Amount': debounceInputAmount,
- 'Output Amount': normalizeBN(
- switchRates?.provider === 'cowprotocol'
- ? switchRates?.destSpot
- : switchRates?.destAmount || 0,
- switchRates?.destDecimals || 18
- ).toString(),
- 'Input Amount USD': switchRates?.srcUSD,
- 'Output Amount USD': switchRates?.destUSD,
- Slippage: safeSlippage,
- });
- }
- }, [ratesError]);
-
- useEffect(() => {
- if (txError && txError.actionBlocked) {
- console.error('tracking error', txError);
- trackEvent('Swap Tx Error', {
- 'Error Message': txError.error?.toString(),
- 'Error Raw': txError.rawError?.toString(),
- 'Error Action': txError.txAction,
- 'Input Token': selectedInputToken.symbol,
- 'Output Token': selectedOutputToken.symbol,
- 'Input Amount': debounceInputAmount,
- 'Output Amount': normalizeBN(
- switchRates?.provider === 'cowprotocol'
- ? switchRates?.destSpot
- : switchRates?.destAmount || 0,
- switchRates?.destDecimals || 18
- ).toString(),
- 'Input Amount USD': switchRates?.srcUSD,
- 'Output Amount USD': switchRates?.destUSD,
- Slippage: safeSlippage,
- });
- }
- }, [txError]);
-
- // Compute HF effect of withdrawing inputAmount (copied from SwitchModalTxDetails)
- const { hfEffectOfFromAmount, hfAfterSwap } = useMemo(() => {
- try {
- if (!poolReserve || !userReserve || !extendedUser || !switchRates || !targetReserve)
- return { hfEffectOfFromAmount: '0' };
-
- // Amounts in human units (mirror SwitchModalTxDetails: intent uses destSpot, market uses destAmount)
- const fromAmount = normalizeBN(switchRates.srcAmount, switchRates.srcDecimals).toString();
- const toAmountRaw = normalizeBN(
- switchRates.provider === 'cowprotocol' ? switchRates.destSpot : switchRates.destAmount,
- switchRates.destDecimals
- ).toString();
- const toAmountAfterSlippage = valueToBigNumber(toAmountRaw)
- .multipliedBy(1 - safeSlippage)
- .toString();
-
- const { hfEffectOfFromAmount, hfAfterSwap } = calculateHFAfterSwap({
- fromAmount,
- fromAssetData: poolReserve,
- fromAssetUserData: userReserve,
- user: extendedUser,
- toAmountAfterSlippage: toAmountAfterSlippage,
- toAssetData: targetReserve,
- });
-
- return {
- hfEffectOfFromAmount: hfEffectOfFromAmount.toString(),
- hfAfterSwap: hfAfterSwap.toString(),
- };
- } catch {
- return { hfEffectOfFromAmount: '0', hfAfterSwap: undefined };
- }
- }, [poolReserve, userReserve, extendedUser, targetReserve, switchRates, safeSlippage]);
-
- const isHFLow = useMemo(() => {
- if (!hfAfterSwap) return false;
-
- const hfNumber = new BigNumber(hfAfterSwap);
-
- if (hfNumber.lt(0)) return false;
-
- return hfNumber.lt(LIQUIDATION_SAFETY_THRESHOLD) && hfNumber.gte(LIQUIDATION_DANGER_THRESHOLD);
- }, [hfAfterSwap]);
- const isLiquidatable = useMemo(() => {
- if (!hfAfterSwap) return false;
-
- const hfNumber = new BigNumber(hfAfterSwap);
-
- if (hfNumber.lt(0)) return false;
-
- return hfNumber.lt(LIQUIDATION_DANGER_THRESHOLD);
- }, [hfAfterSwap]);
-
- const shouldUseFlashloanFn = (healthFactor: string, hfEffectOfFromAmount: string) => {
- return (
- healthFactor !== '-1' &&
- new BigNumber(healthFactor)
- .minus(new BigNumber(hfEffectOfFromAmount))
- .lt(LIQUIDATION_SAFETY_THRESHOLD)
- );
- };
-
- useEffect(() => {
- const shouldUseFlashloanValue = shouldUseFlashloanFn(
- poolReserve && userReserve && extendedUser ? extendedUser?.healthFactor ?? '-1' : '-1',
- poolReserve && userReserve && extendedUser ? hfEffectOfFromAmount ?? '0' : '0'
- );
-
- if (modalType !== ModalType.CollateralSwap) {
- setIsSwapFlowSelected(true);
- } else if (!ratesLoading && !!switchRates?.provider) {
- if (shouldUseFlashloanValue === shouldUseFlashloan) {
- return;
- }
-
- setShouldUseFlashloan(shouldUseFlashloanValue);
- setIsSwapFlowSelected(true);
- }
- }, [modalType, switchRates, ratesLoading, shouldUseFlashloan]);
-
- // Define default slippage for CoW & Paraswap
- useEffect(() => {
- if (switchRates?.provider == 'cowprotocol' && isCowProtocolRates(switchRates)) {
- setSlippage(switchRates.suggestedSlippage.toString());
- } else if (modalType === ModalType.CollateralSwap && shouldUseFlashloan === true) {
- const paraswapSlippage = getParaswapSlippage(
- selectedInputToken?.symbol || '',
- selectedOutputToken?.symbol || ''
- );
- setSlippage(paraswapSlippage);
- }
- }, [
- switchRates,
- shouldUseFlashloan,
- modalType,
- selectedInputToken?.symbol,
- selectedOutputToken?.symbol,
- ]);
-
- const [showSlippageWarning, setShowSlippageWarning] = useState(false);
- useEffect(() => {
- // Debounce to avoid race condition
- const timeout = setTimeout(() => {
- setShowSlippageWarning(
- isCowProtocolRates(switchRates) && Number(slippage) < switchRates?.suggestedSlippage
- );
- }, 500);
- return () => clearTimeout(timeout);
- }, [slippage, switchRates]);
-
- const [cowOpenOrdersTotalAmountFormatted, setCowOpenOrdersTotalAmountFormatted] = useState<
- string | undefined
- >(undefined);
- useEffect(() => {
- if (
- switchRates?.provider == 'cowprotocol' &&
- user &&
- selectedChainId &&
- selectedInputToken &&
- selectedOutputToken
- ) {
- setCowOpenOrdersTotalAmountFormatted(undefined);
-
- getOrders(selectedChainId, user).then((orders) => {
- const token =
- modalType === ModalType.CollateralSwap
- ? selectedInputToken.aToken
- : selectedOutputToken.aToken;
-
- if (!token) {
- return;
- }
-
- const cowOpenOrdersTotalAmount = orders
- .filter(
- (order) =>
- order.sellToken.toLowerCase() == token.toLowerCase() &&
- order.status == OrderStatus.OPEN
- )
- .map((order) => order.sellAmount)
- .reduce((acc, curr) => acc + Number(curr), 0);
- if (cowOpenOrdersTotalAmount > 0) {
- setCowOpenOrdersTotalAmountFormatted(
- normalize(cowOpenOrdersTotalAmount, selectedInputToken.decimals).toString()
- );
- } else {
- setCowOpenOrdersTotalAmountFormatted(undefined);
- }
- });
- } else {
- setCowOpenOrdersTotalAmountFormatted(undefined);
- }
- }, [
- selectedInputToken,
- selectedOutputToken,
- switchRates?.provider,
- selectedChainId,
- user,
- modalType,
- ]);
-
- // No tokens found
- if (
- (initialFromTokens !== undefined && initialFromTokens.length === 0) ||
- (initialToTokens !== undefined && initialToTokens.length === 0)
- ) {
- return (
- close()}>
-
- No eligible assets to swap.
-
-
- );
- }
-
- // Success View
- if (switchRates && switchTxState.success) {
- return (
-
- );
- }
-
- // Eth-Flow requires to leave some assets for gas
- const nativeDecimals = 18;
- const gasRequiredForEthFlow =
- selectedChainId === 1
- ? parseUnits('0.01', nativeDecimals)
- : parseUnits('0.0001', nativeDecimals); // TODO: Ask for better value coming from the SDK
- const requiredAssetsLeftForGas =
- isNativeToken(selectedInputToken.address) &&
- !userIsSmartContractWallet &&
- modalType === ModalType.Switch
- ? gasRequiredForEthFlow
- : undefined;
- const maxAmount = (() => {
- const balance = parseUnits(selectedInputToken.balance, nativeDecimals);
- if (!requiredAssetsLeftForGas) return balance;
- return balance > requiredAssetsLeftForGas ? balance - requiredAssetsLeftForGas : balance;
- })();
- const maxAmountFormatted = maxAmount
- ? normalize(maxAmount.toString(), nativeDecimals).toString()
- : undefined;
-
- const swapDetailsComponent = (
- Minimum new collateral
- }
- />
- );
-
- const lostValue = switchRates
- ? valueLostPercentage(
- Number(switchRates?.destUSD) * (1 - safeSlippage),
- Number(switchRates?.srcUSD)
- )
- : 0;
-
- const showWarning = switchRates
- ? shouldShowWarning(lostValue, Number(switchRates?.srcUSD))
- : false;
- const requireConfirmation = switchRates ? shouldRequireConfirmation(lostValue) : false;
- const requireConfirmationHFlow = isHFLow
- ? shouldRequireConfirmationHFlow(Number(hfAfterSwap))
- : false;
-
- const isSwappingSafetyModuleToken = SAFETY_MODULE_TOKENS.includes(
- selectedInputToken.symbol.toLowerCase()
- );
-
- // Component
- return (
- <>
- {showTitle && (
-
- )}
- {showChangeNetworkWarning && isWrongNetwork.isWrongNetwork && !readOnlyModeAddress && (
-
- )}
-
- {cowOpenOrdersTotalAmountFormatted && (
-
-
- You have open orders for {cowOpenOrdersTotalAmountFormatted} {selectedInputToken.symbol}
- .
Track them in your{' '}
-
- transaction history
-
-
-
- )}
-
-
- {modalType !== ModalType.CollateralSwap && (
-
- )}
-
-
- {!selectedInputToken || !selectedOutputToken ? (
-
- ) : (
- <>
-
-
- token.address !== selectedOutputToken.address &&
- Number(token.balance) !== 0 &&
- // Remove native tokens for non-Safe smart contract wallets
- !(userIsSmartContractWallet && !userIsSafeWallet && token.extensions?.isNative) &&
- // Avoid wrapping
- !(
- isNativeToken(selectedOutputToken.address) &&
- token.address.toLowerCase() ===
- WRAPPED_NATIVE_CURRENCIES[
- selectedChainId as SupportedChainId
- ]?.address.toLowerCase()
- ) &&
- !(
- selectedOutputToken.address.toLowerCase() ===
- WRAPPED_NATIVE_CURRENCIES[
- selectedChainId as SupportedChainId
- ]?.address.toLowerCase() && isNativeToken(token.address)
- )
- )}
- value={inputAmount}
- onChange={handleInputChange}
- usdValue={switchRates?.srcUSD || '0'}
- onSelect={handleSelectedInputToken}
- selectedAsset={selectedInputToken}
- forcedMaxValue={maxAmountFormatted}
- allowCustomTokens={modalType !== ModalType.CollateralSwap}
- />
- {showSwitchInputAndOutputAssetsButton && (
-
-
-
-
-
- )}
-
- token.address !== selectedInputToken.address &&
- // Avoid wrapping
- !(
- isNativeToken(selectedInputToken.address) &&
- token.address.toLowerCase() ===
- WRAPPED_NATIVE_CURRENCIES[
- selectedChainId as SupportedChainId
- ]?.address.toLowerCase()
- ) &&
- !(
- selectedInputToken.address.toLowerCase() ===
- WRAPPED_NATIVE_CURRENCIES[
- selectedChainId as SupportedChainId
- ]?.address.toLowerCase() && isNativeToken(token.address)
- )
- )}
- value={normalizeBN(
- switchRates?.provider === 'cowprotocol'
- ? switchRates?.destSpot
- : switchRates?.destAmount || 0,
- switchRates?.destDecimals || 18
- ).toString()}
- usdValue={
- switchRates?.provider === 'cowprotocol'
- ? switchRates?.destSpotInUsd
- : switchRates?.destUSD || '0'
- }
- loading={
- debounceInputAmount !== '0' &&
- debounceInputAmount !== '' &&
- ratesLoading &&
- !ratesError
- }
- onSelect={handleSelectedOutputToken}
- disableInput={true}
- selectedAsset={selectedOutputToken}
- showBalance={false}
- allowCustomTokens={modalType !== ModalType.CollateralSwap}
- />
-
-
- {switchRates && isSwapFlowSelected && (
- <>
-
- >
- )}
-
- <>
- {(selectedInputToken.extensions?.isUserCustom ||
- selectedOutputToken.extensions?.isUserCustom) && (
-
-
- You selected a custom imported token. Make sure it's the right token.
-
-
- )}
-
- {isSwapFlowSelected && swapDetailsComponent}
-
- {showSlippageWarning && (
-
-
- Slippage is lower than recommended. The swap may be delayed or fail.
-
-
- )}
-
- {showUSDTResetWarning && (
-
-
-
- USDT on Ethereum requires approval reset before a new approval. This will
- require an additional transaction.
-
-
-
- )}
-
- {modalType === ModalType.CollateralSwap && isLiquidatable && (
-
-
-
- Your health factor after this swap will be critically low and may result in
- liquidation. Please choose a different asset or reduce the swap amount to stay
- safe.
-
-
-
- )}
- {modalType === ModalType.CollateralSwap && isHFLow && !isLiquidatable && (
-
-
-
- Low health factor after swap. Your position will carry a higher risk of
- liquidation.
-
-
-
-
- I understand the liquidation risk and want to proceed
-
- {
- setLowHFConfirmed(!lowHFConfirmed);
- }}
- size="small"
- data-cy={'low-hf-checkbox'}
- />
-
-
- )}
-
-
-
- {txError && }
-
- {showWarning && isSwapFlowSelected && (
-
-
- High price impact. This route may return less due to low liquidity.
-
- {requireConfirmation && (
-
-
-
- I confirm the swap with a potential {(lostValue * 100).toFixed(0)}% value
- loss
-
-
- {
- setHighPriceImpactConfirmed(!highPriceImpactConfirmed);
- }}
- size="small"
- data-cy={'high-price-impact-checkbox'}
- />
-
- )}
-
- )}
-
- {isSwappingSafetyModuleToken && (
-
-
-
- For swapping safety module assets please unstake your position{' '}
- close()}>
- here
-
- .
-
-
-
- )}
-
- {isSwapFlowSelected && (
- Number(selectedInputToken.balance) ||
- !user ||
- slippageValidation?.severity === ValidationSeverity.ERROR ||
- isSwappingSafetyModuleToken ||
- (requireConfirmation && !highPriceImpactConfirmed) ||
- (shouldUseFlashloan === true && !!poolReserve && !poolReserve.flashLoanEnabled) ||
- (modalType === ModalType.CollateralSwap && isLiquidatable) ||
- (modalType === ModalType.CollateralSwap &&
- isHFLow &&
- requireConfirmationHFlow &&
- !lowHFConfirmed)
- }
- chainId={selectedChainId}
- switchRates={switchRates}
- modalType={modalType}
- setIsExecutingActions={setIsExecutingActions}
- />
- )}
- >
- >
- )}
- >
- );
-};
diff --git a/src/components/transactions/Switch/CollateralSwap/CollateralSwapActions.tsx b/src/components/transactions/Switch/CollateralSwap/CollateralSwapActions.tsx
deleted file mode 100644
index 8f62f57557..0000000000
--- a/src/components/transactions/Switch/CollateralSwap/CollateralSwapActions.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import {
- API_ETH_MOCK_ADDRESS,
- gasLimitRecommendations,
- ProtocolAction,
-} from '@aave/contract-helpers';
-import { SignatureLike } from '@ethersproject/bytes';
-import { Trans } from '@lingui/macro';
-import { BoxProps } from '@mui/material';
-import { useParaSwapTransactionHandler } from 'src/helpers/useParaSwapTransactionHandler';
-import { ComputedReserveData } from 'src/hooks/app-data-provider/useAppDataProvider';
-import { calculateSignedAmount, SwapTransactionParams } from 'src/hooks/paraswap/common';
-import { useRootStore } from 'src/store/root';
-import { useShallow } from 'zustand/shallow';
-
-import { TxActionsWrapper } from '../../TxActionsWrapper';
-
-interface SwapBaseProps extends BoxProps {
- amountToSwap: string;
- amountToReceive: string;
- poolReserve: ComputedReserveData;
- targetReserve: ComputedReserveData;
- isWrongNetwork: boolean;
- customGasPrice?: string;
- symbol: string;
- blocked: boolean;
- isMaxSelected: boolean;
- useFlashLoan: boolean;
- loading?: boolean;
- signature?: SignatureLike;
- deadline?: string;
- signedAmount?: string;
-}
-
-export interface SwapActionProps extends SwapBaseProps {
- swapCallData: string;
- augustus: string;
-}
-
-export const CollateralSwapActions = ({
- amountToSwap,
- amountToReceive,
- isWrongNetwork,
- sx,
- poolReserve,
- targetReserve,
- isMaxSelected,
- useFlashLoan,
- loading,
- symbol,
- blocked,
- buildTxFn,
- ...props
-}: SwapBaseProps & { buildTxFn: () => Promise }) => {
- const [swapCollateral, currentMarketData] = useRootStore(
- useShallow((state) => [state.swapCollateral, state.currentMarketData])
- );
-
- const { approval, action, approvalTxState, mainTxState, loadingTxns, requiresApproval } =
- useParaSwapTransactionHandler({
- protocolAction: ProtocolAction.swapCollateral,
- handleGetTxns: async (signature, deadline) => {
- const route = await buildTxFn();
- return swapCollateral({
- amountToSwap,
- amountToReceive,
- poolReserve,
- targetReserve,
- isWrongNetwork,
- symbol,
- blocked,
- isMaxSelected,
- useFlashLoan,
- swapCallData: route.swapCallData,
- augustus: route.augustus,
- signature,
- deadline,
- signedAmount: calculateSignedAmount(amountToSwap, poolReserve.decimals),
- });
- },
- handleGetApprovalTxns: async () => {
- return swapCollateral({
- amountToSwap,
- amountToReceive,
- poolReserve,
- targetReserve,
- isWrongNetwork,
- symbol,
- blocked,
- isMaxSelected,
- useFlashLoan: false,
- swapCallData: '0x',
- augustus: API_ETH_MOCK_ADDRESS,
- });
- },
- gasLimitRecommendation: gasLimitRecommendations[ProtocolAction.swapCollateral].limit,
- skip: loading || !amountToSwap || parseFloat(amountToSwap) === 0,
- spender: currentMarketData.addresses.SWAP_COLLATERAL_ADAPTER ?? '',
- deps: [targetReserve.symbol, amountToSwap],
- });
-
- return (
-
- approval({
- amount: calculateSignedAmount(amountToSwap, poolReserve.decimals),
- underlyingAsset: poolReserve.aTokenAddress,
- })
- }
- requiresApproval={requiresApproval}
- actionText={Swap}
- actionInProgressText={Swapping}
- sx={sx}
- fetchingData={loading}
- errorParams={{
- loading: false,
- disabled: blocked,
- content: Swap,
- handleClick: action,
- }}
- tryPermit
- {...props}
- />
- );
-};
diff --git a/src/components/transactions/Switch/CollateralSwap/CollateralSwapModal.tsx b/src/components/transactions/Switch/CollateralSwap/CollateralSwapModal.tsx
deleted file mode 100644
index 716aa9c9b2..0000000000
--- a/src/components/transactions/Switch/CollateralSwap/CollateralSwapModal.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import { SupportedChainId, WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/cow-sdk';
-import { BasicModal } from 'src/components/primitives/BasicModal';
-import {
- ComputedReserveData,
- useAppDataContext,
-} from 'src/hooks/app-data-provider/useAppDataProvider';
-import { TokenInfoWithBalance } from 'src/hooks/generic/useTokensBalance';
-import { ModalContextType, ModalType, useModalContext } from 'src/hooks/useModal';
-import { useRootStore } from 'src/store/root';
-import { TOKEN_LIST, TokenInfo } from 'src/ui-config/TokenList';
-import { displayGhoForMintableMarket } from 'src/utils/ghoUtilities';
-
-import { BaseSwitchModal } from '../BaseSwitchModal';
-
-export const CollateralSwapModal = () => {
- const { args, type, close } = useModalContext() as ModalContextType<{
- underlyingAsset: string;
- }>;
- const { underlyingAsset } = args;
-
- const { user, reserves } = useAppDataContext();
- const currentMarket = useRootStore((store) => store.currentMarket);
- const currentNetworkConfig = useRootStore((store) => store.currentNetworkConfig);
- const baseTokens: TokenInfo[] = reserves.map((reserve) => {
- return {
- address: reserve.underlyingAsset,
- symbol: reserve.symbol,
- logoURI: `/icons/tokens/${reserve.iconSymbol.toLowerCase()}.svg`,
- chainId: currentNetworkConfig.wagmiChain.id,
- name: reserve.name,
- decimals: reserve.decimals,
- };
- });
-
- // Tokens From should be the supplied tokens
- const suppliedPositions =
- user?.userReservesData.filter((userReserve) => userReserve.underlyingBalance !== '0') || [];
-
- const tokensFrom = suppliedPositions
- .map((position) => {
- const baseToken = baseTokens.find(
- (baseToken) =>
- baseToken.address.toLowerCase() === position.reserve.underlyingAsset.toLowerCase()
- );
- if (baseToken) {
- // Prefer showing native symbol (e.g., ETH) instead of WETH when applicable, but keep underlying address
- const realChainId = currentNetworkConfig.wagmiChain.id;
- const wrappedNative =
- WRAPPED_NATIVE_CURRENCIES[realChainId as SupportedChainId]?.address?.toLowerCase();
- const isWrappedNative =
- wrappedNative && position.reserve.underlyingAsset.toLowerCase() === wrappedNative;
- const nativeToken = isWrappedNative
- ? TOKEN_LIST.tokens.find(
- (t) => (t as TokenInfoWithBalance).extensions?.isNative && t.chainId === realChainId
- )
- : undefined;
-
- return {
- ...baseToken,
- symbol: nativeToken?.symbol ?? baseToken.symbol,
- logoURI: nativeToken?.logoURI ?? baseToken.logoURI,
- balance: position.underlyingBalance,
- aToken: position.reserve.aTokenAddress,
- };
- }
- return undefined;
- })
- .filter((token) => token !== undefined)
- .sort((a, b) => {
- const aBalance = parseFloat(a?.balance ?? '0');
- const bBalance = parseFloat(b?.balance ?? '0');
- if (bBalance !== aBalance) {
- return bBalance - aBalance;
- }
- // If balances are equal, sort by symbol alphabetically
- const aSymbol = a?.symbol?.toLowerCase() ?? '';
- const bSymbol = b?.symbol?.toLowerCase() ?? '';
- if (aSymbol < bSymbol) return -1;
- if (aSymbol > bSymbol) return 1;
- return 0;
- });
-
- // Tokens To should be the potential supply tokens (so we have an aToken)
- const tokensToSupply = reserves.filter(
- (reserve: ComputedReserveData) =>
- !(reserve.isFrozen || reserve.isPaused) &&
- !displayGhoForMintableMarket({ symbol: reserve.symbol, currentMarket: currentMarket })
- );
- const tokensTo = tokensToSupply
- .map((reserve) => {
- // Find the base token for this reserve
- const baseToken = baseTokens.find(
- (baseToken) => baseToken.address.toLowerCase() === reserve.underlyingAsset.toLowerCase()
- );
-
- if (!baseToken) return undefined;
-
- const currentCollateral =
- suppliedPositions.find(
- (position) =>
- position.reserve.underlyingAsset.toLowerCase() === reserve.underlyingAsset.toLowerCase()
- )?.underlyingBalance ?? '0';
-
- return {
- ...baseToken,
- aToken: reserve.aTokenAddress,
- balance: currentCollateral,
- };
- })
- .filter((token) => token !== undefined)
- .sort((a, b) => {
- const aBalance = parseFloat(a?.balance ?? '0');
- const bBalance = parseFloat(b?.balance ?? '0');
- if (bBalance !== aBalance) {
- return bBalance - aBalance;
- }
- // If balances are equal, sort by symbol alphabetically
- const aSymbol = a?.symbol?.toLowerCase() ?? '';
- const bSymbol = b?.symbol?.toLowerCase() ?? '';
- if (aSymbol < bSymbol) return -1;
- if (aSymbol > bSymbol) return 1;
- return 0;
- });
-
- const userSelectedInputToken = tokensFrom.find(
- (token) => token.address.toLowerCase() === underlyingAsset?.toLowerCase()
- );
- const defaultInputToken =
- userSelectedInputToken ||
- (tokensFrom.find((token) => token.address.toLowerCase() === underlyingAsset?.toLowerCase()) ??
- tokensFrom.length > 0
- ? tokensFrom[0]
- : undefined);
-
- const defaultOutputToken =
- tokensTo.length > 0
- ? tokensTo.filter(
- (token) =>
- token.address !== defaultInputToken?.address &&
- token.symbol !== defaultInputToken?.symbol
- )[0]
- : undefined;
-
- return (
-
-
-
- );
-};
diff --git a/src/components/transactions/Switch/CollateralSwap/CollateralSwapModalDetails.tsx b/src/components/transactions/Switch/CollateralSwap/CollateralSwapModalDetails.tsx
deleted file mode 100644
index ba6a71bd66..0000000000
--- a/src/components/transactions/Switch/CollateralSwap/CollateralSwapModalDetails.tsx
+++ /dev/null
@@ -1,208 +0,0 @@
-import { valueToBigNumber } from '@aave/math-utils';
-import { ArrowNarrowRightIcon } from '@heroicons/react/outline';
-import { Trans } from '@lingui/macro';
-import { Box, Skeleton, SvgIcon } from '@mui/material';
-import React from 'react';
-import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
-import { Row } from 'src/components/primitives/Row';
-import { TokenIcon } from 'src/components/primitives/TokenIcon';
-import {
- CollateralState,
- DetailsHFLine,
- DetailsIncentivesLine,
- DetailsNumberLine,
-} from 'src/components/transactions/FlowCommons/TxModalDetails';
-import { CollateralType } from 'src/helpers/types';
-
-import { ComputedUserReserveData } from '../../../../hooks/app-data-provider/useAppDataProvider';
-
-export type SupplyModalDetailsProps = {
- showHealthFactor: boolean;
- healthFactor: string;
- healthFactorAfterSwap: string;
- swapSource: ComputedUserReserveData & { collateralType: CollateralType };
- swapTarget: ComputedUserReserveData & { collateralType: CollateralType };
- toAmount: string;
- fromAmount: string;
- loading: boolean;
- showBalance?: boolean;
-};
-
-export const CollateralSwapModalDetails = ({
- showHealthFactor,
- healthFactor,
- healthFactorAfterSwap,
- swapSource,
- swapTarget,
- toAmount,
- fromAmount,
- loading,
- showBalance = true,
-}: SupplyModalDetailsProps) => {
- const sourceAmountAfterSwap = valueToBigNumber(swapSource.underlyingBalance).minus(
- valueToBigNumber(fromAmount)
- );
-
- const targetAmountAfterSwap = valueToBigNumber(swapTarget.underlyingBalance).plus(
- valueToBigNumber(toAmount)
- );
-
- const skeleton: JSX.Element = (
- <>
-
-
- >
- );
-
- return (
- <>
- {healthFactorAfterSwap && (
-
- )}
- Supply apy}
- value={swapSource.reserve.supplyAPY}
- futureValue={swapTarget.reserve.supplyAPY}
- percent
- loading={loading}
- />
- Collateralization} captionVariant="description" mb={4}>
-
- {loading ? (
-
- ) : (
- <>
-
-
-
-
-
-
-
-
-
- >
- )}
-
-
-
- Liquidation threshold}
- value={swapSource.reserve.formattedReserveLiquidationThreshold}
- futureValue={swapTarget.reserve.formattedReserveLiquidationThreshold}
- percent
- visibleDecimals={0}
- loading={loading}
- />
-
- {showBalance && (
- Supply balance after switch}
- captionVariant="description"
- mb={4}
- align="flex-start"
- >
-
-
- {loading ? (
- skeleton
- ) : (
- <>
-
-
-
-
-
- >
- )}
-
-
-
- {loading ? (
- skeleton
- ) : (
- <>
-
-
-
-
-
- >
- )}
-
-
-
- )}
- >
- );
-};
diff --git a/src/components/transactions/Switch/PriceInput.tsx b/src/components/transactions/Switch/PriceInput.tsx
deleted file mode 100644
index 8180e0bcaf..0000000000
--- a/src/components/transactions/Switch/PriceInput.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-import { ExclamationIcon } from '@heroicons/react/outline';
-import { Box, Button, CircularProgress, InputBase, SvgIcon, Typography } from '@mui/material';
-import React, { useRef } from 'react';
-import NumberFormat, { NumberFormatProps } from 'react-number-format';
-import { TokenInfoWithBalance } from 'src/hooks/generic/useTokensBalance';
-import { StaticRate } from 'src/hooks/useStaticRate';
-
-import { FormattedNumber } from '../../primitives/FormattedNumber';
-import { ExternalTokenIcon } from '../../primitives/TokenIcon';
-
-interface CustomProps {
- onChange: (event: { target: { name: string; value: string } }) => void;
- name: string;
- value: string;
-}
-
-export const NumberFormatCustom = React.forwardRef(
- function NumberFormatCustom(props, ref) {
- const { onChange, ...other } = props;
-
- return (
- {
- if (values.value !== props.value)
- onChange({
- target: {
- name: props.name,
- value: values.value || '',
- },
- });
- }}
- thousandSeparator
- isNumericString
- allowNegative={false}
- />
- );
- }
-);
-
-export interface AssetInputProps {
- loading?: boolean;
- originalRate?: StaticRate;
- rate: string;
- rateUsd: string;
- originAsset: TokenInfoWithBalance;
- targetAsset: TokenInfoWithBalance;
- disabled?: boolean;
- onChangeRate: (newRate: string) => void;
- isInvertedRate: boolean;
- setIsInvertedRate: (isInverted: boolean) => void;
-}
-
-export const PriceInput = ({
- loading = false,
- rate,
- rateUsd,
- originAsset,
- targetAsset,
- originalRate,
- onChangeRate,
- disabled = false,
- isInvertedRate,
- setIsInvertedRate,
-}: AssetInputProps) => {
- const inputRef = useRef(null);
-
- const handleRateSwith = () => {
- setIsInvertedRate(!isInvertedRate);
- onChangeRate((Number(rate) === 0 ? 0 : 1 / Number(rate)).toString());
- };
-
- const marketRate = originalRate
- ? isInvertedRate
- ? (1 / Number(originalRate.rate)).toString()
- : originalRate.rate
- : rate;
-
- return (
-
-
- When 1 {isInvertedRate ? targetAsset.symbol : originAsset.symbol} is worth:
-
- ({
- border: `1px solid ${theme.palette.divider}`,
- borderRadius: '6px',
- overflow: 'hidden',
- px: 3,
- py: 2,
- width: '100%',
- })}
- >
-
- {loading ? (
-
-
-
- ) : (
- {
- onChangeRate(e.target.value);
- }}
- />
- )}
-
-
-
-
-
- {loading ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
-
-
- );
-};
diff --git a/src/components/transactions/Switch/SwitchActions.tsx b/src/components/transactions/Switch/SwitchActions.tsx
deleted file mode 100644
index 455dbdc079..0000000000
--- a/src/components/transactions/Switch/SwitchActions.tsx
+++ /dev/null
@@ -1,1151 +0,0 @@
-import {
- API_ETH_MOCK_ADDRESS,
- ERC20Service,
- gasLimitRecommendations,
- ProtocolAction,
-} from '@aave/contract-helpers';
-import { normalize, valueToBigNumber } from '@aave/math-utils';
-import {
- calculateUniqueOrderId,
- COW_PROTOCOL_VAULT_RELAYER_ADDRESS,
- OrderClass,
- SupportedChainId,
-} from '@cowprotocol/cow-sdk';
-import { Trans } from '@lingui/macro';
-import { useQueryClient } from '@tanstack/react-query';
-import { BigNumber, ethers } from 'ethers';
-import { defaultAbiCoder, splitSignature } from 'ethers/lib/utils';
-import stringify from 'json-stringify-deterministic';
-import { useCallback, useEffect, useMemo, useState } from 'react';
-import { isSmartContractWallet } from 'src/helpers/provider';
-import { useParaSwapTransactionHandler } from 'src/helpers/useParaSwapTransactionHandler';
-import { MOCK_SIGNED_HASH } from 'src/helpers/useTransactionHandler';
-import { ComputedReserveData } from 'src/hooks/app-data-provider/useAppDataProvider';
-import {
- calculateSignedAmount,
- fetchExactInTxParams,
- minimumReceivedAfterSlippage,
-} from 'src/hooks/paraswap/common';
-import { useParaswapSellTxParams } from 'src/hooks/paraswap/useParaswapRates';
-import { ModalType, TxStateType, useModalContext } from 'src/hooks/useModal';
-import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
-import { getEthersProvider } from 'src/libs/web3-data-provider/adapters/EthersAdapter';
-import { useRootStore } from 'src/store/root';
-import { TransactionContext, TransactionDetails } from 'src/store/transactionsSlice';
-import { ApprovalMethod } from 'src/store/walletSlice';
-import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
-import { findByChainId } from 'src/ui-config/marketsConfig';
-import { permitByChainAndToken } from 'src/ui-config/permitConfig';
-import { queryKeysFactory } from 'src/ui-config/queries';
-import { wagmiConfig } from 'src/ui-config/wagmiConfig';
-import { GENERAL } from 'src/utils/events';
-import { getNetworkConfig, getProvider } from 'src/utils/marketsAndNetworksConfig';
-import { needsUSDTApprovalReset } from 'src/utils/usdtHelpers';
-import { useShallow } from 'zustand/shallow';
-
-import { TxActionsWrapper } from '../TxActionsWrapper';
-import { APPROVAL_GAS_LIMIT } from '../utils';
-import {
- ADAPTER_APP_CODE,
- COW_APP_DATA,
- getPreSignTransaction,
- getUnsignerOrder,
- HEADER_WIDGET_APP_CODE,
- isNativeToken,
- populateEthFlowTx,
- sendOrder,
- uploadAppData,
-} from './cowprotocol/cowprotocol.helpers';
-import {
- isCowProtocolRates,
- isParaswapRates,
- ParaswapRatesType,
- SwitchRatesType,
-} from './switch.types';
-
-interface SwitchProps {
- inputAmount: string;
- inputToken: string;
- outputToken: string;
- setShowUSDTResetWarning: (showUSDTResetWarning: boolean) => void;
- slippage: string;
- blocked: boolean;
- loading?: boolean;
- isWrongNetwork: boolean;
- chainId: number;
- switchRates?: SwitchRatesType;
- inputSymbol: string;
- outputSymbol: string;
- setShowGasStation: (showGasStation: boolean) => void;
- modalType: ModalType;
- useFlashloan: boolean;
- poolReserve?: ComputedReserveData;
- targetReserve?: ComputedReserveData;
- isMaxSelected: boolean;
- setIsExecutingActions?: (isExecuting: boolean) => void;
-}
-
-interface SignedParams {
- signature: string;
- deadline: string;
- amount: string;
- approvedToken: string;
-}
-
-export const ParaswapSwitchActionsWrapper = ({
- inputAmount: amountToSwap,
- inputSymbol,
- slippage,
- blocked,
- loading,
- isWrongNetwork,
- chainId,
- switchRates,
- poolReserve,
- targetReserve,
- isMaxSelected,
- addTransaction,
- setMainTxState,
- invalidate,
-}: {
- inputAmount: string;
- inputSymbol: string;
- slippage: string;
- blocked: boolean;
- loading?: boolean;
- isWrongNetwork: boolean;
- chainId: number;
- switchRates: ParaswapRatesType;
- poolReserve: ComputedReserveData;
- targetReserve: ComputedReserveData;
- isMaxSelected: boolean;
- addTransaction: (
- txHash: string,
- transaction: TransactionDetails,
- context?: TransactionContext
- ) => void;
- setMainTxState: (txState: TxStateType) => void;
- invalidate: () => void;
-}) => {
- const userAddress = useRootStore.getState().account;
- const [swapCollateral, currentMarketData, trackEvent] = useRootStore(
- useShallow((state) => [state.swapCollateral, state.currentMarketData, state.trackEvent])
- );
-
- const slippageInPercent = (Number(slippage) * 100).toString();
- const outputAmount = normalize(switchRates.destAmount, switchRates.destDecimals);
- const minimumReceived = minimumReceivedAfterSlippage(
- outputAmount,
- slippageInPercent,
- targetReserve.decimals
- );
-
- const buildTxFn = async () => {
- if (!switchRates?.optimalRateData) throw new Error('Route required to build transaction');
-
- // Create SwapData objects with only the required properties
- const swapIn = {
- amount: amountToSwap,
- underlyingAsset: poolReserve.underlyingAsset,
- decimals: poolReserve.decimals,
- supplyAPY: poolReserve.supplyAPY,
- variableBorrowAPY: poolReserve.variableBorrowAPY,
- };
-
- const swapOut = {
- amount: normalize(switchRates.destAmount, switchRates.destDecimals),
- underlyingAsset: targetReserve.underlyingAsset,
- decimals: targetReserve.decimals,
- supplyAPY: targetReserve.supplyAPY,
- variableBorrowAPY: targetReserve.variableBorrowAPY,
- };
-
- const maxSlippage = Number(slippageInPercent);
-
- return await fetchExactInTxParams(
- switchRates.optimalRateData,
- swapIn,
- swapOut,
- chainId,
- userAddress,
- maxSlippage
- );
- };
-
- const {
- approval,
- action,
- approvalTxState,
- mainTxState,
- loadingTxns,
- requiresApproval,
- requestingApproval,
- } = useParaSwapTransactionHandler({
- protocolAction: ProtocolAction.swapCollateral,
- handleGetTxns: async (signature, deadline) => {
- const route = await buildTxFn();
- return swapCollateral({
- amountToSwap,
- amountToReceive: minimumReceived,
- poolReserve,
- targetReserve,
- isWrongNetwork,
- symbol: inputSymbol,
- blocked,
- isMaxSelected,
- useFlashLoan: true,
- swapCallData: route.swapCallData,
- augustus: route.augustus,
- signature,
- deadline,
- signedAmount: calculateSignedAmount(amountToSwap, poolReserve.decimals),
- });
- },
- handleGetApprovalTxns: async () => {
- return swapCollateral({
- amountToSwap,
- amountToReceive: minimumReceived,
- poolReserve,
- targetReserve,
- isWrongNetwork,
- symbol: inputSymbol,
- blocked,
- isMaxSelected,
- useFlashLoan: false,
- swapCallData: '0x',
- augustus: API_ETH_MOCK_ADDRESS,
- });
- },
- gasLimitRecommendation: gasLimitRecommendations[ProtocolAction.swapCollateral].limit,
- skip: loading || !amountToSwap || parseFloat(amountToSwap) === 0,
- spender: currentMarketData.addresses.SWAP_COLLATERAL_ADAPTER ?? '',
- deps: [targetReserve.symbol, amountToSwap],
- });
-
- useEffect(() => {
- if (mainTxState.success) {
- invalidate();
-
- trackEvent(GENERAL.COLLATERAL_SWAP_WITH_FLASHLOAN, {
- chainId,
- inputSymbol,
- outputSymbol: targetReserve.symbol,
- pair: `${inputSymbol}-${targetReserve.symbol}`,
- inputAmount: amountToSwap,
- outputAmount: normalize(switchRates.destAmount, switchRates.destDecimals),
- inputAmountUSD: switchRates.srcUSD,
- outputAmountUSD: switchRates.destUSD,
- slippage,
- provider: 'paraswap',
- useFlashLoan: true,
- modalType: 'CollateralSwap',
- isMaxSelected,
- txHash: mainTxState.txHash,
- status: 'success',
- });
-
- addTransaction(
- mainTxState.txHash || '',
- {
- txState: 'success',
- },
- {
- chainId,
- }
- );
-
- setMainTxState({
- txHash: mainTxState.txHash || '',
- loading: false,
- success: true,
- });
- }
- }, [mainTxState.success]);
-
- return (
-
- approval({
- amount: calculateSignedAmount(amountToSwap, poolReserve.decimals),
- underlyingAsset: poolReserve.aTokenAddress,
- })
- }
- requiresApproval={requiresApproval}
- actionText={requestingApproval ? Checking approval : Swap}
- actionInProgressText={Swapping}
- fetchingData={loading}
- errorParams={{
- loading: false,
- disabled: blocked,
- content: Swap,
- handleClick: action,
- }}
- tryPermit
- />
- );
-};
-
-export const SwitchActions = ({
- inputAmount,
- inputToken,
- outputToken,
- inputSymbol,
- outputSymbol,
- setShowUSDTResetWarning,
- slippage: slippageInPercent,
- blocked,
- loading,
- isWrongNetwork,
- chainId,
- switchRates,
- setShowGasStation,
- modalType,
- useFlashloan,
- poolReserve,
- targetReserve,
- isMaxSelected,
- setIsExecutingActions,
-}: SwitchProps) => {
- const [
- user,
- generateApproval,
- estimateGasLimit,
- walletApprovalMethodPreference,
- generateSignatureRequest,
- addTransaction,
- currentMarketData,
- trackEvent,
- ] = useRootStore(
- useShallow((state) => [
- state.account,
- state.generateApproval,
- state.estimateGasLimit,
- state.walletApprovalMethodPreference,
- state.generateSignatureRequest,
- state.addTransaction,
- state.currentMarketData,
- state.trackEvent,
- ])
- );
-
- const {
- approvalTxState,
- mainTxState,
- loadingTxns,
- setMainTxState,
- setTxError,
- setGasLimit,
- setLoadingTxns,
- setApprovalTxState,
- } = useModalContext();
-
- const { sendTx, signTxData } = useWeb3Context();
- const queryClient = useQueryClient();
- const networkConfig = getNetworkConfig(chainId);
- const [swapCollateral] = useRootStore(useShallow((state) => [state.swapCollateral]));
-
- const [signatureParams, setSignatureParams] = useState();
- const [approvedAmount, setApprovedAmount] = useState(undefined);
- const { mutateAsync: fetchParaswapTxParams } = useParaswapSellTxParams(
- networkConfig.underlyingChainId ?? chainId
- );
- const tryPermit =
- permitByChainAndToken[chainId]?.[inputToken] && switchRates?.provider !== 'cowprotocol';
-
- const useSignature = walletApprovalMethodPreference === ApprovalMethod.PERMIT && tryPermit;
-
- const requiresApproval = useMemo(() => {
- if (
- approvedAmount === undefined ||
- approvedAmount === -1 ||
- inputAmount === '0' ||
- isWrongNetwork
- )
- return false;
- else return approvedAmount < Number(inputAmount);
- }, [approvedAmount, inputAmount, isWrongNetwork]);
- const [requiresApprovalReset, setRequiresApprovalReset] = useState(false);
-
- // Warning for USDT on Ethereum approval reset
- useEffect(() => {
- if (!switchRates || modalType !== ModalType.Switch) {
- return;
- }
-
- const amountToApprove = calculateSignedAmount(inputAmount, switchRates.srcDecimals, 0);
- const currentApproved = calculateSignedAmount(
- approvedAmount?.toString() || '0',
- switchRates.srcDecimals,
- 0
- );
-
- if (needsUSDTApprovalReset(inputSymbol, chainId, currentApproved, amountToApprove)) {
- setShowUSDTResetWarning(true);
- setRequiresApprovalReset(true);
- } else {
- setShowUSDTResetWarning(false);
- setRequiresApprovalReset(false);
- }
- }, [inputSymbol, chainId, approvedAmount, inputAmount, setShowUSDTResetWarning, switchRates]);
-
- const invalidate = () => {
- queryClient.invalidateQueries({
- queryKey: queryKeysFactory.poolReservesDataHumanized(
- findByChainId(chainId) ?? currentMarketData
- ),
- });
-
- queryClient.invalidateQueries({
- queryKey: queryKeysFactory.userPoolReservesDataHumanized(
- user,
- findByChainId(chainId) ?? currentMarketData
- ),
- });
-
- queryClient.invalidateQueries({
- queryKey: queryKeysFactory.transactionHistory(
- user,
- findByChainId(chainId) ?? currentMarketData
- ),
- });
-
- queryClient.invalidateQueries({
- queryKey: queryKeysFactory.poolTokens(user, currentMarketData),
- });
- };
-
- const action = async () => {
- setMainTxState({ ...mainTxState, loading: true });
- if (isParaswapRates(switchRates)) {
- try {
- if (
- useFlashloan &&
- modalType === ModalType.CollateralSwap &&
- poolReserve &&
- targetReserve
- ) {
- // Use fetchExactInTxParams with the SAME token addresses that were used for rate fetching
- // This ensures the route data matches the transaction building context
- const route = await fetchExactInTxParams(
- switchRates.optimalRateData,
- {
- amount: inputAmount,
- underlyingAsset: switchRates.srcToken, // Use the SAME token used in rate fetching
- decimals: switchRates.srcDecimals,
- supplyAPY: poolReserve.supplyAPY,
- variableBorrowAPY: poolReserve.variableBorrowAPY,
- },
- {
- amount: normalize(switchRates.destAmount, switchRates.destDecimals),
- underlyingAsset: switchRates.destToken, // Use the SAME token used in rate fetching
- decimals: switchRates.destDecimals,
- supplyAPY: targetReserve.supplyAPY,
- variableBorrowAPY: targetReserve.variableBorrowAPY,
- },
- chainId,
- user,
- Number(slippageInPercent)
- );
-
- const minAmountToReceive = minimumReceivedAfterSlippage(
- normalize(switchRates.destAmount, switchRates.destDecimals),
- slippageInPercent,
- targetReserve.decimals
- );
-
- const swapCollateralParams = {
- amountToSwap: inputAmount,
- amountToReceive: minAmountToReceive,
- poolReserve,
- targetReserve,
- isWrongNetwork,
- symbol: inputSymbol,
- blocked,
- isMaxSelected,
- useFlashLoan: true,
- swapCallData: route.swapCallData,
- augustus: route.augustus,
- signature: signatureParams?.signature,
- deadline: signatureParams?.deadline,
- signedAmount: calculateSignedAmount(inputAmount, poolReserve.decimals),
- };
-
- const txs = await swapCollateral(swapCollateralParams);
-
- const tx = txs[0];
- const params = await tx.tx();
- delete params.gasPrice;
- const response = await sendTx(params);
-
- try {
- await response.wait(1);
- addTransaction(
- response.hash,
- {
- txState: 'success',
- },
- {
- chainId,
- }
- );
- setMainTxState({
- txHash: response.hash,
- loading: false,
- success: true,
- });
- queryClient.invalidateQueries({
- queryKey: queryKeysFactory.poolTokens(user, currentMarketData),
- });
- } catch (error) {
- const parsedError = getErrorTextFromError(error, TxAction.MAIN_ACTION, false);
- setTxError(parsedError);
- setMainTxState({
- txHash: response.hash,
- loading: false,
- });
- addTransaction(
- response.hash,
- {
- txState: 'failed',
- },
- {
- chainId,
- }
- );
- }
- } else {
- // Normal switch using paraswap
- const tx = await fetchParaswapTxParams({
- srcToken: inputToken,
- srcDecimals: switchRates.srcDecimals,
- destDecimals: switchRates.destDecimals,
- destToken: outputToken,
- route: switchRates.optimalRateData,
- user,
- maxSlippage: Number(slippageInPercent) * 10000,
- permit: signatureParams && signatureParams.signature,
- deadline: signatureParams && signatureParams.deadline,
- partner: 'aave-widget',
- });
- tx.chainId = chainId;
- const txWithGasEstimation = await estimateGasLimit(tx, chainId);
- const response = await sendTx(txWithGasEstimation);
- try {
- await response.wait(1);
- addTransaction(
- response.hash,
- {
- txState: 'success',
- },
- {
- chainId,
- }
- );
- setMainTxState({
- txHash: response.hash,
- loading: false,
- success: true,
- });
- queryClient.invalidateQueries({
- queryKey: queryKeysFactory.poolTokens(user, currentMarketData),
- });
- } catch (error) {
- const parsedError = getErrorTextFromError(error, TxAction.MAIN_ACTION, false);
- setTxError(parsedError);
- setMainTxState({
- txHash: response.hash,
- loading: false,
- });
- addTransaction(
- response.hash,
- {
- txState: 'failed',
- },
- {
- chainId,
- }
- );
- }
- }
- } catch (error) {
- const parsedError = getErrorTextFromError(error, TxAction.MAIN_ACTION, false);
- setTxError(parsedError);
- setMainTxState({
- txHash: undefined,
- loading: false,
- });
- }
- } else if (isCowProtocolRates(switchRates)) {
- if (useFlashloan) {
- setTxError(
- getErrorTextFromError(new Error('Please use flashloan'), TxAction.MAIN_ACTION, true)
- );
- setMainTxState({
- txHash: undefined,
- loading: false,
- });
- return;
- }
-
- try {
- const provider = await getEthersProvider(wagmiConfig, { chainId });
- const destAmountWithSlippage = valueToBigNumber(switchRates.destAmount)
- .multipliedBy(valueToBigNumber(1).minus(valueToBigNumber(slippageInPercent)))
- .toFixed(0);
- const slippageBps = Math.round(Number(slippageInPercent) * 100 * 100); // percent to bps
- const smartSlippage = switchRates.suggestedSlippage == Number(slippageInPercent) * 100;
- const appCode =
- modalType === ModalType.CollateralSwap ? ADAPTER_APP_CODE : HEADER_WIDGET_APP_CODE;
-
- // If srcToken is native, we need to use the eth-flow instead of the orderbook
- if (isNativeToken(inputToken)) {
- const validTo = Math.floor(Date.now() / 1000) + 60 * 30; // 30 minutes
- const ethFlowTx = await populateEthFlowTx(
- switchRates.srcAmount,
- destAmountWithSlippage.toString(),
- outputToken,
- user,
- validTo,
- inputSymbol,
- outputSymbol,
- slippageBps,
- smartSlippage,
- switchRates.quoteId
- );
- const txWithGasEstimation = await estimateGasLimit(ethFlowTx, chainId);
- let response;
- try {
- response = await sendTx(txWithGasEstimation);
- addTransaction(
- response.hash,
- {
- txState: 'success',
- },
- {
- chainId,
- }
- );
-
- setMainTxState({
- loading: false,
- success: true,
- });
-
- const unsignerOrder = await getUnsignerOrder(
- switchRates.srcAmount,
- destAmountWithSlippage.toString(),
- outputToken,
- user,
- chainId,
- inputSymbol,
- outputSymbol,
- slippageBps,
- smartSlippage
- );
- const calculatedOrderId = await calculateUniqueOrderId(chainId, unsignerOrder);
-
- await uploadAppData(
- calculatedOrderId,
- stringify(
- COW_APP_DATA(
- inputSymbol,
- outputSymbol,
- slippageBps,
- smartSlippage,
- OrderClass.MARKET
- )
- ),
- chainId
- );
-
- // CoW takes some time to index the order for 'eth-flow' orders
- setTimeout(() => {
- setMainTxState({
- loading: false,
- success: true,
- txHash: calculatedOrderId,
- });
- }, 1000 * 30); // 30 seconds - if we set less than 30 seconds, the order is not indexed yet and CoW explorer will not find the order
- } catch (error) {
- setTxError(getErrorTextFromError(error, TxAction.MAIN_ACTION, false));
- setMainTxState({
- txHash: response?.hash,
- loading: false,
- });
- if (response?.hash) {
- addTransaction(
- response?.hash,
- {
- txState: 'failed',
- },
- { chainId }
- );
- }
- }
- } else {
- let orderId;
- try {
- if (await isSmartContractWallet(user, provider)) {
- const preSignTransaction = await getPreSignTransaction({
- provider,
- tokenDest: outputToken,
- chainId,
- user,
- amount: switchRates.srcAmount,
- tokenSrc: inputToken,
- tokenSrcDecimals: switchRates.srcDecimals,
- tokenDestDecimals: switchRates.destDecimals,
- afterNetworkCostsBuyAmount:
- switchRates.amountAndCosts.afterNetworkCosts.buyAmount.toString(),
- slippageBps,
- smartSlippage,
- inputSymbol,
- outputSymbol,
- quote: switchRates.order,
- appCode,
- orderBookQuote: switchRates.orderBookQuote,
- });
-
- const response = await sendTx({
- data: preSignTransaction.data,
- to: preSignTransaction.to,
- value: BigNumber.from(preSignTransaction.value),
- gasLimit: BigNumber.from(preSignTransaction.gasLimit),
- });
-
- addTransaction(
- response.hash,
- {
- txState: 'success',
- },
- {
- chainId,
- }
- );
-
- setMainTxState({
- loading: false,
- success: true,
- txHash: preSignTransaction.orderId,
- });
- } else {
- orderId = await sendOrder({
- tokenSrc: inputToken,
- tokenSrcDecimals: switchRates.srcDecimals,
- tokenDest: outputToken,
- tokenDestDecimals: switchRates.destDecimals,
- quote: switchRates.order,
- amount: switchRates.srcAmount,
- afterNetworkCostsBuyAmount:
- switchRates.amountAndCosts.afterNetworkCosts.buyAmount.toString(),
- slippageBps,
- smartSlippage,
- chainId,
- user,
- provider,
- inputSymbol,
- outputSymbol,
- appCode,
- orderBookQuote: switchRates.orderBookQuote,
- });
- setMainTxState({
- loading: false,
- success: true,
- txHash: orderId ?? undefined,
- });
- }
- } catch (error) {
- console.error(error);
- const parsedError = getErrorTextFromError(error, TxAction.MAIN_ACTION, false);
- setTxError(parsedError);
- setMainTxState({
- success: false,
- loading: false,
- });
- }
- }
- } catch (error) {
- const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
- setTxError(parsedError);
- setMainTxState({
- txHash: undefined,
- loading: false,
- success: false,
- });
- }
- } else {
- setTxError(
- getErrorTextFromError(new Error('No sell rates found'), TxAction.MAIN_ACTION, true)
- );
- }
-
- invalidate();
-
- try {
- const baseTrackingData: Record = {
- chainId,
- inputSymbol,
- outputSymbol,
- pair: `${inputSymbol}-${outputSymbol}`,
- inputAmount,
- slippage: slippageInPercent,
- provider: switchRates?.provider || 'unknown',
- inputAmountUSD: switchRates?.srcUSD,
- outputAmountUSD: switchRates?.destUSD,
- };
-
- if (modalType === ModalType.CollateralSwap) {
- trackEvent(GENERAL.COLLATERAL_SWAP_WITHOUT_FLASHLOAN, {
- ...baseTrackingData,
- useFlashLoan: false,
- });
- } else {
- trackEvent(GENERAL.SWAP, {
- ...baseTrackingData,
- });
- }
- } catch (error) {
- console.error('Error tracking swap event:', error);
- }
- };
-
- const approval = async () => {
- let spender;
- if (isParaswapRates(switchRates)) {
- // For ParaSwap: use different spender based on whether we're using flashloan
- if (useFlashloan && modalType === ModalType.CollateralSwap) {
- // For flashloan collateral swaps, approve the Swap Collateral Adapter
- spender = currentMarketData.addresses.SWAP_COLLATERAL_ADAPTER;
- } else {
- // For regular ParaSwap swaps, approve the ParaSwap proxy
- spender = switchRates.optimalRateData.tokenTransferProxy;
- }
- } else if (isCowProtocolRates(switchRates)) {
- spender = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId as SupportedChainId];
- } else {
- // Error
- const parsedError = getErrorTextFromError(
- new Error('Invalid swap provider rates.'),
- TxAction.APPROVAL,
- false
- );
-
- setTxError(parsedError);
- setApprovalTxState({
- txHash: undefined,
- loading: false,
- });
- return;
- }
-
- // Ensure spender is defined
- if (!spender) {
- const parsedError = getErrorTextFromError(
- new Error('Unable to determine spender address for approval.'),
- TxAction.APPROVAL,
- false
- );
-
- setTxError(parsedError);
- setApprovalTxState({
- txHash: undefined,
- loading: false,
- });
- return;
- }
-
- const amountToApprove = calculateSignedAmount(inputAmount, switchRates.srcDecimals, 0);
-
- if (requiresApprovalReset) {
- const resetData = {
- spender,
- user,
- token: inputToken,
- amount: '0',
- };
-
- try {
- if (useSignature) {
- const deadline = Math.floor(Date.now() / 1000 + 3600).toString();
- const signatureRequest = await generateSignatureRequest(
- {
- ...resetData,
- deadline,
- },
- { chainId }
- );
- setApprovalTxState({ ...approvalTxState, loading: true });
- const response = await signTxData(signatureRequest);
- const splitedSignature = splitSignature(response);
- const encodedSignature = defaultAbiCoder.encode(
- ['address', 'address', 'uint256', 'uint256', 'uint8', 'bytes32', 'bytes32'],
- [
- resetData.user,
- resetData.spender,
- resetData.amount,
- deadline,
- splitedSignature.v,
- splitedSignature.r,
- splitedSignature.s,
- ]
- );
- setSignatureParams({
- signature: encodedSignature,
- deadline,
- amount: resetData.amount,
- approvedToken: resetData.spender,
- });
- } else {
- // Create direct ERC20 approval transaction for reset to 0 as ERC20Service requires positive amount
- const abi = new ethers.utils.Interface([
- 'function approve(address spender, uint256 amount)',
- ]);
- const encodedData = abi.encodeFunctionData('approve', [spender, '0']);
- const resetTx = {
- data: encodedData,
- to: inputToken,
- from: user,
- };
- const resetTxWithGasEstimation = await estimateGasLimit(resetTx, chainId);
- setApprovalTxState({ ...approvalTxState, loading: true });
- const resetResponse = await sendTx(resetTxWithGasEstimation);
- await resetResponse.wait(1);
- }
- } catch (error) {
- const parsedError = getErrorTextFromError(error, TxAction.APPROVAL, false);
- console.error(parsedError);
- setTxError(parsedError);
- setApprovalTxState({
- txHash: undefined,
- loading: false,
- });
- }
- fetchApprovedAmount().then(() => {
- setApprovalTxState({
- loading: false,
- success: false,
- });
- });
-
- return;
- }
-
- const approvalData = {
- spender,
- user,
- token: inputToken,
- amount: amountToApprove,
- };
- try {
- if (useSignature) {
- const deadline = Math.floor(Date.now() / 1000 + 3600).toString();
- const signatureRequest = await generateSignatureRequest(
- {
- ...approvalData,
- deadline,
- },
- { chainId }
- );
- setApprovalTxState({ ...approvalTxState, loading: true });
- const response = await signTxData(signatureRequest);
- const splitedSignature = splitSignature(response);
- const encodedSignature = defaultAbiCoder.encode(
- ['address', 'address', 'uint256', 'uint256', 'uint8', 'bytes32', 'bytes32'],
- [
- approvalData.user,
- approvalData.spender,
- approvalData.amount,
- deadline,
- splitedSignature.v,
- splitedSignature.r,
- splitedSignature.s,
- ]
- );
- setSignatureParams({
- signature: encodedSignature,
- deadline,
- amount: approvalData.amount,
- approvedToken: approvalData.spender,
- });
- setApprovalTxState({
- txHash: MOCK_SIGNED_HASH,
- loading: false,
- success: true,
- });
- } else {
- const tx = generateApproval(approvalData, { chainId, amount: amountToApprove });
- const txWithGasEstimation = await estimateGasLimit(tx, chainId);
- setApprovalTxState({ loading: true });
- const response = await sendTx(txWithGasEstimation);
- await response.wait(1);
- fetchApprovedAmount().then(() => {
- setApprovalTxState({
- txHash: response.hash,
- loading: false,
- success: true,
- });
- setTxError(undefined);
- });
- }
- } catch (error) {
- const parsedError = getErrorTextFromError(error, TxAction.APPROVAL, false);
- console.error(parsedError);
- setTxError(parsedError);
- setApprovalTxState({
- txHash: undefined,
- loading: false,
- });
- }
- };
-
- const fetchApprovedAmount = useCallback(async () => {
- if (isParaswapRates(switchRates) && switchRates.optimalRateData.tokenTransferProxy) {
- setSignatureParams(undefined);
- setApprovalTxState({
- txHash: undefined,
- loading: false,
- success: false,
- });
- setLoadingTxns(true);
- const rpc = getProvider(chainId);
- const erc20Service = new ERC20Service(rpc);
- const approvedTargetAmount = await erc20Service.approvedAmount({
- user,
- token: inputToken,
- spender:
- useFlashloan && modalType === ModalType.CollateralSwap
- ? currentMarketData.addresses.SWAP_COLLATERAL_ADAPTER!
- : switchRates.optimalRateData.tokenTransferProxy,
- });
-
- setApprovedAmount(approvedTargetAmount);
- setLoadingTxns(false);
- } else if (isCowProtocolRates(switchRates)) {
- // Check approval to VaultRelayer
- setSignatureParams(undefined);
- setApprovalTxState({
- txHash: undefined,
- loading: false,
- success: false,
- });
- setLoadingTxns(true);
- const rpc = getProvider(chainId);
- const erc20Service = new ERC20Service(rpc);
- const approvedTargetAmount = await erc20Service.approvedAmount({
- user,
- token: inputToken,
- spender: COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId as SupportedChainId],
- });
- setApprovedAmount(approvedTargetAmount);
- setLoadingTxns(false);
- }
- }, [
- chainId,
- setLoadingTxns,
- user,
- inputToken,
- switchRates,
- setApprovalTxState,
- useFlashloan,
- modalType,
- currentMarketData,
- ]);
-
- useEffect(() => {
- if (user) {
- fetchApprovedAmount();
- }
- }, [fetchApprovedAmount, user]);
-
- // Track execution state to pause rate updates during actions
- useEffect(() => {
- const isExecuting = mainTxState.loading || approvalTxState.loading || loadingTxns;
-
- setIsExecutingActions?.(isExecuting);
- }, [mainTxState.loading, approvalTxState.loading, loadingTxns, setIsExecutingActions]);
-
- useEffect(() => {
- let switchGasLimit = 0;
- if (isParaswapRates(switchRates)) {
- switchGasLimit += Number(
- gasLimitRecommendations[ProtocolAction.withdrawAndSwitch].recommended
- );
- }
- if (requiresApproval && !approvalTxState.success) {
- switchGasLimit += Number(APPROVAL_GAS_LIMIT);
- if (requiresApprovalReset) {
- switchGasLimit += Number(APPROVAL_GAS_LIMIT); // Reset approval
- }
- }
- if (isNativeToken(inputToken)) {
- switchGasLimit += Number(
- gasLimitRecommendations[ProtocolAction.withdrawAndSwitch].recommended
- );
- }
- setGasLimit(switchGasLimit.toString());
- setShowGasStation(requiresApproval || isNativeToken(inputToken));
- }, [requiresApproval, approvalTxState, setGasLimit, setShowGasStation, requiresApprovalReset]);
-
- // For flashloan collateral swaps with ParaSwap, use the SwapActions component
- // which has the correct approval logic via useParaSwapTransactionHandler
- if (
- !loading &&
- (useFlashloan || switchRates?.provider === 'paraswap') &&
- modalType === ModalType.CollateralSwap
- ) {
- if (!switchRates || !poolReserve || !targetReserve || !isParaswapRates(switchRates))
- return null;
-
- return (
-
- );
- }
-
- return (
- approval()}
- requiresApproval={!blocked && requiresApproval}
- actionText={Swap}
- actionInProgressText={Swapping}
- errorParams={{
- loading: false,
- disabled: blocked || (!approvalTxState.success && requiresApproval),
- content: Swap,
- handleClick: action,
- }}
- fetchingData={loading}
- blocked={blocked}
- tryPermit={tryPermit}
- />
- );
-};
diff --git a/src/components/transactions/Switch/SwitchAssetInput.tsx b/src/components/transactions/Switch/SwitchAssetInput.tsx
deleted file mode 100644
index b4218cc39e..0000000000
--- a/src/components/transactions/Switch/SwitchAssetInput.tsx
+++ /dev/null
@@ -1,503 +0,0 @@
-import { isAddress } from '@ethersproject/address';
-import { formatUnits } from '@ethersproject/units';
-import { ExclamationIcon } from '@heroicons/react/outline';
-import { XCircleIcon } from '@heroicons/react/solid';
-import { Trans } from '@lingui/macro';
-import { ExpandMore } from '@mui/icons-material';
-import {
- Box,
- Button,
- CircularProgress,
- IconButton,
- InputBase,
- ListItemText,
- MenuItem,
- SvgIcon,
- Typography,
- useTheme,
-} from '@mui/material';
-import React, { useEffect, useRef, useState } from 'react';
-import NumberFormat, { NumberFormatProps } from 'react-number-format';
-import { TokenInfoWithBalance } from 'src/hooks/generic/useTokensBalance';
-import { useRootStore } from 'src/store/root';
-import { useSharedDependencies } from 'src/ui-config/SharedDependenciesProvider';
-
-import { COMMON_SWAPS } from '../../../ui-config/TokenList';
-import { BasicModal } from '../../primitives/BasicModal';
-import { FormattedNumber } from '../../primitives/FormattedNumber';
-import { ExternalTokenIcon } from '../../primitives/TokenIcon';
-import { SearchInput } from '../../SearchInput';
-
-interface CustomProps {
- onChange: (event: { target: { name: string; value: string } }) => void;
- name: string;
- value: string;
-}
-
-export const NumberFormatCustom = React.forwardRef(
- function NumberFormatCustom(props, ref) {
- const { onChange, ...other } = props;
-
- return (
- {
- if (values.value !== props.value)
- onChange({
- target: {
- name: props.name,
- value: values.value || '',
- },
- });
- }}
- thousandSeparator
- isNumericString
- allowNegative={false}
- />
- );
- }
-);
-
-export interface AssetInputProps {
- value: string;
- usdValue: string;
- chainId: number;
- onChange?: (value: string) => void;
- disabled?: boolean;
- disableInput?: boolean;
- onSelect?: (asset: TokenInfoWithBalance) => void;
- assets: TokenInfoWithBalance[];
- maxValue?: string;
- forcedMaxValue?: string;
- loading?: boolean;
- selectedAsset: TokenInfoWithBalance;
- balanceTitle?: string;
- showBalance?: boolean;
- allowCustomTokens?: boolean;
-}
-
-export const SwitchAssetInput = ({
- value,
- usdValue,
- onChange,
- disabled,
- disableInput,
- onSelect,
- assets,
- maxValue,
- forcedMaxValue,
- loading = false,
- chainId,
- selectedAsset,
- balanceTitle,
- showBalance = true,
- allowCustomTokens = true,
-}: AssetInputProps) => {
- const theme = useTheme();
- const handleSelect = (asset: TokenInfoWithBalance) => {
- onSelect && onSelect(asset);
- onChange && onChange('');
- handleClose();
- };
-
- const { erc20Service } = useSharedDependencies();
-
- const [openModal, setOpenModal] = useState(false);
- const inputRef = useRef(null);
-
- const handleClick = () => {
- if (!allowCustomTokens && assets.length === 1) return;
- setOpenModal(true);
- };
-
- const handleClose = () => {
- setOpenModal(false);
- handleCleanSearch();
- };
-
- const [filteredAssets, setFilteredAssets] = useState(assets);
- const [loadingNewAsset, setLoadingNewAsset] = useState(false);
- const user = useRootStore((store) => store.account);
-
- useEffect(() => {
- setFilteredAssets(assets);
- }, [assets]);
-
- const popularAssets = assets.filter((asset) => COMMON_SWAPS.includes(asset.symbol));
- const handleSearchAssetChange = (value: string) => {
- const searchQuery = value.trim().toLowerCase();
- const matchingAssets = assets.filter(
- (asset) =>
- asset.symbol.toLowerCase().includes(searchQuery) ||
- asset.name.toLowerCase().includes(searchQuery) ||
- asset.address.toLowerCase() === searchQuery
- );
- if (matchingAssets.length === 0) {
- // If custom tokens are not allowed, do not attempt to import by address
- if (!allowCustomTokens) {
- setLoadingNewAsset(false);
- setFilteredAssets([]);
- return;
- }
-
- if (isAddress(value)) {
- setLoadingNewAsset(true);
- Promise.all([
- erc20Service.getTokenInfo(value, chainId),
- erc20Service.getBalance(value, user, chainId),
- ])
- .then(([tokenMetadata, userBalance]) => {
- const tokenInfo = {
- chainId: chainId,
- balance: formatUnits(userBalance, tokenMetadata.decimals),
- extensions: {
- isUserCustom: true,
- },
- ...tokenMetadata,
- };
- setFilteredAssets([tokenInfo]);
- })
- .catch(() => setFilteredAssets([]))
- .finally(() => setLoadingNewAsset(false));
- return;
- }
-
- setFilteredAssets([]);
- } else {
- setFilteredAssets(matchingAssets);
- }
- };
-
- const handleCleanSearch = () => {
- setFilteredAssets(assets);
- setLoadingNewAsset(false);
- };
-
- return (
- ({
- border: `1px solid ${theme.palette.divider}`,
- borderRadius: '6px',
- overflow: 'hidden',
- px: 3,
- py: 2,
- width: '100%',
- })}
- >
-
- {loading ? (
-
-
-
- ) : (
- {
- if (!onChange) return;
- if (Number(e.target.value) > Number(maxValue)) {
- onChange('-1');
- } else {
- onChange(e.target.value);
- }
- }}
- inputProps={{
- 'aria-label': 'amount input',
- style: {
- width: '100%',
- fontSize: '21px',
- lineHeight: '28,01px',
- padding: 0,
- height: '28px',
- textOverflow: 'ellipsis',
- whiteSpace: 'nowrap',
- overflow: 'hidden',
- },
- }}
- // eslint-disable-next-line
- inputComponent={NumberFormatCustom as any}
- />
- )}
- {value !== '' && !disableInput && (
- {
- onChange && onChange('');
- }}
- disabled={disabled}
- >
-
-
- )}
-
-
-
-
-
- Select token
-
-
-
-
- {assets.length > 3 && (
-
- {popularAssets.map((asset) => (
- handleSelect(asset)}
- >
-
-
- {asset.symbol}
-
-
- ))}
-
- )}
-
-
- {loadingNewAsset ? (
-
-
-
- ) : filteredAssets.length > 0 ? (
- filteredAssets.map((asset) => (
-
- }
- />
- {asset.extensions?.isUserCustom && (
-
-
-
- )}
- {asset.balance && (
-
- )}
-
- ))
- ) : (
-
- {allowCustomTokens ? (
-
- No results found. You can import a custom token with a contract address
-
- ) : (
- No results found.
- )}
-
- )}
-
-
-
-
-
-
- {loading ? (
-
- ) : (
-
- )}
-
- {showBalance && selectedAsset.balance && (
- <>
-
- {balanceTitle || 'Balance'}
-
-
- {!disableInput && (
-
- )}
- >
- )}
-
-
- );
-};
diff --git a/src/components/transactions/Switch/SwitchErrors.tsx b/src/components/transactions/Switch/SwitchErrors.tsx
deleted file mode 100644
index 46377cc5cc..0000000000
--- a/src/components/transactions/Switch/SwitchErrors.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Trans } from '@lingui/macro';
-import { SxProps, Typography } from '@mui/material';
-import { Warning } from 'src/components/primitives/Warning';
-
-import { SwitchRatesError } from './SwitchRatesError';
-
-interface SwitchErrorsProps {
- ratesError: unknown;
- balance: string;
- inputAmount: string;
- sx?: SxProps;
-}
-
-export const SwitchErrors = ({ ratesError, balance, inputAmount, sx }: SwitchErrorsProps) => {
- if (ratesError) {
- return ;
- } else if (Number(inputAmount) > Number(balance)) {
- return (
-
-
- Your balance is lower than the selected amount.
-
-
- );
- }
- return null;
-};
diff --git a/src/components/transactions/Switch/SwitchLimitOrdersActions.tsx b/src/components/transactions/Switch/SwitchLimitOrdersActions.tsx
deleted file mode 100644
index e957f3e9cd..0000000000
--- a/src/components/transactions/Switch/SwitchLimitOrdersActions.tsx
+++ /dev/null
@@ -1,243 +0,0 @@
-import { valueToBigNumber } from '@aave/math-utils';
-import {
- COW_PROTOCOL_VAULT_RELAYER_ADDRESS,
- OrderClass,
- OrderKind,
- SupportedChainId,
- TradingSdk,
-} from '@cowprotocol/cow-sdk';
-import { Trans } from '@lingui/macro';
-import { useQueryClient } from '@tanstack/react-query';
-import { ComputedReserveData } from 'src/hooks/app-data-provider/useAppDataProvider';
-import { TokenInfoWithBalance } from 'src/hooks/generic/useTokensBalance';
-import { useApprovalTx } from 'src/hooks/useApprovalTx';
-import { useApprovedAmount } from 'src/hooks/useApprovedAmount';
-import { useModalContext } from 'src/hooks/useModal';
-import { getEthersProvider } from 'src/libs/web3-data-provider/adapters/EthersAdapter';
-import { useRootStore } from 'src/store/root';
-import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
-import { findByChainId } from 'src/ui-config/marketsConfig';
-import { queryKeysFactory } from 'src/ui-config/queries';
-import { wagmiConfig } from 'src/ui-config/wagmiConfig';
-import { GENERAL } from 'src/utils/events';
-import { parseUnits } from 'viem';
-import { useShallow } from 'zustand/shallow';
-
-import { TxActionsWrapper } from '../TxActionsWrapper';
-import { checkRequiresApproval } from '../utils';
-import {
- COW_APP_DATA,
- COW_PARTNER_FEE,
- HEADER_WIDGET_APP_CODE,
-} from './cowprotocol/cowprotocol.helpers';
-
-interface SwitchProps {
- inputAmount: string;
- inputToken: TokenInfoWithBalance;
- outputToken: TokenInfoWithBalance;
- outputAmount: string;
- setShowUSDTResetWarning?: (showUSDTResetWarning: boolean) => void;
- blocked: boolean;
- loading?: boolean;
- isWrongNetwork: boolean;
- chainId: number;
- //setShowGasStation: (showGasStation: boolean) => void;
- poolReserve?: ComputedReserveData;
- targetReserve?: ComputedReserveData;
- expirationTime: number;
- inputAmountUSD: number;
- outputAmountUSD: number;
- // setIsExecutingActions?: (isExecuting: boolean) => void;
-}
-
-export const SwitchLimitOrdersActions = ({
- inputAmount,
- inputToken,
- outputToken,
- setShowUSDTResetWarning,
- blocked,
- loading,
- isWrongNetwork,
- chainId,
- outputAmount,
- expirationTime,
- inputAmountUSD,
- outputAmountUSD,
-}: // setShowGasStation,
-// setIsExecutingActions,
-SwitchProps) => {
- const [
- user,
- //addTransaction,
- currentMarketData,
- trackEvent,
- ] = useRootStore(
- useShallow((state) => [
- state.account,
- //state.addTransaction,
- state.currentMarketData,
- state.trackEvent,
- ])
- );
-
- const { approvalTxState, mainTxState, loadingTxns, setMainTxState, setLoadingTxns, setTxError } =
- useModalContext();
-
- const queryClient = useQueryClient();
-
- const {
- data: approvedAmount,
- isFetching: fetchingApprovedAmount,
- refetch: fetchApprovedAmount,
- } = useApprovedAmount({
- chainId,
- token: inputToken.address,
- spender: COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId as SupportedChainId],
- });
-
- setLoadingTxns(fetchingApprovedAmount);
-
- let requiresApproval = false;
- if (approvedAmount !== undefined) {
- requiresApproval = checkRequiresApproval({
- approvedAmount: approvedAmount.toString(),
- amount: inputAmount,
- signedAmount: '0',
- });
- }
-
- const { approval, requiresApprovalReset } = useApprovalTx({
- usePermit: false,
- approvedAmount: {
- amount: approvedAmount?.toString() || '0',
- spender: COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId as SupportedChainId],
- token: inputToken.address,
- user,
- },
- requiresApproval,
- assetAddress: inputToken.address,
- symbol: inputToken.symbol,
- decimals: inputToken.decimals,
- signatureAmount: inputAmount,
- onApprovalTxConfirmed: () => fetchApprovedAmount(),
- setShowUSDTResetWarning,
- });
-
- const invalidate = () => {
- queryClient.invalidateQueries({
- queryKey: queryKeysFactory.poolReservesDataHumanized(
- findByChainId(chainId) ?? currentMarketData
- ),
- });
-
- queryClient.invalidateQueries({
- queryKey: queryKeysFactory.userPoolReservesDataHumanized(
- user,
- findByChainId(chainId) ?? currentMarketData
- ),
- });
-
- queryClient.invalidateQueries({
- queryKey: queryKeysFactory.transactionHistory(
- user,
- findByChainId(chainId) ?? currentMarketData
- ),
- });
-
- queryClient.invalidateQueries({
- queryKey: queryKeysFactory.poolTokens(user, currentMarketData),
- });
- };
-
- const action = async () => {
- try {
- setMainTxState({ ...mainTxState, loading: true });
- invalidate();
- const provider = getEthersProvider(wagmiConfig, { chainId });
- const signer = (await provider).getSigner();
- const tradingSdk = new TradingSdk({ chainId, signer, appCode: HEADER_WIDGET_APP_CODE });
- const receipt = await tradingSdk.postLimitOrder(
- {
- sellAmount: parseUnits(inputAmount, inputToken.decimals).toString(),
- buyAmount: parseUnits(outputAmount, outputToken.decimals).toString(),
- kind: OrderKind.SELL,
- sellToken: inputToken.address,
- buyToken: outputToken.address,
- sellTokenDecimals: inputToken.decimals,
- buyTokenDecimals: outputToken.decimals,
- slippageBps: 0,
- partnerFee: COW_PARTNER_FEE(inputToken.symbol, outputToken.symbol),
- validFor: expirationTime,
- },
- {
- appData: COW_APP_DATA(
- inputToken.symbol,
- outputToken.symbol,
- 0,
- false,
- OrderClass.LIMIT,
- HEADER_WIDGET_APP_CODE
- ),
- }
- );
- setMainTxState({
- loading: false,
- success: true,
- txHash: receipt.orderId ?? undefined,
- });
- try {
- const baseTrackingData: Record = {
- chainId,
- inputSymbol: inputToken.symbol,
- outputSymbol: outputToken.symbol,
- pair: `${inputToken.symbol}-${outputToken.symbol}`,
- inputAmount,
- outputAmount,
- provider: 'cowswap',
- inputAmountUSD,
- outputAmountUSD,
- rate: valueToBigNumber(inputAmount).div(outputAmount).toString(),
- };
-
- trackEvent(GENERAL.LIMIT_ORDER, {
- ...baseTrackingData,
- });
- } catch (error) {
- console.error('Error tracking limit order event:', error);
- }
- } catch (error) {
- const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
- setTxError(parsedError);
- setMainTxState({
- txHash: undefined,
- loading: false,
- });
- }
- };
-
- return (
- approval()}
- requiresApproval={!blocked && requiresApproval}
- actionText={Create limit order}
- actionInProgressText={Creating limit order}
- errorParams={{
- loading: false,
- disabled: blocked || (!approvalTxState.success && requiresApproval),
- content: Create limit order,
- handleClick: action,
- }}
- fetchingData={loading}
- blocked={blocked}
- tryPermit={false}
- requiresApprovalReset={requiresApprovalReset}
- />
- );
-};
diff --git a/src/components/transactions/Switch/SwitchLimitOrdersModalContent.tsx b/src/components/transactions/Switch/SwitchLimitOrdersModalContent.tsx
deleted file mode 100644
index 13a90a9e85..0000000000
--- a/src/components/transactions/Switch/SwitchLimitOrdersModalContent.tsx
+++ /dev/null
@@ -1,374 +0,0 @@
-import { normalize, valueToBigNumber } from '@aave/math-utils';
-import { SupportedChainId, WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/cow-sdk';
-import { Trans } from '@lingui/macro';
-import { Box, CircularProgress, Typography } from '@mui/material';
-import { useEffect, useMemo, useState } from 'react';
-import { TokenInfoWithBalance, useTokensBalance } from 'src/hooks/generic/useTokensBalance';
-import { useCowSwitchRates } from 'src/hooks/switch/useCowSwitchRates';
-import { useGetConnectedWalletType } from 'src/hooks/useGetConnectedWalletType';
-import { useIsWrongNetwork } from 'src/hooks/useIsWrongNetwork';
-import { useModalContext } from 'src/hooks/useModal';
-import { StaticRate, useStaticRate } from 'src/hooks/useStaticRate';
-import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
-import { useRootStore } from 'src/store/root';
-import { CustomMarket, marketsData } from 'src/ui-config/marketsConfig';
-import { GENERAL } from 'src/utils/events';
-import { getNetworkConfig } from 'src/utils/marketsAndNetworksConfig';
-import { parseUnits } from 'viem';
-
-import { TxModalDetails } from '../FlowCommons/TxModalDetails';
-import { ChangeNetworkWarning } from '../Warnings/ChangeNetworkWarning';
-import { CowLowerThanMarketWarning } from '../Warnings/CowLowerThanMarketWarning';
-import { USDTResetWarning } from '../Warnings/USDTResetWarning';
-import { getFilteredTokensForSwitch } from './BaseSwitchModal';
-import { supportedNetworksWithEnabledMarketLimit } from './common';
-import { isNativeToken } from './cowprotocol/cowprotocol.helpers';
-import { Expiry, ExpirySelector } from './ExpirySelector';
-import { NetworkSelector } from './NetworkSelector';
-import { PriceInput } from './PriceInput';
-import { SwitchAssetInput } from './SwitchAssetInput';
-import { SwitchErrors } from './SwitchErrors';
-import { SwitchLimitOrdersActions } from './SwitchLimitOrdersActions';
-import { IntentTxDetails } from './SwitchModalTxDetails';
-import { SwitchTxSuccessView } from './SwitchTxSuccessView';
-
-const calculateMaxAmount = (token: TokenInfoWithBalance, chainId: number) => {
- const nativeDecimals = 18;
- const gasRequiredForEthFlow =
- chainId === 1 ? parseUnits('0.01', nativeDecimals) : parseUnits('0.0001', nativeDecimals); // TODO: Ask for better value coming from the SDK
- const requiredAssetsLeftForGas = isNativeToken(token.address) ? gasRequiredForEthFlow : BigInt(0);
- const balance = parseUnits(token.balance || '0', nativeDecimals);
- const maxAmount =
- balance > requiredAssetsLeftForGas ? balance - requiredAssetsLeftForGas : balance;
- return normalize(maxAmount.toString(), nativeDecimals).toString();
-};
-
-const defaultNetwork = marketsData[CustomMarket.proto_arbitrum_v3];
-
-interface SwitchLimitOrdersInputsProps {
- chainId: number;
- tokens: TokenInfoWithBalance[];
- inputToken: TokenInfoWithBalance;
- inputAmount: string;
- outputAmount: string;
- handleInputAmountChange: (value: string) => void;
- handleInputTokenChange: (token: TokenInfoWithBalance) => void;
- outputToken: TokenInfoWithBalance;
- handleOutputTokenChange: (token: TokenInfoWithBalance) => void;
- rate: string;
- handleRateChange: (value: string) => void;
- initialRate?: StaticRate;
- rateLoading: boolean;
- isInvertedRate: boolean;
- setIsInvertedRate: (isInverted: boolean) => void;
-}
-
-export const SwitchLimitOrdersInputs = ({
- chainId,
- tokens,
- inputToken,
- inputAmount,
- handleInputAmountChange,
- handleInputTokenChange,
- outputToken,
- handleOutputTokenChange,
- rate,
- handleRateChange,
- initialRate,
- rateLoading,
- isInvertedRate,
- setIsInvertedRate,
- outputAmount,
-}: SwitchLimitOrdersInputsProps) => {
- const { isSmartContractWallet } = useGetConnectedWalletType();
- const maxInputAmount = isSmartContractWallet
- ? calculateMaxAmount(inputToken, chainId)
- : inputToken.balance;
-
- const rateUsd = isInvertedRate
- ? Number(rate) * Number(initialRate?.inputUsdPrice || '0')
- : Number(rate) * Number(initialRate?.outputUsdPrice || '0');
-
- return (
-
-
- {Sell}
- token.address !== outputToken.address && !token.extensions?.isNative
- )}
- value={inputAmount}
- onChange={handleInputAmountChange}
- usdValue={(Number(initialRate?.inputUsdPrice) * Number(inputAmount)).toString() || '0'}
- onSelect={handleInputTokenChange}
- selectedAsset={inputToken}
- forcedMaxValue={maxInputAmount}
- allowCustomTokens={true}
- />
-
-
- {Receive at least}
-
- token.address !== inputToken.address &&
- // Avoid wrapping
- !(
- isNativeToken(inputToken.address) &&
- token.address.toLowerCase() ===
- WRAPPED_NATIVE_CURRENCIES[chainId as SupportedChainId]?.address.toLowerCase()
- )
- )}
- value={outputAmount.toString()}
- usdValue={(Number(initialRate?.outputUsdPrice) * Number(outputAmount)).toString() || '0'}
- loading={rateLoading}
- onSelect={handleOutputTokenChange}
- disableInput={true}
- selectedAsset={outputToken}
- showBalance={false}
- allowCustomTokens={true}
- />
-
-
-
- );
-};
-
-interface SwitchLimitOrdersInnerProps {
- tokens: TokenInfoWithBalance[];
- chainId: number;
- setChainId: (chainId: number) => void;
-}
-
-export const SwitchLimitOrdersInner = ({
- tokens,
- chainId,
- setChainId,
-}: SwitchLimitOrdersInnerProps) => {
- const { readOnlyModeAddress } = useWeb3Context();
-
- const [inputToken, setInputToken] = useState(
- tokens.find(
- (token) => token.balance !== '0' && !token.extensions?.isNative && token.symbol !== 'GHO'
- ) || tokens[0]
- );
-
- const { mainTxState } = useModalContext();
-
- const userAddress = useRootStore((store) => store.account);
-
- const [expiry, setExpiry] = useState(Expiry['One week']);
-
- const [inputAmount, setInputAmount] = useState('');
- const [rate, setRate] = useState('');
- const [isInvertedRate, setIsInvertedRate] = useState(false);
-
- const [outputToken, setOutputToken] = useState(
- tokens.find((token) => token.symbol == 'GHO') || tokens[1]
- );
- const [showUSDTResetWarning, setShowUSDTResetWarning] = useState(false);
-
- const { data: staticRate, isLoading: staticRateLoading } = useStaticRate({
- chainId,
- inputToken,
- outputToken,
- });
- const {
- data: quote,
- isLoading: quoteLoading,
- error: quoteError,
- } = useCowSwitchRates({
- chainId,
- amount: inputAmount ? parseUnits(inputAmount, inputToken.decimals).toString() : '0',
- srcUnderlyingToken: inputToken.address,
- destUnderlyingToken: outputToken.address,
- user: userAddress,
- inputSymbol: inputToken.symbol,
- isInputTokenCustom: !!inputToken.extensions?.isCustom,
- isOutputTokenCustom: !!outputToken.extensions?.isCustom,
- outputSymbol: outputToken.symbol,
- srcDecimals: inputToken.decimals,
- destDecimals: outputToken.decimals,
- isTxSuccess: false,
- });
-
- const outputAmount =
- inputAmount && rate
- ? isInvertedRate && Number(rate) !== 0
- ? (Number(inputAmount) * (1 / Number(rate))).toString()
- : (Number(inputAmount) * Number(rate)).toString()
- : '';
-
- const isWrongNetwork = useIsWrongNetwork(chainId);
- const { isSmartContractWallet } = useGetConnectedWalletType();
-
- const showChangeNetworkWarning = isWrongNetwork.isWrongNetwork && !readOnlyModeAddress;
- const selectedNetworkConfig = getNetworkConfig(chainId);
-
- const rateLowerThanMarket =
- staticRate &&
- quote &&
- (isInvertedRate
- ? valueToBigNumber(staticRate.rate).lt(valueToBigNumber(rate))
- : valueToBigNumber(rate).lt(valueToBigNumber(staticRate.rate)));
-
- useEffect(() => {
- if (staticRate) {
- setRate(staticRate.rate);
- }
- }, [staticRate]);
-
- if (quote && mainTxState.success) {
- return (
-
- );
- }
-
- return (
- <>
- {showChangeNetworkWarning && (
-
- )}
-
-
-
-
-
- {quote && (
-
-
-
- )}
- {rateLowerThanMarket && }
- {showUSDTResetWarning && }
-
-
- >
- );
-};
-
-export const SwitchLimitOrdersModalContent = () => {
- const dashboardChainId = useRootStore((store) => store.currentChainId);
- const user = useRootStore((store) => store.account);
-
- const [selectedChainId, setSelectedChainId] = useState(() => {
- if (supportedNetworksWithEnabledMarketLimit.find((elem) => elem.chainId === dashboardChainId))
- return dashboardChainId;
- return defaultNetwork.chainId;
- });
- const tokens = useMemo(() => getFilteredTokensForSwitch(selectedChainId), [selectedChainId]);
-
- const { data: tokensWithBalance } = useTokensBalance(tokens, selectedChainId, user);
- if (!tokensWithBalance) {
- return (
-
-
-
- );
- }
- return (
-
- );
-};
diff --git a/src/components/transactions/Switch/SwitchModalTxDetails.tsx b/src/components/transactions/Switch/SwitchModalTxDetails.tsx
deleted file mode 100644
index 9e69b1ee71..0000000000
--- a/src/components/transactions/Switch/SwitchModalTxDetails.tsx
+++ /dev/null
@@ -1,509 +0,0 @@
-import { normalize, normalizeBN, valueToBigNumber } from '@aave/math-utils';
-import { Trans } from '@lingui/macro';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import {
- Accordion,
- AccordionDetails,
- AccordionSummary,
- Box,
- Skeleton,
- Typography,
-} from '@mui/material';
-import { BigNumber } from 'bignumber.js';
-import { useState } from 'react';
-import { NetworkCostTooltip } from 'src/components/infoTooltips/NetworkCostTooltip';
-import { SwapFeeTooltip } from 'src/components/infoTooltips/SwapFeeTooltip';
-import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
-import { Row } from 'src/components/primitives/Row';
-import { ExternalTokenIcon } from 'src/components/primitives/TokenIcon';
-import { CollateralType } from 'src/helpers/types';
-import {
- ComputedReserveData,
- ComputedUserReserveData,
- ExtendedFormattedUser,
-} from 'src/hooks/app-data-provider/useAppDataProvider';
-import { TokenInfoWithBalance } from 'src/hooks/generic/useTokensBalance';
-import { getDebtCeilingData } from 'src/hooks/useAssetCaps';
-import { ModalType } from 'src/hooks/useModal';
-import { calculateHFAfterSwap } from 'src/utils/hfUtils';
-
-import { TxModalDetails } from '../FlowCommons/TxModalDetails';
-import { getAssetCollateralType } from '../utils';
-import { CollateralSwapModalDetails } from './CollateralSwap/CollateralSwapModalDetails';
-import { SwitchRatesType } from './switch.types';
-
-export const SwitchModalTxDetails = ({
- switchRates,
- selectedOutputToken,
- safeSlippage,
- gasLimit,
- selectedChainId,
- customReceivedTitle,
- reserves,
- user,
- selectedInputToken,
- modalType,
- loading,
-}: {
- switchRates?: SwitchRatesType;
- selectedOutputToken: TokenInfoWithBalance;
- safeSlippage: number;
- gasLimit: string;
- selectedChainId: number;
- showGasStation: boolean | undefined;
- customReceivedTitle?: React.ReactNode;
- reserves: ComputedReserveData[];
- user?: ExtendedFormattedUser;
- selectedInputToken: TokenInfoWithBalance;
- modalType: ModalType;
- loading?: boolean;
-}) => {
- if (!switchRates || !user) return null;
-
- if (loading)
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-
- return (
-
- {modalType === ModalType.CollateralSwap && (
-
- )}
-
- {switchRates.provider === 'cowprotocol' ? (
-
- ) : (
-
- )}
-
- );
-};
-export const IntentTxDetails = ({
- selectedOutputToken,
- selectedInputToken,
- safeSlippage,
- customReceivedTitle,
- networkFee,
- partnerFee,
- outputTokenPriceUsd,
- inputTokenPriceUsd,
- outputAmount,
- inputAmount,
-}: {
- selectedOutputToken: TokenInfoWithBalance;
- selectedInputToken: TokenInfoWithBalance;
- safeSlippage: number;
- customReceivedTitle?: React.ReactNode;
- networkFee: string;
- partnerFee: string;
- outputTokenPriceUsd: number;
- inputTokenPriceUsd: number;
- outputAmount: string;
- inputAmount: string;
-}) => {
- const [costBreakdownExpanded, setCostBreakdownExpanded] = useState(false);
-
- const networkFeeFormatted = normalize(networkFee, selectedOutputToken.decimals);
- const networkFeeUsd = Number(networkFeeFormatted) * outputTokenPriceUsd;
-
- const partnerFeeFormatted = normalize(partnerFee, selectedOutputToken.decimals);
- const partnerFeeUsd = Number(partnerFeeFormatted) * outputTokenPriceUsd;
-
- const totalCostsInUsd = networkFeeUsd + partnerFeeUsd; // + costs.slippageInUsd;
-
- const destUsd = normalizeBN(outputAmount, selectedOutputToken.decimals)
- .multipliedBy(outputTokenPriceUsd)
- .toNumber();
- const srcUsd = normalizeBN(inputAmount, selectedInputToken.decimals)
- .multipliedBy(inputTokenPriceUsd)
- .toNumber();
-
- const receivingInUsd = destUsd * (1 - safeSlippage);
- const sendingInUsd = srcUsd;
-
- const priceImpact = (1 - receivingInUsd / sendingInUsd) * 100;
-
- const destAmountAfterSlippage = normalizeBN(outputAmount, selectedOutputToken.decimals)
- .multipliedBy(1 - safeSlippage)
- .decimalPlaces(selectedOutputToken.decimals, BigNumber.ROUND_UP)
- .toString();
-
- return (
- <>
- {
- setCostBreakdownExpanded(expanded);
- }}
- >
- }
- sx={{
- padding: 0,
- minHeight: '24px',
- height: '24px',
- '.MuiAccordionSummary-content': { margin: 0 },
- }}
- >
- {`Costs & Fees`}}
- captionVariant="description"
- align="flex-start"
- width="100%"
- >
- {!costBreakdownExpanded && (
-
- )}
-
-
-
-
}
- captionVariant="caption"
- align="flex-start"
- >
-
-
-
-
-
-
-
-
-
}
- captionVariant="caption"
- align="flex-start"
- >
-
-
-
-
-
-
-
-
-
-
-
- {`Minimum ${selectedOutputToken.symbol} received`}
- }
- captionVariant="description"
- align="flex-start"
- >
-
-
-
-
-
-
-
- {priceImpact && priceImpact > 0 && priceImpact < 100 && (
- 10 ? 'error' : priceImpact > 5 ? 'warning' : 'text.secondary'}
- >
- (-{priceImpact.toFixed(priceImpact > 3 ? 0 : priceImpact > 1 ? 1 : 2)}%)
-
- )}
-
-
-
- >
- );
-};
-const MarketOrderTxDetails = ({
- switchRates,
- selectedOutputToken,
- safeSlippage,
- customReceivedTitle,
-}: {
- switchRates: SwitchRatesType;
- selectedOutputToken: TokenInfoWithBalance;
- safeSlippage: number;
- customReceivedTitle?: React.ReactNode;
-}) => {
- return (
- <>
- {`Minimum ${selectedOutputToken.symbol} received`}
- }
- captionVariant="description"
- align="flex-start"
- >
-
-
-
-
-
-
-
-
- >
- );
-};
-
-const CollateralSwapModalTxDetailsContent = ({
- switchRates,
- selectedOutputToken,
- safeSlippage,
- reserves,
- user,
- selectedInputToken,
-}: {
- switchRates: SwitchRatesType;
- selectedOutputToken: TokenInfoWithBalance;
- safeSlippage: number;
- customReceivedTitle?: React.ReactNode;
- reserves: ComputedReserveData[];
- user: ExtendedFormattedUser;
- selectedInputToken: TokenInfoWithBalance;
-}) => {
- // Map selected tokens to reserves and user reserves
- const poolReserve = reserves.find(
- (r) => r.underlyingAsset.toLowerCase() === selectedInputToken.address.toLowerCase()
- ) as ComputedReserveData | undefined;
- const targetReserve = reserves.find(
- (r) => r.underlyingAsset.toLowerCase() === selectedOutputToken.address.toLowerCase()
- ) as ComputedReserveData | undefined;
-
- if (!poolReserve || !targetReserve || !user) return null;
-
- const userReserve = user.userReservesData.find(
- (ur) => ur.underlyingAsset.toLowerCase() === poolReserve.underlyingAsset.toLowerCase()
- ) as ComputedUserReserveData | undefined;
- const userTargetReserve = user.userReservesData.find(
- (ur) => ur.underlyingAsset.toLowerCase() === targetReserve.underlyingAsset.toLowerCase()
- ) as ComputedUserReserveData | undefined;
-
- if (!userReserve || !userTargetReserve) return null;
-
- // Show HF only when there are borrows and source reserve is collateralizable
- const showHealthFactor =
- user.totalBorrowsMarketReferenceCurrency !== '0' &&
- poolReserve.reserveLiquidationThreshold !== '0';
-
- // Amounts in human units (mirror other components: intent uses destSpot, market uses destAmount)
- const fromAmount = normalizeBN(switchRates.srcAmount, switchRates.srcDecimals).toString();
- const toAmountRaw = normalizeBN(
- switchRates.provider === 'cowprotocol' ? switchRates.destSpot : switchRates.destAmount,
- switchRates.destDecimals
- ).toString();
- const toAmountAfterSlippage = valueToBigNumber(toAmountRaw)
- .multipliedBy(1 - safeSlippage)
- .toString();
-
- // Compute collateral types
- const { debtCeilingReached: sourceDebtCeiling } = getDebtCeilingData(targetReserve);
- const swapSourceCollateralType: CollateralType = getAssetCollateralType(
- userReserve,
- user.totalCollateralUSD,
- user.isInIsolationMode,
- sourceDebtCeiling
- );
- const { debtCeilingReached: targetDebtCeiling } = getDebtCeilingData(targetReserve);
- const swapTargetCollateralType: CollateralType = getAssetCollateralType(
- userTargetReserve,
- user.totalCollateralUSD,
- user.isInIsolationMode,
- targetDebtCeiling
- );
-
- // Health factor after swap using slippage-adjusted output amount
- const { hfAfterSwap } = calculateHFAfterSwap({
- fromAmount,
- fromAssetData: poolReserve,
- fromAssetUserData: userReserve,
- user,
- toAmountAfterSlippage: toAmountAfterSlippage,
- toAssetData: targetReserve,
- });
-
- return (
-
- );
-};
diff --git a/src/components/transactions/Switch/SwitchRatesError.tsx b/src/components/transactions/Switch/SwitchRatesError.tsx
deleted file mode 100644
index e84e94cb0a..0000000000
--- a/src/components/transactions/Switch/SwitchRatesError.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { SxProps, Typography } from '@mui/material';
-import { Warning } from 'src/components/primitives/Warning';
-import { convertParaswapErrorMessage } from 'src/hooks/paraswap/common';
-
-import { convertCowProtocolErrorMessage } from './cowprotocol/cowprotocol.errors';
-
-interface SwitchRatesErrorProps {
- error: unknown;
- sx?: SxProps;
-}
-
-export const SwitchRatesError = ({ error, sx }: SwitchRatesErrorProps) => {
- let paraswapMessage;
- let cowProtocolMessage;
-
- if (error instanceof Error) {
- paraswapMessage = convertParaswapErrorMessage(error.message);
-
- if (!paraswapMessage) {
- cowProtocolMessage = convertCowProtocolErrorMessage(error.message);
- }
- }
-
- const customErrorMessage =
- error instanceof Error
- ? paraswapMessage ?? cowProtocolMessage ?? error.message
- : 'There was an issue fetching rates.';
-
- return (
-
- {customErrorMessage}
-
- );
-};
diff --git a/src/components/transactions/Switch/cowprotocol/cowprotocol.constants.ts b/src/components/transactions/Switch/cowprotocol/cowprotocol.constants.ts
deleted file mode 100644
index 9d85df98ed..0000000000
--- a/src/components/transactions/Switch/cowprotocol/cowprotocol.constants.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { SupportedChainId } from '@cowprotocol/cow-sdk';
-import { ModalType } from 'src/hooks/useModal';
-
-export const COW_UNSUPPORTED_ASSETS: Partial<
- Record>>
-> = {
- [ModalType.CollateralSwap]: {
- [SupportedChainId.POLYGON]: [
- '0x8eb270e296023e9d92081fdf967ddd7878724424'.toLowerCase(), // aPOLGHST not supported
- '0x38d693ce1df5aadf7bc62595a37d667ad57922e5'.toLowerCase(), // aPolEURS not supported
- '0xea1132120ddcdda2f119e99fa7a27a0d036f7ac9'.toLowerCase(), // aPolSTMATIC not supported
- '0x6533afac2e7bccb20dca161449a13a32d391fb00'.toLowerCase(), // aPolJEUR not supported
- '0x513c7e3a9c69ca3e22550ef58ac1c0088e918fff'.toLowerCase(), // aPolCRV not supported
- '0xebe517846d0f36eced99c735cbf6131e1feb775d'.toLowerCase(), // aPolMIMATIC not supported
- '0xc45a479877e1e9dfe9fcd4056c699575a1045daa'.toLowerCase(), // aPolSUSHI not supported
- '0x8437d7c167dfb82ed4cb79cd44b7a32a1dd95c77'.toLowerCase(), // aPolAGEUR not supported
- '0x724dc807b04555b71ed48a6896b6f41593b8c637'.toLowerCase(), // aPolDPI not supported
- '0x8ffdf2de812095b1d19cb146e4c004587c0a0692'.toLowerCase(), // aPolBAL not supported
- ],
- [SupportedChainId.AVALANCHE]: [
- '0x8eb270e296023e9d92081fdf967ddd7878724424'.toLowerCase(), // AVaMAI not supported
- '0x078f358208685046a11c85e8ad32895ded33a249'.toLowerCase(), // aVaWBTC not supported
- '0xc45a479877e1e9dfe9fcd4056c699575a1045daa'.toLowerCase(), // aVaFRAX not supported
- ],
- [SupportedChainId.GNOSIS_CHAIN]: [
- '0xedbc7449a9b594ca4e053d9737ec5dc4cbccbfb2'.toLowerCase(), // EURe USD Price not supported
- ],
- [SupportedChainId.ARBITRUM_ONE]: [
- '0x62fC96b27a510cF4977B59FF952Dc32378Cc221d'.toLowerCase(), // atBTC does not have good solver liquidity
- ],
- [SupportedChainId.BASE]: [
- '0x90072A4aA69B5Eb74984Ab823EFC5f91e90b3a72'.toLowerCase(), // alBTC does not have good solver liquidity
- ],
- [SupportedChainId.MAINNET]: [
- '0x00907f9921424583e7ffBfEdf84F92B7B2Be4977'.toLowerCase(), // aGHO not supported
- '0x18eFE565A5373f430e2F809b97De30335B3ad96A'.toLowerCase(), // aGHO not supported
- ],
- [SupportedChainId.SEPOLIA]: [
- '0xd190eF37dB51Bb955A680fF1A85763CC72d083D4'.toLowerCase(), // aGHO not supported
- ],
- },
-};
diff --git a/src/components/transactions/Switch/cowprotocol/cowprotocol.helpers.ts b/src/components/transactions/Switch/cowprotocol/cowprotocol.helpers.ts
deleted file mode 100644
index 16041603d3..0000000000
--- a/src/components/transactions/Switch/cowprotocol/cowprotocol.helpers.ts
+++ /dev/null
@@ -1,376 +0,0 @@
-import { API_ETH_MOCK_ADDRESS } from '@aave/contract-helpers';
-import { MetadataApi } from '@cowprotocol/app-data';
-import {
- BuyTokenDestination,
- MAX_VALID_TO_EPOCH,
- OrderBookApi,
- OrderClass,
- OrderKind,
- OrderParameters,
- OrderStatus,
- QuoteAndPost,
- SellTokenSource,
- SigningScheme,
- SupportedChainId,
- TradingSdk,
- UnsignedOrder,
- WRAPPED_NATIVE_CURRENCIES,
-} from '@cowprotocol/cow-sdk';
-import { JsonRpcProvider } from '@ethersproject/providers';
-import { BigNumber, ethers, PopulatedTransaction } from 'ethers';
-import { isSmartContractWallet } from 'src/helpers/provider';
-
-import { getAssetGroup } from '../assetCorrelation.helpers';
-import { isChainIdSupportedByCoWProtocol } from '../switch.constants';
-
-export const COW_EVM_RECIPIENT = '0xC542C2F197c4939154017c802B0583C596438380';
-// export const COW_LENS_RECIPIENT = '0xce4eB8a1f6Bd0e0B9282102DC056B11E9D83b7CA';
-export const COW_PROTOCOL_ETH_FLOW_ADDRESS = '0xbA3cB449bD2B4ADddBc894D8697F5170800EAdeC';
-const COW_CREATE_ORDER_ABI =
- 'function createOrder((address,address,uint256,uint256,bytes32,uint256,uint32,bool,int64)) returns (bytes32)';
-
-export const HEADER_WIDGET_APP_CODE = 'aave-v3-interface-widget';
-export const ADAPTER_APP_CODE = 'aave-v3-interface-aps'; // Use this one for contract adapters so we have different dashboards
-export const COW_PARTNER_FEE = (tokenFromSymbol: string, tokenToSymbol: string) => ({
- volumeBps: getAssetGroup(tokenFromSymbol) == getAssetGroup(tokenToSymbol) ? 15 : 25,
- recipient: COW_EVM_RECIPIENT,
-});
-
-export const COW_APP_DATA = (
- tokenFromSymbol: string,
- tokenToSymbol: string,
- slippageBips: number,
- smartSlippage: boolean,
- orderClass: OrderClass,
- appCode?: string
-) => ({
- appCode: appCode || HEADER_WIDGET_APP_CODE, // todo: use ADAPTER_APP_CODE for contract adapters
- version: '1.4.0',
- metadata: {
- orderClass: { orderClass: orderClass }, // for CoW Swap UI & Analytics
- quote: {
- slippageBips,
- smartSlippage,
- },
- partnerFee: COW_PARTNER_FEE(tokenFromSymbol, tokenToSymbol),
- },
-});
-
-export type CowProtocolActionParams = {
- quote: OrderParameters;
- provider: JsonRpcProvider;
- chainId: number;
- user: string;
- amount: string;
- tokenDest: string;
- tokenSrc: string;
- tokenSrcDecimals: number;
- tokenDestDecimals: number;
- inputSymbol: string;
- outputSymbol: string;
- afterNetworkCostsBuyAmount: string;
- slippageBps: number;
- smartSlippage: boolean;
- appCode?: string;
- orderBookQuote: QuoteAndPost;
-};
-
-export const getPreSignTransaction = async ({
- provider,
- chainId,
- user,
- slippageBps,
- smartSlippage,
- inputSymbol,
- outputSymbol,
- appCode,
- orderBookQuote,
-}: CowProtocolActionParams) => {
- if (!isChainIdSupportedByCoWProtocol(chainId)) {
- throw new Error('Chain not supported.');
- }
-
- const signer = provider?.getSigner();
- if (!signer) {
- throw new Error('No signer found in provider');
- }
-
- const tradingSdk = new TradingSdk({
- chainId,
- signer,
- appCode: appCode || HEADER_WIDGET_APP_CODE,
- });
-
- const isSmartContract = await isSmartContractWallet(user, provider);
- if (!isSmartContract) {
- throw new Error('Only smart contract wallets should use presign.');
- }
-
- const orderResult = await orderBookQuote.postSwapOrderFromQuote({
- additionalParams: {
- signingScheme: SigningScheme.PRESIGN,
- },
- appData: COW_APP_DATA(
- inputSymbol,
- outputSymbol,
- slippageBps,
- smartSlippage,
- OrderClass.MARKET,
- appCode
- ),
- });
-
- const preSignTransaction = await tradingSdk.getPreSignTransaction({
- orderId: orderResult.orderId,
- account: user as `0x${string}`,
- });
-
- return {
- ...preSignTransaction,
- orderId: orderResult.orderId,
- };
-};
-
-// Only for EOA wallets
-export const sendOrder = async ({
- provider,
- chainId,
- user,
- slippageBps,
- inputSymbol,
- outputSymbol,
- smartSlippage,
- appCode,
- orderBookQuote,
-}: CowProtocolActionParams) => {
- const signer = provider?.getSigner();
-
- if (!isChainIdSupportedByCoWProtocol(chainId)) {
- throw new Error('Chain not supported.');
- }
-
- if (!signer) {
- throw new Error('No signer found in provider');
- }
-
- const isSmartContract = await isSmartContractWallet(user, provider);
- if (isSmartContract) {
- throw new Error('Smart contract wallets should use presign.');
- }
-
- return orderBookQuote
- .postSwapOrderFromQuote({
- appData: COW_APP_DATA(
- inputSymbol,
- outputSymbol,
- slippageBps,
- smartSlippage,
- OrderClass.MARKET,
- appCode
- ),
- })
- .then((orderResult) => orderResult.orderId);
-};
-
-export const getOrderStatus = async (orderId: string, chainId: number) => {
- const orderBookApi = new OrderBookApi({ chainId: chainId });
- const status = await orderBookApi.getOrderCompetitionStatus(orderId, {
- chainId,
- });
- return status.type;
-};
-
-export const getOrder = async (orderId: string, chainId: number) => {
- const orderBookApi = new OrderBookApi({ chainId });
- const order = await orderBookApi.getOrder(orderId, {
- chainId,
- });
- return order;
-};
-
-export const getOrders = async (chainId: number, account: string) => {
- const orderBookApi = new OrderBookApi({ chainId });
- const orders = await orderBookApi.getOrders({
- owner: account,
- });
-
- return orders;
-};
-
-export const isOrderLoading = (status: OrderStatus) => {
- return status === OrderStatus.OPEN || status === OrderStatus.PRESIGNATURE_PENDING;
-};
-
-export const isOrderFilled = (status: OrderStatus) => {
- return status === OrderStatus.FULFILLED;
-};
-
-export const isOrderExpired = (status: OrderStatus) => {
- return status === OrderStatus.EXPIRED;
-};
-
-export const isOrderCancelled = (status: OrderStatus) => {
- return status === OrderStatus.CANCELLED;
-};
-
-export const isNativeToken = (token?: string) => {
- return token?.toLowerCase() === API_ETH_MOCK_ADDRESS.toLowerCase();
-};
-
-export const getUnsignerOrder = async (
- sellAmount: string,
- buyAmount: string,
- dstToken: string,
- user: string,
- chainId: number,
- tokenFromSymbol: string,
- tokenToSymbol: string,
- slippageBps: number,
- smartSlippage: boolean,
- appCode?: string
-): Promise => {
- const metadataApi = new MetadataApi();
- const { appDataHex } = await metadataApi.getAppDataInfo(
- COW_APP_DATA(
- tokenFromSymbol,
- tokenToSymbol,
- slippageBps,
- smartSlippage,
- OrderClass.MARKET,
- appCode
- )
- );
-
- return {
- buyToken: dstToken,
- receiver: user,
- sellAmount,
- buyAmount,
- appData: appDataHex,
- feeAmount: '0',
- validTo: MAX_VALID_TO_EPOCH,
- partiallyFillable: false,
- kind: OrderKind.SELL,
- sellToken: WRAPPED_NATIVE_CURRENCIES[chainId as SupportedChainId].address.toLowerCase(),
- buyTokenBalance: BuyTokenDestination.ERC20,
- sellTokenBalance: SellTokenSource.ERC20,
- };
-};
-
-export const populateEthFlowTx = async (
- sellAmount: string,
- buyAmount: string,
- dstToken: string,
- user: string,
- validTo: number,
- tokenFromSymbol: string,
- tokenToSymbol: string,
- slippageBps: number,
- smartSlippage: boolean,
- quoteId?: number,
- appCode?: string
-): Promise => {
- const metadataApi = new MetadataApi();
- const { appDataHex } = await metadataApi.getAppDataInfo(
- COW_APP_DATA(
- tokenFromSymbol,
- tokenToSymbol,
- slippageBps,
- smartSlippage,
- OrderClass.MARKET,
- appCode
- )
- );
-
- const orderData = {
- buyToken: dstToken,
- receiver: user,
- sellAmount,
- buyAmount,
- appData: appDataHex,
- feeAmount: '0',
- validTo,
- partiallyFillable: false,
- quoteId: quoteId || 0,
- };
-
- const value = BigNumber.from(sellAmount);
-
- // Create the contract interface
- const iface = new ethers.utils.Interface([COW_CREATE_ORDER_ABI]);
-
- // Encode the function call
- const data = iface.encodeFunctionData('createOrder', [
- [
- orderData.buyToken,
- orderData.receiver,
- orderData.sellAmount,
- orderData.buyAmount,
- orderData.appData,
- orderData.feeAmount,
- orderData.validTo,
- orderData.partiallyFillable,
- orderData.quoteId,
- ],
- ]);
-
- return {
- to: COW_PROTOCOL_ETH_FLOW_ADDRESS,
- value,
- data,
- };
-};
-
-export const getRecommendedSlippage = (srcUSD: string) => {
- try {
- if (Number(srcUSD) <= 0) {
- return Number(0.5);
- }
-
- if (Number(srcUSD) <= 1) {
- return Number(5.0);
- } else if (Number(srcUSD) <= 5) {
- return Number(2.5);
- } else if (Number(srcUSD) <= 10) {
- return Number(1.5);
- } else {
- return Number(0.5);
- }
- } catch (e) {
- return Number(0.5);
- }
-};
-
-export const uploadAppData = async (orderId: string, appDataHex: string, chainId: number) => {
- const orderBookApi = new OrderBookApi({ chainId });
-
- return orderBookApi.uploadAppData(orderId, appDataHex);
-};
-
-export const generateCoWExplorerLink = (chainId: SupportedChainId, orderId?: string) => {
- if (!orderId) {
- return undefined;
- }
-
- const base = 'https://explorer.cow.fi';
- switch (chainId) {
- case SupportedChainId.MAINNET:
- return `${base}/orders/${orderId}`;
- case SupportedChainId.GNOSIS_CHAIN:
- return `${base}/gc/orders/${orderId}`;
- case SupportedChainId.BASE:
- return `${base}/base/orders/${orderId}`;
- case SupportedChainId.ARBITRUM_ONE:
- return `${base}/arb1/orders/${orderId}`;
- case SupportedChainId.SEPOLIA:
- return `${base}/sepolia/orders/${orderId}`;
- case SupportedChainId.AVALANCHE:
- return `${base}/avax/orders/${orderId}`;
- case SupportedChainId.POLYGON:
- return `${base}/pol/orders/${orderId}`;
- case SupportedChainId.BNB:
- return `${base}/bnb/orders/${orderId}`;
- default:
- throw new Error('Define explorer link for chainId: ' + chainId);
- }
-};
diff --git a/src/components/transactions/Switch/slippage.helpers.ts b/src/components/transactions/Switch/slippage.helpers.ts
deleted file mode 100644
index 95cdcfb963..0000000000
--- a/src/components/transactions/Switch/slippage.helpers.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { getAssetGroup } from 'src/components/transactions/Switch/assetCorrelation.helpers';
-
-export const getParaswapSlippage = (inputSymbol: string, outputSymbol: string): string => {
- const inputGroup = getAssetGroup(inputSymbol);
- const outputGroup = getAssetGroup(outputSymbol);
-
- return inputGroup === outputGroup ? '0.10' : '0.20';
-};
diff --git a/src/components/transactions/Switch/switch.constants.ts b/src/components/transactions/Switch/switch.constants.ts
deleted file mode 100644
index 9eda11a8e4..0000000000
--- a/src/components/transactions/Switch/switch.constants.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { ChainId } from '@aave/contract-helpers';
-import { SupportedChainId } from '@cowprotocol/cow-sdk';
-
-// In the future, we may fetch these configs from our features flag service
-
-export const ParaswapSupportedNetworks = [
- ChainId.mainnet,
- ChainId.polygon,
- ChainId.avalanche,
- ChainId.sepolia,
- ChainId.base,
- ChainId.arbitrum_one,
- ChainId.optimism,
- ChainId.xdai,
- ChainId.bnb,
- ChainId.sonic,
-];
-
-SupportedChainId;
-
-export const CoWProtocolSupportedNetworks = [
- SupportedChainId.MAINNET,
- SupportedChainId.GNOSIS_CHAIN,
- SupportedChainId.ARBITRUM_ONE,
- SupportedChainId.BASE,
- SupportedChainId.SEPOLIA,
- SupportedChainId.AVALANCHE,
- SupportedChainId.POLYGON,
- SupportedChainId.BNB,
-] as const;
-
-export const isChainIdSupportedByCoWProtocol = (chainId: number): chainId is SupportedChainId => {
- return CoWProtocolSupportedNetworks.includes(chainId);
-};
-
-export enum SwitchType {
- CollateralSwap = 'collateralSwap',
- TokenSwap = 'tokenSwap',
-}
diff --git a/src/components/transactions/TxActionsWrapper.tsx b/src/components/transactions/TxActionsWrapper.tsx
index eabd4ba1b6..d3f92eb4d2 100644
--- a/src/components/transactions/TxActionsWrapper.tsx
+++ b/src/components/transactions/TxActionsWrapper.tsx
@@ -33,6 +33,7 @@ interface TxActionsWrapperProps extends BoxProps {
handleClick: () => Promise;
};
tryPermit?: boolean;
+ permitInUse?: boolean;
event?: TrackEventProps;
}
@@ -55,6 +56,7 @@ export const TxActionsWrapper = ({
fetchingData = false,
errorParams,
tryPermit,
+ permitInUse = false,
event,
...rest
}: TxActionsWrapperProps) => {
@@ -143,7 +145,11 @@ export const TxActionsWrapper = ({
{approvalParams && !readOnlyModeAddress && (
-
+
)}
diff --git a/src/components/transactions/Warnings/ChangeNetworkWarning.tsx b/src/components/transactions/Warnings/ChangeNetworkWarning.tsx
index de3651c1b1..7f71b1f56c 100644
--- a/src/components/transactions/Warnings/ChangeNetworkWarning.tsx
+++ b/src/components/transactions/Warnings/ChangeNetworkWarning.tsx
@@ -70,7 +70,11 @@ export const ChangeNetworkWarning = ({
switchNetwork(chainId);
};
return (
-
+
{isAutoSwitching ? (
diff --git a/src/components/transactions/Withdraw/WithdrawAndSwitchActions.tsx b/src/components/transactions/Withdraw/WithdrawAndSwitchActions.tsx
deleted file mode 100644
index 1f842fe6f7..0000000000
--- a/src/components/transactions/Withdraw/WithdrawAndSwitchActions.tsx
+++ /dev/null
@@ -1,288 +0,0 @@
-import { ERC20Service, gasLimitRecommendations, ProtocolAction } from '@aave/contract-helpers';
-import { valueToBigNumber } from '@aave/math-utils';
-import { SignatureLike } from '@ethersproject/bytes';
-import { Trans } from '@lingui/macro';
-import { BoxProps } from '@mui/material';
-import { useQueryClient } from '@tanstack/react-query';
-import { parseUnits } from 'ethers/lib/utils';
-import { useCallback, useEffect, useMemo, useState } from 'react';
-import { MOCK_SIGNED_HASH } from 'src/helpers/useTransactionHandler';
-import { ComputedReserveData } from 'src/hooks/app-data-provider/useAppDataProvider';
-import { calculateSignedAmount, SwapTransactionParams } from 'src/hooks/paraswap/common';
-import { useModalContext } from 'src/hooks/useModal';
-import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
-import { useRootStore } from 'src/store/root';
-import { ApprovalMethod } from 'src/store/walletSlice';
-import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
-import { queryKeysFactory } from 'src/ui-config/queries';
-import { GENERAL } from 'src/utils/events';
-import { useShallow } from 'zustand/shallow';
-
-import { TxActionsWrapper } from '../TxActionsWrapper';
-import { APPROVAL_GAS_LIMIT } from '../utils';
-
-interface WithdrawAndSwitchProps extends BoxProps {
- amountToSwap: string;
- amountToReceive: string;
- poolReserve: ComputedReserveData;
- targetReserve: ComputedReserveData;
- isWrongNetwork: boolean;
- blocked: boolean;
- isMaxSelected: boolean;
- loading?: boolean;
- buildTxFn: () => Promise;
-}
-
-export interface WithdrawAndSwitchActionProps
- extends Pick<
- WithdrawAndSwitchProps,
- 'amountToSwap' | 'amountToReceive' | 'poolReserve' | 'targetReserve' | 'isMaxSelected'
- > {
- augustus: string;
- signatureParams?: SignedParams;
- txCalldata: string;
-}
-
-interface SignedParams {
- signature: SignatureLike;
- deadline: string;
- amount: string;
-}
-
-export const WithdrawAndSwitchActions = ({
- amountToSwap,
- amountToReceive,
- isWrongNetwork,
- sx,
- poolReserve,
- targetReserve,
- isMaxSelected,
- loading,
- blocked,
- buildTxFn,
-}: WithdrawAndSwitchProps) => {
- const [
- withdrawAndSwitch,
- currentMarketData,
- jsonRpcProvider,
- account,
- generateApproval,
- estimateGasLimit,
- walletApprovalMethodPreference,
- generateSignatureRequest,
- addTransaction,
- trackEvent,
- ] = useRootStore(
- useShallow((state) => [
- state.withdrawAndSwitch,
- state.currentMarketData,
- state.jsonRpcProvider,
- state.account,
- state.generateApproval,
- state.estimateGasLimit,
- state.walletApprovalMethodPreference,
- state.generateSignatureRequest,
- state.addTransaction,
- state.trackEvent,
- ])
- );
- const {
- approvalTxState,
- mainTxState,
- loadingTxns,
- setMainTxState,
- setTxError,
- setGasLimit,
- setLoadingTxns,
- setApprovalTxState,
- } = useModalContext();
-
- const { sendTx, signTxData } = useWeb3Context();
- const queryClient = useQueryClient();
-
- const [approvedAmount, setApprovedAmount] = useState(undefined);
- const [signatureParams, setSignatureParams] = useState();
-
- const requiresApproval = useMemo(() => {
- if (
- approvedAmount === undefined ||
- approvedAmount === -1 ||
- amountToSwap === '0' ||
- isWrongNetwork
- )
- return false;
- else return approvedAmount <= Number(amountToSwap);
- }, [approvedAmount, amountToSwap, isWrongNetwork]);
-
- const useSignature = walletApprovalMethodPreference === ApprovalMethod.PERMIT;
-
- const action = async () => {
- try {
- setMainTxState({ ...mainTxState, loading: true });
- const route = await buildTxFn();
- const tx = withdrawAndSwitch({
- poolReserve,
- targetReserve,
- isMaxSelected,
- amountToSwap: parseUnits(amountToSwap, poolReserve.decimals).toString(),
- amountToReceive: parseUnits(amountToReceive, targetReserve.decimals).toString(),
- augustus: route.augustus,
- txCalldata: route.swapCallData,
- signatureParams,
- });
- const txDataWithGasEstimation = await estimateGasLimit(tx);
- const response = await sendTx(txDataWithGasEstimation);
- await response.wait(1);
- queryClient.invalidateQueries({ queryKey: queryKeysFactory.pool });
- queryClient.invalidateQueries({ queryKey: queryKeysFactory.gho });
- setMainTxState({
- txHash: response.hash,
- loading: false,
- success: true,
- });
- addTransaction(response.hash, {
- action: ProtocolAction.withdrawAndSwitch,
- txState: 'success',
- asset: poolReserve.underlyingAsset,
- amount: parseUnits(route.inputAmount, poolReserve.decimals).toString(),
- assetName: poolReserve.name,
- outAsset: targetReserve.underlyingAsset,
- outAssetName: targetReserve.name,
- outAmount: parseUnits(route.outputAmount, targetReserve.decimals).toString(),
- amountUsd: valueToBigNumber(parseUnits(route.inputAmount, poolReserve.decimals).toString())
- .multipliedBy(poolReserve.priceInUSD)
- .toString(),
- outAmountUsd: valueToBigNumber(
- parseUnits(route.outputAmount, targetReserve.decimals).toString()
- )
- .multipliedBy(targetReserve.priceInUSD)
- .toString(),
- });
- } catch (error) {
- const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
- setTxError(parsedError);
- setMainTxState({
- txHash: undefined,
- loading: false,
- });
- trackEvent(GENERAL.TRANSACTION_ERROR, {
- transactiontype: ProtocolAction.withdrawAndSwitch,
- asset: poolReserve.underlyingAsset,
- assetName: poolReserve.name,
- error,
- });
- }
- };
-
- const approval = async () => {
- const amountToApprove = calculateSignedAmount(amountToSwap, poolReserve.decimals);
- const approvalData = {
- user: account,
- token: poolReserve.aTokenAddress,
- spender: currentMarketData.addresses.WITHDRAW_SWITCH_ADAPTER || '',
- amount: amountToApprove,
- };
- try {
- if (useSignature) {
- const deadline = Math.floor(Date.now() / 1000 + 3600).toString();
- const signatureRequest = await generateSignatureRequest({
- ...approvalData,
- deadline,
- });
- setApprovalTxState({ ...approvalTxState, loading: true });
- const response = await signTxData(signatureRequest);
- setSignatureParams({ signature: response, deadline, amount: amountToApprove });
- setApprovalTxState({
- txHash: MOCK_SIGNED_HASH,
- loading: false,
- success: true,
- });
- } else {
- const tx = generateApproval(approvalData);
- const txWithGasEstimation = await estimateGasLimit(tx);
- setApprovalTxState({ ...approvalTxState, loading: true });
- const response = await sendTx(txWithGasEstimation);
- await response.wait(1);
- setApprovalTxState({
- txHash: response.hash,
- loading: false,
- success: true,
- });
- addTransaction(response.hash, {
- action: ProtocolAction.withdrawAndSwitch,
- txState: 'success',
- asset: poolReserve.aTokenAddress,
- amount: parseUnits(amountToApprove, poolReserve.decimals).toString(),
- assetName: `a${poolReserve.symbol}`,
- spender: currentMarketData.addresses.WITHDRAW_SWITCH_ADAPTER,
- });
- setTxError(undefined);
- fetchApprovedAmount(poolReserve.aTokenAddress);
- }
- } catch (error) {
- const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
- setTxError(parsedError);
- if (!approvalTxState.success) {
- setApprovalTxState({
- txHash: undefined,
- loading: false,
- });
- }
- }
- };
-
- const fetchApprovedAmount = useCallback(
- async (aTokenAddress: string) => {
- setLoadingTxns(true);
- const rpc = jsonRpcProvider();
- const erc20Service = new ERC20Service(rpc);
- const approvedTargetAmount = await erc20Service.approvedAmount({
- user: account,
- token: aTokenAddress,
- spender: currentMarketData.addresses.WITHDRAW_SWITCH_ADAPTER || '',
- });
- setApprovedAmount(approvedTargetAmount);
- setLoadingTxns(false);
- },
- [jsonRpcProvider, account, currentMarketData.addresses.WITHDRAW_SWITCH_ADAPTER, setLoadingTxns]
- );
-
- useEffect(() => {
- fetchApprovedAmount(poolReserve.aTokenAddress);
- }, [fetchApprovedAmount, poolReserve.aTokenAddress]);
-
- useEffect(() => {
- let switchGasLimit = 0;
- switchGasLimit = Number(gasLimitRecommendations[ProtocolAction.withdrawAndSwitch].recommended);
- if (requiresApproval && !approvalTxState.success) {
- switchGasLimit += Number(APPROVAL_GAS_LIMIT);
- }
- setGasLimit(switchGasLimit.toString());
- }, [requiresApproval, approvalTxState, setGasLimit]);
-
- return (
- approval()}
- requiresApproval={requiresApproval}
- actionText={Withdraw and Switch}
- actionInProgressText={Withdrawing and Switching}
- sx={sx}
- errorParams={{
- loading: false,
- disabled: blocked || !approvalTxState?.success,
- content: Withdraw and Switch,
- handleClick: action,
- }}
- fetchingData={loading}
- blocked={blocked}
- tryPermit={true}
- />
- );
-};
diff --git a/src/components/transactions/Withdraw/WithdrawAndSwitchModalContent.tsx b/src/components/transactions/Withdraw/WithdrawAndSwitchModalContent.tsx
deleted file mode 100644
index cb2e2f6240..0000000000
--- a/src/components/transactions/Withdraw/WithdrawAndSwitchModalContent.tsx
+++ /dev/null
@@ -1,368 +0,0 @@
-import { valueToBigNumber } from '@aave/math-utils';
-import { ArrowDownIcon } from '@heroicons/react/solid';
-import { Trans } from '@lingui/macro';
-import { Box, Checkbox, Stack, SvgIcon, Typography } from '@mui/material';
-import { useRef, useState } from 'react';
-import { PriceImpactTooltip } from 'src/components/infoTooltips/PriceImpactTooltip';
-import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
-import { TokenIcon } from 'src/components/primitives/TokenIcon';
-import { Warning } from 'src/components/primitives/Warning';
-import {
- ComputedUserReserveData,
- ExtendedFormattedUser,
- useAppDataContext,
-} from 'src/hooks/app-data-provider/useAppDataProvider';
-import { minimumReceivedAfterSlippage } from 'src/hooks/paraswap/common';
-import { useCollateralSwap } from 'src/hooks/paraswap/useCollateralSwap';
-import { useTokenInForTokenOut } from 'src/hooks/token-wrapper/useTokenWrapper';
-import { useModalContext } from 'src/hooks/useModal';
-import { useWrappedTokens } from 'src/hooks/useWrappedTokens';
-import { useZeroLTVBlockingWithdraw } from 'src/hooks/useZeroLTVBlockingWithdraw';
-import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
-import { ListSlippageButton } from 'src/modules/dashboard/lists/SlippageList';
-import { useRootStore } from 'src/store/root';
-import { GENERAL } from 'src/utils/events';
-import { calculateHFAfterWithdraw } from 'src/utils/hfUtils';
-import { roundToTokenDecimals } from 'src/utils/utils';
-import { useShallow } from 'zustand/shallow';
-
-import { Asset, AssetInput } from '../AssetInput';
-import { GasEstimationError } from '../FlowCommons/GasEstimationError';
-import { ModalWrapperProps } from '../FlowCommons/ModalWrapper';
-import { DetailsHFLine, DetailsNumberLine, TxModalDetails } from '../FlowCommons/TxModalDetails';
-import { calculateMaxWithdrawAmount } from './utils';
-import { WithdrawAndSwitchActions } from './WithdrawAndSwitchActions';
-import { WithdrawAndSwitchTxSuccessView } from './WithdrawAndSwitchSuccess';
-import { WithdrawAndUnwrapAction } from './WithdrawAndUnwrapActions';
-import { useWithdrawError } from './WithdrawError';
-
-export enum ErrorType {
- CAN_NOT_WITHDRAW_THIS_AMOUNT,
- POOL_DOES_NOT_HAVE_ENOUGH_LIQUIDITY,
- ZERO_LTV_WITHDRAW_BLOCKED,
-}
-
-export const WithdrawAndSwitchModalContent = ({
- poolReserve,
- userReserve,
- symbol,
- isWrongNetwork,
- user,
-}: ModalWrapperProps & { user: ExtendedFormattedUser }) => {
- const { gasLimit, mainTxState: withdrawTxState, txError } = useModalContext();
- const { currentAccount } = useWeb3Context();
- const { reserves } = useAppDataContext();
- const wrappedTokenReserves = useWrappedTokens();
-
- const [_amount, setAmount] = useState('');
- const [riskCheckboxAccepted, setRiskCheckboxAccepted] = useState(false);
- const amountRef = useRef('');
- const [trackEvent, currentNetworkConfig, currentChainId] = useRootStore(
- useShallow((store) => [store.trackEvent, store.currentNetworkConfig, store.currentChainId])
- );
- const [maxSlippage, setMaxSlippage] = useState('0.1');
-
- let swapTargets = reserves
- .filter((r) => r.underlyingAsset !== poolReserve.underlyingAsset)
- .map((reserve) => ({
- address: reserve.underlyingAsset,
- symbol: reserve.symbol,
- iconSymbol: reserve.iconSymbol,
- }));
-
- // TODO: if withdrawing and unwrapping, should we show that asset at the top of the list?
- swapTargets = [
- ...swapTargets.filter((r) => r.symbol === 'GHO'),
- ...swapTargets.filter((r) => r.symbol !== 'GHO'),
- ];
-
- const [targetReserve, setTargetReserve] = useState(swapTargets[0]);
-
- const isMaxSelected = _amount === '-1';
-
- const swapTarget = user.userReservesData.find(
- (r) => r.underlyingAsset === targetReserve.address
- ) as ComputedUserReserveData;
-
- const maxAmountToWithdraw = calculateMaxWithdrawAmount(user, userReserve, poolReserve);
- const underlyingBalance = valueToBigNumber(userReserve?.underlyingBalance || '0');
-
- let withdrawAndUnwrap = false;
- const wrappedTokenConfig = wrappedTokenReserves.find(
- (config) => config.tokenOut.underlyingAsset === poolReserve.underlyingAsset
- );
- if (wrappedTokenConfig) {
- withdrawAndUnwrap = targetReserve.address === wrappedTokenConfig.tokenIn.underlyingAsset;
- }
-
- const { data: unwrappedAmount, isFetching: loadingTokenInForTokenOut } = useTokenInForTokenOut(
- amountRef.current,
- poolReserve.decimals,
- wrappedTokenConfig?.tokenWrapperAddress || ''
- );
-
- const {
- inputAmountUSD,
- inputAmount,
- outputAmount,
- outputAmountUSD,
- error,
- loading: routeLoading,
- buildTxFn,
- } = useCollateralSwap({
- chainId: currentNetworkConfig.underlyingChainId || currentChainId,
- userAddress: currentAccount,
- swapIn: { ...poolReserve, amount: amountRef.current },
- swapOut: { ...swapTarget.reserve, amount: '0' },
- max: isMaxSelected && maxAmountToWithdraw.eq(underlyingBalance),
- skip: withdrawAndUnwrap || withdrawTxState.loading || false,
- maxSlippage: Number(maxSlippage),
- });
-
- let outputUSD = outputAmountUSD;
- if (withdrawAndUnwrap) {
- outputUSD = valueToBigNumber(unwrappedAmount || '0')
- .multipliedBy(wrappedTokenConfig?.tokenIn.priceInUSD || 0)
- .toString();
- }
-
- const loadingSkeleton =
- (routeLoading && outputAmountUSD === '0') || (withdrawAndUnwrap && loadingTokenInForTokenOut);
-
- const unborrowedLiquidity = valueToBigNumber(poolReserve.unborrowedLiquidity);
-
- const assetsBlockingWithdraw = useZeroLTVBlockingWithdraw();
-
- const withdrawAmount = isMaxSelected ? maxAmountToWithdraw.toString(10) : _amount;
-
- const healthFactorAfterWithdraw = calculateHFAfterWithdraw({
- user,
- userReserve,
- poolReserve,
- withdrawAmount,
- });
-
- const { blockingError, errorComponent } = useWithdrawError({
- assetsBlockingWithdraw,
- poolReserve,
- healthFactorAfterWithdraw,
- withdrawAmount,
- user,
- });
-
- const handleChange = (value: string) => {
- const maxSelected = value === '-1';
- const truncatedValue = roundToTokenDecimals(value, poolReserve.decimals);
- amountRef.current = maxSelected ? maxAmountToWithdraw.toString(10) : truncatedValue;
- setAmount(truncatedValue);
- if (maxSelected && maxAmountToWithdraw.eq(underlyingBalance)) {
- trackEvent(GENERAL.MAX_INPUT_SELECTION, { type: 'withdraw' });
- }
- };
-
- const displayRiskCheckbox =
- healthFactorAfterWithdraw.toNumber() >= 1 &&
- healthFactorAfterWithdraw.toNumber() < 1.5 &&
- userReserve.usageAsCollateralEnabledOnUser;
-
- // calculating input usd value
- const usdValue = valueToBigNumber(withdrawAmount).multipliedBy(
- userReserve?.reserve.priceInUSD || 0
- );
-
- const minimumAmountReceived = minimumReceivedAfterSlippage(
- outputAmount,
- maxSlippage,
- swapTarget.reserve.decimals
- );
-
- if (withdrawTxState.success) {
- let amount = inputAmount;
- let outAmount = minimumAmountReceived;
- if (withdrawAndUnwrap) {
- amount = amountRef.current;
- outAmount = unwrappedAmount || '0';
- }
- return (
-
- );
- }
-
- return (
- <>
- Withdraw}
- value={withdrawAmount}
- onChange={handleChange}
- symbol={symbol}
- assets={[
- {
- balance: maxAmountToWithdraw.toString(10),
- symbol: symbol,
- iconSymbol: poolReserve.isWrappedBaseAsset
- ? currentNetworkConfig.baseAssetSymbol
- : poolReserve.iconSymbol,
- },
- ]}
- usdValue={usdValue.toString(10)}
- isMaxSelected={isMaxSelected}
- disabled={withdrawTxState.loading}
- maxValue={maxAmountToWithdraw.toString(10)}
- balanceText={
- unborrowedLiquidity.lt(underlyingBalance) ? (
- Available
- ) : (
- Supply balance
- )
- }
- />
-
-
-
-
-
-
-
-
-
- Receive (est.)}
- balanceText={Supply balance}
- disableInput
- loading={loadingSkeleton}
- />
-
- {error && !loadingSkeleton && !withdrawAndUnwrap && (
-
- {error}
-
- )}
-
- {blockingError !== undefined && (
-
- {errorComponent}
-
- )}
-
-
- Minimum amount received
-
-
-
-
-
-
-
- }
- />
- )
- }
- >
- Remaining supply}
- value={underlyingBalance.minus(withdrawAmount || '0').toString(10)}
- symbol={
- poolReserve.isWrappedBaseAsset
- ? currentNetworkConfig.baseAssetSymbol
- : poolReserve.symbol
- }
- />
-
-
-
- {txError && }
-
- {displayRiskCheckbox && (
- <>
-
-
- Withdrawing this amount will reduce your health factor and increase risk of
- liquidation.
-
-
-
- {
- setRiskCheckboxAccepted(!riskCheckboxAccepted),
- trackEvent(GENERAL.ACCEPT_RISK, {
- modal: 'Withdraw',
- riskCheckboxAccepted: riskCheckboxAccepted,
- });
- }}
- size="small"
- data-cy={`risk-checkbox`}
- />
-
- I acknowledge the risks involved.
-
-
- >
- )}
-
- {withdrawAndUnwrap ? (
-
- ) : (
-
- )}
- >
- );
-};
diff --git a/src/components/transactions/Withdraw/WithdrawAndSwitchSuccess.tsx b/src/components/transactions/Withdraw/WithdrawAndSwitchSuccess.tsx
deleted file mode 100644
index 1754ff79f7..0000000000
--- a/src/components/transactions/Withdraw/WithdrawAndSwitchSuccess.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { ArrowRightIcon } from '@heroicons/react/outline';
-import { Trans } from '@lingui/macro';
-import { Box, SvgIcon, Typography } from '@mui/material';
-import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
-import { TokenIcon } from 'src/components/primitives/TokenIcon';
-
-import { BaseSuccessView } from '../FlowCommons/BaseSuccess';
-
-export type WithdrawAndSwitchTxSuccessViewProps = {
- txHash?: string;
- amount?: string;
- symbol: string;
- outAmount?: string;
- outSymbol: string;
-};
-
-export const WithdrawAndSwitchTxSuccessView = ({
- txHash,
- amount,
- symbol,
- outAmount,
- outSymbol,
-}: WithdrawAndSwitchTxSuccessViewProps) => {
- return (
-
-
-
- You've successfully withdrew & swapped tokens.
-
-
-
-
- {symbol}
-
-
-
-
-
- {outSymbol}
-
-
-
- );
-};
diff --git a/src/components/transactions/Withdraw/WithdrawModal.tsx b/src/components/transactions/Withdraw/WithdrawModal.tsx
index 89a782168e..490fe3aedd 100644
--- a/src/components/transactions/Withdraw/WithdrawModal.tsx
+++ b/src/components/transactions/Withdraw/WithdrawModal.tsx
@@ -7,7 +7,7 @@ import { isFeatureEnabled } from 'src/utils/marketsAndNetworksConfig';
import { BasicModal } from '../../primitives/BasicModal';
import { ModalWrapper } from '../FlowCommons/ModalWrapper';
-import { WithdrawAndSwitchModalContent } from './WithdrawAndSwitchModalContent';
+import { WithdrawAndSwapModalContent } from '../Swap/modals/request/WithdrawAndSwapModalContent';
import { WithdrawModalContent } from './WithdrawModalContent';
import { WithdrawType, WithdrawTypeSelector } from './WithdrawTypeSelector';
@@ -51,9 +51,9 @@ export const WithdrawModal = () => {
user={user}
/>
)}
- {withdrawType === WithdrawType.WITHDRAWSWITCH && (
+ {withdrawType === WithdrawType.WITHDRAW_AND_SWAP && (
<>
-
+
>
)}
>
diff --git a/src/components/transactions/Withdraw/WithdrawTypeSelector.tsx b/src/components/transactions/Withdraw/WithdrawTypeSelector.tsx
index 1f01ab7372..68705b6b30 100644
--- a/src/components/transactions/Withdraw/WithdrawTypeSelector.tsx
+++ b/src/components/transactions/Withdraw/WithdrawTypeSelector.tsx
@@ -8,7 +8,7 @@ import { useShallow } from 'zustand/shallow';
export enum WithdrawType {
WITHDRAW,
- WITHDRAWSWITCH,
+ WITHDRAW_AND_SWAP,
}
export function WithdrawTypeSelector({
withdrawType,
@@ -43,14 +43,14 @@ export function WithdrawTypeSelector({
- trackEvent(WITHDRAW_MODAL.SWITCH_WITHDRAW_TYPE, { withdrawType: 'Withdraw and Switch' })
+ trackEvent(WITHDRAW_MODAL.SWITCH_WITHDRAW_TYPE, { withdrawType: 'Withdraw and Swap' })
}
>
- Withdraw & Switch
+ Withdraw & Swap
diff --git a/src/helpers/useTransactionHandler.tsx b/src/helpers/useTransactionHandler.tsx
index a8d20ffd06..01feb835c9 100644
--- a/src/helpers/useTransactionHandler.tsx
+++ b/src/helpers/useTransactionHandler.tsx
@@ -302,7 +302,7 @@ export const useTransactionHandler = ({
},
});
} catch (error) {
- console.log(error, 'error');
+ console.error(error, 'error');
const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
setTxError(parsedError);
setMainTxState({
@@ -337,7 +337,7 @@ export const useTransactionHandler = ({
});
} catch (error) {
const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
- console.log(error, parsedError);
+ console.error(error, parsedError);
setTxError(parsedError);
setMainTxState({
txHash: undefined,
diff --git a/src/hooks/paraswap/common.ts b/src/hooks/paraswap/common.ts
index 8a1a5ec856..6cb880dfc9 100644
--- a/src/hooks/paraswap/common.ts
+++ b/src/hooks/paraswap/common.ts
@@ -366,7 +366,15 @@ export const ExactInSwapper = (chainId: ChainId) => {
augustus: (params as TransactionParams).to,
};
} catch (e) {
- console.error(e);
+ console.error(e, {
+ srcToken,
+ srcDecimals,
+ destToken,
+ destDecimals,
+ user,
+ route,
+ maxSlippage,
+ });
throw new Error('Error building transaction parameters');
}
};
@@ -377,7 +385,7 @@ export const ExactInSwapper = (chainId: ChainId) => {
};
};
-const ExactOutSwapper = (chainId: ChainId) => {
+export const ExactOutSwapper = (chainId: ChainId) => {
const { paraswap, feeTarget } = getParaswap(chainId);
const getRate = async (
@@ -435,7 +443,15 @@ const ExactOutSwapper = (chainId: ChainId) => {
augustus: (params as TransactionParams).to,
};
} catch (e) {
- console.log(e);
+ console.error(e, {
+ srcToken,
+ srcDecimals,
+ destToken,
+ destDecimals,
+ user,
+ route,
+ maxSlippage,
+ });
throw new Error('Error building transaction parameters');
}
};
diff --git a/src/hooks/paraswap/useParaswapRates.ts b/src/hooks/paraswap/useParaswapRates.ts
index 850bfb7b52..ca6b52ad00 100644
--- a/src/hooks/paraswap/useParaswapRates.ts
+++ b/src/hooks/paraswap/useParaswapRates.ts
@@ -2,6 +2,7 @@ import { OptimalRate, SwapSide } from '@paraswap/sdk';
import { RateOptions } from '@paraswap/sdk/dist/methods/swap/rates';
import { useMutation, useQuery } from '@tanstack/react-query';
import { BigNumber, constants, PopulatedTransaction } from 'ethers';
+import { SwapProvider } from 'src/components/transactions/Swap/types';
import { queryKeysFactory } from 'src/ui-config/queries';
import { getParaswap } from './common';
@@ -51,7 +52,15 @@ export const useParaswapSellRates = ({
},
});
},
- queryKey: queryKeysFactory.paraswapRates(chainId, amount, srcToken, destToken, user),
+ queryKey: queryKeysFactory.swapQuote(
+ chainId,
+ SwapProvider.PARASWAP,
+ amount,
+ false,
+ srcToken,
+ destToken,
+ user
+ ),
enabled: amount !== '0',
retry: 0,
refetchOnWindowFocus: (query) => (query.state.error ? false : true),
diff --git a/src/hooks/switch/cowprotocol.rates.ts b/src/hooks/switch/cowprotocol.rates.ts
deleted file mode 100644
index d2f94648c5..0000000000
--- a/src/hooks/switch/cowprotocol.rates.ts
+++ /dev/null
@@ -1,214 +0,0 @@
-import { ChainId } from '@aave/contract-helpers';
-import {
- OrderKind,
- QuoteAndPost,
- TradingSdk,
- WRAPPED_NATIVE_CURRENCIES,
-} from '@cowprotocol/cow-sdk';
-import { BigNumber } from 'bignumber.js';
-import {
- COW_PARTNER_FEE,
- isNativeToken,
-} from 'src/components/transactions/Switch/cowprotocol/cowprotocol.helpers';
-import { isChainIdSupportedByCoWProtocol } from 'src/components/transactions/Switch/switch.constants';
-import {
- CowProtocolRatesType,
- ProviderRatesParams,
-} from 'src/components/transactions/Switch/switch.types';
-import { getEthersProvider } from 'src/libs/web3-data-provider/adapters/EthersAdapter';
-import { CoWProtocolPricesService } from 'src/services/CoWProtocolPricesService';
-import { FamilyPricesService } from 'src/services/FamilyPricesService';
-import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
-import { wagmiConfig } from 'src/ui-config/wagmiConfig';
-import { getNetworkConfig } from 'src/utils/marketsAndNetworksConfig';
-
-export const getTokenUsdPrice = async (
- chainId: number,
- tokenAddress: string,
- isTokenCustom: boolean,
- isMainnet: boolean
-) => {
- const cowProtocolPricesService = new CoWProtocolPricesService();
- const familyPricesService = new FamilyPricesService();
-
- try {
- let price;
-
- if (!isTokenCustom && isMainnet) {
- price = await familyPricesService.getTokenUsdPrice(chainId, tokenAddress);
- }
-
- if (price) {
- return price;
- }
-
- return await cowProtocolPricesService.getTokenUsdPrice(chainId, tokenAddress);
- } catch (familyError) {
- console.error(familyError);
- return undefined;
- }
-};
-
-export async function getCowProtocolSellRates({
- chainId,
- amount,
- srcToken,
- srcDecimals,
- destToken,
- destDecimals,
- user,
- inputSymbol,
- outputSymbol,
- setError,
- isInputTokenCustom,
- isOutputTokenCustom,
- appCode,
-}: ProviderRatesParams): Promise {
- const tradingSdk = new TradingSdk({ chainId });
-
- let orderBookQuote: QuoteAndPost | undefined;
- let srcTokenPriceUsd: string | undefined;
- let destTokenPriceUsd: string | undefined;
- try {
- if (!isChainIdSupportedByCoWProtocol(chainId)) {
- throw new Error('Chain not supported by CowProtocol');
- }
-
- // If srcToken is native, we need to use the wrapped token for the quote
- let srcTokenWrapped = srcToken;
- if (isNativeToken(srcToken)) {
- srcTokenWrapped = WRAPPED_NATIVE_CURRENCIES[chainId].address;
- }
-
- let destTokenWrapped = destToken;
- if (isNativeToken(destToken)) {
- destTokenWrapped = WRAPPED_NATIVE_CURRENCIES[chainId].address;
- }
-
- const provider = await getEthersProvider(wagmiConfig, { chainId });
- const signer = provider?.getSigner();
- const isMainnet =
- !getNetworkConfig(chainId as unknown as ChainId).isTestnet &&
- !getNetworkConfig(chainId as unknown as ChainId).isFork;
-
- if (!inputSymbol || !outputSymbol) {
- throw new Error('No input or output symbol provided');
- }
-
- [orderBookQuote, srcTokenPriceUsd, destTokenPriceUsd] = await Promise.all([
- tradingSdk
- .getQuote({
- owner: user as `0x${string}`,
- kind: OrderKind.SELL,
- amount,
- sellToken: srcTokenWrapped,
- sellTokenDecimals: srcDecimals,
- buyToken: destTokenWrapped,
- buyTokenDecimals: destDecimals,
- signer,
- appCode: appCode,
- partnerFee: COW_PARTNER_FEE(inputSymbol, outputSymbol),
- })
- .catch((cowError) => {
- console.error(cowError);
- throw new Error(cowError?.body?.errorType);
- }),
- getTokenUsdPrice(chainId, srcTokenWrapped, isInputTokenCustom ?? false, isMainnet),
- getTokenUsdPrice(chainId, destTokenWrapped, isOutputTokenCustom ?? false, isMainnet),
- ]);
-
- if (!srcTokenPriceUsd || !destTokenPriceUsd) {
- console.error('No price found for token');
- const error = getErrorTextFromError(
- new Error('No price found for token, please try another token'),
- TxAction.MAIN_ACTION,
- true
- );
- setError?.(error);
- console.error(error);
- throw new Error('No price found for token, please try another token');
- }
- } catch (error) {
- console.error('generate error', error);
- setError?.({
- error: error,
- blocking: true,
- actionBlocked: true,
- rawError: error,
- txAction: TxAction.MAIN_ACTION,
- });
-
- throw error;
- }
-
- const srcAmountInUsd = BigNumber(srcTokenPriceUsd).multipliedBy(
- BigNumber(amount).dividedBy(10 ** srcDecimals)
- );
- const destAmountInUsd = BigNumber(destTokenPriceUsd).multipliedBy(
- BigNumber(
- orderBookQuote.quoteResults.amountsAndCosts.afterPartnerFees.buyAmount.toString()
- ).dividedBy(10 ** destDecimals)
- );
-
- const destSpotInUsd = BigNumber(destTokenPriceUsd)
- .multipliedBy(
- BigNumber(orderBookQuote.quoteResults.amountsAndCosts.beforeNetworkCosts.buyAmount.toString())
- )
- .dividedBy(10 ** destDecimals);
-
- if (!orderBookQuote.quoteResults.suggestedSlippageBps) {
- console.error('No suggested slippage found');
- const error = getErrorTextFromError(
- new Error('No suggested slippage found'),
- TxAction.MAIN_ACTION,
- true
- );
- setError?.(error);
- console.error(error);
- throw new Error('No suggested slippage found');
- }
-
- if (!orderBookQuote.quoteResults.amountsAndCosts.afterPartnerFees.buyAmount) {
- console.error('No buy amount found');
- const error = getErrorTextFromError(
- new Error('No buy amount found'),
- TxAction.MAIN_ACTION,
- true
- );
- setError?.(error);
- console.error(error);
- throw new Error('No buy amount found');
- }
-
- let suggestedSlippage = (orderBookQuote.quoteResults.suggestedSlippageBps ?? 100) / 100; // E.g. 100 bps -> 1% 100 / 100 = 1
-
- if (isNativeToken(srcToken)) {
- // Recommended by CoW for potential reimbursments
- if (chainId == 1 && suggestedSlippage < 2) {
- suggestedSlippage = 2;
- } else if (chainId != 1 && suggestedSlippage < 0.5) {
- suggestedSlippage = 0.5;
- }
- }
-
- return {
- srcToken,
- srcUSD: srcAmountInUsd.toString(),
- srcAmount: amount,
- srcDecimals,
- destToken,
- destSpot: orderBookQuote.quoteResults.amountsAndCosts.beforeNetworkCosts.buyAmount.toString(),
- destSpotInUsd: destSpotInUsd.toString(),
- destUSD: destAmountInUsd.toString(),
- destAmount: orderBookQuote.quoteResults.amountsAndCosts.afterPartnerFees.buyAmount.toString(),
- destDecimals,
- orderBookQuote,
- provider: 'cowprotocol',
- order: orderBookQuote.quoteResults.orderToSign,
- quoteId: orderBookQuote.quoteResults.quoteResponse.id,
- suggestedSlippage,
- amountAndCosts: orderBookQuote.quoteResults.amountsAndCosts,
- srcTokenPriceUsd: Number(srcTokenPriceUsd),
- destTokenPriceUsd: Number(destTokenPriceUsd),
- };
-}
diff --git a/src/hooks/switch/paraswap.rates.ts b/src/hooks/switch/paraswap.rates.ts
deleted file mode 100644
index 27e0c4378d..0000000000
--- a/src/hooks/switch/paraswap.rates.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { OptimalRate, SwapSide } from '@paraswap/sdk';
-import { constants } from 'ethers';
-import {
- ParaswapRatesType,
- ProviderRatesParams,
-} from 'src/components/transactions/Switch/switch.types';
-
-import { getParaswap } from '../paraswap/common';
-
-export async function getParaswapSellRates({
- chainId,
- amount,
- srcToken,
- srcDecimals,
- destToken,
- destDecimals,
- user,
- options = {},
-}: ProviderRatesParams): Promise {
- const { paraswap } = getParaswap(chainId);
- return paraswap
- .getRate({
- amount,
- srcToken,
- srcDecimals,
- destToken,
- destDecimals,
- userAddress: user ? user : constants.AddressZero,
- side: SwapSide.SELL,
- options: {
- ...options,
- excludeDEXS: [
- 'ParaSwapPool',
- 'ParaSwapLimitOrders',
- 'SwaapV2',
- 'Hashflow',
- 'Dexalot',
- 'Bebop',
- ],
- },
- })
- .then((paraSwapResponse: OptimalRate) => ({
- srcToken,
- srcUSD: paraSwapResponse.srcUSD,
- srcAmount: paraSwapResponse.srcAmount,
- srcDecimals,
- destToken,
- destUSD: paraSwapResponse.destUSD,
- destAmount: paraSwapResponse.destAmount,
- destDecimals,
- provider: 'paraswap',
- optimalRateData: paraSwapResponse,
- }));
-}
diff --git a/src/hooks/switch/switchProvider.helpers.ts b/src/hooks/switch/switchProvider.helpers.ts
deleted file mode 100644
index d0fb546ae0..0000000000
--- a/src/hooks/switch/switchProvider.helpers.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { COW_UNSUPPORTED_ASSETS } from 'src/components/transactions/Switch/cowprotocol/cowprotocol.constants';
-import { isChainIdSupportedByCoWProtocol } from 'src/components/transactions/Switch/switch.constants';
-import { SwitchProvider } from 'src/components/transactions/Switch/switch.types';
-import { ModalType } from 'src/hooks/useModal';
-
-export const isSwapSupportedByCowProtocol = (
- chainId: number,
- assetFrom: string,
- assetTo: string,
- modalType: ModalType
-) => {
- if (!isChainIdSupportedByCoWProtocol(chainId)) return false;
-
- const unsupportedAssetsPerChainAndModalType =
- COW_UNSUPPORTED_ASSETS[modalType] && COW_UNSUPPORTED_ASSETS[modalType][chainId];
-
- if (unsupportedAssetsPerChainAndModalType === undefined) return true; // No unsupported assets for this chain and modal type
-
- if (unsupportedAssetsPerChainAndModalType === 'ALL') return false; // All assets are unsupported
-
- if (
- unsupportedAssetsPerChainAndModalType.includes(assetFrom.toLowerCase()) ||
- unsupportedAssetsPerChainAndModalType.includes(assetTo.toLowerCase())
- )
- return false;
-
- return true;
-};
-
-export const getSwitchProvider = ({
- chainId,
- assetFrom,
- assetTo,
- shouldUseFlashloan,
- modalType,
-}: {
- chainId: number;
- assetFrom: string;
- assetTo: string;
- shouldUseFlashloan?: boolean;
- modalType: ModalType;
-}): SwitchProvider | undefined => {
- if (shouldUseFlashloan) return 'paraswap';
-
- if (isSwapSupportedByCowProtocol(chainId, assetFrom, assetTo, modalType)) {
- return 'cowprotocol';
- }
-
- return 'paraswap';
-};
diff --git a/src/hooks/switch/useCowSwitchRates.ts b/src/hooks/switch/useCowSwitchRates.ts
deleted file mode 100644
index 0e001541ff..0000000000
--- a/src/hooks/switch/useCowSwitchRates.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { useQuery } from '@tanstack/react-query';
-import { HEADER_WIDGET_APP_CODE } from 'src/components/transactions/Switch/cowprotocol/cowprotocol.helpers';
-import {
- CowProtocolRatesType,
- MultiProviderRatesParams,
-} from 'src/components/transactions/Switch/switch.types';
-import { queryKeysFactory } from 'src/ui-config/queries';
-
-import { getCowProtocolSellRates } from './cowprotocol.rates';
-
-export const useCowSwitchRates = ({
- chainId,
- amount,
- srcUnderlyingToken,
- destUnderlyingToken,
- user,
- inputSymbol,
- isInputTokenCustom,
- isOutputTokenCustom,
- outputSymbol,
- srcDecimals,
- destDecimals,
- isTxSuccess,
- isExecutingActions = false,
-}: MultiProviderRatesParams & {
- isTxSuccess?: boolean;
- isExecutingActions?: boolean;
-}) => {
- return useQuery({
- queryFn: async () => {
- return await getCowProtocolSellRates({
- chainId,
- amount,
- srcToken: srcUnderlyingToken,
- destToken: destUnderlyingToken,
- user,
- srcDecimals,
- destDecimals,
- inputSymbol,
- outputSymbol,
- isInputTokenCustom,
- isOutputTokenCustom,
- appCode: HEADER_WIDGET_APP_CODE,
- });
- },
- queryKey: queryKeysFactory.cowProtocolRates(
- chainId,
- amount,
- srcUnderlyingToken,
- destUnderlyingToken,
- user
- ),
- enabled: amount !== '0' && !isTxSuccess,
- retry: 0,
- throwOnError: false,
- refetchOnWindowFocus: (query) => (query.state.error ? false : true),
- refetchInterval: !isExecutingActions ? 30000 : false, // 30 seconds, but pause during action execution
- });
-};
diff --git a/src/hooks/switch/useMultiProviderSwitchRates.ts b/src/hooks/switch/useMultiProviderSwitchRates.ts
deleted file mode 100644
index 0ddfa11a49..0000000000
--- a/src/hooks/switch/useMultiProviderSwitchRates.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-import { useQuery } from '@tanstack/react-query';
-import { useMemo } from 'react';
-import {
- ADAPTER_APP_CODE,
- HEADER_WIDGET_APP_CODE,
-} from 'src/components/transactions/Switch/cowprotocol/cowprotocol.helpers';
-import {
- MultiProviderRatesParams,
- SwitchRatesType,
-} from 'src/components/transactions/Switch/switch.types';
-import { queryKeysFactory } from 'src/ui-config/queries';
-
-import { ModalType } from '../useModal';
-import { getCowProtocolSellRates } from './cowprotocol.rates';
-import { getParaswapSellRates } from './paraswap.rates';
-import { getSwitchProvider } from './switchProvider.helpers';
-
-export const useMultiProviderSwitchRates = ({
- chainId,
- amount,
- srcUnderlyingToken,
- srcAToken,
- destUnderlyingToken,
- destAToken,
- user,
- inputSymbol,
- isInputTokenCustom,
- isOutputTokenCustom,
- outputSymbol,
- srcDecimals,
- destDecimals,
- isTxSuccess,
- shouldUseFlashloan = false,
- isExecutingActions = false,
- modalType,
-}: MultiProviderRatesParams & {
- isTxSuccess?: boolean;
- shouldUseFlashloan?: boolean;
- isExecutingActions?: boolean;
- modalType: ModalType;
-}) => {
- const provider = useMemo(
- () =>
- getSwitchProvider({
- chainId,
- assetFrom: srcAToken ?? srcUnderlyingToken,
- assetTo: destAToken ?? destUnderlyingToken,
- modalType: modalType,
- shouldUseFlashloan,
- }),
- [
- chainId,
- srcAToken,
- srcUnderlyingToken,
- destAToken,
- destUnderlyingToken,
- modalType,
- shouldUseFlashloan,
- ]
- );
-
- const srcToken = useMemo(() => {
- return modalType === ModalType.CollateralSwap
- ? shouldUseFlashloan === true || provider === 'paraswap'
- ? srcUnderlyingToken
- : srcAToken ?? srcUnderlyingToken
- : srcUnderlyingToken;
- }, [srcAToken, srcUnderlyingToken, provider, modalType, shouldUseFlashloan]);
-
- const destToken = useMemo(() => {
- return modalType === ModalType.CollateralSwap
- ? shouldUseFlashloan === true || provider === 'paraswap'
- ? destUnderlyingToken
- : destAToken ?? destUnderlyingToken
- : destUnderlyingToken;
- }, [destAToken, destUnderlyingToken, provider, modalType, shouldUseFlashloan]);
-
- const appCode =
- modalType === ModalType.CollateralSwap ? ADAPTER_APP_CODE : HEADER_WIDGET_APP_CODE;
-
- return useQuery({
- queryFn: async () => {
- if (!provider) {
- throw new Error('No swap provider found in the selected chain for this pair');
- }
-
- if (srcToken === destToken) {
- throw new Error('Source and destination tokens cannot be the same');
- }
-
- switch (provider) {
- case 'cowprotocol':
- return await getCowProtocolSellRates({
- chainId,
- amount,
- srcToken,
- destToken,
- user,
- srcDecimals,
- destDecimals,
- inputSymbol,
- outputSymbol,
- isInputTokenCustom,
- isOutputTokenCustom,
- appCode,
- });
- case 'paraswap':
- return await getParaswapSellRates({
- chainId,
- amount,
- srcToken,
- destToken,
- user,
- srcDecimals,
- destDecimals,
- options: {
- partner: 'aave-widget',
- },
- });
- }
- },
- queryKey: queryKeysFactory.cowProtocolRates(
- chainId,
- amount,
- srcToken,
- destToken,
- user,
- appCode
- ),
- enabled: amount !== '0' && !isTxSuccess,
- retry: 0,
- throwOnError: false,
- refetchOnWindowFocus: (query) => (query.state.error ? false : true),
- refetchInterval: provider === 'cowprotocol' && !isExecutingActions ? 30000 : false, // 30 seconds, but pause during action execution
- });
-};
diff --git a/src/hooks/useCowOrderToast.tsx b/src/hooks/useCowOrderToast.tsx
deleted file mode 100644
index c701252605..0000000000
--- a/src/hooks/useCowOrderToast.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-import { useQueryClient } from '@tanstack/react-query';
-import {
- createContext,
- PropsWithChildren,
- useCallback,
- useContext,
- useEffect,
- useRef,
- useState,
-} from 'react';
-import { toast } from 'sonner';
-import {
- generateCoWExplorerLink,
- getOrder,
- isOrderCancelled,
- isOrderFilled,
- isOrderLoading,
-} from 'src/components/transactions/Switch/cowprotocol/cowprotocol.helpers';
-import { isCowSwapTransaction } from 'src/modules/history/types';
-import { useRootStore } from 'src/store/root';
-import { findByChainId } from 'src/ui-config/marketsConfig';
-import { queryKeysFactory } from 'src/ui-config/queries';
-import { findTokenSymbol } from 'src/ui-config/TokenList';
-import { GENERAL } from 'src/utils/events';
-import { useShallow } from 'zustand/shallow';
-
-import { useTransactionHistory } from './useTransactionHistory';
-
-interface OrderDetails {
- orderId: string;
- chainId: number;
- interval: NodeJS.Timeout;
-}
-
-interface CowOrderToastContextType {
- trackOrder: (orderId: string, chainId: number) => void;
- stopTracking: (orderId: string) => void;
- hasActiveOrders: boolean;
- setHasActiveOrders: (hasActiveOrders: boolean) => void;
- activeOrdersCount: number;
-}
-
-const CowOrderToastContext = createContext(
- {} as CowOrderToastContextType
-);
-
-export const CowOrderToastProvider: React.FC = ({ children }) => {
- const [activeOrders, setActiveOrders] = useState
+ <>
+
+
+ Transactions
+
+
+ This list may not include all your swaps.
+
+
+ >
}
>
diff --git a/src/modules/history/TransactionMobileRowItem.tsx b/src/modules/history/TransactionMobileRowItem.tsx
index 29709ebc09..866b988452 100644
--- a/src/modules/history/TransactionMobileRowItem.tsx
+++ b/src/modules/history/TransactionMobileRowItem.tsx
@@ -1,17 +1,24 @@
+import { OrderStatus } from '@cowprotocol/cow-sdk';
import { Trans } from '@lingui/macro';
import ArrowOutward from '@mui/icons-material/ArrowOutward';
import { Box, Button, SvgIcon, Typography, useTheme } from '@mui/material';
import React, { useEffect, useState } from 'react';
import { ListItem } from 'src/components/lists/ListItem';
+import { useModalContext } from 'src/hooks/useModal';
import { useRootStore } from 'src/store/root';
import { GENERAL } from 'src/utils/events';
import { useShallow } from 'zustand/shallow';
import { ActionDetails, ActionTextMap } from './actions/ActionDetails';
import { getExplorerLink, getTransactionAction, unixTimestampToFormattedTime } from './helpers';
-import { TransactionHistoryItemUnion } from './types';
+import {
+ ActionName,
+ isCowSwapSubset,
+ isSwapTransaction,
+ TransactionHistoryItemUnion,
+} from './types';
-function ActionTitle({ action }: { action: string }) {
+function ActionTitle({ action }: { action: ActionName }) {
return (
@@ -28,6 +35,7 @@ function TransactionMobileRowItem({ transaction }: TransactionHistoryItemProps)
const [currentNetworkConfig, trackEvent] = useRootStore(
useShallow((state) => [state.currentNetworkConfig, state.trackEvent])
);
+ const { openCancelCowOrder } = useModalContext();
const theme = useTheme();
const explorerLink = getExplorerLink(transaction, currentNetworkConfig);
const action = getTransactionAction(transaction);
@@ -78,16 +86,33 @@ function TransactionMobileRowItem({ transaction }: TransactionHistoryItemProps)
-
- {' '}
+
{unixTimestampToFormattedTime({ unixTimestamp: timestamp })}
+ {isSwapTransaction(transaction) &&
+ isCowSwapSubset(transaction) &&
+ transaction.status === OrderStatus.OPEN && (
+
+ )}
{explorerLink && (
-
+
- {isCowSwapTransaction(transaction) && transaction.status === OrderStatus.OPEN && (
-
- )}
+ {isSwapTransaction(transaction) &&
+ isCowSwapSubset(transaction) &&
+ transaction.status === OrderStatus.OPEN && (
+
+ )}
{!downToMD && explorerLink && (