|
@@ -30,6 +30,7 @@ class TronHelper
|
|
|
const CONFIRMED_NUMBER = 3; // 确认数
|
|
const CONFIRMED_NUMBER = 3; // 确认数
|
|
|
const GLOBAL_TRON_RATE_LIMIT_KEY = 'tron_request_global_rate_limit';
|
|
const GLOBAL_TRON_RATE_LIMIT_KEY = 'tron_request_global_rate_limit';
|
|
|
const GLOBAL_TRON_RATE_LIMIT_SECONDS = 1;
|
|
const GLOBAL_TRON_RATE_LIMIT_SECONDS = 1;
|
|
|
|
|
+ const TRC20_TRANSFER_TOPIC = 'ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
|
|
|
|
|
|
|
|
protected static $client;
|
|
protected static $client;
|
|
|
|
|
|
|
@@ -490,14 +491,16 @@ class TronHelper
|
|
|
|
|
|
|
|
$usdtContract = self::getContractAddress('USDT');
|
|
$usdtContract = self::getContractAddress('USDT');
|
|
|
|
|
|
|
|
- $url = "/v1/accounts/{$address}/transactions/trc20?limit={$limit}";
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
try {
|
|
try {
|
|
|
- $response = self::$client->get($url, [
|
|
|
|
|
|
|
+ $response = self::$client->get("/v1/accounts/{$address}/transactions/trc20", [
|
|
|
'headers' => [
|
|
'headers' => [
|
|
|
'Accept' => 'application/json',
|
|
'Accept' => 'application/json',
|
|
|
],
|
|
],
|
|
|
|
|
+ 'query' => [
|
|
|
|
|
+ 'limit' => $limit,
|
|
|
|
|
+ 'only_confirmed' => 'true',
|
|
|
|
|
+ 'order_by' => 'block_timestamp,desc',
|
|
|
|
|
+ ],
|
|
|
'timeout' => 10,
|
|
'timeout' => 10,
|
|
|
]);
|
|
]);
|
|
|
$data = json_decode($response->getBody()->getContents(), true);
|
|
$data = json_decode($response->getBody()->getContents(), true);
|
|
@@ -510,17 +513,30 @@ class TronHelper
|
|
|
continue; // 不是USDT
|
|
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;
|
|
return $recharges;
|
|
|
} catch (\Throwable $e) {
|
|
} 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账户余额
|
|
* @description: 获取TRC20账户余额
|