用Truffle, Solidity, React, Material UI, Web3 创建一个全栈筹款Dapp(Fundraiser Dapp)

用Truffle, Solidity, React, Material UI, Web3 创建一个全栈筹款Dapp(Fundraiser Dapp)

1.jpeg

图片来源: 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,我们的目录结构现在应该是这样的:

2.png

移除 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

上面的命令运行完后,在同一个终端窗口继续运行compilemigrate

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

安装 Fundraiser

让 React Truffle Box 运行起来,我们就可以与应用程序交互了。

首先需要cd进入 React 的src文件 ,并安装 React Truffle Box 提供的所有依赖。

之后,像我们在前一章节那样启动应用程序的前端:

cd client
npm i

模块安装完成后,启动服务:

npm start

如果全部正确安装并且成功启动,我们现在就可以打开 localhost:3000 ,看到我们的web3应用程序。

3.png

哎呀!我们忘记切换到匹配新应用程序的网络了,要在同一个网络上应用才能正常运行。

首先,让我们进入 MetaMask 并且将网络切换到 Localhost 8545。

Localhost 8545 应该在默认列表中。如果没有,你可以参考上一章内容加上。

当我们运行 truffle develop 时,我们看到 : Truffle Develop started at http://127.0.0.1:9545/ ,把这个地址复制到剪贴板。

4.png

接下来,我们需要打开 MetaMask 并且将这个地址导入。打开 MetaMask extension ,选择 Main Ethereum Network ,选择 Custom RPC 并且将“http://127.0.0.1:9545/” 粘贴到提示输入 “New RPC URL” 的地方。

5.png

-metamask-

然后进入 My Accounts > Import Account

6.png

再将前面运行 truffle develop 时生成的私钥粘贴到 MetaMask 的 import 中并且选择 Import。

7.png

刷新屏幕,你可以看到之前的简单页面现在是这样的:

8.png

应用程序可以运行了,现在从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 应用程序中设置路由。用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.jsNewFundraiser.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-domBrowserRouter在路由中打包应用程序:

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 -

React 和 Material UI

我们先将 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 -

创建 New Fundraiser 页面视图

我们从使用 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} />
) })
}

9.png

- 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 -

10.png - receipt -

参考资料

原文链接:https://betterprogramming.pub/create-a-full-stack-fundraiser-dapp-using-truffle-solidity-react-material-ui-and-web3-222638147c7a

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2022-03-23 17:31
  • 阅读 ( 1093 )
  • 学分 ( 49 )
  • 分类:DApp

0 条评论

请先 登录 后评论
aisiji
aisiji

14 篇文章, 1221 学分