initial commit

This commit is contained in:
emizzle 2019-05-22 13:58:42 +10:00
commit d25e5ba718
No known key found for this signature in database
GPG Key ID: 1FD4BAB3C37EE9BA
31 changed files with 20946 additions and 0 deletions

29
.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.embark
embarkArtifacts
/dist
chains.json
config/livenet/password
config/production/password

107
contracts/DReddit.sol Normal file
View File

@ -0,0 +1,107 @@
pragma solidity ^0.5.0;
// @notice Contract to create posts
contract DReddit {
enum Ballot { NONE, UPVOTE, DOWNVOTE }
struct Post {
uint creationDate;
string description;
address owner;
uint upvotes;
uint downvotes;
mapping(address => Ballot) voters;
}
Post[] public posts;
event NewPost (
uint indexed postId,
address owner,
string 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)
{
return posts.length;
}
// @notice Create Post
// @param _ipfsHash IPFS hash of the content of the post
function create(string memory _ipfsHash) public
{
require(bytes(_ipfsHash).length > 0, "IPFS hash is required");
posts.push(Post({
creationDate: now,
description: _ipfsHash,
owner: msg.sender,
upvotes: 0,
downvotes: 0
}));
emit NewPost(posts.length - 1, msg.sender, _ipfsHash);
}
// @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
{
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.');
Ballot b = Ballot(_vote);
require(b != Ballot.NONE, 'Invalid vote');
if (b == Ballot.UPVOTE) {
p.upvotes++;
} else {
p.downvotes++;
}
p.voters[msg.sender] = b;
emit Vote(_postId, msg.sender, _vote);
}
// @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)
{
if (_postId > posts.length - 1) return false;
Post storage p = posts[_postId];
return (p.voters[msg.sender] == Ballot.NONE);
}
// @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)
{
Post storage p = posts[_postId];
return uint8(p.voters[msg.sender]);
}
}

23
embark.json Normal file
View File

@ -0,0 +1,23 @@
{
"options": {
"solc": {
"optimize": true,
"optimize-runs": 200
}
},
"app": {},
"contracts": [
"contracts/**"
],
"buildDir": "dist/",
"config": "embarkConfig/",
"versions": {
"web3": "1.0.0-beta",
"solc": "0.5.0",
"ipfs-api": "17.2.4"
},
"plugins": {
"embarkjs-connector-web3": {}
},
"generationDir": "src/embarkArtifacts"
}

146
embarkConfig/blockchain.js Normal file
View File

@ -0,0 +1,146 @@
module.exports = {
// applies to all environments
default: {
enabled: true,
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
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)
// Accounts to use as node accounts
// The order here corresponds to the order of `web3.eth.getAccounts`, so the first one is the `defaultAccount`
/*,accounts: [
{
nodeAccounts: true, // Accounts use for the node
numAddresses: "1", // Number of addresses/accounts (defaults to 1)
password: "config/development/devpassword" // Password file for the accounts
},
// Below are additional accounts that will count as `nodeAccounts` in the `deployment` section of your contract config
// Those will not be unlocked in the node itself
{
privateKey: "your_private_key"
},
{
privateKeyFile: "path/to/file", // Either a keystore or a list of keys, separated by , or ;
password: "passwordForTheKeystore" // Needed to decrypt the keystore file
},
{
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
}
]*/
},
// default environment, merges with the settings in default
// assumed to be the intended environment by `embark run` and `embark blockchain`
development: {
ethereumClientName: "geth", // Can be geth or parity (default:geth)
//ethereumClientBin: "geth", // path to the client binary. Useful if it is not in the global PATH
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
datadir: ".embark/development/datadir", // Data directory for the databases and keystore (Geth 1.8.15 and Parity 2.0.4 can use the same base folder, till now they does not conflict with each other)
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)
proxy: true, // Proxy is used to present meaningful information about transactions
targetGasLimit: 8000000, // Target gas limit sets the artificial target gas floor for the blocks to mine
simulatorBlocktime: 0 // Specify blockTime in seconds for automatic mining. Default is 0 and no auto-mining.
},
// merges with the settings in default
// used with "embark run privatenet" and/or "embark blockchain privatenet"
privatenet: {
networkType: "custom",
networkId: 1337,
isDev: false,
datadir: ".embark/privatenet/datadir",
// -- mineWhenNeeded --
// This options is only valid when isDev is false.
// Enabling this option uses our custom script to mine only when needed.
// Embark creates a development account for you (using `geth account new`) and funds the account. This account can be used for
// development (and even imported in to MetaMask). To enable correct usage, a password for this account must be specified
// in the `account > password` setting below.
// NOTE: once `mineWhenNeeded` is enabled, you must run an `embark reset` on your dApp before running
// `embark blockchain` or `embark run` for the first time.
mineWhenNeeded: true,
// -- genesisBlock --
// This option is only valid when mineWhenNeeded is true (which is only valid if isDev is false).
// When enabled, geth uses POW to mine transactions as it would normally, instead of using POA as it does in --dev mode.
// On the first `embark blockchain or embark run` after this option is enabled, geth will create a new chain with a
// genesis block, which can be configured using the `genesisBlock` configuration option below.
genesisBlock: "config/privatenet/genesis.json", // Genesis block to initiate on first creation of a development node
nodiscover: true,
maxpeers: 0,
proxy: true,
accounts: [
{
nodeAccounts: true,
password: "config/privatenet/password" // Password to unlock the account
}
],
targetGasLimit: 8000000,
simulatorBlocktime: 0
},
privateparitynet: {
ethereumClientName: "parity",
networkType: "custom",
networkId: 1337,
isDev: false,
genesisBlock: "config/privatenet/genesis-parity.json", // Genesis block to initiate on first creation of a development node
datadir: ".embark/privatenet/datadir",
mineWhenNeeded: false,
nodiscover: true,
maxpeers: 0,
proxy: true,
accounts: [
{
nodeAccounts: true,
password: "config/privatenet/password"
}
],
targetGasLimit: 8000000,
simulatorBlocktime: 0
},
// merges with the settings in default
// used with "embark run testnet" and/or "embark blockchain testnet"
testnet: {
networkType: "testnet",
syncMode: "light",
accounts: [
{
nodeAccounts: true,
password: "config/testnet/password"
}
]
},
// merges with the settings in default
// used with "embark run livenet" and/or "embark blockchain livenet"
livenet: {
networkType: "livenet",
syncMode: "light",
rpcCorsDomain: "http://localhost:8000",
wsOrigins: "http://localhost:8000",
accounts: [
{
nodeAccounts: true,
password: "config/livenet/password"
}
]
}
// you can name an environment with specific settings and then specify with
// "embark run custom_name" or "embark blockchain custom_name"
//custom_name: {
//}
};

View File

@ -0,0 +1,46 @@
module.exports = {
// default applies to all environments
default: {
enabled: true,
provider: "whisper", // Communication provider. Currently, Embark only supports whisper
available_providers: ["whisper"], // Array of available providers
},
// default environment, merges with the settings in default
// assumed to be the intended environment by `embark run`
development: {
connection: {
host: "localhost", // Host of the blockchain node
port: 8546, // Port of the blockchain node
type: "ws" // Type of connection (ws or rpc)
}
},
// merges with the settings in default
// used with "embark run privatenet"
privatenet: {
},
// merges with the settings in default
// used with "embark run testnet"
testnet: {
},
// merges with the settings in default
// used with "embark run livenet"
livenet: {
},
// you can name an environment with specific settings and then specify with
// "embark run custom_name"
//custom_name: {
//}
// Use this section when you need a specific symmetric or private keys in whisper
/*
,keys: {
symmetricKey: "your_symmetric_key",// Symmetric key for message decryption
privateKey: "your_private_key" // Private Key to be used as a signing key and for message decryption
}
*/
//}
};

91
embarkConfig/contracts.js Normal file
View File

@ -0,0 +1,91 @@
module.exports = {
// default applies to all environments
default: {
// Blockchain node to deploy the contracts
deployment: {
host: "localhost", // Host of the blockchain node
port: 8546, // Port of the blockchain node
type: "ws" // Type of connection (ws or rpc),
// Accounts to use instead of the default account to populate your wallet
// The order here corresponds to the order of `web3.eth.getAccounts`, so the first one is the `defaultAccount`
/*,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", // Either a keystore or a list of keys, separated by , or ;
password: "passwordForTheKeystore" // Needed to decrypt the keystore file
},
{
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
},
{
"nodeAccounts": true // Uses the Ethereum node's accounts
}
]*/
},
// 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"
],
// Automatically call `ethereum.enable` if true.
// If false, the following code must run before sending any transaction: `await EmbarkJS.enableEthereum();`
// Default value is true.
// dappAutoEnable: true,
gas: "auto",
// Strategy for the deployment of the contracts:
// - implicit will try to deploy all the contracts located inside the contracts directory
// or the directory configured for the location of the contracts. This is default one
// when not specified
// - explicit will only attempt to deploy the contracts that are explicity specified inside the
// contracts section.
//strategy: 'implicit',
contracts: {
// example:
//SimpleStorage: {
// args: [ 100 ]
//}
}
},
// default environment, merges with the settings in default
// assumed to be the intended environment by `embark run`
development: {
dappConnection: [
"ws://localhost:8546",
"http://localhost:8545",
"$WEB3" // uses pre existing web3 object if available (e.g in Mist)
]
},
// merges with the settings in default
// used with "embark run privatenet"
privatenet: {
},
// merges with the settings in default
// used with "embark run testnet"
testnet: {
},
// merges with the settings in default
// used with "embark run livenet"
livenet: {
},
// you can name an environment with specific settings and then specify with
// "embark run custom_name" or "embark blockchain custom_name"
//custom_name: {
//}
};

View File

@ -0,0 +1 @@
dev_password

View File

@ -0,0 +1,39 @@
module.exports = {
// default applies to all environments
default: {
enabled: true,
available_providers: ["ens"],
provider: "ens"
},
// default environment, merges with the settings in default
// assumed to be the intended environment by `embark run`
development: {
register: {
rootDomain: "embark.eth",
subdomains: {
'status': '0x1a2f3b98e434c02363f3dac3174af93c1d690914'
}
}
},
// merges with the settings in default
// used with "embark run privatenet"
privatenet: {
},
// merges with the settings in default
// used with "embark run testnet"
testnet: {
},
// merges with the settings in default
// used with "embark run livenet"
livenet: {
},
// you can name an environment with specific settings and then specify with
// "embark run custom_name" or "embark blockchain custom_name"
//custom_name: {
//}
};

24
embarkConfig/pipeline.js Normal file
View File

@ -0,0 +1,24 @@
// Embark has support for Flow enabled by default in its built-in webpack
// config: type annotations will automatically be stripped out of DApp sources
// without any additional configuration. Note that type checking is not
// performed during builds.
// To enable Flow type checking refer to the preconfigured template:
// https://github.com/embark-framework/embark-flow-template
// A new DApp can be created from that template with:
// embark new --template flow
module.exports = {
typescript: false,
enabled:false
// Setting `typescript: true` in this config will disable Flow support in
// Embark's default webpack config and enable TypeScript support: .ts and
// .tsx sources will automatically be transpiled into JavaScript without any
// additional configuration. Note that type checking is not performed during
// builds.
// To enable TypeScript type checking refer to the preconfigured template:
// https://github.com/embark-framework/embark-typescript-template
// A new DApp can be created from that template with:
// embark new --template typescript
};

File diff suppressed because one or more lines are too long

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

59
embarkConfig/storage.js Normal file
View File

@ -0,0 +1,59 @@
module.exports = {
// default applies to all environments
default: {
enabled: true,
ipfs_bin: "ipfs",
available_providers: ["ipfs"],
upload: {
provider: "ipfs",
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)*/
},
// default environment, merges with the settings in default
// assumed to be the intended environment by `embark run`
development: {
enabled: true,
upload: {
provider: "ipfs",
host: "localhost",
port: 5001,
getUrl: "http://localhost:8080/ipfs/"
}
},
// merges with the settings in default
// used with "embark run privatenet"
privatenet: {
},
// merges with the settings in default
// used with "embark run testnet"
testnet: {
},
// merges with the settings in default
// used with "embark run livenet"
livenet: {
},
// you can name an environment with specific settings and then specify with
// "embark run custom_name"
//custom_name: {
//}
};

View File

@ -0,0 +1 @@
test_password

View File

@ -0,0 +1,3 @@
module.exports = {
enabled: false
};

19142
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "dreddit-unicef-surge-2019",
"version": "0.0.1",
"private": true,
"devDependencies": {
"@babel/core": "7.1.2",
"@babel/plugin-proposal-class-properties": "7.1.0",
"@babel/plugin-proposal-decorators": "7.1.2",
"@babel/plugin-proposal-export-namespace-from": "7.0.0",
"@babel/plugin-proposal-function-sent": "7.1.0",
"@babel/plugin-proposal-json-strings": "7.0.0",
"@babel/plugin-proposal-numeric-separator": "7.0.0",
"@babel/plugin-proposal-object-rest-spread": "7.0.0",
"@babel/plugin-proposal-throw-expressions": "7.0.0",
"@babel/plugin-syntax-dynamic-import": "7.0.0",
"@babel/plugin-syntax-import-meta": "7.0.0"
},
"dependencies": {
"@babel/runtime-corejs2": "^7.3.1",
"@material-ui/core": "^3.9.3",
"@material-ui/icons": "^3.0.2",
"@material-ui/lab": "^3.0.0-alpha.30",
"dateformat": "^3.0.3",
"embarkjs-connector-web3": "^4.1.0-beta.1",
"embarkjs-ipfs": "/Users/emizzle/Code/__Github/embk-fw/embark/packages/embarkjs-ipfs",
"embarkjs-whisper": "/Users/emizzle/Code/__Github/embk-fw/embark/packages/embarkjs-whisper",
"lodash": "^4.17.11",
"markdown": "^0.5.0",
"react": "^16.8.6",
"react-blockies": "^1.4.1",
"react-dom": "^16.8.6",
"react-scripts": "2.1.8"
},
"scripts": {
"start": "PORT=3001 react-scripts start",
"build": "react-scripts build",
"test": "embark test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"description": ""
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

43
public/index.html Normal file
View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<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 installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
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="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<title>DReddit with Embark 4</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>

15
public/manifest.json Normal file
View File

@ -0,0 +1,15 @@
{
"short_name": "DReddit",
"name": "DReddit with Embark 4",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

109
src/components/App.js Normal file
View File

@ -0,0 +1,109 @@
import React, {Component} from 'react';
import Create from './Create';
import Header from './Header';
import Post from './Post';
import _ from 'lodash';
import {MuiThemeProvider, createMuiTheme} from '@material-ui/core/styles';
import EmbarkJS from '../embarkArtifacts/embarkjs';
import DReddit from '../embarkArtifacts/contracts/DReddit';
import '../css/app.css';
const theme = createMuiTheme({
palette: {
primary: {
dark: '#398689',
light: '#78BE97',
main: '#0B476D'
// contrastText: will be calculated to contrast with palette.primary.main
},
secondary: {
dark: '#E75E3B',
// light: will be calculated to contrast with palette.secondary.main
main: '#FAB266'
},
// error: will use the default color
},
typography: {
useNextVariants: true,
suppressDeprecationWarnings: true
}
});
class App extends Component {
constructor(props) {
super(props);
this.state = {
'displayForm': false,
'list': [],
'sortBy': 'age',
'sortOrder': 'desc',
'filterBy': ''
};
}
componentDidMount() {
EmbarkJS.onReady(() => {
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 () => {
const {posts, numPosts} = DReddit.methods;
let list = [];
const total = await numPosts().call();
if(total > 0){
for (let i = 0; i < total; i++) {
const currentPost = posts(i).call();
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 <MuiThemeProvider theme={theme}>
<Header toggleForm={this._toggleForm} sortOrder={this._setSortOrder} search={this._search} onCancelSearch={this._search} />
{ displayForm && <Create afterPublish={this._loadPosts} /> }
{ orderedList.map((record) => <Post key={record.id} {...record} filterBy={filterBy} />) }
</MuiThemeProvider>;
}
}
export default App;

128
src/components/Create.js Normal file
View File

@ -0,0 +1,128 @@
/* global web3 */
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';
import EmbarkJS from '../embarkArtifacts/embarkjs';
import DReddit from '../embarkArtifacts/contracts/DReddit';
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
};
const ipfsHash = await EmbarkJS.Storage.saveText(JSON.stringify(textToSave));
const {create} = DReddit.methods;
const toSend = await create(ipfsHash);
//const estimatedGas = await toSend.estimateGas();
let newState = {
isSubmitting: false
};
try {
await toSend.send({from: web3.eth.defaultAccount, gas: 1000000}); //estimatedGas + 1000});
newState.content = '';
newState.title = '';
this.setState(newState);
this.props.afterPublish();
}
catch (error) {
newState.error = error.message;
this.setState(newState);
}
}
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}
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);

120
src/components/Header.js Normal file
View File

@ -0,0 +1,120 @@
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 Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import {withStyles} from '@material-ui/core/styles';
import SearchBar from './SearchBar';
import logo from '../images/embark-logo.png';
const styles = {
root: {
flexGrow: 1
},
flex: {
flexGrow: 1
},
logo: {
display: "inline-block",
verticalAlign: "middle",
width: "40px",
marginRight: "10px"
}
};
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, onCancelSearch} = this.props;
const {anchorEl, sortIndex} = this.state;
const open = Boolean(anchorEl);
return (
<div className={classes.root} >
<AppBar position="fixed">
<Toolbar className={classes.toolBar}>
<Hidden xsDown>
<img src={logo} alt="Logo" className={classes.logo} />
<Typography variant="h4" color="inherit" className={classes.flex}>
DReddit
</Typography>
</Hidden>
<SearchBar
placeholder="Search..."
style={{
margin: '10px 10px',
maxWidth: 280
}}
onChange={(searchValue) => search(searchValue)}
onCancelSearch={onCancelSearch}
/>
<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);

168
src/components/Post.js Normal file
View File

@ -0,0 +1,168 @@
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 '../embarkArtifacts/embarkjs';
import DReddit from '../embarkArtifacts/contracts/DReddit';
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) => {
if(!filterBy) return true;
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);
const ipfsText = await EmbarkJS.Storage.get(this.props.description);
const jsonContent = JSON.parse(ipfsText);
const title = jsonContent.title;
const content = jsonContent.content;
const canVote = await DReddit.methods.canVote(this.props.id).call();
this.setState({
title,
content,
canVote
});
}
_vote = choice => async event => {
event.preventDefault();
this.setState({isSubmitting: true});
const {vote} = DReddit.methods;
const toSend = vote(this.props.id, choice);
const estimatedGas = await toSend.estimateGas();
await toSend.send({gas: estimatedGas + 1000});
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="h6" 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);

220
src/components/SearchBar.js Normal file
View File

@ -0,0 +1,220 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import IconButton from '@material-ui/core/IconButton'
import Input from '@material-ui/core/Input'
import Paper from '@material-ui/core/Paper'
import ClearIcon from '@material-ui/icons/Clear'
import SearchIcon from '@material-ui/icons/Search'
import { grey } from '@material-ui/core/colors'
import withStyles from '@material-ui/core/styles/withStyles'
import classNames from 'classnames'
const styles = {
root: {
height: 48,
display: 'flex',
justifyContent: 'space-between'
},
iconButton: {
opacity: 0.54,
transform: 'scale(1, 1)',
transition: 'transform 200ms cubic-bezier(0.4, 0.0, 0.2, 1)'
},
iconButtonHidden: {
transform: 'scale(0, 0)',
'& > $icon': {
opacity: 0
}
},
iconButtonDisabled: {
opacity: 0.38
},
searchIconButton: {
marginRight: -48
},
icon: {
opacity: 0.54,
transition: 'opacity 200ms cubic-bezier(0.4, 0.0, 0.2, 1)'
},
input: {
width: '100%'
},
searchContainer: {
margin: 'auto 16px',
width: 'calc(100% - 48px - 32px)' // 48px button + 32px margin
}
}
/**
* Material design search bar
* @see [Search patterns](https://material.io/guidelines/patterns/search.html)
*/
class SearchBar extends Component {
constructor (props) {
super(props)
this.state = {
focus: false,
value: this.props.value,
active: false
}
}
componentWillReceiveProps (nextProps) {
if (this.props.value !== nextProps.value) {
this.setState({...this.state, value: nextProps.value})
}
}
handleFocus = (e) => {
this.setState({focus: true})
if (this.props.onFocus) {
this.props.onFocus(e)
}
}
handleBlur = (e) => {
this.setState({focus: false})
if (this.state.value.trim().length === 0) {
this.setState({value: ''})
}
if (this.props.onBlur) {
this.props.onBlur(e)
}
}
handleInput = (e) => {
this.setState({value: e.target.value})
if (this.props.onChange) {
this.props.onChange(e.target.value)
}
}
handleCancel = () => {
this.setState({active: false, value: ''})
if (this.props.onCancelSearch) {
this.props.onCancelSearch()
}
}
handleKeyUp = (e) => {
if (e.charCode === 13 || e.key === 'Enter') {
this.handleRequestSearch()
} else if (this.props.cancelOnEscape && (e.charCode === 27 || e.key === 'Escape')) {
this.handleCancel()
}
if (this.props.onKeyUp) {
this.props.onKeyUp(e)
}
}
handleRequestSearch = () => {
if (this.props.onRequestSearch) {
this.props.onRequestSearch(this.state.value)
}
}
render () {
const { value } = this.state
const {
cancelOnEscape,
className,
classes,
closeIcon,
disabled,
onCancelSearch,
onRequestSearch,
searchIcon,
style,
...inputProps
} = this.props
return (
<Paper
className={classNames(classes.root, className)}
style={style}
>
<div className={classes.searchContainer}>
<Input
{...inputProps}
onBlur={this.handleBlur}
value={value}
onChange={this.handleInput}
onKeyUp={this.handleKeyUp}
onFocus={this.handleFocus}
fullWidth
className={classes.input}
disableUnderline
disabled={disabled}
/>
</div>
<IconButton
onClick={this.handleRequestSearch}
classes={{
root: classNames(classes.iconButton, classes.searchIconButton, {
[classes.iconButtonHidden]: value !== ''
}),
disabled: classes.iconButtonDisabled
}}
disabled={disabled}
>
{React.cloneElement(searchIcon, {
classes: { root: classes.icon }
})}
</IconButton>
<IconButton
onClick={this.handleCancel}
classes={{
root: classNames(classes.iconButton, {
[classes.iconButtonHidden]: value === ''
}),
disabled: classes.iconButtonDisabled
}}
disabled={disabled}
>
{React.cloneElement(closeIcon, {
classes: { root: classes.icon }
})}
</IconButton>
</Paper>
)
}
}
SearchBar.defaultProps = {
className: '',
closeIcon: <ClearIcon style={{ color: grey[500] }} />,
disabled: false,
placeholder: 'Search',
searchIcon: <SearchIcon style={{ color: grey[500] }} />,
style: null,
value: ''
}
SearchBar.propTypes = {
/** Whether to clear search on escape */
cancelOnEscape: PropTypes.bool,
/** Override or extend the styles applied to the component. */
classes: PropTypes.object.isRequired,
/** Custom top-level class */
className: PropTypes.string,
/** Override the close icon. */
closeIcon: PropTypes.node,
/** Disables text field. */
disabled: PropTypes.bool,
/** Fired when the search is cancelled. */
onCancelSearch: PropTypes.func,
/** Fired when the text value changes. */
onChange: PropTypes.func,
/** Fired when the search icon is clicked. */
onRequestSearch: PropTypes.func,
/** Sets placeholder text for the embedded text field. */
placeholder: PropTypes.string,
/** Override the search icon. */
searchIcon: PropTypes.node,
/** Override the inline-styles of the root element. */
style: PropTypes.object,
/** The value of the text field. */
value: PropTypes.string
}
export default withStyles(styles)(SearchBar)

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

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

0
src/images/.gitkeep Normal file
View File

BIN
src/images/embark-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

13
src/index.js Normal file
View File

@ -0,0 +1,13 @@
import EmbarkJS from './embarkArtifacts/embarkjs';
import React from 'react';
import { render } from 'react-dom';
import App from './components/App';
// import your contracts
// e.g if you have a contract named SimpleStorage:
//import SimpleStorage from 'Embark/contracts/SimpleStorage';
EmbarkJS.onReady(() => {
render(<App />, document.getElementById('root'));
});

135
src/serviceWorker.js Normal file
View File

@ -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();
});
}
}

59
test/DReddit_spec.js Normal file
View File

@ -0,0 +1,59 @@
/* global web3, config, contract, assert */
const DReddit = require('Embark/contracts/DReddit');
const ipfsHash = 'Qmc5gCcjYypU7y28oCALwfSvxCBskLuPKWpK4qpterKC7z';
let accounts = [];
let postId = -1;
config({
contracts: {
DReddit: {}
}
}, (err, _accounts) => {
accounts = _accounts;
});
contract('DReddit', () => {
it('should work', () => {
assert.ok(true);
});
it('should be able to create a post and receive it via contract event', async () => {
let receipt = await DReddit.methods.create(web3.utils.fromAscii(ipfsHash)).send();
const event = receipt.events.NewPost;
postId = event.returnValues.postId;
assert.equal(web3.utils.toAscii(event.returnValues.description), ipfsHash);
});
it ('post should have correct data', async () => {
const post = await DReddit.methods.posts(postId).call();
assert.equal(web3.utils.toAscii(post.description), ipfsHash);
assert.equal(post.owner, accounts[0]);
});
it("should not be able to vote in an unexisting post", async () => {
const userCanVote = await DReddit.methods.canVote("123").call();
assert.equal(userCanVote, false);
});
it("should be able to vote in a post if account hasn't voted before", async () => {
const userCanVote = await DReddit.methods.canVote(postId).call();
assert.equal(userCanVote, true);
});
it('should be able to vote in a post', async () => {
const receipt = await DReddit.methods.vote(postId, 1).send();
const Vote = receipt.events.Vote;
assert.equal(Vote.returnValues.voter, accounts[0]);
});
it('shouldn\'t be able to vote twice', async () => {
try {
await DReddit.methods.vote(postId, 1).send();
assert.fail('should have reverted before');
} catch (error){
assert(error.message.search('revert') > -1, 'Revert should happen');
}
});
});