diff --git a/public/images/shutter-icon.svg b/public/images/shutter-icon.svg new file mode 100644 index 0000000000..26acc2846f --- /dev/null +++ b/public/images/shutter-icon.svg @@ -0,0 +1 @@ + .;░░░░░░░░░φ≥≥╓, '"_____'"⌐;░░░░░Γ░╚φ╖, _'__________'\░░░░░░╠╠╠#╓ ___________.'''^░░░φ▒╚╠╠ε_ _ _,____________░░░░░░░╚╠╠╗ , ____________!"░░░░░░▒╚╠╠╦ _ ≈▒%╓ ,_'__'____''_¡'∩░░░░░▒╚░╠ç `²δ╠╗▄▓φ╝╠╝▒▒≥ε,╩#╩░╬└"""_____'__¡'\░░░░░╠▒╠▒▒≥╥, └╙╠▒░░░░#▒▒▒╣▓▄╟▒⌐..¡,_'_______░¡░░░░▒╠╠╠╠▒╚▒░╠╦, ^▄▓╗▄▓█╩▒▄▒░.╙╩╬╩╬╬╠▒░"5╝╬∩_~..__'!''^░░φ╚╬╠╩╚╠░!╙╚╠▒╖ ,▄▒╣╣▓╬▓╬╣╣╣▓▓▒,'"╩╠╫░;░░░╩╩≥░╓▄░░;_!'_'░░░╚▒╠╚░░¡░░░░╚╠╠╗ __╠╣╬╣╬╬╬╬╬╬╩╚╠░_'7╚╠φ▒_____""░│▓▒░,''._!░░░░░░░░░░░░░Σ╠╠╠╣╦_ ░└╚╚╫▒░φ╠╠▒▒╠╬░≥,`-┘└╙░___.░▒░░░╬▒▄░_';_!│░░░∩░░Γ░░░░░╚╬╩╠╠▓▄ _.__^╙╙▓╙╣▀╫▓▓▓█▄▒░≤╓≤φ▒φ▒⌐φ╜└╣╝╙╣╠╝╬#╦≤;\░""'.,,;φ░░░╚╚╚╠╬╠╬╬╬▄_ ,_'^╠╣▓φ▄▄╟███▓▓▒░░;_.░░δ.,╟▒░▒╜__@╬╬╩▒∩┌!"¡░░∩φΓ░▒φ▒░░╚▒╬╠╬╬╬▌ '.ª╝╬╬▓█████╬╬╬╬╬▒╚φ░░░";;φ▒▒▄▌_.▐░╠∩Γ"≥≥░░░░░░φ░▒φ░░░░▒╠╠╬╬╬╬ `░░╚╚╬▓╬╬▓▓╣╬╬╠╠╬╬▒░╙φ░░;"╙╚╠╠_'╙╬▒│".╧φ░░φ░≥░░Γ░░░φ▒╠╠▒▒╠╬╬╣╣╓ "≥░╚╙│╚╫╩╬▓╠▓▓▓▓╬▓▄░░╚φ░░░░Ü__`φ╡░░^,φφφ╙╚░▒▒░░▒░░░╠╠╬╠╠╬╬╬╣╬▒_ _"░;░░░φ╣╣▓▓▀▓╬▓▓▒╙▒░░░╙░░░,"5╚▒░.░░│▓╬░░╚▓▓▒╠▒φ▒░╚╠╠╠╟╬╬╣╬╬╣╣µ ║▒╣▓╫╬╣▓▓▓▓╣╬╚¼▄╣▓▄░≥,╙φ░░░,╙░▒░╠╬▀┘_└║╠▀╬▓▒▒▒╚▒╚╬╬╬╣╣╬╬╬╬╣╬╬▌ ╙`╚╚╠▀╬╝▓▓▓▓░▓▒╣▓▓█▓▄░░└╙▒╚╠░"_'│µ'░"▓▓▒Σ╟╬▒╙░╬╠╟╬╬╬╣╬╬╬╣╣╬╬╬▄ └░░╠╩╬▀╣▌╬▓▓▓██▓▓▓▒░≥_"╚φ░φ▄▓▓▓░"╙╝▒░φ╫╬╬▒╬╬╠╬╬╠╣╣╬╬╬╬╬╬╣╬µ ²░░░░╚╣▓╣▓▓▓▓▀╙▓╣,╫▒░»_╙▒▒╬╬▒;░φ▒▒▓▓▓╬▓╬╩╙▓▓╩╠╠╬╬╬╬╬╬╬╬╬▌ "░φ⌐⌠╠╠▓╣▓▒░φ▓▓╝▓▓█▓▒░▒╙╚╬▒╠╬╬╬╬╣╩╚╬▓▓▒░╙Σ╗ ╙╠╠╠╬╠╬╬╬╬╠ _`φ░│░;φ╣▒╣▓╬▓╣▓█████▄▒░φ╙╙▒░╚╠▒░φ▒╠╬╬╬╣╬` _╙╠╠╠╠╠╠╩ \░░░░╙╚▒░╠▓█▓█████▓╣▒▒φ░░╚║▒▒▒▒╠╬╬╬╣╬▒ ``` "░░░░╙Å█▓████▓▒▓▓▓╬▓▓▓▒▒░╠╢╢╫╬╣╬▓▓╣╬╬╦, "░░░░╩▓╩╙▀▓▓╬██▌╟▓▓▓▓▓▓▒╠╣▓█████▓╬╣╬╬▒ "░░░≡░╓╟▓╬██Å╣▓▓▓▓▓▓▓█████████▓╬▓╬▓╠µ "φ░░╠▀╬▓╣██▓╣▓▓▓▓▓██████▓███▓█████▓╦ ²░φ░▒╚╠╫╬╣╬╣▓██╩╬▓█████▓▓▓▓▓╬╬╬╬╬▌ _"φ▒▒╫▓▓▓╣▓▓▓▓▓╬╬▓▓▓▓▒╠╠╩╠╠╬╬╠╣╣▌ _╚▓▓╬╬▓▓█▓█▓▒▓▓█╬╣╬░φ▒φ╫╬▒╩╠╠⌐ _ │╟▓╬╬╬╬╬╣╬╣▓█╬╬╬▒╠╚░░░░╠╬╜_ `╙╣╩╙ ╙╣╬╬╬▒▒░╠╣╠╠▒▒▒╬╠╣╣╩ _ _ ╙╠╬╬╬╬╬╬╬╬╬╬╝╨╙ \ No newline at end of file diff --git a/src/components/Proposals/ProposalActions/CastVote.tsx b/src/components/Proposals/ProposalActions/CastVote.tsx index 59934232f1..e454872c5b 100644 --- a/src/components/Proposals/ProposalActions/CastVote.tsx +++ b/src/components/Proposals/ProposalActions/CastVote.tsx @@ -1,5 +1,5 @@ -import { Button, Tooltip } from '@chakra-ui/react'; -import { CloseX, Check } from '@decent-org/fractal-ui'; +import { Button, Tooltip, Box, Text, Image, Flex } from '@chakra-ui/react'; +import { CloseX, Check, SingleCheckOutline } from '@decent-org/fractal-ui'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import useSnapshotProposal from '../../../hooks/DAO/loaders/snapshot/useSnapshotProposal'; @@ -12,22 +12,32 @@ import { AzoriusVoteChoice, ExtendedSnapshotProposal, } from '../../../types'; +import WeightedInput from '../../ui/forms/WeightedInput'; import { useVoteContext } from '../ProposalVotes/context/VoteContext'; function Vote({ proposal, extendedSnapshotProposal, + onCastSnapshotVote, }: { proposal: FractalProposal; extendedSnapshotProposal?: ExtendedSnapshotProposal; + onCastSnapshotVote?: () => Promise; }) { const [pending, setPending] = useState(false); - const { t } = useTranslation(['common', 'proposal']); + const { t } = useTranslation(['common', 'proposal', 'transaction']); const { isLoaded: isCurrentBlockLoaded, currentBlockNumber } = useCurrentBlockNumber(); const azoriusProposal = proposal as AzoriusProposal; - const { castVote, castSnapshotVote } = useCastVote({ + const { + castVote, + castSnapshotVote, + handleChangeSnapshotWeightedChoice, + handleSelectSnapshotChoice, + selectedChoice, + snapshotWeightedChoice, + } = useCastVote({ proposal, setPending, extendedSnapshotProposal, @@ -44,7 +54,7 @@ function Vote({ // If user is lucky enough - he could create a proposal and proceed to vote on it // even before the block, in which proposal was created, was mined. // This gives a weird behavior when casting vote fails due to requirement under LinearERC20Voting contract that current block number - // Shouldn't be equal to proposal's start block number. Which is dictated by the need to have voting tokens delegation being "finalized" to prevent proposal hijacking. + // shouldn't be equal to proposal's start block number. Which is dictated by the need to have voting tokens delegation being "finalized" to prevent proposal hijacking. const proposalStartBlockNotFinalized = Boolean( !isSnapshotProposal && isCurrentBlockLoaded && @@ -60,19 +70,85 @@ function Vote({ hasVotedLoading; if (isSnapshotProposal && extendedSnapshotProposal) { + const isWeighted = extendedSnapshotProposal.type === 'weighted'; + const weightedTotalValue = snapshotWeightedChoice.reduce((prev, curr) => prev + curr, 0); + const voteDisabled = + (!isWeighted && typeof selectedChoice === 'undefined') || + (isWeighted && weightedTotalValue === 0); return ( <> - {extendedSnapshotProposal.choices.map((choice, i) => ( - + ))} + + {hasVoted && ( + - {choice} - - ))} + + + {t('successCastVote', { ns: 'transaction' })} + + {t('snapshotRecastVoteHelper', { ns: 'transaction' })} + + )} + + {t('poweredBy')} + + + Snapshot icon + {t('snapshot')} + + {extendedSnapshotProposal.privacy === 'shutter' && ( + + Shutter icon + {t('shutter')} + + )} + + ); } diff --git a/src/components/Proposals/ProposalActions/ProposalAction.tsx b/src/components/Proposals/ProposalActions/ProposalAction.tsx index 0de83e1468..bad0ed455a 100644 --- a/src/components/Proposals/ProposalActions/ProposalAction.tsx +++ b/src/components/Proposals/ProposalActions/ProposalAction.tsx @@ -18,12 +18,15 @@ import { useVoteContext } from '../ProposalVotes/context/VoteContext'; import CastVote from './CastVote'; import { Execute } from './Execute'; +// TODO: Refactor extendedSnapshotProposal and onCastSnapshotVote to the context export function ProposalActions({ proposal, extendedSnapshotProposal, + onCastSnapshotVote, }: { proposal: FractalProposal; extendedSnapshotProposal?: ExtendedSnapshotProposal; + onCastSnapshotVote?: () => Promise; }) { switch (proposal.state) { case FractalProposalState.ACTIVE: @@ -31,6 +34,7 @@ export function ProposalActions({ ); case FractalProposalState.EXECUTABLE: @@ -45,10 +49,12 @@ export function ProposalAction({ proposal, expandedView, extendedSnapshotProposal, + onCastSnapshotVote, }: { proposal: FractalProposal; expandedView?: boolean; extendedSnapshotProposal?: ExtendedSnapshotProposal; + onCastSnapshotVote?: () => Promise; }) { const { node: { daoAddress }, @@ -65,11 +71,12 @@ export function ProposalAction({ ); const showActionButton = - user.votingWeight.gt(0) && - (isActiveProposal || - proposal.state === FractalProposalState.EXECUTABLE || - proposal.state === FractalProposalState.TIMELOCKABLE || - proposal.state === FractalProposalState.TIMELOCKED); + (isSnapshotProposal && canVote) || + (user.votingWeight.gt(0) && + (isActiveProposal || + proposal.state === FractalProposalState.EXECUTABLE || + proposal.state === FractalProposalState.TIMELOCKABLE || + proposal.state === FractalProposalState.TIMELOCKED)); const handleClick = () => { if (isSnapshotProposal) { @@ -125,7 +132,8 @@ export function ProposalAction({ } if (expandedView) { - if (user.votingWeight.eq(0) || (isActiveProposal && !canVote)) return null; + if (!isSnapshotProposal && (user.votingWeight.eq(0) || (isActiveProposal && !canVote))) + return null; return ( @@ -140,6 +148,7 @@ export function ProposalAction({ ); diff --git a/src/components/Proposals/ProposalVotes/context/VoteContext.tsx b/src/components/Proposals/ProposalVotes/context/VoteContext.tsx index 1a9bcada66..81766b1f3d 100644 --- a/src/components/Proposals/ProposalVotes/context/VoteContext.tsx +++ b/src/components/Proposals/ProposalVotes/context/VoteContext.tsx @@ -8,6 +8,7 @@ import { AzoriusProposal, MultisigProposal, GovernanceType, + ExtendedSnapshotProposal, } from '../../../../types'; interface IVoteContext { @@ -36,8 +37,10 @@ export const useVoteContext = () => { export function VoteContextProvider({ proposal, children, + extendedSnapshotProposal, }: { proposal: FractalProposal; + extendedSnapshotProposal?: ExtendedSnapshotProposal; children: ReactNode; }) { const [canVote, setCanVote] = useState(false); @@ -61,8 +64,10 @@ export function VoteContextProvider({ const getHasVoted = useCallback(() => { setHasVotedLoading(true); if (isSnapshotProposal) { - // Snapshot proposals not tracking votes - setHasVoted(false); + setHasVoted( + !!extendedSnapshotProposal && + !!extendedSnapshotProposal.votes.find(vote => vote.voter === user.address) + ); } else if (dao?.isAzorius) { const azoriusProposal = proposal as AzoriusProposal; if (azoriusProposal?.votes) { @@ -75,18 +80,17 @@ export function VoteContextProvider({ ); } setHasVotedLoading(false); - }, [dao, isSnapshotProposal, proposal, user.address]); + }, [dao, isSnapshotProposal, proposal, user.address, extendedSnapshotProposal]); const getCanVote = useCallback( async (refetchUserTokens?: boolean) => { setCanVoteLoading(true); let newCanVote = false; - if (isSnapshotProposal) { - const votingWeightData = await loadVotingWeight(); - newCanVote = votingWeightData.votingWeight > 1; - } if (user.address) { - if (type === GovernanceType.AZORIUS_ERC20) { + if (isSnapshotProposal) { + const votingWeightData = await loadVotingWeight(); + newCanVote = votingWeightData.votingWeight >= 1; + } else if (type === GovernanceType.AZORIUS_ERC20) { newCanVote = user.votingWeight.gt(0) && !hasVoted; } else if (type === GovernanceType.AZORIUS_ERC721) { if (refetchUserTokens) { @@ -131,7 +135,14 @@ export function VoteContextProvider({ return ( {children} diff --git a/src/components/Proposals/SnapshotProposalDetails/SnapshotProposalVotes.tsx b/src/components/Proposals/SnapshotProposalDetails/SnapshotProposalVotes.tsx index f19708fddd..76e81239b8 100644 --- a/src/components/Proposals/SnapshotProposalDetails/SnapshotProposalVotes.tsx +++ b/src/components/Proposals/SnapshotProposalDetails/SnapshotProposalVotes.tsx @@ -15,7 +15,7 @@ interface ISnapshotProposalVotes { export default function SnapshotProposalVotes({ proposal }: ISnapshotProposalVotes) { const { t } = useTranslation('proposal'); const { totalVotesCasted } = useTotalVotes({ proposal }); - const { votes, votesBreakdown, choices, strategies, privacy, state } = proposal; + const { votes, votesBreakdown, choices, strategies, privacy, state, type } = proposal; const strategySymbol = strategies[0].params.symbol; return ( @@ -43,9 +43,15 @@ export default function SnapshotProposalVotes({ proposal }: ISnapshotProposalVot colSpan={4} rowGap={4} > - {choices.map(choice => { + {choices.map((choice, i) => { + const votesBreakdownChoice = + type === 'weighted' ? votesBreakdown[i] : votesBreakdown[choice]; + const votesBreakdownChoiceTotal = + votesBreakdownChoice && votesBreakdownChoice?.total + ? votesBreakdownChoice?.total + : 0; const choicePercentageFromTotal = - ((votesBreakdown[choice]?.total || 0) * 100) / totalVotesCasted; + (votesBreakdownChoiceTotal * 100) / totalVotesCasted; return ( ); diff --git a/src/components/Proposals/SnapshotProposalDetails/index.tsx b/src/components/Proposals/SnapshotProposalDetails/index.tsx index aca9a85e33..8b22d21c7c 100644 --- a/src/components/Proposals/SnapshotProposalDetails/index.tsx +++ b/src/components/Proposals/SnapshotProposalDetails/index.tsx @@ -42,10 +42,14 @@ export default function SnapshotProposalDetails({ proposal }: ISnapshotProposalD {user.address && ( - + diff --git a/src/components/ui/forms/WeightedInput.tsx b/src/components/ui/forms/WeightedInput.tsx new file mode 100644 index 0000000000..030ccdda82 --- /dev/null +++ b/src/components/ui/forms/WeightedInput.tsx @@ -0,0 +1,74 @@ +import { IconButton, Flex, Input, Text } from '@chakra-ui/react'; +import { AddPlus, Minus } from '@decent-org/fractal-ui'; + +interface IWeightedInput { + onChange: (value: number) => void; + label: string; + value: number; + totalValue: number; +} + +export default function WeightedInput({ label, value, totalValue, onChange }: IWeightedInput) { + return ( + + + {label} + + + onChange(Math.max(0, value - 1))} + disabled={!value} + > + + + onChange(Math.max(parseInt(e.target.value), 0))} + value={value.toString()} + type="number" + border="none" + bg="transparent" + padding={0} + textAlign="center" + color="gold.500" + width="48px" + /> + onChange(value + 1)} + mr={3} + > + + + {totalValue ? ((value * 100) / totalValue).toFixed(2) : 0}% + + + ); +} diff --git a/src/hooks/DAO/loaders/snapshot/index.ts b/src/hooks/DAO/loaders/snapshot/index.ts index df748eae9d..855da5e2ec 100644 --- a/src/hooks/DAO/loaders/snapshot/index.ts +++ b/src/hooks/DAO/loaders/snapshot/index.ts @@ -1,8 +1,20 @@ -import { ApolloClient, InMemoryCache } from '@apollo/client'; +import { ApolloClient, InMemoryCache, DefaultOptions } from '@apollo/client'; + +const defaultOptions: DefaultOptions = { + watchQuery: { + fetchPolicy: 'no-cache', + errorPolicy: 'ignore', + }, + query: { + fetchPolicy: 'no-cache', + errorPolicy: 'all', + }, +}; const client = new ApolloClient({ uri: 'https://hub.snapshot.org/graphql', cache: new InMemoryCache(), + defaultOptions, }); export default client; diff --git a/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts b/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts index f509d26324..e162641749 100644 --- a/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts +++ b/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts @@ -128,14 +128,14 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u const isShielded = privacy === 'shutter'; const isClosed = snapshotProposal.state === FractalProposalState.CLOSED; + if (!(isShielded && !isClosed)) { votesQueryResult.forEach((vote: SnapshotVote) => { if (type === 'weighted') { const voteChoices = vote.choice as SnapshotWeightedVotingChoice; - Object.keys(voteChoices).forEach((choiceIndex: any) => { - // In Snapshot API choices are indexed 1-based. The first choice has index 1. - // https://docs.snapshot.org/tools/api#vote - const voteChoice = choices[choiceIndex - 1]; + if (typeof voteChoices === 'number') { + // Means vote casted for single option, and Snapshot API returns just just number then =/ + const voteChoice = voteChoices - 1; const existingChoiceType = votesBreakdown[voteChoice]; if (existingChoiceType) { votesBreakdown[voteChoice] = { @@ -148,7 +148,25 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u votes: [vote], }; } - }); + } else { + Object.keys(voteChoices).forEach((choiceIndex: any) => { + // In Snapshot API choices are indexed 1-based. The first choice has index 1. + // https://docs.snapshot.org/tools/api#vote + const voteChoice = choices[choiceIndex - 1]; + const existingChoiceType = votesBreakdown[voteChoice]; + if (existingChoiceType) { + votesBreakdown[voteChoice] = { + total: existingChoiceType.total + getVoteWeight(vote), + votes: [...existingChoiceType.votes, vote], + }; + } else { + votesBreakdown[voteChoice] = { + total: getVoteWeight(vote), + votes: [vote], + }; + } + }); + } } else { const voteChoice = vote.choice as number; const choiceKey = choices[voteChoice - 1]; diff --git a/src/hooks/DAO/proposal/useCastVote.ts b/src/hooks/DAO/proposal/useCastVote.ts index fcd2bb8aab..ff5498b382 100644 --- a/src/hooks/DAO/proposal/useCastVote.ts +++ b/src/hooks/DAO/proposal/useCastVote.ts @@ -1,6 +1,6 @@ import snapshot from '@snapshot-labs/snapshot.js'; import { ethers } from 'ethers'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'react-toastify'; import { useSigner } from 'wagmi'; @@ -28,6 +28,9 @@ const useCastVote = ({ setPending?: React.Dispatch>; extendedSnapshotProposal?: ExtendedSnapshotProposal; }) => { + const [selectedChoice, setSelectedChoice] = useState(); + const [snapshotWeightedChoice, setSnapshotWeightedChoice] = useState([]); + const { governanceContracts: { ozLinearVotingContract, erc721LinearVotingContract }, governance, @@ -54,8 +57,24 @@ const useCastVote = ({ } }, [setPending, contractCallPending]); + useEffect(() => { + if (extendedSnapshotProposal) { + setSnapshotWeightedChoice(extendedSnapshotProposal.choices.map(() => 0)); + } + }, [extendedSnapshotProposal]); + const { t } = useTranslation('transaction'); + const handleSelectSnapshotChoice = useCallback((choiceIndex: number) => { + setSelectedChoice(choiceIndex); + }, []); + + const handleChangeSnapshotWeightedChoice = useCallback((choiceIndex: number, value: number) => { + setSnapshotWeightedChoice(prevState => + prevState.map((choiceValue, index) => (index === choiceIndex ? value : choiceValue)) + ); + }, []); + const castVote = useCallback( async (vote: number) => { let contractFn; @@ -101,9 +120,13 @@ const useCastVote = ({ ); const castSnapshotVote = useCallback( - async (choice: number) => { + async (onSuccess?: () => Promise) => { if (signer && signer?.provider && address && daoSnapshotURL && extendedSnapshotProposal) { let toastId; + const choice = + extendedSnapshotProposal.type === 'weighted' + ? snapshotWeightedChoice + : (selectedChoice as number) + 1; try { toastId = toast(t('pendingCastVote'), { autoClose: false, @@ -135,7 +158,12 @@ const useCastVote = ({ }); } toast.dismiss(toastId); - toast.success(t('successCastVote')); + toast.success(`${t('successCastVote')}. ${t('snapshotRecastVoteHelper')}`); + setSelectedChoice(undefined); + if (onSuccess) { + // Need to refetch votes after timeout so that Snapshot API has enough time to record the vote + setTimeout(() => onSuccess(), 3000); + } } catch (e) { toast.dismiss(toastId); toast.error('failedCastVote'); @@ -143,10 +171,25 @@ const useCastVote = ({ } } }, - [signer, address, daoSnapshotURL, extendedSnapshotProposal, t] + [ + signer, + address, + daoSnapshotURL, + extendedSnapshotProposal, + t, + selectedChoice, + snapshotWeightedChoice, + ] ); - return { castVote, castSnapshotVote }; + return { + castVote, + castSnapshotVote, + selectedChoice, + snapshotWeightedChoice, + handleSelectSnapshotChoice, + handleChangeSnapshotWeightedChoice, + }; }; export default useCastVote; diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index a112258f65..a4bd181c69 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -17,6 +17,7 @@ "vote": "Vote", "sign": "Sign", "snapshot": "Snapshot", + "shutter": "shutter", "send": "Send", "transfer": "Transfer", "received": "Received", @@ -99,5 +100,6 @@ "invalidSafe2": "Try refreshing the page or changing networks in your wallet.", "invalidChain": "Your currently connected chain is not supported.", "showMore": "Show More", - "showLess": "Show Less" + "showLess": "Show Less", + "poweredBy": "Powered by" } \ No newline at end of file diff --git a/src/i18n/locales/en/transaction.json b/src/i18n/locales/en/transaction.json index 1a09dae458..c8a590d0bf 100644 --- a/src/i18n/locales/en/transaction.json +++ b/src/i18n/locales/en/transaction.json @@ -20,5 +20,6 @@ "pendingTokenClaim": "Claiming {{ symbol }}", "failedTokenClaim": "Failed to claim {{ symbol }}", "successTokenClaim": "You have successfully claimed {{ amount }} {{ symbol }}", + "snapshotRecastVoteHelper": "You can still change your vote until the vote end date", "modifyGovernanceSetAzoriusProposalPendingMessage": "Submitting proposal to modify governance..." } \ No newline at end of file