Skip to main content

Overview

Tron uses the secp256k1 elliptic curve with keccak256 hashing — the same cryptographic scheme as Ethereum. Since Tron addresses are derived from the same keccak256 hash of the public key, you can derive a Tron address directly from the EVM address without key recovery. The only difference is the address encoding: Tron uses base58check with a 0x41 version byte instead of Ethereum’s 0x hex prefix.
PropertyValue
Curvesecp256k1
Root WalletEVM
Address Formatbase58check (T...)
HashingKeccak-256
SerializationTronWeb SDK
Smallest UnitSun (1 TRX = 10^6 sun)

Dependencies

npm install tronweb

Derive Address

Swap the EVM address prefix from 0x to 41 and base58check-encode:
import { TronWeb } from "tronweb";

function deriveTronAddress(evmAddress: string): string {
  const hex = "41" + evmAddress.slice(2).toLowerCase();
  return TronWeb.address.fromHex(hex);
}
This is the simplest derivation of all secp256k1 chains — no key recovery needed. The last 20 bytes of keccak256(uncompressed_pubkey) are identical for both EVM and Tron.

Sign a Message

Sign using Tron’s message prefix via the EVM WaaS provider. TronWeb’s hashMessage computes keccak256("\x19TRON Signed Message:\n" + len + message):
import { keccak256, toUtf8Bytes, concat } from "tronweb/utils";
import { createWaasProvider } from "@dynamic-labs-sdk/client/waas/core";
import type { DynamicClient } from "@dynamic-labs-sdk/client";
import type { WalletAccount } from "@dynamic-labs-sdk/client";

async function signTronMessage(
  message: string,
  evmWallet: WalletAccount,
  dynamicClient: DynamicClient,
): Promise<string> {
  const provider = createWaasProvider({
    sdkClient: dynamicClient,
    chain: "EVM",
  });

  const messageBytes = toUtf8Bytes(message);
  const prefix = toUtf8Bytes("\x19TRON Signed Message:\n");
  const lengthStr = toUtf8Bytes(String(messageBytes.length));
  const messageDigest = keccak256(concat([prefix, lengthStr, messageBytes]));

  const { signature } = await provider.signRawMessage({
    message: strip0x(messageDigest),
    walletAccount: evmWallet,
  });

  return signature;
}

Verify a Signature

TronWeb provides built-in verification that returns the recovered address:
import { TronWeb } from "tronweb";

async function verifyTronSignature(
  message: string,
  signature: string,
): Promise<string> {
  const tw = new TronWeb({ fullHost: "https://nile.trongrid.io" });
  return tw.trx.verifyMessageV2(message, signature);
}

Check Balance

Native TRX Balance

import { TronWeb } from "tronweb";

async function getTronBalance(tronAddress: string): Promise<string> {
  const tw = new TronWeb({ fullHost: "https://nile.trongrid.io" });
  const sunBalance = await tw.trx.getBalance(tronAddress);
  return (Number(sunBalance) / 1_000_000).toString();
}

TRC20 Token Balances

interface TronToken {
  symbol: string;
  contractAddress: string;
  decimals: number;
}

const TRC20_ABI = [
  {
    constant: true,
    inputs: [{ name: "who", type: "address" }],
    name: "balanceOf",
    outputs: [{ type: "uint256" }],
    stateMutability: "View",
    type: "Function",
  },
  {
    inputs: [
      { name: "to", type: "address" },
      { name: "value", type: "uint256" },
    ],
    name: "transfer",
    outputs: [{ type: "bool" }],
    stateMutability: "Nonpayable",
    type: "Function",
  },
];

async function getTrc20Balance(
  tronAddress: string,
  token: TronToken,
): Promise<string> {
  const tw = new TronWeb({ fullHost: "https://nile.trongrid.io" });
  const contract = tw.contract(TRC20_ABI, token.contractAddress);
  const rawBalance = await contract.balanceOf(tronAddress).call({ from: tronAddress });
  return (Number(rawBalance) / 10 ** token.decimals).toString();
}

Send a Transfer

Native TRX Transfer

import { TronWeb } from "tronweb";
import { createWaasProvider } from "@dynamic-labs-sdk/client/waas/core";

async function sendTrxTransfer(
  to: string,
  amount: number,
  tronAddress: string,
  evmWallet: WalletAccount,
  dynamicClient: DynamicClient,
): Promise<string> {
  const tw = new TronWeb({ fullHost: "https://nile.trongrid.io" });

  const amountSun = BigInt(Math.round(amount * 1_000_000));

  // Build unsigned transaction
  const unsignedTx = await tw.transactionBuilder.sendTrx(
    to,
    Number(amountSun),
    tronAddress,
  );

  return signAndBroadcast(unsignedTx, evmWallet, dynamicClient);
}

TRC20 Token Transfer

async function sendTrc20Transfer(
  to: string,
  amount: number,
  tronAddress: string,
  evmWallet: WalletAccount,
  dynamicClient: DynamicClient,
  token: TronToken,
): Promise<string> {
  const tw = new TronWeb({ fullHost: "https://nile.trongrid.io" });

  const amountSmallest = BigInt(Math.round(amount * 10 ** token.decimals));

  const result = await tw.transactionBuilder.triggerSmartContract(
    token.contractAddress,
    "transfer(address,uint256)",
    { feeLimit: 100_000_000, callValue: 0 },
    [
      { type: "address", value: to },
      { type: "uint256", value: amountSmallest.toString() },
    ],
    tronAddress,
  );

  return signAndBroadcast(result.transaction, evmWallet, dynamicClient);
}

Sign and Broadcast Helper

Both TRX and TRC20 transfers share the same signing and broadcasting logic:
async function signAndBroadcast(
  unsignedTx: Record<string, any>,
  evmWallet: WalletAccount,
  dynamicClient: DynamicClient,
): Promise<string> {
  const tw = new TronWeb({ fullHost: "https://nile.trongrid.io" });

  const provider = createWaasProvider({
    sdkClient: dynamicClient,
    chain: "EVM",
  });

  // The transaction ID is the hash to sign
  const txId = strip0x(unsignedTx.txID);

  const { signature } = await provider.signRawMessage({
    message: txId,
    walletAccount: evmWallet,
  });

  // Normalize the v-byte to Tron's expected range (27 or 28)
  const sigBytes = hexToBytes(strip0x(signature));
  const sig65 = new Uint8Array(65);
  sig65.set(sigBytes.slice(0, 64));
  let v = sigBytes[64];
  if (v < 27) v += 27;
  sig65[64] = v;

  // Attach signature and broadcast
  const signedTx = {
    ...unsignedTx,
    signature: [bytesToHex(sig65)],
  };

  const result = await tw.trx.broadcast(signedTx);

  if (!result.result) {
    throw new Error(`Broadcast failed: ${result.message || "unknown error"}`);
  }

  return result.txid;
}
Tron signing is simpler than other chains because TronWeb handles transaction construction and serialization. You only need to sign the transaction ID (a SHA-256 hash) using the EVM wallet’s raw signing capability.