Hkchain

Broadcasting and confirmation

Hkchain adds no broadcast endpoint of its own. You submit through the standard Cosmos transaction service, and the thing worth getting right is not the call — it is the difference between the two answers you get about the same transaction.

Submitting

curl -s -X POST "$REST/cosmos/tx/v1beta1/txs" \
  -H 'Content-Type: application/json' \
  -d '{"tx_bytes":"<base64 encoded signed tx>","mode":"BROADCAST_MODE_SYNC"}'

The response carries a txhash, a code, and a raw_log.

ModeReturns whenUse it when
BROADCAST_MODE_SYNCThe admission checks have runAlmost always. You learn immediately whether the transaction was accepted into the mempool, and why not if it was not.
BROADCAST_MODE_ASYNCThe node has the bytesFire-and-forget only. You get a hash and no verdict.

Prefer SYNC. Every compliance rejection surfaces there, in the same round trip, with a readable reason.

Two verdicts, one hash

This is the part that catches integrations.

code in the broadcast response is the admission verdict, and only for the node you sent it to. 0 means that node accepted the transaction into its mempool. Non-zero means that node rejected it, and it will not be gossiped onward from there.

An admission verdict is not a promise of inclusion. A mempool entry can be evicted — the mempool fills, the entry ages out, the node restarts — and nothing notifies you when that happens. Admission is best read as "not obviously wrong", not as "scheduled".

code on the transaction query is the execution verdict. A transaction can be admitted and then fail later — an insufficient balance, a paused issuer, a message-level authority check, or a compliance gate seeing state that changed since admission. It is in a block and it did not do what you asked.

So a successful broadcast is not a successful transaction. Confirm explicitly:

curl -s "$REST/cosmos/tx/v1beta1/txs/<TXHASH>"
  • 404 — not included yet, or never will be. Keep polling; "Polling" below covers the terminal case.
  • Present, tx_response.code == 0 — done. The state change happened.
  • Present, tx_response.code != 0 — included and failed. raw_log says why.

Transaction hashes are returned as uppercase hex. Query with them in that form — the reference web client upper-cases before calling, and it is the form every other surface reports.

Where the compliance gates land

On the Cosmos path the gates run inside the admission phase. A transfer rejected by a gate therefore fails the broadcast in the common case — you see it in the first response, and the transaction never enters a block.

{
  "tx_response": {
    "txhash": "…",
    "codespace": "compliance",
    "code": 18,
    "raw_log": "recipient=hkchain1…: address is blacklisted"
  }
}

code is a module-registered error number, not an HTTP status, and it is only meaningful together with codespace — code 18 means one thing in compliance and something else entirely in another module. Match on the pair, or match on neither and read raw_log. Errors and rejections maps them.

Because rejection happens before the account sequence advances, the sequence you signed at is still valid. Fix the payload, re-sign at the same sequence, and send again — do not re-read the account and skip a number.

The gates also run a second time when the block is executed, against the state as of that block. Same codespace and code, same raw_log, same untouched sequence — but this one surfaces on the transaction query, because by then the transaction is in a block. Handle a compliance codespace in both places rather than only in the broadcast response.

On the EVM path the gates are somewhere else entirely. An EVM transaction does not carry its transfer through the Cosmos admission phase; the gates run while the call executes, so a rejection is a revert, not a broadcast failure. eth_sendRawTransaction succeeds, the transaction is mined, and eth_getTransactionReceipt reports status: 0x0. Confirm an EVM-side transfer by reading the receipt status, never by reading the submission response.

The receipt tells you that it failed, not why. The gate's message is ABI-encoded into the call's revert data, and an Ethereum receipt has no field for revert data — so the receipt is the confirmation source and something else is the diagnosis. Read the reason by making the same call read-only:

curl -s -X POST "$EVM_RPC" -H 'Content-Type: application/json' -d '{
  "jsonrpc":"2.0","id":1,"method":"eth_call",
  "params":[{"from":"0x<sender>","to":"0x<token>","data":"0x<calldata>"},"latest"]}'

A reverting eth_call answers with a JSON-RPC error rather than a result: code 3, a message of execution reverted: <the gate's message>, and data carrying the same reason ABI-encoded.

Run it before broadcasting and it is a pre-flight check — the gates run identically under eth_call, so a rejection costs neither gas nor a transaction. Run it after the fact and it is a replay: useful, but it is evaluated against the state you point it at, so an identity or sanctions change since the transaction can change the answer.

Polling

Blocks are produced continuously and finality is instant, so a poll interval well under one block time is appropriate — half a second is a reasonable default, and a few seconds of total timeout covers normal inclusion.

Do not treat a timeout as a failure and do not treat it as a pending success. Your deadline expiring tells you nothing about the transaction: it may be waiting in a mempool, or it may have been dropped from one without a trace. Both look identical from outside — a hash that does not resolve.

Three rules for a production poller:

  • Poll by hash, not by account state. Balance changes are not attributable to your transaction.
  • Give up eventually, but reconcile. If you stop polling, record the hash and reconcile it later against a transaction query or the event stream. Nothing about giving up cancels the transaction.
  • Have a terminal path for "dropped". After a reconciliation window of your choosing, treat an unresolved hash as not landed and re-sign — at the same sequence, since an unincluded transaction consumed nothing. Re-broadcasting the identical bytes is also safe: if the original is still in a mempool the duplicate is discarded, and if it already landed the second copy fails on the now-stale sequence rather than paying twice.

Watching instead of polling

For anything beyond a single request-response, subscribe rather than poll. The CometBFT WebSocket delivers transaction results as they are committed, filtered by an event query:

{"jsonrpc":"2.0","method":"subscribe","id":1,
 "params":{"query":"tm.event='Tx' AND message.sender='hkchain1…'"}}

Events and indexing covers the query language, the event names, and the reconnection problem you inherit with any subscription.

Finding a transaction you did not submit

The transaction service also searches by event:

# Everything in one block
curl -s "$REST/cosmos/tx/v1beta1/txs?query=tx.height%3D1234&limit=100&order_by=ORDER_BY_ASC"

# Everything an account sent
curl -s "$REST/cosmos/tx/v1beta1/txs?query=message.sender%3D%27hkchain1...%27&limit=50&order_by=ORDER_BY_DESC"

Values in the query are single-quoted; heights are bare. There is no OR operator — a view that needs several conditions runs several queries and merges.

One subtlety about addresses in these queries. message.sender carries the Bech32 form for Cosmos-native activity — and an EVM transaction emits it twice, once with the hex form of the Ethereum sender and once with the Bech32 form of the account the transfer debited. Searching message.sender on the Bech32 form therefore finds a stablecoin transfer whichever path it entered by. Searching on the hex form finds only the EVM ones:

curl -s "$REST/cosmos/tx/v1beta1/txs?query=message.sender%3D%270x...%27&limit=50"

There is no ethereum_tx.sender attribute. ethereum_tx.recipient exists but is the contract that was called — for a stablecoin transfer, the token's precompile address, not the party that received the money. Do not use it to find payments to an account.

Where to go next