Last active
          October 20, 2025 07:01 
        
      - 
      
- 
        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.
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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"); | |
| } | |
| } | |
| } | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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