doge vor 2 Tagen
Ursprung
Commit
5e8861e3a5
3 geänderte Dateien mit 318 neuen und 21 gelöschten Zeilen
  1. 211 13
      app/Helpers/TronHelper.php
  2. 54 8
      app/Services/RechargeService.php
  3. 53 0
      tests/Unit/TronHelperUsdtTransferTest.php

+ 211 - 13
app/Helpers/TronHelper.php

@@ -30,6 +30,7 @@ class TronHelper
     const CONFIRMED_NUMBER = 3; // 确认数
     const GLOBAL_TRON_RATE_LIMIT_KEY = 'tron_request_global_rate_limit';
     const GLOBAL_TRON_RATE_LIMIT_SECONDS = 1;
+    const TRC20_TRANSFER_TOPIC = 'ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
 
     protected static $client;
 
@@ -490,14 +491,16 @@ class TronHelper
 
         $usdtContract = self::getContractAddress('USDT');
         
-        $url = "/v1/accounts/{$address}/transactions/trc20?limit={$limit}";
-
-        
         try {
-            $response = self::$client->get($url, [
+            $response = self::$client->get("/v1/accounts/{$address}/transactions/trc20", [
                 'headers' => [
                     'Accept' => 'application/json',
                 ],
+                'query' => [
+                    'limit' => $limit,
+                    'only_confirmed' => 'true',
+                    'order_by' => 'block_timestamp,desc',
+                ],
                 'timeout' => 10,
             ]);
             $data = json_decode($response->getBody()->getContents(), true);
@@ -510,17 +513,30 @@ class TronHelper
                     continue; // 不是USDT
                 }
 
-                if($item['to'] == $address){
-                    $recharges[] = [
-                        'txid'         => $item['transaction_id'],
-                        'from_address'         => $item['from'],
-                        'to_address'           => $item['to'],
-                        'amount'       => bcdiv($item['value'], bcpow('10', $item['token_info']['decimals']), 6),
-                        'coin'       => $item['token_info']['symbol'],
-                        'block_time'    => intval($item['block_timestamp'] / 1000),
-                    ];
+                $txid = $item['transaction_id'] ?? '';
+                if (empty($txid)) {
+                    continue;
+                }
+
+                $transfer = self::getUsdtTransferByTxid($txid, $address);
+                if (empty($transfer['success'])) {
+                    Log::warning('忽略非USDT Transfer充值记录', [
+                        'address' => $address,
+                        'txid' => $txid,
+                        'message' => $transfer['message'] ?? '',
+                    ]);
+                    continue;
                 }
 
+                $recharges[] = [
+                    'txid'         => $txid,
+                    'from_address' => $transfer['from_address'],
+                    'to_address'   => $transfer['to_address'],
+                    'amount'       => $transfer['amount'],
+                    'coin'         => 'USDT',
+                    'block_time'   => $transfer['block_time'],
+                    'block_height' => $transfer['block_number'],
+                ];
             }
             return $recharges;
         } catch (\Throwable $e) {
@@ -532,6 +548,188 @@ class TronHelper
         }
     }
 
+    /**
+     * 查询并校验单笔交易是否为打到指定地址的 USDT Transfer 事件。
+     */
+    public static function getUsdtTransferByTxid(string $txid, ?string $expectedToAddress = null): array
+    {
+        self::throttleTronRequest();
+
+        self::init();
+
+        try {
+            $txResp = self::$client->post('/wallet/gettransactioninfobyid', [
+                'json' => ['value' => $txid],
+                'headers' => ['Accept' => 'application/json'],
+            ]);
+            $txData = json_decode($txResp->getBody()->getContents(), true);
+
+            $result = self::parseUsdtTransferEventFromTransactionInfo($txData, $expectedToAddress);
+            if (empty($result['success'])) {
+                return $result;
+            }
+
+            $result['txid'] = $txid;
+            return $result;
+        } catch (\Throwable $e) {
+            return ['success' => false, 'message' => $e->getMessage()];
+        }
+    }
+
+    /**
+     * 查询确认数,同时强制校验交易必须是 USDT Transfer。
+     */
+    public static function getUsdtTransferConfirmation(string $txid, ?string $expectedToAddress = null): array
+    {
+        $transfer = self::getUsdtTransferByTxid($txid, $expectedToAddress);
+        if (empty($transfer['success'])) {
+            return $transfer;
+        }
+
+        try {
+            $blockResp = self::$client->get('/wallet/getnowblock', [
+                'headers' => ['Accept' => 'application/json'],
+            ]);
+            $blockData = json_decode($blockResp->getBody()->getContents(), true);
+            $latestBlock = $blockData['block_header']['raw_data']['number'] ?? 0;
+
+            if ($latestBlock == 0) {
+                return ['success' => false, 'message' => '无法获取最新区块'];
+            }
+
+            $confirmations = $latestBlock - $transfer['block_number'] + 1;
+
+            return $transfer + [
+                'latest_block' => $latestBlock,
+                'confirmations' => $confirmations,
+            ];
+        } catch (\Throwable $e) {
+            return ['success' => false, 'message' => $e->getMessage()];
+        }
+    }
+
+    /**
+     * 从 gettransactioninfobyid 返回值解析 USDT Transfer 事件。
+     */
+    public static function parseUsdtTransferEventFromTransactionInfo(array $txData, ?string $expectedToAddress = null, ?string $usdtContractAddress = null): array
+    {
+        if (empty($txData['blockNumber'])) {
+            return ['success' => false, 'message' => '交易尚未被打包或无效'];
+        }
+
+        if (($txData['receipt']['result'] ?? 'SUCCESS') !== 'SUCCESS') {
+            return ['success' => false, 'invalid' => true, 'message' => '交易执行失败'];
+        }
+
+        $usdtContractAddress = $usdtContractAddress ?: self::getContractAddress('USDT');
+        $usdtLogAddress = self::normalizeTronLogAddress($usdtContractAddress);
+
+        foreach ($txData['log'] ?? [] as $log) {
+            $logAddress = self::normalizeTronLogAddress($log['address'] ?? '');
+            if ($logAddress !== $usdtLogAddress) {
+                continue;
+            }
+
+            $topics = $log['topics'] ?? [];
+            if (count($topics) < 3 || self::stripHexPrefix($topics[0]) !== self::TRC20_TRANSFER_TOPIC) {
+                continue;
+            }
+
+            $fromAddress = self::tronAddressFromEventTopic($topics[1]);
+            $toAddress = self::tronAddressFromEventTopic($topics[2]);
+            if (!$fromAddress || !$toAddress) {
+                continue;
+            }
+
+            if ($expectedToAddress && !self::sameTronAddress($toAddress, $expectedToAddress)) {
+                continue;
+            }
+
+            $amountRaw = self::hexToDecimalString($log['data'] ?? '0');
+            if (bccomp($amountRaw, '0', 0) <= 0) {
+                continue;
+            }
+
+            return [
+                'success' => true,
+                'from_address' => $fromAddress,
+                'to_address' => $toAddress,
+                'amount' => bcdiv($amountRaw, '1000000', 6),
+                'amount_raw' => $amountRaw,
+                'block_number' => $txData['blockNumber'],
+                'block_time' => isset($txData['blockTimeStamp']) ? intval($txData['blockTimeStamp'] / 1000) : null,
+            ];
+        }
+
+        return ['success' => false, 'invalid' => true, 'message' => '未找到匹配的USDT Transfer事件'];
+    }
+
+    private static function normalizeTronLogAddress(string $address): string
+    {
+        $address = trim($address);
+        if (strlen($address) === 34 && substr($address, 0, 1) === 'T') {
+            try {
+                $address = self::base58check2HexString($address);
+            } catch (\Throwable $e) {
+                return strtolower($address);
+            }
+        }
+
+        $address = self::stripHexPrefix($address);
+        if (strlen($address) === 42 && substr($address, 0, 2) === '41') {
+            return substr($address, 2);
+        }
+
+        return $address;
+    }
+
+    private static function tronAddressFromEventTopic(string $topic): ?string
+    {
+        $topic = self::stripHexPrefix($topic);
+        if (strlen($topic) < 40 || !ctype_xdigit($topic)) {
+            return null;
+        }
+
+        try {
+            return self::hexString2Base58check('41' . substr($topic, -40));
+        } catch (\Throwable $e) {
+            return null;
+        }
+    }
+
+    private static function sameTronAddress(string $left, string $right): bool
+    {
+        return trim($left) === trim($right);
+    }
+
+    private static function hexToDecimalString(string $hex): string
+    {
+        $hex = self::stripHexPrefix($hex);
+        if ($hex === '') {
+            return '0';
+        }
+        if (!ctype_xdigit($hex)) {
+            return '0';
+        }
+
+        $decimal = '0';
+        foreach (str_split($hex) as $char) {
+            $decimal = bcadd(bcmul($decimal, '16', 0), (string) hexdec($char), 0);
+        }
+
+        return $decimal;
+    }
+
+    private static function stripHexPrefix(string $hex): string
+    {
+        $hex = strtolower(trim($hex));
+        if (substr($hex, 0, 2) === '0x') {
+            return substr($hex, 2);
+        }
+
+        return $hex;
+    }
+
 
     /**
      * @description: 获取TRC20账户余额

+ 54 - 8
app/Services/RechargeService.php

@@ -445,18 +445,42 @@ class RechargeService extends BaseService
     public static function handleRechargeConfirmation($txid)
     {
         $info = self::findOne(['txid' => $txid]); // 获取充值的信息
+        if (empty($info)) {
+            return;
+        }
 
         // 汇率
         $rate = Config::where('field', 'exchange_rate_rmb')->first()->val ?? 1;
 
         // 待处理进行充值
         if ($info['status'] == static::$MODEL::STATUS_STAY) {
-            $result = TronHelper::getTransactionConfirmations($txid);
+            $walletInfo = WalletService::findOne(['member_id' => $info->member_id, 'coin' => 'USDT']);
+            $expectedToAddress = $walletInfo->address ?? '';
+            if (empty($expectedToAddress) || $info->to_address !== $expectedToAddress) {
+                self::ignoreInvalidAutoRecharge($info, '充值地址不是该会员系统分配的USDT地址', [
+                    'expected_to_address' => $expectedToAddress,
+                    'record_to_address' => $info->to_address,
+                ]);
+                return;
+            }
+
+            $result = TronHelper::getUsdtTransferConfirmation($txid, $expectedToAddress);
+
+            if (empty($result['success'])) {
+                if (!empty($result['invalid'])) {
+                    self::ignoreInvalidAutoRecharge($info, $result['message'] ?? '无效USDT充值交易');
+                }
+                return;
+            }
 
-            if ($result['success']) {
+            if (!empty($result['success'])) {
                 $data = [];
                 $data['block_height'] = $result['block_number'];
-                $data['confirmations'] = $result['latest_block'] - $result['block_number'];
+                $data['confirmations'] = $result['confirmations'];
+                $data['from_address'] = $result['from_address'];
+                $data['to_address'] = $result['to_address'];
+                $data['amount'] = $result['amount'];
+                $data['block_time'] = $result['block_time'];
                 $data['exchange_rate'] = $rate;
 
                 if ($data['confirmations'] >= TronHelper::CONFIRMED_NUMBER) {
@@ -464,7 +488,7 @@ class RechargeService extends BaseService
                     $where = self::getWhere(['txid' => $txid, 'status' => static::$MODEL::STATUS_STAY]);
                     $recharge = static::$MODEL::where($where)->update($data);
 
-                    $amount = floatval($info->amount);
+                    $amount = floatval($result['amount']);
                     $rate_amount = bcmul($amount, $rate, 10); // 汇率转换后分数
 
                     // 更新成功,变动可用余额
@@ -472,23 +496,45 @@ class RechargeService extends BaseService
                         $balanceData = WalletService::updateBalance($info->member_id, $rate_amount);
                         BalanceLogService::addLog($info->member_id, $rate_amount, $balanceData['before_balance'], $balanceData['after_balance'], '充值', $info->id, '');
 
-                        CollectService::createCollect($info->to_address, $info->coin, $info->net);
+                        CollectService::createCollect($result['to_address'], $info->coin, $info->net);
 
-                        $amount = floatval($info->amount);
                         $text = "充值结果通知\n";
                         $text .= "充值数量:{$amount} USDT\n";
-                        $text .= "充值地址:{$info->to_address}\n";
+                        $text .= "充值地址:{$result['to_address']}\n";
                         $text .= "汇率:1 USDT = {$rate} RMB\n";
                         $text .= "折合金额:" . number_format($rate_amount, 2) . " RMB\n";
                         $text .= "状态:成功\n";
                         TopUpService::notifyTransferSuccess($info->member_id, $text);
                     }
+                } else {
+                    static::$MODEL::where(self::getWhere(['txid' => $txid, 'status' => static::$MODEL::STATUS_STAY]))->update($data);
                 }
             }
         }
 
     }
 
+    private static function ignoreInvalidAutoRecharge(Recharge $info, string $reason, array $context = []): void
+    {
+        if (intval($info->type) !== static::$MODEL::TYPE_AUTO || intval($info->status) !== static::$MODEL::STATUS_STAY) {
+            return;
+        }
+
+        static::$MODEL::where(self::getWhere([
+            'id' => $info->id,
+            'status' => static::$MODEL::STATUS_STAY,
+        ]))->update([
+            'status' => static::$MODEL::STATUS_IGNORE,
+        ]);
+
+        Log::warning('忽略无效USDT自动充值记录', array_merge([
+            'id' => $info->id,
+            'member_id' => $info->member_id,
+            'txid' => $info->txid,
+            'reason' => $reason,
+        ], $context));
+    }
+
     /**
      * @description: 同步待处理的充值记录
      * @return {*}
@@ -500,4 +546,4 @@ class RechargeService extends BaseService
             self::handleRechargeConfirmation($v->txid);
         }
     }
-}
+}

+ 53 - 0
tests/Unit/TronHelperUsdtTransferTest.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace Tests\Unit;
+
+use App\Helpers\TronHelper;
+use PHPUnit\Framework\TestCase;
+
+class TronHelperUsdtTransferTest extends TestCase
+{
+    public function test_approval_event_is_not_a_usdt_recharge(): void
+    {
+        $result = TronHelper::parseUsdtTransferEventFromTransactionInfo([
+            'blockNumber' => 83562014,
+            'blockTimeStamp' => 1781355336000,
+            'receipt' => ['result' => 'SUCCESS'],
+            'log' => [[
+                'address' => 'a614f803b6fd780986a42c78ec9c7f77e6ded13c',
+                'topics' => [
+                    '8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925',
+                    '000000000000000000000000333acb6d5137263acb0f5a25f34dd45bd87cca39',
+                    '000000000000000000000000b0c9500c261fa39a70de3b350a9f71fa0d46c474',
+                ],
+                'data' => '0000000000000000000000000000000000000000000000000000000005f5e100',
+            ]],
+        ], 'TS5y6GiRJdkHD8kodi1nZynqy5ojzjuypq', 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t');
+
+        $this->assertFalse($result['success']);
+        $this->assertTrue($result['invalid']);
+    }
+
+    public function test_transfer_event_to_expected_address_is_a_usdt_recharge(): void
+    {
+        $result = TronHelper::parseUsdtTransferEventFromTransactionInfo([
+            'blockNumber' => 83562014,
+            'blockTimeStamp' => 1781355336000,
+            'receipt' => ['result' => 'SUCCESS'],
+            'log' => [[
+                'address' => 'a614f803b6fd780986a42c78ec9c7f77e6ded13c',
+                'topics' => [
+                    TronHelper::TRC20_TRANSFER_TOPIC,
+                    '000000000000000000000000333acb6d5137263acb0f5a25f34dd45bd87cca39',
+                    '000000000000000000000000b0c9500c261fa39a70de3b350a9f71fa0d46c474',
+                ],
+                'data' => '0000000000000000000000000000000000000000000000000000000005f5e100',
+            ]],
+        ], 'TS5y6GiRJdkHD8kodi1nZynqy5ojzjuypq', 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t');
+
+        $this->assertTrue($result['success']);
+        $this->assertSame('TEe5txwddmGjsgP5EfBNG8M3qLM4JR3x7Z', $result['from_address']);
+        $this->assertSame('TS5y6GiRJdkHD8kodi1nZynqy5ojzjuypq', $result['to_address']);
+        $this->assertSame('100.000000', $result['amount']);
+    }
+}