From 9e4e6ea6ef7a06beb03f863e3d7749227ec58b30 Mon Sep 17 00:00:00 2001 From: Wan <495709+wa0x6e@users.noreply.github.com> Date: Wed, 7 Aug 2024 23:36:00 +0900 Subject: [PATCH] feat: add reason when voting (#511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add reason when voting * fix: update voting power fetching * fix: do not pin reason when empty * chore: lint fix * Update apps/ui/src/views/Proposal.vue Co-authored-by: Wiktor Tkaczyński * fix(ui): improve disabled button UI * Update apps/ui/src/components/ProposalsListItem.vue Co-authored-by: Wiktor Tkaczyński * fix: improve typing * refactor: use same VotingPowerItem type * refactor: improve chaining * refactor: use optional chaining * chore: fix formatting * chore: fix missing ; * refactor: improve code readability * fix: fix to match updated name * fix: use web3.account to detect logged in user There's not need to check for authInitiated, as we don't support login and logout while in this page * fix: fix PUBLISH button always stuck on loading state for guest user * fix: always use the full VotingPowerItem type * fix(ux): improve button layout on small screen * refactor: avoid deprecated validateForm usage * fix(ui): decrease margin size * fix(ui): improve error message * fix: fix variable not reactive * chore: update tests to include metadataUri * fix: fix metadataUri being ignored from voteHash * fix: check that voting power is successful, in case threshold is 0 * refactor: save propose and vote potential in vp store * chore: remove console output * fix: remove unecessary template wrapper * Update apps/ui/src/views/Space/Proposals.vue Co-authored-by: Wiktor Tkaczyński --------- Co-authored-by: Wiktor Tkaczyński --- .changeset/breezy-owls-clean.md | 5 + .../src/components/IndicatorVotingPower.vue | 38 ++-- apps/ui/src/components/MessageVotingPower.vue | 45 +++++ apps/ui/src/components/Modal/Vote.vue | 168 ++++++++++++++++++ apps/ui/src/components/Modal/VotingPower.vue | 49 +++-- .../src/components/ProposalVoteApproval.vue | 2 - apps/ui/src/components/ProposalVoteBasic.vue | 4 - .../components/ProposalVoteRankedChoice.vue | 2 - .../components/ProposalVoteSingleChoice.vue | 2 - .../src/components/ProposalVoteWeighted.vue | 2 - apps/ui/src/components/ProposalsListItem.vue | 31 ++-- apps/ui/src/components/Ui/Button.vue | 21 ++- apps/ui/src/composables/useActions.ts | 5 +- apps/ui/src/composables/useVotingPower.ts | 68 +++++++ apps/ui/src/helpers/utils.ts | 33 ++-- apps/ui/src/networks/evm/actions.ts | 8 +- apps/ui/src/networks/offchain/actions.ts | 6 +- apps/ui/src/networks/starknet/actions.ts | 9 +- apps/ui/src/networks/types.ts | 3 +- apps/ui/src/stores/votingPowers.ts | 104 +++++++++++ apps/ui/src/views/Editor.vue | 65 +++---- apps/ui/src/views/Proposal.vue | 134 +++++++------- apps/ui/src/views/Space/Proposals.vue | 69 +++---- .../clients/offchain/ethereum-sig/index.ts | 2 +- packages/sx.js/src/clients/offchain/types.ts | 1 + .../clients/starknet/ethereum-sig/index.ts | 2 +- .../src/clients/starknet/ethereum-tx/index.ts | 2 +- .../clients/starknet/starknet-sig/index.ts | 2 +- .../src/clients/starknet/starknet-tx/index.ts | 2 +- packages/sx.js/src/types/index.ts | 1 + .../__snapshots__/index.test.ts.snap | 12 +- .../starknet/ethereum-sig/index.test.ts | 1 + .../starknet/ethereum-tx/index.test.ts | 5 +- .../__snapshots__/index.test.ts.snap | 10 +- .../starknet/starknet-sig/index.test.ts | 1 + 35 files changed, 645 insertions(+), 269 deletions(-) create mode 100644 .changeset/breezy-owls-clean.md create mode 100644 apps/ui/src/components/MessageVotingPower.vue create mode 100644 apps/ui/src/components/Modal/Vote.vue create mode 100644 apps/ui/src/composables/useVotingPower.ts create mode 100644 apps/ui/src/stores/votingPowers.ts diff --git a/.changeset/breezy-owls-clean.md b/.changeset/breezy-owls-clean.md new file mode 100644 index 000000000..6b9d144e8 --- /dev/null +++ b/.changeset/breezy-owls-clean.md @@ -0,0 +1,5 @@ +--- +"@snapshot-labs/sx": patch +--- + +support submitting a reason when voting diff --git a/apps/ui/src/components/IndicatorVotingPower.vue b/apps/ui/src/components/IndicatorVotingPower.vue index 32c50f3be..428260e45 100644 --- a/apps/ui/src/components/IndicatorVotingPower.vue +++ b/apps/ui/src/components/IndicatorVotingPower.vue @@ -1,40 +1,29 @@ + + diff --git a/apps/ui/src/components/Modal/Vote.vue b/apps/ui/src/components/Modal/Vote.vue new file mode 100644 index 000000000..9555b9afd --- /dev/null +++ b/apps/ui/src/components/Modal/Vote.vue @@ -0,0 +1,168 @@ + + + diff --git a/apps/ui/src/components/Modal/VotingPower.vue b/apps/ui/src/components/Modal/VotingPower.vue index 76de4cfe0..688ebc73e 100644 --- a/apps/ui/src/components/Modal/VotingPower.vue +++ b/apps/ui/src/components/Modal/VotingPower.vue @@ -2,21 +2,18 @@ import { _n, shorten } from '@/helpers/utils'; import { addressValidator as isValidAddress } from '@/helpers/validation'; import { getNetwork } from '@/networks'; -import { VotingPower, VotingPowerStatus } from '@/networks/types'; +import { VotingPowerItem } from '@/stores/votingPowers'; import { NetworkID } from '@/types'; const props = defineProps<{ open: boolean; networkId: NetworkID; - votingPowerSymbol: string; - votingPowers: VotingPower[]; - votingPowerStatus: VotingPowerStatus; - finalDecimals: number; + votingPower?: VotingPowerItem; }>(); defineEmits<{ (e: 'close'); - (e: 'getVotingPower'); + (e: 'fetchVotingPower'); }>(); const network = computed(() => getNetwork(props.networkId)); @@ -25,8 +22,9 @@ const baseNetwork = computed(() => ? getNetwork(network.value.baseNetworkId) : network.value ); -const loading = computed(() => props.votingPowerStatus === 'loading'); -const error = computed(() => props.votingPowerStatus === 'error'); +const loading = computed( + () => !props.votingPower || props.votingPower.status === 'loading' +); -
-
- There was an error fetching your voting power. - - Retry - -
+
+
@@ -67,12 +58,16 @@ const error = computed(() => props.votingPowerStatus === 'error'); />
diff --git a/apps/ui/src/components/ProposalVoteApproval.vue b/apps/ui/src/components/ProposalVoteApproval.vue index aebeebbcd..3f348387b 100644 --- a/apps/ui/src/components/ProposalVoteApproval.vue +++ b/apps/ui/src/components/ProposalVoteApproval.vue @@ -4,7 +4,6 @@ import { Choice, Proposal } from '@/types'; type ApprovalChoice = number[]; const props = defineProps<{ - sendingType: Choice | null; proposal: Proposal; defaultChoice?: Choice; }>(); @@ -45,7 +44,6 @@ function toggleSelectedChoice(choice: number) { Vote diff --git a/apps/ui/src/components/ProposalVoteBasic.vue b/apps/ui/src/components/ProposalVoteBasic.vue index cab3ff8f5..da0c9dba5 100644 --- a/apps/ui/src/components/ProposalVoteBasic.vue +++ b/apps/ui/src/components/ProposalVoteBasic.vue @@ -3,7 +3,6 @@ import { Choice } from '@/types'; withDefaults( defineProps<{ - sendingType: Choice | null; size?: number; }>(), { size: 48 } @@ -23,7 +22,6 @@ const emit = defineEmits<{ '!size-[48px]': size === 48, '!size-[40px]': size === 40 }" - :loading="sendingType === 'for'" @click="emit('vote', 'for')" > @@ -36,7 +34,6 @@ const emit = defineEmits<{ '!size-[48px]': size === 48, '!size-[40px]': size === 40 }" - :loading="sendingType === 'against'" @click="emit('vote', 'against')" > @@ -49,7 +46,6 @@ const emit = defineEmits<{ '!size-[48px]': size === 48, '!size-[40px]': size === 40 }" - :loading="sendingType === 'abstain'" @click="emit('vote', 'abstain')" > diff --git a/apps/ui/src/components/ProposalVoteRankedChoice.vue b/apps/ui/src/components/ProposalVoteRankedChoice.vue index 2f62a1e67..2375e3aef 100644 --- a/apps/ui/src/components/ProposalVoteRankedChoice.vue +++ b/apps/ui/src/components/ProposalVoteRankedChoice.vue @@ -5,7 +5,6 @@ import { Choice, Proposal } from '@/types'; type RankedChoice = number[]; const props = defineProps<{ - sendingType: Choice | null; proposal: Proposal; defaultChoice?: Choice; }>(); @@ -49,7 +48,6 @@ const selectedChoices = ref( Vote diff --git a/apps/ui/src/components/ProposalVoteSingleChoice.vue b/apps/ui/src/components/ProposalVoteSingleChoice.vue index a6e10d93c..747345d31 100644 --- a/apps/ui/src/components/ProposalVoteSingleChoice.vue +++ b/apps/ui/src/components/ProposalVoteSingleChoice.vue @@ -2,7 +2,6 @@ import { Choice, Proposal } from '@/types'; const props = defineProps<{ - sendingType: Choice | null; proposal: Proposal; defaultChoice?: Choice; }>(); @@ -33,7 +32,6 @@ const selectedChoice = ref( diff --git a/apps/ui/src/components/ProposalVoteWeighted.vue b/apps/ui/src/components/ProposalVoteWeighted.vue index b418168c8..b20e7a864 100644 --- a/apps/ui/src/components/ProposalVoteWeighted.vue +++ b/apps/ui/src/components/ProposalVoteWeighted.vue @@ -5,7 +5,6 @@ import { Choice, Proposal } from '@/types'; type WeightedChoice = Record; const props = defineProps<{ - sendingType: Choice | null; proposal: Proposal; defaultChoice?: Choice; }>(); @@ -90,7 +89,6 @@ watch( Vote diff --git a/apps/ui/src/components/ProposalsListItem.vue b/apps/ui/src/components/ProposalsListItem.vue index 141b9ffa6..323ca66cb 100644 --- a/apps/ui/src/components/ProposalsListItem.vue +++ b/apps/ui/src/components/ProposalsListItem.vue @@ -6,22 +6,25 @@ import { Choice, Proposal as ProposalType } from '@/types'; const props = defineProps<{ proposal: ProposalType; showSpace: boolean }>(); const { getTsFromCurrent } = useMetaStore(); -const { vote } = useActions(); +const { modalAccountOpen } = useModal(); +const { web3 } = useWeb3(); + const { votes } = useAccount(); const modalOpenTimeline = ref(false); -const sendingType = ref(null); +const modalOpenVote = ref(false); +const selectedChoice = ref(null); const totalProgress = computed(() => quorumProgress(props.proposal)); -async function handleVoteClick(choice: Choice) { - sendingType.value = choice; - - try { - await vote(props.proposal, choice); - } finally { - sendingType.value = null; +const handleVoteClick = (choice: Choice) => { + if (!web3.value.account) { + modalAccountOpen.value = true; + return; } -} + + selectedChoice.value = choice; + modalOpenVote.value = true; +}; @@ -149,6 +151,13 @@ async function handleVoteClick(choice: Choice) { :proposal="proposal" @close="modalOpenTimeline = false" /> +
diff --git a/apps/ui/src/components/Ui/Button.vue b/apps/ui/src/components/Ui/Button.vue index 19d97d0d0..f2f4ccefb 100644 --- a/apps/ui/src/components/Ui/Button.vue +++ b/apps/ui/src/components/Ui/Button.vue @@ -27,15 +27,32 @@ withDefaults( - diff --git a/apps/ui/src/composables/useActions.ts b/apps/ui/src/composables/useActions.ts index d1ad1dc75..d7de7410a 100644 --- a/apps/ui/src/composables/useActions.ts +++ b/apps/ui/src/composables/useActions.ts @@ -240,7 +240,7 @@ export function useActions() { ); } - async function vote(proposal: Proposal, choice: Choice) { + async function vote(proposal: Proposal, choice: Choice, reason: string) { if (!web3.value.account) return await forceLogin(); const network = getNetwork(proposal.network); @@ -252,7 +252,8 @@ export function useActions() { web3.value.type as Connector, web3.value.account, proposal, - choice + choice, + reason ) ); diff --git a/apps/ui/src/composables/useVotingPower.ts b/apps/ui/src/composables/useVotingPower.ts new file mode 100644 index 000000000..52a508dc2 --- /dev/null +++ b/apps/ui/src/composables/useVotingPower.ts @@ -0,0 +1,68 @@ +import { supportsNullCurrent } from '@/networks'; +import { getIndex as getVotingPowerIndex } from '@/stores/votingPowers'; +import { Proposal, Space } from '@/types'; + +export function useVotingPower() { + const votingPowersStore = useVotingPowersStore(); + const { web3 } = useWeb3(); + const { getCurrent } = useMetaStore(); + + const item = ref(); + const block = ref(null); + + const space = computed(() => + item.value && 'space' in item.value + ? (item.value?.space as Space) + : item.value + ); + + const proposal = computed(() => + item.value && 'snapshot' in item.value + ? (item.value as Proposal) + : undefined + ); + + const proposalSnapshot = computed(() => { + if (!proposal.value) return null; + + return proposal.value.state === 'pending' ? null : proposal.value.snapshot; + }); + + const votingPower = computed( + () => + space.value && + votingPowersStore.votingPowers.get( + getVotingPowerIndex(space.value, block.value) + ) + ); + + function latestBlock(space: Space) { + return supportsNullCurrent(space.network) + ? null + : getCurrent(space.network) ?? 0; + } + + function reset() { + votingPowersStore.reset(); + } + + function fetch(spaceOrProposal: Space | Proposal) { + if (!web3.value.account) return; + + item.value = spaceOrProposal; + block.value = proposal.value + ? proposalSnapshot.value + : latestBlock(space.value as Space); + + votingPowersStore.fetch(item.value, web3.value.account, block.value); + } + + watch( + () => web3.value.account, + account => { + if (!account) reset(); + } + ); + + return { votingPower, fetch, reset }; +} diff --git a/apps/ui/src/helpers/utils.ts b/apps/ui/src/helpers/utils.ts index dd5313eb6..7dc78149d 100644 --- a/apps/ui/src/helpers/utils.ts +++ b/apps/ui/src/helpers/utils.ts @@ -10,7 +10,8 @@ import updateLocale from 'dayjs/plugin/updateLocale'; import sha3 from 'js-sha3'; import { validateAndParseAddress } from 'starknet'; import networks from '@/helpers/networks.json'; -import { Proposal, SpaceMetadata } from '@/types'; +import { VotingPowerItem } from '@/stores/votingPowers'; +import { Choice, Proposal, SpaceMetadata } from '@/types'; import { MAX_SYMBOL_LENGTH } from './constants'; import pkg from '@/../package.json'; import ICCoingecko from '~icons/c/coingecko'; @@ -475,21 +476,24 @@ export function getChoiceWeight( return isNaN(percent) ? 0 : percent; } -export function getChoiceText( - availableChoices: string[], - choice: number | number[] | Record -) { +export function getChoiceText(availableChoices: string[], choice: Choice) { + if (typeof choice === 'string') { + return ['for', 'against', 'abstain'].includes(choice) + ? choice.charAt(0).toUpperCase() + choice.slice(1) + : 'Invalid choice'; + } + if (typeof choice === 'number') { - return availableChoices[choice - 1]; + return availableChoices[choice - 1] || 'Invalid choice'; } if (Array.isArray(choice)) { - return ( - choice.map(index => availableChoices[index - 1]).join(', ') || - 'Blank vote' - ); + if (!choice.length) return 'Blank vote'; + return choice.map(index => availableChoices[index - 1]).join(', '); } + if (!Object.keys(choice).length) return 'Blank vote'; + const total = Object.values(choice).reduce((acc, weight) => acc + weight, 0); return Object.entries(choice) @@ -534,3 +538,12 @@ export function getSocialNetworksLink(data: any) { }) .filter(social => social.href); } + +export function getFormattedVotingPower(votingPower?: VotingPowerItem) { + if (!votingPower) return; + + const { totalVotingPower, decimals, symbol } = votingPower; + const value = _vp(Number(totalVotingPower) / 10 ** decimals); + + return symbol ? `${value} ${symbol}` : value; +} diff --git a/apps/ui/src/networks/evm/actions.ts b/apps/ui/src/networks/evm/actions.ts index 150b81e28..c0eca54f7 100644 --- a/apps/ui/src/networks/evm/actions.ts +++ b/apps/ui/src/networks/evm/actions.ts @@ -395,7 +395,8 @@ export function createActions( connectorType: Connector, account: string, proposal: Proposal, - choice: Choice + choice: Choice, + reason: string ) => { await verifyNetwork(web3, chainId); @@ -427,13 +428,16 @@ export function createActions( }) ); + let pinned: { cid: string; provider: string } | null = null; + if (reason) pinned = await helpers.pin({ reason }); + const data = { space: proposal.space.id, authenticator, strategies: strategiesWithMetadata, proposal: proposal.proposal_id as number, choice: getSdkChoice(choice), - metadataUri: '', + metadataUri: pinned ? `ipfs://${pinned.cid}` : '', chainId }; diff --git a/apps/ui/src/networks/offchain/actions.ts b/apps/ui/src/networks/offchain/actions.ts index 8fe0166df..c1106b80d 100644 --- a/apps/ui/src/networks/offchain/actions.ts +++ b/apps/ui/src/networks/offchain/actions.ts @@ -161,7 +161,8 @@ export function createActions( connectorType: Connector, account: string, proposal: Proposal, - choice: Choice + choice: Choice, + reason: string ): Promise { const data = { space: proposal.space.id, @@ -171,7 +172,8 @@ export function createActions( authenticator: '', strategies: [], metadataUri: '', - privacy: proposal.privacy + privacy: proposal.privacy, + reason }; return client.vote({ diff --git a/apps/ui/src/networks/starknet/actions.ts b/apps/ui/src/networks/starknet/actions.ts index 00366be0c..64f31e2e5 100644 --- a/apps/ui/src/networks/starknet/actions.ts +++ b/apps/ui/src/networks/starknet/actions.ts @@ -390,7 +390,8 @@ export function createActions( connectorType: Connector, account: string, proposal: Proposal, - choice: Choice + choice: Choice, + reason: string ) => { const isContract = await getIsContract(connectorType, account); @@ -424,12 +425,16 @@ export function createActions( }) ); + let pinned: { cid: string; provider: string } | null = null; + if (reason) pinned = await helpers.pin({ reason }); + const data = { space: proposal.space.id, authenticator, strategies: strategiesWithMetadata, proposal: proposal.proposal_id as number, - choice: getSdkChoice(choice) + choice: getSdkChoice(choice), + metadataUri: pinned ? `ipfs://${pinned.cid}` : '' }; if (relayerType === 'starknet') { diff --git a/apps/ui/src/networks/types.ts b/apps/ui/src/networks/types.ts index e1ba30346..e65f4cbb0 100644 --- a/apps/ui/src/networks/types.ts +++ b/apps/ui/src/networks/types.ts @@ -148,7 +148,8 @@ export type ReadOnlyNetworkActions = { connectorType: Connector, account: string, proposal: Proposal, - choice: Choice + choice: Choice, + reason: string ): Promise; followSpace( web3: Web3Provider | Wallet, diff --git a/apps/ui/src/stores/votingPowers.ts b/apps/ui/src/stores/votingPowers.ts new file mode 100644 index 000000000..6360cfc10 --- /dev/null +++ b/apps/ui/src/stores/votingPowers.ts @@ -0,0 +1,104 @@ +import { utils } from '@snapshot-labs/sx'; +import { defineStore } from 'pinia'; +import { getNetwork } from '@/networks'; +import { VotingPower, VotingPowerStatus } from '@/networks/types'; +import { Proposal, Space } from '@/types'; + +const LATEST_BLOCK_NAME = 'latest'; + +type SpaceDetails = Proposal['space']; +export type VotingPowerItem = { + votingPowers: VotingPower[]; + totalVotingPower: bigint; + status: VotingPowerStatus; + symbol: string; + decimals: number; + error: utils.errors.VotingPowerDetailsError | null; + canPropose: boolean; + canVote: boolean; +}; + +export function getIndex(space: SpaceDetails, block: number | null): string { + return `${space.id}:${block ?? LATEST_BLOCK_NAME}`; +} + +export const useVotingPowersStore = defineStore('votingPowers', () => { + const votingPowers = reactive>(new Map()); + + async function fetch( + item: Space | Proposal, + account: string, + block: number | null + ) { + const space = 'space' in item ? item.space : item; + + const existingVotingPower = votingPowers.get(getIndex(space, block)); + if (existingVotingPower && existingVotingPower.status === 'success') return; + + const network = getNetwork(item.network); + + let vpItem: VotingPowerItem = { + status: 'loading', + votingPowers: [], + totalVotingPower: 0n, + decimals: 18, + symbol: space.voting_power_symbol, + error: null, + canPropose: false, + canVote: false + }; + + if (existingVotingPower) { + existingVotingPower.status = 'loading'; + votingPowers.set(getIndex(space, block), existingVotingPower); + } + + try { + const vp = await network.actions.getVotingPower( + space.id, + item.strategies, + item.strategies_params, + space.strategies_parsed_metadata, + account, + { + at: block, + chainId: space.snapshot_chain_id + } + ); + + vpItem = { + ...vpItem, + votingPowers: vp, + totalVotingPower: vp.reduce((acc, b) => acc + b.value, 0n), + status: 'success', + decimals: Math.max(...vp.map(votingPower => votingPower.decimals), 0) + }; + + if ('proposal_threshold' in space) { + vpItem.canPropose = + vpItem.totalVotingPower >= BigInt(space.proposal_threshold); + } else { + vpItem.canVote = vpItem.totalVotingPower > 0n; + } + } catch (e: unknown) { + if (e instanceof utils.errors.VotingPowerDetailsError) { + vpItem.error = e; + } else { + console.warn('Failed to load voting power', e); + } + + vpItem.status = 'error'; + } + votingPowers.set(getIndex(space, block), vpItem); + } + + function reset() { + votingPowers.clear(); + } + + return { + votingPowers, + fetch, + reset + }; +}); diff --git a/apps/ui/src/views/Editor.vue b/apps/ui/src/views/Editor.vue index 8a134ed3b..bc1f35790 100644 --- a/apps/ui/src/views/Editor.vue +++ b/apps/ui/src/views/Editor.vue @@ -4,7 +4,7 @@ import { StrategyWithTreasury } from '@/composables/useTreasuries'; import { resolver } from '@/helpers/resolver'; import { omit } from '@/helpers/utils'; import { validateForm } from '@/helpers/validation'; -import { getNetwork, offchainNetworks, supportsNullCurrent } from '@/networks'; +import { getNetwork, offchainNetworks } from '@/networks'; import { Contact, Transaction, VoteType } from '@/types'; const MAX_BODY_LENGTH = { @@ -51,15 +51,13 @@ const { executionStrategy: walletConnectTransactionExecutionStrategy, reset } = useWalletConnectTransaction(); -const { getCurrent } = useMetaStore(); const spacesStore = useSpacesStore(); const proposalsStore = useProposalsStore(); +const { votingPower, fetch: fetchVotingPower } = useVotingPower(); const modalOpen = ref(false); const previewEnabled = ref(false); const sending = ref(false); -const fetchingVotingPower = ref(true); -const votingPowerValid = ref(false); const network = computed(() => networkId.value ? getNetwork(networkId.value) : null @@ -166,7 +164,7 @@ const canSubmit = computed(() => { if (Object.keys(formErrors.value).length > 0) return false; return web3.value.account - ? !fetchingVotingPower.value && votingPowerValid.value + ? votingPower.value?.canPropose : !web3.value.authLoading; }); @@ -254,35 +252,8 @@ function handleTransactionAccept() { reset(); } -async function getVotingPower() { - if (!space.value || !web3.value.account) return; - - fetchingVotingPower.value = true; - try { - const network = getNetwork(space.value.network); - - const votingPowers = await network.actions.getVotingPower( - space.value.id, - space.value.voting_power_validation_strategy_strategies, - space.value.voting_power_validation_strategy_strategies_params, - space.value.voting_power_validation_strategies_parsed_metadata, - web3.value.account, - { - at: supportsNullCurrent(space.value.network) - ? null - : getCurrent(space.value.network) || 0, - chainId: space.value.snapshot_chain_id - } - ); - - const currentVotingPower = votingPowers.reduce((a, b) => a + b.value, 0n); - votingPowerValid.value = - currentVotingPower >= BigInt(space.value.proposal_threshold); - } catch (e) { - console.warn('Failed to load voting power', e); - } finally { - fetchingVotingPower.value = false; - } +function handleFetchVotingPower() { + space.value && fetchVotingPower(space.value); } watch( @@ -294,7 +265,13 @@ watch( }, { immediate: true } ); -watch([space, () => web3.value.account], () => getVotingPower()); + +watch([space, () => web3.value.account], ([toSpace, toAccount]) => { + if (!toSpace || !proposal.value || !toAccount) return; + + handleFetchVotingPower(); +}); + watch(proposalData, () => { if (!proposal.value) return; @@ -380,7 +357,10 @@ export default defineComponent({ @@ -395,13 +375,14 @@ export default defineComponent({
- - You do not have enough voting power to create proposal in this space. - + :voting-power="votingPower" + action="propose" + @fetch-voting-power="handleFetchVotingPower" + /> + -import { utils } from '@snapshot-labs/sx'; -import { getCacheHash, getStampUrl, sanitizeUrl } from '@/helpers/utils'; -import { getNetwork, offchainNetworks } from '@/networks'; -import { VotingPower, VotingPowerStatus } from '@/networks/types'; +import { + getCacheHash, + getFormattedVotingPower, + getStampUrl, + sanitizeUrl +} from '@/helpers/utils'; +import { offchainNetworks } from '@/networks'; import { Choice } from '@/types'; const route = useRoute(); +const proposalsStore = useProposalsStore(); +const { + votingPower, + fetch: fetchVotingPower, + reset: resetVotingPower +} = useVotingPower(); const { setFavicon } = useFavicon(); const { param } = useRouteParser('space'); const { resolved, address: spaceAddress, networkId } = useResolve(param); const { setTitle } = useTitle(); -const proposalsStore = useProposalsStore(); const { web3 } = useWeb3(); -const { loadVotes, votes } = useAccount(); -const { vote } = useActions(); +const { modalAccountOpen } = useModal(); -const sendingType = ref(null); -const votingPowers = ref([] as VotingPower[]); -const votingPowerStatus = ref('loading'); -const votingPowerDetailsError = - ref(null); +const modalOpenVote = ref(false); +const selectedChoice = ref(null); +const { loadVotes, votes } = useAccount(); const editMode = ref(false); -const network = computed(() => - networkId.value ? getNetwork(networkId.value) : null -); const id = computed(() => route.params.id as string); const proposal = computed(() => { if (!resolved.value || !spaceAddress.value || !networkId.value) { @@ -60,50 +62,22 @@ const currentVote = computed( votes.value[`${proposal.value.network}:${proposal.value.id}`] ); -async function getVotingPower() { - if (!network.value) return; - - votingPowerDetailsError.value = null; - - if (!web3.value.account || !proposal.value) { - votingPowers.value = []; - votingPowerStatus.value = 'success'; +async function handleVoteClick(choice: Choice) { + if (!web3.value.account) { + modalAccountOpen.value = true; return; } - votingPowerStatus.value = 'loading'; - try { - votingPowers.value = await network.value.actions.getVotingPower( - proposal.value.space.id, - proposal.value.strategies, - proposal.value.strategies_params, - proposal.value.space.strategies_parsed_metadata, - web3.value.account, - { - at: proposal.value.state === 'pending' ? null : proposal.value.snapshot, - chainId: proposal.value.space.snapshot_chain_id - } - ); - votingPowerStatus.value = 'success'; - } catch (e: unknown) { - if (e instanceof utils.errors.VotingPowerDetailsError) { - votingPowerDetailsError.value = e; - } else { - console.warn('Failed to load voting power', e); - } - - votingPowers.value = []; - votingPowerStatus.value = 'error'; - } + selectedChoice.value = choice; + modalOpenVote.value = true; } -async function handleVoteClick(choice: Choice) { +async function handleVoteSubmitted() { if (!proposal.value) return; - sendingType.value = choice; + selectedChoice.value = null; try { - await vote(proposal.value, choice); // TODO: Quick fix only for offchain proposals, need a more complete solution for onchain proposals if (offchainNetworks.includes(proposal.value.network)) { proposalsStore.fetchProposal( @@ -114,12 +88,30 @@ async function handleVoteClick(choice: Choice) { await loadVotes(proposal.value.network, [proposal.value.space.id]); } } finally { - sendingType.value = null; editMode.value = false; } } -watch([() => web3.value.account, proposal], () => getVotingPower()); +function handleFetchVotingPower() { + if (!proposal.value) return; + + fetchVotingPower(proposal.value); +} + +watch( + [proposal, () => web3.value.account, () => web3.value.authLoading], + ([toProposal, toAccount, toAuthLoading], [, fromAccount]) => { + if (fromAccount && toAccount && fromAccount !== toAccount) { + resetVotingPower(); + } + + if (toAuthLoading || !toProposal || !toAccount) return; + + handleFetchVotingPower(); + }, + { immediate: true } +); + watch( [networkId, spaceAddress, id], async ([networkId, spaceAddress, id]) => { @@ -223,17 +215,16 @@ watchEffect(() => { v-if="web3.account && networkId && (!currentVote || editMode)" v-slot="props" :network-id="networkId" - :status="votingPowerStatus" - :voting-power-symbol="proposal.space.voting_power_symbol" - :voting-powers="votingPowers" + :voting-power="votingPower" class="mb-2 flex items-center" - @get-voting-power="getVotingPower" + @fetch-voting-power="handleFetchVotingPower" >
@@ -246,21 +237,23 @@ watchEffect(() => { + + +
diff --git a/apps/ui/src/views/Space/Proposals.vue b/apps/ui/src/views/Space/Proposals.vue index ea1be31f8..e10db8769 100644 --- a/apps/ui/src/views/Space/Proposals.vue +++ b/apps/ui/src/views/Space/Proposals.vue @@ -1,22 +1,19 @@