Skip to main content

Overview

Tron uses the secp256k1 elliptic curve — the same as Ethereum. The Tron address is derived from the same public key as the EVM address: take the last 20 bytes of keccak256(pubkey), prepend 0x41, then base58check-encode. This means you can derive the Tron address directly from the EVM address without any additional signing.
PropertyValue
Curvesecp256k1
Root WalletEVM
Address Formatbase58check (T prefix)
HashingKeccak-256 (address)
SerializationTronGrid REST API
Smallest UnitSUN (1 TRX = 1,000,000 SUN)

Dependencies

Add to pubspec.yaml:
dependencies:
  pointycastle: ^3.9.1   # Keccak-256, SHA-256
  bs58: ^1.0.2           # Base58 encoding
  http: ^1.2.2           # HTTP client

Derive Address

Tron and EVM share the same public key, so the Tron address is just a re-encoding of the EVM address:
import 'dart:typed_data';
import 'package:pointycastle/pointycastle.dart';
import 'package:bs58/bs58.dart';

/// Derive a Tron address (T...) from an EVM address (0x...).
/// Both chains use secp256k1; the only difference is the address encoding.
/// Tron = base58check( 0x41 + last20bytes(keccak256(pubkey)) )
/// Since the last 20 bytes are the same as the EVM address, we just re-encode.
String deriveTronAddress(String evmAddress) {
  final addressBytes = hexToBytes(strip0x(evmAddress));
  final payload = Uint8List(21);
  payload[0] = 0x41; // TRON mainnet prefix
  payload.setAll(1, addressBytes);
  return _base58CheckEncode(payload);
}

String _base58CheckEncode(Uint8List payload) {
  final sha = Digest('SHA-256');
  final checksum = sha.process(sha.process(payload)).sublist(0, 4);
  return base58.encode(
    Uint8List.fromList([...payload, ...checksum]),
  );
}

// Usage
final evmWallet = DynamicSDK.instance.wallets.userWallets
    .firstWhere((w) => w.chain == 'EVM');
final tronAddress = deriveTronAddress(evmWallet.address);
// → "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE" (example)

Sign a Message

Tron message signing uses a prefix similar to Ethereum EIP-191, then hashes with Keccak-256:
import 'dart:convert';
import 'dart:typed_data';
import 'package:pointycastle/pointycastle.dart';

/// Compute the TRON message digest for signing.
/// keccak256( "\x19TRON Signed Message:\n" + len(messageBytes) + messageBytes )
String tronMessageDigest(String message) {
  final messageBytes = utf8.encode(message);
  final prefix = utf8.encode('\x19TRON Signed Message:\n');
  final lengthStr = utf8.encode('${messageBytes.length}');
  final combined = Uint8List.fromList([...prefix, ...lengthStr, ...messageBytes]);
  return bytesToHex(Digest('Keccak/256').process(combined));
}

Future<String> signTronMessage(String message) async {
  final evmWallet = DynamicSDK.instance.wallets.userWallets
      .firstWhere((w) => w.chain == 'EVM');

  final hexDigest = tronMessageDigest(message);

  final signature = await DynamicSDK.instance.wallets.signRawMessage(
    walletId: evmWallet.id,
    accountAddress: evmWallet.address,
    message: hexDigest, // 32-byte hex, no 0x prefix
  );

  return signature;
}

Sign a Transaction

Tron transaction signing uses the TronGrid REST API. The API returns a txID (already a 32-byte hash) which you sign directly:
import 'dart:convert';
import 'dart:typed_data';
import 'package:http/http.dart' as http;

const _tronApi = 'https://nile.trongrid.io'; // testnet; use api.trongrid.io for mainnet

/// Send TRX via TronGrid REST API.
/// 1. POST /wallet/createtransaction → unsigned tx with txID
/// 2. Sign txID bytes with signRaw (txID is already 32 bytes — no hashing needed)
/// 3. POST /wallet/broadcasttransaction
Future<String> sendTronTransfer({
  required String to,
  required double amountTrx,
  required String fromTronAddress,
  required SignWithEvm signRaw, // callback: (Uint8List digest) → Future<String>
}) async {
  final amountSun = (amountTrx * 1000000).round();
  final toHex = _tronAddressToHex(to);
  final fromHex = _tronAddressToHex(fromTronAddress);

  // 1. Create unsigned transaction
  final createRes = await http.post(
    Uri.parse('$_tronApi/wallet/createtransaction'),
    headers: {'Content-Type': 'application/json'},
    body: json.encode({
      'to_address': toHex,
      'owner_address': fromHex,
      'amount': amountSun,
      'visible': false,
    }),
  );
  final unsignedTx = json.decode(createRes.body) as Map<String, dynamic>;
  if (unsignedTx['txID'] == null) {
    throw Exception(unsignedTx['Error'] ?? 'Failed to create transaction');
  }

  // 2. Sign the txID bytes (already a 32-byte hash — sign directly, no extra hashing)
  final txIdBytes = hexToBytes(unsignedTx['txID'] as String);
  final signature = await signRaw(txIdBytes);

  // 3. Broadcast
  final broadcastRes = await http.post(
    Uri.parse('$_tronApi/wallet/broadcasttransaction'),
    headers: {'Content-Type': 'application/json'},
    body: json.encode({
      ...unsignedTx,
      'signature': [strip0x(signature)],
    }),
  );
  final result = json.decode(broadcastRes.body) as Map<String, dynamic>;
  if (result['result'] != true) {
    throw Exception(result['message'] ?? 'Broadcast failed');
  }
  return unsignedTx['txID'] as String;
}

String _tronAddressToHex(String address) {
  final decoded = base58.decode(address);
  // Remove 4-byte checksum, keep 21-byte versioned payload
  return bytesToHex(Uint8List.fromList(decoded.sublist(0, decoded.length - 4)));
}

Check Balance

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<String> getTronBalance(String tronAddress) async {
  try {
    final res = await http.get(
      Uri.parse('$_tronApi/v1/accounts/$tronAddress'),
    );
    if (res.statusCode != 200) return '0';
    final data = json.decode(res.body) as Map<String, dynamic>;
    final accounts = data['data'] as List?;
    if (accounts == null || accounts.isEmpty) return '0';
    final balance = (accounts[0] as Map<String, dynamic>)['balance'];
    if (balance == null) return '0';
    return (balance / 1000000).toString();
  } catch (_) {
    return '0';
  }
}