Initial commit

This commit is contained in:
Richard Ramos 2018-08-20 14:42:27 -04:00
parent 08b0bdb767
commit 8c612b715d
36 changed files with 1714 additions and 0 deletions

12
.babelrc Normal file
View File

@ -0,0 +1,12 @@
{
"plugins": [
"transform-object-rest-spread"
],
"presets": [
"stage-2"
],
"ignore": [
"config/",
"node_modules"
]
}

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
/dist
/node_modules
/test

287
.eslintrc Normal file
View File

@ -0,0 +1,287 @@
{
"parser": "babel-eslint",
"plugins": [
"react"
],
"parserOptions": {
"ecmaVersion": 2017,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"env": {
"es6": true,
"browser": true,
"node": true,
"mocha": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"rules": {
"accessor-pairs": "error",
"array-bracket-newline": "error",
"array-bracket-spacing": [
"error",
"never"
],
"array-callback-return": "off",
"array-element-newline": "off",
"arrow-body-style": "off",
"arrow-parens": "off",
"arrow-spacing": [
"error",
{
"after": true,
"before": true
}
],
"block-scoped-var": "error",
"block-spacing": "error",
"brace-style": "off",
"callback-return": "off",
"camelcase": "off",
"capitalized-comments": "off",
"class-methods-use-this": "off",
"comma-dangle": "error",
"comma-spacing": "off",
"comma-style": [
"error",
"last"
],
"complexity": "error",
"computed-property-spacing": [
"error",
"never"
],
"consistent-return": "off",
"consistent-this": "off",
"curly": "off",
"default-case": "error",
"dot-location": [
"error",
"property"
],
"dot-notation": "off",
"eol-last": "error",
"eqeqeq": "off",
"for-direction": "error",
"func-call-spacing": "error",
"func-name-matching": "error",
"func-names": "off",
"func-style": "off",
"function-paren-newline": "off",
"generator-star-spacing": "error",
"getter-return": "error",
"global-require": "off",
"guard-for-in": "off",
"handle-callback-err": "off",
"id-blacklist": "error",
"id-length": "off",
"id-match": "error",
"indent": "off",
"indent-legacy": "off",
"init-declarations": "off",
"jsx-quotes": "error",
"key-spacing": "off",
"keyword-spacing": "off",
"line-comment-position": "off",
"linebreak-style": [
"error",
"unix"
],
"lines-around-comment": "error",
"lines-around-directive": "error",
"max-depth": "error",
"max-len": "off",
"max-lines": "off",
"max-nested-callbacks": "error",
"max-params": "off",
"max-statements": "off",
"max-statements-per-line": "off",
"multiline-ternary": [
"error",
"never"
],
"new-parens": "off",
"newline-after-var": "off",
"newline-before-return": "off",
"newline-per-chained-call": "off",
"no-alert": "error",
"no-array-constructor": "error",
"no-await-in-loop": "error",
"no-bitwise": "error",
"no-buffer-constructor": "error",
"no-caller": "error",
"no-catch-shadow": "error",
"no-confusing-arrow": "error",
"no-console": "off",
"no-continue": "off",
"no-div-regex": "error",
"no-duplicate-imports": "error",
"no-else-return": "off",
"no-empty-function": "off",
"no-eq-null": "error",
"no-eval": "off",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-label": "error",
"no-extra-parens": "off",
"no-floating-decimal": "error",
"no-implicit-coercion": "error",
"no-implicit-globals": "error",
"no-implied-eval": "error",
"no-inline-comments": "off",
"no-inner-declarations": [
"error",
"functions"
],
"no-invalid-this": "off",
"no-iterator": "error",
"no-label-var": "error",
"no-labels": "error",
"no-lone-blocks": "error",
"no-lonely-if": "off",
"no-loop-func": "off",
"no-magic-numbers": "off",
"no-mixed-operators": "error",
"no-mixed-requires": "error",
"no-multi-assign": "error",
"no-multi-spaces": "off",
"no-multi-str": "error",
"no-multiple-empty-lines": "error",
"no-native-reassign": "error",
"no-negated-condition": "off",
"no-negated-in-lhs": "error",
"no-nested-ternary": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-object": "error",
"no-new-require": "error",
"no-new-wrappers": "error",
"no-octal-escape": "error",
"no-param-reassign": "off",
"no-path-concat": "error",
"no-plusplus": "off",
"no-process-env": "off",
"no-process-exit": "off",
"no-proto": "error",
"no-prototype-builtins": "off",
"no-restricted-globals": "error",
"no-restricted-imports": "error",
"no-restricted-modules": "error",
"no-restricted-properties": "error",
"no-restricted-syntax": "error",
"no-return-assign": "error",
"no-return-await": "error",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow": "off",
"no-shadow-restricted-names": "error",
"no-spaced-func": "error",
"no-sync": "off",
"no-tabs": "error",
"no-template-curly-in-string": "error",
"no-ternary": "off",
"no-throw-literal": "error",
"no-trailing-spaces": "off",
"no-undef-init": "error",
"no-undefined": "off",
"no-underscore-dangle": "off",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": "error",
"no-unused-expressions": "error",
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"no-use-before-define": "off",
"no-useless-call": "off",
"no-useless-computed-key": "error",
"no-useless-concat": "error",
"no-useless-constructor": "error",
"no-useless-escape": "off",
"no-useless-rename": "error",
"no-useless-return": "off",
"no-var": "off",
"no-void": "error",
"no-warning-comments": "off",
"no-whitespace-before-property": "error",
"no-with": "error",
"nonblock-statement-body-position": "error",
"object-curly-newline": "off",
"object-curly-spacing": [
"error",
"never"
],
"object-property-newline": "off",
"object-shorthand": "off",
"one-var": "off",
"one-var-declaration-per-line": "off",
"operator-assignment": "off",
"operator-linebreak": "error",
"padded-blocks": "off",
"padding-line-between-statements": "error",
"prefer-arrow-callback": "off",
"prefer-const": "off",
"prefer-destructuring": "off",
"prefer-numeric-literals": "error",
"prefer-promise-reject-errors": "error",
"prefer-reflect": "off",
"prefer-rest-params": "off",
"prefer-spread": "off",
"prefer-template": "off",
"quote-props": "off",
"quotes": "off",
"radix": "error",
"require-await": "error",
"require-jsdoc": "off",
"rest-spread-spacing": "error",
"semi": "error",
"semi-spacing": [
"error",
{
"after": true,
"before": false
}
],
"semi-style": [
"error",
"last"
],
"sort-imports": "off",
"sort-keys": "off",
"sort-vars": "off",
"space-before-blocks": "off",
"space-before-function-paren": "off",
"space-in-parens": [
"error",
"never"
],
"space-infix-ops": "off",
"space-unary-ops": "error",
"spaced-comment": "off",
"strict": "error",
"switch-colon-spacing": "error",
"symbol-description": "error",
"template-curly-spacing": [
"error",
"never"
],
"template-tag-spacing": "error",
"unicode-bom": [
"error",
"never"
],
"valid-jsdoc": "error",
"vars-on-top": "off",
"wrap-iife": "error",
"wrap-regex": "error",
"yield-star-spacing": "error",
"yoda": [
"error",
"never"
]
}
}

43
.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# embark
.embark/
chains.json
config/production/password
config/livenet/password
# egg-related
viper.egg-info/
build/
dist/
.eggs/
# pyenv
.python-version
# dotenv
.env
# virtualenv
.venv/
venv/
ENV/
# Coverage tests
.coverage
.cache/
coverage/
coverageEnv/
coverage.json
# node
node_modules/
npm-debug.log
package-lock.json
# other
.vs/
bin/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Richard Ramos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,47 @@
## Decentralized Reddit dApp built using Embark by Status
Developed for a Mexico workshop, this dApp provides an example of how to build a dApp using Embark.
### Instructions
To follow along, please use the [instructions](./instructions).
### Notes for workshop instructor.
1. This dApp has less functionality than DTwitter, but includes a step for deploying the DApp to testnet, as well as a section to discuss voting mechanism when building the contract.
2. Recommended time: 1 - 1.5hr.
3. For the upload, it's recommended to have deployed the dApp before, and have a short link made using a service like [bit.ly](https://bit.ly). You need also an account that has Ropsten ether to deploy the contracts.
4. After uploading the dApp, access it via status. You can use Status Desktop, or your phone. If you use your phone, you can display its screen on your computer using an app like [Airdroid](https://www.airdroid.com/) (free, for Android) or [Reflector](http://www.airsquirrels.com/reflector/) (for both iOS and Android)
### FAQ
1. Can I use `embark simulator`?
Yes, you can. You can even use `ganache-cli` directly if desired. We are not using for this dApp due to websockets limitations of ganache.
2. Do we have some success stories of people using embark and delivering projects?
* The Status OKR voting app: https://github.com/status-im/contracts/tree/000-snt-voting-dapp
* Vortex demo using embark: https://github.com/Horyus/vortex-demo-embark/blob/master/package.json#L23-L35
* ERC-721 (collectibles) tutorial and demo app: https://github.com/status-im/embark-tutorial-erc721. Tutorial docs here: https://github.com/status-im/embark-tutorial-erc721/tree/tutorial-series/tutorial
3. Where can I learn more about the API of the EmbarkJS object?
* We are currently working on updating the docs, and additionally we are currently making EmbarkJS a library on its own right: https://github.com/embark-framework/EmbarkJS
4. What are the "key" features of Embark?
* Embark is focused on the trinity of web3 components that can be used to create truly decentralised applications
* The original trinity of web3, from the ethereum platform perspective, includes consensus, blockchain, and smart contracts. As well as swarm for decentralized storage and whisper for decentralized communication.
* Embark is a “true” dApp framework to assist the developer in building real dApps. Embark can also be used just for developing contracts as well.
* Embark is meant to help the developer easily integrate with other decentralized technologies, like storage, communication, and (in 3.2) naming systems.
* Embark is meant to be both advanced and easy to use
* Embark has reasonable defaults and tries to make your life easier, but it can always be overridden for configurable for your specific needs
* Embark has a very plugin-driven architecture, so developers can also extend it to their own needs
* Integrated debugger - we are currently working on this, and the developer can always use remix as an alternative
### Known issues with this dApp
##### Issues on localhost
1. Create account - hangs with metamask
* Cause - most likely non-increasing nonce issue with metamask: https://github.com/MetaMask/metamask-extension/issues/1999)
* Solution - open issue
##### Issues on testnet (rinkeby)
1. Sometimes errors with the “known transaction” when deploying a contract.
* *Cause:* not yet known
* *Solution:* only solution so far is to increase the `gasPrice` in contracts -> testnet -> deployment. Embark team currently investigating.
2. Getting contract events (tweets in this case) doesnt work
* *Cause:* Metamask known issue “The current provider doesnt support subscriptions: MetamaskInpageProvider”
* *Solution:* open issue
3. Websockets errors
* *Cause:* known issue with metamask: https://github.com/MetaMask/metamask-extension/issues/1645)
* *Solution:* open issue

9
app/css/dapp.css Normal file
View File

@ -0,0 +1,9 @@
body {
margin: 0;
background: #e8e8e8;
padding-top: 60px;
}
p {
font-size: 14px;
}

0
app/images/.gitkeep Normal file
View File

20
app/index.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<link rel="manifest" href="/manifest.json">
<link rel="shortcut icon" href="/favicon.ico">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="css/dapp.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="text/javascript" src="js/dapp.js"></script>
</body>
</html>

90
app/js/components/App.js Normal file
View File

@ -0,0 +1,90 @@
import React, {Component, Fragment} from 'react';
import Create from './Create';
import Header from './Header';
import Post from './Post';
import _ from 'lodash';
import EmbarkJS from 'Embark/EmbarkJS';
import DReddit from 'Embark/contracts/DReddit';
class App extends Component {
constructor(props) {
super(props);
this.state = {
'displayForm': false,
'list': [],
'sortBy': 'age',
'sortOrder': 'desc',
'filterBy': ''
};
}
componentDidMount() {
// TODO: Invoke the next function as soon as Embark is ready
this._loadPosts();
}
_toggleForm = () => {
this.setState({displayForm: !this.state.displayForm});
}
_setSortOrder = (sortBy) => {
const sortOrder = (this.state.sortOrder == 'asc' && this.state.sortBy == sortBy) || this.state.sortBy != sortBy ? 'desc' : 'asc';
this.setState({sortBy, sortOrder});
}
_loadPosts = async () => {
// TODO: Using the functions `post` and `numPost` from our contract, load the posts
let list = [];
const total = 1; // TODO: Use
if(total > 0){
for (let i = 0; i < total; i++) {
// TODO: the constant `currentPost` should have the info that comes from the `post` function of the contract.
// this object here is just a placeholder.
const currentPost = {
upvotes: 0, downvotes: 0, owner: "0x1234567890123456789012345678901234567890", creationDate: "153399", description: "0x00"
};
list.push(currentPost);
}
list = await Promise.all(list);
list = list.map((value, index) => {
value.id = index;
value.upvotes = parseInt(value.upvotes, 10);
value.downvotes = parseInt(value.downvotes, 10);
return value;
});
}
this.setState({list});
}
_search = (filterBy) => {
this.setState({filterBy});
}
render() {
const {displayForm, list, sortBy, sortOrder, filterBy} = this.state;
let orderedList;
if(sortBy == 'rating'){
orderedList = _.orderBy(list, [function(o) { return o.upvotes - o.downvotes; }, 'creationDate'], [sortOrder, sortOrder]);
} else {
orderedList = _.orderBy(list, 'creationDate', sortOrder);
}
return (<Fragment>
<Header toggleForm={this._toggleForm} sortOrder={this._setSortOrder} search={this._search} />
{ displayForm && <Create afterPublish={this._loadPosts} /> }
{ orderedList.map((record) => <Post key={record.id} {...record} filterBy={filterBy} />) }
</Fragment>
);
}
}
export default App;

118
app/js/components/Create.js Normal file
View File

@ -0,0 +1,118 @@
import React, {Component, Fragment} from 'react';
import Button from '@material-ui/core/Button';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import LinearProgress from '@material-ui/core/LinearProgress';
import PropTypes from 'prop-types';
import TextField from '@material-ui/core/TextField';
import {withStyles} from '@material-ui/core/styles';
// TODO: import EmbarkJS, web3 and our DReddit contract
const styles = theme => ({
textField: {
marginRight: theme.spacing.unit * 2
}
});
class Create extends Component{
constructor(props){
super(props);
this.state = {
'title': '',
'content': '',
'isSubmitting': false,
'error': ''
};
}
handleClick = async event => {
event.preventDefault();
if(this.state.title.trim() == ''){
this.setState({'error': 'Required field'});
return;
}
this.setState({
isSubmitting: true,
error: ''
});
const textToSave = {
'title': this.state.title,
'content': this.state.content
};
// TODO: Save the previous object in IPFS
// TODO: Estimate gas required to invoke the `create` function from the contract
// TODO: Send the transaction
this.setState({
isSubmitting: false,
content: '',
title: ''
});
this.props.afterPublish();
}
handleChange = name => event => {
this.setState({
[name]: event.target.value
});
};
render(){
const {classes} = this.props;
const {error, content, title, isSubmitting} = this.state;
return (<Fragment>
<Card>
<CardContent>
<TextField
id="title"
label="Title"
error={error != ""}
multiline
rowsMax="20"
fullWidth
value={title}
helperText={error}
onChange={this.handleChange('title')}
className={classes.textField}
margin="normal" />
<TextField
id="description"
label="Description"
error={error != ""}
multiline
rowsMax="20"
fullWidth
value={content}
helperText={error}
onChange={this.handleChange('content')}
className={classes.textField}
margin="normal" />
{
<Button variant="contained" color="primary" onClick={this.handleClick} disabled={isSubmitting }>Publish</Button>
}
</CardContent>
</Card>
{ this.state.isSubmitting && <LinearProgress /> }
</Fragment>
);
}
}
Create.propTypes = {
classes: PropTypes.object.isRequired,
afterPublish: PropTypes.func.isRequired
};
export default withStyles(styles)(Create);

111
app/js/components/Header.js Normal file
View File

@ -0,0 +1,111 @@
import React, {Component} from 'react';
import AddIcon from '@material-ui/icons/Add';
import AppBar from '@material-ui/core/AppBar';
import Button from '@material-ui/core/Button';
import Hidden from '@material-ui/core/Hidden';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import PropTypes from 'prop-types';
import SearchBar from 'material-ui-search-bar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import {withStyles} from '@material-ui/core/styles';
const styles = {
root: {
flexGrow: 1
},
flex: {
flexGrow: 1
}
};
const options = [
'Sort by age',
'Sort by rating'
];
class Header extends Component {
constructor(props){
super(props);
this.state = {
anchorEl: null,
sortIndex: 0
};
}
handleClick = event => {
event.preventDefault();
this.setState({anchorEl: event.currentTarget});
};
handleMenuClick = index => event => {
event.preventDefault();
this.setState({selectedIndex: index, anchorEl: null});
this.props.sortOrder(index == 0 ? 'age' : 'rating');
};
handleClose = () => {
this.setState({anchorEl: null});
};
render(){
const {classes, toggleForm, search} = this.props;
const {anchorEl, sortIndex} = this.state;
const open = Boolean(anchorEl);
return (
<div className={classes.root} >
<AppBar position="fixed">
<Toolbar className={classes.toolBar}>
<Hidden smDown>
<Typography variant="display1" color="inherit" className={classes.flex}>
DReddit
</Typography>
</Hidden>
<SearchBar
placeholder="Search..."
style={{
margin: '10px 10px',
maxWidth: 280
}}
onChange={(searchValue) => search(searchValue)}
/>
<Button color="inherit" onClick={toggleForm}>
<AddIcon />
</Button>
<Button color="inherit" onClick={this.handleClick}>
<MoreVertIcon />
</Button>
<Menu
id="long-menu"
anchorEl={anchorEl}
open={open}
onClose={this.handleClose}
PaperProps={{
style: {
width: 200
}
}}>
{options.map((option, i) => (
<MenuItem key={option} selected={i == sortIndex} onClick={this.handleMenuClick(i)}>
{option}
</MenuItem>
))}
</Menu>
</Toolbar>
</AppBar>
</div>
);
}
}
Header.propTypes = {
classes: PropTypes.object.isRequired,
toggleForm: PropTypes.func.isRequired,
sortOrder: PropTypes.func.isRequired,
search: PropTypes.func.isRequired
};
export default withStyles(styles)(Header);

169
app/js/components/Post.js Normal file
View File

@ -0,0 +1,169 @@
import {Card, CardActions, CardContent, CardHeader} from '@material-ui/core';
import React, {Component} from 'react';
import Blockies from 'react-blockies';
import CircularProgress from '@material-ui/core/CircularProgress';
import DownvoteIcon from '@material-ui/icons/ExpandMore';
import IconButton from '@material-ui/core/IconButton';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import PropTypes from 'prop-types';
import Typography from '@material-ui/core/Typography';
import UpvoteIcon from '@material-ui/icons/ExpandLess';
import dateformat from 'dateformat';
import markdownJS from "markdown";
import {withStyles} from '@material-ui/core/styles';
import EmbarkJS from 'Embark/EmbarkJS';
import DReddit from 'Embark/contracts/DReddit';
import web3 from 'Embark/web3';
const markdown = markdownJS.markdown;
const styles = theme => ({
actions: {
marginRight: theme.spacing.unit * 5,
fontSize: 15,
display: 'flex'
},
card: {
margin: theme.spacing.unit,
marginTop: theme.spacing.unit * 4,
position: 'relative'
},
title: {
borderBottom: '1px solid #ccc',
color: '#666'
},
spinner: {
position: 'absolute',
right: theme.spacing.unit * 3
}
});
const ballot = {
NONE: 0,
UPVOTE: 1,
DOWNVOTE: 2
};
const contains = (filterBy, content, title, date, owner) => {
filterBy = filterBy.trim().toLowerCase();
if(filterBy == '') return true;
return content.toLowerCase().indexOf(filterBy) > -1 ||
title.toLowerCase().indexOf(filterBy) > -1 ||
date.indexOf(filterBy) > -1 ||
owner.toLowerCase().indexOf(filterBy) > -1;
};
class Post extends Component {
constructor(props){
super(props);
this.state = {
title: '',
content: '',
isSubmitting: false,
canVote: true,
upvotes: props.upvotes,
downvotes: props.downvotes
};
}
componentDidMount(){
EmbarkJS.onReady(() => {
this._loadAttributes();
});
}
_loadAttributes = async () => {
const ipfsHash = web3.utils.toAscii(this.props.description);
// TODO: Obtain the content from IPFS using the `ipfsHash` variable
// TODO: Fill the `title` and `content` variables with the data obtained from IPFS
const title = "Isaac Asimov's \"Three Laws of Robotics\"";
const content = `1. A robot may not injure a human being or, through inaction, allow a human being to come to harm.\n
2. A robot must obey orders given it by human beings except where such orders would conflict with the First Law.\n
3. A robot must protect its own existence as long as such protection does not conflict with the First or Second Law.`;
// TODO: Determine if the current account can vote or not
const canVote = true;
this.setState({
title,
content,
canVote
});
}
_vote = choice => async event => {
event.preventDefault();
this.setState({isSubmitting: true});
// TODO: Estimate the cost of invoking the function `vote` from the contract
// TODO: Send the transaction
this.setState({
canVote: false,
upvotes: this.state.upvotes + (choice == ballot.UPVOTE ? 1 : 0),
downvotes: this.state.downvotes + (choice == ballot.DOWNVOTE ? 1 : 0)
});
this.setState({isSubmitting: false});
}
render(){
const {title, content, upvotes, downvotes, isSubmitting, canVote} = this.state;
const {creationDate, classes, owner, filterBy} = this.props;
const disabled = isSubmitting || !canVote;
const formattedDate = dateformat(new Date(creationDate * 1000), "yyyy-mm-dd HH:MM:ss");
const mdText = markdown.toHTML(content);
const display = contains(filterBy, content, title, formattedDate, owner);
return display&& <Card className={classes.card}>
<CardHeader title={owner} subheader={formattedDate}
avatar={
<Blockies seed={owner} size={7} scale={5} />
}
action={
<IconButton>
<MoreVertIcon />
</IconButton>
} />
<CardContent>
<Typography variant="title" className={classes.title} gutterBottom>
{title}
</Typography>
<Typography component="div" dangerouslySetInnerHTML={{__html: mdText}} />
</CardContent>
<CardActions disableActionSpacing>
<IconButton className={classes.actions} disabled={disabled} onClick={this._vote(ballot.UPVOTE)}>
<UpvoteIcon />
{upvotes}
</IconButton>
<IconButton className={classes.actions} disabled={disabled} onClick={this._vote(ballot.DOWNVOTE)}>
<DownvoteIcon />
{downvotes}
</IconButton>
{ isSubmitting && <CircularProgress size={14} className={classes.spinner} /> }
</CardActions>
</Card>;
}
}
Post.propTypes = {
filterBy: PropTypes.string,
upvotes: PropTypes.number.isRequired,
downvotes: PropTypes.number.isRequired,
classes: PropTypes.object.isRequired,
id: PropTypes.number.isRequired,
owner: PropTypes.string.isRequired,
creationDate: PropTypes.string.isRequired,
description: PropTypes.string.isRequired
};
export default withStyles(styles)(Post);

10
app/js/dapp.js Normal file
View File

@ -0,0 +1,10 @@
import App from './components/App';
import EmbarkJS from 'Embark/EmbarkJS';
import React from 'react';
import DReddit from 'Embark/contracts/DReddit';
import {render} from 'react-dom';
window.EmbarkJS = EmbarkJS;
window.DReddit = DReddit;
render(<App />, document.getElementById('root'));

62
config/blockchain.js Normal file
View File

@ -0,0 +1,62 @@
module.exports = {
development: {
enabled: true,
networkType: "custom", // Can be: testnet, rinkeby, livenet or custom, in which case, it will use the specified networkId
networkId: "1337", // Network id used when networkType is custom
isDev: true, // Uses and ephemeral proof-of-authority network with a pre-funded developer account, mining enabled
genesisBlock: "config/development/genesis.json", // Genesis block to initiate on first creation of a development node
datadir: ".embark/development/datadir", // Data directory for the databases and keystore
mineWhenNeeded: true, // Uses our custom script (if isDev is false) to mine only when needed
nodiscover: true, // Disables the peer discovery mechanism (manual peer addition)
maxpeers: 0, // Maximum number of network peers (network disabled if set to 0) (default: 25)
rpcHost: "localhost", // HTTP-RPC server listening interface (default: "localhost")
rpcPort: 8545, // HTTP-RPC server listening port (default: 8545)
rpcCorsDomain: "auto", // Comma separated list of domains from which to accept cross origin requests (browser enforced)
// When set to "auto", Embark will automatically set the cors to the address of the webserver
proxy: true, // Proxy is used to present meaningful information about transactions
account: {
// "address": "", // When specified, uses that address instead of the default one for the network
password: "config/development/password" // Password to unlock the account
},
targetGasLimit: 8000000, // Target gas limit sets the artificial target gas floor for the blocks to mine
wsRPC: true, // Enable the WS-RPC server
wsOrigins: "auto", // Origins from which to accept websockets requests
// When set to "auto", Embark will automatically set the cors to the address of the webserver
wsHost: "localhost", // WS-RPC server listening interface (default: "localhost")
wsPort: 8546, // WS-RPC server listening port (default: 8546)
simulatorMnemonic: "example exile argue silk regular smile grass bomb merge arm assist farm", // Mnemonic used by the simulator to generate a wallet
simulatorBlocktime: 0 // Specify blockTime in seconds for automatic mining. Default is 0 and no auto-mining.
},
testnet: {
enabled: true,
networkType: "testnet",
syncMode: "light",
rpcHost: "localhost",
rpcPort: 8545,
rpcCorsDomain: "http://localhost:8000",
account: {
password: "config/testnet/password"
}
},
livenet: {
enabled: true,
networkType: "livenet",
syncMode: "light",
rpcHost: "localhost",
rpcPort: 8545,
rpcCorsDomain: "http://localhost:8000",
account: {
password: "config/livenet/password"
}
},
privatenet: {
enabled: true,
networkType: "custom",
rpcHost: "localhost",
rpcPort: 8545,
rpcCorsDomain: "http://localhost:8000",
datadir: "yourdatadir",
networkId: "123",
bootnodes: ""
}
};

13
config/communication.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
default: {
enabled: true,
provider: "whisper", // Communication provider. Currently, Embark only supports whisper
available_providers: ["whisper"], // Array of available providers
connection: {
host: "localhost", // Host of the blockchain node
port: 8546, // Port of the blockchain node
type: "ws" // Type of connection (ws or rpc)
}
}
};

41
config/contracts.js Normal file
View File

@ -0,0 +1,41 @@
module.exports = {
// default applies to all environments
default: {
// Blockchain node to deploy the contracts
deployment: {
host: "localhost", // Host of the blockchain node
port: 8545, // Port of the blockchain node
type: "rpc" // Type of connection (ws or rpc),
// Accounts to use instead of the default account to populate your wallet
/*,accounts: [
{
privateKey: "your_private_key",
balance: "5 ether" // You can set the balance of the account in the dev environment
// Balances are in Wei, but you can specify the unit with its name
},
{
privateKeyFile: "path/to/file" // You can put more than one key, separated by , or ;
},
{
mnemonic: "12 word mnemonic",
addressIndex: "0", // Optionnal. The index to start getting the address
numAddresses: "1", // Optionnal. The number of addresses to get
hdpath: "m/44'/60'/0'/0/" // Optionnal. HD derivation path
}
]*/
},
// order of connections the dapp should connect to
dappConnection: [
"$WEB3", // uses pre existing web3 object if available (e.g in Mist)
"ws://localhost:8546",
"http://localhost:8545"
],
gas: "auto",
contracts: {
// example:
//SimpleStorage: {
// args: [ 100 ]
//}
}
}
};

View File

@ -0,0 +1,18 @@
{
"config": {
"homesteadBlock": 0,
"byzantiumBlock": 0,
"daoForkSupport": true
},
"nonce": "0x0000000000000042",
"difficulty": "0x0",
"alloc": {
"0x3333333333333333333333333333333333333333": {"balance": "15000000000000000000"}
},
"mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x3333333333333333333333333333333333333333",
"timestamp": "0x00",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"extraData": "0x",
"gasLimit": "0x7a1200"
}

View File

@ -0,0 +1 @@
dev_password

6
config/namesystem.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
default: {
available_providers: ["ens"],
provider: "ens"
}
};

35
config/storage.js Normal file
View File

@ -0,0 +1,35 @@
module.exports = {
default: {
enabled: true,
ipfs_bin: "ipfs",
provider: "ipfs",
available_providers: ["ipfs"],
upload: {
host: "localhost",
port: 5001
},
dappConnection: [
{
provider: "ipfs",
host: "localhost",
port: 5001,
getUrl: "http://localhost:8080/ipfs/"
}
]
// Configuration to start Swarm in the same terminal as `embark run`
/*,account: {
address: "YOUR_ACCOUNT_ADDRESS", // Address of account accessing Swarm
password: "PATH/TO/PASSWORD/FILE" // File containing the password of the account
},
swarmPath: "PATH/TO/SWARM/EXECUTABLE" // Path to swarm executable (default: swarm)*/
},
development: {
enabled: true,
provider: "ipfs",
upload: {
host: "localhost",
port: 5001,
getUrl: "http://localhost:8080/ipfs/"
}
}
};

1
config/testnet/password Normal file
View File

@ -0,0 +1 @@
test_password

5
config/webserver.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
enabled: true,
host: "localhost",
port: 8000
};

0
contracts/.gitkeep Normal file
View File

83
contracts/DReddit.sol Normal file
View File

@ -0,0 +1,83 @@
pragma solidity ^0.4.24;
// @notice Contract to create posts
contract DReddit {
enum Ballot { NONE, UPVOTE, DOWNVOTE }
struct Post {
uint creationDate;
bytes description;
address owner;
uint upvotes;
uint downvotes;
mapping(address => Ballot) voters;
}
Post[] public posts;
event NewPost (
uint indexed postId,
address owner,
bytes description
);
event Vote(
uint indexed postId,
address voter,
uint8 vote
);
// @notice Number of posts created
// @return Num of posts
function numPosts()
public
view
returns(uint)
{
// TODO:
return 1;
}
// @notice Create Post
// @param _description IPFS hash of the content of the post
function create(bytes _description)
public
{
// TODO:
}
// @notice Vote on a post
// @param _postId Id of the post to up/downvote
// @param _vote Vote selection: 0 -> none, 1 -> upvote, 2 -> downvote
function vote(uint _postId, uint8 _vote)
public
{
// TODO:
}
// @notice Determine if the sender can vote on a post
// @param _postId Id of the post
// @return bool that indicates if the sender can vote or not
function canVote(uint _postId)
public
view
returns (bool)
{
// TODO:
return true;
}
// @notice Obtain vote for specific post
// @param _postId Id of the post
// @return uint that represents the vote: 0 -> none, 1 -> upvote, 2 -> downvote
function getVote(uint _postId)
public
view
returns (uint8)
{
// TODO:
return 1;
}
}

18
embark.json Normal file
View File

@ -0,0 +1,18 @@
{
"contracts": ["contracts/**"],
"app": {
"css/dapp.css": ["app/css/**"],
"js/dapp.js": ["app/js/dapp.js"],
"images/": ["app/images/**"],
"index.html": "app/index.html"
},
"buildDir": "dist/",
"config": "config/",
"versions": {
"web3": "1.0.0-beta",
"solc": "0.4.24",
"ipfs-api": "17.2.4",
"p-iteration": "1.1.7"
},
"plugins": {}
}

View File

@ -0,0 +1,17 @@
## Creating a Decentralized Reddit dApp with Embark
### Intro
In this workshop, we'll explore how can we use Embark to simplify the development of a decentralised Reddit dApp (or DReddit). We will use the code-generated contract object, `DReddit`, and the `EmbarkJS` API to interact with our Ethereum smart contract and store image files on the decentralized filesystem `IPFS`.
The final code for this dApp can be found in the [DappCon Workshop dAppp repository](https://github.com/status-im/dappcon-workshop-dapp/blob/master/instructions/1%20Installation.md), however we be using the [`start-here` branch](https://github.com/status-im/dappcon-workshop-dapp/tree/start-here) as a starting point for our workshop. This branch contains some "missing" code that we will be adding together in this workshop.
This dApp uses [React](https://reactjs.org/), but is out of scope for this workshop, so don't worry, we will only be focussing on the pieces that Embark helps us build. In reality, any JS framework can be used with Embark.
## What is Embark?
Embark is a fast, simple and powerful framework to help you develop and deploy fully decentralized applications. Embark allows developers to leverage all decentralised technologies for building dApps, facilitated by code-generating contract objects and an `EmbarkJS` javascript API for use in your dApp. Embark is fully configurable, and can leverage as many or as few decentralized technologies as required. Embark can:
* Automatically run a blockchain node.
* Automatically run an IPFS or Swarm node and make these available via the `EmbarkJS API`. Embark can also upload your entire dApp to IPFS or Swarm.
* Compile and deploy Ethereum smart contracts (solidity, vyper, or bamboo) as you write them. Embark will also code generate your contracts, and will make them available via a JS object.
* Webpack and deploy your dApp assets as you write them.
* Integrate with `ENS`, available via the `EmbarkJS` API (coming in 3.2).
* Write and run contract unit tests

View File

@ -0,0 +1,53 @@
# Installation
## Check your environment
Prior to installing Embark, you should have the following prerequisites on your machine:
#### NodeJS 8.10+
```
node version
> 8.10+
```
If you need to update Node, please [install `nvm`](https://github.com/creationix/nvm#installation) and install/use the LTS version. macOS/Linux commands provided for you below:
```
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
nvm install --lts
nvm use lts
```
#### IPFS 0.4.15+
```
ipfs version
> 0.4.15+
```
[IPFS installation instructions](https://ipfs.io/docs/install/#installing-from-a-prebuilt-package), macOS/Linux command provided for you below:
```
tar xvfz go-ipfs.tar.gz
cd go-ipfs
./install.sh
ipfs init
```
#### Go-ethereum 1.8.11+
```
geth version
> 1.8.11+
```
If you need to [install `geth`](https://github.com/ethereum/go-ethereum/wiki/Building-Ethereum), you can use the below for macOS:
```
brew tap ethereum/ethereum
brew install ethereum
```
## Installing Embark 3.1.7
If you already have Embark installed, please run:
```
embark version
```
Make sure the version is `3.1.7`. If its not, re-install Embark by running:
```
npm install -g embark
```
> Do not use sudo when installing Embark
Re-run `embark version` to ensure we have `3.1.7`.
If you have not installed Embark at all, Embark can be installed by running
`npm install -g embark` (without sudo)

View File

@ -0,0 +1,26 @@
# DReddit Template clone and explore
Now that we have Embark installed properly, lets grab the DReddit dApp template that we will use as the skeleton of our dApp. This template is a website built using React (don't worry if you don't know React, we are not focussing on this part).
## We will...
We will leverage Embarks featureset to write our contract, write unit tests for the contract, and then use these functions inside the website.
## Let's go
Clone the DReddit template into your dApp folder:
```
cd [parent folder] (ie ~/dev/__github)
git clone https://github.com/status-im/dappcon-workshop-dapp
cd dappcon-workshop-dapp
git checkout start-here
```
Lets take a moment to look at the file structure of the dApp and open the template in our IDE.
* `/app` - contains all our assets for the website. These will get webpacked according to our settings in `/embark.json`.
* `/config` - contains all our configuration
* Blockchain - configures options for running geth
* Communication - configures whisper options
* Contracts - configures options for deploying contracts from Embark, as well as the connection to make to geth from the dApp
* Namesystem - configures ENS support (coming in 3.2)
* Storage - configures decentralised storage for IPFS and Swarm. Includes a section for uploading the dApp as well as a dApp connection that can be used in the dApp.
* Webserver - configuration options for the webserver that serves the dApp during development.
* `/contracts` - contains our contacts
* `/test` - contains our mocha tests for testing our contracts
* `embark.json` - configures file locations, webpack options, and library versions to use

View File

@ -0,0 +1,24 @@
## First run
Now lets run our website quickly to see what embark will do for us.
```
npm install
embark run
```
You should see the Embark console and it's components.
* *Contracts* - the top left shows which contracts are deployed and their address. * Modules loaded and running - the top right shows the status of the loaded modules running (or not running) in Embark.
* *Log* - the middle shows log output.
* *Console* - on the bottom row there is a console that will let us interact with `web3` and `ipfs` (try it out by typing `help` to see available commands).
### Embark output
Youll notice from the logs and from the modules that Embark has started Go-ethereum and IPFS processes, compiled and deployed our contract, and webpacked our website for us.
> The contract warning in orange will disappear once we update our contract.
### Take a tour of the barebones DReddit site
The website has several features that *are not yet hooked up to our contract*, but lets take a look around at the website anyway. Launch `http://localhost:8000` in your browser.
Functionality you'll see:
* Create a post
* Upvote / Downvote a post
* Search and view posts
Later, we'll hook this functionality up to a contract.

View File

@ -0,0 +1,74 @@
## Coding our contract
Let's start by modifying `./contracts/DReddit.sol` and see how Embark recompiles our contract whenever we update and save or file.
### Functions
#### Create a post
The `create` function is used to publish our posts. We need to add a new post to `posts[]` and emit an event indicating that a new post was created:
1. Add a `Post` to the `posts[]` array, asigning values to its attributes. For the `creationDate` use `block.timestamp`
```
uint postId = posts.length++;
posts[postId] = Post({
creationDate: block.timestamp,
description: _description,
owner: msg.sender,
upvotes: 0,
downvotes: 0
});
```
2. Emit the `NewPost` event
```
emit NewPost(postId, msg.sender, _description);
```
#### Upvote/Downvote a post
We want to be able to asign a score to the posts. For that, let's work on the function `vote()` from which the users will be able to upvote or downvote each post. NOTE: this voting mechanism is very naive and not recomended to be used on real life due to not being Sybil resistant:
1. Determine if the post exists, and if we have already voted
```
Post storage p = posts[_postId];
require(p.creationDate != 0, "Post does not exist");
require(p.voters[msg.sender] == Ballot.NONE, "You already voted on this post");
```
2. Store the vote and update the post score
```
Ballot b = Ballot(_vote);
if (b == Ballot.UPVOTE) {
p.upvotes++;
} else {
p.downvotes++;
}
l.voters[msg.sender] = b;
```
3. Emit the event
````
emit Vote(_postId, msg.sender, _vote);
````
> Building a Sybil resistant voting mechanism using a Proof of Identity that takes in account Privacy is a great challenge . How would you solve this problem?
#### Determine if an user can vote for a post
The users need to know somehow if they're able to vote on a post or not. There are two scenarios where a user should not be able to vote: a) The post doesn't exist, and b) The user already voted. This logic should be developed inside `canVote()`:
1. Determine if the posts exists
```
if(_postId > posts.length - 1) return false;
```
2. If the post exists, we will only allow voting if the user hasn't voted yet
```
Post storage p = posts[_postId];
return (p.voters[msg.sender] == Ballot.NONE);
```
#### Determine the user's vote
It's possible an user would like to know what was his choice on a previously voted post. Implement this on `getVote()`.
```
Post storage p = posts[_postId];
return uint8(p.voters[msg.sender]);
```
> Notice that on the last two functions, the input and output values related to votes are uin8 instead of an enum. Enum are not a valid input/output value for functions at least on the current solidity version.

View File

@ -0,0 +1,54 @@
## Unit testing
Now that we finished coding our contract, we can proceed to build unit testing for it. Embark generates javascript code for interacting with the contract `DReddit` and will make it possible to use it from both our unit tests and our dApp. There are some tests already implemented in `./test/contract_spec.js`, but we will add a couple that illustrate the most common test case scenarios
### We should be able to create a post and receive it via contract event
```
let receipt = await create(web3.utils.fromAscii(ipfsHash)).send();
const event = receipt.events.NewPost;
postId = event.returnValues.postId;
assert.equal(web3.utils.toAscii(event.returnValues.description), ipfsHash);
```
> This test demonstrates how to send a transaction to a contract. Since `create()` emits an event, we're able to access it in the transaction receipt, and we can use it to perform any assertion we need.
### The post should have correct data
```
const post = await posts(postId).call();
assert.equal(web3.utils.toAscii(post.description), ipfsHash);
assert.equal(post.owner, accounts[0]);
```
> On this test, we read data from the chain, without generating a transaction. Notice we use `call()` instead of `send()` when invoking the `post()` function. A `call` will read the current state of our contract
### We should't be able to vote twice for the same post
```
try {
const receipt = await vote(postId, 1).send();
assert.fail('should have reverted before');
} catch (error){
assert(error.message.search('revert') > -1, 'Revert should happen');
}
```
> On this test we can see the pattern to evaluate a transaction that will revert. It involves using a try/catch block, and `assert.fail()` should be used to guarantee that it will fail the test if it reaches that line. Since `vote()` will revert, it throws an exception that we capture, and evaluate the error message to determine if the transaction reverted.
### Running the tests
To execute the tests and see their result, on a terminal session run:
```
embark test
```
### Results
The results of your test unit should be similar to the following:
```
DReddit contract
✓ should be able to create a post and receive it via contract event
✓ should return 1 post
✓ post should have correct data
✓ should not be able to vote in an unexisting post report
✓ should be able to vote in a post if account hasn't voted before
✓ should be able to vote in a post
✓ should't be able to vote twice
7 passing (163ms)
> All tests passed
```

View File

@ -0,0 +1,99 @@
## Coding our dApp
Let's use our DReddit JS Object, and the `EmbarkJS` API to interact with our contract and IPFS. While we update oru files, notice how Embark watches and recompiles any asset we modify when we save the changes:
###### `Create.js`
1. Start by importing EmbarkJS, web3 and the contract object
```
import EmbarkJS from 'Embark/EmbarkJS';
import EtherPress from 'Embark/contracts/EtherPress';
import web3 from 'Embark/web3';
```
> Both EmbarkJS and web3 are normally imported whenever there's a need to interact with web3 technologies. You'll see these imports present in both `App.js` and `Post.js`
2. Update the `handleClick` event, that is triggered when you press the 'Publish' button. This will save the post on IPFS, and invoke our contract. We need to obtain a gas estimate and add our post using the contract's `create` function.
Save the content of the post in IPFS
```
const ipfsHash = await EmbarkJS.Storage.saveText(JSON.stringify(textToSave));
```
Then, estimate the gas needed to execute the `create` function
```
const {create} = DReddit.methods;
const toSend = await create(web3.utils.toHex(ipfsHash));
const estimatedGas = await toSend.estimateGas();
```
Finally, generate the transaction
```
const receipt = await toSend.send({from: web3.eth.defaultAccount,
gas: estimatedGas + 1000});
```
> Notice that we added 1000 wei to the estimated gas. There are situations where the estimated gas is incorrect depending on the state of the contractor if you're deleting items from an array. Adding a extra wei value helps avoid running into an Out of Gas exception.
###### `App.js`
1. Edit `componentDidMount()`. Execute `this._loadPosts()` as soon as EmbarkJS initializes.
```
EmbarkJS.onReady(() => {
this._loadPosts();
});
```
> `onReady()` is used if you want to execute any action that uses EmbarkJS or web3 as soon as the page loads.
2. Edit `_loadPosts`. Extract the functions `posts` and `numPosts` from the contract in order to read the posts content.
```
const {posts, numPosts} = DReddit.methods;
```
Using `numPosts` obtain the number of posts our contract has.
```
const total = await numPosts().call();
```
And then, inside the loop, invoke `posts` to obtain each post individually
```
const post = posts(i).call();
```
> Notice we aren't using `await` here. It's not a good practice to `await` inside a loop. It's better to load all the promises inside an array, and then, call `Promise.all()` on this array.
###### `Post.js`
```
1. Edit `_loadAttributes`. Obtain the post from IPFS
```
const ipfsText = await EmbarkJS.Storage.get(ipfsHash);
```
Convert the text retrieved from IPFS into a JSON object an populate `title` and `content`
```
const jsonContent = JSON.parse(ipfsText);
const title = jsonContent.title;
const content = jsonContent.content;
```
Determine if the user can vote on this post
```
const canVote = await DReddit.methods.canVote(this.props.id).call();
```
2. Edit `_vote`. Estimate the cost of invoking the `vote` function of our DReddit contract
```
const {vote} = DReddit.methods;
const toSend = vote(this.props.id, choice);
const estimatedGas = await toSend.estimateGas();
```
Send the transaction
```
const receipt = await toSend.send({gas: estimatedGas + 1000});
```
#### Check the dApp
Open a browser, navigate to http://localhost:8000 and admire your creation! and also try to fix any bug we may have introduced.

View File

@ -0,0 +1,43 @@
## Testnet Deployment
Now that we finished building our dApp, we need to deploy it. We'll perform this activity deploying our contracts to the ropsten testnet, and our dApp resources to IPFS using Embark's upload feature.
### Configuring our deployment ethereum account
Let's edit`./config/contracts.js` to add a testnet section that will contain our contract deployment settings. Embark supports different types of configurations for [deployment](https://embark.status.im/docs/contracts.html#Deployer-Account) of smart contracts. For this particular tutorial, we will use a private key that contains Ropsten ETH:
```
"testnet": {
dappConnection: ["$WEB3"],
accounts: [{
privateKey: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" // <-- Use your PK
}]
}
```
> Notice that on dappConnection we only specified $WEB3. This is because we will use a web3 provider that is automatically injected by Status / Metamask / Mist. However, this might change in the future with [EIP-1102](https://eips.ethereum.org/EIPS/eip-1102)
### Storage
For storage, we will use [Infura](https://infura.io) IPFS Gateway to upload our dApp files. This is configured on `./config/storage.js`. In this config file, we need to add a `testnet` section and configure both the `upload`, and `dappConnection` for handling the file uploads during the dApp runtime.
```
testnet: {
upload: {
host: "ipfs.infura.org",
port: 5001,
protocol: "https"
},
dappConnection: [
{
provider: "ipfs",
protocol: "https",
host: "ipfs.infura.io",
port: 5001,
getUrl: "https://ipfs.infura.io/ipfs/"
}
]
}
```
### 3... 2... 1... Deploy!
Once you've done all the configuration, we'll proceed to execute `embark blockchain testnet` in a terminal session and `embark upload testnet` in other. Embark will proceed to compile our contracts, deploy them and upload the files to our dApp
> Notice we are running `embark blockchain testnet` in a separate session in order to be able to see the sync state of our geth light node. The first time this is executed, geth will download a number of headers of the mined blocks, so the initial deployment of our contracts might take longer than usual. After our node synchronizes, our deployments will be faster.
Once the upload process concludes, execute `ipfs daemon` to share the dApp files with the IPFS network, and you would be able to access your dApp in the browser using the URL shown by Embark's upload process. You can access now your dApp via Status, or a Browser using Metamask.

36
package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "dreddit",
"version": "0.0.1",
"description": "",
"scripts": {
"test": "embark test"
},
"author": "",
"license": "MIT",
"homepage": "",
"devDependencies": {
"babel-eslint": "^8.2.6",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-stage-2": "^6.24.1",
"eslint": "^4.19.1",
"eslint-config-standard": "^11.0.0",
"eslint-plugin-import": "^2.13.0",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-promise": "^3.8.0",
"eslint-plugin-standard": "^3.1.0"
},
"dependencies": {
"@material-ui/core": "^1.4.3",
"@material-ui/icons": "^2.0.1",
"@material-ui/lab": "^1.0.0-alpha.9",
"dateformat": "^3.0.3",
"eslint-plugin-react": "^7.10.0",
"lodash": "^4.17.10",
"markdown": "^0.5.0",
"material-ui-search-bar": "^1.0.0-beta.9",
"react": "^16.4.2",
"react-blockies": "^1.3.0",
"react-bootstrap": "^0.32.1",
"react-dom": "^16.4.2"
}
}

65
test/contract_spec.js Normal file
View File

@ -0,0 +1,65 @@
// our contract object to test
const DReddit = require('Embark/contracts/DReddit');
// contract methods we'll be testing
const {numPosts, create, vote, posts, canVote, getVote} = DReddit.methods;
// variables that will be updated in the tests
let accounts;
let postId;
// set up our config test parameters
config({
contracts: {
DReddit: {
// would pass constructor args here if needed
}
}
}, (err, theAccounts) => {
// this is the list of accounts our node / wallet controls.
accounts = theAccounts;
});
// other test parameters
const ipfsHash = 'Qmc5gCcjYypU7y28oCALwfSvxCBskLuPKWpK4qpterKC7z';
// Embark exposes a global contract method as an alias
// for Mocha.describe
contract("DReddit contract", function () {
this.timeout(0);
it("should be able to create a post and receive it via contract event", async function () {
// TODO:
});
it("post should have correct data", async function (){
// TODO:
});
it("one post should be registered", async function () {
const n = await numPosts().call();
assert.equal(n, 1);
});
it("should not be able to vote in an unexisting post", async function () {
const userCanVote = await canVote(123).call();
assert.equal(userCanVote, false);
});
it("should be able to vote in a post if account hasn't voted before", async function () {
const userCanVote = await canVote(postId).call();
assert.equal(userCanVote, true);
});
it("should be able to vote in a post", async function () {
const receipt = await vote(postId, 1).send();
const Vote = receipt.events.Vote;
assert.equal(Vote.returnValues.voter, accounts[0]);
});
it("should't be able to vote twice", async function () {
// TODO:
});
});