Skip to content

Commit

Permalink
feat: add single choice and approval voting type (#46)
Browse files Browse the repository at this point in the history
* fix: show voting buttons for offchain proposals

* refactor: move the network actions to actions.ts

* feat: add support for basic voting for offchain proposals

* fix: fix invalid import type

* fix: use correct sequencer network depending on chainId

* chore: add tests

* Update packages/sx.js/src/clients/offchain/ethereum-sig/index.ts

Co-authored-by: Wiktor Tkaczyński <wiktor.tkaczynski@gmail.com>

* refactor: use same function order definition as other networks

* fix: handle offchain receipt without tx_hash

* fix: fix typing

* fix: throw error on sequencer error response

* chore: add unit test

* chore: fix tests

* fix: show voting form only for supported voting type

* chore: remove cumbersome describe block

* fix: switch to correct chain for offchain actions

* fix: avoid changing type

* chore: improve test

* fix: revert network enforcer

* fix: remove unused import

* refactor: rename voteType

* refactor: rename type

* feat: add support for single choice voting

* fix: remove redundant tooltip

* feat: add support for approval voting

* chore: fix tests

* refactor: extract list to constant

* chore: update changeset

* fix: UI fix

* Update .changeset/chilly-avocados-teach.md

Co-authored-by: Wiktor Tkaczyński <wiktor.tkaczynski@gmail.com>

* Update apps/ui/src/networks/offchain/helpers.ts

Co-authored-by: Wiktor Tkaczyński <wiktor.tkaczynski@gmail.com>

* fix: fix missing parenthesis

* fix: fix botched merged

* refactor: extract vote form into their own component

* refactor: add more type to Choice

* fix(ui): fix glitched focus ring

* fix(ui): remove unused class

* fix(ui): DRY class

* fix: fix Choice type

* Update packages/sx.js/src/clients/starknet/starknet-tx/index.ts

Co-authored-by: Wiktor Tkaczyński <wiktor.tkaczynski@gmail.com>

* refactor: rename events name

* refactor: improve event naming

* fix: tentative type fix

---------

Co-authored-by: Wiktor Tkaczyński <wiktor.tkaczynski@gmail.com>
  • Loading branch information
wa0x6e and Sekhmet authored Feb 21, 2024
1 parent c76f035 commit ff6f988
Show file tree
Hide file tree
Showing 17 changed files with 210 additions and 76 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilly-avocados-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@snapshot-labs/sx": patch
---

add approval and single choice vote support to OffchainEthereumSig
11 changes: 9 additions & 2 deletions apps/ui/src/components/ProposalVote.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { SUPPORTED_VOTING_TYPES } from '@/helpers/constants';
import { _t } from '@/helpers/utils';
import { getNetwork } from '@/networks';
import { Proposal as ProposalType } from '@/types';
Expand All @@ -21,7 +22,11 @@ const isSupported = computed(() => {
network.helpers.isStrategySupported(strategy)
);
return hasSupportedAuthenticator && hasSupportedStrategies && props.proposal.type === 'basic';
return (
hasSupportedAuthenticator &&
hasSupportedStrategies &&
SUPPORTED_VOTING_TYPES.includes(props.proposal.type)
);
});
</script>

Expand Down Expand Up @@ -50,5 +55,7 @@ const isSupported = computed(() => {
<slot v-else-if="!isSupported" name="unsupported">
Voting for this proposal is not supported
</slot>
<slot v-else></slot>
<div v-else class="py-2">
<slot />
</div>
</template>
49 changes: 49 additions & 0 deletions apps/ui/src/components/ProposalVoteApproval.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script setup lang="ts">
import { Choice, Proposal } from '@/types';
defineProps<{
sendingType: Choice | null;
proposal: Proposal;
}>();
const emit = defineEmits<{
(e: 'vote', value: Choice);
}>();
const selectedChoices = ref<number[]>([]);
function toggleSelectedChoice(choice: number) {
if (selectedChoices.value.includes(choice)) {
selectedChoices.value = selectedChoices.value.filter(c => c !== choice);
} else {
selectedChoices.value = [...selectedChoices.value, choice];
}
}
</script>

<template>
<div class="flex flex-col space-y-3">
<div class="flex flex-col space-y-2">
<UiButton
v-for="(choice, index) in proposal.choices"
:key="index"
class="!h-[48px] text-left w-full flex items-center"
:class="{ 'border-skin-text': selectedChoices.includes(index + 1) }"
@click="toggleSelectedChoice(index + 1)"
>
<div class="grow truncate">
{{ choice }}
</div>
<IH-check v-if="selectedChoices.includes(index + 1)" class="shrink-0" />
</UiButton>
</div>
<UiButton
primary
class="!h-[48px] w-full"
:loading="!!sendingType"
@click="emit('vote', selectedChoices)"
>
Vote
</UiButton>
</div>
</template>
50 changes: 50 additions & 0 deletions apps/ui/src/components/ProposalVoteBasic.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script setup lang="ts">
import { Choice } from '@/types';
withDefaults(
defineProps<{
sendingType: Choice | null;
size: number;
}>(),
{ size: 48 }
);
const emit = defineEmits<{
(e: 'vote', value: Choice);
}>();
</script>

<template>
<div class="flex space-x-2">
<UiTooltip title="For">
<UiButton
class="!text-skin-success !border-skin-success !px-0"
:class="{ '!w-[48px] !h-[48px]': size === 48, '!w-[40px] !h-[40px]': size === 40 }"
:loading="sendingType === 'for'"
@click="emit('vote', 'for')"
>
<IH-check class="inline-block" />
</UiButton>
</UiTooltip>
<UiTooltip title="Against">
<UiButton
class="!text-skin-danger !border-skin-danger !px-0"
:class="{ '!w-[48px] !h-[48px]': size === 48, '!w-[40px] !h-[40px]': size === 40 }"
:loading="sendingType === 'against'"
@click="emit('vote', 'against')"
>
<IH-x class="inline-block" />
</UiButton>
</UiTooltip>
<UiTooltip title="Abstain">
<UiButton
class="!text-gray-500 !border-gray-500 !px-0"
:class="{ '!w-[48px] !h-[48px]': size === 48, '!w-[40px] !h-[40px]': size === 40 }"
:loading="sendingType === 'abstain'"
@click="emit('vote', 'abstain')"
>
<IH-minus-sm class="inline-block" />
</UiButton>
</UiTooltip>
</div>
</template>
26 changes: 26 additions & 0 deletions apps/ui/src/components/ProposalVoteSingleChoice.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script setup lang="ts">
import { Choice, Proposal } from '@/types';
defineProps<{
sendingType: Choice | null;
proposal: Proposal;
}>();
const emit = defineEmits<{
(e: 'vote', value: number);
}>();
</script>

<template>
<div class="flex flex-col space-y-2">
<UiButton
v-for="(choice, index) in proposal.choices"
:key="index"
class="!h-[48px] text-left w-full truncate"
:loading="sendingType === index + 1"
@click="emit('vote', index + 1)"
>
{{ choice }}
</UiButton>
</div>
</template>
35 changes: 6 additions & 29 deletions apps/ui/src/components/ProposalsListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,35 +80,12 @@ async function handleVoteClick(choice: Choice) {
<ProposalResults v-if="proposal.type === 'basic'" :proposal="proposal" />
<div v-else />
</template>
<div class="flex space-x-2 py-2">
<UiTooltip title="For">
<UiButton
class="w-[40px] !text-skin-success !border-skin-success !h-[40px] !px-0"
:loading="sendingType === 'for'"
@click="handleVoteClick('for')"
>
<IH-check class="inline-block" />
</UiButton>
</UiTooltip>
<UiTooltip title="Against">
<UiButton
class="w-[40px] !text-skin-danger !border-skin-danger !h-[40px] !px-0"
:loading="sendingType === 'against'"
@click="handleVoteClick('against')"
>
<IH-x class="inline-block" />
</UiButton>
</UiTooltip>
<UiTooltip title="Abstain">
<UiButton
class="w-[40px] !text-gray-500 !border-gray-500 !h-[40px] !px-0"
:loading="sendingType === 'abstain'"
@click="handleVoteClick('abstain')"
>
<IH-minus-sm class="inline-block" />
</UiButton>
</UiTooltip>
</div>
<ProposalVoteBasic
v-if="proposal.type === 'basic'"
:sending-type="sendingType"
:size="40"
@vote="handleVoteClick"
/>
</ProposalVote>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions apps/ui/src/helpers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export const COINGECKO_BASE_ASSETS = {

export const MAX_SYMBOL_LENGTH = 12;
export const CHOICES = ['For', 'Against', 'Abstain'];
export const SUPPORTED_VOTING_TYPES = ['basic', 'single-choice', 'approval'];
3 changes: 2 additions & 1 deletion apps/ui/src/networks/offchain/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export function createActions(
const data = {
space: proposal.space.id,
proposal: proposal.proposal_id as string,
choice: getSdkChoice(choice),
type: proposal.type,
choice: getSdkChoice(proposal.type, choice),
authenticator: '',
strategies: [],
metadataUri: ''
Expand Down
20 changes: 16 additions & 4 deletions apps/ui/src/networks/offchain/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { Choice } from '@/types';

export function getSdkChoice(choice: Choice): number {
if (choice === 'for') return 1;
if (choice === 'against') return 2;
return 3;
export function getSdkChoice(type: string, choice: Choice): number | number[] {
if (type === 'basic') {
if (choice === 'for') return 1;
if (choice === 'against') return 2;
return 3;
}

if (type === 'single-choice') {
return choice as number;
}

if (type === 'approval') {
return choice as number[];
}

throw new Error('Vote type not supported');
}
2 changes: 1 addition & 1 deletion apps/ui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type NetworkID =
| 'matic'
| 'arb1';

export type Choice = 'for' | 'against' | 'abstain';
export type Choice = 'for' | 'against' | 'abstain' | number | number[];

export type SelectedStrategy = {
address: string;
Expand Down
46 changes: 17 additions & 29 deletions apps/ui/src/views/Proposal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -162,35 +162,23 @@ watchEffect(() => {

<h4 class="block eyebrow mb-2 mt-4 first:mt-1">Cast your vote</h4>
<ProposalVote v-if="proposal" :proposal="proposal">
<div class="flex space-x-2 py-2">
<UiTooltip title="For">
<UiButton
class="!text-skin-success !border-skin-success !w-[48px] !h-[48px] !px-0"
:loading="sendingType === 'for'"
@click="handleVoteClick('for')"
>
<IH-check class="inline-block" />
</UiButton>
</UiTooltip>
<UiTooltip title="Against">
<UiButton
class="!text-skin-danger !border-skin-danger !w-[48px] !h-[48px] !px-0"
:loading="sendingType === 'against'"
@click="handleVoteClick('against')"
>
<IH-x class="inline-block" />
</UiButton>
</UiTooltip>
<UiTooltip title="Abstain">
<UiButton
class="!text-gray-500 !border-gray-500 !w-[48px] !h-[48px] !px-0"
:loading="sendingType === 'abstain'"
@click="handleVoteClick('abstain')"
>
<IH-minus-sm class="inline-block" />
</UiButton>
</UiTooltip>
</div>
<ProposalVoteBasic
v-if="proposal.type === 'basic'"
:sending-type="sendingType"
@vote="handleVoteClick"
/>
<ProposalVoteSingleChoice
v-else-if="proposal.type === 'single-choice'"
:proposal="proposal"
:sending-type="sendingType"
@vote="handleVoteClick"
/>
<ProposalVoteApproval
v-else-if="proposal.type === 'approval'"
:proposal="proposal"
:sending-type="sendingType"
@vote="handleVoteClick"
/>
</ProposalVote>
</template>

Expand Down
9 changes: 7 additions & 2 deletions packages/sx.js/src/clients/offchain/ethereum-sig/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { offchainGoerli } from '../../../offchainNetworks';
import { domain, voteTypes } from './types';
import { domain, SingleChoiceVoteTypes, MultipleChoiceVoteTypes } from './types';
import type { Signer, TypedDataSigner, TypedDataField } from '@ethersproject/abstract-signer';
import type { Vote, Envelope, SignatureData, EIP712VoteMessage, EIP712Message } from '../types';
import type { OffchainNetworkConfig } from '../../../types';
Expand Down Expand Up @@ -93,7 +93,12 @@ export class EthereumSig {
metadata: ''
};

const signatureData = await this.sign(signer, message, voteTypes);
let voteType = SingleChoiceVoteTypes;
if (data.type === 'approval') {
voteType = MultipleChoiceVoteTypes;
}

const signatureData = await this.sign(signer, message, voteType);

return {
signatureData,
Expand Down
15 changes: 14 additions & 1 deletion packages/sx.js/src/clients/offchain/ethereum-sig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const domain = {
version: '0.1.4'
};

export const voteTypes = {
export const SingleChoiceVoteTypes = {
Vote: [
{ name: 'from', type: 'address' },
{ name: 'space', type: 'string' },
Expand All @@ -15,3 +15,16 @@ export const voteTypes = {
{ name: 'metadata', type: 'string' }
]
};

export const MultipleChoiceVoteTypes = {
Vote: [
{ name: 'from', type: 'address' },
{ name: 'space', type: 'string' },
{ name: 'timestamp', type: 'uint64' },
{ name: 'proposal', type: 'string' },
{ name: 'choice', type: 'uint32[]' },
{ name: 'reason', type: 'string' },
{ name: 'app', type: 'string' },
{ name: 'metadata', type: 'string' }
]
};
7 changes: 2 additions & 5 deletions packages/sx.js/src/clients/offchain/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import type { TypedDataDomain, TypedDataField } from '@ethersproject/abstract-signer';

enum Choice {
Against = 2,
For = 1,
Abstain = 3
}
type Choice = number | number[];

export type SignatureData = {
address: string;
Expand Down Expand Up @@ -43,4 +39,5 @@ export type Vote = {
proposal: string;
choice: Choice;
metadataUri: string;
type: string;
};
3 changes: 2 additions & 1 deletion packages/sx.js/test/integration/offchain/fixtures/vote.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"strategies": [],
"proposal": "0xcc47146e5e0ac781e8976405a8da4468f2a0c4cdf0c7659353d728fafe46f801",
"choice": 1,
"metadataUri": ""
"metadataUri": "",
"type": "basic"
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ exports[`EthereumSig > should create vote envelope 1`] = `
"proposal": "0xcc47146e5e0ac781e8976405a8da4468f2a0c4cdf0c7659353d728fafe46f801",
"space": "wan-test.eth",
"strategies": [],
"type": "basic",
},
"signatureData": {
"address": "0xf1f09AdC06aAB740AA16004D62Dbd89484d3Be90",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ describe('EthereumSig', () => {
strategies: [],
proposal: '0xcc47146e5e0ac781e8976405a8da4468f2a0c4cdf0c7659353d728fafe46f801',
choice: 1,
metadataUri: ''
metadataUri: '',
type: 'basic'
};

const envelope = await client.vote({
Expand Down

0 comments on commit ff6f988

Please sign in to comment.