hedera-consensus-service

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Hedera 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
@hiero-ledger/sdk
. Two setup patterns:
所有导入均来自
@hiero-ledger/sdk
。有两种配置模式:

Client + 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
freezeWithSigner(wallet)
,
signWithSigner(wallet)
,
executeWithSigner(wallet)
, and
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
    adminKey
    , the topic cannot be updated or deleted (only expiration can be extended).
  • Without a
    submitKey
    , anyone can submit messages.
  • 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
string
or
Uint8Array
. The receipt contains
topicSequenceNumber
(incremented per message) and
topicRunningHash
.
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}`);
消息可以是
string
Uint8Array
类型。交易回执包含
topicSequenceNumber
(每条消息递增)和
topicRunningHash

When 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
TopicMessageQuery
creates a real-time subscription via the mirror node. Messages arrive as they reach consensus.
js
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.
TopicMessageQuery
通过镜像节点创建实时订阅。消息达成共识后会立即推送。
js
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
TopicMessage
has:
  • consensusTimestamp
    — when the message reached consensus
  • contents
    Uint8Array
    message body (automatically reassembled from chunks)
  • sequenceNumber
    — position in the topic (starts at 1)
  • runningHash
    — SHA-384 running hash of the topic at this message
  • chunks
    — individual
    TopicMessageChunk[]
    if the message was chunked
  • initialTransactionId
    — original transaction ID (for chunked messages)
每个接收到的
TopicMessage
包含以下属性:
  • consensusTimestamp
    — 消息达成共识的时间
  • contents
    — 消息主体(
    Uint8Array
    类型,已自动从分片重组)
  • sequenceNumber
    — 消息在主题中的位置(从1开始)
  • runningHash
    — 到该消息为止主题的SHA-384滚动哈希
  • chunks
    — 若消息被分片,则为单个
    TopicMessageChunk[]
    数组
  • initialTransactionId
    — 原始交易ID(针对分片消息)

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
    setChunkSize()
    and
    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()
    ,
    clearSubmitKey()
    , etc.
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
references/api-reference.md
for the full
TopicInfo
property list.
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}`);
完整的
TopicInfo
属性列表请参考
references/api-reference.md

Key Roles

密钥角色

KeyPurpose
adminKey
Update/delete the topic; rotate other keys
submitKey
Authorize message submission (if absent, open to all)
feeScheduleKey
Update custom fee schedule
密钥用途
adminKey
更新/删除主题;轮换其他密钥
submitKey
授权消息提交(若未设置,则所有人都可提交)
feeScheduleKey
更新自定义费用规则

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
setStartTime
.
js
// 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);
多个服务可独立订阅同一主题。每个服务通过
setStartTime
维护自己的游标。
js
// 服务A:处理从最开始的所有消息
new TopicMessageQuery()
    .setTopicId(topicId)
    .setStartTime(0)
    .subscribe(client, null, processMessage);

// 服务B:仅接收从现在开始的新消息
new TopicMessageQuery()
    .setTopicId(topicId)
    .subscribe(client, null, processMessage);

Common Gotchas

常见注意事项

  1. Mirror node sync delay: After creating a topic, wait 3-5 seconds before subscribing. The mirror node needs time to index the new topic.
  2. Chunk reassembly is automatic: When subscribing, you receive complete messages even if they were submitted as multiple chunks. The SDK handles reassembly.
  3. No
    execute()
    for subscriptions
    :
    TopicMessageQuery
    uses
    .subscribe()
    , not
    .execute()
    . It returns a
    SubscriptionHandle
    , not a
    TransactionResponse
    .
  4. Messages are immutable: Once submitted, messages cannot be edited or deleted. Design your message schema with this in mind.
  5. Sequence numbers start at 1: The first message on a topic gets sequence number 1, not 0.
  6. Submit key means access control: If you set a submit key, only holders of that key can post. Omit it for open topics.
  7. String vs Uint8Array:
    setMessage()
    accepts both. Use
    Buffer.from(message.contents).toString("utf8")
    to decode on the subscriber side.
  8. Cleanup: Always call
    handle.unsubscribe()
    when done, and
    client.close()
    when shutting down.
  1. 镜像节点同步延迟:创建主题后,需等待3-5秒再订阅。镜像节点需要时间索引新主题。
  2. 分片自动重组:订阅时,即使消息以多个分片提交,你也会收到完整消息。SDK会处理重组。
  3. 订阅无需调用execute()
    TopicMessageQuery
    使用
    .subscribe()
    而非
    .execute()
    。它返回
    SubscriptionHandle
    ,而非
    TransactionResponse
  4. 消息不可篡改:提交后,消息无法编辑或删除。设计消息模式时需考虑这一点。
  5. 序列号从1开始:主题中的第一条消息序列号为1,而非0。
  6. 提交密钥意味着访问控制:若设置提交密钥,只有持有该密钥的对象才能发布消息。若要开放主题,请勿设置该密钥。
  7. 字符串与Uint8Array
    setMessage()
    接受两种类型。订阅端使用
    Buffer.from(message.contents).toString("utf8")
    解码。
  8. 资源清理:使用完毕后务必调用
    handle.unsubscribe()
    ,关闭程序时调用
    client.close()

Reference Files

参考文件

  • references/api-reference.md
    — Complete list of all HCS classes with their methods and properties
  • references/api-reference.md
    — 所有HCS类的完整方法和属性列表