ソースを参照

同步交易hash工具类

gao 17 時間 前
コミット
3ce0ce0f87

+ 305 - 15
crm-model/src/main/java/com/crm/rely/backend/model/util/EncryptionHashQueryUtil.java

@@ -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;
+    }
 }

+ 7 - 7
crm-settlement/src/main/java/com/crm/settlement/service/impl/VaultodyServiceImpl.java

@@ -128,12 +128,12 @@ public class VaultodyServiceImpl extends BaseSettlementServiceImpl implements Va
             log.error("Vaultody通知回调:address为空,{}", content);
             return BaseResultDto.error();
         }
-//        String direction = itemEntity.getDirection();
+        String direction = itemEntity.getDirection();
         // 只处理入账
-//        if (!"INCOMING_CONFIRMED_TOKEN_TX".equals(event) && !"incoming".equals(direction)) {
-//            log.error("Vaultody通知回调:不是入账类型,{}", content);
-//            return BaseResultDto.error();
-//        }
+        if (!"INCOMING_CONFIRMED_TOKEN_TX".equals(event) && !"incoming".equals(direction)) {
+            log.error("Vaultody通知回调:不是入账类型,{}", content);
+            return BaseResultDto.error();
+        }
 
         if (StringUtils.isEmpty(event)) {
             log.error("Vaultody通知回调:event为空,暂不处理,{}", JSON.toJSONString(entity));
@@ -162,7 +162,7 @@ public class VaultodyServiceImpl extends BaseSettlementServiceImpl implements Va
         try {
 
             txResult = EncryptionHashQueryUtil.query(itemEntity.getTransactionId(), itemEntity.getBlockchain(),
-                    currency);
+                    currency, address);
         } catch (Exception e) {
             log.error("Vaultody通知回调:查询交易hash结果失败,{}", JSON.toJSONString(entity));
             return BaseResultDto.error();
@@ -282,7 +282,7 @@ public class VaultodyServiceImpl extends BaseSettlementServiceImpl implements Va
 
         TxResult txResult;
         try {
-            txResult = EncryptionHashQueryUtil.query(transactionId, itemEntity.getBlockchain(), currency);
+            txResult = EncryptionHashQueryUtil.query(transactionId, itemEntity.getBlockchain(), currency, address);
         } catch (Exception e) {
             log.error("vaultody withdraw callback:查询交易hash结果失败,{}", content);
             return BaseResultDto.error();