Viem 极简教程:与链上合约交互
登链社区
2024-06-24 18:59
订阅此专栏
收藏此文章

Viem 是一个相当新的 web3 库,它专注于 EVM,提供了更好的开发体验,更小的包体积等等。在本文中,将使用 foundry 部署一个简单的合约,并在 node 环境下使用 viem 与部署的链上合约执行读写交互。

  • Viem docs — https://viem.sh/docs/getting-started.html[1]
  • GitHub repository for the code used in this article — https://github.com/CarryWang/viem-playground[2]

使用 foundry 创建合约

首先使用 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 与合约进行交互。首先,创建一个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函数,该函数需要传入一个对象,对象包含两个重要的字段chaintransport

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文件,可以看到里面包含了几个主要字段,abibytecodedeployedBytecodemethodIdentifiersrawMetadatametadata以及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文件,声明并暴露两个变量abiaddressabi这个变量的内容来自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),
});

接下来实现与合约交互,首先实现读合约的操作。导入abiaddress,使用readContract方法,传入addressabifunctionName。注意,读取操作使用的 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的函数单独抽取出来。调用walletClientwriteContract方法,注意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]

参考资料
[1]

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


【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。

登链社区
数据请求中
查看更多

推荐专栏

数据请求中
在 App 打开