From 072b6eb35f5617cae6f3a1abd2bf99495dcbe675 Mon Sep 17 00:00:00 2001 From: Ezerous <ezerous@gmail.com> Date: Tue, 1 Dec 2020 15:24:46 +0200 Subject: [PATCH] PostVoting init --- .../src/options/drizzleOptions.js | 3 +- .../concordia-contracts/contracts/Forum.sol | 40 ++++-- .../contracts/PostVoting.sol | 115 ++++++++++++++++++ .../concordia-contracts/contracts/Voting.sol | 38 +++--- .../migrations/2_deploy_contracts.js | 6 +- 5 files changed, 168 insertions(+), 34 deletions(-) create mode 100644 packages/concordia-contracts/contracts/PostVoting.sol diff --git a/packages/concordia-app/src/options/drizzleOptions.js b/packages/concordia-app/src/options/drizzleOptions.js index 0f9f761..08ca927 100644 --- a/packages/concordia-app/src/options/drizzleOptions.js +++ b/packages/concordia-app/src/options/drizzleOptions.js @@ -9,7 +9,8 @@ const drizzleOptions = { contracts, events: { Forum: ['UserSignedUp', 'UsernameUpdated', 'TopicCreated', 'PostCreated'], - Voting: ['PollCreated', 'UserVoted'], + Voting: ['PollCreated', 'UserVotedPoll'], + PostVoting: ['UserVotedPost'], }, reloadWindowOnNetworkChange: true, reloadWindowOnAccountChange: true, // We need it to reinitialize breeze and create new Orbit databases diff --git a/packages/concordia-contracts/contracts/Forum.sol b/packages/concordia-contracts/contracts/Forum.sol index ab258ad..bd94ce7 100644 --- a/packages/concordia-contracts/contracts/Forum.sol +++ b/packages/concordia-contracts/contracts/Forum.sol @@ -117,7 +117,7 @@ contract Forum { function createPost(uint topicID) public returns (uint) { require(hasUserSignedUp(msg.sender)); // Only registered users can create posts - require(topicID<numTopics); // Only allow posting to a topic that exists + require(topicExists(topicID)); // Only allow posting to a topic that exists uint postID = numPosts++; posts[postID] = Post(postID, msg.sender, block.timestamp, topicID); topics[topicID].postIDs.push(postID); @@ -126,37 +126,51 @@ contract Forum { return postID; } + // Verify that topic exists + function topicExists(uint topicID) public view returns (bool) { + return topicID<numTopics; + } + + // Verify that post exists + function postExists(uint postID) public view returns (bool) { + return postID<numPosts; + } + function getNumberOfTopics() public view returns (uint) { return numTopics; } + function getNumberOfPosts() public view returns (uint) { + return numPosts; + } + function getTopic(uint topicID) public view returns (address, string memory, uint, uint[] memory) { - require(topicID<numTopics); + require(topicExists(topicID)); return ( - topics[topicID].author, - users[topics[topicID].author].username, - topics[topicID].timestamp, - topics[topicID].postIDs + topics[topicID].author, + users[topics[topicID].author].username, + topics[topicID].timestamp, + topics[topicID].postIDs ); } function getTopicPosts(uint topicID) public view returns (uint[] memory) { - require(topicID<numTopics); // Topic should exist + require(topicExists(topicID)); // Topic should exist return topics[topicID].postIDs; } function getTopicAuthor(uint topicID) public view returns (address) { - require(topicID<numTopics); // Topic should exist + require(topicExists(topicID)); // Topic should exist return topics[topicID].author; } function getPost(uint postID) public view returns (address, string memory, uint, uint) { - require(postID<numPosts); + require(postExists(postID)); return ( - posts[postID].author, - users[posts[postID].author].username, - posts[postID].timestamp, - posts[postID].topicID + posts[postID].author, + users[posts[postID].author].username, + posts[postID].timestamp, + posts[postID].topicID ); } } diff --git a/packages/concordia-contracts/contracts/PostVoting.sol b/packages/concordia-contracts/contracts/PostVoting.sol new file mode 100644 index 0000000..5294213 --- /dev/null +++ b/packages/concordia-contracts/contracts/PostVoting.sol @@ -0,0 +1,115 @@ +//SPDX-License-Identifier: MIT +pragma solidity 0.7.5; + +import "./Forum.sol"; + +contract PostVoting { + Forum public forum; + + constructor(Forum addr) { + forum = Forum(addr); + } + + enum Option { NONE, UP, DOWN } // NONE -> 0, UP -> 1, DOWN -> 2 + + Option constant defaultOption = Option.NONE; + + function getDefaultChoice() public pure returns (uint) { + return uint(defaultOption); + } + + struct PostBallot { + mapping (address => Option) votes; + mapping (Option => address[]) voters; + } + + mapping (uint => PostBallot) postBallots; + + event UserVotedPost(address userAddress, uint postID, Option option); + + function getVote(uint postID, address voter) public view returns (Option) { + require(forum.postExists(postID)); + return postBallots[postID].votes[voter]; + } + + // Gets vote count for a specific option (Option.UP/ Option.DOWN) + function getVoteCount(uint postID, Option option) private view returns (uint) { + require(forum.postExists(postID)); + return (postBallots[postID].voters[option].length); + } + + function getUpvoteCount(uint postID) public view returns (uint) { + return (getVoteCount(postID, Option.UP)); + } + + function getDownvoteCount(uint postID) public view returns (uint) { + return (getVoteCount(postID, Option.DOWN)); + } + + // Gets voters for a specific option (Option.UP/ Option.DOWN) + function getVoters(uint postID, Option option) private view returns (address[] memory) { + require(forum.postExists(postID)); + return (postBallots[postID].voters[option]); + } + + function getUpvoters(uint postID) public view returns (address[] memory) { + return (getVoters(postID, Option.UP)); + } + + function getDownvoters(uint postID) public view returns (address[] memory) { + return (getVoters(postID, Option.DOWN)); + } + + function getVoterIndex(uint postID, address voter) private view returns (uint) { + require(forum.hasUserSignedUp(voter)); + require(forum.postExists(postID)); + + PostBallot storage postBallot = postBallots[postID]; + Option votedOption = getVote(postID, voter); + address[] storage optionVoters = postBallot.voters[votedOption]; + + for (uint voterIndex = 0; voterIndex < optionVoters.length; voterIndex++) + if (optionVoters[voterIndex] == voter) + return voterIndex; + + revert("Couldn't find voter's index!"); + } + + function vote(uint postID, Option option) private { + require(forum.hasUserSignedUp(msg.sender)); + require(forum.postExists(postID)); // Only allow voting if post exists + + PostBallot storage postBallot = postBallots[postID]; + address voter = msg.sender; + Option prevOption = postBallot.votes[voter]; + + if(prevOption == option) + return; + + // Remove previous vote if exists + if(prevOption != Option.NONE){ + uint voterIndex = getVoterIndex(postID, voter); + // Swap with last voter address and delete vote + postBallot.voters[prevOption][voterIndex] = postBallot.voters[prevOption][postBallot.voters[prevOption].length - 1]; + postBallot.voters[prevOption].pop(); + } + + // Add new vote + if(option != Option.NONE) + postBallot.voters[option].push(voter); + postBallot.votes[voter] = option; + emit UserVotedPost(voter, postID, option); + } + + function upvote(uint postID) public{ + vote(postID, Option.UP); + } + + function downvote(uint postID) public{ + vote(postID, Option.DOWN); + } + + function unvote(uint postID) public{ + vote(postID, Option.NONE); + } +} diff --git a/packages/concordia-contracts/contracts/Voting.sol b/packages/concordia-contracts/contracts/Voting.sol index 8b17903..cc5c655 100644 --- a/packages/concordia-contracts/contracts/Voting.sol +++ b/packages/concordia-contracts/contracts/Voting.sol @@ -23,10 +23,10 @@ contract Voting { mapping (uint => Poll) polls; event PollCreated(uint topicID); - event UserVoted(address userAddress); + event UserVotedPoll(address userAddress, uint topicID, uint vote); - // Verify that poll exists - function isPollExistent(uint topicID) public view returns (bool) { + // Verifies that a poll exists + function pollExists(uint topicID) public view returns (bool) { if (polls[topicID].timestamp != 0) return true; return false; @@ -34,9 +34,9 @@ contract Voting { function createPoll(uint topicID, uint numOptions, string memory dataHash, bool enableVoteChanges) public returns (uint) { require(forum.hasUserSignedUp(msg.sender)); // Only registered users can create polls - require(topicID<forum.getNumberOfTopics()); // Only allow poll creation if topic exists + require(forum.topicExists(topicID)); // Only allow poll creation if topic exists require (forum.getTopicAuthor(topicID) == msg.sender); // Only allow poll creation from the author of the topic - require(!isPollExistent(topicID)); // Only allow poll creation if it doesn't already exist + require(!pollExists(topicID)); // Only allow poll creation if it doesn't already exist Poll storage poll = polls[topicID]; poll.topicID = topicID; @@ -50,27 +50,27 @@ contract Voting { } function getPollInfo(uint topicID) public view returns (uint, string memory, uint, uint) { - require(isPollExistent(topicID)); + require(pollExists(topicID)); uint totalVotes = getTotalVotes(topicID); return ( - polls[topicID].numOptions, - polls[topicID].dataHash, - polls[topicID].timestamp, - totalVotes + polls[topicID].numOptions, + polls[topicID].dataHash, + polls[topicID].timestamp, + totalVotes ); } function isOptionValid(uint topicID, uint option) public view returns (bool) { - require(isPollExistent(topicID)); + require(pollExists(topicID)); if (option <= polls[topicID].numOptions) // Option 0 is valid as well (no option chosen) return true; return false; } function hasVoted(uint topicID, address voter) public view returns (bool) { - require(isPollExistent(topicID)); + require(pollExists(topicID)); if (polls[topicID].votes[voter] != 0) return true; return false; @@ -83,13 +83,13 @@ contract Voting { // Gets vote count for a specific option function getVoteCount(uint topicID, uint option) public view returns (uint) { - require(isPollExistent(topicID)); + require(pollExists(topicID)); require(isOptionValid(topicID, option)); return (polls[topicID].voters[option].length); } function getTotalVotes(uint topicID) public view returns (uint) { - require(isPollExistent(topicID)); + require(pollExists(topicID)); Poll storage poll = polls[topicID]; uint totalVotes = 0; @@ -102,12 +102,12 @@ contract Voting { // Gets voters for a specific option function getVoters(uint topicID, uint option) public view returns (address[] memory) { - require(isPollExistent(topicID)); + require(pollExists(topicID)); return (polls[topicID].voters[option]); } function getVoterIndex(uint topicID, address voter) public view returns (uint) { - require(isPollExistent(topicID)); + require(pollExists(topicID)); require(hasVoted(topicID, voter)); Poll storage poll = polls[topicID]; uint votedOption = getVote(topicID, voter); @@ -122,7 +122,7 @@ contract Voting { function vote(uint topicID, uint option) public { require(forum.hasUserSignedUp(msg.sender)); - require(isPollExistent(topicID)); + require(pollExists(topicID)); require(isOptionValid(topicID, option)); Poll storage poll = polls[topicID]; address voter = msg.sender; @@ -134,7 +134,7 @@ contract Voting { if(prevOption == 0){ poll.voters[option].push(voter); poll.votes[voter] = option; - emit UserVoted(voter); + emit UserVotedPoll(voter, topicID, option); } else if (poll.enableVoteChanges){ uint voterIndex = getVoterIndex(topicID, voter); @@ -144,7 +144,7 @@ contract Voting { if(option != 0) poll.voters[option].push(voter); poll.votes[voter] = option; - emit UserVoted(voter); + emit UserVotedPoll(voter, topicID, option); } } } diff --git a/packages/concordia-contracts/migrations/2_deploy_contracts.js b/packages/concordia-contracts/migrations/2_deploy_contracts.js index 3e81c0e..df11216 100644 --- a/packages/concordia-contracts/migrations/2_deploy_contracts.js +++ b/packages/concordia-contracts/migrations/2_deploy_contracts.js @@ -1,7 +1,11 @@ const Forum = artifacts.require('Forum'); const Voting = artifacts.require('Voting'); +const PostVoting = artifacts.require('PostVoting'); // eslint-disable-next-line func-names module.exports = function (deployer) { - deployer.deploy(Forum).then((forum) => deployer.deploy(Voting, forum.address)); + deployer.deploy(Forum).then((forum) => { + deployer.deploy(Voting, forum.address); + deployer.deploy(PostVoting, forum.address); + }); };