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);
+  });
 };