|
@@ -0,0 +1,523 @@
|
|
|
|
|
+package com.table.transfer.module.service;
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
|
|
+import cn.hutool.json.JSONUtil;
|
|
|
|
|
+import com.google.common.cache.Cache;
|
|
|
|
|
+import com.google.common.cache.CacheBuilder;
|
|
|
|
|
+import com.table.transfer.module.enpty.ChainCoinDetail;
|
|
|
|
|
+import com.table.transfer.module.enpty.TransactionInfoDTO;
|
|
|
|
|
+import com.table.transfer.module.enpty.TransferRequest;
|
|
|
|
|
+import com.table.transfer.module.enpty.TransferResponse;
|
|
|
|
|
+import com.table.transfer.util.CryptoUtil;
|
|
|
|
|
+import com.table.transfer.util.aUtil;
|
|
|
|
|
+import com.table.transfer.util.ether.ChainCoinDetailManager;
|
|
|
|
|
+import com.table.transfer.util.ether.Web3jManager;
|
|
|
|
|
+import com.table.transfer.util.encryp.RSAEncryptionUtil;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.web3j.abi.FunctionEncoder;
|
|
|
|
|
+import org.web3j.abi.FunctionReturnDecoder;
|
|
|
|
|
+import org.web3j.abi.TypeReference;
|
|
|
|
|
+import org.web3j.abi.datatypes.Address;
|
|
|
|
|
+import org.web3j.abi.datatypes.Bool;
|
|
|
|
|
+import org.web3j.abi.datatypes.Function;
|
|
|
|
|
+import org.web3j.abi.datatypes.Type;
|
|
|
|
|
+import org.web3j.abi.datatypes.generated.Uint256;
|
|
|
|
|
+import org.web3j.crypto.Credentials;
|
|
|
|
|
+import org.web3j.crypto.RawTransaction;
|
|
|
|
|
+import org.web3j.crypto.TransactionEncoder;
|
|
|
|
|
+import org.web3j.protocol.Web3j;
|
|
|
|
|
+import org.web3j.protocol.core.DefaultBlockParameterName;
|
|
|
|
|
+import org.web3j.protocol.core.methods.request.Transaction;
|
|
|
|
|
+import org.web3j.protocol.core.methods.response.*;
|
|
|
|
|
+import org.web3j.utils.Convert;
|
|
|
|
|
+import org.web3j.utils.Numeric;
|
|
|
|
|
+
|
|
|
|
|
+import java.io.IOException;
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.math.BigInteger;
|
|
|
|
|
+import java.math.RoundingMode;
|
|
|
|
|
+import java.rmi.ServerException;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
+
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+public class EthereumService implements BlockchainService {
|
|
|
|
|
+
|
|
|
|
|
+ //EVM 链上的 ERC20 固定的事件签名keccak256 哈希
|
|
|
|
|
+ String TRANSFER_EVENT_SIGNATURE = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
|
|
|
+
|
|
|
|
|
+ // 默认 Gas Limit(保险值)
|
|
|
|
|
+ public static final BigInteger DEFAULT_GAS_LIMIT_NATIVE = BigInteger.valueOf(21_000);
|
|
|
|
|
+ public static final BigInteger DEFAULT_GAS_LIMIT_TOKEN = BigInteger.valueOf(120_000);
|
|
|
|
|
+ // BSC:官方显示平均手续费,后续应该在数据库中配置,2021年时出现过手续费暴涨的情况,手动配置可以提高最低值,提高成功率
|
|
|
|
|
+
|
|
|
|
|
+ // 全局 nonce 锁,到期自动释放
|
|
|
|
|
+ private final Cache<String, Object> nonceLocks = CacheBuilder.newBuilder()
|
|
|
|
|
+ .expireAfterAccess(1, TimeUnit.HOURS) // 1小时无访问自动移除
|
|
|
|
|
+ .build();
|
|
|
|
|
+ private final Map<String, Object> pendingTransfers = new ConcurrentHashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取当前网络 gas price,并设置“最低安全值”
|
|
|
|
|
+ */
|
|
|
|
|
+ public BigInteger getSafeGasPrice(TransferRequest transferRequest) throws IOException {
|
|
|
|
|
+ BigInteger networkGasPrice = Web3jManager.getWeb3j(transferRequest.getChain()).ethGasPrice().send().getGasPrice();
|
|
|
|
|
+ ChainCoinDetail chainCoinDetail = ChainCoinDetailManager.getChainCoinDetail(transferRequest.getAssetCode());
|
|
|
|
|
+ //设置的最低值
|
|
|
|
|
+ BigInteger minGasPrice = chainCoinDetail.getMinGasPrice();
|
|
|
|
|
+ //低于最低手续费,就取最低值
|
|
|
|
|
+ if (networkGasPrice.compareTo(minGasPrice) <= 0) {
|
|
|
|
|
+ networkGasPrice = minGasPrice;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ //设定的最低值就直接加价20%防止拥堵被抛弃
|
|
|
|
|
+ networkGasPrice = networkGasPrice.multiply(BigInteger.valueOf(12)).divide(BigInteger.TEN);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ BigInteger maxGasPrice = chainCoinDetail.getMaxGasPrice();
|
|
|
|
|
+ if (networkGasPrice.compareTo(maxGasPrice) > 0) {
|
|
|
|
|
+ throw new ServerException("手续费超过设定最大值,当前估算Price"+networkGasPrice);
|
|
|
|
|
+ }
|
|
|
|
|
+ return networkGasPrice;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ public void validateTransferRequest(TransferRequest transferRequest) throws Exception {
|
|
|
|
|
+ String pk = RSAEncryptionUtil.decrypt(transferRequest.getEncryptionPrivateKey());
|
|
|
|
|
+ Credentials credentials = Credentials.create(pk);
|
|
|
|
|
+ String fromAddress = credentials.getAddress();
|
|
|
|
|
+ if (!transferRequest.getFromAddress().equalsIgnoreCase(fromAddress)) {
|
|
|
|
|
+ throw new Exception("私钥和from地址不一致");
|
|
|
|
|
+ }
|
|
|
|
|
+ transferRequest.setPrivateKey(pk);
|
|
|
|
|
+ // 验证私钥
|
|
|
|
|
+ if (!CryptoUtil.validatePrivateKey(transferRequest.getPrivateKey())) {
|
|
|
|
|
+ throw new Exception("无效的私钥格式");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 验证地址
|
|
|
|
|
+ if (!CryptoUtil.validateEthAddress(transferRequest.getToAddress())) {
|
|
|
|
|
+ throw new Exception("无效的接收地址");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (ChainCoinDetailManager.getChainCoinDetail(transferRequest.getAssetCode()) == null) {
|
|
|
|
|
+ throw new Exception("不支持" + transferRequest.getAssetCode());
|
|
|
|
|
+ }
|
|
|
|
|
+ //todo 支持的类型校验 合约
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public TransferResponse transfer(TransferRequest transferRequest) throws Exception {
|
|
|
|
|
+ validateTransferRequest(transferRequest);
|
|
|
|
|
+ //交易对
|
|
|
|
|
+ String assetCode = transferRequest.getAssetCode();
|
|
|
|
|
+ //接受地址
|
|
|
|
|
+ String toAddress = transferRequest.getToAddress();
|
|
|
|
|
+ //金额
|
|
|
|
|
+ String amount = transferRequest.getAmount();
|
|
|
|
|
+ //使用对应链的web3j
|
|
|
|
|
+ Web3j web3j = Web3jManager.getWeb3j(transferRequest.getChain());
|
|
|
|
|
+ //私钥
|
|
|
|
|
+ String privateKey = transferRequest.getPrivateKey();
|
|
|
|
|
+ //是否代币转账
|
|
|
|
|
+ boolean isTokenTransfer = transferRequest.isTokenTransfer();
|
|
|
|
|
+ //发送地址
|
|
|
|
|
+ String fromAddress = transferRequest.getFromAddress();
|
|
|
|
|
+ //合约地址
|
|
|
|
|
+ String contractAddress = transferRequest.getContractAddress();
|
|
|
|
|
+ //代币配置
|
|
|
|
|
+ ChainCoinDetail chainCoinDetail = ChainCoinDetailManager.getChainCoinDetail(assetCode);
|
|
|
|
|
+
|
|
|
|
|
+ Credentials credentials = Credentials.create(privateKey);
|
|
|
|
|
+ //重试次数
|
|
|
|
|
+ int retryCount = 0;
|
|
|
|
|
+ //hash
|
|
|
|
|
+ String sentTxHash = null;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ //todo chainId和链检查 必须相同,避免跨链丢币风险。
|
|
|
|
|
+
|
|
|
|
|
+ //4. 转换金额为wei
|
|
|
|
|
+ BigInteger decimalsAmount = toWei(transferRequest.getAmount(), chainCoinDetail.getDecimal());
|
|
|
|
|
+ if (decimalsAmount.compareTo(BigInteger.ONE) < 0) {
|
|
|
|
|
+ return TransferResponse.error("转账金额过低");
|
|
|
|
|
+ }
|
|
|
|
|
+ //2.上锁获取nonce
|
|
|
|
|
+ BigInteger nonce = null;
|
|
|
|
|
+ if (pendingTransfers.containsKey(fromAddress)) {
|
|
|
|
|
+ throw new Exception("该发送地址已有未完成转账");
|
|
|
|
|
+ }
|
|
|
|
|
+ pendingTransfers.put(fromAddress, true);
|
|
|
|
|
+
|
|
|
|
|
+ Object lock = nonceLocks.get(fromAddress, Object::new);
|
|
|
|
|
+ nonce = getNonce(assetCode, fromAddress);
|
|
|
|
|
+ while (retryCount < 3) {
|
|
|
|
|
+ try {
|
|
|
|
|
+
|
|
|
|
|
+ synchronized (lock) {
|
|
|
|
|
+ log.info("【转账开始】账户={},目标={}, 金额={}, nonce={}", fromAddress, toAddress, amount, nonce);
|
|
|
|
|
+ //5. 构建交易
|
|
|
|
|
+ RawTransaction rawTransaction;
|
|
|
|
|
+ //3.估算gas和gaslimit 估算个屁,limit和price估算完全不靠谱
|
|
|
|
|
+// BigInteger gasLimit = estimateGasSafely(transferRequest);
|
|
|
|
|
+ BigInteger gasLimit= isTokenTransfer ? DEFAULT_GAS_LIMIT_TOKEN : DEFAULT_GAS_LIMIT_NATIVE;
|
|
|
|
|
+ BigInteger currentGasPrice = getSafeGasPrice(transferRequest);
|
|
|
|
|
+ log.info("gasLimit: {},currentGasPrice {}", gasLimit, currentGasPrice);
|
|
|
|
|
+ //代币转账
|
|
|
|
|
+ if (isTokenTransfer) {
|
|
|
|
|
+ // 构造代币转账的 function 调用
|
|
|
|
|
+ Function function = new Function(
|
|
|
|
|
+ "transfer",
|
|
|
|
|
+ Arrays.asList(new Address(toAddress), new Uint256(decimalsAmount)),
|
|
|
|
|
+ Arrays.asList(new TypeReference<Bool>() {
|
|
|
|
|
+ })
|
|
|
|
|
+ );
|
|
|
|
|
+ String data = FunctionEncoder.encode(function);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("【代币转账开始】发送者={}, 接收者={}, 代币合约={}, 金额={}, nonce={}", fromAddress, toAddress, contractAddress, decimalsAmount, nonce);
|
|
|
|
|
+ // 构造 RawTransaction:目标是代币合约地址
|
|
|
|
|
+ rawTransaction = RawTransaction.createTransaction(
|
|
|
|
|
+ nonce,
|
|
|
|
|
+ currentGasPrice,
|
|
|
|
|
+ gasLimit,
|
|
|
|
|
+ transferRequest.getContractAddress(),
|
|
|
|
|
+ BigInteger.ZERO,
|
|
|
|
|
+ data
|
|
|
|
|
+ );
|
|
|
|
|
+ } else {
|
|
|
|
|
+ //主币转账
|
|
|
|
|
+ log.info("【转账开始】账户={},目标={}, 金额={}, nonce={}", fromAddress, toAddress, amount, nonce);
|
|
|
|
|
+ rawTransaction = RawTransaction.createEtherTransaction(
|
|
|
|
|
+ nonce,
|
|
|
|
|
+ currentGasPrice,
|
|
|
|
|
+ gasLimit,
|
|
|
|
|
+ toAddress,
|
|
|
|
|
+ decimalsAmount
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ //6.签名交易
|
|
|
|
|
+ byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, chainCoinDetail.getChainId(), credentials);
|
|
|
|
|
+ String hexValue = Numeric.toHexString(signedMessage);
|
|
|
|
|
+ //7.发送交易
|
|
|
|
|
+ EthSendTransaction response = web3j.ethSendRawTransaction(hexValue).send();
|
|
|
|
|
+
|
|
|
|
|
+ if (response.hasError()) {
|
|
|
|
|
+ String errorMsg = response.getError().getMessage();
|
|
|
|
|
+ // 仅对 underpriced 错误重试(同一 nonce)
|
|
|
|
|
+ if (errorMsg.contains("underpriced") || errorMsg.contains("replacement transaction underpriced")) {
|
|
|
|
|
+ retryCount++;
|
|
|
|
|
+ log.warn(" 交易因手续费太低被拒,nonce={},第 {}/3 次重试,gasPrice={}",
|
|
|
|
|
+ nonce, retryCount, currentGasPrice);
|
|
|
|
|
+
|
|
|
|
|
+ // 根据重试次数动态提高 gasPrice
|
|
|
|
|
+ currentGasPrice = currentGasPrice.multiply(BigInteger.valueOf(12)).divide(BigInteger.valueOf(10)); // +20%
|
|
|
|
|
+
|
|
|
|
|
+ // 不超过链上限
|
|
|
|
|
+ if (currentGasPrice.compareTo(chainCoinDetail.getMaxGasPrice()) > 0) {
|
|
|
|
|
+ //todo 超过了链的上限,直接抛弃,然后修改数据库错误提示,告知是因为手续费太高导致的,避免大额手续费浪费
|
|
|
|
|
+ // 更新数据库
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ continue; // 重试(同一 nonce)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 其他错误直接抛出,如 nonce too low, insufficient funds, already known
|
|
|
|
|
+ log.error("节点拒绝交易(不可重试): {}", errorMsg);
|
|
|
|
|
+ throw new Exception("交易失败: " + errorMsg);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 8.成功发送,拿到哈希
|
|
|
|
|
+ sentTxHash = response.getTransactionHash();
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("交易已广播,TxHash={}", sentTxHash);
|
|
|
|
|
+ // 不再重试发送,直接等待确认
|
|
|
|
|
+ break;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ // 网络层异常:可能是超时,但交易可能已在链上
|
|
|
|
|
+ if (sentTxHash != null) {
|
|
|
|
|
+ TransactionInfoDTO transactionInfoByHash = getTransactionInfoByHash(assetCode, sentTxHash);
|
|
|
|
|
+ if (transactionInfoByHash != null) {
|
|
|
|
|
+ log.warn("网络异常,但交易已发送,尝试查询链上状态: {}", sentTxHash);
|
|
|
|
|
+ }
|
|
|
|
|
+ break; // 跳出重试,进入等待
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 连广播都没完成,且不是 underpriced 错误,不重试
|
|
|
|
|
+ log.error("网络异常且无交易哈希,放弃重试", e);
|
|
|
|
|
+ throw new Exception("网络异常,无法发送交易" + e);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("交易过程发生未预期异常", e);
|
|
|
|
|
+ throw e;
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (sentTxHash == null) {
|
|
|
|
|
+ // 说明 3 次都失败了,且没拿到哈希
|
|
|
|
|
+ log.error("交易发送失败:尝试 3 次均被拒绝(gasPrice 过低),nonce={}", nonce);
|
|
|
|
|
+ // 注意:这里不能抛“可重试”异常,否则外部可能再调用
|
|
|
|
|
+ throw new Exception("交易因手续费太低被拒,已尝试3次,请稍后重试");
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("交易已发送,TxHash: {}", sentTxHash);
|
|
|
|
|
+
|
|
|
|
|
+// // 11. 立即验证是否进入 mempool?
|
|
|
|
|
+// if (!isTransactionInMempool(web3j, sentTxHash)) {
|
|
|
|
|
+// log.error("交易未立即进入 mempool,可能广播失败: {}", sentTxHash);
|
|
|
|
|
+// }
|
|
|
|
|
+ return TransferResponse.success(sentTxHash);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("ETH转账失败: {}", e.getMessage());
|
|
|
|
|
+ return TransferResponse.error("ETH转账失败: " + e.getMessage());
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ pendingTransfers.remove(fromAddress);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取gasLimit
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param request
|
|
|
|
|
+ * @return
|
|
|
|
|
+ */
|
|
|
|
|
+ public BigInteger estimateGasSafely(TransferRequest request) {
|
|
|
|
|
+ String chain = request.getChain();
|
|
|
|
|
+ boolean isTokenTransfer = request.isTokenTransfer();
|
|
|
|
|
+ String fromAddress = request.getFromAddress();
|
|
|
|
|
+ String toAddress = request.getToAddress();
|
|
|
|
|
+ String contractAddress = request.getContractAddress();
|
|
|
|
|
+ BigInteger amount = request.getAmountWei();
|
|
|
|
|
+ Web3j web3j = Web3jManager.getWeb3j(chain);
|
|
|
|
|
+ Transaction transaction;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (isTokenTransfer) {
|
|
|
|
|
+ // 代币转账:调用合约的 transfer(to, amount)
|
|
|
|
|
+ Function function = new Function(
|
|
|
|
|
+ "transfer",
|
|
|
|
|
+ Arrays.asList(
|
|
|
|
|
+ new Address(toAddress),
|
|
|
|
|
+ new Uint256(amount != null ? amount : BigInteger.ONE)
|
|
|
|
|
+ ),
|
|
|
|
|
+ List.of()
|
|
|
|
|
+ );
|
|
|
|
|
+ String data = FunctionEncoder.encode(function);
|
|
|
|
|
+ transaction = Transaction.createEthCallTransaction(fromAddress, contractAddress, data);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 主币转账:data 为空
|
|
|
|
|
+ transaction = Transaction.createEthCallTransaction(fromAddress, toAddress, "", BigInteger.ONE);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ EthEstimateGas estimate;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ estimate = web3j.ethEstimateGas(transaction).send();
|
|
|
|
|
+ BigInteger gasUsed = estimate.getAmountUsed();
|
|
|
|
|
+
|
|
|
|
|
+ return gasUsed;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ //删掉重试机制,减少响应时间,失败直接返回默认值
|
|
|
|
|
+ log.error("Gas 估算失败{}", e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 兜底:返回默认值
|
|
|
|
|
+ BigInteger defaultValue = (transaction.getData() == null || transaction.getData().isEmpty()) ?
|
|
|
|
|
+ DEFAULT_GAS_LIMIT_NATIVE : // 主币
|
|
|
|
|
+ DEFAULT_GAS_LIMIT_TOKEN; // 代币
|
|
|
|
|
+
|
|
|
|
|
+ log.error("使用保险默认 Gas Limit: {}", defaultValue);
|
|
|
|
|
+ return defaultValue;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("Gas 估算准备阶段异常: {}", e.getMessage());
|
|
|
|
|
+ e.printStackTrace();
|
|
|
|
|
+ // 返回基于链类型的默认值
|
|
|
|
|
+ return isTokenTransfer ? DEFAULT_GAS_LIMIT_TOKEN : DEFAULT_GAS_LIMIT_NATIVE;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取nonce/交易总数
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return
|
|
|
|
|
+ * @throws Exception
|
|
|
|
|
+ */
|
|
|
|
|
+ public BigInteger getNonce(String assetCode, String address) throws Exception {
|
|
|
|
|
+ Web3j web3j = Web3jManager.getWeb3j(aUtil.assetCode2Chain(assetCode));
|
|
|
|
|
+ EthGetTransactionCount ethGetTransactionCount = web3j
|
|
|
|
|
+ .ethGetTransactionCount(address, DefaultBlockParameterName.PENDING)
|
|
|
|
|
+ .send();
|
|
|
|
|
+ return ethGetTransactionCount.getTransactionCount();
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ private BigInteger toWei(String amount, Integer decimal) throws Exception {
|
|
|
|
|
+ // 简化的转换,实际需要处理小数
|
|
|
|
|
+ BigDecimal pow = BigDecimal.TEN.pow(decimal);
|
|
|
|
|
+ return new BigDecimal(amount).multiply(pow).toBigInteger();
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 目标地址代币余额
|
|
|
|
|
+ */
|
|
|
|
|
+ public BigDecimal getTokenBalance(String assetCode, String contractAddress, String targetAddress) throws Exception {
|
|
|
|
|
+
|
|
|
|
|
+ Function function = new Function("balanceOf",
|
|
|
|
|
+ List.of(new Address(targetAddress)),
|
|
|
|
|
+ List.of(new TypeReference<Uint256>() {
|
|
|
|
|
+ })
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ String data = FunctionEncoder.encode(function);
|
|
|
|
|
+ Transaction transaction = Transaction.createEthCallTransaction(null, contractAddress, data);
|
|
|
|
|
+
|
|
|
|
|
+ EthCall response = Web3jManager.getWeb3j(aUtil.assetCode2Chain(assetCode)).ethCall(transaction, DefaultBlockParameterName.LATEST).send();
|
|
|
|
|
+ log.info("EthCall response: {}", JSONUtil.toJsonStr(response));
|
|
|
|
|
+ List<Type> result = FunctionReturnDecoder.decode(response.getValue(), function.getOutputParameters());
|
|
|
|
|
+
|
|
|
|
|
+ if (result.isEmpty()) {
|
|
|
|
|
+ throw new Exception("获取代币余额失败");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ BigInteger balanceWei = (BigInteger) result.get(0).getValue();
|
|
|
|
|
+ log.info("Raw balance: {}", balanceWei);
|
|
|
|
|
+
|
|
|
|
|
+ int decimals = ChainCoinDetailManager.getChainCoinDetail(assetCode).getDecimal(); // 如果需要精确,应该调用合约的 decimals() 方法
|
|
|
|
|
+ BigDecimal balance = new BigDecimal(balanceWei)
|
|
|
|
|
+ .divide(new BigDecimal(BigInteger.TEN.pow(decimals)), decimals, RoundingMode.DOWN);
|
|
|
|
|
+
|
|
|
|
|
+ return balance;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取目标地址代主币余额
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param targetAddress
|
|
|
|
|
+ * @return
|
|
|
|
|
+ * @throws Exception
|
|
|
|
|
+ */
|
|
|
|
|
+ public BigDecimal getBalance(String chain, String targetAddress) throws Exception {
|
|
|
|
|
+
|
|
|
|
|
+ EthGetBalance balanceResponse = Web3jManager.getWeb3j(chain).ethGetBalance(targetAddress, DefaultBlockParameterName.LATEST).send();
|
|
|
|
|
+ Convert.Unit unit;
|
|
|
|
|
+ if (chain.equalsIgnoreCase("ETH") || chain.equalsIgnoreCase("BSC")) {
|
|
|
|
|
+ unit = Convert.Unit.ETHER;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new Exception("暂不支持的链");
|
|
|
|
|
+ }
|
|
|
|
|
+ return Convert.fromWei(balanceResponse.getBalance().toString(), unit);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Long getLastBlock(String chain) throws IOException {
|
|
|
|
|
+ EthBlockNumber blockNumber = Web3jManager.getWeb3j(chain).ethBlockNumber().send();
|
|
|
|
|
+ return blockNumber.getBlockNumber().longValue();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 校验地址有效性
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param address
|
|
|
|
|
+ * @return
|
|
|
|
|
+ */
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public boolean validateAddress(String address) {
|
|
|
|
|
+ return CryptoUtil.validateEthAddress(address);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 通过hash获取交易信息
|
|
|
|
|
+ */
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public TransactionInfoDTO getTransactionInfoByHash(String assetCode, String transactionHash) throws Exception {
|
|
|
|
|
+ String chain = aUtil.assetCode2Chain(assetCode);
|
|
|
|
|
+ // 查询交易回执
|
|
|
|
|
+ EthGetTransactionReceipt receiptResponse = Web3jManager.getWeb3j(chain).ethGetTransactionReceipt(transactionHash).send();
|
|
|
|
|
+ Optional<TransactionReceipt> receiptOpt = receiptResponse.getTransactionReceipt();
|
|
|
|
|
+ if (!receiptOpt.isPresent()) {
|
|
|
|
|
+ log.warn("交易尚未上链: {}", transactionHash);
|
|
|
|
|
+ throw new ServerException("交易尚未上链: " + transactionHash);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ TransactionReceipt receipt = receiptOpt.get();
|
|
|
|
|
+ // 必须有区块号(已上链)
|
|
|
|
|
+ if (receipt.getBlockNumber() == null) {
|
|
|
|
|
+ log.warn("交易回执无区块号: {}", transactionHash);
|
|
|
|
|
+ throw new ServerException("交易回执无区块号: " + transactionHash);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ //判断交易是否执行成功
|
|
|
|
|
+ boolean isSuccess = "0x1".equals(receipt.getStatus()) || "1".equals(receipt.getStatus());
|
|
|
|
|
+
|
|
|
|
|
+ if (!isSuccess) {
|
|
|
|
|
+ log.warn("交易执行失败 (reverted): {}, status={}", transactionHash, receipt.getStatus());
|
|
|
|
|
+ throw new ServerException(StrUtil.format("交易执行失败 (reverted): {}, status={}", transactionHash, receipt.getStatus()));
|
|
|
|
|
+ }
|
|
|
|
|
+ TransactionReceipt transactionReceipt = receiptOpt.orElseThrow(RuntimeException::new);
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ // 解析 logs 获取真正的 to 地址和 amount
|
|
|
|
|
+ List<Log> logs = transactionReceipt.getLogs();
|
|
|
|
|
+ if (!logs.isEmpty()) {
|
|
|
|
|
+
|
|
|
|
|
+ for (Log log : logs) {
|
|
|
|
|
+ //确保有3个主题并且符合keccak256
|
|
|
|
|
+ if (log.getTopics().size() >= 3 && TRANSFER_EVENT_SIGNATURE.equals(log.getTopics().get(0))) {
|
|
|
|
|
+
|
|
|
|
|
+ // 主题解析:主题0是事件签名,主题1是from,主题2是to
|
|
|
|
|
+ List<String> topics = log.getTopics();
|
|
|
|
|
+ String transferFrom = Numeric.toBigInt(topics.get(1)).toString(16);
|
|
|
|
|
+ transferFrom = "0x" + padZeroes(transferFrom, 40); // 补齐40位(20字节)
|
|
|
|
|
+
|
|
|
|
|
+ String transferTo = Numeric.toBigInt(topics.get(2)).toString(16);
|
|
|
|
|
+ transferTo = "0x" + padZeroes(transferTo, 40); // 补齐40位
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ String data = log.getData();
|
|
|
|
|
+ BigInteger amountValue = Numeric.toBigInt(data);
|
|
|
|
|
+
|
|
|
|
|
+ //计算gas费
|
|
|
|
|
+ BigInteger gasUsed = Numeric.toBigInt(transactionReceipt.getGasUsed().toByteArray());
|
|
|
|
|
+ BigInteger effectiveGasPrice = Numeric.toBigInt(transactionReceipt.getEffectiveGasPrice());
|
|
|
|
|
+ BigInteger totalGasFeeWei = gasUsed.multiply(effectiveGasPrice);
|
|
|
|
|
+ // todo eth和bsc位数一样,后续增加其他EVM币种若是位数不一样这里需要改适配
|
|
|
|
|
+ BigDecimal totalGasFeeEth = Convert.fromWei(totalGasFeeWei.toString(), Convert.Unit.ETHER);
|
|
|
|
|
+
|
|
|
|
|
+ TransactionInfoDTO transactionInfoDTO = new TransactionInfoDTO();
|
|
|
|
|
+ transactionInfoDTO.setStatus("success");
|
|
|
|
|
+
|
|
|
|
|
+ Integer decimal = ChainCoinDetailManager.getChainCoinDetail(assetCode).getDecimal();
|
|
|
|
|
+ // USDT 是 6 位小数,所以除以 10^6
|
|
|
|
|
+
|
|
|
|
|
+ BigDecimal amount = new BigDecimal(amountValue).divide(new BigDecimal(BigInteger.TEN.pow(decimal)), decimal, RoundingMode.DOWN);
|
|
|
|
|
+ transactionInfoDTO.setAmount(amount);
|
|
|
|
|
+ transactionInfoDTO.setGasFee(totalGasFeeEth);
|
|
|
|
|
+ transactionInfoDTO.setHash(transactionReceipt.getTransactionHash());
|
|
|
|
|
+ transactionInfoDTO.setFromAddress(transferFrom);
|
|
|
|
|
+ transactionInfoDTO.setToAddress(transferTo);
|
|
|
|
|
+ return transactionInfoDTO;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new Exception("ethscan解析Hash失败");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String padZeroes(String hex, int length) {
|
|
|
|
|
+ if (hex.length() >= length) return hex;
|
|
|
|
|
+ return "0".repeat(length - hex.length()) + hex;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+}
|