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: Subaccounts #4456

Draft
wants to merge 26 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9060d1f
Add transaction flow for creating (and funding) Subaccounts
iamacook Oct 31, 2024
b60f19f
Fix lint
iamacook Oct 31, 2024
c735475
Don't allow multiple selection of assets
iamacook Oct 31, 2024
2f06915
Fix lint
iamacook Oct 31, 2024
1f75787
Simplify code
iamacook Oct 31, 2024
c8e8e68
Add popover, settings section and success screen
iamacook Nov 28, 2024
71ff216
Merge branch 'dev' into subaccount-demo
iamacook Nov 28, 2024
be43e72
Fix saltNonce collision and remove status screen changes
iamacook Nov 28, 2024
d1451c0
Fix merge issues
iamacook Nov 28, 2024
4385779
Add badge
iamacook Nov 28, 2024
9c6e251
Tweak styles and events
iamacook Nov 28, 2024
f9e5761
Remove debug
iamacook Nov 28, 2024
e5c50ac
Add status screen
iamacook Nov 28, 2024
dc5bb92
Convert Subaccount list to correct elements
iamacook Nov 28, 2024
12d7051
Invalidate subaccount cache after deployment
iamacook Nov 28, 2024
4f58bea
Fix CI and add tests
iamacook Nov 28, 2024
97aeed8
Fix types
iamacook Nov 28, 2024
f2fc1d4
Fix test
iamacook Nov 28, 2024
93a9283
Update icon in flow
iamacook Nov 29, 2024
55cceba
Address design review
iamacook Nov 29, 2024
5b246ad
Merge branch 'dev' into subaccount-demo
iamacook Nov 29, 2024
2315785
Set pointer on show all
iamacook Nov 29, 2024
00fe4ab
Make Subaccount a link
iamacook Nov 29, 2024
6ef1bca
Close modal when navigating to Subaccount and fix colour in dark mode
iamacook Dec 2, 2024
322f47d
Use `Date.now` for `saltNonce`
iamacook Dec 2, 2024
cc3c894
Add fallback name
iamacook Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions public/images/sidebar/subaccounts-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions public/images/sidebar/subaccounts.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 9 additions & 2 deletions src/components/common/ModalDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,20 @@ interface DialogTitleProps {
children: ReactNode
onClose?: ModalProps['onClose']
hideChainIndicator?: boolean
sx?: DialogProps['sx']
}

export const ModalDialogTitle = ({ children, onClose, hideChainIndicator = false, ...other }: DialogTitleProps) => {
export const ModalDialogTitle = ({
children,
onClose,
hideChainIndicator = false,
sx = {},
...other
}: DialogTitleProps) => {
return (
<DialogTitle
data-testid="modal-title"
sx={{ m: 0, px: 3, pt: 3, pb: 2, display: 'flex', alignItems: 'center', fontWeight: 'bold' }}
sx={{ m: 0, px: 3, pt: 3, pb: 2, display: 'flex', alignItems: 'center', fontWeight: 'bold', ...sx }}
{...other}
>
{children}
Expand Down
124 changes: 124 additions & 0 deletions src/components/settings/SubaccountsList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Paper, Grid, Typography, Button, SvgIcon, Tooltip, IconButton } from '@mui/material'
import { useContext, useMemo, useState } from 'react'
import type { ReactElement } from 'react'

import AddIcon from '@/public/images/common/add.svg'
import EditIcon from '@/public/images/common/edit.svg'
import CheckWallet from '@/components/common/CheckWallet'
import EthHashInfo from '@/components/common/EthHashInfo'
import ExternalLink from '@/components/common/ExternalLink'
import { CreateSubaccount } from '@/components/tx-flow/flows/CreateSubaccount'
import EntryDialog from '@/components/address-book/EntryDialog'
import { TxModalContext } from '@/components/tx-flow'
import EnhancedTable from '@/components/common/EnhancedTable'
import useSafeInfo from '@/hooks/useSafeInfo'
import { useGetSafesByOwnerQuery } from '@/store/slices'

import tableCss from '@/components/common/EnhancedTable/styles.module.css'
import Track from '@/components/common/Track'
import { SUBACCOUNT_EVENTS } from '@/services/analytics/events/subaccounts'
import { skipToken } from '@reduxjs/toolkit/query'

export function SubaccountsList(): ReactElement | null {
const { setTxFlow } = useContext(TxModalContext)
const [addressToRename, setAddressToRename] = useState<string | null>(null)

const { safe, safeLoaded, safeAddress } = useSafeInfo()
const { data: subaccounts } = useGetSafesByOwnerQuery(
safeLoaded ? { chainId: safe.chainId, ownerAddress: safeAddress } : skipToken,
)

const rows = useMemo(() => {
return subaccounts?.safes.map((subaccount) => {
return {
cells: {
owner: {
rawValue: subaccount,
content: (
<EthHashInfo address={subaccount} showCopyButton shortAddress={false} showName={true} hasExplorer />
),
},
actions: {
rawValue: '',
sticky: true,
content: (
<div className={tableCss.actions}>
<CheckWallet>
{(isOk) => (
<Track {...SUBACCOUNT_EVENTS.RENAME}>
<Tooltip title={isOk ? 'Rename Subaccount' : undefined}>
<span>
<IconButton onClick={() => setAddressToRename(subaccount)} size="small" disabled={!isOk}>
<SvgIcon component={EditIcon} inheritViewBox fontSize="small" color="border" />
</IconButton>
</span>
</Tooltip>
</Track>
)}
</CheckWallet>
</div>
),
},
},
}
})
}, [subaccounts?.safes])

return (
<>
<Paper sx={{ padding: 4, mt: 2 }}>
<Grid container direction="row" justifyContent="space-between" spacing={3} mb={2}>
<Grid item lg={4} xs={12}>
<Typography variant="h4" fontWeight={700}>
Subaccounts
</Typography>
</Grid>

<Grid item xs>
<Typography sx={{ mb: 3 }}>
Subaccounts are separate wallets owned by your main Account, perfect for organizing different funds and
projects.{' '}
<ExternalLink
// TODO: Add link
href="#"
Comment on lines +82 to +83
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marking this so we don't forget.

>
Learn more
</ExternalLink>
</Typography>

{subaccounts?.safes.length === 0 && (
<Typography sx={{ mb: 3 }}>
You don&apos;t have any Subaccounts yet. Set one up now to better organize your assets
</Typography>
)}

<CheckWallet>
{(isOk) => (
<Button
onClick={() => setTxFlow(<CreateSubaccount />)}
variant="text"
startIcon={<SvgIcon component={AddIcon} inheritViewBox fontSize="small" />}
disabled={!isOk}
sx={{ mb: 3 }}
>
Add Subaccount
</Button>
)}
</CheckWallet>

{rows && rows.length > 0 && <EnhancedTable rows={rows} headCells={[]} />}
</Grid>
</Grid>
</Paper>

{addressToRename && (
<EntryDialog
handleClose={() => setAddressToRename(null)}
defaultValues={{ name: '', address: addressToRename }}
chainIds={[safe.chainId]}
disableAddressInput
/>
)}
</>
)
}
50 changes: 40 additions & 10 deletions src/components/sidebar/SafeListContextMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,35 @@ import ListItemText from '@mui/material/ListItemText'

import EntryDialog from '@/components/address-book/EntryDialog'
import SafeListRemoveDialog from '@/components/sidebar/SafeListRemoveDialog'
import SubaccountsIcon from '@/public/images/sidebar/subaccounts-icon.svg'
import EditIcon from '@/public/images/common/edit.svg'
import DeleteIcon from '@/public/images/common/delete.svg'
import PlusIcon from '@/public/images/common/plus.svg'
import ContextMenu from '@/components/common/ContextMenu'
import { trackEvent, OVERVIEW_EVENTS, OVERVIEW_LABELS } from '@/services/analytics'
import { trackEvent, OVERVIEW_EVENTS, OVERVIEW_LABELS, type AnalyticsEvent } from '@/services/analytics'
import { SvgIcon } from '@mui/material'
import useAddressBook from '@/hooks/useAddressBook'
import { AppRoutes } from '@/config/routes'
import router from 'next/router'
import { CreateSafeOnNewChain } from '@/features/multichain/components/CreateSafeOnNewChain'
import { useGetSafesByOwnerQuery } from '@/store/slices'
import { SubaccountsPopover } from '../SubaccountsPopover'
import { SUBACCOUNT_EVENTS, SUBACCOUNT_LABELS } from '@/services/analytics/events/subaccounts'
import { skipToken } from '@reduxjs/toolkit/query'

enum ModalType {
SUBACCOUNTS = 'subaccounts',
RENAME = 'rename',
REMOVE = 'remove',
ADD_CHAIN = 'add_chain',
}

const defaultOpen = { [ModalType.RENAME]: false, [ModalType.REMOVE]: false, [ModalType.ADD_CHAIN]: false }
const defaultOpen = {
[ModalType.SUBACCOUNTS]: false,
[ModalType.RENAME]: false,
[ModalType.REMOVE]: false,
[ModalType.ADD_CHAIN]: false,
}

const SafeListContextMenu = ({
name,
Expand All @@ -42,10 +53,11 @@ const SafeListContextMenu = ({
rename: boolean
undeployedSafe: boolean
}): ReactElement => {
const { data: subaccounts } = useGetSafesByOwnerQuery(address ? { chainId, ownerAddress: address } : skipToken)
const addressBook = useAddressBook()
const hasName = address in addressBook

const [anchorEl, setAnchorEl] = useState<HTMLElement | undefined>()
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
const [open, setOpen] = useState<typeof defaultOpen>(defaultOpen)

const trackingLabel =
Expand All @@ -56,17 +68,17 @@ const SafeListContextMenu = ({
}

const handleCloseContextMenu = () => {
setAnchorEl(undefined)
setAnchorEl(null)
}

const handleOpenModal =
(type: keyof typeof open, event: typeof OVERVIEW_EVENTS.SIDEBAR_RENAME | typeof OVERVIEW_EVENTS.SIDEBAR_RENAME) =>
() => {
const handleOpenModal = (type: keyof typeof open, event: AnalyticsEvent) => () => {
if (type !== ModalType.SUBACCOUNTS) {
handleCloseContextMenu()
setOpen((prev) => ({ ...prev, [type]: true }))

trackEvent({ ...event, label: trackingLabel })
}
setOpen((prev) => ({ ...prev, [type]: true }))

trackEvent({ ...event, label: trackingLabel })
}

const handleCloseModal = () => {
setOpen(defaultOpen)
Expand All @@ -78,6 +90,20 @@ const SafeListContextMenu = ({
<MoreVertIcon sx={({ palette }) => ({ color: palette.border.main })} />
</IconButton>
<ContextMenu anchorEl={anchorEl} open={!!anchorEl} onClose={handleCloseContextMenu}>
{!undeployedSafe && subaccounts?.safes && subaccounts.safes.length > 0 && (
<MenuItem
onClick={handleOpenModal(ModalType.SUBACCOUNTS, {
...SUBACCOUNT_EVENTS.OPEN_LIST,
label: SUBACCOUNT_LABELS.sidebar,
})}
>
<ListItemIcon>
<SvgIcon component={SubaccountsIcon} inheritViewBox fontSize="small" color="success" />
</ListItemIcon>
<ListItemText data-testid="subaccounts-btn">Subaccounts</ListItemText>
</MenuItem>
)}

{rename && (
<MenuItem onClick={handleOpenModal(ModalType.RENAME, OVERVIEW_EVENTS.SIDEBAR_RENAME)}>
<ListItemIcon>
Expand Down Expand Up @@ -106,6 +132,10 @@ const SafeListContextMenu = ({
)}
</ContextMenu>

{open[ModalType.SUBACCOUNTS] && (
<SubaccountsPopover anchorEl={anchorEl} onClose={handleCloseModal} subaccounts={subaccounts?.safes ?? []} />
)}

{open[ModalType.RENAME] && (
<EntryDialog
handleClose={handleCloseModal}
Expand Down
6 changes: 6 additions & 0 deletions src/components/sidebar/SidebarHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import ExplorerButton from '@/components/common/ExplorerButton'
import CopyTooltip from '@/components/common/CopyTooltip'
import FiatValue from '@/components/common/FiatValue'
import { useAddressResolver } from '@/hooks/useAddressResolver'
import { SubaccountsButton } from '@/components/sidebar/SubaccountsButton'
import { SUBACCOUNT_EVENTS, SUBACCOUNT_LABELS } from '@/services/analytics/events/subaccounts'

const SafeHeader = (): ReactElement => {
const { balances } = useVisibleBalances()
Expand Down Expand Up @@ -114,6 +116,10 @@ const SafeHeader = (): ReactElement => {
<ExplorerButton {...blockExplorerLink} className={css.iconButton} icon={LinkIconBold} />
</Track>

<Track {...SUBACCOUNT_EVENTS.OPEN_LIST} label={SUBACCOUNT_LABELS.header}>
<SubaccountsButton chainId={safe.chainId} safeAddress={safe.address.value} />
</Track>

<CounterfactualStatusButton />

<EnvHintButton />
Expand Down
62 changes: 62 additions & 0 deletions src/components/sidebar/SubaccountInfo/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Tooltip, SvgIcon, Typography, List, ListItem, Box, ListItemAvatar, Avatar, ListItemText } from '@mui/material'
import CheckIcon from '@mui/icons-material/Check'
import type { ReactElement } from 'react'

import SubaccountsIcon from '@/public/images/sidebar/subaccounts-icon.svg'
import Subaccounts from '@/public/images/sidebar/subaccounts.svg'
import InfoIcon from '@/public/images/notifications/info.svg'

export function SubaccountInfo(): ReactElement {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', pt: 1 }}>
<Subaccounts />
<Box sx={{ display: 'flex', gap: 1, py: 2 }}>
<Typography fontWeight={700}>No Subaccounts yet</Typography>
<Tooltip
title="Subaccounts are separate wallets owned by your main Account, perfect for organizing different funds and projects."
placement="top"
arrow
sx={{ ml: 1 }}
>
<span>
<SvgIcon
component={InfoIcon}
inheritViewBox
fontSize="small"
color="border"
sx={{ verticalAlign: 'middle' }}
/>
</span>
</Tooltip>
</Box>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', pt: 1, pb: 4 }}>
<Avatar sx={{ padding: '20px', backgroundColor: 'success.background' }}>
<SvgIcon component={SubaccountsIcon} inheritViewBox color="primary" sx={{ fontSize: 20 }} />
</Avatar>
<Typography variant="body2" fontWeight={700}>
Subaccounts allow you to:
</Typography>
</Box>
<List sx={{ p: 0, display: 'flex', flexDirection: 'column', gap: 2 }}>
{[
'Use them for specific cases such as DeFi operations',
'Install modules to execute transactions, bypassing thresholds',
'Make sure that this Safe is not exposed to additional risks',
].map((item) => {
return (
<ListItem key={item} sx={{ p: 0, pl: 1.5, alignItems: 'unset' }}>
<ListItemAvatar sx={{ minWidth: 'unset', mr: 3 }}>
<Avatar sx={{ width: 25, height: 25, backgroundColor: 'success.background' }}>
<CheckIcon fontSize="small" color="success" />
</Avatar>
</ListItemAvatar>
<ListItemText sx={{ m: 0 }} primaryTypographyProps={{ variant: 'body2' }}>
{item}
</ListItemText>
</ListItem>
)
})}
</List>
</Box>
)
}
Loading
Loading