├── .gitignore ├── Ethereum ├── Contracts │ └── Campaign.sol ├── campaign.js ├── compile.js ├── deploy.js ├── factory.js └── web3.js ├── README.md ├── components ├── ContributeForm.js ├── Header.js ├── Layout.js └── RequestRow.js ├── package.json ├── pages ├── campaigns │ ├── new.js │ ├── requests │ │ ├── index.js │ │ └── new.js │ └── show.js └── index.js ├── routes.js ├── server.js └── test └── Campaign.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /package-lock.json 3 | /Ethereum/build 4 | /Ethereum/.env 5 | /.next 6 | -------------------------------------------------------------------------------- /Ethereum/Contracts/Campaign.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.17; 2 | 3 | contract CampaignFactory{ 4 | // ==== Fields ==== 5 | address[] public deployedCampaigns; 6 | 7 | // ==== Modifier ==== 8 | // ==== create a new contract ==== 9 | function createCampaign(uint minimum) public { 10 | address newCampaign = new Campaign(minimum,msg.sender); 11 | deployedCampaigns.push(newCampaign); 12 | } 13 | 14 | // ==== returning all the address of the deployed contract 15 | function getDeployedCampaigns() public view returns (address[]){ 16 | return deployedCampaigns; 17 | } 18 | 19 | } 20 | 21 | contract Campaign{ 22 | // collection of key value pairs 23 | struct Request{ 24 | string description; 25 | uint value; 26 | address recipient; 27 | bool complete; 28 | uint approvalCount; 29 | mapping(address => bool) approvals; 30 | } 31 | 32 | // === Fields === 33 | Request[] public requests; 34 | address public manager; 35 | uint public minimumContribution; 36 | mapping(address => bool) public approvers; 37 | uint public approversCount; 38 | 39 | 40 | // === Methods === 41 | 42 | // == Modifier == 43 | modifier authorization(){ 44 | require(msg.sender == manager); 45 | _; 46 | } 47 | 48 | // == constructor == 49 | //Setting the manager and minimum amount to contribute 50 | constructor(uint minimum, address creator) public { 51 | manager = creator; 52 | minimumContribution = minimum; 53 | } 54 | 55 | //donate money to compaign and became an approver 56 | function contribute() public payable{ 57 | require(msg.value > minimumContribution); 58 | 59 | if(approvers[msg.sender]!= true){ 60 | approvers[msg.sender] = true; 61 | approversCount++; 62 | } 63 | } 64 | 65 | //creating a new request by the manager 66 | function createRequest(string description, uint value, address recipient) 67 | public authorization{ 68 | Request memory newReq = Request({ 69 | description : description, 70 | value : value, 71 | recipient : recipient, 72 | complete : false, 73 | approvalCount : 0 74 | }); 75 | 76 | requests.push(newReq); 77 | } 78 | 79 | //approving a particular request by the user 80 | function approveRequest(uint index) public { 81 | Request storage request = requests[index]; 82 | 83 | require(approvers[msg.sender]); 84 | require(!request.approvals[msg.sender]); 85 | require(!request.complete); 86 | 87 | request.approvals[msg.sender] = true; 88 | request.approvalCount++; 89 | } 90 | 91 | //final approval of request by the manager and sending the amount 92 | function finalizeRequest(uint index) public authorization{ 93 | Request storage request = requests[index]; 94 | 95 | require(request.approvalCount > (approversCount/2)); 96 | require(!request.complete); 97 | 98 | request.recipient.transfer(request.value); 99 | request.complete = true; 100 | 101 | } 102 | 103 | // function to retrieve Campaign balance, minimumContribution , no of requests , no of Contributors and manager address 104 | function getSummary() public view returns ( 105 | uint, uint, uint, uint, address 106 | ) { 107 | return ( 108 | minimumContribution, 109 | address(this).balance, 110 | requests.length, 111 | approversCount, 112 | manager 113 | ); 114 | } 115 | 116 | // returing no of requests 117 | function getRequestsCount() public view returns (uint) { 118 | return requests.length; 119 | } 120 | 121 | } -------------------------------------------------------------------------------- /Ethereum/campaign.js: -------------------------------------------------------------------------------- 1 | import web3 from './web3'; 2 | import Campaign from './build/Campaign.json'; 3 | 4 | export default (address) => { 5 | return new web3.eth.Contract( 6 | JSON.parse(Campaign.interface), 7 | address 8 | ); 9 | 10 | } -------------------------------------------------------------------------------- /Ethereum/compile.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const solc = require("solc"); 3 | const fs = require("fs-extra"); 4 | 5 | const buildPath = path.resolve(__dirname, "build"); 6 | fs.removeSync(buildPath); // deleting the folder and all the content inside it 7 | 8 | const campaignPath = path.resolve(__dirname, "Contracts", "Campaign.sol"); 9 | const source = fs.readFileSync(campaignPath, "utf8"); 10 | const output = solc.compile(source, 1).contracts; 11 | 12 | fs.ensureDirSync(buildPath); // create a build folder if that folder doesn't exists 13 | 14 | for (let contract in output) { 15 | fs.outputJSONSync( 16 | path.resolve(buildPath, contract.replace(":", "").concat(".json")), 17 | output[contract] 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /Ethereum/deploy.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require('truffle-hdwallet-provider'); 2 | 3 | const Web3 = require('web3'); 4 | const compiledFactory = require('./build/CampaignFactory.json'); 5 | 6 | require('dotenv').config(); 7 | 8 | // const OPTIONS = { 9 | // defaultBlock: 'latest', 10 | // transactionConfirmationBlocks: 1, 11 | // transactionBlockTimeout: 8 12 | // } 13 | const provider = new HDWalletProvider( 14 | process.env.mnemonic, 15 | process.env.link 16 | ); 17 | 18 | const web3 = new Web3(provider); 19 | const deploy = async () => { 20 | const accounts = await web3.eth.getAccounts(); 21 | console.log('Attemping to deploy to accounts ', accounts[0]); 22 | 23 | const result = await new web3.eth.Contract(JSON.parse(compiledFactory.interface)) 24 | .deploy({ data: '0x' + compiledFactory.bytecode }) 25 | .send({ from: accounts[0] }); 26 | 27 | console.log('Contract deploy to ', result.options.address); 28 | }; 29 | 30 | deploy(); -------------------------------------------------------------------------------- /Ethereum/factory.js: -------------------------------------------------------------------------------- 1 | import web3 from './web3'; 2 | import CampaignFactory from './build/CampaignFactory.json'; 3 | 4 | const instance = new web3.eth.Contract( 5 | JSON.parse(CampaignFactory.interface), 6 | '0x6eb1BAb08268e4aBf4585875db85Ce224E68da1f' 7 | ); 8 | 9 | export default instance; -------------------------------------------------------------------------------- /Ethereum/web3.js: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3'; 2 | 3 | let web3; 4 | 5 | if (typeof window !== 'undefined' && typeof window.web3 !== 'undefined') { 6 | // we are in the browser and meta mask is installed 7 | web3 = new Web3(window.web3.currentProvider); 8 | } else { 9 | // we are on the server *OR* meta mask is not running 10 | // creating our own provider 11 | const provider = new Web3.providers.HttpProvider( 12 | 'https://rinkeby.infura.io/v3/01183f6b6e8d4aaf8aa97136aded1264' 13 | ); 14 | 15 | web3 = new Web3(provider); 16 | } 17 | 18 | export default web3; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📜 Campaign Contract 2 | A new way of funding a project or venture by raising small amounts of money from a large number of people via ethereum blockchain technology without any middleman to function or to manage a user’s information. 3 | 4 | ## Screen-shot 5 | ### Home Page 6 | ![Home Page](https://drive.google.com/uc?export=view&id=1_uepSKJ43kZ2hiW7OUYZMPn9nX3EbhRj) 7 | 8 | ### Campaigns Dashboard 9 | 10 | ![Campaigns Dashboard](https://drive.google.com/uc?export=view&id=1RbM93-LrsbgpSObkTa5kp09OBDzH1Qxf) 11 | 12 | ### Campaigns Request 13 | ![Campaigns Request](https://drive.google.com/uc?export=view&id=1uU5qLDy6GXbEjedySqx6AJCxw9Ro57MC) 14 | 15 | 16 | ### `CampaignFactory` 17 | 18 | **Variables** 19 | 20 | | variable |data types | desc | 21 | |--|--|--| 22 | | deployedCampaigns| address[] | addresses of all the deployed contract| 23 | 24 |
**Function** 25 | 26 | | name| desc | 27 | |--|--| 28 | | createCampaign| create a new campaign contract | 29 | |getDeployedCampaigns| return addresses of all the deployed contract| 30 | 31 | ### `Campaign` 32 | 33 | **Variables** 34 | 35 | |variable|data types|desc| 36 | |--|--|--| 37 | |manager |address |address of the person who is managing this compaign| 38 | |minimumContribution|unint|Minimum donation required to be considered a contributor or 'approver' | 39 | |approvers|address[]|List of addresses for every person who has donated money| 40 | |requests|Request[]|List of requests that the manager has created.| 41 | 42 |
**Functions** 43 | 44 | |name| desc | 45 | |--|--| 46 | |Campaign | constructor function that sets the minimumContribution and the owner | 47 | |contribute|called when someone wants to donate money to the compaign and become an 'approver'| 48 | |createRequest|called by the manager to create a new 'spending request'| 49 | |approveRequest|called by each contributor to approve a spending request| 50 | |finalizeRequest|After a request has gotten enough approvals, the manager can call this to get money sent to the vendor| 51 | |getSummary() |function to retrieving Campaign balance, minimumContribution , no of requests , no of Contributors and manager address | 52 | |getRequestsCount() | function returning no of requests | 53 | 54 |
**Request Struc** 55 | 56 | |Name |Type |Purpose| 57 | |--|--|--| 58 | | description |string |Describes why the request is being created| 59 | |value|uint|Amount of money that the manager wants to send to the vendor| 60 | |recipient|address|Address that the money will be sent to| 61 | |complete|bool|True if the request has already been processed (money sent)| 62 | 63 | ### `Prerequisite` 64 | 65 | 1. Install **Metamask** as Google Chrome Extension and Create an account. 66 | 2. Request Ether by sharing your ethereum address in social media
(`https://faucet.rinkeby.io/)` 67 | 3. Get 0.01 ether free by giving the ethereum address
`(http://rinkeby-faucet.com/)` 68 | 4. Create an account in [https://infura.io](https://infura.io/) 69 | 5. Create .env file in Ethereum directory and add these line to it. 70 | 71 | 72 | > mnemonic = 'Your mnemonic code'
73 | link = 'Your infura end point link ' 74 | 75 | 6. Deploy Contract by going into Ethereum Directory and run. 76 | > node deploy.js 77 | 78 | Copy the contract deploy address and replace it in factory.js file. 79 | 80 | 7. Replace your "infura end point link" in web3.js file 81 | 82 | ### `Dependencies Used` 83 | 84 | | Name | Version | Description | 85 | |--|--|--| 86 | | solc | 0.4.26 | Programming language to write smart contracts | 87 | | ganache-cli | 6.5.1 | Local Ethereum Test Network | 88 | | mocha | 6.2.0 | JavaScript test framework | 89 | |truffle-hdwallet-provider | 1.0.16 | The **Truffle HDWallet provider** is a convenient and easy to configure network connection to ethereum through infura.io | 90 | | web3 | 1.0.0-beta.55 | Ethereum JavaScript API which connects to the Generic JSON RPC spec. | 91 | | dotenv| 8.0.0 | Loads environment variables from a `.env` file into `process.env`| 92 | | fs-extra| 8.1.0 | file system methods that aren't included in the native fs | 93 | | next | 9.0.3 | JavaScript framework to build server-side rendering and static web application using React | 94 | | react | 16.9.0 | JavaScript library for creating user interfaces | 95 | | react-dom | 16.9.0 | DOM specific methods that can be used on React application | 96 | | semantic-ui-react | 0.87.3 | react component kit provides pre define react component | 97 | | semantic-ui-css | 2.4.1 | react component kit provides theme to react component as CSS | 98 | | next-routes | 1.4.2 | Dynamic Routes for Next.js | 99 | 100 | ### `Steps` 101 | - **To install dependencies** 102 | > npm install 103 | - **To Compile the Contract** 104 | > node compile.js 105 | - **To test the Contract** 106 | > npm run test 107 | - **To deploy the Contract** 108 | > node deploy.js 109 | - **To run the application** 110 | > npm run dev 111 | 112 | ## `UI Routing` 113 | | Path | Description | 114 | |--|--| 115 | | / | List of Campaigns | 116 | | /campaigns/new | Form to make a campaign | 117 | | /campaigns/0x8147 | Campaign details for campaign at address 0x8147 | 118 | | /campaigns/0x8147/requests | Requests for campaign at address 0x8147 | 119 | | /campaigns/0x8147/requests/new | Form to create a request for campaign at address 0x8147 | -------------------------------------------------------------------------------- /components/ContributeForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form, Input, Message, Button } from 'semantic-ui-react'; 3 | import Campaign from '../Ethereum/campaign'; 4 | import web3 from '../Ethereum/web3'; 5 | import { Router } from '../routes'; 6 | 7 | 8 | export default class ContributeForm extends Component { 9 | state = { 10 | value: '', 11 | errorMessage: '', 12 | loading: false 13 | }; 14 | 15 | onSubmit = async (event) => { 16 | event.preventDefault(); 17 | 18 | const campaign = Campaign(this.props.address); 19 | 20 | this.setState({ 21 | loading: true, 22 | errorMessage: '' 23 | }); 24 | 25 | try { 26 | const accounts = await web3.eth.getAccounts(); 27 | await campaign.methods.contribute().send({ 28 | from: accounts[0], 29 | value: web3.utils.toWei(this.state.value, 'ether') 30 | }); 31 | 32 | Router.replaceRoute(`/campaigns/${this.props.address}`); 33 | } catch (error) { 34 | this.setState({ 35 | errorMessage: error.message 36 | }); 37 | } 38 | 39 | this.setState({ 40 | loading: false, 41 | value: '' 42 | }); 43 | }; 44 | 45 | render() { 46 | return ( 47 |
48 | 49 | 50 | this.setState({ 55 | value: event.target.value 56 | })} 57 | /> 58 | 59 | 60 | 61 | 64 | 65 | ); 66 | } 67 | } -------------------------------------------------------------------------------- /components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Menu } from 'semantic-ui-react'; 3 | import { Link } from '../routes'; 4 | 5 | export default () => { 6 | return ( 7 | 8 | 9 | 10 | CrowdCoin 11 | 12 | 13 | 14 | 15 | 16 | 17 | Campaings 18 | 19 | 20 | 21 | 22 | + 23 | 24 | 25 | 26 | 27 | ); 28 | }; -------------------------------------------------------------------------------- /components/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from './Header'; 3 | import { Container } from 'semantic-ui-react'; 4 | import Head from 'next/head'; 5 | 6 | export default props => { 7 | return ( 8 | 9 | 10 | 11 | 12 |
13 | {props.children} 14 | 15 | ); 16 | }; -------------------------------------------------------------------------------- /components/RequestRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Table, Button, Message, Icon, Popup } from 'semantic-ui-react'; 3 | import web3 from '../Ethereum/web3'; 4 | import Campaign from '../Ethereum/campaign'; 5 | 6 | export default class RequestRow extends Component { 7 | 8 | state = { 9 | errorMessageApprove: '', 10 | loadingApprove: false, 11 | errorMessageFinalize: '', 12 | loadingFinalize: false, 13 | }; 14 | 15 | onApprove = async () => { 16 | 17 | this.setState({ 18 | loadingApprove: true, 19 | errorMessageApprove: '' 20 | }); 21 | 22 | try { 23 | const campaign = Campaign(this.props.address); 24 | 25 | const accounts = await web3.eth.getAccounts(); 26 | await campaign.methods.approveRequest(this.props.id).send({ 27 | from: accounts[0] 28 | }); 29 | } 30 | catch (error) { 31 | this.setState({ 32 | errorMessageApprove: error.message 33 | }); 34 | } finally { 35 | this.setState({ 36 | loadingApprove: false, 37 | }); 38 | } 39 | }; 40 | 41 | onFinalize = async () => { 42 | this.setState({ 43 | loadingFinalize: true, 44 | errorMessageFinalize: '' 45 | }); 46 | try { 47 | const campaign = Campaign(this.props.address); 48 | 49 | const accounts = await web3.eth.getAccounts(); 50 | 51 | await campaign.methods.finalizeRequest(this.props.id).send({ 52 | from: accounts[0] 53 | }); 54 | } catch (error) { 55 | this.setState({ 56 | errorMessageFinalize: error.message 57 | }); 58 | } finally { 59 | this.setState({ 60 | loadingFinalize: false, 61 | }); 62 | } 63 | }; 64 | 65 | render() { 66 | const { Row, Cell } = Table; 67 | const { id, request, approversCount } = this.props; 68 | const readyToFinalize = request.approvalCount > approversCount / 2; 69 | 70 | return ( 71 | 72 | {id} 73 | {request.description} 74 | {web3.utils.fromWei(request.value, 'ether')} (ether) 75 | {request.recipient} 76 | {request.approvalCount}/{approversCount} 77 | 78 | } 83 | hoverable /> 84 | {request.complete ? null : ()} 85 | 86 | 87 | } 92 | hoverable /> 93 | {request.complete ? null : ()} 94 | 95 | 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crowdfund", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --timeout 10000", 8 | "dev": "node server.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "dotenv": "^8.0.0", 14 | "fs-extra": "^8.1.0", 15 | "ganache-cli": "^6.5.1", 16 | "mocha": "^6.2.0", 17 | "next": "^9.0.3", 18 | "next-routes": "^1.4.2", 19 | "react": "^16.9.0", 20 | "react-dom": "^16.9.0", 21 | "semantic-ui-css": "^2.4.1", 22 | "semantic-ui-react": "^0.87.3", 23 | "solc": "^0.4.26", 24 | "truffle-hdwallet-provider": "^1.0.16", 25 | "web3": "^1.0.0-beta.35" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pages/campaigns/new.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form, Button, Input, Message } from 'semantic-ui-react'; 3 | import Layout from '../../components/Layout'; 4 | import factory from '../../Ethereum/factory'; 5 | import web3 from '../../Ethereum/web3'; 6 | import { Router } from '../../routes'; 7 | 8 | export default class CampaignNew extends Component { 9 | state = { 10 | minimumContribution: '', 11 | errorMessage: '', 12 | loading: false 13 | }; 14 | 15 | onSubmit = async (event) => { 16 | event.preventDefault(); // keep the browser to automatically submit the form to the server 17 | 18 | this.setState({ loading: true, errorMessage: '' }); 19 | 20 | try { 21 | const accounts = await web3.eth.getAccounts(); 22 | await factory.methods.createCampaign(this.state.minimumContribution) 23 | .send({ 24 | from: accounts[0] 25 | }); 26 | Router.pushRoute('/'); 27 | } catch (err) { 28 | this.setState({ errorMessage: err.message }); 29 | } finally { 30 | this.setState({ loading: false }); 31 | } 32 | }; 33 | 34 | render() { 35 | return ( 36 | 37 |

Create a Campaign

38 | 39 |
40 | 41 | 42 | { 47 | this.setState({ minimumContribution: event.target.value }) 48 | }} 49 | /> 50 | 51 | 52 | 53 | 54 | 55 |
56 | ); 57 | } 58 | } -------------------------------------------------------------------------------- /pages/campaigns/requests/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Table, Grid, Icon, Label } from 'semantic-ui-react'; 3 | import Layout from '../../../components/Layout'; 4 | import { Link } from '../../../routes'; 5 | import Campaings from '../../../Ethereum/campaign'; 6 | import RequestRow from '../../../components/RequestRow'; 7 | import web3 from '../../../Ethereum/web3'; 8 | 9 | export default class RequestIndex extends Component { 10 | 11 | static async getInitialProps(props) { 12 | const { address } = props.query; 13 | const campaign = Campaings(address); 14 | const requestCount = await campaign.methods.getRequestsCount().call(); 15 | const approversCount = await campaign.methods.approversCount().call(); 16 | const summary = await campaign.methods.getSummary().call(); 17 | const contractBalance = summary[1]; 18 | 19 | const requests = await Promise.all( 20 | Array(parseInt(requestCount)).fill().map((element, index) => { 21 | return campaign.methods.requests(index).call() 22 | }) 23 | ); 24 | 25 | return { address, requests, requestCount, approversCount, contractBalance }; 26 | } 27 | 28 | renderRows() { 29 | return this.props.requests.map((request, index) => { 30 | return ; 37 | }); 38 | } 39 | 40 | render() { 41 | 42 | const { Header, Row, HeaderCell, Body } = Table; 43 | const campaignsBalance = this.props.contractBalance; 44 | 45 | return ( 46 | 47 |

Request

48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
75 | 76 | ID 77 | Description 78 | Amount 79 | Recipient 80 | Approval 81 | Approve 82 | Finalize 83 | 84 |
85 | 86 | 87 | {this.renderRows()} 88 | 89 |
90 | 91 |
Found {this.props.requestCount} requests.
92 | 93 |
94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /pages/campaigns/requests/new.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form, Button, Message, Input } from 'semantic-ui-react'; 3 | import Campaigns from '../../../Ethereum/campaign'; 4 | import web3 from '../../../Ethereum/web3'; 5 | import { Link, Router } from '../../../routes'; 6 | import Layout from '../../../components/Layout'; 7 | 8 | export default class RequestNew extends Component { 9 | 10 | state = { 11 | value: '', 12 | description: '', 13 | recipient: '', 14 | errorMessage: '', 15 | loading: false 16 | }; 17 | 18 | static async getInitialProps(props) { 19 | const { address } = props.query; 20 | return { address }; 21 | } 22 | 23 | onSubmit = async event => { 24 | event.preventDefault(); 25 | this.setState({ loading: true, errorMessage: '' }); 26 | 27 | const campaign = Campaigns(this.props.address); 28 | const { description, value, recipient } = this.state; 29 | 30 | try { 31 | const accounts = await web3.eth.getAccounts(); 32 | 33 | if (recipient == '') { 34 | throw new Error('address cannot be empty'); 35 | } 36 | await campaign.methods.createRequest( 37 | description, 38 | web3.utils.toWei(value, 'ether'), 39 | recipient 40 | ).send({ 41 | from: accounts[0] 42 | }); 43 | 44 | Router.pushRoute(`/campaigns/${this.props.address}/requests/`) 45 | } catch (error) { 46 | this.setState({ 47 | errorMessage: error.message 48 | }); 49 | } finally { 50 | this.setState({ 51 | loading: false 52 | }); 53 | } 54 | }; 55 | 56 | render() { 57 | return ( 58 | 59 | 60 | Back 61 | 62 | 63 |

Create a Request

64 |
65 | 66 | 67 | 70 | this.setState({ description: event.target.value }) 71 | } 72 | /> 73 | 74 | 75 | 76 | 77 | 80 | this.setState({ value: event.target.value }) 81 | } 82 | /> 83 | 84 | 85 | 86 | 87 | 90 | this.setState({ recipient: event.target.value }) 91 | } 92 | /> 93 | 94 | 95 | 96 | 97 | 98 |
99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pages/campaigns/show.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Layout from '../../components/Layout'; 3 | import Campaign from '../../Ethereum/campaign'; 4 | import { Card, Grid, Button } from 'semantic-ui-react' 5 | import web3 from '../../Ethereum/web3'; 6 | import ContributeForm from '../../components/ContributeForm'; 7 | import { Link } from '../../routes'; 8 | 9 | export default class CampaignShow extends Component { 10 | static async getInitialProps(props) { 11 | const campaign = Campaign(props.query.address); 12 | 13 | const summary = await campaign.methods.getSummary().call(); 14 | return { 15 | address: props.query.address, 16 | minimumContribution: summary[0], 17 | campaignBalance: summary[1], 18 | noOfReq: summary[2], 19 | noOfContributor: summary[3], 20 | manager: summary[4] 21 | 22 | }; 23 | } 24 | 25 | renderCard() { 26 | 27 | // de structuring 28 | const { 29 | minimumContribution, 30 | campaignBalance, 31 | noOfReq, 32 | noOfContributor, 33 | manager, 34 | } = this.props; 35 | 36 | const items = [ 37 | { 38 | header: manager, 39 | description: 'The Manager created this Campaign and can create requests to withdraw money.', 40 | meta: 'Address of Manager', 41 | style: { overflowWrap: 'break-word' } 42 | }, 43 | { 44 | header: minimumContribution, 45 | description: 'You must contribute atleast this much wei to become a approver.', 46 | meta: 'Minimum Contribution (wei)', 47 | }, 48 | { 49 | header: noOfReq, 50 | description: 'A request tries to withdraw money from the contract. A request must be approved by approvers.', 51 | meta: 'Number of Requests', 52 | }, 53 | { 54 | header: noOfContributor, 55 | description: 'No of people who have already donated to the campaign.', 56 | meta: 'No of Approvers', 57 | }, 58 | { 59 | header: web3.utils.fromWei(campaignBalance, 'ether'), 60 | description: 'The amount of money campaign has left to spend.', 61 | meta: 'Campaign Balance (ether)', 62 | } 63 | ]; 64 | 65 | return ; 66 | } 67 | 68 | render() { 69 | return ( 70 | 71 |

Campaign Show

72 | 73 | 74 | 75 | 76 | {this.renderCard()} 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 |
99 | ); 100 | } 101 | } -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import factory from '../Ethereum/factory'; 3 | import { Card, Button } from 'semantic-ui-react'; 4 | import Layout from '../components/Layout'; 5 | import { Link } from '../routes'; 6 | 7 | class CampaignIndex extends Component { 8 | 9 | static async getInitialProps() { 10 | // next js execute on the server side 11 | const campaigns = await factory.methods.getDeployedCampaigns().call(); 12 | return { campaigns }; 13 | } 14 | 15 | renderCampaigns() { 16 | const items = this.props.campaigns.map(address => { 17 | return { 18 | header: address, 19 | description: ( 20 | 21 | View Campaign 22 | 23 | ), 24 | fluid: true, 25 | style: { overflowWrap: 'break-word' } 26 | }; 27 | }); 28 | 29 | return ; 30 | } 31 | 32 | render() { 33 | return ( 34 | 35 |
36 |

Open Campaigns

37 | 38 | 39 | 40 |
50 |
51 | ); 52 | } 53 | } 54 | 55 | export default CampaignIndex; -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | // Defines different routes 2 | 3 | const routes = require('next-routes')(); //return a function and () means execute it 4 | 5 | routes 6 | .add('/campaigns/new', '/campaigns/new') 7 | .add('/campaigns/:address', '/campaigns/show') 8 | .add('/campaigns/:address/requests', '/campaigns/requests/index') 9 | .add('/campaigns/:address/requests/new', '/campaigns/requests/new'); 10 | 11 | module.exports = routes; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Boot up 'next' app, tell it to use routes.js 2 | 3 | const { createServer } = require('http'); 4 | const next = require('next'); 5 | 6 | const app = next({ 7 | dev: process.env.NODE_ENV !== 'production' 8 | }); 9 | 10 | const routes = require('./routes'); 11 | const handler = routes.getRequestHandler(app); 12 | 13 | app.prepare().then(() => { 14 | createServer(handler).listen(3000, (err) => { 15 | if (err) throw err; 16 | console.log('Ready on localhost: 3000'); 17 | }); 18 | }); -------------------------------------------------------------------------------- /test/Campaign.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ganache = require('ganache-cli'); 3 | const Web3 = require('web3'); 4 | const web3 = new Web3(ganache.provider()); 5 | 6 | const compileFactory = require('../Ethereum/build/CampaignFactory.json'); 7 | const compileCampaign = require('../Ethereum/build/Campaign.json'); 8 | 9 | let accounts; 10 | let factory; 11 | let campaignAddress; 12 | let campaign; 13 | 14 | beforeEach(async () => { 15 | accounts = await web3.eth.getAccounts(); 16 | 17 | factory = await new web3.eth.Contract(JSON.parse(compileFactory.interface)) 18 | .deploy({ data: compileFactory.bytecode }) 19 | .send({ from: accounts[0], gas: '1000000' }); 20 | 21 | await factory.methods.createCampaign('100').send({ 22 | from: accounts[0], 23 | gas: '1000000' 24 | }); 25 | 26 | [campaignAddress] = await factory.methods.getDeployedCampaigns().call(); // [campaignAddress] => campaignAddress = address[0] 27 | 28 | campaign = await new web3.eth.Contract( 29 | JSON.parse(compileCampaign.interface), 30 | campaignAddress 31 | ); 32 | }); 33 | 34 | describe('Campaigns', () => { 35 | it('deploys a factory and Campaigns', () => { 36 | assert.ok(factory.options.address); 37 | assert.ok(campaign.options.address); 38 | }); 39 | 40 | it('marks caller as the campaign manager', async () => { 41 | const manager = await campaign.methods.manager().call(); 42 | assert.equal(accounts[0], manager); 43 | }); 44 | 45 | it('allows people to contribute money and marks them as approvers', async () => { 46 | await campaign.methods.contribute().send({ 47 | value: 200, 48 | from: accounts[1] 49 | }); 50 | 51 | assert(await campaign.methods.approvers(accounts[1]).call()); 52 | 53 | }); 54 | 55 | it('requires a minimum contribution', async () => { 56 | var check = true; 57 | try { 58 | await campaign.methods.contribute().send({ 59 | from: accounts[0], 60 | value: 200 61 | }); 62 | check = false; 63 | } catch (error) { 64 | check = true; 65 | } 66 | if (check) { 67 | assert(false); 68 | } else { 69 | assert(true); 70 | } 71 | }); 72 | 73 | it('allows a manager to make a payment request', async () => { 74 | await campaign.methods.createRequest('Buy solar light', '100', accounts[1]) 75 | .send({ 76 | from: accounts[0], 77 | gas: 1000000 78 | }); 79 | 80 | const request = await campaign.methods.requests(0).call(); 81 | 82 | assert.equal('Buy solar light', request.description); 83 | 84 | }); 85 | 86 | it('processes requests', async () => { 87 | await campaign.methods.contribute().send({ 88 | from: accounts[0], 89 | value: web3.utils.toWei('10', 'ether') 90 | }); 91 | 92 | var beforeBlnc = await web3.eth.getBalance(accounts[1]); 93 | 94 | await campaign.methods.createRequest('Buy solar light', web3.utils.toWei('5', 'ether'), accounts[1]) 95 | .send({ 96 | from: accounts[0], 97 | gas: 1000000 98 | }); 99 | 100 | await campaign.methods.approveRequest(0).send({ 101 | from: accounts[0], 102 | gas: '1000000' 103 | }); 104 | 105 | await campaign.methods.finalizeRequest(0).send({ 106 | from: accounts[0], 107 | gas: '1000000' 108 | }); 109 | 110 | var afterBlnc = await web3.eth.getBalance(accounts[1]); 111 | beforeBlnc = parseFloat(web3.utils.fromWei(beforeBlnc, 'ether')); 112 | afterBlnc = parseFloat(web3.utils.fromWei(afterBlnc, 'ether')); 113 | 114 | assert(afterBlnc + 4 > beforeBlnc); 115 | 116 | }); 117 | 118 | }); 119 | --------------------------------------------------------------------------------