[區塊鏈-以太坊] 募資平台建立-Part 3. 提出資金花費項目

Chiwen Lai
14 min readJun 5, 2018

--

Part 1. 提案Part 2. 贊助之後,接下來我們將由提案人提出資金花費項目,讓贊助者決定是否應該花掉這筆錢。

  1. 由首頁的合約位址點入,帶到該募資計畫的詳細頁面。
  2. 從募資計畫的詳細頁面點選「查看花費項目」
  3. 在花費項目頁面可查看花費項目的明細,再由贊助人核准。如需新增花費項目,可以點選左上角的「新增花費項目」。
  4. 新增花費項目表單,限由提案人提出。

STEP 1: Backend

▼開啟/contracts/Campaign.sol,在合約內加入花費項目的功能。

contract Campaign {
// 1 - 定義花費項目的基本資料
struct Request {
string description;
uint value;
address recipient;
bool complete;
uint approvalCount;
mapping(address => bool) approvals;
}
// 2 - 所有的花費項目
Request[] public requests;

address public owner;
uint public minimumContribution;
string public campaignName;
mapping(address => bool) public contributors;
uint public contributorsCount;
// 5 - 檢查是否為提案人
modifier ownerOnly() {
require(msg.sender == owner);
_;
}
constructor(uint minimum, string name, address creator) public {
owner = creator;
minimumContribution = minimum;
campaignName = name;
}
function contribute() public payable {
require(msg.value > minimumContribution);
contributors[msg.sender] = true;
contributorsCount++;
}
function getSummary() public view returns (
string, uint, uint, uint, address
){
return (
campaignName,
minimumContribution,
this.balance,
contributorsCount,
owner
);
}
// 5 - 新增花費項目設定為只允許提案人提出
function createRequest(string description, uint value, address recipient) public ownerOnly {
// 3 -
Request memory newRequest = Request({
description: description,
value: value,
recipient: recipient,
complete: false,
approvalCount: 0
});
// 4 -
requests.push(newRequest);
}
//
function getRequestsCount() public view returns(uint) {
return requests.length;
}
}

// 1 — 利用struct型態定義我們花費項目Request的基本資料,包含花費項目說明、需要花多少錢、收件者是誰、是否完成、核准人數。注意,每個欄位後面是接分號而不是逗號。
// 2 — 宣告一個屬於Request型態的陣列,requests存放所有的募資計畫。
// 3 — 設定newRequest為Request型態的變數,並設定為存放在memory,才不會發生TypeError無法轉換的問題。
// 4 — 將新增的花費項目放進requests陣列裡。
// 5 — 限制僅由Owner可以新增花費項目:先新增Function Modifiers ownerOnly(),在createRequest() Function後面加上ownerOnly即可。

STEP 2: 重新部署

▼增加gas:打開deploy.js,將gas設定為2000000。

const deploy = async () => {
const accounts = await web3.eth.getAccounts();
const result = await new web3.eth.Contract(
JSON.parse(compiledFactory.interface)
)
.deploy({ data: compiledFactory.bytecode })
.send({ from: accounts[0], gas: "2000000" });
console.log("Contract deployed to", result.options.address);
};

(TODO: 不知為何,這次部署前如果沒有增加gas,會出現以下的錯誤訊息。)

(node:5066) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: The contract code couldn’t be stored, please check your gas limit.

▼合約寫好後記得重新部署。

$ node compile.js
$ node deploy.js
Contract deployed to 0xaB00Ca5b50E64E991fF2AC43Eb8c90551A8DAE86

▼將新的合約位址更新到factory.js,如下。

import web3 from "./web3";
import CampaignFactory from "./build/CampaignFactory.json";
const instance = new web3.eth.Contract(
JSON.parse(CampaignFactory.interface),
"0xaB00Ca5b50E64E991fF2AC43Eb8c90551A8DAE86"
);
export default instance;

STEP 3: Frontend

▼在routes.js加上url routing,以募資計畫的位址為url。

const routes = require("next-routes")();routes
.add("/campaigns/new", "/campaigns/new")
.add("/campaigns/:address", "/campaigns/show")
.add("/campaigns/:address/requests", "/campaigns/requests/index")
.add("/campaigns/:address/requests/new", "campaigns/requests/new")
;
module.exports = routes;

▼在募資計劃明細show.js加上連結,利用募資計畫的位址作為參數帶入。

render() {
return (
<Layout>
<h3>{this.props.campaignName}</h3>
<Grid>
<Grid.Row>
<Grid.Column width={10}>{this.renderCards()}</Grid.Column>
<Grid.Column width={6}>
<ContributeForm address={this.props.address} />
</Grid.Column>
</Grid.Row>
<Grid.Row>
<Grid.Column>
<Link route={`/campaigns/${this.props.address}/requests`}>
<a>
<Button primary>查看花費項目</Button>
</a>
</Link>
</Grid.Column>
</Grid.Row>

</Grid>
</Layout>
);
}

▼在campaigns目錄下面新建一個requests目錄,在這目錄下新建一個檔案index.js,在這邊列出全部的花費項目。先將需要的資料標示在表頭,下一篇再補上內容。

import React, { Component } from "react";
import { Button, Table } from "semantic-ui-react";
import { Link } from "../../../routes";
import Layout from "../../../components/Layout";
import Campaign from "../../../ethereum/campaign";
class RequestIndex extends Component {
static async getInitialProps(props) {
const { address } = props.query;
const campaign = Campaign(address);
const requestCount = await campaign.methods.getRequestsCount().call();
const requests = await Promise.all(
Array(parseInt(requestCount))
.fill()
.map((element, index) => {
return campaign.methods.requests(index).call();
})
);
return { address, requests, requestCount };
}
render() {
const { Header, Row, HeaderCell, Body } = Table;
return (
<Layout>
<h3>花費項目</h3>
<Link route={`/campaigns/${this.props.address}/requests/new`}>
<a>
<Button primary>新增花費項目</Button>
</a>
</Link>
<Table>
<Header>
<Row>
<HeaderCell>ID</HeaderCell>
<HeaderCell>花費項目</HeaderCell>
<HeaderCell>金額</HeaderCell>
<HeaderCell>廠商位址(以太幣接收方)</HeaderCell>
<HeaderCell>核准人數</HeaderCell>
<HeaderCell>核准</HeaderCell>
<HeaderCell>提案人確認</HeaderCell>
</Row>
</Header>
</Table>
</Layout>
);
}
}
export default RequestIndex;

▼在requests目錄下新建一個檔案new.js,及新增花費項目的表單。

import React, { Component } from "react";
import { Form, Input, Button, Message } from "semantic-ui-react";
import Campaign from "../../../ethereum/campaign";
import web3 from "../../../ethereum/web3";
import { Link, Router } from "../../../routes";
import Layout from "../../../components/Layout";
class RequestNew extends Component {
state = {
description: "",
value: "",
recipient: "",
loading: false,
errorMessage: ""
};
static async getInitialProps(props) {
const { address } = props.query;
return { address };
}
onSubmit = async event => {
event.preventDefault();
const campaign = Campaign(this.props.address);
const { description, value, recipient } = this.state;
this.setState({ loading: true, errorMessage: "" });try {
const accounts = await web3.eth.getAccounts();
await campaign.methods
.createRequest(description, web3.utils.toWei(value, "ether"), recipient)
.send({
from: accounts[0]
});
Router.pushRoute(`/campaigns/${this.props.address}/requests`);
} catch (err) {
this.setState({ errorMessage: err.message });
}
this.setState({ loading: false });
};
render() {
return (
<Layout>
<Link route={`campaigns/${this.props.address}/requests`}>
<a>Back</a>
</Link>
<h3>新增花費項目</h3>
<Form onSubmit={this.onSubmit} error={!!this.state.errorMessage}>
<Form.Field>
<label>花費項目</label>
<Input
value={this.state.description}
onChange={event =>
this.setState({ description: event.target.value })
}
/>
</Form.Field>
<Form.Field>
<label>金額</label>
<Input
label="ether"
labelPosition="right"
value={this.state.value}
onChange={event => this.setState({ value: event.target.value })}
/>
</Form.Field>
<Form.Field>
<label>廠商位址(以太幣接收方)</label>
<Input
value={this.state.recipient}
onChange={event =>
this.setState({ recipient: event.target.value })
}
/>
</Form.Field>
<Message error header="Oops!" content={this.state.errorMessage} />
<Button primary loading={this.state.loading}>
新增
</Button>
</Form>
</Layout>
);
}
}
export default RequestNew;

▼接下來直接在終端機crowdfunding目錄下面輸入

$ npm run dev

即可在瀏覽器http://localhost:3000觀看到更新後的畫面。

>>> 下一篇:Part 4. 核准

--

--