Hkchain

Building and signing a transaction

An Hkchain transaction is a standard Cosmos transaction. Nothing about the envelope, the sign modes, or the signature algorithm is special. What is specific to this chain is one extension option that a stablecoin transfer must carry, and the rules that decide what goes in it.

The envelope

PartValue
MessagesStandard Cosmos messages, including cosmos.bank.v1beta1.MsgSend, plus the Hkchain module messages
Sign modeSIGN_MODE_DIRECT
Signatureeth_secp256k1 over the canonical sign bytes
FeePaid in ahkc, never in a stablecoin
Extension optionsThe Travel Rule payload, for a stablecoin transfer

Extension options are fail-closed

The chain accepts exactly two extension options: the Travel Rule payload (/hkchain.compliance.v1.TravelRulePayload) and the Cosmos EVM dynamic-fee option. Any other extension option is rejected before the transaction is examined at all.

That is a deliberate posture, and it has one trap: if the first extension option is the Ethereum-transaction option, the transaction is routed down the EVM path instead of the Cosmos path. Do not mix the two.

The Travel Rule payload

Every stablecoin transfer carries originator and beneficiary information. The payload is a protobuf message attached as an extension option — not a memo, not a message field:

FieldContents
originator_refHash of the originator's identity record, held off chain
beneficiary_refHash of the beneficiary's identity record
originator_vaspThe originating VASP identifier
beneficiary_vaspThe beneficiary VASP identifier
tier"minimal" or "full"
tx_hashLeave empty. The chain derives it when the payload arrives as an extension option; the field exists for the stored form.

No personal data goes in it. The refs are hashes of records that stay with the institutions that hold them.

Choosing the tier

This is where most first attempts fail, because two independent rules apply and they check different things.

Rule 1 — the claim must be true. tier must equal the tier registered for the sender's identity level: minimal for retail-basic, full for retail-enhanced, institutional and issuer accounts. You do not choose the tier based on the transfer; you state the one your identity level already has. An account cannot claim a tier it was not granted.

Rule 2 — the tier must be sufficient for the amount. At or above the enhanced-data threshold, full is required. A retail-basic account therefore cannot make a large transfer at all — it cannot lower the requirement, and it cannot raise its own tier by asserting one. Read the threshold from $REST/hkchain/compliance/v1/params rather than hard-coding it; the default is the HKD 8,000 line, expressed like every other amount in the base unit — 800000 fen.

Rule 3 — the fields must be complete and match the registry. Both refs are always required. Beyond that, the checks key on the sender's registered tier, not on what the payload claimed:

  • Registered full: both VASP identifiers must be present.
  • Registered minimal: both refs must equal the off-chain references recorded in the identity registry for the two parties. At full tier the refs are per-transfer and VASP-authored, so the chain does not pin them.
  • Either tier: a VASP identifier that is populated must match the one the corresponding identity record carries.

For a transfer signed by a delegated key, all three rules are evaluated against the parent. The payload describes the account that delegated, not the key that signed.

Assembling it

The sequence, in whichever Cosmos client library you use:

  1. Build the message.
  2. Set memo, gas limit and fee amount.
  3. Encode the Travel Rule payload into an Any and set it as an extension option. The transaction builder has to be asked for its extension-options interface; this is the one step a plain builder does not expose.
  4. Set a placeholder signature — public key, sign mode, sequence, empty signature bytes.
  5. Compute the canonical sign bytes for SIGN_MODE_DIRECT with the signer data (address, chain ID, account number, sequence, public key).
  6. Sign those bytes and set the real signature.
  7. Encode the transaction.

Step 4 is not optional and not a formality. The sign bytes are computed over the transaction as it will be broadcast, so the signature slot has to already exist with the right shape when they are computed. Skipping it produces a transaction that is well-formed and fails verification.

In Go, using the Cosmos SDK directly:

builder := txConfig.NewTxBuilder()
builder.SetMsgs(banktypes.NewMsgSend(from, to, sdk.NewCoins(coin)))
builder.SetGasLimit(gasLimit)
builder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("ahkc", feeAmount)))

// Travel Rule payload as an extension option.
extAny, err := codectypes.NewAnyWithValue(&payload)
builder.(authtx.ExtensionOptionsTxBuilder).SetExtensionOptions(extAny)

// Pass 1 — placeholder, so the sign bytes cover the final wire shape.
sig := signing.SignatureV2{
    PubKey:   priv.PubKey(),
    Data:     &signing.SingleSignatureData{SignMode: signing.SignMode_SIGN_MODE_DIRECT},
    Sequence: sequence,
}
builder.SetSignatures(sig)

// Pass 2 — real signature.
signBytes, err := authsigning.GetSignBytesAdapter(
    ctx, txConfig.SignModeHandler(),
    signing.SignMode_SIGN_MODE_DIRECT, signerData, builder.GetTx(),
)
signature, err := priv.Sign(signBytes)
sig.Data = &signing.SingleSignatureData{
    SignMode:  signing.SignMode_SIGN_MODE_DIRECT,
    Signature: signature,
}
builder.SetSignatures(sig)

txBytes, err := txConfig.TxEncoder()(builder.GetTx())

Your encoding configuration must have the Hkchain message types registered, or their type URLs will not resolve on the wire. Each module exposes the usual RegisterInterfaces for that.

Fees

The chain runs a flat fee model — no base-fee auction, no priority ladder. A fee is acceptable if it clears the network's minimum gas price. Read the current figure and multiply:

curl -s "$REST/cosmos/evm/feemarket/v1/params"   # → params.base_fee, in ahkc per gas

A working rule is fee = 2 × base_fee × gas_limit, which absorbs any movement between reading the parameter and the transaction landing. The local default floor is 1 gwei of ahkc per gas.

Gas limits are ordinary; a stablecoin transfer with a payload runs comfortably within a few hundred thousand.

Signing on the EVM path

If you are sending from Ethereum tooling instead, you are not building a Cosmos transaction at all — you are calling a contract method. The payload becomes the third argument:

transferWithTravelRule(address to, uint256 amount, bytes travelRulePayload)

travelRulePayload is the same protobuf message, serialised. The chain unmarshals it and applies the identical three rules. Note that to is the hex address form and amount is in the stablecoin's base unit, exactly as on the Cosmos side.

The plain transfer and transferFrom revert. That is the enforcement, not an omission — see the API Reference.

Where to go next