Tron波场开发

一、前言

文本源自 微博客 且已获授权,请尊重知识产权。

     近段时间公司接到了以为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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值