关键词:web3.js、以太坊转账、ERC20、私钥签名、Rinkeby 测试网、 gas 费用、JavaScript 自动脚本
ETH 与 ERC20 交易区别速览
无论发送 ETH 还是任意 ERC20 代币,底层都需要构造一条“原始交易(rawTx)”。差异只体现在两个字段:
- to:ETH 填收款地址;ERC20 填合约地址。
- value:ETH 发送金额直接写入;ERC20 写
0x0,真实金额藏在 data 字段里的transfer方法。
其余字段(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 重新签名广播即可覆盖原交易。