玩转 Web3:用 Viem 库实现以太坊合约部署与交互

玩转Web3:用Viem库实现以太坊合约部署与交互想一窥Web3开发的奥秘?以太坊智能合约是通往区块链世界的大门,而Viem库让你轻松迈出第一步!本文通过一个TypeScript脚本,带你从连接本地以太坊测试网到部署合约、实现交互,全程手把手实战。不管你是Web3新手还是想探

玩转 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 函数是脚本的入口,执行一系列区块链操作:

  1. 创建公共客户端
const client = getViemClient(config.localRpcUrl);
  • 初始化一个 PublicClient,用于与本地区块链交互。
  1. 查询账户余额
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 单位并打印。
  1. 获取区块高度
const blockNumber = await client.getBlockNumber()
console.log("Block Number:", blockNumber);
  • 查询区块链的最新区块高度并打印。
  1. 获取交易计数(Nonce)
const nonce = await client.getTransactionCount({ address: accountAddress })
console.log("Nonce:", nonce);
  • 查询账户的交易计数(nonce),表示该账户发送的交易数量。
  1. 发送交易
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)。
  • 打印交易哈希。
  1. 等待交易收据
const txReceipt = await client.waitForTransactionReceipt({ hash: txHash })
console.log("Transaction Receipt:", txReceipt);
  • 等待交易被挖矿并获取交易收据(包含 gas 使用情况、交易状态等信息)。
  • 打印收据。
  1. 检查交易后余额
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 并打印。
  1. 部署合约
const contractAddress = await deployContract() as `0x${string}`
console.log("Contract Address:", contractAddress);
  • 调用 deployContract 部署智能合约,并打印合约地址。
  1. 读取合约状态
const retrieve = await client.readContract({
    address: contractAddress,
    abi: ABI,
    functionName: 'retrieve',
    args: [],
}) as bigint
console.log("Retrieved Value:", retrieve.toString());
  • 调用合约的 retrieve 函数(可能是存储合约的获取函数)。
  • 假设返回值为 bigint 类型,转换为字符串并打印。
  1. 与合约交互(写入)
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(可能是更新存储值)。
  • 打印交易哈希。
  1. 等待存储交易收据
const receipt = await client.waitForTransactionReceipt({ hash: storeTx })
console.log("Store Transaction Receipt:", receipt);
  • 等待 store 交易被挖矿并获取收据。
  • 打印收据。
  1. 再次读取合约状态
const newRetrieve = await client.readContract({
    address: contractAddress,
    abi: ABI,
    functionName: 'retrieve',
    args: [],
}) as bigint
console.log("Retrieved Value:", newRetrieve.toString());
  • 再次调用 retrieve 检查合约的更新状态(应反映 store 设置的值 10000)。
  • 打印新值。
  1. 监听新区块
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(表示失败)。

代码功能总结

  1. 连接到本地以太坊区块链(例如 Hardhat 节点)。
  2. 查询账户信息(余额、nonce、区块高度)。
  3. 发送一笔 0.01 ETH 的交易到另一个地址。
  4. 部署智能合约。
  5. 通过读取状态、更新状态和验证更新与合约交互。
  6. 实时监控新区块。

运行

➜ 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 的无限可能吧!

参考

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

0 条评论

请先 登录 后评论
寻月隐君
寻月隐君
0x89EE...a439
不要放弃,如果你喜欢这件事,就不要放弃。如果你不喜欢,那这也不好,因为一个人不应该做自己不喜欢的事。