Hkchain

Events and indexing

Events are the intended integration point for anything that needs to know what the chain did — reconciliation, indexing, notifications, compliance tooling. This page covers how they are shaped, how to receive them, and the three ways the event set does not line up with the message set.

Shape

Hkchain modules emit typed events: protobuf messages declared alongside the transaction and query schemas, in proto/hkchain/<module>/v1/events.proto. On the wire each becomes an ABCI event whose type is the fully-qualified proto name and whose attributes are the message's fields:

type: hkchain.stablecoin.v1.EventMinted
attributes:
  issuer: "\"hkchain1…\""
  amount: "{\"denom\":\"hkd.hsbc\",\"amount\":\"100000\"}"

Three properties of that encoding, all standard Cosmos typed-event behaviour rather than anything specific to this chain:

  • Values are JSON. String values arrive quoted, structured values arrive as objects. Parse each value as JSON rather than reading it as text.
  • Keys are the proto field names, snake_case, sorted alphabetically. The order of attributes carries no meaning.
  • Zero values are present, not omitted. An empty string or a 0 appears as an attribute; do not treat presence as a signal.

Receiving them

Three ways, for three different jobs.

From a transaction you submitted. The transaction query response carries the full event list under tx_response.events. Simplest path when you already have the hash.

Live, over the CometBFT WebSocket. Connect and send a subscribe frame:

{"jsonrpc":"2.0","method":"subscribe","id":1,
 "params":{"query":"tm.event='Tx' AND message.action='/cosmos.bank.v1beta1.MsgSend'"}}

The first frame back is an empty acknowledgement — ignore it and start reading at the first frame that carries a data or events payload. Useful query forms:

QueryDelivers
tm.event='NewBlock'One frame per committed block
tm.event='Tx'Every transaction
tm.event='Tx' AND message.sender='hkchain1…'One account's activity
tm.event='Tx' AND hkchain.compliance.v1.EventFlagEmitted.flag_id EXISTSOnly transactions that raised a compliance flag

The last form is the general pattern: <proto-name>.<field> addresses a typed event's attribute, and EXISTS matches on presence rather than value.

A subscription is not durable. The socket drops, and anything emitted while it was down is not replayed. Reconnect with backoff, record the last height you processed, and backfill — but backfill by two different routes, because not every event belongs to a transaction.

Historically, by search. The transaction service takes the same event query syntax with pagination, which is how you backfill. See Broadcasting and confirmation.

Backfilling: transaction events and block events are different problems

Transaction search only finds events that a transaction produced. Events emitted at a block boundary have no transaction to be found under, so a reconnect routine built on transaction search alone silently loses them.

KindExampleRecover a gap by
Transaction eventsEverything a message emits, plus compliance flags raised while a transfer is admittedTransaction search over the missed height range
Block eventsEventDailyStatement, emitted by the reserve module when a UTC day rolls overRe-reading the state it recorded, or the block results for the missed heights

For the daily statement the state read is both simpler and authoritative — the event announces a record that was written:

curl -s "$REST/hkchain/reserve/v1/daily_statement/hkchain1<issuer>?date=2026-08-09T00:00:00Z"

date is a timestamp, not a bare calendar date — pass it in RFC 3339 form. Any instant within the UTC day resolves to that day's record.

The general form of the second route is CometBFT's block-results endpoint for a specific height, which returns the events a block emitted outside any transaction. Prefer the state query where one exists; it survives pruning and does not require you to know which height the boundary fell on.

Do not derive the event set from the message list

The mapping is deliberately not one-to-one, in three ways that each break a naive indexer.

Opposite operations share one event. Pausing and unpausing both emit EventPauseChanged; freezing and unfreezing both emit EventAccountFrozen; upgrading a level and blacklisting both emit EventLevelChanged. The event carries the resulting state, so read the state from the event rather than inferring it from which message you expected.

Some events have no message behind them. EventFlagEmitted is raised by an advisory gate while a transaction is being admitted — no one submits it. EventDailyStatement is emitted at a block boundary when a day rolls over, not by anyone's transaction. An indexer keyed only on message types will never see either.

Parameter changes emit nothing. Every module has a governance parameter setter, and none of them emits an event. Observe a parameter change by polling the module's Params query, or by watching governance proposals.

There is one more trap, worth stating separately because it is not a mapping issue: EventSARFiled and EventTravelRuleAnchored are declared in the compliance schemas for planned behaviour, and nothing in the current chain emits them. Do not build a consumer that waits on either.

The events worth subscribing to

You want to knowWatch
Stablecoin entered or left circulationEventMinted, EventDistributed, EventBurned, EventRedemptionAcked
An account or an issuer was restrictedEventAccountFrozen, EventPauseChanged
Something needs a humanEventFlagEmitted, then EventFlagEscalated / EventFlagResolved
Identity changedEventEnrolled, EventLevelChanged
Reserve backing was attestedEventAttestationSubmitted, EventAttestationCosigned, EventAttestationFinalized
The daily reserve positionEventDailyStatement
Delegation changedEventSubRoleBound, EventSubRoleRevoked, EventAgentPolicyUpdated

The API Reference carries the complete inventory with field detail.

EVM-side events

The extended ERC-20 interface emits the standard Transfer and Approval events, so ordinary ERC-20 balance tracking works unchanged. They are Ethereum log events, delivered through eth_getLogs and eth_subscribe.

What a stablecoin transfer does not produce is an Hkchain typed transfer event. There is no EventTransferred; on either path a plain transfer is recorded by the bank module's standard transfer and message events, and by the durable audit log below. The typed events in the inventory above describe issuance, identity, reserve, delegation and compliance-review activity — not ordinary movement between holders.

So the double-counting risk is real but differently shaped than "typed event versus log". One EVM transfer emits, in the same transaction:

SourceEvent
The ERC-20 interfaceAn Ethereum Transfer log
The bank module underneath itA transfer ABCI event, plus message.sender
The EVM moduleAn ethereum_tx event and a second message.sender, in hex

All of them describe one movement. Pick one as your source of truth — the Ethereum log if you are an Ethereum-side indexer, the bank transfer event if you index both paths uniformly — and ignore the others rather than summing.

The durable compliance record

Two queries hold what an auditor would ask for, and unlike traces they are chain state:

curl -s "$REST/hkchain/compliance/v1/audit_log?tx_hash=<hash>"
curl -s "$REST/hkchain/compliance/v1/flags?tx_hash=<hash>"

The audit log is append-only. Reports over it are assembled at read time rather than stored, so a report is always a view of the current log rather than a snapshot that can drift from it.

Where to go next