Web3.js 实战:彻底搞懂 ETH 与 ERC20 代币收发全流程

·

关键词:web3.js、以太坊转账、ERC20、私钥签名、Rinkeby 测试网、 gas 费用、JavaScript 自动脚本

ETH 与 ERC20 交易区别速览

无论发送 ETH 还是任意 ERC20 代币,底层都需要构造一条“原始交易(rawTx)”。差异只体现在两个字段:

其余字段(from、nonce、gasLimit、gasPrice、chainId)一致即可。

零基础上手:配置开发环境

本示例基于 Nodejs 14+,操作系统不限。

步骤 1:新建项目并安装依赖

mkdir sendToken && cd sendToken
npm init -y
npm install web3 ethereumjs-tx @truffle/hdwallet-provider

步骤 2:目录分门别类

sendToken/
 ├─ myabi/
 │   └─ ZTA_abi.json            # ERC20 合约 ABI
 ├─ projectConfig/
 │   ├─ net.infuraKey           # Infura Project ID
 │   ├─ net.prikey             # 16 进制私钥,不要泄露
 │   └─ net.mnemonic           # 可选:12 或 24 词助记词
 └─ test/
     ├─ 1.sendEth.js
     └─ 2.sendERC20.js

👉 不想自己拼 ABI?几个主流代币的 ABI 通用模板速查


查询余额:读链上数据无需签名

查询 ETH 余额

const Web3 = require('web3');
const web3 = new Web3('https://rinkeby.infura.io/v3/<YourKey>');

async function getEthBalance(addr) {
  const wei = await web3.eth.getBalance(addr);
  console.log('ETH Balance:', web3.utils.fromWei(wei, 'ether'));
}
await getEthBalance('0xYourAddress');

查询 ERC20 余额

async function getERC20Balance(tokenAddr, userAddr) {
  const abi = require('./myabi/ZTA_abi.json');
  const contract = new web3.eth.Contract(abi, tokenAddr);
  const raw = await contract.methods.balanceOf(userAddr).call();
  console.log('Token Balance:', web3.utils.fromWei(raw, 'ether'));
}
await getERC20Balance('0xTokenContract', '0xYourAddress');

发送交易:三步走

第一步:拿到账户 nonce

const nonce = await web3.eth.getTransactionCount(from, 'pending');

第二步:构造原始交易

通用构造函数:

const Tx = require('ethereumjs-tx').Transaction;
const rawTx = {
  nonce: web3.utils.toHex(nonce),
  gasLimit: web3.utils.toHex(8000000),
  gasPrice: web3.utils.toHex(10e9),
  to: '',               // ETH 填收款地址;ERC20 填合约地址
  value: '',            // ETH 填 wei 级别金额;ERC20 填 '0x0'
  data: '',             // ERC20 时为 transfer 的 ABI
  chainId: 4            // 4 = Rinkeby
}

第三步:私钥签名并广播

const tx = new Tx(rawTx, { chain: 'rinkeby' });
tx.sign(Buffer.from(privKey, 'hex'));
const serialized = '0x' + tx.serialize().toString('hex');

const receipt = await web3.eth.sendSignedTransaction(serialized);
console.log('txHash:', receipt.transactionHash);

案例一:发送 0.02 ETH

文件路径:test/1.sendEth.js

const Tx = require('ethereumjs-tx');
const Web3 = require('web3');
const fs = require('fs');

const infuraKey = fs.readFileSync('./projectConfig/net.infuraKey').toString().trim();
const privKey = fs.readFileSync('./projectConfig/net.prikey').toString().trim();

const web3 = new Web3(`https://rinkeby.infura.io/v3/${infuraKey}`);

(async () => {
  const from = '0xSenderAddress';
  const to   = '0xRecipientAddress';
  const amount = web3.utils.toWei('0.02', 'ether');

  const nonce = await web3.eth.getTransactionCount(from);
  const rawTx = {
    from,
    nonce: web3.utils.toHex(nonce),
    gasLimit: web3.utils.toHex(21000), // 普通转账固定 21000
    gasPrice: web3.utils.toHex(web3.utils.toWei('1', 'gwei')),
    to,
    value: web3.utils.toHex(amount),
    data: '0x',
    chainId: 4
  };

  const tx = new Tx(rawTx, { chain: 'rinkeby' });
  tx.sign(Buffer.from(privKey, 'hex'));

  const serializedTx = tx.serialize();
  const receipt = await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'));
  console.log('ETH Send receipt:', receipt.transactionHash);
})();

👉 完整源码在线演练平台


案例二:发送 0.02 ZTA(ERC20)

与 ETH 的差异在于 data 字段需调用合约方法。

// test/2.sendERC20.js
const rawTx = {
  to: '0xAc194f047E43Ee0Ee10026C0B7AAA66489a0Ec45', // ZTA 合约
  value: '0x0',
  data: contract.methods.transfer(to, web3.utils.toWei('0.02')).encodeABI(),
  nonce, gasLimit, gasPrice, chainId
  // ...其余同 ETH
};

替换 data 字段后,其余签名与广播完全一致。


常见问题 FAQ

Q1:为什么我得到的错误是 nonce too low
A:本地缓存的交易数与链上不同步。每次广播前刷新一次 getTransactionCount,并在 pending 模式下累加即可。

Q2:私钥直接写在文件安全吗?
A:不安全。建议把 projectConfig 加入 .gitignore 并使用环境变量或密钥管理服务。

Q3:gasLimit 设置 8,000,000 会不会浪费?
A:交易只会消耗实际所需 gas;设置偏高不会多扣费,但永远建议 *1.2 后按网络拥堵动态调整。

Q4:Rinkeby 与水龙头的获取额度?
A:每个地址可通过官方水龙头每天领取 0.5–1 ETH,单次链接一次即可。

Q5:如何把脚本部署到云函数?
A:云函数不宜存放私钥;推荐用 AWS KMS 或 GCP Secret Manager 生成签名后由脚本拉取,再用 HTTP 触发或定时器驱动。

Q6:手续费低导致交易卡住怎么办?
A:可使用“加速交易”(replace-by-fee)策略:用同样的 nonce 与更高 gasPrice 重新签名广播即可覆盖原交易。