diff --git a/app/package.json b/app/package.json index 5edff98..61f6601 100644 --- a/app/package.json +++ b/app/package.json @@ -13,6 +13,8 @@ "connected-react-router": "^6.3.1", "drizzle": "^1.3.3", "history": "^4.7.2", + "ipfs": "^0.34.4", + "orbit-db": "^0.19.9", "prop-types": "^15.7.2", "react": "^16.8.1", "react-dom": "^16.8.1", diff --git a/app/src/config/ipfsOptions.js b/app/src/config/ipfsOptions.js new file mode 100644 index 0000000..19a4f94 --- /dev/null +++ b/app/src/config/ipfsOptions.js @@ -0,0 +1,13 @@ +// OrbitDB uses Pubsub which is an experimental feature and need to be turned on manually. +const ipfsOptions = { + EXPERIMENTAL: { + pubsub: true + }, config: { + Addresses: { + Swarm: [ + ] + } + }, +}; + +export default ipfsOptions; \ No newline at end of file diff --git a/app/src/containers/SignUpContainer.js b/app/src/containers/SignUpContainer.js index 743f2ed..13d4b66 100644 --- a/app/src/containers/SignUpContainer.js +++ b/app/src/containers/SignUpContainer.js @@ -1,8 +1,6 @@ import React, { Component } from 'react'; import { Header } from 'semantic-ui-react'; -import {bindActionCreators} from "redux"; -import {push} from "connected-react-router"; import {connect} from "react-redux"; class SignUp extends Component { @@ -29,10 +27,6 @@ class SignUp extends Component { ); } } -const mapDispatchToProps = dispatch => bindActionCreators({ - navigateTo: () => push() -}, dispatch); - const mapStateToProps = state => { return { @@ -40,7 +34,7 @@ const mapStateToProps = state => { } }; -const SignUpContainer = connect(mapStateToProps, mapDispatchToProps)(SignUp); +const SignUpContainer = connect(mapStateToProps)(SignUp); export default SignUpContainer; diff --git a/app/src/index.js b/app/src/index.js index c7f936b..b909cf3 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -4,11 +4,14 @@ import { Provider } from 'react-redux'; import { ConnectedRouter } from 'connected-react-router' import store, {history} from './redux/store'; -import routes from './routes' +import routes from './router/routes' +import { initIPFS } from './orbit' import * as serviceWorker from './utils/serviceWorker'; import './assets/css/index.css'; +initIPFS(); + render( diff --git a/app/src/orbit.js b/app/src/orbit.js new file mode 100644 index 0000000..8cb5090 --- /dev/null +++ b/app/src/orbit.js @@ -0,0 +1,55 @@ +import IPFS from 'ipfs'; +import OrbitDB from 'orbit-db'; +import Keystore from 'orbit-db-keystore'; +import path from 'path'; +import store from './redux/store'; +import ipfsOptions from './config/ipfsOptions' + +let ipfs, orbitdb, topicsDB, postsDB; + +function initIPFS(){ + ipfs = new IPFS(ipfsOptions); + ipfs.on('ready', async () => { + store.dispatch({type: "IPFS_INITIALIZED"}); + }); +} + +async function createDatabases() { + orbitdb = new OrbitDB(ipfs); + topicsDB = await orbitdb.keyvalue('topics'); + postsDB = await orbitdb.keyvalue('posts'); + store.dispatch({ + type: "DATABASES_CREATED", + orbitdb: orbitdb, + topicsDB: topicsDB, + postsDB: postsDB, + id: orbitdb.id + }); + return {id: orbitdb.id, topicsDB: topicsDB.address.root, postsDB: postsDB.address.root, + publicKey: orbitdb.key.getPublic('hex'), privateKey:orbitdb.key.getPrivate('hex')}; +} + +async function loadDatabases(id,mTopicsDB, mPostsDB,publicKey,privateKey) { + let directory = "./orbitdb"; + let keystore = Keystore.create(path.join(directory, id, '/keystore')); + keystore._storage.setItem(id, JSON.stringify({ + publicKey: publicKey, + privateKey: privateKey + })); + orbitdb = new OrbitDB(ipfs,directory,{peerId:id, keystore:keystore}); + topicsDB = await orbitdb.keyvalue('/orbitdb/' + mTopicsDB +'/topics'); + postsDB = await orbitdb.keyvalue('/orbitdb/' + mPostsDB +'/posts'); + + topicsDB.load(); + postsDB.load(); + + store.dispatch({ + type: "DATABASES_LOADED", + orbitdb: orbitdb, + topicsDB: topicsDB, + postsDB: postsDB, + id: orbitdb.id + }); +} + +export { initIPFS, createDatabases, loadDatabases }; \ No newline at end of file diff --git a/app/src/redux/reducers/orbitReducer.js b/app/src/redux/reducers/orbitReducer.js new file mode 100644 index 0000000..5bd3583 --- /dev/null +++ b/app/src/redux/reducers/orbitReducer.js @@ -0,0 +1,49 @@ +const initialState = { + ipfsInitialized: false, + ready: false, + orbitdb: null, + topicsDB: null, + postsDB: null, + id: null +}; + +const orbitReducer = (state = initialState, action) => { + switch (action.type) { + case 'IPFS_INITIALIZED': + return { + ...state, + ipfsInitialized: true + }; + case 'DATABASES_CREATED': + return { + ...state, + ready: true, + orbitdb: action.orbitdb, + topicsDB: action.topicsDB, + postsDB: action.postsDB, + id: action.id + }; + case 'DATABASES_LOADED': + return { + ...state, + ready: true, + orbitdb: action.orbitdb, + topicsDB: action.topicsDB, + postsDB: action.postsDB, + id: action.id + }; + case 'DATABASES_NOT_READY': + return { + ...state, + ready: false, + orbitdb: null, + topicsDB: null, + postsDB: null, + id: null + }; + default: + return state + } +}; + +export default orbitReducer; \ No newline at end of file diff --git a/app/src/redux/reducers/rootReducer.js b/app/src/redux/reducers/rootReducer.js index 532467e..03155e9 100644 --- a/app/src/redux/reducers/rootReducer.js +++ b/app/src/redux/reducers/rootReducer.js @@ -2,9 +2,11 @@ import { combineReducers } from 'redux'; import { drizzleReducers } from 'drizzle'; import { connectRouter } from 'connected-react-router' import userReducer from './userReducer'; +import orbitReducer from "./orbitReducer"; export default (history) => combineReducers({ router: connectRouter(history), user: userReducer, + orbit: orbitReducer, ...drizzleReducers }) \ No newline at end of file diff --git a/app/src/redux/sagas/drizzleUtilsSaga.js b/app/src/redux/sagas/drizzleUtilsSaga.js new file mode 100644 index 0000000..7bd241b --- /dev/null +++ b/app/src/redux/sagas/drizzleUtilsSaga.js @@ -0,0 +1,37 @@ +import getWeb3 from '@drizzle-utils/get-web3'; +import getContractInstance from '@drizzle-utils/get-contract-instance'; +import { call, put, takeLatest, select } from 'redux-saga/effects' + +import Forum from '../../contracts/Forum'; + +const accounts = (state) => state.accounts; +let initFlag, web3, contract; + +function* init() { + if(!initFlag) { + web3 = yield call(getWeb3); + contract = yield call(getContractInstance,{ + web3, + artifact: Forum + }); + initFlag=true; + yield put({type: 'DRIZZLE_UTILS_SAGA_INITIALIZED', ...[]}); + } + else + console.warn("Attempted to reinitialize drizzleUtilsSaga!"); +} + +// If the method below proves to be problematic/ineffective (i.e. getting current account +// from state), consider getting it from @drizzle-utils/get-accounts instead +// with (yield call(getAccounts, {web3}))[0]; +function* getCurrentAccount(){ + return (yield select(accounts))[0]; +} + +function* drizzleUtilsSaga() { + yield takeLatest("DRIZZLE_INITIALIZED", init); +} + +export { web3, contract, getCurrentAccount } + +export default drizzleUtilsSaga; \ No newline at end of file diff --git a/app/src/redux/sagas/orbitSaga.js b/app/src/redux/sagas/orbitSaga.js new file mode 100644 index 0000000..e587645 --- /dev/null +++ b/app/src/redux/sagas/orbitSaga.js @@ -0,0 +1,41 @@ +import { call, put, take, takeLatest } from 'redux-saga/effects' +import { contract, getCurrentAccount} from './drizzleUtilsSaga'; +import { loadDatabases } from '../../orbit' + +let latestAccount; + +function* getOrbitDBInfo() { + yield put({type: 'ORRBIT_GETTING_INFO', ...[]}); + const account = yield call(getCurrentAccount); + if(account!==latestAccount) { + console.log("Deleting local storage.."); + localStorage.clear(); + const txObj1 = yield call(contract.methods["hasUserSignedUp"], ...[account]); + try { + const callResult = yield call(txObj1.call, {address:account}); + if(callResult) { + const txObj2 = yield call(contract.methods["getOrbitDBInfo"], ...[account]); + const info = yield call(txObj2.call, {address: account}); + //TODO: update localStorage OrbitDB stuff + yield call(loadDatabases, info[0], info[1], info[2],info[3], info[4]); + } + else + yield put({type: 'DATABASES_NOT_READY', ...[]}); + + latestAccount=account; + } + catch (error) { + console.error(error); + yield put({type: 'ORBIT_SAGA_ERROR', ...[]}); + } + + } +} + +function* orbitSaga() { + yield take("DRIZZLE_UTILS_SAGA_INITIALIZED"); + yield take('IPFS_INITIALIZED'); + yield takeLatest("ACCOUNT_CHANGED", getOrbitDBInfo); +} + +export default orbitSaga; diff --git a/app/src/redux/sagas/rootSaga.js b/app/src/redux/sagas/rootSaga.js index 6c308c5..9120498 100644 --- a/app/src/redux/sagas/rootSaga.js +++ b/app/src/redux/sagas/rootSaga.js @@ -1,9 +1,11 @@ import { all, fork } from 'redux-saga/effects' import { drizzleSagas } from 'drizzle' +import drizzleUtilsSaga from './drizzleUtilsSaga' import userSaga from './userSaga'; +import orbitSaga from "./orbitSaga"; export default function* root() { - let sagas = [...drizzleSagas, userSaga]; + let sagas = [...drizzleSagas, drizzleUtilsSaga, orbitSaga, userSaga]; yield all( sagas.map(saga => fork(saga)) ) diff --git a/app/src/redux/sagas/userSaga.js b/app/src/redux/sagas/userSaga.js index 24508be..168eb82 100644 --- a/app/src/redux/sagas/userSaga.js +++ b/app/src/redux/sagas/userSaga.js @@ -1,65 +1,43 @@ -import getWeb3 from "@drizzle-utils/get-web3"; -import getContractInstance from "@drizzle-utils/get-contract-instance"; -import getAccounts from "@drizzle-utils/get-accounts"; -import { call, put, take, takeLatest, takeEvery } from 'redux-saga/effects' +import { call, put, take, takeEvery } from 'redux-saga/effects' -import Forum from "../../contracts/Forum.json"; +import { contract, getCurrentAccount } from './drizzleUtilsSaga'; -let initFlag, web3, contract, account; - -function* initUser() { - if(!initFlag) { - web3 = yield call(getWeb3); - contract = yield call(getContractInstance,{ - web3, - artifact: Forum - }); - initFlag=true; - yield put({type: 'USER_SAGA_INITIALIZED', ...[]}); - } - else - console.warn("Attempted to reinitialize userSaga!"); -} +let account; function* updateUserData() { - if(initFlag){ - const currentAccount = (yield call(getAccounts, {web3}))[0]; - if(currentAccount!==account) { - account = currentAccount; - yield put({type: 'ACCOUNT_CHANGED', ...[]}); - } - const txObj1 = yield call(contract.methods["hasUserSignedUp"], ...[account]); - try { - const callResult = yield call(txObj1.call, {address:account}); - if(callResult) { - const txObj2 = yield call(contract.methods["getUsername"], ...[account]); - const username = yield call(txObj2.call, {address:account}); - const dispatchArgs = { - address: account, - username: username - }; - yield put({type: 'USER_HAS_SIGNED_UP', ...dispatchArgs}); - } - else{ - const dispatchArgs = { - address: account - }; - yield put({type: 'USER_IS_GUEST', ...dispatchArgs}); - } + const currentAccount = yield call(getCurrentAccount); + if(currentAccount!==account) { + account = currentAccount; + yield put({type: 'ACCOUNT_CHANGED', ...[]}); + } + const txObj1 = yield call(contract.methods["hasUserSignedUp"], ...[account]); + try { + const callResult = yield call(txObj1.call, {address:account}); + if(callResult) { + const txObj2 = yield call(contract.methods["getUsername"], ...[account]); + const username = yield call(txObj2.call, {address:account}); + const dispatchArgs = { + address: account, + username: username + }; + yield put({type: 'USER_HAS_SIGNED_UP', ...dispatchArgs}); } - catch (error) { - console.error(error); - yield put({type: 'USER_FETCHING_ERROR', ...[]}) + else{ + const dispatchArgs = { + address: account + }; + yield put({type: 'USER_IS_GUEST', ...dispatchArgs}); } } - else - console.warn("Attempted to fetch data without initializing!"); + catch (error) { + console.error(error); + yield put({type: 'USER_FETCHING_ERROR', ...[]}) + } } function* userSaga() { - yield takeLatest("DRIZZLE_INITIALIZED", initUser); - yield take("USER_SAGA_INITIALIZED"); + yield take("DRIZZLE_UTILS_SAGA_INITIALIZED"); yield takeEvery("ACCOUNTS_FETCHED", updateUserData); } diff --git a/app/src/PrivateRoute.js b/app/src/router/PrivateRoute.js similarity index 100% rename from app/src/PrivateRoute.js rename to app/src/router/PrivateRoute.js diff --git a/app/src/routes.js b/app/src/router/routes.js similarity index 57% rename from app/src/routes.js rename to app/src/router/routes.js index b837d4a..e121f3a 100644 --- a/app/src/routes.js +++ b/app/src/router/routes.js @@ -1,10 +1,10 @@ import React from 'react' -import { Route, Switch, Redirect } from 'react-router-dom' +import { Route, Switch } from 'react-router-dom' import PrivateRoute from './PrivateRoute.js'; -import NavBarContainer from './containers/NavBarContainer'; -import HomeContainer from './containers/HomeContainer' -import SignUpContainer from './containers/SignUpContainer' -import NotFound from './components/NotFound' +import NavBarContainer from '../containers/NavBarContainer'; +import HomeContainer from '../containers/HomeContainer' +import SignUpContainer from '../containers/SignUpContainer' +import NotFound from '../components/NotFound' const routes = ( diff --git a/contracts/Forum.sol b/contracts/Forum.sol index 2bae986..c9e5136 100644 --- a/contracts/Forum.sol +++ b/contracts/Forum.sol @@ -4,7 +4,8 @@ contract Forum { //----------------------------------------USER---------------------------------------- struct User { - string username; + string username; // TODO: set an upper bound instead of arbitrary string + OrbitDB orbitdb; uint[] topicIDs; // IDs of the topics the user created uint[] postIDs; // IDs of the posts the user created uint timestamp; @@ -17,10 +18,11 @@ contract Forum { event UserSignedUp(string username, address userAddress); event UsernameUpdated(string newName, string oldName,address userAddress); - function signUp(string memory username) public returns (bool) { + function signUp(string memory username, string memory orbitDBId, string memory orbitTopicsDB, string memory orbitPostsDB, string memory orbitPublicKey, string memory orbitPrivateKey) public returns (bool) { require (!hasUserSignedUp(msg.sender), "User has already signed up."); require(!isUserNameTaken(username), "Username is already taken."); users[msg.sender] = User(username, + OrbitDB(orbitDBId,orbitTopicsDB, orbitPostsDB, orbitPublicKey, orbitPrivateKey), new uint[](0), new uint[](0), block.timestamp, true); userAddresses[username] = msg.sender; emit UserSignedUp(username, msg.sender); @@ -57,9 +59,149 @@ contract Forum { return false; } + function getUserTopics(address userAddress) public view returns (uint[] memory) { + require (hasUserSignedUp(userAddress), "User hasn't signed up yet."); + return users[userAddress].topicIDs; + } + + function getUserPosts(address userAddress) public view returns (uint[] memory) { + require (hasUserSignedUp(userAddress), "User hasn't signed up yet."); + return users[userAddress].postIDs; + } + function getUserDateOfRegister(address userAddress) public view returns (uint) { require (hasUserSignedUp(userAddress), "User hasn't signed up yet."); return users[userAddress].timestamp; } -} + //----------------------------------------OrbitDB---------------------------------------- + struct OrbitDB { + string id; // TODO: set an upper bound instead of arbitrary string + string topicsDB; //TODO: not sure yet which of these are actually needed + string postsDB; + string publicKey; + string privateKey; + } + + + function getOrbitDBId(address userAddress) public view returns (string memory) { + require (hasUserSignedUp(userAddress), "User hasn't signed up."); + return users[userAddress].orbitdb.id; + } + + function getOrbitTopicsDB(address userAddress) public view returns (string memory) { + require (hasUserSignedUp(userAddress), "User hasn't signed up."); + return users[userAddress].orbitdb.topicsDB; + } + + function getOrbitPostsDB(address userAddress) public view returns (string memory) { + require (hasUserSignedUp(userAddress), "User hasn't signed up."); + return users[userAddress].orbitdb.postsDB; + } + + function getOrbitPublicKey(address userAddress) public view returns (string memory) { + require (hasUserSignedUp(userAddress), "User hasn't signed up."); + return users[userAddress].orbitdb.publicKey; + } + + //TODO: encrypt using Metamask in the future + function getOrbitPrivateKey(address userAddress) public view returns (string memory) { + require (hasUserSignedUp(userAddress), "User hasn't signed up."); + return users[userAddress].orbitdb.privateKey; + } + + function getOrbitDBInfo(address userAddress) public view returns (string memory, string memory, string memory, string memory, string memory) { + require (hasUserSignedUp(userAddress), "User hasn't signed up."); + return ( + users[userAddress].orbitdb.id, + users[userAddress].orbitdb.topicsDB, + users[userAddress].orbitdb.postsDB, + users[userAddress].orbitdb.publicKey, + users[userAddress].orbitdb.privateKey + ); + } + + + //----------------------------------------POSTING---------------------------------------- + struct Topic { + uint topicID; + address author; + uint timestamp; + uint[] postIDs; + } + + struct Post { + uint postID; + address author; + uint timestamp; + uint topicID; + } + + uint numTopics; // Total number of topics + uint numPosts; // Total number of posts + + mapping (uint => Topic) topics; + mapping (uint => Post) posts; + + event TopicCreated(uint topicID, uint postID); + event PostCreated(uint postID, uint topicID); + + function createTopic() public returns (uint, uint) { + require(hasUserSignedUp(msg.sender)); // Only registered users can create topics + //Creates topic + uint topicID = numTopics++; + topics[topicID] = Topic(topicID, msg.sender, block.timestamp, new uint[](0)); + users[msg.sender].topicIDs.push(topicID); + + //Adds first post to topic + uint postID = numPosts++; + posts[postID] = Post(postID, msg.sender, block.timestamp, topicID); + topics[topicID].postIDs.push(postID); + users[msg.sender].postIDs.push(postID); + + emit TopicCreated(topicID, postID); + return (topicID, postID); + } + + function createPost(uint topicID) public returns (uint) { + require(hasUserSignedUp(msg.sender)); // Only registered users can create posts + require(topicID