diff --git a/app/package.json b/app/package.json index d784355..fa888c5 100644 --- a/app/package.json +++ b/app/package.json @@ -10,10 +10,11 @@ "connected-react-router": "6.4.0", "drizzle": "1.4.0", "history": "4.9.0", - "ipfs": "github:ipfs/js-ipfs#e849dbcab4a313f7ffc1532a389097ee76344067", + "ipfs": "0.35.0", "lodash.isequal": "4.5.0", - "orbit-db": "0.19.9", - "orbit-db-keystore": "0.1.0", + "orbit-db": "0.21.0-rc.1", + "orbit-db-keystore": "0.2.1", + "orbit-db-identity-provider": "0.1.0", "prop-types": "15.7.2", "react": "16.8.6", "react-content-loader": "4.2.1", @@ -28,7 +29,7 @@ "redux-saga": "0.16.2", "semantic-ui-react": "0.87.1", "uuid": "3.3.2", - "web3": "1.0.0-beta.54" + "web3": "1.0.0-beta.55" }, "devDependencies": { "libp2p-websocket-star-rendezvous": "0.3.0" diff --git a/app/src/containers/LoadingContainer.js b/app/src/containers/LoadingContainer.js index 28b074c..eb7bcf2 100644 --- a/app/src/containers/LoadingContainer.js +++ b/app/src/containers/LoadingContainer.js @@ -10,16 +10,15 @@ class LoadingContainer extends Component { render() { if (this.props.web3.status === 'failed' || !this.props.web3.networkId) { return ( -
+
ethereum_logo

This browser has no connection to the Ethereum network.

Please make sure that:
    -
  • You use MetaMask or a dedicated Ethereum browser
  • -
  • They are pointed to the correct network
  • -
  • Your account is unlocked and the app has the rights to access it
  • +
  • MetaMask is unlocked and pointed to the correct network
  • +
  • The app has been granted the right to connect to your account
@@ -33,7 +32,8 @@ class LoadingContainer extends Component {
ethereum_logo -

We can't find any Ethereum accounts!.

+

We can't find any Ethereum accounts!

+

Please make sure that MetaMask is unlocked.

@@ -76,6 +76,7 @@ class LoadingContainer extends Component {
orbitdb_logo

Preparing OrbitDB...

+

Please sign the transaction in MetaMask to create the databases.

@@ -86,7 +87,7 @@ class LoadingContainer extends Component { return Children.only(this.props.children); return( -
+
app_logo diff --git a/app/src/containers/PostList.js b/app/src/containers/PostList.js index 6de283f..c2de481 100644 --- a/app/src/containers/PostList.js +++ b/app/src/containers/PostList.js @@ -5,6 +5,7 @@ import { drizzle } from '../index'; import Post from './Post'; import PlaceholderContainer from './PlaceholderContainer'; +import { determineDBAddress } from '../utils/orbitUtils'; const contract = 'Forum'; const getPostMethod = 'getPost'; @@ -16,7 +17,8 @@ class PostList extends Component { this.getBlockchainData = this.getBlockchainData.bind(this); this.state = { - dataKeys: [] + dataKeys: [], + dbAddresses: [] }; } @@ -29,20 +31,31 @@ class PostList extends Component { } getBlockchainData() { - const { dataKeys } = this.state; - const { drizzleStatus, postIDs } = this.props; + const { dataKeys, dbAddresses } = this.state; + const { drizzleStatus, postIDs, contracts } = this.props; if (drizzleStatus.initialized) { const dataKeysShallowCopy = dataKeys.slice(); let fetchingNewData = false; - postIDs.forEach((postID) => { + postIDs.forEach(async (postID) => { if (!dataKeys[postID]) { dataKeysShallowCopy[postID] = drizzle.contracts[contract].methods[getPostMethod].cacheCall( - postID, + postID ); fetchingNewData = true; } + else if (!dbAddresses[postID]){ + const fetchedPostData = contracts[contract][getPostMethod][dataKeys[postID]]; + if(fetchedPostData) { + const dbAddress = await determineDBAddress('posts', fetchedPostData.value[0]); + const dbAddressesShallowCopy = dbAddresses.slice(); + dbAddressesShallowCopy[postID] = dbAddress; + this.setState({ + dbAddresses: dbAddressesShallowCopy + }); + } + } }); if (fetchingNewData) { @@ -54,7 +67,7 @@ class PostList extends Component { } render() { - const { dataKeys } = this.state; + const { dataKeys, dbAddresses } = this.state; const { postIDs, contracts, focusOnPost, recentToTheTop } = this.props; const posts = postIDs.map((postID, index) => { @@ -62,13 +75,16 @@ class PostList extends Component { if(dataKeys[postID]) fetchedPostData = contracts[contract][getPostMethod][dataKeys[postID]]; - if(fetchedPostData) { + const dbAddress = dbAddresses[postID]; + if(fetchedPostData && dbAddress) { + const userAddress = fetchedPostData.value[0]; //Also works as an Orbit Identity ID + const postData = { - userAddress: fetchedPostData.value[1], - fullOrbitAddress: `/orbitdb/${fetchedPostData.value[0]}/posts`, - userName: fetchedPostData.value[2], - timestamp: fetchedPostData.value[3]*1000, - topicID: fetchedPostData.value[4] + userAddress, + fullOrbitAddress: `/orbitdb/${dbAddress}/posts`, + userName: fetchedPostData.value[1], + timestamp: fetchedPostData.value[2]*1000, + topicID: fetchedPostData.value[3] }; return( Account address: {address} - - OrbitDB: - {orbitDBId ? orbitDBId - : - - - } - TopicsDB: {topicsDBId ? topicsDBId diff --git a/app/src/containers/TopicContainer.js b/app/src/containers/TopicContainer.js index 52fc3bf..5c672d0 100644 --- a/app/src/containers/TopicContainer.js +++ b/app/src/containers/TopicContainer.js @@ -10,6 +10,7 @@ import NewPost from './NewPost'; import FloatingButton from '../components/FloatingButton'; import { setNavBarTitle } from '../redux/actions/userInterfaceActions.js'; +import { determineDBAddress } from '../utils/orbitUtils'; const contract = 'Forum'; const getTopicMethod = 'getTopic'; @@ -95,16 +96,16 @@ class TopicContainer extends Component { } } - async fetchTopicSubject(orbitDBAddress) { + async fetchTopicSubject(userAddress) { const { topicID } = this.state; - const { contracts, user, orbitDB, setNavBarTitle } = this.props; + const { user, orbitDB, setNavBarTitle } = this.props; let orbitData; - if (contracts[contract][getTopicMethod][this.dataKey].value[1] - === user.address) { + if (userAddress === user.address) { orbitData = orbitDB.topicsDB.get(topicID); } else { - const fullAddress = `/orbitdb/${orbitDBAddress}/topics`; + const dbAddress = await determineDBAddress('topics', userAddress); + const fullAddress = `/orbitdb/${dbAddress}/topics`; const store = await orbitDB.orbitdb.keyvalue(fullAddress); await store.load(); @@ -152,7 +153,7 @@ class TopicContainer extends Component { (
{ this.togglePostingState(); }} onPostCreated={() => { this.postCreated(); }} /> diff --git a/app/src/containers/TopicList.js b/app/src/containers/TopicList.js index dbab22a..0db0cd8 100644 --- a/app/src/containers/TopicList.js +++ b/app/src/containers/TopicList.js @@ -5,6 +5,7 @@ import { drizzle } from '../index'; import Topic from './Topic'; import PlaceholderContainer from './PlaceholderContainer'; +import { determineDBAddress } from '../utils/orbitUtils'; const contract = 'Forum'; const getTopicMethod = 'getTopic'; @@ -16,7 +17,8 @@ class TopicList extends Component { this.getBlockchainData = this.getBlockchainData.bind(this); this.state = { - dataKeys: [] + dataKeys: [], + dbAddresses: [] }; } @@ -29,19 +31,30 @@ class TopicList extends Component { } getBlockchainData() { - const { dataKeys } = this.state; - const { drizzleStatus, topicIDs } = this.props; + const { dataKeys, dbAddresses } = this.state; + const { drizzleStatus, topicIDs, contracts } = this.props; if (drizzleStatus.initialized) { const dataKeysShallowCopy = dataKeys.slice(); let fetchingNewData = false; - topicIDs.forEach((topicID) => { + topicIDs.forEach(async (topicID) => { if (!dataKeys[topicID]) { dataKeysShallowCopy[topicID] = drizzle.contracts[contract].methods[getTopicMethod] .cacheCall(topicID); fetchingNewData = true; } + else if (!dbAddresses[topicID]){ + const fetchedTopicData = contracts[contract][getTopicMethod][dataKeys[topicID]]; + if(fetchedTopicData) { + const dbAddress = await determineDBAddress('topics', fetchedTopicData.value[0]); + const dbAddressesShallowCopy = dbAddresses.slice(); + dbAddressesShallowCopy[topicID] = dbAddress; + this.setState({ + dbAddresses: dbAddressesShallowCopy + }); + } + } }); if (fetchingNewData) { @@ -53,7 +66,7 @@ class TopicList extends Component { } render() { - const { dataKeys } = this.state; + const { dataKeys, dbAddresses } = this.state; const { topicIDs, contracts } = this.props; const topics = topicIDs.map(topicID => { @@ -61,13 +74,15 @@ class TopicList extends Component { if(dataKeys[topicID]) fetchedTopicData = contracts[contract][getTopicMethod][dataKeys[topicID]]; - if(fetchedTopicData) { + const dbAddress = dbAddresses[topicID]; + if(fetchedTopicData && dbAddress) { + const userAddress = fetchedTopicData.value[0]; //Also works as an Orbit Identity ID const topicData = { - userAddress: fetchedTopicData.value[1], - fullOrbitAddress: `/orbitdb/${fetchedTopicData.value[0]}/topics`, - userName: fetchedTopicData.value[2], - timestamp: fetchedTopicData.value[3]*1000, - numberOfReplies: fetchedTopicData.value[4].length + userAddress, + fullOrbitAddress: `/orbitdb/${dbAddress}/topics`, + userName: fetchedTopicData.value[1], + timestamp: fetchedTopicData.value[2]*1000, + numberOfReplies: fetchedTopicData.value[3].length }; return( state.orbit); if(!orbit.ready){ - const { orbitdb, topicsDB, postsDB } = yield call(createTempDatabases); + const { orbitdb, topicsDB, postsDB } = yield call(createDatabases); yield put(updateDatabases(DATABASES_CREATED, orbitdb, topicsDB, postsDB )); - console.debug("Created temporary databases."); } } latestAccount = account; diff --git a/app/src/redux/sagas/transactionsSaga.js b/app/src/redux/sagas/transactionsSaga.js index dca9b5c..b69cb72 100644 --- a/app/src/redux/sagas/transactionsSaga.js +++ b/app/src/redux/sagas/transactionsSaga.js @@ -10,9 +10,8 @@ const transactionsHistory = Object.create(null); function* initTransaction(action) { const dataKey = drizzle.contracts[action.transactionDescriptor.contract] .methods[action.transactionDescriptor.method].cacheSend( - ...(action.transactionDescriptor.params), + ...(action.transactionDescriptor.params) ); - transactionsHistory[dataKey] = action; transactionsHistory[dataKey].state = 'initialized'; } diff --git a/app/src/utils/EthereumIdentityProvider.js b/app/src/utils/EthereumIdentityProvider.js new file mode 100644 index 0000000..734628f --- /dev/null +++ b/app/src/utils/EthereumIdentityProvider.js @@ -0,0 +1,29 @@ +import { web3 } from '../redux/sagas/web3UtilsSaga'; + +class EthereumIdentityProvider { + constructor () { + this.web3 = web3; + } + + // Returns the type of the identity provider + static get type () { return 'ethereum' } + + // Returns the signer's id + async getId () { + return (await this.web3.eth.getAccounts())[0]; + } + + // Returns a signature of pubkeysignature + async signIdentity (data) { + const address = await this.getId(); + return await this.web3.eth.personal.sign(data,address,""); //Password not required for MetaMask + } + + static async verifyIdentity (identity) { + // Verify that identity was signed by the ID + return web3.eth.accounts.recover(identity.publicKey + identity.signatures.id, + identity.signatures.publicKey) === identity.id; + } +} + +export default EthereumIdentityProvider; diff --git a/app/src/utils/orbitUtils.js b/app/src/utils/orbitUtils.js index 705d46f..91a2ef7 100644 --- a/app/src/utils/orbitUtils.js +++ b/app/src/utils/orbitUtils.js @@ -1,12 +1,13 @@ import OrbitDB from 'orbit-db'; -import Keystore from 'orbit-db-keystore'; -import path from 'path'; +import Identities from 'orbit-db-identity-provider'; import IPFS from 'ipfs'; import store from '../redux/store'; -import { DATABASES_LOADED, IPFS_INITIALIZED, updateDatabases } from '../redux/actions/orbitActions'; +import { DATABASES_LOADED, IPFS_INITIALIZED, updateDatabases } from '../redux/actions/orbitActions'; import ipfsOptions from '../config/ipfsOptions'; +import EthereumIdentityProvider from './EthereumIdentityProvider'; function initIPFS() { + Identities.addIdentityProvider(EthereumIdentityProvider); const ipfs = new IPFS(ipfsOptions); ipfs.on('error', (error) => console.error(`IPFS error: ${error}`)); ipfs.on('ready', async () => { @@ -21,63 +22,23 @@ function initIPFS() { }); } -async function createTempDatabases() { - console.debug('Creating temporary databases...'); - const ipfs = getIPFS(); - const orbitdb = new OrbitDB(ipfs); - const topicsDB = await orbitdb.keyvalue('topics'); - const postsDB = await orbitdb.keyvalue('posts'); - return { orbitdb, topicsDB, postsDB }; -} - async function createDatabases() { console.debug('Creating databases...'); - const ipfs = getIPFS(); - const orbitdb = new OrbitDB(ipfs); - const topicsDB = await orbitdb.keyvalue('topics'); - const postsDB = await orbitdb.keyvalue('posts'); - - const orbitKey = orbitdb.keystore.getKey(orbitdb.id); - - return { - identityId: 'Tempus', - identityPublicKey: 'edax', - identityPrivateKey: 'rerum', - orbitdb: orbitdb, - orbitPublicKey: orbitKey.getPublic('hex'), - orbitPrivateKey: orbitKey.getPrivate('hex'), - topicsDB: topicsDB.address.root, - postsDB: postsDB.address.root - }; + const databases = await createDBs(); + console.debug('Databases created successfully.'); + return databases; } -async function loadDatabases(identityId, identityPublicKey, identityPrivateKey, - orbitId, orbitPublicKey, orbitPrivateKey, - topicsDBId, postsDBId) { - const directory = './orbitdb'; - const keystore = Keystore.create(path.join(directory, orbitId, '/keystore')); - - keystore._storage.setItem(orbitId, JSON.stringify({ - publicKey: orbitPublicKey, - privateKey: orbitPrivateKey - })); - - const ipfs = getIPFS(); - const orbitdb = new OrbitDB(ipfs, directory, - { - peerId: orbitId, keystore - }); - const topicsDB = await orbitdb.keyvalue(`/orbitdb/${topicsDBId}/topics`) - .catch((error) => console.error(`TopicsDB init error: ${error}`)); - const postsDB = await orbitdb.keyvalue(`/orbitdb/${postsDBId}/posts`) - .catch((error) => console.error(`PostsDB init error: ${error}`)); +async function loadDatabases() { + console.debug('Loading databases...'); + const { orbitdb, topicsDB, postsDB } = await createDBs(); await topicsDB.load().catch((error) => console.error(`TopicsDB loading error: ${error}`)); await postsDB.load().catch((error) => console.error(`PostsDB loading error: ${error}`)); //It is possible that we lack our own data and need to replicate them from somewhere else topicsDB.events.on('replicate', (address) => { - console.log(`TopicsDB Replicating (${address}).`); + console.log(`TopicsDB replicating (${address}).`); }); topicsDB.events.on('replicated', (address) => { console.log(`TopicsDB replicated (${address}).`); @@ -89,14 +50,26 @@ async function loadDatabases(identityId, identityPublicKey, identityPrivateKey, console.log(`PostsDB replicated (${address}).`); }); - console.debug('Orbit databases loaded successfully.'); + console.debug('Databases loaded successfully.'); store.dispatch(updateDatabases(DATABASES_LOADED, orbitdb, topicsDB, postsDB)); } +async function determineDBAddress(name, identityId){ + return (await getOrbitDB().determineAddress(name, 'keyvalue', { + accessController: { + write: [identityId]} + } + )).root; +} + function getIPFS() { return store.getState().orbit.ipfs; } +function getOrbitDB() { + return store.getState().orbit.orbitdb; +} + async function orbitSagaPut(db, key, value) { await db.put(key, value).catch((error) => console.error(`Orbit put error: ${error}`)); } @@ -114,4 +87,24 @@ async function orbitSagaOpen(orbitdb, address) { return store; } -export { initIPFS, createTempDatabases, createDatabases, loadDatabases, orbitSagaPut, orbitSagaOpen }; +async function createDBs(){ + const ipfs = getIPFS(); + const identity = await Identities.createIdentity({type: 'ethereum'}); + const orbitdb = await OrbitDB.createInstance(ipfs, {identity}); + const topicsDB = await orbitdb.keyvalue('topics') + .catch((error) => console.error(`TopicsDB init error: ${error}`)); + const postsDB = await orbitdb.keyvalue('posts') + .catch((error) => console.error(`PostsDB init error: ${error}`)); + + return { orbitdb, topicsDB, postsDB }; +} + + +export { + initIPFS, + createDatabases, + loadDatabases, + orbitSagaPut, + orbitSagaOpen, + determineDBAddress +}; diff --git a/contracts/Forum.sol b/contracts/Forum.sol index e6dcba2..a49cfb0 100644 --- a/contracts/Forum.sol +++ b/contracts/Forum.sol @@ -5,7 +5,6 @@ contract Forum { //----------------------------------------USER---------------------------------------- struct User { 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; @@ -18,15 +17,10 @@ contract Forum { event UserSignedUp(string username, address userAddress); event UsernameUpdated(string newName, string oldName,address userAddress); - function signUp(string memory username, string memory orbitIdentityId, - string memory orbitIdentityPublicKey, string memory orbitIdentityPrivateKey, - string memory orbitId, string memory orbitPublicKey, string memory orbitPrivateKey, - string memory orbitTopicsDB, string memory orbitPostsDB) public returns (bool) { + 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, - OrbitDB(orbitIdentityId, orbitIdentityPublicKey, orbitIdentityPrivateKey, - orbitId, orbitPublicKey, orbitPrivateKey, orbitTopicsDB, orbitPostsDB), new uint[](0), new uint[](0), block.timestamp, true); userAddresses[username] = msg.sender; emit UserSignedUp(username, msg.sender); @@ -78,82 +72,6 @@ contract Forum { return users[userAddress].timestamp; } - //----------------------------------------OrbitDB---------------------------------------- - // TODO: set upper bounds to strings (instead of being of arbitrary length) - // TODO: not sure if topicsDB//postsDB are actually needed - struct OrbitDB { - string identityId; - string identityPublicKey; - string identityPrivateKey; - string orbitId; - string orbitPublicKey; - string orbitPrivateKey; - string topicsDB; - string postsDB; - } - - function getOrbitIdentityId(address userAddress) public view returns (string memory) { - require (hasUserSignedUp(userAddress), "User hasn't signed up."); - return users[userAddress].orbitdb.identityId; - } - - function getOrbitIdentityPublicKey(address userAddress) public view returns (string memory) { - require (hasUserSignedUp(userAddress), "User hasn't signed up."); - return users[userAddress].orbitdb.identityPublicKey; - } - - function getOrbitIdentityPrivateKey(address userAddress) public view returns (string memory) { - require (hasUserSignedUp(userAddress), "User hasn't signed up."); - return users[userAddress].orbitdb.identityPrivateKey; - } - - - function getOrbitDBId(address userAddress) public view returns (string memory) { - require (hasUserSignedUp(userAddress), "User hasn't signed up."); - return users[userAddress].orbitdb.orbitId; - } - - function getOrbitPublicKey(address userAddress) public view returns (string memory) { - require (hasUserSignedUp(userAddress), "User hasn't signed up."); - return users[userAddress].orbitdb.orbitPublicKey; - } - - //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.orbitPrivateKey; - } - - 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 getOrbitIdentityInfo(address userAddress) public view returns (string memory, string memory, string memory) { - require (hasUserSignedUp(userAddress), "User hasn't signed up."); - return ( - users[userAddress].orbitdb.identityId, - users[userAddress].orbitdb.identityPublicKey, - users[userAddress].orbitdb.identityPrivateKey - ); - } - - 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.orbitId, - users[userAddress].orbitdb.orbitPublicKey, - users[userAddress].orbitdb.orbitPrivateKey, - users[userAddress].orbitdb.topicsDB, - users[userAddress].orbitdb.postsDB - ); - } //----------------------------------------POSTING---------------------------------------- struct Topic { @@ -211,10 +129,10 @@ contract Forum { return numTopics; } - function getTopic(uint topicID) public view returns (string memory, address, string memory, uint, uint[] memory) { + function getTopic(uint topicID) public view returns (address, string memory, uint, uint[] memory) { //require(hasUserSignedUp(msg.sender)); needed? require(topicID