diff --git a/package-lock.json b/package-lock.json index d9a4d0126..ef8ac5809 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "react-lines-ellipsis": "^0.15.0", "react-media": "^1.10.0", "react-modal": "^3.13.1", + "react-query": "^3.19.1", "react-redux": "^7.2.4", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", @@ -5814,6 +5815,14 @@ "node": ">= 8.0.0" } }, + "node_modules/big-integer": { + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -6013,6 +6022,21 @@ "node": ">=8" } }, + "node_modules/broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, "node_modules/brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", @@ -15221,6 +15245,15 @@ "react": ">= 0.14.0" } }, + "node_modules/match-sorter": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.0.tgz", + "integrity": "sha512-efYOf/wUpNb8FgNY+cOD2EIJI1S5I7YPKsw0LBp7wqPh5pmMS6i/wr3ZWwfwrAw1NvqTA2KUReVRWDX84lUcOQ==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -15482,6 +15515,11 @@ "node": ">=8.6" } }, + "node_modules/microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" + }, "node_modules/miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", @@ -16011,6 +16049,14 @@ "resolved": "https://registry.npmjs.org/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz", "integrity": "sha1-DMj20OK2IrR5xA1JnEbWS3Vcb18=" }, + "node_modules/nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=", + "dependencies": { + "big-integer": "^1.6.16" + } + }, "node_modules/nanoid": { "version": "3.1.23", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", @@ -16879,6 +16925,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" + }, "node_modules/oboe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/oboe/-/oboe-2.1.5.tgz", @@ -19924,6 +19975,31 @@ "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17" } }, + "node_modules/react-query": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.19.1.tgz", + "integrity": "sha512-tMBVKlmWevPHYWgI8ZBEgsTulJXSuXsxDbxqANODRnPI+3hd5GRVcc7nNIYSUx3aaULt08rN3EhTMHyTcFUNJw==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-redux": { "version": "7.2.4", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz", @@ -21782,6 +21858,11 @@ "node": ">= 0.10" } }, + "node_modules/remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=" + }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -25583,6 +25664,15 @@ "node": ">= 10.0.0" } }, + "node_modules/unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "dependencies": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -33151,6 +33241,11 @@ "tryer": "^1.0.1" } }, + "big-integer": { + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==" + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -33317,6 +33412,21 @@ "fill-range": "^7.0.1" } }, + "broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "requires": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", @@ -40621,6 +40731,15 @@ "integrity": "sha512-jtQ6VyT7rMT5tPV0g2EJakEnXLiPksnvlYtwQsVVZ611JsWGN8bQ1tVSDX4s6JllfEH6wmsYxNjTUAMrPmNA8w==", "requires": {} }, + "match-sorter": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.0.tgz", + "integrity": "sha512-efYOf/wUpNb8FgNY+cOD2EIJI1S5I7YPKsw0LBp7wqPh5pmMS6i/wr3ZWwfwrAw1NvqTA2KUReVRWDX84lUcOQ==", + "requires": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -40859,6 +40978,11 @@ "picomatch": "^2.2.3" } }, + "microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" + }, "miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", @@ -41278,6 +41402,14 @@ "resolved": "https://registry.npmjs.org/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz", "integrity": "sha1-DMj20OK2IrR5xA1JnEbWS3Vcb18=" }, + "nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=", + "requires": { + "big-integer": "^1.6.16" + } + }, "nanoid": { "version": "3.1.23", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", @@ -41950,6 +42082,11 @@ "es-abstract": "^1.18.2" } }, + "oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" + }, "oboe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/oboe/-/oboe-2.1.5.tgz", @@ -44360,6 +44497,16 @@ "warning": "^4.0.3" } }, + "react-query": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.19.1.tgz", + "integrity": "sha512-tMBVKlmWevPHYWgI8ZBEgsTulJXSuXsxDbxqANODRnPI+3hd5GRVcc7nNIYSUx3aaULt08rN3EhTMHyTcFUNJw==", + "requires": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + } + }, "react-redux": { "version": "7.2.4", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz", @@ -45822,6 +45969,11 @@ "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" }, + "remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=" + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -48860,6 +49012,15 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" }, + "unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "requires": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index fef690183..7caa6153b 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "react-lines-ellipsis": "^0.15.0", "react-media": "^1.10.0", "react-modal": "^3.13.1", + "react-query": "^3.19.1", "react-redux": "^7.2.4", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", diff --git a/src/components/governance/hooks/useGovernanceProposals.ts b/src/components/governance/hooks/useGovernanceProposals.ts index e83590f31..4f4417ff6 100644 --- a/src/components/governance/hooks/useGovernanceProposals.ts +++ b/src/components/governance/hooks/useGovernanceProposals.ts @@ -3,6 +3,7 @@ import { SnapshotProposalResponse, SnapshotType, } from '@openlaw/snapshot-js-erc712'; +import {useQuery} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {BURN_ADDRESS} from '../../../util/constants'; @@ -48,13 +49,29 @@ export function useGovernanceProposals({ const {isMountedRef} = useIsMounted(); + /** + * React Query + */ + + const { + data: snapshotProposalEntriesData, + error: snapshotProposalEntriesError, + } = useQuery( + ['snapshotProposalEntries', actionId], + async () => await getSnapshotProposalsByActionId(actionId), + { + enabled: !!actionId, + } + ); + /** * Cached callbacks */ const handleGetProposalsCached = useCallback(handleGetProposals, [ - actionId, isMountedRef, + snapshotProposalEntriesData, + snapshotProposalEntriesError, ]); /** @@ -119,14 +136,16 @@ export function useGovernanceProposals({ try { setGovernanceProposalsStatus(AsyncStatus.PENDING); - const snapshotProposalEntries = await getSnapshotProposalsByActionId( - actionId - ); + if (snapshotProposalEntriesError) { + throw snapshotProposalEntriesError; + } - if (!isMountedRef.current) return; + if (snapshotProposalEntriesData) { + if (!isMountedRef.current) return; - setGovernanceProposalsStatus(AsyncStatus.FULFILLED); - setGovernanceProposals(snapshotProposalEntries); + setGovernanceProposalsStatus(AsyncStatus.FULFILLED); + setGovernanceProposals(snapshotProposalEntriesData); + } } catch (error) { if (!isMountedRef.current) return; diff --git a/src/components/governance/hooks/useGovernanceProposals.unit.test.ts b/src/components/governance/hooks/useGovernanceProposals.unit.test.ts index a4ee4d56e..dc9ed446b 100644 --- a/src/components/governance/hooks/useGovernanceProposals.unit.test.ts +++ b/src/components/governance/hooks/useGovernanceProposals.unit.test.ts @@ -6,6 +6,7 @@ import {rest, server} from '../../../test/server'; import {SNAPSHOT_HUB_API_URL} from '../../../config'; import {snapshotAPIProposalResponse} from '../../../test/restResponses'; import {useGovernanceProposals} from './useGovernanceProposals'; +import Wrapper from '../../../test/Wrapper'; describe('useGovernanceProposals unit tests', () => { test('should return correct data', async () => { @@ -19,26 +20,34 @@ describe('useGovernanceProposals unit tests', () => { ] ); - const {result, waitForNextUpdate} = await renderHook(() => - useGovernanceProposals({actionId: BURN_ADDRESS}) + const {result, waitForValueToChange} = await renderHook( + () => useGovernanceProposals({actionId: BURN_ADDRESS}), + {wrapper: Wrapper} ); + // Assert initial state expect(result.current.governanceProposals).toMatchObject([]); expect(result.current.governanceProposalsError).toBe(undefined); expect(result.current.governanceProposalsStatus).toBe( AsyncStatus.STANDBY ); - await waitForNextUpdate(); + await waitForValueToChange( + () => result.current.governanceProposalsStatus + ); + // Assert pending state expect(result.current.governanceProposals).toMatchObject([]); expect(result.current.governanceProposalsError).toBe(undefined); expect(result.current.governanceProposalsStatus).toBe( AsyncStatus.PENDING ); - await waitForNextUpdate(); + await waitForValueToChange( + () => result.current.governanceProposalsStatus + ); + // Assert fulfilled state expect( result.current.governanceProposals[0].snapshotProposal ).toMatchObject({ @@ -64,26 +73,34 @@ describe('useGovernanceProposals unit tests', () => { ] ); - const {result, waitForNextUpdate} = await renderHook(() => - useGovernanceProposals({actionId: BURN_ADDRESS}) + const {result, waitForValueToChange} = await renderHook( + () => useGovernanceProposals({actionId: BURN_ADDRESS}), + {wrapper: Wrapper} ); + // Assert initial state expect(result.current.governanceProposals).toMatchObject([]); expect(result.current.governanceProposalsError).toBe(undefined); expect(result.current.governanceProposalsStatus).toBe( AsyncStatus.STANDBY ); - await waitForNextUpdate(); + await waitForValueToChange( + () => result.current.governanceProposalsStatus + ); + // Assert pending state expect(result.current.governanceProposals).toMatchObject([]); expect(result.current.governanceProposalsError).toBe(undefined); expect(result.current.governanceProposalsStatus).toBe( AsyncStatus.PENDING ); - await waitForNextUpdate(); + await waitForValueToChange( + () => result.current.governanceProposalsStatus + ); + // Assert rejected state expect(result.current.governanceProposals).toMatchObject([]); expect(result.current.governanceProposalsError?.message).toMatch( /something went wrong while fetching the Snapshot proposals/i diff --git a/src/components/proposals/hooks/useDaoProposals.ts b/src/components/proposals/hooks/useDaoProposals.ts index eeff4af50..53cb03dc3 100644 --- a/src/components/proposals/hooks/useDaoProposals.ts +++ b/src/components/proposals/hooks/useDaoProposals.ts @@ -1,7 +1,6 @@ -import {AbiItem} from 'web3-utils/types'; -import {useEffect, useState} from 'react'; +import {useEffect, useState, useCallback} from 'react'; import {useSelector} from 'react-redux'; -import Web3 from 'web3'; +import {useQuery} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {multicall, MulticallTuple} from '../../web3/helpers'; @@ -54,12 +53,46 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { const [daoProposalsError, setDaoProposalsError] = useState(); + const [safeProposalIds, setSafeProposalIds] = useState(); + + const [daoProposalsCalls, setDaoProposalsCalls] = + useState(); + /** * Our hooks */ const {web3Instance} = useWeb3Modal(); + /** + * React Query + */ + + const {data: daoProposalsData, error: daoProposalsQueryError} = useQuery( + ['daoProposals', daoProposalsCalls], + async () => { + if (!daoProposalsCalls?.length || !web3Instance) { + return; + } + + return await multicall({ + calls: daoProposalsCalls, + web3Instance, + }); + }, + {enabled: !!daoProposalsCalls?.length && !!web3Instance} + ); + + /** + * Cached callbacks + */ + + const handleGetDaoProposalsCached = useCallback(handleGetDaoProposals, [ + daoProposalsData, + daoProposalsQueryError, + safeProposalIds, + ]); + /** * Effects */ @@ -67,6 +100,7 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { useEffect(() => { if ( !proposalIds.length || + !safeProposalIds || !proposalsAbi || !registryAddress || !web3Instance @@ -74,36 +108,46 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { return; } - handleGetDaoProposals({ - proposalIds, - proposalsAbi, - registryAddress, - web3Instance, - }); - }, [proposalIds, proposalsAbi, registryAddress, web3Instance]); + handleGetDaoProposalsCached(); + }, [ + handleGetDaoProposalsCached, + proposalIds.length, + proposalsAbi, + registryAddress, + safeProposalIds, + web3Instance, + ]); + + useEffect(() => { + if (!proposalIds.length || !web3Instance) { + return; + } + + // Only use hex (more specifically `bytes32`) id's + const safeProposalIdsToSet = proposalIds.filter( + web3Instance.utils.isHexStrict + ); + + setSafeProposalIds(safeProposalIdsToSet); + }, [proposalIds, web3Instance]); + + useEffect(() => { + if (!proposalsAbi || !registryAddress || !safeProposalIds?.length) return; + + const daoProposalsCallsToSet: MulticallTuple[] = safeProposalIds.map( + (id) => [registryAddress, proposalsAbi, [id]] + ); + + setDaoProposalsCalls(daoProposalsCallsToSet); + }, [proposalsAbi, registryAddress, safeProposalIds]); /** * Functions */ - async function handleGetDaoProposals({ - proposalIds, - proposalsAbi, - registryAddress, - web3Instance, - }: { - proposalIds: string[]; - proposalsAbi: AbiItem; - registryAddress: string; - web3Instance: Web3; - }) { + async function handleGetDaoProposals() { try { - if (!proposalIds.length) return; - - // Only use hex (more specifically `bytes32`) id's - const safeProposalIds = proposalIds.filter( - web3Instance.utils.isHexStrict - ); + if (!safeProposalIds) return; if (!safeProposalIds.length) { setDaoProposalsStatus(AsyncStatus.FULFILLED); @@ -116,19 +160,16 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { // Reset error setDaoProposalsError(undefined); - const calls: MulticallTuple[] = safeProposalIds.map((id) => [ - registryAddress, - proposalsAbi, - [id], - ]); - - const proposals = await multicall({ - calls, - web3Instance, - }); + if (daoProposalsQueryError) { + throw daoProposalsQueryError; + } - setDaoProposals(safeProposalIds.map((id, i) => [id, proposals[i]])); - setDaoProposalsStatus(AsyncStatus.FULFILLED); + if (daoProposalsData) { + setDaoProposals( + safeProposalIds.map((id, i) => [id, daoProposalsData[i]]) + ); + setDaoProposalsStatus(AsyncStatus.FULFILLED); + } } catch (error) { setDaoProposals(INITIAL_DAO_PROPOSAL_ENTRIES); setDaoProposalsError(error); diff --git a/src/components/proposals/hooks/useDaoProposals.unit.test.ts b/src/components/proposals/hooks/useDaoProposals.unit.test.ts index 6f1679773..e6335b2ac 100644 --- a/src/components/proposals/hooks/useDaoProposals.unit.test.ts +++ b/src/components/proposals/hooks/useDaoProposals.unit.test.ts @@ -303,12 +303,12 @@ describe('useDaoProposals unit tests', () => { ); /** - * Inject mocked result for `proposals` + * Inject mocked error result for `proposals` * - * @todo Fix needing two injected errors to make test work. + * @todo Fix needing `injectResult` to make test work. */ - mockWeb3Provider.injectError({code: 1234, message: 'Some RPC error!'}); + mockWeb3Provider.injectResult(null); mockWeb3Provider.injectError({code: 1234, message: 'Some RPC error!'}); await waitForValueToChange(() => result.current.daoProposalsStatus); @@ -320,9 +320,9 @@ describe('useDaoProposals unit tests', () => { AsyncStatus.PENDING ); - await waitForValueToChange(() => result.current.daoProposalsError); + await waitForValueToChange(() => result.current.daoProposalsStatus); - // Assert fulfilled + // Assert rejected expect(result.current.daoProposals).toStrictEqual([]); expect(result.current.daoProposalsError).toStrictEqual({ code: 1234, diff --git a/src/components/proposals/hooks/useOffchainVotingResults.ts b/src/components/proposals/hooks/useOffchainVotingResults.ts index cc8663ae4..cedca7a57 100644 --- a/src/components/proposals/hooks/useOffchainVotingResults.ts +++ b/src/components/proposals/hooks/useOffchainVotingResults.ts @@ -3,6 +3,7 @@ import {useCallback, useEffect, useState} from 'react'; import {useSelector} from 'react-redux'; import {VoteChoicesIndex} from '@openlaw/snapshot-js-erc712'; import Web3 from 'web3'; +import {useQuery} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {multicall, MulticallTuple} from '../../web3/helpers'; @@ -58,6 +59,18 @@ export function useOffchainVotingResults( Error | undefined >(); + const [proposalsToMap, setProposalsToMap] = useState< + (SnapshotProposal | undefined)[] + >([]); + + /** + * Variables + */ + + const getPriorAmountABI = bankABI?.find( + (item) => item.name === 'getPriorAmount' + ); + /** * Our hooks */ @@ -66,12 +79,72 @@ export function useOffchainVotingResults( const {isMountedRef} = useIsMounted(); /** - * Variables + * React Query */ - const getPriorAmountABI = bankABI?.find( - (item) => item.name === 'getPriorAmount' - ); + const {data: votingResultsToSetData, error: votingResultsToSetError} = + useQuery( + ['votingResultsToSet', proposalsToMap], + async () => { + if ( + !bankAddress || + !getPriorAmountABI || + !proposalsToMap.length || + !web3Instance + ) + return; + + return await Promise.all( + proposalsToMap.map(async (p) => { + const snapshot = p?.msg.payload.snapshot; + const idInSnapshot = p?.idInSnapshot; + + if (!idInSnapshot || !snapshot) return; + + const voterEntries = p?.votes?.map((v): [string, number] => { + const vote = v[Object.keys(v)[0]]; + + return [ + /** + * Must be the true member's address for calculating voting power. + * This value is (or at least should be) derived from `OffchainVoting.memberAddressesByDelegatedKey`. + */ + vote.msg.payload.metadata.memberAddress, + vote.msg.payload.choice, + ]; + }); + + if (!voterEntries || !voterEntries.length) return; + + // Dedupe any duplicate addresses to be safe. + const voterAddressesAndChoices = Object.entries( + Object.fromEntries(voterEntries) + ); + + try { + const result = await getUnitsPerChoiceCached({ + bankAddress, + getPriorAmountABI, + snapshot, + voterAddressesAndChoices, + web3Instance, + }); + + return [idInSnapshot, result]; + } catch (error) { + return; + } + }) + ); + }, + { + enabled: + !!bankAddress && + !!getPriorAmountABI && + !!proposalsToMap.length && + !!web3Instance, + } + ); /** * Cached callbacks @@ -82,14 +155,41 @@ export function useOffchainVotingResults( [] ); + const buildOffchainVotingResultEntriesCached = useCallback( + buildOffchainVotingResultEntries, + [ + bankAddress, + getPriorAmountABI, + isMountedRef, + proposalsToMap.length, + votingResultsToSetData, + votingResultsToSetError, + web3Instance, + ] + ); + /** * Effects */ // Build result entries of `OffchainVotingResultEntries` useEffect(() => { - const proposalsToMap = Array.isArray(proposals) ? proposals : [proposals]; + buildOffchainVotingResultEntriesCached(); + }, [buildOffchainVotingResultEntriesCached]); + + useEffect(() => { + const proposalsToMapToSet = Array.isArray(proposals) + ? proposals + : [proposals]; + setProposalsToMap(proposalsToMapToSet); + }, [proposals]); + + /** + * Functions + */ + + async function buildOffchainVotingResultEntries() { if ( !bankAddress || !getPriorAmountABI || @@ -99,77 +199,34 @@ export function useOffchainVotingResults( return; } - setOffchainVotingResultsStatus(AsyncStatus.PENDING); - - const votingResultPromises = proposalsToMap.map(async (p) => { - const snapshot = p?.msg.payload.snapshot; - const idInSnapshot = p?.idInSnapshot; - - if (!idInSnapshot || !snapshot) return; - - const voterEntries = p?.votes?.map((v): [string, number] => { - const vote = v[Object.keys(v)[0]]; - - return [ - /** - * Must be the true member's address for calculating voting power. - * This value is (or at least should be) derived from `OffchainVoting.memberAddressesByDelegatedKey`. - */ - vote.msg.payload.metadata.memberAddress, - vote.msg.payload.choice, - ]; - }); - - if (!voterEntries || !voterEntries.length) return; - - // Dedupe any duplicate addresses to be safe. - const voterAddressesAndChoices = Object.entries( - Object.fromEntries(voterEntries) - ); + try { + setOffchainVotingResultsStatus(AsyncStatus.PENDING); - try { - const result = await getUnitsPerChoiceCached({ - bankAddress, - getPriorAmountABI, - snapshot, - voterAddressesAndChoices, - web3Instance, - }); - - return [idInSnapshot, result]; - } catch (error) { - return; + if (votingResultsToSetError) { + throw votingResultsToSetError; } - }); - Promise.all(votingResultPromises) - .then((p) => p.filter((p) => p) as OffchainVotingResultEntries) - .then((r) => { + if (votingResultsToSetData) { + const filteredVotingResultsToSetData = votingResultsToSetData.filter( + (p) => p + ); + if (!isMountedRef.current) return; setOffchainVotingResultsStatus(AsyncStatus.FULFILLED); - setVotingResults(r); + setVotingResults( + filteredVotingResultsToSetData as OffchainVotingResultEntries + ); setOffchainVotingResultsError(undefined); - }) - .catch((error) => { - if (!isMountedRef.current) return; - - setOffchainVotingResultsStatus(AsyncStatus.REJECTED); - setVotingResults([]); - setOffchainVotingResultsError(error); - }); - }, [ - bankAddress, - getPriorAmountABI, - getUnitsPerChoiceCached, - isMountedRef, - proposals, - web3Instance, - ]); + } + } catch (error) { + if (!isMountedRef.current) return; - /** - * Functions - */ + setOffchainVotingResultsStatus(AsyncStatus.REJECTED); + setVotingResults([]); + setOffchainVotingResultsError(error); + } + } async function getUnitsPerChoiceFromContract({ bankAddress, @@ -225,7 +282,7 @@ export function useOffchainVotingResults( const calls = [totalUnitsCall, ...unitsCalls]; const [totalUnitsResult, ...votingResults]: string[] = await multicall({ - calls, + calls: calls, web3Instance, }); diff --git a/src/components/proposals/hooks/useProposalOrDraft.ts b/src/components/proposals/hooks/useProposalOrDraft.ts index c1893eecb..916c575f1 100644 --- a/src/components/proposals/hooks/useProposalOrDraft.ts +++ b/src/components/proposals/hooks/useProposalOrDraft.ts @@ -4,6 +4,7 @@ import { SnapshotProposalResponse, SnapshotType, } from '@openlaw/snapshot-js-erc712'; +import {useQueryClient} from 'react-query'; import { Proposal, @@ -93,6 +94,12 @@ export function useProposalOrDraft( proposalsVotingAdaptersStatus, } = useProposalsVotingAdapter(proposalVotingAdapterId); + /** + * Their hooks + */ + + const queryClient = useQueryClient(); + /** * Cached callbacks */ @@ -175,13 +182,31 @@ export function useProposalOrDraft( if (refetchCount === 0) return; /** - * Provide a different Array reference to force a re-render - * of the `useProposalsVotingAdapter` hook. If the `id` argument changes, - * that's fine as well, but it's unlikely. + * Provide a different Array reference to force a re-render of the + * `useProposalsVotingAdapter` hook. If the `id` argument changes, that's + * fine as well, but it's unlikely. */ setProposalVotingAdapterId([id]); }, [id, refetchCount]); + useEffect(() => { + async function resetQueries() { + if (refetchCount === 0) return; + + /** + * Reset React Queries when `refetchCount` is incremented (proposal is + * sponsored/submitted on chain, proposal is voted on) + * + * Needed so queries can fetch data that has been updated by the change in + * proposal status + * + */ + await queryClient.resetQueries(); + } + + resetQueries(); + }, [queryClient, refetchCount]); + // Set overall async status useEffect(() => { const {STANDBY, PENDING, FULFILLED, REJECTED} = AsyncStatus; diff --git a/src/components/proposals/hooks/useProposalWithOffchainVoteStatus.ts b/src/components/proposals/hooks/useProposalWithOffchainVoteStatus.ts index cfd498d47..d94de5325 100644 --- a/src/components/proposals/hooks/useProposalWithOffchainVoteStatus.ts +++ b/src/components/proposals/hooks/useProposalWithOffchainVoteStatus.ts @@ -363,11 +363,11 @@ export function useProposalWithOffchainVoteStatus({ atExistsInDAO, atProcessedInDAO, atSponsoredInDAO, - status, initialAsyncChecksCompleted, isInVoting, isInVotingGracePeriod, offchainResultSubmitted, + status, ]); /** diff --git a/src/components/proposals/hooks/useProposals.ts b/src/components/proposals/hooks/useProposals.ts index fb60a0e2b..74e921571 100644 --- a/src/components/proposals/hooks/useProposals.ts +++ b/src/components/proposals/hooks/useProposals.ts @@ -1,4 +1,4 @@ -import {useEffect, useMemo, useState} from 'react'; +import {useEffect, useMemo, useState, useCallback} from 'react'; import {useSelector} from 'react-redux'; import { SnapshotDraftResponse, @@ -7,6 +7,7 @@ import { SnapshotProposalResponseData, SnapshotType, } from '@openlaw/snapshot-js-erc712'; +import {useQuery} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {DaoAdapterConstants} from '../../adapters-extensions/enums'; @@ -195,6 +196,58 @@ export function useProposals({ const {proposalsVotes, proposalsVotesError, proposalsVotesStatus} = useProposalsVotes(proposalsVotingAdapters); + /** + * React Query + */ + + const {data: snapshotDraftEntriesData, error: snapshotDraftEntriesError} = + useQuery( + ['snapshotDraftEntries', adapterAddress], + async () => { + if (!adapterAddress) return; + + return await getSnapshotDraftsByAdapterAddress(adapterAddress); + }, + { + enabled: !!adapterAddress, + } + ); + + const { + data: snapshotProposalEntriesData, + error: snapshotProposalEntriesError, + } = useQuery( + ['snapshotProposalEntries', adapterAddress], + async () => { + if (!adapterAddress) return; + + return await getSnapshotProposalsByAdapterAddress(adapterAddress); + }, + { + enabled: !!adapterAddress, + } + ); + + /** + * Variables + */ + + const allSnapshotDraftsAndProposalsError = + snapshotDraftEntriesError || snapshotProposalEntriesError; + + /** + * Cached callbacks + */ + + const handleGetAllSnapshotDraftsAndProposalsCached = useCallback( + handleGetAllSnapshotDraftsAndProposals, + [ + allSnapshotDraftsAndProposalsError, + snapshotDraftEntriesData, + snapshotProposalEntriesData, + ] + ); + /** * Effects */ @@ -213,8 +266,8 @@ export function useProposals({ useEffect(() => { if (!adapterAddress) return; - handleGetAllSnapshotDraftsAndProposals(adapterAddress); - }, [adapterAddress]); + handleGetAllSnapshotDraftsAndProposalsCached(); + }, [adapterAddress, handleGetAllSnapshotDraftsAndProposalsCached]); // Set the DAO proposal IDs we want to work with useEffect(() => { @@ -433,34 +486,31 @@ export function useProposals({ * Functions */ - async function handleGetAllSnapshotDraftsAndProposals( - adapterAddress: string - ) { + async function handleGetAllSnapshotDraftsAndProposals() { try { setSnapshotDraftAndProposalsStatus(AsyncStatus.PENDING); // Reset error setSnapshotDraftAndProposalsError(undefined); - const snapshotDraftEntries = await getSnapshotDraftsByAdapterAddress( - adapterAddress - ); + if (allSnapshotDraftsAndProposalsError) { + throw allSnapshotDraftsAndProposalsError; + } - const snapshotProposalEntries = - await getSnapshotProposalsByAdapterAddress(adapterAddress); + if (snapshotDraftEntriesData && snapshotProposalEntriesData) { + const mergedEntries = [ + ...snapshotDraftEntriesData, + ...snapshotProposalEntriesData, + ]; - const mergedEntries = [ - ...snapshotDraftEntries, - ...snapshotProposalEntries, - ]; + if (!mergedEntries.length) { + setSnapshotDraftAndProposalsStatus(AsyncStatus.FULFILLED); - if (!mergedEntries.length) { - setSnapshotDraftAndProposalsStatus(AsyncStatus.FULFILLED); + return; + } - return; + setSnapshotDraftAndProposals(mergedEntries); + setSnapshotDraftAndProposalsStatus(AsyncStatus.FULFILLED); } - - setSnapshotDraftAndProposals(mergedEntries); - setSnapshotDraftAndProposalsStatus(AsyncStatus.FULFILLED); } catch (error) { setSnapshotDraftAndProposalsStatus(AsyncStatus.REJECTED); setSnapshotDraftAndProposals(INITIAL_ARRAY); diff --git a/src/components/proposals/hooks/useProposals.unit.test.ts b/src/components/proposals/hooks/useProposals.unit.test.ts index 1f330740d..89ed53495 100644 --- a/src/components/proposals/hooks/useProposals.unit.test.ts +++ b/src/components/proposals/hooks/useProposals.unit.test.ts @@ -314,12 +314,9 @@ describe('useProposals unit tests', () => { await waitForValueToChange(() => result.current.proposalsStatus); // Assert fulfilled state - expect(result.current.proposals).toStrictEqual([]); expect(result.current.proposalsError).toBe(undefined); expect(result.current.proposalsStatus).toBe(AsyncStatus.FULFILLED); - await waitForValueToChange(() => result.current.proposals); - // Assert Draft expect(result.current.proposals[0].daoProposal).toEqual({ @@ -556,6 +553,7 @@ describe('useProposals unit tests', () => { ); await act(async () => { + // Assert initial state expect(result.current.proposals).toEqual([]); expect(result.current.proposalsError).toBe(undefined); expect(result.current.proposalsStatus).toBe(AsyncStatus.STANDBY); @@ -757,18 +755,17 @@ describe('useProposals unit tests', () => { await waitForValueToChange(() => result.current.proposalsStatus); + // Assert pending state expect(result.current.proposals).toStrictEqual([]); expect(result.current.proposalsError).toBe(undefined); expect(result.current.proposalsStatus).toBe(AsyncStatus.PENDING); await waitForValueToChange(() => result.current.proposalsStatus); - expect(result.current.proposals).toStrictEqual([]); + // Assert fulfilled state expect(result.current.proposalsError).toBe(undefined); expect(result.current.proposalsStatus).toBe(AsyncStatus.FULFILLED); - await waitForValueToChange(() => result.current.proposals); - // Assert Draft expect(result.current.proposals[0].daoProposal).toEqual({ @@ -1024,6 +1021,7 @@ describe('useProposals unit tests', () => { ); await act(async () => { + // Assert initial state expect(result.current.proposals).toEqual([]); expect(result.current.proposalsError).toBe(undefined); expect(result.current.proposalsStatus).toBe(AsyncStatus.STANDBY); @@ -1032,16 +1030,16 @@ describe('useProposals unit tests', () => { await waitForValueToChange(() => result.current.proposalsStatus); + // Assert pending state expect(result.current.proposals).toEqual([]); expect(result.current.proposalsError).toBe(undefined); expect(result.current.proposalsStatus).toBe(AsyncStatus.PENDING); await waitForValueToChange(() => result.current.proposalsStatus); + // Assert rejected state expect(result.current.proposalsStatus).toBe(AsyncStatus.REJECTED); - await waitForValueToChange(() => result.current.proposalsError); - expect(result.current.proposalsError?.message).toMatch( /something went wrong while fetching the/i ); diff --git a/src/components/proposals/hooks/useProposalsVotes.ts b/src/components/proposals/hooks/useProposalsVotes.ts index b846eb3b7..c50ec9e16 100644 --- a/src/components/proposals/hooks/useProposalsVotes.ts +++ b/src/components/proposals/hooks/useProposalsVotes.ts @@ -1,6 +1,7 @@ +import {AbiItem} from 'web3-utils/types'; import {useCallback, useEffect, useState} from 'react'; +import {useQuery} from 'react-query'; import {useSelector} from 'react-redux'; -import {AbiItem} from 'web3-utils/types'; import {AsyncStatus} from '../../../util/types'; import {multicall, MulticallTuple} from '../../web3/helpers'; @@ -37,10 +38,24 @@ export function useProposalsVotes( const registryAddress = useSelector( (s: StoreState) => s.contracts.DaoRegistryContract?.contractAddress ); - const registryABI = useSelector( - (s: StoreState) => s.contracts.DaoRegistryContract?.abi + + /** + * State + */ + + const [proposalsVotes, setProposalsVotes] = useState( + [] ); + const [proposalsVotesError, setProposalsVotesError] = useState(); + + const [proposalsVotesStatus, setProposalsVotesStatus] = useState( + AsyncStatus.STANDBY + ); + + const [safeProposalVotingAdapters, setSafeProposalVotingAdapters] = + useState(); + /** * Our hooks */ @@ -48,15 +63,53 @@ export function useProposalsVotes( const {web3Instance} = useWeb3Modal(); /** - * State + * React Query */ - const [proposalsVotes, setProposalsVotes] = useState( - [] + const {data: votesDataCallsData, error: votesDataCallsError} = useQuery( + ['votesDataCalls', safeProposalVotingAdapters], + async (): Promise => { + if (!safeProposalVotingAdapters || !registryAddress) return; + + return await Promise.all( + safeProposalVotingAdapters.map( + async ([ + proposalId, + {votingAdapterAddress, getVotingAdapterABI, votingAdapterName}, + ]): Promise => [ + votingAdapterAddress, + await getVotesDataABI(votingAdapterName, getVotingAdapterABI()), + /** + * We build the call arguments the same way for the different voting adapters + * (i.e. [dao, proposalId]). If we need to change this we can move it to another function. + */ + [registryAddress, proposalId], + ] + ) + ); + }, + { + enabled: + !!proposalVotingAdapters.length && + !!safeProposalVotingAdapters && + !!registryAddress && + !!web3Instance, + } ); - const [proposalsVotesError, setProposalsVotesError] = useState(); - const [proposalsVotesStatus, setProposalsVotesStatus] = useState( - AsyncStatus.STANDBY + + const {data: votesDataResults, error: votesDataResultsError} = useQuery( + ['votesDataResults', votesDataCallsData?.length], + async () => { + if (!votesDataCallsData?.length || !web3Instance) { + return; + } + + return await multicall({ + calls: votesDataCallsData, + web3Instance, + }); + }, + {enabled: !!votesDataCallsData?.length && !!web3Instance} ); /** @@ -64,10 +117,11 @@ export function useProposalsVotes( */ const getProposalsVotesOnchainCached = useCallback(getProposalsVotesOnchain, [ - proposalVotingAdapters, - registryABI, - registryAddress, - web3Instance, + safeProposalVotingAdapters, + votesDataCallsData, + votesDataCallsError, + votesDataResults, + votesDataResultsError, ]); /** @@ -75,28 +129,46 @@ export function useProposalsVotes( */ useEffect(() => { - getProposalsVotesOnchainCached(); - }, [getProposalsVotesOnchainCached]); - - /** - * Functions - */ - - async function getProposalsVotesOnchain() { if ( !proposalVotingAdapters.length || - !registryABI || + !safeProposalVotingAdapters || !registryAddress || !web3Instance ) { return; } + getProposalsVotesOnchainCached(); + }, [ + getProposalsVotesOnchainCached, + proposalVotingAdapters.length, + registryAddress, + safeProposalVotingAdapters, + web3Instance, + ]); + + useEffect(() => { + if (!proposalVotingAdapters.length || !web3Instance) { + return; + } + // Only use hex (more specifically `bytes32`) id's - const safeProposalVotingAdapters = proposalVotingAdapters.filter(([id]) => - web3Instance.utils.isHexStrict(id) + const safeProposalVotingAdaptersToSet = proposalVotingAdapters.filter( + ([id]) => web3Instance.utils.isHexStrict(id) ); + setSafeProposalVotingAdapters(safeProposalVotingAdaptersToSet); + }, [proposalVotingAdapters, web3Instance]); + + /** + * Functions + */ + + async function getProposalsVotesOnchain() { + if (!safeProposalVotingAdapters) { + return; + } + if (!safeProposalVotingAdapters.length) { setProposalsVotesStatus(AsyncStatus.FULFILLED); setProposalsVotes([]); @@ -107,43 +179,34 @@ export function useProposalsVotes( try { setProposalsVotesStatus(AsyncStatus.PENDING); + if (votesDataCallsError) { + throw votesDataCallsError; + } + // Build votes results - const votesDataCalls: MulticallTuple[] = await Promise.all( - safeProposalVotingAdapters.map( - async ([ - proposalId, - {votingAdapterAddress, getVotingAdapterABI, votingAdapterName}, - ]): Promise => [ - votingAdapterAddress, - await getVotesDataABI(votingAdapterName, getVotingAdapterABI()), - /** - * We build the call arguments the same way for the different voting adapters - * (i.e. [dao, proposalId]). If we need to change this we can move it to another function. - */ - [registryAddress, proposalId], - ] - ) - ); + if (votesDataCallsData) { + if (votesDataResultsError) { + throw votesDataResultsError; + } - const votesDataResults = await multicall({ - calls: votesDataCalls, - web3Instance, - }); + if (votesDataResults) { + setProposalsVotes( + safeProposalVotingAdapters.map( + ([proposalId, {votingAdapterName}], i) => [ + proposalId, + { + [votingAdapterName]: votesDataResults[i], + }, + ] + ) + ); - setProposalsVotesStatus(AsyncStatus.FULFILLED); - setProposalsVotes( - safeProposalVotingAdapters.map( - ([proposalId, {votingAdapterName}], i) => [ - proposalId, - { - [votingAdapterName]: votesDataResults[i], - }, - ] - ) - ); + setProposalsVotesStatus(AsyncStatus.FULFILLED); + } + } } catch (error) { - setProposalsVotesStatus(AsyncStatus.REJECTED); setProposalsVotes([]); + setProposalsVotesStatus(AsyncStatus.REJECTED); setProposalsVotesError(error); } } diff --git a/src/components/proposals/hooks/useProposalsVotes.unit.test.ts b/src/components/proposals/hooks/useProposalsVotes.unit.test.ts index ad6d0bde3..5e2f0483d 100644 --- a/src/components/proposals/hooks/useProposalsVotes.unit.test.ts +++ b/src/components/proposals/hooks/useProposalsVotes.unit.test.ts @@ -1,9 +1,11 @@ -import {act, renderHook} from '@testing-library/react-hooks'; import {AbiItem} from 'web3-utils/types'; +import {act, renderHook} from '@testing-library/react-hooks'; +import Web3 from 'web3'; import { DEFAULT_ETH_ADDRESS, DEFAULT_PROPOSAL_HASH, + FakeHttpProvider, } from '../../../test/helpers'; import {AsyncStatus} from '../../../util/types'; import {ProposalVotingAdapterData, ProposalVotingAdapterTuple} from '../types'; @@ -44,102 +46,102 @@ describe('useProposalsVotes unit tests', () => { ], ]; + let mockWeb3Provider: FakeHttpProvider; + let web3Instance: Web3; + await act(async () => { - const {result, waitForNextUpdate} = await renderHook( + const {result, waitForValueToChange} = await renderHook( () => useProposalsVotes(proposalsVotingAdapterTuples), { wrapper: Wrapper, initialProps: { useInit: true, useWallet: true, - getProps: ({mockWeb3Provider, web3Instance}) => { - /** - * @note Maintain the same order as the contract's struct. - * @note We use the same, single result for any off-chain votes repsonses for testing only. - */ - const offchainVotesDataResponse = - web3Instance.eth.abi.encodeParameter( - { - Voting: { - snapshot: 'uint256', - reporter: 'address', - resultRoot: 'bytes32', - nbYes: 'uint256', - nbNo: 'uint256', - startingTime: 'uint256', - gracePeriodStartingTime: 'uint256', - forceFailed: 'bool', - isChallenged: 'bool', - fallbackVotesCount: 'uint256', - }, - }, - { - snapshot: '8376297', - reporter: '0xf9731Ad60BeCA05E9FB7aE8Dd4B63BFA49675b68', - resultRoot: - '0x9298a7fccdf7655408a8106ff03c9cbf0610082cc0f00dfe4c8f73f57a60df71', - nbYes: '1', - nbNo: '0', - startingTime: '1617878162', - gracePeriodStartingTime: '1617964640', - forceFailed: false, - isChallenged: false, - fallbackVotesCount: '0', - } - ); - - /** - * @note Maintain the same order as the contract's struct. - */ - const onchainVotesDataResponse = - web3Instance.eth.abi.encodeParameter( - { - Voting: { - nbYes: 'uint256', - nbNo: 'uint256', - startingTime: 'uint256', - blockNumber: 'uint256', - }, - }, - { - blockNumber: '10', - nbNo: '50', - nbYes: '100', - startingTime: '1617878162', - } - ); - - // Mock votes data responses - mockWeb3Provider.injectResult( - web3Instance.eth.abi.encodeParameters( - ['uint256', 'bytes[]'], - [ - 0, - [ - offchainVotesDataResponse, - offchainVotesDataResponse, - onchainVotesDataResponse, - ], - ] - ) - ); + getProps: (p) => { + mockWeb3Provider = p.mockWeb3Provider; + web3Instance = p.web3Instance; }, }, } ); + const offchainVotesDataResponse = web3Instance.eth.abi.encodeParameter( + { + Voting: { + snapshot: 'uint256', + reporter: 'address', + resultRoot: 'bytes32', + nbYes: 'uint256', + nbNo: 'uint256', + startingTime: 'uint256', + gracePeriodStartingTime: 'uint256', + forceFailed: 'bool', + isChallenged: 'bool', + fallbackVotesCount: 'uint256', + }, + }, + { + snapshot: '8376297', + reporter: '0xf9731Ad60BeCA05E9FB7aE8Dd4B63BFA49675b68', + resultRoot: + '0x9298a7fccdf7655408a8106ff03c9cbf0610082cc0f00dfe4c8f73f57a60df71', + nbYes: '1', + nbNo: '0', + startingTime: '1617878162', + gracePeriodStartingTime: '1617964640', + forceFailed: false, + isChallenged: false, + fallbackVotesCount: '0', + } + ); + + const onchainVotesDataResponse = web3Instance.eth.abi.encodeParameter( + { + Voting: { + nbYes: 'uint256', + nbNo: 'uint256', + startingTime: 'uint256', + blockNumber: 'uint256', + }, + }, + { + blockNumber: '10', + nbNo: '50', + nbYes: '100', + startingTime: '1617878162', + } + ); + + // Assert initial state expect(result.current.proposalsVotesStatus).toBe(AsyncStatus.STANDBY); expect(result.current.proposalsVotesError).toBe(undefined); expect(result.current.proposalsVotes).toMatchObject([]); - await waitForNextUpdate(); + await waitForValueToChange(() => result.current.proposalsVotesStatus); + + // Mock votes data responses + mockWeb3Provider.injectResult( + web3Instance.eth.abi.encodeParameters( + ['uint256', 'bytes[]'], + [ + 0, + [ + offchainVotesDataResponse, + offchainVotesDataResponse, + onchainVotesDataResponse, + ], + ] + ) + ); + // Assert pending state expect(result.current.proposalsVotesStatus).toBe(AsyncStatus.PENDING); expect(result.current.proposalsVotesError).toBe(undefined); expect(result.current.proposalsVotes).toMatchObject([]); - await waitForNextUpdate(); + await waitForValueToChange(() => result.current.proposalsVotesStatus); + // Assert fulfilled state expect(result.current.proposalsVotesStatus).toBe(AsyncStatus.FULFILLED); expect(result.current.proposalsVotesError).toBe(undefined); expect(result.current.proposalsVotes).toMatchObject([ @@ -241,60 +243,25 @@ describe('useProposalsVotes unit tests', () => { initialProps: { useInit: true, useWallet: true, - getProps: ({mockWeb3Provider, web3Instance}) => { - /** - * @note Maintain the same order as the contract's struct. - * @note We use the same, single result for any off-chain votes repsonses for testing only. - */ - const offchainVotesDataResponse = - web3Instance.eth.abi.encodeParameter( - { - Voting: { - snapshot: 'uint256', - proposalHash: 'bytes32', - reporter: 'address', - resultRoot: 'bytes32', - nbYes: 'uint256', - nbNo: 'uint256', - startingTime: 'uint256', - gracePeriodStartingTime: 'uint256', - isChallenged: 'bool', - fallbackVotesCount: 'uint256', - }, - }, - { - snapshot: '8376297', - proposalHash: DEFAULT_PROPOSAL_HASH, - reporter: '0xf9731Ad60BeCA05E9FB7aE8Dd4B63BFA49675b68', - resultRoot: - '0x9298a7fccdf7655408a8106ff03c9cbf0610082cc0f00dfe4c8f73f57a60df71', - nbYes: '1', - nbNo: '0', - startingTime: '1617878162', - gracePeriodStartingTime: '1617964640', - isChallenged: false, - fallbackVotesCount: '0', - } - ); - - // Mock votes data responses - mockWeb3Provider.injectResult( - web3Instance.eth.abi.encodeParameters( - ['uint256', 'bytes[]'], - [0, [offchainVotesDataResponse]] - ) - ); - }, }, } ); + // Assert initial state expect(result.current.proposalsVotesStatus).toBe(AsyncStatus.STANDBY); expect(result.current.proposalsVotesError).toBe(undefined); expect(result.current.proposalsVotes).toMatchObject([]); await waitForValueToChange(() => result.current.proposalsVotesStatus); + // Assert pending state + expect(result.current.proposalsVotesStatus).toBe(AsyncStatus.PENDING); + expect(result.current.proposalsVotesError).toBe(undefined); + expect(result.current.proposalsVotes).toMatchObject([]); + + await waitForValueToChange(() => result.current.proposalsVotesStatus); + + // Assert rejected state expect(result.current.proposalsVotesStatus).toBe(AsyncStatus.REJECTED); expect(result.current.proposalsVotesError?.message).toMatch( /no voting adapter name was found for "badvotingadaptername"/i @@ -316,6 +283,9 @@ describe('useProposalsVotes unit tests', () => { ], ]; + let mockWeb3Provider: FakeHttpProvider; + let web3Instance: Web3; + await act(async () => { const {result, waitForValueToChange} = await renderHook( () => useProposalsVotes(proposalsVotingAdapterTuples), @@ -324,67 +294,69 @@ describe('useProposalsVotes unit tests', () => { initialProps: { useInit: true, useWallet: true, - getProps: ({mockWeb3Provider, web3Instance}) => { - /** - * @note Maintain the same order as the contract's struct. - */ - const offchainVotesDataResponse = - web3Instance.eth.abi.encodeParameter( - { - Voting: { - snapshot: 'uint256', - reporter: 'address', - resultRoot: 'bytes32', - nbYes: 'uint256', - nbNo: 'uint256', - startingTime: 'uint256', - gracePeriodStartingTime: 'uint256', - forceFailed: 'bool', - isChallenged: 'bool', - fallbackVotesCount: 'uint256', - }, - }, - { - snapshot: '8376297', - reporter: '0xf9731Ad60BeCA05E9FB7aE8Dd4B63BFA49675b68', - resultRoot: - '0x9298a7fccdf7655408a8106ff03c9cbf0610082cc0f00dfe4c8f73f57a60df71', - nbYes: '1', - nbNo: '0', - startingTime: '1617878162', - gracePeriodStartingTime: '1617964640', - forceFailed: false, - isChallenged: false, - fallbackVotesCount: '0', - } - ); - - // Mock votes data responses - mockWeb3Provider.injectResult( - web3Instance.eth.abi.encodeParameters( - ['uint256', 'bytes[]'], - [0, [offchainVotesDataResponse]] - ) - ); + getProps: (p) => { + mockWeb3Provider = p.mockWeb3Provider; + web3Instance = p.web3Instance; }, }, } ); + const offchainVotesDataResponse = web3Instance.eth.abi.encodeParameter( + { + Voting: { + snapshot: 'uint256', + reporter: 'address', + resultRoot: 'bytes32', + nbYes: 'uint256', + nbNo: 'uint256', + startingTime: 'uint256', + gracePeriodStartingTime: 'uint256', + forceFailed: 'bool', + isChallenged: 'bool', + fallbackVotesCount: 'uint256', + }, + }, + { + snapshot: '8376297', + reporter: '0xf9731Ad60BeCA05E9FB7aE8Dd4B63BFA49675b68', + resultRoot: + '0x9298a7fccdf7655408a8106ff03c9cbf0610082cc0f00dfe4c8f73f57a60df71', + nbYes: '1', + nbNo: '0', + startingTime: '1617878162', + gracePeriodStartingTime: '1617964640', + forceFailed: false, + isChallenged: false, + fallbackVotesCount: '0', + } + ); + + // Assert initial state expect(result.current.proposalsVotesStatus).toBe(AsyncStatus.STANDBY); expect(result.current.proposalsVotesError).toBe(undefined); expect(result.current.proposalsVotes).toMatchObject([]); await waitForValueToChange(() => result.current.proposalsVotesStatus); + // Mock votes data responses + mockWeb3Provider.injectResult( + web3Instance.eth.abi.encodeParameters( + ['uint256', 'bytes[]'], + [0, [offchainVotesDataResponse]] + ) + ); + + // Assert pending state expect(result.current.proposalsVotesStatus).toBe(AsyncStatus.PENDING); expect(result.current.proposalsVotesError).toBe(undefined); expect(result.current.proposalsVotes).toMatchObject([]); await waitForValueToChange(() => result.current.proposalsVotesStatus); + // Assert fulfilled state expect(result.current.proposalsVotesStatus).toBe(AsyncStatus.FULFILLED); - expect(result.current.proposalsVotesError?.message).toBe(undefined); + expect(result.current.proposalsVotesError).toBe(undefined); expect(result.current.proposalsVotes).toMatchObject([ [ '0x4662dd46b8ca7ce0852426f20bc53b02335432089bbe3a4c510b36741d81ca75', @@ -440,59 +412,18 @@ describe('useProposalsVotes unit tests', () => { initialProps: { useInit: true, useWallet: true, - getProps: ({mockWeb3Provider, web3Instance}) => { - /** - * @note Maintain the same order as the contract's struct. - */ - const offchainVotesDataResponse = - web3Instance.eth.abi.encodeParameter( - { - Voting: { - snapshot: 'uint256', - proposalHash: 'bytes32', - reporter: 'address', - resultRoot: 'bytes32', - nbYes: 'uint256', - nbNo: 'uint256', - startingTime: 'uint256', - gracePeriodStartingTime: 'uint256', - isChallenged: 'bool', - fallbackVotesCount: 'uint256', - }, - }, - { - snapshot: '8376297', - proposalHash: DEFAULT_PROPOSAL_HASH, - reporter: '0xf9731Ad60BeCA05E9FB7aE8Dd4B63BFA49675b68', - resultRoot: - '0x9298a7fccdf7655408a8106ff03c9cbf0610082cc0f00dfe4c8f73f57a60df71', - nbYes: '1', - nbNo: '0', - startingTime: '1617878162', - gracePeriodStartingTime: '1617964640', - isChallenged: false, - fallbackVotesCount: '0', - } - ); - - // Mock votes data responses - mockWeb3Provider.injectResult( - web3Instance.eth.abi.encodeParameters( - ['uint256', 'bytes[]'], - [0, [offchainVotesDataResponse]] - ) - ); - }, }, } ); + // Assert initial state expect(result.current.proposalsVotesStatus).toBe(AsyncStatus.STANDBY); expect(result.current.proposalsVotesError).toBe(undefined); expect(result.current.proposalsVotes).toMatchObject([]); await waitForValueToChange(() => result.current.proposalsVotesStatus); + // Assert fulfilled state expect(result.current.proposalsVotesStatus).toBe(AsyncStatus.FULFILLED); expect(result.current.proposalsVotesError).toBe(undefined); expect(result.current.proposalsVotes).toMatchObject([]); @@ -511,53 +442,11 @@ describe('useProposalsVotes unit tests', () => { initialProps: { useInit: true, useWallet: true, - getProps: ({mockWeb3Provider, web3Instance}) => { - /** - * @note Maintain the same order as the contract's struct. - */ - const offchainVotesDataResponse = - web3Instance.eth.abi.encodeParameter( - { - Voting: { - snapshot: 'uint256', - proposalHash: 'bytes32', - reporter: 'address', - resultRoot: 'bytes32', - nbYes: 'uint256', - nbNo: 'uint256', - startingTime: 'uint256', - gracePeriodStartingTime: 'uint256', - isChallenged: 'bool', - fallbackVotesCount: 'uint256', - }, - }, - { - snapshot: '8376297', - proposalHash: DEFAULT_PROPOSAL_HASH, - reporter: '0xf9731Ad60BeCA05E9FB7aE8Dd4B63BFA49675b68', - resultRoot: - '0x9298a7fccdf7655408a8106ff03c9cbf0610082cc0f00dfe4c8f73f57a60df71', - nbYes: '1', - nbNo: '0', - startingTime: '1617878162', - gracePeriodStartingTime: '1617964640', - isChallenged: false, - fallbackVotesCount: '0', - } - ); - - // Mock votes data responses - mockWeb3Provider.injectResult( - web3Instance.eth.abi.encodeParameters( - ['uint256', 'bytes[]'], - [0, [offchainVotesDataResponse]] - ) - ); - }, }, } ); + // Assert initial state expect(result.current.proposalsVotesStatus).toBe(AsyncStatus.STANDBY); expect(result.current.proposalsVotesError).toBe(undefined); expect(result.current.proposalsVotes).toMatchObject([]); diff --git a/src/components/proposals/hooks/useProposalsVotingState.ts b/src/components/proposals/hooks/useProposalsVotingState.ts index 1ede0d237..1d87b3766 100644 --- a/src/components/proposals/hooks/useProposalsVotingState.ts +++ b/src/components/proposals/hooks/useProposalsVotingState.ts @@ -1,6 +1,7 @@ import {useCallback, useEffect, useState} from 'react'; import {useSelector} from 'react-redux'; import {AbiItem} from 'web3-utils/types'; +import {useQuery} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {multicall, MulticallTuple} from '../../web3/helpers'; @@ -35,12 +36,6 @@ export function useProposalsVotingState( (s: StoreState) => s.contracts.DaoRegistryContract?.contractAddress ); - /** - * Our hooks - */ - - const {web3Instance} = useWeb3Modal(); - /** * State */ @@ -54,13 +49,58 @@ export function useProposalsVotingState( const [proposalsVotingStateStatus, setProposalsVotingStateStatus] = useState(AsyncStatus.STANDBY); + const [votingResultABIError, setVotingResultABIError] = useState(); + + const [proposalsVotingStateCalls, setProposalsVotingStateCalls] = + useState(); + + const [safeProposalVotingAdapters, setSafeProposalVotingAdapters] = + useState(); + + /** + * Our hooks + */ + + const {web3Instance} = useWeb3Modal(); + + /** + * React Query + */ + + const { + data: proposalsVotingStateResult, + error: proposalsVotingStateResultError, + } = useQuery( + ['proposalsVotingStateResult', proposalsVotingStateCalls], + async () => { + if (!proposalsVotingStateCalls?.length || !web3Instance) { + return; + } + + return await multicall({ + calls: proposalsVotingStateCalls, + web3Instance, + }); + }, + {enabled: !!proposalsVotingStateCalls?.length && !!web3Instance} + ); + /** * Cached callbacks */ const getProposalsVotingStateOnchainCached = useCallback( getProposalsVotingStateOnchain, - [proposalVotingAdapters, registryAddress, web3Instance] + [ + proposalVotingAdapters.length, + proposalsVotingStateCalls, + proposalsVotingStateResult, + proposalsVotingStateResultError, + registryAddress, + safeProposalVotingAdapters, + votingResultABIError, + web3Instance, + ] ); /** @@ -71,62 +111,96 @@ export function useProposalsVotingState( getProposalsVotingStateOnchainCached(); }, [getProposalsVotingStateOnchainCached]); - /** - * Functions - */ - - async function getProposalsVotingStateOnchain() { - if (!registryAddress || !proposalVotingAdapters.length || !web3Instance) { + useEffect(() => { + if (!proposalVotingAdapters.length || !web3Instance) { return; } // Only use hex (more specifically `bytes32`) id's - const safeProposalVotingAdapters = proposalVotingAdapters.filter(([id]) => - web3Instance.utils.isHexStrict(id) + const safeProposalVotingAdaptersToSet = proposalVotingAdapters.filter( + ([id]) => web3Instance.utils.isHexStrict(id) ); - if (!safeProposalVotingAdapters.length) { - setProposalsVotingStateStatus(AsyncStatus.FULFILLED); + setSafeProposalVotingAdapters(safeProposalVotingAdaptersToSet); + }, [proposalVotingAdapters, web3Instance]); - return; - } + useEffect(() => { + async function setProposalsVotingStateCallsPrepData() { + if (!registryAddress || !safeProposalVotingAdapters?.length) return; - try { const lazyIVotingABI = (await import('../../../abis/IVoting.json')) .default as AbiItem[]; - const votingResultAbi = lazyIVotingABI.find( + const votingResultABI = lazyIVotingABI.find( (ai) => ai.name === 'voteResult' ); - if (!votingResultAbi) { - throw new Error( - 'No "voteResult" ABI function was found on the "IVoting" contract.' + if (!votingResultABI) { + setVotingResultABIError( + new Error( + 'No "voteResult" ABI function was found on the "IVoting" contract.' + ) ); + + return; } - const calls: MulticallTuple[] = safeProposalVotingAdapters.map( - ([proposalId, {votingAdapterAddress}]) => [ - votingAdapterAddress, - votingResultAbi, - [registryAddress, proposalId], - ] - ); + const proposalsVotingStateCallsToSet: MulticallTuple[] = + safeProposalVotingAdapters.map( + ([proposalId, {votingAdapterAddress}]) => [ + votingAdapterAddress, + votingResultABI, + [registryAddress, proposalId], + ] + ); + setProposalsVotingStateCalls(proposalsVotingStateCallsToSet); + } - setProposalsVotingStateStatus(AsyncStatus.PENDING); + setProposalsVotingStateCallsPrepData(); + }, [registryAddress, safeProposalVotingAdapters]); - const proposalsVotingStateResult = await multicall({ - calls, - web3Instance, - }); + /** + * Functions + */ + + async function getProposalsVotingStateOnchain() { + if ( + !registryAddress || + !proposalVotingAdapters.length || + !safeProposalVotingAdapters || + !web3Instance + ) { + return; + } + if (!safeProposalVotingAdapters.length) { setProposalsVotingStateStatus(AsyncStatus.FULFILLED); - setProposaslsVotingState( - safeProposalVotingAdapters.map(([proposalId], i) => [ - proposalId, - proposalsVotingStateResult[i], - ]) - ); + + return; + } + + try { + if (votingResultABIError) { + throw votingResultABIError; + } + + if (!proposalsVotingStateCalls) return; + + setProposalsVotingStateStatus(AsyncStatus.PENDING); + + if (proposalsVotingStateResultError) { + throw proposalsVotingStateResultError; + } + + if (proposalsVotingStateResult) { + setProposalsVotingStateStatus(AsyncStatus.FULFILLED); + setProposaslsVotingState( + safeProposalVotingAdapters.map(([proposalId], i) => [ + proposalId, + proposalsVotingStateResult[i], + ]) + ); + } } catch (error) { setProposalsVotingStateStatus(AsyncStatus.REJECTED); setProposaslsVotingState([]); diff --git a/src/components/proposals/voting/OffchainVotingAction.tsx b/src/components/proposals/voting/OffchainVotingAction.tsx index a705722d8..189337394 100644 --- a/src/components/proposals/voting/OffchainVotingAction.tsx +++ b/src/components/proposals/voting/OffchainVotingAction.tsx @@ -217,7 +217,7 @@ export function OffchainVotingAction( }); // Refetch to show the vote the user submitted - await refetchProposalOrDraft(); + refetchProposalOrDraft(); } catch (error) { setSubmitError(error); } diff --git a/src/index.tsx b/src/index.tsx index dd89b6766..5c610de6c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -10,6 +10,7 @@ import { NormalizedCacheObject, HttpLink, } from '@apollo/client'; +import {QueryClient, QueryClientProvider} from 'react-query'; import { ENVIRONMENT, @@ -80,6 +81,15 @@ export const getApolloClient = ( }), }); +// Create `QueryClient` +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + }, + }, +}); + if (root !== null) { render( @@ -96,15 +106,19 @@ if (root !== null) { }} providerOptions={WALLETCONNECT_PROVIDER_OPTIONS}> - - error ? ( - } /> - ) : isInitComplete ? ( - - ) : null - } - /> + + + error ? ( + } + /> + ) : isInitComplete ? ( + + ) : null + } + /> + diff --git a/src/setupTests.js b/src/setupTests.js index 059fc42a2..cb92fb297 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -1,10 +1,14 @@ // Adds jest-dom's custom assertions require('@testing-library/jest-dom/extend-expect'); +const {setLogger} = require('react-query'); const path = require('path'); /** * Require any env vars for testing environment * as early as possible + * + * @note Do not import anything which `` uses before the env vars are merged + * to avoid the `` using outdated env vars. */ const {parsed: parsedEnv} = require('dotenv').config({ path: `${path.resolve(process.cwd(), 'src/test/.env')}`, @@ -39,6 +43,18 @@ beforeAll(() => { onUnhandledRequest: 'warn', }); + /** + * Turn off `react-query` network error logging + * + * @see https://react-query.tanstack.com/guides/testing#turn-off-network-error-logging + */ + setLogger({ + log: console.log, + warn: console.warn, + // no more errors on the console + error: () => {}, + }); + /** * Mock window.matchMedia which is not supported in JSDOM. * @@ -66,8 +82,14 @@ beforeAll(() => { value: () => {}, }); }); + // If you need to add a handler after calling setupServer for some specific test // this will remove that handler for the rest of them // (which is important for test isolation): -afterEach(() => server.resetHandlers()); -afterAll(() => server.close()); +afterEach(() => { + server.resetHandlers(); +}); + +afterAll(() => { + server.close(); +}); diff --git a/src/test/Wrapper.tsx b/src/test/Wrapper.tsx index defcc7359..5585f7d1e 100644 --- a/src/test/Wrapper.tsx +++ b/src/test/Wrapper.tsx @@ -1,10 +1,11 @@ -import {Store} from 'redux'; +import {ApolloProvider} from '@apollo/react-hooks'; import {MemoryRouter} from 'react-router-dom'; -import type {LocationDescriptor} from 'history'; import {provider as Web3Provider} from 'web3-core/types'; import {Provider} from 'react-redux'; -import {ApolloProvider} from '@apollo/react-hooks'; +import {QueryClient, QueryClientProvider} from 'react-query'; +import {Store} from 'redux'; import React, {useEffect, useMemo, useState} from 'react'; +import type {LocationDescriptor} from 'history'; import Web3 from 'web3'; import { @@ -77,10 +78,27 @@ export default function Wrapper( const [store] = useState(getNewStore()); const [mockWeb3Provider] = useState(new FakeHttpProvider()); + const [web3Instance] = useState( new Web3(mockWeb3Provider as unknown as Web3Provider) ); + const [queryClient] = useState( + new QueryClient({ + defaultOptions: { + queries: { + /** + * Turn retries off for `react-query`, + * unless retries are set when using `useQuery`. + * + * @see https://react-query.tanstack.com/guides/testing#turn-off-retries + */ + retry: false, + }, + }, + }) + ); + /** * Cached values */ @@ -158,26 +176,33 @@ export default function Wrapper( */ useEffect(() => { - return () => { + return function cleanup() { // When `` unmounts, restore the original function. getAdapterAddressMock.then((v) => v?.mockRestore()); }; }, [getAdapterAddressMock]); useEffect(() => { - return () => { + return function cleanup() { // When `` unmounts, restore the original function. getExtensionAddressMock.then((v) => v?.mockRestore()); }; }, [getExtensionAddressMock]); useEffect(() => { - return () => { + return function cleanup() { // When `` unmounts, restore the original function. getVotingAdapterNameMock.then((v) => v?.mockRestore()); }; }, [getVotingAdapterNameMock]); + // Clear `queryClient` cache on unmount + useEffect(() => { + return function cleanup() { + queryClient.clear(); + }; + }, [queryClient]); + useEffect(() => { /** * Setup for `getConnectedMember` in `` @@ -257,7 +282,9 @@ export default function Wrapper( value={web3ContextValues as Web3ModalContextValue}> - {renderChildren(props.children)} + + {renderChildren(props.children)} +