Viem 是一个相当新的 web3 库,它专注于 EVM,提供了更好的开发体验,更小的包体积等等。在本文中,将使用 foundry 部署一个简单的合约,并在 node 环境下使用 viem 与部署的链上合约执行读写交互。
首先使用 foundry 来部署一个简单的合约。首先新建一个 foundry 项目,使用forge init[3]命令。
forge init viem-foundry
创建好后的项目结构会像下面这样。
➜ viem-foundry git:(main) ✗ tree . -L 1
.
├── README.md
├── foundry.toml
├── lib
├── script
├── src
└── test
foundry 会在src
中创建一个叫Counter.sol
的示例合约。对这个合约稍作修改,增加一个decrement
方法。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Counter {
uint256 public number;
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
function decrement() public {
number--;
}
}
同时在test/Counter.t.sol
中增加一个test_Decrement
的测试用例。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";
contract CounterTest is Test {
Counter public counter;
function setUp() public {
counter = new Counter();
counter.setNumber(0);
}
function test_Increment() public {
counter.increment();
assertEq(counter.number(), 1);
}
function test_Decrement() public {
counter.setNumber(1);
counter.decrement();
assertEq(counter.number(), 0);
}
function testFuzz_SetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
}
接下来执行forge build[4]命令。
$ forge build
Compiling 27 files with Solc 0.8.19
Solc 0.8.19 finished in 1.27s
Compiler run successful!
接着执行forge test[5]命令。终端会显示下面的结果,所有的用例都 PASS 都没有问题,就可以部署合约了。
viem-foundry git:(main) forge test
[⠢] Compiling...
No files changed, compilation skipped
Ran 3 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 31098, ~: 31332)
[PASS] test_Decrement() (gas: 21546)
[PASS] test_Increment() (gas: 31359)
Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 12.49ms (8.09ms CPU time)
Ran 1 test suite in 250.79ms (12.49ms CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests)
这里使用 sepolia 作为测试部署。使用forge create[6]命令。这里的--rpc-url
填入 sepolia 的 rpc,可以使用公共的rpc 节点[7],--private-key
就是你的钱包私钥,--etherscan-api-key
用于验证合约用,这个 api-key 可以去https://etherscan.io/login[8]注册账号获得。
forge create --rpc-url <your_rpc_url> \
--private-key <your_private_key> \
--etherscan-api-key <your_etherscan_api_key> \
--verify \
src/Counter.sol:Counter
部署完成后,就可以在 sepolia 的区块链浏览器中查看到部署好的合约,这里已经部署好的合约可以在这里查看[9]。
经过验证后的合约,在Contract
标签栏上会有一个绿色的小勾,并且可以查看合约的源码。
在区块链浏览器中可以直接与合约进行交互,在Write Contract
中可以看到合约中的函数,可以通过链接钱包,进行对合约的写入操作。当然,执行每一个操作都需要支付 gas 费。
在Read Contract
中可以查看所有的 public 变量。
至此,合约的部署工作已经全部完成了。
接下来是 viem 的部分,这一部分主要是关于如何使用 viem 与合约进行交互。首先,创建一个viem-scripts
文件夹,首先使用 pnpm 初始化项目,使用pnpm init
,终端会出现以下信息。
{
"name": "viem-scripts",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
接下来需要安装一些必要的依赖,使用 pnpm 安装。
pnpm install dotenv viem
pnpm install -D typescript ts-node @types/node
安装依赖后,需要在项目中初始化 typescript 的配置文件,输入以下命令。
npx tsc --init
项目根目录下会自动创建一个tsconfig.json
文件,终端显示如下。
Created a new tsconfig.json with:
TS
target: es2016
module: commonjs
strict: true
esModuleInterop: true
skipLibCheck: true
forceConsistentCasingInFileNames: true
You can learn more at https://aka.ms/tsconfig
新建一个index.ts
文件,创建一个client
,调用 viem 提供的createPublicClient
函数,该函数需要传入一个对象,对象包含两个重要的字段chain
和transport
。
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
const client = createPublicClient({
chain: sepolia,
transport: http(),
});
本文的合约部署在 sepolia 上,所以这里的chain
使用sepolia
,viem/chains 中提供了很多公链,更多支持的链可以这个列表[10]。transport
指的是前端与合约的通信方式,viem 中的transport
支持三种模式,分别是 HTTP Transport[11],WebSocket Transport[12] 和 Custom Transport[13] ,这里使用最常用的http
方式。
有了client
,就可以与链做交互了,先做一个最简单的交互,查询当前链的区块高度。输入下面的代码。
async function main() {
const blockNumber = await client.getBlockNumber();
console.log(blockNumber);
}
main();
打开根目录中的package.json
,在scripts
中添加"start": "ts-node index.ts"
。
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "ts-node index.ts"
},
运行pnpm start
,终端会显示目前的区块高度。注意,你运行的时候,这个值会和我的不一样,因为区块数一直在增加。
> viem-scripts@1.0.0 start /viem-playground/viem-scripts
> ts-node index.ts
6153864n
为了和合约交互,需要知道合约的 abi。在 foundry 项目中,当我们执行forge build
后,会把所有项目中涉及到的所有合约进行编译,并在根目录下会生成一个out
文件夹,文件夹内会对应生成每个合约的 json 文件。
.
├── cache
├── lib
├── out
│ ├── Base.sol
│ │ ├── CommonBase.json
│ │ ├── ScriptBase.json
│ │ └── TestBase.json
│ ├── Counter.s.sol
│ │ └── CounterScript.json
│ ├── Counter.sol
│ │ └── Counter.json
│ ├── Counter.t.sol
│ │ └── CounterTest.json
│ ├── IERC165.sol
│ │ └── IERC165.json
│ ├── IERC20.sol
│ │ └── IERC20.json
│ ├── IERC721.sol
│ │ ├── IERC721.json
│ │ ├── IERC721Enumerable.json
│ │ ├── IERC721Metadata.json
│ │ └── IERC721TokenReceiver.json
│ ├── IMulticall3.sol
│ │ └── IMulticall3.json
│ ├── MockERC20.sol
│ │ └── MockERC20.json
│ ├── MockERC721.sol
│ │ ├── IERC721TokenReceiver.json
│ │ └── MockERC721.json
│ ├── Script.sol
│ │ └── Script.json
│ ├── StdAssertions.sol
│ │ └── StdAssertions.json
│ ├── StdChains.sol
│ │ └── StdChains.json
│ ├── StdCheats.sol
│ │ ├── StdCheats.json
│ │ └── StdCheatsSafe.json
│ ├── StdError.sol
│ │ └── stdError.json
│ ├── StdInvariant.sol
│ │ └── StdInvariant.json
│ ├── StdJson.sol
│ │ └── stdJson.json
│ ├── StdMath.sol
│ │ └── stdMath.json
│ ├── StdStorage.sol
│ │ ├── stdStorage.json
│ │ └── stdStorageSafe.json
│ ├── StdStyle.sol
│ │ └── StdStyle.json
│ ├── StdToml.sol
│ │ └── stdToml.json
│ ├── StdUtils.sol
│ │ └── StdUtils.json
│ ├── Test.sol
│ │ └── Test.json
│ ├── Vm.sol
│ │ ├── Vm.json
│ │ └── VmSafe.json
│ ├── console.sol
│ │ └── console.json
│ ├── console2.sol
│ │ └── console2.json
│ └── safeconsole.sol
│ └── safeconsole.json
├── script
├── src
└── test
找到Counter.json
文件,可以看到里面包含了几个主要字段,abi
,bytecode
,deployedBytecode
,methodIdentifiers
,rawMetadata
,metadata
以及id
。这些字段构成了描述一个合约的完整信息。
{
"abi": [
{
"type": "function",
"name": "decrement",
"inputs": [],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "increment",
"inputs": [],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "number",
"inputs": [],
"outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }],
"stateMutability": "view"
},
{
"type": "function",
"name": "setNumber",
"inputs": [
{ "name": "newNumber", "type": "uint256", "internalType": "uint256" }
],
"outputs": [],
"stateMutability": "nonpayable"
}
],
"bytecode": {
"object": "0x6080604052348015600f57600080fd5b506101328061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060465760003560e01c80632baeceb714604b5780633fb5c1cb1460535780638381f58a146063578063d09de08a14607d575b600080fd5b60516083565b005b6051605e36600460a4565b600055565b606b60005481565b60405190815260200160405180910390f35b60516097565b60008054908060908360d2565b9190505550565b60008054908060908360e6565b60006020828403121560b557600080fd5b5035919050565b634e487b7160e01b600052601160045260246000fd5b60008160de5760de60bc565b506000190190565b60006001820160f55760f560bc565b506001019056fea2646970667358221220aa623ffc9dd48bbbf1da02199cadc16e0e718c5d6cc383341f77e57e2cb4412564736f6c63430008190033",
"sourceMap": "65:251:25:-:0;;;;;;;;;;;;;;;;;;;",
"linkReferences": {}
},
"deployedBytecode": {
"object": "0x6080604052348015600f57600080fd5b506004361060465760003560e01c80632baeceb714604b5780633fb5c1cb1460535780638381f58a146063578063d09de08a14607d575b600080fd5b60516083565b005b6051605e36600460a4565b600055565b606b60005481565b60405190815260200160405180910390f35b60516097565b60008054908060908360d2565b9190505550565b60008054908060908360e6565b60006020828403121560b557600080fd5b5035919050565b634e487b7160e01b600052601160045260246000fd5b60008160de5760de60bc565b506000190190565b60006001820160f55760f560bc565b506001019056fea2646970667358221220aa623ffc9dd48bbbf1da02199cadc16e0e718c5d6cc383341f77e57e2cb4412564736f6c63430008190033",
"sourceMap": "65:251:25:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;261:53;;;:::i;:::-;;116:80;;;;;;:::i;:::-;171:6;:18;116:80;88:21;;;;;;;;;345:25:27;;;333:2;318:18;88:21:25;;;;;;;202:53;;;:::i;261:::-;299:6;:8;;;:6;:8;;;:::i;:::-;;;;;;261:53::o;202:::-;240:6;:8;;;:6;:8;;;:::i;14:180:27:-;73:6;126:2;114:9;105:7;101:23;97:32;94:52;;;142:1;139;132:12;94:52;-1:-1:-1;165:23:27;;14:180;-1:-1:-1;14:180:27:o;381:127::-;442:10;437:3;433:20;430:1;423:31;473:4;470:1;463:15;497:4;494:1;487:15;513:136;552:3;580:5;570:39;;589:18;;:::i;:::-;-1:-1:-1;;;625:18:27;;513:136::o;654:135::-;693:3;714:17;;;711:43;;734:18;;:::i;:::-;-1:-1:-1;781:1:27;770:13;;654:135::o",
"linkReferences": {}
},
"methodIdentifiers": {
"decrement()": "2baeceb7",
"increment()": "d09de08a",
"number()": "8381f58a",
"setNumber(uint256)": "3fb5c1cb"
},
"rawMetadata": "{\"compiler\":{\"version\":\"0.8.25+commit.b61c2a91\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[],\"name\":\"decrement\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"increment\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"number\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"newNumber\",\"type\":\"uint256\"}],\"name\":\"setNumber\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/Counter.sol\":\"Counter\"},\"evmVersion\":\"paris\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":forge-std/=lib/forge-std/src/\"]},\"sources\":{\"src/Counter.sol\":{\"keccak256\":\"0xf45df1ccab4c7ce7c7c8b529079d4e3c914cf09350e26d1b2a168e89792c9124\",\"license\":\"UNLICENSED\",\"urls\":[\"bzz-raw://7f911aefa13cda2bf5eadfb5cc672af7dceee749563afb27e237f225a221cb4a\",\"dweb:/ipfs/QmdUkxZJkFgz8oC9ARaKhRJNVRGvyGnC8TFNCvh7ejc5k2\"]}},\"version\":1}",
"metadata": {
"compiler": { "version": "0.8.25+commit.b61c2a91" },
"language": "Solidity",
"output": {
"abi": [
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "function",
"name": "decrement"
},
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "function",
"name": "increment"
},
{
"inputs": [],
"stateMutability": "view",
"type": "function",
"name": "number",
"outputs": [
{ "internalType": "uint256", "name": "", "type": "uint256" }
]
},
{
"inputs": [
{
"internalType": "uint256",
"name": "newNumber",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function",
"name": "setNumber"
}
],
"devdoc": { "kind": "dev", "methods": {}, "version": 1 },
"userdoc": { "kind": "user", "methods": {}, "version": 1 }
},
"settings": {
"remappings": ["forge-std/=lib/forge-std/src/"],
"optimizer": { "enabled": true, "runs": 200 },
"metadata": { "bytecodeHash": "ipfs" },
"compilationTarget": { "src/Counter.sol": "Counter" },
"evmVersion": "paris",
"libraries": {}
},
"sources": {
"src/Counter.sol": {
"keccak256": "0xf45df1ccab4c7ce7c7c8b529079d4e3c914cf09350e26d1b2a168e89792c9124",
"urls": [
"bzz-raw://7f911aefa13cda2bf5eadfb5cc672af7dceee749563afb27e237f225a221cb4a",
"dweb:/ipfs/QmdUkxZJkFgz8oC9ARaKhRJNVRGvyGnC8TFNCvh7ejc5k2"
],
"license": "UNLICENSED"
}
},
"version": 1
},
"id": 25
}
使用 viem 与合约交互需要用到 abi 以及 bytecode。
在viem-scripts
项目文件夹中,新建一个abi.ts
文件,声明并暴露两个变量abi
和address
。abi
这个变量的内容来自Counter.json
,address
就是 sepolia 上部署的Counter
合约地址。注意,下面代码中第 30 行,需要添加as const
关键词,这样 viem 可以智能的读取里面的 function 信息。
export const abi = [
{
type: "function",
name: "decrement",
inputs: [],
outputs: [],
stateMutability: "nonpayable",
},
{
type: "function",
name: "increment",
inputs: [],
outputs: [],
stateMutability: "nonpayable",
},
{
type: "function",
name: "number",
inputs: [],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "setNumber",
inputs: [{ name: "newNumber", type: "uint256", internalType: "uint256" }],
outputs: [],
stateMutability: "nonpayable",
},
] as const;
export const address = "0x6b565dE192A1Be17a4F077B5Fda6b3A100498790" as const;
接下来需要导入私钥,在项目根文件夹下新建.env
文件,添加私钥。下面的私钥是示例,假数据。注意,私钥很重要,不要轻易外泄。
PRIVATE_KEY=45210d79205254d4505912eb32371f7f2f0b059ed771898554f0d0f169c87e45
同时,记得将.env
文件添加到.gitignore
文件中,确保不要将此文件上传至 github 或其他代码托管平台上。
.env
node_modules/
在index.ts
中配置dotenv
,导入.env
中的私钥变量。调用privateKeyToAccount
函数,传入私钥。
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import dotenv from "dotenv";
dotenv.config();
const account = privateKeyToAccount(`0x${process.env.PRIVATE_KEY}`);
使用本地私钥的方式与合约做交互需要使用 viem 的 Wallet Client。导入createWalletClient
方法,并创建一个walletClient
。注意,http
方法接受自定义 rpc 节点链接,如果你有自己的 rpc 可以传入其中,否则 viem 将会使用公共节点,有时候公共节点会比较不稳定,同时也有速度限制。
import { createPublicClient, createWalletClient, http } from "viem";
import { sepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import dotenv from "dotenv";
dotenv.config();
const account = privateKeyToAccount(`0x${process.env.PRIVATE_KEY}`);
const rpc = process.env.ETH_RPC_URL;
const walletClient = createWalletClient({
account,
chain: sepolia,
transport: http(rpc),
});
接下来实现与合约交互,首先实现读合约的操作。导入abi
和address
,使用readContract
方法,传入address
,abi
和functionName
。注意,读取操作使用的 client 是 Public Client 而不是 Wallet Client。
import { abi, address } from "./abi";
async function main() {
// const blockNumber = await client.getBlockNumber();
// console.log(blockNumber);
const number = await client.readContract({
address,
abi,
functionName: "number",
});
console.log(number);
}
main();
完整的代码如下。
import { createPublicClient, createWalletClient, http } from "viem";
import { sepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { abi, address } from "./abi";
import dotenv from "dotenv";
dotenv.config();
const account = privateKeyToAccount(`0x${process.env.PRIVATE_KEY}`);
const rpc = process.env.ETH_RPC_URL;
const walletClient = createWalletClient({
account,
chain: sepolia,
transport: http(rpc),
});
const client = createPublicClient({
chain: sepolia,
transport: http(rpc),
});
async function main() {
// const blockNumber = await client.getBlockNumber();
// console.log(blockNumber);
const number = await client.readContract({
address,
abi,
functionName: "number",
});
console.log(number);
}
main();
运行main
函数。会得到下面的结果。可以看到输出了 101。证明已经成功的从链上读取了函数的返回值。
> viem-scripts@1.0.0 start /viem-playground/viem-scripts
> ts-node index.ts
101n
执行写操作就需要使用 Wallet Client。改造一下main
函数。首先,把获取number
的函数单独抽取出来。调用walletClient
的writeContract
方法,注意args
,这个字段接收一个数组,里面就是调用合约函数时需要传入的参数,传入 number 型变量时需要先转化为 BigInt 类型。调用writeContract
函数后会返回一个哈希值,这个哈希值可以作为waitForTransactionReceipt
的参数。当对合约进行写操作时,可以看作是进行了一笔 Transaction,对于以太坊来说,会在单位时间内打包多笔交易并生成一个新的区块。执行waitForTransactionReceipt
会返回一个receipt
对象,可以获取这次交易的信息。
async function main() {
// const blockNumber = await client.getBlockNumber();
// console.log(blockNumber);
getNumber();
const hash = await walletClient.writeContract({
address,
abi,
functionName: "setNumber",
args: [BigInt(100)],
});
console.log("The hash is:", hash);
const receipt = await client.waitForTransactionReceipt({ hash });
console.log("receipt info:", receipt);
receipt && getNumber();
}
async function getNumber() {
const number = await client.readContract({
address,
abi,
functionName: "number",
});
console.log("The number is:", number);
}
main();
执行main
函数,可以依次看到如下信息。可以看到number
从 101 变成了 100。
> viem-scripts@1.0.0 start /web3/viem-playground/viem-scripts
> ts-node index.ts
The number is: 101n
The hash is: 0x96c55da3ef7b9b0ef209d7329cd74a4fb1b1c493d8efad55814a156d867f96a6
receipt info: {
type: 'eip1559',
from: '0xa0466a82b961e85077d4a8debc35fbf6cf18d464',
to: '0x6b565de192a1be17a4f077b5fda6b3a100498790',
status: 'success',
cumulativeGasUsed: 20730581n,
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
logs: [],
transactionHash: '0x96c55da3ef7b9b0ef209d7329cd74a4fb1b1c493d8efad55814a156d867f96a6',
contractAddress: null,
gasUsed: 26416n,
blockHash: '0xce27361d3088232abd4f63fc3f5aaf9c63c7bd38f3f54f3df08dbe26f8fe6147',
blockNumber: 6167597n,
transactionIndex: 136,
effectiveGasPrice: 1964520099n
}
The number is: 100n
去 sepolia 的区块链浏览器中也能看到每一次调用成功后的 Transaction Hash。
同样,在区块链浏览器中查询也能看到number
更新了。
至此,使用 viem 与合约的交互工作已经完成。viem 的更多功能请参考官方文档[14]。
https://viem.sh/docs/getting-started.html: https://viem.sh/docs/getting-started.html
[2]https://github.com/CarryWang/viem-playground: https://github.com/CarryWang/viem-playground
[3]forge init: https://book.getfoundry.sh/reference/forge/forge-init.html
[4]forge build: https://book.getfoundry.sh/reference/forge/forge-build.html
[5]forge test: https://book.getfoundry.sh/reference/forge/forge-test.html
[6]forge create: https://book.getfoundry.sh/reference/forge/forge-create.html
[7]rpc 节点: https://chainlist.org/chain/11155111
[8]https://etherscan.io/login: https://etherscan.io/login
[9]查看: https://sepolia.etherscan.io/address/0x6b565dE192A1Be17a4F077B5Fda6b3A100498790
[10]列表: https://github.com/wevm/viem/blob/main/src/chains/index.ts
[11]HTTP Transport: https://viem.sh/docs/clients/transports/http
[12]WebSocket Transport: https://viem.sh/docs/clients/transports/websocket
[13]Custom Transport: https://viem.sh/docs/clients/transports/custom
[14]官方文档: https://viem.sh/docs/clients/intro
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。