From b921b82a6d26104fe2ed7b188f017f50df5f17c1 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Tue, 17 Dec 2024 23:50:36 -0500 Subject: [PATCH 01/10] Refactor useKeyValuePairs to check for undefined safeAddress before resetting hats store --- src/hooks/DAO/useKeyValuePairs.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/hooks/DAO/useKeyValuePairs.ts b/src/hooks/DAO/useKeyValuePairs.ts index ad8b80d86..97516c7eb 100644 --- a/src/hooks/DAO/useKeyValuePairs.ts +++ b/src/hooks/DAO/useKeyValuePairs.ts @@ -161,11 +161,9 @@ const useKeyValuePairs = () => { ]); useEffect(() => { - if (!safeAddress) { - return; + if (safeAddress === undefined) { + resetHatsStore(); } - - resetHatsStore(); }, [resetHatsStore, safeAddress]); }; From 264e14ea8537166c2dcc9ea19c29a7d15bb978e5 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Tue, 17 Dec 2024 23:54:08 -0500 Subject: [PATCH 02/10] remove streams retreival from `useHatsTree` --- src/hooks/DAO/loaders/useHatsTree.ts | 147 +-------------------------- 1 file changed, 2 insertions(+), 145 deletions(-) diff --git a/src/hooks/DAO/loaders/useHatsTree.ts b/src/hooks/DAO/loaders/useHatsTree.ts index 5d7488054..a972a3ac1 100644 --- a/src/hooks/DAO/loaders/useHatsTree.ts +++ b/src/hooks/DAO/loaders/useHatsTree.ts @@ -1,19 +1,14 @@ import { useApolloClient } from '@apollo/client'; import { HatsSubgraphClient, Tree } from '@hatsprotocol/sdk-v1-subgraph'; -import { useCallback, useEffect } from 'react'; +import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; -import { Address, formatUnits, getAddress, getContract } from 'viem'; import { usePublicClient } from 'wagmi'; -import { StreamsQueryDocument } from '../../../../.graphclient'; -import { SablierV2LockupLinearAbi } from '../../../assets/abi/SablierV2LockupLinear'; import { useFractal } from '../../../providers/App/AppProvider'; import useIPFSClient from '../../../providers/App/hooks/useIPFSClient'; import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { DecentHatsError } from '../../../store/roles/rolesStoreUtils'; import { useRolesStore } from '../../../store/roles/useRolesStore'; -import { SablierPayment } from '../../../types/roles'; -import { convertStreamIdToBigInt } from '../../streams/useCreateSablierStream'; import { CacheExpiry, CacheKeys } from '../../utils/cache/cacheDefaults'; import { getValue, setValue } from '../../utils/cache/useLocalStorage'; @@ -27,18 +22,10 @@ const useHatsTree = () => { linearVotingErc721WithHatsWhitelistingAddress, }, } = useFractal(); - const { - hatsTreeId, - contextChainId, - hatsTree, - streamsFetched, - setHatsTree, - updateRolesWithStreams, - } = useRolesStore(); + const { hatsTreeId, contextChainId, setHatsTree } = useRolesStore(); const ipfsClient = useIPFSClient(); const { - sablierSubgraph, contracts: { hatsProtocol, erc6551Registry, @@ -47,7 +34,6 @@ const useHatsTree = () => { }, } = useNetworkConfigStore(); const publicClient = usePublicClient(); - const apolloClient = useApolloClient(); useEffect(() => { async function getHatsTree() { @@ -167,135 +153,6 @@ const useHatsTree = () => { linearVotingErc20WithHatsWhitelistingAddress, linearVotingErc721WithHatsWhitelistingAddress, ]); - - const getPaymentStreams = useCallback( - async (paymentRecipient: Address): Promise => { - if (!sablierSubgraph || !publicClient) { - return []; - } - const streamQueryResult = await apolloClient.query({ - query: StreamsQueryDocument, - variables: { recipientAddress: paymentRecipient }, - context: { subgraphSpace: sablierSubgraph.space, subgraphSlug: sablierSubgraph.slug }, - }); - - if (!streamQueryResult.error) { - if (!streamQueryResult.data.streams.length) { - return []; - } - const secondsTimestampToDate = (ts: string) => new Date(Number(ts) * 1000); - const lockupLinearStreams = streamQueryResult.data.streams.filter( - stream => stream.category === 'LockupLinear', - ); - const formattedLinearStreams = lockupLinearStreams.map(lockupLinearStream => { - const parsedAmount = formatUnits( - BigInt(lockupLinearStream.depositAmount), - lockupLinearStream.asset.decimals, - ); - - const startDate = secondsTimestampToDate(lockupLinearStream.startTime); - const endDate = secondsTimestampToDate(lockupLinearStream.endTime); - const cliffDate = lockupLinearStream.cliff - ? 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, - recipient: getAddress(lockupLinearStream.recipient), - asset: { - address: getAddress(lockupLinearStream.asset.address), - name: lockupLinearStream.asset.name, - symbol: lockupLinearStream.asset.symbol, - decimals: lockupLinearStream.asset.decimals, - logo, - }, - amount: { - bigintValue: BigInt(lockupLinearStream.depositAmount), - value: parsedAmount, - }, - isCancelled: lockupLinearStream.canceled, - startDate, - endDate, - cliffDate, - isStreaming: () => { - const start = !lockupLinearStream.cliff - ? startDate.getTime() - : cliffDate !== undefined - ? cliffDate.getTime() - : undefined; - const end = endDate ? endDate.getTime() : undefined; - const cancelled = lockupLinearStream.canceled; - const now = new Date().getTime(); - - return !cancelled && !!start && !!end && start <= now && end > now; - }, - isCancellable: () => - !lockupLinearStream.canceled && !!endDate && endDate.getTime() > Date.now(), - }; - }); - - const streamsWithCurrentWithdrawableAmounts: SablierPayment[] = await Promise.all( - formattedLinearStreams.map(async stream => { - const streamContract = getContract({ - abi: SablierV2LockupLinearAbi, - address: stream.contractAddress, - client: publicClient, - }); - const bigintStreamId = convertStreamIdToBigInt(stream.streamId); - - const newWithdrawableAmount = await streamContract.read.withdrawableAmountOf([ - bigintStreamId, - ]); - return { ...stream, withdrawableAmount: newWithdrawableAmount }; - }), - ); - return streamsWithCurrentWithdrawableAmounts; - } - return []; - }, - [apolloClient, publicClient, sablierSubgraph], - ); - - useEffect(() => { - async function getHatsStreams() { - if (hatsTree && hatsTree.roleHats.length > 0 && !streamsFetched) { - const updatedHatsRoles = await Promise.all( - hatsTree.roleHats.map(async hat => { - if (hat.payments?.length) { - return hat; - } - const payments: SablierPayment[] = []; - if (hat.isTermed) { - const uniqueRecipients = [ - ...new Set(hat.roleTerms.allTerms.map(term => term.nominee)), - ]; - for (const recipient of uniqueRecipients) { - payments.push(...(await getPaymentStreams(recipient))); - } - } else { - if (!hat.smartAddress) { - throw new Error('Smart account address not found'); - } - payments.push(...(await getPaymentStreams(hat.smartAddress))); - } - - return { ...hat, payments }; - }), - ); - - updateRolesWithStreams(updatedHatsRoles); - } - } - - getHatsStreams(); - }, [hatsTree, updateRolesWithStreams, getPaymentStreams, streamsFetched]); }; export { useHatsTree }; From 2ac122d4e3a5dcb6023506cb924700d3a93d844f Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Tue, 17 Dec 2024 23:55:39 -0500 Subject: [PATCH 03/10] update getHatsTree to also retrieve payment data in one go --- src/hooks/DAO/loaders/useHatsTree.ts | 75 +++++++++----- src/store/roles/rolesStoreUtils.ts | 141 ++++++++++++++++++++++++++- src/store/roles/useRolesStore.ts | 2 + src/types/roles.tsx | 6 ++ 4 files changed, 200 insertions(+), 24 deletions(-) diff --git a/src/hooks/DAO/loaders/useHatsTree.ts b/src/hooks/DAO/loaders/useHatsTree.ts index a972a3ac1..0de63e125 100644 --- a/src/hooks/DAO/loaders/useHatsTree.ts +++ b/src/hooks/DAO/loaders/useHatsTree.ts @@ -1,8 +1,9 @@ import { useApolloClient } from '@apollo/client'; import { HatsSubgraphClient, Tree } from '@hatsprotocol/sdk-v1-subgraph'; -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; +import { PublicClient } from 'viem'; import { usePublicClient } from 'wagmi'; import { useFractal } from '../../../providers/App/AppProvider'; import useIPFSClient from '../../../providers/App/hooks/useIPFSClient'; @@ -26,6 +27,7 @@ const useHatsTree = () => { const ipfsClient = useIPFSClient(); const { + sablierSubgraph, contracts: { hatsProtocol, erc6551Registry, @@ -34,22 +36,14 @@ const useHatsTree = () => { }, } = useNetworkConfigStore(); const publicClient = usePublicClient(); + const apolloClient = useApolloClient(); - useEffect(() => { - async function getHatsTree() { - if ( - hatsTreeId === undefined || - hatsTreeId === null || - publicClient === undefined || - contextChainId === null - ) { - return; - } - + const getHatsTree = useCallback( + async (props: { hatsTreeId: number; contextChainId: number; publicClient: PublicClient }) => { try { const tree = await hatsSubgraphClient.getTree({ - chainId: contextChainId, - treeId: hatsTreeId, + chainId: props.contextChainId, + treeId: props.hatsTreeId, props: { hats: { props: { @@ -77,7 +71,7 @@ const useHatsTree = () => { const cacheKey = { cacheName: CacheKeys.IPFS_HASH, hash, - chainId: contextChainId, + chainId: props.contextChainId, } as const; const cachedDetails = getValue(cacheKey); @@ -98,18 +92,21 @@ const useHatsTree = () => { ); const treeWithFetchedDetails: Tree = { ...tree, hats: hatsWithFetchedDetails }; + try { await setHatsTree({ hatsTree: treeWithFetchedDetails, - chainId: BigInt(contextChainId), + chainId: BigInt(props.contextChainId), hatsProtocol, erc6551Registry, hatsAccountImplementation, hatsElectionsImplementation, - publicClient, + publicClient: props.publicClient, whitelistingVotingStrategy: linearVotingErc20WithHatsWhitelistingAddress || linearVotingErc721WithHatsWhitelistingAddress, + apolloClient, + sablierSubgraph, }); } catch (e) { if (e instanceof DecentHatsError) { @@ -119,26 +116,55 @@ const useHatsTree = () => { } catch (e) { setHatsTree({ hatsTree: null, - chainId: BigInt(contextChainId), + chainId: BigInt(props.contextChainId), hatsProtocol, erc6551Registry, hatsAccountImplementation, hatsElectionsImplementation, - publicClient, + publicClient: props.publicClient, + apolloClient, + sablierSubgraph, }); const message = t('invalidHatsTreeIdMessage'); toast.error(message); console.error(e, { message, args: { - network: contextChainId, - hatsTreeId, + network: props.contextChainId, + hatsTreeId: props.hatsTreeId, }, }); } - } + }, + [ + apolloClient, + erc6551Registry, + hatsAccountImplementation, + hatsElectionsImplementation, + hatsProtocol, + ipfsClient, + linearVotingErc20WithHatsWhitelistingAddress, + linearVotingErc721WithHatsWhitelistingAddress, + sablierSubgraph, + setHatsTree, + t, + ], + ); - getHatsTree(); + useEffect(() => { + if ( + hatsTreeId === undefined || + hatsTreeId === null || + publicClient === undefined || + contextChainId === null + ) { + return; + } + getHatsTree({ + hatsTreeId, + contextChainId, + publicClient, + }); }, [ contextChainId, erc6551Registry, @@ -152,6 +178,9 @@ const useHatsTree = () => { t, linearVotingErc20WithHatsWhitelistingAddress, linearVotingErc721WithHatsWhitelistingAddress, + apolloClient, + sablierSubgraph, + getHatsTree, ]); }; diff --git a/src/store/roles/rolesStoreUtils.ts b/src/store/roles/rolesStoreUtils.ts index 166bec4b6..68909f169 100644 --- a/src/store/roles/rolesStoreUtils.ts +++ b/src/store/roles/rolesStoreUtils.ts @@ -1,10 +1,16 @@ +import { ApolloClient } from '@apollo/client'; import { abis } from '@fractal-framework/fractal-contracts'; import { HatsModulesClient } from '@hatsprotocol/modules-sdk'; import { Hat, Tree } from '@hatsprotocol/sdk-v1-subgraph'; -import { Address, Hex, PublicClient, getAddress, getContract } from 'viem'; +import { Address, Hex, PublicClient, formatUnits, getAddress, getContract } from 'viem'; +import { StreamsQueryDocument } from '../../../.graphclient'; import ERC6551RegistryAbi from '../../assets/abi/ERC6551RegistryAbi'; import { HatsElectionsEligibilityAbi } from '../../assets/abi/HatsElectionsEligibilityAbi'; +import { SablierV2LockupLinearAbi } from '../../assets/abi/SablierV2LockupLinear'; import { ERC6551_REGISTRY_SALT } from '../../constants/common'; +import { convertStreamIdToBigInt } from '../../hooks/streams/useCreateSablierStream'; +import { CacheKeys } from '../../hooks/utils/cache/cacheDefaults'; +import { getValue } from '../../hooks/utils/cache/useLocalStorage'; import { DecentAdminHat, DecentHat, @@ -12,6 +18,7 @@ import { DecentRoleHatTerms, DecentTree, RolesStoreData, + SablierPayment, } from '../../types/roles'; export class DecentHatsError extends Error { @@ -258,6 +265,106 @@ const getRoleHatTerms = async ( }; }; +const getPaymentStreams = async ( + paymentRecipient: Address, + publicClient: PublicClient, + apolloClient: ApolloClient, + sablierSubgraph: { + space: number; + slug: string; + }, +): Promise => { + if (!sablierSubgraph || !publicClient) { + return []; + } + const streamQueryResult = await apolloClient.query({ + query: StreamsQueryDocument, + variables: { recipientAddress: paymentRecipient }, + context: { subgraphSpace: sablierSubgraph.space, subgraphSlug: sablierSubgraph.slug }, + }); + + if (!streamQueryResult.error) { + if (!streamQueryResult.data.streams.length) { + return []; + } + const secondsTimestampToDate = (ts: string) => new Date(Number(ts) * 1000); + const lockupLinearStreams = streamQueryResult.data.streams.filter( + stream => stream.category === 'LockupLinear', + ); + const formattedLinearStreams = lockupLinearStreams.map(lockupLinearStream => { + const parsedAmount = formatUnits( + BigInt(lockupLinearStream.depositAmount), + lockupLinearStream.asset.decimals, + ); + + const startDate = secondsTimestampToDate(lockupLinearStream.startTime); + const endDate = secondsTimestampToDate(lockupLinearStream.endTime); + const cliffDate = lockupLinearStream.cliff + ? 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, + recipient: getAddress(lockupLinearStream.recipient), + asset: { + address: getAddress(lockupLinearStream.asset.address), + name: lockupLinearStream.asset.name, + symbol: lockupLinearStream.asset.symbol, + decimals: lockupLinearStream.asset.decimals, + logo, + }, + amount: { + bigintValue: BigInt(lockupLinearStream.depositAmount), + value: parsedAmount, + }, + isCancelled: lockupLinearStream.canceled, + startDate, + endDate, + cliffDate, + isStreaming: () => { + const start = !lockupLinearStream.cliff + ? startDate.getTime() + : cliffDate !== undefined + ? cliffDate.getTime() + : undefined; + const end = endDate ? endDate.getTime() : undefined; + const cancelled = lockupLinearStream.canceled; + const now = new Date().getTime(); + + return !cancelled && !!start && !!end && start <= now && end > now; + }, + isCancellable: () => + !lockupLinearStream.canceled && !!endDate && endDate.getTime() > Date.now(), + }; + }); + + const streamsWithCurrentWithdrawableAmounts: SablierPayment[] = await Promise.all( + formattedLinearStreams.map(async stream => { + const streamContract = getContract({ + abi: SablierV2LockupLinearAbi, + address: stream.contractAddress, + client: publicClient, + }); + const bigintStreamId = convertStreamIdToBigInt(stream.streamId); + + const newWithdrawableAmount = await streamContract.read.withdrawableAmountOf([ + bigintStreamId, + ]); + return { ...stream, withdrawableAmount: newWithdrawableAmount }; + }), + ); + return streamsWithCurrentWithdrawableAmounts; + } + return []; +}; + export const sanitize = async ( hatsTree: undefined | null | Tree, hatsAccountImplementation: Address, @@ -266,6 +373,11 @@ export const sanitize = async ( hats: Address, chainId: bigint, publicClient: PublicClient, + apolloClient: ApolloClient, + sablierSubgraph?: { + space: number; + slug: string; + }, whitelistingVotingStrategy?: Address, ): Promise => { if (hatsTree === undefined || hatsTree === null) { @@ -378,6 +490,32 @@ export const sanitize = async ( canCreateProposals = whitelistedHatsIds.includes(tokenId); } + const payments: SablierPayment[] = []; + if (sablierSubgraph !== undefined) { + if (isTermed) { + const uniqueRecipients = [...new Set(roleTerms.allTerms.map(term => term.nominee))]; + for (const recipient of uniqueRecipients) { + payments.push( + ...(await getPaymentStreams(recipient, publicClient, apolloClient, sablierSubgraph)), + ); + } + } else { + if (!roleHatSmartAccountAddress) { + throw new Error('Smart account address not found'); + } + payments.push( + ...(await getPaymentStreams( + roleHatSmartAccountAddress, + publicClient, + apolloClient, + sablierSubgraph, + )), + ); + } + } else { + // @todo - fallback if sablier subgraph is not supported on network + } + roleHats.push({ id: rawHat.id, prettyId: rawHat.prettyId ?? '', @@ -389,6 +527,7 @@ export const sanitize = async ( roleTerms, isTermed, canCreateProposals, + payments, }); } diff --git a/src/store/roles/useRolesStore.ts b/src/store/roles/useRolesStore.ts index 40c4cc86a..20388ab6c 100644 --- a/src/store/roles/useRolesStore.ts +++ b/src/store/roles/useRolesStore.ts @@ -71,6 +71,8 @@ const useRolesStore = create()((set, get) => ({ params.hatsProtocol, params.chainId, params.publicClient, + params.apolloClient, + params.sablierSubgraph, params.whitelistingVotingStrategy, ); set(() => ({ hatsTree })); diff --git a/src/types/roles.tsx b/src/types/roles.tsx index 8c6155d9f..8ab9a38dc 100644 --- a/src/types/roles.tsx +++ b/src/types/roles.tsx @@ -1,3 +1,4 @@ +import { ApolloClient } from '@apollo/client'; import { Tree } from '@hatsprotocol/sdk-v1-subgraph'; import { Address, Hex, PublicClient } from 'viem'; import { SendAssetsData } from '../components/ui/modals/SendAssetsModal'; @@ -258,6 +259,11 @@ export interface RolesStore extends RolesStoreData { hatsAccountImplementation: Address; hatsElectionsImplementation: Address; publicClient: PublicClient; + apolloClient: ApolloClient; + sablierSubgraph?: { + space: number; + slug: string; + }; whitelistingVotingStrategy?: Address; }) => Promise; refreshWithdrawableAmount: (hatId: Hex, streamId: string, publicClient: PublicClient) => void; From f226c83be0bacb9262b65ce6b721a0875af1c162 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Tue, 17 Dec 2024 23:58:59 -0500 Subject: [PATCH 04/10] refactor useHatsTree to replace 'props' with 'params' for consistency --- src/hooks/DAO/loaders/useHatsTree.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/hooks/DAO/loaders/useHatsTree.ts b/src/hooks/DAO/loaders/useHatsTree.ts index 0de63e125..4f13bbb5c 100644 --- a/src/hooks/DAO/loaders/useHatsTree.ts +++ b/src/hooks/DAO/loaders/useHatsTree.ts @@ -39,11 +39,11 @@ const useHatsTree = () => { const apolloClient = useApolloClient(); const getHatsTree = useCallback( - async (props: { hatsTreeId: number; contextChainId: number; publicClient: PublicClient }) => { + async (params: { hatsTreeId: number; contextChainId: number; publicClient: PublicClient }) => { try { const tree = await hatsSubgraphClient.getTree({ - chainId: props.contextChainId, - treeId: props.hatsTreeId, + chainId: params.contextChainId, + treeId: params.hatsTreeId, props: { hats: { props: { @@ -71,7 +71,7 @@ const useHatsTree = () => { const cacheKey = { cacheName: CacheKeys.IPFS_HASH, hash, - chainId: props.contextChainId, + chainId: params.contextChainId, } as const; const cachedDetails = getValue(cacheKey); @@ -96,12 +96,12 @@ const useHatsTree = () => { try { await setHatsTree({ hatsTree: treeWithFetchedDetails, - chainId: BigInt(props.contextChainId), + chainId: BigInt(params.contextChainId), hatsProtocol, erc6551Registry, hatsAccountImplementation, hatsElectionsImplementation, - publicClient: props.publicClient, + publicClient: params.publicClient, whitelistingVotingStrategy: linearVotingErc20WithHatsWhitelistingAddress || linearVotingErc721WithHatsWhitelistingAddress, @@ -116,12 +116,12 @@ const useHatsTree = () => { } catch (e) { setHatsTree({ hatsTree: null, - chainId: BigInt(props.contextChainId), + chainId: BigInt(params.contextChainId), hatsProtocol, erc6551Registry, hatsAccountImplementation, hatsElectionsImplementation, - publicClient: props.publicClient, + publicClient: params.publicClient, apolloClient, sablierSubgraph, }); @@ -130,8 +130,8 @@ const useHatsTree = () => { console.error(e, { message, args: { - network: props.contextChainId, - hatsTreeId: props.hatsTreeId, + network: params.contextChainId, + hatsTreeId: params.hatsTreeId, }, }); } From b23c8de4e9faf603ae64101b908eb3fbd2dccfe2 Mon Sep 17 00:00:00 2001 From: Kellar Date: Wed, 18 Dec 2024 12:32:31 +0000 Subject: [PATCH 05/10] Cleanup dependency array --- src/hooks/DAO/loaders/useHatsTree.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/hooks/DAO/loaders/useHatsTree.ts b/src/hooks/DAO/loaders/useHatsTree.ts index 4f13bbb5c..ca7a764f0 100644 --- a/src/hooks/DAO/loaders/useHatsTree.ts +++ b/src/hooks/DAO/loaders/useHatsTree.ts @@ -165,23 +165,7 @@ const useHatsTree = () => { contextChainId, publicClient, }); - }, [ - contextChainId, - erc6551Registry, - hatsAccountImplementation, - hatsElectionsImplementation, - hatsProtocol, - hatsTreeId, - ipfsClient, - publicClient, - setHatsTree, - t, - linearVotingErc20WithHatsWhitelistingAddress, - linearVotingErc721WithHatsWhitelistingAddress, - apolloClient, - sablierSubgraph, - getHatsTree, - ]); + }, [contextChainId, getHatsTree, hatsTreeId, publicClient]); }; export { useHatsTree }; From 41a83d9a3ec1fb3cffb2060da8a59609b3180b5a Mon Sep 17 00:00:00 2001 From: Kellar Date: Wed, 18 Dec 2024 12:33:22 +0000 Subject: [PATCH 06/10] cleanup some role types --- src/store/roles/rolesStoreUtils.ts | 6 ++---- src/types/roles.tsx | 17 ++++++----------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/store/roles/rolesStoreUtils.ts b/src/store/roles/rolesStoreUtils.ts index 68909f169..43ebf9f77 100644 --- a/src/store/roles/rolesStoreUtils.ts +++ b/src/store/roles/rolesStoreUtils.ts @@ -13,9 +13,9 @@ import { CacheKeys } from '../../hooks/utils/cache/cacheDefaults'; import { getValue } from '../../hooks/utils/cache/useLocalStorage'; import { DecentAdminHat, - DecentHat, DecentRoleHat, DecentRoleHatTerms, + DecentTopHat, DecentTree, RolesStoreData, SablierPayment, @@ -416,13 +416,12 @@ export const sanitize = async ( whitelistedHatsIds = [...(await whitelistingVotingContract.read.getWhitelistedHatIds())]; } - const topHat: DecentHat = { + const topHat: DecentTopHat = { id: rawTopHat.id, prettyId: rawTopHat.prettyId ?? '', name: topHatMetadata.name, description: topHatMetadata.description, smartAddress: topHatSmartAddress, - canCreateProposals: false, // @dev - we don't care about it since topHat is not displayed }; const rawAdminHat = getRawAdminHat(hatsTree.hats); @@ -446,7 +445,6 @@ export const sanitize = async ( name: adminHatMetadata.name, description: adminHatMetadata.description, smartAddress: adminHatSmartAddress, - canCreateProposals: false, // @dev - we don't care about it since adminHat is not displayed wearer: rawAdminHat.wearers?.length ? rawAdminHat.wearers[0].id : undefined, }; diff --git a/src/types/roles.tsx b/src/types/roles.tsx index 8ab9a38dc..1c6f4db59 100644 --- a/src/types/roles.tsx +++ b/src/types/roles.tsx @@ -5,15 +5,12 @@ import { SendAssetsData } from '../components/ui/modals/SendAssetsModal'; import { BigIntValuePair } from './common'; import { CreateProposalMetadata } from './proposalBuilder'; -export interface DecentHat { +interface DecentHat { id: Hex; prettyId: string; name: string; description: string; smartAddress: Address; - eligibility?: Address; - payments?: SablierPayment[]; - canCreateProposals: boolean; } export interface DecentTopHat extends DecentHat {} @@ -22,7 +19,7 @@ export interface DecentAdminHat extends DecentHat { wearer?: Address; } -export type RoleTerm = { +type RoleTerm = { nominee: Address; termEndDate: Date; termNumber: number; @@ -37,10 +34,12 @@ export type DecentRoleHatTerms = { export interface DecentRoleHat extends Omit { wearerAddress: Address; - eligibility?: Address; smartAddress?: Address; roleTerms: DecentRoleHatTerms; + canCreateProposals: boolean; + payments?: SablierPayment[]; isTermed: boolean; + eligibility?: Address; } export interface DecentTree { @@ -133,11 +132,6 @@ export const BadgeStatusColor: Record = { [EditBadgeStatus.Inactive]: 'neutral-6', }; -export interface TermedParams { - termEndDateTs: bigint; - nominatedWearers: Address[]; -} - export enum RoleFormTermStatus { ReadyToStart, Current, @@ -175,6 +169,7 @@ export type EditedRoleFieldNames = | 'roleType' | 'newTerm' | 'canCreateProposals'; + export interface EditedRole { fieldNames: EditedRoleFieldNames[]; status: EditBadgeStatus; From 039ac72a1bfc67bbba6cda9200291f9fa71ee22d Mon Sep 17 00:00:00 2001 From: Kellar Date: Wed, 18 Dec 2024 13:59:26 +0000 Subject: [PATCH 07/10] Fix undefined bug --- src/hooks/utils/useGetSafeName.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/utils/useGetSafeName.ts b/src/hooks/utils/useGetSafeName.ts index 356c0e720..6884019ec 100644 --- a/src/hooks/utils/useGetSafeName.ts +++ b/src/hooks/utils/useGetSafeName.ts @@ -36,7 +36,7 @@ export const getSafeName = async ( subgraphVersion: subgraph.version, }, }) - ).data?.daos[0].name; + ).data?.daos[0]?.name; if (subgraphName) { return subgraphName; From 8ceb5659fedc69c80c3a07bf145faabe6b376e9e Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:23:02 -0500 Subject: [PATCH 08/10] refactor useHatsTree to optimize key loading logic and incorporate safeAddress --- src/hooks/DAO/loaders/useHatsTree.ts | 31 ++++++++++++++++------------ 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/hooks/DAO/loaders/useHatsTree.ts b/src/hooks/DAO/loaders/useHatsTree.ts index ca7a764f0..d84c4fa8b 100644 --- a/src/hooks/DAO/loaders/useHatsTree.ts +++ b/src/hooks/DAO/loaders/useHatsTree.ts @@ -1,6 +1,6 @@ import { useApolloClient } from '@apollo/client'; import { HatsSubgraphClient, Tree } from '@hatsprotocol/sdk-v1-subgraph'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { PublicClient } from 'viem'; @@ -8,6 +8,7 @@ import { usePublicClient } from 'wagmi'; import { useFractal } from '../../../providers/App/AppProvider'; import useIPFSClient from '../../../providers/App/hooks/useIPFSClient'; import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; +import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { DecentHatsError } from '../../../store/roles/rolesStoreUtils'; import { useRolesStore } from '../../../store/roles/useRolesStore'; import { CacheExpiry, CacheKeys } from '../../utils/cache/cacheDefaults'; @@ -150,22 +151,26 @@ const useHatsTree = () => { t, ], ); - + const node = useDaoInfoStore(); + const safeAddress = node.safe?.address; + const loadKey = useRef(undefined); useEffect(() => { + const key = safeAddress && hatsTreeId ? `${safeAddress}:${hatsTreeId}` : null; if ( - hatsTreeId === undefined || - hatsTreeId === null || - publicClient === undefined || - contextChainId === null + !!hatsTreeId && + !!contextChainId && + !!publicClient && + key !== null && + key !== loadKey.current ) { - return; + getHatsTree({ + hatsTreeId, + contextChainId, + publicClient, + }); } - getHatsTree({ - hatsTreeId, - contextChainId, - publicClient, - }); - }, [contextChainId, getHatsTree, hatsTreeId, publicClient]); + loadKey.current = key; + }, [contextChainId, getHatsTree, hatsTreeId, publicClient, safeAddress]); }; export { useHatsTree }; From e72e445fa96fa40b1736d465eac0c61e02d19d7a Mon Sep 17 00:00:00 2001 From: Kellar Date: Thu, 19 Dec 2024 13:19:52 +0000 Subject: [PATCH 09/10] Don't try to load hats tree if loaded DAO is stuck with the same hats tree id as the previously viewed DAO --- src/hooks/DAO/loaders/useHatsTree.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/hooks/DAO/loaders/useHatsTree.ts b/src/hooks/DAO/loaders/useHatsTree.ts index d84c4fa8b..2c6d678b1 100644 --- a/src/hooks/DAO/loaders/useHatsTree.ts +++ b/src/hooks/DAO/loaders/useHatsTree.ts @@ -153,23 +153,33 @@ const useHatsTree = () => { ); const node = useDaoInfoStore(); const safeAddress = node.safe?.address; - const loadKey = useRef(undefined); + const daoHatTreeloadKey = useRef(); useEffect(() => { const key = safeAddress && hatsTreeId ? `${safeAddress}:${hatsTreeId}` : null; + + const previousSafeAddress = daoHatTreeloadKey.current?.split(':')[0]; + const previousHatsTreeId = daoHatTreeloadKey.current?.split(':')[1]; + if ( !!hatsTreeId && !!contextChainId && !!publicClient && key !== null && - key !== loadKey.current + key !== daoHatTreeloadKey.current && + previousHatsTreeId !== `${hatsTreeId}` // don't try to load hats tree if this new DAO is stuck with the same hats tree id as the previous DAO ) { getHatsTree({ hatsTreeId, contextChainId, publicClient, }); + + console.log('setting loadKey.current', key); + daoHatTreeloadKey.current = key; + } else if (!!safeAddress && safeAddress !== previousSafeAddress) { + // If the safe address changes, reset the load key + daoHatTreeloadKey.current = key; } - loadKey.current = key; }, [contextChainId, getHatsTree, hatsTreeId, publicClient, safeAddress]); }; From f4a8fa8eaa015d68bc3cd61925cf4afe15397d4f Mon Sep 17 00:00:00 2001 From: Kellar Date: Thu, 19 Dec 2024 13:50:56 +0000 Subject: [PATCH 10/10] cleanup --- .../DaoCreator/formComponents/AzoriusTokenDetails.tsx | 2 -- src/hooks/DAO/loaders/useHatsTree.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/src/components/DaoCreator/formComponents/AzoriusTokenDetails.tsx b/src/components/DaoCreator/formComponents/AzoriusTokenDetails.tsx index bd5dbd1c5..6e4797b92 100644 --- a/src/components/DaoCreator/formComponents/AzoriusTokenDetails.tsx +++ b/src/components/DaoCreator/formComponents/AzoriusTokenDetails.tsx @@ -98,8 +98,6 @@ export function AzoriusTokenDetails(props: ICreationStepProps) { let tokenErrorMsg = ''; if (touched.erc20Token?.tokenImportAddress) { - console.log(errors?.erc20Token?.tokenImportAddress); - tokenErrorMsg = errors?.erc20Token?.tokenImportAddress || (!isImportedVotesToken ? t('errorNotVotingToken') : ''); diff --git a/src/hooks/DAO/loaders/useHatsTree.ts b/src/hooks/DAO/loaders/useHatsTree.ts index 2c6d678b1..92540b53a 100644 --- a/src/hooks/DAO/loaders/useHatsTree.ts +++ b/src/hooks/DAO/loaders/useHatsTree.ts @@ -174,7 +174,6 @@ const useHatsTree = () => { publicClient, }); - console.log('setting loadKey.current', key); daoHatTreeloadKey.current = key; } else if (!!safeAddress && safeAddress !== previousSafeAddress) { // If the safe address changes, reset the load key