initial commit
This commit is contained in:
commit
d25e5ba718
|
@ -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
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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: {
|
||||||
|
//}
|
||||||
|
};
|
|
@ -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
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
//}
|
||||||
|
};
|
|
@ -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: {
|
||||||
|
//}
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
dev_password
|
|
@ -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: {
|
||||||
|
//}
|
||||||
|
};
|
|
@ -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
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
dev_password
|
|
@ -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: {
|
||||||
|
//}
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
test_password
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
enabled: false
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
|
@ -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": ""
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 162 KiB |
|
@ -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>
|
|
@ -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"
|
||||||
|
}
|
|
@ -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;
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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)
|
|
@ -0,0 +1,9 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #e8e8e8;
|
||||||
|
padding-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 9.6 KiB |
|
@ -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'));
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
// This optional code is used to register a service worker.
|
||||||
|
// register() is not called by default.
|
||||||
|
|
||||||
|
// This lets the app load faster on subsequent visits in production, and gives
|
||||||
|
// it offline capabilities. However, it also means that developers (and users)
|
||||||
|
// will only see deployed updates on subsequent visits to a page, after all the
|
||||||
|
// existing tabs open on the page have been closed, since previously cached
|
||||||
|
// resources are updated in the background.
|
||||||
|
|
||||||
|
// To learn more about the benefits of this model and instructions on how to
|
||||||
|
// opt-in, read https://bit.ly/CRA-PWA
|
||||||
|
|
||||||
|
const isLocalhost = Boolean(
|
||||||
|
window.location.hostname === 'localhost' ||
|
||||||
|
// [::1] is the IPv6 localhost address.
|
||||||
|
window.location.hostname === '[::1]' ||
|
||||||
|
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||||
|
window.location.hostname.match(
|
||||||
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export function register(config) {
|
||||||
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
|
// The URL constructor is available in all browsers that support SW.
|
||||||
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||||
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||||
|
// from what our page is served on. This might happen if a CDN is used to
|
||||||
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||||
|
|
||||||
|
if (isLocalhost) {
|
||||||
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||||
|
checkValidServiceWorker(swUrl, config);
|
||||||
|
|
||||||
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
|
// service worker/PWA documentation.
|
||||||
|
navigator.serviceWorker.ready.then(() => {
|
||||||
|
console.log(
|
||||||
|
'This web app is being served cache-first by a service ' +
|
||||||
|
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Is not localhost. Just register service worker
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerValidSW(swUrl, config) {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register(swUrl)
|
||||||
|
.then(registration => {
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === 'installed') {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
// At this point, the updated precached content has been fetched,
|
||||||
|
// but the previous service worker will still serve the older
|
||||||
|
// content until all client tabs are closed.
|
||||||
|
console.log(
|
||||||
|
'New content is available and will be used when all ' +
|
||||||
|
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onUpdate) {
|
||||||
|
config.onUpdate(registration);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// At this point, everything has been precached.
|
||||||
|
// It's the perfect time to display a
|
||||||
|
// "Content is cached for offline use." message.
|
||||||
|
console.log('Content is cached for offline use.');
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onSuccess) {
|
||||||
|
config.onSuccess(registration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error during service worker registration:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValidServiceWorker(swUrl, config) {
|
||||||
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
|
fetch(swUrl)
|
||||||
|
.then(response => {
|
||||||
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (
|
||||||
|
response.status === 404 ||
|
||||||
|
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||||
|
) {
|
||||||
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.unregister().then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Service worker found. Proceed as normal.
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log(
|
||||||
|
'No internet connection found. App is running in offline mode.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregister() {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.unregister();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue