web3.js web3-eth-accounts 使用指南:Ethereum 账户管理与交易签名
【免费下载链接】web3.jsCollection of comprehensive TypeScript libraries for Interaction with the Ethereum JSON RPC API and utility functions.项目地址: https://gitcode.com/gh_mirrors/we/web3.js
web3-eth-accounts是 web3.js 的官方子包,专门负责 Ethereum 账户的生成、管理与数据/交易签名。本文基于当前仓库中该包的 README 展开,并结合其源码(account.ts、wallet.ts、tx/目录等)深入讲解账户创建、消息签名、三类 Typed Transaction 签名、V3 Keystore 加解密与内存钱包的完整用法,读者学完后可以直接在 Node.js 或浏览器应用中落地账户管理功能。
包定位与功能总览
web3-eth-accounts是 web3.js 的模块化子包之一,其职责定义在源码注释中:
"The web3.eth.accounts contains functions to generate Ethereum accounts and sign transactions and data."
即:生成 Ethereum 账户、签名交易、签名任意数据。它同时提供:
- 账户生成:
create、privateKeyToAccount - 地址/公钥推导:
privateKeyToAddress、privateKeyToPublicKey - 消息签名与验签:
hashMessage、sign、signRaw、recover、recoverTransaction - 交易签名:
signTransaction(支持 legacy、EIP-2930、EIP-1559) - Keystore:
encrypt、decrypt(V3 JSON Keystore,scrypt / pbkdf2) - 钱包:
Wallet内存钱包(create/add/get/remove/clear/encrypt/decrypt/save/load)
从源码结构看,包的公共导出入口在 packages/web3-eth-accounts/src/index.ts,它统一导出wallet、account、types、schemas以及common(链参数、EIP、硬分叉定义)和tx(交易类型实现)两大内部模块。tx/目录中的 tx/index.ts 明确标注其交易实现源自@ethereumjs/txv4.1.1 的思路,提供了Transaction(legacy)、AccessListEIP2930Transaction、FeeMarketEIP1559Transaction以及TransactionFactory工厂。
安全提示(源自源码官方注释):该包尚未经过审计(NOT been audited),在生产环境使用前必须妥善清理内存、安全保管私钥,并充分测试交易收发功能。请务必阅读本文最后一节的注意事项。
安装与前置条件
安装
推荐使用 NPM 或 Yarn 安装:
# NPM npm install web3-eth-accounts # Yarn yarn add web3-eth-accounts环境要求
依据包的 package.json:
| 项目 | 要求 |
|---|---|
| Node.js | >=14 |
| npm | >=6.12.0 |
| ECMAScript | ES2020(tsconfig目标) |
| 包管理器(开发) | Yarn / Lerna |
包同时提供lib/commonjs(CJS)与lib/esm(ESM)双格式产物,exports字段中require指向 CJS、import指向 ESM,可无缝用于 Node 与打包工具。
依赖关系
该包依赖以下运行时库(见 package.json):
ethereum-cryptography:提供 secp256k1 曲线、AES、scrypt/pbkdf2 等密码学原语@ethereumjs/rlp:RLP 编码crc-32:校验计算web3-errors/web3-types/web3-utils/web3-validator:错误类型、TS 类型、工具函数与 JSON Schema 校验
两种使用方式
方式一:通过web3主包访问(推荐)
安装web3主包后,账户功能挂载在web3.eth.accounts下:
import Web3 from 'web3'; const web3 = new Web3(Web3.givenProvider || 'ws://some.local-or-remote.node:8546'); const account = web3.eth.accounts.create(); const result = web3.eth.accounts.hashMessage('Test Message');这种模式下web3.eth.accounts.signTransaction是有状态的:web3主包在 packages/web3/src/accounts.ts 中通过initAccountsForContext(context)将账户模块与Web3Context绑定,签名前会先调用web3-eth的prepareTransactionForSigning(transaction, context)自动补齐nonce、chainId等网络相关信息。该上下文绑定逻辑在 packages/web3/src/web3.ts 中通过initAccountsForContext(this)注入。
方式二:独立使用子包(轻量应用)
只安装web3-eth-accounts,按需导入函数,适合对包体积敏感的应用:
import { create, hashMessage, signTransaction, Transaction } from 'web3-eth-accounts'; const account = create(); const result = hashMessage('Test Message');⚠️重要区别:独立导入时
signTransaction是无状态的。源码 account.ts 明确说明:由于没有网络访问能力去获取账户nonce与chainId,函数依赖调用方传入完整的交易对象;如需签名不完整的交易对象,应使用web3.eth.accounts.sign。同样,privateKeyToAccount返回的signTransaction会直接抛出TransactionSigningError('Do not have network access to sign the transaction')(见 account.ts)。
账户的创建与导入
create:生成全新账户
const account = web3.eth.accounts.create(); // { // address: '0xbD504f977021b5E5DdccD8741A368b147B3B38bB', // privateKey: '0x964ced1c69ad27a311c432fdc0d8211e987595f7eb34ab405a5f16bdc9563ec5', // signTransaction: [Function], // sign: [Function], // encrypt: [AsyncFunction] // }源码 account.ts 显示create使用secp256k1.utils.randomPrivateKey()(来自经审计的ethereum-cryptography包,基于密码学安全随机数)生成 32 字节私钥,再交给privateKeyToAccount组装账户对象。
privateKeyToAccount:从私钥导入账户
const account = web3.eth.accounts.privateKeyToAccount( '0x348ce564d427a3311b6536bbcff9390d69395b06ed6c486954e971d960fe8709', ); // 返回 { address: '0xb8CE9ab6943e0eCED004cDe8e3bBed6568B2Fa01', privateKey: '0x348c...', sign, signTransaction, encrypt }privateKeyToAddress:私钥推导地址
web3.eth.accounts.privateKeyToAddress( '0xbe6383dad004f233317e46ddb46ad31b16064d14447a95cc1d8c8d4bc61c3728', ); // > '0xEB014f8c8B418Db6b45774c326A0E64C78914dC0'privateKeyToPublicKey:私钥推导公钥
web3.eth.accounts.privateKeyToPublicKey( '0x1e046a882bb38236b646c9f135cf90ad90a140810f439875f2a6dd8e50fa261f', true, // isCompressed: true 返回 33 字节压缩公钥,false 返回 65 字节非压缩公钥 );底层原理:私钥校验与地址推导
上述函数都经过parseAndValidatePrivateKey(account.ts)做统一校验:
- 输入为
string或Uint8Array; - 十六进制字符串长度必须为 66(
0x+ 64 位),否则抛PrivateKeyLengthError; - 字节长度必须为 32 字节,否则抛
PrivateKeyLengthError; - 转换失败抛
InvalidPrivateKeyError。
地址推导过程(见privateKeyToAddress,account.ts):
- 由私钥经 secp256k1 得到非压缩公钥(前缀
0x04); - 去掉前缀字节后对剩余部分做 Keccak-256 哈希(
sha3Raw,注意这里是 Ethereum 的 Keccak 而非 NIST SHA3); - 取哈希后 20 字节(后 40 个十六进制字符)即为地址;
- 经
toChecksumAddress生成 EIP-55 校验和格式地址。
消息签名与验签
hashMessage:生成 Ethereum 签名消息哈希
web3.eth.accounts.hashMessage('Hello world'); // > '0x8144a6fa26be252b86456491fbcd43c1de7e022241845ffea1c3df066f7cfede' // 传入 UTF8 Hex 编码的消息也会被解码后再哈希,结果一致: web3.eth.accounts.hashMessage(web3.utils.utf8ToHex('Hello world')); // > '0x8144a6fa26be252b86456491fbcd43c1de7e022241845ffea1c3df066f7cfede' // skipPrefix=true 时不添加 Ethereum 前缀: web3.eth.accounts.hashMessage('Hello world', true); // > '0xed6c11b0b5b808960df26f5bfc471d04c1995b0ffd2055925ad1be28d6baadfd'源码 account.ts 揭示其实现:消息按"\x19Ethereum Signed Message:\n" + message.length + message包装后,用 Keccak-256(sha3Raw)哈希。skipPrefix参数(默认false)控制是否跳过该前缀。
sign:签名任意数据(带 Ethereum 前缀)
web3.eth.accounts.sign( 'Some data', '0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318', ); // > { // message: 'Some data', // messageHash: '0x1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655', // v: '0x1c', // r: '0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd', // s: '0x6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029', // signature: '0xb91467...f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c' // }signRaw:签名原始数据(无前缀)
signRaw使用hashMessage(data, true)跳过 Ethereum 前缀,直接对原始数据哈希签名(account.ts),适合需要与其他链或自定义协议兼容的场景。
signMessageWithPrivateKey:底层签名组件
sign与signRaw最终都调用signMessageWithPrivateKey(hash, privateKey)(account.ts),它使用 secp256k1 对消息哈希签名,返回{ messageHash, v, r, s, signature }。其中v = recovery + 27,signature为r || s || v的拼接。
recover:从签名恢复地址
const data = 'Some data'; const sigObj = web3.eth.accounts.sign( data, '0xbe6383dad004f233317e46ddb46ad31b16064d14447a95cc1d8c8d4bc61c3728', ); // 方式一:传入签名对象 web3.eth.accounts.recover(sigObj); // 方式二:传入 v / r / s web3.eth.accounts.recover(data, sigObj.v, sigObj.r, sigObj.s); // > '0xEB014f8c8B418Db6b45774c326A0E64C78914dC0'recover支持三种入参形态(签名对象、完整签名串、(data, v, r, s)分量形式),并从签名中解析v(大于 26 时减去 27)恢复公钥再推导地址(account.ts)。
recoverTransaction:从 RLP 交易恢复签名地址
web3.eth.accounts.recoverTransaction( '0xf869808504e3b29200831e848094f0109fc8df283027b6285cc889f5aa624eac1f55843b9aca008025a0c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895a0727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa68', ); // > '0x2c7536E3605D9C16a7a3D7b1898e529396a65c23'实现上先通过TransactionFactory.fromSerializedData解码 RLP 数据得到交易对象,再取getSenderAddress()(account.ts)。
交易签名:支持三种交易类型
signTransaction接受一个TypedTransaction(即Transaction/AccessListEIP2930Transaction/FeeMarketEIP1559Transaction三者之一,见 types.ts),签名后返回{ messageHash, v, r, s, rawTransaction, transactionHash }。
签名 Legacy 交易
import { signTransaction, Transaction } from 'web3-eth-accounts'; signTransaction( new Transaction({ to: '0x118C2E5F57FD62C2B5b46a5ae9216F4FF4011a07', value: '0x186A0', gasLimit: '0x520812', gasPrice: '0x09184e72a000', data: '', chainId: 1, nonce: 0, }), '0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318', );签名 EIP-1559 交易
signTransaction( new Transaction({ to: '0xF0109fC8DF283027b6285cc889F5aA624EaC1F55', maxPriorityFeePerGas: '0x3B9ACA00', maxFeePerGas: '0xB2D05E00', gasLimit: '0x6A4012', value: '0x186A0', data: '', chainId: 1, nonce: 0, }), '0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318', );签名 EIP-2930 交易(带 Access List)
signTransaction( new Transaction({ chainId: 1, nonce: 0, gasPrice: '0x09184e72a000', gasLimit: '0x2710321', to: '0xF0109fC8DF283027b6285cc889F5aA624EaC1F55', value: '0x186A0', data: '', accessList: [ { address: '0x0000000000000000000000000000000000000101', storageKeys: [ '0x0000000000000000000000000000000000000000000000000000000000000000', '0x00000000000000000000000000000000000000000000000000000000000060a7', ], }, ], }), '0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318', );签名返回结构示例
{ "messageHash": "0x28b7b75f7ba48d588a902c1ff4d5d13cc0ca9ac0aaa39562368146923fb853bf", "v": "0x25", "r": "0x601b0017b0e20dd0eeda4b895fbc1a9e8968990953482214f880bae593e71b5", "s": "0x690d984493560552e3ebdcc19a65b9c301ea9ddc82d3ab8cfde60485fd5722ce", "rawTransaction": "0xf869808609184e72a0008352081294118c2e5f57fd62c2b5b46a5ae9216f4ff4011a07830186a08025a00601b0017b0e20dd0eeda4b895fbc1a9e8968990953482214f880bae593e71b5a0690d984493560552e3ebdcc19a65b9c301ea9ddc82d3ab8cfde60485fd5722ce", "transactionHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" }签名实现细节
signTransaction(account.ts)的核心流程:
- 调用
transaction.sign(hexToBytes(privateKey))生成签名; - 校验
v/r/s是否存在(否则抛TransactionSigningError); - 调用
signedTx.validate(true)做严格校验,任何错误都会汇总抛出; rawTransaction为signedTx.serialize()的十六进制;transactionHash为 raw 交易数据的 Keccak-256 哈希。
TransactionFactory(transactionFactory.ts)负责按类型分发:type字段为 0 时创建 legacy 交易、1 时创建 EIP-2930、2 时创建 EIP-1559,无type字段时默认 legacy;还提供fromSerializedData(按首字节判断类型)与fromBlockBodyData(区分 Uint8Array 与数组),并可通过registerTransactionType注册自定义交易类型。此外src/common/目录内置了 mainnet/goerli/sepolia 等链参数、20+ EIP 定义与全部硬分叉配置(如 common/chains/mainnet.ts、common/eips、common/hardforks),保证交易签名时的参数正确性。
V3 Keystore:加密与解密
encrypt将私钥加密为 Ethereum V3 JSON Keystore(Web3 Secret Storage),decrypt反向还原账户。
使用 scrypt 加密
web3.eth.accounts .encrypt( '0x67f476289210e3bef3c1c75e4de993ff0a00663df00def84e73aa7411eac18a6', '123', { n: 8192, iv: web3.utils.hexToBytes('0xbfb43120ae00e9de110f8325143a2709'), salt: web3.utils.hexToBytes('0x210d0ec956787d865358ac45716e6dd42e68d48e346d795746509523aeb477dd'), }, ) .then(console.log);输出示例:
{ "version": 3, "id": "c0cb0a94-4702-4492-b6e6-eb2ac404344a", "address": "cda9a91875fc35c8ac1320e098e584495d66e47c", "crypto": { "ciphertext": "cb3e13e3281ff3861a3f0257fad4c9a51b0eb046f9c7821825c46b210f040b8f", "cipherparams": { "iv": "bfb43120ae00e9de110f8325143a2709" }, "cipher": "aes-128-ctr", "kdf": "scrypt", "kdfparams": { "n": 8192, "r": 8, "p": 1, "dklen": 32, "salt": "210d0ec956787d865358ac45716e6dd42e68d48e346d795746509523aeb477dd" }, "mac": "efbf6d3409f37c0084a79d5fdf9a6f5d97d11447517ef1ea8374f51e581b7efd" } }使用 pbkdf2 加密
web3.eth.accounts .encrypt('0x348ce564d427a3311b6536bbcff9390d69395b06ed6c486954e971d960fe8709', '123', { iv: 'bfb43120ae00e9de110f8325143a2709', salt: '210d0ec956787d865358ac45716e6dd42e68d48e346d795746509523aeb477dd', c: 262144, kdf: 'pbkdf2', }) .then(console.log);输出中kdfparams为{ dklen: 32, salt, c: 262144, prf: 'hmac-sha256' }。
解密 Keystore 还原账户
web3.eth.accounts .decrypt( { version: 3, id: 'c0cb0a94-4702-4492-b6e6-eb2ac404344a', address: 'cda9a91875fc35c8ac1320e098e584495d66e47c', crypto: { ciphertext: 'cb3e13e3281ff3861a3f0257fad4c9a51b0eb046f9c7821825c46b210f040b8f', cipherparams: { iv: 'bfb43120ae00e9de110f8325143a2709' }, cipher: 'aes-128-ctr', kdf: 'scrypt', kdfparams: { n: 8192, r: 8, p: 1, dklen: 32, salt: '210d0ec956787d865358ac45716e6dd42e68d48e346d795746509523aeb477dd', }, mac: 'efbf6d3409f37c0084a79d5fdf9a6f5d97d11447517ef1ea8374f51e581b7efd', }, }, '123', ) .then(console.log); // > { address: '0xcdA9A91875fc35c8Ac1320E098e584495d66e47c', privateKey: '67f4...', sign, signTransaction, encrypt }参数说明与底层逻辑
encrypt的可选参数(CipherOptions,见 account.ts):
| 参数 | 默认值 | 说明 |
|---|---|---|
kdf | 'scrypt' | 密钥派生函数,可选'scrypt'或'pbkdf2',其他值抛InvalidKdfError |
n | 8192 | scrypt 的 CPU/内存成本参数 |
r | 8 | scrypt 块大小参数 |
p | 1 | scrypt 并行化参数 |
c | 262144 | pbkdf2 迭代次数,小于 1000 抛PBKDF2IterationsError |
dklen | 32 | 派生密钥长度 |
iv | 随机 16 字节 | 初始化向量,长度必须为 16 字节,否则抛IVLengthError |
salt | 随机 32 字节 | 盐值 |
加密链路:私钥 → 派生密钥(scrypt/pbkdf2)→ 取派生密钥前 16 字节作为 AES-128-CTR 密钥加密私钥得到ciphertext→mac = Keccak(derivedKey[16:32] || ciphertext)。decrypt则校验 JSON Schema(schemas.ts 中的keyStoreSchema,要求包含crypto/id/version/address字段)、校验version === 3(否则抛KeyStoreVersionError)、按 KDF 派生密钥并比对mac(不一致抛KeyDerivationError),最后用 AES-CTR 解密得到私钥。
Wallet:内存钱包管理多账户
Wallet继承自Web3BaseWallet(wallet.ts),是内存中的多账户容器,其账户可直接被web3.eth.sendTransaction()或合约方法send()内部使用:
import { Web3 } from 'web3'; const web3 = new Web3('http://127.0.0.1:7545'); const wallet = await web3.eth.accounts.wallet.create(2); const signature = wallet.at(0).sign('Test Data'); // 使用钱包内账户 // 先给账户充值,再发送交易(内部用钱包账户签名): const receipt = await web3.eth.sendTransaction({ from: wallet.at(0).address, to: '0xdAC17F958D2ee523a2206206994597C13D831ec7', value: 1, // ... });核心方法速查
| 方法 | 说明 |
|---|---|
create(numberOfAccounts) | 批量生成账户并加入钱包(不会覆盖已有账户),返回钱包本身 |
add(account \| privateKey) | 用账户对象或私钥字符串添加账户;地址重复时打印警告并使用原索引 |
get(addressOrIndex) | 按地址(不区分大小写)或索引获取账户,不存在返回undefined |
remove(addressOrIndex) | 移除账户,成功返回true,找不到返回false |
clear() | 安全清空钱包所有账户(清空_addressMap并将数组长度置 0) |
encrypt(password, options?) | 用密码加密钱包内所有账户,返回 Keystore V3 对象数组 |
decrypt(encryptedWallets, password) | 批量解密 Keystore 数组并逐个add回钱包 |
save(password, keyName?) | 仅浏览器:将加密后的钱包写入localStorage,默认 key 为'web3js_wallet' |
load(password, keyName?) | 仅浏览器:从localStorage读取并解密钱包 |
// 添加账户 web3.eth.accounts.wallet.add('0xbce9b59981303e76c4878b1a6d7b088ec6b9dd5c966b7d5f54d7a749ff683387'); // 移除账户 web3.eth.accounts.wallet.remove('0x85D70633b90e03e0276B98880286D0D055685ed7'); // > true // 整体加密/解密 await web3.eth.accounts.wallet.create(1); await web3.eth.accounts.wallet.encrypt('abc').then(console.log); // 浏览器持久化 await web3.eth.accounts.wallet.save('test#!$'); // > true await web3.eth.accounts.wallet.load('test#!$');save/load通过静态方法Wallet.getStorage()(wallet.ts)探测window.localStorage可用性,并识别QuotaExceededError(Firefox 为NS_ERROR_DOM_QUOTA_REACHED)等存储异常;无可用存储时抛出'Local storage not available.'。内部用_addressMap(地址小写 → 索引)维护 O(1) 的地址查找,_defaultKeyName默认存储键为'web3js_wallet'。
开发与测试脚本
包内package.json提供完整的工程化脚本(见 package.json):
| 脚本 | 说明 |
|---|---|
clean | 用rimraf清理dist/与lib/ |
build | 并行构建 CJS、ESM 与类型声明(build:cjs/build:esm/build:types) |
lint/lint:fix | 用eslint检查 / 自动修复 |
format | 用prettier格式化代码 |
test/test:unit | 运行单元测试(jest,配置于test/unit/jest.config.js) |
test:integration | 运行集成测试(test/integration/jest.config.js) |
test:ci | CI 环境运行带覆盖率输出的测试 |
test:coverage:unit/test:coverage:integration | 输出单元 / 集成测试覆盖率 |
仓库中对应测试位于 packages/web3-eth-accounts/test/unit(含account.test.ts、wallet.test.ts以及common/、tx/子目录的交易与链参数测试)和 packages/web3-eth-accounts/test/integration,fixtures 则提供 EIP-1559/EIP-2930 交易与各链配置的 JSON 样本,可供深入验证签名与解码行为。
安全与生产环境注意事项
结合源码官方注释(account.ts)与包特性,以下几点在生产环境中务必重视:
- 本包未经安全审计:私钥生成、签名、加解密等路径应在充分评估后使用;
- 私钥妥善保管:避免硬编码在源码中,Keystore 密码应使用强口令并独立保存;
- 内存清理:使用完毕后及时清空私钥相关的临时变量与
Uint8Array; - 无状态签名的局限:独立使用
web3-eth-accounts时signTransaction不访问网络,需自行补齐nonce/chainId;在web3主包内使用则会通过 packages/web3/src/accounts.ts 自动补齐; - 交易签名前充分测试:尤其是收款地址与金额,防止误签。
总结
web3-eth-accounts以模块化的方式提供了 Ethereum 账户体系的完整能力:从create/privateKeyToAccount的账户生成与导入,到sign/signRaw/recover的消息签名验签,再到覆盖 legacy、EIP-2930、EIP-1559 三类交易的signTransaction,以及 V3 Keystore 的encrypt/decrypt和可持久化的Wallet内存钱包。它既可独立安装用于轻量应用,也可通过web3.eth.accounts集成进主包以获得网络感知的交易签名能力。相关源码、测试与链参数定义均可在 packages/web3-eth-accounts 目录下继续深入研究。
【免费下载链接】web3.jsCollection of comprehensive TypeScript libraries for Interaction with the Ethereum JSON RPC API and utility functions.项目地址: https://gitcode.com/gh_mirrors/we/web3.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考