Skip to content

Instantly share code, notes, and snippets.

@Sijibomii
Last active October 20, 2025 07:01
Show Gist options
  • Save Sijibomii/8c2e8ec42b5f2f86f9eb743daec15c9a to your computer and use it in GitHub Desktop.
Save Sijibomii/8c2e8ec42b5f2f86f9eb743daec15c9a to your computer and use it in GitHub Desktop.
This Gist contains the complete TRON transaction signing that produces a "not contained of permission" error for TRC20 (USDT) transfers, while TRX transfers work fine.
package com.nomba.global.infrastructure.integrations.trongrid;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nomba.global.infrastructure.common.WalletAddress;
import com.nomba.global.infrastructure.exceptions.TronGridException;
import com.nomba.global.infrastructure.integrations.trongrid.dto.BroadcastTransactionRequest;
import com.nomba.global.infrastructure.integrations.trongrid.dto.BroadcastTransactionResponse;
import com.nomba.global.infrastructure.integrations.trongrid.dto.CallResponse;
import com.nomba.global.infrastructure.integrations.trongrid.dto.ContractCallRequest;
import com.nomba.global.infrastructure.integrations.trongrid.dto.CreateTransactionResponse;
import com.nomba.global.infrastructure.integrations.trongrid.dto.TronGridCreateTransactionRequest;
import com.nomba.global.infrastructure.utils.ServerUtils;
import com.nomba.libs.xhttp.ClientAction;
import com.nomba.libs.xhttp.ClientInterface;
import com.nomba.libs.xhttp.XHttpClient;
import com.nomba.libs.xhttp.XHttpResponse;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.net.http.HttpClient;
import java.util.HashMap;
import java.util.Map;
import org.bitcoinj.core.Base58;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.web3j.utils.Numeric;
public class TronGridClient {
private static final Logger log = LoggerFactory.getLogger(TronGridClient.class);
private final ClientInterface clientInterface;
private final TronGridConfig tronGridConfig;
private static final String ACCEPT_KEY = "accept";
private static final String API_KEY = "x-api-key";
private static final String APPLICATION_JSON = "application/json";
public TronGridClient(TronGridConfig tronGridConfig, ObjectMapper objectMapper) {
this.tronGridConfig = tronGridConfig;
this.clientInterface = XHttpClient.Builder.builder()
.withBaseUrl(tronGridConfig.url())
.withTimeout(tronGridConfig.timeout())
.withEnableLogging(true)
.withObjectMapper(objectMapper)
.withVersion(HttpClient.Version.HTTP_1_1)
.build();
}
public CreateTransactionResponse createTransaction(TronGridCreateTransactionRequest payload) {
String endpoint = "/wallet/createtransaction";
Map<String, String> headers = new HashMap<>();
headers.put(ACCEPT_KEY, APPLICATION_JSON);
headers.put(API_KEY, tronGridConfig.apiKey());
XHttpResponse<CreateTransactionResponse> response = this.clientInterface.send(
endpoint,
ClientAction.POST,
payload.toMap(),
new TypeReference<CreateTransactionResponse>() {},
headers);
if (response.isNotSuccessful() || response.getData() == null) {
throw new TronGridException(
String.format("Failed to create transaction, got %d response code", response.getStatusCode()));
}
return response.getData();
}
public BroadcastTransactionResponse broadcastTransaction(BroadcastTransactionRequest request) throws Exception {
String endpoint = "/wallet/broadcasttransaction";
Map<String, String> headers = new HashMap<>();
headers.put(ACCEPT_KEY, APPLICATION_JSON);
headers.put(API_KEY, tronGridConfig.apiKey());
XHttpResponse<BroadcastTransactionResponse> response = this.clientInterface.send(
endpoint,
ClientAction.POST,
request.toMap(),
new TypeReference<BroadcastTransactionResponse>() {},
headers);
if (response.isNotSuccessful() || response.getData() == null) {
throw new TronGridException(
String.format("Failed to broadcast transaction, got %d response code", response.getStatusCode()));
}
return response.getData();
}
public CreateTransactionResponse callContract(ContractCallRequest payload) {
String endpoint = "/wallet/triggersmartcontract";
Map<String, String> headers = new HashMap<>();
headers.put(ACCEPT_KEY, APPLICATION_JSON);
headers.put(API_KEY, tronGridConfig.apiKey());
XHttpResponse<CallResponse> response = this.clientInterface.send(
endpoint, ClientAction.POST, payload.toMap(), new TypeReference<CallResponse>() {}, headers);
if (response.isNotSuccessful() || response.getData() == null) {
throw new TronGridException(
String.format("Failed to call contract, got %d response code", response.getStatusCode()));
}
CallResponse callResponse = response.getData();
if (!callResponse.result().result()) {
throw new TronGridException(String.format(
"%s: %s",
callResponse.result().code(), callResponse.result().message()));
}
return callResponse.transaction();
}
public BigDecimal getWalletTokenBalance(WalletAddress walletAddress, String contractAddress) {
try {
String method = "balanceOf(address)";
String addressHex = base58ToHexAddress(walletAddress.value());
byte[] addressBytes = Numeric.hexStringToByteArray(addressHex);
byte[] paddedAddress = ServerUtils.leftPadTo32(addressBytes);
String encodedParam = Numeric.toHexStringNoPrefix(paddedAddress);
ContractCallRequest callRequest =
new ContractCallRequest(walletAddress.value(), contractAddress, method, encodedParam, 0L, 0L, true);
XHttpResponse<CallResponse> response = this.clientInterface.send(
"/wallet/triggersmartcontract",
ClientAction.POST,
callRequest.toMap(),
new TypeReference<CallResponse>() {},
Map.of(ACCEPT_KEY, APPLICATION_JSON, API_KEY, tronGridConfig.apiKey()));
if (response.isNotSuccessful() || response.getData() == null) {
throw new TronGridException("Failed to fetch token balance");
}
CallResponse callResponse = response.getData();
if (callResponse.constantResult() == null
|| callResponse.constantResult().isEmpty()) {
return BigDecimal.ZERO;
}
String hexBalance = callResponse.constantResult().get(0);
BigInteger balance = new BigInteger(hexBalance, 16);
return new BigDecimal(balance).divide(BigDecimal.valueOf(1_000_000), 6, RoundingMode.HALF_UP);
} catch (Exception e) {
log.error("Failed to fetch token balance for wallet {}", walletAddress.value(), e);
throw new TronGridException("Failed to fetch token balance");
}
}
private String base58ToHexAddress(String address) {
byte[] decoded = Base58.decode(address);
byte[] addressBytes = new byte[20];
System.arraycopy(decoded, 1, addressBytes, 0, 20);
return Numeric.toHexStringNoPrefix(addressBytes).toLowerCase();
}
public BigDecimal getWalletNativeBalance(WalletAddress walletAddress) {
try {
Map<String, String> headers = Map.of(ACCEPT_KEY, APPLICATION_JSON, API_KEY, tronGridConfig.apiKey());
String endpoint = "/wallet/getaccount";
Map<String, Object> payload = Map.of("address", walletAddress.value(), "visible", true);
XHttpResponse<Map<String, Object>> response = this.clientInterface.send(
endpoint, ClientAction.POST, payload, new TypeReference<Map<String, Object>>() {}, headers);
if (!response.isNotSuccessful() && response.getData() != null) {
Map<String, Object> data = response.getData();
Object balanceSun = data.get("balance");
if (balanceSun != null) {
return new BigDecimal(balanceSun.toString())
.divide(BigDecimal.valueOf(1_000_000), 6, RoundingMode.HALF_UP);
}
}
return BigDecimal.ZERO;
} catch (Exception e) {
log.error("failed to fetch trx balance for wallet {}", walletAddress.value(), e);
throw new TronGridException("Failed to fetch TRX balance");
}
}
}
package com.nomba.global.infrastructure.kms.providers;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nomba.global.infrastructure.common.Blockchain;
import com.nomba.global.infrastructure.common.CryptoTransferType;
import com.nomba.global.infrastructure.common.TokenConfiguration;
import com.nomba.global.infrastructure.common.WalletAddress;
import com.nomba.global.infrastructure.exceptions.KmsProviderException;
import com.nomba.global.infrastructure.exceptions.TronGridException;
import com.nomba.global.infrastructure.integrations.tatum.TatumClient;
import com.nomba.global.infrastructure.integrations.tatum.dto.ExchangeRate;
import com.nomba.global.infrastructure.integrations.trongrid.TronGridClient;
import com.nomba.global.infrastructure.integrations.trongrid.dto.ContractCallRequest;
import com.nomba.global.infrastructure.integrations.trongrid.dto.CreateTransactionResponse;
import com.nomba.global.infrastructure.integrations.trongrid.dto.TronGridCreateTransactionRequest;
import com.nomba.global.infrastructure.kms.dto.CreateTransactionRequest;
import com.nomba.global.infrastructure.kms.dto.Fee;
import com.nomba.global.infrastructure.kms.dto.TronFee;
import com.nomba.global.infrastructure.utils.ServerUtils;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils;
import org.bitcoinj.core.Base58;
import org.bitcoinj.crypto.ChildNumber;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.wallet.DeterministicKeyChain;
import org.bitcoinj.wallet.DeterministicSeed;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.Sign;
import org.web3j.utils.Numeric;
public class TronProvider implements Provider {
private static final long BANDWIDTH_SUN_COST = 1000L;
private static final long COIN_TRANSFER_BANDWIDTH = 350L;
// Based on TRON network data (~30 TRX in SUN)
private static final long TRC20_ENERGY_USAGE = 25_000L;
private static final long ENERGY_PRICE_SUN = 420L;
private static final String TRANSFER_FUNCTION_SIGNATURE = "transfer(address,uint256)";
private static final ObjectMapper OBJECT_MAPPER = ServerUtils.getObjectMapper();
private static final Map<String, TokenConfiguration> TOKEN_CONFIG_BY_TICKER =
TokenConfiguration.loadTokenConfigByTicker();
private final Blockchain blockchain;
private final TronGridClient tronGridClient;
private final TatumClient tatumClient;
public TronProvider(
final Blockchain blockchain, final TronGridClient tronGridClient, final TatumClient tatumClient) {
this.blockchain = blockchain;
this.tronGridClient = tronGridClient;
this.tatumClient = tatumClient;
}
@Override
public Blockchain getBlockchain() {
return blockchain;
}
@Override
public ExchangeRate getNativeCoinRate() {
return tatumClient.getExchangeRate("TRON", "USD");
}
@Override
public String generatePrivateKey(String mnemonic, int index) {
try {
DeterministicSeed seed = new DeterministicSeed(mnemonic, null, "", 0);
DeterministicKeyChain keyChain =
DeterministicKeyChain.builder().seed(seed).build();
// BIP44: m/44'/195'/0'/0/index (SLIP-44 coin type 195 = TRON)
List<ChildNumber> path = Arrays.asList(
new ChildNumber(44, true),
new ChildNumber(195, true),
new ChildNumber(0, true),
ChildNumber.ZERO,
new ChildNumber(index, false));
DeterministicKey key = keyChain.getKeyByPath(path, true);
BigInteger priv = key.getPrivKey();
// Left-pad to 64 hex chars for Trident KeyPair compatibility
String hex = priv.toString(16);
if (hex.length() < 64) {
hex = "0".repeat(64 - hex.length()) + hex;
}
return hex;
} catch (Exception e) {
throw new KmsProviderException("Failed to generate private key", "TRON", e);
}
}
@Override
public BigInteger getWalletNonce(WalletAddress walletAddress) throws KmsProviderException {
return BigInteger.ZERO;
}
@Override
public Fee calculateFee(String baseCurrency, String currency, CryptoTransferType type) {
long feeLimit;
if (type == CryptoTransferType.COIN) {
feeLimit = BANDWIDTH_SUN_COST * COIN_TRANSFER_BANDWIDTH;
} else {
feeLimit = TRC20_ENERGY_USAGE * ENERGY_PRICE_SUN;
}
// Convert SUN → TRX
BigDecimal trxAmount = new BigDecimal(feeLimit).divide(BigDecimal.valueOf(1_000_000), 6, RoundingMode.HALF_UP);
String feeLimitTRX = trxAmount.toPlainString();
// Get exchange rate TRX → USD
final ExchangeRate exchangeRate = tatumClient.getExchangeRate("TRON", "USD");
BigDecimal feeLimitUSD = trxAmount.multiply(exchangeRate.value());
return new TronFee(currency, Instant.now(), feeLimitUSD.toPlainString(), feeLimitTRX, feeLimit, feeLimitTRX);
}
@Override
public BigDecimal getWalletNativeBalance(WalletAddress walletAddress) {
return tronGridClient.getWalletNativeBalance(walletAddress);
}
@Override
public BigDecimal getWalletTokenBalance(WalletAddress walletAddress, String contractAddress) {
return tronGridClient.getWalletTokenBalance(walletAddress, contractAddress);
}
@Override
public String createTransaction(CreateTransactionRequest request)
throws KmsProviderException, NoSuchAlgorithmException, JsonProcessingException {
if (request.getBlockchain() != this.blockchain) {
throw new IllegalArgumentException("Wallet doesn't support this blockchain");
}
validateTransactionRequest(request);
return switch (request.getType()) {
case COIN -> createCoinTransaction(request);
case TOKEN -> createTokenTransaction(request);
};
}
private void validateTransactionRequest(CreateTransactionRequest request) {
if (isBlank(request.getSenderPrivateKey())) {
throw new IllegalArgumentException("Sender private key is required");
}
if (isBlank(request.getRecipientWallet())) {
throw new IllegalArgumentException("Recipient wallet is required");
}
if (request.getAmount() == null || request.getAmount().signum() <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
}
// ----------------------- TRX COIN -----------------------
private String createCoinTransaction(CreateTransactionRequest req)
throws KmsProviderException, NoSuchAlgorithmException, JsonProcessingException {
// Convert TRX to SUN (1 TRX = 1e6 SUN)
BigInteger amountSUN = req.getAmount().movePointRight(6).toBigInteger();
CreateTransactionResponse txResponse = tronGridClient.createTransaction(new TronGridCreateTransactionRequest(
req.getSenderAddress(), req.getRecipientWallet(), amountSUN.longValue(), "", true));
if (txResponse.error() != null && !txResponse.error().isEmpty()) {
if (txResponse.error().contains("balance is not sufficient")) {
throw new TronGridException("Insufficient TRX balance");
}
throw new TronGridException("TronGrid error: " + txResponse.error());
}
// Sign transaction with private key
CreateTransactionResponse signedTx = signTransaction(txResponse, req.getSenderPrivateKey());
if (signedTx == null) {
throw new TronGridException("Unable to sign transaction");
}
return OBJECT_MAPPER.writeValueAsString(signedTx);
}
public CreateTransactionResponse signTransaction(CreateTransactionResponse tx, String privateKeyHex)
throws NoSuchAlgorithmException {
String cleanPrivateKey = privateKeyHex.startsWith("0x") ? privateKeyHex.substring(2) : privateKeyHex;
Credentials credentials = Credentials.create(cleanPrivateKey);
// Decode raw transaction data
byte[] rawDataBytes = Numeric.hexStringToByteArray(tx.rawDataHex());
// Hash the raw data using SHA-256
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(rawDataBytes);
// Sign the hash (addToEthereumFormat=false for TRON compatibility)
Sign.SignatureData signatureData = Sign.signMessage(hash, credentials.getEcKeyPair(), false);
// Convert signature to TRON format (65 bytes: R + S + V)
String signatureHex = formatTronSignature(signatureData);
// Create new signature list with existing signatures plus new one
List<String> signatures = tx.signature() != null ? new ArrayList<>(tx.signature()) : new ArrayList<>();
signatures.add(signatureHex);
// Return new signed transaction
return new CreateTransactionResponse(
tx.txID(), tx.rawDataHex(), tx.rawData(), tx.visible(), signatures, tx.error());
}
private static String formatTronSignature(Sign.SignatureData signatureData) {
// TRON uses 65-byte signature format: 32 bytes R + 32 bytes S + 1 byte V
byte[] signature = new byte[65];
// Copy R (32 bytes)
System.arraycopy(signatureData.getR(), 0, signature, 0, 32);
// Copy S (32 bytes)
System.arraycopy(signatureData.getS(), 0, signature, 32, 32);
// Copy V (1 byte) - getV() returns byte[], so take the first element
signature[64] = signatureData.getV()[0];
// Convert to hex string and remove 0x prefix
return Numeric.toHexString(signature).substring(2);
}
// ----------------------- TRC20 TOKEN -----------------------
private String createTokenTransaction(CreateTransactionRequest req)
throws NoSuchAlgorithmException, JsonProcessingException {
BigInteger tokenAmount = req.getAmount().movePointRight(6).toBigInteger();
String callParameterHex = constructTronTokenTxData(req.getRecipientWallet(), tokenAmount);
String tokenKey = getTokenKey(req);
TokenConfiguration tokenConfiguration = TOKEN_CONFIG_BY_TICKER.get(tokenKey);
if (Objects.isNull(tokenConfiguration)) {
throw new IllegalArgumentException("Unsupported token: " + tokenKey);
}
String tokenAddress = tokenConfiguration.tokenAddress().equalsIgnoreCase("USDT_TRON")
? "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
: tokenConfiguration.tokenAddress();
CreateTransactionResponse response = tronGridClient.callContract(new ContractCallRequest(
req.getSenderAddress(),
tokenAddress,
TRANSFER_FUNCTION_SIGNATURE,
callParameterHex,
req.getFeeLimit().longValue(),
0L,
true));
if (StringUtils.isNotBlank(response.error())) {
throw new TronGridException(response.error());
}
CreateTransactionResponse signedTx = signTransaction(response, req.getSenderPrivateKey());
return OBJECT_MAPPER.writeValueAsString(signedTx);
}
// TRC-20 ABI encoder for transfer(address,uint256)
private String constructTronTokenTxData(String recipientBase58, BigInteger amount) {
// 1. Convert base58 -> hex (41 + 20 bytes -> drop "41")
String recipientHexString = base58ToHexAddress(recipientBase58);
byte[] recipientBytes = Numeric.hexStringToByteArray(recipientHexString);
// 3. Left-pad address and amount to 32 bytes
byte[] paddedAddress = ServerUtils.leftPadTo32(recipientBytes);
byte[] paddedAmount = ServerUtils.leftPadTo32(amount.toByteArray());
// 4. Concatenate
byte[] data = new byte[64];
System.arraycopy(paddedAddress, 0, data, 0, 32);
System.arraycopy(paddedAmount, 0, data, 32, 32);
// 5. Encode hex (no "0x" prefix)
return Numeric.toHexStringNoPrefix(data);
}
// tron addresses are encoded as base58
private String base58ToHexAddress(String address) {
byte[] decoded = Base58.decode(address);
byte[] addressBytes = new byte[20];
System.arraycopy(decoded, 1, addressBytes, 0, 20);
return Numeric.toHexStringNoPrefix(addressBytes).toLowerCase();
}
private static boolean isBlank(String s) {
return s == null || s.trim().isEmpty();
}
private String getTokenKey(CreateTransactionRequest request) {
return String.format("%s_%s", request.getBlockchain().name(), request.getTokenName());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment