|
|
@@ -1,5 +1,6 @@
|
|
|
package com.crm.rely.backend.model.util;
|
|
|
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
import com.alibaba.fastjson.JSONArray;
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
import com.crm.rely.backend.core.exception.ServiceException;
|
|
|
@@ -10,6 +11,7 @@ import lombok.extern.slf4j.Slf4j;
|
|
|
import java.io.ByteArrayOutputStream;
|
|
|
import java.io.InputStream;
|
|
|
import java.math.BigDecimal;
|
|
|
+import java.math.BigInteger;
|
|
|
import java.net.HttpURLConnection;
|
|
|
import java.net.URL;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
@@ -19,16 +21,26 @@ import java.util.Locale;
|
|
|
@Slf4j
|
|
|
public class EncryptionHashQueryUtil {
|
|
|
|
|
|
+ private final static String KEY = "7TCUANPQEXYHVSFV8WHIVSUBGZN9VPEXGY";
|
|
|
+
|
|
|
/* ================= API ================= */
|
|
|
|
|
|
- private static final String ETH_API = "https://eth.blockscout.com/api/v2/transactions/";
|
|
|
- private static final String ETC_API = "https://blockscout.com/etc/mainnet/api/v2/transactions/";
|
|
|
+ private static final String ETH_API = "https://api.etherscan.io/v2/api?apikey=%s" +
|
|
|
+ "&chainid=%s&txhash=%s&module=proxy&action=eth_getTransactionReceipt";
|
|
|
+ private static final String ETC_API = "https://api.etherscan.io/v2/api?apikey=%s" +
|
|
|
+ "&chainid=%s&txhash=%s&module=proxy&action=eth_getTransactionReceipt";
|
|
|
private static final String POLYGON_API = "https://polygon.blockscout.com/api/v2/transactions/";
|
|
|
private static final String TRON_API = "https://apilist.tronscanapi.com/api/transaction-info?hash=";
|
|
|
private static final String SOL_API = "https://api.mainnet-beta.solana.com";
|
|
|
+ private static final String BSC_RPC = "https://bsc-dataseed.binance.org/";
|
|
|
+
|
|
|
|
|
|
/* ================= 合约地址 ================= */
|
|
|
|
|
|
+ // BSC USDT / USDC 合约
|
|
|
+ private static final String BNB_USDT = "0x55d398326f99059fF775485246999027B3197955";
|
|
|
+
|
|
|
+ private static final String BNB_USDC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d";
|
|
|
private static final String ETH_USDT = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
|
|
private static final String ETH_USDC = "0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
|
|
|
|
|
|
@@ -44,10 +56,13 @@ public class EncryptionHashQueryUtil {
|
|
|
private static final String SOL_USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
|
- String txHash = "c370e2f1b6ac1d9f7078282ddad20d70f3623a2eaa8481b498d82585de9d94ee";
|
|
|
- String chain = "tron";
|
|
|
+// String txHash = "c370e2f1b6ac1d9f7078282ddad20d70f3623a2eaa8481b498d82585de9d94ee";
|
|
|
+// String chain = "tron";
|
|
|
|
|
|
- System.out.println(query(txHash, chain));
|
|
|
+ String txHash = "0x9958ee6bf7c8edae52a4c5d07c7f61a8bf477c4dd74564536a3a181441026d28";
|
|
|
+ String chain = "ethereum";
|
|
|
+
|
|
|
+ System.out.println(query(txHash, chain, "usdt", "0x5b5d5c9f3cf3b7a726d26bfa2529b317688e30bf"));
|
|
|
}
|
|
|
|
|
|
/* ================= 统一入口 ================= */
|
|
|
@@ -60,24 +75,233 @@ public class EncryptionHashQueryUtil {
|
|
|
if (Strings.isNullOrEmpty(currency)) {
|
|
|
currency = "usdt";
|
|
|
}
|
|
|
+ return query(txHash, chain, currency, null);
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ public static TxResult query(String txHash, String chain, String currency, String toAddress) throws Exception {
|
|
|
+ if (Strings.isNullOrEmpty(currency)) {
|
|
|
+ currency = "usdt";
|
|
|
+ }
|
|
|
|
|
|
switch (chain.toLowerCase(Locale.ROOT)) {
|
|
|
case "ethereum":
|
|
|
- return evmTxQuery(ETH_API, txHash, ethContract(currency));
|
|
|
+ return ethereumQuery(ETH_API, txHash, ethContract(currency), toAddress, "1");
|
|
|
case "ethereum-classic":
|
|
|
- return evmTxQuery(ETC_API, txHash, ETC_USDT);
|
|
|
+ return ethereumQuery(ETC_API, txHash, ETC_USDT, toAddress, "61");
|
|
|
case "polygon":
|
|
|
return evmTxQuery(POLYGON_API, txHash, polygonContract(currency));
|
|
|
case "tron":
|
|
|
return tronTxQuery(txHash, currency);
|
|
|
case "solana":
|
|
|
return solTxQuery(txHash, currency);
|
|
|
+ case "bnb":
|
|
|
+ case "bsc":
|
|
|
+ case "binance":
|
|
|
+ case "binance-smart-chain":
|
|
|
+ return bscRpcQuery(txHash, bnbContract(currency), toAddress);
|
|
|
default:
|
|
|
throw new ServiceException("Unsupported chain: " + chain);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /* ================= EVM ================= */
|
|
|
+ private static TxResult bscRpcQuery(String txHash, String expectedContract, String toAddress) throws Exception {
|
|
|
+
|
|
|
+ if (Strings.isNullOrEmpty(toAddress)) {
|
|
|
+ throw new ServiceException("Target token transfer not found");
|
|
|
+ }
|
|
|
+
|
|
|
+ String json = evmRpc("eth_getTransactionReceipt", new Object[]{txHash}, BSC_RPC);
|
|
|
+
|
|
|
+ JSONObject root = JSON.parseObject(json);
|
|
|
+ JSONObject result = root.getJSONObject("result");
|
|
|
+
|
|
|
+ if (result == null) {
|
|
|
+ throw new ServiceException("Invalid BSC tx response");
|
|
|
+ }
|
|
|
+
|
|
|
+ String status = result.getString("status");
|
|
|
+ if (status != null && !"0x1".equalsIgnoreCase(status)) {
|
|
|
+ throw new ServiceException("BSC transaction failed");
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONArray logs = result.getJSONArray("logs");
|
|
|
+ if (logs == null || logs.isEmpty()) {
|
|
|
+ throw new ServiceException("No logs");
|
|
|
+ }
|
|
|
+
|
|
|
+ String TRANSFER_TOPIC =
|
|
|
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
|
+
|
|
|
+ String blockHex = result.getString("blockNumber");
|
|
|
+ Long txTime = null;
|
|
|
+
|
|
|
+ if (!Strings.isNullOrEmpty(blockHex)) {
|
|
|
+ txTime = getBscBlockTimestamp(blockHex);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (int i = 0; i < logs.size(); i++) {
|
|
|
+
|
|
|
+ JSONObject log = logs.getJSONObject(i);
|
|
|
+ JSONArray topics = log.getJSONArray("topics");
|
|
|
+
|
|
|
+ if (topics == null || topics.size() < 3) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!TRANSFER_TOPIC.equalsIgnoreCase(topics.getString(0))) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ String contract = log.getString("address");
|
|
|
+ if (Strings.isNullOrEmpty(contract)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!expectedContract.equalsIgnoreCase(contract)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ String from = "0x" + topics.getString(1).substring(26);
|
|
|
+ String to = "0x" + topics.getString(2).substring(26);
|
|
|
+
|
|
|
+ if (!toAddress.equalsIgnoreCase(to)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ String data = log.getString("data");
|
|
|
+ if (Strings.isNullOrEmpty(data) || "0x".equalsIgnoreCase(data)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ BigInteger value = new BigInteger(data.substring(2), 16);
|
|
|
+
|
|
|
+ BigDecimal amount = new BigDecimal(value)
|
|
|
+ .divide(BigDecimal.TEN.pow(18));
|
|
|
+
|
|
|
+ TxResult r = new TxResult();
|
|
|
+ r.setTxHash(txHash);
|
|
|
+ r.setFrom(from);
|
|
|
+ r.setTo(to);
|
|
|
+ r.setAmount(amount);
|
|
|
+ r.setContractAddress(contract);
|
|
|
+ r.setStatus("success");
|
|
|
+ r.setTimestamp(txTime);
|
|
|
+ r.setContent(json);
|
|
|
+
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new ServiceException("Target token transfer not found");
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private static TxResult ethereumQuery(String api, String txHash, String expectedContract, String toAddress,
|
|
|
+ String chainId) throws Exception {
|
|
|
+
|
|
|
+ if (Strings.isNullOrEmpty(toAddress)) {
|
|
|
+ throw new ServiceException("Target token transfer not found");
|
|
|
+ }
|
|
|
+
|
|
|
+ String url = String.format(api, KEY, chainId, txHash);
|
|
|
+ String json = get(url);
|
|
|
+
|
|
|
+ JSONObject root = JSON.parseObject(json);
|
|
|
+ JSONObject result = root.getJSONObject("result");
|
|
|
+
|
|
|
+ if (result == null) {
|
|
|
+ throw new ServiceException("Invalid tx response");
|
|
|
+ }
|
|
|
+ String status = result.getString("status");
|
|
|
+
|
|
|
+ if (status != null && !"0x1".equalsIgnoreCase(status)) {
|
|
|
+ throw new ServiceException("Transaction failed");
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONArray logs = result.getJSONArray("logs");
|
|
|
+
|
|
|
+ if (logs == null || logs.isEmpty()) {
|
|
|
+ throw new ServiceException("No logs");
|
|
|
+ }
|
|
|
+ String TRANSFER_TOPIC =
|
|
|
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
|
+
|
|
|
+ // =========================
|
|
|
+ // 🔥 关键:timestamp 在 block 里
|
|
|
+ // =========================
|
|
|
+ String blockHex = result.getString("blockNumber");
|
|
|
+ Long txTime = null;
|
|
|
+
|
|
|
+ if (!Strings.isNullOrEmpty(blockHex)) {
|
|
|
+ txTime = getBlockTimestamp(blockHex, chainId);
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ for (int i = 0; i < logs.size(); i++) {
|
|
|
+
|
|
|
+ JSONObject log = logs.getJSONObject(i);
|
|
|
+ JSONArray topics = log.getJSONArray("topics");
|
|
|
+
|
|
|
+ if (topics == null || topics.size() < 3) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 必须是 Transfer
|
|
|
+ if (!TRANSFER_TOPIC.equalsIgnoreCase(topics.getString(0))) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ String contract = log.getString("address");
|
|
|
+ if (Strings.isNullOrEmpty(contract)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 过滤 token 合约
|
|
|
+ if (!expectedContract.equalsIgnoreCase(contract)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 解析地址
|
|
|
+ String from = "0x" + topics.getString(1).substring(26);
|
|
|
+ String to = "0x" + topics.getString(2).substring(26);
|
|
|
+
|
|
|
+ // 4. 只匹配目标地址
|
|
|
+ if (!toAddress.equalsIgnoreCase(to)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 解析金额
|
|
|
+ String data = log.getString("data");
|
|
|
+ if (Strings.isNullOrEmpty(data)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ BigInteger value = new BigInteger(data.substring(2), 16);
|
|
|
+
|
|
|
+ // USDT / USDC 默认 6 位
|
|
|
+ BigDecimal amount = new BigDecimal(value)
|
|
|
+ .divide(BigDecimal.TEN.pow(6));
|
|
|
+
|
|
|
+ TxResult r = new TxResult();
|
|
|
+ r.setTxHash(txHash);
|
|
|
+ r.setFrom(from);
|
|
|
+ r.setTo(to);
|
|
|
+ r.setAmount(amount);
|
|
|
+ r.setContractAddress(contract);
|
|
|
+ r.setStatus("success");
|
|
|
+ r.setContent(json);
|
|
|
+
|
|
|
+ // =========================
|
|
|
+ // 🔥 时间戳写入
|
|
|
+ // =========================
|
|
|
+ r.setTimestamp(txTime);
|
|
|
+
|
|
|
+ return r; // 命中直接返回
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ throw new ServiceException("Target token transfer not found");
|
|
|
+ }
|
|
|
|
|
|
private static TxResult evmTxQuery(String api, String txHash, String expectedContract) throws Exception {
|
|
|
|
|
|
@@ -140,8 +364,6 @@ public class EncryptionHashQueryUtil {
|
|
|
throw new ServiceException("Target token transfer not found");
|
|
|
}
|
|
|
|
|
|
- /* ================= Tron ================= */
|
|
|
-
|
|
|
private static TxResult tronTxQuery(String txHash, String currency) throws Exception {
|
|
|
|
|
|
String json = get(TRON_API + txHash);
|
|
|
@@ -167,8 +389,8 @@ public class EncryptionHashQueryUtil {
|
|
|
|
|
|
TxResult r = new TxResult();
|
|
|
r.setTxHash(txHash);
|
|
|
- r.setFrom(t.getString("from"));
|
|
|
- r.setTo(t.getString("to"));
|
|
|
+ r.setFrom(t.getString("from_address"));
|
|
|
+ r.setTo(t.getString("to_address"));
|
|
|
r.setAmount(amount);
|
|
|
r.setContractAddress(contract);
|
|
|
r.setTimestamp(obj.getLongValue("timestamp") / 1000);
|
|
|
@@ -177,7 +399,7 @@ public class EncryptionHashQueryUtil {
|
|
|
return r;
|
|
|
}
|
|
|
|
|
|
- /* ================= Solana ================= */
|
|
|
+ /* ================= Tron ================= */
|
|
|
|
|
|
private static TxResult solTxQuery(String txHash, String currency) throws Exception {
|
|
|
|
|
|
@@ -230,7 +452,7 @@ public class EncryptionHashQueryUtil {
|
|
|
throw new ServiceException("No SPL transfer");
|
|
|
}
|
|
|
|
|
|
- /* ================= 工具 ================= */
|
|
|
+ /* ================= Solana ================= */
|
|
|
|
|
|
private static long parseTimestamp(String ts) {
|
|
|
try {
|
|
|
@@ -240,6 +462,8 @@ public class EncryptionHashQueryUtil {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /* ================= 工具 ================= */
|
|
|
+
|
|
|
private static String ethContract(String c) {
|
|
|
return c.equalsIgnoreCase("usdc") ? ETH_USDC : ETH_USDT;
|
|
|
}
|
|
|
@@ -259,7 +483,9 @@ public class EncryptionHashQueryUtil {
|
|
|
HttpURLConnection conn = (HttpURLConnection) new URL(SOL_API).openConnection();
|
|
|
conn.setRequestMethod("POST");
|
|
|
conn.setDoOutput(true);
|
|
|
- conn.getOutputStream().write(body.toJSONString().getBytes(StandardCharsets.UTF_8));
|
|
|
+ try (java.io.OutputStream os = conn.getOutputStream()) {
|
|
|
+ os.write(body.toJSONString().getBytes(StandardCharsets.UTF_8));
|
|
|
+ }
|
|
|
|
|
|
return read(conn);
|
|
|
}
|
|
|
@@ -292,4 +518,68 @@ public class EncryptionHashQueryUtil {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ private static long getBlockTimestamp(String blockHex, String chainId) throws Exception {
|
|
|
+
|
|
|
+ String url = "https://api.etherscan.io/v2/api"
|
|
|
+ + "?chainid=%s"
|
|
|
+ + "&module=proxy"
|
|
|
+ + "&action=eth_getBlockByNumber"
|
|
|
+ + "&tag=%s"
|
|
|
+ + "&boolean=false"
|
|
|
+ + "&apikey=%s";
|
|
|
+
|
|
|
+ String json = get(String.format(url, chainId, blockHex, KEY));
|
|
|
+
|
|
|
+ JSONObject root = JSON.parseObject(json);
|
|
|
+ JSONObject result = root.getJSONObject("result");
|
|
|
+
|
|
|
+ String tsHex = result.getString("timestamp");
|
|
|
+
|
|
|
+ return Long.parseLong(tsHex.substring(2), 16);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static long getBscBlockTimestamp(String blockHex) throws Exception {
|
|
|
+
|
|
|
+ String json = evmRpc("eth_getBlockByNumber", new Object[]{blockHex, false}, BSC_RPC);
|
|
|
+
|
|
|
+ JSONObject root = JSON.parseObject(json);
|
|
|
+ JSONObject result = root.getJSONObject("result");
|
|
|
+
|
|
|
+ if (result == null) {
|
|
|
+ throw new ServiceException("Invalid BSC block response");
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ String tsHex = result.getString("timestamp");
|
|
|
+
|
|
|
+ return Long.parseLong(tsHex.substring(2), 16);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String evmRpc(String method, Object[] params, String rpcUrl) throws Exception {
|
|
|
+
|
|
|
+ JSONObject body = new JSONObject();
|
|
|
+ body.put("jsonrpc", "2.0");
|
|
|
+ body.put("id", 1);
|
|
|
+ body.put("method", method);
|
|
|
+ body.put("params", params);
|
|
|
+
|
|
|
+ HttpURLConnection conn = (HttpURLConnection) new URL(rpcUrl).openConnection();
|
|
|
+ conn.setRequestMethod("POST");
|
|
|
+ conn.setDoOutput(true);
|
|
|
+ conn.setRequestProperty("Content-Type", "application/json");
|
|
|
+ conn.setRequestProperty("User-Agent", "Mozilla/5.0");
|
|
|
+
|
|
|
+ try (java.io.OutputStream os = conn.getOutputStream()) {
|
|
|
+
|
|
|
+ os.write(body.toJSONString().getBytes(StandardCharsets.UTF_8));
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ return read(conn);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String bnbContract(String c) {
|
|
|
+ return c.equalsIgnoreCase("usdc") ? BNB_USDC : BNB_USDT;
|
|
|
+ }
|
|
|
}
|