用Truffle, Solidity, React, Material UI, Web3 创建一个全栈筹款Dapp(Fundraiser Dapp)
图片来源: Shubham Dhage
我们将使用 React Truffle Box 为 web3 应用生成前端代码,让它可以快速运行起来并与 web3 交互。从为筹款应用(fundraiser)创建一个新目录开始,接着在目录下创建一个新的 Truffle React Box...
首先,为 fundraiser 创建一个新的空仓库,再进入该目录对 React Truffle Box 进行 unbox:
mdkir fundraiser
cd fundraiser
truffle unbox react
不包括node_modules
,我们的目录结构现在应该是这样的:
移除 contracts/SimpleStorage.sol
, migrations/2_deploy_contracts.js
和空的test
文件夹:
rm contracts/SimpleStorage.sol \ migrations/2_deploy_contracts.js \ test/*
然后,需要为合约和迁移创建新文件。 先创建空文件:
touch contracts/FundraiserFactory.sol
touch contracts/Fundraiser.sol
touch migrations/2\_factory\_contract\_migrations.js
我们还需要安装OpenZeppelin
,因为我们用的是 Ownable 合约:
npm install @openzeppelin/contracts
在Fundraiser.sol
文件中,需要修改 import 语句,才能使用我们刚从 OpenZeppelin 安装的node\_module
:
import '../client/node_modules/@openzeppelin/contracts/ownership/Ownable.sol';
Fundraiser.sol
的代码如下:
pragma solidity >0.4.23;
import "@openzeppelin/contracts/access/Ownable.sol";
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
contract Fundraiser is Ownable {
using SafeMath for uint256;
string public name;
string public url;
string public imageURL;
string public description;
address payable public beneficiary;
address public custodian;
struct Donation {
uint256 value;
uint256 date;
}
mapping (address => Donation[]) private _donations;
uint256 public totalDonations;
uint256 public donationsCount;
event DonationReceived (address indexed donor, uint256 value);
event Withdraw(uint256 amount);
constructor (string memory _name, string memory _url, string memory _imageURL, string memory _description, address payable _beneficiary, address _custodian) public {
name = _name;
url = _url;
imageURL = _imageURL;
description = _description;
beneficiary = _beneficiary;
transferOwnership(_custodian);
}
function setBeneficiary (address payable _beneficiary) public onlyOwner {
beneficiary = _beneficiary;
}
function myDonationsCount () public view returns (uint256) {
return _donations[msg.sender].length;
}
function donate () public payable {
Donation memory donation = Donation({
value: msg.value,
date: block.timestamp
});
_donations[msg.sender].push(donation);
totalDonations = totalDonations.add(msg.value);
donationsCount++;
emit DonationReceived (msg.sender, msg.value);
}
function myDonations () public view returns (uint256[] memory values, uint256[] memory dates) {
uint256 count = myDonationsCount();
values = new uint256[](count);
dates = new uint256[](count);
for (uint256 i = 0; i < count; i++) {
Donation storage donation = _donations[msg.sender][i];
values[i] = donation.value;
dates[i] = donation.date;
}
return (values, dates);
}
function withdraw () public onlyOwner {
uint256 balance = address(this).balance;
beneficiary.transfer(balance);
emit Withdraw(balance);
}
fallback () external payable {
totalDonations = totalDonations.add(msg.value);
donationsCount++;
}
}
-Fundraiser.sol-
FundraiserFactory.sol
代码:
pragma solidity >0.4.23;
import "./Fundraiser.sol";
contract FundraiserFactory {
Fundraiser[] private _fundraisers;
uint256 constant maxLimit = 20;
event FundraiserCreated (Fundraiser indexed fundraiser, address indexed owner);
function fundraisersCount () public view returns (uint256) {
return _fundraisers.length;
}
function createFundraiser (string memory name, string memory url, string memory imageURL, string memory description, address payable beneficiary) public {
Fundraiser fundraiser = new Fundraiser (name, url, imageURL, description, beneficiary, msg.sender);
_fundraisers.push(fundraiser);
emit FundraiserCreated(fundraiser, msg.sender);
}
function fundraisers (uint256 limit, uint256 offset) public view returns (Fundraiser[] memory coll) {
require (offset <= fundraisersCount(), "offset out of bounds");
uint256 size = fundraisersCount() - offset;
size = size < limit ? size : limit;
size = size < maxLimit ? size : maxLimit;
coll = new Fundraiser[](size);
for (uint256 i = 0; i < size; i++) {
coll[i] = _fundraisers[offset + i];
}
return coll;
}
}
-FundraiserFactory.sol-
添加上面的合约后,再次运行 Truffle 开发环境。
在 fundraiser 仓库,运行命令编译并迁移Fundraiser
合约,以便前端应用与之交互:
truffle develop
上面的命令运行完后,在同一个终端窗口继续运行compile
和migrate
:
compile
migrate
如果你在运行
migrate
时遇到问题卡住了,可以尝试运行migrate — reset
.
如果你的合约已经迁移完成,输出应该是这样的:
Starting migrations...
======================
> Network name: 'develop'
> Network id: 5777
> Block gas limit: 0x6691b7
1_initial_migration.js
======================
Replacing 'Migrations'
----------------------
> transaction hash: 0xf04ee2a0c62330e7a051148d4660de...
> Blocks: 0 Seconds: 0
> contract address: 0x6Af651D4c6E9f32a627381B...
> block number: 1
> block timestamp: 1566526994
> account: 0xb94454C83ff541c82391b...
> balance: 99.99477342
> gas used: 261329
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.00522658 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.00522658 ETH
2_factory_contract_migrations.js
================================
Replacing 'Factory'
-------------------
> transaction hash: 0xebc5a26bbe12f52b809d9144...
> Blocks: 0 Seconds: 0
> contract address: 0xB7780C9AD3ef38bb4C8B48fab37...
> block number: 3
> block timestamp: 1566526995
> account: 0xb94454C83ff541c82391b4...
> balance: 99.95063086
> gas used: 2165105
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.0433021 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.0433021 ETH
Summary
=======
> Total deployments: 2
> Final cost: 0.04852868 ETH
让 React Truffle Box 运行起来,我们就可以与应用程序交互了。
首先需要cd
进入 React 的src
文件 ,并安装 React Truffle Box 提供的所有依赖。
之后,像我们在前一章节那样启动应用程序的前端:
cd client
npm i
模块安装完成后,启动服务:
npm start
如果全部正确安装并且成功启动,我们现在就可以打开 localhost:3000 ,看到我们的web3应用程序。
哎呀!我们忘记切换到匹配新应用程序的网络了,要在同一个网络上应用才能正常运行。
首先,让我们进入 MetaMask 并且将网络切换到 Localhost 8545。
Localhost 8545 应该在默认列表中。如果没有,你可以参考上一章内容加上。
当我们运行 truffle develop 时,我们看到 : Truffle Develop started at http://127.0.0.1:9545/ ,把这个地址复制到剪贴板。
接下来,我们需要打开 MetaMask 并且将这个地址导入。打开 MetaMask extension ,选择 Main Ethereum Network ,选择 Custom RPC 并且将“http://127.0.0.1:9545/” 粘贴到提示输入 “New RPC URL” 的地方。
-metamask-
然后进入 My Accounts > Import Account
再将前面运行 truffle develop 时生成的私钥粘贴到 MetaMask 的 import 中并且选择 Import。
刷新屏幕,你可以看到之前的简单页面现在是这样的:
应用程序可以运行了,现在从App.js文件开始。删除一些示例代码,准备前端与 fundraiser 交互。
先进入到client/src
目录并且打开App.js
文件,我们需要移除旧的 React 代码并且用我们自己的代码替换:
1 import React, { useState, useEffect } from "react";
2 import FactoryContract from "./contracts/Factory.json";
3 import getWeb3 from "./utils/getWeb3";
4 import "./App.css";
5 const App = () => {
6 const [state, setState] =
7 useState({web3: null, accounts: null, contract: null});
8 const [storageValue, setStorageValue] = useState(0);
9 useEffect(() => {
10 const init = async() => {
11 try {
12 const web3 = await getWeb3();
13 const accounts = await web3.eth.getAccounts();
14 const networkId = await web3.eth.net.getId();
15 const deployedNetwork = FactoryContract.networks[networkId];
16 const instance = new web3.eth.Contract(
17 FactoryContract.abi,
18 deployedNetwork && deployedNetwork.address,
19 );
20 setState({web3, accounts, contract: instance});
21 } catch(error) {
22 alert(
23 `Failed to load web3, accounts, or contract.
24 Check console for details.`,
25 )
26 console.error(error);
27 }
28 }
29 init();
30 }, []);
31 const runExample = async () => {
32 const { accounts, contract } = state;
33 };
34 return(
35 <div>
36 <h1>Fundraiser</h1>
37 </div>
38 ); }
39 export default App;
- App.js -
我们还需要在 React 应用程序中设置路由。用react-router-dom
,让用户可以在导航栏中选择不同内容时看到不同的页面。
从安装 npm 包开始:
npm install — save react-router-dom
安装好 npm 包后,重启前端服务,从 App.js
文件中的 react-router-dom 导入必要的文件,如下:
import { BrowserRouter as Router, Route, NavLink } from "react-router-dom"
还需要为主页和新的 fundraiser 导入两个新组件在路由中使用:
import NewFundraiser from './NewFundraiser'
import Home from './Home'
接下来,将渲染函数替换为下面的代码,用 Material UI 提供的导航栏来导航到应用程序的不同页面:
1 <Router>
2 <div>
3 <nav>
4 <ul>
5 <li>
6 <NavLink to="/">Home</NavLink>
7 </li>
8 <li>
9 <NavLink to="/new/">New</NavLink>
10 </li>
11 </ul>
12 </nav>
13
14 <Route path="/" exact component={Home} />
15 <Route path="/new/" component={NewFundraiser} />
16 </div>
17 </Router>
- App.js -
创建两个新文件:Home.js
和 NewFundraiser.js
.
我们将使用主页组件作为应用程序的主登录页面,并使用 New Fundraiser 页面在应用程序中创建一个新的筹款活动:
touch Home.js
touch NewFundraiser.js
让我们开始创建Home
视图,在[Home.js](https://github.com/ac12644/fundraiser_dapp/blob/master/client/src/Home.js)
文件中,使用下面的代码并保存文件:
1 import React, { useState, useEffect } from "react";
2 import FundraiserCard from './FundraiserCard';
3 const Home = () => {
4 useEffect(() => {}, []);
5
6 const displayFundraisers = () => {
7 return funds.map( (fundraiser) => {
8 return (
9 <FundraiserCard fundraiser={fundraiser} key={fundraiser}/>
10 );
11 });
12 }
13
14 return (
15 <div className="main-container">
16 {displayFundraisers()}
17 </div>
18 ) }
19
20 export default Home;
- Home.js -
在NewFundraiser.js
文件中添加下面的代码:
1 import React, { useState, useEffect } from "react";
2 const NewFundraiser = () => {
3 useEffect(() => {}, []);
4 return (
5 <div><h2>Create a New Fundraiser</h2></div>
6 ) }
7 export default NewFundraiser;
- NewFundraiser.js -
我们还需要修改index.js
文件中的代码,才可以正确的渲染路由。
用react-router-dom
的BrowserRouter
在路由中打包应用程序:
1 import React from 'react';
2 import ReactDOM from 'react-dom';
3 import { BrowserRouter } from 'react-router-dom'
4 import App from './App';
5
6 ReactDOM.render((
7 <BrowserRouter>
8 <App />
9 </BrowserRouter>
10 ), document.getElementById('root'))
- index.js -
我们先将 Material UI 安装到应用程序中。
在client
目录中运行 install 命令在 fundraiser 应用中安装 react-bootstrap ,如下:
npm install @material-ui/core --save
从简单的应用栏开始。
首先,将所有 import 添加到App.js
文件的顶部,这样我们就可以使用需要的 Material UI 组件了:
import { makeStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
接下来,添加样式让 fundraiser 应用更好看。
在useEffect
函数之后添加样式:
const useStyles = makeStyles({
root: {
flexGrow: 1,
},
});
const classes = useStyles();
导入正确的文件,添加样式代码后,再替换实际的渲染代码,就可以看到新的导航栏了:
1 <Router>
2 <div>
3 <AppBar position="static" color="default">
4 <Toolbar>
5 <Typography variant="h6" color="inherit">
6 <NavLink className="nav-link" to="/">Home</NavLink>
7 </Typography>
8 <NavLink className="nav-link" to="/new/">New Fundraiser</NavLink>
9 </Toolbar>
10 </AppBar>
11 <Route path="/" exact component={Index} />
12 <Route path="/new/" component={NewFundraiser} />
13 </div>
14 </Router>
- App.js -
我们快速添加一点小样式到NavLink
组件,让它看起来更专业。
在App.css
文件中, 添加下面的代码:
1 body {
2 margin: 0 !important;
3 }
4 .nav-link {
5 color: inherit;
6 text-decoration: none;
7 margin-right: 15px;
8 }
9 .nav-link:hover,
10 .nav-link:active,
11 .nav-link:visited {
12 color: black;
13 text-decoration: none;
14 }
- App.css -
我们从使用 Material UI 的文本字段组件开始。添加一个 import 语句到NewFundraiser.js
文件中最新的 import 语句之后。现在,继续获取 Web3,就可以访问工厂合约[第 42 行]了。
在NewFundraiser.js
文件中,更新useEffect
函数以使用 Web3 代码。下面的代码将创建一个新的合约实例,并设置 Web3 的状态、合约和当前账户。
接下来,我们需要导入合约并指向在NewFundraiser.js
文件中本地部署的合约[第 6-7 行]。
1 import React, { useState, useEffect } from "react";
2 import { makeStyles } from '@material-ui/core/styles';
3 import TextField from '@material-ui/core/TextField';
4 import Button from '@material-ui/core/Button';
5 import detectEthereumProvider from '@metamask/detect-provider';
6 import getWeb3 from './utils/getWeb3';
7 import FactoryContract from './contracts/Factory.json';
8 import Web3 from 'web3'
9
10 const useStyles = makeStyles (theme => ({
11 container: {
12 display: 'flex',
13 flexWrap: 'wrap',
14 },
15 textField: {
16 marginLeft: theme.spacing(1),
17 marginRight: theme.spacing(1),
18 },
19 button: {
20 margin: theme.spacing(1),
21 },
22 }));
23
24 const NewFundraiser = () => {
25 const [labelWidth, setLabelWidth] = React.useState(0);
26 const labelRef = React.useRef(null);
27 const [ web3, setWeb3 ] = useState(null);
28 const classes = useStyles();
29 const [ name, setFundraiserName ] = useState(null);
30 const [ url, setFundraiserWebsite ] = useState(null);
31 const [ description, setFundraiserDescription ] = useState(null);
32 const [ imageURL, setImage ] = useState(null);
33 const [ beneficiary, setAddress ] = useState(null);
34 const [ custodian, setCustodian ] = useState(null);
35 const [ contract, setContract] = useState(null);
36 const [ accounts, setAccounts ] = useState(null);
37
38 useEffect (() => {
39 init();
40 }, []);
41
42 const init = async() => {
43 try {
44 const provider = await detectEthereumProvider();
45 const web3 = new Web3(provider);
46 const networkId = await web3.eth.net.getId();
47 const deployedNetwork = FactoryContract.networks[networkId];
48 const accounts = await web3.eth.getAccounts();
49 const instance = new web3.eth.Contract(
50 FactoryContract.abi, deployedNetwork && deployedNetwork.address,
51 );
52 setWeb3(web3);
53 setContract(instance);
54 setAccounts(accounts);
55 } catch (error) {
56 alert(`Failed to load web3, accounts, or contract. Check console for details.`,);
57 console.error(error);
58 }
59 }
60
61 const handleSubmit = async () => {
62 const provider = await detectEthereumProvider();
63 const web3 = new Web3(provider);
64 const networkId = await web3.eth.net.getId();
65 const deployedNetwork = FactoryContract.networks[networkId];
66 const accounts = await web3.eth.getAccounts();
67 const instance = new web3.eth.Contract(
68 FactoryContract.abi,
69 deployedNetwork && deployedNetwork.address,
70 );
71 await contract.methods.createFundraiser(name, url, imageURL, description, beneficiary).send({ from: accounts[0] });
72 alert('Successfully created fundraiser')
73 };
74
75 return (
76 <div className="main-container">
77 <h2>
78 Create a New Fundraiser
79 </h2>
80 <label>Name</label>
81 <TextField id="outlined-bare" className={classes.textField} placeholder="Fundraiser Name" margin="normal" onChange={ (e) => setFundraiserName(e.target.value) } variant="outlined" inputProps={{ 'aria-label': 'bare' }} />
82 <label>Website</label>
83 <TextField id="outlined-bare" className={classes.textField} placeholder="Fundraiser Website" margin="normal" onChange={ (e) => setFundraiserWebsite(e.target.value) } variant="outlined" inputProps={{ 'aria-label': 'bare' }} />
84 <label>Description</label>
85 <TextField id="outlined-bare" className={classes.textField} placeholder="Fundraiser Description" margin="normal" onChange={ (e) => setFundraiserDescription(e.target.value) } variant="outlined" inputProps={{ 'aria-label': 'bare' }} />
86 <label>Image</label>
87 <TextField id="outlined-bare" className={classes.textField} placeholder="Fundraiser Image" margin="normal" onChange={ (e) => setImage(e.target.value) } variant="outlined" inputProps={{ 'aria-label': 'bare' }} />
88 <label>Address</label>
89 <TextField id="outlined-bare" className={classes.textField} placeholder="Fundraiser Address" margin="normal" onChange={ (e) => setAddress(e.target.value) } variant="outlined" inputProps={{ 'aria-label': 'bare' }} />
90 <label>Custodian</label>
91 <TextField id="outlined-bare" className={classes.textField} placeholder="Fundraiser Custodian" margin="normal" onChange={ (e) => setCustodian(e.target.value) } variant="outlined" inputProps={{ 'aria-label': 'bare' }} />
92
93 <Button onClick={handleSubmit} variant="contained" className={classes.button}>
94 Submit
95 </Button>
96 </div>
97 );
98 }
99 export default NewFundraiser;
- NewFundraiser.js -
现在我们收到了来自合约的数据,接下来让前端向用户显示筹款卡片。
第一件事是创建一个用于显示 Card 组件的新组件。创建一个新文件 FundraiserCard.js:
1 import React, { useEffect } from 'react';
2 import { makeStyles } from '@material-ui/core/styles';
3 import Card from '@material-ui/core/Card';
4 import CardActionArea from '@material-ui/core/CardActionArea';
5 import CardActions from '@material-ui/core/CardActions';
6 import CardContent from '@material-ui/core/CardContent';
7 import CardMedia from '@material-ui/core/CardMedia';
8 import Button from '@material-ui/core/Button';
9 import Typography from '@material-ui/core/Typography';
10 import FundraiserContract from "./contracts/Fundraiser.json";
11 import Web3 from 'web3';
12
13 const useStyles = makeStyles({
14 card: {
15 maxWidth: 450,
16 height: 400
17 },
18 media: {
19 height: 140,
20 }, });
21
22 const FundraiserCard = ({fundraiser}) => {
23 const classes = useStyles();
24 const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'))
25 const [ contract, setContract] = useState(null)
26 const [ accounts, setAccounts ] = useState(null)
27 const [ fundName, setFundname ] = useState(null)
28 const [ description, setDescription ] = useState(null)
29 const [ totalDonations, setTotalDonations ] = useState(null)
30 const [ donationCount, setDonationCount ] = useState(null)
31 const [ imageURL, setImageURL ] = useState(null)
32 const [ url, setURL ] = useState(null)
33 useEffect(() => {
34 // we'll add in the Web3 call here
35 }, []);
36 return (
37 <div className="fundraiser-card-content">
38 <Card className={classes.card}>
39 <CardActionArea>
40 <CardMedia
41 className={classes.media}
42 image={props.fundraiser.image}
43 title="Fundraiser Image"
44 />
45 <CardContent>
46 <Typography gutterBottom variant="h5" component="h2">
47 {fundName}
48 </Typography>
49 <Typography variant="body2" color="textSecondary" component="p">
50 <p>{description}</p>
51 </Typography>
52 </CardContent>
53 </CardActionArea>
54 <CardActions>
55 <Button size="small" color="primary">
56 View More
57 </Button>
58 </CardActions>
59 </Card>
60 </div>
61 ) }
62 export default FundraiserCard;
- FundraiserCard.js -
如果你打开 localhost:3000 ,会发现它现在是空白的。我们需要创建一个渲染函数来遍历筹款活动并将它们显示在卡片上。
现在,切换到Home.js
文件,遍历筹款活动列表并用 Card 组件分别显示每个筹款活动:
const displayFundraisers = () => {
return funds.map((fundraiser) => {
return (
<FundraiserCard fundraiser={fundraiser} />
) })
}
- fundraiser -
接下来,是向用户显示筹款活动的更多信息。复制下面的代码到FundraiserCard.js
:
1 import React, { useState, useEffect } from "react";
2 import { makeStyles } from '@material-ui/core/styles';
3 import Card from '@material-ui/core/Card';
4 import CardActionArea from '@material-ui/core/CardActionArea';
5 import CardActions from '@material-ui/core/CardActions';
6 import CardContent from '@material-ui/core/CardContent';
7 import CardMedia from '@material-ui/core/CardMedia';
8 import Typography from '@material-ui/core/Typography';
9 import Web3 from 'web3';
10 import FundraiserContract from './contracts/Fundraiser.json';
11 import detectEthereumProvider from '@metamask/detect-provider';
12 import Button from '@material-ui/core/Button';
13
14 import Dialog from '@material-ui/core/Dialog';
15 import DialogActions from '@material-ui/core/DialogActions';
16 import DialogContent from '@material-ui/core/DialogContent';
17 import DialogContentText from '@material-ui/core/DialogContentText';
18 import DialogTitle from '@material-ui/core/DialogTitle';
19
20 import FilledInput from '@material-ui/core/FilledInput';
21 import FormControl from '@material-ui/core/FormControl';
22 import FormHelperText from '@material-ui/core/FormHelperText';
23 import Input from '@material-ui/core/Input';
24 import InputLabel from '@material-ui/core/InputLabel';
25 import OutlinedInput from '@material-ui/core/OutlinedInput';
26 import { Link } from 'react-router-dom';
27
28 const cc = require('cryptocompare');
29
30 const useStyles = makeStyles (theme => ({
31 card: {
32 maxWidth: 450,
33 height: 400
34 },
35 media: {
36 height: 140,
37 },
38 button: {
39 margin: theme.spacing(1),
40 },
41 input: {
42 display: 'none',
43 },
44 container: {
45 display: 'flex',
46 flexWrap: 'wrap',
47 },
48 formControl: {
49 margin: theme.spacing(1),
50 display: 'table-cell'
51 },
52 paper: {
53 position: 'absolute',
54 width: 400,
55 backgroundColor: theme.palette.background.paper,
56 boxShadow: 'none',
57 padding: 4,
58 },
59 }));
60
61 const FundraiserCard = ({fundraiser}) => {
62 const classes = useStyles();
63 const [ web3, setWeb3 ] = useState(null);
64 const [ url, setURL ] = useState(null);
65 const [ description, setDescription ] = useState(null);
66 const [ imageURL, setImageURL ] = useState(null);
67 const [ fundName, setFundName ] = useState(null);
68 const [ totalDonations, setTotalDonations ] = useState(null);
69 const [ donationCount, setDonationCount ] = useState(null);
70 const [ contract, setContract] = useState(null);
71 const [ accounts, setAccounts ] = useState(null);
72 const [ open, setOpen ] = useState(false);
73 const [ donationAmount, setDonationAmount ] = useState(null);
74 const [ exchangeRate, setExchangeRate ] = useState(null);
75 const [ userDonations, setUserDonations ] = useState(null);
76 const [ isOwner, setIsOwner ] = useState(false);
77 const [ newBeneficiary, setNewBeneficiary ] = useState(null);
78
79 const ethAmount = (donationAmount / exchangeRate || 0).toFixed(4);
80
81 useEffect (() => {
82 if (fundraiser) {
83 init (fundraiser);
84 }
85 }, [fundraiser]);
86
87 const init = async (fundraiser) => {
88 try {
89 const fund = fundraiser;
90 const provider = await detectEthereumProvider();
91 const web3 = new Web3(provider);
92 const networkId = await web3.eth.net.getId();
93 const deployedNetwork = FundraiserContract.networks[networkId];
94 const accounts = await web3.eth.getAccounts();
95 const instance = new web3.eth.Contract(FundraiserContract.abi, fund);
96 setWeb3 (web3);
97 setContract (instance);
98 setAccounts (accounts);
99
100 const name = await instance.methods.name().call();
101 const description = await instance.methods.description().call();
102 const totalDonations = await instance.methods.totalDonations().call();
103 const imageURL = await instance.methods.imageURL().call();
104 const url = await instance.methods.url().call();
105 setFundName(name);
106 setDescription(description);
107 setImageURL(imageURL);
108 setURL(url);
109
110 var exchangeRate = 0;
111 await cc.price('ETH', ['USD']).then(prices => {
112 exchangeRate = prices.USD;
113 setExchangeRate(prices.USD);
114 }).catch(console.error);
115
116 const eth = web3.utils.fromWei(totalDonations, 'ether');
117 const dollarDonationAmount = exchangeRate * eth;
118 setTotalDonations(dollarDonationAmount.toFixed(2));
119 const userDonations = instance.methods.myDonations().call({ from: accounts[0] });
120 console.log(userDonations);
121 setUserDonations(userDonations);
122 const isUser = accounts[0];
123 const isOwner = await instance.methods.owner().call();
124 if (isOwner === accounts[0]) {
125 setIsOwner(true);
126 }
127 } catch (error) {
128 alert(`Failed to load web3, accounts, or contract. Check console for details.`,);
129 console.error(error);
130 }
131 }
132
133 window.ethereum.on('accountsChanged', function (accounts) {
134 window.location.reload()
135 })
136
137 const handleOpen = () => {
138 setOpen(true);
139 };
140
141 const handleClose = () => {
142 setOpen(false);
143 };
144
145 const submitFunds = async() => {
146 const ethTotal = donationAmount / exchangeRate;
147 const donation = web3.utils.toWei(ethTotal.toString());
148 await contract.methods.donate().send({
149 from: accounts[0],
150 value: donation,
151 gas: 650000
152 });
153 setOpen(false);
154 }
155
156 const withdrawalFunds = async () => {
157 await contract.methods.withdraw().send({ from: accounts[0] });
158 alert('Funds Withdrawn!');
159 setOpen(false);
160 }
161
162 const setBeneficiary = async() => {
163 await contract.methods.setBeneficiary(newBeneficiary).send({ from: accounts[0] });
164 alert(`Fundraiser Beneficiary Changed`);
165 setOpen(false);
166 }
167
168 const renderDonationsList = () => {
169 var donations = userDonations;
170 if (donations === null) {
171 return null;
172 };
173
174 const totalDonations = donations.length;
175 let donationList = [];
176 var i;
177 for (i = 0; i < totalDonations; i++) {
178 const ethAmount = web3.utils.fromWei(donations.values[i], 'ether');
179 const userDonation = exchangeRate * ethAmount;
180 const donationDate = donations.dates[i];
181 donationList.push({ donationAmount: userDonation.toFixed(2), date: donationDate });
182 }
183
184 return donationList.map((donation) => {
185 return (
186 <div className="donation-list">
187 <p>
188 ${donation.donationAmount}
189 </p>
190 <Button variant="contained" color="primary">
191 <Link className="donation-receipt-link" to={{ pathname: '/receipts', state: { fund: fundName, donation: donation.donationAmount, date: donation.date } }}>
192 Request Receipt
193 </Link>
194 </Button>
195 </div>
196 );
197 });
198 }
199
200 return (
201 <div className="fundraiser-card-content">
202 <Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
203 <DialogTitle id="form-dialog-title">
204 Donate to {fundName}
205 </DialogTitle>
206 <DialogContent>
207 <DialogContentText>
208 <img src={imageURL} width='200px' height='130px' />
209 <p>
210 {description}
211 </p>
212 <FormControl className={classes.formControl}>
213 $<Input id="component-simple" value={donationAmount} onChange={ (e) => setDonationAmount(e.target.value)} placeholder="0.00" />
214 </FormControl>
215 <p>ETH: {ethAmount}</p>
216 <Button onClick={submitFunds} variant="contained" color="primary">
217 Donate
218 </Button>
219 <div>
220 <h3>
221 My donations
222 </h3>
223 {renderDonationsList()}
224 </div>
225 { isOwner &&
226 <div>
227 <FormControl className={classes.formControl}>
228 Beneficiary:
229 <Input value={newBeneficiary} onChange={ (e) => setNewBeneficiary(e.target.value) } placeholder="Set Beneficiary" />
230 </FormControl>
231 <Button variant="contained" style={{ marginTop: 20 }} color="primary" onClick={setBeneficiary} >
232 Set Beneficiary
233 </Button>
234 </div>
235 }
236 </DialogContentText>
237 </DialogContent>
238 <DialogActions>
239 <Button onClick={handleClose} color="primary">
240 Cancel
241 </Button>
242 { isOwner &&
243 <Button variant="contained" color="primary" onClick={withdrawalFunds}>
244 Withdrawal
245 </Button>
246 }
247 </DialogActions>
248 </Dialog>
249 <Card className={classes.card} onClick={handleOpen}>
250 <CardActionArea>
251 {imageURL ? (
252 <CardMedia className={classes.media} image={imageURL} title="Fundraiser Image"/>
253 ) : (<></>)
254 };
255 <CardContent>
256 <Typography gutterBottom variant="h5" component="h2">
257 {fundName}
258 </Typography>
259 <Typography variant="body2" color="textSecondary" component="div">
260 <p>
261 {description}
262 </p>
263 <p>
264 Total Donations: ${totalDonations}
265 </p>
266 </Typography>
267 </CardContent>
268 </CardActionArea>
269 <CardActions>
270 <Button onClick={handleOpen} variant="contained" className={classes.button}>
271 View More
272 </Button>
273 </CardActions>
274 </Card>
275 </div>
276 );
277 }
278
279 export default FundraiserCard;
- FundraiserCard.js -
App.js
代码如下:
1 import React, { useState, useEffect } from "react";
2 import { BrowserRouter as Router, Route, NavLink } from "react-router-dom"
3 import NewFundraiser from './NewFundraiser'
4 import Home from './Home'
5 import FactoryContract from "./contracts/Factory.json";
6 import getWeb3 from "./utils/getWeb3";
7 import "./App.css";
8 import { makeStyles } from '@material-ui/core/styles';
9 import AppBar from '@material-ui/core/AppBar';
10 import Toolbar from '@material-ui/core/Toolbar';
11 import Typography from '@material-ui/core/Typography';
12
13 const App = () => {
14 const useStyles = makeStyles({
15 root: {
16 flexGrow: 1,
17 },
18 });
19 const classes = useStyles();
20
21 const [state, setState] =
22 useState({web3: null, accounts: null, contract: null});
23 const [storageValue, setStorageValue] = useState(0);
24
25 useEffect(() => {
26 const init = async() => {
27 try {
28 const web3 = await getWeb3();
29 const accounts = await web3.eth.getAccounts();
30 const networkId = await web3.eth.net.getId();
31 const deployedNetwork = FactoryContract.networks[networkId];
32 const instance = new web3.eth.Contract(
33 FactoryContract.abi,
34 deployedNetwork && deployedNetwork.address,
35 );
36 setState({web3, accounts, contract: instance});
37 } catch(error) {
38 alert(
39 `Failed to load web3, accounts, or contract.
40 Check console for details.`,
41 )
42 console.error(error);
43 }
44 }
45 init();
46 }, []);
47
48 const runExample = async () => {
49 const { accounts, contract } = state;
50 };
51
52 return(
53 <Router>
54 <div className={classes.root}>
55 <AppBar position="static" color="default">
56 <Toolbar>
57 <Typography variant="h6" color="inherit">
58 <NavLink className={classes.navLink} to="/">Home</NavLink>
59 <NavLink className={classes.navLink} to="/new">New</NavLink>
60 </Typography>
61 </Toolbar>
62 </AppBar>
63 <Route path="/" exact component={Home} />
64 <Route path="/new/" component={NewFundraiser} />
65 <Route path="/receipts" component={Receipts} />
66 </div>
67 </Router>
68 ); }
69
70 export default App;
- App.js -
Receipts.js
的代码:
1 import React, { useState, useEffect } from "react";
2
3 const Receipts = (props) => {
4 const [ donation, setDonation ] = useState(null);
5 const [ fundName, setFundName ] = useState(null);
6 const [ date, setDate ] = useState(null);
7
8 useEffect(() => {
9 const { donation, date, fund } = props.location.state;
10 const formattedDate = new Date(parseInt(date * 1000));
11 setDonation(donation);
12 setDate(formattedDate.toString());
13 setFundName(fund);
14 }, []);
15
16 return (
17 <div className="receipt-container">
18 <div className="receipt-header">
19 <h3>
20 Thank you for your donation to {fundName}
21 </h3>
22 </div>
23 <div className="receipt-info">
24 <div>
25 Date of Donation: {date}
26 </div>
27 <div>
28 Donation Value: ${donation}
29 </div>
30 </div>
31 </div>
32 );
33 }
34
35 export default Receipts;
- Receipts.js -
- receipt -
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!