From 4c3da35adc4363d81fea43c0dd0eadd09113f97a Mon Sep 17 00:00:00 2001 From: CedarMist <134699267+CedarMist@users.noreply.github.com> Date: Thu, 3 Oct 2024 09:51:05 +0100 Subject: [PATCH 1/3] move to flags & add poll weighting --- hardhat/contracts/PollManager.sol | 146 +++++++++++++++++++----------- hardhat/test/PollManager.spec.ts | 31 ++----- 2 files changed, 101 insertions(+), 76 deletions(-) diff --git a/hardhat/contracts/PollManager.sol b/hardhat/contracts/PollManager.sol index e6b5d78..2b72eb4 100644 --- a/hardhat/contracts/PollManager.sol +++ b/hardhat/contracts/PollManager.sol @@ -15,6 +15,13 @@ contract PollManager is IERC165, IPollManager { uint256 public constant MAX_CHOICES = 8; + uint8 constant FLAG_ACTIVE = 1<<0; + uint8 constant FLAG_PUBLISH_VOTERS = 1<<1; + uint8 constant FLAG_PUBLISH_VOTES = 1<<2; + uint8 constant FLAG_HIDDEN = 1<<3; + uint8 constant FLAG_WEIGHT_LOG10 = 1<<4; + uint8 constant FLAG_WEIGHT_ONE = 1<<5; + // ------------------------------------------------------------------------ // ERRORS @@ -54,16 +61,13 @@ contract PollManager is IERC165, IPollManager { struct ProposalParams { uint8 numChoices; - bool publishVoters; - bool publishVotes; - bool isHidden; - uint64 closeTimestamp; + uint8 flags; // bit flag of FLAG_ vars + uint32 closeTimestamp; // approx year 2106 IPollACL acl; - string metadata; + bytes metadata; } struct Proposal { - bool active; uint8 topChoice; ProposalParams params; } @@ -156,7 +160,7 @@ contract PollManager is IERC165, IPollManager { } function createFor( - ProposalParams calldata in_params, + ProposalParams memory in_params, bytes calldata in_aclData, address in_owner ) @@ -172,6 +176,8 @@ contract PollManager is IERC165, IPollManager { revert Create_InvalidACL(); } + in_params.flags |= FLAG_ACTIVE; + if (in_params.numChoices == 0) { revert Create_NoChoices(); } @@ -187,7 +193,6 @@ contract PollManager is IERC165, IPollManager { } PROPOSALS[proposalId] = Proposal({ - active: true, params: in_params, topChoice:0 }); @@ -209,7 +214,7 @@ contract PollManager is IERC165, IPollManager { } // Hidden proposals will not show in the public list - if( ! in_params.isHidden ) + if( 0 == (in_params.flags & FLAG_HIDDEN) ) { ACTIVE_PROPOSALS.add(proposalId); @@ -235,7 +240,7 @@ contract PollManager is IERC165, IPollManager { Proposal storage proposal = PROPOSALS[in_proposalId]; // Proposal must be active to vote - if (!proposal.active) { + if ( 0 == (proposal.params.flags & FLAG_ACTIVE) ) { revert Vote_NotActive(); } @@ -253,6 +258,17 @@ contract PollManager is IERC165, IPollManager { } } + function log10(uint x) internal pure returns (uint result) + { + // XXX: is it necessary to make it constant time? + unchecked { + while (x >= 10) { + x /= 10; + result++; + } + } + } + function internal_castVote( address in_voter, bytes32 in_proposalId, @@ -261,14 +277,34 @@ contract PollManager is IERC165, IPollManager { ) internal { + Proposal storage proposal = PROPOSALS[in_proposalId]; + uint8 flags = proposal.params.flags; + + // Cannot vote if we have 0 weight. Prevents internal confusion with weight=0 + if( 0 == (flags & FLAG_ACTIVE) ) + { + revert Vote_NotActive(); + } + uint weight = canVoteOnPoll(in_proposalId, in_voter, in_data); - Proposal storage proposal = PROPOSALS[in_proposalId]; + // User is not allowed to vote if they have zero-weight + if( weight == 0 ) { + revert Vote_NotAllowed(); + } + + if( 0 != flags & FLAG_WEIGHT_LOG10 ) { + weight = log10(weight); + } + else if( 0 != flags & FLAG_WEIGHT_ONE ) { + weight = 1; + } uint256 numChoices = proposal.params.numChoices; - if (in_choiceId >= numChoices) { - require(false, "Vote_UnknownChoice()"); + if (in_choiceId >= numChoices) + { + revert Vote_UnknownChoice(); } Ballot storage ballot = s_ballots[in_proposalId]; @@ -307,7 +343,7 @@ contract PollManager is IERC165, IPollManager { { ballot.totalVotes += existingWeight; - if (proposal.params.publishVotes || proposal.params.publishVoters) + if ( 0 != (flags & (FLAG_PUBLISH_VOTERS|FLAG_PUBLISH_VOTES)) ) { ballot.voters.push(in_voter); } @@ -329,7 +365,7 @@ contract PollManager is IERC165, IPollManager { external { if( msg.sender != address(GASLESS_VOTER) ) { - require(false, "Vote_NotAllowed()"); + revert Vote_NotAllowed(); } internal_castVote(in_voter, in_proposalId, in_choiceId, in_data); @@ -397,12 +433,12 @@ contract PollManager is IERC165, IPollManager { /// Permanently delete a proposal and associated data /// If the proposal isn't closed, it will be closed first - /// XXX: function close(bytes32 in_proposalId) public { Proposal storage proposal = PROPOSALS[in_proposalId]; - if (!proposal.active) { + uint8 flags = proposal.params.flags; + if ( 0 == (flags & FLAG_ACTIVE) ) { revert Close_NotActive(); } @@ -436,11 +472,11 @@ contract PollManager is IERC165, IPollManager { } } - PROPOSALS[in_proposalId].topChoice = uint8(topChoice); - PROPOSALS[in_proposalId].active = false; + proposal.topChoice = uint8(topChoice); + proposal.params.flags = flags & ~FLAG_ACTIVE; // If proposal isn't hidden, remove from active list, to past list + emit events - if( ! proposal.params.isHidden ) + if( 0 == flags & FLAG_HIDDEN ) { ACTIVE_PROPOSALS.remove(in_proposalId); s_pastProposalsIndex[in_proposalId] = PAST_PROPOSALS.length; @@ -459,11 +495,13 @@ contract PollManager is IERC165, IPollManager { external { Proposal storage proposal = PROPOSALS[in_proposalId]; - if ( 0 == proposal.params.numChoices) { + if ( 0 == proposal.params.numChoices ) { revert Destroy_NotFound(); } - if( proposal.active ) + uint8 flags = proposal.params.flags; + + if( 0 == flags & FLAG_ACTIVE ) { close(in_proposalId); } @@ -480,7 +518,7 @@ contract PollManager is IERC165, IPollManager { delete s_ballots[in_proposalId]; // Remove proposal from past proposals list - if( ! proposal.params.isHidden ) + if( 0 != (flags & FLAG_HIDDEN) ) { uint idx = s_pastProposalsIndex[in_proposalId]; uint cnt = PAST_PROPOSALS.length; @@ -501,7 +539,7 @@ contract PollManager is IERC165, IPollManager { Ballot storage ballot = s_ballots[in_proposalId]; // Cannot get vote counts while poll is still active - if (proposal.active) { + if ( 0 != (proposal.params.flags & FLAG_ACTIVE) ) { revert Poll_StillActive(); } @@ -513,31 +551,45 @@ contract PollManager is IERC165, IPollManager { return unmaskedVoteCounts; } - function getVotes(bytes32 in_proposalId, uint in_offset, uint in_limit) - external view - returns ( - uint out_count, - address[] memory out_voters, - Choice[] memory out_choices - ) + function internal_paginateBallot(bytes32 in_proposalId, uint in_offset, uint in_limit) + internal view + returns (uint out_count, uint out_limit, Ballot storage out_ballot) { Proposal storage proposal = PROPOSALS[in_proposalId]; - Ballot storage ballot = s_ballots[in_proposalId]; + out_ballot = s_ballots[in_proposalId]; + + uint8 flags = proposal.params.flags; - if (!proposal.params.publishVotes) { + if ( 0 == (flags & FLAG_PUBLISH_VOTES) ) { revert Poll_NotPublishingVotes(); } - if (proposal.active) { + if ( 0 != (flags & FLAG_ACTIVE) ) { revert Poll_StillActive(); } - out_count = ballot.voters.length; + out_count = out_ballot.voters.length; if ((in_offset + in_limit) > out_count) { - in_limit = out_count - in_offset; + out_limit = out_count - in_offset; + } + else { + out_limit = out_limit; } + } + + function getVotes(bytes32 in_proposalId, uint in_offset, uint in_limit) + external view + returns ( + uint out_count, + address[] memory out_voters, + Choice[] memory out_choices + ) + { + Ballot storage ballot; + + (out_count, in_limit, ballot) = internal_paginateBallot(in_proposalId, in_offset, in_limit); out_choices = new Choice[](in_limit); out_voters = new address[](in_limit); @@ -557,23 +609,9 @@ contract PollManager is IERC165, IPollManager { address[] memory out_voters ) { - Proposal storage proposal = PROPOSALS[in_proposalId]; - Ballot storage ballot = s_ballots[in_proposalId]; - - if (!proposal.params.publishVoters) { - revert Poll_NotPublishingVoters(); - } - - if (proposal.active) { - revert Poll_StillActive(); - } + Ballot storage ballot; - out_count = ballot.voters.length; - - if ((in_offset + in_limit) > out_count) - { - in_limit = out_count - in_offset; - } + (out_count, in_limit, ballot) = internal_paginateBallot(in_proposalId, in_offset, in_limit); out_voters = new address[](in_limit); @@ -588,11 +626,11 @@ contract PollManager is IERC165, IPollManager { external view returns (bool) { - return PROPOSALS[in_id].active; + return PROPOSALS[in_id].params.flags & FLAG_ACTIVE != 0; } function getProposalId( - ProposalParams calldata in_params, + ProposalParams memory in_params, bytes calldata in_aclData, address in_owner ) diff --git a/hardhat/test/PollManager.spec.ts b/hardhat/test/PollManager.spec.ts index 3478324..e27b76b 100644 --- a/hardhat/test/PollManager.spec.ts +++ b/hardhat/test/PollManager.spec.ts @@ -121,20 +121,16 @@ describe('PollManager', function () { TEST_PROPOSALS = [ { - isHidden: false, + flags: 1n, numChoices: 4n, - publishVotes: false, - publishVoters: false, - closeTimestamp: BigInt(new Date().getTime() + 1), + closeTimestamp: BigInt(Math.round(new Date().getTime()/1000) + 1), acl: acl_allowall_addr, metadata: '', }, { - isHidden: false, + flags: 1n, numChoices: 3n, - publishVotes: true, - publishVoters: false, - closeTimestamp: BigInt(new Date().getTime() + 2), + closeTimestamp: BigInt(Math.round(new Date().getTime()/1000) + 2), acl: acl_allowall_addr, metadata: '', }, @@ -165,10 +161,9 @@ describe('PollManager', function () { expect(ap_paginated.out_proposals.length).eq(1); const x = ap_paginated.out_proposals[0].proposal.params; - expect(x.isHidden).eq(p.isHidden); + expect(x.flags).eq(p.flags); expect(x.metadata).eq(p.metadata); expect(x.numChoices).eq(p.numChoices); - expect(x.publishVotes).eq(p.publishVotes); expect(x.closeTimestamp).eq(p.closeTimestamp); expect(x.acl).eq(p.acl); } @@ -192,11 +187,9 @@ describe('PollManager', function () { const propId = await addProposal( pm, { - isHidden: false, + flags: 0, metadata: '', numChoices: 3n, - publishVotes: false, - publishVoters: false, closeTimestamp: 0n, acl: await acl_tokenholder.getAddress(), }, @@ -255,11 +248,9 @@ describe('PollManager', function () { const propId = await addProposal( pm, { - isHidden: false, + flags: 0, metadata: '', numChoices: 3n, - publishVotes: false, - publishVoters: false, closeTimestamp: 0n, acl: await acl_storageproof.getAddress(), }, @@ -314,11 +305,9 @@ describe('PollManager', function () { const proposalId = await addProposal( pm, { - isHidden: false, + flags: 0, metadata: '', numChoices: 3n, - publishVotes: false, - publishVoters: false, closeTimestamp: 0n, acl: await acl_allowall.getAddress(), }, @@ -417,11 +406,9 @@ describe('PollManager', function () { const proposalId = await addProposal( pm, { - isHidden: false, + flags: 0, metadata: '', numChoices: 3n, - publishVotes: false, - publishVoters: false, closeTimestamp: 0n, acl: await acl_storageproof.getAddress(), }, From 829ebab747d28f660ffb755dcfc023c833f81f39 Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Thu, 3 Oct 2024 14:59:52 +0200 Subject: [PATCH 2/3] Add CBOR dependency --- frontend/package.json | 1 + pnpm-lock.yaml | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index 3065b45..b856ee4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ "@oasisprotocol/sapphire-ethers-v6": "^6.0.1", "@oasisprotocol/sapphire-paratime": "^2.0.1", "@phosphor-icons/core": "^2.0.8", + "cbor-web": "^9.0.2", "ethers": "6.10.0", "framer-motion": "^11.5.4", "lru-cache": "10.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15cd8e3..bfc032f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,6 +63,9 @@ importers: '@phosphor-icons/core': specifier: ^2.0.8 version: 2.1.1 + cbor-web: + specifier: ^9.0.2 + version: 9.0.2 ethers: specifier: 6.10.0 version: 6.10.0 @@ -4346,6 +4349,11 @@ packages: resolution: {integrity: sha512-ulDEYPv7asdKvqahuAY35c1selLdzDwHqugK92hfkzvlDCwXRRelDkR+Er33md/PtnpqHemgkuDPanZ4fiYZ8w==} dev: true + /cbor-web@9.0.2: + resolution: {integrity: sha512-N6gU2GsJS8RR5gy1d9wQcSPgn9FGJFY7KNvdDRlwHfz6kCxrQr2TDnrjXHmr6TFSl6Fd0FC4zRnityEldjRGvQ==} + engines: {node: '>=16'} + dev: false + /cborg@1.10.2: resolution: {integrity: sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==} hasBin: true From efbab4c84b20e3e79b11b515740f1ff6de8f9ee2 Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Thu, 3 Oct 2024 14:59:38 +0200 Subject: [PATCH 3/3] Follow on-chain storage format change - Fold separate flags into a bigint, as bits - Compress metadata wit CBOR - Introduce versioned storage format for metadata - Add proper error handling around invalid metadata --- frontend/src/components/PollCard/index.tsx | 6 ++- frontend/src/constants/config.ts | 18 +++++-- frontend/src/hooks/useExtendedPoll.ts | 41 +++++++++++----- frontend/src/hooks/useProposalFromChain.ts | 3 +- .../pages/DashboardPage/useDashboardData.ts | 15 +++--- frontend/src/pages/PollPage/hook.ts | 7 +-- frontend/src/types/index.ts | 1 + frontend/src/types/poll-flags.ts | 23 +++++++++ frontend/src/types/poll.ts | 1 - frontend/src/utils/poll.utils.ts | 47 +++++++++++++------ 10 files changed, 113 insertions(+), 49 deletions(-) create mode 100644 frontend/src/types/poll-flags.ts diff --git a/frontend/src/components/PollCard/index.tsx b/frontend/src/components/PollCard/index.tsx index 3f1c979..334d9e0 100644 --- a/frontend/src/components/PollCard/index.tsx +++ b/frontend/src/components/PollCard/index.tsx @@ -1,4 +1,4 @@ -import { Proposal } from '../../types' +import { isPollActive, Proposal } from '../../types' import { FC, MouseEventHandler, useEffect } from 'react' import { Link } from 'react-router-dom' import classes from './index.module.css' @@ -94,7 +94,7 @@ export const PollCard: FC<{ const { id: pollId, - proposal: { active }, + proposal: { params }, ipfsParams: { name, description, @@ -111,6 +111,8 @@ export const PollCard: FC<{ }, } + const active = isPollActive(params) + const renderedDescription = description const textMatches = searchPatterns.length diff --git a/frontend/src/constants/config.ts b/frontend/src/constants/config.ts index 4ef8b84..8aa8387 100644 --- a/frontend/src/constants/config.ts +++ b/frontend/src/constants/config.ts @@ -1,5 +1,5 @@ // EIP-3085: wallet_addEthereumChain RPC Method -import { ExtendedPoll } from '../types' +import { ExtendedPoll, FLAG_ACTIVE } from '../types' import { randomchoice, chain_info } from '@oasisprotocol/blockvote-contracts' export const METAMASK_HOME_PAGE_URL = 'https://metamask.io/' @@ -56,7 +56,9 @@ export const demoPoll1 = { id: 'demo', proposal: { id: '0xdemo', - active: true, + params: { + flags: FLAG_ACTIVE, + }, }, ipfsParams: { creator: 'demo', @@ -73,7 +75,9 @@ export const demoPoll2 = { id: 'demo', proposal: { id: '0xdemo', - active: true, + params: { + flags: FLAG_ACTIVE, + }, }, ipfsParams: { creator: 'demo', @@ -90,7 +94,9 @@ export const demoPoll3 = { id: 'demo', proposal: { id: '0xdemo', - active: true, + params: { + flags: FLAG_ACTIVE, + }, }, ipfsParams: { creator: 'demo', @@ -107,7 +113,9 @@ export const demoPoll4 = { id: 'demo', proposal: { id: '0xdemo', - active: true, + params: { + flags: FLAG_ACTIVE, + }, }, ipfsParams: { creator: 'demo', diff --git a/frontend/src/hooks/useExtendedPoll.ts b/frontend/src/hooks/useExtendedPoll.ts index ebf597a..5af5f08 100644 --- a/frontend/src/hooks/useExtendedPoll.ts +++ b/frontend/src/hooks/useExtendedPoll.ts @@ -1,11 +1,22 @@ -import { ListOfVotes, ExtendedPoll, PollResults, Proposal, ListOfVoters } from '../types' +import { + ListOfVotes, + ExtendedPoll, + PollResults, + Proposal, + ListOfVoters, + isPollActive, + shouldPublishVotes, + shouldPublishVoters, + inactivatePoll, + Poll, +} from '../types' import { useEffect, useMemo, useState } from 'react' import { dashboard, demoSettings, getDemoPoll } from '../constants/config' import { usePollGaslessStatus } from './usePollGaslessStatus' import { usePollPermissions } from './usePollPermissions' import { useEthereum } from './useEthereum' import { useContracts } from './useContracts' -import { decodeBase64, toUtf8String } from 'ethers' +import { decodePollMetadata } from '../utils/poll.utils' import { getVerdict } from '../components/InputFields' const noVoters: ListOfVoters = { out_count: 0n, out_voters: [] } @@ -36,10 +47,14 @@ export const useExtendedPoll = ( let correctiveAction: (() => void) | undefined - const ipfsParams = useMemo( - () => (metadata ? JSON.parse(toUtf8String(decodeBase64(metadata))) : undefined), - [metadata], - ) + const [ipfsParams, ipfsError] = useMemo((): [Poll | undefined, string | undefined] => { + try { + return metadata ? [decodePollMetadata(metadata), undefined] : [undefined, undefined] + } catch (e) { + console.log('metadata problem on poll', proposal?.id, e) + return [undefined, "Invalid metadata, poll can't be displayed."] + } + }, [metadata]) useEffect( // Update poll object @@ -77,10 +92,10 @@ export const useExtendedPoll = ( correctiveAction = checkPermissions } - const isActive = !!proposal?.active + const isActive = isPollActive(proposal?.params) const loadVotes = async () => { - if (isDemo || !proposal || !pollManager || !ipfsParams || proposal.active) return + if (isDemo || !proposal || !pollManager || !ipfsParams || isActive) return if (params.onDashboard && !dashboard.showResults) return @@ -90,7 +105,7 @@ export const useExtendedPoll = ( setVoteCounts(voteCounts) setWinningChoice(proposal.topChoice) - if (proposal.params.publishVotes) { + if (shouldPublishVotes(proposal.params)) { const loadedVotes: ListOfVotes = { out_count: 1000n, // Fake number, will be updated when the first batch is loaded out_voters: [], @@ -103,7 +118,7 @@ export const useExtendedPoll = ( loadedVotes.out_choices.push(...newVotes.out_choices) } setVotes(loadedVotes) - } else if (proposal.params.publishVoters) { + } else if (shouldPublishVoters(proposal.params)) { const loadedVoters: ListOfVoters = { out_count: 1000n, // Fake number, will be updated when the first batch is loaded out_voters: [], @@ -124,7 +139,7 @@ export const useExtendedPoll = ( useEffect( // Load votes, when the stars are right () => void loadVotes(), - [proposal, proposal?.active, params.onDashboard, dashboard.showResults, pollManager, ipfsParams, isDemo], + [proposal, isActive, params.onDashboard, dashboard.showResults, pollManager, ipfsParams, isDemo], ) const pollResults = useMemo(() => { @@ -155,7 +170,7 @@ export const useExtendedPoll = ( ...poll, proposal: { ...poll.proposal, - active: false, + params: inactivatePoll(poll.proposal.params), topChoice: 0n, }, }) @@ -178,7 +193,7 @@ export const useExtendedPoll = ( isActive, isDemo, isLoading: false, - error: undefined, + error: ipfsError, correctiveAction, poll, voteCounts, diff --git a/frontend/src/hooks/useProposalFromChain.ts b/frontend/src/hooks/useProposalFromChain.ts index 1302864..f93d468 100644 --- a/frontend/src/hooks/useProposalFromChain.ts +++ b/frontend/src/hooks/useProposalFromChain.ts @@ -30,7 +30,7 @@ export const useProposalFromChain = (proposalId: string) => { try { setIsLoading(true) const data = await pollManager.PROPOSALS(proposalId) - const [active, topChoice, params] = data + const [topChoice, params] = data const acl = data?.params?.acl if (!acl || acl === ZeroAddress) { // setError('Found proposal with invalid ACL.') @@ -39,7 +39,6 @@ export const useProposalFromChain = (proposalId: string) => { } else { setProposal({ id: proposalId, - active, topChoice, params, }) diff --git a/frontend/src/pages/DashboardPage/useDashboardData.ts b/frontend/src/pages/DashboardPage/useDashboardData.ts index 6acfcb0..fcea84d 100644 --- a/frontend/src/pages/DashboardPage/useDashboardData.ts +++ b/frontend/src/pages/DashboardPage/useDashboardData.ts @@ -1,6 +1,6 @@ import { useContracts } from '../../hooks/useContracts' import { useEffect, useMemo, useState } from 'react' -import { PollManager, Proposal } from '../../types' +import { isPollActive, PollManager, Proposal } from '../../types' import { useEthereum } from '../../hooks/useEthereum' import { useBooleanField, useOneOfField, useTextField } from '../../components/InputFields' import { useNavigate } from 'react-router-dom' @@ -22,9 +22,9 @@ export type WantedStatus = 'active' | 'completed' | 'all' export const isPollStatusAcceptable = (proposal: Proposal, wantedStatus: WantedStatus): boolean => { switch (wantedStatus) { case 'active': - return proposal.active + return isPollActive(proposal.params) case 'completed': - return !proposal.active + return !isPollActive(proposal.params) case 'all': return true } @@ -135,8 +135,8 @@ async function fetchProposals( } result.out_proposals.forEach(({ id, proposal }) => { - const [active, topChoice, params] = proposal - proposalList.push({ id, active, topChoice, params }) + const [topChoice, params] = proposal + proposalList.push({ id, topChoice, params }) }) if (result.out_proposals.length < FETCH_BATCH_SIZE) { @@ -204,10 +204,7 @@ export const useDashboardData = () => { } const allProposals = useMemo( - () => [ - ...activeProposals.map((p): Proposal => ({ ...p, active: true })), - ...pastProposals.map((p): Proposal => ({ ...p, active: false })), - ], + () => [...activeProposals, ...pastProposals], [activeProposals, pastProposals, userAddress], ) diff --git a/frontend/src/pages/PollPage/hook.ts b/frontend/src/pages/PollPage/hook.ts index bc3ae9d..d1aa410 100644 --- a/frontend/src/pages/PollPage/hook.ts +++ b/frontend/src/pages/PollPage/hook.ts @@ -18,6 +18,7 @@ import { getVerdict } from '../../components/InputFields' import { useExtendedPoll } from '../../hooks/useExtendedPoll' import { useProposalFromChain } from '../../hooks/useProposalFromChain' import { useNavigate } from 'react-router-dom' +import { isPollActive } from '../../types' export const usePollData = (pollId: string) => { const navigate = useNavigate() @@ -362,7 +363,7 @@ export const usePollData = (pollId: string) => { () => { if ( isDemo && - poll?.proposal.active && + isPollActive(poll?.proposal.params) && remainingTime?.isPastDue && remainingTime.totalSeconds < demoSettings.waitSecondsBeforeFormallyCompleting + 5 && remainingTime.totalSeconds >= demoSettings.waitSecondsBeforeFormallyCompleting @@ -380,7 +381,7 @@ export const usePollData = (pollId: string) => { if (hasCompleted) { if (!poll) { // console.log("No poll loaded, waiting to load") - } else if (poll.proposal.active) { + } else if (isPollActive(poll.proposal.params)) { // console.log("Apparently, we have completed a poll, but we still perceive it as active, so scheduling a reload...") setTimeout(() => { // console.log("Reloading now") @@ -400,7 +401,7 @@ export const usePollData = (pollId: string) => { isLoading: isProposalLoading || isLoading, error: proposalError ?? error, poll, - active: !!poll?.proposal?.active, + active: isPollActive(poll?.proposal?.params), selectedChoice: winningChoice ?? selectedChoice, canSelect, diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index a89a91d..ceecce9 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -1,4 +1,5 @@ export type { DataEntry } from './data-entry' export type * from './poll' +export * from './poll-flags' export type { IconSize } from './icon-size' export type { IconProps } from './icon-props' diff --git a/frontend/src/types/poll-flags.ts b/frontend/src/types/poll-flags.ts new file mode 100644 index 0000000..af2511b --- /dev/null +++ b/frontend/src/types/poll-flags.ts @@ -0,0 +1,23 @@ +import { PollManager } from './poll' + +export const FLAG_ACTIVE = 1n << 0n +export const FLAG_PUBLISH_VOTERS = 1n << 1n +export const FLAG_PUBLISH_VOTES = 1n << 2n +export const FLAG_HIDDEN = 1n << 3n +export const FLAG_WEIGHT_LOG10 = 1n << 4n +export const FLAG_WEIGHT_ONE = 1n << 5n + +type Params = PollManager.ProposalParamsStructOutput + +export const isPollActive = (params: Params | undefined): boolean => !!((params?.flags ?? 0n) & FLAG_ACTIVE) + +export const inactivatePoll = (params: Params): Params => ({ + ...params, + flags: isPollActive(params) ? params.flags ^ FLAG_ACTIVE : params.flags, +}) + +export const shouldPublishVotes = (params: Params | undefined): boolean => + !!((params?.flags ?? 0n) & FLAG_PUBLISH_VOTES) + +export const shouldPublishVoters = (params: Params | undefined): boolean => + !!((params?.flags ?? 0n) & FLAG_PUBLISH_VOTERS) diff --git a/frontend/src/types/poll.ts b/frontend/src/types/poll.ts index e5fc855..1798e95 100644 --- a/frontend/src/types/poll.ts +++ b/frontend/src/types/poll.ts @@ -12,7 +12,6 @@ export type { export type Proposal = { id: string - active: boolean topChoice: bigint params: PollManager.ProposalParamsStructOutput } diff --git a/frontend/src/utils/poll.utils.ts b/frontend/src/utils/poll.utils.ts index 8f994ba..df740dc 100644 --- a/frontend/src/utils/poll.utils.ts +++ b/frontend/src/utils/poll.utils.ts @@ -1,12 +1,6 @@ -import { - AbiCoder, - BytesLike, - encodeBase64, - getAddress, - JsonRpcProvider, - ParamType, - toUtf8Bytes, -} from 'ethers' +import { AbiCoder, BytesLike, getAddress, JsonRpcProvider, ParamType } from 'ethers' + +import { encode as cborEncode, decode as cborDecode } from 'cbor-web' import { chain_info, @@ -25,7 +19,7 @@ import { } from '@oasisprotocol/blockvote-contracts' export type { ContractType, NftType } from '@oasisprotocol/blockvote-contracts' export { isToken } from '@oasisprotocol/blockvote-contracts' -import { Poll, PollManager } from '../types' +import { FLAG_HIDDEN, FLAG_PUBLISH_VOTERS, FLAG_PUBLISH_VOTES, Poll, PollManager } from '../types' import { EthereumContext } from '../providers/EthereumContext' import { DecisionWithReason, denyWithReason } from '../components/InputFields' import { FetcherFetchOptions } from './StoredLRUCache' @@ -141,6 +135,27 @@ export type CreatePollProps = { completionTime: Date | undefined } +const CURRENT_ENCODING_VERSION = 0 + +const encodePollMetadata = (poll: Poll): Uint8Array => { + const encoded = cborEncode({ v: CURRENT_ENCODING_VERSION, data: poll }) + // console.log('Encoded poll data', encoded) + return encoded +} + +export const decodePollMetadata = (metadata: string): Poll => { + const { v, data } = cborDecode(metadata.substring(2), { preferWeb: true, encoding: 'hex' }) + + if (typeof v !== 'number') throw new Error('Unknown poll data format') + + switch (v as number) { + case CURRENT_ENCODING_VERSION: + return data as Poll + default: + throw new Error(`Unrecognized poll data format version: ${v}`) + } +} + export const createPoll = async ( pollManager: PollManager, creator: string, @@ -176,14 +191,18 @@ export const createPoll = async ( // console.log('Compiling poll', poll) + let pollFlags: bigint = 0n + + if (poll.options.publishVoters) pollFlags |= FLAG_PUBLISH_VOTERS + if (poll.options.publishVotes) pollFlags |= FLAG_PUBLISH_VOTES + if (isHidden) pollFlags |= FLAG_HIDDEN + const proposalParams: PollManager.ProposalParamsStruct = { - metadata: encodeBase64(toUtf8Bytes(JSON.stringify(poll))), + metadata: encodePollMetadata(poll), numChoices: answers.length, - publishVotes: poll.options.publishVotes, - publishVoters: poll.options.publishVoters, closeTimestamp: poll.options.closeTimestamp, acl: aclOptions.address, - isHidden, + flags: pollFlags, } console.log('params are', proposalParams)