diff --git a/package.json b/package.json index 939757ef4e..c7e12fa92f 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@safe-global/protocol-kit": "^4.1.0", "@safe-global/safe-apps-sdk": "^9.1.0", "@safe-global/safe-deployments": "^1.37.3", - "@safe-global/safe-gateway-typescript-sdk": "3.22.4-beta.1", + "@safe-global/safe-gateway-typescript-sdk": "3.22.3-beta.12", "@safe-global/safe-modules-deployments": "^1.2.0", "@sentry/react": "^7.91.0", "@spindl-xyz/attribution-lite": "^1.4.0", diff --git a/src/features/swap/components/LegalDisclaimer/index.tsx b/src/components/common/WidgetDisclaimer/index.tsx similarity index 77% rename from src/features/swap/components/LegalDisclaimer/index.tsx rename to src/components/common/WidgetDisclaimer/index.tsx index a324af6889..3c5969d784 100644 --- a/src/features/swap/components/LegalDisclaimer/index.tsx +++ b/src/components/common/WidgetDisclaimer/index.tsx @@ -4,7 +4,11 @@ import { Typography } from '@mui/material' import css from './styles.module.css' -const LegalDisclaimerContent = () => ( +const linkSx = { + textDecoration: 'none', +} + +const WidgetDisclaimer = ({ widgetName }: { widgetName: string }) => (
@@ -12,21 +16,21 @@ const LegalDisclaimerContent = () => ( - Please note that we do not own, control, maintain or audit the CoW Swap Widget. Use of the widget is subject to + Please note that we do not own, control, maintain or audit the {widgetName}. Use of the widget is subject to third party terms & conditions. We are not liable for any loss you may suffer in connection with interacting with the widget, which is at your own risk. Our{' '} - + terms {' '} contain more detailed provisions binding on you relating to such third party content. By clicking "continue" you re-confirm to have read and understood our{' '} - + terms {' '} and this message, and agree to them. @@ -35,4 +39,4 @@ const LegalDisclaimerContent = () => (
) -export default LegalDisclaimerContent +export default WidgetDisclaimer diff --git a/src/features/swap/components/LegalDisclaimer/styles.module.css b/src/components/common/WidgetDisclaimer/styles.module.css similarity index 100% rename from src/features/swap/components/LegalDisclaimer/styles.module.css rename to src/components/common/WidgetDisclaimer/styles.module.css diff --git a/src/components/dashboard/Assets/index.tsx b/src/components/dashboard/Assets/index.tsx index 8df89c1644..12aca61614 100644 --- a/src/components/dashboard/Assets/index.tsx +++ b/src/components/dashboard/Assets/index.tsx @@ -43,7 +43,7 @@ const NoAssets = () => ( ) -const AssetRow = ({ item, showSwap }: { item: SafeBalanceResponse['items'][number]; showSwap: boolean }) => ( +const AssetRow = ({ item, showSwap }: { item: SafeBalanceResponse['items'][number]; showSwap?: boolean }) => ( { +const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest, isNativeEmbed }: AppFrameProps): ReactElement => { const { safe, safeLoaded } = useSafeInfo() const addressBook = useAddressBook() const chainId = useChainId() @@ -98,11 +97,14 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrame } setAppIsLoading(false) - gtmTrackPageview(`${router.pathname}?appUrl=${router.query.appUrl}`, router.asPath) - }, [appUrl, iframeRef, setAppIsLoading, router]) + + if (!isNativeEmbed) { + gtmTrackPageview(`${router.pathname}?appUrl=${router.query.appUrl}`, router.asPath) + } + }, [appUrl, iframeRef, setAppIsLoading, router, isNativeEmbed]) useEffect(() => { - if (!appIsLoading && !isBackendAppsLoading) { + if (!isNativeEmbed && !appIsLoading && !isBackendAppsLoading) { trackSafeAppEvent( { ...SAFE_APPS_EVENTS.OPEN_APP, @@ -110,7 +112,7 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrame appName, ) } - }, [appIsLoading, isBackendAppsLoading, appName]) + }, [appIsLoading, isBackendAppsLoading, appName, isNativeEmbed]) if (!safeLoaded) { return
@@ -118,9 +120,11 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrame return ( <> - - {`Safe Apps - Viewer - ${remoteApp ? remoteApp.name : UNKNOWN_APP_NAME}`} - + {!isNativeEmbed && ( + + {`Safe{Wallet} - Safe Apps${remoteApp ? ' - ' + remoteApp.name : ''}`} + + )}
{thirdPartyCookiesDisabled && setThirdPartyCookiesDisabled(false)} />} @@ -160,7 +164,7 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrame transactions={transactions} /> - {permissionsRequest && ( + {!isNativeEmbed && permissionsRequest && ( , href: AppRoutes.stake, - tag: , - disabled: true, - tooltip: 'Native staking is coming soon, stay tuned!', + tag: , }, { label: 'Transactions', diff --git a/src/components/sidebar/SidebarNavigation/index.tsx b/src/components/sidebar/SidebarNavigation/index.tsx index d7b38f8544..c5980ba59a 100644 --- a/src/components/sidebar/SidebarNavigation/index.tsx +++ b/src/components/sidebar/SidebarNavigation/index.tsx @@ -19,12 +19,13 @@ import { isRouteEnabled } from '@/utils/chains' import { trackEvent } from '@/services/analytics' import { SWAP_EVENTS, SWAP_LABELS } from '@/services/analytics/events/swaps' import { GeoblockingContext } from '@/components/common/GeoblockingProvider' -import { Tooltip } from '@mui/material' const getSubdirectory = (pathname: string): string => { return pathname.split('/')[1] } +const geoBlockedRoutes = [AppRoutes.swap, AppRoutes.stake] + const Navigation = (): ReactElement => { const chain = useCurrentChain() const router = useRouter() @@ -32,14 +33,14 @@ const Navigation = (): ReactElement => { const currentSubdirectory = getSubdirectory(router.pathname) const queueSize = useQueuedTxsLength() const isBlockedCountry = useContext(GeoblockingContext) + const enabledNavItems = useMemo(() => { return navItems.filter((item) => { - const enabled = isRouteEnabled(item.href, chain) - - if (item.href === AppRoutes.swap && isBlockedCountry) { + if (isBlockedCountry && geoBlockedRoutes.includes(item.href)) { return false } - return enabled + + return isRouteEnabled(item.href, chain) }) }, [chain, isBlockedCountry]) @@ -76,23 +77,26 @@ const Navigation = (): ReactElement => { } return ( - - handleNavigationClick(item.href)}> - - {item.icon && {item.icon}} - - - {item.label} - - {ItemTag} - - - - + handleNavigationClick(item.href)} + key={item.href} + > + + {item.icon && {item.icon}} + + + {item.label} + + {ItemTag} + + + ) })} diff --git a/src/components/transactions/TrustedToggle/TrustedToggleButton.tsx b/src/components/transactions/TrustedToggle/TrustedToggleButton.tsx index c5bc393a8d..918d64d91f 100644 --- a/src/components/transactions/TrustedToggle/TrustedToggleButton.tsx +++ b/src/components/transactions/TrustedToggle/TrustedToggleButton.tsx @@ -10,7 +10,7 @@ const _TrustedToggleButton = ({ }: { onlyTrusted: boolean setOnlyTrusted: (on: boolean) => void - hasDefaultTokenlist: boolean + hasDefaultTokenlist?: boolean }): ReactElement | null => { const onClick = () => { setOnlyTrusted(!onlyTrusted) diff --git a/src/components/transactions/TxDetails/Summary/index.tsx b/src/components/transactions/TxDetails/Summary/index.tsx index 45c92e1b25..bd87099a66 100644 --- a/src/components/transactions/TxDetails/Summary/index.tsx +++ b/src/components/transactions/TxDetails/Summary/index.tsx @@ -2,13 +2,14 @@ import type { ReactElement } from 'react' import React, { useState } from 'react' import { Link, Box } from '@mui/material' import { generateDataRowValue, TxDataRow } from '@/components/transactions/TxDetails/Summary/TxDataRow' -import { isMultisigDetailedExecutionInfo } from '@/utils/transaction-guards' +import { isCustomTxInfo, isMultisigDetailedExecutionInfo } from '@/utils/transaction-guards' import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' import { Operation } from '@safe-global/safe-gateway-typescript-sdk' import { dateString } from '@/utils/formatters' import css from './styles.module.css' import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' import SafeTxGasForm from '../SafeTxGasForm' +import DecodedData from '../TxData/DecodedData' interface Props { txDetails: TransactionDetails @@ -30,6 +31,8 @@ const Summary = ({ txDetails, defaultExpanded = false }: Props): ReactElement => refundReceiver = detailedExecutionInfo.refundReceiver?.value } + const isCustom = isCustomTxInfo(txDetails.txInfo) + return ( <> {txHash && ( @@ -67,6 +70,12 @@ const Summary = ({ txDetails, defaultExpanded = false }: Props): ReactElement => {expanded && ( + {!isCustom && ( + + + + )} + {`${txData.operation} (${Operation[txData.operation].toLowerCase()})`} diff --git a/src/components/transactions/TxDetails/TxData/index.tsx b/src/components/transactions/TxDetails/TxData/index.tsx index a19e55bc69..a197b02818 100644 --- a/src/components/transactions/TxDetails/TxData/index.tsx +++ b/src/components/transactions/TxDetails/TxData/index.tsx @@ -1,11 +1,14 @@ import SettingsChangeTxInfo from '@/components/transactions/TxDetails/TxData/SettingsChange' import type { SpendingLimitMethods } from '@/utils/transaction-guards' +import { isStakingTxExitInfo } from '@/utils/transaction-guards' import { isCancellationTxInfo, isCustomTxInfo, isMultisigDetailedExecutionInfo, + isOrderTxInfo, isSettingsChangeTxInfo, isSpendingLimitMethod, + isStakingTxDepositInfo, isSupportedSpendingLimitAddress, isTransferTxInfo, } from '@/utils/transaction-guards' @@ -16,6 +19,9 @@ import RejectionTxInfo from '@/components/transactions/TxDetails/TxData/Rejectio import DecodedData from '@/components/transactions/TxDetails/TxData/DecodedData' import TransferTxInfo from '@/components/transactions/TxDetails/TxData/Transfer' import useChainId from '@/hooks/useChainId' +import SwapOrder from '@/features/swap/components/SwapOrder' +import StakingTxDepositDetails from '@/features/stake/components/StakingTxDepositDetails' +import StakingTxExitDetails from '@/features/stake/components/StakingTxExitDetails' const TxData = ({ txDetails, @@ -30,6 +36,18 @@ const TxData = ({ const txInfo = txDetails.txInfo const toInfo = isCustomTxInfo(txDetails.txInfo) ? txDetails.txInfo.to : undefined + if (isOrderTxInfo(txDetails.txInfo)) { + return + } + + if (isStakingTxDepositInfo(txDetails.txInfo)) { + return + } + + if (isStakingTxExitInfo(txDetails.txInfo)) { + return + } + if (isTransferTxInfo(txInfo)) { return } diff --git a/src/components/transactions/TxDetails/index.tsx b/src/components/transactions/TxDetails/index.tsx index 1d9a4febf4..1387362d0b 100644 --- a/src/components/transactions/TxDetails/index.tsx +++ b/src/components/transactions/TxDetails/index.tsx @@ -33,7 +33,6 @@ import useIsPending from '@/hooks/useIsPending' import { isImitation, isTrustedTx } from '@/utils/transactions' import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' -import { SwapOrder } from '@/features/swap/components/SwapOrder' import { useGetTransactionDetailsQuery } from '@/store/gateway' import { asError } from '@/services/exceptions/utils' import { POLLING_INTERVAL } from '@/config/constants' @@ -77,14 +76,6 @@ const TxDetailsBlock = ({ txSummary, txDetails }: TxDetailsProps): ReactElement <> {/* /Details */}
- {isOrderTxInfo(txDetails.txInfo) && ( -
- Error parsing data
}> - - -
- )} -
diff --git a/src/components/transactions/TxDetails/styles.module.css b/src/components/transactions/TxDetails/styles.module.css index 9d9e8de2b4..5c3bc427dc 100644 --- a/src/components/transactions/TxDetails/styles.module.css +++ b/src/components/transactions/TxDetails/styles.module.css @@ -22,22 +22,11 @@ .txData, .txSummary, .advancedDetails, -.txModule, -.swapOrder { +.txModule { padding: var(--space-2); } -.swapOrderTransfer { - border-top: 1px solid var(--color-border-light); - margin-top: var(--space-2); - margin-left: calc(var(--space-2) * -1); - margin-right: calc(var(--space-2) * -1); - padding: var(--space-2); - padding-top: var(--space-3); -} - -.txData, -.swapOrder { +.txData { border-bottom: 1px solid var(--color-border-light); } @@ -59,8 +48,7 @@ padding: 0 var(--space-1); } -.multiSend, -.swapOrder { +.multiSend { border-bottom: 1px solid var(--color-border-light); } diff --git a/src/components/transactions/TxInfo/index.tsx b/src/components/transactions/TxInfo/index.tsx index 8dbd055ddd..f5f12fdf22 100644 --- a/src/components/transactions/TxInfo/index.tsx +++ b/src/components/transactions/TxInfo/index.tsx @@ -19,12 +19,16 @@ import { isNativeTokenTransfer, isSettingsChangeTxInfo, isTransferTxInfo, + isStakingTxDepositInfo, + isStakingTxExitInfo, } from '@/utils/transaction-guards' import { ellipsis, shortenAddress } from '@/utils/formatters' import { useCurrentChain } from '@/hooks/useChains' import { SwapTx } from '@/features/swap/components/SwapTxInfo/SwapTx' +import StakingTxExitInfo from '@/features/stake/components/StakingTxExitInfo' import { Box } from '@mui/material' import css from './styles.module.css' +import StakingTxDepositInfo from '@/features/stake/components/StakingTxDepositInfo' export const TransferTx = ({ info, @@ -125,10 +129,6 @@ const TxInfo = ({ info, ...rest }: { info: TransactionInfo; omitSign?: boolean; return } - if (isCustomTxInfo(info)) { - return - } - if (isCreationTxInfo(info)) { return } @@ -137,6 +137,18 @@ const TxInfo = ({ info, ...rest }: { info: TransactionInfo; omitSign?: boolean; return } + if (isStakingTxDepositInfo(info)) { + return + } + + if (isStakingTxExitInfo(info)) { + return + } + + if (isCustomTxInfo(info)) { + return + } + return <> } diff --git a/src/components/transactions/TxStatusChip/index.tsx b/src/components/transactions/TxStatusChip/index.tsx index c9bb49c874..339236fb75 100644 --- a/src/components/transactions/TxStatusChip/index.tsx +++ b/src/components/transactions/TxStatusChip/index.tsx @@ -1,19 +1,28 @@ import type { ReactElement, ReactNode } from 'react' import { Typography, Chip } from '@mui/material' -const TxStatusChip = ({ - children, - color, -}: { +export type TxStatusChipProps = { children: ReactNode color?: 'primary' | 'secondary' | 'info' | 'warning' | 'success' | 'error' -}): ReactElement => { +} + +const TxStatusChip = ({ children, color }: TxStatusChipProps): ReactElement => { return ( + {children} } diff --git a/src/components/tx-flow/flows/SignMessage/index.tsx b/src/components/tx-flow/flows/SignMessage/index.tsx index ac9503ef59..6c608ba339 100644 --- a/src/components/tx-flow/flows/SignMessage/index.tsx +++ b/src/components/tx-flow/flows/SignMessage/index.tsx @@ -8,6 +8,8 @@ import SafeAppIconCard from '@/components/safe-apps/SafeAppIconCard' import { ErrorBoundary } from '@sentry/react' import { type BaseTransaction } from '@safe-global/safe-apps-sdk' import { SWAP_TITLE } from '@/features/swap/constants' +import { STAKE_TITLE } from '@/features/stake/constants' +import { getStakeTitle } from '@/features/stake/helpers/utils' const APP_LOGO_FALLBACK_IMAGE = '/images/apps/apps-icon.svg' const APP_NAME_FALLBACK = 'Sign message' @@ -26,7 +28,14 @@ export const AppTitle = ({ const appName = name || APP_NAME_FALLBACK const appLogo = logoUri || APP_LOGO_FALLBACK_IMAGE - const title = name === SWAP_TITLE ? getSwapTitle(swapParams.tradeType, txs) : appName + let title = appName + if (name === SWAP_TITLE) { + title = getSwapTitle(swapParams.tradeType, txs) || title + } + + if (name === STAKE_TITLE) { + title = getStakeTitle(txs) || title + } return ( diff --git a/src/components/tx/AdvancedParams/AdvancedParamsForm.tsx b/src/components/tx/AdvancedParams/AdvancedParamsForm.tsx index 9e1ae1c838..a01ea2c64a 100644 --- a/src/components/tx/AdvancedParams/AdvancedParamsForm.tsx +++ b/src/components/tx/AdvancedParams/AdvancedParamsForm.tsx @@ -15,7 +15,7 @@ type AdvancedParamsFormProps = { onSubmit: (params: AdvancedParameters) => void recommendedGasLimit?: AdvancedParameters['gasLimit'] isExecution: boolean - isEIP1559: boolean + isEIP1559?: boolean willRelay?: boolean } diff --git a/src/components/tx/ConfirmationOrder/ConfirmationOrderHeader.tsx b/src/components/tx/ConfirmationOrder/ConfirmationOrderHeader.tsx new file mode 100644 index 0000000000..89b09f0fd3 --- /dev/null +++ b/src/components/tx/ConfirmationOrder/ConfirmationOrderHeader.tsx @@ -0,0 +1,77 @@ +import { Stack, Box, Typography, SvgIcon } from '@mui/material' +import EastRoundedIcon from '@mui/icons-material/EastRounded' +import TokenIcon from '@/components/common/TokenIcon' +import TokenAmount from '@/components/common/TokenAmount' + +export type InfoBlock = { + value: string + label: string + tokenInfo?: { + decimals: number + symbol: string + logoUri?: string | null + } +} + +const ConfirmationOrderHeader = ({ blocks, showArrow }: { blocks: [InfoBlock, InfoBlock]; showArrow?: boolean }) => { + return ( + + {blocks.map((block, index) => ( + + {block.tokenInfo && ( + + + + )} + + + + {block.label} + + + + {block.tokenInfo ? ( + + ) : ( + block.value + )} + + + + {showArrow && index === 0 && ( + + + + )} + + ))} + + ) +} + +export default ConfirmationOrderHeader diff --git a/src/components/tx/ConfirmationOrder/index.tsx b/src/components/tx/ConfirmationOrder/index.tsx new file mode 100644 index 0000000000..33142de06f --- /dev/null +++ b/src/components/tx/ConfirmationOrder/index.tsx @@ -0,0 +1,23 @@ +import StrakingConfirmationTx from '@/features/stake/components/StakingConfirmationTx' +import SwapOrderConfirmationView from '@/features/swap/components/SwapOrderConfirmationView' +import type useDecodeTx from '@/hooks/useDecodeTx' +import { isAnyStakingConfirmationView, isAnySwapConfirmationViewOrder } from '@/utils/transaction-guards' + +type OrderConfirmationViewProps = { + decodedData: ReturnType[0] + toAddress: string +} + +const ConfirmationOrder = ({ decodedData, toAddress }: OrderConfirmationViewProps) => { + if (isAnySwapConfirmationViewOrder(decodedData)) { + return + } + + if (isAnyStakingConfirmationView(decodedData)) { + return + } + + return null +} + +export default ConfirmationOrder diff --git a/src/components/tx/DecodedTx/index.tsx b/src/components/tx/DecodedTx/index.tsx index ac3ddfcd8e..44d8c3087a 100644 --- a/src/components/tx/DecodedTx/index.tsx +++ b/src/components/tx/DecodedTx/index.tsx @@ -41,9 +41,9 @@ const DecodedTx = ({ showMultisend = true, showMethodCall = false, }: DecodedTxProps): ReactElement => { - const isMultisend = !!decodedData?.parameters?.[0]?.valueDecoded const chainId = useChainId() - const isMethodCallInAdvanced = !showMethodCall || isMultisend + const isMultisend = !!decodedData?.parameters?.[0]?.valueDecoded + const isMethodCallInAdvanced = !showMethodCall || (isMultisend && showMultisend) const { data: txDetails, diff --git a/src/components/tx/GasParams/index.tsx b/src/components/tx/GasParams/index.tsx index 9aaabf6a94..3510cb1d88 100644 --- a/src/components/tx/GasParams/index.tsx +++ b/src/components/tx/GasParams/index.tsx @@ -28,7 +28,7 @@ const GasDetail = ({ name, value, isLoading }: { name: string; value: string; is type GasParamsProps = { params: AdvancedParameters isExecution: boolean - isEIP1559: boolean + isEIP1559?: boolean onEdit?: () => void gasLimitError?: Error willRelay?: boolean diff --git a/src/components/tx/SignOrExecuteForm/index.tsx b/src/components/tx/SignOrExecuteForm/index.tsx index e830e75db9..2a493b05bd 100644 --- a/src/components/tx/SignOrExecuteForm/index.tsx +++ b/src/components/tx/SignOrExecuteForm/index.tsx @@ -27,15 +27,13 @@ import { trackEvent } from '@/services/analytics' import useChainId from '@/hooks/useChainId' import ExecuteThroughRoleForm from './ExecuteThroughRoleForm' import { findAllowingRole, findMostLikelyRole, useRoles } from './ExecuteThroughRoleForm/hooks' -import { isConfirmationViewOrder, isCustomTxInfo } from '@/utils/transaction-guards' -import SwapOrderConfirmationView from '@/features/swap/components/SwapOrderConfirmationView' -import { isSettingTwapFallbackHandler } from '@/features/swap/helpers/utils' -import { TwapFallbackHandlerWarning } from '@/features/swap/components/TwapFallbackHandlerWarning' +import { isCustomTxInfo, isGenericConfirmation } from '@/utils/transaction-guards' import useIsSafeOwner from '@/hooks/useIsSafeOwner' import { BlockaidBalanceChanges } from '../security/blockaid/BlockaidBalanceChange' import { Blockaid } from '../security/blockaid' import TxData from '@/components/transactions/TxDetails/TxData' +import ConfirmationOrder from '@/components/tx/ConfirmationOrder' import { useApprovalInfos } from '../ApprovalEditor/hooks/useApprovalInfos' import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' @@ -97,7 +95,7 @@ export const SignOrExecuteForm = ({ const [decodedData] = useDecodeTx(safeTx) const isBatchable = props.isBatchable !== false && safeTx && !isDelegateCall(safeTx) - const isSwapOrder = isConfirmationViewOrder(decodedData) + const { data: txDetails } = useGetTransactionDetailsQuery( chainId && props.txId ? { @@ -115,7 +113,6 @@ export const SignOrExecuteForm = ({ const { safe } = useSafeInfo() const isSafeOwner = useIsSafeOwner() const isCounterfactualSafe = !safe.deployed - const isChangingFallbackHandler = isSettingTwapFallbackHandler(decodedData) // Check if a Zodiac Roles mod is enabled and if the user is a member of any role that allows the transaction const roles = useRoles( @@ -153,11 +150,9 @@ export const SignOrExecuteForm = ({ {props.children} - {isChangingFallbackHandler && } - - {isSwapOrder && ( + {decodedData && ( }> - + )} @@ -172,7 +167,9 @@ export const SignOrExecuteForm = ({ txId={props.txId} decodedData={decodedData} showMultisend={!props.isBatch} - showMethodCall={props.showMethodCall && !showTxDetails && !isSwapOrder && !isApproval} + showMethodCall={ + props.showMethodCall && !showTxDetails && !isApproval && isGenericConfirmation(decodedData) + } /> )} diff --git a/src/config/routes.ts b/src/config/routes.ts index 0509085fa5..8f65fa5bb7 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -4,6 +4,7 @@ export const AppRoutes = { wc: '/wc', terms: '/terms', swap: '/swap', + stake: '/stake', privacy: '/privacy', licenses: '/licenses', index: '/', @@ -45,7 +46,6 @@ export const AppRoutes = { share: { safeApp: '/share/safe-app', }, - stake: '/stake', transactions: { tx: '/transactions/tx', queue: '/transactions/queue', diff --git a/src/features/recovery/hooks/useIsRecoverySupported.ts b/src/features/recovery/hooks/useIsRecoverySupported.ts index 099578df0c..549070dea7 100644 --- a/src/features/recovery/hooks/useIsRecoverySupported.ts +++ b/src/features/recovery/hooks/useIsRecoverySupported.ts @@ -2,5 +2,5 @@ import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' export function useIsRecoverySupported(): boolean { - return useHasFeature(FEATURES.RECOVERY) + return useHasFeature(FEATURES.RECOVERY) ?? false } diff --git a/src/features/stake/components/StakePage/index.tsx b/src/features/stake/components/StakePage/index.tsx new file mode 100644 index 0000000000..7ab3382e1e --- /dev/null +++ b/src/features/stake/components/StakePage/index.tsx @@ -0,0 +1,28 @@ +import { Stack } from '@mui/material' +import Disclaimer from '@/components/common/Disclaimer' +import WidgetDisclaimer from '@/components/common/WidgetDisclaimer' +import useStakeConsent from '@/features/stake/useStakeConsent' +import StakingWidget from '../StakingWidget' + +const StakePage = () => { + const { isConsentAccepted, onAccept } = useStakeConsent() + + return ( + <> + {isConsentAccepted === undefined ? null : isConsentAccepted ? ( + + ) : ( + + } + onAccept={onAccept} + buttonText="Continue" + /> + + )} + + ) +} + +export default StakePage diff --git a/src/features/stake/components/StakingConfirmationTx/Deposit.tsx b/src/features/stake/components/StakingConfirmationTx/Deposit.tsx new file mode 100644 index 0000000000..c7fccc3fc2 --- /dev/null +++ b/src/features/stake/components/StakingConfirmationTx/Deposit.tsx @@ -0,0 +1,91 @@ +import { Typography, Stack, Box } from '@mui/material' +import FieldsGrid from '@/components/tx/FieldsGrid' +import type { StakingTxDepositInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { + ConfirmationViewTypes, + type NativeStakingDepositConfirmationView, +} from '@safe-global/safe-gateway-typescript-sdk' +import ConfirmationOrderHeader from '@/components/tx/ConfirmationOrder/ConfirmationOrderHeader' +import { formatVisualAmount, formatDurationFromSeconds } from '@/utils/formatters' +import { formatCurrency } from '@/utils/formatNumber' +import StakingStatus from '@/features/stake/components/StakingStatus' + +type StakingOrderConfirmationViewProps = { + order: NativeStakingDepositConfirmationView | StakingTxDepositInfo +} + +const CURRENCY = 'USD' + +const StakingConfirmationTxDeposit = ({ order }: StakingOrderConfirmationViewProps) => { + const isOrder = order.type === ConfirmationViewTypes.KILN_NATIVE_STAKING_DEPOSIT + + return ( + + {isOrder && ( + + )} + + + {formatVisualAmount(order.expectedAnnualReward, order.tokenInfo.decimals)} {order.tokenInfo.symbol} + {' ('} + {formatCurrency(order.expectedFiatAnnualReward, CURRENCY)}) + + + + {formatVisualAmount(order.expectedMonthlyReward, order.tokenInfo.decimals)} {order.tokenInfo.symbol} + {' ('} + {formatCurrency(order.expectedFiatMonthlyReward, CURRENCY)}) + + + {order.fee}% + + + {isOrder ? ( + + You will own{' '} + + {order.numValidators} Ethereum validator{order.numValidators === 1 ? '' : 's'} + + + ) : ( + {order.numValidators} + )} + + {formatDurationFromSeconds(order.estimatedEntryTime)} + Approx. every 5 days after 4 days from activation + + {!isOrder && ( + + + + )} + + {isOrder && ( + + Earn ETH rewards with dedicated validators. Rewards must be withdrawn manually, and you can request a + withdrawal at any time. + + )} + + + ) +} + +export default StakingConfirmationTxDeposit diff --git a/src/features/stake/components/StakingConfirmationTx/Exit.tsx b/src/features/stake/components/StakingConfirmationTx/Exit.tsx new file mode 100644 index 0000000000..4c68d07073 --- /dev/null +++ b/src/features/stake/components/StakingConfirmationTx/Exit.tsx @@ -0,0 +1,48 @@ +import { Typography, Stack, Alert } from '@mui/material' +import FieldsGrid from '@/components/tx/FieldsGrid' +import type { StakingTxExitInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { formatDurationFromSeconds } from '@/utils/formatters' +import { type NativeStakingValidatorsExitConfirmationView } from '@safe-global/safe-gateway-typescript-sdk/dist/types/decoded-data' +import ConfirmationOrderHeader from '@/components/tx/ConfirmationOrder/ConfirmationOrderHeader' + +type StakingOrderConfirmationViewProps = { + order: NativeStakingValidatorsExitConfirmationView | StakingTxExitInfo +} + +const StakingConfirmationTxExit = ({ order }: StakingOrderConfirmationViewProps) => { + const withdrawIn = formatDurationFromSeconds(order.estimatedExitTime + order.estimatedWithdrawalTime, [ + 'days', + 'hours', + ]) + + return ( + + + + Up to {withdrawIn} + + + The selected amount and any rewards will be withdrawn from Dedicated Staking for ETH after the validator exit. + + + + This transaction is a withdrawal request. After it's executed, you'll need to complete a separate + withdrawal transaction. + + + ) +} + +export default StakingConfirmationTxExit diff --git a/src/features/stake/components/StakingConfirmationTx/index.tsx b/src/features/stake/components/StakingConfirmationTx/index.tsx new file mode 100644 index 0000000000..407546f03a --- /dev/null +++ b/src/features/stake/components/StakingConfirmationTx/index.tsx @@ -0,0 +1,25 @@ +import type { AnyStakingConfirmationView } from '@safe-global/safe-gateway-typescript-sdk' +import { ConfirmationViewTypes, type StakingTxInfo } from '@safe-global/safe-gateway-typescript-sdk' +import StakingConfirmationTxDeposit from '@/features/stake/components/StakingConfirmationTx/Deposit' +import StakingConfirmationTxExit from '@/features/stake/components/StakingConfirmationTx/Exit' + +type StakingOrderConfirmationViewProps = { + order: AnyStakingConfirmationView | StakingTxInfo +} + +const StrakingConfirmationTx = ({ order }: StakingOrderConfirmationViewProps) => { + const isDeposit = order.type === ConfirmationViewTypes.KILN_NATIVE_STAKING_DEPOSIT + const isExit = order.type === ConfirmationViewTypes.KILN_NATIVE_STAKING_VALIDATORS_EXIT + + if (isDeposit) { + return + } + + if (isExit) { + return + } + + return null +} + +export default StrakingConfirmationTx diff --git a/src/features/stake/components/StakingStatus/index.tsx b/src/features/stake/components/StakingStatus/index.tsx new file mode 100644 index 0000000000..d8ae9ab853 --- /dev/null +++ b/src/features/stake/components/StakingStatus/index.tsx @@ -0,0 +1,77 @@ +import { NativeStakingExitStatus, NativeStakingStatus } from '@safe-global/safe-gateway-typescript-sdk' +import { SvgIcon } from '@mui/material' +import CheckIcon from '@/public/images/common/circle-check.svg' +import ClockIcon from '@/public/images/common/clock.svg' +import SignatureIcon from '@/public/images/common/document_signature.svg' +import TxStatusChip, { type TxStatusChipProps } from '@/components/transactions/TxStatusChip' + +const ColorIcons: Record< + NativeStakingStatus | NativeStakingExitStatus, + | { + color: TxStatusChipProps['color'] + icon?: React.ComponentType + text: string + } + | undefined +> = { + [NativeStakingStatus.AWAITING_ENTRY]: { + color: 'info', + icon: ClockIcon, + text: 'Activating', + }, + [NativeStakingStatus.REQUESTED_EXIT]: { + color: 'info', + icon: ClockIcon, + text: 'Requested exit', + }, + [NativeStakingStatus.SIGNATURE_NEEDED]: { + color: 'warning', + icon: SignatureIcon, + text: 'Signature needed', + }, + [NativeStakingStatus.AWAITING_EXECUTION]: { + color: 'warning', + icon: ClockIcon, + text: 'Awaiting execution', + }, + [NativeStakingStatus.VALIDATION_STARTED]: { + color: 'success', + icon: CheckIcon, + text: 'Validation started', + }, + [NativeStakingStatus.WITHDRAWN]: { + color: 'success', + icon: CheckIcon, + text: 'Withdrawn', + }, + [NativeStakingExitStatus.READY_TO_WITHDRAW]: { + color: 'success', + icon: CheckIcon, + text: 'Ready to withdraw', + }, + [NativeStakingExitStatus.REQUEST_PENDING]: { + color: 'info', + icon: ClockIcon, + text: 'Request pending', + }, + [NativeStakingStatus.UNKNOWN]: undefined, +} + +const capitalizedStatus = (status: string) => + status + .toLowerCase() + .replace(/_/g, ' ') + .replace(/^\w/g, (l) => l.toUpperCase()) + +const StakingStatus = ({ status }: { status: NativeStakingStatus | NativeStakingExitStatus }) => { + const config = ColorIcons[status] + + return ( + + {config?.icon && } + {config?.text || capitalizedStatus(status)} + + ) +} + +export default StakingStatus diff --git a/src/features/stake/components/StakingTxDepositDetails/index.tsx b/src/features/stake/components/StakingTxDepositDetails/index.tsx new file mode 100644 index 0000000000..1a3e3c9d0a --- /dev/null +++ b/src/features/stake/components/StakingTxDepositDetails/index.tsx @@ -0,0 +1,21 @@ +import { Box } from '@mui/material' +import type { StakingTxDepositInfo, TransactionData } from '@safe-global/safe-gateway-typescript-sdk' +import FieldsGrid from '@/components/tx/FieldsGrid' +import SendAmountBlock from '@/components/tx-flow/flows/TokenTransfer/SendAmountBlock' +import StakingConfirmationTxDeposit from '@/features/stake/components/StakingConfirmationTx/Deposit' + +const StakingTxDepositDetails = ({ info, txData }: { info: StakingTxDepositInfo; txData?: TransactionData }) => { + return ( + + {txData && ( + + )} + + {info.annualNrr.toFixed(3)}% + + + + ) +} + +export default StakingTxDepositDetails diff --git a/src/features/stake/components/StakingTxDepositInfo/index.tsx b/src/features/stake/components/StakingTxDepositInfo/index.tsx new file mode 100644 index 0000000000..d7d4bf150d --- /dev/null +++ b/src/features/stake/components/StakingTxDepositInfo/index.tsx @@ -0,0 +1,8 @@ +import type { StakingTxInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { camelCaseToSpaces } from '@/utils/formatters' + +export const StakingTxDepositInfo = ({ info }: { info: StakingTxInfo }) => { + return <>{camelCaseToSpaces(info.type).toLowerCase()} +} + +export default StakingTxDepositInfo diff --git a/src/features/stake/components/StakingTxExitDetails/index.tsx b/src/features/stake/components/StakingTxExitDetails/index.tsx new file mode 100644 index 0000000000..31e4839cd4 --- /dev/null +++ b/src/features/stake/components/StakingTxExitDetails/index.tsx @@ -0,0 +1,34 @@ +import { Box } from '@mui/material' +import type { StakingTxExitInfo, TransactionData } from '@safe-global/safe-gateway-typescript-sdk' +import FieldsGrid from '@/components/tx/FieldsGrid' +import TokenAmount from '@/components/common/TokenAmount' +import StakingStatus from '@/features/stake/components/StakingStatus' +import { formatDurationFromSeconds } from '@/utils/formatters' + +const StakingTxExitDetails = ({ info }: { info: StakingTxExitInfo; txData?: TransactionData }) => { + const withdrawIn = formatDurationFromSeconds(info.estimatedExitTime + info.estimatedWithdrawalTime, ['days', 'hours']) + return ( + + + + + + + {info.numValidators} Validator{info.numValidators > 1 ? 's' : ''} + + + Up to {withdrawIn} + + + + + + ) +} + +export default StakingTxExitDetails diff --git a/src/features/stake/components/StakingTxExitInfo/index.tsx b/src/features/stake/components/StakingTxExitInfo/index.tsx new file mode 100644 index 0000000000..60fa8ca9e0 --- /dev/null +++ b/src/features/stake/components/StakingTxExitInfo/index.tsx @@ -0,0 +1,17 @@ +import type { StakingTxInfo } from '@safe-global/safe-gateway-typescript-sdk' +import TokenAmount from '@/components/common/TokenAmount' + +const StakingTxExitInfo = ({ info }: { info: StakingTxInfo }) => { + return ( + <> + + + ) +} + +export default StakingTxExitInfo diff --git a/src/features/stake/components/StakingWidget/index.tsx b/src/features/stake/components/StakingWidget/index.tsx new file mode 100644 index 0000000000..cc76afbed4 --- /dev/null +++ b/src/features/stake/components/StakingWidget/index.tsx @@ -0,0 +1,35 @@ +import { useMemo } from 'react' +import { useDarkMode } from '@/hooks/useDarkMode' +import AppFrame from '@/components/safe-apps/AppFrame' +import { getEmptySafeApp } from '@/components/safe-apps/utils' + +const widgetAppData = { + url: 'https://safe.widget.testnet.kiln.fi/earn', + name: 'Stake', + iconUrl: '/images/common/stake.svg', + chainIds: ['17000', '11155111', '1', '42161', '137', '56', '8453', '10'], +} + +const StakingWidget = () => { + const isDarkMode = useDarkMode() + + const appData = useMemo( + () => ({ + ...getEmptySafeApp(), + ...widgetAppData, + url: widgetAppData.url + `?theme=${isDarkMode ? 'dark' : 'light'}`, + }), + [isDarkMode], + ) + + return ( + + ) +} + +export default StakingWidget diff --git a/src/features/stake/constants.ts b/src/features/stake/constants.ts new file mode 100644 index 0000000000..59b8a35621 --- /dev/null +++ b/src/features/stake/constants.ts @@ -0,0 +1 @@ +export const STAKE_TITLE = 'Stake' diff --git a/src/features/stake/helpers/utils.ts b/src/features/stake/helpers/utils.ts new file mode 100644 index 0000000000..69d5693554 --- /dev/null +++ b/src/features/stake/helpers/utils.ts @@ -0,0 +1,17 @@ +import { id } from 'ethers' +import type { BaseTransaction } from '@safe-global/safe-apps-sdk' + +const WITHDRAW_SIGHASH = id('requestValidatorsExit(bytes)').slice(0, 10) + +export const getStakeTitle = (txs: BaseTransaction[] | undefined) => { + const hashToLabel = { + [WITHDRAW_SIGHASH]: 'Withdraw request', + } + + const stakeTitle = txs + ?.map((tx) => hashToLabel[tx.data.slice(0, 10)]) + .filter(Boolean) + .join(' and ') + + return stakeTitle +} diff --git a/src/features/stake/useStakeConsent.ts b/src/features/stake/useStakeConsent.ts new file mode 100644 index 0000000000..4566126551 --- /dev/null +++ b/src/features/stake/useStakeConsent.ts @@ -0,0 +1,28 @@ +import { localItem } from '@/services/local-storage/local' +import { useCallback, useEffect, useState } from 'react' + +const STAKE_CONSENT_STORAGE_KEY = 'stakeDisclaimerAcceptedV1' +const stakeConsentStorage = localItem(STAKE_CONSENT_STORAGE_KEY) + +const useStakeConsent = (): { + isConsentAccepted: boolean | undefined + onAccept: () => void +} => { + const [isConsentAccepted, setIsConsentAccepted] = useState() + + const onAccept = useCallback(() => { + setIsConsentAccepted(true) + stakeConsentStorage.set(true) + }, [setIsConsentAccepted]) + + useEffect(() => { + setIsConsentAccepted(stakeConsentStorage.get() || false) + }, [setIsConsentAccepted]) + + return { + isConsentAccepted, + onAccept, + } +} + +export default useStakeConsent diff --git a/src/features/swap/components/StatusLabel/index.tsx b/src/features/swap/components/StatusLabel/index.tsx index e8a186714b..67910f4f2a 100644 --- a/src/features/swap/components/StatusLabel/index.tsx +++ b/src/features/swap/components/StatusLabel/index.tsx @@ -1,4 +1,4 @@ -import { Chip as MuiChip, SvgIcon } from '@mui/material' +import { SvgIcon } from '@mui/material' import type { OrderStatuses } from '@safe-global/safe-gateway-typescript-sdk' import type { ReactElement } from 'react' import CheckIcon from '@/public/images/common/circle-check.svg' @@ -6,6 +6,7 @@ import ClockIcon from '@/public/images/common/clock.svg' import BlockIcon from '@/public/images/common/block.svg' import SignatureIcon from '@/public/images/common/document_signature.svg' import CircleIPartialFillcon from '@/public/images/common/circle-partial-fill.svg' +import TxStatusChip, { type TxStatusChipProps } from '@/components/transactions/TxStatusChip' type CustomOrderStatuses = OrderStatuses | 'partiallyFilled' type Props = { @@ -14,72 +15,51 @@ type Props = { type StatusProps = { label: string - color: string - backgroundColor: string - iconColor: string - icon: any + color: TxStatusChipProps['color'] + icon: React.ComponentType } const statusMap: Record = { presignaturePending: { label: 'Execution needed', - color: 'warning.main', - backgroundColor: 'warning.background', - iconColor: 'warning.main', + color: 'warning', icon: SignatureIcon, }, fulfilled: { label: 'Filled', - color: 'success.dark', - backgroundColor: 'secondary.background', - iconColor: 'success.dark', + color: 'success', icon: CheckIcon, }, open: { label: 'Open', - color: 'warning.main', - backgroundColor: 'warning.background', - iconColor: 'warning.main', + color: 'warning', icon: ClockIcon, }, cancelled: { label: 'Cancelled', - color: 'error.main', - backgroundColor: 'error.background', - iconColor: 'error.main', + color: 'error', icon: BlockIcon, }, expired: { label: 'Expired', - color: 'primary.light', - backgroundColor: 'background.main', - iconColor: 'border.main', + color: 'primary', icon: ClockIcon, }, partiallyFilled: { label: 'Partially filled', - color: 'success.dark', - backgroundColor: 'secondary.background', - iconColor: 'success.dark', + color: 'success', icon: CircleIPartialFillcon, }, } export const StatusLabel = (props: Props): ReactElement => { const { status } = props - const { label, color, icon, iconColor, backgroundColor } = statusMap[status] + const { label, color, icon } = statusMap[status] return ( - } - /> + + + {label} + ) } diff --git a/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx b/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx index 4cc8020a89..8fcf7d3db9 100644 --- a/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx +++ b/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx @@ -1,11 +1,15 @@ -import type { OrderConfirmationView } from '@safe-global/safe-gateway-typescript-sdk' +import type { SwapOrderConfirmationView, TwapOrderConfirmationView } from '@safe-global/safe-gateway-typescript-sdk' import { getOrderFeeBps } from '@/features/swap/helpers/utils' import { DataRow } from '@/components/common/Table/DataRow' import { HelpCenterArticle } from '@/config/constants' import { HelpIconTooltip } from '@/features/swap/components/HelpIconTooltip' import MUILink from '@mui/material/Link' -export const OrderFeeConfirmationView = ({ order }: { order: Pick }) => { +export const OrderFeeConfirmationView = ({ + order, +}: { + order: Pick +}) => { const bps = getOrderFeeBps(order) if (Number(bps) === 0) { diff --git a/src/features/swap/components/SwapOrderConfirmationView/index.tsx b/src/features/swap/components/SwapOrderConfirmationView/index.tsx index 8c34512b69..2c986286b5 100644 --- a/src/features/swap/components/SwapOrderConfirmationView/index.tsx +++ b/src/features/swap/components/SwapOrderConfirmationView/index.tsx @@ -6,9 +6,8 @@ import { DataTable } from '@/components/common/Table/DataTable' import { compareAsc } from 'date-fns' import { Alert, Typography } from '@mui/material' import { formatAmount } from '@/utils/formatNumber' -import { formatVisualAmount } from '@/utils/formatters' import { getLimitPrice, getOrderClass, getSlippageInPercent } from '@/features/swap/helpers/utils' -import type { OrderConfirmationView } from '@safe-global/safe-gateway-typescript-sdk' +import type { AnySwapOrderConfirmationView } from '@safe-global/safe-gateway-typescript-sdk' import { StartTimeValue } from '@safe-global/safe-gateway-typescript-sdk' import { ConfirmationViewTypes } from '@safe-global/safe-gateway-typescript-sdk' import SwapTokens from '@/features/swap/components/SwapTokens' @@ -20,13 +19,15 @@ import { PartDuration } from '@/features/swap/components/SwapOrder/rows/PartDura import { PartSellAmount } from '@/features/swap/components/SwapOrder/rows/PartSellAmount' import { PartBuyAmount } from '@/features/swap/components/SwapOrder/rows/PartBuyAmount' import { OrderFeeConfirmationView } from '@/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView' +import { isSettingTwapFallbackHandler } from '@/features/swap/helpers/utils' +import { TwapFallbackHandlerWarning } from '@/features/swap/components/TwapFallbackHandlerWarning' type SwapOrderProps = { - order: OrderConfirmationView + order: AnySwapOrderConfirmationView settlementContract: string } -export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrderProps): ReactElement => { +export const SwapOrderConfirmation = ({ order, settlementContract }: SwapOrderProps): ReactElement => { const { owner, kind, validUntil, sellToken, buyToken, sellAmount, buyAmount, explorerUrl, receiver } = order const isTwapOrder = order.type === ConfirmationViewTypes.COW_SWAP_TWAP_ORDER @@ -38,25 +39,26 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd const slippage = getSlippageInPercent(order) const isSellOrder = kind === 'sell' + const isChangingFallbackHandler = isSettingTwapFallbackHandler(order) return ( -
+ <> + {isChangingFallbackHandler && } +
, @@ -143,8 +145,8 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd />
)} -
+ ) } -export default SwapOrderConfirmationView +export default SwapOrderConfirmation diff --git a/src/features/swap/components/SwapTokens/index.stories.tsx b/src/features/swap/components/SwapTokens/index.stories.tsx index 7f5e9b29db..06d9b077a4 100644 --- a/src/features/swap/components/SwapTokens/index.stories.tsx +++ b/src/features/swap/components/SwapTokens/index.stories.tsx @@ -28,16 +28,22 @@ export const Default: Story = { first: { value: '100', label: 'Sell', - logoUri: - 'https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0x0625aFB445C3B6B7B929342a04A22599fd5dBB59.png', - tokenSymbol: 'COW', + tokenInfo: { + decimals: 18, + logoUri: + 'https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0x0625aFB445C3B6B7B929342a04A22599fd5dBB59.png', + symbol: 'COW', + }, }, second: { value: '86', label: 'For at least', - logoUri: - 'https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984.png', - tokenSymbol: 'UNI', + tokenInfo: { + decimals: 18, + logoUri: + 'https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984.png', + symbol: 'UNI', + }, }, }, } diff --git a/src/features/swap/components/SwapTokens/index.tsx b/src/features/swap/components/SwapTokens/index.tsx index d42d876498..15d02327a7 100644 --- a/src/features/swap/components/SwapTokens/index.tsx +++ b/src/features/swap/components/SwapTokens/index.tsx @@ -1,53 +1,7 @@ -import type { ReactElement } from 'react' -import TokenIcon from '@/components/common/TokenIcon' -import { SvgIcon, Typography } from '@mui/material' -import Stack from '@mui/material/Stack' -import css from './styles.module.css' -import EastRoundedIcon from '@mui/icons-material/EastRounded' +import ConfirmationOrderHeader, { type InfoBlock } from '@/components/tx/ConfirmationOrder/ConfirmationOrderHeader' -const SwapToken = ({ - value, - tokenSymbol, - label, - logoUri, -}: { - value: string - tokenSymbol: string - label?: string - logoUri?: string -}): ReactElement => { - return ( -
- - - - {label} - - - {value} {tokenSymbol} - - -
- ) -} - -type SwapToken = { - value: string - tokenSymbol: string - label: string - logoUri: string -} - -const SwapTokens = ({ first, second }: { first: SwapToken; second: SwapToken }) => { - return ( -
- -
- -
- -
- ) +const SwapTokens = ({ first, second }: { first: InfoBlock; second: InfoBlock }) => { + return } export default SwapTokens diff --git a/src/features/swap/components/SwapTokens/styles.module.css b/src/features/swap/components/SwapTokens/styles.module.css deleted file mode 100644 index 5dd2ad6751..0000000000 --- a/src/features/swap/components/SwapTokens/styles.module.css +++ /dev/null @@ -1,32 +0,0 @@ -.container { - display: flex; - gap: var(--space-1); - position: relative; -} - -.swapToken { - display: inline-flex; - align-items: center; - flex-wrap: wrap; - gap: var(--space-2); - color: var(--color-text-primary); - background: var(--color-background-main); - padding: var(--space-2) var(--space-3); - border-radius: 6px; - flex: 1; -} - -.icon { - display: flex; - align-items: center; - justify-content: center; - background: var(--color-background-paper); - border-radius: 50%; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - width: 40px; - height: 40px; - padding: var(--space-1); -} diff --git a/src/features/swap/components/TwapFallbackHandlerWarning/index.tsx b/src/features/swap/components/TwapFallbackHandlerWarning/index.tsx index 470bb515b4..7c9234a3c7 100644 --- a/src/features/swap/components/TwapFallbackHandlerWarning/index.tsx +++ b/src/features/swap/components/TwapFallbackHandlerWarning/index.tsx @@ -1,15 +1,17 @@ -import { Alert, Box, SvgIcon } from '@mui/material' +import { Alert, SvgIcon } from '@mui/material' import InfoOutlinedIcon from '@/public/images/notifications/info.svg' export const TwapFallbackHandlerWarning = () => { return ( - - }> - Enable TWAPs and submit order. - {` `} - To enable TWAP orders you need to set a custom fallback handler. This software is developed by CoW Swap and Safe - will not be responsible for any possible issues with it. - - + } + sx={{ mb: 1 }} + > + Enable TWAPs and submit order. + {` `} + To enable TWAP orders you need to set a custom fallback handler. This software is developed by CoW Swap and Safe + will not be responsible for any possible issues with it. + ) } diff --git a/src/features/swap/index.tsx b/src/features/swap/index.tsx index 10f5a34fa8..b2ed794996 100644 --- a/src/features/swap/index.tsx +++ b/src/features/swap/index.tsx @@ -3,7 +3,7 @@ import { type CowSwapWidgetParams, TradeType } from '@cowprotocol/widget-lib' import type { OnTradeParamsPayload } from '@cowprotocol/events' import { type CowEventListeners, CowEvents } from '@cowprotocol/events' import { type MutableRefObject, useEffect, useMemo, useRef, useState } from 'react' -import { Box, Container, Grid, useTheme } from '@mui/material' +import { Box, useTheme } from '@mui/material' import { SafeAppAccessPolicyTypes, type SafeAppData, @@ -20,14 +20,13 @@ import useWallet from '@/hooks/wallets/useWallet' import BlockedAddress from '@/components/common/BlockedAddress' import useSwapConsent from './useSwapConsent' import Disclaimer from '@/components/common/Disclaimer' -import LegalDisclaimerContent from '@/features/swap/components/LegalDisclaimer' +import WidgetDisclaimer from '@/components/common/WidgetDisclaimer' import { selectSwapParams, setSwapParams, type SwapState } from './store/swapParamsSlice' import { setSwapOrder } from '@/store/swapOrderSlice' import useChainId from '@/hooks/useChainId' import { type BaseTransaction } from '@safe-global/safe-apps-sdk' import { APPROVAL_SIGNATURE_HASH } from '@/components/tx/ApprovalEditor/utils/approvals' import { id } from 'ethers' -import useIsSwapFeatureEnabled from './hooks/useIsSwapFeatureEnabled' import { LIMIT_ORDER_TITLE, SWAP_TITLE, @@ -81,7 +80,6 @@ const SwapWidget = ({ sell }: Params) => { const darkMode = useDarkMode() const chainId = useChainId() const dispatch = useAppDispatch() - const isSwapFeatureEnabled = useIsSwapFeatureEnabled() const swapParams = useAppSelector(selectSwapParams) const { safeAddress, safeLoading } = useSafeInfo() const [recipientAddress, setRecipientAddress] = useState('') @@ -300,16 +298,13 @@ const SwapWidget = ({ sell }: Params) => { } if (!isConsentAccepted) { - return } onAccept={onAccept} buttonText="Continue" /> - } - - if (!isSwapFeatureEnabled) { return ( - - -
Swaps are not supported on this chain
-
-
+ } + onAccept={onAccept} + buttonText="Continue" + /> ) } diff --git a/src/hooks/__tests__/useDecodeTx.test.ts b/src/hooks/__tests__/useDecodeTx.test.ts index f8c7fd7912..99889dc11f 100644 --- a/src/hooks/__tests__/useDecodeTx.test.ts +++ b/src/hooks/__tests__/useDecodeTx.test.ts @@ -83,13 +83,14 @@ describe('useDecodeTx', () => { const safeTx = createMockSafeTransaction({ data: '0x1234567890abcdef', // non-empty data to: faker.finance.ethereumAddress(), + value: '1000000', }) const { result } = renderHook(() => useDecodeTx(safeTx)) await waitFor(async () => { expect(getConfirmationView).toHaveBeenCalledTimes(1) - expect(getConfirmationView).toHaveBeenCalledWith('5', '0x789', '0x1234567890abcdef', safeTx.data.to) + expect(getConfirmationView).toHaveBeenCalledWith('5', '0x789', '0x1234567890abcdef', safeTx.data.to, '1000000') }) }) diff --git a/src/hooks/useChains.ts b/src/hooks/useChains.ts index 669c99865a..c04e848e01 100644 --- a/src/hooks/useChains.ts +++ b/src/hooks/useChains.ts @@ -37,7 +37,7 @@ export const useCurrentChain = (): ChainInfo | undefined => { * @param feature name of the feature to check for * @returns `true`, if the feature is enabled on the current chain. Otherwise `false` */ -export const useHasFeature = (feature: FEATURES): boolean => { +export const useHasFeature = (feature: FEATURES): boolean | undefined => { const currentChain = useCurrentChain() - return !!currentChain && hasFeature(currentChain, feature) + return currentChain ? hasFeature(currentChain, feature) : undefined } diff --git a/src/hooks/useDecodeTx.ts b/src/hooks/useDecodeTx.ts index 72d9b57040..25d7c5cc07 100644 --- a/src/hooks/useDecodeTx.ts +++ b/src/hooks/useDecodeTx.ts @@ -1,10 +1,5 @@ import { type SafeTransaction } from '@safe-global/safe-core-sdk-types' -import { - getConfirmationView, - type BaselineConfirmationView, - type OrderConfirmationView, - type DecodedDataResponse, -} from '@safe-global/safe-gateway-typescript-sdk' +import { getConfirmationView, type AnyConfirmationView } from '@safe-global/safe-gateway-typescript-sdk' import { getNativeTransferData } from '@/services/tx/tokenTransferParams' import { isEmptyHexData } from '@/utils/hex' import type { AsyncResult } from './useAsync' @@ -12,30 +7,27 @@ import useAsync from './useAsync' import useChainId from './useChainId' import useSafeAddress from '@/hooks/useSafeAddress' -const useDecodeTx = ( - tx?: SafeTransaction, -): AsyncResult => { +const useDecodeTx = (tx?: SafeTransaction): AsyncResult => { const chainId = useChainId() const safeAddress = useSafeAddress() - const encodedData = tx?.data.data - const isEmptyData = !!encodedData && isEmptyHexData(encodedData) - const isRejection = isEmptyData && tx?.data.value === '0' + const { to, value, data } = tx?.data || {} - const [data, error, loading] = useAsync< - DecodedDataResponse | BaselineConfirmationView | OrderConfirmationView | undefined - >( + return useAsync( () => { - if (!encodedData || isEmptyData) { - const nativeTransfer = isEmptyData && !isRejection ? getNativeTransferData(tx?.data) : undefined + if (to === undefined || value === undefined) return + + const isEmptyData = !!data && isEmptyHexData(data) + if (!data || isEmptyData) { + const isRejection = isEmptyData && value === '0' + const nativeTransfer = isEmptyData && !isRejection ? getNativeTransferData({ to, value }) : undefined return Promise.resolve(nativeTransfer) } - return getConfirmationView(chainId, safeAddress, encodedData, tx.data.to) + + return getConfirmationView(chainId, safeAddress, data, to, value) }, - [chainId, encodedData, isEmptyData, tx?.data, isRejection, safeAddress], + [chainId, safeAddress, to, value, data], false, ) - - return [data, error, loading] } export default useDecodeTx diff --git a/src/hooks/useTransactionType.tsx b/src/hooks/useTransactionType.tsx index 8b891ce61a..7b33dedd08 100644 --- a/src/hooks/useTransactionType.tsx +++ b/src/hooks/useTransactionType.tsx @@ -88,6 +88,18 @@ export const getTransactionType = (tx: TransactionSummary, addressBook: AddressB text: TWAP_ORDER_TITLE, } } + case TransactionInfoType.NATIVE_STAKING_DEPOSIT: { + return { + icon: '/images/common/stake.svg', + text: 'Stake', + } + } + case TransactionInfoType.NATIVE_STAKING_VALIDATORS_EXIT: { + return { + icon: '/images/common/stake.svg', + text: 'Withdraw request', + } + } case TransactionInfoType.CUSTOM: { if (isMultiSendTxInfo(tx.txInfo) && !tx.safeAppInfo) { return { @@ -110,6 +122,12 @@ export const getTransactionType = (tx: TransactionSummary, addressBook: AddressB } } + return { + icon: toAddress?.logoUri || '/images/transactions/custom.svg', + text: addressBookName || toAddress?.name || 'Contract interaction', + } + } + default: { if (tx.safeAppInfo) { return { icon: tx.safeAppInfo.logoUri, @@ -117,12 +135,6 @@ export const getTransactionType = (tx: TransactionSummary, addressBook: AddressB } } - return { - icon: toAddress?.logoUri || '/images/transactions/custom.svg', - text: addressBookName || toAddress?.name || 'Contract interaction', - } - } - default: { return { icon: '/images/transactions/custom.svg', text: addressBookName || 'Contract interaction', diff --git a/src/pages/stake.tsx b/src/pages/stake.tsx new file mode 100644 index 0000000000..2b629a2676 --- /dev/null +++ b/src/pages/stake.tsx @@ -0,0 +1,32 @@ +import type { NextPage } from 'next' +import Head from 'next/head' +import dynamic from 'next/dynamic' +import { Typography } from '@mui/material' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' + +const LazyStakePage = dynamic(() => import('@/features/stake/components/StakePage'), { ssr: false }) + +const StakePage: NextPage = () => { + const isFeatureEnabled = useHasFeature(FEATURES.STAKING) + + return ( + <> + + {'Safe{Wallet} – Stake'} + + + {isFeatureEnabled === true ? ( + + ) : isFeatureEnabled === false ? ( +
+ + Staking is not available on this network. + +
+ ) : null} + + ) +} + +export default StakePage diff --git a/src/pages/swap.tsx b/src/pages/swap.tsx index 35c3cf2aba..0a18c061b8 100644 --- a/src/pages/swap.tsx +++ b/src/pages/swap.tsx @@ -1,10 +1,10 @@ import type { NextPage } from 'next' import Head from 'next/head' import { useRouter } from 'next/router' -import { GeoblockingContext } from '@/components/common/GeoblockingProvider' -import { useContext } from 'react' -import { AppRoutes } from '@/config/routes' import dynamic from 'next/dynamic' +import { Typography } from '@mui/material' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' // Cow Swap expects native token addresses to be in the format '0xeeee...eeee' const adjustEthAddress = (address: string) => { @@ -16,14 +16,11 @@ const adjustEthAddress = (address: string) => { } const SwapWidgetNoSSR = dynamic(() => import('@/features/swap'), { ssr: false }) -const Swap: NextPage = () => { + +const SwapPage: NextPage = () => { const router = useRouter() - const isBlockedCountry = useContext(GeoblockingContext) const { token, amount } = router.query - - if (isBlockedCountry) { - router.replace(AppRoutes['403']) - } + const isFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) let sell = undefined if (token && amount) { @@ -39,11 +36,17 @@ const Swap: NextPage = () => { {'Safe{Wallet} – Swap'} -
- +
+ {isFeatureEnabled === true ? ( + + ) : isFeatureEnabled === false ? ( + + Swaps are not supported on this network. + + ) : null}
) } -export default Swap +export default SwapPage diff --git a/src/services/tx/extractTxInfo.ts b/src/services/tx/extractTxInfo.ts index 6fc690beb4..ece76f119f 100644 --- a/src/services/tx/extractTxInfo.ts +++ b/src/services/tx/extractTxInfo.ts @@ -61,6 +61,9 @@ const extractTxInfo = ( return txDetails.txData?.value ?? '0' case 'SwapOrder': return txDetails.txData?.value ?? '0' + case 'NativeStakingDeposit': + case 'NativeStakingValidatorsExit': + return txDetails.txData?.value ?? '0' case 'Custom': return txDetails.txInfo.value case 'Creation': @@ -87,6 +90,13 @@ const extractTxInfo = ( throw new Error('Order tx data does not have a `to` field') } return orderTo + case 'NativeStakingDeposit': + case 'NativeStakingValidatorsExit': + const stakingTo = txDetails.txData?.to.value + if (!stakingTo) { + throw new Error('Staking tx data does not have a `to` field') + } + return stakingTo case 'Custom': return txDetails.txInfo.to.value case 'Creation': diff --git a/src/services/tx/tokenTransferParams.ts b/src/services/tx/tokenTransferParams.ts index 00fff7e8f2..e64ee02ba8 100644 --- a/src/services/tx/tokenTransferParams.ts +++ b/src/services/tx/tokenTransferParams.ts @@ -1,5 +1,5 @@ import type { MetaTransactionData } from '@safe-global/safe-core-sdk-types' -import type { DecodedDataResponse } from '@safe-global/safe-gateway-typescript-sdk' +import { ConfirmationViewTypes, type BaselineConfirmationView } from '@safe-global/safe-gateway-typescript-sdk' import { safeParseUnits } from '@/utils/formatters' import { Interface } from 'ethers' import { sameAddress } from '@/utils/addresses' @@ -64,19 +64,23 @@ export const createNftTransferParams = ( } } -export const getNativeTransferData = (data: MetaTransactionData): DecodedDataResponse => { +export const getNativeTransferData = ({ + to, + value, +}: Pick): BaselineConfirmationView => { return { + type: ConfirmationViewTypes.GENERIC, method: '', parameters: [ { name: 'to', type: 'address', - value: data.to, + value: to, }, { name: 'value', type: 'uint256', - value: data.value, + value, }, ], } diff --git a/src/styles/globals.css b/src/styles/globals.css index 3b324c2692..d9574f20cd 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -14,10 +14,6 @@ main { width: 100%; } -main.swapWrapper { - height: calc(100vh - 52px); -} - a { color: inherit; text-decoration: none; diff --git a/src/utils/__tests__/transactions.test.ts b/src/utils/__tests__/transactions.test.ts index 52c47123ac..2fa9ae7559 100644 --- a/src/utils/__tests__/transactions.test.ts +++ b/src/utils/__tests__/transactions.test.ts @@ -82,6 +82,15 @@ describe('transactions', () => { expect(getTxOrigin(app)).toBe('{"url":"https://test.com","name":"Test name"}') }) + it('should return a stringified object with the app name and url with a query param', () => { + const app = { + url: 'https://test.com/hello?world=1', + name: 'Test name', + } as SafeAppData + + expect(getTxOrigin(app)).toBe('{"url":"https://test.com/hello","name":"Test name"}') + }) + it('should limit the origin to 200 characters with preference of the URL', () => { const app = { url: 'https://test.com/' + 'a'.repeat(160), diff --git a/src/utils/chains.ts b/src/utils/chains.ts index 10e8c721d1..0e5179df62 100644 --- a/src/utils/chains.ts +++ b/src/utils/chains.ts @@ -34,13 +34,13 @@ export enum FEATURES { RELAY_NATIVE_SWAPS = 'RELAY_NATIVE_SWAPS', ZODIAC_ROLES = 'ZODIAC_ROLES', SAFE_141 = 'SAFE_141', - STAKE_TEASER = 'STAKE_TEASER', + STAKING = 'STAKING', } export const FeatureRoutes = { [AppRoutes.apps.index]: FEATURES.SAFE_APPS, [AppRoutes.swap]: FEATURES.NATIVE_SWAPS, - [AppRoutes.stake]: FEATURES.STAKE_TEASER, + [AppRoutes.stake]: FEATURES.STAKING, [AppRoutes.balances.nfts]: FEATURES.ERC721, [AppRoutes.settings.notifications]: FEATURES.PUSH_NOTIFICATIONS, } diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts index 253549f8b5..60c5fbea41 100644 --- a/src/utils/formatters.ts +++ b/src/utils/formatters.ts @@ -1,6 +1,7 @@ import type { BigNumberish } from 'ethers' import { formatUnits, parseUnits } from 'ethers' import { formatAmount, formatAmountPrecise } from './formatNumber' +import { formatDuration, intervalToDuration } from 'date-fns' const GWEI = 'gwei' @@ -94,3 +95,11 @@ export const formatError = (error: Error & { reason?: string }): string => { if (!reason.endsWith('.')) reason += '.' return ` ${capitalize(reason)}` } + +export const formatDurationFromSeconds = ( + seconds: number, + format: Array<'years' | 'months' | 'days' | 'hours' | 'minutes' | 'seconds'> = ['hours', 'minutes'], +) => { + const duration = intervalToDuration({ start: 0, end: seconds * 1000 }) + return formatDuration(duration, { format }) +} diff --git a/src/utils/transaction-guards.ts b/src/utils/transaction-guards.ts index a92fe19a12..57e8c5e0ef 100644 --- a/src/utils/transaction-guards.ts +++ b/src/utils/transaction-guards.ts @@ -6,7 +6,6 @@ import type { Creation, Custom, DateLabel, - DecodedDataResponse, DetailedExecutionInfo, Erc20Transfer, Erc721Transfer, @@ -18,8 +17,10 @@ import type { MultisigExecutionDetails, MultisigExecutionInfo, NativeCoinTransfer, + NativeStakingDepositConfirmationView, Order, - OrderConfirmationView, + AnyConfirmationView, + AnySwapOrderConfirmationView, SafeInfo, SettingsChange, SwapOrder, @@ -32,6 +33,9 @@ import type { TransferInfo, TwapOrder, TwapOrderConfirmationView, + AnyStakingConfirmationView, + StakingTxExitInfo, + StakingTxDepositInfo, } from '@safe-global/safe-gateway-typescript-sdk' import { ConfirmationViewTypes, @@ -48,6 +52,7 @@ import { sameAddress } from '@/utils/addresses' import type { NamedAddress } from '@/components/new-safe/create/types' import type { RecoveryQueueItem } from '@/features/recovery/services/recovery-state' import { ethers } from 'ethers' +import type { NativeStakingValidatorsExitConfirmationView } from '@safe-global/safe-gateway-typescript-sdk/dist/types/decoded-data' export const isTxQueued = (value: TransactionStatus): boolean => { return [TransactionStatus.AWAITING_CONFIRMATIONS, TransactionStatus.AWAITING_EXECUTION].includes(value) @@ -119,14 +124,17 @@ export const isTwapOrderTxInfo = (value: TransactionInfo): value is TwapOrder => return value.type === TransactionInfoType.TWAP_ORDER } -export const isConfirmationViewOrder = ( - decodedData: DecodedDataResponse | BaselineConfirmationView | OrderConfirmationView | undefined, -): decodedData is OrderConfirmationView => { - return isSwapConfirmationViewOrder(decodedData) || isTwapConfirmationViewOrder(decodedData) +export const isStakingTxDepositInfo = (value: TransactionInfo): value is StakingTxDepositInfo => { + return value.type === TransactionInfoType.NATIVE_STAKING_DEPOSIT +} + +export const isStakingTxExitInfo = (value: TransactionInfo): value is StakingTxExitInfo => { + console.log('is staking tx exit info', value, TransactionInfoType.NATIVE_STAKING_VALIDATORS_EXIT) + return value.type === TransactionInfoType.NATIVE_STAKING_VALIDATORS_EXIT } export const isTwapConfirmationViewOrder = ( - decodedData: DecodedDataResponse | BaselineConfirmationView | OrderConfirmationView | undefined, + decodedData: AnyConfirmationView | undefined, ): decodedData is TwapOrderConfirmationView => { if (decodedData && 'type' in decodedData) { return decodedData.type === ConfirmationViewTypes.COW_SWAP_TWAP_ORDER @@ -136,7 +144,7 @@ export const isTwapConfirmationViewOrder = ( } export const isSwapConfirmationViewOrder = ( - decodedData: DecodedDataResponse | BaselineConfirmationView | OrderConfirmationView | undefined, + decodedData: AnyConfirmationView | undefined, ): decodedData is SwapOrderConfirmationView => { if (decodedData && 'type' in decodedData) { return decodedData.type === ConfirmationViewTypes.COW_SWAP_ORDER @@ -145,6 +153,45 @@ export const isSwapConfirmationViewOrder = ( return false } +export const isAnySwapConfirmationViewOrder = ( + decodedData: AnyConfirmationView | undefined, +): decodedData is AnySwapOrderConfirmationView => { + return isSwapConfirmationViewOrder(decodedData) || isTwapConfirmationViewOrder(decodedData) +} + +export const isStakingDepositConfirmationView = ( + decodedData: AnyConfirmationView | undefined, +): decodedData is NativeStakingDepositConfirmationView => { + if (decodedData && 'type' in decodedData) { + return decodedData?.type === ConfirmationViewTypes.KILN_NATIVE_STAKING_DEPOSIT + } + return false +} + +export const isStakingExitConfirmationView = ( + decodedData: AnyConfirmationView | undefined, +): decodedData is NativeStakingValidatorsExitConfirmationView => { + if (decodedData && 'type' in decodedData) { + return decodedData?.type === ConfirmationViewTypes.KILN_NATIVE_STAKING_VALIDATORS_EXIT + } + return false +} + +export const isAnyStakingConfirmationView = ( + decodedData: AnyConfirmationView | undefined, +): decodedData is AnyStakingConfirmationView => { + return isStakingDepositConfirmationView(decodedData) || isStakingExitConfirmationView(decodedData) +} + +export const isGenericConfirmation = ( + decodedData: AnyConfirmationView | undefined, +): decodedData is BaselineConfirmationView => { + if (decodedData && 'type' in decodedData) { + return decodedData.type === ConfirmationViewTypes.GENERIC + } + return false +} + export const isCancelledSwapOrder = (value: TransactionInfo) => { return isSwapOrderTxInfo(value) && value.status === 'cancelled' } diff --git a/src/utils/transactions.ts b/src/utils/transactions.ts index a3e151e739..304f5555e1 100644 --- a/src/utils/transactions.ts +++ b/src/utils/transactions.ts @@ -34,6 +34,7 @@ import { toBeHex, AbiCoder } from 'ethers' import { type BaseTransaction } from '@safe-global/safe-apps-sdk' import { id } from 'ethers' import { isEmptyHexData } from '@/utils/hex' +import { getOriginPath } from './url' export const makeTxFromDetails = (txDetails: TransactionDetails): Transaction => { const getMissingSigners = ({ @@ -186,7 +187,7 @@ export const getTxOrigin = (app?: Partial): string | undefined => { try { // Must include empty string to avoid including the length of `undefined` const maxUrlLength = MAX_ORIGIN_LENGTH - JSON.stringify({ url: '', name: '' }).length - const trimmedUrl = url.slice(0, maxUrlLength) + const trimmedUrl = getOriginPath(url).slice(0, maxUrlLength) const maxNameLength = Math.max(0, maxUrlLength - trimmedUrl.length) const trimmedName = name.slice(0, maxNameLength) diff --git a/src/utils/url.ts b/src/utils/url.ts index d0322e4748..a9c5eb19fd 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -1,8 +1,8 @@ -const trimTrailingSlash = (url: string): string => { +export const trimTrailingSlash = (url: string): string => { return url.replace(/\/$/, '') } -const isSameUrl = (url1: string, url2: string): boolean => { +export const isSameUrl = (url1: string, url2: string): boolean => { return trimTrailingSlash(url1) === trimTrailingSlash(url2) } export const prefixedAddressRe = /[a-z0-9-]+\:0x[a-f0-9]{40}/i @@ -10,11 +10,11 @@ const invalidProtocolRegex = /^(\W*)(javascript|data|vbscript)/im const ctrlCharactersRegex = /[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim const urlSchemeRegex = /^([^:]+):/gm const relativeFirstCharacters = ['.', '/'] -const isRelativeUrl = (url: string): boolean => { +export const isRelativeUrl = (url: string): boolean => { return relativeFirstCharacters.indexOf(url[0]) > -1 } -const sanitizeUrl = (url: string): string => { +export const sanitizeUrl = (url: string): string => { const sanitizedUrl = url.replace(ctrlCharactersRegex, '').trim() if (isRelativeUrl(sanitizedUrl)) { @@ -34,4 +34,12 @@ const sanitizeUrl = (url: string): string => { return sanitizedUrl } -export { trimTrailingSlash, isSameUrl, sanitizeUrl, isRelativeUrl } +export const getOriginPath = (url: string): string => { + try { + const { origin, pathname } = new URL(url) + return origin + (pathname === '/' ? '' : pathname) + } catch (e) { + console.error('Error parsing URL', url, e) + return url + } +} diff --git a/yarn.lock b/yarn.lock index 2039d231b8..2df338f673 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4183,10 +4183,10 @@ dependencies: semver "^7.6.2" -"@safe-global/safe-gateway-typescript-sdk@3.22.4-beta.1": - version "3.22.4-beta.1" - resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.22.4-beta.1.tgz#ef8d0506d8c747124fae721a9baf99dda71af189" - integrity sha512-adxHiSeUc47MqkW7BM50U5xy6144rDEf0jyftzGXrBkG+nv/oL55SZQ/DdAsxyI1Mns02gzawa3Up+MfA8SKCQ== +"@safe-global/safe-gateway-typescript-sdk@3.22.3-beta.12": + version "3.22.3-beta.12" + resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.22.3-beta.12.tgz#2ebc398d6c8c4f27cc68865dbd7e7603add099ae" + integrity sha512-BBcyYNa8RDN5tQrYHOp3Bdo3CTo7r6ZB0Nzgce5OI7rKjorgCv0TprVTW8w8YVMXa74wC6uT06udXAB0IDRu8A== "@safe-global/safe-gateway-typescript-sdk@^3.5.3": version "3.21.2" @@ -16694,16 +16694,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -16791,14 +16782,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -18482,7 +18466,7 @@ workbox-window@7.0.0: "@types/trusted-types" "^2.0.2" workbox-core "7.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -18500,15 +18484,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"