hedera-consensus-service
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHedera Consensus Service (HCS) — JavaScript SDK
Hedera Consensus Service (HCS) — JavaScript SDK
HCS provides a decentralized, ordered message log with consensus timestamps. It works like a pub/sub system: you create topics, submit messages to them, and subscribe to receive messages in real time via mirror nodes. Messages are immutable, ordered, and timestamped by network consensus — useful for audit trails, event logs, supply chain tracking, and decentralized communication.
HCS 提供带有共识时间戳的去中心化有序消息日志。它的工作方式类似发布/订阅(pub/sub)系统:你可以创建主题,向主题提交消息,并通过镜像节点订阅以实时接收消息。消息具有不可篡改性、有序性,并由网络共识打上时间戳——适用于审计追踪、事件日志、供应链追踪和去中心化通信场景。
Setup
配置
All imports come from . Two setup patterns:
@hiero-ledger/sdk所有导入均来自。有两种配置模式:
@hiero-ledger/sdkClient + setOperator (direct)
客户端 + setOperator(直接模式)
js
import { Client, AccountId, PrivateKey } from "@hiero-ledger/sdk";
const client = Client.forName(process.env.HEDERA_NETWORK)
.setOperator(
AccountId.fromString(process.env.OPERATOR_ID),
PrivateKey.fromStringECDSA(process.env.OPERATOR_KEY),
);js
import { Client, AccountId, PrivateKey } from "@hiero-ledger/sdk";
const client = Client.forName(process.env.HEDERA_NETWORK)
.setOperator(
AccountId.fromString(process.env.OPERATOR_ID),
PrivateKey.fromStringECDSA(process.env.OPERATOR_KEY),
);Wallet + LocalProvider (signer-based)
钱包 + LocalProvider(签名者模式)
js
import { Wallet, LocalProvider } from "@hiero-ledger/sdk";
const provider = new LocalProvider();
const wallet = new Wallet(process.env.OPERATOR_ID, process.env.OPERATOR_KEY, provider);With the signer pattern, use , , , and .
freezeWithSigner(wallet)signWithSigner(wallet)executeWithSigner(wallet)getReceiptWithSigner(wallet)js
import { Wallet, LocalProvider } from "@hiero-ledger/sdk";
const provider = new LocalProvider();
const wallet = new Wallet(process.env.OPERATOR_ID, process.env.OPERATOR_KEY, provider);使用签名者模式时,需调用、、和方法。
freezeWithSigner(wallet)signWithSigner(wallet)executeWithSigner(wallet)getReceiptWithSigner(wallet)Creating a Topic
创建主题
js
import { TopicCreateTransaction } from "@hiero-ledger/sdk";
const { topicId } = await (
await new TopicCreateTransaction()
.setTopicMemo("My event log")
.setAdminKey(operatorKey) // allows update/delete
.setSubmitKey(operatorKey) // restricts who can post
.execute(client)
).getReceipt(client);
console.log(`Topic created: ${topicId.toString()}`);Key behaviors:
- Without an , the topic cannot be updated or deleted (only expiration can be extended).
adminKey - Without a , anyone can submit messages.
submitKey - Default auto-renew period is 90 days.
js
import { TopicCreateTransaction } from "@hiero-ledger/sdk";
const { topicId } = await (
await new TopicCreateTransaction()
.setTopicMemo("My event log")
.setAdminKey(operatorKey) // 允许更新/删除主题
.setSubmitKey(operatorKey) // 限制可发布消息的对象
.execute(client)
).getReceipt(client);
console.log(`Topic created: ${topicId.toString()}`);核心特性:
- 若未设置,主题无法被更新或删除(仅可延长有效期)。
adminKey - 若未设置,任何人都可提交消息。
submitKey - 默认自动续期时间为90天。
Topic with Custom Fees
自定义费用的主题
Topics can charge per-message fees (Hbar or token-denominated):
js
import { TopicCreateTransaction, CustomFixedFee, Hbar } from "@hiero-ledger/sdk";
const fee = new CustomFixedFee()
.setAmount(new Hbar(1).toTinybars())
.setFeeCollectorAccountId(collectorId);
const { topicId } = await (
await new TopicCreateTransaction()
.setAdminKey(operatorKey)
.setSubmitKey(operatorKey)
.setFeeScheduleKey(operatorKey)
.setCustomFees([fee])
.addFeeExemptKey(trustedKey) // this key skips fees
.execute(client)
).getReceipt(client);When paying custom fees, submitters can set a maximum they're willing to pay:
js
import { CustomFeeLimit, CustomFixedFee, Hbar, HbarUnit } from "@hiero-ledger/sdk";
const limit = new CustomFeeLimit()
.setAccountId(payerId)
.setFees([
new CustomFixedFee().setAmount(Hbar.from(2, HbarUnit.Hbar).toTinybars())
]);
await new TopicMessageSubmitTransaction()
.setTopicId(topicId)
.setMessage("Hello")
.setCustomFeeLimits([limit])
.execute(client);主题可设置每条消息的费用(以Hbar或代币计价):
js
import { TopicCreateTransaction, CustomFixedFee, Hbar } from "@hiero-ledger/sdk";
const fee = new CustomFixedFee()
.setAmount(new Hbar(1).toTinybars())
.setFeeCollectorAccountId(collectorId);
const { topicId } = await (
await new TopicCreateTransaction()
.setAdminKey(operatorKey)
.setSubmitKey(operatorKey)
.setFeeScheduleKey(operatorKey)
.setCustomFees([fee])
.addFeeExemptKey(trustedKey) // 该密钥可免缴费用
.execute(client)
).getReceipt(client);支付自定义费用时,提交者可设置愿意支付的最高限额:
js
import { CustomFeeLimit, CustomFixedFee, Hbar, HbarUnit } from "@hiero-ledger/sdk";
const limit = new CustomFeeLimit()
.setAccountId(payerId)
.setFees([
new CustomFixedFee().setAmount(Hbar.from(2, HbarUnit.Hbar).toTinybars())
]);
await new TopicMessageSubmitTransaction()
.setTopicId(topicId)
.setMessage("Hello")
.setCustomFeeLimits([limit])
.execute(client);Submitting Messages
提交消息
js
import { TopicMessageSubmitTransaction } from "@hiero-ledger/sdk";
const response = await new TopicMessageSubmitTransaction()
.setTopicId(topicId)
.setMessage("Hello, Hedera!")
.execute(client);
const receipt = await response.getReceipt(client);
console.log(`Sequence: ${receipt.topicSequenceNumber}`);Messages can be or . The receipt contains (incremented per message) and .
stringUint8ArraytopicSequenceNumbertopicRunningHashjs
import { TopicMessageSubmitTransaction } from "@hiero-ledger/sdk";
const response = await new TopicMessageSubmitTransaction()
.setTopicId(topicId)
.setMessage("Hello, Hedera!")
.execute(client);
const receipt = await response.getReceipt(client);
console.log(`Sequence: ${receipt.topicSequenceNumber}`);消息可以是或类型。交易回执包含(每条消息递增)和。
stringUint8ArraytopicSequenceNumbertopicRunningHashWhen a submit key exists
存在提交密钥时
If the topic has a submit key, messages must be signed by it:
js
await (
await new TopicMessageSubmitTransaction()
.setTopicId(topicId)
.setMessage("Authorized message")
.freezeWith(client)
.sign(submitKey)
).execute(client);若主题设置了提交密钥,消息必须由该密钥签名:
js
await (
await new TopicMessageSubmitTransaction()
.setTopicId(topicId)
.setMessage("Authorized message")
.freezeWith(client)
.sign(submitKey)
).execute(client);Subscribing to Messages
订阅消息
The creates a real-time subscription via the mirror node. Messages arrive as they reach consensus.
TopicMessageQueryjs
import { TopicMessageQuery } from "@hiero-ledger/sdk";
const handle = new TopicMessageQuery()
.setTopicId(topicId)
.setStartTime(0) // from the beginning
.subscribe(
client,
(message, error) => console.error("Error:", error),
(message) => {
console.log(
`[${message.consensusTimestamp}] #${message.sequenceNumber}: ` +
Buffer.from(message.contents).toString("utf8")
);
},
);
// Later, to stop receiving:
handle.unsubscribe();Important: After creating a topic, wait a few seconds before subscribing — the mirror node needs time to sync the new topic.
TopicMessageQueryjs
import { TopicMessageQuery } from "@hiero-ledger/sdk";
const handle = new TopicMessageQuery()
.setTopicId(topicId)
.setStartTime(0) // 从最开始接收
.subscribe(
client,
(message, error) => console.error("Error:", error),
(message) => {
console.log(
`[${message.consensusTimestamp}] #${message.sequenceNumber}: ` +
Buffer.from(message.contents).toString("utf8")
);
},
);
// 后续停止接收时调用:
handle.unsubscribe();重要提示:创建主题后,需等待几秒再订阅——镜像节点需要时间同步新主题。
Subscription Options
订阅选项
js
new TopicMessageQuery()
.setTopicId(topicId)
.setStartTime(startTimestamp) // receive from this time forward
.setEndTime(endTimestamp) // stop after this time
.setLimit(100) // max messages to receive
.setMaxAttempts(20) // retry attempts (default: 20)
.setMaxBackoff(8000) // max retry delay ms (default: 8000)
.setErrorHandler((msg, err) => {}) // error callback
.setCompletionHandler(() => {}) // fires when limit/endTime reached
.subscribe(client, errorHandler, messageHandler);js
new TopicMessageQuery()
.setTopicId(topicId)
.setStartTime(startTimestamp) // 从此时间点开始接收
.setEndTime(endTimestamp) // 到此时间点停止接收
.setLimit(100) // 最多接收的消息数量
.setMaxAttempts(20) // 重试次数(默认:20)
.setMaxBackoff(8000) // 最大重试延迟毫秒数(默认:8000)
.setErrorHandler((msg, err) => {}) // 错误回调函数
.setCompletionHandler(() => {}) // 达到数量限制/结束时间时触发
.subscribe(client, errorHandler, messageHandler);TopicMessage Properties
TopicMessage 属性
Each received has:
TopicMessage- — when the message reached consensus
consensusTimestamp - —
contentsmessage body (automatically reassembled from chunks)Uint8Array - — position in the topic (starts at 1)
sequenceNumber - — SHA-384 running hash of the topic at this message
runningHash - — individual
chunksif the message was chunkedTopicMessageChunk[] - — original transaction ID (for chunked messages)
initialTransactionId
每个接收到的包含以下属性:
TopicMessage- — 消息达成共识的时间
consensusTimestamp - — 消息主体(
contents类型,已自动从分片重组)Uint8Array - — 消息在主题中的位置(从1开始)
sequenceNumber - — 到该消息为止主题的SHA-384滚动哈希
runningHash - — 若消息被分片,则为单个
chunks数组TopicMessageChunk[] - — 原始交易ID(针对分片消息)
initialTransactionId
Chunked Messages
分片消息
Messages larger than 1024 bytes are automatically split into chunks. Each chunk is a separate transaction on the network. The SDK handles splitting on submit and reassembly on subscribe.
js
const largeMessage = "x".repeat(5000); // 5KB message
// Option 1: execute() returns first chunk's response
const response = await new TopicMessageSubmitTransaction()
.setTopicId(topicId)
.setMessage(largeMessage)
.execute(client);
// Option 2: executeAll() returns all chunk responses
const responses = await new TopicMessageSubmitTransaction()
.setTopicId(topicId)
.setMessage(largeMessage)
.setMaxChunks(30) // default: 20 (max ~20KB at 1024/chunk)
.setChunkSize(2048) // override chunk size (default: 1024)
.executeAll(client);
for (const resp of responses) {
const receipt = await resp.getReceipt(client);
console.log(`Chunk seq: ${receipt.topicSequenceNumber}`);
}Limits:
- Default chunk size: 1024 bytes
- Default max chunks: 20 (so ~20KB max message by default)
- Configurable via and
setChunkSize()setMaxChunks() - Subscribers automatically reassemble chunks into a single
TopicMessage
超过1024字节的消息会自动拆分为分片。每个分片是网络上的独立交易。SDK会处理提交时的拆分和订阅时的重组。
js
const largeMessage = "x".repeat(5000); // 5KB消息
// 方式1:execute()返回第一个分片的响应
const response = await new TopicMessageSubmitTransaction()
.setTopicId(topicId)
.setMessage(largeMessage)
.execute(client);
// 方式2:executeAll()返回所有分片的响应
const responses = await new TopicMessageSubmitTransaction()
.setTopicId(topicId)
.setMessage(largeMessage)
.setMaxChunks(30) // 默认:20(按1024字节/分片计算,最大约20KB)
.setChunkSize(2048) // 覆盖默认分片大小(默认:1024)
.executeAll(client);
for (const resp of responses) {
const receipt = await resp.getReceipt(client);
console.log(`Chunk seq: ${receipt.topicSequenceNumber}`);
}限制:
- 默认分片大小:1024字节
- 默认最大分片数:20(因此默认最大消息约20KB)
- 可通过和
setChunkSize()配置setMaxChunks() - 订阅者会自动将分片重组为单个
TopicMessage
Updating a Topic
更新主题
js
import { TopicUpdateTransaction } from "@hiero-ledger/sdk";
await new TopicUpdateTransaction()
.setTopicId(topicId)
.setTopicMemo("Updated memo")
.setSubmitKey(newSubmitKey)
.execute(client);All update operations require the admin key. Key-specific updates:
- Changing the admin key requires both old and new admin keys to sign
- Setting a new auto-renew account requires that account to sign
- You can clear keys with ,
clearAdminKey(), etc.clearSubmitKey()
js
import { TopicUpdateTransaction } from "@hiero-ledger/sdk";
await new TopicUpdateTransaction()
.setTopicId(topicId)
.setTopicMemo("Updated memo")
.setSubmitKey(newSubmitKey)
.execute(client);所有更新操作都需要管理员密钥。密钥相关的更新规则:
- 更改管理员密钥需要旧密钥和新密钥共同签名
- 设置新的自动续期账户需要该账户签名
- 可通过、
clearAdminKey()等方法清除密钥clearSubmitKey()
Updating Topic Fees
更新主题费用
js
await new TopicUpdateTransaction()
.setTopicId(topicId)
.setCustomFees([newFee])
.addFeeExemptKey(anotherKey)
.execute(client);js
await new TopicUpdateTransaction()
.setTopicId(topicId)
.setCustomFees([newFee])
.addFeeExemptKey(anotherKey)
.execute(client);Deleting a Topic
删除主题
js
import { TopicDeleteTransaction } from "@hiero-ledger/sdk";
await new TopicDeleteTransaction()
.setTopicId(topicId)
.execute(client);Requires the admin key. After deletion, no operations on the topic will succeed.
js
import { TopicDeleteTransaction } from "@hiero-ledger/sdk";
await new TopicDeleteTransaction()
.setTopicId(topicId)
.execute(client);需要管理员密钥。删除后,该主题的所有操作都将失败。
Querying Topic Info
查询主题信息
js
import { TopicInfoQuery } from "@hiero-ledger/sdk";
const info = await new TopicInfoQuery()
.setTopicId(topicId)
.execute(client);
console.log(`Memo: ${info.topicMemo}`);
console.log(`Sequence: ${info.sequenceNumber}`);
console.log(`Admin key: ${info.adminKey}`);
console.log(`Submit key: ${info.submitKey}`);See for the full property list.
references/api-reference.mdTopicInfojs
import { TopicInfoQuery } from "@hiero-ledger/sdk";
const info = await new TopicInfoQuery()
.setTopicId(topicId)
.execute(client);
console.log(`Memo: ${info.topicMemo}`);
console.log(`Sequence: ${info.sequenceNumber}`);
console.log(`Admin key: ${info.adminKey}`);
console.log(`Submit key: ${info.submitKey}`);完整的属性列表请参考。
TopicInforeferences/api-reference.mdKey Roles
密钥角色
| Key | Purpose |
|---|---|
| Update/delete the topic; rotate other keys |
| Authorize message submission (if absent, open to all) |
| Update custom fee schedule |
| 密钥 | 用途 |
|---|---|
| 更新/删除主题;轮换其他密钥 |
| 授权消息提交(若未设置,则所有人都可提交) |
| 更新自定义费用规则 |
Common Patterns
常见模式
Event Log / Audit Trail
事件日志 / 审计追踪
Create a topic per entity or event type. Submit structured JSON messages. Subscribe from a service to build a read model.
js
const event = JSON.stringify({
type: "ORDER_PLACED",
orderId: "12345",
timestamp: Date.now(),
data: { items: 3, total: 99.99 },
});
await new TopicMessageSubmitTransaction()
.setTopicId(ordersTopic)
.setMessage(event)
.execute(client);为每个实体或事件类型创建一个主题。提交结构化JSON消息。通过服务订阅来构建可读模型。
js
const event = JSON.stringify({
type: "ORDER_PLACED",
orderId: "12345",
timestamp: Date.now(),
data: { items: 3, total: 99.99 },
});
await new TopicMessageSubmitTransaction()
.setTopicId(ordersTopic)
.setMessage(event)
.execute(client);Pub/Sub with Multiple Subscribers
多订阅者的发布/订阅
Multiple services can subscribe to the same topic independently. Each maintains its own cursor via .
setStartTimejs
// Service A: process all messages from the beginning
new TopicMessageQuery()
.setTopicId(topicId)
.setStartTime(0)
.subscribe(client, null, processMessage);
// Service B: only new messages from now
new TopicMessageQuery()
.setTopicId(topicId)
.subscribe(client, null, processMessage);多个服务可独立订阅同一主题。每个服务通过维护自己的游标。
setStartTimejs
// 服务A:处理从最开始的所有消息
new TopicMessageQuery()
.setTopicId(topicId)
.setStartTime(0)
.subscribe(client, null, processMessage);
// 服务B:仅接收从现在开始的新消息
new TopicMessageQuery()
.setTopicId(topicId)
.subscribe(client, null, processMessage);Common Gotchas
常见注意事项
-
Mirror node sync delay: After creating a topic, wait 3-5 seconds before subscribing. The mirror node needs time to index the new topic.
-
Chunk reassembly is automatic: When subscribing, you receive complete messages even if they were submitted as multiple chunks. The SDK handles reassembly.
-
Nofor subscriptions:
execute()usesTopicMessageQuery, not.subscribe(). It returns a.execute(), not aSubscriptionHandle.TransactionResponse -
Messages are immutable: Once submitted, messages cannot be edited or deleted. Design your message schema with this in mind.
-
Sequence numbers start at 1: The first message on a topic gets sequence number 1, not 0.
-
Submit key means access control: If you set a submit key, only holders of that key can post. Omit it for open topics.
-
String vs Uint8Array:accepts both. Use
setMessage()to decode on the subscriber side.Buffer.from(message.contents).toString("utf8") -
Cleanup: Always callwhen done, and
handle.unsubscribe()when shutting down.client.close()
-
镜像节点同步延迟:创建主题后,需等待3-5秒再订阅。镜像节点需要时间索引新主题。
-
分片自动重组:订阅时,即使消息以多个分片提交,你也会收到完整消息。SDK会处理重组。
-
订阅无需调用execute():使用
TopicMessageQuery而非.subscribe()。它返回.execute(),而非SubscriptionHandle。TransactionResponse -
消息不可篡改:提交后,消息无法编辑或删除。设计消息模式时需考虑这一点。
-
序列号从1开始:主题中的第一条消息序列号为1,而非0。
-
提交密钥意味着访问控制:若设置提交密钥,只有持有该密钥的对象才能发布消息。若要开放主题,请勿设置该密钥。
-
字符串与Uint8Array:接受两种类型。订阅端使用
setMessage()解码。Buffer.from(message.contents).toString("utf8") -
资源清理:使用完毕后务必调用,关闭程序时调用
handle.unsubscribe()。client.close()
Reference Files
参考文件
- — Complete list of all HCS classes with their methods and properties
references/api-reference.md
- — 所有HCS类的完整方法和属性列表
references/api-reference.md