BscApi.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. <?php
  2. namespace app\common\logic;
  3. use app\common\model\OfflineRechargeRecordModel;
  4. use app\common\model\OfflineRechargeVerifyModel;
  5. use app\common\model\ParametersModel;
  6. use app\common\model\ServersModel;
  7. use app\common\model\UserModel;
  8. use fast\Http;
  9. use fast\RechargeOrderType;
  10. use fast\RechargeStatus;
  11. use fast\RechargeType;
  12. use think\Cache;
  13. use think\Env;
  14. use think\Log;
  15. /**
  16. * 币安链API
  17. */
  18. class BscApi
  19. {
  20. protected $transactionLimt = 1;//识别到的最小到账金额,低于此值的到账金额不入账
  21. protected $bsc_api_key = '8TP62I7UJF2AY7TAH3TAR4R3IM7N9ZCHP2';
  22. protected $bsc_api_url = 'https://api.etherscan.io/v2/api?chainid=56';//56为币安链主网ID
  23. protected $contract_address = '0x55d398326f99059ff775485246999027b3197955';//交易币种的合约地址,默认为usdt的:0x55d398326f99059ff775485246999027b3197955,测试合约:0xcf3271b1be72f834e4dd35b9d558d583894473f1
  24. /**
  25. * value的放大比例 固定为: 10的18次方
  26. * @var int
  27. */
  28. private string $valueScale = '1000000000000000000';
  29. public function __construct($contract_address = '') {
  30. // 初始化逻辑代码
  31. $key = Env::get('rental.bsc_api_key');
  32. if(!empty($key)){
  33. $this->bsc_api_key = $key;
  34. }
  35. if(empty($contract_address)){
  36. $contract_address = $this->contract_address;
  37. }
  38. $this->contract_address = strtolower($contract_address);//转小写
  39. }
  40. /**
  41. * 根据哈希查看交易详情
  42. *
  43. * 此接口为抓取币安浏览器页面html代码而来
  44. * 访问地址:https://bscscan.com/tx/0x48351c4c215645f2404741343c9be17b0ce494d70a7dba1bf11d1f5c2311de57
  45. */
  46. public function getInfoByTransactionHash($tx_hash = '0x48351c4c215645f2404741343c9be17b0ce494d70a7dba1bf11d1f5c2311de57')
  47. {
  48. if(empty($tx_hash)){
  49. return _error('hash不能为空');
  50. }
  51. // 读取远程页面地址
  52. $url = $this->bsc_api_url . "tx/" . $tx_hash;
  53. dump($url);
  54. // 使用 file_get_contents 函数读取远程页面
  55. //$html = file_get_contents($url);
  56. $html = Http::get($url);
  57. if(empty($html)){
  58. return _error('bscscan.com访问失败');
  59. }
  60. // dump($html);
  61. $dom = new \DOMDocument();
  62. libxml_use_internal_errors(true);//关掉错误提醒
  63. $dom->loadHTML($html);
  64. libxml_clear_errors();
  65. $get_html_value = $dom->getElementById('spanTxHash');
  66. if(empty($get_html_value)){
  67. return _error('当前页面没找到hash');
  68. }
  69. $resp['hash'] = $get_html_value->nodeValue;
  70. //$get_html_value = '<a class="text-break me-2" href="/address/' . $this->contract_address . '">' . $this->contract_address . '</a>';
  71. //$get_html_value = '<a class="me-1" href="/token/' . $this->contract_address . '" target="_parent">BSC-USD</a>';
  72. $get_html_value = '<a data-highlight-value data-highlight-target="' . $this->contract_address . '" class="d-flex align-items-center gap-0.5" href="/token/' . $this->contract_address . '">';
  73. if (!(strstr($html, $get_html_value))) {
  74. Log::error('合约地址和配置信息不相符:' . $tx_hash);
  75. return _error('合约地址和配置信息不相符');
  76. }
  77. $xpath = new \DOMXPath($dom);
  78. $get_html_value = $xpath->query('//span[@class="badge bg-success bg-opacity-10 border border-success border-opacity-25 text-green-600 fw-medium text-start text-wrap py-1.5 px-2"]');
  79. if ($get_html_value->length > 0 && $get_html_value->item(0)->nodeValue == 'Success') {
  80. $resp['pay_status'] = 1;
  81. } else {
  82. return _error('交易失败');
  83. }
  84. $get_html_value = $xpath->query('//span[@class="me-1"]');
  85. if ($get_html_value->length > 0) {
  86. $amount = $get_html_value->item(1)->nodeValue;//所有 span class='me-1' 的元素中,第二个就是额度,超过1000会有 逗号分割
  87. $resp['amount'] = str_replace(',', "", $amount);
  88. } else {
  89. return _error('当前页面没找到交易额度');
  90. }
  91. $get_html_value = $dom->getElementById('chunk_decodeori_0_1');
  92. if(empty($get_html_value)){
  93. return _error('当前页面没找到from_address');
  94. }
  95. $resp['from_address'] = strtolower($get_html_value->nodeValue);//转小写
  96. $get_html_value = $dom->getElementById('chunk_decodeori_0_2');
  97. if(empty($get_html_value)){
  98. return _error('当前页面没找到to_address');
  99. }
  100. $resp['to_address'] = strtolower($get_html_value->nodeValue);
  101. return _success($resp);
  102. }
  103. /**
  104. * 根据钱包地址查询交易记录
  105. *
  106. * 通过接口拿到数据,便利交易数据,返回可用格式
  107. *
  108. * 此接口为抓取币安交易所API
  109. * 使用前需要在交易所申请 bsc_api_key
  110. * 数据请求地址:https://api.bscscan.com/api?module=account&action=tokentx&contractaddress=0x55d398326f99059ff775485246999027b3197955&page=1&offset=10000&endblock=99999999&apikey=VTCKIP346DCRWB6JNS4KDANUJJEQN9VAKW&address=0xca3dfc6d52131bd0df91a6302e48df5f50288256&startblock=33138777
  111. * 请求地址参数:https://api.bscscan.com/api?
  112. * module=account //固定值
  113. * &action=tokentx //固定值
  114. * &contractaddress= 合约地址
  115. * &page=1 //页数
  116. * &offset=10000 //每页条数
  117. * &startblock=33138777 //起始区块
  118. * &endblock=99999999 //结束区块
  119. * &apikey=币安交易所apikey
  120. * &address=0xca3dfc6d52131bd0df91a6302e48df5f50288256 //查询钱包地址
  121. *
  122. * 数据返回格式
  123. * {
  124. * "status":"1",
  125. * "message":"OK",
  126. * "result":[
  127. * {
  128. * "blockNumber":"33138803",
  129. * "timeStamp":"1698920688",
  130. * "hash":"0x0e1d85533edb27e8ee246721145eb15c439d943315608e837cd0dbff9b80d7a2",
  131. * "nonce":"9",
  132. * "blockHash":"0x986e923d94cb9390f7de8054ba9db3bdd0abf37a6c52ca7b28f3f79dd87b33a8",
  133. * "from":"0x86daae6c4dac3cd8a56191af7ea1d13546538dbe",
  134. * "contractAddress":"0x55d398326f99059ff775485246999027b3197955",
  135. * "to":"0xca3dfc6d52131bd0df91a6302e48df5f50288256",
  136. * "value":"400000000000000000000",
  137. * "tokenName":"Binance-Peg BSC-USD",
  138. * "tokenSymbol":"BSC-USD",
  139. * "tokenDecimal":"18",
  140. * "transactionIndex":"17",
  141. * "gas":"52224",
  142. * "gasPrice":"5000000000",
  143. * "gasUsed":"47424",
  144. * "cumulativeGasUsed":"1297987",
  145. * "input":"deprecated",
  146. * "confirmations":"3883"
  147. * },
  148. * {…………},
  149. * {…………},
  150. * ]
  151. * }
  152. * @$address 查询地址
  153. * @$from_to 是查询付款交易(from),还剩收款交易(to),默认from
  154. */
  155. public function getTransactionRecordsByAddress(string $from, string $to, int $start_block, int $end_block = 99999999)
  156. {
  157. if(empty($from) && empty($to)){
  158. return _error('钱包地址不能都为空');
  159. }
  160. $from = strtolower($from);
  161. $to = strtolower($to);
  162. $address = $from;
  163. if(empty($from)){
  164. $address =$to;
  165. }
  166. // 读取远程页面地址
  167. $url = $this->bsc_api_url . "&module=account&action=tokentx&contractaddress=" . $this->contract_address;
  168. $url .= "&page=1&offset=10000&endblock=" . $end_block ."&apikey=" . $this->bsc_api_key;
  169. $url .= "&startblock=" . $start_block . "&address=" . $address;
  170. Log::log($url);
  171. $body = Http::get($url);
  172. if (empty($body)) {
  173. Log::log('api返回内容为空');
  174. return _error('api返回内容为空');
  175. }
  176. // 转成数组
  177. $rsArr = json_decode($body, true);
  178. if (empty($rsArr) || !is_array($rsArr)) {
  179. Log::log('api返回数据异常,json decode失败');
  180. return _error('api返回数据异常');
  181. }
  182. if ($rsArr['status'] != '1') {
  183. if($rsArr['message'] == 'No transactions found'){
  184. Log::log('无交易记录:' . $rsArr['message']);
  185. return _success();
  186. }
  187. Log::log('api返回status不为1,错误信息:' . $rsArr['message']);
  188. return _error('api返回status不为1,错误信息:' . $rsArr['message']);
  189. }
  190. $success_arr = [];
  191. // 查询匹配交易列表
  192. foreach ($rsArr['result'] as $v) {
  193. if (strtolower($v['contractAddress']) != $this->contract_address) {
  194. continue;//合约地址不相符,不处理。一般币种为USDT的合约地址:0x55d398326f99059ff775485246999027b3197955
  195. }
  196. $amount = bcdiv($v['value'], $this->valueScale, 6);//交易金额
  197. if(!($amount > 0)){
  198. continue;//交易金额必须大于0
  199. }
  200. $info = [
  201. 'hash' => $v['hash'],
  202. 'from' => strtolower($v['from']),//转小写
  203. 'to' => strtolower($v['to']),
  204. 'contract' => $v['contractAddress'],
  205. 'amount' => $amount,
  206. 'block_number' => $v['blockNumber'],
  207. 'time' => $v['timeStamp'],
  208. ];
  209. if(!empty($from) && !empty($to)){
  210. if($info['from'] == $from && $info['to'] == $to){
  211. //判断交易是否成功
  212. $rsData = $this->getHashStatus($info['hash']);
  213. if($rsData['code'] == 0){//当前哈希交易状态为失败,跳出循环,等待下次判断,若真是失败交易,过几分钟后,这个数据不会再出现,这个接口拿到的都是有效的交易数据
  214. continue;//当前交易失败
  215. }
  216. Log::log('成功1单');
  217. $success_arr[] = $info;
  218. }
  219. }elseif (!empty($from)){
  220. if($info['from'] == $from){
  221. //判断交易是否成功
  222. $rsData = $this->getHashStatus($info['hash']);
  223. if($rsData['code'] == 0){
  224. continue;//当前交易失败
  225. }
  226. $success_arr[] = $info;
  227. }
  228. }elseif (!empty($to)){
  229. if($info['to'] == $to){
  230. //判断交易是否成功
  231. $rsData = $this->getHashStatus($info['hash']);
  232. if($rsData['code'] == 0){
  233. continue;//当前交易失败
  234. }
  235. $success_arr[] = $info;
  236. }
  237. }
  238. }
  239. return _success($success_arr);
  240. }
  241. /**
  242. * 获取哈希地址成功状态
  243. *
  244. * api接口返回数据格式
  245. * {
  246. "status": "1",
  247. "message": "OK",
  248. "result": {
  249. "status": "1"
  250. }
  251. }
  252. * @param $orderInfo
  253. * @return array|string
  254. */
  255. public function getHashStatus($tx_hash):array
  256. {
  257. if (empty($tx_hash)) {
  258. return _error('hash值不能为空');
  259. }
  260. $url = $this->bsc_api_url . "&module=transaction&action=gettxreceiptstatus";
  261. $url .= "&apikey=" . $this->bsc_api_key;
  262. $url .= "&txhash=" . $tx_hash;
  263. $body = Http::get($url);
  264. if (empty($body)) {
  265. return _error('状态api返回内容为空');
  266. }
  267. // 转成数组
  268. $rsArr = json_decode($body, true);
  269. if (empty($rsArr) || !is_array($rsArr)) {
  270. return _error('状态api返回数据异常,json转换失败');
  271. }
  272. if ($rsArr['status'] != '1') {
  273. return _error('状态api返回status不为1,当前值为:' . $rsArr['status']);
  274. }
  275. if ($rsArr['result']['status'] != 1) {
  276. return _error('状态api返回result中的status不为1,当前值为:' . $rsArr['result']['status']);
  277. }
  278. return _success();
  279. }
  280. /**
  281. * 根据时间戳获取最近的区块高度
  282. *
  283. * api接口返回数据格式
  284. * {
  285. "status": "1",
  286. "message": "OK",
  287. "result": "33164048"
  288. }
  289. * @param $orderInfo
  290. * @return array|string
  291. */
  292. public function getBlockNoByTime($time):array
  293. {
  294. if(!Cache::has('block_' . $time)){
  295. if (empty($time)) {
  296. return _error('时间戳不能为空');
  297. }
  298. //https://api.bscscan.com/api?module=block&action=getblocknobytime&timestamp=1601510400&closest=before&apikey=YourApiKeyToken
  299. $url = $this->bsc_api_url . "&module=block&action=getblocknobytime&";
  300. $url .= "&closest=before";//closest 值还有个参数 after 控制返回接近时间戳之前还是之后的区块高度
  301. $url .= "&apikey=" . $this->bsc_api_key;
  302. $url .= "&timestamp=" . $time;
  303. dump($url);
  304. $body = Http::get($url);
  305. if (empty($body)) {
  306. return _error('获取区块高度api返回内容为空');
  307. }
  308. // 转成数组
  309. $rsArr = json_decode($body, true);
  310. if (empty($rsArr) || !is_array($rsArr)) {
  311. return _error('获取区块高度api返回数据异常,json转换失败');
  312. }
  313. if ($rsArr['status'] != '1') {
  314. return _error($rsArr['message'] . ' -- ' . $rsArr['result']);
  315. }
  316. Cache::set('block_' . $time, _success($rsArr['result']), 3600);
  317. }
  318. //获取开始预约前的区块高度
  319. return Cache::get('block_' . $time);
  320. }
  321. public function getBlcokNoByCache($time){
  322. if(!Cache::has('block')){
  323. $get_block = (new BscApi())->getBlockNoByTime($start_time);
  324. if($get_block['code'] == 0){
  325. Log::info($get_block['msg'] . date('Y-m-d H:i:s'));
  326. dump('获取区块高度有误');
  327. dump($get_block);
  328. return;
  329. }
  330. Cache::set('block', $get_block['data'], 3600);
  331. }
  332. //获取开始预约前的区块高度
  333. $start_block = Cache::get('block');
  334. }
  335. /**
  336. * 获取钱包地址的BNB余额
  337. *
  338. * api接口返回数据格式
  339. * {
  340. "status": "1",
  341. "message": "OK",
  342. "result": "4156708818000000"
  343. * }
  344. * @param $orderInfo
  345. * @return array|string
  346. */
  347. public function getBnbBalance($address):array
  348. {
  349. if (empty($address)) {
  350. return _error('钱包地址不能为空');
  351. }
  352. $url = $this->bsc_api_url . "&module=account&action=balance";
  353. $url .= "&apikey=" . $this->bsc_api_key;
  354. $url .= "&address=" . $address;
  355. $body = Http::get($url);
  356. if (empty($body)) {
  357. return _error('状态api返回内容为空');
  358. }
  359. // 转成数组
  360. $rsArr = json_decode($body, true);
  361. if (empty($rsArr) || !is_array($rsArr)) {
  362. return _error('状态api返回数据异常,json转换失败');
  363. }
  364. if ($rsArr['status'] != '1') {
  365. return _error('状态api返回status不为1,当前值为:' . $rsArr['status']);
  366. }
  367. $amount = bcdiv($rsArr['result'], $this->valueScale, 6);//交易金额
  368. return _success($amount);
  369. }
  370. /**
  371. * 获取钱包地址的代币余额
  372. *
  373. * api接口返回数据格式
  374. * {
  375. "status": "1",
  376. "message": "OK",
  377. "result": "4156708818000000"
  378. * }
  379. * @param $orderInfo
  380. * @return array|string
  381. */
  382. public function getTokenBalance($address, $contract_address = ''):array
  383. {
  384. if (empty($address)) {
  385. return _error('钱包地址不能为空');
  386. }
  387. if(empty($contract_address)){
  388. $contract_address = $this->contract_address;
  389. }
  390. $url = $this->bsc_api_url . "&module=account&action=tokenbalance";
  391. $url .= "&apikey=" . $this->bsc_api_key;
  392. $url .= "&address=" . $address;
  393. $url .= "&contractaddress=" . $contract_address;
  394. $body = Http::get($url);
  395. if (empty($body)) {
  396. return _error('状态api返回内容为空');
  397. }
  398. // 转成数组
  399. $rsArr = json_decode($body, true);
  400. if (empty($rsArr) || !is_array($rsArr)) {
  401. return _error('状态api返回数据异常,json转换失败');
  402. }
  403. if ($rsArr['status'] != '1') {
  404. return _error('状态api返回status不为1,当前值为:' . $rsArr['status']);
  405. }
  406. $amount = bcdiv($rsArr['result'], $this->valueScale, 6);//交易金额
  407. return _success($amount);
  408. }
  409. }