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"