使用Viem构建命令行钱包:Web3交易全流程技术指南概述本文档详细阐述使用Viem库在Web3环境中构建命令行钱包的完整流程,涵盖从钱包创建到交易发送的四个核心环节。Viem是一个类型安全的以太坊交互库,提供低级别原语,适用于构建钱包、DApps和其他以太坊工具。环境准备
<!--StartFragment-->
本文档详细阐述使用 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
Viem 提供两种账户管理方式:
// 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)
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)
// 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<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<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<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
}
}
// 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<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<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<number> {
return await publicClient.getTransactionCount({
address,
blockTag: 'pending'
})
}
// 获取 Gas 价格(EIP-1559)
async getGasPrice(publicClient: any): Promise<{
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
}
}
}
// src/transaction.ts(续)
export class TransactionFiller {
constructor(
private publicClient: any,
private fromAddress: Address
) {}
// 填充交易参数
async fillTransactionParams(
params: TransactionParams
): Promise<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<{
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 < 0n) {
errors.push('转账金额不能为负数')
}
if (params.gas && params.gas <= 0n) {
errors.push('Gas 限制必须大于 0')
}
if (params.gasPrice && params.gasPrice <= 0n) {
errors.push('Gas 价格必须大于 0')
}
if (params.maxFeePerGas && params.maxFeePerGas <= 0n) {
errors.push('maxFeePerGas 必须大于 0')
}
if (params.maxPriorityFeePerGas && params.maxPriorityFeePerGas <= 0n) {
errors.push('maxPriorityFeePerGas 必须大于 0')
}
return errors
}
}
// 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<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<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<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<Hex[]> {
const signedTransactions: Hex[] = []
for (let i = 0; i < transactions.length; i++) {
console.log(`签名交易 ${i + 1}/${transactions.length}...`)
try {
const signedTx = await this.signTransaction(transactions[i])
signedTransactions.push(signedTx)
// 添加延迟以避免 RPC 限制
if (i < 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<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<Address> {
const { recoverMessageAddress } = await import('viem')
const recoveredAddress = await recoverMessageAddress({
message,
signature
})
return recoveredAddress
}
}
// 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<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)
}
}
// 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<Hex> {
const {
waitForConfirmation = true,
confirmations = 1,
timeout = 60000,
retryCount = 3
} = options
let lastError: Error | null = null
// 重试机制
for (let attempt = 1; attempt <= 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 < 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<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<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<{
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<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<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<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<Hex[]> {
const txHashes: Hex[] = []
for (let i = 0; i < 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 < 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
}
}
// 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<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<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<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<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<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<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<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<{
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}`)
}
}
}
// 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 <address>', '查询指定地址的余额')
.option('-t, --token <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 <address>', '接收方地址')
.requiredOption('-a, --amount <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 <address>', '代币合约地址')
.requiredOption('-t, --to <address>', '接收方地址')
.requiredOption('-a, --amount <amount>', '发送数量')
.option('-d, --decimals <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 <name>', '网络名称(mainnet, sepolia, polygon等)')
.option('-u, --url <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 <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()
}
# .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密钥
// 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"]
}
// 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"
}
// src/batch-processor.ts
export class BatchTransactionProcessor {
private pendingTransactions: Array<{
tx: any
promise: Promise<string>
resolve: (hash: string) => void
reject: (error: Error) => void
}> = []
private isProcessing = false
constructor(private wallet: CommandLineWallet) {}
// 添加交易到批处理队列
addTransaction(transaction: any): Promise<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<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 = []
}
}
// src/transaction-monitor.ts
export class TransactionMonitor {
private monitoredTransactions = new Map<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<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<{
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
}
}
// 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 }
}
}
// 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 <= 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 <= endDate) {
logs.push(logEntry)
}
} catch (error) {
console.warn(`解析日志行失败: ${line}`)
}
})
}
currentDate.setDate(currentDate.getDate() + 1)
}
return logs
}
}
// 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
// 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)
})
# 1. 克隆项目
git clone <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
# 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"]
私钥管理
网络连接
交易安全
审计和监控
RPC 连接失败
错误: 无法连接到 RPC 节点
解决方案:
1. 检查网络连接
2. 验证 RPC URL 配置
3. 尝试备用 RPC 节点
Gas 不足
错误: insufficient funds for gas * price + value
解决方案:
1. 检查账户余额
2. 降低 Gas 价格
3. 减少转账金额
Nonce 太低
错误: nonce too low
解决方案:
1. 等待 pending 交易确认
2. 手动设置更高的 nonce
3. 使用交易加速或取消功能
交易超时
错误: 交易确认超时
解决方案:
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
})
})
连接池管理
缓存策略
批量处理
异步处理
V1.1 - 多链支持增强
V1.2 - 高级功能
<!--EndFragment-->
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!