使用Hardhat部署至Moonbeam¶
概览¶
Hardhat是一个灵活可拓展的以太坊开发环境,它能帮助开发者简化智能合约开发过程。 由于Moonbeam与以太坊兼容,您可以在Moonbeam上使用Hardhat开发和部署智能合约。
Hardhat采用基于任务的开发方式,开发者可以定义和执行任务以执行特定操作。这些操作包括编译和部署合约、运行测试等等。这些任务的可配置性很高,您可以创建、自定义和执行不同任务来满足您的需求。
您还可以通过使用插件来扩展Hardhat的功能。插件是外部扩展应用,它们可与Hardhat集成以提供额外的功能与工具来简化工作流程。有些插件包括了常见的以太坊库,例如Ethers.js,viem和为Chai Assertion库添加以太坊功能的插件等等。 所有这些插件都可用于在Moonbeam上扩展您的Hardhat项目。
本指南将简要介绍Hardhat,并向您展示如何使用Hardhat在Moonbase Alpha测试网上编译、部署和调试以太坊智能合约。本指南还适用于Moonbeam、Moonriver或 Moonbeam开发节点。
请注意,尽管Hardhat带有一个Hardhat Network组件,它能提供一个本地开发环境,但您应该使用本地Moonbeam开发节点来代替。您可以像连接任何其他网络一样将 Moonbeam开发节点与Hardhat相连。
查看先决条件¶
在开始之前,您将需要准备以下内容:
-
具有拥有一定数量资金的账户。 您可以每24小时一次从Moonbase Alpha水龙头上获取DEV代币以在Moonbase Alpha上进行测试
- 要在Moonbeam或Moonriver网络上测试本指南中的示例,您可以从受支持的网络端点提供商之一获取您自己的端点和API密钥
创建Hardhat项目¶
如果您还未有Hardhat项目,您将需要创建一个。为此,您可以执行以下步骤:
-
为您的项目创建一个目录
mkdir hardhat && cd hardhat
-
初始化将创建
package.json
文件的项目npm init -y
-
安装Hardhat
npm install hardhat
-
创建Hardhat项目
npx hardhat init
注意事项
npx
用于运行安装在项目中的本地可执行文件。虽然Hardhat可以全网安装,但是我们建议在每个项目中进行本地安装,这样您可以按项目控制项目版本。 -
系统将会显示菜单,允许您创建新的项目或使用范本项目。在本示例中,您可以选择Create an empty hardhat.config.js,这会为您的项目创建一个Hardhat配置文件。
👷 Welcome to Hardhat v2.22.2 👷
What do you want to do? … Create a JavaScript project Create a TypeScript project Create a TypeScript project (with Viem) Quit
Hardhat配置文件¶
设置Hardhat配置文件是您Hardhat项目的开始。它定义了您Hardhat项目的不同设定以及可选项。比如使用的Solidity编译器版本以及您将部署智能合约的目标网络。
第一步,您的 hardhat.config.js
应包含以下内容:
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: '0.8.20',
};
在这个例子中,您会使用 0.8.20
版本的Solidity编译器版本;但是如果您的其他智能合约需要更新的版本,请记得在这里更改版本号。
下一步,您需要修改您的配置文件,在其中依据您部署智能合约的目标网络来添加网络配置。部署在Moonbeam网络需要配置以下这些选项:
这个例子中的网络为Moonbase Alpha,您也可以修改这个配置以适配其他Moonbeam网络:
module.exports = {
solidity: '0.8.20',
networks: {
moonbeam: {
url: 'INSERT_RPC_API_ENDPOINT', // 输入您的RPC URL
chainId: 1284, // (hex: 0x504),
accounts: ['INSERT_PRIVATE_KEY'],
},
},
};
module.exports = {
solidity: '0.8.20',
networks: {
moonriver: {
url: 'INSERT_RPC_API_ENDPOINT', // 输入您的RPC URL
chainId: 1285, // (hex: 0x505),
accounts: ['INSERT_PRIVATE_KEY'],
},
},
};
module.exports = {
solidity: '0.8.20',
networks: {
moonbase: {
url: 'https://rpc.api.moonbase.moonbeam.network', // 输入您的RPC URL
chainId: 1287, // (hex: 0x507),
accounts: ['INSERT_PRIVATE_KEY'],
},
},
};
module.exports = {
solidity: '0.8.20',
networks: {
dev: {
url: 'http://127.0.0.1:9944', // Insert your RPC URL here
chainId: 1281, // (hex: 0x501),
accounts: ['INSERT_PRIVATE_KEY'],
},
},
};
请记住
以上代码只是示例,请千万不要在您的Javascript文件中储存私钥
如果您想要在项目中使用插件,您需要安装插件并将通过hardhat.config.js
文件将其导入。当一个插件导入后,它会成为Hardhat Runtime Environment的一部分,您可以在任务,脚本或别的地方使用该插件。
在这个范例中,您可以安装 hardhat-ethers
插件并且将其导入配置文件,这个插件为Ethers.js代码库提供了一个方便的封装,用于网络交互。
npm install @nomicfoundation/hardhat-ethers ethers
您也需要另外安装 hardhat-ignition-ethers
插件以使用Hardhat Ignition部署智能合约。您可以使用以下命令安装插件:
npm install --save-dev @nomicfoundation/hardhat-ignition-ethers
To import both plugins, add the following require
statements to the top of the Hardhat configuration file:
/** @type import('hardhat/config').HardhatUserConfig */
require('@nomicfoundation/hardhat-ethers');
require('@nomicfoundation/hardhat-ignition-ethers');
const privateKey = 'INSERT_PRIVATE_KEY';
module.exports = {
solidity: '0.8.20',
networks: {
moonbase: {
url: 'https://rpc.api.moonbase.moonbeam.network',
chainId: 1287, // 0x507 in hex,
accounts: [privateKey]
}
}
};
关于其他配置选项的信息,请阅读Hardhat文档中的Configuration部分。
合约文件¶
现在您已经完成项目的配置,可以开始创建您的智能合约了。接下来要部署的合约并不复杂,它被命名为Box
,它能储存一个数值并且读取这个数值。
使用以下步骤添加合约:
-
创建
contracts
目录mkdir contracts
-
创建
Box.sol
文件touch contracts/Box.sol
-
打开文件,并为其添加以下合约内容:
// contracts/Box.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.1; contract Box { uint256 private value; // 在数值变化时被调用 event ValueChanged(uint256 newValue); // 在合约中储存新数值 function store(uint256 newValue) public { value = newValue; emit ValueChanged(newValue); } // 读取最后储存的数值 function retrieve() public view returns (uint256) { return value; } }
编译Solidity¶
下一步是编译Box.sol
智能合约,您可以使用内置的compile
任务。这个任务会在contracts
目录中寻找Solidity文件并且根据hardhat.config.js
中定义的版本与设置来编译这些文件。
您可以简单运行以下命令编译合约:
npx hardhat compile
编译后,将会创建一个artifacts
目录:这保存了合约的字节码和元数据,为.json
文件。您可以将此目录添加至您的.gitignore
。
如果您在编译后修改了合约内容,您可以用以上命令再次编译合约。Hardhat会自动识别更改内容并且重编译合约。如果没有更新内容,那将不会进行编译。如果有需求,您可以使用clean
任务来强制重新编译,缓存文件会被清理且旧的artifect文件会被删除。
部署合约¶
您将使用Hardhat Ignition来部署合约,Hardhat Ignition是一个用于部署智能合约的声明式框架。它能够简化智能合约部署与测试中重复任务的管理。详细信息请参照Hardhat Ignition文档。
设置Ignition模块的正确文件架构,请先创建一个名为ignition
的文件夹和一个名为modules
的子目录。然后在其中添加一个名为Box.js
的新文件。您可以使用以下命令一次性完成这三个步骤:
mkdir ignition ignition/modules && touch ignition/modules/Box.js
从Hardhat Ignition模块导入buildModule函数 使用buildModule导出一个模块 使用getAccount方法选择部署者账户 部署Box合约 从模块返回一个对象。这使得Box合约可以在Hardhat测试和脚本中进行交互
- 从 Hardhat Ignition 模块导入
buildModule
函数 - 使用
buildModule
导出模块 - 使用
getAccount
方法选择部署账户 - 部署
Box
合约 - 从模块返回一个对象。这使得
Box
合约能够在Hardhat测试和脚本中交互
// 1. Import the `buildModule` function from the Hardhat Ignition module
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");
// 2. Export a module using `buildModule`
module.exports = buildModule("BoxModule", (m) => {
// 3. Use the `getAccount` method to select the deployer account
const deployer = m.getAccount(0);
// 4. Deploy the `Box` contract
const box = m.contract("Box", [], {
from: deployer,
});
// 5. Return an object from the module
return { box };
});
使用以下命令来运行脚本,部署Box.sol
合约;您需要给这个命令定义一个部署合约的网络名称,这个名称已在hardhat.config.js
中定义。如果您没有定义网络,hardhat将会默认在本地网络上部署合约
npx hardhat ignition deploy ./ignition/modules/Box.js --network moonbase
注意事项
如果您正在使用另一个Moonbeam网络,请确保您已指定正确的网络。网络名称需要与hardhat.config.js
中所定义的网络相匹配。
您将会收到提示确认要部署到的网络。在您确认后几秒钟合约就会被部署,您将在终端中看到合约地址。
✅ Confirm deploy to network moonbase (1287)? … yes Hardhat Ignition 🚀
Deploying [ BoxModule ]
Batch #1 Executed BoxModule#Box
[ BoxModule ] successfully deployed 🚀
Deployed Addresses
BoxModule#Box - 0xfBD78CE8C9E1169851119754C4Ea2f70AB159289
恭喜您,您的合约已完成!请保存地址,用于后续与合约实例的交互。
与合约交互¶
使用Hardhat与新合约交互的方式有几种:您可以使用console
任务来启用一个可交互的JavaScript控制台;或者您也可以创建一个脚本并用run
任务来执行它。
使用Hardhat控制台¶
Hardhat控制台与任务和脚本使用同样的执行环境,因此它也能自动使用hardhat.config.js
中定义的参数与插件。
执行以下命令开启Hardhat console
:
npx hardhat console --network moonbase
接下来,您可以执行以下步骤(一次输入一行):
-
创建一个
Box.sol
合约的本地实例const Box = await ethers.getContractFactory('Box');
-
使用合约地址,将本地实例连接至已部署的合约
const box = await Box.attach('0xfBD78CE8C9E1169851119754C4Ea2f70AB159289');
-
与此合约交互。在本示例中,您可以调用
store
方法并存储一个简单的值await box.store(5);
交易将通过您在hardhat.config.js
中定义的账户进行签署并传送至网络。后台输出将如下所示:
Welcome to Node.js v20.9.0. Type ".help" for more information. const Box = await ethers.getContractFactory('Box'); undefined
const box = await Box.attach('0xfBD78CE8C9E1169851119754C4Ea2f70AB159289'); undefined
await box.store(5); ContractTransactionResponse {
provider: HardhatEthersProvider { ... },
blockNumber: null,
blockHash: null,
index: undefined,
hash: '0x1c49a64a601fc5dd184f0a368a91130cb49203ec0f533c6fcf20445c68e20264',
type: 2,
to: '0xa84caB60db6541573a091e5C622fB79e175E17be',
from: '0x3B939FeaD1557C741Ff06492FD0127bd287A421e',
nonce: 87,
gasLimit: 45881n,
gasPrice: 1107421875n,
maxPriorityFeePerGas: 1n,
maxFeePerGas: 1107421875n,
data: '0x6057361d0000000000000000000000000000000000000000000000000000000000000005',
value: 0n,
chainId: 5678n,
signature: Signature { r: "0x9233b9cc4ae6879b7e08b9f1a4bfb175c8216eee0099966eca4a305c7f369ecc", s: "0x7663688633006b5a449d02cb08311569fadf2f9696bd7fe65417860a3b5fc57d", yParity: 0, networkV: null },
accessList: [],
blobVersionedHashes: null
} await box.retrieve(); 5n
请注意您的地址将被标记为from
,即合约地址,以及正在传送的data
。现在,您可以通过运行以下命令来检索数值:
await box.retrieve();
您将看到5
或者您初始存储的数值。
使用脚本¶
与部署脚本相同,您也可以创建一个脚本来与合约交互。您可以将脚本存入scripts
目录,并使用内置的run
来运行它。
首先,在scripts
目录下创建一个set-value.js
文件:
touch scripts/set-value.js
然后将以下代码复制到set-value.js
文件中:
// scripts/set-value.js
async function main() {
// Create instance of the Box contract
const Box = await ethers.getContractFactory('Box');
// Connect the instance to the deployed contract
const box = await Box.attach('0xfBD78CE8C9E1169851119754C4Ea2f70AB159289');
// Store a new value
await box.store(2);
// Retrieve the value
const value = await box.retrieve();
console.log(`The new value is: ${value}`);
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
运行以下命令来执行脚本:
npx hardhat run --network moonbase scripts/set-value.js
这个脚本应当返回2
这个数值。
The new value is: 2
使用Hardhat进行分叉¶
您可以使用Hardhat分叉(fork) 包括Moonbeam在内的任何EVM兼容链。分叉是在本地模拟实时Moonbeam网络,使您可以在本地测试环境中与已部署在Moonbeam上的合约交互。因为Hardhat的分叉是基于EVM实现,您可以通过Moonbeam和Hardhat支持的标准以太坊JSON-RPC方法与分叉网络交互。
您需要了解一些使用Hardhat进行分叉的注意事项。您无法与任何Moonbeam预编译合约及其函数交互。预编译是Substrate实现的一部分,因此无法在模拟的EVM环境中复制。这将阻止您与Moonbeam上的跨链资产和基于Substrate的功能(例如质押和治理)进行交互。
当前分叉Moonbeam存在一个问题,为了解决此问题,您需要先手动修补Hardhat。您可以通过GitHub上的问题和相关PR获取更多信息。
修补Hardhat¶
在开始之前,您需要应用一个临时补丁来解决RPC错误,直到Hardhat修复了根本问题。错误如下所示:
Error HH604: Error running JSON-RPC server: Invalid JSON-RPC response's result.
Errors: Invalid value null supplied to : RpcBlockWithTransactions | null/transactions: RpcTransaction Array/0: RpcTransaction/accessList: Array<{ address: DATA, storageKeys: Array<DATA> | null }> | undefined, Invalid value null supplied to : RpcBlockWithTransactions | null/transactions: RpcTransaction Array/1: RpcTransaction/accessList: Array<{ address: DATA, storageKeys: Array<DATA> | null }> | undefined, Invalid value null supplied to : RpcBlockWithTransactions | null/transactions: RpcTransaction Array/2: RpcTransaction/accessList: Array<{ address: DATA, storageKeys: Array<DATA> | null }> | undefined
要修补Hardhat,您需要打开您项目中的node_modules/hardhat/internal/hardhat-network/jsonrpc/client.js
文件。接下来,添加addAccessList
函数并更新_perform
和_performBatch
函数。
现在,您可以移除预先存在的_perform
和_performBatch
函数,并在其中添加以下代码片段:
addAccessList(method, rawResult) {
if (
method.startsWith('eth_getBlock') &&
rawResult &&
rawResult.transactions?.length
) {
rawResult.transactions.forEach((t) => {
if (t.accessList == null) t.accessList = [];
});
}
}
async _perform(method, params, tType, getMaxAffectedBlockNumber) {
const cacheKey = this._getCacheKey(method, params);
const cachedResult = this._getFromCache(cacheKey);
if (cachedResult !== undefined) {
return cachedResult;
}
if (this._forkCachePath !== undefined) {
const diskCachedResult = await this._getFromDiskCache(
this._forkCachePath,
cacheKey,
tType
);
if (diskCachedResult !== undefined) {
this._storeInCache(cacheKey, diskCachedResult);
return diskCachedResult;
}
}
const rawResult = await this._send(method, params);
this.addAccessList(method, rawResult);
const decodedResult = (0, decodeJsonRpcResponse_1.decodeJsonRpcResponse)(
rawResult,
tType
);
const blockNumber = getMaxAffectedBlockNumber(decodedResult);
if (this._canBeCached(blockNumber)) {
this._storeInCache(cacheKey, decodedResult);
if (this._forkCachePath !== undefined) {
await this._storeInDiskCache(this._forkCachePath, cacheKey, rawResult);
}
}
return decodedResult;
}
async _performBatch(batch, getMaxAffectedBlockNumber) {
// Perform Batch caches the entire batch at once.
// It could implement something more clever, like caching per request
// but it's only used in one place, and those other requests aren't
// used anywhere else.
const cacheKey = this._getBatchCacheKey(batch);
const cachedResult = this._getFromCache(cacheKey);
if (cachedResult !== undefined) {
return cachedResult;
}
if (this._forkCachePath !== undefined) {
const diskCachedResult = await this._getBatchFromDiskCache(
this._forkCachePath,
cacheKey,
batch.map((b) => b.tType)
);
if (diskCachedResult !== undefined) {
this._storeInCache(cacheKey, diskCachedResult);
return diskCachedResult;
}
}
const rawResults = await this._sendBatch(batch);
const decodedResults = rawResults.map((result, i) => {
this.addAccessList(batch[i].method, result);
return (0, decodeJsonRpcResponse_1.decodeJsonRpcResponse)(
result,
batch[i].tType
);
});
const blockNumber = getMaxAffectedBlockNumber(decodedResults);
if (this._canBeCached(blockNumber)) {
this._storeInCache(cacheKey, decodedResults);
if (this._forkCachePath !== undefined) {
await this._storeInDiskCache(this._forkCachePath, cacheKey, rawResults);
}
}
return decodedResults;
}
然后,您可以通过运行以下命令使用patch-package自动应用代码包:
npx patch-package hardhat
随即会创建一个patches
目录,现在您可以分叉Moonbeam且在运行时不会遇到任何错误了。
分叉Moonbeam¶
您可以从命令行分叉Moonbeam或配置您的Hardhat项目以始终从您的hardhat.config.js
文件运行此分叉。要分叉Moonbeam或Moonriver,需要用到您自己的端点和API密钥,您可以从端点提供商所支持的列表中获取。
要从命令行分叉Moonbeam,您可以从您的Hardhat项目目录中运行以下命令:
npx hardhat node --fork INSERT_RPC_API_ENDPOINT
npx hardhat node --fork INSERT_RPC_API_ENDPOINT
npx hardhat node --fork https://rpc.api.moonbase.moonbeam.network
如果您想要配置您的Hardhat项目,您可以使用以下配置更新您的hardhat.config.js
文件:
...
networks: {
hardhat: {
forking: {
url: 'INSERT_RPC_API_ENDPOINT',
},
},
},
...
...
networks: {
hardhat: {
forking: {
url: 'INSERT_RPC_API_ENDPOINT',
},
},
},
...
...
networks: {
hardhat: {
forking: {
url: 'https://rpc.api.moonbase.moonbeam.network',
},
},
},
...
当您启动Hardhat分叉时,您会有20个预先注资10,000个测试Token的开发账户。分叉好的实例位于http://127.0.0.1:8545/
。在您的终端中,将会显示类似以下输出:
Any funds sent to them on Mainnet or any other live network WILL BE LOST.
要验证您是否已经分叉好网络,您可以查询最新区块号:
curl --data '{"method":"eth_blockNumber","params":[],"id":1,"jsonrpc":"2.0"}' -H "Content-Type: application/json" -X POST localhost:8545
如果您已经将result
从16进制转换成十进制,您应该在分叉网络时获得了最新区块号。您可以使用区块浏览器交叉查询区块号。
在这里,您可以部署新的合约到您的Moonbeam分叉实例,或者通过创建已部署合约的本地实例与已部署合约交互。
要与已部署合约交互,您可以使用ethers
在scripts
目录中创建新的脚本。因为您将使用Hardhat运行此脚本,因此您无需导入任何库。在脚本中,您可以使用以下代码片段访问网络上的实时合约。
const hre = require('hardhat');
async function main() {
const provider = new ethers.JsonRpcProvider(
'http://127.0.0.1:8545/'
);
const contract = new ethers.Contract(
'INSERT_CONTRACT_ADDRESS',
'INSERT_CONTRACT_ABI',
provider
);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});