From bb47e8d53a21c1622c9470ad82b0ac07c65be491 Mon Sep 17 00:00:00 2001
From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com>
Date: Sat, 20 Jul 2024 12:29:36 +0900
Subject: [PATCH 01/30] feat: add reason when voting
---
.changeset/breezy-owls-clean.md | 5 +
.../src/components/IndicatorVotingPower.vue | 47 +++---
apps/ui/src/components/MessageVotingPower.vue | 47 ++++++
apps/ui/src/components/Modal/Vote.vue | 143 ++++++++++++++++++
apps/ui/src/components/Modal/VotingPower.vue | 37 ++---
.../src/components/ProposalVoteApproval.vue | 8 +-
apps/ui/src/components/ProposalVoteBasic.vue | 4 -
.../components/ProposalVoteRankedChoice.vue | 8 +-
.../components/ProposalVoteSingleChoice.vue | 4 +-
.../src/components/ProposalVoteWeighted.vue | 8 +-
apps/ui/src/components/ProposalsListItem.vue | 34 +++--
apps/ui/src/components/Ui/Button.vue | 19 ++-
apps/ui/src/composables/useActions.ts | 5 +-
apps/ui/src/composables/useVotingPower.ts | 52 +++++++
apps/ui/src/helpers/utils.ts | 31 +++-
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 | 92 +++++++++++
apps/ui/src/views/Editor.vue | 66 +++-----
apps/ui/src/views/Proposal.vue | 129 +++++++---------
apps/ui/src/views/Space/Proposals.vue | 59 +++-----
.../clients/offchain/ethereum-sig/index.ts | 2 +-
packages/sx.js/src/clients/offchain/types.ts | 1 +
.../clients/starknet/ethereum-sig/index.ts | 4 +-
.../clients/starknet/starknet-sig/index.ts | 4 +-
.../src/clients/starknet/starknet-tx/index.ts | 2 +-
packages/sx.js/src/types/index.ts | 1 +
.../__snapshots__/index.test.ts.snap | 1 +
.../starknet/ethereum-sig/index.test.ts | 1 +
.../starknet/ethereum-tx/index.test.ts | 3 +-
.../__snapshots__/index.test.ts.snap | 10 +-
.../starknet/starknet-sig/index.test.ts | 1 +
34 files changed, 587 insertions(+), 267 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 47d9fed84..01edd8b3e 100644
--- a/apps/ui/src/components/IndicatorVotingPower.vue
+++ b/apps/ui/src/components/IndicatorVotingPower.vue
@@ -1,38 +1,33 @@
+
+
+
+ There was an error fetching your voting power.
+
+ Retry
+
+
+
+ You do not have enough voting power to vote.
+
+
+ You do not have enough voting power to create proposal in this space.
+
+
diff --git a/apps/ui/src/components/Modal/Vote.vue b/apps/ui/src/components/Modal/Vote.vue
new file mode 100644
index 000000000..2f293a31a
--- /dev/null
+++ b/apps/ui/src/components/Modal/Vote.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+ Cast your vote
+
+
+
+
+
+ - Choice
+ -
+
+ No choice selected
+
+ - Voting power
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+ Confirm
+
+
+
+
+
diff --git a/apps/ui/src/components/Modal/VotingPower.vue b/apps/ui/src/components/Modal/VotingPower.vue
index 95f616914..f220375a5 100644
--- a/apps/ui/src/components/Modal/VotingPower.vue
+++ b/apps/ui/src/components/Modal/VotingPower.vue
@@ -1,4 +1,5 @@
@@ -33,15 +37,14 @@ const error = computed(() => props.votingPowerStatus === 'error');
Your voting power
-
-
- There was an error fetching your voting power.
-
- Retry
-
-
+
+
@@ -57,12 +60,12 @@ const error = computed(() => props.votingPowerStatus === 'error');
/>
{{
- _n(Number(strategy.value) / 10 ** finalDecimals, 'compact', {
+ _n(Number(strategy.value) / 10 ** votingPower.decimals, 'compact', {
maximumFractionDigits: 2,
formatDust: true
})
}}
- {{ votingPowerSymbol }}
+ {{ votingPower.symbol }}
diff --git a/apps/ui/src/components/ProposalVoteApproval.vue b/apps/ui/src/components/ProposalVoteApproval.vue
index c8bb54e3b..8ef901f34 100644
--- a/apps/ui/src/components/ProposalVoteApproval.vue
+++ b/apps/ui/src/components/ProposalVoteApproval.vue
@@ -2,7 +2,6 @@
import { Choice, Proposal } from '@/types';
defineProps<{
- sendingType: Choice | null;
proposal: Proposal;
}>();
@@ -37,12 +36,7 @@ function toggleSelectedChoice(choice: number) {
-
+
Vote
diff --git a/apps/ui/src/components/ProposalVoteBasic.vue b/apps/ui/src/components/ProposalVoteBasic.vue
index fece1d9e1..ea89d5a07 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 }
@@ -20,7 +19,6 @@ const emit = defineEmits<{
@@ -30,7 +28,6 @@ const emit = defineEmits<{
@@ -40,7 +37,6 @@ const emit = defineEmits<{
diff --git a/apps/ui/src/components/ProposalVoteRankedChoice.vue b/apps/ui/src/components/ProposalVoteRankedChoice.vue
index cd86aa2fb..716514990 100644
--- a/apps/ui/src/components/ProposalVoteRankedChoice.vue
+++ b/apps/ui/src/components/ProposalVoteRankedChoice.vue
@@ -3,7 +3,6 @@ import Draggable from 'vuedraggable';
import { Choice, Proposal } from '@/types';
const props = defineProps<{
- sendingType: Choice | null;
proposal: Proposal;
}>();
@@ -38,12 +37,7 @@ const selectedChoices = ref(props.proposal.choices.map((_, i) => i + 1
-
+
Vote
diff --git a/apps/ui/src/components/ProposalVoteSingleChoice.vue b/apps/ui/src/components/ProposalVoteSingleChoice.vue
index dc8f211ff..8ed122d7a 100644
--- a/apps/ui/src/components/ProposalVoteSingleChoice.vue
+++ b/apps/ui/src/components/ProposalVoteSingleChoice.vue
@@ -1,8 +1,7 @@
@@ -119,12 +119,7 @@ async function handleVoteClick(choice: Choice) {
-
+
@@ -134,6 +129,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..fe1451972 100644
--- a/apps/ui/src/components/Ui/Button.vue
+++ b/apps/ui/src/components/Ui/Button.vue
@@ -27,7 +27,7 @@ withDefaults(
-
diff --git a/apps/ui/src/composables/useActions.ts b/apps/ui/src/composables/useActions.ts
index acf3ba2ae..f2b03647a 100644
--- a/apps/ui/src/composables/useActions.ts
+++ b/apps/ui/src/composables/useActions.ts
@@ -210,7 +210,7 @@ export function useActions() {
await wrapPromise(space.network, network.actions.setMetadata(auth.web3, space, metadata));
}
- 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);
@@ -222,7 +222,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..7af8ac67f
--- /dev/null
+++ b/apps/ui/src/composables/useVotingPower.ts
@@ -0,0 +1,52 @@
+import type { Space, Proposal } from '@/types';
+
+export function useVotingPower() {
+ const { web3 } = useWeb3();
+ const votingPowersStore = useVotingPowersStore();
+
+ const item = ref();
+
+ const space = computed(() =>
+ item.value && 'space' in item.value ? (item.value?.space as Space) : item.value
+ );
+
+ const snapshot = computed(() =>
+ item.value && 'snapshot' in item.value ? item.value.snapshot : undefined
+ );
+
+ const votingPower = computed(
+ () => space.value && votingPowersStore.get(space.value, snapshot.value)
+ );
+
+ const hasVoteVp = computed(
+ () => (votingPower.value && votingPower.value.totalVotingPower > 0n) || false
+ );
+
+ const hasProposeVp = computed(
+ () =>
+ (votingPower.value &&
+ space.value &&
+ votingPower.value.totalVotingPower >= BigInt(space.value.proposal_threshold)) ||
+ false
+ );
+
+ function reset() {
+ votingPowersStore.reset();
+ }
+
+ function fetch(spaceOrProposal: Space | Proposal) {
+ if (!web3.value.account) return;
+
+ item.value = spaceOrProposal;
+ votingPowersStore.fetch(item.value, web3.value.account, snapshot.value);
+ }
+
+ watch(
+ () => web3.value.account,
+ account => {
+ if (!account) reset();
+ }
+ );
+
+ return { votingPower, hasVoteVp, hasProposeVp, fetch, reset };
+}
diff --git a/apps/ui/src/helpers/utils.ts b/apps/ui/src/helpers/utils.ts
index ab170c655..b0ca1bd95 100644
--- a/apps/ui/src/helpers/utils.ts
+++ b/apps/ui/src/helpers/utils.ts
@@ -11,7 +11,7 @@ import { upload as pin } from '@snapshot-labs/pineapple';
import networks from '@/helpers/networks.json';
import pkg from '@/../package.json';
import type { Web3Provider } from '@ethersproject/providers';
-import type { Proposal, SpaceMetadata } from '@/types';
+import type { Choice, Proposal, SpaceMetadata } from '@/types';
import { MAX_SYMBOL_LENGTH } from './constants';
import ICX from '~icons/c/x';
import ICDiscord from '~icons/c/discord';
@@ -436,18 +436,24 @@ export function getChoiceWeight(selectedChoices: Record, index:
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)) {
+ 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)
@@ -484,3 +490,16 @@ export function getSocialNetworksLink(data: any) {
})
.filter(social => social.href);
}
+
+export function getFormattedVotingPower(votingPower?: {
+ totalVotingPower: bigint;
+ decimals: number;
+ symbol: string;
+}) {
+ 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 59ba5dde8..d3f98db46 100644
--- a/apps/ui/src/networks/evm/actions.ts
+++ b/apps/ui/src/networks/evm/actions.ts
@@ -347,7 +347,8 @@ export function createActions(
connectorType: Connector,
account: string,
proposal: Proposal,
- choice: Choice
+ choice: Choice,
+ reason: string
) => {
await verifyNetwork(web3, chainId);
@@ -376,13 +377,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 c51a0553c..d2778402e 100644
--- a/apps/ui/src/networks/offchain/actions.ts
+++ b/apps/ui/src/networks/offchain/actions.ts
@@ -139,7 +139,8 @@ export function createActions(
connectorType: Connector,
account: string,
proposal: Proposal,
- choice: Choice
+ choice: Choice,
+ reason: string
): Promise {
const data = {
space: proposal.space.id,
@@ -149,7 +150,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 9d58b400d..9530c2ed8 100644
--- a/apps/ui/src/networks/starknet/actions.ts
+++ b/apps/ui/src/networks/starknet/actions.ts
@@ -334,7 +334,8 @@ export function createActions(
connectorType: Connector,
account: string,
proposal: Proposal,
- choice: Choice
+ choice: Choice,
+ reason: string
) => {
const isContract = await getIsContract(connectorType, account);
@@ -365,12 +366,16 @@ export function createActions(
})
);
+ let pinned: { cid: string; provider: string } | null = null;
+ 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 f4abe9186..30a5229cf 100644
--- a/apps/ui/src/networks/types.ts
+++ b/apps/ui/src/networks/types.ts
@@ -127,7 +127,8 @@ export type ReadOnlyNetworkActions = {
connectorType: Connector,
account: string,
proposal: Proposal,
- choice: Choice
+ choice: Choice,
+ metadataCid?: string
): Promise;
followSpace(web3: Web3Provider | Wallet, networkId: NetworkID, spaceId: string, from?: string);
unfollowSpace(web3: Web3Provider | Wallet, networkId: NetworkID, spaceId: string, from?: string);
diff --git a/apps/ui/src/stores/votingPowers.ts b/apps/ui/src/stores/votingPowers.ts
new file mode 100644
index 000000000..d27ea72de
--- /dev/null
+++ b/apps/ui/src/stores/votingPowers.ts
@@ -0,0 +1,92 @@
+import { defineStore } from 'pinia';
+import { utils } from '@snapshot-labs/sx';
+import { getNetwork, supportsNullCurrent } from '@/networks';
+import type { Proposal, Space } from '@/types';
+import type { VotingPower, VotingPowerStatus } from '@/networks/types';
+
+type VotingPowerItem = {
+ votingPowers: VotingPower[];
+ totalVotingPower: bigint;
+ status: VotingPowerStatus;
+ symbol: string;
+ decimals: number;
+ error: utils.errors.VotingPowerDetailsError | null;
+};
+
+export const useVotingPowersStore = defineStore('votingPowers', () => {
+ const { getCurrent } = useMetaStore();
+
+ const votingPowers = reactive