diff --git a/src/components/Proposals/ProposalActions/ProposalAction.tsx b/src/components/Proposals/ProposalActions/ProposalAction.tsx
index c28c14db14..c3e09daced 100644
--- a/src/components/Proposals/ProposalActions/ProposalAction.tsx
+++ b/src/components/Proposals/ProposalActions/ProposalAction.tsx
@@ -1,17 +1,12 @@
import { Button, Flex, Text } from '@chakra-ui/react';
-import { useRouter } from 'next/navigation';
+import Link from 'next/link';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { BACKGROUND_SEMI_TRANSPARENT } from '../../../constants/common';
import { DAO_ROUTES } from '../../../constants/routes';
import useSnapshotProposal from '../../../hooks/DAO/loaders/snapshot/useSnapshotProposal';
import { useFractal } from '../../../providers/App/AppProvider';
-import {
- ExtendedSnapshotProposal,
- FractalProposal,
- FractalProposalState,
- SnapshotProposal,
-} from '../../../types';
+import { ExtendedSnapshotProposal, FractalProposal, FractalProposalState } from '../../../types';
import ContentBox from '../../ui/containers/ContentBox';
import { ProposalCountdown } from '../../ui/proposal/ProposalCountdown';
import { useVoteContext } from '../ProposalVotes/context/VoteContext';
@@ -60,7 +55,6 @@ export function ProposalAction({
node: { daoAddress },
readOnly: { user, dao },
} = useFractal();
- const { push } = useRouter();
const { t } = useTranslation();
const { isSnapshotProposal } = useSnapshotProposal(proposal);
const { canVote } = useVoteContext();
@@ -78,16 +72,6 @@ export function ProposalAction({
proposal.state === FractalProposalState.TIMELOCKABLE ||
proposal.state === FractalProposalState.TIMELOCKED));
- const handleClick = () => {
- if (isSnapshotProposal) {
- push(
- DAO_ROUTES.proposal.relative(daoAddress, (proposal as SnapshotProposal).snapshotProposalId)
- );
- } else {
- push(DAO_ROUTES.proposal.relative(daoAddress, proposal.proposalId));
- }
- };
-
const labelKey = useMemo(() => {
switch (proposal.state) {
case FractalProposalState.ACTIVE:
@@ -119,12 +103,17 @@ export function ProposalAction({
if (!showActionButton) {
if (!expandedView) {
return (
-
+
+
);
}
// This means that Proposal in state where there's no action to perform
@@ -155,11 +144,16 @@ export function ProposalAction({
}
return (
-
+
+
);
}
diff --git a/src/components/Proposals/ProposalInfo.tsx b/src/components/Proposals/ProposalInfo.tsx
index 123481654a..e0b8ab4cd8 100644
--- a/src/components/Proposals/ProposalInfo.tsx
+++ b/src/components/Proposals/ProposalInfo.tsx
@@ -1,4 +1,4 @@
-import { Box, Flex, Text, Image, Button } from '@chakra-ui/react';
+import { Box, Flex, Text, Image, Button, Link } from '@chakra-ui/react';
import { Shield } from '@decent-org/fractal-ui';
import { useTranslation } from 'react-i18next';
import useSnapshotProposal from '../../hooks/DAO/loaders/snapshot/useSnapshotProposal';
@@ -41,14 +41,12 @@ export function ProposalInfo({
/>
{(proposal as ExtendedSnapshotProposal).privacy === 'shutter' && (
);
diff --git a/src/hooks/DAO/loaders/snapshot/index.ts b/src/hooks/DAO/loaders/snapshot/index.ts
index 855da5e2ec..4c6c14b179 100644
--- a/src/hooks/DAO/loaders/snapshot/index.ts
+++ b/src/hooks/DAO/loaders/snapshot/index.ts
@@ -11,10 +11,9 @@ const defaultOptions: DefaultOptions = {
},
};
-const client = new ApolloClient({
- uri: 'https://hub.snapshot.org/graphql',
- cache: new InMemoryCache(),
- defaultOptions,
-});
-
-export default client;
+export const createClient = (uri: string) =>
+ new ApolloClient({
+ uri: `https://${uri.includes('testnet') ? 'testnet.' : ''}hub.snapshot.org/graphql`,
+ cache: new InMemoryCache(),
+ defaultOptions,
+ });
diff --git a/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts b/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts
index e6e0f15e7c..bbdd9904a6 100644
--- a/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts
+++ b/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts
@@ -1,5 +1,6 @@
import { gql } from '@apollo/client';
import { useCallback, useMemo, useState } from 'react';
+import { logError } from '../../../../helpers/errorLogging';
import { useFractal } from '../../../../providers/App/AppProvider';
import {
ExtendedSnapshotProposal,
@@ -9,7 +10,8 @@ import {
SnapshotVote,
SnapshotWeightedVotingChoice,
} from '../../../../types';
-import client from './';
+import useSnapshotSpaceName from './useSnapshotSpaceName';
+import { createClient } from './';
export default function useSnapshotProposal(proposal: FractalProposal | null | undefined) {
const [extendedSnapshotProposal, setExtendedSnapshotProposal] =
@@ -20,6 +22,12 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u
user: { address },
},
} = useFractal();
+ const daoSnapshotSpaceName = useSnapshotSpaceName();
+ const client = useMemo(() => {
+ if (daoSnapshotURL) {
+ return createClient(daoSnapshotURL);
+ }
+ }, [daoSnapshotURL]);
const snapshotProposal = proposal as SnapshotProposal;
const isSnapshotProposal = useMemo(
@@ -28,7 +36,7 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u
);
const loadProposal = useCallback(async () => {
- if (snapshotProposal?.snapshotProposalId) {
+ if (snapshotProposal?.snapshotProposalId && client) {
const proposalQueryResult = await client
.query({
query: gql`
@@ -188,17 +196,22 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u
votes: votesQueryResult,
} as ExtendedSnapshotProposal);
}
- }, [snapshotProposal?.snapshotProposalId, proposal, snapshotProposal?.state]);
+ }, [snapshotProposal?.snapshotProposalId, proposal, snapshotProposal?.state, client]);
const loadVotingWeight = useCallback(async () => {
- if (snapshotProposal?.snapshotProposalId) {
+ const emptyVotingWeight = {
+ votingWeight: 0,
+ votingWeightByStrategy: [0],
+ votingState: '',
+ };
+ if (snapshotProposal?.snapshotProposalId && client) {
const queryResult = await client
.query({
query: gql`
query UserVotingWeight {
vp(
voter: "${address}"
- space: "${daoSnapshotURL}"
+ space: "${daoSnapshotSpaceName}"
proposal: "${snapshotProposal.snapshotProposalId}"
) {
vp
@@ -208,6 +221,10 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u
}`,
})
.then(({ data: { vp } }) => {
+ if (!vp) {
+ logError('Error while retrieving Snapshot voting weight', vp);
+ return emptyVotingWeight;
+ }
return {
votingWeight: vp.vp,
votingWeightByStrategy: vp.vp_by_strategy,
@@ -218,12 +235,8 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u
return queryResult;
}
- return {
- votingWeight: 0,
- votingWeightByStrategy: [0],
- votingState: '',
- };
- }, [address, daoSnapshotURL, snapshotProposal?.snapshotProposalId]);
+ return emptyVotingWeight;
+ }, [address, snapshotProposal?.snapshotProposalId, client, daoSnapshotSpaceName]);
return {
loadVotingWeight,
diff --git a/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts b/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts
index 4d660805d9..45a4d6bd92 100644
--- a/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts
+++ b/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts
@@ -1,27 +1,35 @@
import { gql } from '@apollo/client';
-import { useCallback, useEffect, useRef } from 'react';
+import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useFractal } from '../../../../providers/App/AppProvider';
import { FractalGovernanceAction } from '../../../../providers/App/governance/action';
import { ActivityEventType, FractalProposalState } from '../../../../types';
import { SnapshotProposal } from '../../../../types/daoProposal';
-import client from './';
+import useSnapshotSpaceName from './useSnapshotSpaceName';
+import { createClient } from './';
export const useSnapshotProposals = () => {
const {
node: { daoSnapshotURL },
action,
} = useFractal();
+ const daoSnapshotSpaceName = useSnapshotSpaceName();
const currentSnapshotURL = useRef();
+ const client = useMemo(() => {
+ if (daoSnapshotURL) {
+ return createClient(daoSnapshotURL);
+ }
+ }, [daoSnapshotURL]);
const loadSnapshotProposals = useCallback(async () => {
- client
- .query({
- query: gql`
+ if (client) {
+ client
+ .query({
+ query: gql`
query Proposals {
proposals(
first: 50,
where: {
- space_in: ["${daoSnapshotURL}"]
+ space_in: ["${daoSnapshotSpaceName}"]
},
orderBy: "created",
orderDirection: desc
@@ -42,39 +50,40 @@ export const useSnapshotProposals = () => {
}
}
`,
- })
- .then(result => {
- const proposals: SnapshotProposal[] = result.data.proposals.map((proposal: any) => {
- return {
- eventDate: new Date(proposal.start * 1000),
- eventType: ActivityEventType.Governance,
- state:
- proposal.state === 'active'
- ? FractalProposalState.ACTIVE
- : proposal.state === 'closed'
- ? FractalProposalState.CLOSED
- : FractalProposalState.PENDING,
+ })
+ .then(result => {
+ const proposals: SnapshotProposal[] = result.data.proposals.map((proposal: any) => {
+ return {
+ eventDate: new Date(proposal.start * 1000),
+ eventType: ActivityEventType.Governance,
+ state:
+ proposal.state === 'active'
+ ? FractalProposalState.ACTIVE
+ : proposal.state === 'closed'
+ ? FractalProposalState.CLOSED
+ : FractalProposalState.PENDING,
- proposalId: proposal.id,
- snapshotProposalId: proposal.id,
- targets: [],
- title: proposal.title,
- description: proposal.body,
- startTime: proposal.start,
- endTime: proposal.end,
- };
- });
+ proposalId: proposal.id,
+ snapshotProposalId: proposal.id,
+ targets: [],
+ title: proposal.title,
+ description: proposal.body,
+ startTime: proposal.start,
+ endTime: proposal.end,
+ };
+ });
- action.dispatch({
- type: FractalGovernanceAction.SET_SNAPSHOT_PROPOSALS,
- payload: proposals,
+ action.dispatch({
+ type: FractalGovernanceAction.SET_SNAPSHOT_PROPOSALS,
+ payload: proposals,
+ });
});
- });
- }, [action, daoSnapshotURL]);
+ }
+ }, [action, daoSnapshotSpaceName, client]);
useEffect(() => {
- if (!daoSnapshotURL || daoSnapshotURL === currentSnapshotURL.current) return;
- currentSnapshotURL.current = daoSnapshotURL;
+ if (!daoSnapshotSpaceName || daoSnapshotSpaceName === currentSnapshotURL.current) return;
+ currentSnapshotURL.current = daoSnapshotSpaceName;
loadSnapshotProposals();
- }, [daoSnapshotURL, loadSnapshotProposals]);
+ }, [daoSnapshotSpaceName, loadSnapshotProposals]);
};
diff --git a/src/hooks/DAO/loaders/snapshot/useSnapshotSpaceName.ts b/src/hooks/DAO/loaders/snapshot/useSnapshotSpaceName.ts
new file mode 100644
index 0000000000..478930a086
--- /dev/null
+++ b/src/hooks/DAO/loaders/snapshot/useSnapshotSpaceName.ts
@@ -0,0 +1,9 @@
+import { useFractal } from '../../../../providers/App/AppProvider';
+
+export default function useSnapshotSpaceName() {
+ const {
+ node: { daoSnapshotURL },
+ } = useFractal();
+
+ return daoSnapshotURL?.split('/').pop();
+}
diff --git a/src/hooks/DAO/proposal/useCastVote.ts b/src/hooks/DAO/proposal/useCastVote.ts
index ff5498b382..d077a7e714 100644
--- a/src/hooks/DAO/proposal/useCastVote.ts
+++ b/src/hooks/DAO/proposal/useCastVote.ts
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify';
import { useSigner } from 'wagmi';
import { useVoteContext } from '../../../components/Proposals/ProposalVotes/context/VoteContext';
+import { logError } from '../../../helpers/errorLogging';
import { useFractal } from '../../../providers/App/AppProvider';
import {
AzoriusGovernance,
@@ -167,7 +168,7 @@ const useCastVote = ({
} catch (e) {
toast.dismiss(toastId);
toast.error('failedCastVote');
- console.error('Error while casting Snapshot vote', e);
+ logError('Error while casting Snapshot vote', e);
}
}
},
diff --git a/src/hooks/DAO/proposal/useSubmitProposal.ts b/src/hooks/DAO/proposal/useSubmitProposal.ts
index 9dc80667ec..073f4b03e9 100644
--- a/src/hooks/DAO/proposal/useSubmitProposal.ts
+++ b/src/hooks/DAO/proposal/useSubmitProposal.ts
@@ -9,7 +9,7 @@ import { BigNumber, Signer, utils } from 'ethers';
import { getAddress, isAddress } from 'ethers/lib/utils';
import { useCallback, useMemo, useState, useEffect } from 'react';
import { toast } from 'react-toastify';
-import { useSigner } from 'wagmi';
+import { useProvider, useSigner } from 'wagmi';
import { ADDRESS_MULTISIG_METADATA } from '../../../constants/common';
import { buildSafeAPIPost, encodeMultiSend } from '../../../helpers';
import { logError } from '../../../helpers/errorLogging';
@@ -24,6 +24,7 @@ import {
ProposalMetadata,
} from '../../../types';
import { buildSafeApiUrl, getAzoriusModuleFromModules } from '../../../utils';
+import { getAverageBlockTime } from '../../../utils/contract';
import useSignerOrProvider from '../../utils/useSignerOrProvider';
import { useFractalModules } from '../loaders/useFractalModules';
import { useDAOProposals } from '../loaders/useProposals';
@@ -61,6 +62,7 @@ export default function useSubmitProposal() {
const [canUserCreateProposal, setCanUserCreateProposal] = useState(false);
const loadDAOProposals = useDAOProposals();
const { data: signer } = useSigner();
+ const provider = useProvider();
const {
node: { safe, fractalModules },
@@ -295,6 +297,7 @@ export default function useSubmitProposal() {
});
setPendingCreateTx(true);
+ let success = false;
try {
const transactions = proposalData.targets.map((target, index) => ({
to: target,
@@ -316,12 +319,12 @@ export default function useSubmitProposal() {
})
)
).wait();
- await loadDAOProposals();
+ success = true;
+ toast.dismiss(toastId);
+ toast(successToastMessage);
if (successCallback) {
successCallback(safeAddress!);
}
- toast.dismiss(toastId);
- toast(successToastMessage);
} catch (e) {
toast.dismiss(toastId);
toast(failedToastMessage);
@@ -329,8 +332,17 @@ 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]
+ [loadDAOProposals, provider]
);
const submitProposal = useCallback(
diff --git a/src/hooks/utils/useAsyncRetry.ts b/src/hooks/utils/useAsyncRetry.ts
index dc8f629169..2781cbffdd 100644
--- a/src/hooks/utils/useAsyncRetry.ts
+++ b/src/hooks/utils/useAsyncRetry.ts
@@ -1,4 +1,5 @@
import { useCallback } from 'react';
+import { logError } from '../../helpers/errorLogging';
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
@@ -23,7 +24,7 @@ export function useAsyncRetry() {
return result;
}
} catch (error) {
- console.error('Error in requestWithRetries:', error);
+ logError('Error in requestWithRetries:', error);
}
currentRetries += 1;