一、前言
近段时间公司接到了以为HK客户的一个项目,该项目主要涉及trx、usdt的相互转账
,由于tron官方API都是英文且查阅起来不是很方便,加之相对于日常开发任务,tron涉及到的技术算是比较偏,因此下文记录一些Tron常用操作,方便自己积累、同行阅读。
二、具体实现
2.1 yaml配置
tron:
transactionFee: 0.01
tronDomainOnline: false
address: TC32xxx(系统账号)
privateKey: ENC(1qjg178(钱包私钥,加密配置))
apiKey: 72fe3xxx(官网申请的apiKey)
#usdt智能合约地址
usdtContract: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
# 查询钱包余额
trxTransferUrl: https://apilist.tronscanapi.com/api/transfer/trx
ustdTransferUrl: https://api.trongrid.io/v1/accounts/{address}/transactions/trc2
# 转账详情查询地址
infoUrl: https://apilist.tronscanapi.com/api/transaction-info?hash=
# 账户余额查询地址
balanceUrl: https://apilist.tronscanapi.com/api/account/tokens?address=
# 冻结余额
freezeBalanceUrl: https://api.trongrid.io/wallet/freezebalancev2
对应的配置实体:
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.tron.trident.core.ApiWrapper;
import java.math.BigDecimal;
/**
* FileName: Tron
* Author: wxz
* Date: 2024/12/6 11:26
* Description: tron配置
*/
@Component
@Getter
@Setter
@ConfigurationProperties(prefix = "tron")
public class TronConfig {
private String address;
private String trxTransferUrl;
private String usdtTransferUrl;
private String balanceUrl;
private String infoUrl;
private String apiKey;
private String privateKey;
private String usdtContract;
private String freezeBalanceUrl;
private boolean tronDomainOnline;
private BigDecimal transactionFee;
@Bean
public ApiWrapper apiWrapper() {
if (this.isTronDomainOnline()) {
return ApiWrapper.ofMainnet(this.getPrivateKey(), this.getApiKey());
}
return new ApiWrapper("grpc.trongrid.io:50051", "grpc.trongrid.io:50052", this.getPrivateKey());
}
public ApiWrapper apiWrapper(String privateKey) {
if (this.isTronDomainOnline()) {
return ApiWrapper.ofMainnet(privateKey, this.getApiKey());
}
return new ApiWrapper("grpc.trongrid.io:50051", "grpc.trongrid.io:50052", privateKey);
}
}
2.2 Tron操作工具类
import cn.hutool.core.util.RandomUtil;
import com.tron.config.TronConfig;
import com.tron.dto.AccountBalance;
import com.tron.dto.AccountBalance.Balance;
import com.tron.dto.FreezeDto;
import com.tron.dto.TransferDto;
import com.tron.entity.AjaxResult;
import com.tron.excetion.UtilException;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Configuration;
import org.tron.common.crypto.Sha256Sm3Hash;
import org.tron.common.utils.Base58;
import org.tron.keystore.StringUtils;
import org.tron.keystore.WalletFile;
import org.tron.trident.core.ApiWrapper;
import org.tron.trident.core.contract.Contract;
import org.tron.trident.core.contract.Trc20Contract;
import org.tron.trident.core.exceptions.IllegalException;
import org.tron.trident.core.key.KeyPair;
import org.tron.trident.proto.Chain;
import org.tron.trident.proto.Chain.Transaction;
import org.tron.trident.proto.Response.Account;
import org.tron.trident.proto.Response.AccountResourceMessage;
import org.tron.trident.proto.Response.TransactionExtention;
import org.tron.trident.utils.Convert;
import org.tron.trident.utils.Numeric;
import org.tron.walletserver.WalletApi;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
@Configuration
@Log4j2
@RequiredArgsConstructor
public class TronUtil {
private final TronConfig config;
/**
* 激活账户: √
*
* @param hexAddress
* @return
*/
public AjaxResult activate(String hexAddress) {
TransactionExtention transaction = null;
ApiWrapper wrapper = config.apiWrapper();
try {
transaction = wrapper.createAccount(wrapper.keyPair.toBase58CheckAddress(), hexAddress);
Transaction signedTxn = wrapper.signTransaction(transaction);
log.info("账号激活结果:{}", signedTxn.toString());
String s = wrapper.broadcastTransaction(signedTxn);
wrapper.close();
return AjaxResult.success(s);
}
catch (IllegalException e) {
e.printStackTrace();
log.error("账号激活失败,原因: {}", e.getMessage());
return AjaxResult.error(e.getMessage());
}
finally {
wrapper.close();
}
}
/**
* 创建账户 √
*
* @return
*/
public static KeyPair generate() {
KeyPair keyPair = ApiWrapper.generateAddress();
log.info("生成账号信息: {}", keyPair.toString());
return keyPair;
}
/**
* 查询账户 √
*
* @param hexAddress
*/
public Account getAccount(String hexAddress) {
ApiWrapper wrapper = config.apiWrapper();
Account account = wrapper.getAccount(hexAddress);
wrapper.close();
return account;
}
/**
* 转账; 如果付款账号没有交易所需带宽/能量,会冻结以获取; 如果转账金额就是账户可用余额,会在转账金额里面扣除冻结金额,
*/
public void transferTrx(TransferDto dto) {
ApiWrapper wrapper = config.apiWrapper(dto.getPrivateKey());
try {
long finalTransferAmount = getFinalTransferAmount(wrapper, dto.getFromAddress(), dto.getAmount());
TransactionExtention transaction = wrapper.transfer(dto.getFromAddress(), dto.getToAddress(),
finalTransferAmount);
Transaction signedTxn = wrapper.signTransaction(transaction, wrapper.keyPair);
String ret = wrapper.broadcastTransaction(signedTxn);
log.info("正在进行转账交易,from :{} ,to :{} ,balance:{} , 广播结果:{}", dto.getFromAddress(), dto.getToAddress(),
dto.getAmount(), ret);
}
catch (Exception e) {
e.printStackTrace();
log.error("转账发生错误,原因:{}", e.getMessage());
}
finally {
wrapper.close();
}
}
/**
* 获取本次实际能交易的金额
*
* @param fromAddress 付款账户
* @param amount 交易金额
* @return 实际能交易的金额
*/
private long getFinalTransferAmount(ApiWrapper wrapper, String fromAddress, long amount) {
AccountResourceMessage accountResource = wrapper.getAccountResource(fromAddress);
long netLimit = accountResource.getNetLimit();
long netUsed = accountResource.getNetUsed();
long energyLimit = accountResource.getEnergyLimit();
long energyUsed = accountResource.getEnergyUsed();
log.info("账号带宽:{}", (netLimit - netUsed));
log.info("账号能量:{}", (energyLimit - energyUsed));
if ((netUsed < netLimit) && (energyUsed < energyLimit)) {
return amount;
}
Account account = this.getAccount(fromAddress);
if (amount > account.getBalance()) {
throw new UtilException("本次交易金额( " + amount + " )大于账户可用余额 " + account.getBalance() + ",交易失败.");
}
//本次交易手续费
long freezeAmount = calculateDynamicFee(wrapper, fromAddress);
log.error("账户能量不足以支持本次转账,尝试冻结部分余额.....");
FreezeDto dto = new FreezeDto();
dto.setAddress(fromAddress).setBalance(freezeAmount);
long freeze = 0;
if ((netUsed - netLimit) <= 0) {
dto.setResourceCode(0);
if (!delegateResource(dto)) {
//资源委派失败,冻结付款账号余额以获取交易资格
freeze += freezeAmount;
freezeTrxForResources(wrapper, dto);
}
}
if ((energyUsed - energyLimit) <= 0) {
dto.setResourceCode(1);
if (!delegateResource(dto)) {
freeze += freezeAmount;
freezeTrxForResources(wrapper, dto);
}
}
long totalConsume = freeze + amount;
//账户余额小于本次交易总费用时,返回实际可交易金额
if (totalConsume > account.getBalance()) {
return account.getBalance() - freeze;
}
return amount;
}
/**
* 资源委派
*/
private boolean delegateResource(FreezeDto dto) {
try {
WalletFile walletFile =
WalletApi.CreateWalletFile(Numeric.hexStringToByteArray(RandomUtil.randomString(18)),
Numeric.hexStringToByteArray(config.getPrivateKey()));
WalletApi api = new WalletApi(walletFile);
return api.delegateResource(api.getAddress(), dto.getBalance(), dto.getResourceCode(),
decode58Check(dto.getAddress()), false, 0);
}
catch (Exception e) {
log.error("资源委派失败,原因:{}", e.getMessage());
return false;
}
}
/**
* 动态调整矿工费率
*/
private long calculateDynamicFee(ApiWrapper wrapper, String fromAddress) {
// 获取账户资源信息
AccountResourceMessage accountResource = wrapper.getAccountResource(fromAddress);
long netLimit = accountResource.getNetLimit();
long netUsed = accountResource.getNetUsed();
long energyLimit = accountResource.getEnergyLimit();
long energyUsed = accountResource.getEnergyUsed();
// 根据资源使用情况动态调整费用
// 网络较拥堵时增加费用
if ((netUsed >= netLimit * 0.8) || (energyUsed >= energyLimit * 0.8)) {
return Convert.toSun("2", Convert.Unit.TRX).longValue();
}
return Convert.toSun("1", Convert.Unit.TRX).longValue();
}
/**
* 冻结trx以获取带宽和能量
*
* @return
*/
public void freezeTrxForResources(ApiWrapper wrapper, FreezeDto dto) {
String pass = RandomUtil.randomString(18);
byte[] passwd = StringUtils.char2Byte(pass.toCharArray());
try {
WalletFile walletFile = WalletApi.CreateWalletFile(passwd,
Numeric.hexStringToByteArray(wrapper.keyPair.toPrivateKey()));
WalletApi api = new WalletApi(walletFile);
FreezeUtil freezeUtil = new FreezeUtil();
boolean b = freezeUtil.freezeBalanceV2(api.getAddress(), dto.getBalance(), dto.getResourceCode(),
walletFile, pass);
if (!b) {
throw new com.tron.util.UtilException("冻结资金失败,无法操作");
}
}
catch (Exception e) {
log.error("冻结资金出现问题,原因: {}, 金额:{}", e.getMessage(), dto.getBalance());
throw new com.tron.util.UtilException("冻结资金失败,无法操作");
}
}
public static byte[] decode58Check(String input) {
byte[] decodeCheck = Base58.decode(input);
if (decodeCheck.length <= 4) {
return null;
}
byte[] decodeData = new byte[decodeCheck.length - 4];
System.arraycopy(decodeCheck, 0, decodeData, 0, decodeData.length);
byte[] hash0 = Sha256Sm3Hash.hash(decodeData);
byte[] hash1 = Sha256Sm3Hash.hash(hash0);
return hash1[0] == decodeCheck[decodeData.length] && hash1[1] == decodeCheck[decodeData.length + 1] && hash1[2] == decodeCheck[decodeData.length + 2] && hash1[3] == decodeCheck[decodeData.length + 3] ? decodeData : null;
}
/**
* 转USDT
*
* @param dto
*/
public void transferUSDT(TransferDto dto) {
ApiWrapper wrapper = config.apiWrapper(dto.getPrivateKey());
Contract contract = this.getUsdtContract();
Trc20Contract token = new Trc20Contract(contract, wrapper.keyPair.toBase58CheckAddress(), wrapper);
log.error(token);
long finalTransferAmount = getFinalTransferAmount(wrapper, dto.getFromAddress(), dto.getAmount());
String transferStr = token.transfer(dto.getToAddress(), finalTransferAmount, 1, "memo", 1);
log.info(transferStr);
wrapper.close();
}
/**
* 获取ustd转账合约
*/
public Contract getUsdtContract() {
ApiWrapper wrapper = config.apiWrapper();
Contract contract = wrapper.getContract(config.getUsdtContract());
wrapper.close();
return contract;
}
/**
* 查询交易状态
*
* @param txid
*/
public String getTransactionStatusById(String txid) throws IllegalException {
ApiWrapper client = config.apiWrapper();
Chain.Transaction getTransaction = client.getTransactionById(txid);
client.close();
return getTransaction.getRet(0).getContractRet().name();
}
/**
* 获取USTD余额
*
* @return 账户余额
*/
public AccountBalance getBalance(String address) {
AccountBalance accountBalance = new AccountBalance(address);
List<Balance> list = new ArrayList<>();
Account account = this.getAccount(address);
list.add(new Balance("trx", account.getBalance()));
ApiWrapper wrapper = config.apiWrapper();
Trc20Contract token = new Trc20Contract(this.getUsdtContract(), address, wrapper);
wrapper.close();
BigInteger bigInteger = token.balanceOf(address);
list.add(new Balance("usdt", bigInteger.longValue()));
accountBalance.setBalance(list);
return accountBalance;
}
}
2.3 Tron操作工具类
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.extern.log4j.Log4j2;
import org.tron.api.GrpcAPI.Return;
import org.tron.api.GrpcAPI.TransactionExtention;
import org.tron.api.GrpcAPI.TransactionSignWeight;
import org.tron.api.GrpcAPI.TransactionSignWeight.Result.response_code;
import org.tron.common.crypto.ECKey;
import org.tron.common.crypto.Hash;
import org.tron.common.crypto.Sha256Sm3Hash;
import org.tron.common.utils.TransactionUtils;
import org.tron.common.utils.Utils;
import org.tron.core.exception.CancelException;
import org.tron.core.exception.CipherException;
import org.tron.keystore.StringUtils;
import org.tron.keystore.Wallet;
import org.tron.keystore.WalletFile;
import org.tron.protos.Protocol.Transaction;
import org.tron.protos.Protocol.Transaction.Contract.ContractType;
import org.tron.protos.contract.BalanceContract.FreezeBalanceV2Contract;
import org.tron.protos.contract.BalanceContract.TransferContract;
import org.tron.protos.contract.SmartContractOuterClass.CreateSmartContract;
import org.tron.walletserver.GrpcClient;
import java.io.IOException;
import java.util.Objects;
import static com.tron.util.TronUtil.decode58Check;
import static org.tron.walletserver.WalletApi.*;
/**
* FileName: FreezeUtil
* Author: wxz
* Date: 2024/12/18 17:28
* Description:
*/
@Log4j2
public class FreezeUtil {
private static final GrpcClient rpcCli = init();
private static FreezeBalanceV2Contract createFreezeBalanceContractV2(byte[] address, long frozen_balance,
int resourceCode) {
org.tron.protos.contract.BalanceContract.FreezeBalanceV2Contract.Builder builder =
FreezeBalanceV2Contract.newBuilder();
ByteString byteAddress = ByteString.copyFrom(address);
builder.setOwnerAddress(byteAddress).setFrozenBalance(frozen_balance).setResourceValue(resourceCode);
return builder.build();
}
/**
* 冻结
*
* @param ownerAddress
* @param frozen_balance
* @param resourceCode
* @param walletFile
* @param password
* @return
* @throws IOException
* @throws CancelException
*/
public boolean freezeBalanceV2(byte[] ownerAddress, long frozen_balance, int resourceCode, WalletFile walletFile,
String password) throws IOException, CancelException {
FreezeBalanceV2Contract contract = createFreezeBalanceContractV2(ownerAddress, frozen_balance, resourceCode);
TransactionExtention transactionExtention = rpcCli.createTransaction2(contract);
return this.processTransactionExtention(transactionExtention, walletFile, password.toCharArray());
}
private boolean processTransactionExtention(TransactionExtention transactionExtention, WalletFile walletFile,
char[] password) throws IOException, CancelException {
if (transactionExtention == null) {
return false;
}
Return ret = transactionExtention.getResult();
if (!ret.getResult()) {
log.error("Code = " + ret.getCode());
log.error("Message = " + ret.getMessage().toStringUtf8());
return false;
}
Transaction transaction = transactionExtention.getTransaction();
if (transaction.getRawData().getContractCount() == 0) {
log.error("Transaction is empty");
return false;
}
if (transaction.getRawData().getContract(0).getType() == ContractType.ShieldedTransferContract) {
return false;
}
transaction = this.signTransaction(transaction, walletFile, password);
this.showTransactionAfterSign(transaction);
return rpcCli.broadcastTransaction(transaction);
}
private void showTransactionAfterSign(Transaction transaction) throws InvalidProtocolBufferException {
if (transaction.getRawData().getContract(0).getType() == ContractType.CreateSmartContract) {
CreateSmartContract createSmartContract = (CreateSmartContract) transaction.getRawData()
.getContract(0)
.getParameter()
.unpack(CreateSmartContract.class);
}
}
private byte[] generateContractAddress(byte[] ownerAddress, Transaction trx) {
byte[] txRawDataHash = Sha256Sm3Hash.of(trx.getRawData().toByteArray()).getBytes();
byte[] combined = new byte[txRawDataHash.length + ownerAddress.length];
System.arraycopy(txRawDataHash, 0, combined, 0, txRawDataHash.length);
System.arraycopy(ownerAddress, 0, combined, txRawDataHash.length, ownerAddress.length);
return Hash.sha3omit12(combined);
}
private Transaction signTransaction(Transaction transaction, WalletFile walletFile, char[] password) throws IOException, CancelException {
if (transaction.getRawData().getTimestamp() == 0L) {
transaction = TransactionUtils.setTimestamp(transaction);
}
transaction = TransactionUtils.setExpirationTime(transaction);
try {
byte[] passwd = StringUtils.char2Byte(password);
transaction = TransactionUtils.sign(transaction, this.getEcKey(walletFile, passwd));
TransactionSignWeight weight = getTransactionSignWeight(transaction);
if (weight.getResult().getCode() == response_code.ENOUGH_PERMISSION) {
return transaction;
}
if (weight.getResult().getCode() != response_code.NOT_ENOUGH_PERMISSION) {
throw new CancelException(weight.getResult().getMessage());
}
System.out.println("Current signWeight is:");
System.out.println(Utils.printTransactionSignWeight(weight));
System.out.println("Please confirm if continue add signature enter y or Y, else any other");
}
catch (Exception e) {
}
this.showTransactionAfterSign(transaction);
throw new CancelException("User cancelled");
}
private ECKey getEcKey(WalletFile walletFile, byte[] password) throws CipherException {
return Wallet.decrypt(password, walletFile);
}
/**
* 转账
*
* @param ownerAddress
* @param to
* @param amount
* @param walletFile
* @param password
* @return
* @throws IOException
* @throws CancelException
*/
public boolean sendCoin(byte[] ownerAddress, String to, long amount, WalletFile walletFile, String password) throws IOException, CancelException {
TransferContract contract = createTransferContract(Objects.requireNonNull(decode58Check(to)), ownerAddress,
amount);
TransactionExtention transactionExtention = rpcCli.createTransaction2(contract);
return this.processTransactionExtention(transactionExtention, walletFile, password.toCharArray());
}
}
涉及到的DTO:
import lombok.Data;
import lombok.experimental.Accessors;
/**
* FileName: TransferDto
* Author: wxz
* Date: 2024/12/13 17:28
* Description:
*/
@Data
@Accessors(chain = true)
public class TransferDto {
private String fromAddress;
private String toAddress;
/**
* 转账金额,单位为sun(1/1000000trx)
*/
private long amount;
private String privateKey;
}
@Data
@Accessors(chain = true)
public class FreezeDto {
/**
* 账户地址
*/
@JsonProperty("owner_address")
private String address;
/**
* 冻结余额
*/
@JsonProperty("frozen_balance")
private long balance;
private final boolean visible = true;
public void setResourceCode(int resourceCode) {
if (resourceCode>1 || resourceCode<0){
throw new UtilException("换取的资源类型不正确");
}
this.resourceCode = resourceCode;
}
/**
* 冻结资源换取的资源类型: BANDWIDTH(0)、ENERGY(1) ; 默认为:BANDWIDTH
*/
private int resourceCode;
}
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* FileName: AccountBalance
* Author: wxz
* Date: 2024/12/16 14:38
* Description: 账户余额
*/
@Data
public class AccountBalance {
private String address;
private List<Balance> balance = new ArrayList<>();
public AccountBalance(String address) {
this.address = address;
}
@Data
@NoArgsConstructor
public static class Balance {
/**
* 余额类型" trx/ustd
*/
private String tokenAbbr;
private long balance;
public Balance(String tokenAbbr, long balance) {
this.tokenAbbr = tokenAbbr;
this.balance = balance;
}
}
public long getBalanceByAbbr(String abbr) {
if (StrUtil.isEmpty(abbr) || this.balance == null || this.balance.size() <= 0) return 0;
return this.balance.stream()
.filter(b -> abbr.equalsIgnoreCase(b.getTokenAbbr()))
.findFirst()
.map(Balance::getBalance)
.orElse(0L);
}
}
三、总结
上述util中,FreezeUtil
是经过我自己拆包、封装的tron提供的控制台工具而来,该工具主要提供账户资金冻结功能,专门拆包、封装这个工具的原因是tron官方提供的API已经升级了,不再支持资金冻结操作,可是又没有找到官方提供的新API,索性就自己拆包了一个控制台操作的接口,重新封装成一个util。
三、涉及到的资源
上文中涉及到的资源已提供,连接如下:点击下载
官方API:
https://developers.tron.network/reference/background