From f1121979c9bcfcda0a088cf27dd8040713a107d1 Mon Sep 17 00:00:00 2001 From: jdville03 Date: Tue, 27 Jul 2021 19:21:50 -0700 Subject: [PATCH 01/25] WIP cache proposal data --- package-lock.json | 161 ++++++++++++++++++ package.json | 1 + .../proposals/hooks/useDaoProposals.ts | 37 +++- .../proposals/hooks/useProposals.ts | 33 +++- src/index.tsx | 36 +++- src/test/Wrapper.tsx | 7 +- 6 files changed, 249 insertions(+), 26 deletions(-) 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/proposals/hooks/useDaoProposals.ts b/src/components/proposals/hooks/useDaoProposals.ts index eeff4af50..497297b16 100644 --- a/src/components/proposals/hooks/useDaoProposals.ts +++ b/src/components/proposals/hooks/useDaoProposals.ts @@ -1,7 +1,8 @@ -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 {AbiItem} from 'web3-utils/types'; +import {useQueryClient} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {multicall, MulticallTuple} from '../../web3/helpers'; @@ -60,6 +61,20 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { const {web3Instance} = useWeb3Modal(); + /** + * Their hooks + */ + + const queryClient = useQueryClient(); + + /** + * Cached callbacks + */ + + const handleGetDaoProposalsCached = useCallback(handleGetDaoProposals, [ + queryClient, + ]); + /** * Effects */ @@ -74,13 +89,19 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { return; } - handleGetDaoProposals({ + handleGetDaoProposalsCached({ proposalIds, proposalsAbi, registryAddress, web3Instance, }); - }, [proposalIds, proposalsAbi, registryAddress, web3Instance]); + }, [ + handleGetDaoProposalsCached, + proposalIds, + proposalsAbi, + registryAddress, + web3Instance, + ]); /** * Functions @@ -122,10 +143,10 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { [id], ]); - const proposals = await multicall({ - calls, - web3Instance, - }); + const proposals = await queryClient.fetchQuery( + ['daoProposals', calls], + async () => await multicall({calls, web3Instance}) + ); setDaoProposals(safeProposalIds.map((id, i) => [id, proposals[i]])); setDaoProposalsStatus(AsyncStatus.FULFILLED); diff --git a/src/components/proposals/hooks/useProposals.ts b/src/components/proposals/hooks/useProposals.ts index fb60a0e2b..2a3a63520 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 {useQueryClient} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {DaoAdapterConstants} from '../../adapters-extensions/enums'; @@ -195,6 +196,21 @@ export function useProposals({ const {proposalsVotes, proposalsVotesError, proposalsVotesStatus} = useProposalsVotes(proposalsVotingAdapters); + /** + * Their hooks + */ + + const queryClient = useQueryClient(); + + /** + * Cached callbacks + */ + + const handleGetAllSnapshotDraftsAndProposalsCached = useCallback( + handleGetAllSnapshotDraftsAndProposals, + [queryClient] + ); + /** * Effects */ @@ -213,8 +229,8 @@ export function useProposals({ useEffect(() => { if (!adapterAddress) return; - handleGetAllSnapshotDraftsAndProposals(adapterAddress); - }, [adapterAddress]); + handleGetAllSnapshotDraftsAndProposalsCached(adapterAddress); + }, [adapterAddress, handleGetAllSnapshotDraftsAndProposalsCached]); // Set the DAO proposal IDs we want to work with useEffect(() => { @@ -441,12 +457,15 @@ export function useProposals({ // Reset error setSnapshotDraftAndProposalsError(undefined); - const snapshotDraftEntries = await getSnapshotDraftsByAdapterAddress( - adapterAddress + const snapshotDraftEntries = await queryClient.fetchQuery( + ['snapshotDraftsByAdapterAddress', adapterAddress], + async () => await getSnapshotDraftsByAdapterAddress(adapterAddress) ); - const snapshotProposalEntries = - await getSnapshotProposalsByAdapterAddress(adapterAddress); + const snapshotProposalEntries = await queryClient.fetchQuery( + ['snapshotProposalsByAdapterAddress', adapterAddress], + async () => await getSnapshotProposalsByAdapterAddress(adapterAddress) + ); const mergedEntries = [ ...snapshotDraftEntries, diff --git a/src/index.tsx b/src/index.tsx index dd89b6766..c071b0098 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -10,6 +10,9 @@ import { NormalizedCacheObject, HttpLink, } from '@apollo/client'; +import {QueryClient, QueryClientProvider} from 'react-query'; +// @todo Remove react-query dev tools before merging +import {ReactQueryDevtools} from 'react-query/devtools'; import { ENVIRONMENT, @@ -80,6 +83,15 @@ export const getApolloClient = ( }), }); +// Create `QueryClient` +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + }, + }, +}); + if (root !== null) { render( @@ -96,15 +108,21 @@ if (root !== null) { }} providerOptions={WALLETCONNECT_PROVIDER_OPTIONS}> - - error ? ( - } /> - ) : isInitComplete ? ( - - ) : null - } - /> + + + error ? ( + } + /> + ) : isInitComplete ? ( + + ) : null + } + /> + {/* @todo Remove react-query dev tools before merging */} + + diff --git a/src/test/Wrapper.tsx b/src/test/Wrapper.tsx index defcc7359..774f24034 100644 --- a/src/test/Wrapper.tsx +++ b/src/test/Wrapper.tsx @@ -6,6 +6,7 @@ import {Provider} from 'react-redux'; import {ApolloProvider} from '@apollo/react-hooks'; import React, {useEffect, useMemo, useState} from 'react'; import Web3 from 'web3'; +import {QueryClientProvider} from 'react-query'; import { Web3ModalContext, @@ -14,7 +15,7 @@ import { import {AsyncStatus} from '../util/types'; import {CHAINS as mockChains, WALLETCONNECT_PROVIDER_OPTIONS} from '../config'; import {DEFAULT_ETH_ADDRESS, FakeHttpProvider, getNewStore} from './helpers'; -import {getApolloClient} from '../index'; +import {getApolloClient, queryClient} from '../index'; import {VotingAdapterName} from '../components/adapters-extensions/enums'; import App from '../App'; import Init from '../Init'; @@ -257,7 +258,9 @@ export default function Wrapper( value={web3ContextValues as Web3ModalContextValue}> - {renderChildren(props.children)} + + {renderChildren(props.children)} + From 5a609f805e2266af38e54fc5b92ef5fb986140b0 Mon Sep 17 00:00:00 2001 From: jdville03 Date: Tue, 27 Jul 2021 23:16:06 -0700 Subject: [PATCH 02/25] WIP use queryClient --- .../proposals/hooks/useDaoProposals.ts | 5 +- .../hooks/useOffchainVotingResults.ts | 231 ++++++++++++------ .../proposals/hooks/useProposals.ts | 10 +- .../proposals/hooks/useProposalsVotes.ts | 56 +++-- .../hooks/useProposalsVotingAdapter.ts | 92 ++++--- .../hooks/useProposalsVotingState.ts | 20 +- 6 files changed, 281 insertions(+), 133 deletions(-) diff --git a/src/components/proposals/hooks/useDaoProposals.ts b/src/components/proposals/hooks/useDaoProposals.ts index 497297b16..9352d4e0b 100644 --- a/src/components/proposals/hooks/useDaoProposals.ts +++ b/src/components/proposals/hooks/useDaoProposals.ts @@ -145,7 +145,10 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { const proposals = await queryClient.fetchQuery( ['daoProposals', calls], - async () => await multicall({calls, web3Instance}) + async () => await multicall({calls, web3Instance}), + { + staleTime: 60000, + } ); setDaoProposals(safeProposalIds.map((id, i) => [id, proposals[i]])); diff --git a/src/components/proposals/hooks/useOffchainVotingResults.ts b/src/components/proposals/hooks/useOffchainVotingResults.ts index cc8663ae4..9e98c91f8 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 {useQueryClient} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {multicall, MulticallTuple} from '../../web3/helpers'; @@ -65,6 +66,12 @@ export function useOffchainVotingResults( const {web3Instance} = useWeb3Modal(); const {isMountedRef} = useIsMounted(); + /** + * Their hooks + */ + + const queryClient = useQueryClient(); + /** * Variables */ @@ -77,9 +84,21 @@ export function useOffchainVotingResults( * Cached callbacks */ - const getUnitsPerChoiceCached = useCallback( - getUnitsPerChoiceFromContract, - [] + const getUnitsPerChoiceCached = useCallback(getUnitsPerChoiceFromContract, [ + queryClient, + ]); + + const buildOffchainVotingResultEntriesCached = useCallback( + buildOffchainVotingResultEntries, + [ + bankAddress, + getPriorAmountABI, + getUnitsPerChoiceCached, + isMountedRef, + proposals, + queryClient, + web3Instance, + ] ); /** @@ -88,6 +107,14 @@ export function useOffchainVotingResults( // Build result entries of `OffchainVotingResultEntries` useEffect(() => { + buildOffchainVotingResultEntriesCached(); + }, [buildOffchainVotingResultEntriesCached]); + + /** + * Functions + */ + + async function buildOffchainVotingResultEntries() { const proposalsToMap = Array.isArray(proposals) ? proposals : [proposals]; if ( @@ -101,75 +128,129 @@ export function useOffchainVotingResults( 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; + // 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) 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; + // } + // }); - // Dedupe any duplicate addresses to be safe. - const voterAddressesAndChoices = Object.entries( - Object.fromEntries(voterEntries) - ); + try { + const votingResultsToSet: OffchainVotingResultEntries = + await queryClient.fetchQuery( + ['votingResultsToSet', proposalsToMap], + async () => + 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) 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; + } + }) + ).then((p) => p.filter((p) => p) as OffchainVotingResultEntries), + { + staleTime: 60000, + } + ); + + if (!isMountedRef.current) return; + + setOffchainVotingResultsStatus(AsyncStatus.FULFILLED); + setVotingResults(votingResultsToSet); + setOffchainVotingResultsError(undefined); + } catch (error) { + if (!isMountedRef.current) return; - try { - const result = await getUnitsPerChoiceCached({ - bankAddress, - getPriorAmountABI, - snapshot, - voterAddressesAndChoices, - web3Instance, - }); - - return [idInSnapshot, result]; - } catch (error) { - return; - } - }); - - Promise.all(votingResultPromises) - .then((p) => p.filter((p) => p) as OffchainVotingResultEntries) - .then((r) => { - if (!isMountedRef.current) return; - - setOffchainVotingResultsStatus(AsyncStatus.FULFILLED); - setVotingResults(r); - setOffchainVotingResultsError(undefined); - }) - .catch((error) => { - if (!isMountedRef.current) return; - - setOffchainVotingResultsStatus(AsyncStatus.REJECTED); - setVotingResults([]); - setOffchainVotingResultsError(error); - }); - }, [ - bankAddress, - getPriorAmountABI, - getUnitsPerChoiceCached, - isMountedRef, - proposals, - web3Instance, - ]); + setOffchainVotingResultsStatus(AsyncStatus.REJECTED); + setVotingResults([]); + setOffchainVotingResultsError(error); + } - /** - * Functions - */ + // Promise.all(votingResultPromises) + // .then((p) => p.filter((p) => p) as OffchainVotingResultEntries) + // .then((r) => { + // if (!isMountedRef.current) return; + + // setOffchainVotingResultsStatus(AsyncStatus.FULFILLED); + // setVotingResults(r); + // setOffchainVotingResultsError(undefined); + // }) + // .catch((error) => { + // if (!isMountedRef.current) return; + + // setOffchainVotingResultsStatus(AsyncStatus.REJECTED); + // setVotingResults([]); + // setOffchainVotingResultsError(error); + // }); + } async function getUnitsPerChoiceFromContract({ bankAddress, @@ -224,10 +305,14 @@ export function useOffchainVotingResults( const calls = [totalUnitsCall, ...unitsCalls]; - const [totalUnitsResult, ...votingResults]: string[] = await multicall({ - calls, - web3Instance, - }); + const [totalUnitsResult, ...votingResults]: string[] = + await queryClient.fetchQuery( + ['totalUnitsResultAndVotingResults', calls], + async () => await multicall({calls: calls, web3Instance}), + { + staleTime: 60000, + } + ); // Set Units values for choices votingResults.forEach((units, i) => { diff --git a/src/components/proposals/hooks/useProposals.ts b/src/components/proposals/hooks/useProposals.ts index 2a3a63520..d049983b8 100644 --- a/src/components/proposals/hooks/useProposals.ts +++ b/src/components/proposals/hooks/useProposals.ts @@ -459,12 +459,18 @@ export function useProposals({ const snapshotDraftEntries = await queryClient.fetchQuery( ['snapshotDraftsByAdapterAddress', adapterAddress], - async () => await getSnapshotDraftsByAdapterAddress(adapterAddress) + async () => await getSnapshotDraftsByAdapterAddress(adapterAddress), + { + staleTime: 60000, + } ); const snapshotProposalEntries = await queryClient.fetchQuery( ['snapshotProposalsByAdapterAddress', adapterAddress], - async () => await getSnapshotProposalsByAdapterAddress(adapterAddress) + async () => await getSnapshotProposalsByAdapterAddress(adapterAddress), + { + staleTime: 60000, + } ); const mergedEntries = [ diff --git a/src/components/proposals/hooks/useProposalsVotes.ts b/src/components/proposals/hooks/useProposalsVotes.ts index b846eb3b7..72e050bfa 100644 --- a/src/components/proposals/hooks/useProposalsVotes.ts +++ b/src/components/proposals/hooks/useProposalsVotes.ts @@ -1,6 +1,7 @@ import {useCallback, useEffect, useState} from 'react'; import {useSelector} from 'react-redux'; import {AbiItem} from 'web3-utils/types'; +import {useQueryClient} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {multicall, MulticallTuple} from '../../web3/helpers'; @@ -47,6 +48,12 @@ export function useProposalsVotes( const {web3Instance} = useWeb3Modal(); + /** + * Their hooks + */ + + const queryClient = useQueryClient(); + /** * State */ @@ -65,6 +72,7 @@ export function useProposalsVotes( const getProposalsVotesOnchainCached = useCallback(getProposalsVotesOnchain, [ proposalVotingAdapters, + queryClient, registryABI, registryAddress, web3Instance, @@ -108,27 +116,37 @@ export function useProposalsVotes( setProposalsVotesStatus(AsyncStatus.PENDING); // 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], - ] - ) + const votesDataCalls: MulticallTuple[] = await queryClient.fetchQuery( + ['votesDataCalls', safeProposalVotingAdapters], + async () => + 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], + ] + ) + ), + { + staleTime: 60000, + } ); - const votesDataResults = await multicall({ - calls: votesDataCalls, - web3Instance, - }); + const votesDataResults = await queryClient.fetchQuery( + ['votesDataResults', votesDataCalls], + async () => await multicall({calls: votesDataCalls, web3Instance}), + { + staleTime: 60000, + } + ); setProposalsVotesStatus(AsyncStatus.FULFILLED); setProposalsVotes( diff --git a/src/components/proposals/hooks/useProposalsVotingAdapter.ts b/src/components/proposals/hooks/useProposalsVotingAdapter.ts index 58e9bf7ca..8ab3ce041 100644 --- a/src/components/proposals/hooks/useProposalsVotingAdapter.ts +++ b/src/components/proposals/hooks/useProposalsVotingAdapter.ts @@ -1,5 +1,6 @@ import {useCallback, useEffect, useState} from 'react'; import {useSelector} from 'react-redux'; +import {useQueryClient} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {BURN_ADDRESS} from '../../../util/constants'; @@ -50,6 +51,12 @@ export function useProposalsVotingAdapter( const {web3Instance} = useWeb3Modal(); + /** + * Their hooks + */ + + const queryClient = useQueryClient(); + /** * State */ @@ -70,7 +77,7 @@ export function useProposalsVotingAdapter( const getProposalsVotingAdaptersOnchainCached = useCallback( getProposalsVotingAdaptersOnchain, - [proposalIds, registryABI, registryAddress, web3Instance] + [proposalIds, queryClient, registryABI, registryAddress, web3Instance] ); /** @@ -125,10 +132,15 @@ export function useProposalsVotingAdapter( setProposalsVotingAdaptersStatus(AsyncStatus.PENDING); - const votingAdapterAddressResults: string[] = await multicall({ - calls: votingAdapterCalls, - web3Instance, - }); + const votingAdapterAddressResults: string[] = + await queryClient.fetchQuery( + ['votingAdapterAddressResults', votingAdapterCalls], + async () => + await multicall({calls: votingAdapterCalls, web3Instance}), + { + staleTime: 60000, + } + ); const {default: lazyIVotingABI} = await import( '../../../abis/IVoting.json' @@ -177,35 +189,49 @@ export function useProposalsVotingAdapter( [], ]); - const adapterNameResults: VotingAdapterName[] = await multicall({ - calls: votingAdapterNameCalls, - web3Instance, - }); - - const votingAdaptersToSet = await Promise.all( - filteredProposalIds.map( - async (id, i): Promise => { - const votingAdapterABI = await getVotingAdapterABI( - adapterNameResults[i] - ); - const votingAdapterAddress = filteredVotingAdapterAddressResults[i]; - - return [ - id, - { - votingAdapterName: adapterNameResults[i], - votingAdapterAddress, - getVotingAdapterABI: () => votingAdapterABI, - getWeb3VotingAdapterContract: () => - new web3Instance.eth.Contract( - votingAdapterABI, - votingAdapterAddress - ) as any as T, - }, - ]; + const adapterNameResults: VotingAdapterName[] = + await queryClient.fetchQuery( + ['adapterNameResults', votingAdapterNameCalls], + async () => + await multicall({calls: votingAdapterNameCalls, web3Instance}), + { + staleTime: 60000, } - ) - ); + ); + + const votingAdaptersToSet: ProposalVotingAdapterTuple[] = + await queryClient.fetchQuery( + ['votingAdaptersToSet', filteredProposalIds], + async () => + await Promise.all( + filteredProposalIds.map( + async (id, i): Promise => { + const votingAdapterABI = await getVotingAdapterABI( + adapterNameResults[i] + ); + const votingAdapterAddress = + filteredVotingAdapterAddressResults[i]; + + return [ + id, + { + votingAdapterName: adapterNameResults[i], + votingAdapterAddress, + getVotingAdapterABI: () => votingAdapterABI, + getWeb3VotingAdapterContract: () => + new web3Instance.eth.Contract( + votingAdapterABI, + votingAdapterAddress + ) as any as T, + }, + ]; + } + ) + ), + { + staleTime: 60000, + } + ); setProposalsVotingAdapters(votingAdaptersToSet); setProposalsVotingAdaptersStatus(AsyncStatus.FULFILLED); diff --git a/src/components/proposals/hooks/useProposalsVotingState.ts b/src/components/proposals/hooks/useProposalsVotingState.ts index 1ede0d237..2a2bd3ed4 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 {useQueryClient} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {multicall, MulticallTuple} from '../../web3/helpers'; @@ -41,6 +42,12 @@ export function useProposalsVotingState( const {web3Instance} = useWeb3Modal(); + /** + * Their hooks + */ + + const queryClient = useQueryClient(); + /** * State */ @@ -60,7 +67,7 @@ export function useProposalsVotingState( const getProposalsVotingStateOnchainCached = useCallback( getProposalsVotingStateOnchain, - [proposalVotingAdapters, registryAddress, web3Instance] + [proposalVotingAdapters, queryClient, registryAddress, web3Instance] ); /** @@ -115,10 +122,13 @@ export function useProposalsVotingState( setProposalsVotingStateStatus(AsyncStatus.PENDING); - const proposalsVotingStateResult = await multicall({ - calls, - web3Instance, - }); + const proposalsVotingStateResult = await queryClient.fetchQuery( + ['proposalsVotingStateResult', calls], + async () => await multicall({calls, web3Instance}), + { + staleTime: 60000, + } + ); setProposalsVotingStateStatus(AsyncStatus.FULFILLED); setProposaslsVotingState( From 060efba016da2e57a7d8ef5a517378ae29b7bf04 Mon Sep 17 00:00:00 2001 From: jdville03 Date: Wed, 28 Jul 2021 23:59:45 -0700 Subject: [PATCH 03/25] WIP refactor with useQuery hooks --- .../proposals/hooks/useDaoProposals.ts | 80 ++-- .../hooks/useOffchainVotingResults.ts | 242 +++++------- .../proposals/hooks/useProposals.ts | 81 ++-- .../proposals/hooks/useProposalsVotes.ts | 148 ++++--- .../hooks/useProposalsVotingAdapter.ts | 364 ++++++++++++------ .../hooks/useProposalsVotingState.ts | 164 +++++--- 6 files changed, 651 insertions(+), 428 deletions(-) diff --git a/src/components/proposals/hooks/useDaoProposals.ts b/src/components/proposals/hooks/useDaoProposals.ts index 9352d4e0b..7242235d5 100644 --- a/src/components/proposals/hooks/useDaoProposals.ts +++ b/src/components/proposals/hooks/useDaoProposals.ts @@ -2,7 +2,7 @@ import {useEffect, useState, useCallback} from 'react'; import {useSelector} from 'react-redux'; import Web3 from 'web3'; import {AbiItem} from 'web3-utils/types'; -import {useQueryClient} from 'react-query'; +import {useQuery} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {multicall, MulticallTuple} from '../../web3/helpers'; @@ -55,6 +55,11 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { const [daoProposalsError, setDaoProposalsError] = useState(); + const [safeProposalIds, setSafeProposalIds] = useState(); + + const [daoProposalsCalls, setDaoProposalsCalls] = + useState(); + /** * Our hooks */ @@ -65,14 +70,25 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { * Their hooks */ - const queryClient = useQueryClient(); + const {data: daoProposalsData, error: daoProposalsQueryError} = useQuery( + ['daoProposals', daoProposalsCalls], + async () => + await multicall({ + calls: daoProposalsCalls as MulticallTuple[], + web3Instance: web3Instance as Web3, + }), + {enabled: !!daoProposalsCalls && !!web3Instance} + ); /** * Cached callbacks */ const handleGetDaoProposalsCached = useCallback(handleGetDaoProposals, [ - queryClient, + daoProposalsCalls, + daoProposalsData, + daoProposalsQueryError, + safeProposalIds, ]); /** @@ -103,6 +119,29 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { 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 */ @@ -119,12 +158,7 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { web3Instance: Web3; }) { try { - if (!proposalIds.length) return; - - // Only use hex (more specifically `bytes32`) id's - const safeProposalIds = proposalIds.filter( - web3Instance.utils.isHexStrict - ); + if (!proposalIds.length || !safeProposalIds) return; if (!safeProposalIds.length) { setDaoProposalsStatus(AsyncStatus.FULFILLED); @@ -137,22 +171,18 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { // Reset error setDaoProposalsError(undefined); - const calls: MulticallTuple[] = safeProposalIds.map((id) => [ - registryAddress, - proposalsAbi, - [id], - ]); - - const proposals = await queryClient.fetchQuery( - ['daoProposals', calls], - async () => await multicall({calls, web3Instance}), - { - staleTime: 60000, - } - ); - - setDaoProposals(safeProposalIds.map((id, i) => [id, proposals[i]])); - setDaoProposalsStatus(AsyncStatus.FULFILLED); + if (!daoProposalsCalls) return; + + if (daoProposalsQueryError) { + throw daoProposalsQueryError; + } + + 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/useOffchainVotingResults.ts b/src/components/proposals/hooks/useOffchainVotingResults.ts index 9e98c91f8..4049cbe5c 100644 --- a/src/components/proposals/hooks/useOffchainVotingResults.ts +++ b/src/components/proposals/hooks/useOffchainVotingResults.ts @@ -3,7 +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 {useQueryClient} from 'react-query'; +import {useQuery} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {multicall, MulticallTuple} from '../../web3/helpers'; @@ -59,44 +59,103 @@ export function useOffchainVotingResults( Error | undefined >(); + const [proposalsToMap, setProposalsToMap] = useState< + (SnapshotProposal | undefined)[] + >([]); + /** - * Our hooks + * Variables */ - const {web3Instance} = useWeb3Modal(); - const {isMountedRef} = useIsMounted(); + const getPriorAmountABI = bankABI?.find( + (item) => item.name === 'getPriorAmount' + ); /** - * Their hooks + * Our hooks */ - const queryClient = useQueryClient(); + const {web3Instance} = useWeb3Modal(); + const {isMountedRef} = useIsMounted(); /** - * Variables + * Their hooks */ - const getPriorAmountABI = bankABI?.find( - (item) => item.name === 'getPriorAmount' - ); + const {data: votingResultsToSetData, error: votingResultsToSetError} = + useQuery( + ['votingResultsToSet', proposalsToMap], + async () => { + 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) return; + + // Dedupe any duplicate addresses to be safe. + const voterAddressesAndChoices = Object.entries( + Object.fromEntries(voterEntries) + ); + + try { + const result = await getUnitsPerChoiceCached({ + bankAddress: bankAddress as string, + getPriorAmountABI: getPriorAmountABI as AbiItem, + snapshot, + voterAddressesAndChoices, + web3Instance: web3Instance as Web3, + }); + + return [idInSnapshot, result]; + } catch (error) { + return; + } + }) + ); + }, + { + enabled: + !!bankAddress && + !!getPriorAmountABI && + !!proposalsToMap.length && + !!web3Instance, + } + ); /** * Cached callbacks */ - const getUnitsPerChoiceCached = useCallback(getUnitsPerChoiceFromContract, [ - queryClient, - ]); + const getUnitsPerChoiceCached = useCallback( + getUnitsPerChoiceFromContract, + [] + ); const buildOffchainVotingResultEntriesCached = useCallback( buildOffchainVotingResultEntries, [ bankAddress, getPriorAmountABI, - getUnitsPerChoiceCached, isMountedRef, - proposals, - queryClient, + proposalsToMap.length, + votingResultsToSetData, + votingResultsToSetError, web3Instance, ] ); @@ -110,13 +169,19 @@ export function useOffchainVotingResults( buildOffchainVotingResultEntriesCached(); }, [buildOffchainVotingResultEntriesCached]); + useEffect(() => { + const proposalsToMapToSet = Array.isArray(proposals) + ? proposals + : [proposals]; + + setProposalsToMap(proposalsToMapToSet); + }, [proposals]); + /** * Functions */ async function buildOffchainVotingResultEntries() { - const proposalsToMap = Array.isArray(proposals) ? proposals : [proposals]; - if ( !bankAddress || !getPriorAmountABI || @@ -126,106 +191,26 @@ 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) 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; - // } - // }); - try { - const votingResultsToSet: OffchainVotingResultEntries = - await queryClient.fetchQuery( - ['votingResultsToSet', proposalsToMap], - async () => - 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) 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; - } - }) - ).then((p) => p.filter((p) => p) as OffchainVotingResultEntries), - { - staleTime: 60000, - } + setOffchainVotingResultsStatus(AsyncStatus.PENDING); + + if (votingResultsToSetError) { + throw votingResultsToSetError; + } + + if (votingResultsToSetData) { + const filteredVotingResultsToSetData = votingResultsToSetData.filter( + (p) => p ); - if (!isMountedRef.current) return; + if (!isMountedRef.current) return; - setOffchainVotingResultsStatus(AsyncStatus.FULFILLED); - setVotingResults(votingResultsToSet); - setOffchainVotingResultsError(undefined); + setOffchainVotingResultsStatus(AsyncStatus.FULFILLED); + setVotingResults( + filteredVotingResultsToSetData as OffchainVotingResultEntries + ); + setOffchainVotingResultsError(undefined); + } } catch (error) { if (!isMountedRef.current) return; @@ -233,23 +218,6 @@ export function useOffchainVotingResults( setVotingResults([]); setOffchainVotingResultsError(error); } - - // Promise.all(votingResultPromises) - // .then((p) => p.filter((p) => p) as OffchainVotingResultEntries) - // .then((r) => { - // if (!isMountedRef.current) return; - - // setOffchainVotingResultsStatus(AsyncStatus.FULFILLED); - // setVotingResults(r); - // setOffchainVotingResultsError(undefined); - // }) - // .catch((error) => { - // if (!isMountedRef.current) return; - - // setOffchainVotingResultsStatus(AsyncStatus.REJECTED); - // setVotingResults([]); - // setOffchainVotingResultsError(error); - // }); } async function getUnitsPerChoiceFromContract({ @@ -305,14 +273,10 @@ export function useOffchainVotingResults( const calls = [totalUnitsCall, ...unitsCalls]; - const [totalUnitsResult, ...votingResults]: string[] = - await queryClient.fetchQuery( - ['totalUnitsResultAndVotingResults', calls], - async () => await multicall({calls: calls, web3Instance}), - { - staleTime: 60000, - } - ); + const [totalUnitsResult, ...votingResults]: string[] = await multicall({ + calls: calls, + web3Instance, + }); // Set Units values for choices votingResults.forEach((units, i) => { diff --git a/src/components/proposals/hooks/useProposals.ts b/src/components/proposals/hooks/useProposals.ts index d049983b8..4f7b8f7df 100644 --- a/src/components/proposals/hooks/useProposals.ts +++ b/src/components/proposals/hooks/useProposals.ts @@ -7,7 +7,7 @@ import { SnapshotProposalResponseData, SnapshotType, } from '@openlaw/snapshot-js-erc712'; -import {useQueryClient} from 'react-query'; +import {useQuery} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {DaoAdapterConstants} from '../../adapters-extensions/enums'; @@ -200,7 +200,34 @@ export function useProposals({ * Their hooks */ - const queryClient = useQueryClient(); + const {data: snapshotDraftEntriesData, error: snapshotDraftEntriesError} = + useQuery( + ['snapshotDraftEntries', adapterAddress], + async () => + await getSnapshotDraftsByAdapterAddress(adapterAddress as string), + { + enabled: !!adapterAddress, + } + ); + + const { + data: snapshotProposalEntriesData, + error: snapshotProposalEntriesError, + } = useQuery( + ['snapshotProposalEntries', adapterAddress], + async () => + await getSnapshotProposalsByAdapterAddress(adapterAddress as string), + { + enabled: !!adapterAddress, + } + ); + + /** + * Variables + */ + + const allSnapshotDraftsAndProposalsError = + snapshotDraftEntriesError || snapshotProposalEntriesError; /** * Cached callbacks @@ -208,7 +235,11 @@ export function useProposals({ const handleGetAllSnapshotDraftsAndProposalsCached = useCallback( handleGetAllSnapshotDraftsAndProposals, - [queryClient] + [ + allSnapshotDraftsAndProposalsError, + snapshotDraftEntriesData, + snapshotProposalEntriesData, + ] ); /** @@ -229,7 +260,7 @@ export function useProposals({ useEffect(() => { if (!adapterAddress) return; - handleGetAllSnapshotDraftsAndProposalsCached(adapterAddress); + handleGetAllSnapshotDraftsAndProposalsCached(); }, [adapterAddress, handleGetAllSnapshotDraftsAndProposalsCached]); // Set the DAO proposal IDs we want to work with @@ -449,43 +480,31 @@ export function useProposals({ * Functions */ - async function handleGetAllSnapshotDraftsAndProposals( - adapterAddress: string - ) { + async function handleGetAllSnapshotDraftsAndProposals() { try { setSnapshotDraftAndProposalsStatus(AsyncStatus.PENDING); // Reset error setSnapshotDraftAndProposalsError(undefined); - const snapshotDraftEntries = await queryClient.fetchQuery( - ['snapshotDraftsByAdapterAddress', adapterAddress], - async () => await getSnapshotDraftsByAdapterAddress(adapterAddress), - { - staleTime: 60000, - } - ); + if (allSnapshotDraftsAndProposalsError) { + throw allSnapshotDraftsAndProposalsError; + } - const snapshotProposalEntries = await queryClient.fetchQuery( - ['snapshotProposalsByAdapterAddress', adapterAddress], - async () => await getSnapshotProposalsByAdapterAddress(adapterAddress), - { - staleTime: 60000, - } - ); + 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/useProposalsVotes.ts b/src/components/proposals/hooks/useProposalsVotes.ts index 72e050bfa..d24f78dee 100644 --- a/src/components/proposals/hooks/useProposalsVotes.ts +++ b/src/components/proposals/hooks/useProposalsVotes.ts @@ -1,7 +1,8 @@ import {useCallback, useEffect, useState} from 'react'; import {useSelector} from 'react-redux'; import {AbiItem} from 'web3-utils/types'; -import {useQueryClient} from 'react-query'; +import {useQuery} from 'react-query'; +import Web3 from 'web3'; import {AsyncStatus} from '../../../util/types'; import {multicall, MulticallTuple} from '../../web3/helpers'; @@ -43,27 +44,69 @@ export function useProposalsVotes( ); /** - * Our hooks + * State */ - const {web3Instance} = useWeb3Modal(); + const [proposalsVotes, setProposalsVotes] = useState( + [] + ); + + const [proposalsVotesError, setProposalsVotesError] = useState(); + + const [proposalsVotesStatus, setProposalsVotesStatus] = useState( + AsyncStatus.STANDBY + ); + + const [safeProposalVotingAdapters, setSafeProposalVotingAdapters] = + useState(); /** - * Their hooks + * Our hooks */ - const queryClient = useQueryClient(); + const {web3Instance} = useWeb3Modal(); /** - * State + * Their hooks */ - const [proposalsVotes, setProposalsVotes] = useState( - [] + const {data: votesDataCallsData, error: votesDataCallsError} = useQuery( + ['votesDataCalls', safeProposalVotingAdapters], + async () => + await Promise.all( + (safeProposalVotingAdapters as ProposalVotingAdapterTuple[]).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 as string, proposalId], + ] + ) + ), + { + enabled: + !!proposalVotingAdapters.length && + !!safeProposalVotingAdapters && + !!registryABI && + !!registryAddress && + !!web3Instance, + } ); - const [proposalsVotesError, setProposalsVotesError] = useState(); - const [proposalsVotesStatus, setProposalsVotesStatus] = useState( - AsyncStatus.STANDBY + + const {data: votesDataResults, error: votesDataResultsError} = useQuery( + ['votesDataResults', votesDataCallsData], + async () => + await multicall({ + calls: votesDataCallsData as MulticallTuple[], + web3Instance: web3Instance as Web3, + }), + {enabled: !!votesDataCallsData && !!web3Instance} ); /** @@ -71,10 +114,14 @@ export function useProposalsVotes( */ const getProposalsVotesOnchainCached = useCallback(getProposalsVotesOnchain, [ - proposalVotingAdapters, - queryClient, + proposalVotingAdapters.length, registryABI, registryAddress, + safeProposalVotingAdapters, + votesDataCallsData, + votesDataCallsError, + votesDataResults, + votesDataResultsError, web3Instance, ]); @@ -86,6 +133,19 @@ export function useProposalsVotes( getProposalsVotesOnchainCached(); }, [getProposalsVotesOnchainCached]); + useEffect(() => { + if (!proposalVotingAdapters.length || !web3Instance) { + return; + } + + // Only use hex (more specifically `bytes32`) id's + const safeProposalVotingAdaptersToSet = proposalVotingAdapters.filter( + ([id]) => web3Instance.utils.isHexStrict(id) + ); + + setSafeProposalVotingAdapters(safeProposalVotingAdaptersToSet); + }, [proposalVotingAdapters, web3Instance]); + /** * Functions */ @@ -93,6 +153,7 @@ export function useProposalsVotes( async function getProposalsVotesOnchain() { if ( !proposalVotingAdapters.length || + !safeProposalVotingAdapters || !registryABI || !registryAddress || !web3Instance @@ -100,11 +161,6 @@ export function useProposalsVotes( return; } - // Only use hex (more specifically `bytes32`) id's - const safeProposalVotingAdapters = proposalVotingAdapters.filter(([id]) => - web3Instance.utils.isHexStrict(id) - ); - if (!safeProposalVotingAdapters.length) { setProposalsVotesStatus(AsyncStatus.FULFILLED); setProposalsVotes([]); @@ -115,50 +171,30 @@ export function useProposalsVotes( try { setProposalsVotesStatus(AsyncStatus.PENDING); + if (votesDataCallsError) { + throw votesDataCallsError; + } + // Build votes results - const votesDataCalls: MulticallTuple[] = await queryClient.fetchQuery( - ['votesDataCalls', safeProposalVotingAdapters], - async () => - await Promise.all( + if (votesDataCallsData) { + if (votesDataResultsError) { + throw votesDataResultsError; + } + + if (votesDataResults) { + setProposalsVotesStatus(AsyncStatus.FULFILLED); + setProposalsVotes( safeProposalVotingAdapters.map( - async ([ + ([proposalId, {votingAdapterName}], i) => [ 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], + { + [votingAdapterName]: votesDataResults[i], + }, ] ) - ), - { - staleTime: 60000, - } - ); - - const votesDataResults = await queryClient.fetchQuery( - ['votesDataResults', votesDataCalls], - async () => await multicall({calls: votesDataCalls, web3Instance}), - { - staleTime: 60000, + ); } - ); - - setProposalsVotesStatus(AsyncStatus.FULFILLED); - setProposalsVotes( - safeProposalVotingAdapters.map( - ([proposalId, {votingAdapterName}], i) => [ - proposalId, - { - [votingAdapterName]: votesDataResults[i], - }, - ] - ) - ); + } } catch (error) { setProposalsVotesStatus(AsyncStatus.REJECTED); setProposalsVotes([]); diff --git a/src/components/proposals/hooks/useProposalsVotingAdapter.ts b/src/components/proposals/hooks/useProposalsVotingAdapter.ts index 8ab3ce041..29ef63fee 100644 --- a/src/components/proposals/hooks/useProposalsVotingAdapter.ts +++ b/src/components/proposals/hooks/useProposalsVotingAdapter.ts @@ -1,6 +1,7 @@ import {useCallback, useEffect, useState} from 'react'; import {useSelector} from 'react-redux'; -import {useQueryClient} from 'react-query'; +import {useQuery} from 'react-query'; +import Web3 from 'web3'; import {AsyncStatus} from '../../../util/types'; import {BURN_ADDRESS} from '../../../util/constants'; @@ -9,7 +10,7 @@ import {multicall, MulticallTuple} from '../../web3/helpers'; import {ProposalVotingAdapterData} from '../types'; import {StoreState} from '../../../store/types'; import {useWeb3Modal} from '../../web3/hooks'; -import {VotingAdapterName} from '../../adapters-extensions/enums'; +// import {VotingAdapterName} from '../../adapters-extensions/enums'; type ProposalVotingAdapterTuple = [ proposalId: string, @@ -45,18 +46,6 @@ export function useProposalsVotingAdapter( (s: StoreState) => s.contracts.DaoRegistryContract?.abi ); - /** - * Our hooks - */ - - const {web3Instance} = useWeb3Modal(); - - /** - * Their hooks - */ - - const queryClient = useQueryClient(); - /** * State */ @@ -71,13 +60,122 @@ export function useProposalsVotingAdapter( const [proposalsVotingAdaptersStatus, setProposalsVotingAdaptersStatus] = useState(AsyncStatus.STANDBY); + const [safeProposalIds, setSafeProposalIds] = useState(); + + const [votingAdapterCalls, setVotingAdapterCalls] = + useState(); + + const [votingAdapterABIError, setVotingAdapterABIError] = useState(); + + const [filteredProposalIds, setFilteredProposalIds] = useState(); + + const [ + filteredVotingAdapterAddressResults, + setFilteredVotingAdapterAddressResults, + ] = useState(); + + const [votingAdapterNameCalls, setVotingAdapterNameCalls] = + useState(); + + const [adapterNameABIError, setAdapterNameABIError] = useState(); + + /** + * Our hooks + */ + + const {web3Instance} = useWeb3Modal(); + + /** + * Their hooks + */ + + const { + data: votingAdapterAddressResults, + error: votingAdapterAddressResultsError, + } = useQuery( + ['votingAdapterAddressResults', votingAdapterCalls], + async () => + await multicall({ + calls: votingAdapterCalls as MulticallTuple[], + web3Instance: web3Instance as Web3, + }), + {enabled: !!votingAdapterCalls && !!web3Instance} + ); + + const {data: adapterNameResults, error: adapterNameResultsError} = useQuery( + ['adapterNameResults', votingAdapterNameCalls], + async () => + await multicall({ + calls: votingAdapterNameCalls as MulticallTuple[], + web3Instance: web3Instance as Web3, + }), + {enabled: !!votingAdapterNameCalls && !!web3Instance} + ); + + const {data: votingAdaptersToSetData, error: votingAdaptersToSetError} = + useQuery( + ['votingAdaptersToSet', filteredProposalIds], + async () => + await Promise.all( + (filteredProposalIds as string[]).map( + async (id, i): Promise => { + const votingAdapterABI = await getVotingAdapterABI( + adapterNameResults[i] + ); + const votingAdapterAddress = ( + filteredVotingAdapterAddressResults as string[] + )[i]; + + return [ + id, + { + votingAdapterName: adapterNameResults[i], + votingAdapterAddress, + getVotingAdapterABI: () => votingAdapterABI, + getWeb3VotingAdapterContract: () => + new (web3Instance as Web3).eth.Contract( + votingAdapterABI, + votingAdapterAddress + ) as any as T, + }, + ]; + } + ) + ), + { + enabled: + !!filteredProposalIds && + !!filteredVotingAdapterAddressResults && + !!adapterNameResults && + !!web3Instance, + } + ); + /** * Cached callbacks */ const getProposalsVotingAdaptersOnchainCached = useCallback( getProposalsVotingAdaptersOnchain, - [proposalIds, queryClient, registryABI, registryAddress, web3Instance] + [ + adapterNameABIError, + adapterNameResults, + adapterNameResultsError, + filteredProposalIds, + filteredVotingAdapterAddressResults, + proposalIds.length, + registryABI, + registryAddress, + safeProposalIds, + votingAdapterABIError, + votingAdapterAddressResults, + votingAdapterAddressResultsError, + votingAdapterCalls, + votingAdapterNameCalls, + votingAdaptersToSetData, + votingAdaptersToSetError, + web3Instance, + ] ); /** @@ -88,6 +186,98 @@ export function useProposalsVotingAdapter( getProposalsVotingAdaptersOnchainCached(); }, [getProposalsVotingAdaptersOnchainCached]); + 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 (!registryABI || !registryAddress || !safeProposalIds?.length) return; + + const votingAdapterABI = registryABI.find( + (ai) => ai.name === 'votingAdapter' + ); + + if (!votingAdapterABI) { + setVotingAdapterABIError( + new Error( + 'No "votingAdapter" ABI function was found in the DAO registry ABI.' + ) + ); + + return; + } + + // `DaoRegistry.votingAdapter` calls + const votingAdapterCallsToSet: MulticallTuple[] = safeProposalIds.map( + (id) => [registryAddress, votingAdapterABI, [id]] + ); + + setVotingAdapterCalls(votingAdapterCallsToSet); + }, [registryABI, registryAddress, safeProposalIds]); + + useEffect(() => { + async function setProposalsVotingAdaptersPrepData() { + if (!safeProposalIds || !votingAdapterAddressResults || !registryABI) + return; + + /** + * Filter out `safeProposalIds` which are not sponsored (i.e. voting adapter address === `BURN_ADDRESS`). + * Filter out `votingAdapterAddressResults` which equal the `BURN_ADDRESS`. + * + * This ensures these two arrays maintain the same length as they rely on indexes for the + * proposals to match up to the array of `multicall` results. + */ + const filteredProposalIdsToSet = safeProposalIds.filter( + (_id, i) => votingAdapterAddressResults[i] !== BURN_ADDRESS + ); + setFilteredProposalIds(filteredProposalIdsToSet); + + const filteredVotingAdapterAddressResultsToSet = + votingAdapterAddressResults.filter((a: string) => a !== BURN_ADDRESS); + setFilteredVotingAdapterAddressResults( + filteredVotingAdapterAddressResultsToSet + ); + + const {default: lazyIVotingABI} = await import( + '../../../abis/IVoting.json' + ); + const adapterNameABI = (lazyIVotingABI as typeof registryABI).find( + (ai) => ai.name === 'getAdapterName' + ); + + if (!adapterNameABI) { + setAdapterNameABIError( + new Error( + 'No "getAdapterName" ABI function was found in the IVoting ABI.' + ) + ); + + return; + } + + const votingAdapterNameCallsToSet: MulticallTuple[] = + filteredVotingAdapterAddressResultsToSet.map( + (votingAdapterAddress: string) => [ + votingAdapterAddress, + adapterNameABI, + [], + ] + ); + setVotingAdapterNameCalls(votingAdapterNameCallsToSet); + } + + setProposalsVotingAdaptersPrepData(); + }, [registryABI, safeProposalIds, votingAdapterAddressResults]); + /** * Functions */ @@ -95,6 +285,7 @@ export function useProposalsVotingAdapter( async function getProposalsVotingAdaptersOnchain(): Promise { if ( !proposalIds.length || + !safeProposalIds || !registryABI || !registryAddress || !web3Instance @@ -102,9 +293,6 @@ export function useProposalsVotingAdapter( return; } - // Only use hex (more specifically `bytes32`) id's - const safeProposalIds = proposalIds.filter(web3Instance.utils.isHexStrict); - if (!safeProposalIds.length) { setProposalsVotingAdapters(INITIAL_VOTING_ADAPTERS); setProposalsVotingAdaptersStatus(AsyncStatus.FULFILLED); @@ -113,128 +301,54 @@ export function useProposalsVotingAdapter( } try { - const votingAdapterABI = registryABI.find( - (ai) => ai.name === 'votingAdapter' - ); - - if (!votingAdapterABI) { - throw new Error( - 'No "votingAdapter" ABI function was found in the DAO registry ABI.' - ); + if (votingAdapterABIError) { + throw votingAdapterABIError; } - // `DaoRegistry.votingAdapter` calls - const votingAdapterCalls: MulticallTuple[] = safeProposalIds.map((id) => [ - registryAddress, - votingAdapterABI, - [id], - ]); + if (!votingAdapterCalls) return; setProposalsVotingAdaptersStatus(AsyncStatus.PENDING); - const votingAdapterAddressResults: string[] = - await queryClient.fetchQuery( - ['votingAdapterAddressResults', votingAdapterCalls], - async () => - await multicall({calls: votingAdapterCalls, web3Instance}), - { - staleTime: 60000, - } - ); - - const {default: lazyIVotingABI} = await import( - '../../../abis/IVoting.json' - ); - - const getAdapterNameABI = (lazyIVotingABI as typeof registryABI).find( - (ai) => ai.name === 'getAdapterName' - ); - - if (!getAdapterNameABI) { - throw new Error( - 'No "getAdapterName" ABI function was found in the IVoting ABI.' - ); + if (votingAdapterAddressResultsError) { + throw votingAdapterAddressResultsError; } - /** - * Filter out `safeProposalIds` which are not sponsored (i.e. voting adapter address === `BURN_ADDRESS`). - * Filter out `votingAdapterAddressResults` which equal the `BURN_ADDRESS`. - * - * This ensures these two arrays maintain the same length as they rely on indexes for the - * proposals to match up to the array of `multicall` results. - */ + if (votingAdapterAddressResults) { + if (adapterNameABIError) { + throw adapterNameABIError; + } - const filteredProposalIds = safeProposalIds.filter( - (_id, i) => votingAdapterAddressResults[i] !== BURN_ADDRESS - ); + if (!filteredProposalIds || !filteredVotingAdapterAddressResults) + return; - const filteredVotingAdapterAddressResults = - votingAdapterAddressResults.filter((a) => a !== BURN_ADDRESS); + /** + * Exit early if there's no voting adapter addresses. + * It means no proposals were found to be sponsored + */ + if (!filteredVotingAdapterAddressResults.length) { + setProposalsVotingAdapters(INITIAL_VOTING_ADAPTERS); + setProposalsVotingAdaptersStatus(AsyncStatus.FULFILLED); - /** - * Exit early if there's no voting adapter addresses. - * It means no proposals were found to be sponsored - */ - if (!filteredVotingAdapterAddressResults.length) { - setProposalsVotingAdapters(INITIAL_VOTING_ADAPTERS); - setProposalsVotingAdaptersStatus(AsyncStatus.FULFILLED); + return; + } - return; - } + if (!votingAdapterNameCalls) return; - const votingAdapterNameCalls: MulticallTuple[] = - filteredVotingAdapterAddressResults.map((votingAdapterAddress) => [ - votingAdapterAddress, - getAdapterNameABI, - [], - ]); - - const adapterNameResults: VotingAdapterName[] = - await queryClient.fetchQuery( - ['adapterNameResults', votingAdapterNameCalls], - async () => - await multicall({calls: votingAdapterNameCalls, web3Instance}), - { - staleTime: 60000, - } - ); + if (adapterNameResultsError) { + throw adapterNameResultsError; + } - const votingAdaptersToSet: ProposalVotingAdapterTuple[] = - await queryClient.fetchQuery( - ['votingAdaptersToSet', filteredProposalIds], - async () => - await Promise.all( - filteredProposalIds.map( - async (id, i): Promise => { - const votingAdapterABI = await getVotingAdapterABI( - adapterNameResults[i] - ); - const votingAdapterAddress = - filteredVotingAdapterAddressResults[i]; - - return [ - id, - { - votingAdapterName: adapterNameResults[i], - votingAdapterAddress, - getVotingAdapterABI: () => votingAdapterABI, - getWeb3VotingAdapterContract: () => - new web3Instance.eth.Contract( - votingAdapterABI, - votingAdapterAddress - ) as any as T, - }, - ]; - } - ) - ), - { - staleTime: 60000, + if (adapterNameResults) { + if (votingAdaptersToSetError) { + throw votingAdaptersToSetError; } - ); - setProposalsVotingAdapters(votingAdaptersToSet); - setProposalsVotingAdaptersStatus(AsyncStatus.FULFILLED); + if (votingAdaptersToSetData) { + setProposalsVotingAdapters(votingAdaptersToSetData); + setProposalsVotingAdaptersStatus(AsyncStatus.FULFILLED); + } + } + } } catch (error) { setProposalsVotingAdaptersStatus(AsyncStatus.REJECTED); setProposalsVotingAdapters([]); diff --git a/src/components/proposals/hooks/useProposalsVotingState.ts b/src/components/proposals/hooks/useProposalsVotingState.ts index 2a2bd3ed4..6fff34201 100644 --- a/src/components/proposals/hooks/useProposalsVotingState.ts +++ b/src/components/proposals/hooks/useProposalsVotingState.ts @@ -1,7 +1,8 @@ import {useCallback, useEffect, useState} from 'react'; import {useSelector} from 'react-redux'; import {AbiItem} from 'web3-utils/types'; -import {useQueryClient} from 'react-query'; +import {useQuery} from 'react-query'; +import Web3 from 'web3'; import {AsyncStatus} from '../../../util/types'; import {multicall, MulticallTuple} from '../../web3/helpers'; @@ -36,18 +37,6 @@ export function useProposalsVotingState( (s: StoreState) => s.contracts.DaoRegistryContract?.contractAddress ); - /** - * Our hooks - */ - - const {web3Instance} = useWeb3Modal(); - - /** - * Their hooks - */ - - const queryClient = useQueryClient(); - /** * State */ @@ -61,13 +50,53 @@ 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(); + + /** + * Their hooks + */ + + const { + data: proposalsVotingStateResult, + error: proposalsVotingStateResultError, + } = useQuery( + ['proposalsVotingStateResult', proposalsVotingStateCalls], + async () => + await multicall({ + calls: proposalsVotingStateCalls as MulticallTuple[], + web3Instance: web3Instance as Web3, + }), + {enabled: !!proposalsVotingStateCalls && !!web3Instance} + ); + /** * Cached callbacks */ const getProposalsVotingStateOnchainCached = useCallback( getProposalsVotingStateOnchain, - [proposalVotingAdapters, queryClient, registryAddress, web3Instance] + [ + proposalVotingAdapters.length, + proposalsVotingStateCalls, + proposalsVotingStateResult, + proposalsVotingStateResultError, + registryAddress, + safeProposalVotingAdapters, + votingResultABIError, + web3Instance, + ] ); /** @@ -78,65 +107,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 queryClient.fetchQuery( - ['proposalsVotingStateResult', calls], - async () => await multicall({calls, web3Instance}), - { - staleTime: 60000, - } - ); + /** + * 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([]); From 8a5316463fd623eca9dc97e708eda885f4faa39d Mon Sep 17 00:00:00 2001 From: jdville03 Date: Mon, 2 Aug 2021 15:08:18 -0700 Subject: [PATCH 04/25] apply caching strategy to governance proposals --- .../hooks/useGovernanceProposals.ts | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/components/governance/hooks/useGovernanceProposals.ts b/src/components/governance/hooks/useGovernanceProposals.ts index e83590f31..f8123eb1d 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,25 @@ export function useGovernanceProposals({ const {isMountedRef} = useIsMounted(); + 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 +132,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; From 676fdc2552888a12d9ce8c9c10a007c97676f701 Mon Sep 17 00:00:00 2001 From: jdville03 Date: Tue, 3 Aug 2021 00:18:51 -0700 Subject: [PATCH 05/25] apply proposal data caching to individual proposal details --- .../proposals/hooks/useProposalOrDraft.ts | 221 +++++++++++------- 1 file changed, 143 insertions(+), 78 deletions(-) diff --git a/src/components/proposals/hooks/useProposalOrDraft.ts b/src/components/proposals/hooks/useProposalOrDraft.ts index c1893eecb..b04ca9640 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 {useQuery} from 'react-query'; import { Proposal, @@ -29,6 +30,65 @@ const ERROR_PROPOSAL: string = 'Something went wrong while getting the proposal.'; const ERROR_PROPOSAL_NOT_FOUND: string = 'Proposal was not found.'; +// Gets Draft (unsponsored Proposal) from Snapshot Hub +async function getSnapshotDraftById( + id: string, + abortController?: AbortController +) { + try { + const draft = await fetch( + `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/draft/${id}`, + {signal: abortController?.signal} + ); + + if (!draft.ok) { + throw new Error(ERROR_PROPOSAL); + } + + const draftJSON: SnapshotDraftResponse = await draft.json(); + + return draftJSON; + } catch (error) { + throw error; + } +} + +// Gets Proposal from Snapshot Hub +async function getSnapshotProposalById( + id: string, + abortController?: AbortController, + type?: ProposalOrDraftSnapshotType +) { + try { + /** + * @note `searchUniqueDraftId` includes the draft id in the search for the proposal + * as a Tribute proposal's ID hash could be the Snapshot Draft's ID. + */ + const proposal = await fetch( + `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/proposal/${id}?searchUniqueDraftId=true&includeVotes=true`, + {signal: abortController?.signal} + ); + + if (!proposal.ok) { + /** + * If `type` is set then we know we can determine `handleGetDraft` + * will not be called after in `handleGetProposalOrDraft`. + */ + if (type === SnapshotType.proposal) { + throw new Error(ERROR_PROPOSAL); + } + + return; + } + + const proposalJSON: SnapshotProposalResponse = await proposal.json(); + + return proposalJSON; + } catch (error) { + throw error; + } +} + /** * useProposalOrDraft * @@ -83,6 +143,33 @@ export function useProposalOrDraft( const [refetchCount, updateRefetchCount] = useCounter(); + /** + * Their hooks + */ + + const {data: snapshotProposalEntryData, error: snapshotProposalEntryError} = + useQuery( + ['snapshotProposalEntry', id], + async () => await getSnapshotProposalById(id, abortController, type), + { + enabled: !!id && !!abortController?.signal, + } + ); + + const {data: snapshotDraftEntryData, error: snapshotDraftEntryError} = + useQuery( + ['snapshotDraftEntry', id], + async () => await getSnapshotDraftById(id, abortController), + { + enabled: + !!id && + !!abortController?.signal && + (type === SnapshotType.draft || + (!!snapshotProposalEntryData && + !Object.keys(snapshotProposalEntryData).length)), + } + ); + /** * Fetch on-chain voting adapter data for proposals. * Only returns data for proposals of which voting adapters have been assigned (i.e. sponsored). @@ -98,15 +185,15 @@ export function useProposalOrDraft( */ const handleGetDraftCached = useCallback(handleGetDraft, [ - abortController?.signal, - id, isMountedRef, + snapshotDraftEntryData, + snapshotDraftEntryError, ]); const handleGetProposalCached = useCallback(handleGetProposal, [ - abortController?.signal, - id, isMountedRef, + snapshotProposalEntryData, + snapshotProposalEntryError, type, ]); @@ -257,38 +344,33 @@ export function useProposalOrDraft( try { setProposalStatus(AsyncStatus.PENDING); - const response = await fetch( - `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/draft/${id}`, - {signal: abortController?.signal} - ); - - if (!response.ok) { - throw new Error(ERROR_PROPOSAL); + if (snapshotDraftEntryError) { + throw snapshotDraftEntryError; } - const responseJSON: SnapshotDraftResponse = await response.json(); - - if (!isMountedRef.current) return; + if (snapshotDraftEntryData) { + if (!isMountedRef.current) return; - // @note API does not provide a 404 - if (!responseJSON || !Object.keys(responseJSON).length) { - setProposalNotFound(true); + // @note API does not provide a 404 + if (!Object.keys(snapshotDraftEntryData).length) { + setProposalNotFound(true); - throw new Error(ERROR_PROPOSAL_NOT_FOUND); - } + throw new Error(ERROR_PROPOSAL_NOT_FOUND); + } - const idKey = Object.keys(responseJSON)[0]; - // Get the `SnapshotDraftResponseData` by the address key of the single result. - const draft: SnapshotDraft = { - idInDAO: idKey, - idInSnapshot: idKey, - ...responseJSON[idKey], - }; + const idKey = Object.keys(snapshotDraftEntryData)[0]; + // Get the `SnapshotDraftResponseData` by the address key of the single result. + const draft: SnapshotDraft = { + idInDAO: idKey, + idInSnapshot: idKey, + ...snapshotDraftEntryData[idKey], + }; - setProposalStatus(AsyncStatus.FULFILLED); - setSnapshotDraft(draft); + setProposalStatus(AsyncStatus.FULFILLED); + setSnapshotDraft(draft); - return draft; + return draft; + } } catch (error) { if (!isMountedRef.current) return; @@ -301,60 +383,43 @@ export function useProposalOrDraft( try { setProposalStatus(AsyncStatus.PENDING); - /** - * @note `searchUniqueDraftId` includes the draft id in the search for the proposal - * as a Tribute proposal's ID hash could be the Snapshot Draft's ID. - */ - const response = await fetch( - `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/proposal/${id}?searchUniqueDraftId=true&includeVotes=true`, - {signal: abortController?.signal} - ); - - if (!response.ok) { - /** - * If `type` is set then we know we can determine `handleGetDraft` - * will not be called after in `handleGetProposalOrDraft`. - */ - if (type === SnapshotType.proposal) { - throw new Error(ERROR_PROPOSAL); - } - - return; + if (snapshotProposalEntryError) { + throw snapshotProposalEntryError; } - const responseJSON: SnapshotProposalResponse = await response.json(); - - if (!isMountedRef.current) return; - - // @note API does not provide a 404 - if (!responseJSON || !Object.keys(responseJSON).length) { - /** - * If `type` is set then we know we can determine `handleGetDraft` - * will not be called after in `handleGetProposalOrDraft`. - */ - if (type === SnapshotType.proposal) { - setProposalNotFound(true); - throw new Error(ERROR_PROPOSAL_NOT_FOUND); + if (snapshotProposalEntryData) { + if (!isMountedRef.current) return; + + // @note API does not provide a 404 + if (!Object.keys(snapshotProposalEntryData).length) { + /** + * If `type` is set then we know we can determine `handleGetDraft` + * will not be called after in `handleGetProposalOrDraft`. + */ + if (type === SnapshotType.proposal) { + setProposalNotFound(true); + throw new Error(ERROR_PROPOSAL_NOT_FOUND); + } + + return; } - return; + const idKey = Object.keys(snapshotProposalEntryData)[0]; + // Determine ID submitted to DAO, i.e. if there's a Draft ID hash, we should use that. + const proposalId: string = + snapshotProposalEntryData[idKey]?.data.erc712DraftHash || idKey; + // Get the `SnapshotProposalResponseData` by the address key of the single result. + const proposal: SnapshotProposal = { + idInDAO: proposalId, + idInSnapshot: idKey, + ...snapshotProposalEntryData[idKey], + }; + + setProposalStatus(AsyncStatus.FULFILLED); + setSnapshotProposal(proposal); + + return proposal; } - - const idKey = Object.keys(responseJSON)[0]; - // Determine ID submitted to DAO, i.e. if there's a Draft ID hash, we should use that. - const proposalId: string = - responseJSON[idKey]?.data.erc712DraftHash || idKey; - // Get the `SnapshotProposalResponseData` by the address key of the single result. - const proposal: SnapshotProposal = { - idInDAO: proposalId, - idInSnapshot: idKey, - ...responseJSON[idKey], - }; - - setProposalStatus(AsyncStatus.FULFILLED); - setSnapshotProposal(proposal); - - return proposal; } catch (error) { if (!isMountedRef.current) return; From dd12b94f46652303d9e9a7f76645bb6653b14369 Mon Sep 17 00:00:00 2001 From: jdville03 Date: Tue, 3 Aug 2021 00:33:22 -0700 Subject: [PATCH 06/25] Revert "apply proposal data caching to individual proposal details" This reverts commit 32615783a48b4a4d9097424ebae0f6bd628edf02. --- .../proposals/hooks/useProposalOrDraft.ts | 221 +++++++----------- 1 file changed, 78 insertions(+), 143 deletions(-) diff --git a/src/components/proposals/hooks/useProposalOrDraft.ts b/src/components/proposals/hooks/useProposalOrDraft.ts index b04ca9640..c1893eecb 100644 --- a/src/components/proposals/hooks/useProposalOrDraft.ts +++ b/src/components/proposals/hooks/useProposalOrDraft.ts @@ -4,7 +4,6 @@ import { SnapshotProposalResponse, SnapshotType, } from '@openlaw/snapshot-js-erc712'; -import {useQuery} from 'react-query'; import { Proposal, @@ -30,65 +29,6 @@ const ERROR_PROPOSAL: string = 'Something went wrong while getting the proposal.'; const ERROR_PROPOSAL_NOT_FOUND: string = 'Proposal was not found.'; -// Gets Draft (unsponsored Proposal) from Snapshot Hub -async function getSnapshotDraftById( - id: string, - abortController?: AbortController -) { - try { - const draft = await fetch( - `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/draft/${id}`, - {signal: abortController?.signal} - ); - - if (!draft.ok) { - throw new Error(ERROR_PROPOSAL); - } - - const draftJSON: SnapshotDraftResponse = await draft.json(); - - return draftJSON; - } catch (error) { - throw error; - } -} - -// Gets Proposal from Snapshot Hub -async function getSnapshotProposalById( - id: string, - abortController?: AbortController, - type?: ProposalOrDraftSnapshotType -) { - try { - /** - * @note `searchUniqueDraftId` includes the draft id in the search for the proposal - * as a Tribute proposal's ID hash could be the Snapshot Draft's ID. - */ - const proposal = await fetch( - `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/proposal/${id}?searchUniqueDraftId=true&includeVotes=true`, - {signal: abortController?.signal} - ); - - if (!proposal.ok) { - /** - * If `type` is set then we know we can determine `handleGetDraft` - * will not be called after in `handleGetProposalOrDraft`. - */ - if (type === SnapshotType.proposal) { - throw new Error(ERROR_PROPOSAL); - } - - return; - } - - const proposalJSON: SnapshotProposalResponse = await proposal.json(); - - return proposalJSON; - } catch (error) { - throw error; - } -} - /** * useProposalOrDraft * @@ -143,33 +83,6 @@ export function useProposalOrDraft( const [refetchCount, updateRefetchCount] = useCounter(); - /** - * Their hooks - */ - - const {data: snapshotProposalEntryData, error: snapshotProposalEntryError} = - useQuery( - ['snapshotProposalEntry', id], - async () => await getSnapshotProposalById(id, abortController, type), - { - enabled: !!id && !!abortController?.signal, - } - ); - - const {data: snapshotDraftEntryData, error: snapshotDraftEntryError} = - useQuery( - ['snapshotDraftEntry', id], - async () => await getSnapshotDraftById(id, abortController), - { - enabled: - !!id && - !!abortController?.signal && - (type === SnapshotType.draft || - (!!snapshotProposalEntryData && - !Object.keys(snapshotProposalEntryData).length)), - } - ); - /** * Fetch on-chain voting adapter data for proposals. * Only returns data for proposals of which voting adapters have been assigned (i.e. sponsored). @@ -185,15 +98,15 @@ export function useProposalOrDraft( */ const handleGetDraftCached = useCallback(handleGetDraft, [ + abortController?.signal, + id, isMountedRef, - snapshotDraftEntryData, - snapshotDraftEntryError, ]); const handleGetProposalCached = useCallback(handleGetProposal, [ + abortController?.signal, + id, isMountedRef, - snapshotProposalEntryData, - snapshotProposalEntryError, type, ]); @@ -344,33 +257,38 @@ export function useProposalOrDraft( try { setProposalStatus(AsyncStatus.PENDING); - if (snapshotDraftEntryError) { - throw snapshotDraftEntryError; + const response = await fetch( + `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/draft/${id}`, + {signal: abortController?.signal} + ); + + if (!response.ok) { + throw new Error(ERROR_PROPOSAL); } - if (snapshotDraftEntryData) { - if (!isMountedRef.current) return; + const responseJSON: SnapshotDraftResponse = await response.json(); - // @note API does not provide a 404 - if (!Object.keys(snapshotDraftEntryData).length) { - setProposalNotFound(true); + if (!isMountedRef.current) return; - throw new Error(ERROR_PROPOSAL_NOT_FOUND); - } + // @note API does not provide a 404 + if (!responseJSON || !Object.keys(responseJSON).length) { + setProposalNotFound(true); + + throw new Error(ERROR_PROPOSAL_NOT_FOUND); + } - const idKey = Object.keys(snapshotDraftEntryData)[0]; - // Get the `SnapshotDraftResponseData` by the address key of the single result. - const draft: SnapshotDraft = { - idInDAO: idKey, - idInSnapshot: idKey, - ...snapshotDraftEntryData[idKey], - }; + const idKey = Object.keys(responseJSON)[0]; + // Get the `SnapshotDraftResponseData` by the address key of the single result. + const draft: SnapshotDraft = { + idInDAO: idKey, + idInSnapshot: idKey, + ...responseJSON[idKey], + }; - setProposalStatus(AsyncStatus.FULFILLED); - setSnapshotDraft(draft); + setProposalStatus(AsyncStatus.FULFILLED); + setSnapshotDraft(draft); - return draft; - } + return draft; } catch (error) { if (!isMountedRef.current) return; @@ -383,43 +301,60 @@ export function useProposalOrDraft( try { setProposalStatus(AsyncStatus.PENDING); - if (snapshotProposalEntryError) { - throw snapshotProposalEntryError; + /** + * @note `searchUniqueDraftId` includes the draft id in the search for the proposal + * as a Tribute proposal's ID hash could be the Snapshot Draft's ID. + */ + const response = await fetch( + `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/proposal/${id}?searchUniqueDraftId=true&includeVotes=true`, + {signal: abortController?.signal} + ); + + if (!response.ok) { + /** + * If `type` is set then we know we can determine `handleGetDraft` + * will not be called after in `handleGetProposalOrDraft`. + */ + if (type === SnapshotType.proposal) { + throw new Error(ERROR_PROPOSAL); + } + + return; } - if (snapshotProposalEntryData) { - if (!isMountedRef.current) return; - - // @note API does not provide a 404 - if (!Object.keys(snapshotProposalEntryData).length) { - /** - * If `type` is set then we know we can determine `handleGetDraft` - * will not be called after in `handleGetProposalOrDraft`. - */ - if (type === SnapshotType.proposal) { - setProposalNotFound(true); - throw new Error(ERROR_PROPOSAL_NOT_FOUND); - } - - return; + const responseJSON: SnapshotProposalResponse = await response.json(); + + if (!isMountedRef.current) return; + + // @note API does not provide a 404 + if (!responseJSON || !Object.keys(responseJSON).length) { + /** + * If `type` is set then we know we can determine `handleGetDraft` + * will not be called after in `handleGetProposalOrDraft`. + */ + if (type === SnapshotType.proposal) { + setProposalNotFound(true); + throw new Error(ERROR_PROPOSAL_NOT_FOUND); } - const idKey = Object.keys(snapshotProposalEntryData)[0]; - // Determine ID submitted to DAO, i.e. if there's a Draft ID hash, we should use that. - const proposalId: string = - snapshotProposalEntryData[idKey]?.data.erc712DraftHash || idKey; - // Get the `SnapshotProposalResponseData` by the address key of the single result. - const proposal: SnapshotProposal = { - idInDAO: proposalId, - idInSnapshot: idKey, - ...snapshotProposalEntryData[idKey], - }; - - setProposalStatus(AsyncStatus.FULFILLED); - setSnapshotProposal(proposal); - - return proposal; + return; } + + const idKey = Object.keys(responseJSON)[0]; + // Determine ID submitted to DAO, i.e. if there's a Draft ID hash, we should use that. + const proposalId: string = + responseJSON[idKey]?.data.erc712DraftHash || idKey; + // Get the `SnapshotProposalResponseData` by the address key of the single result. + const proposal: SnapshotProposal = { + idInDAO: proposalId, + idInSnapshot: idKey, + ...responseJSON[idKey], + }; + + setProposalStatus(AsyncStatus.FULFILLED); + setSnapshotProposal(proposal); + + return proposal; } catch (error) { if (!isMountedRef.current) return; From c5cd5f0b21f96ca9ef64d4913e7980663b403d1c Mon Sep 17 00:00:00 2001 From: jdville03 Date: Tue, 3 Aug 2021 10:07:49 -0700 Subject: [PATCH 07/25] Revert "Revert "apply proposal data caching to individual proposal details"" This reverts commit faa91beb8b7e5d6bb11a647dcdd2290966033c67. --- .../proposals/hooks/useProposalOrDraft.ts | 221 +++++++++++------- 1 file changed, 143 insertions(+), 78 deletions(-) diff --git a/src/components/proposals/hooks/useProposalOrDraft.ts b/src/components/proposals/hooks/useProposalOrDraft.ts index c1893eecb..b04ca9640 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 {useQuery} from 'react-query'; import { Proposal, @@ -29,6 +30,65 @@ const ERROR_PROPOSAL: string = 'Something went wrong while getting the proposal.'; const ERROR_PROPOSAL_NOT_FOUND: string = 'Proposal was not found.'; +// Gets Draft (unsponsored Proposal) from Snapshot Hub +async function getSnapshotDraftById( + id: string, + abortController?: AbortController +) { + try { + const draft = await fetch( + `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/draft/${id}`, + {signal: abortController?.signal} + ); + + if (!draft.ok) { + throw new Error(ERROR_PROPOSAL); + } + + const draftJSON: SnapshotDraftResponse = await draft.json(); + + return draftJSON; + } catch (error) { + throw error; + } +} + +// Gets Proposal from Snapshot Hub +async function getSnapshotProposalById( + id: string, + abortController?: AbortController, + type?: ProposalOrDraftSnapshotType +) { + try { + /** + * @note `searchUniqueDraftId` includes the draft id in the search for the proposal + * as a Tribute proposal's ID hash could be the Snapshot Draft's ID. + */ + const proposal = await fetch( + `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/proposal/${id}?searchUniqueDraftId=true&includeVotes=true`, + {signal: abortController?.signal} + ); + + if (!proposal.ok) { + /** + * If `type` is set then we know we can determine `handleGetDraft` + * will not be called after in `handleGetProposalOrDraft`. + */ + if (type === SnapshotType.proposal) { + throw new Error(ERROR_PROPOSAL); + } + + return; + } + + const proposalJSON: SnapshotProposalResponse = await proposal.json(); + + return proposalJSON; + } catch (error) { + throw error; + } +} + /** * useProposalOrDraft * @@ -83,6 +143,33 @@ export function useProposalOrDraft( const [refetchCount, updateRefetchCount] = useCounter(); + /** + * Their hooks + */ + + const {data: snapshotProposalEntryData, error: snapshotProposalEntryError} = + useQuery( + ['snapshotProposalEntry', id], + async () => await getSnapshotProposalById(id, abortController, type), + { + enabled: !!id && !!abortController?.signal, + } + ); + + const {data: snapshotDraftEntryData, error: snapshotDraftEntryError} = + useQuery( + ['snapshotDraftEntry', id], + async () => await getSnapshotDraftById(id, abortController), + { + enabled: + !!id && + !!abortController?.signal && + (type === SnapshotType.draft || + (!!snapshotProposalEntryData && + !Object.keys(snapshotProposalEntryData).length)), + } + ); + /** * Fetch on-chain voting adapter data for proposals. * Only returns data for proposals of which voting adapters have been assigned (i.e. sponsored). @@ -98,15 +185,15 @@ export function useProposalOrDraft( */ const handleGetDraftCached = useCallback(handleGetDraft, [ - abortController?.signal, - id, isMountedRef, + snapshotDraftEntryData, + snapshotDraftEntryError, ]); const handleGetProposalCached = useCallback(handleGetProposal, [ - abortController?.signal, - id, isMountedRef, + snapshotProposalEntryData, + snapshotProposalEntryError, type, ]); @@ -257,38 +344,33 @@ export function useProposalOrDraft( try { setProposalStatus(AsyncStatus.PENDING); - const response = await fetch( - `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/draft/${id}`, - {signal: abortController?.signal} - ); - - if (!response.ok) { - throw new Error(ERROR_PROPOSAL); + if (snapshotDraftEntryError) { + throw snapshotDraftEntryError; } - const responseJSON: SnapshotDraftResponse = await response.json(); - - if (!isMountedRef.current) return; + if (snapshotDraftEntryData) { + if (!isMountedRef.current) return; - // @note API does not provide a 404 - if (!responseJSON || !Object.keys(responseJSON).length) { - setProposalNotFound(true); + // @note API does not provide a 404 + if (!Object.keys(snapshotDraftEntryData).length) { + setProposalNotFound(true); - throw new Error(ERROR_PROPOSAL_NOT_FOUND); - } + throw new Error(ERROR_PROPOSAL_NOT_FOUND); + } - const idKey = Object.keys(responseJSON)[0]; - // Get the `SnapshotDraftResponseData` by the address key of the single result. - const draft: SnapshotDraft = { - idInDAO: idKey, - idInSnapshot: idKey, - ...responseJSON[idKey], - }; + const idKey = Object.keys(snapshotDraftEntryData)[0]; + // Get the `SnapshotDraftResponseData` by the address key of the single result. + const draft: SnapshotDraft = { + idInDAO: idKey, + idInSnapshot: idKey, + ...snapshotDraftEntryData[idKey], + }; - setProposalStatus(AsyncStatus.FULFILLED); - setSnapshotDraft(draft); + setProposalStatus(AsyncStatus.FULFILLED); + setSnapshotDraft(draft); - return draft; + return draft; + } } catch (error) { if (!isMountedRef.current) return; @@ -301,60 +383,43 @@ export function useProposalOrDraft( try { setProposalStatus(AsyncStatus.PENDING); - /** - * @note `searchUniqueDraftId` includes the draft id in the search for the proposal - * as a Tribute proposal's ID hash could be the Snapshot Draft's ID. - */ - const response = await fetch( - `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/proposal/${id}?searchUniqueDraftId=true&includeVotes=true`, - {signal: abortController?.signal} - ); - - if (!response.ok) { - /** - * If `type` is set then we know we can determine `handleGetDraft` - * will not be called after in `handleGetProposalOrDraft`. - */ - if (type === SnapshotType.proposal) { - throw new Error(ERROR_PROPOSAL); - } - - return; + if (snapshotProposalEntryError) { + throw snapshotProposalEntryError; } - const responseJSON: SnapshotProposalResponse = await response.json(); - - if (!isMountedRef.current) return; - - // @note API does not provide a 404 - if (!responseJSON || !Object.keys(responseJSON).length) { - /** - * If `type` is set then we know we can determine `handleGetDraft` - * will not be called after in `handleGetProposalOrDraft`. - */ - if (type === SnapshotType.proposal) { - setProposalNotFound(true); - throw new Error(ERROR_PROPOSAL_NOT_FOUND); + if (snapshotProposalEntryData) { + if (!isMountedRef.current) return; + + // @note API does not provide a 404 + if (!Object.keys(snapshotProposalEntryData).length) { + /** + * If `type` is set then we know we can determine `handleGetDraft` + * will not be called after in `handleGetProposalOrDraft`. + */ + if (type === SnapshotType.proposal) { + setProposalNotFound(true); + throw new Error(ERROR_PROPOSAL_NOT_FOUND); + } + + return; } - return; + const idKey = Object.keys(snapshotProposalEntryData)[0]; + // Determine ID submitted to DAO, i.e. if there's a Draft ID hash, we should use that. + const proposalId: string = + snapshotProposalEntryData[idKey]?.data.erc712DraftHash || idKey; + // Get the `SnapshotProposalResponseData` by the address key of the single result. + const proposal: SnapshotProposal = { + idInDAO: proposalId, + idInSnapshot: idKey, + ...snapshotProposalEntryData[idKey], + }; + + setProposalStatus(AsyncStatus.FULFILLED); + setSnapshotProposal(proposal); + + return proposal; } - - const idKey = Object.keys(responseJSON)[0]; - // Determine ID submitted to DAO, i.e. if there's a Draft ID hash, we should use that. - const proposalId: string = - responseJSON[idKey]?.data.erc712DraftHash || idKey; - // Get the `SnapshotProposalResponseData` by the address key of the single result. - const proposal: SnapshotProposal = { - idInDAO: proposalId, - idInSnapshot: idKey, - ...responseJSON[idKey], - }; - - setProposalStatus(AsyncStatus.FULFILLED); - setSnapshotProposal(proposal); - - return proposal; } catch (error) { if (!isMountedRef.current) return; From 45cd00c45f68d79e010a5dc00826a34ec9db953f Mon Sep 17 00:00:00 2001 From: jdville03 Date: Tue, 3 Aug 2021 14:55:58 -0700 Subject: [PATCH 08/25] WIP refetch for proposal actions updates --- .../hooks/useOffchainVotingResults.ts | 2 +- .../proposals/hooks/useProposalOrDraft.ts | 66 +++++++++++-------- .../proposals/voting/OffchainVotingAction.tsx | 2 +- 3 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/components/proposals/hooks/useOffchainVotingResults.ts b/src/components/proposals/hooks/useOffchainVotingResults.ts index 4049cbe5c..737e56190 100644 --- a/src/components/proposals/hooks/useOffchainVotingResults.ts +++ b/src/components/proposals/hooks/useOffchainVotingResults.ts @@ -106,7 +106,7 @@ export function useOffchainVotingResults( ]; }); - if (!voterEntries) return; + if (!voterEntries || !voterEntries.length) return; // Dedupe any duplicate addresses to be safe. const voterAddressesAndChoices = Object.entries( diff --git a/src/components/proposals/hooks/useProposalOrDraft.ts b/src/components/proposals/hooks/useProposalOrDraft.ts index b04ca9640..507114423 100644 --- a/src/components/proposals/hooks/useProposalOrDraft.ts +++ b/src/components/proposals/hooks/useProposalOrDraft.ts @@ -143,33 +143,6 @@ export function useProposalOrDraft( const [refetchCount, updateRefetchCount] = useCounter(); - /** - * Their hooks - */ - - const {data: snapshotProposalEntryData, error: snapshotProposalEntryError} = - useQuery( - ['snapshotProposalEntry', id], - async () => await getSnapshotProposalById(id, abortController, type), - { - enabled: !!id && !!abortController?.signal, - } - ); - - const {data: snapshotDraftEntryData, error: snapshotDraftEntryError} = - useQuery( - ['snapshotDraftEntry', id], - async () => await getSnapshotDraftById(id, abortController), - { - enabled: - !!id && - !!abortController?.signal && - (type === SnapshotType.draft || - (!!snapshotProposalEntryData && - !Object.keys(snapshotProposalEntryData).length)), - } - ); - /** * Fetch on-chain voting adapter data for proposals. * Only returns data for proposals of which voting adapters have been assigned (i.e. sponsored). @@ -180,6 +153,39 @@ export function useProposalOrDraft( proposalsVotingAdaptersStatus, } = useProposalsVotingAdapter(proposalVotingAdapterId); + /** + * Their hooks + */ + + const { + data: snapshotProposalEntryData, + error: snapshotProposalEntryError, + refetch: snapshotProposalEntryRefetch, + } = useQuery( + ['snapshotProposalEntry', id], + async () => await getSnapshotProposalById(id, abortController, type), + { + enabled: !!id && !!abortController?.signal, + } + ); + + const { + data: snapshotDraftEntryData, + error: snapshotDraftEntryError, + // refetch: snapshotDraftEntryRefetch, + } = useQuery( + ['snapshotDraftEntry', id], + async () => await getSnapshotDraftById(id, abortController), + { + enabled: + !!id && + !!abortController?.signal && + (type === SnapshotType.draft || + (!!snapshotProposalEntryData && + !Object.keys(snapshotProposalEntryData).length)), + } + ); + /** * Cached callbacks */ @@ -258,6 +264,12 @@ export function useProposalOrDraft( type, ]); + useEffect(() => { + if (refetchCount === 0) return; + + snapshotProposalEntryRefetch(); + }, [refetchCount, snapshotProposalEntryRefetch]); + useEffect(() => { if (refetchCount === 0) return; 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); } From f12fb4a4f5792f0188133a50a51a14c06d3a3762 Mon Sep 17 00:00:00 2001 From: jdville03 Date: Fri, 6 Aug 2021 10:22:17 -0700 Subject: [PATCH 09/25] fix proposal action transitions --- .../proposals/hooks/useProposalOrDraft.ts | 80 ++-- .../useProposalWithOffchainVoteStatus.ts | 2 +- .../hooks/useProposalsVotingAdapter.ts | 362 ++++++------------ 3 files changed, 160 insertions(+), 284 deletions(-) diff --git a/src/components/proposals/hooks/useProposalOrDraft.ts b/src/components/proposals/hooks/useProposalOrDraft.ts index 507114423..de302362e 100644 --- a/src/components/proposals/hooks/useProposalOrDraft.ts +++ b/src/components/proposals/hooks/useProposalOrDraft.ts @@ -4,7 +4,7 @@ import { SnapshotProposalResponse, SnapshotType, } from '@openlaw/snapshot-js-erc712'; -import {useQuery} from 'react-query'; +import {useQuery, useQueryClient} from 'react-query'; import { Proposal, @@ -157,34 +157,30 @@ export function useProposalOrDraft( * Their hooks */ - const { - data: snapshotProposalEntryData, - error: snapshotProposalEntryError, - refetch: snapshotProposalEntryRefetch, - } = useQuery( - ['snapshotProposalEntry', id], - async () => await getSnapshotProposalById(id, abortController, type), - { - enabled: !!id && !!abortController?.signal, - } - ); + const queryClient = useQueryClient(); - const { - data: snapshotDraftEntryData, - error: snapshotDraftEntryError, - // refetch: snapshotDraftEntryRefetch, - } = useQuery( - ['snapshotDraftEntry', id], - async () => await getSnapshotDraftById(id, abortController), - { - enabled: - !!id && - !!abortController?.signal && - (type === SnapshotType.draft || - (!!snapshotProposalEntryData && - !Object.keys(snapshotProposalEntryData).length)), - } - ); + const {data: snapshotProposalEntryData, error: snapshotProposalEntryError} = + useQuery( + ['snapshotProposalEntry', id], + async () => await getSnapshotProposalById(id, abortController, type), + { + enabled: !!id && !!abortController?.signal, + } + ); + + const {data: snapshotDraftEntryData, error: snapshotDraftEntryError} = + useQuery( + ['snapshotDraftEntry', id], + async () => await getSnapshotDraftById(id, abortController), + { + enabled: + !!id && + !!abortController?.signal && + (type === SnapshotType.draft || + (!!snapshotProposalEntryData && + !Object.keys(snapshotProposalEntryData).length)), + } + ); /** * Cached callbacks @@ -265,21 +261,25 @@ export function useProposalOrDraft( ]); useEffect(() => { - if (refetchCount === 0) return; + async function invalidateSnapshotProposalEntryQuery() { + if (refetchCount === 0) return; - snapshotProposalEntryRefetch(); - }, [refetchCount, snapshotProposalEntryRefetch]); + /** + *Refetch `snapshotProposalEntry` query when `refetchCount` is incremented + *(proposal is sponsored/submitted on chain, proposal is voted on) + */ + await queryClient.invalidateQueries('snapshotProposalEntry'); - useEffect(() => { - 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. + */ + setProposalVotingAdapterId([id]); + } - /** - * 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]); + invalidateSnapshotProposalEntryQuery(); + }, [id, queryClient, refetchCount]); // Set overall async status useEffect(() => { 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/useProposalsVotingAdapter.ts b/src/components/proposals/hooks/useProposalsVotingAdapter.ts index 29ef63fee..a089f8c3d 100644 --- a/src/components/proposals/hooks/useProposalsVotingAdapter.ts +++ b/src/components/proposals/hooks/useProposalsVotingAdapter.ts @@ -1,7 +1,6 @@ import {useCallback, useEffect, useState} from 'react'; import {useSelector} from 'react-redux'; -import {useQuery} from 'react-query'; -import Web3 from 'web3'; +import {useQueryClient} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {BURN_ADDRESS} from '../../../util/constants'; @@ -10,7 +9,7 @@ import {multicall, MulticallTuple} from '../../web3/helpers'; import {ProposalVotingAdapterData} from '../types'; import {StoreState} from '../../../store/types'; import {useWeb3Modal} from '../../web3/hooks'; -// import {VotingAdapterName} from '../../adapters-extensions/enums'; +import {VotingAdapterName} from '../../adapters-extensions/enums'; type ProposalVotingAdapterTuple = [ proposalId: string, @@ -47,109 +46,30 @@ export function useProposalsVotingAdapter( ); /** - * State + * Our hooks */ - const [proposalsVotingAdapters, setProposalsVotingAdapters] = useState< - ProposalVotingAdapterTuple[] - >(INITIAL_VOTING_ADAPTERS); - - const [proposalsVotingAdaptersError, setProposalsVotingAdaptersError] = - useState(); - - const [proposalsVotingAdaptersStatus, setProposalsVotingAdaptersStatus] = - useState(AsyncStatus.STANDBY); - - const [safeProposalIds, setSafeProposalIds] = useState(); - - const [votingAdapterCalls, setVotingAdapterCalls] = - useState(); - - const [votingAdapterABIError, setVotingAdapterABIError] = useState(); - - const [filteredProposalIds, setFilteredProposalIds] = useState(); - - const [ - filteredVotingAdapterAddressResults, - setFilteredVotingAdapterAddressResults, - ] = useState(); - - const [votingAdapterNameCalls, setVotingAdapterNameCalls] = - useState(); - - const [adapterNameABIError, setAdapterNameABIError] = useState(); + const {web3Instance} = useWeb3Modal(); /** - * Our hooks + * Their hooks */ - const {web3Instance} = useWeb3Modal(); + const queryClient = useQueryClient(); /** - * Their hooks + * State */ - const { - data: votingAdapterAddressResults, - error: votingAdapterAddressResultsError, - } = useQuery( - ['votingAdapterAddressResults', votingAdapterCalls], - async () => - await multicall({ - calls: votingAdapterCalls as MulticallTuple[], - web3Instance: web3Instance as Web3, - }), - {enabled: !!votingAdapterCalls && !!web3Instance} - ); + const [proposalsVotingAdapters, setProposalsVotingAdapters] = useState< + ProposalVotingAdapterTuple[] + >(INITIAL_VOTING_ADAPTERS); - const {data: adapterNameResults, error: adapterNameResultsError} = useQuery( - ['adapterNameResults', votingAdapterNameCalls], - async () => - await multicall({ - calls: votingAdapterNameCalls as MulticallTuple[], - web3Instance: web3Instance as Web3, - }), - {enabled: !!votingAdapterNameCalls && !!web3Instance} - ); + const [proposalsVotingAdaptersError, setProposalsVotingAdaptersError] = + useState(); - const {data: votingAdaptersToSetData, error: votingAdaptersToSetError} = - useQuery( - ['votingAdaptersToSet', filteredProposalIds], - async () => - await Promise.all( - (filteredProposalIds as string[]).map( - async (id, i): Promise => { - const votingAdapterABI = await getVotingAdapterABI( - adapterNameResults[i] - ); - const votingAdapterAddress = ( - filteredVotingAdapterAddressResults as string[] - )[i]; - - return [ - id, - { - votingAdapterName: adapterNameResults[i], - votingAdapterAddress, - getVotingAdapterABI: () => votingAdapterABI, - getWeb3VotingAdapterContract: () => - new (web3Instance as Web3).eth.Contract( - votingAdapterABI, - votingAdapterAddress - ) as any as T, - }, - ]; - } - ) - ), - { - enabled: - !!filteredProposalIds && - !!filteredVotingAdapterAddressResults && - !!adapterNameResults && - !!web3Instance, - } - ); + const [proposalsVotingAdaptersStatus, setProposalsVotingAdaptersStatus] = + useState(AsyncStatus.STANDBY); /** * Cached callbacks @@ -157,25 +77,7 @@ export function useProposalsVotingAdapter( const getProposalsVotingAdaptersOnchainCached = useCallback( getProposalsVotingAdaptersOnchain, - [ - adapterNameABIError, - adapterNameResults, - adapterNameResultsError, - filteredProposalIds, - filteredVotingAdapterAddressResults, - proposalIds.length, - registryABI, - registryAddress, - safeProposalIds, - votingAdapterABIError, - votingAdapterAddressResults, - votingAdapterAddressResultsError, - votingAdapterCalls, - votingAdapterNameCalls, - votingAdaptersToSetData, - votingAdaptersToSetError, - web3Instance, - ] + [proposalIds, queryClient, registryABI, registryAddress, web3Instance] ); /** @@ -186,48 +88,69 @@ export function useProposalsVotingAdapter( getProposalsVotingAdaptersOnchainCached(); }, [getProposalsVotingAdaptersOnchainCached]); - useEffect(() => { - if (!proposalIds.length || !web3Instance) { + /** + * Functions + */ + + async function getProposalsVotingAdaptersOnchain(): Promise { + if ( + !proposalIds.length || + !registryABI || + !registryAddress || + !web3Instance + ) { return; } // Only use hex (more specifically `bytes32`) id's - const safeProposalIdsToSet = proposalIds.filter( - web3Instance.utils.isHexStrict - ); + const safeProposalIds = proposalIds.filter(web3Instance.utils.isHexStrict); - setSafeProposalIds(safeProposalIdsToSet); - }, [proposalIds, web3Instance]); + if (!safeProposalIds.length) { + setProposalsVotingAdapters(INITIAL_VOTING_ADAPTERS); + setProposalsVotingAdaptersStatus(AsyncStatus.FULFILLED); - useEffect(() => { - if (!registryABI || !registryAddress || !safeProposalIds?.length) return; + return; + } - const votingAdapterABI = registryABI.find( - (ai) => ai.name === 'votingAdapter' - ); + try { + const votingAdapterABI = registryABI.find( + (ai) => ai.name === 'votingAdapter' + ); - if (!votingAdapterABI) { - setVotingAdapterABIError( - new Error( + if (!votingAdapterABI) { + throw new Error( 'No "votingAdapter" ABI function was found in the DAO registry ABI.' - ) - ); + ); + } - return; - } + // `DaoRegistry.votingAdapter` calls + const votingAdapterCalls: MulticallTuple[] = safeProposalIds.map((id) => [ + registryAddress, + votingAdapterABI, + [id], + ]); - // `DaoRegistry.votingAdapter` calls - const votingAdapterCallsToSet: MulticallTuple[] = safeProposalIds.map( - (id) => [registryAddress, votingAdapterABI, [id]] - ); + setProposalsVotingAdaptersStatus(AsyncStatus.PENDING); - setVotingAdapterCalls(votingAdapterCallsToSet); - }, [registryABI, registryAddress, safeProposalIds]); + const votingAdapterAddressResults: string[] = + await queryClient.fetchQuery( + ['votingAdapterAddressResults', votingAdapterCalls], + async () => await multicall({calls: votingAdapterCalls, web3Instance}) + ); - useEffect(() => { - async function setProposalsVotingAdaptersPrepData() { - if (!safeProposalIds || !votingAdapterAddressResults || !registryABI) - return; + const {default: lazyIVotingABI} = await import( + '../../../abis/IVoting.json' + ); + + const getAdapterNameABI = (lazyIVotingABI as typeof registryABI).find( + (ai) => ai.name === 'getAdapterName' + ); + + if (!getAdapterNameABI) { + throw new Error( + 'No "getAdapterName" ABI function was found in the IVoting ABI.' + ); + } /** * Filter out `safeProposalIds` which are not sponsored (i.e. voting adapter address === `BURN_ADDRESS`). @@ -236,119 +159,72 @@ export function useProposalsVotingAdapter( * This ensures these two arrays maintain the same length as they rely on indexes for the * proposals to match up to the array of `multicall` results. */ - const filteredProposalIdsToSet = safeProposalIds.filter( - (_id, i) => votingAdapterAddressResults[i] !== BURN_ADDRESS - ); - setFilteredProposalIds(filteredProposalIdsToSet); - const filteredVotingAdapterAddressResultsToSet = - votingAdapterAddressResults.filter((a: string) => a !== BURN_ADDRESS); - setFilteredVotingAdapterAddressResults( - filteredVotingAdapterAddressResultsToSet + const filteredProposalIds = safeProposalIds.filter( + (_id, i) => votingAdapterAddressResults[i] !== BURN_ADDRESS ); - const {default: lazyIVotingABI} = await import( - '../../../abis/IVoting.json' - ); - const adapterNameABI = (lazyIVotingABI as typeof registryABI).find( - (ai) => ai.name === 'getAdapterName' - ); + const filteredVotingAdapterAddressResults = + votingAdapterAddressResults.filter((a) => a !== BURN_ADDRESS); - if (!adapterNameABI) { - setAdapterNameABIError( - new Error( - 'No "getAdapterName" ABI function was found in the IVoting ABI.' - ) - ); + /** + * Exit early if there's no voting adapter addresses. + * It means no proposals were found to be sponsored + */ + if (!filteredVotingAdapterAddressResults.length) { + setProposalsVotingAdapters(INITIAL_VOTING_ADAPTERS); + setProposalsVotingAdaptersStatus(AsyncStatus.FULFILLED); return; } - const votingAdapterNameCallsToSet: MulticallTuple[] = - filteredVotingAdapterAddressResultsToSet.map( - (votingAdapterAddress: string) => [ - votingAdapterAddress, - adapterNameABI, - [], - ] + const votingAdapterNameCalls: MulticallTuple[] = + filteredVotingAdapterAddressResults.map((votingAdapterAddress) => [ + votingAdapterAddress, + getAdapterNameABI, + [], + ]); + + const adapterNameResults: VotingAdapterName[] = + await queryClient.fetchQuery( + ['adapterNameResults', votingAdapterNameCalls], + async () => + await multicall({calls: votingAdapterNameCalls, web3Instance}) ); - setVotingAdapterNameCalls(votingAdapterNameCallsToSet); - } - - setProposalsVotingAdaptersPrepData(); - }, [registryABI, safeProposalIds, votingAdapterAddressResults]); - /** - * Functions - */ - - async function getProposalsVotingAdaptersOnchain(): Promise { - if ( - !proposalIds.length || - !safeProposalIds || - !registryABI || - !registryAddress || - !web3Instance - ) { - return; - } + const votingAdaptersToSet: ProposalVotingAdapterTuple[] = + await queryClient.fetchQuery( + ['votingAdaptersToSet', filteredProposalIds], + async () => + await Promise.all( + filteredProposalIds.map( + async (id, i): Promise => { + const votingAdapterABI = await getVotingAdapterABI( + adapterNameResults[i] + ); + const votingAdapterAddress = + filteredVotingAdapterAddressResults[i]; + + return [ + id, + { + votingAdapterName: adapterNameResults[i], + votingAdapterAddress, + getVotingAdapterABI: () => votingAdapterABI, + getWeb3VotingAdapterContract: () => + new web3Instance.eth.Contract( + votingAdapterABI, + votingAdapterAddress + ) as any as T, + }, + ]; + } + ) + ) + ); - if (!safeProposalIds.length) { - setProposalsVotingAdapters(INITIAL_VOTING_ADAPTERS); + setProposalsVotingAdapters(votingAdaptersToSet); setProposalsVotingAdaptersStatus(AsyncStatus.FULFILLED); - - return; - } - - try { - if (votingAdapterABIError) { - throw votingAdapterABIError; - } - - if (!votingAdapterCalls) return; - - setProposalsVotingAdaptersStatus(AsyncStatus.PENDING); - - if (votingAdapterAddressResultsError) { - throw votingAdapterAddressResultsError; - } - - if (votingAdapterAddressResults) { - if (adapterNameABIError) { - throw adapterNameABIError; - } - - if (!filteredProposalIds || !filteredVotingAdapterAddressResults) - return; - - /** - * Exit early if there's no voting adapter addresses. - * It means no proposals were found to be sponsored - */ - if (!filteredVotingAdapterAddressResults.length) { - setProposalsVotingAdapters(INITIAL_VOTING_ADAPTERS); - setProposalsVotingAdaptersStatus(AsyncStatus.FULFILLED); - - return; - } - - if (!votingAdapterNameCalls) return; - - if (adapterNameResultsError) { - throw adapterNameResultsError; - } - - if (adapterNameResults) { - if (votingAdaptersToSetError) { - throw votingAdaptersToSetError; - } - - if (votingAdaptersToSetData) { - setProposalsVotingAdapters(votingAdaptersToSetData); - setProposalsVotingAdaptersStatus(AsyncStatus.FULFILLED); - } - } - } } catch (error) { setProposalsVotingAdaptersStatus(AsyncStatus.REJECTED); setProposalsVotingAdapters([]); From 995b03d813b2cf8223e8df0edb2e391b2cfd5e03 Mon Sep 17 00:00:00 2001 From: jdville03 Date: Fri, 6 Aug 2021 23:48:05 -0700 Subject: [PATCH 10/25] format comment --- src/components/proposals/hooks/useProposalOrDraft.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/proposals/hooks/useProposalOrDraft.ts b/src/components/proposals/hooks/useProposalOrDraft.ts index de302362e..4f264d92e 100644 --- a/src/components/proposals/hooks/useProposalOrDraft.ts +++ b/src/components/proposals/hooks/useProposalOrDraft.ts @@ -265,8 +265,9 @@ export function useProposalOrDraft( if (refetchCount === 0) return; /** - *Refetch `snapshotProposalEntry` query when `refetchCount` is incremented - *(proposal is sponsored/submitted on chain, proposal is voted on) + * Refetch `snapshotProposalEntry` query when `refetchCount` is + * incremented (proposal is sponsored/submitted on chain, proposal is + * voted on) */ await queryClient.invalidateQueries('snapshotProposalEntry'); From 6219f89c9a2b713ec369a1f53fada9acfa523604 Mon Sep 17 00:00:00 2001 From: jdville03 Date: Tue, 10 Aug 2021 00:41:31 -0700 Subject: [PATCH 11/25] fix proposal action transitions --- .../proposals/hooks/useProposalOrDraft.ts | 246 +++++++----------- .../hooks/useProposalsVotingAdapter.ts | 84 +++--- 2 files changed, 129 insertions(+), 201 deletions(-) diff --git a/src/components/proposals/hooks/useProposalOrDraft.ts b/src/components/proposals/hooks/useProposalOrDraft.ts index 4f264d92e..0264fd64c 100644 --- a/src/components/proposals/hooks/useProposalOrDraft.ts +++ b/src/components/proposals/hooks/useProposalOrDraft.ts @@ -4,7 +4,7 @@ import { SnapshotProposalResponse, SnapshotType, } from '@openlaw/snapshot-js-erc712'; -import {useQuery, useQueryClient} from 'react-query'; +import {useQueryClient} from 'react-query'; import { Proposal, @@ -30,65 +30,6 @@ const ERROR_PROPOSAL: string = 'Something went wrong while getting the proposal.'; const ERROR_PROPOSAL_NOT_FOUND: string = 'Proposal was not found.'; -// Gets Draft (unsponsored Proposal) from Snapshot Hub -async function getSnapshotDraftById( - id: string, - abortController?: AbortController -) { - try { - const draft = await fetch( - `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/draft/${id}`, - {signal: abortController?.signal} - ); - - if (!draft.ok) { - throw new Error(ERROR_PROPOSAL); - } - - const draftJSON: SnapshotDraftResponse = await draft.json(); - - return draftJSON; - } catch (error) { - throw error; - } -} - -// Gets Proposal from Snapshot Hub -async function getSnapshotProposalById( - id: string, - abortController?: AbortController, - type?: ProposalOrDraftSnapshotType -) { - try { - /** - * @note `searchUniqueDraftId` includes the draft id in the search for the proposal - * as a Tribute proposal's ID hash could be the Snapshot Draft's ID. - */ - const proposal = await fetch( - `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/proposal/${id}?searchUniqueDraftId=true&includeVotes=true`, - {signal: abortController?.signal} - ); - - if (!proposal.ok) { - /** - * If `type` is set then we know we can determine `handleGetDraft` - * will not be called after in `handleGetProposalOrDraft`. - */ - if (type === SnapshotType.proposal) { - throw new Error(ERROR_PROPOSAL); - } - - return; - } - - const proposalJSON: SnapshotProposalResponse = await proposal.json(); - - return proposalJSON; - } catch (error) { - throw error; - } -} - /** * useProposalOrDraft * @@ -159,43 +100,20 @@ export function useProposalOrDraft( const queryClient = useQueryClient(); - const {data: snapshotProposalEntryData, error: snapshotProposalEntryError} = - useQuery( - ['snapshotProposalEntry', id], - async () => await getSnapshotProposalById(id, abortController, type), - { - enabled: !!id && !!abortController?.signal, - } - ); - - const {data: snapshotDraftEntryData, error: snapshotDraftEntryError} = - useQuery( - ['snapshotDraftEntry', id], - async () => await getSnapshotDraftById(id, abortController), - { - enabled: - !!id && - !!abortController?.signal && - (type === SnapshotType.draft || - (!!snapshotProposalEntryData && - !Object.keys(snapshotProposalEntryData).length)), - } - ); - /** * Cached callbacks */ const handleGetDraftCached = useCallback(handleGetDraft, [ + abortController?.signal, + id, isMountedRef, - snapshotDraftEntryData, - snapshotDraftEntryError, ]); const handleGetProposalCached = useCallback(handleGetProposal, [ + abortController?.signal, + id, isMountedRef, - snapshotProposalEntryData, - snapshotProposalEntryError, type, ]); @@ -261,26 +179,30 @@ export function useProposalOrDraft( ]); useEffect(() => { - async function invalidateSnapshotProposalEntryQuery() { + 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. + */ + setProposalVotingAdapterId([id]); + }, [id, refetchCount]); + + useEffect(() => { + async function resetQueries() { if (refetchCount === 0) return; /** - * Refetch `snapshotProposalEntry` query when `refetchCount` is + * Reset queries when `refetchCount` is * incremented (proposal is sponsored/submitted on chain, proposal is * voted on) */ - await queryClient.invalidateQueries('snapshotProposalEntry'); - - /** - * 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]); + await queryClient.resetQueries(); } - invalidateSnapshotProposalEntryQuery(); - }, [id, queryClient, refetchCount]); + resetQueries(); + }, [queryClient, refetchCount]); // Set overall async status useEffect(() => { @@ -357,33 +279,38 @@ export function useProposalOrDraft( try { setProposalStatus(AsyncStatus.PENDING); - if (snapshotDraftEntryError) { - throw snapshotDraftEntryError; + const response = await fetch( + `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/draft/${id}`, + {signal: abortController?.signal} + ); + + if (!response.ok) { + throw new Error(ERROR_PROPOSAL); } - if (snapshotDraftEntryData) { - if (!isMountedRef.current) return; + const responseJSON: SnapshotDraftResponse = await response.json(); - // @note API does not provide a 404 - if (!Object.keys(snapshotDraftEntryData).length) { - setProposalNotFound(true); + if (!isMountedRef.current) return; - throw new Error(ERROR_PROPOSAL_NOT_FOUND); - } + // @note API does not provide a 404 + if (!responseJSON || !Object.keys(responseJSON).length) { + setProposalNotFound(true); - const idKey = Object.keys(snapshotDraftEntryData)[0]; - // Get the `SnapshotDraftResponseData` by the address key of the single result. - const draft: SnapshotDraft = { - idInDAO: idKey, - idInSnapshot: idKey, - ...snapshotDraftEntryData[idKey], - }; + throw new Error(ERROR_PROPOSAL_NOT_FOUND); + } - setProposalStatus(AsyncStatus.FULFILLED); - setSnapshotDraft(draft); + const idKey = Object.keys(responseJSON)[0]; + // Get the `SnapshotDraftResponseData` by the address key of the single result. + const draft: SnapshotDraft = { + idInDAO: idKey, + idInSnapshot: idKey, + ...responseJSON[idKey], + }; - return draft; - } + setProposalStatus(AsyncStatus.FULFILLED); + setSnapshotDraft(draft); + + return draft; } catch (error) { if (!isMountedRef.current) return; @@ -396,43 +323,60 @@ export function useProposalOrDraft( try { setProposalStatus(AsyncStatus.PENDING); - if (snapshotProposalEntryError) { - throw snapshotProposalEntryError; + /** + * @note `searchUniqueDraftId` includes the draft id in the search for the proposal + * as a Tribute proposal's ID hash could be the Snapshot Draft's ID. + */ + const response = await fetch( + `${SNAPSHOT_HUB_API_URL}/api/${SPACE}/proposal/${id}?searchUniqueDraftId=true&includeVotes=true`, + {signal: abortController?.signal} + ); + + if (!response.ok) { + /** + * If `type` is set then we know we can determine `handleGetDraft` + * will not be called after in `handleGetProposalOrDraft`. + */ + if (type === SnapshotType.proposal) { + throw new Error(ERROR_PROPOSAL); + } + + return; } - if (snapshotProposalEntryData) { - if (!isMountedRef.current) return; - - // @note API does not provide a 404 - if (!Object.keys(snapshotProposalEntryData).length) { - /** - * If `type` is set then we know we can determine `handleGetDraft` - * will not be called after in `handleGetProposalOrDraft`. - */ - if (type === SnapshotType.proposal) { - setProposalNotFound(true); - throw new Error(ERROR_PROPOSAL_NOT_FOUND); - } - - return; + const responseJSON: SnapshotProposalResponse = await response.json(); + + if (!isMountedRef.current) return; + + // @note API does not provide a 404 + if (!responseJSON || !Object.keys(responseJSON).length) { + /** + * If `type` is set then we know we can determine `handleGetDraft` + * will not be called after in `handleGetProposalOrDraft`. + */ + if (type === SnapshotType.proposal) { + setProposalNotFound(true); + throw new Error(ERROR_PROPOSAL_NOT_FOUND); } - const idKey = Object.keys(snapshotProposalEntryData)[0]; - // Determine ID submitted to DAO, i.e. if there's a Draft ID hash, we should use that. - const proposalId: string = - snapshotProposalEntryData[idKey]?.data.erc712DraftHash || idKey; - // Get the `SnapshotProposalResponseData` by the address key of the single result. - const proposal: SnapshotProposal = { - idInDAO: proposalId, - idInSnapshot: idKey, - ...snapshotProposalEntryData[idKey], - }; - - setProposalStatus(AsyncStatus.FULFILLED); - setSnapshotProposal(proposal); - - return proposal; + return; } + + const idKey = Object.keys(responseJSON)[0]; + // Determine ID submitted to DAO, i.e. if there's a Draft ID hash, we should use that. + const proposalId: string = + responseJSON[idKey]?.data.erc712DraftHash || idKey; + // Get the `SnapshotProposalResponseData` by the address key of the single result. + const proposal: SnapshotProposal = { + idInDAO: proposalId, + idInSnapshot: idKey, + ...responseJSON[idKey], + }; + + setProposalStatus(AsyncStatus.FULFILLED); + setSnapshotProposal(proposal); + + return proposal; } catch (error) { if (!isMountedRef.current) return; diff --git a/src/components/proposals/hooks/useProposalsVotingAdapter.ts b/src/components/proposals/hooks/useProposalsVotingAdapter.ts index a089f8c3d..58e9bf7ca 100644 --- a/src/components/proposals/hooks/useProposalsVotingAdapter.ts +++ b/src/components/proposals/hooks/useProposalsVotingAdapter.ts @@ -1,6 +1,5 @@ import {useCallback, useEffect, useState} from 'react'; import {useSelector} from 'react-redux'; -import {useQueryClient} from 'react-query'; import {AsyncStatus} from '../../../util/types'; import {BURN_ADDRESS} from '../../../util/constants'; @@ -51,12 +50,6 @@ export function useProposalsVotingAdapter( const {web3Instance} = useWeb3Modal(); - /** - * Their hooks - */ - - const queryClient = useQueryClient(); - /** * State */ @@ -77,7 +70,7 @@ export function useProposalsVotingAdapter( const getProposalsVotingAdaptersOnchainCached = useCallback( getProposalsVotingAdaptersOnchain, - [proposalIds, queryClient, registryABI, registryAddress, web3Instance] + [proposalIds, registryABI, registryAddress, web3Instance] ); /** @@ -132,11 +125,10 @@ export function useProposalsVotingAdapter( setProposalsVotingAdaptersStatus(AsyncStatus.PENDING); - const votingAdapterAddressResults: string[] = - await queryClient.fetchQuery( - ['votingAdapterAddressResults', votingAdapterCalls], - async () => await multicall({calls: votingAdapterCalls, web3Instance}) - ); + const votingAdapterAddressResults: string[] = await multicall({ + calls: votingAdapterCalls, + web3Instance, + }); const {default: lazyIVotingABI} = await import( '../../../abis/IVoting.json' @@ -185,43 +177,35 @@ export function useProposalsVotingAdapter( [], ]); - const adapterNameResults: VotingAdapterName[] = - await queryClient.fetchQuery( - ['adapterNameResults', votingAdapterNameCalls], - async () => - await multicall({calls: votingAdapterNameCalls, web3Instance}) - ); - - const votingAdaptersToSet: ProposalVotingAdapterTuple[] = - await queryClient.fetchQuery( - ['votingAdaptersToSet', filteredProposalIds], - async () => - await Promise.all( - filteredProposalIds.map( - async (id, i): Promise => { - const votingAdapterABI = await getVotingAdapterABI( - adapterNameResults[i] - ); - const votingAdapterAddress = - filteredVotingAdapterAddressResults[i]; - - return [ - id, - { - votingAdapterName: adapterNameResults[i], - votingAdapterAddress, - getVotingAdapterABI: () => votingAdapterABI, - getWeb3VotingAdapterContract: () => - new web3Instance.eth.Contract( - votingAdapterABI, - votingAdapterAddress - ) as any as T, - }, - ]; - } - ) - ) - ); + const adapterNameResults: VotingAdapterName[] = await multicall({ + calls: votingAdapterNameCalls, + web3Instance, + }); + + const votingAdaptersToSet = await Promise.all( + filteredProposalIds.map( + async (id, i): Promise => { + const votingAdapterABI = await getVotingAdapterABI( + adapterNameResults[i] + ); + const votingAdapterAddress = filteredVotingAdapterAddressResults[i]; + + return [ + id, + { + votingAdapterName: adapterNameResults[i], + votingAdapterAddress, + getVotingAdapterABI: () => votingAdapterABI, + getWeb3VotingAdapterContract: () => + new web3Instance.eth.Contract( + votingAdapterABI, + votingAdapterAddress + ) as any as T, + }, + ]; + } + ) + ); setProposalsVotingAdapters(votingAdaptersToSet); setProposalsVotingAdaptersStatus(AsyncStatus.FULFILLED); From c63291964f727364d5f9ee76c20015865659925a Mon Sep 17 00:00:00 2001 From: jdville03 Date: Tue, 10 Aug 2021 00:42:07 -0700 Subject: [PATCH 12/25] format comments --- src/components/proposals/hooks/useProposalOrDraft.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/proposals/hooks/useProposalOrDraft.ts b/src/components/proposals/hooks/useProposalOrDraft.ts index 0264fd64c..bbd32acc3 100644 --- a/src/components/proposals/hooks/useProposalOrDraft.ts +++ b/src/components/proposals/hooks/useProposalOrDraft.ts @@ -182,9 +182,9 @@ 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]); @@ -194,9 +194,8 @@ export function useProposalOrDraft( if (refetchCount === 0) return; /** - * Reset queries when `refetchCount` is - * incremented (proposal is sponsored/submitted on chain, proposal is - * voted on) + * Reset queries when `refetchCount` is incremented (proposal is + * sponsored/submitted on chain, proposal is voted on) */ await queryClient.resetQueries(); } From 4d207a4897e4119b5ae86179e789ba2ac5cfe312 Mon Sep 17 00:00:00 2001 From: jdville03 Date: Tue, 10 Aug 2021 00:45:26 -0700 Subject: [PATCH 13/25] WIP update tests --- src/index.tsx | 2 +- src/setupTests.js | 7 ++++++- src/test/Wrapper.tsx | 13 +++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index c071b0098..e698c8438 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -84,7 +84,7 @@ export const getApolloClient = ( }); // Create `QueryClient` -export const queryClient = new QueryClient({ +const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, diff --git a/src/setupTests.js b/src/setupTests.js index 059fc42a2..50e575ca0 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -1,3 +1,5 @@ +import {queryClient} from './test/Wrapper'; + // Adds jest-dom's custom assertions require('@testing-library/jest-dom/extend-expect'); const path = require('path'); @@ -69,5 +71,8 @@ beforeAll(() => { // 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()); +afterEach(() => { + server.resetHandlers(); + queryClient.clear(); +}); afterAll(() => server.close()); diff --git a/src/test/Wrapper.tsx b/src/test/Wrapper.tsx index 774f24034..f59136ade 100644 --- a/src/test/Wrapper.tsx +++ b/src/test/Wrapper.tsx @@ -6,7 +6,7 @@ import {Provider} from 'react-redux'; import {ApolloProvider} from '@apollo/react-hooks'; import React, {useEffect, useMemo, useState} from 'react'; import Web3 from 'web3'; -import {QueryClientProvider} from 'react-query'; +import {QueryClient, QueryClientProvider} from 'react-query'; import { Web3ModalContext, @@ -15,7 +15,7 @@ import { import {AsyncStatus} from '../util/types'; import {CHAINS as mockChains, WALLETCONNECT_PROVIDER_OPTIONS} from '../config'; import {DEFAULT_ETH_ADDRESS, FakeHttpProvider, getNewStore} from './helpers'; -import {getApolloClient, queryClient} from '../index'; +import {getApolloClient} from '../index'; import {VotingAdapterName} from '../components/adapters-extensions/enums'; import App from '../App'; import Init from '../Init'; @@ -54,6 +54,15 @@ type WrapperProps = { locationEntries?: LocationDescriptor[]; }; +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // turns retries off + retry: false, + }, + }, +}); + /** * Similar to our app code, `` provides a wrapper for tests which need the following to run: * From e5ee5e4eded6868e2955119b25fbf8387e44bf1d Mon Sep 17 00:00:00 2001 From: jdville03 Date: Tue, 10 Aug 2021 11:20:46 -0700 Subject: [PATCH 14/25] clean up hook after caching update --- .../proposals/hooks/useDaoProposals.ts | 29 ++++--------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/src/components/proposals/hooks/useDaoProposals.ts b/src/components/proposals/hooks/useDaoProposals.ts index 7242235d5..c335d7c86 100644 --- a/src/components/proposals/hooks/useDaoProposals.ts +++ b/src/components/proposals/hooks/useDaoProposals.ts @@ -1,7 +1,6 @@ import {useEffect, useState, useCallback} from 'react'; import {useSelector} from 'react-redux'; import Web3 from 'web3'; -import {AbiItem} from 'web3-utils/types'; import {useQuery} from 'react-query'; import {AsyncStatus} from '../../../util/types'; @@ -85,7 +84,6 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { */ const handleGetDaoProposalsCached = useCallback(handleGetDaoProposals, [ - daoProposalsCalls, daoProposalsData, daoProposalsQueryError, safeProposalIds, @@ -98,6 +96,7 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { useEffect(() => { if ( !proposalIds.length || + !safeProposalIds || !proposalsAbi || !registryAddress || !web3Instance @@ -105,17 +104,13 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { return; } - handleGetDaoProposalsCached({ - proposalIds, - proposalsAbi, - registryAddress, - web3Instance, - }); + handleGetDaoProposalsCached(); }, [ handleGetDaoProposalsCached, - proposalIds, + proposalIds.length, proposalsAbi, registryAddress, + safeProposalIds, web3Instance, ]); @@ -146,19 +141,9 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { * Functions */ - async function handleGetDaoProposals({ - proposalIds, - proposalsAbi, - registryAddress, - web3Instance, - }: { - proposalIds: string[]; - proposalsAbi: AbiItem; - registryAddress: string; - web3Instance: Web3; - }) { + async function handleGetDaoProposals() { try { - if (!proposalIds.length || !safeProposalIds) return; + if (!safeProposalIds) return; if (!safeProposalIds.length) { setDaoProposalsStatus(AsyncStatus.FULFILLED); @@ -171,8 +156,6 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { // Reset error setDaoProposalsError(undefined); - if (!daoProposalsCalls) return; - if (daoProposalsQueryError) { throw daoProposalsQueryError; } From f48349354d84ecb4844c11557066c77ab8dfc629 Mon Sep 17 00:00:00 2001 From: jdville03 Date: Tue, 10 Aug 2021 15:52:44 -0700 Subject: [PATCH 15/25] update useProposals unit tests --- .../proposals/hooks/useProposals.unit.test.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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 ); From 02698a95991478271398b387e8a5e9ea31ba9eba Mon Sep 17 00:00:00 2001 From: jdville03 Date: Tue, 10 Aug 2021 16:08:26 -0700 Subject: [PATCH 16/25] update useGovernanceProposals unit tests --- .../hooks/useGovernanceProposals.unit.test.ts | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) 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 From 4d54a70831025f92b850cdb2b5cbffd11344a7f6 Mon Sep 17 00:00:00 2001 From: jdville03 Date: Tue, 10 Aug 2021 23:34:17 -0700 Subject: [PATCH 17/25] update useDaoProposals unit test --- .../proposals/hooks/useDaoProposals.unit.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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, From 377b58a313e2647229ab416e4689cc12e27055a2 Mon Sep 17 00:00:00 2001 From: jdville03 Date: Wed, 11 Aug 2021 08:31:49 -0700 Subject: [PATCH 18/25] update comment --- src/test/Wrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/Wrapper.tsx b/src/test/Wrapper.tsx index f59136ade..3b7cb274e 100644 --- a/src/test/Wrapper.tsx +++ b/src/test/Wrapper.tsx @@ -57,7 +57,7 @@ type WrapperProps = { export const queryClient = new QueryClient({ defaultOptions: { queries: { - // turns retries off + // turns retries off to test error results retry: false, }, }, From 506f2cc50805a5358c046254f5b28661998c4ed7 Mon Sep 17 00:00:00 2001 From: jdville03 Date: Thu, 12 Aug 2021 07:53:54 -0700 Subject: [PATCH 19/25] WIP fix last of failing tests --- .../proposals/hooks/useProposalsVotes.ts | 33 ++++++++++--------- .../hooks/useProposalsVotes.unit.test.ts | 26 ++++++++++++--- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/components/proposals/hooks/useProposalsVotes.ts b/src/components/proposals/hooks/useProposalsVotes.ts index d24f78dee..db2dd2fab 100644 --- a/src/components/proposals/hooks/useProposalsVotes.ts +++ b/src/components/proposals/hooks/useProposalsVotes.ts @@ -39,9 +39,6 @@ export function useProposalsVotes( const registryAddress = useSelector( (s: StoreState) => s.contracts.DaoRegistryContract?.contractAddress ); - const registryABI = useSelector( - (s: StoreState) => s.contracts.DaoRegistryContract?.abi - ); /** * State @@ -93,7 +90,6 @@ export function useProposalsVotes( enabled: !!proposalVotingAdapters.length && !!safeProposalVotingAdapters && - !!registryABI && !!registryAddress && !!web3Instance, } @@ -114,15 +110,11 @@ export function useProposalsVotes( */ const getProposalsVotesOnchainCached = useCallback(getProposalsVotesOnchain, [ - proposalVotingAdapters.length, - registryABI, - registryAddress, safeProposalVotingAdapters, votesDataCallsData, votesDataCallsError, votesDataResults, votesDataResultsError, - web3Instance, ]); /** @@ -130,8 +122,23 @@ export function useProposalsVotes( */ useEffect(() => { + if ( + !proposalVotingAdapters.length || + !safeProposalVotingAdapters || + !registryAddress || + !web3Instance + ) { + return; + } + getProposalsVotesOnchainCached(); - }, [getProposalsVotesOnchainCached]); + }, [ + getProposalsVotesOnchainCached, + proposalVotingAdapters.length, + registryAddress, + safeProposalVotingAdapters, + web3Instance, + ]); useEffect(() => { if (!proposalVotingAdapters.length || !web3Instance) { @@ -151,13 +158,7 @@ export function useProposalsVotes( */ async function getProposalsVotesOnchain() { - if ( - !proposalVotingAdapters.length || - !safeProposalVotingAdapters || - !registryABI || - !registryAddress || - !web3Instance - ) { + if (!safeProposalVotingAdapters) { return; } diff --git a/src/components/proposals/hooks/useProposalsVotes.unit.test.ts b/src/components/proposals/hooks/useProposalsVotes.unit.test.ts index ad6d0bde3..4bc289787 100644 --- a/src/components/proposals/hooks/useProposalsVotes.unit.test.ts +++ b/src/components/proposals/hooks/useProposalsVotes.unit.test.ts @@ -45,7 +45,7 @@ describe('useProposalsVotes unit tests', () => { ]; await act(async () => { - const {result, waitForNextUpdate} = await renderHook( + const {result, waitForValueToChange} = await renderHook( () => useProposalsVotes(proposalsVotingAdapterTuples), { wrapper: Wrapper, @@ -128,18 +128,21 @@ describe('useProposalsVotes unit tests', () => { } ); + // 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); + // 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([ @@ -289,12 +292,21 @@ describe('useProposalsVotes unit tests', () => { } ); + // 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 @@ -371,20 +383,23 @@ describe('useProposalsVotes unit tests', () => { } ); + // 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 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', @@ -487,12 +502,14 @@ describe('useProposalsVotes unit tests', () => { } ); + // 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([]); @@ -558,6 +575,7 @@ describe('useProposalsVotes unit tests', () => { } ); + // Assert initial state expect(result.current.proposalsVotesStatus).toBe(AsyncStatus.STANDBY); expect(result.current.proposalsVotesError).toBe(undefined); expect(result.current.proposalsVotes).toMatchObject([]); From 628fafcef7febda053e6b575b6bc0f45b42f36a8 Mon Sep 17 00:00:00 2001 From: jdville03 Date: Thu, 12 Aug 2021 10:22:55 -0700 Subject: [PATCH 20/25] remove react query dev tools --- src/index.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index e698c8438..5c610de6c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -11,8 +11,6 @@ import { HttpLink, } from '@apollo/client'; import {QueryClient, QueryClientProvider} from 'react-query'; -// @todo Remove react-query dev tools before merging -import {ReactQueryDevtools} from 'react-query/devtools'; import { ENVIRONMENT, @@ -120,8 +118,6 @@ if (root !== null) { ) : null } /> - {/* @todo Remove react-query dev tools before merging */} - From 944ee782042d563484932b387eb46420d21532dd Mon Sep 17 00:00:00 2001 From: jdville03 Date: Thu, 12 Aug 2021 10:34:16 -0700 Subject: [PATCH 21/25] update queryClient import in setupTests --- src/setupTests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setupTests.js b/src/setupTests.js index 50e575ca0..839378ec1 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -1,4 +1,4 @@ -import {queryClient} from './test/Wrapper'; +const {queryClient} = require('./test/Wrapper'); // Adds jest-dom's custom assertions require('@testing-library/jest-dom/extend-expect'); From b9c8273bb922eddbc8ef5319672736f5e8bdf62c Mon Sep 17 00:00:00 2001 From: jdville03 Date: Thu, 12 Aug 2021 17:23:39 -0700 Subject: [PATCH 22/25] update testing clear cache --- src/setupTests.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/setupTests.js b/src/setupTests.js index 839378ec1..bf47558e3 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -1,3 +1,4 @@ +const {act} = require('@testing-library/react-hooks'); const {queryClient} = require('./test/Wrapper'); // Adds jest-dom's custom assertions @@ -73,6 +74,7 @@ beforeAll(() => { // (which is important for test isolation): afterEach(() => { server.resetHandlers(); - queryClient.clear(); + // clear global cache + act(() => queryClient.clear()); }); afterAll(() => server.close()); From 59bfd4c81557664d822572f35fbd434df5975b4b Mon Sep 17 00:00:00 2001 From: Jeremiah Naylor-Trein Date: Fri, 13 Aug 2021 13:36:22 +0100 Subject: [PATCH 23/25] fixes outdated env vars being set --- src/setupTests.js | 27 +++++++++++++++++++------ src/test/Wrapper.tsx | 47 +++++++++++++++++++++++++++++--------------- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/setupTests.js b/src/setupTests.js index bf47558e3..cb92fb297 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -1,13 +1,14 @@ -const {act} = require('@testing-library/react-hooks'); -const {queryClient} = require('./test/Wrapper'); - // 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')}`, @@ -42,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. * @@ -69,12 +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(); - // clear global cache - act(() => queryClient.clear()); }); -afterAll(() => server.close()); + +afterAll(() => { + server.close(); +}); diff --git a/src/test/Wrapper.tsx b/src/test/Wrapper.tsx index 3b7cb274e..5585f7d1e 100644 --- a/src/test/Wrapper.tsx +++ b/src/test/Wrapper.tsx @@ -1,12 +1,12 @@ -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 {QueryClient, QueryClientProvider} from 'react-query'; import { Web3ModalContext, @@ -54,15 +54,6 @@ type WrapperProps = { locationEntries?: LocationDescriptor[]; }; -export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - // turns retries off to test error results - retry: false, - }, - }, -}); - /** * Similar to our app code, `` provides a wrapper for tests which need the following to run: * @@ -87,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 */ @@ -168,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 `` From 73189fdf5076070d1700145a7386d127eda03909 Mon Sep 17 00:00:00 2001 From: Jeremiah Naylor-Trein Date: Fri, 13 Aug 2021 15:29:33 +0100 Subject: [PATCH 24/25] fixes useProposalsVotes tests and makes slight improvements to code --- .../proposals/hooks/useProposalsVotes.ts | 40 +- .../hooks/useProposalsVotes.unit.test.ts | 359 ++++++------------ 2 files changed, 139 insertions(+), 260 deletions(-) diff --git a/src/components/proposals/hooks/useProposalsVotes.ts b/src/components/proposals/hooks/useProposalsVotes.ts index db2dd2fab..b11f7e35c 100644 --- a/src/components/proposals/hooks/useProposalsVotes.ts +++ b/src/components/proposals/hooks/useProposalsVotes.ts @@ -1,8 +1,7 @@ -import {useCallback, useEffect, useState} from 'react'; -import {useSelector} from 'react-redux'; import {AbiItem} from 'web3-utils/types'; +import {useCallback, useEffect, useState} from 'react'; import {useQuery} from 'react-query'; -import Web3 from 'web3'; +import {useSelector} from 'react-redux'; import {AsyncStatus} from '../../../util/types'; import {multicall, MulticallTuple} from '../../web3/helpers'; @@ -69,8 +68,10 @@ export function useProposalsVotes( const {data: votesDataCallsData, error: votesDataCallsError} = useQuery( ['votesDataCalls', safeProposalVotingAdapters], - async () => - await Promise.all( + async (): Promise => { + if (!registryAddress) return; + + return await Promise.all( (safeProposalVotingAdapters as ProposalVotingAdapterTuple[]).map( async ([ proposalId, @@ -82,10 +83,11 @@ export function useProposalsVotes( * 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 as string, proposalId], + [registryAddress, proposalId], ] ) - ), + ); + }, { enabled: !!proposalVotingAdapters.length && @@ -96,13 +98,18 @@ export function useProposalsVotes( ); const {data: votesDataResults, error: votesDataResultsError} = useQuery( - ['votesDataResults', votesDataCallsData], - async () => - await multicall({ - calls: votesDataCallsData as MulticallTuple[], - web3Instance: web3Instance as Web3, - }), - {enabled: !!votesDataCallsData && !!web3Instance} + ['votesDataResults', votesDataCallsData?.length], + async () => { + if (!votesDataCallsData?.length || !web3Instance) { + return; + } + + return await multicall({ + calls: votesDataCallsData, + web3Instance, + }); + }, + {enabled: !!votesDataCallsData?.length && !!web3Instance} ); /** @@ -183,7 +190,6 @@ export function useProposalsVotes( } if (votesDataResults) { - setProposalsVotesStatus(AsyncStatus.FULFILLED); setProposalsVotes( safeProposalVotingAdapters.map( ([proposalId, {votingAdapterName}], i) => [ @@ -194,11 +200,13 @@ export function useProposalsVotes( ] ) ); + + 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 4bc289787..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,6 +46,9 @@ describe('useProposalsVotes unit tests', () => { ], ]; + let mockWeb3Provider: FakeHttpProvider; + let web3Instance: Web3; + await act(async () => { const {result, waitForValueToChange} = await renderHook( () => useProposalsVotes(proposalsVotingAdapterTuples), @@ -52,82 +57,61 @@ 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', - 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); @@ -135,6 +119,21 @@ describe('useProposalsVotes unit tests', () => { 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); @@ -244,50 +243,6 @@ 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]] - ) - ); - }, }, } ); @@ -328,6 +283,9 @@ describe('useProposalsVotes unit tests', () => { ], ]; + let mockWeb3Provider: FakeHttpProvider; + let web3Instance: Web3; + await act(async () => { const {result, waitForValueToChange} = await renderHook( () => useProposalsVotes(proposalsVotingAdapterTuples), @@ -336,53 +294,44 @@ 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); @@ -390,6 +339,14 @@ describe('useProposalsVotes unit tests', () => { 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); @@ -455,49 +412,6 @@ 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]] - ) - ); - }, }, } ); @@ -528,49 +442,6 @@ 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]] - ) - ); - }, }, } ); From e3ff95b60084de03a3acce932372b29c154da310 Mon Sep 17 00:00:00 2001 From: jdville03 Date: Fri, 13 Aug 2021 13:17:42 -0700 Subject: [PATCH 25/25] improve use of react queries --- .../hooks/useGovernanceProposals.ts | 4 ++++ .../proposals/hooks/useDaoProposals.ts | 20 +++++++++++-------- .../hooks/useOffchainVotingResults.ts | 16 +++++++++++---- .../proposals/hooks/useProposalOrDraft.ts | 6 +++++- .../proposals/hooks/useProposals.ts | 16 ++++++++++----- .../proposals/hooks/useProposalsVotes.ts | 6 +++--- .../hooks/useProposalsVotingState.ts | 20 +++++++++++-------- 7 files changed, 59 insertions(+), 29 deletions(-) diff --git a/src/components/governance/hooks/useGovernanceProposals.ts b/src/components/governance/hooks/useGovernanceProposals.ts index f8123eb1d..4f4417ff6 100644 --- a/src/components/governance/hooks/useGovernanceProposals.ts +++ b/src/components/governance/hooks/useGovernanceProposals.ts @@ -49,6 +49,10 @@ export function useGovernanceProposals({ const {isMountedRef} = useIsMounted(); + /** + * React Query + */ + const { data: snapshotProposalEntriesData, error: snapshotProposalEntriesError, diff --git a/src/components/proposals/hooks/useDaoProposals.ts b/src/components/proposals/hooks/useDaoProposals.ts index c335d7c86..53cb03dc3 100644 --- a/src/components/proposals/hooks/useDaoProposals.ts +++ b/src/components/proposals/hooks/useDaoProposals.ts @@ -1,6 +1,5 @@ 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'; @@ -66,17 +65,22 @@ export function useDaoProposals(proposalIds: string[]): UseDaoProposalsReturn { const {web3Instance} = useWeb3Modal(); /** - * Their hooks + * React Query */ const {data: daoProposalsData, error: daoProposalsQueryError} = useQuery( ['daoProposals', daoProposalsCalls], - async () => - await multicall({ - calls: daoProposalsCalls as MulticallTuple[], - web3Instance: web3Instance as Web3, - }), - {enabled: !!daoProposalsCalls && !!web3Instance} + async () => { + if (!daoProposalsCalls?.length || !web3Instance) { + return; + } + + return await multicall({ + calls: daoProposalsCalls, + web3Instance, + }); + }, + {enabled: !!daoProposalsCalls?.length && !!web3Instance} ); /** diff --git a/src/components/proposals/hooks/useOffchainVotingResults.ts b/src/components/proposals/hooks/useOffchainVotingResults.ts index 737e56190..cedca7a57 100644 --- a/src/components/proposals/hooks/useOffchainVotingResults.ts +++ b/src/components/proposals/hooks/useOffchainVotingResults.ts @@ -79,13 +79,21 @@ export function useOffchainVotingResults( const {isMountedRef} = useIsMounted(); /** - * Their hooks + * React Query */ 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; @@ -115,11 +123,11 @@ export function useOffchainVotingResults( try { const result = await getUnitsPerChoiceCached({ - bankAddress: bankAddress as string, - getPriorAmountABI: getPriorAmountABI as AbiItem, + bankAddress, + getPriorAmountABI, snapshot, voterAddressesAndChoices, - web3Instance: web3Instance as Web3, + web3Instance, }); return [idInSnapshot, result]; diff --git a/src/components/proposals/hooks/useProposalOrDraft.ts b/src/components/proposals/hooks/useProposalOrDraft.ts index bbd32acc3..916c575f1 100644 --- a/src/components/proposals/hooks/useProposalOrDraft.ts +++ b/src/components/proposals/hooks/useProposalOrDraft.ts @@ -194,8 +194,12 @@ export function useProposalOrDraft( if (refetchCount === 0) return; /** - * Reset queries when `refetchCount` is incremented (proposal is + * 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(); } diff --git a/src/components/proposals/hooks/useProposals.ts b/src/components/proposals/hooks/useProposals.ts index 4f7b8f7df..74e921571 100644 --- a/src/components/proposals/hooks/useProposals.ts +++ b/src/components/proposals/hooks/useProposals.ts @@ -197,14 +197,17 @@ export function useProposals({ useProposalsVotes(proposalsVotingAdapters); /** - * Their hooks + * React Query */ const {data: snapshotDraftEntriesData, error: snapshotDraftEntriesError} = useQuery( ['snapshotDraftEntries', adapterAddress], - async () => - await getSnapshotDraftsByAdapterAddress(adapterAddress as string), + async () => { + if (!adapterAddress) return; + + return await getSnapshotDraftsByAdapterAddress(adapterAddress); + }, { enabled: !!adapterAddress, } @@ -215,8 +218,11 @@ export function useProposals({ error: snapshotProposalEntriesError, } = useQuery( ['snapshotProposalEntries', adapterAddress], - async () => - await getSnapshotProposalsByAdapterAddress(adapterAddress as string), + async () => { + if (!adapterAddress) return; + + return await getSnapshotProposalsByAdapterAddress(adapterAddress); + }, { enabled: !!adapterAddress, } diff --git a/src/components/proposals/hooks/useProposalsVotes.ts b/src/components/proposals/hooks/useProposalsVotes.ts index b11f7e35c..c50ec9e16 100644 --- a/src/components/proposals/hooks/useProposalsVotes.ts +++ b/src/components/proposals/hooks/useProposalsVotes.ts @@ -63,16 +63,16 @@ export function useProposalsVotes( const {web3Instance} = useWeb3Modal(); /** - * Their hooks + * React Query */ const {data: votesDataCallsData, error: votesDataCallsError} = useQuery( ['votesDataCalls', safeProposalVotingAdapters], async (): Promise => { - if (!registryAddress) return; + if (!safeProposalVotingAdapters || !registryAddress) return; return await Promise.all( - (safeProposalVotingAdapters as ProposalVotingAdapterTuple[]).map( + safeProposalVotingAdapters.map( async ([ proposalId, {votingAdapterAddress, getVotingAdapterABI, votingAdapterName}, diff --git a/src/components/proposals/hooks/useProposalsVotingState.ts b/src/components/proposals/hooks/useProposalsVotingState.ts index 6fff34201..1d87b3766 100644 --- a/src/components/proposals/hooks/useProposalsVotingState.ts +++ b/src/components/proposals/hooks/useProposalsVotingState.ts @@ -2,7 +2,6 @@ import {useCallback, useEffect, useState} from 'react'; import {useSelector} from 'react-redux'; import {AbiItem} from 'web3-utils/types'; import {useQuery} from 'react-query'; -import Web3 from 'web3'; import {AsyncStatus} from '../../../util/types'; import {multicall, MulticallTuple} from '../../web3/helpers'; @@ -65,7 +64,7 @@ export function useProposalsVotingState( const {web3Instance} = useWeb3Modal(); /** - * Their hooks + * React Query */ const { @@ -73,12 +72,17 @@ export function useProposalsVotingState( error: proposalsVotingStateResultError, } = useQuery( ['proposalsVotingStateResult', proposalsVotingStateCalls], - async () => - await multicall({ - calls: proposalsVotingStateCalls as MulticallTuple[], - web3Instance: web3Instance as Web3, - }), - {enabled: !!proposalsVotingStateCalls && !!web3Instance} + async () => { + if (!proposalsVotingStateCalls?.length || !web3Instance) { + return; + } + + return await multicall({ + calls: proposalsVotingStateCalls, + web3Instance, + }); + }, + {enabled: !!proposalsVotingStateCalls?.length && !!web3Instance} ); /**