了解区块链技术(Patrick Collins)(七)

node版本:v16.14.2 yarn版本:1.22.15

1、hardhat配置

(1)创建hardhat-fund-me-fcc

添加hardhatyarn add --dev hardhat等等

生成目录包括:node_modules,packages.json,yarn.lock

yarn hardhat

(2)创建.env文件,.gitignore文件

.npmignore可以在项目推送为NPM包的时候忽略一些文件 -- del

.prettierrc.prettierignore:分号

.solhint.json.solhintignore

solhint运行一个程序的过程,该程序会分析代码是否存在潜在错误,还会做一些格式化,eslint是对JavaScript代码进行lint方法,solhint是对Solidity代码进行lint方法

官网:https://github.com/protofire/solhint

yarn add solhint 版本:solhint": "^4.1.1

简单的小例子:

在合约中定义了uint256 someVar,这并不好,我们要准确的说出变量的可见性

初始化一个配置文件:

solhint --init

配置文件:

{
  "extends": "solhint:recommended",
  "plugins": [],
  "rules": {
    "avoid-suicide": "error",
    "avoid-sha3": "warn"
  }
}

运行:yarn solhint --init contracts/*.sol

✖ 11 problems (0 errors, 11 warnings)

(3)复制FundMe.solPriceConverter.sol

告诉hardhat在哪里获取,npmjs.com

npm包管理工具下载@chainlink/contracts

yarn add --dev @chainlink/contracts

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";//这样hardhat就知道从哪里导入了
// 在remix中我们只需要从npm或者Github中直接导入"@chainlink/contracts"

运行yarn hardhat compile

会看到目录新生成artifacts和cache两个目录

(4)部署代码

脚本部署,当使用原生ethers或只使用hardhat进行工作时,跟踪所有部署是一件非常棘手的事情,如果只使用一个部署脚本,就不会把我们的部署保存在文件中,可能会使测试和部署脚本之间无法完全兼容

使用hardhat-deploy

yarn add --dev hardhat-deploy,一个用于可复制的部署及简便测试的hardhat插件

一旦安装完成,在hardhat.config.jsrequire进来

config中添加:

require("@nomicfoundation/hardhat-toolbox");
require("hardhat-deploy");
// else 
require("solidity-coverage")
require("hardhat-gas-reporter")

之后就可以删除我们的deploy脚本了

yarn hardhat

会看到:

deploy task是我们部署合约的时候主要用到的task,不在scripts中编写部署脚本

mkdir deploy

是许多hardhat-deploy模块部署代码和编写脚本的地方,在脚本中要使用ethers.js,添加:

hardhat-deploy-ethers

yarn add --dev hardhat @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers ethers

"@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers",

我们在deploy中添加的所欲脚本都会在执行yarn hardhat deploy时运行

创建01-deploy-fund-me.js

sample

也可以:

// 从hre中提取出我们需要的函数和变量
// JavaScript语法糖
module.exports = async ({ getNamedAccounts, deployments }) => {
    // 用于部署智能合约和记录日志
    const { deploy, log } = deployments;
    // 通过getNamedAccounts获取名为deployer的账户对象,这个账户通常是部署者
    const { deployer } = await getNamedAccounts;
    // 获取当前网络的链ID
    const chainId = network.config.chainId
}
require("@nomicfoundation/hardhat-toolbox");
require("hardhat-deploy");
/** @type import('hardhat/config').HardhatUserConfig */
const SEPOLIA_RPC_URL = process.env.SEPOLIA_RPC_URL || "https://eth-rinkeby";
const PRIVATE_KEY = process.env.PRIVATE_KEY || "0xkey";
const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY || "key";
const COINMARKETCAP_API_KEY = process.env.COINMARKETCAP_API_KEY || "key";
​
const { ProxyAgent, setGlobalDispatcher } = require("undici");
const proxyAgent = new ProxyAgent("http://192.168.2.15:7890");
setGlobalDispatcher(proxyAgent);
​
module.exports = {
  solidity: {
    compilers: [
      { version: "0.8.19" },
      { version: "0.6.6" }
    ]
  },
  defaultNetwork: "hardhat",
  networks: {
    sepolia: {
      url: SEPOLIA_RPC_URL,
      accounts: [PRIVATE_KEY],//在包含私钥的列表中,假如有私钥1,私钥2,私钥3,很容易让人混淆,可以为每一个账户取一个名字
      chainId: 11155111,
      timeout: 60000,
      blockConfirmations: 6,
    },
    localhost: {
      url: "http://127.0.0.1:8545/",
      chainId: 31337,
    }
  },
  etherscan: {
    // Your API key for Etherscan
    // Obtain one at https://etherscan.io/
    apiKey: {
      sepolia: "DZS4JIXINN56WBDCTADA2G92IUCRFJI5JI",
    }
  },
  sourcify: {
    enabled: true,
    apiUrl: "https://sourcify.dev/server",
    browserUrl: "https://repo.sourcify.dev",
  },
  gasReporter: {
    //测试时运行
    enabled: true,
    outputFile: "gas-report.txt",
    noColors: true,
  },
  namedAccounts: {
    deployer: {
      default: 0,
    },
    user: {
      default: 1,
    },
  },
};
​

Mocking & helper-hardhat-config

部署到Rinkeby测试网或者主网上很慢,将部署测试网作为本地测试的最后一步,理论情况下,我们应该先部署到本地网络,但是在PriceConverter.sol中,有一处硬编码处

 AggregatorV3Interface priceFeed = AggregatorV3Interface(
            0x694AA1769357215DE4FAC081bf1f309aDC325306
        );

如果想在hardhat network上运行,hardhat network是一个空白的区块链,每次脚本执行完成之后都会被销毁,在本地节点上工作,喂价合约是不存在的,其中的某些代码无法更新数据

(1)通过分叉一个区块链,可以在其中保留硬编码内容

(2)或者使用mocks来完成所有的操作

什么是mocking ?

主要用于单元测试受测对象可能依赖于其他复杂的对象,为了隔离某些对象的行为,使用mocks对象来替换其他对象,模拟只是对象的行为。

创建一个模拟真实对象行为的对象(虚拟的喂价合约,在本地能够使用并进行控制)

切换链的时候会发生什么 ?不同网络的喂价会有差异,把喂价合约地址参数化(当使用本地主机或者hardhat-network需要使用到mock)

FundMe.sol进行重构:

    AggregatorV3Interface public priceFeed;
    // 接收价格数据的地址,根据所在链传递ETH/USD的喂价地址
    constructor(address priceFeedAddress) {
        priceFeed = AggregatorV3Interface(priceFeedAddress);
        i_owner = msg.sender;
    }
     require(
            msg.value.getConversionRate(priceFeed) > minimumUsd,
            "didn't send enough"
        );
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
​
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
​
library PriceConvert {
    // 一个库中的所有函数都是internal的
    function getPrice(
        AggregatorV3Interface priceFeed
    ) internal view returns (uint256) {
        (, int256 price, , , ) = priceFeed.latestRoundData();
        return uint256(price * 10**10);
    }
​
    function getVersion(
        AggregatorV3Interface priceFeed
    ) internal view returns (uint256) {
        return priceFeed.version();
    }
​
    function getConversionRate(
        uint256 ethAmount,
        AggregatorV3Interface priceFeed
    ) internal view returns (uint256) {
        uint256 ethPrice = getPrice(priceFeed);
        uint256 ethAmountInUsd = (ethAmount * ethPrice) / 10**18;
        return ethAmountInUsd;
    }
}

添加yarn add --dev dotenv

执行yarn hardhat compile

const { network } = require("hardhat");
​
module.exports = async (getNamedAccounts, deployments) => {
    // 用于部署智能合约和记录日志
    const { deploy, log } = deployments;
    // 通过getNamedAccounts获取名为deployer的账户对象,这个账户通常是部署这
    const { deployer } = await getNamedAccounts;
    // 获取当前网络的链ID
    const chainId = network.config.chainId
    // 想要添加的覆盖选项的列表
    const address = "0x694AA1769357215DE4FAC081bf1f309aDC325306";
​
    const fundMe = await deploy("FundMe",{
        from: deployer, // 指定谁在部署
        args: [address],       // 传递给构造函数的参数  
        log: true,
    });
}

本质上还是硬编码,解决方法

aave是存在于多个链上的一个协议,所以它必须把代码部署到多个链上,并使用不同的地址,aave其中helper-config能够满足我们的需求

创建helper-hardhat-config.js,用于定义网络配置

// 定义网络配置
​
const networkConfig = {
    11155111: {
        name: "sepolia",
        ethUsdPriceFeed: "0x694AA1769357215DE4FAC081bf1f309aDC325306",
    }
    // 同理
};
const developmentChain = ["hardhat", "localhost"];
const DECIMALS = 8;
const INITIAL_ANSWER = 2000;
// 导出networkConfig,以便其他脚本可以使用
module.exports = {
    networkConfig,
    developmentChain,
    DECIMALS,
    INITIAL_ANSWER,
}

如果使用的是本地网络,应该怎么做呢 ?Mock的思想是如果某个合约不存在,我们就部署一个最小化的版本进行我们的本地测试

deploy文件夹下,创建00-deploy-mocks.js去部署一个自己的喂价合约

const { network, getNamedAccounts, deployments } = require("hardhat");
const { developmentChain, DECIMALS, INITIAL_ANSWER } = require("../helper-hardhat-config");
// 部署一个自己的mock喂价合约
module.exports = async () => {
    const { deployer } = await getNamedAccounts();
    const { deploy, log } = deployments;
    const chainId = network.config.chainId
    // 添加mocks虚拟合约到我么的contracts文件夹中
    //将虚拟合约和其他合约分离,其只是进行用于测试使用
    if (developmentChain.includes(network.name)) {
        log("local network detected ......");
        await deploy("MockV3Aggregator", {
            contract: "MockV3Aggregator",
            from: deployer,
            log: true,
            args: [DECIMALS, INITIAL_ANSWER],
        });
        log("Mocks deployed");
    }
}
​
module.exports.tags = ["all", "mocks"];

contracts目录下创建test/MockV3Aggregator.sol,创建虚拟的喂价合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
​
// 定义我们的Mock喂价聚合器
import "@chainlink/contracts/src/v0.6/tests/MockV3Aggregator.sol";

yarn hardhat compile

使用以下命令部署:

yarn hardhat deploy --network hardhat --tags mocks

01-deploy-fund-me.js

const { network } = require("hardhat");
const { networkConfig, developmentChain } = require("../helper-hardhat-config");
​
module.exports = async ({ getNamedAccounts, deployments }) => {
    const { deployer } = await getNamedAccounts();
    const { deploy, log } = deployments;
    const chainId = network.config.chainId;
    console.log(network.name);
    // const address = "0x694AA1769357215DE4FAC081bf1f309aDC325306";
    // const ethUsdPriceFeedAddress = networkConfig[chainId]["ethUsdPriceFeed"];
    let ethUsdPriceFeedAddress;
    if (developmentChain.includes(network.name)) {
        const ethUsdAggrator = await deployments.get("MockV3Aggregator");
        ethUsdPriceFeedAddress = ethUsdAggrator.address;
        console.log(`1234567890:${ethUsdPriceFeedAddress}`);
    } else {
        ethUsdPriceFeedAddress = networkConfig[chainId].ethUsdPriceFeed;
        console.log(`1234567890:${ethUsdPriceFeedAddress}`);
    }
    const args = [ethUsdPriceFeedAddress];
    const fundMe = await deploy("FundMe", {
        from: deployer, // 指定谁在部署
        args: args,       // 传递给构造函数的参数  
        log: true,
    });
    log("fundMe deployed ----------------")
}
module.exports.tags = ["all", "fundme"];    

使用以下命令部署:

yarn hardhat deploy --network sepolia --tags fundme

自动验证:

utils文件夹中写验证的脚本

const { run } = require("hardhat");
const verify = async (contractAddress, args) => {
    console.log("Verifying contract ....");
    try {
        await run("verify:verify", {
            address: contractAddress,
            constructorArguments: args,
        });
    } catch (e) {
        if (e.message.toLowerCase().includes("verified")) {
            console.log("ok");
        } else {
            console.log(e);
        }
    }
};
module.exports = {
    verify
}
// 01-deploy-fund-me.js
 if (!developmentChain.includes(network.name) && process.env.ETHERSCAN_API_KEY) {
        await verify(fundMe.address, args);
    }
    log("verify ok ------------");

其中验证合约的时候会出现连接超时的问题,参考以下可以解决:

https://github.com/smartcontractkit/full-blockchain-solidity-course-js/discussions/2247#discussioncomment-5496669

hardhat-config.js中添加:

// 其中127.0.0.1,如果是物理主机是Windows,换成Windows主机的IP地址
const { ProxyAgent, setGlobalDispatcher } = require("undici");
const proxyAgent = new ProxyAgent("http://127.0.0.1:7890");
setGlobalDispatcher(proxyAgent);
// 关闭正在使用的防火墙

Solidity代码风格:

https://docs.soliditylang.org/en/v0.8.19/style-guide.html

// pragma
// import
// Error Code  FundMe__NotOwner() // 定位错误的位置
// Interface Library contract
// NatSpec 以太坊自然语言规范格式
// @title @autor @notice @dev @param @return natSpec能自动的帮我们创建文档
// 合约内部 
// 类型声明(using PriceConverter for uint256)、状态变量(大小写混合、下划线)、事件、修饰器、构造器、fallback()、recieve()

测试fundMe合约

优化我们的智能合约,使其更快速、更加节省gas,至少要进行两种类型的测试

创建staginguint文件夹

单元测试是一种软件测试方法,用于测试源代码的各个单元,测试代码的最小部分,确保所有代码在本地都可以工作,之后在实际的测试网上进行Staging测试,确保我们的代码能和其他的合约正常工作

local hardhat forked hardhat

单元测试之后,我们就要进行暂存(Staging)测试或者集成(Integration)测试

创建fundMe.test.js

describe("fundMe", async function () {
    beforeEach();
});

运行yarn hardhat coverage,我们漏掉了很多东西

FundMe.test.js

const { deployments, ethers, getNamedAccounts } = require("hardhat");
const { assert, expect } = require("chai");
describe("fundMe", async function () {
    let fundMe;
    let mockV3Aggregator;
    beforeEach(async () => {
        deployer = (await getNamedAccounts()).deployer;
​
        await deployments.fixture(["all"]);// 换一下位置
        fundMe = await ethers.getContract("FundMe", deployer);
        mockV3Aggregator = await ethers.getContract("MockV3Aggregator", deployer);
    });
    describe("constructor", async () => {
        it("sets the aggregator address correctly", async function () {
            console.log("1234567890");
            const response = await fundMe.priceFeed();
            assert.equal(response, mockV3Aggregator.target);
        });
    });
});

结果:

 describe("fund", async () => {
        it("Fails if you don't send enough ETH", async function () {
            await fundMe.fund();
        });
    });
// 仅仅这样写的话会报错,waffle

waffle可以通过expect关键字来预期交易回滚和交易失败

   it("update the amount funded data structure", async function () {
            // 发送1ETH
            await fundMe.fund({ value: sendValue });
            const response = await fundMe.addressToAmountFonded(deployer);
            // f39Fd6e51aad88F6F4ce6aB8827279cffFb92266
​
            assert.equal(response.toString(), sendValue);
        });

可以看到范围小了一些:

   it("Adds funder to array of funders", async function () {
            await fundMe.fund({ value: sendValue });
            const funder = await fundMe.funders(0);
            console.log(funder);
            assert.equal(funder, deployer);
        });

接下来写取款的测试:

// 完整代码
 describe("withdraw", async () => {
        // 希望执行it之前账户是由一些钱在里面的
        beforeEach(async () => {
            await fundMe.fund({ value: sendValue });
        });
        it("withdraw ETH from a single founder", async () => {
            // 获取fundMe合约的初始金额以及deployer的初始金额
            const startingFundMeBalance = await ethers.provider.getBalance(fundMe.target);
            const startingdeployerBalance = await ethers.provider.getBalance(deployer);
​
            const transactionResponse = await fundMe.withdraw();
            const transactionReceipt = await transactionResponse.wait(1);
​
            const { gasUsed, gasPrice } = transactionReceipt;
            const gasCost = gasUsed * gasPrice;
​
            const endingFundMeBalance = await ethers.provider.getBalance(fundMe.target);
            const endingdeployerBalance = await ethers.provider.getBalance(deployer);
​
            assert.equal(endingFundMeBalance, 0);
            assert.equal((startingFundMeBalance +
                startingdeployerBalance).toString(),
                (endingdeployerBalance + gasCost).toString()
            );
        });
    });

Debug

debug的时候,可以看到:

其中GasUsed * GasPrice就是我们为gas支付的全部金额了

调试工具

在solidity中使用hardhat的console.log( )

import "hardhat/console.log"

测试fundMe

测试多个资助者

it("allow us withdraw with multiple funders", async () => {
            const accounts = await ethers.getSigners();
            // 从1开始,账户0是deployer
            for (let i = 1; i < 6; i++) {
                //
                const fundMeConnectedContract = await fundMe.connect(accounts[i]);
                console.log(accounts[i]);
                await fundMeConnectedContract.fund({ value: sendValue });
            }
            // 获取合约余额
            const startingFundMeBalance = await ethers.provider.getBalance(fundMe.target);
            const startingdeployerBalance = await ethers.provider.getBalance(deployer);
            // 取钱
            const transactionResponse = await fundMe.withdraw();
            const transactionReceipt = await transactionResponse.wait(1);
​
            const { gasUsed, gasPrice } = transactionReceipt;
            const gasCost = gasUsed * gasPrice;
​
            const endingFundMeBalance = await ethers.provider.getBalance(fundMe.target);
            const endingdeployerBalance = await ethers.provider.getBalance(deployer);
​
            assert.equal(endingFundMeBalance, 0);
            assert.equal((startingFundMeBalance +
                startingdeployerBalance).toString(),
                (endingdeployerBalance + gasCost).toString()
            );
​
            await expect(fundMe.funders(0)).to.be.reverted;
​
            for (i = 1; i < 6; i++) {
                assert.equal(await fundMe.addressToAmountFonded(accounts[i].address), 0);
            }
        });

测试OnlyOwner是否有效:

 it("Only the owner to withdraw", async () => {
        const accounts = await ethers.getSigners()
        const attacker = accounts[1];
        // 连接到账户
        const attackerConnectContract = await fundMe.connect(attacker);
        await expect(attackerConnectContract.withdraw()).to.be.revertedWith("FundMe__NotOwner");
    });
error FundMe__NotOwner(); // 确定错误来自哪里

Storage:

gasReporter设置为true,执行yarn hardhat test,看我们消耗的gas

如何减小我们的gas消费呢?

  uint256 public minimumUsd = 50 * 10 * 18;
    address[] public funders; //记录发送资金的funder
    mapping(address => uint256) public addressToAmountFonded; // 记录每个地址资金发送的数量
    //部署合约后立刻调用,通过构造函数设置合约的拥有者
    address public immutable i_owner;
​
    AggregatorV3Interface public priceFeed; // 喂价

当我们保存或存储这些全局变量或Storage变量,会发生什么 ?

https://docs.soliditylang.org/en/v0.8.24/internals/layout_in_storage.html

可以将Storage理解为一个包含我们创建的所有变量的数组或者链表

uint256 favoriteNumber能永久存在是由于被存储在了一个Storage的地方,每一个Storage类型的变量都被放在了一个32字长的槽位上,favoriteNumber的值假如是25,那么存储在槽位中的就是uint256的16进制表示

对于动态数组或者mapping,内部的元素是以哈希函数来存储的,对象本身会占一个槽位,如果执行push操作,我们将在Storage的位置上存储值

constant变量本身已经成为合约字节码的一部分

如果有一些变量只存在于合约的运行期间,在合约中并不是永久存在的,会添加到Memory数据结构中,在函数运行结束后就会删除

字符串是一个动态大小的数组,需要将其存储在Storage中还是Memory(消耗gas)中,需不需要去清除它

读取或写入Storage都会消耗大量的gas,代码编译后的操作码和字节码,将字节码拆开解析就会发现字节码是由许多操作码组成,操作码表示需要执行多少计算才能运行我们的代码并完成所需的操作

gas费用是通过操作码的计算成本来计算的,每个操作码都有一个在以太坊网络中预定义的固定的gas成本

artifacts/build-info

不同的操作码对应的不同gas:https://github.com/crytic/evm-opcodes

命名规范:

添加s_前缀表示Storage存储变量,并且访问会产生gas费用

i_表示immutable不可变变量,要比普通变量要便宜的多

    function withdraw() public onlyOwner {
        for (
            uint256 funderIndex = 0;
            funderIndex < s_funders.length;
            funderIndex = funderIndex + 1
        ) {
            address funder = s_funders[funderIndex];
            s_addressToAmountFonded[funder] = 0;
        }
        // 重置s_funders数组
        s_funders = new address[](0); //零表示初始只有零个元素
        (bool callSuccess, ) = payable(msg.sender).call{
            value: address(this).balance
        }("");
        require(callSuccess, "call failed");
    }

s_funders.length每一次都要从Storage中读取,gas费就会很高

    function cheaperWithdraw() public payable onlyOwner {
        address[] memory funders = s_funders;
        for (
            uint256 funderIndex = 0;
            funderIndex < funders.length;
            funderIndex++
        ) {
            address funder = funders[funderIndex];
            s_addressToAmountFonded[funder] = 0;
        }
        s_funders = new address[](0);
        (bool success, ) = i_owner.call{value: address(this).balance}("");
        require(success);
    }

变量设置成internal或private会更加节省gas费

 uint256 public s_minimumUsd = 50 * 10 * 18;
    address[] private s_funders; //记录发送资金的funder
    mapping(address => uint256) private s_addressToAmountFonded; // 记录每个地址资金发送的数量
    //合约所有者不需要对其他人或者其他合约公开
    address private immutable i_owner;
​
    AggregatorV3Interface private s_priceFeed; // 喂价

对于所有private类型的变量可以写上对应的getter方法

  • 16
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值