在本教程中,我们将使用 Viem typescript 库 + React (Next.js) 构建一个功能齐全的 Dapp。我们将涵盖连接钱包、转账加密货币、与智能合约交互(例如铸造 NFT)以及查询区块链的必要步骤。
Viem 是现有底层 Ethereum 接口(如 web3.js 和 ethers.js)的 Typescript 替代方案。它支持浏览器原生的 BigInt,并自动从 ABIs 和 EIP-712 中 推断类型 (infer types)。它具有 35kb 的打包体积、可摇树优化 (tree-shakable) 的设计以最小化最终的打包大小,以及 99.8% 的测试覆盖率。查看他们的 基准测试和完整文档。
本教程的结构十分简洁,解释说明通过内联代码注释提供。只需复制粘贴代码并阅读说明即可。
以下是本教程的大纲:
- Viem 术语解析:Client | Transport | Chain
- 快速入门:使用 React + Viem 设置 Client 与 Transport
- 第 1 部分:使用 React + Viem 连接 Web3 钱包
- 第 2 部分:使用 React + Viem 转账加密货币
- 第 3 部分:使用 React + Viem 铸造 NFT
你将要构建的项目展示:

Viem 术语解析:Client | Transport | Chain
Viem 有三个基本概念:Client、Transport 和 Chain。
- Viem 中的 Client 类似于 Ether.js 的 Provider。它提供了在 Ethereum 上执行常见操作的 typescript 函数。根据操作的不同,它将属于三种客户端类型之一。
- Public Client 是“公共” JSON RPC API 方法的接口,例如获取区块号、查询账户余额、访问智能合约上的 “view” 函数以及其他只读、不改变状态的操作。这些函数被称为 Public Actions。
- Wallet Client 是与 Ethereum 账户交互的接口,例如发送交易、签名消息、请求地址、切换链以及需要用户权限的操作。例如,铸造 NFT 会改变状态,因此这将在 wallet client 下完成。这些函数被称为 Wallet Actions。
- Test Client 用于创建模拟交易以进行测试。这通常用于单元测试中。
- Transport 与 Client 一起实例化,它代表执行请求的中间层。Transport 分为三种类型:
- HTTP Transport,利用 HTTP JSON-RPC;
- WebSocket Transport,通过 WebSocket JSON-RPC 进行实时连接;
- Custom Transport,通过 EIP-1193 请求方法处理请求;
- Fallback 允许你在列表中指定多个 transport。如果其中一个失败,它将沿列表向下查找可用的 transport。稍后将提供示例。
- Chain 指的是用于建立连接的 EVM 兼容链,它们通过 chain 对象(由 chain id 标识)进行标识。一个 client 只能同时实例化一个 chain。我们可以使用提供的 Viem chain 库,例如 polygon、eth mainnet 或者手动构建你自己的链。
Public Client
下面是如何声明一个 Public Client。
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const publicClient = createPublicClient({
chain: mainnet,
transport: http()
})
下面是如何使用 Public Actions。
const balance = await publicClient.getBalance({
address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
})
const block = await publicClient.getBlockNumber()
以下是可用的 Public Actions:
- getChainId
- getGasPrice
- signMessage
- verifyMessage
- getTransactionReceipt
- 更多内容请参阅 Public Action 文档
Viem 有一个名为 Optimization Public Client 的功能,支持 eth_call Aggregation,通过发送批量请求来提高性能。该功能的文档非常完善。
Wallet Client
下面是如何建立一个 Wallet Client:
import { createWalletClient, custom } from 'viem'
import { mainnet } from 'viem/chains'
const walletClient = createWalletClient({
chain: mainnet,transport: custom(window.ethereum)
})
下面是如何使用 Wallet Actions:
// Get's the user address
const [address] = await walletClient.getAddresses()
// Sends a transaction
const hash = await walletClient.sendTransaction({
account: address,
to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC',
value: parseEther('0.001')
})
可用的 wallet 函数:
- requestAddresses (Metamask 等钱包可能需要用户先调用 requestAddresses)
- switchChain
- signMessage
- getPermissions
- sendTransaction
- 更多内容请参阅 Wallet Action 文档
我们将在本教程中演示如何使用 sendTransaction 和 getAddresses。
可选内容
你可以使用 Public Actions 来扩展 Wallet Client。这有助于避免处理多个 client。以下代码片段使用 public actions 扩展了 wallet client。
import { createWalletClient, http, publicActions } from 'viem'
import { mainnet } from 'viem/chains'
const extendedClient = createWalletClient({
chain: mainnet,
transport: http()
}).extend(publicActions)
// Public Action
const block = await extendedClient.getBlockNumber()
// Wallet Action
const [address] = await extendedClient.getAddresses();
Test Client
Test Client 提供了一个接口,用于通过本地测试节点(如 Anvil 或 Hardhat)创建影子账户、挖掘区块和模拟交易。我们不会详细讨论这一点,但你可以阅读更多关于 Test Client 文档 的内容。
Transport
我们一次只能传递一种 transport(用于连接区块链的协议),以下是每种 transport 的使用示例。
HTTP
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const transport = http('https://eth-mainnet.g.alchemy.com/v2/...')
const client = createPublicClient({
chain: mainnet,
transport,
})
如果没有提供 url,transport 将回退到公共 RPC URL。建议传入经过身份验证的 RPC URL,以尽量减少速率限制(rate-limiting)问题。
WebSocket
import { createPublicClient, webSocket } from 'viem'
import { mainnet } from 'viem/chains'
const transport = webSocket('wss://eth-mainnet.g.alchemy.com/v2/...')
const client = createPublicClient({
chain: mainnet,
transport,
})
出于上述同样的原因,Transport 将回退到公共 RPC URL。
注意 在上述两个示例中,transport 是由为 transport 指定的 URL 类型决定的。第一个 URL 是 HTTPS URL,第二个是 WSS URL。
Custom (EIP-1193)(我们将使用这种方式)
该 transport 用于与提供 EIP-1193 provider 的注入式钱包集成,例如 WalletConnect、Coinbase SDK 和 Metamask。
import { createWalletClient, custom } from 'viem'
import { mainnet } from 'viem/chains'
const client = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum)
})
Fallback
此 transport 接受多个 Transport。如果一个 transport 失败,它将采取给定的下一种 transport 方法。在以下示例中,如果 Alchemy 失败,它将回退到 Infura。
import { createPublicClient, fallback, http } from 'viem'
import { mainnet } from 'viem/chains'
const alchemy = http('https://eth-mainnet.g.alchemy.com/v2/...')
const infura = http('https://mainnet.infura.io/v3/...')
const client = createPublicClient({
chain: mainnet,
transport: fallback([alchemy, infura]),
})
Chains
Viem 通过 viem/chains 库提供流行的 EVM 兼容链,例如:Polygon、Optimism、Avalanche,更多内容请参阅 Viem Chains 文档。
你可以通过将链作为参数传递来切换链,例如 polygonMumbai。
import { createPublicClient, http } from 'viem'
import { polygonMumbai } from 'viem/chains'
const client = createPublicClient({
chain: polygonMumbai,
transport: http(),
})
你也可以构建继承自 Chain 类型的自定义 chain 对象(来源:Viem.sh)。
import { Chain } from 'viem'
export const avalanche = {
id: 43_114,
name: 'Avalanche',
network: 'avalanche',
nativeCurrency: {
decimals: 18,
name: 'Avalanche',
symbol: 'AVAX',
},
rpcUrls: {
public: {
https: ['https://api.avax.network/ext/bc/C/rpc']
},
default: {
https: ['https://api.avax.network/ext/bc/C/rpc']
},
},
blockExplorers: {
etherscan: {
name: 'SnowTrace',
url: 'https://snowtrace.io'
},
default: {
name: 'SnowTrace',
url: 'https://snowtrace.io'
},
},
contracts: {multicall3: {
address: '0xca11bde05977b3631167028862be2a173976ca11',
blockCreated: 11_907_934,
},
},
} as const satisfies Chain
请记住,一次只能为 client 分配一个 chain。
快速入门:使用 React + Viem 设置 Client 与 Transport
为了简单起见,我们建议使用 polygonMumbai 或 Sepolia 作为你的测试网络。
第 1 步:创建 Next.js 项目并安装 Viem
首先使用以下命令创建你的 Next.js 项目:
npx create-next-app@latest myapp
为以下选项勾选 [yes]:
- Typescript
- ESLint
- Tailwind
- App Router (首选)
在 vscode 中打开你的项目。
使用以下命令之一安装 Viem:
npm i viem
pnpm i viem
yarn add viem
第 2 步:设置 Client 与 Transport
在 app 目录中,创建两个新文件:
- client.ts
- walletButton.tsx
你的 app 目录结构应该如下所示:
app
├── client.ts
├── globals.css
├── layout.tsx
├── page.tsx
└── walletButton.tsx
Client.ts
我们将在一个单独的 typescript 文件中初始化 Client 和 Transport。继续将以下代码复制粘贴到 client.ts 中。
// client.ts
import { createWalletClient, createPublicClient, custom, http } from "viem";
import { polygonMumbai, mainnet } from "viem/chains";
import "viem/window";
// Instantiate Public Client
const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
});
// Instantiate Wallet Clientconst walletClient = createWalletClient({
chain: polygonMumbai,
transport: custom(window.ethereum),
});
这不可避免地会抛出类型错误,因为某些浏览器(如 Safari)不支持 window.ethereum 对象,所以 window.ethereum 可能是 undefined。
我们可以通过检查 window.ethereum 是否存在或未定义来处理该错误。
// client.ts
import { createWalletClient, createPublicClient, custom, http } from "viem";
import { polygonMumbai } from "viem/chains";
import "viem/window";
export function ConnectWalletClient() {
// Check for window.ethereum
let transport;
if (window.ethereum) {
transport = custom(window.ethereum);
} else {
const errorMessage ="MetaMask or another web3 wallet is not installed. Please install one to proceed.";
throw new Error(errorMessage);
}
// Delcalre a Wallet Client
const walletClient = createWalletClient({
chain: polygonMumbai,
transport: transport,
});
return walletClient;
}
export function ConnectPublicClient() {
// Check for window.ethereum
let transport;
if (window.ethereum) {
transport = custom(window.ethereum);
} else {
const errorMessage ="MetaMask or another web3 wallet is not installed. Please install one to proceed.";
throw new Error(errorMessage);
}
// Delcare a Public Client
const publicClient = createPublicClient({
chain: polygonMumbai,
transport: transport,
});
return publicClient;
}
这仍然允许你在不支持 window.ethereum 的浏览器中打开该网站。
建议保持 walletClient 和 publicClient 的 chain 一致,否则你可能会遇到不兼容的 chain 错误。
第 1 部分:使用 React + Viem 连接 Web3 钱包
本节演示 Viem Client 如何连接到你的 Web3 钱包。
第 2 步:创建一个连接 Web3 钱包的按钮
walletButton.tsx
现在我们将创建一个 client component,用于处理连接 Web3 钱包的逻辑。
该按钮实例化一个 walletClient 并请求用户的钱包地址,如果钱包尚未连接,它将提示连接,最后输出其地址。
这里的代码很多,但请重点关注 handleClick() 函数。
// walletButton.tsx
"use client";
import { useState } from "react";
import { ConnectWalletClient, ConnectPublicClient } from "./client";
export default function WalletButton() {
//State variables for address & balance
const [address, setAddress] = useState<string | null>(null);
const [balance, setBalance] = useState<BigInt>(BigInt(0));
// Function requests connection and retrieves the address of wallet
// Then it retrievies the balance of the address
// Finally it updates the value for address & balance variable
async function handleClick() {
try {
// Instantiate a Wallet & Public Client
const walletClient = ConnectWalletClient();
const publicClient = ConnectPublicClient();
// Performs Wallet Action to retrieve wallet address
const [address] = await walletClient.getAddresses();
// Performs Public Action to retrieve address balance
const balance = await publicClient.getBalance({ address });
// Update values for address & balance state variable
setAddress(address);
setBalance(balance);
} catch (error) {
// Error handling
alert(`Transaction failed: ${error}`);
}
}
// Unimportant Section Below / Nice to Have UI
return (
<>
<Status address={address} balance={balance} />
<button className="px-8 py-2 rounded-md bg-[#1e2124] flex flex-row items-center justify-center border border-[#1e2124] hover:border hover:border-indigo-600 shadow-md shadow-indigo-500/10"
onClick={handleClick}
>
<img src="https://upload.wikimedia.org/wikipedia/commons/3/36/MetaMask_Fox.svg" alt="MetaMask Fox" style={{ width: "25px", height: "25px" }} />
<h1 className="mx-auto">Connect Wallet</h1>
</button></>);}
// Displays the wallet address once it’s successfuly connected
// You do not have to read it, it's just frontend stuff
function Status({
address,
balance,}: {
address: string | null;
balance: BigInt;
}) {
if (!address) {
return (
<div className="flex items-center">
<div className="border bg-red-600 border-red-600 rounded-full w-1.5 h-1.5 mr-2">
</div>
<div>Disconnected</div>
</div>);
}
return (
<div className="flex items-center w-full">
<div className="border bg-green-500 border-green-500 rounded-full w-1.5 h-1.5 mr-2"></div>
<div className="text-xs md:text-xs">{address} <br /> Balance: {balance.toString()}</div>
</div>
);
}
第 3 步:插入 walletButton 组件
page.tsx
剩下的就是设计主页面并导入 WalletButton 组件。我们注释掉了一些你稍后会添加的代码。
import WalletButton from "./walletButton";
// import MintButton from "./mintButton";
// import SendButton from "./sendButton";
export default function Home() {
return (
<main className="min-h-screen">
<div className="flex flex-col items-center justify-center h-screen ">
<a href="https://rareskills.io" target="_blank" className="text-white font-bold text-3xl hover:text-[#0044CC]" > Viem.sh </a>
<div className="h-[300px] min-w-[150px] flex flex-col justify-between backdrop-blur-2xl bg-[#290330]/30 rounded-lg mx-auto p-7 text-white border border-purple-950">
<WalletButton />
{/* <SendButton />
<MintButton /> */}
</div>
<a href="https://rareskills.io" target="_blank" className="text-white font-bold text-3xl hover:text-[#0044CC]" > Rareskills.io </a>
</div>
</main>
);
}
globals.css
一些漂亮的背景 UI,用以下代码替换 globals.css。
@tailwind base;@tailwind components;@tailwind utilities;
body {
background-color: #0c002e;
background-image: radial-gradient(
at 100% 100%,rgb(84, 2, 103) 0px,
transparent 50%),
radial-gradient(at 0% 0%, rgb(97, 0, 118) 0px, transparent 50%);}
第 4 步:运行网站并测试
当你点击该按钮时,它应该会启动与钱包的连接。一旦你授权,它看起来应该是这样的:
npm run dev

点击按钮后,将显示以下内容:

第 2 部分:使用 React + Viem 转账加密货币
现在我们的钱包已连接,可以开始转账加密货币了。我们将使用 sendTransaction 这项 wallet action。
- 从 Matic Faucet 获取一些 Matic。
第 5 步:添加加密货币转账功能
在 app 目录中创建一个新的 tsx 文件 sendButton.tsx。
app
├── client.ts
├── globals.css
├── layout.tsx
├── page.tsx
├── sendButton.tsx
└── walletButton.tsx
sendButton.tsx
我们将创建一个按钮来启动 sendTransaction 操作。Viem 让我们非常轻松地实现这一点。逻辑流程应该与 walletButton.tsx 相似,即实例化 walletClient 并执行 Wallet Client actions。
"use client";
import { parseEther } from "viem";
import { ConnectWalletClient} from "./client";
export default function SendButton() {
//Send Transaction Function
async function handleClick() {
try {
// Declare wallet client
const walletClient = ConnectWalletClient();
// Get the main wallet address
const [address] = await walletClient.getAddresses();
// sendTransaction is a Wallet action.
// It returns the transaction hash
// requires 3 parameters to transfer cryptocurrency,
// account, to and value
const hash = await walletClient.sendTransaction({
account: address,
to: "Account_Address",
value: parseEther("0.001"), // send 0.001 matic
});
// Display the transaction hash in an alert
alert(`Transaction successful. Transaction Hash: ${hash}`);
} catch (error) {
// Handle Error
alert(`Transaction failed: ${error}`);
}
}
return (
<button
className="py-2.5 px-2 rounded-md bg-[#1e2124] flex flex-row items-center justify-center border border-[#1e2124] hover:border hover:border-indigo-600 shadow-md shadow-indigo-500/10"
onClick={handleClick}>
Send Transaction
</button>
);
}
第 6 步:插入 sendButton 组件
page.tsx
取消关于 sendButton 组件代码行的注释。
import WalletButton from "./walletButton";
import SendButton from "./sendButton";
// import MintButton from "./mintButton";
export default function Home() {
return (
<main className="min-h-screen">
<div className="flex flex-col items-center justify-center h-screen ">
<a href="https://rareskills.io" target="_blank" className="text-white font-bold text-3xl hover:text-[#0044CC]" > Viem.sh </a>
<div className="h-[300px] min-w-[150px] flex flex-col justify-between backdrop-blur-2xl bg-[#290330]/30 rounded-lg mx-auto p-7 text-white border border-purple-950">
<WalletButton />
<SendButton />
{/* <MintButton /> */}
</div>
<a href="https://rareskills.io" target="_blank" className="text-white font-bold text-3xl hover:text-[#0044CC]" > Rareskills.io </a>
</div>
</main>
);
}
你的浏览器现在应该看起来像这样!

第 3 部分:使用 React + Viem 铸造 NFT
本节将讨论如何与智能合约交互,并通过铸造 NFT 提供示例。
要与智能合约交互,我们需要两样东西:
- 合约地址
- 合约 ABI
对于此示例,我们将使用 RareSkills 的合约进行演示,它有一个 Mint 函数,该函数除了记录你的铸造次数外不执行任何实际操作。
- RareSkills 的合约地址:0x7E6Ddd9dC419ee2F10eeAa8cBB72C215B9Eb5E23
- RareSkills 的合约 ABI
你也可以随时使用你自己的合约。
第 7 步:添加与智能合约交互的功能
创建两个新文件 abi.ts 和 mintButton.tsx。
app
├── abi.ts
├── client.ts
├── globals.css
├── layout.tsx
├── mintButton.tsx
├── page.tsx
├── sendButton.tsx
└── walletButton.tsx
abi.ts
复制并粘贴 RareSkills 的合约 ABI 或你自己的 ABI。
// abi.ts
export const wagmiAbi = [...contract abi...] as const;
请记住完全遵循此格式,不要忘记末尾的 “as const;”。
Contract Instance 与 Contract Action 方法
Contract Instance 方法
//Contract Instance
const contract = getContract({
address: "0x7E6Ddd9dC419ee2F10eeAa8cBB72C215B9Eb5E23",
abi: wagmiAbi,
publicClient,
walletClient,
});
getContract 函数创建我们的合约实例 contract。创建后,我们可以调用合约方法、监听事件等。这是一种更简单的方法,因为我们不必为了执行 contract action 而重复传递 address 和 abi 属性。
参数:
- address
- abi
- publicClient (可选)
- walletClient (可选)
我们必须传递 address 和 abi 参数。传递 publicClient 和 walletClient 是可选的,但这允许我们根据 client 的类型访问一组特定的合约方法。
publicClient 的可用合约方法:
walletClient 的可用合约方法:
通常,调用合约 Instance 方法遵循以下格式:
// function
contract.(estimateGas|read|simulate|write).(functionName)(args, options)
// event
contract.(createEventFilter|watchEvent).(eventName)(args, options)
使用 Contract Instance 调用合约方法
// Read Contract symbol
const symbol = await contract.read.symbol();
// Read Contract name
const name = await contract.read.name();
// Call mint method
const result = await contract.write.mint({account: address});
上面的示例通过 contract 实例调用读取和写入的合约方法。如果你使用的是 Type-script,它将自动完成有关可用合约方法的提示。
read.symbol() 和 read.name() 非常直观易懂。另一方面,写入函数:
const result = await contract.write.mint({account: address});
采用 {account: address} 作为其必需参数,其他所有参数都是可选的。如果你不知道需要添加哪些参数,可以将鼠标悬停在“mint()”关键字上,VS Code 会给你提示。
Contract Action 方法 —— 繁琐的方式
上一节中的代码是以下内容的语法糖。我们包含此部分是为了向你展示底层发生了什么。
此代码获取合约的 totalSupply:
const totalSupply = await publicClient.readContract({
address: '0x7E6Ddd9dC419ee2F10eeAa8cBB72C215B9Eb5E23',
abi: wagmiAbi,
functionName: 'totalSupply',
})
很繁琐对吧?你必须反复传递 address 和 abi。
这相当于使用 Contract Action 方法调用上面示例中的 mint 函数。成功时,它会返回交易哈希。
const hash = await walletClient.writeContract({
address: "0x7E6Ddd9dC419ee2F10eeAa8cBB72C215B9Eb5E23",
abi: wagmiAbi,
functionName: "mint",
account,
});
为了保持最小的打包体积,请使用 Contract Action;虽然 Contract instance 提供了更多功能,但它增加了内存消耗。
mintButton.tsx
为了演示改变状态的交易,我们将创建一个按钮,调用智能合约的 mint 函数,同时查询它的 name、symbol 和 totalSupply。
我们同时利用了 Contract Instance 和 Contract Action 方法来展示它是如何实现的。
"use client";
import { formatEther, getContract } from "viem";
import { wagmiAbi } from "./abi";
import { ConnectWalletClient, ConnectPublicClient } from "./client";
export default function MintButton() {
// Function to Interact With Smart Contract
async function handleClick() {
// Declare Client
const walletClient = ConnectWalletClient();
const publicClient = ConnectPublicClient();
// Create a Contract Instance
// Pass publicClient to perform Public Client Contract Methods
// Pass walletClient to perform Wallet Client Contract Methods
const contract = getContract({
address: "0x7E6Ddd9dC419ee2F10eeAa8cBB72C215B9Eb5E23",
abi: wagmiAbi,
publicClient,
walletClient,
});
// Reads the view state function symbol via Contract Instance method
const symbol = await contract.read.symbol();
// Reads the view state function name via Contract Instance method
const name = await contract.read.name();
// Reads the view state function symbol via Contract Action method
const totalSupply = await publicClient.readContract({
address: '0x7E6Ddd9dC419ee2F10eeAa8cBB72C215B9Eb5E23',
abi: wagmiAbi,
functionName: 'totalSupply',
})
// Format ether converts BigInt(Wei) to String(Ether)
const totalSupplyInEther = formatEther(totalSupply);
alert(`Symbol: ${symbol}\nName: ${name}\ntotalSupply: ${totalSupplyInEther}`);
try {
// Declare Wallet Client and Retrieve wallet address
const client = walletClient;
const [address] = await client.getAddresses();
// Writes the state-changin function mint via Contract Instance method.
const result = await contract.write.mint({
account: address
});
alert(`${result} ${name}`);
} catch (error) {
// Handle any errors that occur during the transaction
alert(`Transaction failed: ${error}`);
}}
return (
<>
<button
className="py-2.5 px-2 rounded-md bg-[#1e2124] flex flex-row items-center justify-center border border-[#1e2124] hover:border hover:border-indigo-600 shadow-md shadow-indigo-500/10"
onClick={handleClick}>
<svg
className="w-4 h-4 mr-2 -ml-1 text-[#626890]"
aria-hidden="true"
focusable="false"
data-prefix="fab"
data-icon="ethereum"
role="img"
xmlns="https://www.w3.org/2000/svg"
viewBox="0 0 320 512">
<path
fill="currentColor"
d="M311.9 260.8L160 353.6 8 260.8 160 0l151.9 260.8zM160 383.4L8 290.6 160 512l152-221.4-152 92.8z">
</path>
</svg>
<h1 className="text-center">Mint</h1>
</button>
</>
);
}
恭喜完成本教程!你的最终成品应该看起来像这样:

作者信息
本文由 RareSkills 研究实习生 Aymeric Taylor (LinkedIn, X) 共同编写。
最初发布于 2023 年 8 月 10 日