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 { body.app {
overflow: auto; overflow: auto;
margin: 1em !important; margin: 0;
} }
#root { #root {
@ -18,4 +18,5 @@ body.app {
.ui.inverted.menu { .ui.inverted.menu {
background: #0B2540; background: #0B2540;
border-radius: 0;
} }

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

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

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

@ -1,8 +1,8 @@
import React, { import React, {
memo, useEffect, useMemo, useState, memo, useEffect, useMemo, useState, useCallback,
} from 'react'; } from 'react';
import { import {
Dimmer, Icon, Image, Feed, Placeholder, Dimmer, Icon, Image, Feed, Placeholder, Ref,
} from 'semantic-ui-react'; } from 'semantic-ui-react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -22,12 +22,13 @@ const { orbit } = breeze;
const PostListRow = (props) => { const PostListRow = (props) => {
const { const {
id: postId, postIndexInTopic, postCallHash, loading, id: postId, postIndex, postCallHash, loading, focus,
} = props; } = props;
const getPostResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getPost); const getPostResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getPost);
const [postAuthorAddress, setPostAuthorAddress] = useState(null); const [postAuthorAddress, setPostAuthorAddress] = useState(null);
const [postAuthor, setPostAuthor] = useState(null); const [postAuthor, setPostAuthor] = useState(null);
const [timeAgo, setTimeAgo] = useState(null); const [timeAgo, setTimeAgo] = useState(null);
const [topicId, setTopicId] = useState(null);
const [postContent, setPostContent] = useState(null); const [postContent, setPostContent] = useState(null);
const [postAuthorMeta, setPostAuthorMeta] = useState(null); const [postAuthorMeta, setPostAuthorMeta] = useState(null);
const userAddress = useSelector((state) => state.user.address); const userAddress = useSelector((state) => state.user.address);
@ -41,6 +42,7 @@ const PostListRow = (props) => {
setPostAuthorAddress(getPostResults[postCallHash].value[0]); setPostAuthorAddress(getPostResults[postCallHash].value[0]);
setPostAuthor(getPostResults[postCallHash].value[1]); setPostAuthor(getPostResults[postCallHash].value[1]);
setTimeAgo(getPostResults[postCallHash].value[2] * 1000); setTimeAgo(getPostResults[postCallHash].value[2] * 1000);
setTopicId(getPostResults[postCallHash].value[3]);
} }
}, [getPostResults, loading, postCallHash]); }, [getPostResults, loading, postCallHash]);
@ -116,18 +118,31 @@ const PostListRow = (props) => {
return authorAvatar; return authorAvatar;
}, [authorAvatar, postAuthorAddress]); }, [authorAvatar, postAuthorAddress]);
const focusRef = useCallback((node) => {
if (focus && node !== null) {
node.scrollIntoView({ behavior: 'smooth' });
}
}, [focus]);
return useMemo(() => ( 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"> <Feed.Label className="post-profile-picture">
{authorAvatarLink} {authorAvatarLink}
</Feed.Label> </Feed.Label>
</Ref>
<Feed.Content> <Feed.Content>
<Feed.Summary> <Feed.Summary>
<div> <Link to={`/topics/${topicId}/#post-${postId}`}>
<span className="post-summary-meta-index"> <span className="post-summary-meta-index">
{t('post.list.row.post.id', { id: postIndexInTopic })} {t('post.list.row.post.id', { id: postIndex })}
</span> </span>
</div> </Link>
{postAuthor !== null && setPostAuthorAddress !== null && timeAgo !== null {postAuthor !== null && setPostAuthorAddress !== null && timeAgo !== null
? ( ? (
<> <>
@ -147,19 +162,22 @@ const PostListRow = (props) => {
</Feed.Content> </Feed.Content>
</Dimmer.Dimmable> </Dimmer.Dimmable>
), [ ), [
authorAvatarLink, loading, postAuthor, postAuthorAddress, postContent, postIndexInTopic, t, timeAgo, authorAvatarLink, focusRef, loading, postAuthor, postAuthorAddress, postContent, postId, postIndex, t, timeAgo,
topicId,
]); ]);
}; };
PostListRow.defaultProps = { PostListRow.defaultProps = {
loading: false, loading: false,
focus: false,
}; };
PostListRow.propTypes = { PostListRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
postIndexInTopic: PropTypes.number.isRequired, postIndex: PropTypes.number.isRequired,
postCallHash: PropTypes.string, postCallHash: PropTypes.string,
loading: PropTypes.bool, loading: PropTypes.bool,
focus: PropTypes.bool,
}; };
export default memo(PostListRow); 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 { contracts: { [FORUM_CONTRACT]: { methods: { getPost: { cacheCall: getPostChainData } } } } } = drizzle;
const PostList = (props) => { const PostList = (props) => {
const { postIds, loading } = props; const { postIds, loading, focusOnPost } = props;
const [getPostCallHashes, setGetPostCallHashes] = useState([]); const [getPostCallHashes, setGetPostCallHashes] = useState([]);
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized); const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed); const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
@ -47,14 +47,15 @@ const PostList = (props) => {
return ( return (
<PostListRow <PostListRow
id={postId} id={postId}
postIndexInTopic={index + 1} postIndex={index + 1}
key={postId} key={postId}
postCallHash={postHash && postHash.hash} postCallHash={postHash && postHash.hash}
loading={postHash === undefined} loading={postHash === undefined}
focus={postId === focusOnPost}
/> />
); );
}); });
}, [getPostCallHashes, loading, postIds]); }, [focusOnPost, getPostCallHashes, loading, postIds]);
return ( return (
<Dimmer.Dimmable as={Feed} blurring dimmed={loading} id="post-list" size="large"> <Dimmer.Dimmable as={Feed} blurring dimmed={loading} id="post-list" size="large">
@ -67,6 +68,7 @@ const PostList = (props) => {
PostList.propTypes = { PostList.propTypes = {
postIds: PropTypes.arrayOf(PropTypes.number).isRequired, postIds: PropTypes.arrayOf(PropTypes.number).isRequired,
loading: PropTypes.bool, loading: PropTypes.bool,
focusOnPost: PropTypes.number,
}; };
export default PostList; 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 { drizzle } from '../../redux/store';
import { FORUM_CONTRACT } from '../../constants/contracts/ContractNames'; import { FORUM_CONTRACT } from '../../constants/contracts/ContractNames';
const { contracts: { [FORUM_CONTRACT]: { methods: { getNumberOfTopics } } } } = drizzle; const { contracts: { [FORUM_CONTRACT]: { methods: { numTopics } } } } = drizzle;
const Home = () => { const Home = () => {
const [numberOfTopicsCallHash, setNumberOfTopicsCallHash] = useState(''); const [numberOfTopicsCallHash, setNumberOfTopicsCallHash] = useState('');
const getNumberOfTopicsResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getNumberOfTopics); const numTopicsResults = useSelector((state) => state.contracts[FORUM_CONTRACT].numTopics);
useEffect(() => { useEffect(() => {
setNumberOfTopicsCallHash(getNumberOfTopics.cacheCall()); setNumberOfTopicsCallHash(numTopics.cacheCall());
}, []); }, []);
const numberOfTopics = useMemo(() => (getNumberOfTopicsResults[numberOfTopicsCallHash] !== undefined const numberOfTopics = useMemo(() => (numTopicsResults[numberOfTopicsCallHash] !== undefined
? parseInt(getNumberOfTopicsResults[numberOfTopicsCallHash].value, 10) ? parseInt(numTopicsResults[numberOfTopicsCallHash].value, 10)
: null), : null),
[getNumberOfTopicsResults, numberOfTopicsCallHash]); [numTopicsResults, numberOfTopicsCallHash]);
return useMemo(() => ( return useMemo(() => (
<Container id="home-container" textAlign="center"> <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 TopicView = (props) => {
const { const {
topicId, topicAuthorAddress: initialTopicAuthorAddress, topicAuthor: initialTopicAuthor, topicId, topicAuthorAddress: initialTopicAuthorAddress, topicAuthor: initialTopicAuthor,
timestamp: initialTimestamp, postIds: initialPostIds, timestamp: initialTimestamp, postIds: initialPostIds, focusOnPost,
} = props; } = props;
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized); const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed); const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
@ -174,7 +174,7 @@ const TopicView = (props) => {
</Step> </Step>
</Step.Group> </Step.Group>
</Dimmer.Dimmable> </Dimmer.Dimmable>
<PostList postIds={postIds || []} loading={postIds === null} /> <PostList postIds={postIds || []} loading={postIds === null} focusOnPost={focusOnPost} />
{topicSubject !== null && postIds !== null && hasSignedUp && ( {topicSubject !== null && postIds !== null && hasSignedUp && (
<PostCreate <PostCreate
topicId={topicId} topicId={topicId}
@ -192,6 +192,7 @@ TopicView.propTypes = {
topicAuthor: PropTypes.string, topicAuthor: PropTypes.string,
timestamp: PropTypes.number, timestamp: PropTypes.number,
postIds: PropTypes.arrayOf(PropTypes.number), postIds: PropTypes.arrayOf(PropTypes.number),
focusOnPost: PropTypes.number,
}; };
export default TopicView; export default TopicView;

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

@ -1,18 +1,22 @@
import React from 'react'; import React from 'react';
import { useRouteMatch } from 'react-router'; import { useLocation, useRouteMatch } from 'react-router';
import TopicCreate from './TopicCreate'; import TopicCreate from './TopicCreate';
import TopicView from './TopicView'; import TopicView from './TopicView';
const Topic = () => { const Topic = () => {
const match = useRouteMatch(); const match = useRouteMatch();
const { id: topicId } = match.params; 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' return topicId === 'new'
? ( ? (
<TopicCreate /> <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-unused-vars': 'warn',
'no-console': 'warn', 'no-console': 'warn',
'no-shadow': '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': { 'settings': {
'import/resolver': { '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 //SPDX-License-Identifier: MIT
pragma solidity 0.7.1; pragma solidity 0.8.0;
pragma experimental ABIEncoderV2;
contract Forum { 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---------------------------------------- //----------------------------------------USER----------------------------------------
struct User { struct User {
@ -20,18 +24,17 @@ contract Forum {
event UsernameUpdated(string newName, string oldName, address userAddress); event UsernameUpdated(string newName, string oldName, address userAddress);
function signUp(string memory username) public returns (bool) { function signUp(string memory username) public returns (bool) {
require (!hasUserSignedUp(msg.sender), "User has already signed up."); require(!hasUserSignedUp(msg.sender), USER_HAS_NOT_SIGNED_UP);
require(!isUserNameTaken(username), "Username is already taken."); require(!isUserNameTaken(username), USERNAME_TAKEN);
users[msg.sender] = User(username, users[msg.sender] = User(username, new uint[](0), new uint[](0), block.timestamp, true);
new uint[](0), new uint[](0), block.timestamp, true);
userAddresses[username] = msg.sender; userAddresses[username] = msg.sender;
emit UserSignedUp(username, msg.sender); emit UserSignedUp(username, msg.sender);
return true; return true;
} }
function updateUsername(string memory newUsername) public returns (bool) { function updateUsername(string memory newUsername) public returns (bool) {
require (hasUserSignedUp(msg.sender), "User hasn't signed up yet."); require(hasUserSignedUp(msg.sender), USER_HAS_NOT_SIGNED_UP);
require(!isUserNameTaken(newUsername), "Username is already taken."); require(!isUserNameTaken(newUsername), USERNAME_TAKEN);
string memory oldUsername = getUsername(msg.sender); string memory oldUsername = getUsername(msg.sender);
delete userAddresses[users[msg.sender].username]; delete userAddresses[users[msg.sender].username];
users[msg.sender].username = newUsername; users[msg.sender].username = newUsername;
@ -41,7 +44,7 @@ contract Forum {
} }
function getUsername(address userAddress) public view returns (string memory) { 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; return users[userAddress].username;
} }
@ -60,22 +63,22 @@ contract Forum {
} }
function getUserTopics(address userAddress) public view returns (uint[] memory) { 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; return users[userAddress].topicIDs;
} }
function getUserPosts(address userAddress) public view returns (uint[] memory) { 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; return users[userAddress].postIDs;
} }
function getUserDateOfRegister(address userAddress) public view returns (uint) { 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; return users[userAddress].timestamp;
} }
function getUser(address userAddress) public view returns (User memory) { 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]; return users[userAddress];
} }
@ -94,8 +97,8 @@ contract Forum {
uint topicID; uint topicID;
} }
uint numTopics; // Total number of topics uint public numTopics; // Total number of topics
uint numPosts; // Total number of posts uint public numPosts; // Total number of posts
mapping(uint => Topic) topics; mapping(uint => Topic) topics;
mapping(uint => Post) posts; mapping(uint => Post) posts;
@ -104,7 +107,7 @@ contract Forum {
event PostCreated(uint postID, uint topicID); event PostCreated(uint postID, uint topicID);
function createTopic() public returns (uint, uint) { 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 //Creates topic
uint topicID = numTopics++; uint topicID = numTopics++;
topics[topicID] = Topic(topicID, msg.sender, block.timestamp, new uint[](0)); topics[topicID] = Topic(topicID, msg.sender, block.timestamp, new uint[](0));
@ -121,8 +124,8 @@ contract Forum {
} }
function createPost(uint topicID) public returns (uint) { function createPost(uint topicID) public returns (uint) {
require(hasUserSignedUp(msg.sender)); // Only registered users can create posts require(hasUserSignedUp(msg.sender), USER_HAS_NOT_SIGNED_UP);
require(topicID<numTopics); // Only allow posting to a topic that exists require(topicExists(topicID), TOPIC_DOES_NOT_EXIST);
uint postID = numPosts++; uint postID = numPosts++;
posts[postID] = Post(postID, msg.sender, block.timestamp, topicID); posts[postID] = Post(postID, msg.sender, block.timestamp, topicID);
topics[topicID].postIDs.push(postID); topics[topicID].postIDs.push(postID);
@ -131,12 +134,16 @@ contract Forum {
return postID; return postID;
} }
function getNumberOfTopics() public view returns (uint) { function topicExists(uint topicID) public view returns (bool) {
return numTopics; 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) { function getTopic(uint topicID) public view returns (address, string memory, uint, uint[] memory) {
require(topicID<numTopics); require(topicExists(topicID), TOPIC_DOES_NOT_EXIST);
return ( return (
topics[topicID].author, topics[topicID].author,
users[topics[topicID].author].username, users[topics[topicID].author].username,
@ -146,12 +153,17 @@ contract Forum {
} }
function getTopicPosts(uint topicID) public view returns (uint[] memory) { 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; 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) { function getPost(uint postID) public view returns (address, string memory, uint, uint) {
require(postID<numPosts); require(postExists(postID), POST_DOES_NOT_EXIST);
return ( return (
posts[postID].author, posts[postID].author,
users[posts[postID].author].username, users[posts[postID].author].username,

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

@ -1,9 +1,9 @@
//SPDX-License-Identifier: MIT //SPDX-License-Identifier: MIT
pragma solidity 0.7.1; pragma solidity 0.8.0;
contract Migrations { contract Migrations {
address public owner; address public owner;
uint public last_completed_migration; uint public lastCompletedMigration;
constructor() { constructor() {
owner = msg.sender; owner = msg.sender;
@ -14,11 +14,11 @@ contract Migrations {
} }
function setCompleted(uint completed) public restricted { function setCompleted(uint completed) public restricted {
last_completed_migration = completed; lastCompletedMigration = completed;
} }
function upgrade(address new_address) public restricted { function upgrade(address newAddress) public restricted {
Migrations upgraded = Migrations(new_address); Migrations upgraded = Migrations(newAddress);
upgraded.setCompleted(last_completed_migration); 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 { try {
// eslint-disable-next-line global-require
Forum = require('./build/Forum.json'); Forum = require('./build/Forum.json');
Voting = require('./build/Voting.json');
PostVoting = require('./build/PostVoting.json');
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error("Could not require contract artifacts. Haven't you run compile yet?"); console.error("Could not require contract artifacts. Haven't you run compile yet?");
} }
module.exports = { module.exports = {
contracts: [Forum], contracts: [Forum, Voting, PostVoting],
forumContract: Forum, forumContract: Forum,
}; };

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

@ -1,6 +1,12 @@
const Forum = artifacts.require('Forum'); const Forum = artifacts.require('Forum');
const Voting = artifacts.require('Voting');
const PostVoting = artifacts.require('PostVoting');
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
module.exports = function (deployer) { 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": { "dependencies": {
"@openzeppelin/contracts": "~3.2.0", "@openzeppelin/contracts": "~3.2.0",
"truffle": "~5.1.45" "truffle": "~5.1.55"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^6.8.0", "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! // to customize your Truffle configuration!
compilers: { compilers: {
solc: { solc: {
version: '0.7.1', version: '0.8.0',
}, },
}, },
contracts_build_directory: path.join(__dirname, 'build/'), contracts_build_directory: path.join(__dirname, 'build/'),

969
yarn.lock

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