进阶 docs.web3js.org 2026-10-19 02:32:29 · 18 阅读

第8章 Web3.js 教程第 8 章:发送交易(Transactions)

发送交易 本教程将带你在开发网络上使用账户发送交易,包括如何订阅与交易相关的事件。教程涉及以太坊的基础概念,如账户、以太币单位、交易和 gas 费用,以及 Hardhat 开发环境的基本用法。 概述 以下是本教程的操作步骤概览: 检查前置条件 新建目录并初始化一个 Node.js 项目 配置 Web3.js 和 Hardhat 发送交易并查看结果 发送交易并订阅其事件 发送原始交易(raw transaction) 提示:如果你在阅读过程中遇到问题或有任何疑问,欢迎随时求助。我们友好的社区很乐意为你提供帮助!加入我们的 Discord 服务器,前往 #web3js-general 频道,与其他开发者交流并获取你需要的支持。 第 1 步:前置条件 本教程假设你已熟悉命令行的基本操作,并了解 JavaScript 和 Node.js。开始之前,请确保已安装 Node.js 及其包管理器 npm。 $: node -v
# 版本可能不同,建议使用当前稳定版
v18.16.1
$: npm -v
9.5.1
第 2 步:新建目录并初始化 Node.js 项目 首先,为项目创建一个新目录并进入其中: mkdir account-transactions-tutorial
cd account-transactions-tutorial
接下来,使用 npm 初始化一个新的 Node.js 项目: npm init -y
这会在项目目录中生成一个 package.json 文件。 第 3 步:配置 Web3.js 和 Hardhat 使用 npm 安装所需的包: npm i web3 hardhat
然后初始化 Hardhat 项目: npx hardhat init
初始化过程中会提示几个问题,每项都选择默认选项即可。初始化完成后,会生成一系列新的文件和目录。 执行以下命令启动 Hardhat 开发网络: npx hardhat node
执行后会输出如下内容,其中包含连接开发网络所需的 URL,以及开发网络的测试账户: Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========

WARNING: 这些账户及其私钥是公开已知的。
将任何资金发送到主网或其他生产网络上的这些账户都将丢失。

账户 #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
私钥:

...

账户 #19: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199 (10000 ETH)
私钥:

WARNING: 这些账户及其私钥是公开已知的。
将任何资金发送到主网或其他生产网络上的这些账户都将丢失。
infoHardhat 开发网络中的测试账户预存有 ETH,用于测试目的。本教程将使用这些账户来发送交易。 Hardhat 开发网络需要保持在启动它的终端中运行。请在项目目录中打开一个新的终端实例,以执行本教程中其余的命令。 接下来,在你的项目目录中创建一个名为 index.js 的新文件,并添加以下代码: const { Web3 } = require('web3');

const web3 = new Web3('http://127.0.0.1:8545/');

// 将链 ID 打印到控制台
web3.eth
.getChainId()
.then(result => {
console.log('Chain ID: ' + result);
})
.catch(error => {
console.error(error);
});
这段代码建立到 Hardhat 开发网络的 Web3.js 连接,并将链 ID 打印到控制台。 note本教程使用 Hardhat 开发网络的默认 URL(http://127.0.0.1:8545/)。请确保使用启动 Hardhat 开发网络时提供的实际 URL。 运行以下命令以测试连接: node index.js
如果一切正常,Hardhat 开发网络的链 ID 将打印到控制台: Chain ID: 31337
步骤 4:发送交易并查看结果​ 在你的项目目录中创建一个名为 transaction-receipt.js 的新文件,并添加以下代码: note请将 privateKey 变量的值替换为 Hardhat 开发网络测试账户的其中一个私钥。 const { Web3 } = require('web3');

async function main() {
const web3 = new Web3('http://127.0.0.1:8545/');

// 使用 Hardhat 测试账户的私钥创建一个新的 Web3.js 账户对象
const privateKey = '';
// 该账户通过钱包创建,使用更方便
const sender = web3.eth.accounts.wallet.add(privateKey)[0];

// 生成一个新的随机 Web3.js 账户对象用于接收交易
const receiver = web3.eth.accounts.create();

// 打印初始余额
console.log(
'发送方初始余额:',
// 以 wei 为单位的账户余额
await web3.eth.getBalance(sender.address),
);
console.log(
'接收方初始余额:',
// 以 wei 为单位的账户余额
await web3.eth.getBalance(receiver.address),
);

// 签名并发送交易
const receipt = await web3.eth.sendTransaction({
from: sender.address,
to: receiver.address,
// 以 wei 为单位的转账金额
value: 100,
});

// 打印交易回执
console.log(receipt);

// 打印最终余额
console.log('发送方最终余额:', await web3.eth.getBalance(sender.address));
console.log('接收方最终余额:', await web3.eth.getBalance(receiver.address));
}

main();
该脚本使用 web3.eth.sendTransaction 函数来发送交易。此函数的参数由 Transaction 接口定义。 note默认情况下,Web3.js 使用 wei 作为账户余额和交易价值的计量单位。web3-utils 包提供了辅助函数,可用于将其他以太单位与 wei 进行相互转换。 在本例中,账户是通过 Wallet 创建的,这会将该账户添加到 Web3.js 的内部上下文(即 Web3Context)中。当从通过 Wallet 创建并添加到上下文的账户发送交易时,Web3.js 会自动使用该账户的私钥对交易进行签名。关于如何从未通过钱包创建且未添加到上下文的账户发送交易,步骤 6:发送原始交易中有所描述。 执行以下命令运行 transaction-receipt.js 中的代码: node transaction-receipt.js
输出结果应与以下内容类似: Initial sender balance: 10000000000000000000000n
Initial receiver balance: 0n
{
blockHash: '0x9a7250ca7947b84972bf973da232656280f2bb5eab71777a34fb49c08585a2b2',
blockNumber: 1n,
cumulativeGasUsed: 21000n,
effectiveGasPrice: 3375000000n,
from: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
gasUsed: 21000n,
logs: [],
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
status: 1n,
to: '0xc14a6278b4a8d4f9eb198b75b7013d58b1de94da',
transactionHash: '0xaf4b89e10c811e3f2c954447e40f7ecac073ca41fa4a896a3a86299cae24b339',
transactionIndex: 0n,
type: 2n
}
Final sender balance: 9999999929124999999900n
Final receiver balance: 100n
可以看到,发送方余额减少的数量比实际转给接收方的金额要多。这是因为发送方还需要用余额支付交易的 gas 费用。交易回执中会记录实际消耗的 gas 数量(cumulativeGasUsed)以及以 wei 为单位的 gas 价格(effectiveGasPrice)。从发送方余额中扣除的总金额,等于转账金额加上 gas 费用(cumulativeGasUsed 乘以 effectiveGasPrice)。 infoGas 和其他费用是 Ethereum 开发者必须理解的重要概念,详情可参阅 Gas and Priority Fees 指南。 第 5 步:发送交易并订阅其事件 在上一个例子中,transaction-receipt.js 脚本演示了如何向网络发送交易,并在交易成功后查看结果。但交易的生命周期其实包含更多阶段,而 Web3.js 可以轻松订阅这些生命周期阶段,并为每个阶段创建自定义的处理函数。Web3.js 支持订阅以下交易生命周期事件: Sending - Web3.js 正准备把交易发送到网络 Sent - 交易已发送到网络 Transaction hash - 已生成交易的哈希 Receipt - 交易已被打包进区块 Confirmation - 包含该交易的区块已最终确认 Error - 交易过程中遇到问题 在项目目录中新建一个名为 transaction-events.js 的文件,并添加以下代码: const { Web3 } = require('web3');

const web3 = new Web3('http://127.0.0.1:8545/');

const privateKey = '';
const sender = web3.eth.accounts.wallet.add(privateKey)[0];

const receiver = web3.eth.accounts.create();

web3.eth
.sendTransaction({
from: sender.address,
to: receiver.address,
value: 100,
})
.on('sending', sending => {
console.log('Sending:', sending);
})
.on('sent', sent => {
console.log('Sent:', sent);
})
.on('transactionHash', transactionHash => {
console.log('Transaction Hash:', transactionHash);
})
.on('receipt', receipt => {
console.log('Receipt:', receipt);
})
.on('confirmation', confirmation => {
console.log('Confirmation:', confirmation);
process.exit(0);
})
.on('error', error => {
console.log('Error:', error);
process.exit(1);
});
运行 transaction-events.js 中的代码,请执行以下命令: node transaction-events.js
输出结果应与以下内容相似: Sending: {
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
to: '0x077bd1F472a0C7eD3214583d4B0fE8b21e4b3b5A',
value: '0x64',
gasPrice: undefined,
maxPriorityFeePerGas: '0x9502f900',
maxFeePerGas: '0xfd51da80'
}
Sent: {
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
to: '0x077bd1F472a0C7eD3214583d4B0fE8b21e4b3b5A',
value: '0x64',
gasPrice: undefined,
maxPriorityFeePerGas: '0x9502f900',
maxFeePerGas: '0xfd51da80'
}
Transaction Hash: 0x66b4cd592e82ea09dee4015277aee9299bafce369819a82b4797a06ba010c6b1
Receipt: {
blockHash: '0xeedfe16bb67dae4cc2064f68ddcef1b83deb4b1eaf19b99c2a175aafaded36b9',
blockNumber: 2n,
cumulativeGasUsed: 21000n,
effectiveGasPrice: 3265778125n,
from: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
gasUsed: 21000n,
logs: [],
logsBloom: '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
status: 1n,
to: '0x077bd1f472a0c7ed3214583d4b0fe8b21e4b3b5a',
transactionHash: '0x66b4cd592e82ea09dee4015277aee99bafce369819a82b4797a06ba010c6b1',
transactionIndex: 0n,
type: 2n
}
Confirmation: {
confirmations: 1n,
receipt: {
blockHash: '0xeedfe16bb67dae4cc2064f68ddcef1b83deb4b1eaf19b99c2a175aafaded36b9',
blockNumber: 2n,
cumulativeGasUsed: 21000n,
effectiveGasPrice: 3265778125n,
from: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
gasUsed: 21000n,
logs: [],
logsBloom: '0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
status: 1n,
to: '0x077bd1f472a0c7ed3214583d4b0fe8b21e4b3b5a',
transactionHash: '0x66b4cd592e82ea09dee4015277aee99bafce369819a82b4797a06ba010c6b1',
transactionIndex: 0n,
type: 2n
},
latestBlockHash: '0xeedfe16bb67dae4cc2064f68ddcef1b83deb4b1eaf19b99c2a175aafaded36b9'
}
由于交易应当成功执行,控制台不应记录错误事件。若想查看错误事件,可通过修改 sender 变量的值,使用一个随机账户发送交易,具体如下: const sender = web3.eth.accounts.wallet.create(1)[0];
这个改动会触发错误,因为新生成的随机账户没有余额,无法支付交易的气体费用。 按之前步骤重新运行更新后的脚本。输出应与以下内容类似: Sending: {
from: '0x2F2f42075567cd9823961A4e04171cEbdc45E390',
to: '0xb52F3110C8d3eFDa5137A8A935eE35aACCBB3613',
value: '0x64',
gasPrice: undefined,
maxPriorityFeePerGas: '0x9502f900',
maxFeePerGas: '0xf04caa9a'
}
Error: InvalidResponseError: Returned error: Sender doesn't have enough funds to send tx. The max upfront cost is: 84666712806350 and the sender's balance is: 0.
at Web3RequestManager._processJsonRpcResponse (.../web3-core/lib/commonjs/web3_request_manager.js:281:23)
at Web3RequestManager. (.../web3-core/lib/commonjs/web3_request_manager.js:167:29)
错误日志(保留原文):
at Generator.next (<anonymous>)
at fulfilled (.../web3-core/lib/commonjs/web3_request_manager.js:21:58)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
cause: {
code: -32000,
message: "Sender doesn't have enough funds to send tx. The max upfront cost is: 84666712806350 and the sender's balance is: 0.",
data: {
message: "Sender doesn't have enough funds to send tx. The max upfront cost is: 84666712806350 and the sender's balance is: 0.",
data: null
}
},
code: 101,
data: {
message: "Sender doesn't have enough funds to send tx. The max upfront cost is: 84666712806350 and the sender's balance is: 0.",
data: null
},
request: {
jsonrpc: '2.0',
id: 'bf7e8ddd-f2bb-45ab-9168-8c577cd1bcb9',
method: 'eth_sendRawTransaction',
params: [
'0x02f86c827a6980849502f90084f04caa9a82520994b52f3110c8d3efda5137a8a935ee35aaccbb36136480c080a040b3b5d9a72ced0ec70750157a34972233b51e49a511a813763487f84905841ca06237e4a9b1e00934c6fabd32588ed44b6b55bb4aadb67939410a7a0cb801e2f1'
]
}
}
第 6 步:发送原始交易(Raw Transaction)​ 前面的例子都借助了 Web3.js 提供的便利,用 Wallet 创建的账户自动签署交易。本教程的最后一个例子将演示如何使用非钱包账户手动签署交易。 在项目目录下新建一个名为 raw-transaction.js 的文件,加入以下代码: const { Web3 } = require('web3');

async function main() {
const web3 = new Web3('http://127.0.0.1:8545/');

const privateKey = '<redacted>';
// 不通过钱包,直接导入 Hardhat 测试账户
const sender = web3.eth.accounts.privateKeyToAccount(privateKey);

const receiver = web3.eth.accounts.create();

// 用于计算交易的 maxFeePerGas
const block = await web3.eth.getBlock();

const transaction = {
from: sender.address,
to: receiver.address,
value: 100,
// 原始交易必须包含以下两个属性
maxFeePerGas: block.baseFeePerGas * 2n,
maxPriorityFeePerGas: 100000,
};

const signedTransaction = await web3.eth.accounts.signTransaction(
transaction,
sender.privateKey,
);
const receipt = await web3.eth.sendSignedTransaction(signedTransaction.rawTransaction);
在下面的示例中,`signTransaction` 函数会先使用发送方的私钥手动对交易进行签名,随后通过 `sendSignedTransaction` 函数将交易广播到网络。需要注意的是,与依赖 Web3.js 上下文来自动构建最终交易的前述示例不同,这里需要在 `Transaction` 对象中显式包含 `maxFeePerGas` 和 `maxPriorityFeePerGas` 属性。这些属性的具体含义超出了本教程的讨论范围,但请注意 `maxFeePerGas` 是通过从网络获取的实际值(`baseFeePerGas`)计算得出的,而 `maxPriorityFeePerGas` 则被设定为一个被认为足够高的数值。 运行以下命令以执行 raw-transaction.js 中的代码: node raw-transaction.js
输出应与以下内容类似: {
blockHash: '0xb83ccddddb4e715e51765182e184251d55bd4bc5da982691b169d21e39b62559',
blockNumber: 3n,
cumulativeGasUsed: 21000n,
effectiveGasPrice: 670289871n,
from: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
gasUsed: 21000n,
logs: [],
logsBloom: '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
status: 1n,
to: '0x9f676309956465c7e4d188f58149a9e8f945d477',
transactionHash: '0xe56a9aee0f4927d5ce1eefaf2a323d32eb51e762bcf2ef33989c101302379bfc',
transactionIndex: 0n,
type: 2n
}
结论 本教程展示了如何使用账户发送交易,包括如何订阅与交易相关的事件。从账户发送交易最简单的做法是结合 Wallet 创建账户,这样既会把该账户添加到 Web3.js 上下文中,还能自动化使用账户私钥对交易进行签名的流程。但对于非钱包账户,或需要将对交易进行签名和发送这两者解耦的场景(例如离线签名),Web3.js 提供了便捷的接口,允许在发送前先手动对交易进行签名。

评论 (0)