From f9f7a8b7a9a355d2d72c202bb13ebb080cfc51c1 Mon Sep 17 00:00:00 2001 From: Peiman <25097709+Rickk137@users.noreply.github.com> Date: Wed, 22 May 2024 17:12:14 +0330 Subject: [PATCH] Account permission management (#276) * feat: account permission management * ref: deps * feat(account-settings): hooks are taking in now arrays * ref(deps) * feat(AddPermissionModal) * feat(transferownership): added modal * feat: remove all permission button * feat(Modals): passing in the refetch function to modals * ref(deps) * fix: account management issues and improvements * fix: deps --------- Co-authored-by: fritzschoff Co-authored-by: max <39312833+fritzschoff@users.noreply.github.com> --- liquidity/lib/etherscanLink/etherscanLink.ts | 3 +- liquidity/lib/useAccountInfo/index.ts | 1 + liquidity/lib/useAccountInfo/package.json | 13 ++ .../useAccountInfo/useAccountPermissions.ts | 43 ++++ liquidity/lib/useManagePermissions/index.ts | 1 + .../lib/useManagePermissions/package.json | 12 ++ .../useManagePermissions.ts | 79 +++++++ liquidity/lib/useTransferAccountId/index.ts | 1 + .../lib/useTransferAccountId/package.json | 11 + .../useTransferAccountId.ts | 18 ++ liquidity/ui/package.json | 3 + liquidity/ui/src/Router.tsx | 2 + .../ui/src/components/Address/Address.tsx | 43 ++++ liquidity/ui/src/components/Address/index.ts | 1 + .../Permissions/AccountPermissions.ts | 1 + .../Permissions/AddPermissionModal.tsx | 130 +++++++++++ .../components/Permissions/DelegationIcon.tsx | 30 +++ .../components/Permissions/PermissionRow.tsx | 166 +++++++++++++++ .../Permissions/PermissionTable.tsx | 201 ++++++++++++++++++ .../Permissions/PermissionTableLoading.tsx | 30 +++ .../components/Permissions/Permissions.tsx | 48 +++++ .../Permissions/TransferOwnershipModal.tsx | 79 +++++++ .../src/layouts/Default/NetworkController.tsx | 21 +- liquidity/ui/src/pages/Account/Settings.tsx | 29 +++ yarn.lock | 36 ++++ 25 files changed, 997 insertions(+), 5 deletions(-) create mode 100644 liquidity/lib/useAccountInfo/index.ts create mode 100644 liquidity/lib/useAccountInfo/package.json create mode 100644 liquidity/lib/useAccountInfo/useAccountPermissions.ts create mode 100644 liquidity/lib/useManagePermissions/index.ts create mode 100644 liquidity/lib/useManagePermissions/package.json create mode 100644 liquidity/lib/useManagePermissions/useManagePermissions.ts create mode 100644 liquidity/lib/useTransferAccountId/index.ts create mode 100644 liquidity/lib/useTransferAccountId/package.json create mode 100644 liquidity/lib/useTransferAccountId/useTransferAccountId.ts create mode 100644 liquidity/ui/src/components/Address/Address.tsx create mode 100644 liquidity/ui/src/components/Address/index.ts create mode 100644 liquidity/ui/src/components/Permissions/AccountPermissions.ts create mode 100644 liquidity/ui/src/components/Permissions/AddPermissionModal.tsx create mode 100644 liquidity/ui/src/components/Permissions/DelegationIcon.tsx create mode 100644 liquidity/ui/src/components/Permissions/PermissionRow.tsx create mode 100644 liquidity/ui/src/components/Permissions/PermissionTable.tsx create mode 100644 liquidity/ui/src/components/Permissions/PermissionTableLoading.tsx create mode 100644 liquidity/ui/src/components/Permissions/Permissions.tsx create mode 100644 liquidity/ui/src/components/Permissions/TransferOwnershipModal.tsx create mode 100644 liquidity/ui/src/pages/Account/Settings.tsx diff --git a/liquidity/lib/etherscanLink/etherscanLink.ts b/liquidity/lib/etherscanLink/etherscanLink.ts index 46a77b9ee..c0595bee9 100644 --- a/liquidity/lib/etherscanLink/etherscanLink.ts +++ b/liquidity/lib/etherscanLink/etherscanLink.ts @@ -10,13 +10,14 @@ export function etherscanLink({ switch (chain) { case 'sepolia': return `https://sepolia.etherscan.io/${isTx ? 'tx' : 'address'}/${address}`; + case 'arbitrum': + return `https://arbiscan.io/${isTx ? 'tx' : 'address'}/${address}`; case 'optimism': return `https://optimistic.etherscan.io/${isTx ? 'tx' : 'address'}/${address}`; case 'base': return `https://basescan.org/${isTx ? 'tx' : 'address'}/${address}`; case 'base-sepolia': return `https://sepolia.basescan.org/${isTx ? 'tx' : 'address'}/${address}`; - case 'mainnet': default: return `https://etherscan.io/${isTx ? 'tx' : 'address'}/${address}`; diff --git a/liquidity/lib/useAccountInfo/index.ts b/liquidity/lib/useAccountInfo/index.ts new file mode 100644 index 000000000..e4fa819aa --- /dev/null +++ b/liquidity/lib/useAccountInfo/index.ts @@ -0,0 +1 @@ +export * from './useAccountPermissions'; diff --git a/liquidity/lib/useAccountInfo/package.json b/liquidity/lib/useAccountInfo/package.json new file mode 100644 index 000000000..e085f9b22 --- /dev/null +++ b/liquidity/lib/useAccountInfo/package.json @@ -0,0 +1,13 @@ +{ + "name": "@snx-v3/useAccountPermissions", + "private": true, + "main": "index.ts", + "version": "0.0.1", + "dependencies": { + "@snx-v3/useAccountProxy": "workspace:*", + "@snx-v3/useBlockchain": "workspace:*", + "@snx-v3/useCoreProxy": "workspace:*", + "@tanstack/react-query": "^5.8.3", + "ethers": "^5.7.2" + } +} diff --git a/liquidity/lib/useAccountInfo/useAccountPermissions.ts b/liquidity/lib/useAccountInfo/useAccountPermissions.ts new file mode 100644 index 000000000..04d9d33b5 --- /dev/null +++ b/liquidity/lib/useAccountInfo/useAccountPermissions.ts @@ -0,0 +1,43 @@ +import { useAccountProxy } from '@snx-v3/useAccountProxy'; +import { useNetwork } from '@snx-v3/useBlockchain'; +import { useCoreProxy } from '@snx-v3/useCoreProxy'; +import { useQuery } from '@tanstack/react-query'; +import { utils } from 'ethers'; + +export function useAccountPermissions(accountId: string | undefined) { + const { data: CoreProxy } = useCoreProxy(); + const { network } = useNetwork(); + + return useQuery({ + queryKey: [`${network?.id}-${network?.preset}`, 'account-permissions', accountId], + queryFn: async function () { + if (!CoreProxy || !accountId) throw new Error('Should be disabled'); + const permissions = await CoreProxy.getAccountPermissions(accountId); + + return permissions.reduce( + (acc, { user, permissions }) => ({ + ...acc, + [user.toLowerCase()]: permissions.map((r: string) => utils.parseBytes32String(r)), + }), + {} + ) as { + [key: string]: string[]; + }; + }, + enabled: Boolean(CoreProxy?.address), + }); +} + +export function useAccountOwner(accountId: string | undefined) { + const { data: AccountProxy } = useAccountProxy(); + const { network } = useNetwork(); + + return useQuery({ + queryKey: [`${network?.id}-${network?.preset}`, 'account-owner', accountId], + queryFn: async function () { + if (!AccountProxy || !accountId) throw new Error('Should be disabled'); + return await AccountProxy.ownerOf(accountId); + }, + enabled: Boolean(AccountProxy?.address), + }); +} diff --git a/liquidity/lib/useManagePermissions/index.ts b/liquidity/lib/useManagePermissions/index.ts new file mode 100644 index 000000000..3484ae487 --- /dev/null +++ b/liquidity/lib/useManagePermissions/index.ts @@ -0,0 +1 @@ +export * from './useManagePermissions'; diff --git a/liquidity/lib/useManagePermissions/package.json b/liquidity/lib/useManagePermissions/package.json new file mode 100644 index 000000000..a17ee837e --- /dev/null +++ b/liquidity/lib/useManagePermissions/package.json @@ -0,0 +1,12 @@ +{ + "name": "@snx-v3/useManagePermissions", + "private": true, + "main": "index.ts", + "version": "0.0.1", + "dependencies": { + "@snx-v3/useCoreProxy": "workspace:*", + "@snx-v3/useMulticall3": "workspace:*", + "@tanstack/react-query": "^5.8.3", + "ethers": "^5.7.2" + } +} diff --git a/liquidity/lib/useManagePermissions/useManagePermissions.ts b/liquidity/lib/useManagePermissions/useManagePermissions.ts new file mode 100644 index 000000000..42b2a1e82 --- /dev/null +++ b/liquidity/lib/useManagePermissions/useManagePermissions.ts @@ -0,0 +1,79 @@ +import { utils } from 'ethers'; +import { useCoreProxy } from '@snx-v3/useCoreProxy'; +import { useMutation } from '@tanstack/react-query'; +import { useMulticall3 } from '@snx-v3/useMulticall3'; + +type Permissions = Array; +const getPermissionDiff = ( + existing: Permissions, + selected: Permissions +): { + grants: Permissions; + revokes: Permissions; +} => { + let grants: Permissions = [], + revokes: Permissions = []; + existing.concat(selected).forEach((permission) => { + if (!existing.includes(permission)) { + grants = [...grants, permission]; + } + if (!selected.includes(permission)) { + revokes = [...revokes, permission]; + } + }); + return { grants, revokes }; +}; + +export const useManagePermissions = ({ + accountId, + target, + existing = [], + selected = [], +}: { + accountId: string; + target: string; + existing: Permissions; + selected: Permissions; +}) => { + const { data: CoreProxy } = useCoreProxy(); + const { data: multicall } = useMulticall3(); + + return useMutation({ + mutationFn: async () => { + if (!CoreProxy || !multicall) { + return; + } + + const { grants, revokes } = getPermissionDiff(existing, selected); + + try { + const grantCalls = grants.map((permission) => ({ + target: CoreProxy.address, + callData: CoreProxy.interface.encodeFunctionData('grantPermission', [ + accountId, + utils.formatBytes32String(permission), + target, + ]), + allowFailure: false, + requireSuccess: true, + })); + + const revokeCalls = revokes.map((permission) => ({ + target: CoreProxy.address, + callData: CoreProxy.interface.encodeFunctionData('revokePermission', [ + accountId, + utils.formatBytes32String(permission), + target, + ]), + allowFailure: false, + requireSuccess: true, + })); + + const tx = await multicall.aggregate3([...grantCalls, ...revokeCalls]); + await tx.wait(); + } catch (error: any) { + throw error; + } + }, + }); +}; diff --git a/liquidity/lib/useTransferAccountId/index.ts b/liquidity/lib/useTransferAccountId/index.ts new file mode 100644 index 000000000..dcdc21f3c --- /dev/null +++ b/liquidity/lib/useTransferAccountId/index.ts @@ -0,0 +1 @@ +export * from './useTransferAccountId'; diff --git a/liquidity/lib/useTransferAccountId/package.json b/liquidity/lib/useTransferAccountId/package.json new file mode 100644 index 000000000..bcc73e882 --- /dev/null +++ b/liquidity/lib/useTransferAccountId/package.json @@ -0,0 +1,11 @@ +{ + "name": "@snx-v3/useTransferAccountId", + "private": true, + "main": "index.ts", + "version": "0.0.1", + "dependencies": { + "@snx-v3/useAccountProxy": "workspace:*", + "@snx-v3/useBlockchain": "workspace:*", + "@tanstack/react-query": "^5.8.3" + } +} diff --git a/liquidity/lib/useTransferAccountId/useTransferAccountId.ts b/liquidity/lib/useTransferAccountId/useTransferAccountId.ts new file mode 100644 index 000000000..27bacc80b --- /dev/null +++ b/liquidity/lib/useTransferAccountId/useTransferAccountId.ts @@ -0,0 +1,18 @@ +import { useAccountProxy } from '@snx-v3/useAccountProxy'; +import { useWallet } from '@snx-v3/useBlockchain'; +import { useMutation } from '@tanstack/react-query'; + +export function useTransferAccountId(to: string, accountId: string) { + const { data: AccountProxy } = useAccountProxy(); + const { activeWallet } = useWallet(); + + return useMutation({ + mutationFn: async () => { + if (!AccountProxy) throw new Error('CoreProxy or Multicall not defined'); + if (!activeWallet?.address) throw new Error('Wallet is not connected'); + const tx = await AccountProxy.transferFrom(activeWallet.address, to, accountId); + const response = await tx.wait(); + return response; + }, + }); +} diff --git a/liquidity/ui/package.json b/liquidity/ui/package.json index 34292c988..81737ab17 100644 --- a/liquidity/ui/package.json +++ b/liquidity/ui/package.json @@ -34,6 +34,7 @@ "@snx-v3/isBaseAndromeda": "workspace:*", "@snx-v3/useAccountCollateral": "workspace:*", "@snx-v3/useAccountCollateralUnlockDate": "workspace:*", + "@snx-v3/useAccountPermissions": "workspace:*", "@snx-v3/useAccounts": "workspace:*", "@snx-v3/useApprove": "workspace:*", "@snx-v3/useApr": "workspace:*", @@ -48,6 +49,7 @@ "@snx-v3/useGetUSDTokens": "workspace:*", "@snx-v3/useLiquidityPosition": "workspace:*", "@snx-v3/useLiquidityPositions": "workspace:*", + "@snx-v3/useManagePermissions": "workspace:*", "@snx-v3/useMarketNamesById": "workspace:*", "@snx-v3/useParams": "workspace:*", "@snx-v3/usePoolConfiguration": "workspace:*", @@ -55,6 +57,7 @@ "@snx-v3/usePools": "workspace:*", "@snx-v3/useRewards": "workspace:^", "@snx-v3/useTokenBalance": "workspace:*", + "@snx-v3/useTransferAccountId": "workspace:*", "@snx-v3/useTransferableSynthetix": "workspace:*", "@snx-v3/useUSDProxy": "workspace:*", "@snx-v3/useVaultsData": "workspace:*", diff --git a/liquidity/ui/src/Router.tsx b/liquidity/ui/src/Router.tsx index fa9aea379..d67b95b26 100644 --- a/liquidity/ui/src/Router.tsx +++ b/liquidity/ui/src/Router.tsx @@ -7,12 +7,14 @@ import { Manage } from './pages/Manage'; import { Pool } from './pages/Pool'; import { NotFoundPage } from './pages/404'; import { Pools } from './pages/Pools'; +import { Settings } from './pages/Account/Settings'; export const Router = () => { return ( }> }> + } /> } /> } /> } /> diff --git a/liquidity/ui/src/components/Address/Address.tsx b/liquidity/ui/src/components/Address/Address.tsx new file mode 100644 index 000000000..5ac85452d --- /dev/null +++ b/liquidity/ui/src/components/Address/Address.tsx @@ -0,0 +1,43 @@ +import { CopyIcon, ExternalLinkIcon } from '@chakra-ui/icons'; +import { Flex, Tooltip } from '@chakra-ui/react'; +import { etherscanLink } from '@snx-v3/etherscanLink'; +import { prettyString } from '@snx-v3/format'; +import { useNetwork } from '@snx-v3/useBlockchain'; +import { FC, useMemo } from 'react'; + +interface AddressProps { + address: string; +} + +export const Address: FC = ({ address }) => { + const { network } = useNetwork(); + const link = useMemo( + () => + etherscanLink({ + chain: network?.name || '', + address, + }), + [address, network?.name] + ); + return ( + + {prettyString(address)} + { + navigator.clipboard.writeText(address); + }} + cursor="pointer" + _hover={{ + color: 'cyan', + }} + /> + + + + + ); +}; diff --git a/liquidity/ui/src/components/Address/index.ts b/liquidity/ui/src/components/Address/index.ts new file mode 100644 index 000000000..48615dc14 --- /dev/null +++ b/liquidity/ui/src/components/Address/index.ts @@ -0,0 +1 @@ +export * from './Address'; diff --git a/liquidity/ui/src/components/Permissions/AccountPermissions.ts b/liquidity/ui/src/components/Permissions/AccountPermissions.ts new file mode 100644 index 000000000..bceffe9d7 --- /dev/null +++ b/liquidity/ui/src/components/Permissions/AccountPermissions.ts @@ -0,0 +1 @@ +export const permissionsList = ['ADMIN', 'BURN', 'DELEGATE', 'MINT', 'REWARDS', 'WITHDRAW']; diff --git a/liquidity/ui/src/components/Permissions/AddPermissionModal.tsx b/liquidity/ui/src/components/Permissions/AddPermissionModal.tsx new file mode 100644 index 000000000..993d4c178 --- /dev/null +++ b/liquidity/ui/src/components/Permissions/AddPermissionModal.tsx @@ -0,0 +1,130 @@ +import { + Badge, + Button, + Divider, + Flex, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Spinner, + Text, +} from '@chakra-ui/react'; +import { useMemo, useState } from 'react'; +import { permissionsList } from './AccountPermissions'; +import { utils } from 'ethers'; +import { useManagePermissions } from '@snx-v3/useManagePermissions'; +import { useAccountOwner, useAccountPermissions } from '@snx-v3/useAccountPermissions'; + +export function AddPermissionModal({ + accountId, + isOpen, + onClose, + refetch, +}: { + accountId: string; + isOpen: boolean; + onClose: () => void; + refetch: () => void; +}) { + const [address, setAddress] = useState(''); + const [selectedPermission, setSelectedPermissions] = useState([]); + const { mutateAsync: submit, isPending } = useManagePermissions({ + target: address, + accountId, + existing: [], + selected: selectedPermission, + }); + + const { data: permissionData } = useAccountPermissions(accountId); + const { data: accountOwner } = useAccountOwner(accountId); + + const isAddressValid = useMemo(() => { + return ( + utils.isAddress(address) && + accountOwner?.toLowerCase() !== address.toLowerCase() && + permissionData && + !permissionData[address.toLowerCase()] + ); + }, [accountOwner, address, permissionData]); + + const isFormValid = useMemo(() => { + return selectedPermission.length > 0 && isAddressValid; + }, [isAddressValid, selectedPermission.length]); + + return ( + + + + New Permission + + + + + + Address + + { + setAddress(e.target.value.trim()); + }} + value={address} + isInvalid={!isAddressValid && !!address} + /> + + Select Permissions + + + {permissionsList.map((permission) => ( + + setSelectedPermissions((state) => { + if (state.includes(permission)) { + return state.filter((s) => s !== permission); + } + return [...state, permission]; + }) + } + variant="outline" + key={permission} + color={selectedPermission.includes(permission) ? 'cyan' : 'gray'} + textTransform="capitalize" + cursor="pointer" + bg="gray.900" + colorScheme={selectedPermission.includes(permission) ? 'cyan' : 'gray'} + > + {permission} + + ))} + + + + + {isPending ? ( + + ) : ( + + )} + + + + ); +} diff --git a/liquidity/ui/src/components/Permissions/DelegationIcon.tsx b/liquidity/ui/src/components/Permissions/DelegationIcon.tsx new file mode 100644 index 000000000..008e78e86 --- /dev/null +++ b/liquidity/ui/src/components/Permissions/DelegationIcon.tsx @@ -0,0 +1,30 @@ +export const DelegationIcon = () => { + return ( + + + + + + + ); +}; diff --git a/liquidity/ui/src/components/Permissions/PermissionRow.tsx b/liquidity/ui/src/components/Permissions/PermissionRow.tsx new file mode 100644 index 000000000..94fc05d1d --- /dev/null +++ b/liquidity/ui/src/components/Permissions/PermissionRow.tsx @@ -0,0 +1,166 @@ +import { DeleteIcon, EditIcon } from '@chakra-ui/icons'; +import { Badge, Td, Tr, Text, Button, Flex, IconButton } from '@chakra-ui/react'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { permissionsList } from './AccountPermissions'; +import { useManagePermissions } from '@snx-v3/useManagePermissions'; +import { Address } from '../Address'; + +interface Props { + address: string; + currentPermissions: Array; + accountId: string; + refetch: () => void; + isOwner: boolean; +} + +export const PermissionRow: FC = ({ + address, + currentPermissions, + accountId, + refetch, + isOwner, +}) => { + const [isEdit, setIsEdit] = useState(false); + const [permissions, setPermissions] = useState([...currentPermissions]); + + const { + mutate: submit, + isPending, + isSuccess, + } = useManagePermissions({ + accountId, + target: address, + selected: permissions, + existing: currentPermissions, + }); + + useEffect(() => { + if (isSuccess) { + refetch(); + setIsEdit(false); + } + }, [isSuccess, refetch]); + + const selectPermission = useCallback( + (permission: string) => { + const index = permissions.findIndex((p) => p === permission); + if (index < 0) { + setPermissions([...permissions, permission]); + } else { + const list = [...permissions]; + list.splice(index, 1); + setPermissions(list); + } + }, + [permissions] + ); + + if (isEdit) { + return ( + + + +
+ + + + + {permissionsList.map((permission) => ( + selectPermission(permission)} + colorScheme={permissions.includes(permission) ? 'cyan' : 'gray'} + color={permissions.includes(permission) ? 'cyan' : 'gray'} + variant="outline" + bg={permissions.includes(permission) ? 'cyan.900' : 'gray.900'} + size="md" + textTransform="capitalize" + key={permission.concat('permission-row')} + > + {permission} + + ))} + + + + + + + + ); + } + + return ( + + + +
+ + + + + {currentPermissions.map((r) => ( + + {r} + + ))} + + + + {isOwner && ( + <> + { + setPermissions([...currentPermissions]); + setIsEdit(true); + }} + size="sm" + aria-label="edit" + variant="outline" + colorScheme="gray" + icon={} + mr="2" + /> + { + setPermissions([]); + submit(); + }} + size="sm" + aria-label="delete" + icon={} + /> + + )} + + + ); +}; diff --git a/liquidity/ui/src/components/Permissions/PermissionTable.tsx b/liquidity/ui/src/components/Permissions/PermissionTable.tsx new file mode 100644 index 000000000..28dc8d4c6 --- /dev/null +++ b/liquidity/ui/src/components/Permissions/PermissionTable.tsx @@ -0,0 +1,201 @@ +import { + Badge, + Button, + Flex, + Heading, + Skeleton, + Table, + TableContainer, + Tbody, + Td, + Text, + Th, + Thead, + Tr, + useDisclosure, +} from '@chakra-ui/react'; +import { PermissionRow } from './PermissionRow'; +import { useAccountOwner, useAccountPermissions } from '@snx-v3/useAccountPermissions'; +import { prettyString } from '@snx-v3/format'; +import { useWallet } from '@snx-v3/useBlockchain'; +import { AddPermissionModal } from './AddPermissionModal'; +import { TransferOwnershipModal } from './TransferOwnershipModal'; +import { PermissionTableLoading } from './PermissionTableLoading'; +import { useMemo } from 'react'; +import { Address } from '../Address'; + +export default function PermissionTable({ + accountId, + refetchAccounts, +}: { + accountId: string; + refetchAccounts: () => void; +}) { + const { + isOpen: isPermissionOpen, + onClose: onPermissionClose, + onOpen: onPermissionOpen, + } = useDisclosure(); + const { + isOpen: isTransferOpen, + onClose: onTransferClose, + onOpen: onTransferOpen, + } = useDisclosure(); + + const { activeWallet } = useWallet(); + const { data: permissions, isLoading, refetch } = useAccountPermissions(accountId); + const { + data: accountOwner, + isLoading: loadingOwner, + refetch: refetchAccountOwner, + } = useAccountOwner(accountId); + + const isOwner = useMemo( + () => !!(accountOwner && accountOwner?.toLowerCase() === activeWallet?.address.toLowerCase()), + [accountOwner, activeWallet?.address] + ); + return ( + <> + + + + Account #{prettyString(accountId, 4, 4)} + + {isOwner && ( + + )} + + + + + + + + + + + + + + + + + + {isLoading && } + + {!isLoading && + permissions && + Object.keys(permissions) + .filter((target) => permissions[target]?.length > 0) + .map((target) => ( + + ))} + +
+ Address + + Permissions +
+ + {accountOwner && ( + +
+ + )} + +
+ + OWNER + + + {isOwner && ( + + )} +
+
+ + + { + refetch(); + refetchAccountOwner(); + refetchAccounts(); + }} + /> + + ); +} diff --git a/liquidity/ui/src/components/Permissions/PermissionTableLoading.tsx b/liquidity/ui/src/components/Permissions/PermissionTableLoading.tsx new file mode 100644 index 000000000..38a42e468 --- /dev/null +++ b/liquidity/ui/src/components/Permissions/PermissionTableLoading.tsx @@ -0,0 +1,30 @@ +import { Skeleton, Td, Text, Tr } from '@chakra-ui/react'; +import { prettyString } from '@snx-v3/format'; +import { ethers } from 'ethers'; + +export function PermissionTableLoading() { + const rows = Array.from({ length: 2 }, (_, i) => i); + return ( + <> + {rows.map((row) => { + return ( + + + + + {prettyString(ethers.constants.AddressZero)}{' '} + + + + + - + + + - + + + ); + })} + + ); +} diff --git a/liquidity/ui/src/components/Permissions/Permissions.tsx b/liquidity/ui/src/components/Permissions/Permissions.tsx new file mode 100644 index 000000000..13e44175c --- /dev/null +++ b/liquidity/ui/src/components/Permissions/Permissions.tsx @@ -0,0 +1,48 @@ +import { Button, Flex, Heading, Link, Text } from '@chakra-ui/react'; +import { useAccounts } from '@snx-v3/useAccounts'; +import PermissionTable from './PermissionTable'; +import { DelegationIcon } from './DelegationIcon'; + +export default function Permissions() { + const { data: accounts, refetch: refetchAccounts } = useAccounts(); + + return ( + + + {accounts?.map((account) => ( + + ))} + + + + + Delegate Permissions + + + Delegation enables a wallet to execute functions on behalf of another wallet/account: + delegate, borrow, withdraw, claim, but not transfer. Manage addresses and their powers + below. + + + + + + + ); +} diff --git a/liquidity/ui/src/components/Permissions/TransferOwnershipModal.tsx b/liquidity/ui/src/components/Permissions/TransferOwnershipModal.tsx new file mode 100644 index 000000000..d43b3ff0c --- /dev/null +++ b/liquidity/ui/src/components/Permissions/TransferOwnershipModal.tsx @@ -0,0 +1,79 @@ +import { + Button, + Divider, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Spinner, + Text, +} from '@chakra-ui/react'; +import { prettyString } from '@snx-v3/format'; +import { useTransferAccountId } from '@snx-v3/useTransferAccountId'; +import { utils } from 'ethers'; +import { useState } from 'react'; + +export function TransferOwnershipModal({ + isOpen, + onClose, + accountId, + refetch, +}: { + isOpen: boolean; + onClose: () => void; + accountId: string; + refetch: () => void; +}) { + const [to, setTo] = useState(''); + const { isPending, mutateAsync: submit } = useTransferAccountId(to, accountId); + + return ( + + + + Transfer Ownership + + + + + Account #{prettyString(accountId, 4, 4)} + + + Enter the wallet address you would like to transfer this account to: + + { + setTo(e.target.value.trim()); + }} + value={to} + /> + + + {isPending ? ( + + ) : ( + + )} + + + + ); +} diff --git a/liquidity/ui/src/layouts/Default/NetworkController.tsx b/liquidity/ui/src/layouts/Default/NetworkController.tsx index cf0068011..f2e08a64e 100644 --- a/liquidity/ui/src/layouts/Default/NetworkController.tsx +++ b/liquidity/ui/src/layouts/Default/NetworkController.tsx @@ -3,6 +3,8 @@ import { Badge, Button, Flex, + IconButton, + Link, Menu, MenuButton, MenuItem, @@ -17,7 +19,7 @@ import { prettyString } from '@snx-v3/format'; import { networks } from '../../utils/onboard'; import { useLocalStorage } from '../../hooks'; import { LOCAL_STORAGE_KEYS } from '../../utils/constants'; -import { CopyIcon } from '@chakra-ui/icons'; +import { CopyIcon, SettingsIcon } from '@chakra-ui/icons'; import { useAccounts, useCreateAccount } from '@snx-v3/useAccounts'; import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { Tooltip } from '@snx-v3/Tooltip'; @@ -210,9 +212,20 @@ export function NetworkController() { rounded="base" gap="2" > - - Account(s) - + + + Account(s) + + + } + aria-label="account settings" + /> + + {accounts?.map((account) => ( + + Account Settings + + + + + Account Settings + + + + + ); +} diff --git a/yarn.lock b/yarn.lock index 4a85a0f66..501848da8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6448,6 +6448,7 @@ __metadata: "@snx-v3/isBaseAndromeda": "workspace:*" "@snx-v3/useAccountCollateral": "workspace:*" "@snx-v3/useAccountCollateralUnlockDate": "workspace:*" + "@snx-v3/useAccountPermissions": "workspace:*" "@snx-v3/useAccounts": "workspace:*" "@snx-v3/useApprove": "workspace:*" "@snx-v3/useApr": "workspace:*" @@ -6462,6 +6463,7 @@ __metadata: "@snx-v3/useGetUSDTokens": "workspace:*" "@snx-v3/useLiquidityPosition": "workspace:*" "@snx-v3/useLiquidityPositions": "workspace:*" + "@snx-v3/useManagePermissions": "workspace:*" "@snx-v3/useMarketNamesById": "workspace:*" "@snx-v3/useParams": "workspace:*" "@snx-v3/usePoolConfiguration": "workspace:*" @@ -6469,6 +6471,7 @@ __metadata: "@snx-v3/usePools": "workspace:*" "@snx-v3/useRewards": "workspace:^" "@snx-v3/useTokenBalance": "workspace:*" + "@snx-v3/useTransferAccountId": "workspace:*" "@snx-v3/useTransferableSynthetix": "workspace:*" "@snx-v3/useUSDProxy": "workspace:*" "@snx-v3/useVaultsData": "workspace:*" @@ -6693,6 +6696,18 @@ __metadata: languageName: unknown linkType: soft +"@snx-v3/useAccountPermissions@workspace:*, @snx-v3/useAccountPermissions@workspace:liquidity/lib/useAccountInfo": + version: 0.0.0-use.local + resolution: "@snx-v3/useAccountPermissions@workspace:liquidity/lib/useAccountInfo" + dependencies: + "@snx-v3/useAccountProxy": "workspace:*" + "@snx-v3/useBlockchain": "workspace:*" + "@snx-v3/useCoreProxy": "workspace:*" + "@tanstack/react-query": "npm:^5.8.3" + ethers: "npm:^5.7.2" + languageName: unknown + linkType: soft + "@snx-v3/useAccountProxy@workspace:*, @snx-v3/useAccountProxy@workspace:liquidity/lib/useAccountProxy": version: 0.0.0-use.local resolution: "@snx-v3/useAccountProxy@workspace:liquidity/lib/useAccountProxy" @@ -7077,6 +7092,17 @@ __metadata: languageName: unknown linkType: soft +"@snx-v3/useManagePermissions@workspace:*, @snx-v3/useManagePermissions@workspace:liquidity/lib/useManagePermissions": + version: 0.0.0-use.local + resolution: "@snx-v3/useManagePermissions@workspace:liquidity/lib/useManagePermissions" + dependencies: + "@snx-v3/useCoreProxy": "workspace:*" + "@snx-v3/useMulticall3": "workspace:*" + "@tanstack/react-query": "npm:^5.8.3" + ethers: "npm:^5.7.2" + languageName: unknown + linkType: soft + "@snx-v3/useMarketNamesById@workspace:*, @snx-v3/useMarketNamesById@workspace:liquidity/lib/useMarketNamesById": version: 0.0.0-use.local resolution: "@snx-v3/useMarketNamesById@workspace:liquidity/lib/useMarketNamesById" @@ -7270,6 +7296,16 @@ __metadata: languageName: unknown linkType: soft +"@snx-v3/useTransferAccountId@workspace:*, @snx-v3/useTransferAccountId@workspace:liquidity/lib/useTransferAccountId": + version: 0.0.0-use.local + resolution: "@snx-v3/useTransferAccountId@workspace:liquidity/lib/useTransferAccountId" + dependencies: + "@snx-v3/useAccountProxy": "workspace:*" + "@snx-v3/useBlockchain": "workspace:*" + "@tanstack/react-query": "npm:^5.8.3" + languageName: unknown + linkType: soft + "@snx-v3/useTransferableSynthetix@workspace:*, @snx-v3/useTransferableSynthetix@workspace:liquidity/lib/useTransferableSynthetix": version: 0.0.0-use.local resolution: "@snx-v3/useTransferableSynthetix@workspace:liquidity/lib/useTransferableSynthetix"