diff --git a/package-lock.json b/package-lock.json index d75db4993..c1c3bc7f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@chakra-ui/anatomy": "^2.2.2", "@chakra-ui/react": "^2.8.2", "@fontsource/space-mono": "^5.0.19", - "@fractal-framework/fractal-contracts": "^1.4.1", + "@fractal-framework/fractal-contracts": "^1.4.2", "@graphprotocol/client-apollo": "^1.0.16", "@hatsprotocol/modules-sdk": "^1.4.0", "@hatsprotocol/sdk-v1-core": "^0.9.0", @@ -4996,9 +4996,9 @@ "integrity": "sha512-gz9yaKtXCY+HutNvQ4APc15xwZ1f6pWXve5N55x5m/hOoGqgB9Auf3l7CitHNhNJkSKEmaM45M29b0rFeudXlg==" }, "node_modules/@fractal-framework/fractal-contracts": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@fractal-framework/fractal-contracts/-/fractal-contracts-1.4.1.tgz", - "integrity": "sha512-QVbj/pqjxUesAKQxrBJCyr7+Y0g6zRgX7b69dwfFS8LVwIu1sstYG9hKTzKay0PGAy0LzA5+npBltTf5ikwDCg==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@fractal-framework/fractal-contracts/-/fractal-contracts-1.4.2.tgz", + "integrity": "sha512-aUXxg+4lpyFh4L15QJnQS7x17if2F+hC1VtIVV+7F12XB6dOIi0FkZikR1nQglLcjbI2O/rlZfLD32o6C9DjDg==", "license": "MIT", "dependencies": { "@gnosis.pm/zodiac": "^1.1.4", diff --git a/package.json b/package.json index 7ccdfecc7..4fb92ce4e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@chakra-ui/anatomy": "^2.2.2", "@chakra-ui/react": "^2.8.2", "@fontsource/space-mono": "^5.0.19", - "@fractal-framework/fractal-contracts": "^1.4.1", + "@fractal-framework/fractal-contracts": "^1.4.2", "@graphprotocol/client-apollo": "^1.0.16", "@hatsprotocol/modules-sdk": "^1.4.0", "@hatsprotocol/sdk-v1-core": "^0.9.0", diff --git a/src/components/ui/forms/DatePicker.tsx b/src/components/ui/forms/DatePicker.tsx index 0af223e1f..57eaaf525 100644 --- a/src/components/ui/forms/DatePicker.tsx +++ b/src/components/ui/forms/DatePicker.tsx @@ -195,7 +195,7 @@ export function DatePicker({ const handleDateChange = (e: OnDateChangeValue) => { if (e instanceof Date) { - onChange?.(new Date(e.setHours(0, 0, 0, 0))); + onChange?.(e); onClose(); // Close the menu after date selection } }; diff --git a/src/hooks/DAO/loaders/useHatsTree.ts b/src/hooks/DAO/loaders/useHatsTree.ts index d7ce1bb56..0dbe4d0e2 100644 --- a/src/hooks/DAO/loaders/useHatsTree.ts +++ b/src/hooks/DAO/loaders/useHatsTree.ts @@ -271,10 +271,10 @@ const useHatsTree = () => { return hat; } const payments: SablierPayment[] = []; - // @todo - update Datepicker to choose more precise dates (Date.now()) if (hat.isTermed) { - const recipients = hat.roleTerms.allTerms.map(term => term.nominee); - const uniqueRecipients = [...new Set(recipients)]; + const uniqueRecipients = [ + ...new Set(hat.roleTerms.allTerms.map(term => term.nominee)), + ]; for (const recipient of uniqueRecipients) { payments.push(...(await getPaymentStreams(recipient))); } diff --git a/src/hooks/DAO/useKeyValuePairs.ts b/src/hooks/DAO/useKeyValuePairs.ts index 7a558a7f9..38615ef52 100644 --- a/src/hooks/DAO/useKeyValuePairs.ts +++ b/src/hooks/DAO/useKeyValuePairs.ts @@ -2,7 +2,7 @@ import { abis } from '@fractal-framework/fractal-contracts'; import { hatIdToTreeId } from '@hatsprotocol/sdk-v1-core'; import { useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { GetContractEventsReturnType, getContract } from 'viem'; +import { Address, GetContractEventsReturnType, getContract } from 'viem'; import { usePublicClient } from 'wagmi'; import { logError } from '../../helpers/errorLogging'; import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; @@ -55,14 +55,50 @@ const getHatsTreeId = ( } }; +const getHatIdsToStreamIds = ( + events: GetContractEventsReturnType | undefined, + sablierV2LockupLinear: Address, + chainId: number, +) => { + if (!events) { + return []; + } + + const hatIdToStreamIdEvents = events.filter( + event => event.args.key && event.args.key === 'hatIdToStreamId', + ); + + const hatIdIdsToStreamIds = []; + for (const event of hatIdToStreamIdEvents) { + const hatIdToStreamId = event.args.value; + if (hatIdToStreamId !== undefined) { + const [hatId, streamId] = hatIdToStreamId.split(':'); + hatIdIdsToStreamIds.push({ + hatId: BigInt(hatId), + streamId: `${sablierV2LockupLinear.toLowerCase()}-${chainId}-${streamId}`, + }); + continue; + } + logError({ + message: "KVPairs 'hatIdToStreamId' without a value", + network: chainId, + args: { + transactionHash: event.transactionHash, + logIndex: event.logIndex, + }, + }); + } + return hatIdIdsToStreamIds; +}; + const useKeyValuePairs = () => { const publicClient = usePublicClient(); const node = useDaoInfoStore(); const { chain, - contracts: { keyValuePairs }, + contracts: { keyValuePairs, sablierV2LockupLinear }, } = useNetworkConfig(); - const { setHatsTreeId } = useRolesStore(); + const { setHatKeyValuePairData } = useRolesStore(); const [searchParams] = useSearchParams(); const safeAddress = node.safe?.address; @@ -81,14 +117,19 @@ const useKeyValuePairs = () => { }); keyValuePairsContract.getEvents .ValueUpdated({ theAddress: safeAddress }, { fromBlock: 0n }) - .then(safeEvents => - setHatsTreeId({ + .then(safeEvents => { + setHatKeyValuePairData({ contextChainId: chain.id, hatsTreeId: getHatsTreeId(safeEvents, chain.id), - }), - ) + streamIdsToHatIds: getHatIdsToStreamIds(safeEvents, sablierV2LockupLinear, chain.id), + }); + }) .catch(error => { - setHatsTreeId({ hatsTreeId: null, contextChainId: chain.id }); + setHatKeyValuePairData({ + hatsTreeId: null, + contextChainId: chain.id, + streamIdsToHatIds: [], + }); logError(error); }); @@ -102,9 +143,10 @@ const useKeyValuePairs = () => { // time to index, and do that most cleanly by not even telling the rest // of our code that we have the hats tree id until some time has passed. setTimeout(() => { - setHatsTreeId({ - hatsTreeId: getHatsTreeId(logs, chain.id), + setHatKeyValuePairData({ contextChainId: chain.id, + hatsTreeId: getHatsTreeId(logs, chain.id), + streamIdsToHatIds: getHatIdsToStreamIds(logs, sablierV2LockupLinear, chain.id), }); }, 20_000); }, @@ -113,7 +155,15 @@ const useKeyValuePairs = () => { return () => { unwatch(); }; - }, [chain.id, keyValuePairs, safeAddress, publicClient, searchParams, setHatsTreeId]); + }, [ + chain.id, + keyValuePairs, + safeAddress, + publicClient, + searchParams, + setHatKeyValuePairData, + sablierV2LockupLinear, + ]); }; export { useKeyValuePairs }; diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index f11124af6..7d0686010 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -639,6 +639,7 @@ export default function useCreateRoles() { hats: [hatStruct], topHatId: BigInt(hatsTree.topHat.id), topHatAccount: hatsTree.topHat.smartAddress, + keyValuePairs, }, ], }); @@ -670,6 +671,7 @@ export default function useCreateRoles() { erc6551Registry, hatsAccount1ofNMasterCopy, hatsElectionsEligibilityMasterCopy, + keyValuePairs, ], ); diff --git a/src/store/roles/useRolesStore.ts b/src/store/roles/useRolesStore.ts index 158e4baf7..3f6da74f1 100644 --- a/src/store/roles/useRolesStore.ts +++ b/src/store/roles/useRolesStore.ts @@ -5,6 +5,14 @@ import { convertStreamIdToBigInt } from '../../hooks/streams/useCreateSablierStr import { DecentRoleHat, RolesStore } from '../../types/roles'; import { initialHatsStore, sanitize } from './rolesStoreUtils'; +const streamIdToHatIdMap = new Map(); +const getStreamIdToHatIdMap = () => { + return Array.from(streamIdToHatIdMap.entries()).map(([streamId, hatId]) => ({ + streamId, + hatId, + })); +}; + const useRolesStore = create()((set, get) => ({ ...initialHatsStore, getHat: hatId => { @@ -39,16 +47,21 @@ const useRolesStore = create()((set, get) => ({ return matches[0]; }, - setHatsTreeId: args => + setHatKeyValuePairData: args => { + const { hatsTreeId, contextChainId, streamIdsToHatIds } = args; + for (const { hatId, streamId } of streamIdsToHatIds) { + streamIdToHatIdMap.set(streamId, hatId); + } set(() => { - const { hatsTreeId, contextChainId } = args; // if `hatsTreeId` is null or undefined, // set `hatsTree` to that same value if (typeof hatsTreeId !== 'number') { return { hatsTreeId, hatsTree: hatsTreeId, streamsFetched: false, contextChainId: null }; } return { hatsTreeId, streamsFetched: false, contextChainId }; - }), + }); + }, + setHatsTree: async params => { const hatsTree = await sanitize( params.hatsTree, @@ -96,10 +109,23 @@ const useRolesStore = create()((set, get) => ({ updateRolesWithStreams: (updatedRoles: DecentRoleHat[]) => { const existingHatsTree = get().hatsTree; if (!existingHatsTree) return; + const streamIdsToHatIdsMap = getStreamIdToHatIdMap(); const updatedDecentTree = { ...existingHatsTree, - roleHats: updatedRoles, + roleHats: updatedRoles.map(roleHat => { + 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, + }; + }), }; set(() => ({ hatsTree: updatedDecentTree, streamsFetched: true })); @@ -129,6 +155,7 @@ const useRolesStore = create()((set, get) => ({ }, })); }, + resetHatsStore: () => set(() => initialHatsStore), })); diff --git a/src/types/roles.tsx b/src/types/roles.tsx index e1000b477..71c9fe133 100644 --- a/src/types/roles.tsx +++ b/src/types/roles.tsx @@ -243,7 +243,11 @@ export interface RolesStoreData { export interface RolesStore extends RolesStoreData { getHat: (hatId: Hex) => DecentRoleHat | null; getPayment: (hatId: Hex, streamId: string) => SablierPayment | null; - setHatsTreeId: (args: { contextChainId: number | null; hatsTreeId?: number | null }) => void; + setHatKeyValuePairData: (args: { + contextChainId: number | null; + hatsTreeId?: number | null; + streamIdsToHatIds: { hatId: BigInt; streamId: string }[]; + }) => void; setHatsTree: (params: { hatsTree: Tree | null | undefined; chainId: bigint;