玩转 Web3:用 Viem 库实现以太坊合约部署与交互
·
8min
·
Paxon Qiao
Table of Contents
玩转 Web3:用 Viem 库实现以太坊合约部署与交互
想一窥 Web3 开发的奥秘?以太坊智能合约是通往区块链世界的大门,而 Viem 库让你轻松迈出第一步!本文通过一个 TypeScript 脚本,带你从连接本地以太坊测试网到部署合约、实现交互,全程手把手实战。不管你是 Web3 新手还是想探索新工具的开发者,这篇教程都能让你快速上手,玩转区块链开发的乐趣!
本文献上一场 Web3 开发的实战盛宴!通过一个基于 Viem 库的 TypeScript 脚本,我们将带你连接以太坊本地测试网(如 Hardhat),查询账户信息、发送交易、部署智能合约,并与合约互动,甚至实时监控区块变化。结合一个简单的 Storage 合约和详细的运行结果,这篇教程让你轻松掌握 Viem 的核心用法,快速开启 Web3 开发之旅!
实操
import { createPublicClient, createWalletClient, defineChain, http, hexToBigInt, getContract } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { ABI, BYTECODE } from "../abi/storage";
import { ethers } from "ethers";
import config from '../config';
export const localChain = (url: string) => defineChain({
id: 31337,
name: 'Testnet',
network: 'Testnet',
nativeCurrency: {
name: 'ETH',
symbol: 'ETH',
decimals: 18,
},
rpcUrls: {
default: {
http: [url],
},
},
testnet: true,
})
function toViemAddress(address: string): string {
return address.startsWith("0x") ? address : `0x${address}`
}
export function getViemClient(url: string) {
return createPublicClient({
chain: localChain(url),
transport: http(url),
})
}
export function remove0x(privateKey: string): string {
return privateKey.startsWith('0x') ? privateKey.slice(2) : privateKey;
}
const privateKey = remove0x(config.privateKey);
console.log("privateKey: ", privateKey);
const walletClient = createWalletClient({
chain: localChain(config.localRpcUrl),
transport: http(config.localRpcUrl),
account: privateKeyToAccount(config.privateKey as `0x${string}`),
})
export async function deployContract(): Promise<string> {
const hash = await walletClient.deployContract({
abi: ABI,
bytecode: `0x${BYTECODE}`,
args: []
})
const publicClient = getViemClient(config.localRpcUrl)
const receipt = await publicClient.waitForTransactionReceipt({ hash })
if (!receipt.contractAddress) {
throw new Error('Contract deployment failed: no contract address in receipt')
}
return receipt.contractAddress
}
async function main() {
const client = getViemClient(config.localRpcUrl);
const accountAddress = toViemAddress(privateKeyToAccount(config.privateKey).address) as `0x${string}`
const balance = await client.getBalance({
address: accountAddress,
})
console.log("Account Balance:", ethers.formatEther(balance), "ETH");
const blockNumber = await client.getBlockNumber()
console.log("Block Number:", blockNumber);
const nonce = await client.getTransactionCount({ address: accountAddress })
console.log("Nonce:", nonce);
// const txHash = await walletClient.sendTransaction({ to: config.accountAddress2, value: hexToBigInt('0x10000') })
const txHash = await walletClient.sendTransaction({ to: config.accountAddress2, value: BigInt(10_000_000_000_000_000) })
console.log("Transaction Hash:", txHash);
const txReceipt = await client.waitForTransactionReceipt({ hash: txHash })
console.log("Transaction Receipt:", txReceipt);
const balanceAfter = await client.getBalance({ address: accountAddress })
console.log("Account Balance After:", ethers.formatEther(balanceAfter), "ETH");
const balance2 = await client.getBalance({ address: config.accountAddress2 })
console.log("Account Balance2:", ethers.formatEther(balance2), "ETH");
const contractAddress = await deployContract() as `0x${string}`
console.log("Contract Address:", contractAddress);
const retrieve = await client.readContract({
address: contractAddress,
abi: ABI,
functionName: 'retrieve',
args: [],
}) as bigint
console.log("Retrieved Value:", retrieve.toString());
const deployedContract = getContract({ address: contractAddress, abi: ABI, client: walletClient })
const storeTx = await deployedContract.write.store([10000])
console.log("Store Transaction Hash:", storeTx);
const receipt = await client.waitForTransactionReceipt({ hash: storeTx })
console.log("Store Transaction Receipt:", receipt);
const newRetrieve = await client.readContract({
address: contractAddress,
abi: ABI,
functionName: 'retrieve',
args: [],
}) as bigint
console.log("Retrieved Value:", newRetrieve.toString());
client.watchBlockNumber({
onBlockNumber: (blockNumber) => {
console.log(`block is ${blockNumber}`)
},
onError: (error) => {
console.error(`error is ${error}`)
}
})
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
这段代码是一个使用 viem 库与本地以太坊区块链交互的 TypeScript 脚本,用于部署智能合约、执行交易和与合约交互。以下是代码的逐部分解释:
导入和初始化
import { createPublicClient, createWalletClient, defineChain, http, hexToBigInt, getContract } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { ABI, BYTECODE } from "../abi/storage";
import { ethers } from "ethers";
import config from '../config';
- viem: 一个现代以太坊客户端库,用于与区块链交互。createPublicClient、createWalletClient 等函数用于创建读取和写入区块链的客户端。
- privateKeyToAccount: 将私钥转换为账户对象,用于签名交易。
- ABI 和 BYTECODE: 从 ../abi/storage 导入,分别是智能合约的应用程序二进制接口(ABI)和字节码。
- ethers: 仅用于格式化以太币单位(例如将 wei 转换为 ETH)。
- config: 环境变了配置文件,包含 privateKey(私钥)、localRpcUrl(本地 RPC 地址)、accountAddress2(另一个账户地址)等。
链定义
export const localChain = (url: string) => defineChain({
id: 31337,
name: 'Testnet',
network: 'Testnet',
nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
rpcUrls: { default: { http: [url] } },
testnet: true,
})
- 定义了一个自定义链,链 ID 为 31337(Hardhat/Anvil 常用的本地开发链 ID)。
- 配置链的名称(Testnet)、货币(ETH,18 位小数)、RPC URL(通过 url 参数传入)。
- 标记为测试网。
工具函数
function toViemAddress(address: string): string {
return address.startsWith("0x") ? address : `0x${address}`
}
- 确保地址以 0x 开头。如果没有,添加 0x 前缀,使其成为有效的以太坊地址格式。
export function getViemClient(url: string) {
return createPublicClient({
chain: localChain(url),
transport: http(url),
})
}
- 创建一个 PublicClient,用于只读的区块链交互(例如查询余额、读取合约状态)。
- 使用 localChain 和 HTTP 传输协议,通过提供的 url 连接到区块链。
export function remove0x(privateKey: string): string {
return privateKey.startsWith('0x') ? privateKey.slice(2) : privateKey;
}
- 如果私钥以 0x 开头,移除该前缀,用于规范化私钥格式。
钱包客户端设置
const privateKey = remove0x(config.privateKey);
console.log("privateKey: ", privateKey);
const walletClient = createWalletClient({
chain: localChain(config.localRpcUrl),
transport: http(config.localRpcUrl),
account: privateKeyToAccount(config.privateKey as `0x${string}`),
})
- 从 config.privateKey 中移除 0x 前缀并打印私钥(仅用于调试,生产环境中应避免)。
- 创建一个 WalletClient,用于签名和发送交易:
- 使用 localChain 和 config.localRpcUrl 配置链。
- 使用 privateKeyToAccount 从私钥生成账户对象。
合约部署
export async function deployContract(): Promise<string> {
const hash = await walletClient.deployContract({
abi: ABI,
bytecode: `0x${BYTECODE}`,
args: []
})
const publicClient = getViemClient(config.localRpcUrl)
const receipt = await publicClient.waitForTransactionReceipt({ hash })
if (!receipt.contractAddress) {
throw new Error('Contract deployment failed: no contract address in receipt')
}
return receipt.contractAddress
}
- 使用 walletClient.deployContract 部署智能合约:
- abi: 合约的 ABI(来自 ../abi/storage)。
- bytecode: 合约的字节码(添加 0x 前缀)。
- args: 没有构造函数参数(空数组)。
- 使用 publicClient.waitForTransactionReceipt 等待交易确认并获取收据。
- 检查收据中是否包含 contractAddress,如果没有则抛出错误。
- 返回部署的合约地址。
主函数
main 函数是脚本的入口,执行一系列区块链操作:
- 创建公共客户端
const client = getViemClient(config.localRpcUrl);
- 初始化一个 PublicClient,用于与本地区块链交互。
- 查询账户余额
const accountAddress = toViemAddress(privateKeyToAccount(config.privateKey).address) as `0x${string}`
const balance = await client.getBalance({ address: accountAddress })
console.log("Account Balance:", ethers.formatEther(balance), "ETH");
- 从私钥派生账户地址。
- 查询账户余额,并使用 ethers.formatEther 将 wei 转换为 ETH 单位并打印。
- 获取区块高度
const blockNumber = await client.getBlockNumber()
console.log("Block Number:", blockNumber);
- 查询区块链的最新区块高度并打印。
- 获取交易计数(Nonce)
const nonce = await client.getTransactionCount({ address: accountAddress })
console.log("Nonce:", nonce);
- 查询账户的交易计数(nonce),表示该账户发送的交易数量。
- 发送交易
const txHash = await walletClient.sendTransaction({ to: config.accountAddress2, value: BigInt(10_000_000_000_000_000) })
console.log("Transaction Hash:", txHash);
- 从钱包账户向 config.accountAddress2 发送一笔交易。
- 转账金额为 0.01 ETH(即 10,000,000,000,000,000 wei)。
- 打印交易哈希。
- 等待交易收据
const txReceipt = await client.waitForTransactionReceipt({ hash: txHash })
console.log("Transaction Receipt:", txReceipt);
- 等待交易被挖矿并获取交易收据(包含 gas 使用情况、交易状态等信息)。
- 打印收据。
- 检查交易后余额
const balanceAfter = await client.getBalance({ address: accountAddress })
console.log("Account Balance After:", ethers.formatEther(balanceAfter), "ETH");
const balance2 = await client.getBalance({ address: config.accountAddress2 })
console.log("Account Balance2:", ethers.formatEther(balance2), "ETH");
- 查询发送账户(accountAddress)和接收账户(config.accountAddress2)的余额。
- 将余额从 wei 转换为 ETH 并打印。
- 部署合约
const contractAddress = await deployContract() as `0x${string}`
console.log("Contract Address:", contractAddress);
- 调用 deployContract 部署智能合约,并打印合约地址。
- 读取合约状态
const retrieve = await client.readContract({
address: contractAddress,
abi: ABI,
functionName: 'retrieve',
args: [],
}) as bigint
console.log("Retrieved Value:", retrieve.toString());
- 调用合约的 retrieve 函数(可能是存储合约的获取函数)。
- 假设返回值为 bigint 类型,转换为字符串并打印。
- 与合约交互(写入)
const deployedContract = getContract({ address: contractAddress, abi: ABI, client: walletClient })
const storeTx = await deployedContract.write.store([10000])
console.log("Store Transaction Hash:", storeTx);
- 使用 getContract 创建合约实例,以便与部署的合约交互。
- 调用合约的 store 函数,传入参数 10000(可能是更新存储值)。
- 打印交易哈希。
- 等待存储交易收据
const receipt = await client.waitForTransactionReceipt({ hash: storeTx })
console.log("Store Transaction Receipt:", receipt);
- 等待 store 交易被挖矿并获取收据。
- 打印收据。
- 再次读取合约状态
const newRetrieve = await client.readContract({
address: contractAddress,
abi: ABI,
functionName: 'retrieve',
args: [],
}) as bigint
console.log("Retrieved Value:", newRetrieve.toString());
- 再次调用 retrieve 检查合约的更新状态(应反映 store 设置的值 10000)。
- 打印新值。
- 监听新区块
client.watchBlockNumber({
onBlockNumber: (blockNumber) => {
console.log(`block is ${blockNumber}`)
},
onError: (error) => {
console.error(`error is ${error}`)
}
})
- 设置监听器,实时打印新区块的高度。
- 包含错误处理,打印任何监听错误。
错误处理和执行
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
- 执行 main 函数并捕获任何错误。
- 如果发生错误,打印错误并将进程退出码设为 1(表示失败)。
代码功能总结
- 连接到本地以太坊区块链(例如 Hardhat 节点)。
- 查询账户信息(余额、nonce、区块高度)。
- 发送一笔 0.01 ETH 的交易到另一个地址。
- 部署智能合约。
- 通过读取状态、更新状态和验证更新与合约交互。
- 实时监控新区块。
运行
➜ ts-node src/viem/index.ts
privateKey: ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
Account Balance: 10000.0 ETH
Block Number: 0n
Nonce: 0
Transaction Hash: 0xdea4e55c8911c8aea966eca343a80d984d65637ec449d36582040c052534ccb6
Transaction Receipt: {
type: 'eip1559',
status: 'success',
cumulativeGasUsed: 21000n,
logs: [],
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
transactionHash: '0xdea4e55c8911c8aea966eca343a80d984d65637ec449d36582040c052534ccb6',
transactionIndex: 0,
blockHash: '0x47016456f9bafcb20744e43fa3790199fb3e31d3026697e7b22ed0febd11a2bc',
blockNumber: 1n,
gasUsed: 21000n,
effectiveGasPrice: 2000000000n,
blobGasPrice: 1n,
from: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
contractAddress: null
}
Account Balance After: 9999.989958 ETH
Account Balance2: 10000.01 ETH
Contract Address: 0xe7f1725e7734ce288f8367e1bb143e90bb3f0512
Retrieved Value: 0
Store Transaction Hash: 0x2ab751740067986e36f401cfafddaf80214574aa7b1ad808051e23a46581d13f
Store Transaction Receipt: {
type: 'eip1559',
status: 'success',
cumulativeGasUsed: 43730n,
logs: [],
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
transactionHash: '0x2ab751740067986e36f401cfafddaf80214574aa7b1ad808051e23a46581d13f',
transactionIndex: 0,
blockHash: '0x6f78d44d1c8262391d4ba1cad0cc274907c58abd4abe124f2cd549321d8fb2fc',
blockNumber: 3n,
gasUsed: 43730n,
effectiveGasPrice: 1766675216n,
blobGasPrice: 1n,
from: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
to: '0xe7f1725e7734ce288f8367e1bb143e90bb3f0512',
contractAddress: null
}
Retrieved Value: 10000
block is 3
合约代码
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;
/**
* @title Storage
* @dev Store & retrieve value in a variable
* @custom:dev-run-script ./scripts/deploy_with_ethers.ts
*/
contract Storage {
uint256 number;
/**
* @dev Store value in variable
* @param num value to store
*/
function store(uint256 num) public {
number = num;
}
/**
* @dev Return value
* @return value of 'number'
*/
function retrieve() public view returns (uint256) {
return number;
}
}
总结
通过这场 Web3 冒险,我们用 Viem 库解锁了以太坊开发的完整流程:从搭建本地测试网到部署 Storage 合约,再到交易和状态交互,每一步都清晰可见。Viem 的简洁高效让区块链开发不再遥不可及!无论你是想初探 Web3 还是寻找更顺手的工具,这篇教程都为你点亮了一盏明灯。快动手试试,结合代码和参考资源,继续探索 Web3 的无限可能吧!