Browse Source

Merge branch 'develop' into feature/94-add-an-about-menu-item

# Conflicts:
#	yarn.lock
develop
Apostolos Fanakis 4 years ago
parent
commit
1375aff1a9
  1. 3
      packages/concordia-app/src/assets/css/index.css
  2. 2
      packages/concordia-app/src/assets/css/loading-component.css
  3. 36
      packages/concordia-app/src/components/PostList/PostListRow/index.jsx
  4. 8
      packages/concordia-app/src/components/PostList/index.jsx
  5. 12
      packages/concordia-app/src/views/Home/index.jsx
  6. 5
      packages/concordia-app/src/views/Topic/TopicView/index.jsx
  7. 8
      packages/concordia-app/src/views/Topic/index.jsx
  8. 4
      packages/concordia-contracts/.eslintrc.js
  9. 8
      packages/concordia-contracts/.solhint.json
  10. 58
      packages/concordia-contracts/contracts/Forum.sol
  11. 12
      packages/concordia-contracts/contracts/Migrations.sol
  12. 109
      packages/concordia-contracts/contracts/PostVoting.sol
  13. 155
      packages/concordia-contracts/contracts/Voting.sol
  14. 8
      packages/concordia-contracts/index.js
  15. 8
      packages/concordia-contracts/migrations/2_deploy_contracts.js
  16. 2
      packages/concordia-contracts/package.json
  17. 112
      packages/concordia-contracts/test/TestVoting.sol
  18. 2
      packages/concordia-contracts/truffle-config.js
  19. 969
      yarn.lock

3
packages/concordia-app/src/assets/css/index.css

@ -1,6 +1,6 @@
body.app {
overflow: auto;
margin: 1em !important;
margin: 0;
}
#root {
@ -18,4 +18,5 @@ body.app {
.ui.inverted.menu {
background: #0B2540;
border-radius: 0;
}

2
packages/concordia-app/src/assets/css/loading-component.css

@ -3,7 +3,7 @@ body {
}
.loading-screen {
margin-top: 10em;
margin-top: 12em;
text-align: center;
font-size: large;
}

36
packages/concordia-app/src/components/PostList/PostListRow/index.jsx

@ -1,8 +1,8 @@
import React, {
memo, useEffect, useMemo, useState,
memo, useEffect, useMemo, useState, useCallback,
} from 'react';
import {
Dimmer, Icon, Image, Feed, Placeholder,
Dimmer, Icon, Image, Feed, Placeholder, Ref,
} from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
@ -22,12 +22,13 @@ const { orbit } = breeze;
const PostListRow = (props) => {
const {
id: postId, postIndexInTopic, postCallHash, loading,
id: postId, postIndex, postCallHash, loading, focus,
} = props;
const getPostResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getPost);
const [postAuthorAddress, setPostAuthorAddress] = useState(null);
const [postAuthor, setPostAuthor] = useState(null);
const [timeAgo, setTimeAgo] = useState(null);
const [topicId, setTopicId] = useState(null);
const [postContent, setPostContent] = useState(null);
const [postAuthorMeta, setPostAuthorMeta] = useState(null);
const userAddress = useSelector((state) => state.user.address);
@ -41,6 +42,7 @@ const PostListRow = (props) => {
setPostAuthorAddress(getPostResults[postCallHash].value[0]);
setPostAuthor(getPostResults[postCallHash].value[1]);
setTimeAgo(getPostResults[postCallHash].value[2] * 1000);
setTopicId(getPostResults[postCallHash].value[3]);
}
}, [getPostResults, loading, postCallHash]);
@ -116,18 +118,31 @@ const PostListRow = (props) => {
return authorAvatar;
}, [authorAvatar, postAuthorAddress]);
const focusRef = useCallback((node) => {
if (focus && node !== null) {
node.scrollIntoView({ behavior: 'smooth' });
}
}, [focus]);
return useMemo(() => (
<Dimmer.Dimmable as={Feed.Event} blurring dimmed={loading}>
<Dimmer.Dimmable
as={Feed.Event}
blurring
dimmed={loading}
id={`post-${postId}`}
>
<Ref innerRef={focusRef}>
<Feed.Label className="post-profile-picture">
{authorAvatarLink}
</Feed.Label>
</Ref>
<Feed.Content>
<Feed.Summary>
<div>
<Link to={`/topics/${topicId}/#post-${postId}`}>
<span className="post-summary-meta-index">
{t('post.list.row.post.id', { id: postIndexInTopic })}
{t('post.list.row.post.id', { id: postIndex })}
</span>
</div>
</Link>
{postAuthor !== null && setPostAuthorAddress !== null && timeAgo !== null
? (
<>
@ -147,19 +162,22 @@ const PostListRow = (props) => {
</Feed.Content>
</Dimmer.Dimmable>
), [
authorAvatarLink, loading, postAuthor, postAuthorAddress, postContent, postIndexInTopic, t, timeAgo,
authorAvatarLink, focusRef, loading, postAuthor, postAuthorAddress, postContent, postId, postIndex, t, timeAgo,
topicId,
]);
};
PostListRow.defaultProps = {
loading: false,
focus: false,
};
PostListRow.propTypes = {
id: PropTypes.number.isRequired,
postIndexInTopic: PropTypes.number.isRequired,
postIndex: PropTypes.number.isRequired,
postCallHash: PropTypes.string,
loading: PropTypes.bool,
focus: PropTypes.bool,
};
export default memo(PostListRow);

8
packages/concordia-app/src/components/PostList/index.jsx

@ -11,7 +11,7 @@ import { FORUM_CONTRACT } from '../../constants/contracts/ContractNames';
const { contracts: { [FORUM_CONTRACT]: { methods: { getPost: { cacheCall: getPostChainData } } } } } = drizzle;
const PostList = (props) => {
const { postIds, loading } = props;
const { postIds, loading, focusOnPost } = props;
const [getPostCallHashes, setGetPostCallHashes] = useState([]);
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
@ -47,14 +47,15 @@ const PostList = (props) => {
return (
<PostListRow
id={postId}
postIndexInTopic={index + 1}
postIndex={index + 1}
key={postId}
postCallHash={postHash && postHash.hash}
loading={postHash === undefined}
focus={postId === focusOnPost}
/>
);
});
}, [getPostCallHashes, loading, postIds]);
}, [focusOnPost, getPostCallHashes, loading, postIds]);
return (
<Dimmer.Dimmable as={Feed} blurring dimmed={loading} id="post-list" size="large">
@ -67,6 +68,7 @@ const PostList = (props) => {
PostList.propTypes = {
postIds: PropTypes.arrayOf(PropTypes.number).isRequired,
loading: PropTypes.bool,
focusOnPost: PropTypes.number,
};
export default PostList;

12
packages/concordia-app/src/views/Home/index.jsx

@ -8,20 +8,20 @@ import './styles.css';
import { drizzle } from '../../redux/store';
import { FORUM_CONTRACT } from '../../constants/contracts/ContractNames';
const { contracts: { [FORUM_CONTRACT]: { methods: { getNumberOfTopics } } } } = drizzle;
const { contracts: { [FORUM_CONTRACT]: { methods: { numTopics } } } } = drizzle;
const Home = () => {
const [numberOfTopicsCallHash, setNumberOfTopicsCallHash] = useState('');
const getNumberOfTopicsResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getNumberOfTopics);
const numTopicsResults = useSelector((state) => state.contracts[FORUM_CONTRACT].numTopics);
useEffect(() => {
setNumberOfTopicsCallHash(getNumberOfTopics.cacheCall());
setNumberOfTopicsCallHash(numTopics.cacheCall());
}, []);
const numberOfTopics = useMemo(() => (getNumberOfTopicsResults[numberOfTopicsCallHash] !== undefined
? parseInt(getNumberOfTopicsResults[numberOfTopicsCallHash].value, 10)
const numberOfTopics = useMemo(() => (numTopicsResults[numberOfTopicsCallHash] !== undefined
? parseInt(numTopicsResults[numberOfTopicsCallHash].value, 10)
: null),
[getNumberOfTopicsResults, numberOfTopicsCallHash]);
[numTopicsResults, numberOfTopicsCallHash]);
return useMemo(() => (
<Container id="home-container" textAlign="center">

5
packages/concordia-app/src/views/Topic/TopicView/index.jsx

@ -24,7 +24,7 @@ const { orbit } = breeze;
const TopicView = (props) => {
const {
topicId, topicAuthorAddress: initialTopicAuthorAddress, topicAuthor: initialTopicAuthor,
timestamp: initialTimestamp, postIds: initialPostIds,
timestamp: initialTimestamp, postIds: initialPostIds, focusOnPost,
} = props;
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
@ -174,7 +174,7 @@ const TopicView = (props) => {
</Step>
</Step.Group>
</Dimmer.Dimmable>
<PostList postIds={postIds || []} loading={postIds === null} />
<PostList postIds={postIds || []} loading={postIds === null} focusOnPost={focusOnPost} />
{topicSubject !== null && postIds !== null && hasSignedUp && (
<PostCreate
topicId={topicId}
@ -192,6 +192,7 @@ TopicView.propTypes = {
topicAuthor: PropTypes.string,
timestamp: PropTypes.number,
postIds: PropTypes.arrayOf(PropTypes.number),
focusOnPost: PropTypes.number,
};
export default TopicView;

8
packages/concordia-app/src/views/Topic/index.jsx

@ -1,18 +1,22 @@
import React from 'react';
import { useRouteMatch } from 'react-router';
import { useLocation, useRouteMatch } from 'react-router';
import TopicCreate from './TopicCreate';
import TopicView from './TopicView';
const Topic = () => {
const match = useRouteMatch();
const { id: topicId } = match.params;
const location = useLocation();
const postHash = location.hash;
const postId = postHash ? postHash.substring('#post-'.length) : null;
const focusPostId = postId ? parseInt(postId, 10) : null;
return topicId === 'new'
? (
<TopicCreate />
)
: (
<TopicView topicId={parseInt(topicId, 10)} />
<TopicView topicId={parseInt(topicId, 10)} focusOnPost={focusPostId} />
);
};

4
packages/concordia-contracts/.eslintrc.js

@ -31,7 +31,9 @@ module.exports = {
'no-unused-vars': 'warn',
'no-console': 'warn',
'no-shadow': 'warn',
"no-multi-str": "warn"
'no-multi-str': 'warn',
'one-var': ["error", { "uninitialized": "always" }],
'one-var-declaration-per-line': ['error', 'initializations']
},
'settings': {
'import/resolver': {

8
packages/concordia-contracts/.solhint.json

@ -1,3 +1,9 @@
{
"extends": "solhint:default"
"extends": "solhint:recommended",
"rules": {
"compiler-version": ["error","~0.8.0"],
"func-visibility": ["warn",{"ignoreConstructors" : true}],
"not-rely-on-time": "off",
"state-visibility": "off"
}
}

58
packages/concordia-contracts/contracts/Forum.sol

@ -1,8 +1,12 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.7.1;
pragma experimental ABIEncoderV2;
pragma solidity 0.8.0;
contract Forum {
// Error messages for require()
string public constant USER_HAS_NOT_SIGNED_UP = "User hasn't signed up yet.";
string public constant USERNAME_TAKEN = "Username is already taken.";
string public constant TOPIC_DOES_NOT_EXIST = "Topic doesn't exist.";
string public constant POST_DOES_NOT_EXIST = "Post doesn't exist.";
//----------------------------------------USER----------------------------------------
struct User {
@ -20,18 +24,17 @@ contract Forum {
event UsernameUpdated(string newName, string oldName, address userAddress);
function signUp(string memory username) public returns (bool) {
require (!hasUserSignedUp(msg.sender), "User has already signed up.");
require(!isUserNameTaken(username), "Username is already taken.");
users[msg.sender] = User(username,
new uint[](0), new uint[](0), block.timestamp, true);
require(!hasUserSignedUp(msg.sender), USER_HAS_NOT_SIGNED_UP);
require(!isUserNameTaken(username), USERNAME_TAKEN);
users[msg.sender] = User(username, new uint[](0), new uint[](0), block.timestamp, true);
userAddresses[username] = msg.sender;
emit UserSignedUp(username, msg.sender);
return true;
}
function updateUsername(string memory newUsername) public returns (bool) {
require (hasUserSignedUp(msg.sender), "User hasn't signed up yet.");
require(!isUserNameTaken(newUsername), "Username is already taken.");
require(hasUserSignedUp(msg.sender), USER_HAS_NOT_SIGNED_UP);
require(!isUserNameTaken(newUsername), USERNAME_TAKEN);
string memory oldUsername = getUsername(msg.sender);
delete userAddresses[users[msg.sender].username];
users[msg.sender].username = newUsername;
@ -41,7 +44,7 @@ contract Forum {
}
function getUsername(address userAddress) public view returns (string memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up yet.");
require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
return users[userAddress].username;
}
@ -60,22 +63,22 @@ contract Forum {
}
function getUserTopics(address userAddress) public view returns (uint[] memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up yet.");
require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
return users[userAddress].topicIDs;
}
function getUserPosts(address userAddress) public view returns (uint[] memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up yet.");
require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
return users[userAddress].postIDs;
}
function getUserDateOfRegister(address userAddress) public view returns (uint) {
require (hasUserSignedUp(userAddress), "User hasn't signed up yet.");
require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
return users[userAddress].timestamp;
}
function getUser(address userAddress) public view returns (User memory) {
require(hasUserSignedUp(userAddress), "User hasn't signed up yet.");
require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
return users[userAddress];
}
@ -94,8 +97,8 @@ contract Forum {
uint topicID;
}
uint numTopics; // Total number of topics
uint numPosts; // Total number of posts
uint public numTopics; // Total number of topics
uint public numPosts; // Total number of posts
mapping(uint => Topic) topics;
mapping(uint => Post) posts;
@ -104,7 +107,7 @@ contract Forum {
event PostCreated(uint postID, uint topicID);
function createTopic() public returns (uint, uint) {
require(hasUserSignedUp(msg.sender)); // Only registered users can create topics
require(hasUserSignedUp(msg.sender), USER_HAS_NOT_SIGNED_UP);
//Creates topic
uint topicID = numTopics++;
topics[topicID] = Topic(topicID, msg.sender, block.timestamp, new uint[](0));
@ -121,8 +124,8 @@ 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(hasUserSignedUp(msg.sender), USER_HAS_NOT_SIGNED_UP);
require(topicExists(topicID), TOPIC_DOES_NOT_EXIST);
uint postID = numPosts++;
posts[postID] = Post(postID, msg.sender, block.timestamp, topicID);
topics[topicID].postIDs.push(postID);
@ -131,12 +134,16 @@ contract Forum {
return postID;
}
function getNumberOfTopics() public view returns (uint) {
return numTopics;
function topicExists(uint topicID) public view returns (bool) {
return topicID < numTopics;
}
function postExists(uint postID) public view returns (bool) {
return postID < numPosts;
}
function getTopic(uint topicID) public view returns (address, string memory, uint, uint[] memory) {
require(topicID<numTopics);
require(topicExists(topicID), TOPIC_DOES_NOT_EXIST);
return (
topics[topicID].author,
users[topics[topicID].author].username,
@ -146,12 +153,17 @@ contract Forum {
}
function getTopicPosts(uint topicID) public view returns (uint[] memory) {
require(topicID<numTopics); // Topic should exist
require(topicExists(topicID), TOPIC_DOES_NOT_EXIST);
return topics[topicID].postIDs;
}
function getTopicAuthor(uint topicID) public view returns (address) {
require(topicExists(topicID), TOPIC_DOES_NOT_EXIST);
return topics[topicID].author;
}
function getPost(uint postID) public view returns (address, string memory, uint, uint) {
require(postID<numPosts);
require(postExists(postID), POST_DOES_NOT_EXIST);
return (
posts[postID].author,
users[posts[postID].author].username,

12
packages/concordia-contracts/contracts/Migrations.sol

@ -1,9 +1,9 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.7.1;
pragma solidity 0.8.0;
contract Migrations {
address public owner;
uint public last_completed_migration;
uint public lastCompletedMigration;
constructor() {
owner = msg.sender;
@ -14,11 +14,11 @@ contract Migrations {
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
lastCompletedMigration = completed;
}
function upgrade(address new_address) public restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
function upgrade(address newAddress) public restricted {
Migrations upgraded = Migrations(newAddress);
upgraded.setCompleted(lastCompletedMigration);
}
}

109
packages/concordia-contracts/contracts/PostVoting.sol

@ -0,0 +1,109 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
import "./Forum.sol";
contract PostVoting {
Forum public forum;
constructor(Forum addr) {
forum = Forum(addr);
}
enum Option { DEFAULT, UP, DOWN } // DEFAULT -> 0, UP -> 1, DOWN -> 2
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), forum.POST_DOES_NOT_EXIST());
return postBallots[postID].votes[voter];
}
// Gets vote count for a specific option (Option.UP/ Option.DOWN only!)
function getVoteCount(uint postID, Option option) private view returns (uint) {
require(forum.postExists(postID), forum.POST_DOES_NOT_EXIST());
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), forum.POST_DOES_NOT_EXIST());
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), forum.USER_HAS_NOT_SIGNED_UP());
require(forum.postExists(postID), forum.POST_DOES_NOT_EXIST());
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), forum.USER_HAS_NOT_SIGNED_UP());
require(forum.postExists(postID), forum.POST_DOES_NOT_EXIST());
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.DEFAULT) {
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.DEFAULT)
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.DEFAULT);
}
}

155
packages/concordia-contracts/contracts/Voting.sol

@ -0,0 +1,155 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
import "./Forum.sol";
contract Voting {
// Error messages for require()
string constant TOPIC_POLL_DIFFERENT_CREATOR = "Only topic's author can create a poll.";
string constant POLL_EXISTS = "Poll already exists.";
string constant POLL_DOES_NOT_EXIST = "Poll does not exist.";
string constant INVALID_OPTION = "Invalid option.";
string constant USER_HAS_NOT_VOTED = "User hasn't voted.";
Forum public forum;
constructor(Forum addr) {
forum = Forum(addr);
}
struct Poll {
uint topicID;
uint numOptions;
string dataHash;
mapping(address => uint) votes;
mapping(uint => address[]) voters;
bool enableVoteChanges;
uint timestamp;
}
mapping(uint => Poll) polls;
event PollCreated(uint topicID);
event UserVotedPoll(address userAddress, uint topicID, uint vote);
function pollExists(uint topicID) public view returns (bool) {
if (polls[topicID].timestamp != 0)
return true;
return false;
}
function createPoll(uint topicID, uint numOptions, string memory dataHash, bool enableVoteChanges) public returns (uint) {
require(forum.hasUserSignedUp(msg.sender), forum.USER_HAS_NOT_SIGNED_UP());
require(forum.topicExists(topicID), forum.TOPIC_DOES_NOT_EXIST());
require(forum.getTopicAuthor(topicID) == msg.sender, TOPIC_POLL_DIFFERENT_CREATOR);
require(!pollExists(topicID), POLL_EXISTS);
Poll storage poll = polls[topicID];
poll.topicID = topicID;
poll.numOptions = numOptions;
poll.dataHash = dataHash;
poll.enableVoteChanges = enableVoteChanges;
poll.timestamp = block.timestamp;
emit PollCreated(topicID);
return topicID;
}
function getPollInfo(uint topicID) public view returns (uint, string memory, uint, uint) {
require(pollExists(topicID), POLL_DOES_NOT_EXIST);
uint totalVotes = getTotalVotes(topicID);
return (
polls[topicID].numOptions,
polls[topicID].dataHash,
polls[topicID].timestamp,
totalVotes
);
}
function isOptionValid(uint topicID, uint option) public view returns (bool) {
require(pollExists(topicID), POLL_DOES_NOT_EXIST);
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(pollExists(topicID), POLL_DOES_NOT_EXIST);
if (polls[topicID].votes[voter] != 0)
return true;
return false;
}
function getVote(uint topicID, address voter) public view returns (uint) {
require(hasVoted(topicID, voter), USER_HAS_NOT_VOTED);
return polls[topicID].votes[voter];
}
function getVoteCount(uint topicID, uint option) public view returns (uint) {
require(pollExists(topicID), POLL_DOES_NOT_EXIST);
require(isOptionValid(topicID, option), INVALID_OPTION);
return (polls[topicID].voters[option].length);
}
function getTotalVotes(uint topicID) public view returns (uint) {
require(pollExists(topicID), POLL_DOES_NOT_EXIST);
Poll storage poll = polls[topicID];
uint totalVotes = 0;
for (uint pollOption = 1; pollOption <= poll.numOptions; pollOption++)
totalVotes += poll.voters[pollOption].length;
return totalVotes;
}
// Gets voters for a specific option
function getVoters(uint topicID, uint option) public view returns (address[] memory) {
require(pollExists(topicID), POLL_DOES_NOT_EXIST);
return (polls[topicID].voters[option]);
}
function getVoterIndex(uint topicID, address voter) public view returns (uint) {
require(pollExists(topicID), POLL_DOES_NOT_EXIST);
require(hasVoted(topicID, voter), USER_HAS_NOT_VOTED);
Poll storage poll = polls[topicID];
uint votedOption = getVote(topicID, voter);
address[] storage optionVoters = poll.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 topicID, uint option) public {
require(forum.hasUserSignedUp(msg.sender), forum.USER_HAS_NOT_SIGNED_UP());
require(pollExists(topicID), POLL_DOES_NOT_EXIST);
require(isOptionValid(topicID, option), INVALID_OPTION);
Poll storage poll = polls[topicID];
address voter = msg.sender;
uint prevOption = poll.votes[voter];
if (prevOption == option)
return;
// Voter hasn't voted before
if (prevOption == 0) {
poll.voters[option].push(voter);
poll.votes[voter] = option;
emit UserVotedPoll(voter, topicID, option);
}
else if (poll.enableVoteChanges) {
uint voterIndex = getVoterIndex(topicID, voter);
// Swap with last voter address and delete vote
poll.voters[prevOption][voterIndex] = poll.voters[prevOption][poll.voters[prevOption].length - 1];
poll.voters[prevOption].pop();
if (option != 0)
poll.voters[option].push(voter);
poll.votes[voter] = option;
emit UserVotedPoll(voter, topicID, option);
}
}
}

8
packages/concordia-contracts/index.js

@ -1,14 +1,16 @@
let Forum;
let Forum, Voting, PostVoting;
/* eslint-disable global-require */
try {
// eslint-disable-next-line global-require
Forum = require('./build/Forum.json');
Voting = require('./build/Voting.json');
PostVoting = require('./build/PostVoting.json');
} catch (e) {
// eslint-disable-next-line no-console
console.error("Could not require contract artifacts. Haven't you run compile yet?");
}
module.exports = {
contracts: [Forum],
contracts: [Forum, Voting, PostVoting],
forumContract: Forum,
};

8
packages/concordia-contracts/migrations/2_deploy_contracts.js

@ -1,6 +1,12 @@
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);
return deployer.deploy(Forum)
.then(async (forum) => Promise.all([
deployer.deploy(Voting, forum.address),
deployer.deploy(PostVoting, forum.address),
]));
};

2
packages/concordia-contracts/package.json

@ -16,7 +16,7 @@
},
"dependencies": {
"@openzeppelin/contracts": "~3.2.0",
"truffle": "~5.1.45"
"truffle": "~5.1.55"
},
"devDependencies": {
"eslint": "^6.8.0",

112
packages/concordia-contracts/test/TestVoting.sol

@ -0,0 +1,112 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Forum.sol";
import "../contracts/Voting.sol";
contract TestVoting {
Forum forum;
uint firstTopicId;
function beforeAll() public {
forum = Forum(DeployedAddresses.Forum());
forum.signUp("testAccount");
(firstTopicId,) = forum.createTopic();
}
function testIfPollExists() public {
Voting voting = Voting(DeployedAddresses.Voting());
bool actual = voting.pollExists(firstTopicId);
Assert.equal(actual, false, "Poll should not exist");
}
function testCreatePoll() public {
Voting voting = Voting(DeployedAddresses.Voting());
uint actual = voting.createPoll(firstTopicId, 3, "asdf", false);
Assert.equal(actual, firstTopicId, "Topic Id should be 1");
}
function testGetTotalVotes() public {
Voting voting = Voting(DeployedAddresses.Voting());
uint actual = voting.getTotalVotes(firstTopicId);
Assert.equal(actual, 0, "Topic Id should be 0");
}
function testGetPollInfo() public {
Voting voting = Voting(DeployedAddresses.Voting());
(uint actualNumberOfOptions, string memory actualDataHash, , uint actualNumberOfVotes) = voting.getPollInfo(firstTopicId);
Assert.equal(actualNumberOfOptions, 3, "Number of votes should be 0");
Assert.equal(actualDataHash, "asdf", "Number of votes should be 0");
Assert.equal(actualNumberOfVotes, 0, "Number of votes should be 0");
}
function testVote() public {
Voting voting = Voting(DeployedAddresses.Voting());
voting.vote(firstTopicId, 1);
uint votesActual = voting.getTotalVotes(firstTopicId);
Assert.equal(votesActual, 1, "Number of votes should be 1");
}
function testGetVoteCount() public {
Voting voting = Voting(DeployedAddresses.Voting());
uint actualVotesOption0 = voting.getVoteCount(firstTopicId, 1);
uint actualVotesOption1 = voting.getVoteCount(firstTopicId, 2);
uint actualVotesOption2 = voting.getVoteCount(firstTopicId, 3);
Assert.equal(actualVotesOption0, 1, "Vote count is not correct");
Assert.equal(actualVotesOption1, 0, "Vote count is not correct");
Assert.equal(actualVotesOption2, 0, "Vote count is not correct");
}
function testChangeVoteWhenDisabled() public {
Voting voting = Voting(DeployedAddresses.Voting());
(uint topicId,) = forum.createTopic();
voting.createPoll(topicId, 3, "asdf", false);
voting.vote(topicId, 1);
uint actualVotesOption0 = voting.getVoteCount(topicId, 1);
uint actualVotesOption1 = voting.getVoteCount(topicId, 2);
voting.vote(topicId, 2);
uint actualVotesOption2 = voting.getVoteCount(topicId, 1);
uint actualVotesOption3 = voting.getVoteCount(topicId, 2);
Assert.equal(actualVotesOption0, 1, "Number of votes should be 1");
Assert.equal(actualVotesOption1, 0, "Number of votes should be 0");
Assert.equal(actualVotesOption2, 1, "Number of votes should be 1");
Assert.equal(actualVotesOption3, 0, "Number of votes should be 0");
}
function testChangeVoteWhenEnabled() public {
Voting voting = Voting(DeployedAddresses.Voting());
(uint topicId,) = forum.createTopic();
voting.createPoll(topicId, 3, "asdf", true);
voting.vote(topicId, 1);
uint actualVotesOption0 = voting.getVoteCount(topicId, 1);
uint actualVotesOption1 = voting.getVoteCount(topicId, 2);
voting.vote(topicId, 2);
uint actualVotesOption2 = voting.getVoteCount(topicId, 1);
uint actualVotesOption3 = voting.getVoteCount(topicId, 2);
Assert.equal(actualVotesOption0, 1, "Number of votes should be 1");
Assert.equal(actualVotesOption1, 0, "Number of votes should be 0");
Assert.equal(actualVotesOption2, 0, "Number of votes should be 0");
Assert.equal(actualVotesOption3, 1, "Number of votes should be 1");
}
}

2
packages/concordia-contracts/truffle-config.js

@ -10,7 +10,7 @@ module.exports = {
// to customize your Truffle configuration!
compilers: {
solc: {
version: '0.7.1',
version: '0.8.0',
},
},
contracts_build_directory: path.join(__dirname, 'build/'),

969
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save