使用 Viem 构建命令行钱包:Web3 交易全流程技术指南

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

使用Viem构建命令行钱包:Web3交易全流程技术指南概述本文档详细阐述使用Viem库在Web3环境中构建命令行钱包的完整流程,涵盖从钱包创建到交易发送的四个核心环节。Viem是一个类型安全的以太坊交互库,提供低级别原语,适用于构建钱包、DApps和其他以太坊工具。环境准备

<!--StartFragment-->

使用 Viem 构建命令行钱包:Web3 交易全流程技术指南

概述

本文档详细阐述使用 Viem 库在 Web3 环境中构建命令行钱包的完整流程,涵盖从钱包创建到交易发送的四个核心环节。Viem 是一个类型安全的以太坊交互库,提供低级别原语,适用于构建钱包、DApps 和其他以太坊工具。

环境准备

安装依赖

# 创建项目并初始化
mkdir cli-wallet && cd cli-wallet
npm init -y

# 安装 Viem 和相关依赖
npm install viem dotenv

# 安装 TypeScript(可选但推荐)
npm install -D typescript @types/node tsx

项目结构

cli-wallet/
├── src/
│   ├── wallet.ts          # 钱包核心类
│   ├── commands.ts        # 命令行接口
│   └── utils.ts           # 工具函数
├── .env                   # 环境变量(私钥、RPC URL等)
├── tsconfig.json         # TypeScript 配置
└── package.json

核心概念

1. 账户体系

Viem 提供两种账户管理方式:

  • 外部持有账户(EOA)​​:由私钥控制的标准以太坊账户
  • 合约账户​:由智能合约代码控制的账户

2. 交易类型

  • 传统交易(Legacy)​​:包含 gasPrice 的标准交易
  • EIP-1559 交易​:包含 baseFee 和 priorityFee 的新型交易结构
  • EIP-2930 交易​:包含访问列表的交易类型

3. 客户端类型

  • 公共客户端​:用于读取链上数据
  • 钱包客户端​:用于签名和发送交易
  • 测试客户端​:用于本地开发测试

完整实现流程

第一步:创建钱包账号

1.1 从私钥创建账户

// src/wallet.ts
import { privateKeyToAccount, type PrivateKeyAccount } from 'viem/accounts'
import { type Hex } from 'viem'

export class WalletManager {
  private account: PrivateKeyAccount

  constructor(privateKey: Hex) {
    if (!privateKey.startsWith('0x') || privateKey.length !== 66) {
      throw new Error('无效的私钥格式。私钥应为66个字符(包含0x前缀)')
    }

    this.account = privateKeyToAccount(privateKey)
    console.log(`✅ 钱包创建成功`)
    console.log(`   地址: ${this.account.address}`)
  }

  getAddress(): Hex {
    return this.account.address
  }
}

// 使用示例
import 'dotenv/config'

const privateKey = process.env.PRIVATE_KEY as Hex
const wallet = new WalletManager(privateKey)

1.2 从助记词创建账户(BIP-39标准)

import { 
  generateMnemonic, 
  english, 
  mnemonicToAccount,
  type Mnemonic 
} from 'viem/accounts'

export class HDWalletManager {
  private mnemonic: string
  private account: PrivateKeyAccount

  constructor(phrase?: string) {
    // 生成或使用现有助记词
    this.mnemonic = phrase || generateMnemonic(english)

    // 从助记词派生第一个账户(路径:m/44'/60'/0'/0/0)
    this.account = mnemonicToAccount(this.mnemonic, {
      path: "m/44'/60'/0'/0/0"
    })
  }

  // 派生不同路径的账户
  deriveAccount(index: number = 0): PrivateKeyAccount {
    return mnemonicToAccount(this.mnemonic, {
      path: `m/44'/60'/0'/0/${index}`
    })
  }

  getMnemonic(): string {
    return this.mnemonic
  }
}

// 使用示例
const hdWallet = new HDWalletManager()
console.log('助记词:', hdWallet.getMnemonic())
console.log('主账户地址:', hdWallet.getAddress())

// 派生第五个账户
const account5 = hdWallet.deriveAccount(5)
console.log('派生账户地址:', account5.address)

第二步:初始化客户端

2.1 配置多链支持

// src/wallet.ts
import { 
  createWalletClient, 
  createPublicClient, 
  http, 
  type WalletClient,
  type PublicClient,
  type Chain
} from 'viem'
import { mainnet, sepolia, polygon, arbitrum } from 'viem/chains'

export interface ChainConfig {
  name: string
  chain: Chain
  rpcUrl: string
}

export class WalletClientManager {
  private walletClient: WalletClient
  private publicClient: PublicClient
  private currentChain: Chain

  constructor(
    account: PrivateKeyAccount,
    chainName: string = 'sepolia',
    rpcUrl?: string
  ) {
    // 选择链
    this.currentChain = this.selectChain(chainName)

    // 使用提供的 RPC URL 或默认值
    const transportUrl = rpcUrl || this.getDefaultRpcUrl(chainName)

    // 创建钱包客户端(用于签名和发送交易)
    this.walletClient = createWalletClient({
      account,
      chain: this.currentChain,
      transport: http(transportUrl)
    })

    // 创建公共客户端(用于读取数据)
    this.publicClient = createPublicClient({
      chain: this.currentChain,
      transport: http(transportUrl)
    })
  }

  private selectChain(chainName: string): Chain {
    const chains: Record&lt;string, Chain> = {
      'mainnet': mainnet,
      'sepolia': sepolia,
      'polygon': polygon,
      'arbitrum': arbitrum
    }

    const chain = chains[chainName.toLowerCase()]
    if (!chain) {
      throw new Error(`不支持的链: ${chainName}`)
    }

    return chain
  }

  private getDefaultRpcUrl(chainName: string): string {
    const urls: Record&lt;string, string> = {
      'mainnet': 'https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY',
      'sepolia': 'https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY',
      'polygon': 'https://polygon-mainnet.g.alchemy.com/v2/YOUR_API_KEY'
    }

    return urls[chainName.toLowerCase()] || ''
  }

  // 切换链
  async switchChain(newChainName: string, rpcUrl?: string): Promise&lt;void> {
    this.currentChain = this.selectChain(newChainName)
    const transportUrl = rpcUrl || this.getDefaultRpcUrl(newChainName)

    this.walletClient = createWalletClient({
      account: this.walletClient.account!,
      chain: this.currentChain,
      transport: http(transportUrl)
    })

    this.publicClient = createPublicClient({
      chain: this.currentChain,
      transport: http(transportUrl)
    })

    console.log(`✅ 已切换到 ${newChainName} 网络`)
  }

  getWalletClient(): WalletClient {
    return this.walletClient
  }

  getPublicClient(): PublicClient {
    return this.publicClient
  }

  getCurrentChain(): Chain {
    return this.currentChain
  }
}

第三步:构造交易

3.1 基础交易参数

// src/transaction.ts
import { 
  type Chain, 
  type Address, 
  type Hex,
  parseEther,
  parseGwei,
  encodeFunctionData,
  zeroAddress
} from 'viem'
import { erc20Abi } from 'viem/contracts'

export interface TransactionParams {
  to: Address
  value?: bigint
  data?: Hex
  gas?: bigint
  nonce?: number
  maxFeePerGas?: bigint
  maxPriorityFeePerGas?: bigint
  gasPrice?: bigint
  chainId?: number
}

export class TransactionBuilder {
  constructor(private chain: Chain) {}

  // 创建 ETH 转账交易
  createEthTransfer(
    to: Address,
    amount: string, // 以 ETH 为单位,例如 "0.1"
    options: Partial&lt;TransactionParams> = {}
  ): TransactionParams {
    return {
      to,
      value: parseEther(amount),
      gas: 21000n, // 标准 ETH 转账的 Gas 限制
      chainId: this.chain.id,
      ...options
    }
  }

  // 创建 ERC-20 代币转账交易
  createErc20Transfer(
    tokenAddress: Address,
    to: Address,
    amount: string, // 以代币最小单位为单位
    decimals: number = 18
  ): TransactionParams {
    // 计算实际金额
    const amountBigInt = BigInt(amount) * 10n ** BigInt(decimals)

    // 编码 transfer 函数调用
    const data = encodeFunctionData({
      abi: erc20Abi,
      functionName: 'transfer',
      args: [to, amountBigInt]
    })

    return {
      to: tokenAddress,
      value: 0n,
      data,
      chainId: this.chain.id
    }
  }

  // 创建智能合约部署交易
  createContractDeployment(
    bytecode: Hex,
    constructorArgs: any[] = [],
    value: string = '0'
  ): TransactionParams {
    // 编码构造函数参数(如果有)
    // 注意:实际实现需要根据具体合约 ABI 编码
    const data = bytecode // 这里需要包含编码后的构造函数参数

    return {
      to: zeroAddress, // 合约部署时 to 地址为 0
      value: parseEther(value),
      data,
      chainId: this.chain.id
    }
  }

  // 估算 Gas
  async estimateGas(
    publicClient: any,
    transaction: TransactionParams,
    from: Address
  ): Promise&lt;bigint> {
    try {
      const gas = await publicClient.estimateGas({
        ...transaction,
        account: from
      })

      // 添加 10% 的安全余量
      return gas * 110n / 100n
    } catch (error) {
      console.warn('Gas 估算失败,使用默认值:', error)
      return 30000n // 默认 Gas 限制
    }
  }

  // 获取当前 nonce
  async getNonce(publicClient: any, address: Address): Promise&lt;number> {
    return await publicClient.getTransactionCount({
      address,
      blockTag: 'pending'
    })
  }

  // 获取 Gas 价格(EIP-1559)
  async getGasPrice(publicClient: any): Promise&lt;{
    maxFeePerGas: bigint
    maxPriorityFeePerGas: bigint
  }> {
    const [block, feeHistory] = await Promise.all([
      publicClient.getBlock(),
      publicClient.getFeeHistory({
        blockCount: 1,
        rewardPercentiles: [25, 50, 75]
      })
    ])

    const baseFee = block.baseFeePerGas || parseGwei('30')
    const maxPriorityFeePerGas = feeHistory.reward?.[0] || parseGwei('1.5')

    // 计算 maxFeePerGas:基础费用 + 最大优先费用
    const maxFeePerGas = baseFee + maxPriorityFeePerGas

    return {
      maxFeePerGas,
      maxPriorityFeePerGas
    }
  }
}

3.2 交易参数填充

// src/transaction.ts(续)
export class TransactionFiller {
  constructor(
    private publicClient: any,
    private fromAddress: Address
  ) {}

  // 填充交易参数
  async fillTransactionParams(
    params: TransactionParams
  ): Promise&lt;TransactionParams> {
    const filledParams = { ...params }

    // 1. 填充 chainId(如果未提供)
    if (!filledParams.chainId) {
      const chainId = await this.publicClient.getChainId()
      filledParams.chainId = Number(chainId)
    }

    // 2. 填充 nonce(如果未提供)
    if (filledParams.nonce === undefined) {
      filledParams.nonce = await this.publicClient.getTransactionCount({
        address: this.fromAddress,
        blockTag: 'pending'
      })
    }

    // 3. 根据链类型填充 Gas 相关参数
    const block = await this.publicClient.getBlock()
    const supportsEIP1559 = block.baseFeePerGas !== undefined

    if (supportsEIP1559) {
      // EIP-1559 链
      if (!filledParams.maxFeePerGas || !filledParams.maxPriorityFeePerGas) {
        const gasPrice = await this.getGasPriceEIP1559()
        filledParams.maxFeePerGas = gasPrice.maxFeePerGas
        filledParams.maxPriorityFeePerGas = gasPrice.maxPriorityFeePerGas
      }
    } else {
      // 传统链
      if (!filledParams.gasPrice) {
        filledParams.gasPrice = await this.publicClient.getGasPrice()
      }
    }

    // 4. 估算 Gas(如果未提供)
    if (!filledParams.gas) {
      try {
        filledParams.gas = await this.publicClient.estimateGas({
          ...filledParams,
          account: this.fromAddress
        })
        // 添加 10% 的安全余量
        filledParams.gas = filledParams.gas * 110n / 100n
      } catch (error) {
        console.warn('Gas 估算失败,使用默认值')
        filledParams.gas = 30000n
      }
    }

    return filledParams
  }

  private async getGasPriceEIP1559(): Promise&lt;{
    maxFeePerGas: bigint
    maxPriorityFeePerGas: bigint
  }> {
    const [block, feeHistory] = await Promise.all([
      this.publicClient.getBlock(),
      this.publicClient.getFeeHistory({
        blockCount: 5,
        rewardPercentiles: [25]
      })
    ])

    const baseFee = block.baseFeePerGas || parseGwei('20')
    const priorityFee = feeHistory.reward?.[0]?.[0] || parseGwei('1.5')

    // 建议:基础费用的 2 倍 + 优先费用
    const maxFeePerGas = baseFee * 2n + priorityFee

    return {
      maxFeePerGas,
      maxPriorityFeePerGas: priorityFee
    }
  }

  // 验证交易参数
  validateTransactionParams(params: TransactionParams): string[] {
    const errors: string[] = []

    if (!params.to) {
      errors.push('接收地址(to)不能为空')
    }

    if (params.value && params.value &lt; 0n) {
      errors.push('转账金额不能为负数')
    }

    if (params.gas && params.gas &lt;= 0n) {
      errors.push('Gas 限制必须大于 0')
    }

    if (params.gasPrice && params.gasPrice &lt;= 0n) {
      errors.push('Gas 价格必须大于 0')
    }

    if (params.maxFeePerGas && params.maxFeePerGas &lt;= 0n) {
      errors.push('maxFeePerGas 必须大于 0')
    }

    if (params.maxPriorityFeePerGas && params.maxPriorityFeePerGas &lt;= 0n) {
      errors.push('maxPriorityFeePerGas 必须大于 0')
    }

    return errors
  }
}

第四步:签名交易

4.1 签名实现

// src/signer.ts
import { 
  type WalletClient,
  type Hex,
  type TransactionRequest,
  type Account,
  signTransaction,
  signMessage,
  signTypedData
} from 'viem'
import { readFileSync } from 'fs'
import { join } from 'path'

export class TransactionSigner {
  constructor(private walletClient: WalletClient) {}

  // 签名交易(返回签名后的原始交易)
  async signTransaction(transaction: TransactionRequest): Promise&lt;Hex> {
    if (!this.walletClient.account) {
      throw new Error('钱包客户端未设置账户')
    }

    try {
      // 使用 Viem 的 signTransaction 动作
      const signedTx = await signTransaction(
        this.walletClient,
        transaction
      )

      console.log('✅ 交易签名成功')
      console.log(`   原始交易长度: ${signedTx.length} 字节`)

      return signedTx
    } catch (error: any) {
      throw new Error(`交易签名失败: ${error.message}`)
    }
  }

  // 签名消息(EIP-191)
  async signMessage(message: string): Promise&lt;Hex> {
    if (!this.walletClient.account) {
      throw new Error('钱包客户端未设置账户')
    }

    const signature = await this.walletClient.signMessage({
      account: this.walletClient.account,
      message
    })

    console.log('✅ 消息签名成功')
    console.log(`   签名: ${signature}`)

    return signature
  }

  // 签名类型化数据(EIP-712)
  async signTypedData(domain: any, types: any, message: any): Promise&lt;Hex> {
    if (!this.walletClient.account) {
      throw new Error('钱包客户端未设置账户')
    }

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

    console.log('✅ 类型化数据签名成功')
    console.log(`   签名: ${signature}`)

    return signature
  }

  // 批量签名多个交易
  async signMultipleTransactions(
    transactions: TransactionRequest[]
  ): Promise&lt;Hex[]> {
    const signedTransactions: Hex[] = []

    for (let i = 0; i &lt; transactions.length; i++) {
      console.log(`签名交易 ${i + 1}/${transactions.length}...`)

      try {
        const signedTx = await this.signTransaction(transactions[i])
        signedTransactions.push(signedTx)

        // 添加延迟以避免 RPC 限制
        if (i &lt; transactions.length - 1) {
          await new Promise(resolve => setTimeout(resolve, 100))
        }
      } catch (error: any) {
        console.error(`交易 ${i + 1} 签名失败:`, error.message)
        throw error
      }
    }

    return signedTransactions
  }

  // 导出签名到文件
  async exportSignatureToFile(
    signature: Hex,
    filename: string = 'signature.txt'
  ): Promise&lt;void> {
    const fs = require('fs')
    const path = require('path')

    const filePath = path.join(process.cwd(), 'signatures', filename)

    // 确保目录存在
    const dir = path.dirname(filePath)
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true })
    }

    fs.writeFileSync(filePath, signature)
    console.log(`✅ 签名已保存到: ${filePath}`)
  }

  // 验证签名(使用公钥恢复地址)
  async recoverAddressFromSignature(
    message: string,
    signature: Hex
  ): Promise&lt;Address> {
    const { recoverMessageAddress } = await import('viem')

    const recoveredAddress = await recoverMessageAddress({
      message,
      signature
    })

    return recoveredAddress
  }
}

4.2 离线签名

// src/offline-signer.ts
import { 
  type Hex,
  type TransactionRequest,
  type Account,
  signTransaction as viemSignTransaction
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'

export class OfflineSigner {
  private account: Account

  constructor(privateKey: Hex) {
    this.account = privateKeyToAccount(privateKey)
  }

  // 离线签名交易
  async signTransactionOffline(
    transaction: TransactionRequest
  ): Promise&lt;Hex> {
    // 验证必要参数
    this.validateOfflineTransaction(transaction)

    // 创建虚拟客户端用于签名
    const mockClient = {
      account: this.account,
      chain: {
        id: transaction.chainId || 1
      },
      // 其他必要属性
      key: 'mock',
      name: 'Mock Client',
      transport: {
        type: 'mock'
      },
      type: 'walletClient'
    }

    // 签名交易
    const signedTx = await viemSignTransaction(
      mockClient as any,
      transaction
    )

    return signedTx
  }

  // 验证离线交易参数
  private validateOfflineTransaction(transaction: TransactionRequest): void {
    const requiredFields = ['to', 'gas', 'nonce', 'chainId']

    for (const field of requiredFields) {
      if (!(field in transaction)) {
        throw new Error(`离线签名需要 ${field} 字段`)
      }
    }

    // 检查 EIP-1559 或传统交易参数
    const hasEIP1559 = transaction.maxFeePerGas && transaction.maxPriorityFeePerGas
    const hasLegacy = transaction.gasPrice

    if (!hasEIP1559 && !hasLegacy) {
      throw new Error('需要提供 Gas 价格参数(gasPrice 或 maxFeePerGas/maxPriorityFeePerGas)')
    }
  }

  // 创建已签名的交易对象
  createSignedTransactionObject(
    signedTx: Hex,
    transaction: TransactionRequest
  ): any {
    return {
      raw: signedTx,
      tx: {
        ...transaction,
        hash: this.calculateTxHash(signedTx)
      },
      v: this.extractV(signedTx),
      r: this.extractR(signedTx),
      s: this.extractS(signedTx)
    }
  }

  // 提取签名组件(简化版)
  private extractV(signedTx: Hex): string {
    // 实际实现需要解析 RLP 编码
    return '0x' + signedTx.slice(-2)
  }

  private extractR(signedTx: Hex): string {
    return '0x' + signedTx.slice(2, 66)
  }

  private extractS(signedTx: Hex): string {
    return '0x' + signedTx.slice(66, 130)
  }

  private calculateTxHash(signedTx: Hex): Hex {
    const { keccak256 } = require('viem')
    return keccak256(signedTx)
  }
}

第五步:发送交易

5.1 交易发送管理器

// src/transaction-sender.ts
import { 
  type WalletClient, 
  type PublicClient,
  type Hex,
  type Address,
  type TransactionReceipt
} from 'viem'

export interface SendTransactionOptions {
  waitForConfirmation?: boolean
  confirmations?: number
  timeout?: number // 超时时间(毫秒)
  retryCount?: number // 重试次数
}

export class TransactionSender {
  constructor(
    private walletClient: WalletClient,
    private publicClient: PublicClient
  ) {}

  // 发送原始交易(已签名的)
  async sendRawTransaction(
    signedTx: Hex,
    options: SendTransactionOptions = {}
  ): Promise&lt;Hex> {
    const {
      waitForConfirmation = true,
      confirmations = 1,
      timeout = 60000,
      retryCount = 3
    } = options

    let lastError: Error | null = null

    // 重试机制
    for (let attempt = 1; attempt &lt;= retryCount; attempt++) {
      try {
        console.log(`发送交易 (尝试 ${attempt}/${retryCount})...`)

        // 发送交易
        const txHash = await this.walletClient.sendRawTransaction({
          serializedTransaction: signedTx
        })

        console.log('✅ 交易已发送到网络')
        console.log(`   交易哈希: ${txHash}`)
        console.log(`   浏览器链接: ${this.getExplorerLink(txHash)}`)

        // 等待确认(如果需要)
        if (waitForConfirmation) {
          const receipt = await this.waitForTransaction(
            txHash,
            confirmations,
            timeout
          )
          console.log('✅ 交易已确认')
          console.log(`   区块高度: ${receipt.blockNumber}`)
          console.log(`   Gas 使用量: ${receipt.gasUsed}`)
          console.log(`   状态: ${receipt.status === 1 ? '成功' : '失败'}`)

          if (receipt.status !== 1) {
            throw new Error('交易执行失败')
          }
        }

        return txHash

      } catch (error: any) {
        lastError = error
        console.warn(`发送失败 (尝试 ${attempt}):`, error.message)

        // 如果不是最后一次尝试,等待后重试
        if (attempt &lt; retryCount) {
          const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000)
          console.log(`等待 ${delay}ms 后重试...`)
          await new Promise(resolve => setTimeout(resolve, delay))
        }
      }
    }

    throw new Error(`交易发送失败: ${lastError?.message}`)
  }

  // 发送并签名交易(一步完成)
  async sendTransaction(
    transaction: any,
    options: SendTransactionOptions = {}
  ): Promise&lt;Hex> {
    if (!this.walletClient.account) {
      throw new Error('钱包客户端未设置账户')
    }

    // 发送交易(Viem 会自动签名)
    const txHash = await this.walletClient.sendTransaction({
      ...transaction,
      account: this.walletClient.account
    })

    console.log('✅ 交易已发送')
    console.log(`   交易哈希: ${txHash}`)

    // 等待确认
    if (options.waitForConfirmation !== false) {
      await this.waitForTransaction(
        txHash,
        options.confirmations || 1,
        options.timeout || 60000
      )
    }

    return txHash
  }

  // 等待交易确认
  async waitForTransaction(
    txHash: Hex,
    confirmations: number = 1,
    timeout: number = 60000
  ): Promise&lt;TransactionReceipt> {
    console.log(`等待交易确认 (${confirmations} 个区块)...`)

    const startTime = Date.now()

    return new Promise((resolve, reject) => {
      const checkInterval = setInterval(async () => {
        try {
          // 检查是否超时
          if (Date.now() - startTime > timeout) {
            clearInterval(checkInterval)
            reject(new Error(`等待交易确认超时 (${timeout}ms)`))
            return
          }

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

          if (receipt) {
            // 获取当前区块高度
            const currentBlock = await this.publicClient.getBlockNumber()
            const confirmationsCount = Number(currentBlock - receipt.blockNumber)

            if (confirmationsCount >= confirmations) {
              clearInterval(checkInterval)
              resolve(receipt)
            } else {
              console.log(`已确认 ${confirmationsCount}/${confirmations} 个区块...`)
            }
          }
        } catch (error) {
          // 忽略临时错误,继续轮询
        }
      }, 2000) // 每2秒检查一次

      // 初始检查
      setTimeout(() => {
        checkInterval
      }, 0)
    })
  }

  // 获取交易状态
  async getTransactionStatus(txHash: Hex): Promise&lt;{
    receipt: TransactionReceipt | null
    confirmations: number
    isConfirmed: boolean
    isFailed: boolean
  }> {
    try {
      const receipt = await this.publicClient.getTransactionReceipt({
        hash: txHash
      })

      if (!receipt) {
        // 交易可能还在内存池中
        const tx = await this.publicClient.getTransaction({ hash: txHash })

        return {
          receipt: null,
          confirmations: 0,
          isConfirmed: false,
          isFailed: false
        }
      }

      const currentBlock = await this.publicClient.getBlockNumber()
      const confirmations = Number(currentBlock - receipt.blockNumber)

      return {
        receipt,
        confirmations,
        isConfirmed: confirmations > 0,
        isFailed: receipt.status === 0
      }
    } catch (error) {
      throw new Error(`获取交易状态失败: ${error}`)
    }
  }

  // 取消待处理交易
  async cancelTransaction(
    nonce: number,
    gasPriceMultiplier: number = 1.1
  ): Promise&lt;Hex> {
    if (!this.walletClient.account) {
      throw new Error('钱包客户端未设置账户')
    }

    const accountAddress = this.walletClient.account.address

    // 获取当前 Gas 价格
    const gasPrice = await this.publicClient.getGasPrice()
    const cancelGasPrice = BigInt(Math.floor(Number(gasPrice) * gasPriceMultiplier))

    // 发送取消交易(发送到自己的地址,金额为 0)
    const cancelTxHash = await this.walletClient.sendTransaction({
      account: this.walletClient.account,
      to: accountAddress,
      value: 0n,
      nonce,
      gasPrice: cancelGasPrice,
      gas: 21000n
    })

    console.log('✅ 取消交易已发送')
    console.log(`   交易哈希: ${cancelTxHash}`)

    return cancelTxHash
  }

  // 加速交易
  async speedUpTransaction(
    txHash: Hex,
    gasPriceMultiplier: number = 1.2
  ): Promise&lt;Hex> {
    if (!this.walletClient.account) {
      throw new Error('钱包客户端未设置账户')
    }

    // 获取原始交易
    const originalTx = await this.publicClient.getTransaction({ hash: txHash })

    if (!originalTx) {
      throw new Error('未找到交易')
    }

    // 计算新的 Gas 价格
    let newMaxFeePerGas: bigint | undefined
    let newMaxPriorityFeePerGas: bigint | undefined
    let newGasPrice: bigint | undefined

    if (originalTx.maxFeePerGas && originalTx.maxPriorityFeePerGas) {
      // EIP-1559 交易
      newMaxFeePerGas = BigInt(Math.floor(Number(originalTx.maxFeePerGas) * gasPriceMultiplier))
      newMaxPriorityFeePerGas = BigInt(Math.floor(Number(originalTx.maxPriorityFeePerGas) * gasPriceMultiplier))
    } else if (originalTx.gasPrice) {
      // 传统交易
      newGasPrice = BigInt(Math.floor(Number(originalTx.gasPrice) * gasPriceMultiplier))
    }

    // 发送加速交易(使用相同的 nonce)
    const speedUpTxHash = await this.walletClient.sendTransaction({
      account: this.walletClient.account,
      to: originalTx.to!,
      value: originalTx.value,
      data: originalTx.input,
      nonce: originalTx.nonce,
      gas: originalTx.gas,
      maxFeePerGas: newMaxFeePerGas,
      maxPriorityFeePerGas: newMaxPriorityFeePerGas,
      gasPrice: newGasPrice,
      chainId: originalTx.chainId
    })

    console.log('✅ 加速交易已发送')
    console.log(`   交易哈希: ${speedUpTxHash}`)

    return speedUpTxHash
  }

  // 获取浏览器链接
  private getExplorerLink(txHash: Hex): string {
    const chainId = this.publicClient.chain?.id

    const explorers: Record&lt;number, string> = {
      1: `https://etherscan.io/tx/${txHash}`,
      11155111: `https://sepolia.etherscan.io/tx/${txHash}`,
      137: `https://polygonscan.com/tx/${txHash}`,
      42161: `https://arbiscan.io/tx/${txHash}`
    }

    return explorers[chainId || 1] || `https://etherscan.io/tx/${txHash}`
  }

  // 批量发送交易
  async sendBatchTransactions(
    transactions: any[],
    options: SendTransactionOptions = {}
  ): Promise&lt;Hex[]> {
    const txHashes: Hex[] = []

    for (let i = 0; i &lt; transactions.length; i++) {
      console.log(`发送交易 ${i + 1}/${transactions.length}...`)

      try {
        const txHash = await this.sendTransaction(transactions[i], {
          ...options,
          waitForConfirmation: false // 批量发送时不等待确认
        })

        txHashes.push(txHash)

        // 添加延迟以避免 nonce 冲突
        if (i &lt; transactions.length - 1) {
          await new Promise(resolve => setTimeout(resolve, 500))
        }
      } catch (error: any) {
        console.error(`交易 ${i + 1} 发送失败:`, error.message)
        throw error
      }
    }

    // 等待所有交易确认
    if (options.waitForConfirmation !== false) {
      console.log('等待所有交易确认...')

      for (const txHash of txHashes) {
        await this.waitForTransaction(
          txHash,
          options.confirmations || 1,
          options.timeout || 60000
        )
      }
    }

    return txHashes
  }
}

第六步:完整钱包实现

6.1 主钱包类

// src/wallet-main.ts
import { type Hex, type Address } from 'viem'
import { WalletManager } from './wallet'
import { WalletClientManager } from './wallet'
import { TransactionBuilder, TransactionFiller } from './transaction'
import { TransactionSigner } from './signer'
import { TransactionSender, type SendTransactionOptions } from './transaction-sender'

export class CommandLineWallet {
  private walletManager: WalletManager
  private clientManager: WalletClientManager
  private transactionBuilder: TransactionBuilder
  private transactionFiller: TransactionFiller | null = null
  private transactionSigner: TransactionSigner
  private transactionSender: TransactionSender

  constructor(
    privateKey: Hex,
    chainName: string = 'sepolia',
    rpcUrl?: string
  ) {
    // 1. 创建钱包账户
    this.walletManager = new WalletManager(privateKey)
    const account = this.walletManager.getAccount()

    // 2. 初始化客户端
    this.clientManager = new WalletClientManager(account, chainName, rpcUrl)

    // 3. 初始化交易构建器
    this.transactionBuilder = new TransactionBuilder(
      this.clientManager.getCurrentChain()
    )

    // 4. 初始化交易签名器
    this.transactionSigner = new TransactionSigner(
      this.clientManager.getWalletClient()
    )

    // 5. 初始化交易发送器
    this.transactionSender = new TransactionSender(
      this.clientManager.getWalletClient(),
      this.clientManager.getPublicClient()
    )

    // 6. 初始化交易填充器(需要公共客户端)
    this.transactionFiller = new TransactionFiller(
      this.clientManager.getPublicClient(),
      account.address
    )

    console.log('💰 命令行钱包初始化完成')
    console.log(`   网络: ${chainName}`)
    console.log(`   地址: ${account.address}`)
  }

  // 获取余额
  async getBalance(address?: Address): Promise&lt;bigint> {
    const targetAddress = address || this.walletManager.getAddress()

    try {
      const balance = await this.clientManager.getPublicClient().getBalance({
        address: targetAddress
      })

      return balance
    } catch (error: any) {
      throw new Error(`获取余额失败: ${error.message}`)
    }
  }

  // 获取代币余额
  async getTokenBalance(
    tokenAddress: Address,
    address?: Address
  ): Promise&lt;bigint> {
    const { readContract } = await import('viem/actions')
    const { erc20Abi } = await import('viem/contracts')

    const targetAddress = address || this.walletManager.getAddress()

    try {
      const balance = await readContract(this.clientManager.getPublicClient(), {
        address: tokenAddress,
        abi: erc20Abi,
        functionName: 'balanceOf',
        args: [targetAddress]
      })

      return balance as bigint
    } catch (error: any) {
      throw new Error(`获取代币余额失败: ${error.message}`)
    }
  }

  // 发送 ETH
  async sendEth(
    to: Address,
    amount: string, // ETH 数量,如 "0.1"
    options: SendTransactionOptions = {}
  ): Promise&lt;Hex> {
    try {
      // 1. 构建交易
      const transaction = this.transactionBuilder.createEthTransfer(to, amount)

      // 2. 填充交易参数
      const filledTransaction = await this.transactionFiller!.fillTransactionParams(transaction)

      // 3. 发送交易
      const txHash = await this.transactionSender.sendTransaction(
        filledTransaction,
        options
      )

      return txHash
    } catch (error: any) {
      throw new Error(`发送 ETH 失败: ${error.message}`)
    }
  }

  // 发送代币
  async sendToken(
    tokenAddress: Address,
    to: Address,
    amount: string, // 代币数量
    decimals: number = 18,
    options: SendTransactionOptions = {}
  ): Promise&lt;Hex> {
    try {
      // 1. 构建交易
      const transaction = this.transactionBuilder.createErc20Transfer(
        tokenAddress,
        to,
        amount,
        decimals
      )

      // 2. 填充交易参数
      const filledTransaction = await this.transactionFiller!.fillTransactionParams(transaction)

      // 3. 发送交易
      const txHash = await this.transactionSender.sendTransaction(
        filledTransaction,
        options
      )

      return txHash
    } catch (error: any) {
      throw new Error(`发送代币失败: ${error.message}`)
    }
  }

  // 签名并发送原始交易
  async signAndSend(
    transaction: any,
    options: SendTransactionOptions = {}
  ): Promise&lt;Hex> {
    try {
      // 1. 填充交易参数
      const filledTransaction = await this.transactionFiller!.fillTransactionParams(transaction)

      // 2. 签名交易
      const signedTx = await this.transactionSigner.signTransaction(filledTransaction)

      // 3. 发送原始交易
      const txHash = await this.transactionSender.sendRawTransaction(
        signedTx,
        options
      )

      return txHash
    } catch (error: any) {
      throw new Error(`签名并发送失败: ${error.message}`)
    }
  }

  // 获取交易历史
  async getTransactionHistory(
    address?: Address,
    limit: number = 10
  ): Promise&lt;any[]> {
    const targetAddress = address || this.walletManager.getAddress()

    // 注意:大多数公共 RPC 节点不提供交易历史查询
    // 实际实现可能需要使用 Etherscan API 或其他索引服务
    console.warn('获取交易历史需要 Etherscan API 或其他索引服务')

    // 这里返回空数组,实际实现需要集成外部 API
    return []
  }

  // 导出钱包信息
  exportWalletInfo(): {
    address: Address
    chainId: number
    network: string
  } {
    return {
      address: this.walletManager.getAddress(),
      chainId: this.clientManager.getCurrentChain().id,
      network: this.clientManager.getCurrentChain().name
    }
  }

  // 切换网络
  async switchNetwork(chainName: string, rpcUrl?: string): Promise&lt;void> {
    await this.clientManager.switchChain(chainName, rpcUrl)

    // 更新交易构建器的链信息
    this.transactionBuilder = new TransactionBuilder(
      this.clientManager.getCurrentChain()
    )

    // 更新交易填充器
    this.transactionFiller = new TransactionFiller(
      this.clientManager.getPublicClient(),
      this.walletManager.getAddress()
    )
  }

  // 获取 Gas 价格信息
  async getGasInfo(): Promise&lt;{
    gasPrice: bigint | null
    maxFeePerGas: bigint | null
    maxPriorityFeePerGas: bigint | null
    baseFeePerGas: bigint | null
  }> {
    const publicClient = this.clientManager.getPublicClient()

    try {
      const block = await publicClient.getBlock()

      return {
        gasPrice: await publicClient.getGasPrice().catch(() => null),
        maxFeePerGas: block.baseFeePerGas ? 
          block.baseFeePerGas * 2n : null,
        maxPriorityFeePerGas: 1500000000n, // 1.5 Gwei
        baseFeePerGas: block.baseFeePerGas || null
      }
    } catch (error) {
      throw new Error(`获取 Gas 信息失败: ${error}`)
    }
  }
}

6.2 命令行界面

// src/cli.ts
#!/usr/bin/env node

import { Command } from 'commander'
import { parseEther, formatEther } from 'viem'
import { CommandLineWallet } from './wallet-main'
import 'dotenv/config'

const program = new Command()

// 从环境变量或配置文件读取私钥
const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`
const RPC_URL = process.env.RPC_URL || 'https://eth-sepolia.g.alchemy.com/v2/demo'
const CHAIN = process.env.CHAIN || 'sepolia'

// 验证私钥
if (!PRIVATE_KEY || !PRIVATE_KEY.startsWith('0x') || PRIVATE_KEY.length !== 66) {
  console.error('错误: 无效的私钥。请设置正确的 PRIVATE_KEY 环境变量')
  process.exit(1)
}

// 创建钱包实例
const wallet = new CommandLineWallet(PRIVATE_KEY, CHAIN, RPC_URL)

program
  .name('cli-wallet')
  .description('命令行 Web3 钱包')
  .version('1.0.0')

// 余额查询命令
program
  .command('balance')
  .description('查询余额')
  .option('-a, --address &lt;address>', '查询指定地址的余额')
  .option('-t, --token &lt;address>', '查询代币余额')
  .action(async (options) => {
    try {
      if (options.token) {
        const balance = await wallet.getTokenBalance(options.token, options.address)
        console.log(`代币余额: ${balance}`)
      } else {
        const balance = await wallet.getBalance(options.address)
        const ethBalance = formatEther(balance)
        console.log(`ETH 余额: ${ethBalance}`)
      }
    } catch (error: any) {
      console.error(`查询失败: ${error.message}`)
    }
  })

// 发送 ETH 命令
program
  .command('send-eth')
  .description('发送 ETH')
  .requiredOption('-t, --to &lt;address>', '接收方地址')
  .requiredOption('-a, --amount &lt;amount>', '发送数量(ETH)')
  .option('-w, --wait', '等待交易确认', true)
  .action(async (options) => {
    try {
      console.log(`发送 ${options.amount} ETH 到 ${options.to}...`)

      const txHash = await wallet.sendEth(
        options.to,
        options.amount,
        {
          waitForConfirmation: options.wait,
          confirmations: 1
        }
      )

      console.log(`✅ 交易发送成功`)
      console.log(`   交易哈希: ${txHash}`)
    } catch (error: any) {
      console.error(`发送失败: ${error.message}`)
    }
  })

// 发送代币命令
program
  .command('send-token')
  .description('发送 ERC-20 代币')
  .requiredOption('-c, --contract &lt;address>', '代币合约地址')
  .requiredOption('-t, --to &lt;address>', '接收方地址')
  .requiredOption('-a, --amount &lt;amount>', '发送数量')
  .option('-d, --decimals &lt;number>', '代币小数位数', '18')
  .action(async (options) => {
    try {
      console.log(`发送 ${options.amount} 代币到 ${options.to}...`)

      const txHash = await wallet.sendToken(
        options.contract,
        options.to,
        options.amount,
        parseInt(options.decimals),
        {
          waitForConfirmation: true,
          confirmations: 1
        }
      )

      console.log(`✅ 代币发送成功`)
      console.log(`   交易哈希: ${txHash}`)
    } catch (error: any) {
      console.error(`发送失败: ${error.message}`)
    }
  })

// 切换网络命令
program
  .command('switch-network')
  .description('切换网络')
  .requiredOption('-n, --network &lt;name>', '网络名称(mainnet, sepolia, polygon等)')
  .option('-u, --url &lt;rpc-url>', '自定义 RPC URL')
  .action(async (options) => {
    try {
      await wallet.switchNetwork(options.network, options.url)
      console.log(`✅ 已切换到 ${options.network} 网络`)
    } catch (error: any) {
      console.error(`切换失败: ${error.message}`)
    }
  })

// Gas 信息命令
program
  .command('gas')
  .description('查看当前 Gas 价格')
  .action(async () => {
    try {
      const gasInfo = await wallet.getGasInfo()

      console.log('⛽ 当前 Gas 价格:')
      if (gasInfo.gasPrice) {
        console.log(`   传统 Gas 价格: ${formatEther(gasInfo.gasPrice)} ETH`)
      }
      if (gasInfo.baseFeePerGas) {
        console.log(`   基础费用: ${formatEther(gasInfo.baseFeePerGas)} ETH`)
      }
      if (gasInfo.maxFeePerGas) {
        console.log(`   建议 maxFeePerGas: ${formatEther(gasInfo.maxFeePerGas)} ETH`)
      }
      if (gasInfo.maxPriorityFeePerGas) {
        console.log(`   建议 maxPriorityFeePerGas: ${formatEther(gasInfo.maxPriorityFeePerGas)} ETH`)
      }
    } catch (error: any) {
      console.error(`获取失败: ${error.message}`)
    }
  })

// 钱包信息命令
program
  .command('info')
  .description('查看钱包信息')
  .action(() => {
    const info = wallet.exportWalletInfo()

    console.log('💰 钱包信息:')
    console.log(`   地址: ${info.address}`)
    console.log(`   网络: ${info.network}`)
    console.log(`   链 ID: ${info.chainId}`)
  })

// 签名消息命令
program
  .command('sign-message')
  .description('签名消息')
  .requiredOption('-m, --message &lt;text>', '要签名的消息')
  .action(async (options) => {
    try {
      // 这里需要调用钱包的签名方法
      console.log(`签名消息: ${options.message}`)
      console.warn('此功能待实现')
    } catch (error: any) {
      console.error(`签名失败: ${error.message}`)
    }
  })

program.parse(process.argv)

// 如果没有提供命令,显示帮助信息
if (!process.argv.slice(2).length) {
  program.outputHelp()
}

6.3 环境配置

# .env 文件
PRIVATE_KEY=0x你的私钥
RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY
CHAIN=sepolia

# 可选的配置
# MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
# POLYGON_RPC_URL=https://polygon-mainnet.g.alchemy.com/v2/YOUR_API_KEY
# ETHERSCAN_API_KEY=你的Etherscan API密钥

6.4 TypeScript 配置

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

6.5 包配置文件

// package.json
{
  "name": "cli-wallet",
  "version": "1.0.0",
  "description": "基于 Viem 的命令行 Web3 钱包",
  "main": "dist/cli.js",
  "bin": {
    "web3-wallet": "./dist/cli.js"
  },
  "scripts": {
    "build": "tsc",
    "start": "tsx src/cli.ts",
    "dev": "nodemon --exec tsx src/cli.ts",
    "test": "jest",
    "lint": "eslint src/**/*.ts"
  },
  "dependencies": {
    "viem": "^2.0.0",
    "commander": "^11.0.0",
    "dotenv": "^16.0.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0",
    "tsx": "^4.0.0",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "eslint": "^8.0.0",
    "nodemon": "^3.0.0"
  },
  "keywords": ["web3", "ethereum", "wallet", "cli", "viem"],
  "author": "Your Name",
  "license": "MIT"
}

高级功能

7.1 批量交易处理器

// src/batch-processor.ts
export class BatchTransactionProcessor {
  private pendingTransactions: Array&lt;{
    tx: any
    promise: Promise&lt;string>
    resolve: (hash: string) => void
    reject: (error: Error) => void
  }> = []

  private isProcessing = false

  constructor(private wallet: CommandLineWallet) {}

  // 添加交易到批处理队列
  addTransaction(transaction: any): Promise&lt;string> {
    return new Promise((resolve, reject) => {
      this.pendingTransactions.push({
        tx: transaction,
        promise: new Promise((res, rej) => {
          this.pendingTransactions[this.pendingTransactions.length - 1].resolve = res
          this.pendingTransactions[this.pendingTransactions.length - 1].reject = rej
        }),
        resolve,
        reject
      })

      // 如果未在处理,启动处理
      if (!this.isProcessing) {
        this.processBatch()
      }
    })
  }

  // 处理批处理队列
  private async processBatch(): Promise&lt;void> {
    if (this.isProcessing || this.pendingTransactions.length === 0) {
      return
    }

    this.isProcessing = true

    while (this.pendingTransactions.length > 0) {
      const batch = this.pendingTransactions.splice(0, 5) // 每次处理5个交易

      try {
        const results = await Promise.allSettled(
          batch.map(item => this.wallet.sendEth(
            item.tx.to,
            item.tx.amount,
            { waitForConfirmation: false }
          ))
        )

        // 处理结果
        results.forEach((result, index) => {
          const item = batch[index]

          if (result.status === 'fulfilled') {
            item.resolve(result.value)
          } else {
            item.reject(result.reason)
          }
        })

        // 批次间延迟
        if (this.pendingTransactions.length > 0) {
          await new Promise(resolve => setTimeout(resolve, 1000))
        }
      } catch (error) {
        // 标记所有批次交易为失败
        batch.forEach(item => {
          item.reject(error as Error)
        })
      }
    }

    this.isProcessing = false
  }

  // 获取队列状态
  getQueueStatus(): {
    pending: number
    isProcessing: boolean
  } {
    return {
      pending: this.pendingTransactions.length,
      isProcessing: this.isProcessing
    }
  }

  // 清空队列
  clearQueue(): void {
    this.pendingTransactions = []
  }
}

7.2 交易监控器

// src/transaction-monitor.ts
export class TransactionMonitor {
  private monitoredTransactions = new Map&lt;string, {
    txHash: string
    startTime: number
    confirmations: number
    status: 'pending' | 'confirmed' | 'failed'
    callback?: (status: string) => void
  }>()

  constructor(private wallet: CommandLineWallet) {
    // 启动监控循环
    this.startMonitoring()
  }

  // 添加监控交易
  monitorTransaction(
    txHash: string,
    callback?: (status: string) => void
  ): void {
    this.monitoredTransactions.set(txHash, {
      txHash,
      startTime: Date.now(),
      confirmations: 0,
      status: 'pending',
      callback
    })
  }

  // 启动监控
  private async startMonitoring(): Promise&lt;void> {
    setInterval(async () => {
      for (const [txHash, info] of this.monitoredTransactions.entries()) {
        if (info.status !== 'pending') continue

        try {
          const status = await this.wallet.getTransactionStatus(txHash)

          if (status.receipt) {
            info.confirmations = status.confirmations
            info.status = status.isFailed ? 'failed' : 'confirmed'

            // 触发回调
            if (info.callback) {
              info.callback(info.status)
            }

            // 如果已确认,从监控列表中移除
            if (status.confirmations > 12) {
              this.monitoredTransactions.delete(txHash)
            }
          }
        } catch (error) {
          console.error(`监控交易 ${txHash} 失败:`, error)
        }
      }
    }, 5000) // 每5秒检查一次
  }

  // 获取监控状态
  getMonitoringStatus(): Array&lt;{
    txHash: string
    duration: number
    confirmations: number
    status: string
  }> {
    const now = Date.now()
    const result = []

    for (const info of this.monitoredTransactions.values()) {
      result.push({
        txHash: info.txHash,
        duration: now - info.startTime,
        confirmations: info.confirmations,
        status: info.status
      })
    }

    return result
  }
}

安全最佳实践

8.1 私钥管理

// src/security/key-manager.ts
import * as crypto from 'crypto'
import * as fs from 'fs'
import * as path from 'path'

export class SecureKeyManager {
  private encryptionKey: Buffer

  constructor(encryptionPassword: string) {
    // 从密码派生加密密钥
    this.encryptionKey = this.deriveKey(encryptionPassword)
  }

  // 加密私钥
  encryptPrivateKey(privateKey: string): string {
    const iv = crypto.randomBytes(16)
    const cipher = crypto.createCipheriv('aes-256-gcm', this.encryptionKey, iv)

    let encrypted = cipher.update(privateKey, 'utf8', 'hex')
    encrypted += cipher.final('hex')

    const authTag = cipher.getAuthTag()

    // 组合 IV、加密数据和认证标签
    return Buffer.concat([iv, authTag, Buffer.from(encrypted, 'hex')]).toString('base64')
  }

  // 解密私钥
  decryptPrivateKey(encryptedData: string): string {
    const data = Buffer.from(encryptedData, 'base64')

    const iv = data.subarray(0, 16)
    const authTag = data.subarray(16, 32)
    const encrypted = data.subarray(32)

    const decipher = crypto.createDecipheriv('aes-256-gcm', this.encryptionKey, iv)
    decipher.setAuthTag(authTag)

    let decrypted = decipher.update(encrypted)
    decrypted = Buffer.concat([decrypted, decipher.final()])

    return decrypted.toString('utf8')
  }

  // 安全保存加密私钥
  saveEncryptedKey(encryptedKey: string, filePath: string): void {
    const dir = path.dirname(filePath)

    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
    }

    fs.writeFileSync(filePath, encryptedKey, { mode: 0o600 })
  }

  // 从文件加载加密私钥
  loadEncryptedKey(filePath: string): string {
    if (!fs.existsSync(filePath)) {
      throw new Error(`密钥文件不存在: ${filePath}`)
    }

    return fs.readFileSync(filePath, 'utf8')
  }

  // 从密码派生密钥
  private deriveKey(password: string): Buffer {
    const salt = crypto.randomBytes(32)
    return crypto.scryptSync(password, salt, 32)
  }

  // 生成安全助记词
  generateSecureMnemonic(): {
    mnemonic: string
    encrypted: string
  } {
    const { generateMnemonic, english } = require('viem/accounts')
    const mnemonic = generateMnemonic(english)
    const encrypted = this.encryptPrivateKey(mnemonic)

    return { mnemonic, encrypted }
  }
}

8.2 审计日志

// src/security/audit-logger.ts
import * as fs from 'fs'
import * as path from 'path'

export class AuditLogger {
  private logDir: string

  constructor(logDir: string = './logs') {
    this.logDir = logDir

    if (!fs.existsSync(this.logDir)) {
      fs.mkdirSync(this.logDir, { recursive: true })
    }
  }

  // 记录交易
  logTransaction(
    type: string,
    from: string,
    to: string,
    amount: string,
    txHash: string,
    status: string,
    metadata: any = {}
  ): void {
    const logEntry = {
      timestamp: new Date().toISOString(),
      type,
      from,
      to,
      amount,
      txHash,
      status,
      metadata,
      ip: this.getClientIP()
    }

    this.writeLog('transactions', logEntry)
  }

  // 记录错误
  logError(
    context: string,
    error: Error,
    metadata: any = {}
  ): void {
    const logEntry = {
      timestamp: new Date().toISOString(),
      context,
      error: {
        message: error.message,
        stack: error.stack
      },
      metadata,
      ip: this.getClientIP()
    }

    this.writeLog('errors', logEntry)
  }

  // 记录钱包活动
  logWalletActivity(
    action: string,
    address: string,
    metadata: any = {}
  ): void {
    const logEntry = {
      timestamp: new Date().toISOString(),
      action,
      address,
      metadata,
      ip: this.getClientIP()
    }

    this.writeLog('activities', logEntry)
  }

  // 写入日志文件
  private writeLog(category: string, data: any): void {
    const date = new Date().toISOString().split('T')[0]
    const logFile = path.join(this.logDir, `${category}-${date}.log`)

    const logLine = JSON.stringify(data) + '\n'

    fs.appendFileSync(logFile, logLine, { flag: 'a' })
  }

  // 获取客户端 IP(简化版)
  private getClientIP(): string {
    // 在实际应用中,应该从请求头获取
    return '127.0.0.1'
  }

  // 查询日志
  queryLogs(
    category: string,
    startDate: Date,
    endDate: Date
  ): any[] {
    const logs: any[] = []
    const currentDate = new Date(startDate)

    while (currentDate &lt;= endDate) {
      const dateStr = currentDate.toISOString().split('T')[0]
      const logFile = path.join(this.logDir, `${category}-${dateStr}.log`)

      if (fs.existsSync(logFile)) {
        const content = fs.readFileSync(logFile, 'utf8')
        const lines = content.trim().split('\n')

        lines.forEach(line => {
          try {
            const logEntry = JSON.parse(line)
            const logDate = new Date(logEntry.timestamp)

            if (logDate >= startDate && logDate &lt;= endDate) {
              logs.push(logEntry)
            }
          } catch (error) {
            console.warn(`解析日志行失败: ${line}`)
          }
        })
      }

      currentDate.setDate(currentDate.getDate() + 1)
    }

    return logs
  }
}

测试用例

9.1 单元测试

// test/wallet.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { CommandLineWallet } from '../src/wallet-main'
import { parseEther } from 'viem'

// 使用测试网私钥(仅用于测试)
const TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
const TEST_RPC_URL = 'https://eth-sepolia.g.alchemy.com/v2/demo'

describe('CommandLineWallet', () => {
  let wallet: CommandLineWallet

  beforeEach(() => {
    wallet = new CommandLineWallet(TEST_PRIVATE_KEY, 'sepolia', TEST_RPC_URL)
  })

  it('应该正确创建钱包实例', () => {
    const info = wallet.exportWalletInfo()
    expect(info.address).toBeDefined()
    expect(info.chainId).toBe(11155111) // Sepolia 链 ID
    expect(info.network).toBe('Sepolia')
  })

  it('应该获取余额', async () => {
    const balance = await wallet.getBalance()
    expect(balance).toBeDefined()
    expect(typeof balance).toBe('bigint')
  })

  it('应该切换网络', async () => {
    await wallet.switchNetwork('mainnet')
    const info = wallet.exportWalletInfo()
    expect(info.chainId).toBe(1)
    expect(info.network).toBe('Ethereum')
  })

  it('应该获取 Gas 信息', async () => {
    const gasInfo = await wallet.getGasInfo()
    expect(gasInfo).toBeDefined()

    // 检查 Gas 信息字段
    expect(gasInfo).toHaveProperty('gasPrice')
    expect(gasInfo).toHaveProperty('maxFeePerGas')
    expect(gasInfo).toHaveProperty('maxPriorityFeePerGas')
    expect(gasInfo).toHaveProperty('baseFeePerGas')
  })
})

describe('交易构建', () => {
  it('应该构建 ETH 转账交易', () => {
    const { TransactionBuilder } = require('../src/transaction')
    const { sepolia } = require('viem/chains')

    const builder = new TransactionBuilder(sepolia)
    const tx = builder.createEthTransfer(
      '0xC27018ca6c6DfF213583eB504df4a039Cc7d8043',
      '0.1'
    )

    expect(tx.to).toBe('0xC27018ca6c6DfF213583eB504df4a039Cc7d8043')
    expect(tx.value).toEqual(parseEther('0.1'))
    expect(tx.chainId).toBe(11155111)
  })

  it('应该构建 ERC-20 转账交易', () => {
    const { TransactionBuilder } = require('../src/transaction')
    const { sepolia } = require('viem/chains')

    const builder = new TransactionBuilder(sepolia)
    const tx = builder.createErc20Transfer(
      '0xTokenAddress',
      '0xRecipientAddress',
      '100',
      18
    )

    expect(tx.to).toBe('0xTokenAddress')
    expect(tx.value).toEqual(0n)
    expect(tx.data).toBeDefined()
  })
})

// 运行测试:npx vitest run

9.2 集成测试

// test/integration.test.ts
import { describe, it, expect, beforeAll } from 'vitest'
import { CommandLineWallet } from '../src/wallet-main'

// 集成测试需要使用真实的 RPC 节点和测试网
describe('集成测试(需要 Sepolia 测试网)', () => {
  let wallet: CommandLineWallet

  beforeAll(() => {
    // 使用测试账户的私钥
    const testPrivateKey = process.env.TEST_PRIVATE_KEY
    if (!testPrivateKey) {
      throw new Error('请设置 TEST_PRIVATE_KEY 环境变量')
    }

    wallet = new CommandLineWallet(
      testPrivateKey as `0x${string}`,
      'sepolia',
      process.env.TEST_RPC_URL
    )
  })

  it('应该发送测试交易', async () => {
    // 发送少量 ETH 到测试地址
    const testAmount = '0.0001'
    const testRecipient = '0xC27018ca6c6DfF213583eB504df4a039Cc7d8043'

    const txHash = await wallet.sendEth(
      testRecipient,
      testAmount,
      {
        waitForConfirmation: true,
        confirmations: 1
      }
    )

    expect(txHash).toBeDefined()
    expect(txHash.startsWith('0x')).toBe(true)
    expect(txHash.length).toBe(66) // 32字节哈希 + 0x前缀
  }, 30000) // 30秒超时

  it('应该处理交易失败', async () => {
    // 尝试发送超过余额的 ETH
    const hugeAmount = '10000'
    const testRecipient = '0xC27018ca6c6DfF213583eB504df4a039Cc7d8043'

    await expect(
      wallet.sendEth(testRecipient, hugeAmount)
    ).rejects.toThrow()
  }, 30000)
})

部署和使用说明

10.1 安装和使用

# 1. 克隆项目
git clone &lt;repository-url>
cd cli-wallet

# 2. 安装依赖
npm install

# 3. 配置环境变量
cp .env.example .env
# 编辑 .env 文件,填入你的私钥和 RPC URL

# 4. 构建项目
npm run build

# 5. 链接到全局命令
npm link

# 6. 使用命令行钱包
web3-wallet --help
web3-wallet balance
web3-wallet send-eth --to 0xRecipientAddress --amount 0.1

10.2 Docker 部署

# Dockerfile
FROM node:18-alpine

WORKDIR /app

# 复制包管理文件
COPY package*.json ./

# 安装依赖
RUN npm ci --only=production

# 复制构建后的代码
COPY dist/ ./dist/

# 复制环境变量模板
COPY .env.example .env

# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

USER nodejs

# 设置入口点
ENTRYPOINT ["node", "dist/cli.js"]

10.3 安全配置检查清单

  1. 私钥管理

    • 私钥不硬编码在代码中
    • 使用环境变量或密钥管理服务
    • 实现加密存储
    • 设置文件权限 (600)
  2. 网络连接

    • 使用 HTTPS RPC 端点
    • 实现重试和超时机制
    • 验证 SSL 证书
  3. 交易安全

    • Gas 价格限制
    • 交易金额验证
    • 地址格式验证
    • 交易模拟和估算
  4. 审计和监控

    • 记录所有交易
    • 实现错误跟踪
    • 设置操作阈值

故障排除

常见问题

  1. RPC 连接失败

    错误: 无法连接到 RPC 节点
    解决方案:
    1. 检查网络连接
    2. 验证 RPC URL 配置
    3. 尝试备用 RPC 节点
  2. Gas 不足

    错误: insufficient funds for gas * price + value
    解决方案:
    1. 检查账户余额
    2. 降低 Gas 价格
    3. 减少转账金额
  3. Nonce 太低

    错误: nonce too low
    解决方案:
    1. 等待 pending 交易确认
    2. 手动设置更高的 nonce
    3. 使用交易加速或取消功能
  4. 交易超时

    错误: 交易确认超时
    解决方案:
    1. 增加 Gas 价格
    2. 检查网络拥堵情况
    3. 增加超时时间

调试模式

启用详细日志:

// 在代码中添加调试日志
import { createWalletClient, http, createPublicClient } from 'viem'
import { mainnet } from 'viem/chains'

const client = createWalletClient({
  chain: mainnet,
  transport: http('https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY', {
    // 启用详细日志
    fetchOptions: {
      headers: {
        'Content-Type': 'application/json'
      }
    },
    retryCount: 3,
    retryDelay: 1000
  })
})

性能优化建议

  1. 连接池管理

    • 复用 RPC 连接
    • 实现连接健康检查
    • 设置连接超时
  2. 缓存策略

    • 缓存 Gas 价格
    • 缓存账户 nonce
    • 缓存代币信息
  3. 批量处理

    • 批量查询余额
    • 批量发送交易
    • 批量获取交易回执
  4. 异步处理

    • 使用异步队列
    • 实现事件驱动架构
    • 优化 Promise 链

扩展功能路线图

  1. V1.1 - 多链支持增强

    • 支持更多 EVM 兼容链
    • 跨链桥接功能
    • Layer 2 集成
  2. V1.2 - 高级功能

    • 智能合约交互
    • DeFi 协议集成
    • NFT 支持

<!--EndFragment-->

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

0 条评论

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