Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Safenet): Safenet feature flag #4810

Merged
merged 14 commits into from
Jan 27, 2025
Merged
1 change: 1 addition & 0 deletions .github/actions/build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ runs:
NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING }}
NEXT_PUBLIC_SPINDL_SDK_KEY: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SPINDL_SDK_KEY }}
NEXT_PUBLIC_ECOSYSTEM_ID_ADDRESS: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_ECOSYSTEM_ID_ADDRESS }}
NEXT_PUBLIC_HAS_SAFENET_FEATURE: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_HAS_SAFENET_FEATURE }}
NEXT_PUBLIC_SAFENET_API_URL: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SAFENET_API_URL }}
- name: Save Next.js Build Cache & Cypress cache
if: steps.restore-nc.outputs.cache-hit-nc != 'true'
Expand Down
55 changes: 30 additions & 25 deletions apps/web/src/components/safe-apps/AppFrame/useAppCommunicator.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,44 @@
import type { MutableRefObject } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { getAddress } from 'ethers'
import type {
SafeAppData,
ChainInfo as WebCoreChainInfo,
TransactionDetails,
} from '@safe-global/safe-gateway-typescript-sdk'
import { SAFENET_API_URL } from '@/config/constants'
import { useHasSafenetFeature } from '@/features/safenet'
import type { SafePermissionsRequest } from '@/hooks/safe-apps/permissions'
import { useDarkMode } from '@/hooks/useDarkMode'
import { createSafeAppsWeb3Provider } from '@/hooks/wallets/web3'
import { SAFE_APPS_EVENTS, trackSafeAppEvent } from '@/services/analytics'
import { Errors, logError } from '@/services/exceptions'
import AppCommunicator from '@/services/safe-apps/AppCommunicator'
import { useAppSelector } from '@/store'
import { useGetSafenetConfigQuery } from '@/store/safenet'
import { selectRpc } from '@/store/settingsSlice'
import { QueryStatus } from '@reduxjs/toolkit/query'
import { skipToken } from '@reduxjs/toolkit/query/react'
import type {
AddressBookItem,
BaseTransaction,
ChainInfo,
EIP712TypedData,
EnvironmentInfo,
GetBalanceParams,
GetTxBySafeTxHashParams,
RequestId,
RPCPayload,
RequestId,
SafeBalances,
SafeInfoExtended,
SafeSettings,
SendTransactionRequestParams,
SendTransactionsParams,
SignMessageParams,
SignTypedMessageParams,
ChainInfo,
SafeBalances,
SafeInfoExtended,
} from '@safe-global/safe-apps-sdk'
import { Methods, RPC_CALLS } from '@safe-global/safe-apps-sdk'
import type { Permission, PermissionRequest } from '@safe-global/safe-apps-sdk/dist/types/types/permissions'
import type { SafeSettings } from '@safe-global/safe-apps-sdk'
import AppCommunicator from '@/services/safe-apps/AppCommunicator'
import { Errors, logError } from '@/services/exceptions'
import type { SafePermissionsRequest } from '@/hooks/safe-apps/permissions'
import { SAFE_APPS_EVENTS, trackSafeAppEvent } from '@/services/analytics'
import { useAppSelector } from '@/store'
import { selectRpc } from '@/store/settingsSlice'
import { createSafeAppsWeb3Provider } from '@/hooks/wallets/web3'
import { useDarkMode } from '@/hooks/useDarkMode'
import { QueryStatus } from '@reduxjs/toolkit/query'
import { SAFENET_API_URL } from '@/config/constants'
import { useGetSafenetConfigQuery } from '@/store/safenet'
import type {
SafeAppData,
TransactionDetails,
ChainInfo as WebCoreChainInfo,
} from '@safe-global/safe-gateway-typescript-sdk'
import { getAddress } from 'ethers'
import type { MutableRefObject } from 'react'
import { useEffect, useMemo, useState } from 'react'

export enum CommunicatorMessages {
REJECT_TRANSACTION_MESSAGE = 'Transaction was rejected',
Expand Down Expand Up @@ -77,7 +79,10 @@ const useAppCommunicator = (
): AppCommunicator | undefined => {
const [communicator, setCommunicator] = useState<AppCommunicator | undefined>(undefined)
const customRpc = useAppSelector(selectRpc)
const { data: safenetConfig, status: safenetConfigStatus } = useGetSafenetConfigQuery()
const hasSafenetFeature = useHasSafenetFeature()
const { data: safenetConfig, status: safenetConfigStatus } = useGetSafenetConfigQuery(
!hasSafenetFeature ? skipToken : undefined,
)
const shouldUseSafenetRpc =
safenetConfigStatus === QueryStatus.fulfilled &&
chain &&
Expand Down
13 changes: 10 additions & 3 deletions apps/web/src/components/settings/SettingsHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import type { ReactElement } from 'react'

import NavTabs from '@/components/common/NavTabs'
import PageHeader from '@/components/common/PageHeader'
import { generalSettingsNavItems, settingsNavItems } from '@/components/sidebar/SidebarNavigation/config'
import css from '@/components/common/PageHeader/styles.module.css'
import useSafeAddress from '@/hooks/useSafeAddress'
import { generalSettingsNavItems, settingsNavItems } from '@/components/sidebar/SidebarNavigation/config'
import { AppRoutes } from '@/config/routes'
import { useHasSafenetFeature } from '@/features/safenet'
import { useCurrentChain } from '@/hooks/useChains'
import useSafeAddress from '@/hooks/useSafeAddress'
import { isRouteEnabled } from '@/utils/chains'
import madProps from '@/utils/mad-props'

Expand All @@ -16,8 +18,13 @@ export const SettingsHeader = ({
safeAddress: ReturnType<typeof useSafeAddress>
chain: ReturnType<typeof useCurrentChain>
}): ReactElement => {
const hasSafenetFeature = useHasSafenetFeature()

const navItems = safeAddress
? settingsNavItems.filter((route) => isRouteEnabled(route.href, chain))
? settingsNavItems.filter(
(route) =>
isRouteEnabled(route.href, chain) || (hasSafenetFeature && route.href === AppRoutes.settings.safenet),
)
: generalSettingsNavItems

return (
Expand Down
17 changes: 10 additions & 7 deletions apps/web/src/components/sidebar/Sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { useCallback, useState, type ReactElement } from 'react'
import { Box, Divider, Drawer } from '@mui/material'
import ChevronRight from '@mui/icons-material/ChevronRight'
import { Box, Divider, Drawer } from '@mui/material'
import { useCallback, useState, type ReactElement } from 'react'

import ChainIndicatorSafenet from '@/components/common/ChainIndicator/ChainIndicatorSafenet'
import IndexingStatus from '@/components/sidebar/IndexingStatus'
import SidebarFooter from '@/components/sidebar/SidebarFooter'
import SidebarHeader from '@/components/sidebar/SidebarHeader'
import SidebarNavigation from '@/components/sidebar/SidebarNavigation'
import SidebarFooter from '@/components/sidebar/SidebarFooter'
import IndexingStatus from '@/components/sidebar/IndexingStatus'

import css from './styles.module.css'
import { trackEvent, OVERVIEW_EVENTS } from '@/services/analytics'
import ChainIndicator from '@/components/common/ChainIndicator'
import MyAccounts from '@/features/myAccounts'
import useIsSafenetEnabled from '@/hooks/useIsSafenetEnabled'
import { OVERVIEW_EVENTS, trackEvent } from '@/services/analytics'
import css from './styles.module.css'

const Sidebar = (): ReactElement => {
const isSafenetEnabled = useIsSafenetEnabled()
const [isDrawerOpen, setIsDrawerOpen] = useState<boolean>(false)

const onDrawerToggle = useCallback(() => {
Expand All @@ -28,7 +31,7 @@ const Sidebar = (): ReactElement => {
return (
<div data-testid="sidebar-container" className={css.container}>
<div className={css.scroll}>
<ChainIndicatorSafenet />
{isSafenetEnabled ? <ChainIndicatorSafenet /> : <ChainIndicator showLogo={false} />}

{/* Open the safes list */}
<button data-testid="open-safes-icon" className={css.drawerButton} onClick={onDrawerToggle}>
Expand Down
162 changes: 81 additions & 81 deletions apps/web/src/components/sidebar/SidebarHeader/index.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,40 @@
import TokenAmount from '@/components/common/TokenAmount'
import CounterfactualStatusButton from '@/features/counterfactual/CounterfactualStatusButton'
import { type ReactElement } from 'react'
import Typography from '@mui/material/Typography'
import IconButton from '@mui/material/IconButton'
import Skeleton from '@mui/material/Skeleton'
import Tooltip from '@mui/material/Tooltip'
import Typography from '@mui/material/Typography'
import { type ReactElement } from 'react'

import useSafeInfo from '@/hooks/useSafeInfo'
import SafeIcon from '@/components/common/SafeIcon'
import NewTxButton from '@/components/sidebar/NewTxButton'
import { useAppSelector } from '@/store'

import css from './styles.module.css'
import QrIconBold from '@/public/images/sidebar/qr-bold.svg'
import useSafeInfo from '@/hooks/useSafeInfo'
import CopyIconBold from '@/public/images/sidebar/copy-bold.svg'
import LinkIconBold from '@/public/images/sidebar/link-bold.svg'
import QrIconBold from '@/public/images/sidebar/qr-bold.svg'
import { useAppSelector } from '@/store'
import css from './styles.module.css'

import { selectSettings } from '@/store/settingsSlice'
import { useCurrentChain } from '@/hooks/useChains'
import { getBlockExplorerLink } from '@/utils/chains'
import CopyTooltip from '@/components/common/CopyTooltip'
import EthHashInfo from '@/components/common/EthHashInfo'
import QrCodeButton from '../QrCodeButton'
import Track from '@/components/common/Track'
import { OVERVIEW_EVENTS } from '@/services/analytics/events/overview'
import { SvgIcon } from '@mui/material'
import { useVisibleBalances } from '@/hooks/useVisibleBalances'
import EnvHintButton from '@/components/settings/EnvironmentVariables/EnvHintButton'
import useSafeAddress from '@/hooks/useSafeAddress'
import ExplorerButton from '@/components/common/ExplorerButton'
import CopyTooltip from '@/components/common/CopyTooltip'
import FiatValue from '@/components/common/FiatValue'
import { useAddressResolver } from '@/hooks/useAddressResolver'
import GradientBoxSafenet from '@/components/common/GradientBoxSafenet'
import Track from '@/components/common/Track'
import EnvHintButton from '@/components/settings/EnvironmentVariables/EnvHintButton'
import { useAddressResolver } from '@/hooks/useAddressResolver'
import { useCurrentChain } from '@/hooks/useChains'
import useIsSafenetEnabled from '@/hooks/useIsSafenetEnabled'
import useSafeAddress from '@/hooks/useSafeAddress'
import { useVisibleBalances } from '@/hooks/useVisibleBalances'
import { OVERVIEW_EVENTS } from '@/services/analytics/events/overview'
import { selectSettings } from '@/store/settingsSlice'
import { getBlockExplorerLink } from '@/utils/chains'
import { SvgIcon } from '@mui/material'
import QrCodeButton from '../QrCodeButton'

const SafeHeader = (): ReactElement => {
const isSafenetEnabled = useIsSafenetEnabled()
const { balances } = useVisibleBalances()
const safeAddress = useSafeAddress()
const { safe } = useSafeInfo()
Expand All @@ -46,80 +47,79 @@ const SafeHeader = (): ReactElement => {

const blockExplorerLink = chain ? getBlockExplorerLink(chain, safeAddress) : undefined

return (
<GradientBoxSafenet variant="bottom">
<div className={css.container}>
<div className={css.info}>
<div data-testid="safe-header-info" className={css.safe}>
<div data-testid="safe-icon">
{safeAddress ? (
<SafeIcon address={safeAddress} threshold={threshold} owners={owners?.length} />
) : (
<Skeleton variant="circular" width={40} height={40} />
)}
</div>
const header = (
<div className={css.container}>
<div className={css.info}>
<div data-testid="safe-header-info" className={css.safe}>
<div data-testid="safe-icon">
{safeAddress ? (
<SafeIcon address={safeAddress} threshold={threshold} owners={owners?.length} />
) : (
<Skeleton variant="circular" width={40} height={40} />
)}
</div>

<div className={css.address}>
{safeAddress ? (
<EthHashInfo address={safeAddress} shortAddress showAvatar={false} name={ens} />
) : (
<Typography variant="body2">
<Skeleton variant="text" width={86} />
<Skeleton variant="text" width={120} />
</Typography>
)}
<div className={css.address}>
{safeAddress ? (
<EthHashInfo address={safeAddress} shortAddress showAvatar={false} name={ens} />
) : (
<Typography variant="body2">
<Skeleton variant="text" width={86} />
<Skeleton variant="text" width={120} />
</Typography>
)}

<Typography data-testid="currency-section" variant="body2" fontWeight={700}>
{safe.deployed ? (
balances.fiatTotal ? (
<FiatValue value={balances.fiatTotal} />
) : (
<Skeleton variant="text" width={60} />
)
<Typography data-testid="currency-section" variant="body2" fontWeight={700}>
{safe.deployed ? (
balances.fiatTotal ? (
<FiatValue value={balances.fiatTotal} />
) : (
<TokenAmount
value={balances.items[0]?.balance}
decimals={balances.items[0]?.tokenInfo.decimals}
tokenSymbol={balances.items[0]?.tokenInfo.symbol}
/>
)}
</Typography>
</div>
<Skeleton variant="text" width={60} />
)
) : (
<TokenAmount
value={balances.items[0]?.balance}
decimals={balances.items[0]?.tokenInfo.decimals}
tokenSymbol={balances.items[0]?.tokenInfo.symbol}
/>
)}
</Typography>
</div>
</div>

<div className={css.iconButtons}>
<Track {...OVERVIEW_EVENTS.SHOW_QR} label="sidebar">
<QrCodeButton>
<Tooltip title="Open QR code" placement="top">
<IconButton className={css.iconButton}>
<SvgIcon component={QrIconBold} inheritViewBox color="primary" fontSize="small" />
</IconButton>
</Tooltip>
</QrCodeButton>
</Track>

<Track {...OVERVIEW_EVENTS.COPY_ADDRESS}>
<CopyTooltip text={addressCopyText}>
<IconButton data-testid="copy-address-btn" className={css.iconButton}>
<SvgIcon component={CopyIconBold} inheritViewBox color="primary" fontSize="small" />
<div className={css.iconButtons}>
<Track {...OVERVIEW_EVENTS.SHOW_QR} label="sidebar">
<QrCodeButton>
<Tooltip title="Open QR code" placement="top">
<IconButton className={css.iconButton}>
<SvgIcon component={QrIconBold} inheritViewBox color="primary" fontSize="small" />
</IconButton>
</CopyTooltip>
</Track>
</Tooltip>
</QrCodeButton>
</Track>

<Track {...OVERVIEW_EVENTS.OPEN_EXPLORER}>
<ExplorerButton {...blockExplorerLink} className={css.iconButton} icon={LinkIconBold} />
</Track>
<Track {...OVERVIEW_EVENTS.COPY_ADDRESS}>
<CopyTooltip text={addressCopyText}>
<IconButton data-testid="copy-address-btn" className={css.iconButton}>
<SvgIcon component={CopyIconBold} inheritViewBox color="primary" fontSize="small" />
</IconButton>
</CopyTooltip>
</Track>

<CounterfactualStatusButton />
<Track {...OVERVIEW_EVENTS.OPEN_EXPLORER}>
<ExplorerButton {...blockExplorerLink} className={css.iconButton} icon={LinkIconBold} />
</Track>

<EnvHintButton />
</div>
</div>
<CounterfactualStatusButton />

<NewTxButton />
<EnvHintButton />
</div>
</div>
</GradientBoxSafenet>

<NewTxButton />
</div>
)
return isSafenetEnabled ? <GradientBoxSafenet variant="bottom">{header}</GradientBoxSafenet> : header
}

export default SafeHeader
17 changes: 8 additions & 9 deletions apps/web/src/components/sidebar/SidebarNavigation/config.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import type { ReactElement } from 'react'
import React from 'react'
import { Chip } from '@/components/common/Chip'
import { AppRoutes } from '@/config/routes'
import HomeIcon from '@/public/images/sidebar/home.svg'
import AssetsIcon from '@/public/images/sidebar/assets.svg'
import TransactionIcon from '@/public/images/sidebar/transactions.svg'
import ABIcon from '@/public/images/sidebar/address-book.svg'
import AppsIcon from '@/public/images/apps/apps-icon.svg'
import SettingsIcon from '@/public/images/sidebar/settings.svg'
import BridgeIcon from '@/public/images/common/bridge.svg'
import SwapIcon from '@/public/images/common/swap.svg'
import StakeIcon from '@/public/images/common/stake.svg'
import SwapIcon from '@/public/images/common/swap.svg'
import ABIcon from '@/public/images/sidebar/address-book.svg'
import AssetsIcon from '@/public/images/sidebar/assets.svg'
import HomeIcon from '@/public/images/sidebar/home.svg'
import SettingsIcon from '@/public/images/sidebar/settings.svg'
import TransactionIcon from '@/public/images/sidebar/transactions.svg'
import { SvgIcon } from '@mui/material'
import { Chip } from '@/components/common/Chip'
import type { ReactElement } from 'react'

export type NavItem = {
label: string
Expand Down
Loading
Loading