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