diff --git a/package-lock.json b/package-lock.json index c1c3bc7f3..444ea0d0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "react-router-dom": "^6.22.0", "remark-gfm": "^4.0.0", "sonner": "^1.5.0", + "use-between": "^1.3.5", "viem": "^2.13.1", "vite": "^5.3.4", "vite-plugin-checker": "^0.6.4", @@ -46019,6 +46020,15 @@ "node": ">=0.10.0" } }, + "node_modules/use-between": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/use-between/-/use-between-1.3.5.tgz", + "integrity": "sha512-IP9eJfszZr0aah/6i/pzaM7n/QgMPwWKJ+mnWqT5O0qFhLnztPbkVC6L7zI6ygeBIMJHfmUGvsw0b28pyrEGSA==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/use-callback-ref": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz", diff --git a/package.json b/package.json index 4fb92ce4e..6e0e0c29b 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "react-router-dom": "^6.22.0", "remark-gfm": "^4.0.0", "sonner": "^1.5.0", + "use-between": "^1.3.5", "viem": "^2.13.1", "vite": "^5.3.4", "vite-plugin-checker": "^0.6.4", diff --git a/src/assets/css/sentry.css b/src/assets/css/sentry.css index c503704b6..420d46565 100644 --- a/src/assets/css/sentry.css +++ b/src/assets/css/sentry.css @@ -1,3 +1,10 @@ #sentry-feedback { z-index: 1; + --font-family: 'DM Sans'; + --background: var(--colors-neutral-2); + --actor-hover-background: var(--colors-neutral-3); + --actor-border: '1px solid var(--colors-neutral-4)'; + --actor-border-radius: 0.75rem; + --foreground: var(--colors-white-0); + --font-size: 16px; } diff --git a/src/components/DAOTreasury/components/Transactions.tsx b/src/components/DAOTreasury/components/Transactions.tsx index 59c6ef124..884330977 100644 --- a/src/components/DAOTreasury/components/Transactions.tsx +++ b/src/components/DAOTreasury/components/Transactions.tsx @@ -188,9 +188,8 @@ export function PaginationCount({ shownTransactions }: { shownTransactions: numb type="address" value={safe.address} p={0} + isTextLink textStyle="labels-large" - outline="unset" - outlineOffset="unset" borderWidth={0} > {t('transactionsTotalCount', { count: totalTransfers })} diff --git a/src/components/DaoCreator/formComponents/AzoriusGovernance.tsx b/src/components/DaoCreator/formComponents/AzoriusGovernance.tsx index 37f6509dd..361ef3fdc 100644 --- a/src/components/DaoCreator/formComponents/AzoriusGovernance.tsx +++ b/src/components/DaoCreator/formComponents/AzoriusGovernance.tsx @@ -36,11 +36,11 @@ function DayStepperInput({ - {t('days', { ns: 'common' })} onInputChange(Number(val))} /> diff --git a/src/components/DaoCreator/formComponents/Multisig.tsx b/src/components/DaoCreator/formComponents/Multisig.tsx index c06f88ce8..668cf19e4 100644 --- a/src/components/DaoCreator/formComponents/Multisig.tsx +++ b/src/components/DaoCreator/formComponents/Multisig.tsx @@ -142,11 +142,12 @@ export function Multisig(props: ICreationStepProps) { } isRequired > - {/* @todo replace with stepper input */} - validateNumber(value, 'multisig.signatureThreshold')} - value={values.multisig.signatureThreshold} - /> + + validateNumber(value, 'multisig.signatureThreshold')} + value={values.multisig.signatureThreshold} + /> + diff --git a/src/components/Roles/RolePaymentDetails.tsx b/src/components/Roles/RolePaymentDetails.tsx index 0842494d5..574bcf1d4 100644 --- a/src/components/Roles/RolePaymentDetails.tsx +++ b/src/components/Roles/RolePaymentDetails.tsx @@ -319,8 +319,12 @@ export function RolePaymentDetails({ mx={4} > - + diff --git a/src/components/SafeSettings/ERC20TokenContainer.tsx b/src/components/SafeSettings/ERC20TokenContainer.tsx index aab5f5ad5..4cb1d153e 100644 --- a/src/components/SafeSettings/ERC20TokenContainer.tsx +++ b/src/components/SafeSettings/ERC20TokenContainer.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { useFractal } from '../../providers/App/AppProvider'; import { AzoriusGovernance } from '../../types'; import { formatCoin } from '../../utils'; -import { StyledBox } from '../ui/containers/StyledBox'; import { DisplayAddress } from '../ui/links/DisplayAddress'; import { BarLoader } from '../ui/loaders/BarLoader'; @@ -15,13 +14,17 @@ export function ERC20TokenContainer() { const { votesToken } = azoriusGovernance; return ( - + {t('governanceTokenTitle')} {votesToken ? ( {/* TOKEN NAME */} @@ -76,6 +79,6 @@ export function ERC20TokenContainer() { )} - + ); } diff --git a/src/components/SafeSettings/ERC721TokensContainer.tsx b/src/components/SafeSettings/ERC721TokensContainer.tsx index 0ea09dad3..289f99df5 100644 --- a/src/components/SafeSettings/ERC721TokensContainer.tsx +++ b/src/components/SafeSettings/ERC721TokensContainer.tsx @@ -1,8 +1,7 @@ -import { Flex, Grid, GridItem, Text } from '@chakra-ui/react'; +import { Box, Flex, Grid, GridItem, Text } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; import { useFractal } from '../../providers/App/AppProvider'; import { AzoriusGovernance } from '../../types'; -import { StyledBox } from '../ui/containers/StyledBox'; import { DisplayAddress } from '../ui/links/DisplayAddress'; import { BarLoader } from '../ui/loaders/BarLoader'; @@ -14,7 +13,13 @@ export function ERC721TokensContainer() { const { erc721Tokens } = azoriusGovernance; return ( - + {t('governanceERC721TokenTitle')} {erc721Tokens ? ( @@ -97,6 +102,6 @@ export function ERC721TokensContainer() { )} - + ); } diff --git a/src/components/SafeSettings/SettingsContentBox.tsx b/src/components/SafeSettings/SettingsContentBox.tsx index 7ccb6c09b..d6e58f2f7 100644 --- a/src/components/SafeSettings/SettingsContentBox.tsx +++ b/src/components/SafeSettings/SettingsContentBox.tsx @@ -1,6 +1,5 @@ import { BoxProps } from '@chakra-ui/react'; import { PropsWithChildren } from 'react'; -import { NEUTRAL_2_84 } from '../../constants/common'; import { StyledBox } from '../ui/containers/StyledBox'; export function SettingsContentBox({ children, ...props }: PropsWithChildren) { @@ -9,7 +8,6 @@ export function SettingsContentBox({ children, ...props }: PropsWithChildren {children} diff --git a/src/components/SafeSettings/Signers/SignersContainer.tsx b/src/components/SafeSettings/Signers/SignersContainer.tsx index f6cca9046..460556a7f 100644 --- a/src/components/SafeSettings/Signers/SignersContainer.tsx +++ b/src/components/SafeSettings/Signers/SignersContainer.tsx @@ -1,11 +1,10 @@ -import { Button, Flex, Hide, HStack, Icon, Show, Text } from '@chakra-ui/react'; +import { Box, Button, Flex, Hide, HStack, Icon, Show, Text } from '@chakra-ui/react'; import { MinusCircle, PlusCircle } from '@phosphor-icons/react'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Address, getAddress } from 'viem'; import { useAccount } from 'wagmi'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; -import { StyledBox } from '../../ui/containers/StyledBox'; import { DisplayAddress } from '../../ui/links/DisplayAddress'; import { ModalType } from '../../ui/modals/ModalProvider'; import { useDecentModal } from '../../ui/modals/useDecentModal'; @@ -102,7 +101,7 @@ export function SignersContainer() { }, [account, signers]); return ( - + {t('signers', { ns: 'common' })} {userIsSigner && ( @@ -131,6 +130,6 @@ export function SignersContainer() { threshold={safe?.threshold} /> ))} - + ); } diff --git a/src/components/ui/forms/NumberStepperInput.tsx b/src/components/ui/forms/NumberStepperInput.tsx index 169f767ae..c9b2b5bbd 100644 --- a/src/components/ui/forms/NumberStepperInput.tsx +++ b/src/components/ui/forms/NumberStepperInput.tsx @@ -1,6 +1,8 @@ import { Button, HStack, + InputGroup, + InputRightElement, NumberDecrementStepper, NumberIncrementStepper, NumberInput, @@ -11,9 +13,11 @@ import { Plus, Minus } from '@phosphor-icons/react'; export function NumberStepperInput({ value, onChange, + unitHint, }: { value?: string | number; onChange: (val: string) => void; + unitHint?: string; }) { const stepperButton = (direction: 'inc' | 'dec') => ( } - options={favoritesList.map(favorite => ({ - optionKey: `${favorite.networkPrefix}:${favorite.address}`, - onClick: () => {}, - renderer: () => ( - - ), - }))} + options={ + !favoritesList.length + ? [ + { + optionKey: 'empty-favorites', + onClick: () => {}, + renderer: () => ( + {t('emptyFavorites', { ns: 'dashboard' })} + ), + }, + ] + : favoritesList.map(favorite => ({ + optionKey: `${favorite.networkPrefix}:${favorite.address}`, + onClick: () => {}, + renderer: () => ( + + ), + })) + } buttonAs={Button} buttonProps={{ variant: 'tertiary', diff --git a/src/hooks/DAO/loaders/governance/useERC20LinearToken.ts b/src/hooks/DAO/loaders/governance/useERC20LinearToken.ts index 9a2434234..3ed34423d 100644 --- a/src/hooks/DAO/loaders/governance/useERC20LinearToken.ts +++ b/src/hooks/DAO/loaders/governance/useERC20LinearToken.ts @@ -1,5 +1,5 @@ import { abis } from '@fractal-framework/fractal-contracts'; -import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { getContract } from 'viem'; import { useAccount, usePublicClient } from 'wagmi'; import { useFractal } from '../../../../providers/App/AppProvider'; @@ -10,25 +10,13 @@ export const useERC20LinearToken = ({ onMount = true }: { onMount?: boolean }) = const tokenAccount = useRef(); const { - governanceContracts: { votesTokenAddress, underlyingTokenAddress }, + governanceContracts: { votesTokenAddress }, action, } = useFractal(); const user = useAccount(); const account = user.address; const publicClient = usePublicClient(); - const underlyingTokenContract = useMemo(() => { - if (!underlyingTokenAddress || !publicClient) { - return; - } - - return getContract({ - abi: abis.VotesERC20, - address: underlyingTokenAddress, - client: publicClient, - }); - }, [publicClient, underlyingTokenAddress]); - const loadERC20Token = useCallback(async () => { if (!votesTokenAddress || !publicClient) { return; @@ -57,26 +45,6 @@ export const useERC20LinearToken = ({ onMount = true }: { onMount?: boolean }) = action.dispatch({ type: FractalGovernanceAction.SET_TOKEN_DATA, payload: tokenData }); }, [action, publicClient, votesTokenAddress]); - const loadUnderlyingERC20Token = useCallback(async () => { - if (!underlyingTokenContract) { - return; - } - - const [tokenName, tokenSymbol] = await Promise.all([ - underlyingTokenContract.read.name(), - underlyingTokenContract.read.symbol(), - ]); - const tokenData = { - name: tokenName, - symbol: tokenSymbol, - address: underlyingTokenContract.address, - }; - action.dispatch({ - type: FractalGovernanceAction.SET_UNDERLYING_TOKEN_DATA, - payload: tokenData, - }); - }, [underlyingTokenContract, action]); - const loadERC20TokenAccountData = useCallback(async () => { if (!account || !votesTokenAddress || !publicClient) { action.dispatch({ type: FractalGovernanceAction.RESET_TOKEN_ACCOUNT_DATA }); @@ -209,5 +177,5 @@ export const useERC20LinearToken = ({ onMount = true }: { onMount?: boolean }) = }; }, [account, loadERC20TokenAccountData, onMount, publicClient, votesTokenAddress]); - return { loadERC20Token, loadUnderlyingERC20Token, loadERC20TokenAccountData }; + return { loadERC20Token, loadERC20TokenAccountData }; }; diff --git a/src/hooks/DAO/loaders/useDecentTreasury.ts b/src/hooks/DAO/loaders/useDecentTreasury.ts index 73eb13b1c..5b743079e 100644 --- a/src/hooks/DAO/loaders/useDecentTreasury.ts +++ b/src/hooks/DAO/loaders/useDecentTreasury.ts @@ -22,6 +22,8 @@ import { } from '../../../types'; import { formatCoin } from '../../../utils'; import { MOCK_MORALIS_ETH_ADDRESS } from '../../../utils/address'; +import { CacheExpiry, CacheKeys } from '../../utils/cache/cacheDefaults'; +import { setValue } from '../../utils/cache/useLocalStorage'; export const useDecentTreasury = () => { // tracks the current valid DAO address / chain; helps prevent unnecessary calls @@ -168,12 +170,14 @@ export const useDecentTreasury = () => { // make unique .filter((value, index, self) => self.indexOf(value) === index) // turn them into Address type - .map(address => getAddress(address)); + .map(getAddress); const transfersTokenInfo = await Promise.all( tokenAddressesOfTransfers.map(async address => { + let tokenInfo: TokenInfoResponse; + try { - return await safeAPI.getToken(address); + tokenInfo = await safeAPI.getToken(address); } catch (e) { const fallbackTokenData = tokenBalances?.find( tokenBalanceData => getAddress(tokenBalanceData.tokenAddress) === address, @@ -198,7 +202,7 @@ export const useDecentTreasury = () => { }; } - return { + tokenInfo = { address, name: fallbackTokenData.name, symbol: fallbackTokenData.symbol, @@ -206,6 +210,13 @@ export const useDecentTreasury = () => { logoUri: fallbackTokenData.logo, }; } + + setValue( + { cacheName: CacheKeys.TOKEN_INFO, tokenAddress: address }, + tokenInfo, + CacheExpiry.NEVER, + ); + return tokenInfo; }), ); diff --git a/src/hooks/DAO/loaders/useFavorites.ts b/src/hooks/DAO/loaders/useFavorites.ts index 5b36ae775..2c07abcf1 100644 --- a/src/hooks/DAO/loaders/useFavorites.ts +++ b/src/hooks/DAO/loaders/useFavorites.ts @@ -1,13 +1,22 @@ import { useState } from 'react'; +import { useBetween } from 'use-between'; import { Address } from 'viem'; import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { CacheKeys, CacheExpiry, FavoritesCacheValue } from '../../utils/cache/cacheDefaults'; import { getValue, setValue } from '../../utils/cache/useLocalStorage'; -export const useAccountFavorites = () => { +const useSharedAccountFavorites = () => { const [favoritesList, setFavoritesList] = useState( getValue({ cacheName: CacheKeys.FAVORITES }) || [], ); + return { + favoritesList, + setFavoritesList, + }; +}; + +export const useAccountFavorites = () => { + const { favoritesList, setFavoritesList } = useBetween(useSharedAccountFavorites); const { addressPrefix } = useNetworkConfigStore(); const toggleFavorite = (address: Address, name: string) => { diff --git a/src/hooks/DAO/loaders/useFractalGovernance.ts b/src/hooks/DAO/loaders/useFractalGovernance.ts index b843ae12c..3187a6800 100644 --- a/src/hooks/DAO/loaders/useFractalGovernance.ts +++ b/src/hooks/DAO/loaders/useFractalGovernance.ts @@ -31,7 +31,7 @@ export const useFractalGovernance = () => { const loadDAOProposals = useLoadDAOProposals(); const loadERC20Strategy = useERC20LinearStrategy(); const loadERC721Strategy = useERC721LinearStrategy(); - const { loadERC20Token, loadUnderlyingERC20Token } = useERC20LinearToken({}); + const { loadERC20Token } = useERC20LinearToken({}); const { loadLockedVotesToken } = useLockRelease({}); const loadERC721Tokens = useERC721Tokens(); const ipfsClient = useIPFSClient(); @@ -117,7 +117,6 @@ export const useFractalGovernance = () => { }); loadERC20Strategy(); loadERC20Token(); - loadUnderlyingERC20Token(); if (lockReleaseAddress) { loadLockedVotesToken(); } @@ -138,7 +137,6 @@ export const useFractalGovernance = () => { } }, [ governanceContracts, - loadUnderlyingERC20Token, loadERC20Strategy, loadERC20Token, loadLockedVotesToken, diff --git a/src/hooks/DAO/loaders/useGovernanceContracts.ts b/src/hooks/DAO/loaders/useGovernanceContracts.ts index 412b8c5e6..56e7a7c60 100644 --- a/src/hooks/DAO/loaders/useGovernanceContracts.ts +++ b/src/hooks/DAO/loaders/useGovernanceContracts.ts @@ -6,6 +6,7 @@ import LockReleaseAbi from '../../../assets/abi/LockRelease'; import { useFractal } from '../../../providers/App/AppProvider'; import { GovernanceContractAction } from '../../../providers/App/governanceContracts/action'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; +import { DecentModule } from '../../../types'; import { getAzoriusModuleFromModules } from '../../../utils'; import { useAddressContractType } from '../../utils/useAddressContractType'; import useVotingStrategyAddress from '../../utils/useVotingStrategiesAddresses'; @@ -23,115 +24,111 @@ export const useGovernanceContracts = () => { const safeAddress = safe?.address; - const loadGovernanceContracts = useCallback(async () => { - if (!modules) { - return; - } - const azoriusModule = getAzoriusModuleFromModules(modules); - - const votingStrategies = await getVotingStrategies(); + const loadGovernanceContracts = useCallback( + async (daoModules: DecentModule[]) => { + const azoriusModule = getAzoriusModuleFromModules(daoModules); - if (!azoriusModule || !votingStrategies) { - action.dispatch({ - type: GovernanceContractAction.SET_GOVERNANCE_CONTRACT_ADDRESSES, - payload: {}, - }); - return; - } - - if (!publicClient) { - throw new Error('Public Client is not set!'); - } - - let linearVotingErc20Address: Address | undefined; - let linearVotingErc721Address: Address | undefined; - let linearVotingErc20WithHatsWhitelistingAddress: Address | undefined; - let linearVotingErc721WithHatsWhitelistingAddress: Address | undefined; - let votesTokenAddress: Address | undefined; - let underlyingTokenAddress: Address | undefined; - let lockReleaseAddress: Address | undefined; - - const setGovTokenAddress = async (erc20VotingStrategyAddress: Address) => { - if (votesTokenAddress) { + const votingStrategies = await getVotingStrategies(); + if (!azoriusModule || !votingStrategies) { + action.dispatch({ + type: GovernanceContractAction.SET_GOVERNANCE_CONTRACT_ADDRESSES, + payload: {}, + }); return; } - const ozLinearVotingContract = getContract({ - abi: abis.LinearERC20Voting, - address: erc20VotingStrategyAddress, - client: publicClient, - }); - const govTokenAddress = await ozLinearVotingContract.read.governanceToken(); - // govTokenAddress might be either - // - a valid VotesERC20 contract - // - a valid LockRelease contract - // - or none of these which is against business logic + if (!publicClient) { + throw new Error('Public Client is not set!'); + } - const { isVotesErc20 } = await getAddressContractType(govTokenAddress); + let linearVotingErc20Address: Address | undefined; + let linearVotingErc721Address: Address | undefined; + let linearVotingErc20WithHatsWhitelistingAddress: Address | undefined; + let linearVotingErc721WithHatsWhitelistingAddress: Address | undefined; + let votesTokenAddress: Address | undefined; + let lockReleaseAddress: Address | undefined; - if (isVotesErc20) { - votesTokenAddress = govTokenAddress; - } else { - const possibleLockRelease = getContract({ - address: govTokenAddress, - abi: LockReleaseAbi, - client: { public: publicClient }, + const setGovTokenAddress = async (erc20VotingStrategyAddress: Address) => { + if (votesTokenAddress) { + return; + } + const ozLinearVotingContract = getContract({ + abi: abis.LinearERC20Voting, + address: erc20VotingStrategyAddress, + client: publicClient, }); - try { - const lockedTokenAddress = await possibleLockRelease.read.token(); - lockReleaseAddress = govTokenAddress; - votesTokenAddress = lockedTokenAddress; - } catch { - throw new Error('Unknown governance token type'); + const govTokenAddress = await ozLinearVotingContract.read.governanceToken(); + // govTokenAddress might be either + // - a valid VotesERC20 contract + // - a valid LockRelease contract + // - or none of these which is against business logic + + const { isVotesErc20 } = await getAddressContractType(govTokenAddress); + + if (isVotesErc20) { + votesTokenAddress = govTokenAddress; + } else { + const possibleLockRelease = getContract({ + address: govTokenAddress, + abi: LockReleaseAbi, + client: { public: publicClient }, + }); + + try { + votesTokenAddress = await possibleLockRelease.read.token(); + lockReleaseAddress = govTokenAddress; + } catch { + throw new Error('Unknown governance token type'); + } } + }; + + await Promise.all( + votingStrategies.map(async votingStrategy => { + const { + strategyAddress, + isLinearVotingErc20, + isLinearVotingErc721, + isLinearVotingErc20WithHatsProposalCreation, + isLinearVotingErc721WithHatsProposalCreation, + } = votingStrategy; + if (isLinearVotingErc20) { + linearVotingErc20Address = strategyAddress; + await setGovTokenAddress(strategyAddress); + } else if (isLinearVotingErc721) { + linearVotingErc721Address = strategyAddress; + } else if (isLinearVotingErc20WithHatsProposalCreation) { + linearVotingErc20WithHatsWhitelistingAddress = strategyAddress; + await setGovTokenAddress(strategyAddress); + } else if (isLinearVotingErc721WithHatsProposalCreation) { + linearVotingErc721WithHatsWhitelistingAddress = strategyAddress; + } + }), + ); + + if ( + linearVotingErc20Address || + linearVotingErc20WithHatsWhitelistingAddress || + linearVotingErc721Address || + linearVotingErc721WithHatsWhitelistingAddress + ) { + action.dispatch({ + type: GovernanceContractAction.SET_GOVERNANCE_CONTRACT_ADDRESSES, + payload: { + linearVotingErc20Address, + linearVotingErc20WithHatsWhitelistingAddress, + linearVotingErc721Address, + linearVotingErc721WithHatsWhitelistingAddress, + votesTokenAddress, + lockReleaseAddress, + moduleAzoriusAddress: azoriusModule.moduleAddress, + }, + }); } - }; - - await Promise.all( - votingStrategies.map(async votingStrategy => { - const { - strategyAddress, - isLinearVotingErc20, - isLinearVotingErc721, - isLinearVotingErc20WithHatsProposalCreation, - isLinearVotingErc721WithHatsProposalCreation, - } = votingStrategy; - if (isLinearVotingErc20) { - linearVotingErc20Address = strategyAddress; - await setGovTokenAddress(strategyAddress); - } else if (isLinearVotingErc721) { - linearVotingErc721Address = strategyAddress; - } else if (isLinearVotingErc20WithHatsProposalCreation) { - linearVotingErc20WithHatsWhitelistingAddress = strategyAddress; - await setGovTokenAddress(strategyAddress); - } else if (isLinearVotingErc721WithHatsProposalCreation) { - linearVotingErc721WithHatsWhitelistingAddress = strategyAddress; - } - }), - ); - - if ( - linearVotingErc20Address || - linearVotingErc20WithHatsWhitelistingAddress || - linearVotingErc721Address || - linearVotingErc721WithHatsWhitelistingAddress - ) { - action.dispatch({ - type: GovernanceContractAction.SET_GOVERNANCE_CONTRACT_ADDRESSES, - payload: { - linearVotingErc20Address, - linearVotingErc20WithHatsWhitelistingAddress, - linearVotingErc721Address, - linearVotingErc721WithHatsWhitelistingAddress, - votesTokenAddress, - underlyingTokenAddress, - lockReleaseAddress, - moduleAzoriusAddress: azoriusModule.moduleAddress, - }, - }); - } - }, [action, modules, getVotingStrategies, publicClient, getAddressContractType]); + }, + [action, getVotingStrategies, publicClient, getAddressContractType], + ); useEffect(() => { if ( @@ -139,7 +136,7 @@ export const useGovernanceContracts = () => { currentValidAddress.current !== safeAddress && modules !== null ) { - loadGovernanceContracts(); + loadGovernanceContracts(modules); currentValidAddress.current = safeAddress; } if (!safeAddress) { diff --git a/src/hooks/DAO/loaders/useHatsTree.ts b/src/hooks/DAO/loaders/useHatsTree.ts index e4f0a2e1c..5d7488054 100644 --- a/src/hooks/DAO/loaders/useHatsTree.ts +++ b/src/hooks/DAO/loaders/useHatsTree.ts @@ -19,7 +19,7 @@ import { getValue, setValue } from '../../utils/cache/useLocalStorage'; const hatsSubgraphClient = new HatsSubgraphClient({}); -const useHatsTree = ({ safeAddress }: { safeAddress: Address | undefined }) => { +const useHatsTree = () => { const { t } = useTranslation('roles'); const { governanceContracts: { @@ -34,7 +34,6 @@ const useHatsTree = ({ safeAddress }: { safeAddress: Address | undefined }) => { streamsFetched, setHatsTree, updateRolesWithStreams, - resetHatsStore, } = useRolesStore(); const ipfsClient = useIPFSClient(); @@ -200,6 +199,12 @@ const useHatsTree = ({ safeAddress }: { safeAddress: Address | undefined }) => { ? secondsTimestampToDate(lockupLinearStream.cliffTime) : undefined; + const logo = + getValue({ + cacheName: CacheKeys.TOKEN_INFO, + tokenAddress: getAddress(lockupLinearStream.asset.address), + })?.logoUri || ''; + return { streamId: lockupLinearStream.id, contractAddress: lockupLinearStream.contract.address, @@ -209,7 +214,7 @@ const useHatsTree = ({ safeAddress }: { safeAddress: Address | undefined }) => { name: lockupLinearStream.asset.name, symbol: lockupLinearStream.asset.symbol, decimals: lockupLinearStream.asset.decimals, - logo: '', // @todo - how do we get logo? + logo, }, amount: { bigintValue: BigInt(lockupLinearStream.depositAmount), @@ -291,12 +296,6 @@ const useHatsTree = ({ safeAddress }: { safeAddress: Address | undefined }) => { getHatsStreams(); }, [hatsTree, updateRolesWithStreams, getPaymentStreams, streamsFetched]); - - useEffect(() => { - if (safeAddress === undefined) { - resetHatsStore(); - } - }, [resetHatsStore, safeAddress]); }; export { useHatsTree }; diff --git a/src/hooks/DAO/useKeyValuePairs.ts b/src/hooks/DAO/useKeyValuePairs.ts index 87f7e93c4..ad8b80d86 100644 --- a/src/hooks/DAO/useKeyValuePairs.ts +++ b/src/hooks/DAO/useKeyValuePairs.ts @@ -97,7 +97,7 @@ const useKeyValuePairs = () => { chain, contracts: { keyValuePairs, sablierV2LockupLinear }, } = useNetworkConfigStore(); - const { setHatKeyValuePairData } = useRolesStore(); + const { setHatKeyValuePairData, resetHatsStore } = useRolesStore(); const safeAddress = node.safe?.address; @@ -159,6 +159,14 @@ const useKeyValuePairs = () => { setHatKeyValuePairData, sablierV2LockupLinear, ]); + + useEffect(() => { + if (!safeAddress) { + return; + } + + resetHatsStore(); + }, [resetHatsStore, safeAddress]); }; export { useKeyValuePairs }; diff --git a/src/hooks/utils/cache/cacheDefaults.ts b/src/hooks/utils/cache/cacheDefaults.ts index fb98a8cb3..13b98cee9 100644 --- a/src/hooks/utils/cache/cacheDefaults.ts +++ b/src/hooks/utils/cache/cacheDefaults.ts @@ -1,3 +1,4 @@ +import { TokenInfoResponse } from '@safe-global/api-kit'; import { Address } from 'viem'; import { AzoriusProposal, DaoHierarchyInfo } from '../../../types'; @@ -34,52 +35,60 @@ export enum CacheKeys { MIGRATION = 'Migration', IPFS_HASH = 'IPFS Hash', HIERARCHY_DAO_INFO = 'Hierarchy DAO Info', + TOKEN_INFO = 'Token Info', // indexDB keys DECODED_TRANSACTION_PREFIX = 'decode_trans_', MULTISIG_METADATA_PREFIX = 'm_m_', } -export type CacheKey = { +type CacheKey = { cacheName: CacheKeys; version: number; }; -export interface FavoritesCacheKey extends CacheKey { +interface FavoritesCacheKey extends CacheKey { cacheName: CacheKeys.FAVORITES; } -export interface MasterCacheKey extends CacheKey { +interface MasterCacheKey extends CacheKey { cacheName: CacheKeys.MASTER_COPY; chainId: number; proxyAddress: Address; moduleProxyFactoryAddress: Address; } -export interface ProposalCacheKey extends CacheKey { +interface ProposalCacheKey extends CacheKey { cacheName: CacheKeys.PROPOSAL_CACHE; proposalId: string; contractAddress: Address; } -export interface AverageBlockTimeCacheKey extends CacheKey { +interface AverageBlockTimeCacheKey extends CacheKey { cacheName: CacheKeys.AVERAGE_BLOCK_TIME; chainId: number; } -export interface IPFSHashCacheKey extends CacheKey { +interface IPFSHashCacheKey extends CacheKey { cacheName: CacheKeys.IPFS_HASH; hash: string; chainId: number; } -export interface HierarchyDAOInfoCacheKey extends CacheKey { +interface HierarchyDAOInfoCacheKey extends CacheKey { cacheName: CacheKeys.HIERARCHY_DAO_INFO; chainId: number; daoAddress: Address; } +interface TokenInfoCacheKey extends CacheKey { + cacheName: CacheKeys.TOKEN_INFO; + chainId: number; + tokenAddress: Address; +} + export type CacheKeyType = | FavoritesCacheKey | HierarchyDAOInfoCacheKey + | TokenInfoCacheKey | MasterCacheKey | ProposalCacheKey | AverageBlockTimeCacheKey @@ -105,6 +114,7 @@ type CacheKeyToValueMap = { [CacheKeys.MIGRATION]: number; [CacheKeys.IPFS_HASH]: string; [CacheKeys.HIERARCHY_DAO_INFO]: DaoHierarchyInfo; + [CacheKeys.TOKEN_INFO]: TokenInfoResponse; }; export type CacheValueType = T extends { cacheName: infer U } @@ -123,6 +133,7 @@ export const CACHE_VERSIONS: { [key: string]: number } = Object.freeze({ [CacheKeys.PROPOSAL_CACHE]: 1, [CacheKeys.AVERAGE_BLOCK_TIME]: 1, [CacheKeys.HIERARCHY_DAO_INFO]: 1, + [CacheKeys.TOKEN_INFO]: 1, }); /** diff --git a/src/i18n/locales/en/breadcrumbs.json b/src/i18n/locales/en/breadcrumbs.json index f38ecdab1..ac37a1bf7 100644 --- a/src/i18n/locales/en/breadcrumbs.json +++ b/src/i18n/locales/en/breadcrumbs.json @@ -12,6 +12,6 @@ "proposalTemplate": "{{proposalTemplateTitle}}", "proposalTemplateNew": "Create Proposal Template", "editDAO": "Edit DAO", - "parentLink": "Parent-Safe", + "parentLink": "Parent DAO", "settings": "Settings" } diff --git a/src/i18n/locales/en/daoCreate.json b/src/i18n/locales/en/daoCreate.json index ded50b146..491a8553c 100644 --- a/src/i18n/locales/en/daoCreate.json +++ b/src/i18n/locales/en/daoCreate.json @@ -1,10 +1,10 @@ { "addNFTButton": "Import another token", "errorMinSigners": "Number of owners must be greater than 0.", - "errorLowSignerThreshold": "Threshold must be greater than 0", + "errorLowSignerThreshold": "Threshold must be greater than 0.", "errorHighSignerThreshold": "Threshold must be less than number of owners.", "labelSigThreshold": "Threshold", - "helperSigThreshold": "The number of owners required to approve a transaction on this multisig", + "helperSigThreshold": "The number of owners required to approve a transaction on this multisig.", "titleSignerAddresses": "Owners", "subTitleSignerAddresses": "These addresses will be able to approve or reject transactions on this multisig.", "titleGetStarted": "Get Started", @@ -12,18 +12,18 @@ "titleConfigureERC721": "Configure ERC-721 NFTs", "titleConfigureMultisig": "Configure Multisig", "titleSignerConfig": "Signer Parameters", - "titleGovConfig": "Governance Parameters", + "titleGovConfig": "Governance", "titleFunding": "Add Funding", "titleNFTsParams": "Import NFT", "titleNFTDetails": "Details", "nftDetailsToken": "Token", "nftDetailsWeight": "Weight", "n/a": "n/a", - "labelConnectWallet": "To deploy a new Decent Safe", - "titleFundingOptions": "Go to parent Safe", + "labelConnectWallet": "To deploy a new DAO", + "titleFundingOptions": "Go to parent DAO", "labelSelectNFT": "Select NFT", "labelNFTAddress": "NFT Address", - "helperNFTAddress": "Import an existing ERC-721 token", + "helperNFTAddress": "Import an existing ERC-721 token.", "labelNFTWeight": "Weight", "helperNFTWeight": "How many votes is this token worth?", "labelVotingPeriod": "Voting Period", @@ -40,19 +40,19 @@ "labelTimelockPeriod": "Timelock Period", "helperTimelockPeriod": "The length of time required between passing a proposal and it being executable onchain.", "labelFreezeVotesThreshold": "Freeze Votes Threshold", - "helperFreezeVotesThreshold": "Total votes required by the parent (out of {{totalVotes}}) to freeze this child Safe entirely", + "helperFreezeVotesThreshold": "Total votes required by the parent (out of {{totalVotes}}) to freeze this child DAO entirely.", "labelFreezeProposalPeriod": "Freeze Proposal Period", - "helperFreezeProposalPeriod": "The length of time (in minutes) for a Freeze Vote's starting and ending point", + "helperFreezeProposalPeriod": "The length of time (in minutes) for a Freeze Vote's starting and ending point.", "exampleFreezeProposalPeriod": "10,080 minutes = 1 week", "labelFreezePeriod": "Freeze Period", - "helperFreezePeriod": "The length of time (in minutes) a successful Freeze Vote will freeze this child Safe", + "helperFreezePeriod": "The length of time (in minutes) a successful Freeze Vote will freeze this child DAO.", "exampleFreezePeriod": "10,080 minutes = 1 week", - "freezeGuardDescription": "These configuration values may be changed later on by passing a proposal to do so on this child Safe", + "freezeGuardDescription": "This configuration may be changed later via a proposal on this child DAO.", "errorDuplicateAddress": "Duplicate Address", "errorNoAllocation": "Enter a token allocation", "errorAllocation": "Invalid token allocation", "labelParentAllocation": "Parent token holder claiming", - "helperParentAllocation": "The total number of tokens claimable by all parent token holders", + "helperParentAllocation": "The total number of tokens claimable by all parent token holders.", "labelAddAllocation": "Add Allocation", "labelAddProposer": "", "labelTokenName": "Name", @@ -64,13 +64,13 @@ "labelMultisigGov": "Multisig (m of n)", "descMultisigGov": "Best for small groups and/or frequent onchain activity.", "labelAzoriusErc20Gov": "ERC-20 Token Voting", - "labelAzoriusErc20HatsWhitelistingGov": "Token Voting + Whitelisted Proposers Safe", + "labelAzoriusErc20HatsWhitelistingGov": "Token Voting + Whitelisted Proposers DAO", "descAzoriusErc20Gov": "Best for distributing power through liquid fungible tokens.", - "descAzoriusErc20HatsWhitelistingGov": "Proposals can be created only by whitelisted Roles members", + "descAzoriusErc20HatsWhitelistingGov": "Proposals can be created only by whitelisted Roles members.", "labelAzoriusErc721Gov": "ERC-721 Token Voting", - "labelAzoriusErc721HatsWhitelistingGov": "NFT Voting + Whitelisted Proposers Safe", + "labelAzoriusErc721HatsWhitelistingGov": "NFT Voting + Whitelisted Proposers DAO", "descAzoriusErc721Gov": "Best for adding governance capabilities to an NFT collection.", - "descAzoriusErc721HatsWhitelistingGov": "Proposals can be created only by whitelisted Roles members", + "descAzoriusErc721HatsWhitelistingGov": "Proposals can be created only by whitelisted Roles members.", "labelDAOName": "Name", "daoNamePlaceholder": "Name", "helperDAOName": "What is your DAO called?", @@ -82,7 +82,7 @@ "titleTokenContract": "Contract", "titleAddress": "Address", "titleAmount": "Amount", - "createSubDAOPendingToastMessage": "Creating your child Safe proposal", + "createSubDAOPendingToastMessage": "Creating your child DAO proposal", "createSubDAOSuccessToastMessage": "Deployment successful", "createSubDAOFailureToastMessage": "Deployment failed", "helperAllocations": "Any unallocated tokens will be placed in the newly deployed DAO treasury.", @@ -101,8 +101,8 @@ "tooltipNftVoting": "NFT Voting allows a group of ERC-721 (NFT) holders to propose and vote on transactions. Multiple NFT addresses can be used. <1>Learn more", "errorUnsupportedCreateOption": "Previously selected Governance option is not supported on this network.", "attachFractalModuleLabel": "Enable Clawback", - "attachFractalModuleDescription": "This setting controls whether Parent Safe will be able to execute arbitrary transactions on Child Safe bypassing voting process on Child Safe.", - "fractalModuleAttachedDescription": "This setting can not be modified as Fractal Module already attached to the Safe.", "networks": "Networks", - "networkDescription": "What network would you like to deploy this DAO on?" + "networkDescription": "What network would you like to deploy this DAO on?", + "attachFractalModuleDescription": "This setting controls whether Parent DAO will be able to execute arbitrary transactions on Child DAO bypassing voting process on Child DAO.", + "fractalModuleAttachedDescription": "This setting can not be modified as Fractal Module already attached to the DAO." } diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index c33e6f921..3536ecd62 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -15,7 +15,7 @@ "modulesAndGuardsTitle": "Modules and Guards", "permissionsTitle": "Permissions", "daoSettingsGeneral": "General", - "daoSettingsGovernance": "Governance Parameters", + "daoSettingsGovernance": "Governance", "daoMetadataName": "DAO Name", "daoMetadataSnapshot": "Snapshot Space", "daoMetadataConnectSnapshot": "Connect to Snapshot Space", diff --git a/src/models/AzoriusTxBuilder.ts b/src/models/AzoriusTxBuilder.ts index b4ddb411b..b777efcfe 100644 --- a/src/models/AzoriusTxBuilder.ts +++ b/src/models/AzoriusTxBuilder.ts @@ -88,8 +88,13 @@ export class AzoriusTxBuilder extends BaseTxBuilder { if (daoData.votingStrategyType === VotingStrategyType.LINEAR_ERC20) { daoData = daoData as AzoriusERC20DAO; - if (daoData.isVotesToken) { - this.predictedTokenAddress = daoData.tokenImportAddress as Address; + if (!daoData.isTokenImported) { + this.setEncodedSetupTokenData(); + this.setPredictedTokenAddress(); + } else { + if (daoData.isVotesToken) { + this.predictedTokenAddress = daoData.tokenImportAddress as Address; + } } } } diff --git a/src/models/DaoTxBuilder.ts b/src/models/DaoTxBuilder.ts index eaf0c019f..f06917c8b 100644 --- a/src/models/DaoTxBuilder.ts +++ b/src/models/DaoTxBuilder.ts @@ -136,7 +136,7 @@ export class DaoTxBuilder extends BaseTxBuilder { ]); // build token if token is not imported - if (data.isVotesToken && data.votingStrategyType === VotingStrategyType.LINEAR_ERC20) { + if (!data.isTokenImported && data.votingStrategyType === VotingStrategyType.LINEAR_ERC20) { txs.push(azoriusTxBuilder.buildCreateTokenTx()); } diff --git a/src/pages/dao/SafeController.tsx b/src/pages/dao/SafeController.tsx index 76b142c25..52dd46844 100644 --- a/src/pages/dao/SafeController.tsx +++ b/src/pages/dao/SafeController.tsx @@ -44,7 +44,7 @@ export function SafeController() { useAzoriusListeners(); useKeyValuePairs(); - useHatsTree({ safeAddress }); + useHatsTree(); // the order of the if blocks of these next three error states matters if (invalidQuery) { diff --git a/src/pages/dao/roles/edit/details/SafeRoleEditDetailsPage.tsx b/src/pages/dao/roles/edit/details/SafeRoleEditDetailsPage.tsx index 70c0c5389..e4991c5ed 100644 --- a/src/pages/dao/roles/edit/details/SafeRoleEditDetailsPage.tsx +++ b/src/pages/dao/roles/edit/details/SafeRoleEditDetailsPage.tsx @@ -46,7 +46,6 @@ export function SafeRoleEditDetailsPage() { if (hatIndex === undefined) return null; const role = values.hats[hatIndex]; - if (!role) return null; const goBackToRolesEdit = () => { backupRoleEditing.current = values.roleEditing; @@ -101,7 +100,7 @@ export function SafeRoleEditDetailsPage() { path: DAO_ROUTES.rolesEdit.relative(addressPrefix, safe.address), }, { - terminus: role.name ?? t('new'), + terminus: role?.name ?? t('new'), path: '', }, ]} diff --git a/src/pages/dao/settings/governance/SafeGovernanceSettingsPage.tsx b/src/pages/dao/settings/governance/SafeGovernanceSettingsPage.tsx index 5841d80f4..97a483aa1 100644 --- a/src/pages/dao/settings/governance/SafeGovernanceSettingsPage.tsx +++ b/src/pages/dao/settings/governance/SafeGovernanceSettingsPage.tsx @@ -1,4 +1,4 @@ -import { Show, Text } from '@chakra-ui/react'; +import { Box, Show, Text } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; import { zeroAddress } from 'viem'; import { InfoGovernance } from '../../../../components/DaoDashboard/Info/InfoGovernance'; @@ -6,7 +6,6 @@ import { ERC20TokenContainer } from '../../../../components/SafeSettings/ERC20To import { ERC721TokensContainer } from '../../../../components/SafeSettings/ERC721TokensContainer'; import { SettingsContentBox } from '../../../../components/SafeSettings/SettingsContentBox'; import { SignersContainer } from '../../../../components/SafeSettings/Signers/SignersContainer'; -import { StyledBox } from '../../../../components/ui/containers/StyledBox'; import NestedPageHeader from '../../../../components/ui/page/Header/NestedPageHeader'; import { DAO_ROUTES } from '../../../../constants/routes'; import { useFractal } from '../../../../providers/App/AppProvider'; @@ -40,18 +39,26 @@ export function SafeGovernanceSettingsPage() { {(isERC20Governance || isERC721Governance) && ( - + {t('daoSettingsGovernance')} - - + + + + )} {isERC20Governance ? ( diff --git a/src/pages/dao/settings/permissions/SafePermissionsSettingsPage.tsx b/src/pages/dao/settings/permissions/SafePermissionsSettingsPage.tsx index bb331a6a2..5edbb0327 100644 --- a/src/pages/dao/settings/permissions/SafePermissionsSettingsPage.tsx +++ b/src/pages/dao/settings/permissions/SafePermissionsSettingsPage.tsx @@ -152,15 +152,17 @@ export function SafePermissionsSettingsPage() { - } - aria-label={t('edit')} - opacity={0} - color="neutral-6" - border="none" - /> + {canUserCreateProposal && ( + } + aria-label={t('edit')} + opacity={0} + color="neutral-6" + border="none" + /> + )} )} diff --git a/src/providers/App/governance/action.ts b/src/providers/App/governance/action.ts index db3dd64f2..42c7045b9 100644 --- a/src/providers/App/governance/action.ts +++ b/src/providers/App/governance/action.ts @@ -6,7 +6,6 @@ import { FractalProposalState, VotesData, VotingStrategy, - UnderlyingTokenData, GovernanceType, ERC721TokenData, } from '../../../types'; @@ -30,7 +29,6 @@ export enum FractalGovernanceAction { SET_TOKEN_ACCOUNT_DATA = 'SET_TOKEN_ACCOUNT_DATA', SET_CLAIMING_CONTRACT = 'SET_CLAIMING_CONTRACT', RESET_TOKEN_ACCOUNT_DATA = 'RESET_TOKEN_ACCOUNT_DATA', - SET_UNDERLYING_TOKEN_DATA = 'SET_UNDERLYING_TOKEN_DATA', PENDING_PROPOSAL_ADD = 'PENDING_PROPOSAL_ADD', } @@ -88,10 +86,6 @@ export type FractalGovernanceActions = type: FractalGovernanceAction.SET_TOKEN_DATA; payload: ERC20TokenData; } - | { - type: FractalGovernanceAction.SET_UNDERLYING_TOKEN_DATA; - payload: UnderlyingTokenData; - } | { type: FractalGovernanceAction.SET_TOKEN_ACCOUNT_DATA; payload: VotesData; diff --git a/src/providers/App/governance/reducer.ts b/src/providers/App/governance/reducer.ts index 83369377f..3e29e341b 100644 --- a/src/providers/App/governance/reducer.ts +++ b/src/providers/App/governance/reducer.ts @@ -185,10 +185,6 @@ export const governanceReducer = (state: FractalGovernance, action: FractalGover case FractalGovernanceAction.SET_ERC721_TOKENS_DATA: { return { ...state, erc721Tokens: action.payload }; } - case FractalGovernanceAction.SET_UNDERLYING_TOKEN_DATA: { - const { votesToken } = state as AzoriusGovernance; - return { ...state, votesToken: { ...votesToken, underlyingTokenData: action.payload } }; - } case FractalGovernanceAction.SET_TOKEN_ACCOUNT_DATA: { const { votesToken } = state as AzoriusGovernance; return { ...state, votesToken: { ...votesToken, ...action.payload } }; diff --git a/src/store/roles/useRolesStore.ts b/src/store/roles/useRolesStore.ts index 3f6da74f1..40c4cc86a 100644 --- a/src/store/roles/useRolesStore.ts +++ b/src/store/roles/useRolesStore.ts @@ -117,12 +117,11 @@ const useRolesStore = create()((set, get) => ({ const filteredStreamIds = streamIdsToHatIdsMap .filter(ids => ids.hatId === BigInt(roleHat.id)) .map(ids => ids.streamId); + return { ...roleHat, payments: roleHat.isTermed - ? roleHat.payments?.filter(payment => { - return filteredStreamIds.includes(payment.streamId); - }) + ? roleHat.payments?.filter(payment => filteredStreamIds.includes(payment.streamId)) : roleHat.payments, }; }), diff --git a/src/types/account.ts b/src/types/account.ts index 6f0281c86..c1585af17 100644 --- a/src/types/account.ts +++ b/src/types/account.ts @@ -6,10 +6,6 @@ export interface VotesData { delegatee: Address | null; votingWeight: bigint | null; } -export type UnderlyingTokenData = Omit< - ERC20TokenData, - 'totalSupply' | 'decimals' | 'underlyingTokenData' ->; export interface BaseTokenData { name: string; @@ -19,7 +15,6 @@ export interface BaseTokenData { export interface ERC20TokenData extends BaseTokenData { decimals: number; totalSupply: bigint; - underlyingTokenData?: UnderlyingTokenData; } export interface ERC721TokenData extends BaseTokenData { diff --git a/src/types/fractal.ts b/src/types/fractal.ts index 19fd85439..7013ab696 100644 --- a/src/types/fractal.ts +++ b/src/types/fractal.ts @@ -216,7 +216,6 @@ export interface FractalGovernanceContracts { moduleAzoriusAddress?: Address; votesTokenAddress?: Address; lockReleaseAddress?: Address; - underlyingTokenAddress?: Address; isLoaded: boolean; }