Hkchain

Errors and rejections

A Cosmos transaction can fail in four places, and telling them apart is the whole job: each has a different cause, a different remedy, and a different effect on your account.

WhereSurfaces asAccount sequenceWhat it means
Admission — genericNon-zero code on the broadcast responseUnchangedMalformed, badly signed, underfunded, wrong chain ID
Admission — compliance gateNon-zero code on the broadcast response, codespace: "compliance"UnchangedA gate rejected the transfer. Never entered a block.
Block execution — pre-execution checksNon-zero code on the transaction query, often codespace: "compliance"UnchangedAdmitted, then rejected against the block's state. In a block, but nothing ran
Message executionNon-zero code on the transaction queryAdvancedIncluded in a block and failed while running

The first three cost nothing but the round trip and leave the sequence you signed at still valid. Only the last consumed gas and burned a sequence number.

The distinction that catches people is between rows three and four, because both surface on the transaction query. Row three means the transaction was admitted and the world changed underneath it — the recipient was blacklisted, the issuer paused — so it can be re-signed at the same sequence once the cause is addressed. Row four means your message ran and failed. Read codespace: a compliance codespace on a transaction query is row three.

The EVM path fails differently; "EVM-side failures" at the end of this page covers it.

Reading a rejection

Two fields carry the answer:

  • codespace names the module that produced the error.
  • raw_log carries the readable message, already assembled: the offending address, amount or mismatch, followed by the error's registered text.
recipient=hkchain1abc…: address is blacklisted
payload tier="full" does not match sender's KYC-registered tier="minimal" (level=LEVEL_RETAIL_BASIC)

code is a per-module number and is only meaningful with its codespace. Match on the pair if you must branch in code; prefer matching on nothing and surfacing raw_log.

Compliance gate rejections

Five gates can reject. Each maps to one registered error in the compliance codespace:

CodeErrorRaised byTypical cause
18address is blacklistedkyc_gateSender or recipient blacklisted. The message names which.
19counterparty is sanctionedsanctionsA party's VASP is on the sanctions list
20travel rule requirements not mettravel_ruleMissing payload, wrong tier, incomplete fields, or a mismatch against the identity registry
21sub-role signer not allowed to sign this message typerole_gateA delegated key signed outside its allowlist
13 / 11 / 14invalid agent policy envelope / sub-role binding revoked / agent policy not foundagent_policyOver a cap, delegation revoked, or an agent key with no policy attached

Diagnosing a Travel Rule rejection

Code 20 covers several distinct failures. The message distinguishes them:

Message containsFix
travel rule payload not foundThe payload was not attached, or was attached as something other than an extension option
payload tier=… does not match sender's KYC-registered tier=…State the tier your level is registered with, not the one the amount seems to need
cannot originate transfer requiring tier=…The sender's level is too low for this amount. Nothing about the payload will fix it.
originator_ref and beneficiary_ref are requiredBoth refs must be present at any tier
full tier requires originator_vasp and beneficiary_vaspA full-tier sender must populate both VASP identifiers
…_vasp=… does not match KYC-registered VASP …The identifier disagrees with the identity record
minimal-tier …_ref does not match KYC ivms101_off_chain_refAt minimal tier the refs are pinned to the registry, not per-transfer

For a transfer signed by a delegated key, every one of these is evaluated against the parent account. A payload describing the agent will fail the registry checks.

The gate trace

When raw_log is not enough, ask for the gate-by-gate record:

curl -s "$REST/hkchain/compliance/v1/tx_trace/<TXHASH>"

Each row is one gate: its name, BLOCKING or ADVISORY, a status, a reason, and gas consumed. Statuses are PASS, FAIL, FLAGGED. The top-level accepted is true only when every blocking gate passed — an advisory FLAGGED does not flip it.

Reasons are written for a compliance reader, with a stable prefix per gate:

GatePrefix on failure
kyc_gateKYC gate rejected: …
sanctionsSanctions screening rejected: …
travel_ruleTravel Rule check rejected: …
role_gateRole-gate rejected: …
agent_policyAgent policy rejected: …

Advisory gates carry a fixed label instead — Reviewer flag: transfer at or above HKD 8,000 threshold, Reviewer flag: transfer-density burst inside rolling window. A FLAGGED row means a human will look at the transfer, not that anything failed.

A rejected transaction still has a trace: the rows up to and including the failure are recorded before the error propagates.

When the trace is gone

Traces are held in memory on the node that ran the transaction — 256 entries, one hour, whichever comes first — and are not chain state. When the lookup misses, re-run the same bytes through simulation:

curl -s -X POST "$REST/hkchain/compliance/v1/simulate_trace" \
  -H 'Content-Type: application/json' \
  -d '{"tx_bytes":"<base64 signed tx>"}'

The response has the same shape with simulated: true. It runs against current state on a discarded copy, so it answers "what would happen now", which is the same answer as long as nothing relevant changed.

Simulation is also the right pre-flight check: run it before broadcasting and a rejection costs you no round trip at all. Two limits on it:

  • It decodes a Cosmos transaction. The bytes are decoded with the chain's transaction decoder and the gates run over the messages found inside. It is not an EVM call simulator: pointing it at an Ethereum transaction does not execute the call, so it cannot report on a transfer made through the ERC-20 interface. The pre-flight check for that path is a read-only eth_call of the same method with the same arguments, which runs the gates and returns the revert reason without submitting anything.
  • It requires the module's simulation hook to be wired at node start-up. A node that has not wired it answers with a configuration error rather than a trace.

Module errors

Errors raised while a message executes. These are execution failures — the transaction is in a block.

stablecoin

CodeMeaning
2, 3, 4Denom invalid, not registered, or already registered
5, 6Signer is not the governance authority / not the denom's issuer
7Reserve attestation stale or missing — minting requires fresh backing
8Issuer activity is paused
9Issuer treasury has insufficient minted-but-undistributed balance
10, 11Invalid amount / address
12, 13Redemption request not found / not open
14Account frozen
15, 16Invalid params / invalid pause scope

kyc

CodeMeaning
2, 3Identity not found / already exists
4Invalid level
5, 6Signer is not an authorised identity provider / not the governance authority
7Invalid address
8Invalid level transition

reserve

CodeMeaning
2, 3, 4, 5Invalid address / params, unauthorised authority / issuer
6, 7, 8Attestation payload invalid, not found, or in the wrong state for this transition
9, 10, 11Auditor not registered / already registered / revoked
12, 13Invalid coin / invalid period

compliance — beyond the gate errors above

CodeMeaning
2, 3, 4, 5Invalid address / params / authority / rules
6, 7, 8Flag not found, wrong status for this transition, invalid decision
9, 10, 11, 12Sub-role binding exists / not found / revoked / invalid role
13, 14Invalid agent policy envelope / policy not found
15, 16Signer is not a bound reviewer / officer
17Travel Rule payload not found
22, 23Simulation not configured at node start-up / transaction bytes failed to decode

Sequence collisions

Two transactions signed at the same sequence: the first is admitted, the second is rejected. This does carry a code, and knowing which one saves an investigation:

FieldValue
codespacesdk
code32
raw_logaccount sequence mismatch, expected N, got M: incorrect account sequence

It arrives from the signature-verification stage, so a client that classifies by stage rather than by code reports it as a signature problem — which is misleading, because the signature was fine and the sequence was not. Branch on sdk/32 and read the expected and received numbers out of raw_log; they tell you exactly how far your local counter has drifted.

Track sequence locally when you submit in parallel; the account query reflects committed state and lags anything you have already broadcast.

The failure with no code at all

A transaction that never lands. The broadcast returned 0, so it was admitted somewhere, but the hash does not resolve and no error is ever produced. There is nothing to read: an admitted transaction can sit in a mempool indefinitely, and it can equally be dropped from one — evicted when the mempool fills, aged out, or lost when the node restarts — without notifying anyone.

Treat this as a state your integration owns rather than one the chain will resolve for you: after a reconciliation window of your choosing, declare the hash not landed and re-sign at the same sequence. See Broadcasting and confirmation for the polling shape.

EVM-side failures

A compliance rejection on the EVM path becomes a revert, with the gate's error carried up verbatim as the revert reason. The same causes and the same messages reach you, in an Ethereum-shaped envelope.

The envelope is the part to get right. A revert is not a rejected submission: eth_sendRawTransaction returns a hash, the transaction is mined, and the receipt records the failure as status: 0x0. The reason is not in the receipt — it is in the call's revert data, which an Ethereum receipt has no field for. An eth_call with the same arguments returns it: a JSON-RPC error, code 3, execution reverted: <the gate's message>.

Cosmos pathEVM path
A gate rejection appearsOn the broadcast response, or on the transaction queryOn the receipt — status: 0x0, without a reason
The transaction isUsually not in a blockIn a block
GasNot consumedConsumed up to the revert
Diagnose it withThe gate trace, or simulate_trace before sendingAn eth_call of the same method — as a pre-flight, or replayed afterwards

A gate trace is still recorded for an EVM-originated transfer, but it is keyed by the Cosmos transaction hash of the wrapping transaction — not by the Ethereum transaction hash your tooling reports.

Vanilla transfer and transferFrom revert unconditionally, before anything else runs:

HKCHAIN: vanilla transfer disabled — use transferWithTravelRule
HKCHAIN: vanilla transferFrom disabled — use transferFromWithTravelRule

If an ERC-20 library reports an unexplained revert on a transfer that looks correct, this is almost always it: the library called the standard method.

Where to go next