Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Issue #2638] "unwanted" toasts - HatsTreeId not getting reset when Network Changes #2639

Merged
merged 11 commits into from
Dec 19, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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') : '');
Expand Down
234 changes: 59 additions & 175 deletions src/hooks/DAO/loaders/useHatsTree.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
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 { Address, formatUnits, getAddress, getContract } from 'viem';
import { PublicClient } 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 { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore';
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';

Expand All @@ -27,14 +24,7 @@ const useHatsTree = () => {
linearVotingErc721WithHatsWhitelistingAddress,
},
} = useFractal();
const {
hatsTreeId,
contextChainId,
hatsTree,
streamsFetched,
setHatsTree,
updateRolesWithStreams,
} = useRolesStore();
const { hatsTreeId, contextChainId, setHatsTree } = useRolesStore();

const ipfsClient = useIPFSClient();
const {
Expand All @@ -49,21 +39,12 @@ const useHatsTree = () => {
const publicClient = usePublicClient();
const apolloClient = useApolloClient();

useEffect(() => {
async function getHatsTree() {
if (
hatsTreeId === undefined ||
hatsTreeId === null ||
publicClient === undefined ||
contextChainId === null
) {
return;
}

const getHatsTree = useCallback(
async (params: { hatsTreeId: number; contextChainId: number; publicClient: PublicClient }) => {
try {
const tree = await hatsSubgraphClient.getTree({
chainId: contextChainId,
treeId: hatsTreeId,
chainId: params.contextChainId,
treeId: params.hatsTreeId,
props: {
hats: {
props: {
Expand Down Expand Up @@ -91,7 +72,7 @@ const useHatsTree = () => {
const cacheKey = {
cacheName: CacheKeys.IPFS_HASH,
hash,
chainId: contextChainId,
chainId: params.contextChainId,
} as const;

const cachedDetails = getValue(cacheKey);
Expand All @@ -112,18 +93,21 @@ const useHatsTree = () => {
);

const treeWithFetchedDetails: Tree = { ...tree, hats: hatsWithFetchedDetails };

try {
await setHatsTree({
hatsTree: treeWithFetchedDetails,
chainId: BigInt(contextChainId),
chainId: BigInt(params.contextChainId),
hatsProtocol,
erc6551Registry,
hatsAccountImplementation,
hatsElectionsImplementation,
publicClient,
publicClient: params.publicClient,
whitelistingVotingStrategy:
linearVotingErc20WithHatsWhitelistingAddress ||
linearVotingErc721WithHatsWhitelistingAddress,
apolloClient,
sablierSubgraph,
});
} catch (e) {
if (e instanceof DecentHatsError) {
Expand All @@ -133,169 +117,69 @@ const useHatsTree = () => {
} catch (e) {
setHatsTree({
hatsTree: null,
chainId: BigInt(contextChainId),
chainId: BigInt(params.contextChainId),
hatsProtocol,
erc6551Registry,
hatsAccountImplementation,
hatsElectionsImplementation,
publicClient,
publicClient: params.publicClient,
apolloClient,
sablierSubgraph,
});
const message = t('invalidHatsTreeIdMessage');
toast.error(message);
console.error(e, {
message,
args: {
network: contextChainId,
hatsTreeId,
network: params.contextChainId,
hatsTreeId: params.hatsTreeId,
},
});
}
}

getHatsTree();
}, [
contextChainId,
erc6551Registry,
hatsAccountImplementation,
hatsElectionsImplementation,
hatsProtocol,
hatsTreeId,
ipfsClient,
publicClient,
setHatsTree,
t,
linearVotingErc20WithHatsWhitelistingAddress,
linearVotingErc721WithHatsWhitelistingAddress,
]);

const getPaymentStreams = useCallback(
async (paymentRecipient: Address): Promise<SablierPayment[]> => {
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],
[
apolloClient,
erc6551Registry,
hatsAccountImplementation,
hatsElectionsImplementation,
hatsProtocol,
ipfsClient,
linearVotingErc20WithHatsWhitelistingAddress,
linearVotingErc721WithHatsWhitelistingAddress,
sablierSubgraph,
setHatsTree,
t,
],
);

const node = useDaoInfoStore();
const safeAddress = node.safe?.address;
const daoHatTreeloadKey = useRef<string | null>();
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 };
}),
);
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 !== 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,
});

updateRolesWithStreams(updatedHatsRoles);
}
daoHatTreeloadKey.current = key;
} else if (!!safeAddress && safeAddress !== previousSafeAddress) {
// If the safe address changes, reset the load key
daoHatTreeloadKey.current = key;
}

getHatsStreams();
}, [hatsTree, updateRolesWithStreams, getPaymentStreams, streamsFetched]);
}, [contextChainId, getHatsTree, hatsTreeId, publicClient, safeAddress]);
};

export { useHatsTree };
6 changes: 2 additions & 4 deletions src/hooks/DAO/useKeyValuePairs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,9 @@ const useKeyValuePairs = () => {
]);

useEffect(() => {
if (!safeAddress) {
return;
if (safeAddress === undefined) {
resetHatsStore();
}

resetHatsStore();
}, [resetHatsStore, safeAddress]);
};

Expand Down
2 changes: 1 addition & 1 deletion src/hooks/utils/useGetSafeName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const getSafeName = async (
subgraphVersion: subgraph.version,
},
})
).data?.daos[0].name;
).data?.daos[0]?.name;

if (subgraphName) {
return subgraphName;
Expand Down
Loading
Loading