Encoding addresses and data

How addresses, transaction data, and smart-contract parameters are encoded on the TRON API surface. Covers base58 vs hex addresses, the visible flag, ABI encoding for contract calls, and the difference between raw_data and raw_data_hex.

The TRON API has two encoding choices that catch new integrators:

  • Addresses can be sent in two formats — base58check (T...) or hex (41...) — controlled by the visible flag
  • Transaction data appears both as a structured JSON object (raw_data) and as its protobuf serialization (raw_data_hex)

Getting either wrong typically manifests as a signature-verification failure or a CONTRACT_VALIDATE_ERROR. This page covers both encodings, the conversion procedures, and the bugs they cause.

📘

Prerequisites


Address formats

TRON addresses exist in two interchangeable forms:

FormatExampleLengthWhen used
base58checkTJRabPrwbZy45sbavfcjinPJC18kjpRTv834 chars, starts with TUser-facing (wallets, explorers, prose)
hex415CBDD86A2FA8DC4BDDD8A8F69DBA48572EEC07FB42 chars, starts with 41 (Mainnet)Wire-level (protobuf, default API requests)

Both encode the same 21-byte address: a 1-byte network prefix (0x41 for all current TRON production networks — Mainnet, Shasta, and Nile) followed by 20 bytes derived from the public key. See Accounts for the derivation procedure.

📘

Legacy 0xa0 prefix

The 0xa0 prefix you may see in older docs or in Constant.java is a legacy net.type = testnet config option (framework/src/main/resources/config.conf notes "'testnet' is not related to TRON network Nile, Shasta or private net"). No active TRON network uses 0xa0.

The visible flag

Most API requests and responses accept a visible field that controls which address format is used throughout the payload:

visibleAddress format in request and response
false (default)hex (41...)
truebase58check (T...)
POST /wallet/createtransaction
Content-Type: application/json

{
  "visible": true,
  "owner_address": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
  "to_address": "TXT7aiRQCLiMgsMZgFceLYbQu3MynssYUK",
  "amount": 1000000
}

If you send a base58 address with visible: false (or omit the flag), the node returns a validation error. The reverse — hex with visible: true — is also rejected.

Some endpoints have a visible parameter in the URL query string (?visible=true) instead of in the JSON body. Check the endpoint documentation.

Converting between formats

All SDKs expose conversion helpers; you should rarely need to do this by hand.

SDKbase58 → hexhex → base58
TronWebtronWeb.address.toHex(b58)tronWeb.address.fromHex(hex)
TridentNumeric.toHexString(Base58Check.base58ToBytes(b58))Base58Check.bytesToBase58(Numeric.hexStringToByteArray(hex))
gotron-sdkcommon.DecodeCheck(b58)common.EncodeCheck(hexBytes)

If you must convert manually:

  • hex → base58check: prepend the 0x41 prefix byte if missing, compute double SHA-256 of the 21-byte address, append the first 4 bytes of that double-hash, base58-encode the resulting 25 bytes
  • base58check → hex: base58-decode the 34-char string to 25 bytes, verify the trailing 4-byte checksum, take bytes 0–20 as the 21-byte address

FullNode HTTP APIs normally use the 21-byte hex form with the 41 network prefix when visible is false or omitted. JSON-RPC eth_* methods instead accept 20-byte Ethereum-style addresses and add the 41 prefix internally. Follow the parameter format documented for the endpoint rather than passing one form unchanged between the two API families.


Transaction body: raw_data vs raw_data_hex

Constructor endpoints return both forms of the transaction body:

  • raw_data — a structured JSON object containing the contract, TAPOS reference, expiration, and timestamp
  • raw_data_hex — the protobuf serialization of raw_data, hex-encoded

The signing hash is SHA-256(raw_data_bytes), where raw_data_bytes is the protobuf-serialized form. Practically: SDKs serialize raw_data themselves or use raw_data_hex directly. Both produce the same bytes.

Do not change field values in raw_data or reorder elements in its arrays between constructing and signing a transaction. Those changes alter the protobuf serialization, producing a different txID and invalidating the original signature. Reordering JSON object keys alone does not change the parsed protobuf message, but reserializing the object can introduce other differences. Computing the signing hash from the bytes decoded from the node's raw_data_hex is the safest approach.


Smart-contract parameter ABI encoding

When calling a contract through /wallet/triggersmartcontract or /wallet/triggerconstantcontract, provide the call data in one of these two forms:

  • Supply function_selector and parameter. Set function_selector to the function signature, such as transfer(address,uint256), and put only the ABI-encoded arguments in parameter. The node derives and prepends the function selector.
  • Supply data with the complete ABI calldata: the 4-byte function selector followed by the encoded arguments. When using this form, omit function_selector and parameter.

TRON uses the same ABI encoding as Ethereum, with one address-format difference.

Encoding rules

  • Function selector — first 4 bytes of Keccak-256(function_signature), where function_signature is the function name followed by parenthesized parameter types: transfer(address,uint256) → 0xa9059cbb. In a raw HTTP request, the function_selector field takes the function signature, not these 4 bytes.
  • Arguments — each argument padded to 32 bytes:
    • uint256 — big-endian 32-byte integer
    • address — 20 bytes of the address (drop the 0x41 prefix), left-padded with zeros to 32 bytes
    • string, bytes — length-prefixed dynamic encoding (see Solidity ABI spec)

Use the standard 20-byte address for ABI encoding. Remove the 0x41 network prefix and left-pad the remaining 20 bytes to a 32-byte ABI word. ABI decoders generated by TRON Solidity retain compatibility with a historical 21-byte form, but this is TRON-specific behavior and is not supported by every standard ABI tool.

Example: TRC-20 transfer

Call transfer(address,uint256) on a TRC-20 contract, sending 100 tokens to TXT7aiRQCLiMgsMZgFceLYbQu3MynssYUK:

The function signature is transfer(address,uint256), and its selector is a9059cbb. The encoded arguments are:

to_address (32B): 000000000000000000000000eba1c3a4a5e7a26acab1e10cd6e6c6e2db6ee9c2
amount (32B):     0000000000000000000000000000000000000000000000000000000005f5e100

For a raw HTTP request, use either of the following forms. These fragments show only the ABI-related fields; the complete request must also include required fields such as owner_address and contract_address.

Let the node derive and prepend the function selector:

{
  "function_selector": "transfer(address,uint256)",
  "parameter": "000000000000000000000000eba1c3a4a5e7a26acab1e10cd6e6c6e2db6ee9c20000000000000000000000000000000000000000000000000000000005f5e100"
}

Alternatively, concatenate the selector and encoded arguments yourself and pass the complete calldata in data:

{
  "data": "a9059cbb000000000000000000000000eba1c3a4a5e7a26acab1e10cd6e6c6e2db6ee9c20000000000000000000000000000000000000000000000000000000005f5e100"
}

Do not include a9059cbb in parameter when using the first form. The node would prepend another selector derived from function_selector.

SDKs handle ABI encoding automatically when you pass typed arguments to high-level methods (tronWeb.transactionBuilder.triggerSmartContract(...)). You only need to encode manually when working with raw HTTP or when generating a transaction without a deployed contract ABI.


TRC-10 identification

TRC-10 tokens are identified by a numeric token ID, not a contract address. But the asset_name field in TRC-10 contracts means different things depending on when the transaction landed:

Block rangeWhat asset_name containsExample
Block 5537806 onward (the vast majority of blocks)The numeric token ID"1002000"
Before block 5537806 (early-2019 and earlier)The token's name string"USDJ"

Why the change? Early TRON required every TRC-10 token name to be unique, so the name itself worked as an identifier. A governance proposal (TRONSCAN #14) later allowed duplicate names — after that point, only the numeric ID stays unique.

For new integrations: treat asset_name as a token ID and reject pre-cutoff blocks. If you scan the full chain history (block explorers, historical indexers), branch on block number to parse correctly.


Common encoding mistakes

  • visible flag mismatch across construct and sign — the construct response and the broadcast request must use the same flag value. SDKs handle this; raw HTTP integrators must propagate the flag.
  • Including the 0x41 prefix in ABI-encoded addresses — ABI uses 20-byte addresses, not 21-byte Mainnet form. Drop the prefix when ABI-encoding.
  • Sending a Base58 address with visible: false — the node interprets the input as hexadecimal and rejects the request because the address format does not match. Either set visible: true or convert the address.
  • Hashing raw_data_hex as a string instead of its bytes — the signing hash is over the protobuf bytes, not over the hex string's ASCII. Use SDK signing calls to avoid this.
  • Changing field values or array-element order in raw_data before signing — these changes alter the protobuf bytes and invalidate the original signature. JSON object key order itself does not affect the parsed protobuf message; to avoid reserialization differences, hash the bytes decoded from raw_data_hex.
  • Using TRC-10 token name as a stable identifier — names were de-duplicated after proposal #14; production code should key on numeric ID.

Related resources