Skip to content

Commit

Permalink
feat: add support for offchain proposal edition (#85)
Browse files Browse the repository at this point in the history
* feat: add support for offchain basic proposal creation

* fix: set voting delay and period when available

* fix: get discussion from payload

* chore: remove unused variable/import

* refactor: extract value to constant

* fix: get correct block number on proposal creation

* fix: add support for only members proposal validation

* fix: fetch voting power for proposal creation

* refactor: improve readability

* refactor: improve code

* fix: remove debug value

* fix: fix timestamp mismatch when using voting delay/period

* chore: fix import order

* fix: address comparison should not depend on case

* fix: add try/catch to fetch call

* feat: add vote type selector to proposal editor

* fix: show choices selector

* fix: add choices customization

* fix: fix botched merge

* fix: add choices customization

* fix: add support for choices reordering

* refactor: extract vote types/choices to its own component

* fix(ui): implement spacing from design system

* fix(ui): show error message on empty choices

* fix: validate choices through JSON validation

* fix(ui): add support for dark theme

* refactor: code cleanup

* chore: code cleanup

* refactor: refactoring

* refactor: remove unused value

* refactor: rename constant to reflect its type

* Update apps/ui/src/components/EditorVote.vue

Co-authored-by: Wiktor Tkaczyński <[email protected]>

* fix: display voting type/choices depending on network support

* fix(ui): fix for small screen

* fix: fix missing item-key for draggable component

* refactor:

* chore: fix strictMode warning

* fix(ui): UI fixes to conform to figma

* fix(ui): remove uneeded import

* fix(ui): reset choices when changing vote type

* refactor: split EditorVote type into 2

* refactor: extract to UiSelector component

* fix(ui): increase border radius

* fix(ui): default to 3 empty choices for other voting type

* fix(ui): use same placeholder for errored input

* fix(ui): default to 2 empty choices

* fix(ui): do not show error for empty choices

* fix(ui): add illustration image for voting type

* fix(ui): add animation to draggable

* fix(ux): add validation to choices

* fix(ui): keep same styling as existing form imput

* feat: add support for offchain proposal edition

* fix: do not show choices form when not relevant

* fix(ui): do not show drag icon for basic choices

* chore: update changeset

* fix: move display condtion inside Editor

* chore: remove unused import

* fix: fix typing error

---------

Co-authored-by: Wiktor Tkaczyński <[email protected]>
  • Loading branch information
wa0x6e and Sekhmet authored Mar 1, 2024
1 parent 4761c33 commit 0cef521
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 52 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-beds-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@snapshot-labs/sx": patch
---

add proposal edition support to OffchainEthereumSig
8 changes: 1 addition & 7 deletions apps/ui/src/components/EditorChoices.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,7 @@ defineProps<{
<template #item="{ index }">
<div>
<div class="flex items-center rounded-lg bg-skin-border h-[40px] gap-[12px] pl-2.5">
<div
class="text-skin-text"
:class="{
'handle cursor-grab': proposal.type !== 'basic',
'cursor-not-allowed': proposal.type === 'basic'
}"
>
<div v-if="proposal.type !== 'basic'" class="text-skin-text handle cursor-grab">
<IC-drag />
</div>
<div class="grow">
Expand Down
44 changes: 21 additions & 23 deletions apps/ui/src/components/EditorVotingType.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,29 +51,27 @@ function handleVoteTypeSelected(type: VoteType) {
</script>

<template>
<template v-if="votingTypes.length > 1 || votingTypes[0] !== 'basic'">
<div class="s-base mb-5">
<h4 class="eyebrow mb-2.5">Voting type</h4>
<div class="flex flex-col gap-[12px]">
<UiSelector
v-for="(type, index) in votingTypes"
:key="index"
:is-active="proposal.type === type"
@click="handleVoteTypeSelected(type as VoteType)"
>
<img
:src="VOTING_TYPES_INFO[type].image"
:alt="VOTING_TYPES_INFO[type].label"
class="w-[122px] hidden sm:block shrink-0"
/>
<div class="grow">
<span class="text-skin-heading">{{ VOTING_TYPES_INFO[type].label }}</span>
<div>
{{ VOTING_TYPES_INFO[type].description }}
</div>
<div class="s-base mb-5">
<h4 class="eyebrow mb-2.5">Voting type</h4>
<div class="flex flex-col gap-[12px]">
<UiSelector
v-for="(type, index) in votingTypes"
:key="index"
:is-active="proposal.type === type"
@click="handleVoteTypeSelected(type as VoteType)"
>
<img
:src="VOTING_TYPES_INFO[type].image"
:alt="VOTING_TYPES_INFO[type].label"
class="w-[122px] hidden sm:block shrink-0"
/>
<div class="grow">
<span class="text-skin-heading">{{ VOTING_TYPES_INFO[type].label }}</span>
<div>
{{ VOTING_TYPES_INFO[type].description }}
</div>
</UiSelector>
</div>
</div>
</UiSelector>
</div>
</template>
</div>
</template>
1 change: 0 additions & 1 deletion apps/ui/src/components/FormProfile.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script setup lang="ts">
import { MAX_SYMBOL_LENGTH } from '@/helpers/constants';
import { validateForm } from '@/helpers/validation';
import { getNetwork } from '@/networks';
import { SpaceMetadataTreasury, SpaceMetadataDelegation } from '@/types';
const props = withDefaults(
Expand Down
6 changes: 5 additions & 1 deletion apps/ui/src/composables/useActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ export function useActions() {
title: string,
body: string,
discussion: string,
type: VoteType,
choices: string[],
executionStrategy: string | null,
execution: Transaction[]
) {
Expand All @@ -278,7 +280,7 @@ export function useActions() {
return false;
}

const network = getReadWriteNetwork(space.network);
const network = getNetwork(space.network);

const transactions = execution.map((tx: Transaction) => ({
...tx,
Expand All @@ -289,6 +291,8 @@ export function useActions() {
title,
body,
discussion,
type,
choices: choices.filter(c => !!c),
execution: transactions
});
if (!pinned || !pinned.cid) return false;
Expand Down
4 changes: 3 additions & 1 deletion apps/ui/src/helpers/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { VoteType } from '@/types';

export const ETH_CONTRACT = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';

export const CHAIN_IDS = {
Expand All @@ -23,4 +25,4 @@ export const COINGECKO_BASE_ASSETS = {

export const MAX_SYMBOL_LENGTH = 12;
export const BASIC_CHOICES = ['For', 'Against', 'Abstain'];
export const SUPPORTED_VOTING_TYPES = ['basic', 'single-choice', 'approval'];
export const SUPPORTED_VOTING_TYPES: VoteType[] = ['basic', 'single-choice', 'approval'] as const;
36 changes: 36 additions & 0 deletions apps/ui/src/networks/offchain/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,42 @@ export function createActions(

return client.propose({ signer: web3.getSigner(), data });
},
async updateProposal(
web3: Web3Provider,
connectorType: Connector,
account: string,
space: Space,
proposalId: number | string,
cid: string
) {
let payload: {
title: string;
body: string;
discussion: string;
type: VoteType;
choices: string[];
};

try {
const res = await fetch(getUrl(cid) as string);
payload = await res.json();
} catch (e) {
throw new Error('Failed to fetch proposal metadata');
}

const data = {
proposal: proposalId as string,
space: space.id,
title: payload.title,
body: payload.body,
type: payload.type,
discussion: payload.discussion,
choices: payload.choices,
plugins: '{}'
};

return client.updateProposal({ signer: web3.getSigner(), data });
},
vote(
web3: Web3Provider,
connectorType: Connector,
Expand Down
20 changes: 10 additions & 10 deletions apps/ui/src/networks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ export type ReadOnlyNetworkActions = {
executionStrategy: string | null,
transactions: MetaTransaction[]
): Promise<any>;
updateProposal(
web3: Web3Provider,
connectorType: Connector,
account: string,
space: Space,
proposalId: number | string,
cid: string,
executionStrategy: string | null,
transactions: MetaTransaction[]
): Promise<any>;
vote(
web3: Web3Provider,
connectorType: Connector,
Expand Down Expand Up @@ -134,16 +144,6 @@ export type NetworkActions = ReadOnlyNetworkActions & {
}
);
setMetadata(web3: Web3Provider, space: Space, metadata: SpaceMetadata);
updateProposal(
web3: Web3Provider,
connectorType: Connector,
account: string,
space: Space,
proposalId: number | string,
cid: string,
executionStrategy: string | null,
transactions: MetaTransaction[]
);
cancelProposal(web3: Web3Provider, proposal: Proposal);
finalizeProposal(web3: Web3Provider, proposal: Proposal);
receiveProposal(web3: Web3Provider, proposal: Proposal);
Expand Down
10 changes: 7 additions & 3 deletions apps/ui/src/views/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getNetwork, supportsNullCurrent } from '@/networks';
import { compareAddresses, omit } from '@/helpers/utils';
import { CHAIN_IDS } from '@/helpers/constants';
import { validateForm } from '@/helpers/validation';
import { RequiredProperty, SelectedStrategy, VoteType, SpaceMetadataTreasury } from '@/types';
import { RequiredProperty, SelectedStrategy, SpaceMetadataTreasury } from '@/types';
type StrategyWithTreasury = SelectedStrategy & {
treasury: RequiredProperty<SpaceMetadataTreasury>;
Expand Down Expand Up @@ -181,6 +181,8 @@ async function handleProposeClick() {
proposal.value.title,
proposal.value.body,
proposal.value.discussion,
proposal.value.type,
proposal.value.choices,
proposal.value.executionStrategy?.address ?? null,
proposal.value.executionStrategy?.address ? proposal.value.execution : []
);
Expand Down Expand Up @@ -371,8 +373,10 @@ export default defineComponent({
/>
<UiLinkPreview :key="proposalKey || ''" :url="proposal.discussion" />
</div>
<EditorVotingType v-model="proposal" :voting-types="votingTypes as VoteType[]" />
<EditorChoices v-model="proposal" :definition="CHOICES_DEFINITION" />
<template v-if="votingTypes && (votingTypes.length > 1 || votingTypes[0] !== 'basic')">
<EditorVotingType v-model="proposal" :voting-types="votingTypes" />
<EditorChoices v-model="proposal" :definition="CHOICES_DEFINITION" />
</template>
<div
v-if="
space &&
Expand Down
2 changes: 2 additions & 0 deletions apps/ui/src/views/Proposal/Overview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ async function handleEditClick() {
title: props.proposal.title,
body: props.proposal.body,
discussion: props.proposal.discussion,
type: props.proposal.type,
choices: props.proposal.choices,
executionStrategy:
props.proposal.execution_strategy_type === 'none'
? null
Expand Down
26 changes: 22 additions & 4 deletions packages/sx.js/src/clients/offchain/ethereum-sig/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ import {
proposeTypes,
basicVoteTypes,
singleChoiceVoteTypes,
approvalVoteTypes
approvalVoteTypes,
updateProposalTypes
} from './types';
import type { Signer, TypedDataSigner, TypedDataField } from '@ethersproject/abstract-signer';
import type {
Vote,
Propose,
UpdateProposal,
Envelope,
SignatureData,
EIP712VoteMessage,
EIP712Message,
EIP712ProposeMessage
EIP712ProposeMessage,
EIP712UpdateProposal
} from '../types';
import type { OffchainNetworkConfig } from '../../../types';

Expand All @@ -37,7 +40,7 @@ export class EthereumSig {
this.sequencerUrl = opts?.sequencerUrl || SEQUENCER_URLS[this.networkConfig.eip712ChainId];
}

public async sign<T extends EIP712VoteMessage | EIP712ProposeMessage>(
public async sign<T extends EIP712VoteMessage | EIP712ProposeMessage | EIP712UpdateProposal>(
signer: Signer & TypedDataSigner,
message: T,
types: Record<string, TypedDataField[]>
Expand All @@ -60,7 +63,7 @@ export class EthereumSig {
};
}

public async send(envelope: Envelope<Vote>) {
public async send(envelope: Envelope<Vote | Propose | UpdateProposal>) {
const { address, signature: sig, domain, types, message } = envelope.signatureData!;
const payload = {
address,
Expand Down Expand Up @@ -108,6 +111,21 @@ export class EthereumSig {
};
}

public async updateProposal({
signer,
data
}: {
signer: Signer & TypedDataSigner;
data: UpdateProposal;
}): Promise<Envelope<UpdateProposal>> {
const signatureData = await this.sign(signer, data, updateProposalTypes);

return {
signatureData,
data
};
}

public async vote({
signer,
data
Expand Down
15 changes: 15 additions & 0 deletions packages/sx.js/src/clients/offchain/ethereum-sig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,18 @@ export const proposeTypes = {
{ name: 'app', type: 'string' }
]
};

export const updateProposalTypes = {
UpdateProposal: [
{ name: 'proposal', type: 'string' },
{ name: 'from', type: 'address' },
{ name: 'space', type: 'string' },
{ name: 'timestamp', type: 'uint64' },
{ name: 'type', type: 'string' },
{ name: 'title', type: 'string' },
{ name: 'body', type: 'string' },
{ name: 'discussion', type: 'string' },
{ name: 'choices', type: 'string[]' },
{ name: 'plugins', type: 'string' }
]
};
30 changes: 28 additions & 2 deletions packages/sx.js/src/clients/offchain/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type SignatureData = {
message: Record<string, any>;
};

export type Envelope<T extends Vote | Propose> = {
export type Envelope<T extends Vote | Propose | UpdateProposal> = {
signatureData?: SignatureData;
data: T;
};
Expand Down Expand Up @@ -48,7 +48,22 @@ export type EIP712ProposeMessage = {
from?: string;
};

export type EIP712Message = Required<EIP712VoteMessage | EIP712ProposeMessage>;
export type EIP712UpdateProposal = {
proposal: string;
space: string;
type: string;
title: string;
body: string;
discussion: string;
choices: string[];
plugins: string;
timestamp?: number;
from?: string;
};

export type EIP712Message = Required<
EIP712VoteMessage | EIP712ProposeMessage | EIP712UpdateProposal
>;

export type Vote = {
space: string;
Expand All @@ -75,3 +90,14 @@ export type Propose = {
app: string;
timestamp?: number;
};

export type UpdateProposal = {
proposal: string;
space: string;
type: string;
title: string;
body: string;
discussion: string;
choices: string[];
plugins: string;
};

0 comments on commit 0cef521

Please sign in to comment.