mirror of https://gitlab.com/ecentrics/concordia
Ezerous
4 years ago
commit
29bdd3dd8e
40 changed files with 18644 additions and 0 deletions
@ -0,0 +1,10 @@ |
|||||
|
# Set the default behavior, in case people don't have core.autocrlf set. |
||||
|
* text=auto eol=lf |
||||
|
|
||||
|
# Denote all files that are truly binary and should not be modified. |
||||
|
*.png binary |
||||
|
*.jpg binary |
||||
|
*.ico binary |
||||
|
|
||||
|
# Solidity |
||||
|
*.sol linguist-language=Solidity |
@ -0,0 +1,34 @@ |
|||||
|
# Node |
||||
|
/node_modules |
||||
|
packages/*/node_modules |
||||
|
packages/concordia-contracts/build |
||||
|
|
||||
|
# IDE |
||||
|
.DS_Store |
||||
|
.idea |
||||
|
|
||||
|
# Build Directories |
||||
|
/build |
||||
|
/src/build |
||||
|
/packages/concordia-app/build |
||||
|
/packages/concordia-contracts/build |
||||
|
|
||||
|
# Logs |
||||
|
/log |
||||
|
npm-debug.log* |
||||
|
yarn-debug.log* |
||||
|
yarn-error.log* |
||||
|
|
||||
|
# Docker volumes |
||||
|
docker/volumes |
||||
|
docker/reports |
||||
|
docker/env/concordia.env |
||||
|
|
||||
|
# Misc |
||||
|
.env.local |
||||
|
.env.development.local |
||||
|
.env.test.local |
||||
|
.env.production.local |
||||
|
|
||||
|
# Lerna |
||||
|
*.lerna_backup |
@ -0,0 +1,8 @@ |
|||||
|
{ |
||||
|
"version": "0.0.1", |
||||
|
"npmClient": "yarn", |
||||
|
"useWorkspaces": true, |
||||
|
"packages": [ |
||||
|
"packages/*" |
||||
|
] |
||||
|
} |
@ -0,0 +1,8 @@ |
|||||
|
{ |
||||
|
"name": "apella", |
||||
|
"private": true, |
||||
|
"workspaces": { |
||||
|
"packages": ["packages/*"], |
||||
|
"nohoist": ["**/web3", "**/web3/**"] |
||||
|
} |
||||
|
} |
@ -0,0 +1,50 @@ |
|||||
|
{ |
||||
|
"name": "concordia-app", |
||||
|
"version": "0.1.0", |
||||
|
"private": true, |
||||
|
"scripts": { |
||||
|
"start": "react-scripts start", |
||||
|
"build": "react-scripts build", |
||||
|
"test": "react-scripts test", |
||||
|
"eject": "react-scripts eject", |
||||
|
"postinstall": "patch-package", |
||||
|
"rendezvous": "star-signal --port=9090 --host=127.0.0.1" |
||||
|
}, |
||||
|
"eslintConfig": { |
||||
|
"extends": "react-app" |
||||
|
}, |
||||
|
"browserslist": { |
||||
|
"production": [ |
||||
|
">0.2%", |
||||
|
"not dead", |
||||
|
"not op_mini all" |
||||
|
], |
||||
|
"development": [ |
||||
|
"last 1 chrome version", |
||||
|
"last 1 firefox version", |
||||
|
"last 1 safari version" |
||||
|
] |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@ezerous/breeze": "0.1.0", |
||||
|
"@ezerous/drizzle": "0.2.1", |
||||
|
"@reduxjs/toolkit": "1.4.0", |
||||
|
"concordia-contracts": "0.1.0", |
||||
|
"level": "6.0.1", |
||||
|
"prop-types": "15.7.2", |
||||
|
"react": "16.13.1", |
||||
|
"react-dom": "16.13.1", |
||||
|
"react-redux": "7.2.1", |
||||
|
"react-router": "5.2.0", |
||||
|
"react-router-dom": "5.2.0", |
||||
|
"react-scripts": "3.4.3", |
||||
|
"redux-saga": "1.1.3", |
||||
|
"web3": "1.2.11" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"libp2p-webrtc-star": "0.20.0", |
||||
|
"orbit-db-identity-provider": "0.3.1", |
||||
|
"patch-package": "6.2.2", |
||||
|
"postinstall-postinstall": "2.1.0" |
||||
|
} |
||||
|
} |
@ -0,0 +1,15 @@ |
|||||
|
diff --git a/node_modules/web3-eth/src/index.js b/node_modules/web3-eth/src/index.js
|
||||
|
index 469c149..6f7f1bc 100644
|
||||
|
--- a/node_modules/web3-eth/src/index.js
|
||||
|
+++ b/node_modules/web3-eth/src/index.js
|
||||
|
@@ -343,8 +343,8 @@ var Eth = function Eth() {
|
||||
|
// add ABI |
||||
|
this.abi = abi; |
||||
|
|
||||
|
- // add ENS
|
||||
|
- this.ens = new ENS(this);
|
||||
|
+ // add ENS (Removed because of https://github.com/ethereum/web3.js/issues/2665#issuecomment-687164093)
|
||||
|
+ // this.ens = new ENS(this);
|
||||
|
|
||||
|
var methods = [ |
||||
|
new Method({ |
After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,39 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="utf-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
||||
|
<meta name="theme-color" content="#000000"> |
||||
|
<!-- |
||||
|
manifest.json provides metadata used when your web app is added to the |
||||
|
homescreen on Android. See https://developers.google.com/web/fundamentals/web-app-manifest/ |
||||
|
--> |
||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> |
||||
|
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> |
||||
|
<!-- |
||||
|
Notice the use of %PUBLIC_URL% in the tags above. |
||||
|
It will be replaced with the URL of the `public` folder during the build. |
||||
|
Only files inside the `public` folder can be referenced from the HTML. |
||||
|
|
||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will |
||||
|
work correctly both with client-side routing and a non-root public URL. |
||||
|
Learn how to configure a non-root public URL by running `npm run build`. |
||||
|
--> |
||||
|
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css" /> |
||||
|
<title>Concordia</title> |
||||
|
</head> |
||||
|
<body> |
||||
|
<noscript>You need to enable JavaScript to run this app.</noscript> |
||||
|
<div id="root"></div> |
||||
|
<!-- |
||||
|
This HTML file is a template. |
||||
|
If you open it directly in the browser, you will see an empty page. |
||||
|
|
||||
|
You can add webfonts, meta tags, or analytics to this file. |
||||
|
The build step will place the bundled scripts into the <body> tag. |
||||
|
|
||||
|
To begin the development, run `npm start` or `yarn start`. |
||||
|
To create a production bundle, use `npm run build` or `yarn build`. |
||||
|
--> |
||||
|
</body> |
||||
|
</html> |
@ -0,0 +1,15 @@ |
|||||
|
{ |
||||
|
"short_name": "Apella", |
||||
|
"name": "Apella", |
||||
|
"icons": [ |
||||
|
{ |
||||
|
"src": "favicon.ico", |
||||
|
"sizes": "64x64 32x32 24x24 16x16", |
||||
|
"type": "image/x-icon" |
||||
|
} |
||||
|
], |
||||
|
"start_url": ".", |
||||
|
"display": "standalone", |
||||
|
"theme_color": "#000000", |
||||
|
"background_color": "#ffffff" |
||||
|
} |
@ -0,0 +1,2 @@ |
|||||
|
# https://www.robotstxt.org/robotstxt.html |
||||
|
User-agent: * |
@ -0,0 +1,4 @@ |
|||||
|
body { |
||||
|
margin: 10em; |
||||
|
padding: 0; |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
.loading-screen { |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.loading-img { |
||||
|
margin-bottom: 30px; |
||||
|
height: 100px; |
||||
|
} |
||||
|
|
||||
|
ul { |
||||
|
list-style-position: inside; |
||||
|
} |
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 216 KiB |
After Width: | Height: | Size: 112 KiB |
@ -0,0 +1,24 @@ |
|||||
|
import React from 'react' |
||||
|
import { Provider } from 'react-redux' |
||||
|
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom' |
||||
|
import LoadingContainer from './LoadingContainer' |
||||
|
import PropTypes from 'prop-types' |
||||
|
import NotFound from '../components/NotFound'; |
||||
|
|
||||
|
const App = ({ store }) => ( |
||||
|
<Provider store={store}> |
||||
|
<LoadingContainer> |
||||
|
<Router> |
||||
|
<Switch> |
||||
|
<Route component={NotFound} /> |
||||
|
</Switch> |
||||
|
</Router> |
||||
|
</LoadingContainer> |
||||
|
</Provider> |
||||
|
) |
||||
|
|
||||
|
App.propTypes = { |
||||
|
store: PropTypes.object.isRequired |
||||
|
} |
||||
|
|
||||
|
export default App |
@ -0,0 +1,72 @@ |
|||||
|
// Modified version of https://github.com/trufflesuite/drizzle/blob/develop/packages/react-plugin/src/DrizzleContext.js
|
||||
|
import React from "react"; |
||||
|
|
||||
|
const Context = React.createContext(); |
||||
|
|
||||
|
class Provider extends React.Component { |
||||
|
state = { |
||||
|
drizzleState: null, |
||||
|
drizzleInitialized: false, |
||||
|
breezeState: null, |
||||
|
breezeInitialized: false |
||||
|
}; |
||||
|
|
||||
|
componentDidMount() { |
||||
|
const { drizzle, breeze } = this.props; |
||||
|
// subscribe to changes in the store, keep state up-to-date
|
||||
|
this.unsubscribe = drizzle.store.subscribe(() => { |
||||
|
const drizzleState = drizzle.store.getState(); |
||||
|
const breezeState = breeze.store.getState(); |
||||
|
|
||||
|
if (drizzleState.drizzleStatus.initialized) { |
||||
|
this.setState({ |
||||
|
drizzleState, |
||||
|
drizzleInitialized: true |
||||
|
}); |
||||
|
} |
||||
|
if (breezeState.breezeStatus.initialized) { |
||||
|
this.setState({ |
||||
|
breezeState: breezeState, |
||||
|
breezeInitialized: true |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
this.unsubscribe = breeze.store.subscribe(() => { |
||||
|
const breezeState = breeze.store.getState(); |
||||
|
if (breezeState.breezeStatus.initialized) { |
||||
|
this.setState({ |
||||
|
breezeState: breezeState, |
||||
|
breezeInitialized: true |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
componentWillUnmount() { |
||||
|
this.unsubscribe(); |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
return ( |
||||
|
<Context.Provider |
||||
|
value={{ |
||||
|
drizzle: this.props.drizzle, |
||||
|
drizzleState: this.state.drizzleState, |
||||
|
drizzleInitialized: this.state.drizzleInitialized, |
||||
|
breeze: this.props.breeze, |
||||
|
breezeState: this.state.breezeState, |
||||
|
breezeInitialized: this.state.breezeInitialized |
||||
|
}} |
||||
|
> |
||||
|
{this.props.children} |
||||
|
</Context.Provider> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default { |
||||
|
Context: Context, |
||||
|
Consumer: Context.Consumer, |
||||
|
Provider |
||||
|
}; |
@ -0,0 +1,115 @@ |
|||||
|
import React, { Children, Component } from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import '../assets/css/loading-container.css'; |
||||
|
import ethereum_logo from '../assets/images/ethereum_logo.svg'; |
||||
|
import ipfs_logo from '../assets/images/ipfs_logo.svg'; |
||||
|
import orbitdb_logo from '../assets/images/orbitdb_logo.png'; |
||||
|
import logo from '../assets/images/logo.png'; |
||||
|
|
||||
|
class LoadingContainer extends Component { |
||||
|
render() { |
||||
|
if (this.props.web3.status === 'failed' || !this.props.web3.networkId) { |
||||
|
return ( |
||||
|
<main className="loading-screen"> |
||||
|
<div className="pure-g"> |
||||
|
<div className="pure-u-1-1"> |
||||
|
<img src={ethereum_logo} alt="ethereum_logo" className="loading-img" /> |
||||
|
<p><strong>This browser has no connection to the Ethereum network</strong></p> |
||||
|
Please make sure that: |
||||
|
<ul> |
||||
|
<li>MetaMask is unlocked and pointed to the correct network</li> |
||||
|
<li>The app has been granted the right to connect to your account</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
</main> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (this.props.web3.status === 'initialized' && Object.keys(this.props.accounts).length === 0) { |
||||
|
return ( |
||||
|
<main className="loading-screen"> |
||||
|
<div> |
||||
|
<img src={ethereum_logo} alt="ethereum_logo" className="loading-img" /> |
||||
|
<p><strong>We can't find any Ethereum accounts!</strong></p> |
||||
|
<p>Please make sure that MetaMask is unlocked.</p> |
||||
|
</div> |
||||
|
</main> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (!this.props.contractDeployed) { |
||||
|
return ( |
||||
|
<main className="loading-screen"> |
||||
|
<div> |
||||
|
<img src={ethereum_logo} alt="ethereum_logo" className="loading-img" /> |
||||
|
<p><strong>No contracts found on the current network!</strong></p> |
||||
|
<p>Please make sure that you are connected to the correct network |
||||
|
and the contracts are deployed.</p> |
||||
|
</div> |
||||
|
</main> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (!this.props.contractInitialized) { |
||||
|
return ( |
||||
|
<main className="loading-screen"> |
||||
|
<div> |
||||
|
<img src={ethereum_logo} alt="ethereum_logo" className="loading-img" /> |
||||
|
<p><strong>Initializing contracts...</strong></p> |
||||
|
</div> |
||||
|
</main> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (!this.props.ipfsInitialized) { |
||||
|
return ( |
||||
|
<main className="loading-screen"> |
||||
|
<div> |
||||
|
<img src={ipfs_logo} alt="ipfs_logo" className="loading-img" /> |
||||
|
<p><strong>Initializing IPFS...</strong></p> |
||||
|
</div> |
||||
|
</main> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (!this.props.orbitInitialized) { |
||||
|
const message = process.env.NODE_ENV === 'development' |
||||
|
? 'If needed, please sign the transaction in MetaMask to create the databases.' |
||||
|
: 'Please sign the transaction in MetaMask to create the databases.'; |
||||
|
return ( |
||||
|
<main className="loading-screen"> |
||||
|
<div> |
||||
|
<img src={orbitdb_logo} alt="orbitdb_logo" className="loading-img" /> |
||||
|
<p><strong>Preparing OrbitDB...</strong></p> |
||||
|
<p>{message}</p> |
||||
|
</div> |
||||
|
</main> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (this.props.drizzleStatus.initialized) return Children.only(this.props.children); |
||||
|
|
||||
|
return ( |
||||
|
<main className="loading-screen"> |
||||
|
<div> |
||||
|
<img src={logo} alt="app_logo" className="loading-img" /> |
||||
|
<p><strong>Loading dapp...</strong></p> |
||||
|
</div> |
||||
|
</main> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const mapStateToProps = (state) => ({ |
||||
|
accounts: state.accounts, |
||||
|
drizzleStatus: state.drizzleStatus, |
||||
|
breezeStatus: state.breezeStatus, |
||||
|
web3: state.web3, |
||||
|
contractInitialized: state.contracts.Forum.initialized, |
||||
|
contractDeployed: state.contracts.Forum.deployed, |
||||
|
ipfsInitialized: state.ipfs.initialized, |
||||
|
orbitInitialized: state.orbit.initialized, |
||||
|
}); |
||||
|
|
||||
|
export default connect(mapStateToProps)(LoadingContainer); |
@ -0,0 +1,13 @@ |
|||||
|
import React from 'react'; |
||||
|
import pageNotFound from '../assets/images/PageNotFound.jpg'; |
||||
|
|
||||
|
const NotFound = () => ( |
||||
|
<div style={{ |
||||
|
textAlign: 'center', |
||||
|
}} |
||||
|
> |
||||
|
<img src={pageNotFound} alt="Page not found!" /> |
||||
|
</div> |
||||
|
); |
||||
|
|
||||
|
export default NotFound; |
@ -0,0 +1,29 @@ |
|||||
|
import React from 'react'; |
||||
|
import { render } from 'react-dom'; |
||||
|
import App from './components/App' |
||||
|
import store from './redux/store'; |
||||
|
import { Drizzle } from '@ezerous/drizzle' |
||||
|
import { Breeze } from '@ezerous/breeze' |
||||
|
|
||||
|
import AppContext from "./components/AppContext"; |
||||
|
|
||||
|
import drizzleOptions from './options/drizzleOptions'; |
||||
|
import * as serviceWorker from './utils/serviceWorker'; |
||||
|
|
||||
|
import './assets/css/index.css'; |
||||
|
import breezeOptions from './options/breezeOptions'; |
||||
|
|
||||
|
const drizzle = new Drizzle(drizzleOptions, store); |
||||
|
const breeze = new Breeze(breezeOptions, store); |
||||
|
|
||||
|
render( |
||||
|
<AppContext.Provider drizzle={drizzle} breeze={breeze}> |
||||
|
<App store={store} /> |
||||
|
</AppContext.Provider>, |
||||
|
document.getElementById('root') |
||||
|
); |
||||
|
|
||||
|
serviceWorker.unregister(); // See also: http://bit.ly/CRA-PWA
|
||||
|
|
||||
|
|
||||
|
|
@ -0,0 +1,42 @@ |
|||||
|
import web3Options from './web3Options'; |
||||
|
import EthereumIdentityProvider from '../orbit/ΕthereumIdentityProvider'; |
||||
|
import {orbitTypes} from '@ezerous/breeze' |
||||
|
|
||||
|
const { web3 } = web3Options; |
||||
|
EthereumIdentityProvider.setWeb3(web3); |
||||
|
|
||||
|
const breezeOptions = { |
||||
|
ipfs: { |
||||
|
config: { |
||||
|
Addresses: { |
||||
|
Swarm: [ |
||||
|
// Use local signaling server (see also rendezvous script in package.json)
|
||||
|
// For more information: https://github.com/libp2p/js-libp2p-webrtc-star
|
||||
|
'/ip4/127.0.0.1/tcp/9090/wss/p2p-webrtc-star' |
||||
|
], |
||||
|
}, |
||||
|
}, |
||||
|
preload: { |
||||
|
enabled: false, |
||||
|
}, |
||||
|
init: { |
||||
|
emptyRepo: true, |
||||
|
} |
||||
|
}, |
||||
|
web3, |
||||
|
orbit: { |
||||
|
identityProvider: EthereumIdentityProvider, |
||||
|
databases: [ |
||||
|
{ |
||||
|
name: 'topics', |
||||
|
type: orbitTypes.ORBIT_TYPE_KEYVALUE |
||||
|
}, |
||||
|
{ |
||||
|
name: 'posts', |
||||
|
type: orbitTypes.ORBIT_TYPE_KEYVALUE |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default breezeOptions; |
@ -0,0 +1,15 @@ |
|||||
|
// See also: https://truffleframework.com/docs/drizzle/reference/drizzle-options
|
||||
|
import { contracts } from 'concordia-contracts'; |
||||
|
import web3Options from './web3Options'; |
||||
|
|
||||
|
const drizzleOptions = { |
||||
|
web3: { |
||||
|
customProvider: web3Options.web3 |
||||
|
}, |
||||
|
contracts, |
||||
|
events: { |
||||
|
Forum: ['UserSignedUp', 'UsernameUpdated', 'TopicCreated', 'PostCreated'] |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default drizzleOptions; |
@ -0,0 +1,14 @@ |
|||||
|
import Web3 from 'web3'; |
||||
|
import EthereumIdentityProvider from '../orbit/ΕthereumIdentityProvider'; |
||||
|
|
||||
|
const { WEB3_URL, WEB3_PORT } = process.env; |
||||
|
|
||||
|
const web3 = new Web3(Web3.givenProvider || `ws://${WEB3_URL}:${WEB3_PORT}`); |
||||
|
|
||||
|
EthereumIdentityProvider.setWeb3(web3); |
||||
|
|
||||
|
const web3Options = { |
||||
|
web3 |
||||
|
}; |
||||
|
|
||||
|
export default web3Options; |
@ -0,0 +1,23 @@ |
|||||
|
import level from 'level'; |
||||
|
|
||||
|
/* Used in development only to store the identity.signatures.publicKey so developers don't have to |
||||
|
repeatedly sign theOrbitDB creation transaction in MetaMask when React development server reloads |
||||
|
the app */ |
||||
|
const concordiaDB = level('./concordia/identities'); |
||||
|
|
||||
|
async function storeIdentitySignaturePubKey(key, signaturePubKey) { |
||||
|
await concordiaDB.put(key, signaturePubKey); |
||||
|
} |
||||
|
|
||||
|
// If it exists, it returns the identity.signatures.publicKey for the given key (key is the
|
||||
|
// concatenation of identity.publicKey + identity.signatures.id
|
||||
|
async function getIdentitySignaturePubKey(key) { |
||||
|
try { |
||||
|
return await concordiaDB.get(key); |
||||
|
} catch (err) { |
||||
|
if (err && err.notFound) return null; // Not found
|
||||
|
throw err; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { storeIdentitySignaturePubKey, getIdentitySignaturePubKey }; |
@ -0,0 +1,11 @@ |
|||||
|
// https://github.com/orbitdb/orbit-db/blob/master/GUIDE.md#address
|
||||
|
export async function determineDBAddress({orbit, dbName, type, identityId}) { |
||||
|
const ipfsMultihash = (await orbit.determineAddress(dbName, type, { |
||||
|
accessController: { write: [identityId] }, |
||||
|
})).root; |
||||
|
return `/orbitdb/${ipfsMultihash}/${dbName}`; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
@ -0,0 +1,108 @@ |
|||||
|
import { getIdentitySignaturePubKey, storeIdentitySignaturePubKey } from './levelUtils'; |
||||
|
import IdentityProvider from "orbit-db-identity-provider"; |
||||
|
|
||||
|
const LOGGING_PREFIX = 'EthereumIdentityProvider: '; |
||||
|
|
||||
|
class EthereumIdentityProvider extends IdentityProvider{ |
||||
|
constructor(options = {}) { |
||||
|
if(!EthereumIdentityProvider.web3) |
||||
|
throw new Error(LOGGING_PREFIX + "Couldn't create identity, because web3 wasn't set. " + |
||||
|
"Please use setWeb3(web3) first!"); |
||||
|
|
||||
|
super(options); |
||||
|
|
||||
|
// Orbit's Identity Id (user's Ethereum address) - Optional (will be grabbed later if omitted)
|
||||
|
const id = options.id; |
||||
|
if(id){ |
||||
|
if(EthereumIdentityProvider.web3.utils.isAddress(id)) |
||||
|
this.id = options.id; |
||||
|
else |
||||
|
throw new Error(LOGGING_PREFIX + "Couldn't create identity, because an invalid id was supplied."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
static get type() { return 'ethereum'; } |
||||
|
|
||||
|
async getId() { |
||||
|
// Id wasn't in the constructor, grab it now
|
||||
|
if(!this.id) { |
||||
|
const accounts = await EthereumIdentityProvider.web3.eth.getAccounts(); |
||||
|
if(!accounts[0]) |
||||
|
throw new Error(LOGGING_PREFIX + "Couldn't create identity, because no web3 accounts were found (locked Metamask?)."); |
||||
|
|
||||
|
this.id = accounts[0]; |
||||
|
} |
||||
|
return this.id; |
||||
|
} |
||||
|
|
||||
|
async signIdentity(data) { |
||||
|
if (process.env.NODE_ENV === 'development') { //Don't sign repeatedly while in development
|
||||
|
console.debug(LOGGING_PREFIX + 'Attempting to find stored Orbit identity data...'); |
||||
|
const signaturePubKey = await getIdentitySignaturePubKey(data); |
||||
|
if (signaturePubKey) { |
||||
|
const identityInfo = { |
||||
|
id: this.id, |
||||
|
pubKeySignId: data, |
||||
|
signaturePubKey, |
||||
|
}; |
||||
|
if (await EthereumIdentityProvider.verifyIdentityInfo(identityInfo)) { |
||||
|
console.debug(LOGGING_PREFIX + 'Found and verified stored Orbit identity data!'); |
||||
|
return signaturePubKey; |
||||
|
} |
||||
|
console.debug(LOGGING_PREFIX + "Stored Orbit identity data couldn't be verified."); |
||||
|
} else |
||||
|
console.debug(LOGGING_PREFIX + 'No stored Orbit identity data were found.'); |
||||
|
} |
||||
|
return await this.doSignIdentity(data); |
||||
|
} |
||||
|
|
||||
|
async doSignIdentity(data) { |
||||
|
try { |
||||
|
const signaturePubKey = await EthereumIdentityProvider.web3.eth.personal.sign(data, this.id, ''); |
||||
|
if (process.env.NODE_ENV === 'development') { |
||||
|
storeIdentitySignaturePubKey(data, signaturePubKey) |
||||
|
.then(() => { |
||||
|
console.debug(LOGGING_PREFIX + 'Successfully stored current Orbit identity data.'); |
||||
|
}) |
||||
|
.catch(() => { |
||||
|
console.warn(LOGGING_PREFIX + "Couldn't store current Orbit identity data..."); |
||||
|
}); |
||||
|
} |
||||
|
return signaturePubKey; // Password not required for MetaMask
|
||||
|
} catch (error) { |
||||
|
if(error.code && error.code === 4001){ |
||||
|
console.debug(LOGGING_PREFIX + 'User denied message signature.'); |
||||
|
return await this.doSignIdentity(data); |
||||
|
} |
||||
|
else{ |
||||
|
console.error(LOGGING_PREFIX + 'Failed to sign data.'); |
||||
|
console.error(error); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
static async verifyIdentity(identity) { |
||||
|
// Verify that identity was signed by the ID
|
||||
|
return new Promise(resolve => { |
||||
|
resolve(EthereumIdentityProvider.web3.eth.accounts.recover(identity.publicKey + identity.signatures.id, |
||||
|
identity.signatures.publicKey) === identity.id) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
static async verifyIdentityInfo(identityInfo) { |
||||
|
// Verify that identity was signed by the ID
|
||||
|
return new Promise(resolve => { |
||||
|
resolve(EthereumIdentityProvider.web3.eth.accounts.recover(identityInfo.pubKeySignId, |
||||
|
identityInfo.signaturePubKey) === identityInfo.id) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// Initialize by supplying a web3 object
|
||||
|
static setWeb3(web3){ |
||||
|
EthereumIdentityProvider.web3 = web3; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
EthereumIdentityProvider.web3 = {}; |
||||
|
|
||||
|
export default EthereumIdentityProvider; |
@ -0,0 +1,23 @@ |
|||||
|
import { put, all, take } from 'redux-saga/effects' |
||||
|
|
||||
|
import { breezeActions } from '@ezerous/breeze' |
||||
|
import { drizzleActions } from '@ezerous/drizzle' |
||||
|
|
||||
|
export function * initOrbitDatabases (action) { |
||||
|
const { account, breeze} = action; |
||||
|
yield put(breezeActions.orbit.orbitInit(breeze, account)); //same as breeze.initOrbit(account);
|
||||
|
} |
||||
|
|
||||
|
function * orbitSaga () { |
||||
|
// Not sure which will come first
|
||||
|
const res = yield all([ |
||||
|
take(breezeActions.breeze.BREEZE_INITIALIZED), |
||||
|
take(action => action.type === drizzleActions.account.ACCOUNTS_FETCHED |
||||
|
&& action.accounts.length > 0) |
||||
|
]); |
||||
|
|
||||
|
yield initOrbitDatabases({breeze:res[0].breeze, account: res[1].accounts[0]}); |
||||
|
} |
||||
|
|
||||
|
export default orbitSaga |
||||
|
|
@ -0,0 +1,15 @@ |
|||||
|
import { all, fork } from 'redux-saga/effects'; |
||||
|
import { drizzleSagas } from '@ezerous/drizzle'; |
||||
|
import { breezeSagas } from '@ezerous/breeze' |
||||
|
import orbitSaga from './orbitSaga' |
||||
|
|
||||
|
export default function* root() { |
||||
|
const sagas = [ |
||||
|
...drizzleSagas, |
||||
|
...breezeSagas, |
||||
|
orbitSaga |
||||
|
]; |
||||
|
yield all( |
||||
|
sagas.map((saga) => fork(saga)), |
||||
|
); |
||||
|
} |
@ -0,0 +1,23 @@ |
|||||
|
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; |
||||
|
import { drizzleReducers, drizzleMiddlewares, generateContractsInitialState } from '@ezerous/drizzle'; |
||||
|
import { breezeReducers, breezeMiddlewares } from '@ezerous/breeze' |
||||
|
import createSagaMiddleware from 'redux-saga'; |
||||
|
import rootSaga from './sagas/rootSaga'; |
||||
|
import drizzleOptions from '../options/drizzleOptions'; |
||||
|
|
||||
|
const initialState = { |
||||
|
contracts: generateContractsInitialState(drizzleOptions), |
||||
|
}; |
||||
|
|
||||
|
const sagaMiddleware = createSagaMiddleware(); |
||||
|
|
||||
|
const store = configureStore({ |
||||
|
reducer: {...drizzleReducers, ...breezeReducers }, |
||||
|
middleware: getDefaultMiddleware({ |
||||
|
serializableCheck: false, //https://redux.js.org/style-guide/style-guide/#do-not-put-non-serializable-values-in-state-or-actions
|
||||
|
}).concat(drizzleMiddlewares).concat(breezeMiddlewares).concat(sagaMiddleware), |
||||
|
preloadedState: initialState |
||||
|
}) |
||||
|
|
||||
|
sagaMiddleware.run(rootSaga); |
||||
|
export default store; |
@ -0,0 +1,135 @@ |
|||||
|
// This optional code is used to register a service worker.
|
||||
|
// register() is not called by default.
|
||||
|
|
||||
|
// This lets the app load faster on subsequent visits in production, and gives
|
||||
|
// it offline capabilities. However, it also means that developers (and users)
|
||||
|
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
|
// existing tabs open on the page have been closed, since previously cached
|
||||
|
// resources are updated in the background.
|
||||
|
|
||||
|
// To learn more about the benefits of this model and instructions on how to
|
||||
|
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
|
||||
|
const isLocalhost = Boolean( |
||||
|
window.location.hostname === 'localhost' || |
||||
|
// [::1] is the IPv6 localhost address.
|
||||
|
window.location.hostname === '[::1]' || |
||||
|
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
|
window.location.hostname.match( |
||||
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
export function register(config) { |
||||
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { |
||||
|
// The URL constructor is available in all browsers that support SW.
|
||||
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); |
||||
|
if (publicUrl.origin !== window.location.origin) { |
||||
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
|
// from what our page is served on. This might happen if a CDN is used to
|
||||
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
window.addEventListener('load', () => { |
||||
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; |
||||
|
|
||||
|
if (isLocalhost) { |
||||
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
|
checkValidServiceWorker(swUrl, config); |
||||
|
|
||||
|
// Add some additional logging to localhost, pointing developers to the
|
||||
|
// service worker/PWA documentation.
|
||||
|
navigator.serviceWorker.ready.then(() => { |
||||
|
console.log( |
||||
|
'This web app is being served cache-first by a service ' + |
||||
|
'worker. To learn more, visit https://bit.ly/CRA-PWA' |
||||
|
); |
||||
|
}); |
||||
|
} else { |
||||
|
// Is not localhost. Just register service worker
|
||||
|
registerValidSW(swUrl, config); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function registerValidSW(swUrl, config) { |
||||
|
navigator.serviceWorker |
||||
|
.register(swUrl) |
||||
|
.then(registration => { |
||||
|
registration.onupdatefound = () => { |
||||
|
const installingWorker = registration.installing; |
||||
|
if (installingWorker == null) { |
||||
|
return; |
||||
|
} |
||||
|
installingWorker.onstatechange = () => { |
||||
|
if (installingWorker.state === 'installed') { |
||||
|
if (navigator.serviceWorker.controller) { |
||||
|
// At this point, the updated precached content has been fetched,
|
||||
|
// but the previous service worker will still serve the older
|
||||
|
// content until all client tabs are closed.
|
||||
|
console.log( |
||||
|
'New content is available and will be used when all ' + |
||||
|
'tabs for this page are closed. See https://bit.ly/CRA-PWA.' |
||||
|
); |
||||
|
|
||||
|
// Execute callback
|
||||
|
if (config && config.onUpdate) { |
||||
|
config.onUpdate(registration); |
||||
|
} |
||||
|
} else { |
||||
|
// At this point, everything has been precached.
|
||||
|
// It's the perfect time to display a
|
||||
|
// "Content is cached for offline use." message.
|
||||
|
console.log('Content is cached for offline use.'); |
||||
|
|
||||
|
// Execute callback
|
||||
|
if (config && config.onSuccess) { |
||||
|
config.onSuccess(registration); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
}; |
||||
|
}) |
||||
|
.catch(error => { |
||||
|
console.error('Error during service worker registration:', error); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function checkValidServiceWorker(swUrl, config) { |
||||
|
// Check if the service worker can be found. If it can't reload the page.
|
||||
|
fetch(swUrl) |
||||
|
.then(response => { |
||||
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
|
const contentType = response.headers.get('content-type'); |
||||
|
if ( |
||||
|
response.status === 404 || |
||||
|
(contentType != null && contentType.indexOf('javascript') === -1) |
||||
|
) { |
||||
|
// No service worker found. Probably a different app. Reload the page.
|
||||
|
navigator.serviceWorker.ready.then(registration => { |
||||
|
registration.unregister().then(() => { |
||||
|
window.location.reload(); |
||||
|
}); |
||||
|
}); |
||||
|
} else { |
||||
|
// Service worker found. Proceed as normal.
|
||||
|
registerValidSW(swUrl, config); |
||||
|
} |
||||
|
}) |
||||
|
.catch(() => { |
||||
|
console.log( |
||||
|
'No internet connection found. App is running in offline mode.' |
||||
|
); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export function unregister() { |
||||
|
if ('serviceWorker' in navigator) { |
||||
|
navigator.serviceWorker.ready.then(registration => { |
||||
|
registration.unregister(); |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -0,0 +1,159 @@ |
|||||
|
//SPDX-License-Identifier: MIT |
||||
|
pragma solidity 0.7.1; |
||||
|
|
||||
|
contract Forum { |
||||
|
|
||||
|
//----------------------------------------USER---------------------------------------- |
||||
|
struct User { |
||||
|
string username; // TODO: set an upper bound instead of arbitrary string |
||||
|
uint[] topicIDs; // IDs of the topics the user created |
||||
|
uint[] postIDs; // IDs of the posts the user created |
||||
|
uint timestamp; |
||||
|
bool signedUp; // Helper variable for hasUserSignedUp() |
||||
|
} |
||||
|
|
||||
|
mapping (address => User) users; |
||||
|
mapping (string => address) userAddresses; |
||||
|
|
||||
|
event UserSignedUp(string username, address userAddress); |
||||
|
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); |
||||
|
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."); |
||||
|
string memory oldUsername = getUsername(msg.sender); |
||||
|
delete userAddresses[users[msg.sender].username]; |
||||
|
users[msg.sender].username = newUsername; |
||||
|
userAddresses[newUsername] = msg.sender; |
||||
|
emit UsernameUpdated(newUsername, oldUsername, msg.sender); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
function getUsername(address userAddress) public view returns (string memory) { |
||||
|
require (hasUserSignedUp(userAddress), "User hasn't signed up yet."); |
||||
|
return users[userAddress].username; |
||||
|
} |
||||
|
|
||||
|
function getUserAddress(string memory username) public view returns (address) { |
||||
|
return userAddresses[username]; |
||||
|
} |
||||
|
|
||||
|
function hasUserSignedUp(address userAddress) public view returns (bool) { |
||||
|
return users[userAddress].signedUp; |
||||
|
} |
||||
|
|
||||
|
function isUserNameTaken(string memory username) public view returns (bool) { |
||||
|
if (getUserAddress(username)!=address(0)) |
||||
|
return true; |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//----------------------------------------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<numTopics); // Only allow posting to a topic that exists |
||||
|
uint postID = numPosts++; |
||||
|
posts[postID] = Post(postID, msg.sender, block.timestamp, topicID); |
||||
|
topics[topicID].postIDs.push(postID); |
||||
|
users[msg.sender].postIDs.push(postID); |
||||
|
emit PostCreated(postID, topicID); |
||||
|
return postID; |
||||
|
} |
||||
|
|
||||
|
function getNumberOfTopics() public view returns (uint) { |
||||
|
return numTopics; |
||||
|
} |
||||
|
|
||||
|
function getTopic(uint topicID) public view returns (address, string memory, uint, uint[] memory) { |
||||
|
//TODO: require(hasUserSignedUp(msg.sender)); needed? |
||||
|
require(topicID<numTopics); |
||||
|
return ( |
||||
|
topics[topicID].author, |
||||
|
users[topics[topicID].author].username, |
||||
|
topics[topicID].timestamp, |
||||
|
topics[topicID].postIDs |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function getTopicPosts(uint topicID) public view returns (uint[] memory) { |
||||
|
require(topicID<numTopics); // Topic should exist |
||||
|
return topics[topicID].postIDs; |
||||
|
} |
||||
|
|
||||
|
function getPost(uint postID) public view returns (address, string memory, uint, uint) { |
||||
|
//TODO: require(hasUserSignedUp(msg.sender)); needed? |
||||
|
require(postID<numPosts); |
||||
|
return ( |
||||
|
posts[postID].author, |
||||
|
users[posts[postID].author].username, |
||||
|
posts[postID].timestamp, |
||||
|
posts[postID].topicID |
||||
|
); |
||||
|
} |
||||
|
} |
@ -0,0 +1,23 @@ |
|||||
|
pragma solidity 0.7.1; |
||||
|
|
||||
|
contract Migrations { |
||||
|
address public owner; |
||||
|
uint public last_completed_migration; |
||||
|
|
||||
|
constructor() public { |
||||
|
owner = msg.sender; |
||||
|
} |
||||
|
|
||||
|
modifier restricted() { |
||||
|
if (msg.sender == owner) _; |
||||
|
} |
||||
|
|
||||
|
function setCompleted(uint completed) public restricted { |
||||
|
last_completed_migration = completed; |
||||
|
} |
||||
|
|
||||
|
function upgrade(address new_address) public restricted { |
||||
|
Migrations upgraded = Migrations(new_address); |
||||
|
upgraded.setCompleted(last_completed_migration); |
||||
|
} |
||||
|
} |
@ -0,0 +1,13 @@ |
|||||
|
let Forum; |
||||
|
|
||||
|
try { |
||||
|
// eslint-disable-next-line global-require
|
||||
|
Forum = require('./build/Forum.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] |
||||
|
}; |
@ -0,0 +1,5 @@ |
|||||
|
const Migrations = artifacts.require('./Migrations.sol'); |
||||
|
|
||||
|
module.exports = function (deployer) { |
||||
|
deployer.deploy(Migrations); |
||||
|
}; |
@ -0,0 +1,5 @@ |
|||||
|
const Forum = artifacts.require('Forum'); |
||||
|
|
||||
|
module.exports = function (deployer) { |
||||
|
deployer.deploy(Forum); |
||||
|
}; |
@ -0,0 +1,22 @@ |
|||||
|
{ |
||||
|
"name": "concordia-contracts", |
||||
|
"version": "0.1.0", |
||||
|
"private": true, |
||||
|
"description": "Contracts used to power Concordia", |
||||
|
"main": "index.js", |
||||
|
"scripts": { |
||||
|
"compile": "yarn truffle compile", |
||||
|
"lint": "yarn _eslint && yarn _solhint", |
||||
|
"_eslint": "yarn eslint . --format table", |
||||
|
"_solhint": "yarn solhint --formatter table contracts/*.sol test/*.sol", |
||||
|
"test": "yarn truffle test", |
||||
|
"migrate": "yarn truffle migrate --network develop" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@openzeppelin/contracts": "3.1.0", |
||||
|
"truffle": "5.1.43" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"solhint": "3.2.0" |
||||
|
} |
||||
|
} |
@ -0,0 +1,27 @@ |
|||||
|
const path = require("path"); |
||||
|
|
||||
|
const { GANACHE_HOST } = process.env; |
||||
|
const { GANACHE_PORT } = process.env; |
||||
|
|
||||
|
module.exports = { |
||||
|
// See <http://truffleframework.com/docs/advanced/configuration>
|
||||
|
// to customize your Truffle configuration!
|
||||
|
compilers: { |
||||
|
solc: { |
||||
|
version: "0.7.1" |
||||
|
} |
||||
|
}, |
||||
|
contracts_build_directory: path.join(__dirname, 'build/'), |
||||
|
networks: { |
||||
|
develop: { |
||||
|
host: GANACHE_HOST || '127.0.0.1', |
||||
|
port: GANACHE_PORT || '8545', |
||||
|
network_id: '*', |
||||
|
}, |
||||
|
test: { |
||||
|
host: GANACHE_HOST || '127.0.0.1', |
||||
|
port: GANACHE_PORT || '8546', |
||||
|
network_id: '*', |
||||
|
} |
||||
|
} |
||||
|
}; |
File diff suppressed because it is too large
Loading…
Reference in new issue