代码地址
以太坊客户端
EVM
智能合约的运行环境,是一个虚拟机
以太坊客户端
- 定义:也就是 EVM 的载体,也就是区块链网络中的节点的程序,只要符合网络规范,任何语言都可以实现客户端
- 常见的客户端:Geth(Go 实现)、OpenEthereum(Rust 实现),通过 RPC 提供服务
账户
本质都是一个 20 个字节表示的地址
外部账户(EOA):由私钥控制(比如用户的地址)
合约账户:代码控制(比如合约代码的部署地址)
注意:交易只能从外部账号发出,合约只能被动执行。合约之间的交互称为消息,所有的 gas 都由外部账号支付
Gas
GAS 是一个工作量单位,复杂度越大,所需 gas 越多。费用=gas 数量*gas 单价(以太币计价 gwei)
单位
- 最小单位:Wei
- 10^9 Wei = 1 Gwei
- 10^12 Wei = 1 Szabo
- 10^15 Wei = 1 Finney
- 10^18 Wei = 1 Ether
网络
合约的编译、部署、测试
1. Remix
在contracts里添加Counter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Counter{
uint public counter;
constructor(){
counter = 0;
}
function count() public{
counter+=1;
}
}
编辑器左侧面板有文件夹区域、搜索区域、编译区域、部署区域。
编译区域可选编译器版本。
部署区域可选部署链,成功后会显示部署地址。部署合约也是一笔交易,需要在钱包确认,也需要gas费。部署成功后会显示交互面板,橙色是触发交易,蓝色仅读取。点击橙色触发交易,需要钱包确认,需要gas费。点击蓝色读就不需要。
实际操作:environment 选择 injected web3,这里连接 metamusk 钱包,钱包啥网络,就是啥网络。推荐用 goerli(eth 测试网),去goerli faucet申请一点测试币即可。部署前会连接钱包,然后就是用账号部署上去,部署完后可以去测试链查状态。
部署成功后,点两次橙色触发交易,再点蓝色可以读到此时counter值的状态,控制台打印:
{
"0": "uint256: 2"
}
注意 remix 中的 environment 如果是默认的,那就是虚拟网络,在区块链浏览器是查不到的,而且不需要钱包验证,也不需要gas费
2. Truffle
Truffle:编译、部署、测试合约的一整套开发工具
ganache是开发区块链,提供本地模拟的链上环境
Truffle 安装 npm install -g truffle
创建工程 truffle init或者truffle unbox metacoin(我用的第二个,相当于使用metacoin这个模版,注意需要手动mkdir一个folder,再在里面执行创建,注意配置proxy)
truffle工程包含
- contracts:智能合约目录
- migrations:迁移文件,用来指示如何部署智能合约
- test:智能合约测试用例文件夹
- truffle-config.js:配置文件,配置truffle连接的网络及编译选项
- build:编译结果目录
改造下folder
删除contracts、migrations、test下的文件
contracts里添加Counter.sol
solidity// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Counter{ uint public counter; constructor(){ counter = 0; } function count() public{ counter+=1; } }
合约编译
//使用命令
truffle compile
在那之前需要配置truffle-config.js,不配置就是默认的0.5.16版本
module.exports={
compilers:{
solc:{
version:"0.8.9"
}
}
}
编译完成会输出在build目录,同时也会在命令行输出编译使用的编译器
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
编译结果是json(与合约一一对应),里面abi是和前端交互的,bytecode就是最后部署在链上的东西
接下来是合约的部署
//使用命令
truffle migrate
truffle migrate --network networkname //可以在后面加network参数和network名字,部署到指定network
在那之前,需要先写好配置文件
编写部署脚本
migrations里添加1_counter.js
jsconst Counter = artifacts.require("Counter"); module.exports = function (deployer) { deployer.deploy(Counter); };
部署到本地节点(我没有搞)
先启动一个服务模拟链上环境
ganache-cli -p 7545
(ganache是开发区块链,提供本地模拟的链上环境)然后添加dev的network
js//truffle-config.js development网络配置 module.exports={ networks:{ development:{ host:"127.0.0.1", port:7545, network_id:"*" } } }
truffle migrate — network development
部署,本地部署,不需要验证和gas费
部署到链上
需要部署到链上的节点,所以先去infura.io创建一个project,然后copy project id(选择endpoints是goerli)
当前目录新建两个隐藏文件,.api_key和.mnemonic,.gitignore要添加这两项,分别存储上一步的id和你的钱包账户助记词(主要为了防止外泄)
初始化npm ,添加truffle-hdwallet-provider包
shellnpm init npm i truffle-hdwallet-provider -s .gitignore 添加node_modules
config.js里的network里添加新的network goerli
jsconst HDWalletProvider = require("truffle-hdwallet-provider"); const fs = require('fs'); const api_key = fs.readFileSync('.api_key').toString().trim(); const mnemonic = fs.readFileSync('.mnemonic').toString().trim(); module.exports = { networks: { goerli: { provider: () => { return new HDWalletProvider(mnemonic, 'https://goerli.infura.io/v3/' + api_key) }, network_id: '5', gas: 4465030, gasPrice: 10000000000, }, } };
执行
truffle migrate — network goerli
部署到goerli,这里会消耗gas费,但没有metamusk弹窗确认的过程,因为我输入了助记词,他直接在钱包扣款了shell//命令行输出 Compiling your contracts... =========================== > Everything is up to date, there is nothing to compile. Migrations dry-run (simulation) =============================== > Network name: 'goerli-fork' > Network id: 5 > Block gas limit: 30000000 (0x1c9c380) 1_counter.js ============ Deploying 'Counter' ------------------- > block number: 6818292 > block timestamp: 1651505825 > account: 0x736D76f4C2d4b4CCced0CCA92d3dF0F0e456F35D > balance: 0.04813429249775099 > gas used: 135269 (0x21065) > gas price: 10 gwei > value sent: 0 ETH > total cost: 0.00135269 ETH ------------------------------------- > Total cost: 0.00135269 ETH Summary ======= > Total deployments: 1 > Final cost: 0.00135269 ETH Starting migrations... ====================== > Network name: 'goerli' > Network id: 5 > Block gas limit: 29970705 (0x1c95111) 1_counter.js ============ Deploying 'Counter' ------------------- > transaction hash: 0x376710e125c35233b468bd232444acc2239c38e837aeecf9ef9de6762715eca8 > Blocks: 0 Seconds: 5 > contract address: 0xAc8dC14e7aC85556Bcc22c052FbB5Bc05D4E77D8 > block number: 6818298 > block timestamp: 1651505843 > account: 0x736D76f4C2d4b4CCced0CCA92d3dF0F0e456F35D > balance: 0.04813429249775099 > gas used: 135269 (0x21065) > gas price: 10 gwei > value sent: 0 ETH > total cost: 0.00135269 ETH > Saving artifacts ------------------------------------- > Total cost: 0.00135269 ETH Summary ======= > Total deployments: 1 > Final cost: 0.00135269 ETH
补充内容
Truffle console使用(可以直接在控制台调用合约)
先把合约部署到development
truffle console --network development
开启调用模式truffle-min.sh(压缩artifacts文件)
我们compile后,是会在build里输出一个json,但是这个json很大,所以可以借助这个脚本来对这个json进行压缩,可以从几千行压缩到几十行,然后我们后续的开发有引用这个json的需求的时候,就可以引入这个压缩的json
3. Hardhat
Hardhat:编译、部署、测试和调试以太坊应用的开发环境,围绕task(任务)和plugins(插件)概念设计
在命令行运行Hardhat时,都是在运行任务,例如:npx hardhat compile就是运行compile任务
Hardhat node:开发区块链,提供本地模拟的链上环境
创建工程
mkdir hardhat-demo //创建一个folder
cd hardhat-demo
npm init //初始化npm
npm i -s hardhat //当前目录下安装hardhat
npx hardhat //在当前目录下创建项目,可以选basic-sample(注意,这一步要开启shell proxy)
contracts里新建Counter.sol,添加一下内容
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "hardhat/console.sol";
contract Counter{
uint public counter;
constructor(){
counter = 0;
}
function count() public{
counter+=1;
console.log("curr counter:",counter);
}
function add(uint x) public{
counter=counter+x;
}
}
合约编译
//先修改hardhat.config.js,设置编译器版本
module.exports={
Solidity:"0.8.9"
}
//然后命令行输入
npx hardhat compile
部署
编写部署脚本
js//scripts/deploy_counter.js const hre = require("hardhat"); async function main() { //await hre.run('compile'); const Counter = await hre.ethers.getContractFactory("Counter"); const counter = await Counter.deploy(); //deploy里的括号可以传参,具体看合约里的construtor有没有参数而定 await counter.deployed(); console.log("Counter deployed to:", counter.address); } main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); });
如果想要一次部署多个合约,比如这里有counter和greeter合约,那么可以这么写
js//scripts/deploy.js const { ethers } = require("hardhat"); async function main() { const [deployer] = await ethers.getSigners(); console.log('Deploying contracts with the account: ' + deployer.address); // Deploy Greeter const Greeter = await ethers.getContractFactory("Greeter"); const greeter = await Greeter.deploy("Hello, Hardhat!"); // Deploy Counter const Counter = await hre.ethers.getContractFactory("Counter"); const counter = await Counter.deploy(); // console.log console.log("Greeter deployed to:", greeter.address); console.log("Counter deployed to:", counter.address); } main() .then(() => process.exit()) .catch(error => { console.error(error); process.exit(1); })
部署到本地网络
先启动一个本地网络
npx hardhat node
,然后再根据端口值来修改hardhat.config.js,增加以下内容js//hardhat.config.js module.exports={ networks:{ development:{ url:"http://127.0.0.1:8545", chainId:31337 } } }
执行部署
npx hardhat run scripts/deploy_xxx.js [--network 网络]
,而这里的话,就是执行npx hardhat run scripts/deploy_counter.js --network development
部署到链上
需要部署到链上的节点,所以先去infura.io创建一个project,然后copy project id(选择endpoints是goerli)
本地添加.api_key和.mnemonic,.gitignore添加过滤这两项,然后修改hardhat.config.js,增加以下内容
js//hardhat.config.js const fs = require('fs'); const api_key = fs.readFileSync('.api_key').toString().trim(); const mnemonic = fs.readFileSync('.mnemonic').toString().trim(); module.exports={ networks:{ goerli:{ url:`https://goerli.infura.io/v3/${api_key}`, accounts:{ mnemonic:mnemonic, } } } }
执行部署npx hardhat run scripts/deploy.js --network goerli
测试
const {expect} = require("chai");
const {ethers} = require("hardhat");
describe("Counter", function () {
it("counter should be 0 when init , be added 1 after count be called, be added x after add be called with x", async function () {
const Counter = await ethers.getContractFactory("Counter");
const counterInstance = await Counter.deploy();
await counterInstance.deployed();
expect(await counterInstance.counter()).to.equal(0);
const setCountTx = await counterInstance.count();
// wait until the transaction is mined
await setCountTx.wait();
expect(await counterInstance.counter()).to.equal(1);
const addCountTx = await counterInstance.add(10);
await addCountTx.wait();
expect(await counterInstance.counter()).to.equal(11);
});
});
npx hardhat test
执行测试
实战
调试利器:console.log
solidityimport "hardhat/console.sol"; console.log(counter);
灵活参数部署,利用hardhat可以在代码中引用(这里值的是合约初始化的参数,也就是constructor需要的参数,比如Greeter合约,这里可以在部署脚本里传入参数,比如deploy_greeter.js所写。也可以用nodejs的方式,在命令行传入参数,然后在部署脚本里取出来)
- solidity
// Counter.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "hardhat/console.sol"; contract Counter{ uint public counter; constructor(uint _counter){ counter = _counter; } function count() public{ counter+=1; console.log("curr counter:",counter); } function add(uint x) public{ counter=counter+x; } } // test_counter.js const {expect} = require("chai"); const {ethers} = require("hardhat"); describe("Counter", function () { it("counter should be 0 when init , be added 1 after count be called, be added x after add be called with x", async function () { const Counter = await ethers.getContractFactory("Counter"); const counterInstance = await Counter.deploy(0); await counterInstance.deployed(); expect(await counterInstance.counter()).to.equal(0); const setCountTx = await counterInstance.count(); // wait until the transaction is mined await setCountTx.wait(); expect(await counterInstance.counter()).to.equal(1); const addCountTx = await counterInstance.add(10); await addCountTx.wait(); expect(await counterInstance.counter()).to.equal(11); }); }); //deploy_counter.js //其余相同,只需改下面这一句 const counter = await Counter.deploy(0);
设置deploy脚本
jsconst {ethers} = require("hardhat"); const params=process.argv; const value=params[2]; console.log("Counter deploy with value:",value); async function main() { //await hre.run('compile'); const Counter = await ethers.getContractFactory("Counter"); const counter = await Counter.deploy(value); await counter.deployed(); console.log("Counter deployed to:", counter.address); } main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); });
执行node脚本,并传递参数
需要两个命令,可以在命令行依次执行,也可以写成一个.sh,
sh ./deploy_by_param.sh
执行这个.sh即可shell// ./deploy_by_param.sh export HARDHAT_NETWORK='development' //设置network的值是development;这就是node执行hardhat脚本附着hardhat参数的方式+ node scripts/deploy_by_param.js 10 //在这里传入初始化参数是10
代码扁平:
npx hardhat flatten xxx.sol > xxx.sol
本质的意义是,当一个合约的里面有import引用的时候,可以用这个命令把引用的代码和本身的代码放在一起,方便看,这里的话就是
npx hardhat flatten contracts/Counter.sol >>Counter.sol
代码验证
当合约部署在链上后,可以通过这个命令来对代码进行验证
shell1.先安装 npm i hardhat-etherscan --dev 2.hardhat.config.js里添加require("@nomiclabs/hardhat-etherscan") 3.添加scankey,const scankey=`${scanKey}`; //因为调用etherscan的api,所以需要去他那里申请一个key, https://etherscan.io/,这个key最好也用隐藏文件,不要暴露出去 4.在配置里,networks的下面,添加同级项 etherscan:{apiKey:scankey} 5.命令行执行npx hardhat verify address --network xxx // address是合约的部署地址,xxx是要验证的网络
ABI导出
当合约部署后,需要导出ABI供前端调用,默认的deploy就会导出一个总的json里面包括abi,不过比较大而已,这里是仅导出abi的json
shell1.先安装 npm i hardhat-abi-exporter --dev 2.hardhat.config.js里添加require("hardhat-abi-exporter") 3.hardhat.config.js里配置下导出,新建./abi目录来存放 abiExporter: { path: './abi', runOnCompile: true, clear: true, flat: true, spacing: 2, pretty: false, } 4.npx hardhat export-abi
自定义Task
在hardhat.config.js里可以写task,然后通过
npx hardhat taskName
完成一些想要做的事,也可以在task文件夹里写,然后在hardhat.config.js用require引入jstask("accounts","Prints the list of accounts",async(taskArgs,hre)=>{ const accounts =await hre.ethers.getSigners(); for(const account of accounts){ console.log(account.address) } }) //命令行 npx hardhat accounts执行
书写脚本去调用合约,实现前端与合约的交互
前端想要调用合约,那就得知道合约的地址和abi,之前有用npm处理了abi的单独导出,但是并没有处理合约的地址,所以这里我们改造下deploy脚本,让它自动保存地址和abi到一个data的目录下
js// scripts/deploy_counter.js const hre = require("hardhat"); async function main() { //await hre.run('compile'); const Counter = await hre.ethers.getContractFactory("Counter"); const counter = await Counter.deploy(0); await counter.deployed(); console.log("Counter deployed to:", counter.address); saveFrontendFiles(counter); } function saveFrontendFiles(counter) { const fs = require("fs"); const contractsDir = "./data"; if (!fs.existsSync(contractsDir)) { fs.mkdirSync(contractsDir); } fs.writeFileSync( contractsDir + "/contract-address.json", JSON.stringify({ Counter: counter.address }, undefined, 2) ); const CounterArtifact = artifacts.readArtifactSync("Counter"); fs.writeFileSync( contractsDir + "/Counter.json", JSON.stringify(CounterArtifact, null, 2) ); } main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); }); //然后执行脚本 npx hardhat run scripts/deploy_counter.js --network development
新建action.js实现前端调用合约的逻辑
jsconst { ethers } = require("hardhat") const CounterArtifact = require("../data/Counter.json") const contractAddress = require("../data/contract-address.json"); (async () => { // init const provider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:8545/'); // Then, we initialize the contract using that provider and the token's // artifact. You can do this same thing with your contracts. const counter = new ethers.Contract( contractAddress.Counter, CounterArtifact.abi, provider.getSigner(0) ); console.log("counter值是:",await counter.counter()) await counter.count(); console.log("counter值是:",await counter.counter()) })()