diff --git a/src/components/DaoCreator/formComponents/AzoriusGovernance.tsx b/src/components/DaoCreator/formComponents/AzoriusGovernance.tsx index c7acda7f76..81a593907f 100644 --- a/src/components/DaoCreator/formComponents/AzoriusGovernance.tsx +++ b/src/components/DaoCreator/formComponents/AzoriusGovernance.tsx @@ -25,16 +25,12 @@ import { DAOCreateMode } from './EstablishEssentials'; export function AzoriusGovernance(props: ICreationStepProps) { const { values, setFieldValue, isSubmitting, transactionPending, isSubDAO, mode } = props; - const { - safe, - nodeHierarchy: { parentAddress }, - fractalModules, - } = useDaoInfoStore(); + const { safe, subgraphInfo, modules } = useDaoInfoStore(); - const fractalModule = useMemo( - () => fractalModules.find(_module => _module.moduleType === FractalModuleType.FRACTAL), - [fractalModules], - ); + const fractalModule = useMemo(() => { + if (!modules) return null; + return modules.find(_module => _module.moduleType === FractalModuleType.FRACTAL); + }, [modules]); const [showCustomNonce, setShowCustomNonce] = useState(); const { t } = useTranslation(['daoCreate', 'common']); @@ -189,7 +185,7 @@ export function AzoriusGovernance(props: ICreationStepProps) { - {!!parentAddress && ( + {!!subgraphInfo?.parentAddress && ( { - if (isEdit) { - setFieldValue('essentials.daoName', daoName, false); - if (safeAddress && createAccountSubstring(safeAddress) !== daoName) { + if (isEdit && subgraphInfo) { + setFieldValue('essentials.daoName', subgraphInfo.daoName, false); + if (safeAddress && createAccountSubstring(safeAddress) !== subgraphInfo.daoName) { // Pre-fill the snapshot URL form field when editing - setFieldValue('essentials.snapshotENS', daoSnapshotENS || '', false); + setFieldValue('essentials.snapshotENS', subgraphInfo.daoSnapshotENS || '', false); } } - }, [setFieldValue, mode, daoName, daoSnapshotENS, isEdit, safeAddress]); + }, [ + setFieldValue, + mode, + subgraphInfo?.daoName, + subgraphInfo?.daoSnapshotENS, + isEdit, + safeAddress, + subgraphInfo, + ]); const daoNameDisabled = - isEdit && !!daoName && !!safeAddress && createAccountSubstring(safeAddress) !== daoName; + isEdit && + !!subgraphInfo?.daoName && + !!safeAddress && + createAccountSubstring(safeAddress) !== subgraphInfo?.daoName; // If in governance edit mode and snapshot URL is already set, disable the field - const snapshotENSDisabled = isEdit && !!daoSnapshotENS; + const snapshotENSDisabled = isEdit && !!subgraphInfo?.daoSnapshotENS; const handleGovernanceChange = (value: string) => { if (value === GovernanceType.AZORIUS_ERC20) { diff --git a/src/components/DaoDashboard/Info/ParentLink.tsx b/src/components/DaoDashboard/Info/ParentLink.tsx index b51610977d..0adf1499dd 100644 --- a/src/components/DaoDashboard/Info/ParentLink.tsx +++ b/src/components/DaoDashboard/Info/ParentLink.tsx @@ -9,11 +9,11 @@ import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; * Displays a link to the current DAO's parent, if it has one. */ export function ParentLink() { - const { nodeHierarchy } = useDaoInfoStore(); + const { subgraphInfo } = useDaoInfoStore(); const { addressPrefix } = useNetworkConfig(); const { t } = useTranslation('breadcrumbs'); - if (!nodeHierarchy.parentAddress) { + if (!subgraphInfo?.parentAddress) { return null; } @@ -21,7 +21,7 @@ export function ParentLink() { { - if (!child.safe) { - throw new Error('Child node does not have a safe'); - } - return { - safeAddress: child.safe.address, - }; - }), - }, - }; -} - /** * A recursive component that displays a "hierarchy" of DAOInfoCards. * @@ -49,10 +39,130 @@ export function DaoHierarchyNode({ depth: number; }) { const { safe: currentSafe } = useDaoInfoStore(); + const { t } = useTranslation('common'); + const safeApi = useSafeAPI(); const [hierarchyNode, setHierarchyNode] = useState(); - const { addressPrefix } = useNetworkConfig(); + const [hasErrorLoading, setErrorLoading] = useState(false); + const { addressPrefix, subgraph } = useNetworkConfig(); const chainId = useChainId(); - const { loadDao } = useLoadDAONode(); + const publicClient = usePublicClient(); + + const { getAddressContractType } = useAddressContractType(); + const lookupModules = useDecentModules(); + + const [getDAOInfo] = useLazyQuery(DAOQueryDocument, { + context: { + subgraphSpace: subgraph.space, + subgraphSlug: subgraph.slug, + subgraphVersion: subgraph.version, + }, + }); + + const getVotingStrategies = useCallback( + async (azoriusModule: DecentModule) => { + if (!publicClient) { + throw new Error('Public Client is not set!'); + } + + const azoriusContract = getContract({ + abi: abis.Azorius, + address: azoriusModule.moduleAddress, + client: publicClient, + }); + + const [strategies, nextStrategy] = await azoriusContract.read.getStrategies([ + SENTINEL_ADDRESS, + 3n, + ]); + const result = Promise.all( + [...strategies, nextStrategy] + .filter( + strategyAddress => + strategyAddress !== SENTINEL_ADDRESS && strategyAddress !== zeroAddress, + ) + .map(async strategyAddress => ({ + ...(await getAddressContractType(strategyAddress)), + strategyAddress, + })), + ); + + return result; + }, + [getAddressContractType, publicClient], + ); + + const getGovernanceTypes = useCallback( + async (azoriusModule: DecentModule) => { + const votingStrategies = await getVotingStrategies(azoriusModule); + + if (!votingStrategies) { + throw new Error('No voting strategies found'); + } + + if (!publicClient) { + throw new Error('Public Client is not set!'); + } + + let governanceTypes: DaoHierarchyStrategyType[] = []; + + await Promise.all( + votingStrategies.map(async votingStrategy => { + const { + isLinearVotingErc20, + isLinearVotingErc721, + isLinearVotingErc20WithHatsProposalCreation, + isLinearVotingErc721WithHatsProposalCreation, + } = votingStrategy; + if (isLinearVotingErc20) { + governanceTypes.push('ERC-20'); + } else if (isLinearVotingErc721) { + governanceTypes.push('ERC-721'); + } else if (isLinearVotingErc20WithHatsProposalCreation) { + governanceTypes.push('ERC-20'); + } else if (isLinearVotingErc721WithHatsProposalCreation) { + governanceTypes.push('ERC-721'); + } + }), + ); + return governanceTypes; + }, + [getVotingStrategies, publicClient], + ); + + const loadDao = useCallback( + async (_safeAddress: Address): Promise => { + if (!safeApi) { + throw new Error('Safe API not ready'); + } + try { + const safe = await safeApi.getSafeInfo(_safeAddress); + const graphRawNodeData = await getDAOInfo({ variables: { safeAddress: _safeAddress } }); + const modules = await lookupModules(safe.modules); + const graphDAOData = graphRawNodeData.data?.daos[0]; + const azoriusModule = getAzoriusModuleFromModules(modules ?? []); + const votingStrategies: DaoHierarchyStrategyType[] = azoriusModule + ? await getGovernanceTypes(azoriusModule) + : ['MULTISIG']; + if (!graphRawNodeData || !graphDAOData) { + throw new Error('No data found'); + } + return { + daoName: graphDAOData.name ?? null, + safeAddress: _safeAddress, + parentAddress: graphDAOData.parentAddress ?? null, + childAddresses: graphDAOData.hierarchy.map(child => child.address), + daoSnapshotENS: graphDAOData.snapshotENS ?? null, + proposalTemplatesHash: graphDAOData.proposalTemplatesHash ?? null, + modules, + votingStrategies, + }; + } catch (e) { + setErrorLoading(true); + return; + } + }, + [getDAOInfo, getGovernanceTypes, lookupModules, safeApi], + ); useEffect(() => { if (safeAddress) { @@ -66,22 +176,18 @@ export function DaoHierarchyNode({ return; } loadDao(safeAddress).then(_node => { - const errorNode = _node as WithError; - if (!errorNode.error) { - const fnode = _node as DaoInfo; - const theNode = transformNode(fnode, safeAddress); - setValue( - { - cacheName: CacheKeys.HIERARCHY_DAO_INFO, - chainId, - daoAddress: safeAddress, - }, - theNode, - ); - setHierarchyNode(theNode); - } else if (errorNode.error === 'errorFailedSearch') { - setHierarchyNode(undefined); + if (!_node) { + setErrorLoading(true); } + setValue( + { + cacheName: CacheKeys.HIERARCHY_DAO_INFO, + chainId, + daoAddress: safeAddress, + }, + _node, + ); + setHierarchyNode(_node); }); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -100,6 +206,29 @@ export function DaoHierarchyNode({ ); } + if (hasErrorLoading) { + return ( + +
+ + {t('errorMySafesNotLoaded')} + +
+
+ ); + } + const isCurrentViewingDAO = currentSafe?.address.toLowerCase() === hierarchyNode.safeAddress.toLocaleLowerCase(); return ( @@ -124,15 +253,16 @@ export function DaoHierarchyNode({ daoName={hierarchyNode?.daoName ?? hierarchyNode.safeAddress} daoSnapshotENS={hierarchyNode?.daoSnapshotENS} isCurrentViewingDAO={isCurrentViewingDAO} + votingStrategies={hierarchyNode.votingStrategies} /> {/* CHILD NODES */} - {hierarchyNode?.nodeHierarchy.childNodes.map(childNode => { + {hierarchyNode?.childAddresses.map(childAddress => { return ( diff --git a/src/components/Proposals/MultisigProposalDetails/TxActions.tsx b/src/components/Proposals/MultisigProposalDetails/TxActions.tsx index 7eef7242de..216c3146d6 100644 --- a/src/components/Proposals/MultisigProposalDetails/TxActions.tsx +++ b/src/components/Proposals/MultisigProposalDetails/TxActions.tsx @@ -2,7 +2,7 @@ import { Box, Button, Text, Flex } from '@chakra-ui/react'; import { abis } from '@fractal-framework/fractal-contracts'; import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { getAddress, getContract, isHex } from 'viem'; +import { getAddress, getContract, isHex, zeroAddress } from 'viem'; import { useAccount, useWalletClient } from 'wagmi'; import GnosisSafeL2Abi from '../../../assets/abi/GnosisSafeL2'; import { Check } from '../../../assets/theme/custom/icons/Check'; @@ -50,7 +50,7 @@ export function TxActions({ proposal }: { proposal: MultisigProposal }) { const { loadSafeMultisigProposals } = useSafeMultisigProposals(); const { data: walletClient } = useWalletClient(); - const isOwner = safe?.owners?.includes(userAccount.address || ''); + const isOwner = safe?.owners?.includes(userAccount.address ?? zeroAddress); if (!isOwner) return null; diff --git a/src/components/Proposals/ProposalInfo.tsx b/src/components/Proposals/ProposalInfo.tsx index 3abfeda4d3..dbe0237e42 100644 --- a/src/components/Proposals/ProposalInfo.tsx +++ b/src/components/Proposals/ProposalInfo.tsx @@ -23,7 +23,7 @@ export function ProposalInfo({ }) { const metaData = useGetMetadata(proposal); const { t } = useTranslation('proposal'); - const { daoSnapshotENS } = useDaoInfoStore(); + const { subgraphInfo } = useDaoInfoStore(); const { snapshotProposal } = useSnapshotProposal(proposal); const [modalType, props] = useMemo(() => { @@ -62,10 +62,10 @@ export function ProposalInfo({ showIcon={false} textColor="neutral-7" /> - {snapshotProposal && ( + {snapshotProposal && subgraphInfo && ( <> {(proposal as ExtendedSnapshotProposal).privacy === 'shutter' && (