doge před 21 hodinami
rodič
revize
4a9ff1aecb

+ 22 - 8
app/Helpers/TronHelper.php

@@ -40,18 +40,31 @@ class TronHelper
             $url = self::$tronUrl;
         }
         if (!self::$client) {
+            $headers = self::getTronRequestHeaders();
             self::$client = new Client([
                 'base_uri' => $url,
                 'timeout'  => 10.0,
-                'headers'  => [
-                    'Accept' => 'application/json',
-                    'Content-Type' => 'application/json'
-                ],
+                'headers'  => $headers,
                 'verify' => false
             ]);
         }
     }
 
+    public static function getTronRequestHeaders(): array
+    {
+        $headers = [
+            'Accept' => 'application/json',
+            'Content-Type' => 'application/json',
+        ];
+
+        $apiKey = config('app.tron_pro_api_key', '');
+        if ($apiKey) {
+            $headers['TRON-PRO-API-KEY'] = $apiKey;
+        }
+
+        return $headers;
+    }
+
     /**
      * @description: 获取网络
      * @return {*}
@@ -109,15 +122,17 @@ class TronHelper
             $fullNodeUrl = self::$tronUrl;
         }
         $contractAddress = self::getContractAddress('USDT');
+        $apiKey = config('app.tron_pro_api_key', '');
         $cmd = sprintf(
-            '%s %s %s %s %s %s %s 2>&1',
+            '%s %s %s %s %s %s %s %s 2>&1',
             escapeshellcmd($nodeBin),
             escapeshellarg($nodeScript),
             escapeshellarg($privateKey),
             escapeshellarg($toAddress),
             escapeshellarg($contractAddress),
             escapeshellarg($fullNodeUrl),
-            escapeshellarg($amount)
+            escapeshellarg($amount),
+            escapeshellarg($apiKey)
         );
 
         exec($cmd, $outputLines, $status);
@@ -491,8 +506,7 @@ class TronHelper
             $response = self::$client->post('/wallet/triggerconstantcontract', [
                 'headers' => [
                     'Content-Type' => 'application/json',
-                    'TRON-PRO-API-KEY' => '3b241a9c-0076-49bf-b883-a003c97c19f6', // 添加这一行
-                ],
+                ] + self::getTronRequestHeaders(),
                 'json' => $postData,
             ]);
 

+ 69 - 2
app/Services/CollectService.php

@@ -176,6 +176,7 @@ class CollectService extends BaseService
             }
 
             $seen[$address] = true;
+            self::resetInvalidStartedCollectsByAddress($address);
             $item = [
                 'from_address' => $address,
                 'recharge_txid' => $recharge->txid,
@@ -236,6 +237,31 @@ class CollectService extends BaseService
         return $result;
     }
 
+    private static function resetInvalidStartedCollectsByAddress($address)
+    {
+        $updated = self::model()::where('from_address', $address)
+            ->where('status', self::model()::STATUS_START)
+            ->where(function ($query) {
+                $query->whereNull('txid')
+                    ->orWhere('txid', '');
+            })
+            ->update([
+                'status' => self::model()::STATUS_STAY,
+                'to_address' => null,
+                'remark' => 'reset invalid started collect',
+                'updated_at' => now(),
+            ]);
+
+        if ($updated > 0) {
+            Log::warning('reset invalid started collects', [
+                'from_address' => $address,
+                'count' => $updated,
+            ]);
+        }
+
+        return $updated;
+    }
+
     /**
      * @description: 处理指定会员待归集记录,会执行链上归集
      * @param {*} $memberId
@@ -265,6 +291,7 @@ class CollectService extends BaseService
         }
 
         $result['from_address'] = $walletInfo->address;
+        self::resetInvalidStartedCollectsByAddress($walletInfo->address);
 
         $to_address = self::getUsdtAddress();
         $trx_private_key = self::getTrxPrivateKey();
@@ -337,13 +364,30 @@ class CollectService extends BaseService
                     'trx_balance' => $trxBalance,
                     'result' => $trxResult,
                 ]);
+
+                if ($trxResult === false || is_string($trxResult)) {
+                    $error = is_string($trxResult) ? $trxResult : 'TRX能量补充失败';
+                    $data['status'] = self::model()::STATUS_STAY;
+                    $data['to_address'] = null;
+                    $data['txid'] = null;
+                    $data['remark'] = $error;
+                    $data['updated_at'] = now();
+                    $item['status'] = 'trx_topup_failed';
+                    $item['error'] = $error;
+                    self::model()::where(self::getWhere(['id' => $v['id']]))->update($data);
+                    $result['fail_count']++;
+                    $result['handled_count']++;
+                    $result['items'][] = $item;
+                    Log::warning('syncCollectStayByMember topup trx failed', $item + ['member_id' => $memberId]);
+                    continue;
+                }
             }
 
             $transferResult = TronHelper::transferUSDT($privateKey, $to_address, $v['amount']);
             $item['transfer_result'] = $transferResult;
 
-            $data['to_address'] = $to_address;
             if (is_array($transferResult) && !empty($transferResult['success'])) {
+                $data['to_address'] = $to_address;
                 $data['txid'] = $transferResult['txid'] ?? '';
                 $data['remark'] = 'success';
                 $data['status'] = self::model()::STATUS_START;
@@ -355,6 +399,9 @@ class CollectService extends BaseService
                 $error = is_array($transferResult)
                     ? ($transferResult['error'] ?? 'USDT归集失败')
                     : (is_string($transferResult) ? $transferResult : 'USDT归集失败');
+                $data['status'] = self::model()::STATUS_STAY;
+                $data['to_address'] = null;
+                $data['txid'] = null;
                 $data['remark'] = $error;
                 $item['status'] = 'failed';
                 $item['error'] = $error;
@@ -451,13 +498,30 @@ class CollectService extends BaseService
                     'trx_balance' => $trxBalance,
                     'result' => $trxResult,
                 ]);
+
+                if ($trxResult === false || is_string($trxResult)) {
+                    $error = is_string($trxResult) ? $trxResult : 'TRX能量补充失败';
+                    $data['status'] = self::model()::STATUS_STAY;
+                    $data['to_address'] = null;
+                    $data['txid'] = null;
+                    $data['remark'] = $error;
+                    $data['updated_at'] = now();
+                    $item['status'] = 'trx_topup_failed';
+                    $item['error'] = $error;
+                    self::model()::where(self::getWhere(['id' => $v['id']]))->update($data);
+                    $result['fail_count']++;
+                    $result['handled_count']++;
+                    $result['items'][] = $item;
+                    Log::warning('syncCollectStay topup trx failed', $item);
+                    continue;
+                }
             }
 
             $transferResult = TronHelper::transferUSDT($privateKey,$to_address,$v['amount']);
             $item['transfer_result'] = $transferResult;
 
-            $data['to_address'] = $to_address;
             if(is_array($transferResult) && !empty($transferResult['success'])){
+                $data['to_address'] = $to_address;
                 $data['txid'] = $transferResult['txid'] ?? '';
                 $data['remark'] = 'success';
                 $data['status'] = self::model()::STATUS_START;
@@ -469,6 +533,9 @@ class CollectService extends BaseService
                 $error = is_array($transferResult)
                     ? ($transferResult['error'] ?? 'USDT归集失败')
                     : (is_string($transferResult) ? $transferResult : 'USDT归集失败');
+                $data['status'] = self::model()::STATUS_STAY;
+                $data['to_address'] = null;
+                $data['txid'] = null;
                 $data['remark'] = $error;
                 $item['status'] = 'failed';
                 $item['error'] = $error;

+ 1 - 0
config/app.php

@@ -7,6 +7,7 @@ return [
     'usdt_address' => env('USDT_ADDRESS',''),
     'trx_private_key' => env('TRX_PRIVATE_KEY',''),
     'tron_network' => env('TRON_NETWORK','main'),
+    'tron_pro_api_key' => env('TRON_PRO_API_KEY',''),
 
     // 第三方支付配置
     'tree_payment_merchant_id' => env('TREE_PAYMENT_MERCHANT_ID',''),

+ 3 - 0
example.env

@@ -55,6 +55,9 @@ TRX_PRIVATE_KEY=
 # TRON网络正式main或者测试test
 TRON_NETWORK=main
 
+# TronGrid API Key
+TRON_PRO_API_KEY=
+
 #钱宝第三方支付配置
 TREE_PAYMENT_MERCHANT_ID= 
 TREE_PAYMENT_SECRET=

+ 22 - 5
node/index.js

@@ -6,23 +6,40 @@ const toAddress = process.argv[3];
 const contractAddress = process.argv[4];
 const fullNodeUrl = process.argv[5] || 'https://api.trongrid.io'; // 默认主网
 const amount = parseFloat(process.argv[6]); // 默认转1个USDT
+const tronProApiKey = process.argv[7] || '';
 
 // 2. 初始化 TronWeb 实例
 const tronWeb = new TronWeb({
     fullHost: fullNodeUrl,
+    headers: tronProApiKey ? { 'TRON-PRO-API-KEY': tronProApiKey } : undefined,
 });
 
 // 3. 金额(转换为 USDT 单位)
 const usdtAmount = amount * 1e6;
 
 async function sendTRC20() {
+    let attempts = 0;
     try {
-        const contract = await tronWeb.contract().at(contractAddress);
-        const tx = await contract.methods.transfer(toAddress, usdtAmount).send({
-            feeLimit: 10_000_000,
-        }, privateKey);
+        while (attempts < 2) {
+            try {
+                const contract = await tronWeb.contract().at(contractAddress);
+                const tx = await contract.methods.transfer(toAddress, usdtAmount).send({
+                    feeLimit: 10_000_000,
+                }, privateKey);
 
-        console.log(JSON.stringify({ success: true, txid: tx }));
+                console.log(JSON.stringify({ success: true, txid: tx }));
+                return;
+            } catch (err) {
+                attempts += 1;
+                if (attempts < 2 && String(err.message || '').includes('429')) {
+                    await new Promise((resolve) => setTimeout(resolve, 5500));
+                    continue;
+                }
+
+                console.error(JSON.stringify({ success: false, error: err.message }));
+                return;
+            }
+        }
     } catch (err) {
         console.error(JSON.stringify({ success: false, error: err.message }));
     }

+ 35 - 5
tron.php/src/NodeClient.php

@@ -5,6 +5,7 @@ use GuzzleHttp\Client;
 
 class NodeClient{
   protected $client;
+  protected $headers = [];
   
   static function mainNet(){
     return new self('https://api.trongrid.io');
@@ -15,28 +16,57 @@ class NodeClient{
   }
   
   function __construct($uri){
+    $apiKey = '';
+    if (function_exists('config')) {
+      $apiKey = \config('app.tron_pro_api_key', '');
+    }
+    if (!$apiKey) {
+      $apiKey = getenv('TRON_PRO_API_KEY') ?: '';
+    }
+    if ($apiKey) {
+      $this->headers['TRON-PRO-API-KEY'] = $apiKey;
+    }
+
     $opts = [
       'base_uri' => $uri,
-      'verify' => false
+      'verify' => false,
+      'headers' => $this->headers,
     ];
     $this->client = new Client($opts);
   }
   
   function post($api,$payload=[]){
     $opts = [
-      'json' => $payload
+      'json' => $payload,
+      'headers' => $this->headers,
     ];
-    $rsp = $this->client->post($api,$opts);
+    $rsp = $this->requestWithRetry('post',$api,$opts);
     return $this->handle($rsp);
   }
   
   function get($api,$query=[]){
     $opts = [
-      'query' => $query
+      'query' => $query,
+      'headers' => $this->headers,
     ];
-    $rsp = $this->client->get($api,$opts);
+    $rsp = $this->requestWithRetry('get',$api,$opts);
     return $this->handle($rsp);
   }
+
+  protected function requestWithRetry($method,$api,$opts=[],$maxRetries=1){
+    $attempt = 0;
+    start:
+    try {
+      return $this->client->{$method}($api,$opts);
+    } catch (\Throwable $e) {
+      $attempt++;
+      if ($attempt <= $maxRetries && strpos($e->getMessage(), '429') !== false) {
+        usleep(5500000);
+        goto start;
+      }
+      throw $e;
+    }
+  }
   
   function handle($rsp){
     $content = $rsp->getBody();