<think>我们正在使用Truffle的fundraiser模板来构建一个众筹DApp。该模板通常包含智能合约和React前端。我们将按照以下步骤进行:步骤1:初始化项目步骤2:编写智能合约(创建项目、捐款、提款等功能)步骤3:编译和部署合约步骤4:编写React前端(使用MaterialUI和Web3.js)与合约交互步骤5:测试具体需要修改的文件和步骤如下:1.初始化项目使用Truffleunbox命令创建项目:```bashtruffleunboxtruffle-box/fundraiser-box```2.修改智能合约通常,模板中已经有一个Fundraiser.sol合约,但我们需要完善它,添加项目创建、捐款、提款等功能。需要修改的文件:`contracts/Fundraiser.sol`示例合约代码(根据需求调整):```soliditypragmasolidity^0.8.0;contractFundraiser{structProject{uintid;stringname;stringdescription;addresspayableowner;uintgoal;uintdeadline;uinttotalDonations;boolisActive;}uintpublicnextProjectId=1;mapping(uint=>Project)publicprojects;mapping(uint=>mapping(address=>uint))publicdonations;eventProjectCreated(uintid,stringname,uintgoal,uintdeadline);eventDonationMade(uintprojectId,addressdonor,uintamount);eventWithdrawal(uintprojectId,addressowner,uintamount);functioncreateProject(stringmemory_name,stringmemory_description,uint_goal,uint_duration)public{uintdeadline=block.timestamp+_duration;projects[nextProjectId]=Project(nextProjectId,_name,_description,payable(msg.sender),_goal,deadline,0,true);emitProjectCreated(nextProjectId,_name,_goal,deadline);nextProjectId++;}functiondonate(uint_projectId)publicpayable{require(projects[_projectId].isActive,"Projectisnotactive");require(block.timestamp<projects[_projectId].deadline,"Deadlinehaspassed");require(msg.value>0,"Donationmustbegreaterthan0");projects[_projectId].totalDonations+=msg.value;donations[_projectId][msg.sender]+=msg.value;emitDonationMade(_projectId,msg.sender,msg.value);}functionwithdraw(uint_projectId)public{Projectstorageproject=projects[_projectId];require(project.isActive,"Projectisnotactive");require(block.timestamp>=project.deadline,"Deadlinenotreached");require(project.totalDonations>=project.goal,"Goalnotreached");require(msg.sender==project.owner,"Onlyownercanwithdraw");project.isActive=false;project.owner.transfer(project.totalDonations);emitWithdrawal(_projectId,msg.sender,project.totalDonations);}}```3.部署脚本在`migrations/`目录下创建部署脚本,例如:`2_deploy_contracts.js````javascriptconstFundraiser=artifacts.require("Fundraiser");module.exports=function(deployer){deployer.deploy(Fundraiser);};```4.编译和部署```bashtrufflecompiletrufflemigrate--reset```5.修改前端(React组件)前端代码通常在`client/`目录下。我们需要修改以下文件:-`client/src/App.js`:主应用组件-创建新的组件:`CreateProject.js`,`ProjectList.js`,`ProjectDetail.js`等使用MaterialUI和Web3.js进行交互。示例:在`App.js`中连接钱包并加载合约```javascriptimportReact,{useEffect,useState}from'react';importWeb3from'web3';importFundraiserContractfrom'./contracts/Fundraiser.json';import{Button,AppBar,Toolbar,Typography}from'@material-ui/core';functionApp(){const[web3,setWeb3]=useState(null);const[accounts,setAccounts]=useState([]);const[contract,setContract]=useState(null);useEffect(()=>{constinit=async()=>{//检查是否安装MetaMaskif(window.ethereum){constweb3Instance=newWeb3(window.ethereum);setWeb3(web3Instance);try{//请求账户访问constaccounts=awaitwindow.ethereum.request({method:'eth_requestAccounts'});setAccounts(accounts);//获取网络IDconstnetworkId=awaitweb3Instance.eth.net.getId();constdeployedNetwork=FundraiserContract.networks[networkId];//实例化合约constcontractInstance=newweb3Instance.eth.Contract(FundraiserContract.abi,deployedNetwork&&deployedNetwork.address);setContract(contractInstance);}catch(error){console.error("Couldnotconnecttocontract",error);}}};init();},[]);return(<div><AppBarposition="static"><Toolbar><Typographyvariant="h6">众筹平台</Typography>{accounts.length>0&&(<Typographyvariant="subtitle1"style={{marginLeft:'auto'}}>账户:{accounts[0]}</Typography>)}</Toolbar></AppBar>{/*其他组件*/}</div>);}exportdefaultApp;```6.创建项目组件(CreateProject.js)```javascriptimportReact,{useState}from'react';import{TextField,Button,Box}from'@material-ui/core';functionCreateProject({contract,account}){const[name,setName]=useState('');const[description,setDescription]=useState('');const[goal,setGoal]=useState('');const[duration,setDuration]=useState('');consthandleSubmit=async(e)=>{e.preventDefault();//将目标金额转换为weiconstgoalInWei=web3.utils.toWei(goal,'ether');awaitcontract.methods.createProject(name,description,goalInWei,duration).send({from:account});//重置表单setName('');setDescription('');setGoal('');setDuration('');};return(<Boxcomponent="form"onSubmit={handleSubmit}style={{display:'flex',flexDirection:'column',maxWidth:300}}><TextFieldlabel="项目名称"value={name}onChange={e=>setName(e.target.value)}required/><TextFieldlabel="描述"value={description}onChange={e=>setDescription(e.target.value)}required/><TextFieldlabel="目标金额(ETH)"type="number"value={goal}onChange={e=>setGoal(e.target.value)}required/><TextFieldlabel="持续时间(秒)"type="number"value={duration}onChange={e=>setDuration(e.target.value)}required/><Buttontype="submit"variant="contained"color="primary">创建项目</Button></Box>);}exportdefaultCreateProject;```7.项目列表和详情组件类似地,创建组件来显示项目列表和项目详情,并允许用户捐款和项目所有者提款。8.测试编写测试脚本测试合约功能,同时测试前端功能。以上步骤概述了如何基于truffleunboxfundraiser模板完成众筹DApp。具体实现时,需要根据实际需求调整合约和前端代码。注意:以上代码仅为示例,实际开发中需要处理错误、加载状态等。引用:[^1]:关于如何安装MetaMask和导入私钥的说明。[^2]:关于Truffleunbox的说明。§§相关问题§§1.如何测试智能合约的提款功能?2.如何在前端实时更新项目列表?3.如何处理众筹失败后的退款?</think>基于 Truffle unbox fundraiser 模板完成众筹 DApp 的开发,需要修改以下文件并按照具体步骤操作:
### 需要修改的文件及步骤
#### 1. 智能合约修改 (`contracts/Fundraiser.sol`)
```solidity
pragma solidity ^0.8.0;
contract Fundraiser {
struct Project {
uint id;
string name;
string description;
address payable owner;
uint goal;
uint deadline;
uint totalDonations;
bool isActive;
}
uint public nextProjectId = 1;
mapping(uint => Project) public projects;
mapping(uint => mapping(address => uint)) public donations;
event ProjectCreated(
uint id,
string name,
uint goal,
uint deadline
);
event DonationMade(
uint projectId,
address donor,
uint amount
);
event Withdrawal(uint projectId, address owner, uint amount);
function createProject(
string memory _name,
string memory _description,
uint _goal,
uint _duration
) public {
uint deadline = block.timestamp + _duration;
projects[nextProjectId] = Project(
nextProjectId,
_name,
_description,
payable(msg.sender),
_goal,
deadline,
0,
true
);
emit ProjectCreated(nextProjectId, _name, _goal, deadline);
nextProjectId++;
}
function donate(uint _projectId) public payable {
require(projects[_projectId].isActive, "Project is not active");
require(block.timestamp < projects[_projectId].deadline, "Deadline has passed");
require(msg.value > 0, "Donation must be greater than 0");
projects[_projectId].totalDonations += msg.value;
donations[_projectId][msg.sender] += msg.value;
emit DonationMade(_projectId, msg.sender, msg.value);
}
function withdraw(uint _projectId) public {
Project storage project = projects[_projectId];
require(project.isActive, "Project is not active");
require(block.timestamp >= project.deadline, "Deadline not reached");
require(project.totalDonations >= project.goal, "Goal not reached");
require(msg.sender == project.owner, "Only owner can withdraw");
project.isActive = false;
project.owner.transfer(project.totalDonations);
emit Withdrawal(_projectId, msg.sender, project.totalDonations);
}
}
```
#### 2. 迁移脚本修改 (`migrations/2_deploy_contracts.js`)
```javascript
const Fundraiser = artifacts.require("Fundraiser");
module.exports = function (deployer) {
deployer.deploy(Fundraiser);
};
```
#### 3. 前端组件修改 (`client/src/`)
##### 项目创建组件 (`CreateProject.js`)
```jsx
import React, { useState } from 'react';
import { TextField, Button, Box } from '@material-ui/core';
import Web3 from 'web3';
function CreateProject({ contract, account }) {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [goal, setGoal] = useState('');
const [duration, setDuration] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
// 将目标金额转换为wei
const goalInWei = Web3.utils.toWei(goal, 'ether');
await contract.methods.createProject(
name,
description,
goalInWei,
duration * 86400 // 天数转秒
).send({ from: account });
// 重置表单
setName('');
setDescription('');
setGoal('');
setDuration('');
};
return (
<Box component="form" onSubmit={handleSubmit}>
<TextField label="项目名称" value={name} onChange={e => setName(e.target.value)} fullWidth required />
<TextField label="描述" value={description} onChange={e => setDescription(e.target.value)} fullWidth multiline required />
<TextField label="目标金额(ETH)" type="number" value={goal} onChange={e => setGoal(e.target.value)} fullWidth required />
<TextField label="持续时间(天)" type="number" value={duration} onChange={e => setDuration(e.target.value)} fullWidth required />
<Button type="submit" variant="contained" color="primary">创建项目</Button>
</Box>
);
}
```
##### 项目列表组件 (`ProjectList.js`)
```jsx
import React, { useEffect, useState } from 'react';
import { Card, CardContent, Typography, Button, Grid } from '@material-ui/core';
import Web3 from 'web3';
function ProjectList({ contract, account }) {
const [projects, setProjects] = useState([]);
useEffect(() => {
const loadProjects = async () => {
const projectCount = await contract.methods.nextProjectId().call();
const loadedProjects = [];
for (let i = 1; i < projectCount; i++) {
const project = await contract.methods.projects(i).call();
loadedProjects.push({
...project,
id: i,
goal: Web3.utils.fromWei(project.goal, 'ether')
});
}
setProjects(loadedProjects);
};
loadProjects();
}, [contract]);
return (
<Grid container spacing={3}>
{projects.map(project => (
<Grid item xs={12} sm={6} md={4} key={project.id}>
<Card>
<CardContent>
<Typography variant="h5">{project.name}</Typography>
<Typography>{project.description}</Typography>
<Typography>目标: {project.goal} ETH</Typography>
<Typography>已筹集: {Web3.utils.fromWei(project.totalDonations, 'ether')} ETH</Typography>
<Button
variant="contained"
color="primary"
onClick={() => window.location = `/project/${project.id}`}
>
查看详情
</Button>
</CardContent>
</Card>
</Grid>
))}
</Grid>
);
}
```
##### 项目详情组件 (`ProjectDetail.js`)
```jsx
import React, { useState, useEffect } from 'react';
import { Card, CardContent, Typography, Button, TextField, LinearProgress } from '@material-ui/core';
import Web3 from 'web3';
function ProjectDetail({ contract, account, match }) {
const [project, setProject] = useState(null);
const [donationAmount, setDonationAmount] = useState('');
const projectId = match.params.id;
useEffect(() => {
const loadProject = async () => {
const projectData = await contract.methods.projects(projectId).call();
setProject({
...projectData,
id: projectId,
goal: Web3.utils.fromWei(projectData.goal, 'ether'),
totalDonations: Web3.utils.fromWei(projectData.totalDonations, 'ether')
});
};
loadProject();
}, [contract, projectId]);
const handleDonate = async () => {
const amountInWei = Web3.utils.toWei(donationAmount, 'ether');
await contract.methods.donate(projectId).send({
from: account,
value: amountInWei
});
setDonationAmount('');
};
const handleWithdraw = async () => {
await contract.methods.withdraw(projectId).send({ from: account });
};
if (!project) return <div>加载中...</div>;
const progress = (project.totalDonations / project.goal) * 100;
const isOwner = project.owner.toLowerCase() === account.toLowerCase();
return (
<Card>
<CardContent>
<Typography variant="h4">{project.name}</Typography>
<Typography>{project.description}</Typography>
<Typography>目标金额: {project.goal} ETH</Typography>
<Typography>已筹集: {project.totalDonations} ETH</Typography>
<LinearProgress variant="determinate" value={progress} />
<TextField
label="捐赠金额 (ETH)"
type="number"
value={donationAmount}
onChange={e => setDonationAmount(e.target.value)}
fullWidth
style={{ margin: '20px 0' }}
/>
<Button
variant="contained"
color="primary"
onClick={handleDonate}
disabled={!donationAmount}
>
捐赠
</Button>
{isOwner && (
<Button
variant="contained"
color="secondary"
onClick={handleWithdraw}
style={{ marginLeft: 10 }}
>
提取资金
</Button>
)}
</CardContent>
</Card>
);
}
```
#### 4. 主应用文件 (`client/src/App.js`)
```jsx
import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import { AppBar, Toolbar, Typography, Button } from '@material-ui/core';
import Web3 from 'web3';
import FundraiserContract from './contracts/Fundraiser.json';
import ProjectList from './ProjectList';
import CreateProject from './CreateProject';
import ProjectDetail from './ProjectDetail';
function App() {
const [web3, setWeb3] = useState(null);
const [account, setAccount] = useState('');
const [contract, setContract] = useState(null);
useEffect(() => {
const init = async () => {
if (window.ethereum) {
const web3Instance = new Web3(window.ethereum);
setWeb3(web3Instance);
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
setAccount(accounts[0]);
const networkId = await web3Instance.eth.net.getId();
const deployedNetwork = FundraiserContract.networks[networkId];
const contractInstance = new web3Instance.eth.Contract(
FundraiserContract.abi,
deployedNetwork && deployedNetwork.address
);
setContract(contractInstance);
} catch (error) {
console.error("初始化失败", error);
}
}
};
init();
}, []);
return (
<Router>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" style={{ flexGrow: 1 }}>
众筹平台
</Typography>
{account && (
<Typography variant="subtitle1">
账户: {account.slice(0, 6)}...{account.slice(-4)}
</Typography>
)}
</Toolbar>
</AppBar>
<div style={{ padding: 20 }}>
<Switch>
<Route exact path="/">
<CreateProject contract={contract} account={account} />
<ProjectList contract={contract} account={account} />
</Route>
<Route path="/project/:id">
<ProjectDetail contract={contract} account={account} />
</Route>
</Switch>
</div>
</Router>
);
}
```
### 完整开发步骤
1. **初始化项目**
```bash
truffle unbox truffle-box/fundraiser-box
cd client
npm install @material-ui/core web3 react-router-dom
```
2. **修改智能合约**
- 替换 `contracts/Fundraiser.sol` 为上述合约代码
- 更新 `migrations/2_deploy_contracts.js` 部署脚本
3. **编译和部署合约**
```bash
truffle compile
truffle migrate --reset
```
4. **配置前端**
- 在 `client/src` 目录中创建组件文件:
- `CreateProject.js`
- `ProjectList.js`
- `ProjectDetail.js`
- 更新 `App.js` 主文件
5. **启动开发服务器**
```bash
cd client
npm start
```
6. **测试功能**
- 创建新项目
- 向项目捐款
- 达到目标后提取资金
- 验证状态更新和事件触发
7. **生产环境构建**
```bash
npm run build
```
### 关键功能说明
1. **项目创建**:
- 用户输入项目详情和目标金额
- 智能合约记录项目信息并分配唯一ID
2. **资金募集**:
- 捐赠者向项目发送ETH
- 合约跟踪总捐赠额和捐赠者记录
- 实时更新进度条
3. **资金提取**:
- 项目到期且达到目标后
- 仅项目所有者可提取资金
- 提取后项目标记为已完成
### 注意事项
1. 时间单位转换:
```javascript
// 前端:天数转秒
duration * 86400
// 合约:秒转可读时间
new Date(deadline * 1000).toLocaleString()
```
2. 金额单位转换:
```javascript
// ETH转Wei
Web3.utils.toWei(amount, 'ether')
// Wei转ETH
Web3.utils.fromWei(amount, 'ether')
```
3. 权限控制:
```solidity
require(msg.sender == project.owner, "Only owner can withdraw");
```
4. 条件检查:
```solidity
require(block.timestamp >= project.deadline, "Deadline not reached");
require(project.totalDonations >= project.goal, "Goal not reached");
```
[^1]: MetaMask 钱包安装指南
[^2]: Truffle 项目配置说明