Viem极简教程:与链上合约交互

  • Carry
  • 更新于 2024-06-24 18:55
  • 阅读 2212

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

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

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

    使用foundry创建合约

    首先使用foundry来部署一个简单的合约。首先新建一个foundry项目,使用forge init命令。

    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`的测试用例。
```solidity
// 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命令。

$ forge build
Compiling 27 files with Solc 0.8.19
Solc 0.8.19 finished in 1.27s
Compiler run successful!

接着执行forge test命令。终端会显示下面的结果,所有的用例都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命令。这里的--rpc-url填入sepolia的rpc,可以使用公共的rpc节点--private-key就是你的钱包私钥,--etherscan-api-key用于验证合约用,这个api-key可以去https://etherscan.io/login注册账号获得。

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的区块链浏览器中查看到部署好的合约,这里已经部署好的合约可以在这里查看

image.png 经过验证后的合约,在Contract标签栏上会有一个绿色的小勾,并且可以查看合约的源码。

image.png 在区块链浏览器中可以直接与合约进行交互,在Write Contract中可以看到合约中的函数,可以通过链接钱包,进行对合约的写入操作。当然,执行每一个操作都需要支付gas费。

image.pngRead Contract中可以查看所有的public变量。

image.png 至此,合约的部署工作已经全部完成了。


使用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中提供了很多公链,更多支持的链可以这个列表transport指的是前端与合约的通信方式,viem中的transport支持三种模式,分别是 HTTP TransportWebSocket TransportCustom Transport ,这里使用最常用的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。

image.png 同样,在区块链浏览器中查询也能看到number更新了。

image.png 至此,使用viem与合约的交互工作已经完成。viem的更多功能请参考官方文档

点赞 5
收藏 6
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
Carry
Carry
0xa046...d464
Less is More