diff --git a/package-lock.json b/package-lock.json index 3920ad4ac8..c44819cfc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,7 +66,6 @@ "@testing-library/react": "^14.2.1", "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", - "@types/remove-markdown": "^0.3.4", "@typescript-eslint/eslint-plugin": "^6.19.0", "@typescript-eslint/parser": "^6.19.0", "@vitejs/plugin-react": "^4.2.1", @@ -11650,12 +11649,6 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "peer": true }, - "node_modules/@types/remove-markdown": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@types/remove-markdown/-/remove-markdown-0.3.4.tgz", - "integrity": "sha512-i753EH/p02bw7bLlpfS/4CV1rdikbGiLabWyVsAvsFid3cA5RNU1frG7JycgY+NSnFwtoGlElvZVceCytecTDA==", - "dev": true - }, "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", diff --git a/package.json b/package.json index 3f0158faf4..73740da9c2 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,6 @@ "@testing-library/react": "^14.2.1", "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", - "@types/remove-markdown": "^0.3.4", "@typescript-eslint/eslint-plugin": "^6.19.0", "@typescript-eslint/parser": "^6.19.0", "@vitejs/plugin-react": "^4.2.1", diff --git a/src/components/ui/proposal/Markdown.tsx b/src/components/ui/proposal/Markdown.tsx index f1b63fe2fa..ce1bfa3b59 100644 --- a/src/components/ui/proposal/Markdown.tsx +++ b/src/components/ui/proposal/Markdown.tsx @@ -89,7 +89,7 @@ export default function Markdown({ truncate, content, collapsedLines = 6 }: IMar maxWidth="100%" > Promise, + dispatch: Dispatch, +): TypedListener => { + return async (_strategyAddress, proposalId, proposer, transactions, metadata) => { + // Wait for a block before processing. + // We've seen that calling smart contract functions in `mapProposalCreatedEventToProposal` + // which include the `proposalId` error out because the RPC node (rather, the block it's on) + // doesn't see this proposal yet (despite the event being caught in the app...). + const averageBlockTime = await getAverageBlockTime(provider); + await new Promise(resolve => setTimeout(resolve, averageBlockTime * 1000)); + + if (!metadata) { + return; + } + + const metaDataEvent: ProposalMetadata = JSON.parse(metadata); + const proposalData = { + metaData: { + title: metaDataEvent.title, + description: metaDataEvent.description, + documentationUrl: metaDataEvent.documentationUrl, + }, + transactions: transactions, + decodedTransactions: await decodeTransactions(decode, transactions), + }; + + const proposal = await mapProposalCreatedEventToProposal( + erc20StrategyContract, + erc721StrategyContract, + strategyType, + proposalId, + proposer, + azoriusContract, + provider, + Promise.resolve(undefined), + Promise.resolve(undefined), + Promise.resolve(undefined), + proposalData, + ); + + dispatch({ + type: FractalGovernanceAction.UPDATE_PROPOSALS_NEW, + payload: proposal, + }); + }; +}; + +const erc20VotedEventListener = ( + erc20StrategyContract: LinearERC20Voting, + strategyType: VotingStrategyType, + dispatch: Dispatch, +): TypedListener => { + return async (voter, proposalId, voteType, weight) => { + const votesSummary = await getProposalVotesSummary( + erc20StrategyContract, + undefined, + strategyType, + BigNumber.from(proposalId), + ); + + dispatch({ + type: FractalGovernanceAction.UPDATE_NEW_AZORIUS_ERC20_VOTE, + payload: { + proposalId: proposalId.toString(), + voter, + support: voteType, + weight, + votesSummary, + }, + }); + }; +}; + +const erc721VotedEventListener = ( + erc721StrategyContract: LinearERC721Voting, + strategyType: VotingStrategyType, + dispatch: Dispatch, +): TypedListener => { + return async (voter, proposalId, voteType, tokenAddresses, tokenIds) => { + const votesSummary = await getProposalVotesSummary( + undefined, + erc721StrategyContract, + strategyType, + BigNumber.from(proposalId), + ); + + dispatch({ + type: FractalGovernanceAction.UPDATE_NEW_AZORIUS_ERC721_VOTE, + payload: { + proposalId: proposalId.toString(), + voter, + support: voteType, + tokenAddresses, + tokenIds: tokenIds.map(tokenId => tokenId.toString()), + votesSummary, + }, + }); + }; +}; + +export const useAzoriusListeners = () => { + const { + action, + governanceContracts: { + azoriusContractAddress, + ozLinearVotingContractAddress, + erc721LinearVotingContractAddress, + }, + } = useFractal(); + + const baseContracts = useSafeContracts(); + const provider = useEthersProvider(); + const decode = useSafeDecoder(); + + const azoriusContract = useMemo(() => { + if (!baseContracts || !azoriusContractAddress) { + return; + } + + return baseContracts.fractalAzoriusMasterCopyContract.asProvider.attach(azoriusContractAddress); + }, [azoriusContractAddress, baseContracts]); + + const strategyType = useMemo(() => { + if (ozLinearVotingContractAddress) { + return VotingStrategyType.LINEAR_ERC20; + } else if (erc721LinearVotingContractAddress) { + return VotingStrategyType.LINEAR_ERC721; + } else { + return undefined; + } + }, [ozLinearVotingContractAddress, erc721LinearVotingContractAddress]); + + const erc20StrategyContract = useMemo(() => { + if (!baseContracts || !ozLinearVotingContractAddress) { + return undefined; + } + + return baseContracts.linearVotingMasterCopyContract.asProvider.attach( + ozLinearVotingContractAddress, + ); + }, [baseContracts, ozLinearVotingContractAddress]); + + const erc721StrategyContract = useMemo(() => { + if (!baseContracts || !erc721LinearVotingContractAddress) { + return undefined; + } + + return baseContracts.linearVotingERC721MasterCopyContract.asProvider.attach( + erc721LinearVotingContractAddress, + ); + }, [baseContracts, erc721LinearVotingContractAddress]); + + useEffect(() => { + if (!azoriusContract || !provider || !strategyType) { + return; + } + + const proposalCreatedFilter = azoriusContract.filters.ProposalCreated(); + const listener = proposalCreatedEventListener( + azoriusContract, + erc20StrategyContract, + erc721StrategyContract, + provider, + strategyType, + decode, + action.dispatch, + ); + + azoriusContract.on(proposalCreatedFilter, listener); + + return () => { + azoriusContract.off(proposalCreatedFilter, listener); + }; + }, [ + action.dispatch, + azoriusContract, + decode, + erc20StrategyContract, + erc721StrategyContract, + provider, + strategyType, + ]); + + useEffect(() => { + if (strategyType !== VotingStrategyType.LINEAR_ERC20 || !erc20StrategyContract) { + return; + } + + const votedEvent = erc20StrategyContract.filters.Voted(); + const listener = erc20VotedEventListener(erc20StrategyContract, strategyType, action.dispatch); + + erc20StrategyContract.on(votedEvent, listener); + + return () => { + erc20StrategyContract.off(votedEvent, listener); + }; + }, [action.dispatch, erc20StrategyContract, strategyType]); + + useEffect(() => { + if (strategyType !== VotingStrategyType.LINEAR_ERC721 || !erc721StrategyContract) { + return; + } + + const votedEvent = erc721StrategyContract.filters.Voted(); + const listener = erc721VotedEventListener( + erc721StrategyContract, + strategyType, + action.dispatch, + ); + + erc721StrategyContract.on(votedEvent, listener); + + return () => { + erc721StrategyContract.off(votedEvent, listener); + }; + }, [action.dispatch, erc721StrategyContract, strategyType]); + + useEffect(() => { + if (!azoriusContract) { + return; + } + + const timeLockPeriodFilter = azoriusContract.filters.TimelockPeriodUpdated(); + const timelockPeriodListener: TypedListener = timelockPeriod => { + action.dispatch({ + type: FractalGovernanceAction.UPDATE_TIMELOCK_PERIOD, + payload: BigNumber.from(timelockPeriod), + }); + }; + + azoriusContract.on(timeLockPeriodFilter, timelockPeriodListener); + + return () => { + azoriusContract.off(timeLockPeriodFilter, timelockPeriodListener); + }; + }, [action, azoriusContract]); +}; diff --git a/src/hooks/DAO/loaders/governance/useAzoriusProposals.ts b/src/hooks/DAO/loaders/governance/useAzoriusProposals.ts index 127fa75646..1e8b61d0f5 100644 --- a/src/hooks/DAO/loaders/governance/useAzoriusProposals.ts +++ b/src/hooks/DAO/loaders/governance/useAzoriusProposals.ts @@ -1,31 +1,43 @@ import { LinearERC20Voting, LinearERC721Voting } from '@fractal-framework/fractal-contracts'; -import { TypedListener } from '@fractal-framework/fractal-contracts/dist/typechain-types/common'; -import { ProposalCreatedEvent } from '@fractal-framework/fractal-contracts/dist/typechain-types/contracts/azorius/Azorius'; +import { + Azorius, + ProposalExecutedEvent, +} from '@fractal-framework/fractal-contracts/dist/typechain-types/contracts/azorius/Azorius'; import { VotedEvent as ERC20VotedEvent } from '@fractal-framework/fractal-contracts/dist/typechain-types/contracts/azorius/LinearERC20Voting'; import { VotedEvent as ERC721VotedEvent } from '@fractal-framework/fractal-contracts/dist/typechain-types/contracts/azorius/LinearERC721Voting'; -import { BigNumber } from 'ethers'; -import { useCallback, useEffect, useMemo } from 'react'; -import { logError } from '../../../../helpers/errorLogging'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useFractal } from '../../../../providers/App/AppProvider'; -import { FractalGovernanceAction } from '../../../../providers/App/governance/action'; import { useEthersProvider } from '../../../../providers/Ethers/hooks/useEthersProvider'; -import { ProposalMetadata, MetaTransaction, VotingStrategyType } from '../../../../types'; -import { AzoriusProposal, ProposalData } from '../../../../types/daoProposal'; -import { mapProposalCreatedEventToProposal, getProposalVotesSummary } from '../../../../utils'; +import { ProposalMetadata, VotingStrategyType, DecodedTransaction } from '../../../../types'; +import { AzoriusProposal } from '../../../../types/daoProposal'; +import { Providers } from '../../../../types/network'; +import { mapProposalCreatedEventToProposal, decodeTransactions } from '../../../../utils'; import useSafeContracts from '../../../safe/useSafeContracts'; -import { useAsyncRetry } from '../../../utils/useAsyncRetry'; import { useSafeDecoder } from '../../../utils/useSafeDecoder'; export const useAzoriusProposals = () => { + const currentAzoriusAddress = useRef(); + const { governanceContracts: { azoriusContractAddress, ozLinearVotingContractAddress, erc721LinearVotingContractAddress, }, - action, } = useFractal(); + const baseContracts = useSafeContracts(); + const provider = useEthersProvider(); + const decode = useSafeDecoder(); + + const azoriusContract = useMemo(() => { + if (!baseContracts || !azoriusContractAddress) { + return; + } + + return baseContracts.fractalAzoriusMasterCopyContract.asProvider.attach(azoriusContractAddress); + }, [azoriusContractAddress, baseContracts]); + const strategyType = useMemo(() => { if (ozLinearVotingContractAddress) { return VotingStrategyType.LINEAR_ERC20; @@ -35,265 +47,153 @@ export const useAzoriusProposals = () => { return undefined; } }, [ozLinearVotingContractAddress, erc721LinearVotingContractAddress]); - const provider = useEthersProvider(); - const decode = useSafeDecoder(); - const decodeTransactions = useCallback( - async (transactions: MetaTransaction[]) => { - const decodedTransactions = await Promise.all( - transactions.map(async tx => { - return decode(tx.value.toString(), tx.to, tx.data); - }), - ); - return decodedTransactions.flat(); - }, - [decode], - ); - const loadAzoriusProposals = useCallback(async (): Promise => { - if ( - !azoriusContractAddress || - !(ozLinearVotingContractAddress || erc721LinearVotingContractAddress) || - !strategyType || - !provider || - !baseContracts - ) { - return []; + const erc20StrategyContract = useMemo(() => { + if (!baseContracts || !ozLinearVotingContractAddress) { + return undefined; } - const azoriusContract = - baseContracts.fractalAzoriusMasterCopyContract.asProvider.attach(azoriusContractAddress); - const proposalCreatedFilter = azoriusContract.filters.ProposalCreated(); - const proposalCreatedEvents = await azoriusContract.queryFilter(proposalCreatedFilter); - let strategyContract: LinearERC20Voting | LinearERC721Voting; - if (ozLinearVotingContractAddress) { - strategyContract = baseContracts.linearVotingMasterCopyContract.asProvider.attach( - ozLinearVotingContractAddress, - ); - } else if (erc721LinearVotingContractAddress) { - strategyContract = baseContracts.linearVotingMasterCopyContract.asProvider.attach( - erc721LinearVotingContractAddress, - ); - } else { - logError('No strategy contract found'); - return []; + return baseContracts.linearVotingMasterCopyContract.asProvider.attach( + ozLinearVotingContractAddress, + ); + }, [baseContracts, ozLinearVotingContractAddress]); + + const erc721StrategyContract = useMemo(() => { + if (!baseContracts || !erc721LinearVotingContractAddress) { + return undefined; } - const proposals = await Promise.all( - proposalCreatedEvents.map(async ({ args }) => { - let proposalData; - if (args.metadata) { - const metadataEvent: ProposalMetadata = JSON.parse(args.metadata); - const decodedTransactions = await decodeTransactions(args.transactions); - proposalData = { - metaData: { - title: metadataEvent.title, - description: metadataEvent.description, - documentationUrl: metadataEvent.documentationUrl, - }, - transactions: args.transactions, - decodedTransactions, - }; - } - return mapProposalCreatedEventToProposal( - strategyContract, - strategyType, - args.proposalId, - args.proposer, - azoriusContract, - provider, - proposalData, - ); - }), + return baseContracts.linearVotingERC721MasterCopyContract.asProvider.attach( + erc721LinearVotingContractAddress, ); - return proposals; - }, [ - decodeTransactions, - ozLinearVotingContractAddress, - erc721LinearVotingContractAddress, - azoriusContractAddress, - provider, - strategyType, - baseContracts, - ]); + }, [baseContracts, erc721LinearVotingContractAddress]); - const { requestWithRetries } = useAsyncRetry(); - // Azrious proposals are listeners - const proposalCreatedListener: TypedListener = useCallback( - async (strategyAddress, proposalId, proposer, transactions, _metadata) => { - if ( - !azoriusContractAddress || - !(ozLinearVotingContractAddress || erc721LinearVotingContractAddress) || - !strategyType || - !provider || - !baseContracts - ) { - return; - } - let proposalData: ProposalData | undefined; - const azoriusContract = - baseContracts.fractalAzoriusMasterCopyContract.asProvider.attach(azoriusContractAddress); - if (_metadata) { - const metaDataEvent: ProposalMetadata = JSON.parse(_metadata); - proposalData = { - metaData: { - title: metaDataEvent.title, - description: metaDataEvent.description, - documentationUrl: metaDataEvent.documentationUrl, - }, - transactions: transactions, - decodedTransactions: await decodeTransactions(transactions), - }; - } - let strategyContract: LinearERC20Voting | LinearERC721Voting; - if (ozLinearVotingContractAddress) { - strategyContract = - baseContracts.linearVotingMasterCopyContract.asProvider.attach(strategyAddress); - } else if (erc721LinearVotingContractAddress) { - strategyContract = - baseContracts.linearVotingERC721MasterCopyContract.asProvider.attach(strategyAddress); - } else { - logError('No strategy contract found'); - return []; - } - const func = async () => { - return mapProposalCreatedEventToProposal( - strategyContract, - strategyType, - proposalId, - proposer, - azoriusContract, - provider, - proposalData, - ); - }; - const proposal = await requestWithRetries(func, 5, 7000); - if (proposal) { - action.dispatch({ - type: FractalGovernanceAction.UPDATE_PROPOSALS_NEW, - payload: proposal, - }); - } - }, - [ - baseContracts, - azoriusContractAddress, - provider, - decodeTransactions, - action, - requestWithRetries, - strategyType, - ozLinearVotingContractAddress, - erc721LinearVotingContractAddress, - ], - ); + const erc20VotedEvents = useMemo(async () => { + if (!erc20StrategyContract) { + return; + } - const erc20ProposalVotedEventListener: TypedListener = useCallback( - async (voter, proposalId, support, weight) => { - if (!ozLinearVotingContractAddress || !strategyType || !baseContracts) { - return; - } - const strategyContract = baseContracts.linearVotingMasterCopyContract.asProvider.attach( - ozLinearVotingContractAddress, - ); + const filter = erc20StrategyContract.filters.Voted(); + const events = await erc20StrategyContract.queryFilter(filter); - const votesSummary = await getProposalVotesSummary( - strategyContract, - strategyType, - BigNumber.from(proposalId), - ); + return events; + }, [erc20StrategyContract]); - action.dispatch({ - type: FractalGovernanceAction.UPDATE_NEW_AZORIUS_ERC20_VOTE, - payload: { - proposalId: proposalId.toString(), - voter, - support, - weight, - votesSummary, - }, - }); - }, - [ozLinearVotingContractAddress, action, strategyType, baseContracts], - ); + const erc721VotedEvents = useMemo(async () => { + if (!erc721StrategyContract) { + return; + } - const erc721ProposalVotedEventListener: TypedListener = useCallback( - async (voter, proposalId, support, tokenAddresses, tokenIds) => { - if (!erc721LinearVotingContractAddress || !strategyType || !baseContracts) { - return; - } - const strategyContract = baseContracts.linearVotingMasterCopyContract.asProvider.attach( - erc721LinearVotingContractAddress, - ); - const votesSummary = await getProposalVotesSummary( - strategyContract, - strategyType, - BigNumber.from(proposalId), - ); + const filter = erc721StrategyContract.filters.Voted(); + const events = await erc721StrategyContract.queryFilter(filter); - action.dispatch({ - type: FractalGovernanceAction.UPDATE_NEW_AZORIUS_ERC721_VOTE, - payload: { - proposalId: proposalId.toString(), - voter, - support, - tokenAddresses, - tokenIds: tokenIds.map(tokenId => tokenId.toString()), - votesSummary, - }, - }); - }, - [erc721LinearVotingContractAddress, action, strategyType, baseContracts], - ); + return events; + }, [erc721StrategyContract]); - useEffect(() => { - if (!azoriusContractAddress || !baseContracts) { + const executedEvents = useMemo(async () => { + if (!azoriusContract) { return; } - const azoriusContract = - baseContracts.fractalAzoriusMasterCopyContract.asProvider.attach(azoriusContractAddress); - const proposalCreatedFilter = azoriusContract.filters.ProposalCreated(); - - azoriusContract.on(proposalCreatedFilter, proposalCreatedListener); + const filter = azoriusContract.filters.ProposalExecuted(); + const events = await azoriusContract.queryFilter(filter); - return () => { - azoriusContract.off(proposalCreatedFilter, proposalCreatedListener); - }; - }, [azoriusContractAddress, proposalCreatedListener, baseContracts]); + return events; + }, [azoriusContract]); useEffect(() => { - if (ozLinearVotingContractAddress && baseContracts) { - const ozLinearVotingContract = baseContracts.linearVotingMasterCopyContract.asProvider.attach( - ozLinearVotingContractAddress, - ); + if (!azoriusContractAddress) { + currentAzoriusAddress.current = undefined; + } - const votedEvent = ozLinearVotingContract.filters.Voted(); + if (azoriusContractAddress && currentAzoriusAddress.current !== azoriusContractAddress) { + currentAzoriusAddress.current = azoriusContractAddress; + } + }, [azoriusContractAddress]); + + const loadAzoriusProposals = useCallback( + async ( + _azoriusContract: Azorius | undefined, + _erc20StrategyContract: LinearERC20Voting | undefined, + _erc721StrategyContract: LinearERC721Voting | undefined, + _strategyType: VotingStrategyType | undefined, + _erc20VotedEvents: Promise, + _erc721VotedEvents: Promise, + _executedEvents: Promise, + _provider: Providers | undefined, + _decode: ( + value: string, + to: string, + data?: string | undefined, + ) => Promise, + _proposalLoaded: (proposal: AzoriusProposal) => void, + ) => { + if (!_strategyType || !_azoriusContract || !_provider) { + return; + } - ozLinearVotingContract.on(votedEvent, erc20ProposalVotedEventListener); + const proposalCreatedFilter = _azoriusContract.filters.ProposalCreated(); + const proposalCreatedEvents = ( + await _azoriusContract.queryFilter(proposalCreatedFilter) + ).reverse(); - return () => { - ozLinearVotingContract.off(votedEvent, erc20ProposalVotedEventListener); - }; - } else if (erc721LinearVotingContractAddress && baseContracts) { - const erc721LinearVotingContract = - baseContracts.linearVotingMasterCopyContract.asProvider.attach( - erc721LinearVotingContractAddress, + for (const proposalCreatedEvent of proposalCreatedEvents) { + let proposalData; + if (proposalCreatedEvent.args.metadata) { + const metadataEvent: ProposalMetadata = JSON.parse(proposalCreatedEvent.args.metadata); + const decodedTransactions = await decodeTransactions( + _decode, + proposalCreatedEvent.args.transactions, + ); + proposalData = { + metaData: { + title: metadataEvent.title, + description: metadataEvent.description, + documentationUrl: metadataEvent.documentationUrl, + }, + transactions: proposalCreatedEvent.args.transactions, + decodedTransactions, + }; + } + + const proposal = await mapProposalCreatedEventToProposal( + _erc20StrategyContract, + _erc721StrategyContract, + _strategyType, + proposalCreatedEvent.args.proposalId, + proposalCreatedEvent.args.proposer, + _azoriusContract, + _provider, + _erc20VotedEvents, + _erc721VotedEvents, + _executedEvents, + proposalData, ); - const votedEvent = erc721LinearVotingContract.filters.Voted(); - erc721LinearVotingContract.on(votedEvent, erc721ProposalVotedEventListener); + if (currentAzoriusAddress.current !== azoriusContractAddress) { + // The DAO has changed, don't load the just-fetched proposal, + // into state, and get out of this function completely. + return; + } - return () => { - erc721LinearVotingContract.off(votedEvent, erc721ProposalVotedEventListener); - }; - } - }, [ - ozLinearVotingContractAddress, - erc721LinearVotingContractAddress, - erc20ProposalVotedEventListener, - erc721ProposalVotedEventListener, - baseContracts, - ]); + _proposalLoaded(proposal); + } + }, + [azoriusContractAddress], + ); - return loadAzoriusProposals; + return (proposalLoaded: (proposal: AzoriusProposal) => void) => { + return loadAzoriusProposals( + azoriusContract, + erc20StrategyContract, + erc721StrategyContract, + strategyType, + erc20VotedEvents, + erc721VotedEvents, + executedEvents, + provider, + decode, + proposalLoaded, + ); + }; }; diff --git a/src/hooks/DAO/loaders/governance/useERC20LinearStrategy.ts b/src/hooks/DAO/loaders/governance/useERC20LinearStrategy.ts index 049a774be3..44824c558a 100644 --- a/src/hooks/DAO/loaders/governance/useERC20LinearStrategy.ts +++ b/src/hooks/DAO/loaders/governance/useERC20LinearStrategy.ts @@ -1,5 +1,4 @@ import { TypedListener } from '@fractal-framework/fractal-contracts/dist/typechain-types/common'; -import { TimelockPeriodUpdatedEvent } from '@fractal-framework/fractal-contracts/dist/typechain-types/contracts/MultisigFreezeGuard'; import { QuorumNumeratorUpdatedEvent } from '@fractal-framework/fractal-contracts/dist/typechain-types/contracts/azorius/BaseQuorumPercent'; import { VotingPeriodUpdatedEvent } from '@fractal-framework/fractal-contracts/dist/typechain-types/contracts/azorius/LinearERC20Voting'; import { BigNumber } from 'ethers'; @@ -14,7 +13,6 @@ import { useTimeHelpers } from '../../../utils/useTimeHelpers'; export const useERC20LinearStrategy = () => { const { - governance: { type }, governanceContracts: { ozLinearVotingContractAddress, azoriusContractAddress }, action, } = useFractal(); @@ -68,7 +66,7 @@ export const useERC20LinearStrategy = () => { ]); useEffect(() => { - if (!ozLinearVotingContractAddress || !baseContracts || !type) { + if (!ozLinearVotingContractAddress || !baseContracts) { return; } const ozLinearVotingContract = baseContracts.linearVotingMasterCopyContract.asProvider.attach( @@ -86,10 +84,10 @@ export const useERC20LinearStrategy = () => { return () => { ozLinearVotingContract.off(votingPeriodfilter, listener); }; - }, [ozLinearVotingContractAddress, action, baseContracts, type]); + }, [ozLinearVotingContractAddress, action, baseContracts]); useEffect(() => { - if (!ozLinearVotingContractAddress || !baseContracts || !type) { + if (!ozLinearVotingContractAddress || !baseContracts) { return; } const ozLinearVotingContract = baseContracts.linearVotingMasterCopyContract.asProvider.attach( @@ -108,26 +106,7 @@ export const useERC20LinearStrategy = () => { return () => { ozLinearVotingContract.off(quorumNumeratorUpdatedFilter, quorumNumeratorUpdatedListener); }; - }, [ozLinearVotingContractAddress, action, baseContracts, type]); - - useEffect(() => { - if (!azoriusContractAddress || !baseContracts || !type) { - return; - } - const azoriusContract = - baseContracts.fractalAzoriusMasterCopyContract.asProvider.attach(azoriusContractAddress); - const timeLockPeriodFilter = azoriusContract.filters.TimelockPeriodUpdated(); - const timelockPeriodListener: TypedListener = timelockPeriod => { - action.dispatch({ - type: FractalGovernanceAction.UPDATE_TIMELOCK_PERIOD, - payload: BigNumber.from(timelockPeriod), - }); - }; - azoriusContract.on(timeLockPeriodFilter, timelockPeriodListener); - return () => { - azoriusContract.off(timeLockPeriodFilter, timelockPeriodListener); - }; - }, [azoriusContractAddress, action, baseContracts, type]); + }, [ozLinearVotingContractAddress, action, baseContracts]); return loadERC20Strategy; }; diff --git a/src/hooks/DAO/loaders/governance/useERC721LinearStrategy.ts b/src/hooks/DAO/loaders/governance/useERC721LinearStrategy.ts index 95524cd032..95a14c3a3c 100644 --- a/src/hooks/DAO/loaders/governance/useERC721LinearStrategy.ts +++ b/src/hooks/DAO/loaders/governance/useERC721LinearStrategy.ts @@ -1,5 +1,4 @@ import { TypedListener } from '@fractal-framework/fractal-contracts/dist/typechain-types/common'; -import { TimelockPeriodUpdatedEvent } from '@fractal-framework/fractal-contracts/dist/typechain-types/contracts/MultisigFreezeGuard'; import { VotingPeriodUpdatedEvent, QuorumThresholdUpdatedEvent, @@ -17,7 +16,6 @@ import { useTimeHelpers } from '../../../utils/useTimeHelpers'; export const useERC721LinearStrategy = () => { const { governanceContracts: { erc721LinearVotingContractAddress, azoriusContractAddress }, - governance: { type }, action, } = useFractal(); const provider = useEthersProvider(); @@ -73,7 +71,7 @@ export const useERC721LinearStrategy = () => { ]); useEffect(() => { - if (!erc721LinearVotingContractAddress || !baseContracts || !type) { + if (!erc721LinearVotingContractAddress || !baseContracts) { return; } const erc721LinearVotingContract = @@ -91,10 +89,10 @@ export const useERC721LinearStrategy = () => { return () => { erc721LinearVotingContract.off(votingPeriodfilter, listener); }; - }, [erc721LinearVotingContractAddress, action, baseContracts, type]); + }, [erc721LinearVotingContractAddress, action, baseContracts]); useEffect(() => { - if (!erc721LinearVotingContractAddress || !baseContracts || !type) { + if (!erc721LinearVotingContractAddress || !baseContracts) { return; } const erc721LinearVotingContract = @@ -115,27 +113,7 @@ export const useERC721LinearStrategy = () => { return () => { erc721LinearVotingContract.off(quorumThresholdUpdatedFilter, quorumThresholdUpdatedListener); }; - }, [erc721LinearVotingContractAddress, action, baseContracts, type]); - - useEffect(() => { - if (!azoriusContractAddress || !baseContracts || !type) { - return; - } - const azoriusContract = - baseContracts.fractalAzoriusMasterCopyContract.asProvider.attach(azoriusContractAddress); - - const timeLockPeriodFilter = azoriusContract.filters.TimelockPeriodUpdated(); - const timelockPeriodListener: TypedListener = timelockPeriod => { - action.dispatch({ - type: FractalGovernanceAction.UPDATE_TIMELOCK_PERIOD, - payload: BigNumber.from(timelockPeriod), - }); - }; - azoriusContract.on(timeLockPeriodFilter, timelockPeriodListener); - return () => { - azoriusContract.off(timeLockPeriodFilter, timelockPeriodListener); - }; - }, [azoriusContractAddress, action, baseContracts, type]); + }, [erc721LinearVotingContractAddress, action, baseContracts]); return loadERC721Strategy; }; diff --git a/src/hooks/DAO/loaders/useFractalNode.ts b/src/hooks/DAO/loaders/useFractalNode.ts index 293c9b2f2f..52836b4322 100644 --- a/src/hooks/DAO/loaders/useFractalNode.ts +++ b/src/hooks/DAO/loaders/useFractalNode.ts @@ -8,7 +8,6 @@ import { NodeAction } from '../../../providers/App/node/action'; import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; import { Node } from '../../../types'; import { mapChildNodes } from '../../../utils/hierarchy'; -import { useAsyncRetry } from '../../utils/useAsyncRetry'; import { useLazyDAOName } from '../useDAOName'; import { useFractalModules } from './useFractalModules'; @@ -33,7 +32,6 @@ export const useFractalNode = ( const { getDaoName } = useLazyDAOName(); const lookupModules = useFractalModules(); - const { requestWithRetries } = useAsyncRetry(); const formatDAOQuery = useCallback((result: { data?: DAOQueryQuery }, _daoAddress: string) => { if (!result.data) { @@ -102,11 +100,7 @@ export const useFractalNode = ( try { if (!safeAPI) throw new Error('SafeAPI not set'); - - safeInfo = await requestWithRetries( - () => safeAPI.getSafeInfo(utils.getAddress(_daoAddress)), - 5, - ); + safeInfo = await safeAPI.getSafeInfo(utils.getAddress(_daoAddress)); } catch (e) { reset({ error: true }); return; @@ -129,7 +123,7 @@ export const useFractalNode = ( payload: safeInfo, }); }, - [action, lookupModules, requestWithRetries, reset, safeAPI], + [action, lookupModules, reset, safeAPI], ); useEffect(() => { diff --git a/src/hooks/DAO/loaders/useProposals.ts b/src/hooks/DAO/loaders/useProposals.ts index 7d487f7c94..fdc58d6219 100644 --- a/src/hooks/DAO/loaders/useProposals.ts +++ b/src/hooks/DAO/loaders/useProposals.ts @@ -21,22 +21,23 @@ export const useDAOProposals = () => { clearIntervals(); if (type === GovernanceType.AZORIUS_ERC20 || type === GovernanceType.AZORIUS_ERC721) { // load Azorius proposals and strategies - const proposals = await loadAzoriusProposals(); - action.dispatch({ - type: FractalGovernanceAction.SET_PROPOSALS, - payload: proposals, + loadAzoriusProposals(proposal => { + action.dispatch({ + type: FractalGovernanceAction.SET_AZORIUS_PROPOSAL, + payload: proposal, + }); }); } else if (type === GovernanceType.MULTISIG) { // load mulisig proposals setMethodOnInterval(loadSafeMultisigProposals); } }, [ + clearIntervals, type, loadAzoriusProposals, action, - loadSafeMultisigProposals, setMethodOnInterval, - clearIntervals, + loadSafeMultisigProposals, ]); return loadDAOProposals; diff --git a/src/hooks/DAO/proposal/useSubmitProposal.ts b/src/hooks/DAO/proposal/useSubmitProposal.ts index 2d5d2ec4c7..e4fa5a562e 100644 --- a/src/hooks/DAO/proposal/useSubmitProposal.ts +++ b/src/hooks/DAO/proposal/useSubmitProposal.ts @@ -17,7 +17,6 @@ import { useEthersSigner } from '../../../providers/Ethers/hooks/useEthersSigner import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; import { MetaTransaction, ProposalExecuteData, ProposalMetadata } from '../../../types'; import { buildSafeApiUrl, getAzoriusModuleFromModules } from '../../../utils'; -import { getAverageBlockTime } from '../../../utils/contract'; import useSafeContracts from '../../safe/useSafeContracts'; import useSignerOrProvider from '../../utils/useSignerOrProvider'; import { useFractalModules } from '../loaders/useFractalModules'; @@ -224,7 +223,6 @@ export default function useSubmitProposal() { }); setPendingCreateTx(true); - let success = false; try { const transactions = proposalData.targets.map((target, index) => ({ to: target, @@ -246,7 +244,6 @@ export default function useSubmitProposal() { }), ) ).wait(); - success = true; toast.dismiss(toastId); toast(successToastMessage); if (successCallback) { @@ -259,17 +256,8 @@ export default function useSubmitProposal() { } finally { setPendingCreateTx(false); } - - if (success) { - const averageBlockTime = await getAverageBlockTime(provider); - // Frequently there's an error in loadDAOProposals if we're loading the proposal immediately after proposal creation - // The error occurs because block of proposal creation not yet mined and trying to fetch underlying data of voting weight for new proposal fails with that error - // The code that throws an error: https://github.com/decent-dao/fractal-contracts/blob/develop/contracts/azorius/LinearERC20Voting.sol#L205-L211 - // So to avoid showing error toast - we're marking proposal creation as success and only then re-fetching proposals - setTimeout(loadDAOProposals, averageBlockTime * 1.5 * 1000); - } }, - [loadDAOProposals, provider, addressPrefix], + [provider, addressPrefix], ); const submitProposal = useCallback( diff --git a/src/hooks/DAO/useDAOController.ts b/src/hooks/DAO/useDAOController.ts index 1f09864fae..cf64f02197 100644 --- a/src/hooks/DAO/useDAOController.ts +++ b/src/hooks/DAO/useDAOController.ts @@ -2,6 +2,7 @@ import { utils } from 'ethers'; import { useSearchParams } from 'react-router-dom'; import { useFractal } from '../../providers/App/AppProvider'; import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useAzoriusListeners } from './loaders/governance/useAzoriusListeners'; import { useERC20Claim } from './loaders/governance/useERC20Claim'; import { useSnapshotProposals } from './loaders/snapshot/useSnapshotProposals'; import { useFractalFreeze } from './loaders/useFractalFreeze'; @@ -48,6 +49,7 @@ export default function useDAOController() { useFractalTreasury(); useERC20Claim(); useSnapshotProposals(); + useAzoriusListeners(); return { invalidQuery, wrongNetwork, errorLoading }; } diff --git a/src/providers/App/governance/action.ts b/src/providers/App/governance/action.ts index 282c87bca7..0293593ea8 100644 --- a/src/providers/App/governance/action.ts +++ b/src/providers/App/governance/action.ts @@ -16,6 +16,7 @@ import { ProposalTemplate } from '../../../types/createProposalTemplate'; export enum FractalGovernanceAction { SET_GOVERNANCE_TYPE = 'SET_GOVERNANCE_TYPE', SET_PROPOSALS = 'SET_PROPOSALS', + SET_AZORIUS_PROPOSAL = 'SET_AZORIUS_PROPOSAL', SET_SNAPSHOT_PROPOSALS = 'SET_SNAPSHOT_PROPOSALS', SET_PROPOSAL_TEMPLATES = 'SET_PROPOSAL_TEMPLATES', SET_STRATEGY = 'SET_STRATEGY', @@ -60,6 +61,10 @@ export type FractalGovernanceActions = type: FractalGovernanceAction.SET_PROPOSALS; payload: FractalProposal[]; } + | { + type: FractalGovernanceAction.SET_AZORIUS_PROPOSAL; + payload: FractalProposal; + } | { type: FractalGovernanceAction.SET_SNAPSHOT_PROPOSALS; payload: FractalProposal[]; diff --git a/src/providers/App/governance/reducer.ts b/src/providers/App/governance/reducer.ts index 987772409f..6a3216f691 100644 --- a/src/providers/App/governance/reducer.ts +++ b/src/providers/App/governance/reducer.ts @@ -44,6 +44,12 @@ export const governanceReducer = (state: FractalGovernance, action: FractalGover ], }; } + case FractalGovernanceAction.SET_AZORIUS_PROPOSAL: { + return { + ...state, + proposals: [...(proposals || []), action.payload], + }; + } case FractalGovernanceAction.SET_PROPOSAL_TEMPLATES: { return { ...state, proposalTemplates: action.payload }; } diff --git a/src/providers/NetworkConfig/web3-modal.config.ts b/src/providers/NetworkConfig/web3-modal.config.ts index 92f03e3aad..855397160f 100644 --- a/src/providers/NetworkConfig/web3-modal.config.ts +++ b/src/providers/NetworkConfig/web3-modal.config.ts @@ -30,6 +30,9 @@ export const wagmiConfig = defaultWagmiConfig({ transports: { [mainnet.id]: http( `https://eth-mainnet.g.alchemy.com/v2/${import.meta.env.VITE_APP_ALCHEMY_MAINNET_API_KEY}`, + { + batch: true, + }, ), [sepolia.id]: http( `https://eth-sepolia.g.alchemy.com/v2/${import.meta.env.VITE_APP_ALCHEMY_SEPOLIA_API_KEY}`, diff --git a/src/utils/azorius.ts b/src/utils/azorius.ts index 1061d44e8e..7ee1edfe92 100644 --- a/src/utils/azorius.ts +++ b/src/utils/azorius.ts @@ -3,6 +3,9 @@ import { LinearERC20Voting, LinearERC721Voting, } from '@fractal-framework/fractal-contracts'; +import { ProposalExecutedEvent } from '@fractal-framework/fractal-contracts/dist/typechain-types/contracts/azorius/Azorius'; +import { VotedEvent as ERC20VotedEvent } from '@fractal-framework/fractal-contracts/dist/typechain-types/contracts/azorius/LinearERC20Voting'; +import { VotedEvent as ERC721VotedEvent } from '@fractal-framework/fractal-contracts/dist/typechain-types/contracts/azorius/LinearERC721Voting'; import { SafeMultisigTransactionWithTransfersResponse } from '@safe-global/safe-service-client'; import { BigNumber } from 'ethers'; import { strategyFractalProposalStates } from '../constants/strategy'; @@ -24,6 +27,7 @@ import { DecodedTransaction, VotingStrategyType, ERC721ProposalVote, + MetaTransaction, } from '../types'; import { Providers } from '../types/network'; import { getTimeStamp } from './contract'; @@ -37,23 +41,17 @@ export const getAzoriusProposalState = async ( }; const getQuorum = async ( - strategyContract: LinearERC20Voting | LinearERC721Voting, + erc20StrategyContract: LinearERC20Voting | undefined, + erc721StrategyContract: LinearERC721Voting | undefined, strategyType: VotingStrategyType, proposalId: BigNumber, ) => { let quorum; - if (strategyType === VotingStrategyType.LINEAR_ERC20) { - try { - quorum = await (strategyContract as LinearERC20Voting).quorumVotes(proposalId); - } catch (e) { - // For who knows reason - strategy.quorumVotes might give you an error - // Seems like occuring when token deployment haven't worked properly - logError('Error while getting strategy quorum', e); - quorum = BigNumber.from(0); - } - } else if (strategyType === VotingStrategyType.LINEAR_ERC721) { - quorum = await (strategyContract as LinearERC721Voting).quorumThreshold(); + if (strategyType === VotingStrategyType.LINEAR_ERC20 && erc20StrategyContract) { + quorum = await erc20StrategyContract.quorumVotes(proposalId); + } else if (strategyType === VotingStrategyType.LINEAR_ERC721 && erc721StrategyContract) { + quorum = await erc721StrategyContract.quorumThreshold(); } else { quorum = BigNumber.from(0); } @@ -62,19 +60,44 @@ const getQuorum = async ( }; export const getProposalVotesSummary = async ( - strategy: LinearERC20Voting | LinearERC721Voting, + erc20Strategy: LinearERC20Voting | undefined, + erc721Strategy: LinearERC721Voting | undefined, strategyType: VotingStrategyType, proposalId: BigNumber, ): Promise => { try { - const { yesVotes, noVotes, abstainVotes } = await strategy.getProposalVotes(proposalId); + if (erc20Strategy !== undefined && erc721Strategy !== undefined) { + logError("we don't support multiple strategy contracts"); + throw new Error("we don't support multiple strategy contracts"); + } - return { - yes: yesVotes, - no: noVotes, - abstain: abstainVotes, - quorum: await getQuorum(strategy, strategyType, proposalId), - }; + if (erc20Strategy !== undefined) { + const { yesVotes, noVotes, abstainVotes } = await erc20Strategy.getProposalVotes(proposalId); + + return { + yes: yesVotes, + no: noVotes, + abstain: abstainVotes, + quorum: await getQuorum(erc20Strategy, erc721Strategy, strategyType, proposalId), + }; + } else if (erc721Strategy !== undefined) { + const { yesVotes, noVotes, abstainVotes } = await erc721Strategy.getProposalVotes(proposalId); + + return { + yes: yesVotes, + no: noVotes, + abstain: abstainVotes, + quorum: await getQuorum(erc20Strategy, erc721Strategy, strategyType, proposalId), + }; + } else { + const zero = BigNumber.from(0); + return { + yes: zero, + no: zero, + abstain: zero, + quorum: zero, + }; + } } catch (e) { // Sometimes loading DAO proposals called in the moment when proposal was **just** created // Thus, calling `getProposalVotes` for such a proposal reverts with error @@ -90,44 +113,92 @@ export const getProposalVotesSummary = async ( } }; -export const getProposalVotes = async ( - strategyContract: LinearERC20Voting | LinearERC721Voting, +const getProposalVotes = ( + erc20VotedEvents: ERC20VotedEvent[] | undefined, + erc721VotedEvents: ERC721VotedEvent[] | undefined, proposalId: BigNumber, -): Promise => { - const voteEventFilter = strategyContract.filters.Voted(); - const votes = await strategyContract.queryFilter(voteEventFilter); - const proposalVotesEvent = votes.filter(voteEvent => proposalId.eq(voteEvent.args.proposalId)); +): (ProposalVote | ERC721ProposalVote)[] => { + if (erc20VotedEvents !== undefined && erc721VotedEvents !== undefined) { + logError("two voting contracts? we don't support that."); + return []; + } - return proposalVotesEvent.map(({ args: { voter, voteType, ...rest } }) => { - return { + if (erc20VotedEvents !== undefined) { + const erc20ProposalVoteEvents = erc20VotedEvents.filter(voteEvent => + proposalId.eq(voteEvent.args.proposalId), + ); + + return erc20ProposalVoteEvents.map(({ args: { voter, voteType, ...rest } }) => ({ ...rest, voter, choice: VOTE_CHOICES[voteType], - }; - }); + })); + } else if (erc721VotedEvents !== undefined) { + const erc721ProposalVoteEvents = erc721VotedEvents.filter(voteEvent => + proposalId.eq(voteEvent.args.proposalId), + ); + + return erc721ProposalVoteEvents.map(({ args: { voter, voteType, tokenIds, ...rest } }) => ({ + ...rest, + voter, + choice: VOTE_CHOICES[voteType], + weight: BigNumber.from(1), + tokenIds: tokenIds.map(id => id.toString()), + })); + } + + return []; }; export const mapProposalCreatedEventToProposal = async ( - strategyContract: LinearERC20Voting | LinearERC721Voting, + erc20StrategyContract: LinearERC20Voting | undefined, + erc721StrategyContract: LinearERC721Voting | undefined, strategyType: VotingStrategyType, proposalId: BigNumber, proposer: string, azoriusContract: Azorius, provider: Providers, + erc20VotedEvents: Promise, + erc721VotedEvents: Promise, + executedEvents: Promise, data?: ProposalData, ) => { - const { endBlock, startBlock, abstainVotes, yesVotes, noVotes } = - await strategyContract.getProposalVotes(proposalId); - const quorum = await getQuorum(strategyContract, strategyType, proposalId); + if (erc20StrategyContract !== undefined && erc721StrategyContract !== undefined) { + logError("we don't support multiple strategy contracts"); + throw new Error("we don't support multiple strategy contracts"); + } + + let proposalVotes = { + startBlock: 0, + endBlock: 0, + noVotes: BigNumber.from(0), + yesVotes: BigNumber.from(0), + abstainVotes: BigNumber.from(0), + }; + + if (erc20StrategyContract !== undefined) { + proposalVotes = await erc20StrategyContract.getProposalVotes(proposalId); + } else if (erc721StrategyContract !== undefined) { + proposalVotes = await erc721StrategyContract.getProposalVotes(proposalId); + } + + const quorum = await getQuorum( + erc20StrategyContract, + erc721StrategyContract, + strategyType, + proposalId, + ); + + const deadlineSeconds = await getTimeStamp(proposalVotes.endBlock, provider); + const block = await provider.getBlock(proposalVotes.startBlock); - const deadlineSeconds = await getTimeStamp(endBlock, provider); const state = await getAzoriusProposalState(azoriusContract, proposalId); - const votes = await getProposalVotes(strategyContract, proposalId); - const block = await provider.getBlock(startBlock); + const votes = getProposalVotes(await erc20VotedEvents, await erc721VotedEvents, proposalId); + const votesSummary = { - yes: yesVotes, - no: noVotes, - abstain: abstainVotes, + yes: proposalVotes.yesVotes, + no: proposalVotes.noVotes, + abstain: proposalVotes.abstainVotes, quorum, }; @@ -135,9 +206,7 @@ export const mapProposalCreatedEventToProposal = async ( let transactionHash: string | undefined; if (state === FractalProposalState.EXECUTED) { - const proposalExecutedFilter = azoriusContract.filters.ProposalExecuted(); - const proposalExecutedEvents = await azoriusContract.queryFilter(proposalExecutedFilter); - const executedEvent = proposalExecutedEvents.find(event => + const executedEvent = (await executedEvents)?.find(event => BigNumber.from(event.args[0]).eq(proposalId), ); transactionHash = executedEvent?.transactionHash; @@ -149,7 +218,7 @@ export const mapProposalCreatedEventToProposal = async ( proposalId: proposalId.toString(), targets, proposer, - startBlock: BigNumber.from(startBlock), + startBlock: BigNumber.from(proposalVotes.startBlock), transactionHash, deadlineMs: deadlineSeconds * 1000, state, @@ -225,3 +294,13 @@ export const parseDecodedData = ( export function getAzoriusModuleFromModules(modules: FractalModuleData[]) { return modules.find(module => module.moduleType === FractalModuleType.AZORIUS); } + +export const decodeTransactions = async ( + _decode: (value: string, to: string, data?: string | undefined) => Promise, + _transactions: MetaTransaction[], +) => { + const decodedTransactions = await Promise.all( + _transactions.map(async tx => _decode(tx.value.toString(), tx.to, tx.data)), + ); + return decodedTransactions.flat(); +};