Viem 使用指南

  • 曲弯
  • 发布于 1天前
  • 阅读 25

Viem完全指南(个人总结记录)Viem简介Viem是一个类型安全、轻量级的以太坊TypeScript接口库,提供比ethers.js和web3.js更好的开发者体验。核心特性​类型安全:完整的TypeScript支持​轻量级:比传统库小70%以上​模块

<!--StartFragment-->

Viem 完全指南

Viem 简介

Viem 是一个类型安全、轻量级的以太坊 TypeScript 接口库,提供比 ethers.js 和 web3.js 更好的开发者体验。

核心特性

  • 类型安全: 完整的 TypeScript 支持
  • 轻量级: 比传统库小 70% 以上
  • 模块化: 按需导入,减少打包体积
  • 多链支持: 内置主流链配置
  • 钱包集成: 支持多种钱包连接方式

安装与配置

基础安装

# 使用 npm
npm install viem

# 使用 yarn
yarn add viem

# 使用 pnpm
pnpm add viem

TypeScript 配置

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM"],
    "module": "ESNext",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  }
}

客户端创建

公共客户端(只读操作)

import { createPublicClient, http } from 'viem'
import { mainnet, sepolia, polygon } from 'viem/chains'

// 主网客户端
const mainnetClient = createPublicClient({
  chain: mainnet,
  transport: http()
})

// Sepolia 测试网
const sepoliaClient = createPublicClient({
  chain: sepolia,
  transport: http('https://eth-sepolia.g.alchemy.com/v2/your-api-key')
})

// 多 RPC 端点(故障转移)
const robustClient = createPublicClient({
  chain: mainnet,
  transport: http([
    'https://eth-mainnet.g.alchemy.com/v2/your-api-key',
    'https://eth-mainnet.public.blastapi.io',
    'https://cloudflare-eth.com'
  ], {
    retryCount: 3,
    retryDelay: 1000
  })
})

钱包客户端(可签名操作)

import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { mainnet } from 'viem/chains'

// 使用私钥创建钱包客户端
const privateKey = '0x...' // 你的私钥
const account = privateKeyToAccount(privateKey)

const walletClient = createWalletClient({
  account,
  chain: mainnet,
  transport: http()
})

// 浏览器钱包(MetaMask 等)
const browserWalletClient = createWalletClient({
  chain: mainnet,
  transport: custom(window.ethereum
})

测试客户端

import { createTestClient, http } from 'viem'
import { foundry } from 'viem/chains'

// 用于本地测试(Anvil、Hardhat)
const testClient = createTestClient({
  chain: foundry,
  mode: 'anvil',
  transport: http('http://localhost:8545')
})

// 常用测试操作
await testClient.setNextBlockTimestamp({ timestamp: 1678883200 })
await testClient.mine({ blocks: 1 })
await testClient.impersonateAccount({ address: '0x...' })

链上数据读取

基础数据查询

import { parseEther, formatEther } from 'viem'

// 获取余额
const balance = await publicClient.getBalance({
  address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
})
console.log('余额:', formatEther(balance), 'ETH')

// 获取区块信息
const block = await publicClient.getBlock()
console.log('最新区块:', block.number, block.hash)

// 获取交易信息
const transaction = await publicClient.getTransaction({
  hash: '0x...'
})

// 获取交易收据
const receipt = await publicClient.getTransactionReceipt({
  hash: '0x...'
})

批量查询

// 批量获取多个地址的余额
const addresses = [
  '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
  '0x742d35Cc6634C0532925a3b8D5C9eA6A2A3b8D5C'
]

const balances = await Promise.all(
  addresses.map(address => publicClient.getBalance({ address }))
)

// 使用 multicall 批量调用(更高效)
import { createPublicClient, http, parseAbi } from 'viem'
import { mainnet } from 'viem/chains'

const client = createPublicClient({
  chain: mainnet,
  transport: http()
})

const erc20Abi = parseAbi([
  'function balanceOf(address) view returns (uint256)',
  'function totalSupply() view returns (uint256)'
])

const results = await client.multicall({
  contracts: [
    {
      address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI
      abi: erc20Abi,
      functionName: 'balanceOf',
      args: ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045']
    },
    {
      address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
      abi: erc20Abi,
      functionName: 'totalSupply'
    }
  ]
})

事件日志查询

// 查询历史事件
const logs = await publicClient.getLogs({
  address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', // UNI
  event: parseAbiItem('event Transfer(address indexed, address indexed, uint256)'),
  fromBlock: 18000000n,
  toBlock: 18001000n
})

// 过滤特定事件
const filteredLogs = await publicClient.getLogs({
  event: parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)'),
  args: {
    from: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
  }
})

交易发送

基础交易

import { parseEther } from 'viem'

// 发送 ETH
const hash = await walletClient.sendTransaction({
  to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
  value: parseEther('0.1')
})

console.log('交易哈希:', hash)

// 等待交易确认
const receipt = await publicClient.waitForTransactionReceipt({ 
  hash 
})

if (receipt.status === 'success') {
  console.log('交易成功!')
} else {
  console.log('交易失败!')
}

智能合约交易

import { parseAbi } from 'viem'

const erc20Abi = parseAbi([
  'function transfer(address to, uint256 amount) returns (bool)',
  'function approve(address spender, uint256 amount) returns (bool)'
])

// ERC20 转账
const hash = await walletClient.writeContract({
  address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI
  abi: erc20Abi,
  functionName: 'transfer',
  args: [
    '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
    parseEther('100')
  ]
})

交易参数配置

// 自定义 gas 参数
const hash = await walletClient.sendTransaction({
  to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
  value: parseEther('0.1'),
  gas: 21000n,
  maxFeePerGas: parseGwei('30'),
  maxPriorityFeePerGas: parseGwei('1.5')
})

// 使用自定义 nonce
const nonce = await publicClient.getTransactionCount({
  address: walletClient.account.address
})

const hash = await walletClient.sendTransaction({
  to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
  value: parseEther('0.1'),
  nonce: nonce
})

合约交互

合约读取

import { parseAbi } from 'viem'

const uniswapV3PoolAbi = parseAbi([
  'function token0() view returns (address)',
  'function token1() view returns (address)',
  'function fee() view returns (uint24)',
  'function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)'
])

// 读取合约状态
const token0 = await publicClient.readContract({
  address: '0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640', // USDC/WETH Pool
  abi: uniswapV3PoolAbi,
  functionName: 'token0'
})

const slot0 = await publicClient.readContract({
  address: '0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640',
  abi: uniswapV3PoolAbi,
  functionName: 'slot0'
})

console.log('当前价格:', slot0.sqrtPriceX96)

合约写入

// 复杂的合约交互
const uniswapRouterAbi = parseAbi([
  'function exactInputSingle(tuple(address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) payable returns (uint256 amountOut)'
])

const hash = await walletClient.writeContract({
  address: '0xE592427A0AEce92De3Edee1F18E0157C05861564', // Uniswap V3 Router
  abi: uniswapRouterAbi,
  functionName: 'exactInputSingle',
  args: [{
    tokenIn: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
    tokenOut: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH
    fee: 500,
    recipient: walletClient.account.address,
    deadline: BigInt(Math.floor(Date.now() / 1000) + 60 * 20), // 20分钟后过期
    amountIn: parseUnits('1000', 6), // 1000 USDC (6 decimals)
    amountOutMinimum: parseEther('0.5'), // 最少接收 0.5 ETH
    sqrtPriceLimitX96: 0n
  }],
  value: 0n // 不是支付 ETH 的交易
})

事件监听

// 实时监听合约事件
const unwatch = publicClient.watchContractEvent({
  address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', // UNI
  abi: parseAbi(['event Transfer(address indexed, address indexed, uint256)']),
  eventName: 'Transfer',
  onLogs: (logs) => {
    logs.forEach((log) => {
      console.log('转账事件:', {
        发送方: log.args[0],
        接收方: log.args[1],
        金额: log.args[2],
        交易哈希: log.transactionHash
      })
    })
  }
})

// 停止监听
// unwatch()

签名与验证

消息签名

import { hashMessage, verifyMessage } from 'viem'

// 签名文本消息
const signature = await walletClient.signMessage({
  message: 'Hello from Viem!'
})

// 验证签名
const isValid = await verifyMessage({
  address: walletClient.account.address,
  message: 'Hello from Viem!',
  signature
})

// 签名结构化数据(EIP-712)
const domain = {
  name: 'Ether Mail',
  version: '1',
  chainId: 1,
  verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC'
} as const

const types = {
  Person: [
    { name: 'name', type: 'string' },
    { name: 'wallet', type: 'address' }
  ],
  Mail: [
    { name: 'from', type: 'Person' },
    { name: 'to', type: 'Person' },
    { name: 'contents', type: 'string' }
  ]
} as const

const message = {
  from: {
    name: 'Cow',
    wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'
  },
  to: {
    name: 'Bob',
    wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'
  },
  contents: 'Hello, Bob!'
} as const

const signature = await walletClient.signTypedData({
  domain,
  types,
  primaryType: 'Mail',
  message
})

交易签名

// 签名未发送的交易
const signedTransaction = await walletClient.signTransaction({
  to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
  value: parseEther('0.1')
})

// 以后可以发送已签名的交易
const hash = await publicClient.sendRawTransaction({
  serializedTransaction: signedTransaction
})

多链支持

多链配置

import { createPublicClient, http, createWalletClient } from 'viem'
import { 
  mainnet, 
  sepolia, 
  polygon, 
  arbitrum, 
  optimism,
  bsc 
} from 'viem/chains'

// 多链客户端工厂
function createChainClient(chain) {
  return createPublicClient({
    chain,
    transport: http()
  })
}

const clients = {
  eth: createChainClient(mainnet),
  sep: createChainClient(sepolia),
  poly: createChainClient(polygon),
  arb: createChainClient(arbitrum),
  opt: createChainClient(optimism),
  bsc: createChainClient(bsc)
}

// 跨链余额查询
async function getMultichainBalances(address) {
  const results = await Promise.allSettled(
    Object.entries(clients).map(async ([chainName, client]) => {
      const balance = await client.getBalance({ address })
      return { chain: chainName, balance }
    })
  )

  return results
    .filter(result => result.status === 'fulfilled')
    .map(result => result.value)
}

链切换

import { switchChain } from 'viem/actions'

// 在钱包中切换链
await switchChain(walletClient, { chain: polygon })

// 检查当前链
const currentChain = await walletClient.getChainId()

// 添加自定义链
await walletClient.addChain({
  chain: {
    id: 12345,
    name: 'Custom Chain',
    network: 'custom',
    nativeCurrency: {
      decimals: 18,
      name: 'Ether',
      symbol: 'ETH',
    },
    rpcUrls: {
      default: { http: ['https://custom.chain.rpc'] },
      public: { http: ['https://custom.chain.rpc'] },
    },
  }
})

错误处理

基础错误处理

// 使用 try-catch 包装操作
async function safeContractCall(contractCall) {
  try {
    const result = await contractCall()
    return { success: true, data: result }
  } catch (error) {
    console.error('合约调用失败:', error)

    // 分类处理不同错误
    if (error.message.includes('insufficient funds')) {
      return { success: false, error: '余额不足' }
    } else if (error.message.includes('user rejected')) {
      return { success: false, error: '用户拒绝交易' }
    } else if (error.message.includes('execution reverted')) {
      return { success: false, error: '合约执行失败' }
    } else {
      return { success: false, error: '未知错误' }
    }
  }
}

// 使用示例
const result = await safeContractCall(() => 
  publicClient.readContract({
    address: '0x...',
    abi: erc20Abi,
    functionName: 'balanceOf',
    args: ['0x...']
  })
)

重试机制

async function withRetry(operation, maxRetries = 3, delay = 1000) {
  for (let attempt = 1; attempt &lt;= maxRetries; attempt++) {
    try {
      return await operation()
    } catch (error) {
      if (attempt === maxRetries) throw error

      console.log(`尝试 ${attempt} 失败,${delay}ms 后重试...`)
      await new Promise(resolve => setTimeout(resolve, delay * attempt))
    }
  }
}

// 使用重试机制
const balance = await withRetry(() => 
  publicClient.getBalance({ 
    address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' 
  })
)

性能优化

批量操作

// 使用 multicall 批量读取
async function getMultipleBalances(tokenAddresses, userAddress) {
  const contracts = tokenAddresses.map(address => ({
    address,
    abi: parseAbi(['function balanceOf(address) view returns (uint256)']),
    functionName: 'balanceOf',
    args: [userAddress]
  }))

  const results = await publicClient.multicall({ contracts })
  return results.map((result, index) => ({
    token: tokenAddresses[index],
    balance: result.result
  }))
}

缓存策略

// 简单的内存缓存
class CacheManager {
  private cache = new Map()
  private ttl = 60000 // 1分钟缓存

  async getOrSet(key, operation) {
    const cached = this.cache.get(key)

    if (cached && Date.now() - cached.timestamp &lt; this.ttl) {
      return cached.data
    }

    const data = await operation()
    this.cache.set(key, { data, timestamp: Date.now() })
    return data
  }
}

const cache = new CacheManager()

// 使用缓存的余额查询
const balance = await cache.getOrSet(
  `balance-${address}`,
  () => publicClient.getBalance({ address })
)

请求优化

// 使用 WebSocket 实时连接
import { webSocket } from 'viem'

const wsClient = createPublicClient({
  chain: mainnet,
  transport: webSocket('wss://eth-mainnet.g.alchemy.com/v2/your-api-key')
})

// 请求去重
class RequestDeduplicator {
  private pending = new Map()

  async dedupe(key, operation) {
    if (this.pending.has(key)) {
      return this.pending.get(key)
    }

    const promise = operation().finally(() => {
      this.pending.delete(key)
    })

    this.pending.set(key, promise)
    return promise
  }
}

实战项目

1. 代币交换监控器

class TokenSwapMonitor {
  private unwatchers: (() => void)[] = []

  constructor(private publicClient, private dexRouterAddress) {}

  startMonitoring(pairs) {
    const abi = parseAbi([
      'event Swap(address indexed sender, address indexed to, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out)'
    ])

    pairs.forEach(pairAddress => {
      const unwatch = this.publicClient.watchContractEvent({
        address: pairAddress,
        abi,
        eventName: 'Swap',
        onLogs: this.handleSwap.bind(this)
      })

      this.unwatchers.push(unwatch)
    })
  }

  private handleSwap(logs) {
    logs.forEach(log => {
      const { sender, to, amount0In, amount1In, amount0Out, amount1Out } = log.args

      console.log('检测到交换:', {
        交易者: sender,
        接收者: to,
        输入金额: { amount0In, amount1In },
        输出金额: { amount0Out, amount1Out },
        交易哈希: log.transactionHash
      })
    })
  }

  stop() {
    this.unwatchers.forEach(unwatch => unwatch())
  }
}

2. 多链资产看板

class MultiChainDashboard {
  constructor(private chains) {}

  async getPortfolio(address) {
    const portfolio = await Promise.all(
      this.chains.map(async (chain) => {
        const client = createPublicClient({
          chain: chain,
          transport: http()
        })

        const nativeBalance = await client.getBalance({ address })
        // 这里可以添加 ERC20 代币余额查询

        return {
          chain: chain.name,
          nativeBalance,
          tokens: [] // 代币列表
        }
      })
    )

    return portfolio
  }
}

3. Gas 价格优化器

class GasOptimizer {
  constructor(private publicClient) {}

  async getOptimalGasPrice() {
    const [block, feeHistory] = await Promise.all([
      this.publicClient.getBlock(),
      this.publicClient.getFeeHistory({
        blockCount: 10,
        rewardPercentiles: [25, 50, 75]
      })
    ])

    const baseFee = block.baseFeePerGas || 0n
    const priorityFees = feeHistory.reward.flat()
    const averagePriorityFee = priorityFees.reduce((a, b) => a + b, 0n) / BigInt(priorityFees.length)

    return {
      maxFeePerGas: baseFee * 2n + averagePriorityFee,
      maxPriorityFeePerGas: averagePriorityFee
    }
  }
}

迁移指南

从 ethers.js 迁移

// ethers.js 代码
import { ethers } from 'ethers'
const provider = new ethers.providers.JsonRpcProvider('https://...')
const balance = await provider.getBalance(address)
const tx = await signer.sendTransaction({ to, value })

// 对应的 Viem 代码
import { createPublicClient, http, createWalletClient } from 'viem'
const publicClient = createPublicClient({ transport: http('https://...') })
const balance = await publicClient.getBalance({ address })
const hash = await walletClient.sendTransaction({ to, value })

从 web3.js 迁移

// web3.js 代码
import Web3 from 'web3'
const web3 = new Web3('https://...')
const balance = await web3.eth.getBalance(address)
const tx = await web3.eth.sendTransaction({ to, value })

// 对应的 Viem 代码
import { createPublicClient, http, createWalletClient } from 'viem'
const publicClient = createPublicClient({ transport: http('https://...') })
const balance = await publicClient.getBalance({ address })
const hash = await walletClient.sendTransaction({ to, value })

最佳实践总结

  1. 类型安全: 充分利用 TypeScript 类型推断
  2. 错误处理: 对所有操作进行适当的错误处理
  3. 性能优化: 使用批量操作和缓存策略
  4. 链管理: 合理配置多链支持
  5. 资源清理: 及时清理事件监听器和定时器
  6. 安全考虑: 妥善管理私钥和签名操作

<!--EndFragment-->

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

0 条评论

请先 登录 后评论
曲弯
曲弯
0xb51E...CADb
江湖只有他的大名,没有他的介绍。