目录

区块链入门:在本地网络开发自己的加密数字货币(Token)-傻瓜币(FoolCoin)

注意:该项目仅供学习区块链知识,不作为任何投资建议。市场有风险,投资需谨慎。

本文项目代码:

https://github.com/hicoldcat/eth-solidity-token-example

原文地址:

https://hicoldcat.com/posts/blockchain/my-token/

1、初始化项目

Hardhat是一个编译、部署、测试和调试以太坊应用的开发环境。它可以帮助开发人员管理和自动化构建智能合约和DApps过程中固有的重复性任务,并围绕这一工作流程轻松引入更多功能。这意味着hardhat最核心的地方是编译、运行和测试智能合约。

创建npm项目eth-solidity-token-example,进入项目文件夹,安装hardhat

1
2
3
npm init

npm install --save-dev hardhat

创建Hardhat项目

1
npx hardhat

https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/20220429160229.png

2、开发合约

Solidity是一门面向合约的、为实现智能合约而创建的高级编程语言。

contracts 目录下有Greeter.sol文件,这是hardhat提供的一个demo文件,实现了简单的打招呼功能。.sol文件是Solidity文件的后缀。Solidity 是在Ethereum 上开发智能合约的一门编程语言,具体语法可以参考上面的官方文档。

在contracts目录下,创建我们自己的代币合约文件Fool.sol(傻瓜币),代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/// @custom:security-contact hicoldcat@foxmail.com
contract Fool is ERC20, ERC20Burnable, Pausable, Ownable {
    constructor() ERC20("Fool", "FOOL") {
        _mint(msg.sender, 100000000 * 10 ** decimals());
    }

    function pause() public onlyOwner {
        _pause();
    }

    function unpause() public onlyOwner {
        _unpause();
    }

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    function _beforeTokenTransfer(address from, address to, uint256 amount)
        internal
        whenNotPaused
        override
    {
        super._beforeTokenTransfer(from, to, amount);
    }
}

首先,需要说一下代码中is之后继承的几个接口和合约标准,

ERC20

该智能合约继承了ERC20标准。ERC20是以太坊同质化代币的标准,由V神2015年提出。ERCEthereum Request for Comment 的缩写,因为是从EIPs20号提案通过的,因此称为ERC20。其他标准如ERC721(非同质化代币),就是常说的NFT

代码中的ERC20是由@openzeppelin/contracts包提供的IERC20的实现。主要实现了如返回代币总数totalSupply(),返回特定账户余额balanceOf(account),转账到指定账户transfer(to, amount),允许某个账户代持主账户代币的剩余数量allowance(owner, spender),委托特定账户一定数量的代币approve(spender, amount),从账户转账到另一个账户代币transferFrom(from, to, amount)等方法,和一些如代币名称、简写等一些属性的方法。详细可以去看官方ERC20具体实现。

ERC20Burnable

实现允许代币持有者可以以区块链允许的方式来销毁他们自己的代币或者他们被委托的代币的方法。

代码主要包括了burn(uint256 amount)销毁当前调用者指定数量的代币,和burnFrom(address account, uint256 amount)销毁指定账户指定数量的代币。可参考代码@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol

Pausable

使代币具有可以暂停转移、暂停铸造和暂停燃烧的功能。这个功能一般会对于出现一些重大错误或者阻止一些错误交易时来冻结所有代币等场景下有很重要的作用。

所以调用代码pause()unpause()会实现暂停和停止暂停交易的功能。

钩子函数_beforeTokenTransfer(address from, address to, uint256 amount)会在未被暂停交易,并且交易之前调用,包括铸造和燃烧。

Ownable

智能合约最基本的访问控制机制,只有账户所有者能够对特定功能有访问权限。账户所有者指的是部署合约的账户。如代码中具有修饰符onlyOwnerpause()unpause(),就是只能由账户所有者调用。

至此,一个基本的代币合约就完成了。需要注意的是,上面代码中,一些基本的如获取Token名称的方法name(),交易转账的方法transfer(address to, uint256 amount)等,都封装到了import "@openzeppelin/contracts/token/ERC20/ERC20.sol";中,详细代码如下,也可以通过github源码https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol去查看。

3、编译合约

在项目根目录下运行如下命令:

1
 npx hardhat compile

hardhat会查找项目下所有的智能合约,并根据hardhat.config.js配置文件生成编译完成之后的artifacts文件目录。

https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/20220502154746.png

4、部署合约

1、首先,创建部署脚本。在项目scripts目录下,创建deploy.js部署脚本,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const hre = require("hardhat");

async function main() {
    const [owner] = await hre.ethers.getSigners();

    console.log(`部署合约的账户地址为:`, owner.address);
    console.log("账户余额为:", (await owner.getBalance()).toString());
    console.log("合约部署的链ID为:", (await owner.getChainId()).toString());

    // 获取Fool智能合约
    const Fool = await hre.ethers.getContractFactory("Fool");
    const fool = await Fool.deploy();

    // 部署合约
    await fool.deployed();

    console.log("当前合约部署地址为:", fool.address);
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

2、启动本地节点,运行localhost测试网络,

1
 npx hardhat node

https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/20220502161938.png

注意:这些账户地址和私钥都是公开在网络中的,千万不要在主网上向这些地址转币,否则会丢失掉!!!

3、在另一个终端中,使用localhost测试网络部署智能合约。

1
npx hardhat run --network localhost scripts/deploy.js

https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/20220502162649.png

可以看到,合约已经部署到了localhost测试网络上,合约持有人,账户余额,合约部署的链ID,合约地址如下:

1
2
3
4
部署合约的账户地址为: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
账户余额为: 10000000000000000000000
合约部署的链ID为: 31337
当前合约部署地址为: 0x5FbDB2315678afecb367f032d93F642f64180aa3

此时,我们之前运行的本地节点上,也收到了部署的信息,如下

https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/20220502162917.png

其中,值得注意的是如下代码:

1
2
3
4
5
6
7
8
eth_sendTransaction
  Contract deployment: Fool
  Contract address:    0x5fbdb2315678afecb367f032d93f642f64180aa3
  Transaction:         0x38ea936e49e59db88cd0e83b25eb78a7f0485aeb21178a36f0838ce7c892037a
  From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
  Value:               0 ETH
  Gas used:            1999705 of 1999705
  Block #1:            0xb12312912bdbb42b8d933767e041ead1ef6d50685b6c6ca858d89401dcd31582

上面显示的地址和我们打印出来的地址是一致的。此外,0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266是我们部署本地节点时,获取到的Account 0作为我们当前默认的账户。

5、搭建本地测试页面

ethers.js是一个专门为以太坊生态提供的JavaScript工具类库,内置了一些钱包和合约的调用方法和其他一些工具函数。

为了方便测试,我们搭建了react项目来实现基本的测试功能。

在新的终端中运行下面命令在项目根目录下生成web文件夹

1
npx create-react-app web --template typescript

进入web目录,在web目录下安装ethersantdnpm包,然后增加.env环境变量文件,内容如下:

1
2
3
4
5
REACT_APP_CONTARCT_ADDRESS = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
REACT_APP_DEPLOYER = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
REACT_APP_DEPLOYER_PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
REACT_APP_RECIVER = "0x70997970c51812dc3a010c7d01b50e0d17dc79c8"
REACT_APP_RECIVER_PRIVATE_KEY = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"

上面包含了之前获取到的合约地址、部署合约的账户地址和私钥、模拟要接收转账的账户地址和私钥。

当前目录下运行项目npm run start,将会自动在浏览器中打开http://localhost:3000页面。

修改App.tsx为如下内容:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
import { useEffect, useState } from 'react';
import { ethers } from 'ethers';
import ReactJson from 'react-json-view'
import { Button, Card, Col, Input, InputNumber, Layout, Modal, Row, Spin } from 'antd';

import FoolToken from './artifacts/Fool.json';

import './App.css';

const { Content } = Layout;

function App() {
  const {
    REACT_APP_CONTARCT_ADDRESS,
    REACT_APP_DEPLOYER,
    REACT_APP_DEPLOYER_PRIVATE_KEY,
    REACT_APP_RECIVER,
    REACT_APP_RECIVER_PRIVATE_KEY
  } = process.env

  const [loading, setLoading] = useState(false);
  const [modal, setModal] = useState<any>({});

  const [name, setName] = useState('');
  const [symbol, setSymbol] = useState('');
  const [decimals, setDecimals] = useState('');

  const [supply, setSupply] = useState('');
  const [ownerBlance, setOwnerBlance] = useState('');
  const [ownerWalletBlance, setOwnerWalletBlance] = useState('');

  const [reciverBlance, setReciverBlance] = useState('');

  const [number, setNumber] = useState(0);
  const [address, setAddress] = useState(REACT_APP_RECIVER)

  const [txs, setTxs] = useState<any>({})

  let provider = new ethers.providers.JsonRpcProvider("http://localhost:8545")
  let wallet = new ethers.Wallet(REACT_APP_DEPLOYER_PRIVATE_KEY!, provider);
  let contract = new ethers.Contract(REACT_APP_CONTARCT_ADDRESS!, FoolToken.abi, provider) // Read-Only
  // let contractRW = new ethers.Contract(REACT_APP_CONTARCT_ADDRESS!, FoolToken.abi, wallet) // Read-Write
  let contractWithSigner = contract.connect(wallet);

  // 合约名称
  async function getName() {
    setName(await contractWithSigner.name());
  }

  // 合约符号
  async function getSymbol() {
    setSymbol(await contractWithSigner.symbol());
  }

  // Decimals
  async function getDecimals() {
    setDecimals(await contractWithSigner.decimals());
  }

  // 合约总供应量
  async function getSupply() {
    setSupply(await contractWithSigner.totalSupply().then((balance: any) => ethers.utils.formatEther(balance)))
  }

  // owner 余额
  async function getOwnerBalance() {
    setOwnerBlance(await contractWithSigner.balanceOf(REACT_APP_DEPLOYER).then((balance: any) => ethers.utils.formatEther(balance)))
  }

  // owner eth 余额
  async function getOwnerWalletBalance() {
    setOwnerWalletBlance(await wallet.getBalance().then((balance: any) => ethers.utils.formatEther(balance)))
  }

  // reciver 余额
  async function getReciverBalance() {
    setReciverBlance(await contractWithSigner.balanceOf(REACT_APP_RECIVER).then((balance: any) => ethers.utils.formatEther(balance)))
  }

  // 查询数据
  const refetch = () => {
    getName()
    getSymbol()
    getDecimals()

    getSupply()
    getOwnerBalance()
    getReciverBalance()

    getOwnerWalletBalance()
  }

  // 发币
  const sendToken = async () => {
    setLoading(true);
    let numberOfTokens = ethers.utils.parseUnits(number.toString(), 18);
    console.log(`numberOfTokens: ${numberOfTokens}`);
    const transaction = await contractWithSigner.transfer(address, numberOfTokens);
    await transaction.wait();
    console.log(`${number} Tokens successfully sent to ${address}`);
    setLoading(false)
    refetch()
  }

  // 交易信息callback
  const onContractTransfer = (from: any, to: any, value: any, event: any) => {
    if (!txs[event.transactionHash]) {
      console.log(event.transactionHash, ethers.utils.formatUnits(value), new Date().getTime());
      setTxs({
        ...txs, [event.transactionHash]: {
          from,
          to,
          value: ethers.utils.formatUnits(value),
          transactionHash: event.transactionHash
        }
      })
    }
  }

  // 点击交易hash显示交易详情
  const showTxDetail = async ({ name, value }: any) => {
    if (name === "transactionHash") {
      const tx = await provider.getTransaction(value)
      setModal({
        hash: value,
        data: tx
      })
    }
  }

  useEffect(() => {
    refetch()
    contractWithSigner.on('Transfer', onContractTransfer)
    return () => {
      contractWithSigner.removeListener('Transfer', () => {
        console.log('contractWithSigner removeListener Transfer ');
      })
    }
  }, [])

  return (
    <Spin spinning={loading}>
      <Layout className='app'>
        <Content>
          <div className='header'>
            <h1>{`傻瓜币(Name:${name} Symbol:${symbol} Decimals:${decimals})`}</h1>
            合约地址:
            <span className='address'>{REACT_APP_CONTARCT_ADDRESS}</span>
            合约供应量:
            <span className='address'>{supply}</span>
            <div className='notic'>注意:账户地址和私钥均属于网络公开测试,千万不要用于私人转账使用!!!</div>
          </div>
          <Row className='content'>

            {/* Owner */}
            <Col span={12} className="left">
              <Card title='创建者'>
                <div>
                  创建者账户:
                  <span className='address'>{address}</span>
                </div>
                <div>
                  创建者私钥:
                  <span className='address'>{REACT_APP_DEPLOYER_PRIVATE_KEY}</span>
                </div>
                <div>
                  ETH余额:
                  <span className='address'>{ownerWalletBlance}</span>
                </div>
                <div>
                  Fool币余额:
                  <span className='address'>{ownerBlance}</span>
                </div>


                <div className='operate'>
                  <div style={{ marginTop: 16 }}>
                    <Input prefix="收款地址:" value={REACT_APP_RECIVER} readOnly className="input" />
                  </div>
                  <div style={{ marginTop: 16 }}>
                    <InputNumber prefix="转账数量:" value={number} min={0} className="input" onChange={v => setNumber(v)} />
                  </div>

                  <Button style={{ marginTop: 16 }} onClick={sendToken}>发币</Button>
                </div>
              </Card>

            </Col>

            {/* Reciver */}
            <Col span={12} className="right">
              <Card title='接收者'>
                <div>
                  接收者账户:
                  <span className='address'>{REACT_APP_RECIVER}</span>
                </div>
                <div>
                  接收者私钥:
                  <span className='address'>{REACT_APP_RECIVER_PRIVATE_KEY}</span>
                </div>
                <div>
                  Fool余额:
                  <span className='address'>{reciverBlance}</span>
                </div>
              </Card>

            </Col>
          </Row>

          <Card className='tx' title="交易信息" style={{ overflowY: "auto" }}>
            <ReactJson displayDataTypes={false} name={false} src={Object.values(txs)} onSelect={showTxDetail} />
          </Card>

          <Modal
            centered
            destroyOnClose
            width={1000}
            visible={modal && modal.hash}
            title={`交易信息:${modal?.hash}`}
            onOk={() => setModal({})}
            onCancel={() => setModal({})}
          >
            <ReactJson
              style={{ height: 800, overflowY: 'auto' }}
              theme="chalk"
              src={modal && modal.data}
              collapseStringsAfterLength={100}
              onSelect={showTxDetail}
              name={false}
              displayDataTypes={false} />
          </Modal>
        </Content>
      </Layout>
    </Spin>
  );
}

export default App;

页面效果如下:

https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/20220503192911.png

合约创建者信息卡片会显示账户地址和私钥,以及当前账户剩余的以太币余额(注意:这个余额是本地网络启动时的默认余额,上文打印中也显示有1000ETH,交易过程中会消耗一定的gas,所以余额是1000ETH - 累计gas消耗的ETH),还有当前智能合约我们部署的Fool币数量。

此外,还有我们要转账的收款账户地址和转账Fool币数量。

接收者信息卡片展示的是接收者的账户地址和私钥。其实接收者私钥完全不需要展示出来,因为我们向接收者转账不需要知道接收者私钥,但这里为了直观,还是展示出来了。

点击发币按钮,就会发送转账交易。

https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/demo.gif

下方会监听交易信息,并打印出来交易数据。点击transactionHash会显示该笔交易的详细信息弹窗如下

https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/20220503193814.png

同时,在我们运行的本地网络节点中,也会收到查询和交易的相关信息:

https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/20220503194119.png

https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/20220503194159.png

6、总结

注意:每次启动项目需要重新部署合约,并且需要更新一下web目录下面的.env下面的合约地址!如果修改了合约内容,需要重新编译合约,并且将编译的文件夹/artifacts/contracts/Fool.sol/Fool.json文件拷贝到/web/src/artifacts/Fool.json!

本次Example简单演示了如何部署一个自己的代币合约到本地网络,并通过开发基于react的Dapp页面,实现代币的发送和交易信息查看。

区块链基础知识推荐北大肖臻教授的《区块链技术与应用》

此外,也可以在github上查找更多相关的Repository ,比如https://github.com/frankiefab100/Blockchain-Development-Resources

之后,如果有需求,我也会再写一些NFT相关的DEMO,和更复杂的合约。

https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/my.png