├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── src ├── Address.php ├── Api.php ├── Block.php ├── Crypto │ ├── Base58.php │ ├── Keccak.php │ └── Secp.php ├── Exceptions │ ├── TransactionException.php │ └── TronErrorException.php ├── Interfaces │ └── WalletInterface.php ├── Requests.php ├── Support │ ├── ABI.php │ ├── Formatter.php │ ├── Hash.php │ ├── Key.php │ ├── Secp.php │ └── Utils.php ├── TRC20.php ├── TRX.php ├── Transaction.php ├── Transactions.php └── english.txt └── tests ├── TRC20Test.php └── TRXTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | *.lock 4 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Fenguoz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 概述 2 | 波场API 接口封装, 目前支持波场的 TRX 和 TRC20 中常用生成地址,发起转账,离线签名等功能. 3 | 库基于官方Tron API实现, 除密码学ECC库外无第三方依赖引用. 4 | 5 | ## 特点 6 | 7 | 1. 一套写法兼容 TRON 网络中 TRX 货币和 TRC 系列所有通证 8 | 1. 接口方法可可灵活增减 9 | 10 | Tips: php版本最低支持8.0且需要安装GMP扩展! 11 | 12 | ## 支持方法 13 | 14 | - 生成地址 `generateAddress()` 15 | - 生成带助记词的钱包地址 `generateAddressWithMnemonic()` 16 | - 从私钥获取助记词 `getPhraseFromPrivateKey()` 17 | - 从助记词获取私钥 `getPrivateKeyFromPhrase()` 18 | - 从公钥获取钱包地址 `getAddressHexFromPublicKey()` 19 | - 使用Net方式验证钱包地址 `validateAddress()` 20 | - 获取账户信息 `accountInfo(string $address)` 21 | - 使用TXID获取交易信息 `getTransactionInfoById(string $txid)` 22 | - 私钥获取钱包地址 `getAddressByPrivateKey(string $privateKeyHex)` 23 | - 查询TRX余额 `balance(string $address)` 24 | - 查询Token余额 `$trc20->balance(string $address)` 25 | - 交易转账(离线签名、带转账备注) `transfer(string $private_key, string $from, string $to, float $amount, string $message = null)` 26 | - TRC20交易转账(离线签名、带转账备注) `transferTRC20(string $private_key, string $from, string $to, float $amount, string $message = null, float $fee_limit = 150000000)` 27 | - 查询最新区块 `getNowBlock()` 28 | - 使用blockID获取区块信息 `getBlockByNum(int $blockID)` 29 | - 根据交易哈希查询信息 `getTransactionById(string $txHash)` 30 | - 查询钱包地址交易记录 `getTransactionsByAddress(string $address,int $limit = 20)` 31 | - 快捷方法: 获取钱包支出记录 `getTransactionsFromAddress(string $address)` 获取钱包收入记录 `getTransactionsToAddress(string $address)` 32 | - 查询TRC20地址交易记录 `getTransactionsByTrc20(string $address, int $mintimestamp = null, int $maxtimestamp = null, bool $confirmed = null, bool $to = false,bool $from = false, $limit = 20)` 33 | 34 | ## 快速开始 35 | 36 | ### 安装 37 | 38 | ```bash 39 | > composer require anonymhk/tronApi 40 | ``` 41 | 42 | ### 接口调用 43 | 44 | ``` php 45 | # ApiKey需自行申请 46 | # Mainnet: https://api.trongrid.io 47 | # Shasta Testnet: https://api.shasta.trongrid.io 48 | # Nile Testnet: https://nile.trongrid.io 49 | 50 | $uri = 'https://api.shasta.trongrid.io';// shasta testnet 51 | $trxWallet = new TRX(apiurl: $uri); 52 | 53 | $addressData = $trxWallet->generateAddress(); 54 | var_dump($addressData); 55 | 56 | 57 | $option = [ 58 | 'headers' => [ 59 | 'TRON-PRO-API-KEY' => ApiSecret, 60 | 'Content-Type' => 'application/json' 61 | ], 62 | 'contract_address' => 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',// USDT TRC20 63 | 'decimals' => 6, 64 | ]; 65 | $trc20Wallet = new TRC20(apiurl: $uri, option: $option); 66 | $addressData = $trc20Wallet->balance(ADDRESS); 67 | var_dump($addressData); 68 | 69 | ``` 70 | 71 | 72 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anonymhk/tronapi", 3 | "description": "Support TRON's TRX and TRC20, A PHP library to create Tron wallet addresses and manage wallets using the Tron API", 4 | "keywords": [ 5 | "php", 6 | "tron", 7 | "trx", 8 | "trc20" 9 | ], 10 | "type": "library", 11 | "homepage": "https://github.com/anonymhk/tronapi", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "anonymhk" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.0", 20 | "ext-curl": "*", 21 | "ext-gmp": "*", 22 | "simplito/elliptic-php": "^1.0", 23 | "kornrunner/secp256k1": "^0.2", 24 | "kornrunner/keccak": "^1.1", 25 | "phpseclib/phpseclib": "~3.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "TronApi\\": "src/" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Address.php: -------------------------------------------------------------------------------- 1 | privateKey = $privateKey; 23 | $this->address = $address; 24 | } 25 | 26 | /** 27 | * Dont rely on this. Always use Wallet::validateAddress to double check 28 | * against tronGrid. 29 | * 30 | * @return bool 31 | */ 32 | public function isValid(): bool 33 | { 34 | if (strlen($this->address) !== Address::ADDRESS_SIZE) { 35 | return false; 36 | } 37 | 38 | return Utils::verifyAddress($this->address); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Api.php: -------------------------------------------------------------------------------- 1 | sender = new Requests($apiurl, $options); 16 | } 17 | 18 | /** 19 | * Abstracts some common functionality like formatting the post data 20 | * along with error handling. 21 | * 22 | * @throws TronErrorException 23 | */ 24 | public function post(string $endpoint, array $data = []) 25 | { 26 | $res_data = $this->sender->request('POST', $endpoint, $data); 27 | $this->checkForErrorResponse($res_data); 28 | return $res_data; 29 | } 30 | 31 | /** 32 | * Abstracts some common functionality like 33 | * along with error handling. 34 | * 35 | * @throws TronErrorException 36 | */ 37 | public function get(string $endpoint, array $data = []) 38 | { 39 | $res_data = $this->sender->request('GET', $endpoint, $data); 40 | $this->checkForErrorResponse($res_data); 41 | return $res_data; 42 | } 43 | 44 | /** 45 | * Check if the response has an error and throw it. 46 | * 47 | * @param $data 48 | * @throws TronErrorException 49 | */ 50 | private function checkForErrorResponse($data) 51 | { 52 | if (isset($data['Error'])) { 53 | throw new TronErrorException($data['Error']); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Block.php: -------------------------------------------------------------------------------- 1 | blockID = $blockID; 18 | $this->block_header = $block_header; 19 | $this->transactions = $transactions; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Crypto/Base58.php: -------------------------------------------------------------------------------- 1 | 256): 15 | throw new InvalidArgumentException('Invalid base argument , The base must be between 2 and 256'); 16 | endif; 17 | $value = strval(null); 18 | if(is_null($digits)) $digits = self::digits($base); 19 | while($dec > $base - 1): 20 | $rest = gmp_mod($dec,$base); 21 | $dec = gmp_div($dec,$base); 22 | $value = $digits[intval($rest)].$value; 23 | endwhile; 24 | $value = $digits[intval($dec)].$value; 25 | return $value; 26 | else: 27 | throw new Exception('gmp extension is needed !'); 28 | endif; 29 | } 30 | static public function bc2bin(string | int $dec) : string { 31 | return self::dec2base($dec,256); 32 | } 33 | static public function encode(string | int $dec,int $length = 58) : string { 34 | return self::dec2base($dec,$length,'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'); 35 | } 36 | static public function encodeAddress(string $string,int $prefix = 0,bool $compressed = false) : string { 37 | $string = hex2bin($string); 38 | if($prefix) $string = chr($prefix).$string; 39 | if($compressed) $string = $string.chr($compressed); 40 | $hash = hash('sha256',$string,true); 41 | $hash = hash('sha256',$hash,true); 42 | $checksum = $string.substr($hash,0,4); 43 | return self::encode(self::bin2bc($checksum)); 44 | } 45 | static public function base2dec(string $value,int $base,$digits = false) : string | int { 46 | if(extension_loaded('gmp')): 47 | if($base < 2 or $base > 256): 48 | throw new InvalidArgumentException('Invalid base argument , The base must be between 2 and 256'); 49 | endif; 50 | if($base < 37) $value = strtolower($value); 51 | if($digits === false) $digits = self::digits($base); 52 | $size = strlen($value); 53 | $dec = strval(0); 54 | for($loop = 0;$loop < $size;$loop++): 55 | $element = strpos($digits,$value[$loop]); 56 | $power = gmp_pow($base,$size - $loop - 1); 57 | $dec = gmp_add($dec,gmp_mul($element,$power)); 58 | endfor; 59 | return ($dec <= PHP_INT_MAX and $dec >= PHP_INT_MIN) ? gmp_intval($dec) : gmp_strval($dec); 60 | else: 61 | throw new Exception('gmp extension is needed !'); 62 | endif; 63 | } 64 | static public function bin2bc(string $value) : string | int { 65 | return self::base2dec($value,256); 66 | } 67 | static public function decode(string $value,int $length = 58) : string | int { 68 | return self::base2dec($value,$length,'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'); 69 | } 70 | static public function decodeAddress(string $string,int $prefix = 0,bool $compressed = false) : string { 71 | $string = self::bc2bin(self::decode($string)); 72 | $string = substr($string,0,-4); 73 | if($prefix) $string = substr($string,$prefix); 74 | if($compressed) $string = substr($string,0,-$compressed); 75 | return bin2hex($string); 76 | } 77 | static public function digits(int $base) : string { 78 | if($base > 64): 79 | $digits = implode(array_map(chr(...),range(0,255))); 80 | else: 81 | $digits = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'; 82 | endif; 83 | $digits = substr($digits,0,$base); 84 | return $digits; 85 | } 86 | } -------------------------------------------------------------------------------- /src/Crypto/Keccak.php: -------------------------------------------------------------------------------- 1 | > 31)) & (0xFFFFFFFF), 37 | $bc[($i + 4) % 5][1] ^ (($bc[($i + 1) % 5][1] << 1) | ($bc[($i + 1) % 5][0] >> 31)) & (0xFFFFFFFF) 38 | ]; 39 | for($j = 0;$j < 25;$j += 5): 40 | $st[$j + $i] = [ 41 | $st[$j + $i][0] ^ $t[0], 42 | $st[$j + $i][1] ^ $t[1] 43 | ]; 44 | endfor; 45 | endfor; 46 | $t = $st[1]; 47 | for($i = 0;$i < 24;$i++): 48 | $j = self::$keccakf_piln[$i]; 49 | $bc[0] = $st[$j]; 50 | $n = self::$keccakf_rotc[$i]; 51 | $hi = $t[0]; 52 | $lo = $t[1]; 53 | if($n >= 32): 54 | $n -= 32; 55 | $hi = $t[1]; 56 | $lo = $t[0]; 57 | endif; 58 | $st[$j] =[ 59 | (($hi << $n) | ($lo >> (32 - $n))) & (0xFFFFFFFF), 60 | (($lo << $n) | ($hi >> (32 - $n))) & (0xFFFFFFFF) 61 | ]; 62 | $t = $bc[0]; 63 | endfor; 64 | for($j = 0;$j < 25;$j += 5): 65 | for($i = 0;$i < 5;$i++): 66 | $bc[$i] = $st[$j + $i]; 67 | endfor; 68 | for($i = 0;$i < 5;$i++): 69 | $st[$j + $i] = [ 70 | $st[$j + $i][0] ^ ~$bc[($i + 1) % 5][0] & $bc[($i + 2) % 5][0], 71 | $st[$j + $i][1] ^ ~$bc[($i + 1) % 5][1] & $bc[($i + 2) % 5][1] 72 | ]; 73 | endfor; 74 | endfor; 75 | $st[0] = [ 76 | $st[0][0] ^ $keccakf_rndc[$round][0], 77 | $st[0][1] ^ $keccakf_rndc[$round][1] 78 | ]; 79 | endfor; 80 | return $st; 81 | } 82 | static private function keccak64(string $in_raw,int $capacity,int $outputlength,int $suffix,bool $raw_output) : string { 83 | $capacity /= 8; 84 | $inlen = mb_strlen($in_raw,self::ENCODING); 85 | $rsiz = 200 - 2 * $capacity; 86 | $rsizw = $rsiz / 8; 87 | $st = array_fill(0,25,[0,0]); 88 | for($in_t = 0;$inlen >= $rsiz;$inlen -= $rsiz,$in_t += $rsiz): 89 | for($i = 0;$i < $rsizw;$i++): 90 | $t = unpack('V*',mb_substr($in_raw,intval($i * 8 + $in_t),8,self::ENCODING)); 91 | $st[$i] = [ 92 | $st[$i][0] ^ $t[2], 93 | $st[$i][1] ^ $t[1] 94 | ]; 95 | endfor; 96 | $st = self::keccakf64($st,self::KECCAK_ROUNDS); 97 | endfor; 98 | $temp = mb_substr($in_raw,(int) $in_t,(int) $inlen,self::ENCODING); 99 | $temp = str_pad($temp,(int) $rsiz,chr(0),STR_PAD_RIGHT); 100 | $temp = substr_replace($temp,chr($suffix),$inlen,1); 101 | $temp = substr_replace($temp,chr(ord($temp[intval($rsiz - 1)]) | 0x80),$rsiz - 1,1); 102 | for($i = 0;$i < $rsizw;$i++): 103 | $t = unpack('V*',mb_substr($temp,$i * 8,8,self::ENCODING)); 104 | $st[$i] = [ 105 | $st[$i][0] ^ $t[2], 106 | $st[$i][1] ^ $t[1] 107 | ]; 108 | endfor; 109 | $st = self::keccakf64($st,self::KECCAK_ROUNDS); 110 | $out = strval(null); 111 | for($i = 0;$i < 25;$i++): 112 | $out .= $t = pack('V*',$st[$i][1],$st[$i][0]); 113 | endfor; 114 | $r = mb_substr($out,0,intval($outputlength / 8),self::ENCODING); 115 | return $raw_output ? $r : bin2hex($r); 116 | } 117 | static private function keccakf32(array $st,int $rounds) : array { 118 | $keccakf_rndc = [ 119 | [0x0000,0x0000,0x0000,0x0001],[0x0000,0x0000,0x0000,0x8082],[0x8000,0x0000,0x0000,0x0808a],[0x8000,0x0000,0x8000,0x8000], 120 | [0x0000,0x0000,0x0000,0x808b],[0x0000,0x0000,0x8000,0x0001],[0x8000,0x0000,0x8000,0x08081],[0x8000,0x0000,0x0000,0x8009], 121 | [0x0000,0x0000,0x0000,0x008a],[0x0000,0x0000,0x0000,0x0088],[0x0000,0x0000,0x8000,0x08009],[0x0000,0x0000,0x8000,0x000a], 122 | [0x0000,0x0000,0x8000,0x808b],[0x8000,0x0000,0x0000,0x008b],[0x8000,0x0000,0x0000,0x08089],[0x8000,0x0000,0x0000,0x8003], 123 | [0x8000,0x0000,0x0000,0x8002],[0x8000,0x0000,0x0000,0x0080],[0x0000,0x0000,0x0000,0x0800a],[0x8000,0x0000,0x8000,0x000a], 124 | [0x8000,0x0000,0x8000,0x8081],[0x8000,0x0000,0x0000,0x8080],[0x0000,0x0000,0x8000,0x00001],[0x8000,0x0000,0x8000,0x8008] 125 | ]; 126 | $bc = []; 127 | for($round = 0;$round < $rounds;$round++): 128 | for($i = 0;$i < 5;$i++): 129 | $bc[$i] = [ 130 | $st[$i][0] ^ $st[$i + 5][0] ^ $st[$i + 10][0] ^ $st[$i + 15][0] ^ $st[$i + 20][0], 131 | $st[$i][1] ^ $st[$i + 5][1] ^ $st[$i + 10][1] ^ $st[$i + 15][1] ^ $st[$i + 20][1], 132 | $st[$i][2] ^ $st[$i + 5][2] ^ $st[$i + 10][2] ^ $st[$i + 15][2] ^ $st[$i + 20][2], 133 | $st[$i][3] ^ $st[$i + 5][3] ^ $st[$i + 10][3] ^ $st[$i + 15][3] ^ $st[$i + 20][3] 134 | ]; 135 | endfor; 136 | for($i = 0;$i < 5;$i++): 137 | $t = [ 138 | $bc[($i + 4) % 5][0] ^ ((($bc[($i + 1) % 5][0] << 1) | ($bc[($i + 1) % 5][1] >> 15)) & (0xFFFF)), 139 | $bc[($i + 4) % 5][1] ^ ((($bc[($i + 1) % 5][1] << 1) | ($bc[($i + 1) % 5][2] >> 15)) & (0xFFFF)), 140 | $bc[($i + 4) % 5][2] ^ ((($bc[($i + 1) % 5][2] << 1) | ($bc[($i + 1) % 5][3] >> 15)) & (0xFFFF)), 141 | $bc[($i + 4) % 5][3] ^ ((($bc[($i + 1) % 5][3] << 1) | ($bc[($i + 1) % 5][0] >> 15)) & (0xFFFF)) 142 | ]; 143 | for($j = 0;$j < 25;$j += 5): 144 | $st[$j + $i] = [ 145 | $st[$j + $i][0] ^ $t[0], 146 | $st[$j + $i][1] ^ $t[1], 147 | $st[$j + $i][2] ^ $t[2], 148 | $st[$j + $i][3] ^ $t[3] 149 | ]; 150 | endfor; 151 | endfor; 152 | $t = $st[1]; 153 | for($i = 0;$i < 24;$i++): 154 | $j = self::$keccakf_piln[$i]; 155 | $bc[0] = $st[$j]; 156 | $n = self::$keccakf_rotc[$i] >> 4; 157 | $m = self::$keccakf_rotc[$i] % 16; 158 | $st[$j] = [ 159 | ((($t[(0+$n) %4] << $m) | ($t[(1+$n) %4] >> (16-$m))) & (0xFFFF)), 160 | ((($t[(1+$n) %4] << $m) | ($t[(2+$n) %4] >> (16-$m))) & (0xFFFF)), 161 | ((($t[(2+$n) %4] << $m) | ($t[(3+$n) %4] >> (16-$m))) & (0xFFFF)), 162 | ((($t[(3+$n) %4] << $m) | ($t[(0+$n) %4] >> (16-$m))) & (0xFFFF)) 163 | ]; 164 | $t = $bc[0]; 165 | endfor; 166 | for($j = 0;$j < 25;$j += 5): 167 | for($i = 0;$i < 5;$i++): 168 | $bc[$i] = $st[$j + $i]; 169 | endfor; 170 | for($i = 0;$i < 5;$i++): 171 | $st[$j + $i] = [ 172 | $st[$j + $i][0] ^ ~$bc[($i + 1) % 5][0] & $bc[($i + 2) % 5][0], 173 | $st[$j + $i][1] ^ ~$bc[($i + 1) % 5][1] & $bc[($i + 2) % 5][1], 174 | $st[$j + $i][2] ^ ~$bc[($i + 1) % 5][2] & $bc[($i + 2) % 5][2], 175 | $st[$j + $i][3] ^ ~$bc[($i + 1) % 5][3] & $bc[($i + 2) % 5][3] 176 | ]; 177 | endfor; 178 | endfor; 179 | $st[0] = [ 180 | $st[0][0] ^ $keccakf_rndc[$round][0], 181 | $st[0][1] ^ $keccakf_rndc[$round][1], 182 | $st[0][2] ^ $keccakf_rndc[$round][2], 183 | $st[0][3] ^ $keccakf_rndc[$round][3] 184 | ]; 185 | endfor; 186 | return $st; 187 | } 188 | static private function keccak32(string $in_raw,int $capacity,int $outputlength,int $suffix,bool $raw_output) : string { 189 | $capacity /= 8; 190 | $inlen = mb_strlen($in_raw,self::ENCODING); 191 | $rsiz = 200 - 2 * $capacity; 192 | $rsizw = $rsiz / 8; 193 | $st = array_fill(0,25,[0,0,0,0]); 194 | for($in_t = 0;$inlen >= $rsiz;$inlen -= $rsiz,$in_t += $rsiz): 195 | for($i = 0;$i < $rsizw;$i++): 196 | $t = unpack('v*',mb_substr($in_raw,intval($i * 8 + $in_t),8,self::ENCODING)); 197 | $st[$i] = [ 198 | $st[$i][0] ^ $t[4], 199 | $st[$i][1] ^ $t[3], 200 | $st[$i][2] ^ $t[2], 201 | $st[$i][3] ^ $t[1] 202 | ]; 203 | endfor; 204 | $st = self::keccakf32($st,self::KECCAK_ROUNDS); 205 | endfor; 206 | $temp = mb_substr($in_raw,(int) $in_t,(int) $inlen,self::ENCODING); 207 | $temp = str_pad($temp,(int) $rsiz,chr(0),STR_PAD_RIGHT); 208 | $temp = substr_replace($temp,chr($suffix),$inlen,1); 209 | $temp = substr_replace($temp,chr((int) $temp[intval($rsiz - 1)] | 0x80),$rsiz - 1,1); 210 | for($i = 0;$i < $rsizw;$i++): 211 | $t = unpack('v*',mb_substr($temp,$i * 8,8,self::ENCODING)); 212 | $st[$i] = [ 213 | $st[$i][0] ^ $t[4], 214 | $st[$i][1] ^ $t[3], 215 | $st[$i][2] ^ $t[2], 216 | $st[$i][3] ^ $t[1] 217 | ]; 218 | endfor; 219 | $st = self::keccakf32($st,self::KECCAK_ROUNDS); 220 | $out = strval(null); 221 | for($i = 0;$i < 25;$i++): 222 | $out .= $t = pack('v*',$st[$i][3],$st[$i][2],$st[$i][1],$st[$i][0]); 223 | endfor; 224 | $r = mb_substr($out,0,intval($outputlength / 8),self::ENCODING); 225 | return $raw_output ? $r: bin2hex($r); 226 | } 227 | static private function keccak(string $in_raw,int $capacity,int $outputlength,int $suffix,bool $raw_output) : string { 228 | return self::$x64 ? self::keccak64($in_raw,$capacity,$outputlength,$suffix,$raw_output) : self::keccak32($in_raw,$capacity,$outputlength,$suffix,$raw_output); 229 | } 230 | static public function hash(string $in,int $mdlen,bool $raw_output = false) : string { 231 | if(in_array($mdlen,[224,256,384,512],true)): 232 | return self::keccak($in,$mdlen,$mdlen,self::LFSR,$raw_output); 233 | else: 234 | throw new Exception('Unsupported Keccak Hash output size'); 235 | endif; 236 | } 237 | static public function shake(string $in,int $security_level,int $outlen,bool $raw_output = false) : string { 238 | if(in_array($security_level,[128,256],true)): 239 | return self::keccak($in,$security_level,$outlen,0x1f,$raw_output); 240 | else: 241 | throw new Exception('Unsupported Keccak Shake security level'); 242 | endif; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/Crypto/Secp.php: -------------------------------------------------------------------------------- 1 | sign($message,$privatekey); 13 | $recoveryparam = bin2hex(chr($sign->recoveryParam)); 14 | $r = $sign->r->toString(16); 15 | $s = $sign->s->toString(16); 16 | return $r.$s.$recoveryparam; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Exceptions/TransactionException.php: -------------------------------------------------------------------------------- 1 | curl = curl_init(); 16 | // 设置请求超时时间,单位是秒 17 | $request_timeout = 10; 18 | // 设置连接超时时间,单位是秒 19 | $connect_timeout = 5; 20 | if(isset($options['req_timeout'])){ 21 | $request_timeout = $options['req_timeout']; 22 | } 23 | if(isset($options['connect_timeout'])){ 24 | $connect_timeout = $options['connect_timeout']; 25 | } 26 | if(!empty($options['headers'])){ 27 | $this->curl_headers = $options['headers']; 28 | } 29 | curl_setopt($this->curl, CURLOPT_CONNECTTIMEOUT, $connect_timeout); 30 | curl_setopt($this->curl, CURLOPT_TIMEOUT, $request_timeout); 31 | curl_setopt($this->curl,CURLOPT_RETURNTRANSFER,true); 32 | } 33 | 34 | /** 35 | * 发起CURL请求 36 | * @throws TronErrorException 37 | */ 38 | public function request(string $method,string $path,array $data = array()) : mixed { 39 | switch($method){ 40 | case 'POST': 41 | curl_setopt($this->curl,CURLOPT_URL,(filter_var($path,FILTER_VALIDATE_URL) ? $path : $this->url.'/'.$path)); 42 | curl_setopt($this->curl,CURLOPT_POSTFIELDS,json_encode($data)); 43 | break; 44 | case 'GET': 45 | curl_setopt($this->curl,CURLOPT_URL,(filter_var($path,FILTER_VALIDATE_URL) ? $path : $this->url.'/'.$path.'?'.http_build_query($data))); 46 | curl_setopt($this->curl,CURLOPT_CUSTOMREQUEST,$method); 47 | break; 48 | default: 49 | error_log('The request method is inappropriate for the URL !'); 50 | break; 51 | } 52 | 53 | curl_setopt($this->curl,CURLOPT_HTTPHEADER, $this->curl_headers); 54 | $result = curl_exec($this->curl); 55 | $error = curl_error($this->curl); 56 | return is_bool($result) ? throw new TronErrorException($error) : json_decode($result, true); 57 | } 58 | public function __destruct(){ 59 | curl_close($this->curl); 60 | } 61 | public function __clone() : void { 62 | $this->curl = curl_init(); 63 | curl_setopt($this->curl,CURLOPT_RETURNTRANSFER,true); 64 | } 65 | } -------------------------------------------------------------------------------- /src/Support/ABI.php: -------------------------------------------------------------------------------- 1 | function 45 | public $events_encoded; 46 | 47 | const NUM_ZEROS = 64; 48 | 49 | 50 | public function Init(string $baseJSON) 51 | { 52 | $this->functions = []; 53 | $this->events = []; 54 | $this->other_objects = []; 55 | $this->events_encoded = []; 56 | $parsedJSON = json_decode($baseJSON); 57 | 58 | foreach ($parsedJSON as $func) 59 | { 60 | if($func->type == 'constructor') { 61 | $this->constructor = $func; 62 | } 63 | else if($func->type == 'event') { 64 | $this->events[$func->name] = $func; 65 | $this->events_encoded[$this->GetSignatureFromEvent($func)] = $func; 66 | } 67 | else if($func->type == 'function') { 68 | $this->functions[$func->name] = $func; 69 | } 70 | else { 71 | $this->other_objects []= $func; 72 | } 73 | } 74 | } 75 | 76 | 77 | public function GetFunction(?string $function_name) 78 | { 79 | if (empty($function_name)) return $this->constructor; 80 | 81 | if(!empty($this->functions[$function_name])) { 82 | return $this->functions[$function_name]; 83 | } 84 | else { 85 | return null; 86 | } 87 | } 88 | 89 | 90 | public function GetEvent(?string $event_name) 91 | { 92 | if (empty($event_hash)) return null; 93 | 94 | if(!empty($this->events[$event_name])) { 95 | return $this->events[$event_name]; 96 | } 97 | else { 98 | return null; 99 | } 100 | } 101 | 102 | 103 | public function GetEventFromHash(?string $event_hash) 104 | { 105 | if (empty($event_hash)) return null; 106 | 107 | if (!empty($this->events_encoded[$event_hash])) { 108 | return $this->events_encoded[$event_hash]; 109 | } 110 | else { 111 | return null; 112 | } 113 | } 114 | 115 | 116 | private static function GetParameterType(?string $abi_string_type) 117 | { 118 | //bytes 119 | if ($abi_string_type == 'bytes') return VariableType::Bytes; 120 | else if (Utils::stringContains($abi_string_type, 'bytes')) return VariableType::BytesFixed; 121 | 122 | //dynamic 123 | else if (Utils::stringContains($abi_string_type, 'tuple')) return VariableType::Tuple; 124 | else if (Utils::stringContains($abi_string_type, 'string')) return VariableType::String; 125 | 126 | //static 127 | else if (Utils::stringContains($abi_string_type, 'uint') ) return VariableType::UInt; 128 | else if (Utils::stringContains($abi_string_type, 'int') ) return VariableType::Int; 129 | else if (Utils::stringContains($abi_string_type, 'fixed') ) return VariableType::Int; 130 | else if (Utils::stringContains($abi_string_type, 'bool')) return VariableType::Bool; 131 | else if (Utils::stringContains($abi_string_type, 'address')) return VariableType::Address; 132 | 133 | //error 134 | else { 135 | var_dump("parameter error: " . $abi_string_type); 136 | } 137 | 138 | return VariableType::Int; 139 | } 140 | 141 | 142 | private static function IsStaticParameter(int $vType) : bool 143 | { 144 | return $vType == VariableType::UInt 145 | || $vType == VariableType::Int 146 | || $vType == VariableType::Bool 147 | || $vType == VariableType::Address 148 | || $vType == VariableType::BytesFixed; 149 | } 150 | 151 | 152 | private static function ExistsDynamicParameter(array $components) : bool 153 | { 154 | foreach ($components as $comp) 155 | { 156 | if (is_string($comp)) 157 | { 158 | $isStatic = self::IsStaticParameter(self::GetParameterType($comp)); 159 | } 160 | else 161 | { 162 | if (isset($comp->components)) { 163 | $isStatic = !self::ExistsDynamicParameter($comp->components); 164 | } 165 | else { 166 | $isStatic = self::IsStaticParameter(self::GetParameterType($comp->type)); 167 | } 168 | } 169 | 170 | if (!$isStatic) { 171 | return true; 172 | } 173 | } 174 | 175 | return false; 176 | } 177 | 178 | 179 | public function isCallFunction($function_name) 180 | { 181 | $function = $this->GetFunction($function_name); 182 | if ($function == null) return false; 183 | 184 | $stateMutability = ""; 185 | if (isset($function->stateMutability)) $stateMutability = $function->stateMutability; 186 | 187 | return ($stateMutability == 'pure' || $stateMutability == 'view'); 188 | } 189 | 190 | 191 | public function isSendFunction($function_name) 192 | { 193 | $function = $this->GetFunction($function_name); 194 | if ($function == null) return false; 195 | 196 | $stateMutability = ""; 197 | if (isset($function->stateMutability)) $stateMutability = $function->stateMutability; 198 | 199 | return ($stateMutability != 'pure' && $stateMutability != 'view'); 200 | } 201 | 202 | 203 | 204 | /***************************************ENCODE */ 205 | 206 | 207 | public function EncodeData($function_name, $data) 208 | { 209 | $function = $this->GetFunction($function_name); 210 | $data = $this->forceWrapperArray($function, $data); 211 | 212 | $hashData = "0x"; 213 | 214 | if($function_name != '') { 215 | //function signature (first 4 bytes) (not for constructor) 216 | $signature = $this->GetSignatureFromFunction($function); 217 | $sha3 = Keccak::hash($signature, 256); 218 | $hashData .= substr($sha3,0, 8); 219 | } 220 | 221 | if ($function !== null) { 222 | $hashData .= self::EncodeGroup($function->inputs, $data); 223 | } 224 | 225 | return $hashData; 226 | } 227 | 228 | 229 | public function GetSignatureFromEvent($function) 230 | { 231 | $signature = $this->GetSignatureFromFunction($function); 232 | return '0x' . Keccak::hash($signature, 256); 233 | } 234 | 235 | 236 | private function GetSignatureFromFunction($function) 237 | { 238 | $signature = $function->name . $this->GetSignatureFromFunction_Inputs($function->inputs); 239 | return $signature; 240 | } 241 | 242 | 243 | private function GetSignatureFromFunction_Inputs($function_inputs) 244 | { 245 | $signature = "("; 246 | foreach($function_inputs as $input) 247 | { 248 | $type = $input->type; 249 | if ($type == 'tuple') $type = $this->GetSignatureFromFunction_Inputs($input->components); 250 | else if ($type == 'tuple[]') $type = $this->GetSignatureFromFunction_Inputs($input->components) . '[]'; 251 | else if ($type == 'uint' || $type == 'int') $type .= '256'; 252 | else if ($type == 'uint[]') $type = 'uint256[]'; 253 | else if ($type == 'int[]') $type = 'int256[]'; 254 | 255 | $signature .= $type . ','; 256 | } 257 | 258 | if (count($function_inputs) > 0) $signature = substr($signature, 0, -1); 259 | $signature .= ')'; 260 | 261 | return $signature; 262 | } 263 | 264 | 265 | private function forceWrapperArray($function, $data) 266 | { 267 | if ($function === null || count($function->inputs) === 0) { 268 | $data = []; 269 | } 270 | else if ($data === null) { 271 | $data = []; 272 | } 273 | else if(!is_array($data)) { 274 | $data = [$data]; 275 | } 276 | else 277 | { 278 | $tempData = $data; 279 | $input = $function->inputs[0]; 280 | $input_array_dimension = substr_count($input->type, '['); 281 | 282 | while ($input_array_dimension > 0) { 283 | if (is_array($tempData[0])) { 284 | if($input_array_dimension == 1) break; 285 | else $tempData = $tempData[0]; 286 | } 287 | else { 288 | $data = [$data]; 289 | break; 290 | } 291 | 292 | $input_array_dimension--; 293 | } 294 | } 295 | 296 | return $data; 297 | } 298 | 299 | 300 | public static function EncodeGroup(array $inputs, $data) : string 301 | { 302 | $hashData = ""; 303 | $currentDynamicIndex = 0; 304 | { 305 | $staticInputCount = 0; 306 | foreach ($inputs as $input) { 307 | $varType = self::GetParameterType($input->type); 308 | 309 | // for non-tuple item, we'll have in-place value or offset 310 | if ($varType != VariableType::Tuple) { 311 | $staticInputCount++; 312 | continue; 313 | } 314 | 315 | // for tuple we'll have several in place values or one pointer to the start of several in-place values 316 | if (self::ExistsDynamicParameter($input->components)) { 317 | $staticInputCount++; 318 | } else { 319 | $staticInputCount += count($input->components); 320 | } 321 | } 322 | $currentDynamicIndex = $staticInputCount * self::NUM_ZEROS / 2; 323 | } 324 | 325 | //parameters 326 | $i = 0; 327 | foreach ($inputs as $pos => $input) 328 | { 329 | $var_name = $pos; 330 | if (is_object($input)) { 331 | if (isset($input->name)) $var_name = $input->name; 332 | } 333 | else if (is_string($input)){ 334 | $var_name = $input; 335 | } 336 | 337 | $inputData = is_object($data) ? $data->$var_name : $data[$pos]; 338 | if (is_array($data) && $inputData === null) $inputData = $data[$var_name]; 339 | 340 | $hashData .= self::EncodeInput($input, $inputData, 1, $currentDynamicIndex); 341 | 342 | if (isset($input->hash)) $currentDynamicIndex += strlen($input->hash) / 2; 343 | $i++; 344 | } 345 | 346 | foreach($inputs as $pos => $input) { 347 | $hashData .= self::EncodeInput($input, null, 2, $currentDynamicIndex); 348 | } 349 | 350 | if (count($inputs) == 0) { 351 | $hashData .= self::NUM_ZEROS / 2; 352 | } 353 | 354 | return $hashData; 355 | } 356 | 357 | 358 | public static function EncodeParameters_External(array $input_types, array $data) : string 359 | { 360 | $inputs = array(); 361 | foreach($input_types as $it) { 362 | $input = new stdClass(); 363 | $input->name = $it; 364 | $input->type = $it; 365 | $inputs []= $input; 366 | } 367 | 368 | return '0x' . self::EncodeGroup($inputs, $data); 369 | } 370 | 371 | 372 | public static function EncodeParameter_External(string $input_name, $data) : string 373 | { 374 | $hashData = ""; 375 | $currentDynamicIndex = self::NUM_ZEROS / 2; 376 | 377 | $input = new stdClass(); 378 | $input->type = $input_name; 379 | 380 | $hashData .= self::EncodeInput($input, $data, 1, $currentDynamicIndex); 381 | 382 | if (isset($input->hash)) $currentDynamicIndex += strlen($input->hash) / 2; 383 | 384 | $hashData .= self::EncodeInput($input, null, 2, $currentDynamicIndex); 385 | 386 | return '0x' . $hashData; 387 | } 388 | 389 | 390 | 391 | 392 | private static function EncodeInput_Array($full_input, $inputData, $isStaticLength) 393 | { 394 | $inputs = []; 395 | $currentDynamicIndex = count($inputData) * self::NUM_ZEROS / 2; 396 | 397 | //prepare clean input 398 | $last_array_marker = strrpos($full_input->type, '['); 399 | $clean_type = substr($full_input->type, 0, $last_array_marker); 400 | 401 | $clean_internalType = ""; 402 | if (isset($full_input->internalType)) { 403 | $last_array_marker = strrpos($full_input->internalType, '['); 404 | $clean_internalType = substr($full_input->internalType, 0, $last_array_marker); 405 | } 406 | 407 | $hashData = ""; 408 | 409 | if (!$isStaticLength) { 410 | //add array length 411 | $hashData = self::EncodeInput_UInt(count($inputData)); 412 | } 413 | 414 | foreach ($inputData as $pos => $element) 415 | { 416 | $input = new stdClass(); 417 | $input->type = $clean_type; 418 | $input->internalType = $clean_internalType; 419 | if (isset($full_input->components)) { 420 | $input->components = $full_input->components; 421 | } 422 | $inputs []= $input; 423 | 424 | $hashData .= self::EncodeInput($input, $element, 1, $currentDynamicIndex); 425 | 426 | if (isset($input->hash)) $currentDynamicIndex += strlen($input->hash) / 2; 427 | } 428 | 429 | foreach($inputs as $pos => $input) 430 | { 431 | $data = $inputData[$pos]; 432 | $hashData .= self::EncodeInput($input, $data, 2, $currentDynamicIndex); 433 | } 434 | 435 | if (count($inputs) == 0) { 436 | $hashData .= self::NUM_ZEROS / 2; 437 | } 438 | 439 | return $hashData; 440 | } 441 | 442 | 443 | private static function EncodeInput($input, $inputData, $round, &$currentDynamicIndex) 444 | { 445 | $hash = ""; 446 | 447 | if($round == 1) 448 | { 449 | $input_type = is_string($input) ? $input : $input->type; 450 | $varType = self::GetParameterType($input_type); 451 | 452 | //dynamic 453 | if (Utils::stringContains($input->type, '[')) 454 | { 455 | //arrays with all static parameters have no initial array offset 456 | $isStaticArray = self::IsStaticParameter($varType); 457 | if ($varType == VariableType::Tuple) { 458 | $isStaticArray = !self::ExistsDynamicParameter($input->components); 459 | } 460 | $isStaticLength = $isStaticArray && !Utils::stringContains($input_type, '[]'); 461 | 462 | $res = self::EncodeInput_Array($input, $inputData, $isStaticLength); 463 | if (!$isStaticLength) { 464 | $input->hash = $res; 465 | return self::EncodeInput_UInt($currentDynamicIndex); 466 | } 467 | return $res; 468 | } 469 | else if ($varType == VariableType::Tuple) 470 | { 471 | $res = self::EncodeGroup($input->components, $inputData); 472 | 473 | // if the tuple is dynamic, we return offset and add tuple's data at the end 474 | if (self::ExistsDynamicParameter($input->components)) { 475 | $input->hash = $res; 476 | return self::EncodeInput_UInt($currentDynamicIndex); 477 | } 478 | return $res; 479 | } 480 | else if ($varType == VariableType::String) { 481 | $input->hash = self::EncodeInput_String($inputData); 482 | $res = self::EncodeInput_UInt($currentDynamicIndex); 483 | return $res; 484 | } 485 | else if ($varType == VariableType::Bytes) { 486 | $input->hash = self::EncodeInput_Bytes($inputData); 487 | $res = self::EncodeInput_UInt($currentDynamicIndex); 488 | return $res; 489 | } 490 | //static 491 | else if ($varType == VariableType::UInt) { 492 | return self::EncodeInput_UInt($inputData); 493 | } 494 | else if ($varType == VariableType::Int) { 495 | return self::EncodeInput_Int($inputData); 496 | } 497 | else if ($varType == VariableType::Bool) { 498 | return self::EncodeInput_Bool($inputData); 499 | } 500 | else if ($varType == VariableType::Address) { 501 | return self::EncodeInput_Address($inputData); 502 | } 503 | else if ($varType == VariableType::BytesFixed) { 504 | return self::EncodeInput_BytesFixed($inputData); 505 | } 506 | } 507 | else if($round == 2) 508 | { 509 | if (isset($input->hash) and $input->hash != '') { 510 | return $input->hash; 511 | } 512 | } 513 | 514 | return $hash; 515 | } 516 | 517 | private static function EncodeInput_UInt($data) 518 | { 519 | if (is_string($data) && ctype_digit($data)) { 520 | $bn = Utils::toBn($data); 521 | $hash = self::AddZeros($bn->toHex(true), true); 522 | } 523 | else if ($data instanceof BigNumber) { 524 | $hash = self::AddZeros($data->toHex(true), true); 525 | } 526 | else if (is_int($data) || is_long($data)) { 527 | $hash = self::AddZeros(dechex($data), true); 528 | } 529 | else { 530 | throw new Exception("EncodeInput_UInt, not valid input type"); 531 | } 532 | 533 | return $hash; 534 | } 535 | 536 | private static function EncodeInput_Int($data) 537 | { 538 | if (is_string($data) && ctype_digit($data)) { 539 | $bn = Utils::toBn($data); 540 | $hash = self::AddNegativeF($bn->toHex(true), true); 541 | } 542 | else if ($data instanceof BigNumber) { 543 | if($data->toString()[0] == '-') 544 | $hash = self::AddNegativeF($data->toHex(true), true); 545 | else 546 | $hash = self::AddZeros($data->toHex(true), true); 547 | } 548 | else if (is_int($data) || is_long($data)) { 549 | $hash = self::AddZerosOrF(dechex($data), true); 550 | } 551 | else { 552 | throw new Exception("EncodeInput_Int, not valid input type"); 553 | } 554 | 555 | return $hash; 556 | } 557 | 558 | private static function EncodeInput_Bool($data) 559 | { 560 | $hash = $data ? '1' : '0'; 561 | $hash = self::AddZeros($hash, true); 562 | return $hash; 563 | } 564 | 565 | private static function EncodeInput_Address($data) 566 | { 567 | $hash = self::AddZeros(substr($data, 2), true); 568 | return $hash; 569 | } 570 | 571 | private static function EncodeInput_String($data) 572 | { 573 | //length + hexa string 574 | $hash = self::EncodeInput_UInt(strlen($data)) . self::AddZeros(bin2hex($data), false); 575 | 576 | return $hash; 577 | } 578 | 579 | private static function EncodeInput_Bytes($data) 580 | { 581 | $hexa = $data; 582 | 583 | //I'm not proud of this. Official parsers seem to handle 0x as 0x0 when input is type bytes 584 | //I think it can cause problems when you want to use bytes as a string, because you can't save the string "0x" 585 | //but looking at issue #50 it seems clear that the current evm behaviour is this. 586 | if ($data == '0x') { 587 | $data = ''; 588 | } 589 | 590 | //if data is not a valid hexa, it means its a binary rep 591 | if (substr($data, 0, 2) != '0x' || !ctype_xdigit(substr($data, 2)) || strlen($data) % 2 != 0) { 592 | $hexa = bin2hex($data); 593 | } 594 | 595 | if (substr($hexa, 0, 2) == '0x') { 596 | $hexa = substr($hexa, 2); 597 | } 598 | 599 | //length + hexa string 600 | $hash = self::EncodeInput_UInt(strlen($hexa) / 2) . self::AddZeros($hexa, false); 601 | 602 | return $hash; 603 | } 604 | 605 | 606 | private static function EncodeInput_BytesFixed($data) 607 | { 608 | $hexa = $data; 609 | 610 | //if data is not a valid hexa, it means its a binary rep 611 | if (substr($data, 0, 2) != '0x' || !ctype_xdigit(substr($data, 2)) || strlen($data) % 2 != 0) { 612 | $hexa = bin2hex($data); 613 | } 614 | 615 | if (substr($hexa, 0, 2) == '0x') { 616 | $hexa = substr($hexa, 2); 617 | } 618 | 619 | //length + hexa string 620 | $hash = self::AddZeros($hexa, false); 621 | 622 | return $hash; 623 | } 624 | 625 | 626 | private static function AddZeros($data, $add_left) 627 | { 628 | $remaining = strlen($data); 629 | if ($remaining > self::NUM_ZEROS) $remaining %= self::NUM_ZEROS; 630 | $total = self::NUM_ZEROS - $remaining; 631 | 632 | $res = $data; 633 | 634 | if ($total > 0) { 635 | for($i=0; $i < $total; $i++) { 636 | if($add_left) $res = '0'.$res; 637 | else $res .= '0'; 638 | } 639 | } 640 | 641 | return $res; 642 | } 643 | 644 | 645 | private static function AddNegativeF($data, $add_left) 646 | { 647 | $remaining = strlen($data); 648 | if ($remaining > self::NUM_ZEROS) $remaining %= self::NUM_ZEROS; 649 | $total = self::NUM_ZEROS - $remaining; 650 | 651 | $res = $data; 652 | 653 | if ($total > 0) { 654 | for($i=0; $i < $total; $i++) { 655 | if($add_left) $res = 'f'.$res; 656 | else $res .= 'f'; 657 | } 658 | } 659 | 660 | return $res; 661 | } 662 | 663 | 664 | private static function AddZerosOrF($data, $add_left) 665 | { 666 | $valueToAdd = (strtolower($data[0]) == 'f' && strlen($data) == 16) ? 'f' : '0'; 667 | 668 | $remaining = strlen($data); 669 | if ($remaining > self::NUM_ZEROS) $remaining %= self::NUM_ZEROS; 670 | $total = self::NUM_ZEROS - $remaining; 671 | 672 | $res = $data; 673 | 674 | if ($total > 0) { 675 | for($i=0; $i < $total; $i++) { 676 | if($add_left) $res = $valueToAdd.$res; 677 | else $res .= $valueToAdd; 678 | } 679 | } 680 | 681 | return $res; 682 | } 683 | 684 | 685 | /***************************************DECODE */ 686 | 687 | public function DecodeData($function_name, $encoded) 688 | { 689 | $encoded = substr($encoded, 2); 690 | $function = $this->GetFunction($function_name); 691 | 692 | $decoded = self::DecodeGroup($function->outputs, $encoded, 0); 693 | 694 | return $decoded; 695 | } 696 | 697 | 698 | public static function DecodeGroup($outputs, $encoded, $index) 699 | { 700 | $group = new stdClass(); 701 | $first_index = $index; 702 | $elem_index = 1; 703 | $tuple_count = 1; 704 | $array_count = 1; 705 | $output_count = count($outputs); 706 | 707 | 708 | foreach ($outputs as $output) 709 | { 710 | $output_type = is_string($output) ? $output : $output->type; 711 | $varType = self::GetParameterType($output_type); 712 | $output_type_offset = self::GetOutputOffset($output); 713 | $var_name = ''; 714 | 715 | //dynamic 716 | if(Utils::stringContains($output->type, '[')) 717 | { 718 | $var_name = $output->name != '' ? $output->name : 'array_'.$array_count; 719 | 720 | //arrays with all static parameters have no initial array offset 721 | $isStaticArray = self::IsStaticParameter($varType); 722 | if ($varType == VariableType::Tuple) { 723 | $isStaticArray = !self::ExistsDynamicParameter($output->components); 724 | } 725 | $isStaticLength = $isStaticArray && !Utils::stringContains($output->type, '[]'); 726 | 727 | $dynamic_data_start = 0; 728 | if ($isStaticLength) $dynamic_data_start = $index; 729 | else $dynamic_data_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 730 | 731 | $group->$var_name = self::DecodeInput_Array($output, $encoded, $dynamic_data_start); 732 | $array_count++; 733 | } 734 | else if ($varType == VariableType::Tuple) 735 | { 736 | $var_name = $output->name != '' ? $output->name : 'tuple_'.$tuple_count; 737 | 738 | //tuples with only static parameters have no initial tuple offset 739 | $hasDynamicParameters = self::ExistsDynamicParameter($output->components); 740 | 741 | $dynamic_data_start = $index; 742 | if ($hasDynamicParameters) { 743 | $dynamic_data_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 744 | } 745 | 746 | $group->$var_name = self::DecodeGroup($output->components, $encoded, $dynamic_data_start); 747 | $tuple_count++; 748 | } 749 | else if ($varType == VariableType::String) 750 | { 751 | $var_name = $output->name != '' ? $output->name : 'elem_'.$elem_index; 752 | $dynamic_data_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 753 | $group->$var_name = self::DecodeInput_String($encoded, $dynamic_data_start); 754 | } 755 | else if ($varType == VariableType::Bytes) 756 | { 757 | $var_name = $output->name != '' ? $output->name : 'elem_'.$elem_index; 758 | $dynamic_data_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 759 | $group->$var_name = self::DecodeInput_Bytes($encoded, $dynamic_data_start); 760 | } 761 | //static 762 | else 763 | { 764 | $var_name = 'result'; 765 | if($output->name != '') $var_name = $output->name; 766 | else if($output_count > 1) $var_name = 'elem_'.$elem_index; 767 | 768 | $group->$var_name = self::DecodeInput_Generic($varType, $encoded, $index); 769 | } 770 | 771 | $elem_index++; 772 | $index += $output_type_offset * self::NUM_ZEROS; 773 | } 774 | 775 | return $group; 776 | } 777 | 778 | 779 | public static function DecodeParameter_External(string $output_type, string $encoded) 780 | { 781 | $output = new stdClass(); 782 | $output->type = $output_type; 783 | $varType = self::GetParameterType($output_type); 784 | $dynamic_data_start = self::NUM_ZEROS; 785 | 786 | $res = ""; 787 | 788 | if (substr($encoded, 0, 2) == '0x') { 789 | $encoded = substr($encoded, 2); 790 | } 791 | 792 | //dynamic 793 | if(Utils::stringContains($output->type, '[')) 794 | { 795 | //arrays with all static parameters have no initial array offset 796 | $isStaticArray = self::IsStaticParameter($varType); 797 | if ($varType == VariableType::Tuple) { 798 | $isStaticArray = !self::ExistsDynamicParameter($output->components); 799 | } 800 | $isStaticLength = $isStaticArray && !Utils::stringContains($output->type, '[]'); 801 | 802 | $dynamic_data_start = 0; 803 | if ($isStaticLength) $dynamic_data_start = 0; 804 | else $dynamic_data_start = 0 + self::DecodeInput_UInt_Internal($encoded, 0) * 2; 805 | 806 | $res = self::DecodeInput_Array($output, $encoded, $dynamic_data_start); 807 | } 808 | else if ($varType == VariableType::Tuple) 809 | { 810 | //tuples with only static parameters have no initial tuple offset 811 | $res = self::DecodeGroup($output->components, $encoded, $dynamic_data_start); 812 | } 813 | else if ($varType == VariableType::String) 814 | { 815 | $res = self::DecodeInput_String($encoded, $dynamic_data_start); 816 | } 817 | else if ($varType == VariableType::Bytes) 818 | { 819 | $res = self::DecodeInput_Bytes($encoded, $dynamic_data_start); 820 | } 821 | //simple 822 | else 823 | { 824 | $res = self::DecodeInput_Generic($varType, $encoded, 0); 825 | } 826 | 827 | return $res; 828 | } 829 | 830 | 831 | private static function DecodeInput_Array($output, $encoded, $index) 832 | { 833 | $array = []; 834 | $first_index = $index; 835 | 836 | $clean_output = clone $output; 837 | $last_array_marker = strrpos($clean_output->type, '['); 838 | $clean_output->type = substr($clean_output->type, 0, $last_array_marker); 839 | 840 | $varType = self::GetParameterType($clean_output->type); 841 | $isStaticType = self::IsStaticParameter($varType); 842 | if ($varType == VariableType::Tuple) { 843 | $isStaticType = !self::ExistsDynamicParameter($output->components); 844 | } 845 | 846 | $length = 0; 847 | if ($isStaticType) { 848 | $last_array_marker_end = strrpos($output->type, ']'); 849 | $length = (int) substr($output->type, $last_array_marker + 1, $last_array_marker_end - $last_array_marker - 1); 850 | } 851 | 852 | if ($length <= 0) { 853 | $length = self::DecodeInput_UInt_Internal($encoded, $first_index); 854 | $first_index += self::NUM_ZEROS; 855 | $index += self::NUM_ZEROS; 856 | } 857 | 858 | $element_offset = 1; 859 | if ($isStaticType) { 860 | $element_offset = self::GetOutputOffset($clean_output); 861 | } 862 | 863 | for ($i = 0; $i < $length; $i++) 864 | { 865 | $res = "error"; 866 | if (Utils::stringContains($clean_output->type, '[')) 867 | { 868 | $isStaticLength = $isStaticType && !Utils::stringContains($clean_output->type, '[]'); 869 | //arrays with all static parameters have no initial array offset 870 | $element_start = $index; 871 | if ($isStaticLength) { 872 | $element_start = $index; 873 | } 874 | else { 875 | $element_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 876 | } 877 | 878 | $res = self::DecodeInput_Array($clean_output, $encoded, $element_start); 879 | } 880 | else if($varType == VariableType::Tuple) 881 | { 882 | //tuple with all static parameters have no initial array offset 883 | if($isStaticType) { 884 | $element_start = $index; 885 | } 886 | else { 887 | $element_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 888 | } 889 | 890 | $res = self::DecodeGroup($clean_output->components, $encoded, $element_start); 891 | } 892 | else if($varType == VariableType::String) 893 | { 894 | $element_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 895 | $res = self::DecodeInput_String($encoded, $element_start); 896 | } 897 | else if($varType == VariableType::Bytes) 898 | { 899 | $element_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 900 | $res = self::DecodeInput_Bytes($encoded, $element_start); 901 | } 902 | else 903 | { 904 | $res = self::DecodeInput_Generic($varType, $encoded, $index); 905 | } 906 | 907 | $array []= $res; 908 | $index += self::NUM_ZEROS * $element_offset; 909 | } 910 | 911 | return $array; 912 | } 913 | 914 | 915 | private static function DecodeInput_Generic($variableType, $encoded, $start) 916 | { 917 | if($variableType == VariableType::String) { 918 | return self::DecodeInput_String($encoded, $start); 919 | } 920 | else if($variableType == VariableType::UInt) { 921 | return self::DecodeInput_UInt($encoded, $start); 922 | } 923 | else if($variableType == VariableType::Int) { 924 | return self::DecodeInput_Int($encoded, $start); 925 | } 926 | else if($variableType == VariableType::Bool) { 927 | return self::DecodeInput_Bool($encoded, $start); 928 | } 929 | else if($variableType == VariableType::Address) { 930 | return self::DecodeInput_Address($encoded, $start); 931 | } 932 | else if($variableType == VariableType::BytesFixed) { 933 | return self::DecodeInput_BytesFixed($encoded, $start); 934 | } 935 | else if($variableType == VariableType::Bytes) { 936 | return self::DecodeInput_Bytes($encoded, $start); 937 | } 938 | } 939 | 940 | 941 | private static function DecodeInput_UInt_Internal($encoded, $start) 942 | { 943 | $partial = substr($encoded, $start, 64); 944 | $partial = self::RemoveZeros($partial, true); 945 | 946 | return hexdec($partial); 947 | } 948 | 949 | 950 | private static function DecodeInput_UInt($encoded, $start) 951 | { 952 | $partial = substr($encoded, $start, 64); 953 | $partial = self::RemoveZeros($partial, true); 954 | 955 | $partial_big = new BigNumber($partial, 16); 956 | 957 | return $partial_big; 958 | } 959 | 960 | 961 | private static function DecodeInput_Int($encoded, $start) 962 | { 963 | $partial = substr($encoded, $start, 64); 964 | $partial_big = new BigNumber($partial, -16); 965 | 966 | return $partial_big; 967 | } 968 | 969 | 970 | private static function DecodeInput_Bool($encoded, $start) 971 | { 972 | $partial = substr($encoded, $start, 64); 973 | $partial = self::RemoveZeros($partial, true); 974 | return $partial == '1'; 975 | } 976 | 977 | 978 | private static function DecodeInput_Address($encoded, $start) 979 | { 980 | $partial = self::RemoveZeros(substr($encoded, $start, 64), true); 981 | 982 | //add zero padding from left for 20 bytes 983 | $partial = str_pad($partial, 40, '0', STR_PAD_LEFT); 984 | 985 | return '0x'.$partial; 986 | } 987 | 988 | 989 | private static function DecodeInput_String($encoded, $start) 990 | { 991 | $length = self::DecodeInput_UInt_Internal($encoded, $start); 992 | $start += self::NUM_ZEROS; 993 | 994 | $partial = substr($encoded, $start, $length * 2); 995 | return hex2bin($partial); 996 | } 997 | 998 | 999 | private static function DecodeInput_BytesFixed($encoded, $start) 1000 | { 1001 | $partial = self::RemoveZeros(substr($encoded, $start, 64), false); 1002 | return hex2bin($partial); 1003 | } 1004 | 1005 | 1006 | private static function DecodeInput_Bytes($encoded, $start) 1007 | { 1008 | $length = self::DecodeInput_UInt_Internal($encoded, $start); 1009 | $start += self::NUM_ZEROS; 1010 | 1011 | $partial = substr($encoded, $start, $length * 2); 1012 | return hex2bin($partial); 1013 | } 1014 | 1015 | 1016 | private static function RemoveZeros($data, $remove_left) 1017 | { 1018 | $index = $remove_left ? 0 : strlen($data) - 1; 1019 | $current = substr($data, $index, 1); 1020 | while ($current == '0') 1021 | { 1022 | if ($remove_left) { 1023 | $data = substr($data, 1, strlen($data) - 1); 1024 | } 1025 | else { 1026 | $data = substr($data, 0, -1); 1027 | $index--; 1028 | } 1029 | $current = substr($data, $index, 1); 1030 | } 1031 | 1032 | return $data; 1033 | } 1034 | 1035 | 1036 | private static function GetOutputOffset ($output) : int 1037 | { 1038 | $output_type = is_string($output) ? $output : $output->type; 1039 | $varType = self::GetParameterType($output_type); 1040 | 1041 | if (Utils::stringContains($output_type, '[')) 1042 | { 1043 | $last_array_marker = strrpos($output->type, '['); 1044 | $last_array_marker_end = strrpos($output->type, ']'); 1045 | $length = (int) substr($output->type, $last_array_marker + 1, $last_array_marker_end - $last_array_marker - 1); 1046 | 1047 | if ($length > 0) 1048 | { 1049 | if ($varType == VariableType::Tuple) 1050 | { 1051 | if (!self::ExistsDynamicParameter($output->components)) { 1052 | return $length * self::GetOutputOffset_StaticComponents($output->components); 1053 | } 1054 | } 1055 | else if (self::IsStaticParameter($varType)) 1056 | { 1057 | return $length; 1058 | } 1059 | } 1060 | } 1061 | else if ($varType == VariableType::Tuple) 1062 | { 1063 | if (!self::ExistsDynamicParameter($output->components)) { 1064 | return self::GetOutputOffset_StaticComponents($output->components); 1065 | } 1066 | } 1067 | 1068 | return 1; 1069 | } 1070 | 1071 | 1072 | private static function GetOutputOffset_StaticComponents($components) : int 1073 | { 1074 | $offset = 0; 1075 | 1076 | foreach ($components as $comp) 1077 | { 1078 | $output_type = is_string($comp) ? $comp : $comp->type; 1079 | $varType = self::GetParameterType($output_type); 1080 | 1081 | if (Utils::stringContains($output_type, '[') || $varType == VariableType::Tuple) { 1082 | $offset += self::GetOutputOffset($comp); 1083 | } 1084 | else { 1085 | $offset++; 1086 | } 1087 | } 1088 | 1089 | return $offset; 1090 | } 1091 | 1092 | 1093 | 1094 | //EVENTS 1095 | 1096 | //parses event parameters 1097 | //event inputs are splitted between indexed topics and encoded data string 1098 | public function DecodeEvent($event_object, $log) : stdClass 1099 | { 1100 | $res = new stdClass(); 1101 | $res->indexed = array(); 1102 | $res->indexed []= $event_object->name; 1103 | 1104 | $res->data = array(); 1105 | 1106 | //split inputs between indexed and raw data 1107 | $indexed_index = 1; 1108 | $data_inputs = array(); 1109 | 1110 | foreach ($event_object->inputs as $input) 1111 | { 1112 | if ($input->indexed) 1113 | { 1114 | $input_type = is_string($input) ? $input : $input->type; 1115 | $varType = self::GetParameterType($input_type); 1116 | $res->indexed[$input->name] = $this->DecodeInput_Generic($varType, $log->topics[$indexed_index], 2); 1117 | 1118 | $indexed_index++; 1119 | } 1120 | else 1121 | { 1122 | $data_inputs []= $input; 1123 | } 1124 | } 1125 | 1126 | //parse raw data 1127 | $encoded = substr($log->data, 2); 1128 | $res->data = $this->DecodeGroup($data_inputs, $encoded, 0); 1129 | 1130 | //Return 1131 | return $res; 1132 | } 1133 | } -------------------------------------------------------------------------------- /src/Support/Formatter.php: -------------------------------------------------------------------------------- 1 | toHex(true); 50 | $padded = mb_substr($bnHex, 0, 1); 51 | 52 | if ($padded !== 'f') { 53 | $padded = '0'; 54 | } 55 | return implode('', array_fill(0, $digit - mb_strlen($bnHex), $padded)) . $bnHex; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Support/Hash.php: -------------------------------------------------------------------------------- 1 | keyFromPrivate($privateKey, 'hex'); 64 | $publicKey = $privateKey->getPublic(false, 'hex'); 65 | 66 | return $publicKey; 67 | } 68 | 69 | public static function getBase58CheckAddress(string $addressHex): string 70 | { 71 | $addressBin = hex2bin($addressHex); 72 | $hash0 = Hash::SHA256($addressBin); 73 | $hash1 = Hash::SHA256($hash0); 74 | $checksum = substr($hash1, 0, 4); 75 | $checksum = $addressBin . $checksum; 76 | 77 | return Base58::encode(Base58::bin2bc($checksum)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Support/Secp.php: -------------------------------------------------------------------------------- 1 | sign($message, $privateKey, ['canonical' => false]); 18 | 19 | return $sign->toHex() . bin2hex(implode('', array_map('chr', [$sign->getRecoveryParam()]))); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Support/Utils.php: -------------------------------------------------------------------------------- 1 | '0', 29 | 'wei' => '1', 30 | 'kwei' => '1000', 31 | 'Kwei' => '1000', 32 | 'babbage' => '1000', 33 | 'femtoether' => '1000', 34 | 'mwei' => '1000000', 35 | 'Mwei' => '1000000', 36 | 'lovelace' => '1000000', 37 | 'picoether' => '1000000', 38 | 'gwei' => '1000000000', 39 | 'Gwei' => '1000000000', 40 | 'shannon' => '1000000000', 41 | 'nanoether' => '1000000000', 42 | 'nano' => '1000000000', 43 | 'szabo' => '1000000000000', 44 | 'microether' => '1000000000000', 45 | 'micro' => '1000000000000', 46 | 'finney' => '1000000000000000', 47 | 'milliether' => '1000000000000000', 48 | 'milli' => '1000000000000000', 49 | 'ether' => '1000000000000000000', 50 | 'kether' => '1000000000000000000000', 51 | 'grand' => '1000000000000000000000', 52 | 'mether' => '1000000000000000000000000', 53 | 'gether' => '1000000000000000000000000000', 54 | 'tether' => '1000000000000000000000000000000' 55 | ]; 56 | 57 | 58 | 59 | /** 60 | * hexToBn 61 | * decoding hex number into decimal 62 | * 63 | * @param string $value 64 | * @return int 65 | */ 66 | public static function hexToBn($value) 67 | { 68 | $value = self::stripZero($value); 69 | return (new BigNumber($value, 16)); 70 | } 71 | 72 | /** 73 | * toHex 74 | * Encoding string or integer or numeric string(is not zero prefixed) or big number to hex. 75 | * 76 | * @param string|int|BigNumber $value 77 | * @param bool $isPrefix 78 | * @return string 79 | */ 80 | public static function toHex($value, $isPrefix=false) 81 | { 82 | if (is_numeric($value) && !is_float($value) && !is_double($value)) { 83 | // turn to hex number 84 | $bn = self::toBn($value); 85 | $hex = $bn->toHex(true); 86 | $hex = preg_replace('/^0+(?!$)/', '', $hex); 87 | } elseif (is_string($value)) { 88 | $value = self::stripZero($value); 89 | $hex = implode('', unpack('H*', $value)); 90 | } elseif ($value instanceof BigNumber) { 91 | $hex = $value->toHex(true); 92 | $hex = preg_replace('/^0+(?!$)/', '', $hex); 93 | } else { 94 | $type_error = gettype($value); 95 | throw new InvalidArgumentException("The value to Utils::toHex() function is not supported: value=$value type=$type_error. Only int, hex string, BigNumber or int string representation are allowed."); 96 | } 97 | 98 | if ($isPrefix) { 99 | return self::addZeroPrefix($hex); 100 | } 101 | return $hex; 102 | } 103 | 104 | /** 105 | * hexToBin 106 | * 107 | * @param string 108 | * @return string 109 | */ 110 | public static function hexToBin($value) 111 | { 112 | if (!is_string($value)) { 113 | throw new InvalidArgumentException('The value to hexToBin function must be string.'); 114 | } 115 | if (self::isZeroPrefixed($value)) { 116 | $count = 1; 117 | $value = str_replace('0x', '', $value, $count); 118 | } 119 | return pack('H*', $value); 120 | } 121 | 122 | /** 123 | * isZeroPrefixed 124 | * 125 | * @param string 126 | * @return bool 127 | */ 128 | public static function isZeroPrefixed($value) 129 | { 130 | if (!is_string($value)) { 131 | //throw new InvalidArgumentException('The value to isZeroPrefixed function must be string.'); 132 | } 133 | return (strpos($value, '0x') === 0); 134 | } 135 | 136 | /** 137 | * stripZero 138 | * 139 | * @param string $value 140 | * @return string 141 | */ 142 | public static function stripZero($value) 143 | { 144 | if (self::isZeroPrefixed($value)) { 145 | $count = 1; 146 | return str_replace('0x', '', $value, $count); 147 | } 148 | return $value; 149 | } 150 | 151 | /** 152 | * addZeroPrefix 153 | * 154 | * @param string 155 | * @return string 156 | */ 157 | public static function addZeroPrefix($value) 158 | { 159 | $value = '' . $value; 160 | 161 | if (self::isZeroPrefixed($value)) return $value; 162 | 163 | //remove leading 0s 164 | $value = ltrim($value, "0"); 165 | 166 | return '0x' . $value; 167 | } 168 | 169 | /** 170 | * forceAllNumbersHex 171 | * 172 | * @param object[] 173 | * @return object[] 174 | */ 175 | public static function forceAllNumbersHex($params) 176 | { 177 | foreach($params as $key => $param) 178 | { 179 | if ($key !== 'chainId') 180 | { 181 | if(is_numeric($param) || $param instanceof BigNumber) 182 | { 183 | $params[$key] = self::toHex($param, true); 184 | } 185 | else if(is_array($param)) 186 | { 187 | foreach($param as $sub_key => $sub_param) 188 | { 189 | if ($sub_key !== 'chainId') 190 | { 191 | if(is_numeric($sub_param) || $sub_param instanceof BigNumber) { 192 | $param[$sub_key] = self::toHex($sub_param, true); 193 | } 194 | } 195 | } 196 | 197 | $params[$key] = $param; 198 | } 199 | } 200 | } 201 | 202 | return $params; 203 | } 204 | 205 | 206 | 207 | /** 208 | * isNegative 209 | * 210 | * @param string 211 | * @return bool 212 | */ 213 | public static function isNegative($value) 214 | { 215 | if (!is_string($value)) { 216 | throw new InvalidArgumentException('The value to isNegative function must be string.'); 217 | } 218 | return (strpos($value, '-') === 0); 219 | } 220 | 221 | /** 222 | * isHex 223 | * 224 | * @param string $value 225 | * @return bool 226 | */ 227 | public static function isHex($value) 228 | { 229 | return (is_string($value) && preg_match('/^(0x)?[a-f0-9]*$/', $value) === 1); 230 | } 231 | 232 | /** 233 | * sha3 234 | * keccak256 235 | * 236 | * @param string $value 237 | * @return string 238 | */ 239 | public static function sha3($value) 240 | { 241 | if (!is_string($value)) { 242 | throw new InvalidArgumentException('The value to sha3 function must be string.'); 243 | } 244 | if (strpos($value, '0x') === 0) { 245 | $value = self::hexToBin($value); 246 | } 247 | $hash = Keccak::hash($value, 256); 248 | 249 | if ($hash === self::SHA3_NULL_HASH) { 250 | return null; 251 | } 252 | return '0x' . $hash; 253 | } 254 | 255 | /** 256 | * toString 257 | * 258 | * @param mixed $value 259 | * @return string 260 | */ 261 | public static function toString($value) 262 | { 263 | $value = (string) $value; 264 | 265 | return $value; 266 | } 267 | 268 | /** 269 | * toWei 270 | * Change number from unit to wei. 271 | * For example: 272 | * $wei = Utils::toWei('1', 'kwei'); 273 | * $wei->toString(); // 1000 274 | * 275 | * @param BigNumber|string $number 276 | * @param string $unit 277 | * @return \phpseclib3\Math\BigInteger 278 | */ 279 | public static function toWei($number, string $unit) 280 | { 281 | if (!isset(self::UNITS[$unit])) { 282 | throw new InvalidArgumentException('toWei doesn\'t support ' . $unit . ' unit.'); 283 | } 284 | 285 | return self::toWei_Internal($number, self::UNITS[$unit]); 286 | } 287 | 288 | 289 | /** 290 | * toWeiFromDecimals 291 | * Change number from unit that has decimals to wei. 292 | * For example: 293 | * $wei = Utils::toWeiFromDecimals('0.01', 8); //1000000 294 | * $wei->toString(); // 1000 295 | * 296 | * @param BigNumber|string $number 297 | * @param string $unit 298 | * @return \phpseclib3\Math\BigInteger 299 | */ 300 | public static function toWeiFromDecimals($number, int $numberOfDecimals) 301 | { 302 | $exponent = str_pad('1', $numberOfDecimals + 1, '0', STR_PAD_RIGHT); 303 | return self::toWei_Internal($number, $exponent); 304 | } 305 | 306 | 307 | /** 308 | * toWei_Internal 309 | * Internal private fucntion to convert a number in "unti" to string. 310 | * The unit string is 1000...000 having # decimal zero positions 311 | * 312 | * @param BigNumber|string $number 313 | * @param string $unit_value 314 | * @return \phpseclib3\Math\BigInteger 315 | */ 316 | private static function toWei_Internal($number, string $unit_value) 317 | { 318 | if (!is_string($number) && !($number instanceof BigNumber)) { 319 | throw new InvalidArgumentException('toWei number must be string or bignumber.'); 320 | } 321 | $bn = self::toBn($number); 322 | 323 | $bnt = new BigNumber($unit_value); 324 | 325 | if (is_array($bn)) { 326 | // fraction number 327 | list($whole, $fraction, $fractionLength, $negative1) = $bn; 328 | 329 | if ($fractionLength > strlen($unit_value)) { 330 | throw new InvalidArgumentException('toWei fraction part is out of limit.'); 331 | } 332 | $whole = $whole->multiply($bnt); 333 | 334 | $powerBase = (new BigNumber(10))->pow(new BigNumber($fractionLength)); 335 | $base = new BigNumber($powerBase->toString()); 336 | $fraction = $fraction->multiply($bnt)->divide($base)[0]; 337 | 338 | if ($negative1 !== false) { 339 | return $whole->add($fraction)->multiply($negative1); 340 | } 341 | return $whole->add($fraction); 342 | } 343 | 344 | return $bn->multiply($bnt); 345 | } 346 | 347 | 348 | /** 349 | * toEther 350 | * Change number from unit to ether. 351 | * For example: 352 | * list($bnq, $bnr) = Utils::toEther('1', 'kether'); 353 | * $bnq->toString(); // 1000 354 | * 355 | * @param BigNumber|string|int $number 356 | * @param string $unit 357 | * @return array 358 | */ 359 | public static function toEther($number, $unit) 360 | { 361 | // if ($unit === 'ether') { 362 | // throw new InvalidArgumentException('Please use another unit.'); 363 | // } 364 | $wei = self::toWei($number, $unit); 365 | $bnt = new BigNumber(self::UNITS['ether']); 366 | 367 | return $wei->divide($bnt); 368 | } 369 | 370 | 371 | /** 372 | * fromWei 373 | * Change number from wei to unit. 374 | * For example: 375 | * list($bnq, $bnr) = Utils::fromWei('1000', 'kwei'); 376 | * $bnq->toString(); // 1 377 | * 378 | * @param BigNumber|string|int $number 379 | * @param string $unit 380 | * @return \phpseclib3\Math\BigInteger 381 | */ 382 | public static function fromWei($number, $unit) 383 | { 384 | $bn = self::toBn($number); 385 | 386 | if (!is_string($unit)) { 387 | throw new InvalidArgumentException('fromWei unit must be string.'); 388 | } 389 | if (!isset(self::UNITS[$unit])) { 390 | throw new InvalidArgumentException('fromWei doesn\'t support ' . $unit . ' unit.'); 391 | } 392 | $bnt = new BigNumber(self::UNITS[$unit]); 393 | 394 | return $bn->divide($bnt); 395 | } 396 | 397 | 398 | 399 | /** 400 | * toWeiString 401 | * Change number from unit to wei. and show a string representation 402 | * For example: 403 | * $wei = Utils::toWeiString('1', 'kwei'); // 1000 404 | * 405 | * @param BigNumber|string $number 406 | * @param string $unit 407 | * @return string 408 | */ 409 | public static function toWeiString($number, $unit) : string 410 | { 411 | $conv = self::toWei($number, $unit); 412 | return $conv->toString(); 413 | } 414 | 415 | /** 416 | * toWeiStringFromDecimals 417 | * Change number from decimals to wei. and show a string representation 418 | * For example: 419 | * $wei = Utils::toWeiStringFromDecimals('1', 'kwei'); // 1000 420 | * 421 | * @param BigNumber|string $number 422 | * @param int $numberOfDecimals 423 | * @return string 424 | */ 425 | public static function toWeiStringFromDecimals($number, int $numberOfDecimals) : string 426 | { 427 | $conv = self::toWeiFromDecimals($number, $numberOfDecimals); 428 | return $conv->toString(); 429 | } 430 | 431 | 432 | /** 433 | * toEtherString 434 | * Change number from unit to ether. and show a string representation 435 | * For example: 436 | * $ether = Utils::toEtherString('1', 'kether'); // 1000 437 | * 438 | * @param BigNumber|string|int $number 439 | * @param string $unit 440 | * @return string 441 | */ 442 | public static function toEtherString($number, $unit) : string 443 | { 444 | $conversion = self::toEther($number, $unit); 445 | return self::transformDivisionToString($conversion, self::UNITS[$unit], self::UNITS['ether']); 446 | } 447 | 448 | 449 | /** 450 | * fromWeiToString 451 | * Change number from wei to unit. and show a string representation 452 | * For example: 453 | * $kwei = Utils::fromWei('1001', 'kwei'); // 1.001 454 | * 455 | * @param BigNumber|string|int $number 456 | * @param string $unit 457 | * @return \phpseclib3\Math\BigInteger 458 | */ 459 | public static function fromWeiToString($number, $unit) : string 460 | { 461 | $conversion = self::fromWei($number, $unit); 462 | return self::transformDivisionToString($conversion, self::UNITS['wei'], self::UNITS[$unit]); 463 | } 464 | 465 | 466 | /** 467 | * fromWeiToDecimalsString 468 | * Change number from wei to number of decimals. 469 | * For example: 470 | * $stringNumber = Utils::fromWeiToDecimalsString('1000000', 8); //0.01 471 | * 472 | * @param BigNumber|string|int $number 473 | * @param int $numberOfDecimals 474 | * @return string 475 | */ 476 | public static function fromWeiToDecimalsString($number, int $numberOfDecimals) : string 477 | { 478 | $bn = self::toBn($number); 479 | 480 | $exponent = str_pad('1', $numberOfDecimals + 1, '0', STR_PAD_RIGHT); 481 | 482 | $bnt = new BigNumber($exponent); 483 | 484 | $conversion = $bn->divide($bnt); 485 | 486 | return self::transformDivisionToString($conversion, self::UNITS['wei'], $exponent); 487 | } 488 | 489 | 490 | /** 491 | * transformDivisionToString 492 | * Internal private fucntion to convert a [quotient, remainder] BigNumber division result, 493 | * to a human readable unit.decimals (12.3920012000) 494 | * The unit string is 1000...000 having # decimal zero positions 495 | * 496 | * @param (\phpseclib3\Math\BigInteg, \phpseclib3\Math\BigInteg) $divisionArray 497 | * @param string $unitZerosOrigin string representing the origin unit's number of zeros 498 | * @param string $unitZerosOrigin string representing the origin unit's number of zeros 499 | * @return string 500 | */ 501 | private static function transformDivisionToString($divisionArray, $unitZerosOrigin, $unitZerosDestiny) : string 502 | { 503 | $left = $divisionArray[0]->toString(); 504 | $right = $divisionArray[1]->toString(); 505 | 506 | if ($right != "0") 507 | { 508 | $bnt_wei = new BigNumber($unitZerosOrigin); 509 | $bnt_unit = new BigNumber($unitZerosDestiny); 510 | 511 | $right_lead_zeros = strlen($bnt_unit->toString()) - strlen($bnt_wei->toString()) - strlen($right); 512 | 513 | for ($i = 0; $i < $right_lead_zeros; $i++) $right = '0' . $right; 514 | $right = rtrim($right, "0"); 515 | 516 | return $left . '.' . $right; 517 | } 518 | else 519 | { 520 | return $left; 521 | } 522 | } 523 | 524 | /** 525 | * jsonMethodToString 526 | * 527 | * @param stdClass|array $json 528 | * @return string 529 | */ 530 | public static function jsonMethodToString($json) : string 531 | { 532 | if ($json instanceof stdClass) { 533 | // one way to change whole json stdClass to array type 534 | // $jsonString = json_encode($json); 535 | 536 | // if (JSON_ERROR_NONE !== json_last_error()) { 537 | // throw new InvalidArgumentException('json_decode error: ' . json_last_error_msg()); 538 | // } 539 | // $json = json_decode($jsonString, true); 540 | 541 | // another way to change whole json to array type but need the depth 542 | // $json = self::jsonToArray($json, $depth) 543 | 544 | // another way to change json to array type but not whole json stdClass 545 | $json = (array) $json; 546 | $typeName = []; 547 | 548 | foreach ($json['inputs'] as $param) { 549 | if (isset($param->type)) { 550 | $typeName[] = $param->type; 551 | } 552 | } 553 | return $json['name'] . '(' . implode(',', $typeName) . ')'; 554 | } elseif (!is_array($json)) { 555 | throw new InvalidArgumentException('jsonMethodToString json must be array or stdClass.'); 556 | } 557 | if (isset($json['name']) && strpos($json['name'], '(') > 0) { 558 | return $json['name']; 559 | } 560 | $typeName = []; 561 | 562 | foreach ($json['inputs'] as $param) { 563 | if (isset($param['type'])) { 564 | $typeName[] = $param['type']; 565 | } 566 | } 567 | return $json['name'] . '(' . implode(',', $typeName) . ')'; 568 | } 569 | 570 | /** 571 | * jsonToArray 572 | * 573 | * @param stdClass|array $json 574 | * @return array 575 | */ 576 | public static function jsonToArray($json) 577 | { 578 | if ($json instanceof stdClass) { 579 | $json = (array) $json; 580 | $typeName = []; 581 | 582 | foreach ($json as $key => $param) { 583 | if (is_array($param)) { 584 | foreach ($param as $subKey => $subParam) { 585 | $json[$key][$subKey] = self::jsonToArray($subParam); 586 | } 587 | } elseif ($param instanceof stdClass) { 588 | $json[$key] = self::jsonToArray($param); 589 | } 590 | } 591 | } elseif (is_array($json)) { 592 | foreach ($json as $key => $param) { 593 | if (is_array($param)) { 594 | foreach ($param as $subKey => $subParam) { 595 | $json[$key][$subKey] = self::jsonToArray($subParam); 596 | } 597 | } elseif ($param instanceof stdClass) { 598 | $json[$key] = self::jsonToArray($param); 599 | } 600 | } 601 | } 602 | return $json; 603 | } 604 | 605 | /** 606 | * toBn 607 | * Change number or number string to bignumber. 608 | * 609 | * @param BigNumber|string|int $number 610 | * @return array|\phpseclib3\Math\BigInteger 611 | */ 612 | public static function toBn($number) 613 | { 614 | if ($number instanceof BigNumber){ 615 | $bn = $number; 616 | } 617 | elseif (is_int($number)) { 618 | $bn = new BigNumber($number); 619 | } 620 | elseif (is_numeric($number)) { 621 | $number = (string) $number; 622 | 623 | if (self::isNegative($number)) { 624 | $count = 1; 625 | $number = str_replace('-', '', $number, $count); 626 | $negative1 = new BigNumber(-1); 627 | } 628 | if (strpos($number, '.') > 0) { 629 | $comps = explode('.', $number); 630 | 631 | if (count($comps) > 2) { 632 | throw new InvalidArgumentException('toBn number must be a valid number.'); 633 | } 634 | $whole = $comps[0]; 635 | $fraction = $comps[1]; 636 | 637 | return [ 638 | new BigNumber($whole), 639 | new BigNumber($fraction), 640 | strlen($comps[1]), 641 | isset($negative1) ? $negative1 : false 642 | ]; 643 | } else { 644 | $bn = new BigNumber($number); 645 | } 646 | if (isset($negative1)) { 647 | $bn = $bn->multiply($negative1); 648 | } 649 | } 650 | elseif (is_string($number)) { 651 | $number = mb_strtolower($number); 652 | 653 | if (self::isNegative($number)) { 654 | $count = 1; 655 | $number = str_replace('-', '', $number, $count); 656 | $negative1 = new BigNumber(-1); 657 | } 658 | if (self::isZeroPrefixed($number) || preg_match('/^[0-9a-f]+$/i', $number) === 1) { 659 | $number = self::stripZero($number); 660 | $bn = new BigNumber($number, 16); 661 | } elseif (empty($number)) { 662 | $bn = new BigNumber(0); 663 | } else { 664 | throw new InvalidArgumentException('toBn number must be valid hex string.'); 665 | } 666 | if (isset($negative1)) { 667 | $bn = $bn->multiply($negative1); 668 | } 669 | } 670 | else { 671 | throw new InvalidArgumentException('toBn number must be BigNumber, string or int.'); 672 | } 673 | return $bn; 674 | } 675 | 676 | 677 | public static function getRandomHex(int $length) 678 | { 679 | return bin2hex(openssl_random_pseudo_bytes($length / 2)); 680 | } 681 | 682 | 683 | public static function stringContains(string $haystack, string $needle) 684 | { 685 | return empty($needle) || strpos($haystack, $needle) !== false; 686 | } 687 | 688 | public static function toDecimals($number, int $decimals = 6, int $keep_decimal = 4) 689 | { 690 | $base = bcpow('10', $decimals, 0); 691 | return bcdiv($number, $base, $keep_decimal); 692 | } 693 | 694 | public static function addressToHex(string $address) : string { 695 | if(strlen($address) == 42 and str_starts_with($address,strval(41))): 696 | return $address; 697 | else: 698 | return Base58::decodeAddress($address); 699 | endif; 700 | } 701 | 702 | public static function hexToAddress(string $address) : string { 703 | if(ctype_xdigit($address)): 704 | return Base58::encodeAddress($address); 705 | else: 706 | return $address; 707 | endif; 708 | } 709 | 710 | /** 711 | * 验证TRC地址是否有效 712 | */ 713 | public static function verifyAddress(string $address) : bool { 714 | if(preg_match('/^T[A-HJ-NP-Za-km-z1-9]{33}$/',$address)): 715 | $hex = self::addressToHex($address); 716 | $wallet = self::hexToAddress($hex); 717 | return $wallet === $address; 718 | else: 719 | return false; 720 | endif; 721 | } 722 | 723 | /** 724 | * 根据精度展示资产 725 | */ 726 | public static function formatBalance($number, int $decimals = 6){ 727 | return number_format(self::toDecimals($number, $decimals), 2); 728 | } 729 | 730 | /** 731 | * 使用私钥获取公钥 732 | */ 733 | public static function getPublicKeyFromPrivateKey($private_key) { 734 | $ec = new EC('secp256k1'); 735 | $keyPair = $ec->keyFromPrivate($private_key); 736 | return $keyPair->getPublic()->encode('hex'); 737 | } 738 | 739 | /** 740 | * 获取指点时间戳的Unix 741 | */ 742 | public static function getUnixTimestamp($minute = 0, $need_millisecond = true){ 743 | if($minute == 0){ 744 | $symbol = "now"; 745 | } 746 | else if($minute > 0){ 747 | $symbol = "+{$minute}"; 748 | } 749 | else{ 750 | $symbol = "{$minute}"; 751 | } 752 | 753 | return $need_millisecond ? strtotime($symbol)."000" : strtotime($symbol); 754 | } 755 | } -------------------------------------------------------------------------------- /src/TRC20.php: -------------------------------------------------------------------------------- 1 | contractAddress = $options['contract_address']; 19 | $this->decimals = isset($options['decimals']) ? $options['decimals'] : 1e6; 20 | } 21 | 22 | /** 23 | * 获取Token余额 24 | */ 25 | public function balance(string $address, bool $sun = false) 26 | { 27 | $format = Formatter::toAddressFormat($address); 28 | $body = $this->api->post('/wallet/triggersmartcontract', [ 29 | 'contract_address' => $this->contractAddress, 30 | 'function_selector' => 'balanceOf(address)', 31 | 'parameter' => $format, 32 | 'owner_address' => $address, 33 | ]); 34 | 35 | if (isset($body->result->code)) { 36 | throw new TronErrorException(hex2bin($body->result->message)); 37 | } 38 | 39 | $balance = Utils::formatBalance(hexdec($body->constant_result[0]), $this->decimals); 40 | return $balance; 41 | } 42 | 43 | /** 44 | * 发送TRC20交易 45 | */ 46 | public function transferTRC20(string $private_key, string $from, string $to, float $amount, $message = null, float $fee_limit = 150000000): Transaction 47 | { 48 | $toFormat = Formatter::toAddressFormat($to); 49 | $trans_amount = $this->toTronValue($amount, $this->decimals); 50 | //$abi_encoded = ABI::EncodeParameters_External(['address', 'uint256'], [$toFormat, $trans_amount]); 51 | $numberFormat = Formatter::toIntegerFormat($trans_amount); 52 | 53 | $body = $this->api->post('/wallet/triggersmartcontract', [ 54 | 'contract_address' => $this->contractAddress, 55 | 'function_selector' => 'transfer(address,uint256)', 56 | 'parameter' => "{$toFormat}{$numberFormat}", 57 | 'fee_limit' => $fee_limit, 58 | 'owner_address' => $from, 59 | ]); 60 | 61 | if (isset($body['result']['code'])) { 62 | throw new TransactionException(hex2bin($body['result']['message'])); 63 | } 64 | 65 | $tradeobj = $this->signTransaction($private_key, $body['transaction']); 66 | if(!is_null($message)) $tradeobj['raw_data']->data = bin2hex($message); 67 | $response = $this->sendRawTransaction($tradeobj); 68 | 69 | if (isset($response['result']) && $response['result'] == true) { 70 | return new Transaction( 71 | $body['transaction']['txID'], 72 | $body['transaction']['raw_data'], 73 | 'PACKING' 74 | ); 75 | } else { 76 | throw new TransactionException('Transfer Fail'); 77 | } 78 | 79 | /** 80 | * 输出格式 81 | */ 82 | // array(2) { 83 | // ["result"]=> 84 | // array(1) { 85 | // ["result"]=> 86 | // bool(true) 87 | // } 88 | // ["transaction"]=> 89 | // array(4) { 90 | // ["visible"]=> 91 | // bool(false) 92 | // ["txID"]=> 93 | // string(64) "da36d6703ca7a2e80f541239604060e1e3ac1067918119e29016e3a6669ded7d" 94 | // ["raw_data"]=> 95 | // array(6) { 96 | // ["contract"]=> 97 | // array(1) { 98 | // [0]=> 99 | // array(2) { 100 | // ["parameter"]=> 101 | // array(2) { 102 | // ["value"]=> 103 | // array(3) { 104 | // ["data"]=> 105 | // string(136) "a9059cbb00000000000000000000004181675c7bfef3ed7628328c9e17bf466dcd4c0d7f00000000000000000000000000000000000000000000000000000000000f4240" 106 | // ["owner_address"]=> 107 | // string(42) "4141649143892622978c04834fe90e1d5a2b9d3bfa" 108 | // ["contract_address"]=> 109 | // string(42) "41a614f803b6fd780986a42c78ec9c7f77e6ded13c" 110 | // } 111 | // ["type_url"]=> 112 | // string(49) "type.googleapis.com/protocol.TriggerSmartContract" 113 | // } 114 | // ["type"]=> 115 | // string(20) "TriggerSmartContract" 116 | // } 117 | // } 118 | // ["ref_block_bytes"]=> 119 | // string(4) "b130" 120 | // ["ref_block_hash"]=> 121 | // string(16) "a6f4f6f62098062e" 122 | // ["expiration"]=> 123 | // int(1674849414000) 124 | // ["fee_limit"]=> 125 | // int(100000000) 126 | // ["timestamp"]=> 127 | // int(1674849356035) 128 | // } 129 | // ["raw_data_hex"]=> 130 | // string(422) "0a02b1302208a6f4f6f62098062e40f0d6b7a6df305aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a154141649143892622978c04834fe90e1d5a2b9d3bfa121541a614f803b6fd780986a42c78ec9c7f77e6ded13c2244a9059cbb00000000000000000000004181675c7bfef3ed7628328c9e17bf466dcd4c0d7f00000000000000000000000000000000000000000000000000000000000f4240708392b4a6df30900180c2d72f" 131 | // } 132 | // } 133 | } 134 | 135 | /** 136 | * 获取地址交易记录 137 | */ 138 | public function getTransactionsByTrc20(string $address, int $mintimestamp = null, int $maxtimestamp = null, bool $confirmed = null, bool $to = false,bool $from = false, $limit = 20) 139 | { 140 | $data = [ 141 | 'contract_address' => $this->contractAddress, 142 | 'only_to' => $to, 143 | 'only_from' => $from, 144 | 'limit' => max(min($limit,200),20) 145 | ]; 146 | if($mintimestamp != null){ 147 | $data['min_timestamp'] = date('Y-m-d\TH:i:s.v\Z',$mintimestamp); 148 | } 149 | if($maxtimestamp != null){ 150 | $data['max_timestamp'] = date('Y-m-d\TH:i:s.v\Z',$maxtimestamp); 151 | } 152 | if(!is_null($confirmed)){ 153 | $data[$confirmed ? 'only_confirmed' : 'only_unconfirmed'] = true; 154 | } 155 | 156 | $url_param = http_build_query($data); 157 | 158 | $url = "v1/accounts/{$address}/transactions/trc20?{$url_param}"; 159 | $body = $this->api->get($url); 160 | 161 | if (isset($body['data']) && $body['success']) { 162 | return $body['data']; 163 | } 164 | throw new TransactionException('Transaction Fail'); 165 | } 166 | 167 | /** 168 | * 获取钱包的支出记录 169 | */ 170 | public function getTransactionsFromAddress(string $address,int $limit = 20) : object { 171 | return $this->getTransactionsByTrc20(address : $address,limit : $limit, from : true); 172 | } 173 | 174 | /** 175 | * 获取钱包地址的收入记录 176 | */ 177 | public function getTransactionsToAddress(string $address,int $limit = 20) : object { 178 | return $this->getTransactionsByTrc20(address : $address,limit : $limit, to : true); 179 | } 180 | 181 | /** 182 | * Sign the transaction, the api has the risk of leaking the private key, 183 | * please make sure to call the api in a secure environment 184 | * 185 | * @param $transaction 186 | * @param string|null $message 187 | * @return array 188 | * @throws TronErrorException 189 | */ 190 | private function signTransaction($private_key, $transaction, string $message = null): array 191 | { 192 | if(!is_array($transaction)) { 193 | throw new TronErrorException('Invalid transaction provided'); 194 | } 195 | 196 | if(isset($transaction['Error'])) 197 | throw new TronErrorException($transaction['Error']); 198 | 199 | 200 | if(isset($transaction['signature'])) { 201 | throw new TronErrorException('Transaction is already signed'); 202 | } 203 | 204 | if(!is_null($message)) { 205 | $transaction['raw_data']['data'] = bin2hex($message); 206 | } 207 | $signature = Support\Secp::sign($transaction['txID'], $private_key); 208 | $transaction['signature'] = [$signature]; 209 | 210 | return $transaction; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/TRX.php: -------------------------------------------------------------------------------- 1 | api = new Api($apiurl, $options); 24 | } 25 | 26 | /** 27 | * 生成钱包地址 28 | */ 29 | public function generateAddress(): array 30 | { 31 | $ec = new EC('secp256k1'); 32 | $key = $ec->genKeyPair(); 33 | $priv = $ec->keyFromPrivate($key->priv); 34 | $privKey = $priv->getPrivate(enc : 'hex'); 35 | $pubKey = $priv->getPublic(enc : 'hex'); 36 | $address = $this->getAddressHexFromPublicKey($pubKey); 37 | if(!$this->validateAddress($address)) throw new TronErrorException('address validate fail!'); 38 | $wallet = Utils::hexToAddress($address); 39 | return [ 40 | 'privatekey'=>$privKey, 41 | 'publickey'=>$pubKey, 42 | 'address'=>$address, 43 | 'wallet'=>$wallet 44 | ]; 45 | } 46 | 47 | /** 48 | * 生成带助记词的钱包地址 49 | */ 50 | public function generateAddressWithMnemonic(): array 51 | { 52 | $ec = new EC('secp256k1'); 53 | $key = $ec->genKeyPair(); 54 | $priv = $ec->keyFromPrivate($key->priv); 55 | $privKey = $priv->getPrivate(enc : 'hex'); 56 | $pubKey = $priv->getPublic(enc : 'hex'); 57 | $mnemonic = $this->getPhraseFromPrivateKey($privKey); 58 | $calcPrivKey = $this->getPrivateKeyFromPhrase($mnemonic); 59 | if($calcPrivKey != $privKey) throw new TronErrorException('mnemonic validate fail!'); 60 | $address = $this->getAddressHexFromPublicKey($pubKey); 61 | if(!$this->validateAddress($address)) throw new TronErrorException('address validate fail!'); 62 | $wallet = Utils::hexToAddress($address); 63 | 64 | return [ 65 | 'privatekey'=>$privKey, 66 | 'publickey'=>$pubKey, 67 | 'address'=>$address, 68 | 'wallet'=>$wallet, 69 | 'mnemonic' => $mnemonic 70 | ]; 71 | } 72 | 73 | /** 74 | * 从公钥获取钱包地址 75 | */ 76 | public function getAddressHexFromPublicKey(string $publickey) : string { 77 | $publickey = hex2bin($publickey); 78 | $publickey = substr($publickey,-64); 79 | $hash = Keccak::hash($publickey,256); 80 | return strval(41).substr($hash,24); 81 | } 82 | 83 | /** 84 | * 使用Net方式验证钱包地址 85 | */ 86 | public function validateAddress(string $address): bool 87 | { 88 | $res = $this->api->post('/wallet/validateaddress', [ 89 | 'address' => $address, 90 | ]); 91 | 92 | return !$res || empty($res['result']) ? false : true; 93 | } 94 | 95 | /** 96 | * 获取账户信息 97 | */ 98 | public function accountInfo($address) 99 | { 100 | $url = "v1/accounts/{$address}?only_confirmed=true"; 101 | $body = $this->api->get($url); 102 | 103 | if (isset($body['error'])) { 104 | throw new TransactionException($body['error']); 105 | } 106 | if (isset($body['data']) && $body['success']) { 107 | if (sizeof($body['data'])){ 108 | return $body['data'][0]; 109 | } 110 | } 111 | throw new TransactionException('get accountInfo fail'); 112 | } 113 | 114 | /** 115 | * 使用TXID获取交易信息 116 | */ 117 | public function getTransactionInfoById($txid) 118 | { 119 | $body = $this->api->post('/wallet/gettransactioninfobyid', [ 120 | 'value' => $txid 121 | ]); 122 | if (isset($body->result->code)) { 123 | throw new TronErrorException(hex2bin($body->result->message)); 124 | } 125 | return $body; 126 | } 127 | 128 | /** 129 | * 私钥获取钱包地址 130 | */ 131 | public function getAddressByPrivateKey(string $privateKeyHex): Address 132 | { 133 | try { 134 | $addressHex = Address::ADDRESS_PREFIX . SupportKey::privateKeyToAddress($privateKeyHex); 135 | $addressBase58 = SupportKey::getBase58CheckAddress($addressHex); 136 | } catch (InvalidArgumentException $e) { 137 | throw new TronErrorException($e->getMessage()); 138 | } 139 | $address = new Address($addressBase58, $privateKeyHex); 140 | $validAddress = $this->validateAddress($address->address); 141 | if (!$validAddress) { 142 | throw new TronErrorException('Invalid private key'); 143 | } 144 | 145 | return $address; 146 | } 147 | 148 | public function getAccount(string $address){ 149 | $account = $this->api->post('walletsolidity/getaccount',[ 150 | 'address' => Utils::addressToHex($address), 151 | ]); 152 | return $account; 153 | } 154 | 155 | /** 156 | * 获取TRX余额 157 | */ 158 | public function balance(string $address, bool $sun = false) 159 | { 160 | $account = $this->getAccount($address); 161 | return ($sun ? $account['balance'] : Utils::toDecimals($account['balance'])); 162 | } 163 | 164 | public function transfer(string $private_key, string $from, string $to, float $amount, $message = null): Transaction 165 | { 166 | if($from === $to) throw new InvalidArgumentException('The from and to arguments cannot be the same !'); 167 | $data = [ 168 | 'owner_address' => $from, 169 | 'to_address' => $to, 170 | 'amount'=> $this->toTronValue($amount) 171 | ]; 172 | 173 | $transaction = $this->api->post('wallet/createtransaction',$data); 174 | 175 | $signature = $this->signature($private_key, $transaction); 176 | if(!is_null($message)) $signature['raw_data']->data = bin2hex($message); 177 | $broadcast = (array) $this->sendRawTransaction($signature); 178 | 179 | if (isset($broadcast['result']) && $broadcast['result'] == true) { 180 | return new Transaction( 181 | $transaction['txID'], 182 | $transaction['raw_data'], 183 | 'PACKING' 184 | ); 185 | } else { 186 | throw new TransactionException(hex2bin($broadcast['message'])); 187 | } 188 | } 189 | 190 | /** 191 | * 获取当前区块信息 192 | */ 193 | public function getNowBlock(): Block 194 | { 195 | $block = $this->api->post('wallet/getnowblock'); 196 | $transactions = isset($block['transactions']) ? $block['transactions'] : []; 197 | return new Block($block['blockID'], $block['block_header'], $transactions); 198 | } 199 | 200 | 201 | /** 202 | * 使用blockID获取区块信息 203 | */ 204 | public function getBlockByNum(int $blockID): Block 205 | { 206 | $block = $this->api->post('wallet/getblockbynum', [ 207 | 'num' => intval($blockID) 208 | ]); 209 | 210 | $transactions = isset($block['transactions']) ? $block['transactions'] : []; 211 | return new Block($block['blockID'], $block['block_header'], $transactions); 212 | } 213 | 214 | /** 215 | * 使用Hash获取交易详情 216 | */ 217 | public function getTransactionById(string $txHash): Transaction 218 | { 219 | $response = $this->api->post('wallet/gettransactionbyid', [ 220 | 'value' => $txHash 221 | ]); 222 | 223 | if(!$response) { 224 | throw new TronErrorException('Transaction not found'); 225 | } 226 | 227 | return new Transaction( 228 | $response['txID'], 229 | $response['raw_data'], 230 | $response['ret'][0]['contractRet'] ?? '' 231 | ); 232 | } 233 | 234 | /** 235 | * 从私钥获取助记词 236 | */ 237 | public function getPhraseFromPrivateKey(string $privatekey,int $base = 16) : string { 238 | if(extension_loaded('gmp')): 239 | $words = $this->getWords(); 240 | srand($base); 241 | shuffle($words); 242 | $integer = gmp_init($privatekey,$base); 243 | $split = str_split(gmp_strval($integer),3); 244 | foreach($split as $number => $i): 245 | if(count($split) === ($number + 1)): 246 | if(str_starts_with($i,'00')): // strlen($i) === 2 || 3 && $i in range(0,9) 247 | $phrases []= $words[intval($i) + 2000 + (strlen($i) * 10)]; 248 | elseif(str_starts_with($i,'0')): // strlen($i) === 1 || 2 || 3 && $i in range(0,99) 249 | $phrases []= $words[intval($i) + 1000 + (strlen($i) * 100)]; 250 | else: 251 | $phrases []= $words[intval($i) + 0]; 252 | endif; 253 | else: 254 | $phrases []= $words[intval($i)]; 255 | endif; 256 | endforeach; 257 | else: 258 | throw new TronErrorException('gmp extension is needed !'); 259 | endif; 260 | return implode(chr(32),$phrases); 261 | } 262 | 263 | /** 264 | * 从助记词获取私钥 265 | */ 266 | public function getPrivateKeyFromPhrase(string $phrase,int $base = 16) : string { 267 | if(extension_loaded('gmp')): 268 | $words = $this->getWords(); 269 | srand($base); 270 | shuffle($words); 271 | $split = explode(chr(32),$phrase); 272 | $integer = []; 273 | $new_privatekey = ''; 274 | foreach($split as $number => $i): 275 | $index = array_search($i,$words); 276 | if($index === false): 277 | throw new TronErrorException('The word '.$i.' was not found !'); 278 | else: 279 | if(count($split) === ($number + 1)): 280 | if($index >= 2000): 281 | $index -= 2000; // A number to recognize zeros 282 | $repeat = intdiv($index,10); 283 | $index -= ($repeat * 10); // strlen($i) === 2 || 3 284 | $last = str_pad(strval($index),$repeat,strval(0),STR_PAD_LEFT); 285 | elseif($index >= 1000): 286 | $index -= 1000; // A number to recognize zeros 287 | $repeat = intdiv($index,100); // strlen($i) === 1 || 2 || 3 288 | $index -= ($repeat * 100); 289 | $last = str_pad(strval($index),$repeat,strval(0),STR_PAD_LEFT); 290 | else: 291 | $last = strval($index); 292 | endif; 293 | var_dump(implode($integer).$last); 294 | $new_privatekey = gmp_strval(implode($integer).$last,$base); 295 | $new_privatekey = strlen($new_privatekey) % 2 ? strval(0).$new_privatekey : $new_privatekey; 296 | else: 297 | $index = str_pad(strval($index),3,strval(0),STR_PAD_LEFT); 298 | $integer []= $index; 299 | endif; 300 | endif; 301 | endforeach; 302 | return $new_privatekey; 303 | else: 304 | throw new TronErrorException('gmp extension is needed !'); 305 | endif; 306 | } 307 | 308 | public function getTransactionsRelated(string $address, bool $confirmed = null,bool $to = false,bool $from = false,bool $searchinternal = true,int $limit = 20,string $order = 'block_timestamp,desc',int $mintimestamp = null,int $maxtimestamp = null) : object { 309 | $data = [ 310 | 'only_to' => $to, 311 | 'only_from' => $from, 312 | 'search_internal' => $searchinternal, 313 | 'limit' => max(min($limit,200),20), 314 | 'order_by' => $order 315 | ]; 316 | if(!is_null($confirmed)){ 317 | $data[$confirmed ? 'only_confirmed' : 'only_unconfirmed'] = true; 318 | } 319 | 320 | if(!is_null($mintimestamp)){ 321 | $data['min_timestamp'] = date('Y-m-d\TH:i:s.v\Z',$mintimestamp); 322 | } 323 | if(!is_null($maxtimestamp)){ 324 | $data['max_timestamp'] = date('Y-m-d\TH:i:s.v\Z',$maxtimestamp); 325 | } 326 | $transactions = $this->api->get('v1/accounts/'.$address.'/transactions',$data); 327 | return $transactions; 328 | } 329 | 330 | /** 331 | * 获取钱包地址的交易记录 332 | */ 333 | public function getTransactionsByAddress(string $address,int $limit = 20) : object { 334 | return $this->getTransactionsRelated(address : $address,limit : $limit); 335 | } 336 | 337 | /** 338 | * 获取钱包的支出记录 339 | */ 340 | public function getTransactionsFromAddress(string $address,int $limit = 20) : object { 341 | return $this->getTransactionsRelated(address : $address,limit : $limit, from : true); 342 | } 343 | 344 | /** 345 | * 获取钱包地址的收入记录 346 | */ 347 | public function getTransactionsToAddress(string $address,int $limit = 20) : object { 348 | return $this->getTransactionsRelated(address : $address,limit : $limit, to : true); 349 | } 350 | 351 | protected function sendRawTransaction(array $response) : object { 352 | if(isset($response['signature']) === false or is_array($response['signature']) === false) throw new InvalidArgumentException('response has not been signature !'); 353 | $broadcast = $this->api->post('wallet/broadcasttransaction', $response); 354 | return $broadcast; 355 | } 356 | 357 | private function signature(string $private_key, array $response) : array { 358 | if(!empty($private_key)): 359 | if(isset($response['Error'])): 360 | throw new TronErrorException($response['Error']); 361 | else: 362 | if(isset($response['signature'])): 363 | throw new TronErrorException('response is already signed !'); 364 | elseif(isset($response['txID']) === false): 365 | throw new TronErrorException('The response does not have txID key !'); 366 | else: 367 | $signature = Secp::sign($response['txID'], $private_key); 368 | $response['signature'] = array($signature); 369 | endif; 370 | endif; 371 | else: 372 | throw new TronErrorException('private key is not set'); 373 | endif; 374 | return $response; 375 | } 376 | 377 | private function getWords() : array { 378 | if(file_exists(__DIR__.DIRECTORY_SEPARATOR.'english.txt') === false) throw new TronErrorException('english.txt file doesn\'t exists !'); 379 | return explode(PHP_EOL,file_get_contents(__DIR__.DIRECTORY_SEPARATOR.'english.txt')); 380 | } 381 | 382 | /** 383 | * Convert float to trx format 384 | * 385 | * @param $double 386 | * @return int 387 | */ 388 | protected function toTronValue($double, $decimals = 1e6): int { 389 | return (int) bcmul((string)$double, (string)$decimals,0); 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /src/Transaction.php: -------------------------------------------------------------------------------- 1 | txID = $txID; 15 | $this->raw_data = $rawData; 16 | $this->contractRet = $contractRet; 17 | } 18 | 19 | public function isSigned(): bool 20 | { 21 | return (bool)sizeof($this->signature); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Transactions.php: -------------------------------------------------------------------------------- 1 | transactions = array_filter($transactions,fn(object $transaction) : bool => isset($transaction->success) and $transaction->success === true); 15 | $this->position = 0; 16 | } 17 | public function current() : mixed { 18 | return $this->transactions[$this->position]; 19 | } 20 | public function key() : mixed { 21 | return $this->position; 22 | } 23 | public function next() : void { 24 | $transaction = $this->transactions[$this->position]; 25 | if(isset($transaction->meta->links->next)): 26 | $transactions = $this->sender->request('GET',$transaction->meta->links->next); 27 | if(isset($transactions->success) and $transactions->success === true): 28 | $transactions->iterator = new self($this->sender,array($transactions)); 29 | endif; 30 | $this->transactions []= $transactions; 31 | endif; 32 | $this->position++; 33 | } 34 | public function rewind() : void { 35 | $this->position = 0; 36 | } 37 | public function valid() : bool { 38 | return isset($this->transactions[$this->position]); 39 | } 40 | } -------------------------------------------------------------------------------- /src/english.txt: -------------------------------------------------------------------------------- 1 | abandon 2 | ability 3 | able 4 | about 5 | above 6 | absent 7 | absorb 8 | abstract 9 | absurd 10 | abuse 11 | access 12 | accident 13 | account 14 | accuse 15 | achieve 16 | acid 17 | acoustic 18 | acquire 19 | across 20 | act 21 | action 22 | actor 23 | actress 24 | actual 25 | adapt 26 | add 27 | addict 28 | address 29 | adjust 30 | admit 31 | adult 32 | advance 33 | advice 34 | aerobic 35 | affair 36 | afford 37 | afraid 38 | again 39 | age 40 | agent 41 | agree 42 | ahead 43 | aim 44 | air 45 | airport 46 | aisle 47 | alarm 48 | album 49 | alcohol 50 | alert 51 | alien 52 | all 53 | alley 54 | allow 55 | almost 56 | alone 57 | alpha 58 | already 59 | also 60 | alter 61 | always 62 | amateur 63 | amazing 64 | among 65 | amount 66 | amused 67 | analyst 68 | anchor 69 | ancient 70 | anger 71 | angle 72 | angry 73 | animal 74 | ankle 75 | announce 76 | annual 77 | another 78 | answer 79 | antenna 80 | antique 81 | anxiety 82 | any 83 | apart 84 | apology 85 | appear 86 | apple 87 | approve 88 | april 89 | arch 90 | arctic 91 | area 92 | arena 93 | argue 94 | arm 95 | armed 96 | armor 97 | army 98 | around 99 | arrange 100 | arrest 101 | arrive 102 | arrow 103 | art 104 | artefact 105 | artist 106 | artwork 107 | ask 108 | aspect 109 | assault 110 | asset 111 | assist 112 | assume 113 | asthma 114 | athlete 115 | atom 116 | attack 117 | attend 118 | attitude 119 | attract 120 | auction 121 | audit 122 | august 123 | aunt 124 | author 125 | auto 126 | autumn 127 | average 128 | avocado 129 | avoid 130 | awake 131 | aware 132 | away 133 | awesome 134 | awful 135 | awkward 136 | axis 137 | baby 138 | bachelor 139 | bacon 140 | badge 141 | bag 142 | balance 143 | balcony 144 | ball 145 | bamboo 146 | banana 147 | banner 148 | bar 149 | barely 150 | bargain 151 | barrel 152 | base 153 | basic 154 | basket 155 | battle 156 | beach 157 | bean 158 | beauty 159 | because 160 | become 161 | beef 162 | before 163 | begin 164 | behave 165 | behind 166 | believe 167 | below 168 | belt 169 | bench 170 | benefit 171 | best 172 | betray 173 | better 174 | between 175 | beyond 176 | bicycle 177 | bid 178 | bike 179 | bind 180 | biology 181 | bird 182 | birth 183 | bitter 184 | black 185 | blade 186 | blame 187 | blanket 188 | blast 189 | bleak 190 | bless 191 | blind 192 | blood 193 | blossom 194 | blouse 195 | blue 196 | blur 197 | blush 198 | board 199 | boat 200 | body 201 | boil 202 | bomb 203 | bone 204 | bonus 205 | book 206 | boost 207 | border 208 | boring 209 | borrow 210 | boss 211 | bottom 212 | bounce 213 | box 214 | boy 215 | bracket 216 | brain 217 | brand 218 | brass 219 | brave 220 | bread 221 | breeze 222 | brick 223 | bridge 224 | brief 225 | bright 226 | bring 227 | brisk 228 | broccoli 229 | broken 230 | bronze 231 | broom 232 | brother 233 | brown 234 | brush 235 | bubble 236 | buddy 237 | budget 238 | buffalo 239 | build 240 | bulb 241 | bulk 242 | bullet 243 | bundle 244 | bunker 245 | burden 246 | burger 247 | burst 248 | bus 249 | business 250 | busy 251 | butter 252 | buyer 253 | buzz 254 | cabbage 255 | cabin 256 | cable 257 | cactus 258 | cage 259 | cake 260 | call 261 | calm 262 | camera 263 | camp 264 | can 265 | canal 266 | cancel 267 | candy 268 | cannon 269 | canoe 270 | canvas 271 | canyon 272 | capable 273 | capital 274 | captain 275 | car 276 | carbon 277 | card 278 | cargo 279 | carpet 280 | carry 281 | cart 282 | case 283 | cash 284 | casino 285 | castle 286 | casual 287 | cat 288 | catalog 289 | catch 290 | category 291 | cattle 292 | caught 293 | cause 294 | caution 295 | cave 296 | ceiling 297 | celery 298 | cement 299 | census 300 | century 301 | cereal 302 | certain 303 | chair 304 | chalk 305 | champion 306 | change 307 | chaos 308 | chapter 309 | charge 310 | chase 311 | chat 312 | cheap 313 | check 314 | cheese 315 | chef 316 | cherry 317 | chest 318 | chicken 319 | chief 320 | child 321 | chimney 322 | choice 323 | choose 324 | chronic 325 | chuckle 326 | chunk 327 | churn 328 | cigar 329 | cinnamon 330 | circle 331 | citizen 332 | city 333 | civil 334 | claim 335 | clap 336 | clarify 337 | claw 338 | clay 339 | clean 340 | clerk 341 | clever 342 | click 343 | client 344 | cliff 345 | climb 346 | clinic 347 | clip 348 | clock 349 | clog 350 | close 351 | cloth 352 | cloud 353 | clown 354 | club 355 | clump 356 | cluster 357 | clutch 358 | coach 359 | coast 360 | coconut 361 | code 362 | coffee 363 | coil 364 | coin 365 | collect 366 | color 367 | column 368 | combine 369 | come 370 | comfort 371 | comic 372 | common 373 | company 374 | concert 375 | conduct 376 | confirm 377 | congress 378 | connect 379 | consider 380 | control 381 | convince 382 | cook 383 | cool 384 | copper 385 | copy 386 | coral 387 | core 388 | corn 389 | correct 390 | cost 391 | cotton 392 | couch 393 | country 394 | couple 395 | course 396 | cousin 397 | cover 398 | coyote 399 | crack 400 | cradle 401 | craft 402 | cram 403 | crane 404 | crash 405 | crater 406 | crawl 407 | crazy 408 | cream 409 | credit 410 | creek 411 | crew 412 | cricket 413 | crime 414 | crisp 415 | critic 416 | crop 417 | cross 418 | crouch 419 | crowd 420 | crucial 421 | cruel 422 | cruise 423 | crumble 424 | crunch 425 | crush 426 | cry 427 | crystal 428 | cube 429 | culture 430 | cup 431 | cupboard 432 | curious 433 | current 434 | curtain 435 | curve 436 | cushion 437 | custom 438 | cute 439 | cycle 440 | dad 441 | damage 442 | damp 443 | dance 444 | danger 445 | daring 446 | dash 447 | daughter 448 | dawn 449 | day 450 | deal 451 | debate 452 | debris 453 | decade 454 | december 455 | decide 456 | decline 457 | decorate 458 | decrease 459 | deer 460 | defense 461 | define 462 | defy 463 | degree 464 | delay 465 | deliver 466 | demand 467 | demise 468 | denial 469 | dentist 470 | deny 471 | depart 472 | depend 473 | deposit 474 | depth 475 | deputy 476 | derive 477 | describe 478 | desert 479 | design 480 | desk 481 | despair 482 | destroy 483 | detail 484 | detect 485 | develop 486 | device 487 | devote 488 | diagram 489 | dial 490 | diamond 491 | diary 492 | dice 493 | diesel 494 | diet 495 | differ 496 | digital 497 | dignity 498 | dilemma 499 | dinner 500 | dinosaur 501 | direct 502 | dirt 503 | disagree 504 | discover 505 | disease 506 | dish 507 | dismiss 508 | disorder 509 | display 510 | distance 511 | divert 512 | divide 513 | divorce 514 | dizzy 515 | doctor 516 | document 517 | dog 518 | doll 519 | dolphin 520 | domain 521 | donate 522 | donkey 523 | donor 524 | door 525 | dose 526 | double 527 | dove 528 | draft 529 | dragon 530 | drama 531 | drastic 532 | draw 533 | dream 534 | dress 535 | drift 536 | drill 537 | drink 538 | drip 539 | drive 540 | drop 541 | drum 542 | dry 543 | duck 544 | dumb 545 | dune 546 | during 547 | dust 548 | dutch 549 | duty 550 | dwarf 551 | dynamic 552 | eager 553 | eagle 554 | early 555 | earn 556 | earth 557 | easily 558 | east 559 | easy 560 | echo 561 | ecology 562 | economy 563 | edge 564 | edit 565 | educate 566 | effort 567 | egg 568 | eight 569 | either 570 | elbow 571 | elder 572 | electric 573 | elegant 574 | element 575 | elephant 576 | elevator 577 | elite 578 | else 579 | embark 580 | embody 581 | embrace 582 | emerge 583 | emotion 584 | employ 585 | empower 586 | empty 587 | enable 588 | enact 589 | end 590 | endless 591 | endorse 592 | enemy 593 | energy 594 | enforce 595 | engage 596 | engine 597 | enhance 598 | enjoy 599 | enlist 600 | enough 601 | enrich 602 | enroll 603 | ensure 604 | enter 605 | entire 606 | entry 607 | envelope 608 | episode 609 | equal 610 | equip 611 | era 612 | erase 613 | erode 614 | erosion 615 | error 616 | erupt 617 | escape 618 | essay 619 | essence 620 | estate 621 | eternal 622 | ethics 623 | evidence 624 | evil 625 | evoke 626 | evolve 627 | exact 628 | example 629 | excess 630 | exchange 631 | excite 632 | exclude 633 | excuse 634 | execute 635 | exercise 636 | exhaust 637 | exhibit 638 | exile 639 | exist 640 | exit 641 | exotic 642 | expand 643 | expect 644 | expire 645 | explain 646 | expose 647 | express 648 | extend 649 | extra 650 | eye 651 | eyebrow 652 | fabric 653 | face 654 | faculty 655 | fade 656 | faint 657 | faith 658 | fall 659 | false 660 | fame 661 | family 662 | famous 663 | fan 664 | fancy 665 | fantasy 666 | farm 667 | fashion 668 | fat 669 | fatal 670 | father 671 | fatigue 672 | fault 673 | favorite 674 | feature 675 | february 676 | federal 677 | fee 678 | feed 679 | feel 680 | female 681 | fence 682 | festival 683 | fetch 684 | fever 685 | few 686 | fiber 687 | fiction 688 | field 689 | figure 690 | file 691 | film 692 | filter 693 | final 694 | find 695 | fine 696 | finger 697 | finish 698 | fire 699 | firm 700 | first 701 | fiscal 702 | fish 703 | fit 704 | fitness 705 | fix 706 | flag 707 | flame 708 | flash 709 | flat 710 | flavor 711 | flee 712 | flight 713 | flip 714 | float 715 | flock 716 | floor 717 | flower 718 | fluid 719 | flush 720 | fly 721 | foam 722 | focus 723 | fog 724 | foil 725 | fold 726 | follow 727 | food 728 | foot 729 | force 730 | forest 731 | forget 732 | fork 733 | fortune 734 | forum 735 | forward 736 | fossil 737 | foster 738 | found 739 | fox 740 | fragile 741 | frame 742 | frequent 743 | fresh 744 | friend 745 | fringe 746 | frog 747 | front 748 | frost 749 | frown 750 | frozen 751 | fruit 752 | fuel 753 | fun 754 | funny 755 | furnace 756 | fury 757 | future 758 | gadget 759 | gain 760 | galaxy 761 | gallery 762 | game 763 | gap 764 | garage 765 | garbage 766 | garden 767 | garlic 768 | garment 769 | gas 770 | gasp 771 | gate 772 | gather 773 | gauge 774 | gaze 775 | general 776 | genius 777 | genre 778 | gentle 779 | genuine 780 | gesture 781 | ghost 782 | giant 783 | gift 784 | giggle 785 | ginger 786 | giraffe 787 | girl 788 | give 789 | glad 790 | glance 791 | glare 792 | glass 793 | glide 794 | glimpse 795 | globe 796 | gloom 797 | glory 798 | glove 799 | glow 800 | glue 801 | goat 802 | goddess 803 | gold 804 | good 805 | goose 806 | gorilla 807 | gospel 808 | gossip 809 | govern 810 | gown 811 | grab 812 | grace 813 | grain 814 | grant 815 | grape 816 | grass 817 | gravity 818 | great 819 | green 820 | grid 821 | grief 822 | grit 823 | grocery 824 | group 825 | grow 826 | grunt 827 | guard 828 | guess 829 | guide 830 | guilt 831 | guitar 832 | gun 833 | gym 834 | habit 835 | hair 836 | half 837 | hammer 838 | hamster 839 | hand 840 | happy 841 | harbor 842 | hard 843 | harsh 844 | harvest 845 | hat 846 | have 847 | hawk 848 | hazard 849 | head 850 | health 851 | heart 852 | heavy 853 | hedgehog 854 | height 855 | hello 856 | helmet 857 | help 858 | hen 859 | hero 860 | hidden 861 | high 862 | hill 863 | hint 864 | hip 865 | hire 866 | history 867 | hobby 868 | hockey 869 | hold 870 | hole 871 | holiday 872 | hollow 873 | home 874 | honey 875 | hood 876 | hope 877 | horn 878 | horror 879 | horse 880 | hospital 881 | host 882 | hotel 883 | hour 884 | hover 885 | hub 886 | huge 887 | human 888 | humble 889 | humor 890 | hundred 891 | hungry 892 | hunt 893 | hurdle 894 | hurry 895 | hurt 896 | husband 897 | hybrid 898 | ice 899 | icon 900 | idea 901 | identify 902 | idle 903 | ignore 904 | ill 905 | illegal 906 | illness 907 | image 908 | imitate 909 | immense 910 | immune 911 | impact 912 | impose 913 | improve 914 | impulse 915 | inch 916 | include 917 | income 918 | increase 919 | index 920 | indicate 921 | indoor 922 | industry 923 | infant 924 | inflict 925 | inform 926 | inhale 927 | inherit 928 | initial 929 | inject 930 | injury 931 | inmate 932 | inner 933 | innocent 934 | input 935 | inquiry 936 | insane 937 | insect 938 | inside 939 | inspire 940 | install 941 | intact 942 | interest 943 | into 944 | invest 945 | invite 946 | involve 947 | iron 948 | island 949 | isolate 950 | issue 951 | item 952 | ivory 953 | jacket 954 | jaguar 955 | jar 956 | jazz 957 | jealous 958 | jeans 959 | jelly 960 | jewel 961 | job 962 | join 963 | joke 964 | journey 965 | joy 966 | judge 967 | juice 968 | jump 969 | jungle 970 | junior 971 | junk 972 | just 973 | kangaroo 974 | keen 975 | keep 976 | ketchup 977 | key 978 | kick 979 | kid 980 | kidney 981 | kind 982 | kingdom 983 | kiss 984 | kit 985 | kitchen 986 | kite 987 | kitten 988 | kiwi 989 | knee 990 | knife 991 | knock 992 | know 993 | lab 994 | label 995 | labor 996 | ladder 997 | lady 998 | lake 999 | lamp 1000 | language 1001 | laptop 1002 | large 1003 | later 1004 | latin 1005 | laugh 1006 | laundry 1007 | lava 1008 | law 1009 | lawn 1010 | lawsuit 1011 | layer 1012 | lazy 1013 | leader 1014 | leaf 1015 | learn 1016 | leave 1017 | lecture 1018 | left 1019 | leg 1020 | legal 1021 | legend 1022 | leisure 1023 | lemon 1024 | lend 1025 | length 1026 | lens 1027 | leopard 1028 | lesson 1029 | letter 1030 | level 1031 | liar 1032 | liberty 1033 | library 1034 | license 1035 | life 1036 | lift 1037 | light 1038 | like 1039 | limb 1040 | limit 1041 | link 1042 | lion 1043 | liquid 1044 | list 1045 | little 1046 | live 1047 | lizard 1048 | load 1049 | loan 1050 | lobster 1051 | local 1052 | lock 1053 | logic 1054 | lonely 1055 | long 1056 | loop 1057 | lottery 1058 | loud 1059 | lounge 1060 | love 1061 | loyal 1062 | lucky 1063 | luggage 1064 | lumber 1065 | lunar 1066 | lunch 1067 | luxury 1068 | lyrics 1069 | machine 1070 | mad 1071 | magic 1072 | magnet 1073 | maid 1074 | mail 1075 | main 1076 | major 1077 | make 1078 | mammal 1079 | man 1080 | manage 1081 | mandate 1082 | mango 1083 | mansion 1084 | manual 1085 | maple 1086 | marble 1087 | march 1088 | margin 1089 | marine 1090 | market 1091 | marriage 1092 | mask 1093 | mass 1094 | master 1095 | match 1096 | material 1097 | math 1098 | matrix 1099 | matter 1100 | maximum 1101 | maze 1102 | meadow 1103 | mean 1104 | measure 1105 | meat 1106 | mechanic 1107 | medal 1108 | media 1109 | melody 1110 | melt 1111 | member 1112 | memory 1113 | mention 1114 | menu 1115 | mercy 1116 | merge 1117 | merit 1118 | merry 1119 | mesh 1120 | message 1121 | metal 1122 | method 1123 | middle 1124 | midnight 1125 | milk 1126 | million 1127 | mimic 1128 | mind 1129 | minimum 1130 | minor 1131 | minute 1132 | miracle 1133 | mirror 1134 | misery 1135 | miss 1136 | mistake 1137 | mix 1138 | mixed 1139 | mixture 1140 | mobile 1141 | model 1142 | modify 1143 | mom 1144 | moment 1145 | monitor 1146 | monkey 1147 | monster 1148 | month 1149 | moon 1150 | moral 1151 | more 1152 | morning 1153 | mosquito 1154 | mother 1155 | motion 1156 | motor 1157 | mountain 1158 | mouse 1159 | move 1160 | movie 1161 | much 1162 | muffin 1163 | mule 1164 | multiply 1165 | muscle 1166 | museum 1167 | mushroom 1168 | music 1169 | must 1170 | mutual 1171 | myself 1172 | mystery 1173 | myth 1174 | naive 1175 | name 1176 | napkin 1177 | narrow 1178 | nasty 1179 | nation 1180 | nature 1181 | near 1182 | neck 1183 | need 1184 | negative 1185 | neglect 1186 | neither 1187 | nephew 1188 | nerve 1189 | nest 1190 | net 1191 | network 1192 | neutral 1193 | never 1194 | news 1195 | next 1196 | nice 1197 | night 1198 | noble 1199 | noise 1200 | nominee 1201 | noodle 1202 | normal 1203 | north 1204 | nose 1205 | notable 1206 | note 1207 | nothing 1208 | notice 1209 | novel 1210 | now 1211 | nuclear 1212 | number 1213 | nurse 1214 | nut 1215 | oak 1216 | obey 1217 | object 1218 | oblige 1219 | obscure 1220 | observe 1221 | obtain 1222 | obvious 1223 | occur 1224 | ocean 1225 | october 1226 | odor 1227 | off 1228 | offer 1229 | office 1230 | often 1231 | oil 1232 | okay 1233 | old 1234 | olive 1235 | olympic 1236 | omit 1237 | once 1238 | one 1239 | onion 1240 | online 1241 | only 1242 | open 1243 | opera 1244 | opinion 1245 | oppose 1246 | option 1247 | orange 1248 | orbit 1249 | orchard 1250 | order 1251 | ordinary 1252 | organ 1253 | orient 1254 | original 1255 | orphan 1256 | ostrich 1257 | other 1258 | outdoor 1259 | outer 1260 | output 1261 | outside 1262 | oval 1263 | oven 1264 | over 1265 | own 1266 | owner 1267 | oxygen 1268 | oyster 1269 | ozone 1270 | pact 1271 | paddle 1272 | page 1273 | pair 1274 | palace 1275 | palm 1276 | panda 1277 | panel 1278 | panic 1279 | panther 1280 | paper 1281 | parade 1282 | parent 1283 | park 1284 | parrot 1285 | party 1286 | pass 1287 | patch 1288 | path 1289 | patient 1290 | patrol 1291 | pattern 1292 | pause 1293 | pave 1294 | payment 1295 | peace 1296 | peanut 1297 | pear 1298 | peasant 1299 | pelican 1300 | pen 1301 | penalty 1302 | pencil 1303 | people 1304 | pepper 1305 | perfect 1306 | permit 1307 | person 1308 | pet 1309 | phone 1310 | photo 1311 | phrase 1312 | physical 1313 | piano 1314 | picnic 1315 | picture 1316 | piece 1317 | pig 1318 | pigeon 1319 | pill 1320 | pilot 1321 | pink 1322 | pioneer 1323 | pipe 1324 | pistol 1325 | pitch 1326 | pizza 1327 | place 1328 | planet 1329 | plastic 1330 | plate 1331 | play 1332 | please 1333 | pledge 1334 | pluck 1335 | plug 1336 | plunge 1337 | poem 1338 | poet 1339 | point 1340 | polar 1341 | pole 1342 | police 1343 | pond 1344 | pony 1345 | pool 1346 | popular 1347 | portion 1348 | position 1349 | possible 1350 | post 1351 | potato 1352 | pottery 1353 | poverty 1354 | powder 1355 | power 1356 | practice 1357 | praise 1358 | predict 1359 | prefer 1360 | prepare 1361 | present 1362 | pretty 1363 | prevent 1364 | price 1365 | pride 1366 | primary 1367 | print 1368 | priority 1369 | prison 1370 | private 1371 | prize 1372 | problem 1373 | process 1374 | produce 1375 | profit 1376 | program 1377 | project 1378 | promote 1379 | proof 1380 | property 1381 | prosper 1382 | protect 1383 | proud 1384 | provide 1385 | public 1386 | pudding 1387 | pull 1388 | pulp 1389 | pulse 1390 | pumpkin 1391 | punch 1392 | pupil 1393 | puppy 1394 | purchase 1395 | purity 1396 | purpose 1397 | purse 1398 | push 1399 | put 1400 | puzzle 1401 | pyramid 1402 | quality 1403 | quantum 1404 | quarter 1405 | question 1406 | quick 1407 | quit 1408 | quiz 1409 | quote 1410 | rabbit 1411 | raccoon 1412 | race 1413 | rack 1414 | radar 1415 | radio 1416 | rail 1417 | rain 1418 | raise 1419 | rally 1420 | ramp 1421 | ranch 1422 | random 1423 | range 1424 | rapid 1425 | rare 1426 | rate 1427 | rather 1428 | raven 1429 | raw 1430 | razor 1431 | ready 1432 | real 1433 | reason 1434 | rebel 1435 | rebuild 1436 | recall 1437 | receive 1438 | recipe 1439 | record 1440 | recycle 1441 | reduce 1442 | reflect 1443 | reform 1444 | refuse 1445 | region 1446 | regret 1447 | regular 1448 | reject 1449 | relax 1450 | release 1451 | relief 1452 | rely 1453 | remain 1454 | remember 1455 | remind 1456 | remove 1457 | render 1458 | renew 1459 | rent 1460 | reopen 1461 | repair 1462 | repeat 1463 | replace 1464 | report 1465 | require 1466 | rescue 1467 | resemble 1468 | resist 1469 | resource 1470 | response 1471 | result 1472 | retire 1473 | retreat 1474 | return 1475 | reunion 1476 | reveal 1477 | review 1478 | reward 1479 | rhythm 1480 | rib 1481 | ribbon 1482 | rice 1483 | rich 1484 | ride 1485 | ridge 1486 | rifle 1487 | right 1488 | rigid 1489 | ring 1490 | riot 1491 | ripple 1492 | risk 1493 | ritual 1494 | rival 1495 | river 1496 | road 1497 | roast 1498 | robot 1499 | robust 1500 | rocket 1501 | romance 1502 | roof 1503 | rookie 1504 | room 1505 | rose 1506 | rotate 1507 | rough 1508 | round 1509 | route 1510 | royal 1511 | rubber 1512 | rude 1513 | rug 1514 | rule 1515 | run 1516 | runway 1517 | rural 1518 | sad 1519 | saddle 1520 | sadness 1521 | safe 1522 | sail 1523 | salad 1524 | salmon 1525 | salon 1526 | salt 1527 | salute 1528 | same 1529 | sample 1530 | sand 1531 | satisfy 1532 | satoshi 1533 | sauce 1534 | sausage 1535 | save 1536 | say 1537 | scale 1538 | scan 1539 | scare 1540 | scatter 1541 | scene 1542 | scheme 1543 | school 1544 | science 1545 | scissors 1546 | scorpion 1547 | scout 1548 | scrap 1549 | screen 1550 | script 1551 | scrub 1552 | sea 1553 | search 1554 | season 1555 | seat 1556 | second 1557 | secret 1558 | section 1559 | security 1560 | seed 1561 | seek 1562 | segment 1563 | select 1564 | sell 1565 | seminar 1566 | senior 1567 | sense 1568 | sentence 1569 | series 1570 | service 1571 | session 1572 | settle 1573 | setup 1574 | seven 1575 | shadow 1576 | shaft 1577 | shallow 1578 | share 1579 | shed 1580 | shell 1581 | sheriff 1582 | shield 1583 | shift 1584 | shine 1585 | ship 1586 | shiver 1587 | shock 1588 | shoe 1589 | shoot 1590 | shop 1591 | short 1592 | shoulder 1593 | shove 1594 | shrimp 1595 | shrug 1596 | shuffle 1597 | shy 1598 | sibling 1599 | sick 1600 | side 1601 | siege 1602 | sight 1603 | sign 1604 | silent 1605 | silk 1606 | silly 1607 | silver 1608 | similar 1609 | simple 1610 | since 1611 | sing 1612 | siren 1613 | sister 1614 | situate 1615 | six 1616 | size 1617 | skate 1618 | sketch 1619 | ski 1620 | skill 1621 | skin 1622 | skirt 1623 | skull 1624 | slab 1625 | slam 1626 | sleep 1627 | slender 1628 | slice 1629 | slide 1630 | slight 1631 | slim 1632 | slogan 1633 | slot 1634 | slow 1635 | slush 1636 | small 1637 | smart 1638 | smile 1639 | smoke 1640 | smooth 1641 | snack 1642 | snake 1643 | snap 1644 | sniff 1645 | snow 1646 | soap 1647 | soccer 1648 | social 1649 | sock 1650 | soda 1651 | soft 1652 | solar 1653 | soldier 1654 | solid 1655 | solution 1656 | solve 1657 | someone 1658 | song 1659 | soon 1660 | sorry 1661 | sort 1662 | soul 1663 | sound 1664 | soup 1665 | source 1666 | south 1667 | space 1668 | spare 1669 | spatial 1670 | spawn 1671 | speak 1672 | special 1673 | speed 1674 | spell 1675 | spend 1676 | sphere 1677 | spice 1678 | spider 1679 | spike 1680 | spin 1681 | spirit 1682 | split 1683 | spoil 1684 | sponsor 1685 | spoon 1686 | sport 1687 | spot 1688 | spray 1689 | spread 1690 | spring 1691 | spy 1692 | square 1693 | squeeze 1694 | squirrel 1695 | stable 1696 | stadium 1697 | staff 1698 | stage 1699 | stairs 1700 | stamp 1701 | stand 1702 | start 1703 | state 1704 | stay 1705 | steak 1706 | steel 1707 | stem 1708 | step 1709 | stereo 1710 | stick 1711 | still 1712 | sting 1713 | stock 1714 | stomach 1715 | stone 1716 | stool 1717 | story 1718 | stove 1719 | strategy 1720 | street 1721 | strike 1722 | strong 1723 | struggle 1724 | student 1725 | stuff 1726 | stumble 1727 | style 1728 | subject 1729 | submit 1730 | subway 1731 | success 1732 | such 1733 | sudden 1734 | suffer 1735 | sugar 1736 | suggest 1737 | suit 1738 | summer 1739 | sun 1740 | sunny 1741 | sunset 1742 | super 1743 | supply 1744 | supreme 1745 | sure 1746 | surface 1747 | surge 1748 | surprise 1749 | surround 1750 | survey 1751 | suspect 1752 | sustain 1753 | swallow 1754 | swamp 1755 | swap 1756 | swarm 1757 | swear 1758 | sweet 1759 | swift 1760 | swim 1761 | swing 1762 | switch 1763 | sword 1764 | symbol 1765 | symptom 1766 | syrup 1767 | system 1768 | table 1769 | tackle 1770 | tag 1771 | tail 1772 | talent 1773 | talk 1774 | tank 1775 | tape 1776 | target 1777 | task 1778 | taste 1779 | tattoo 1780 | taxi 1781 | teach 1782 | team 1783 | tell 1784 | ten 1785 | tenant 1786 | tennis 1787 | tent 1788 | term 1789 | test 1790 | text 1791 | thank 1792 | that 1793 | theme 1794 | then 1795 | theory 1796 | there 1797 | they 1798 | thing 1799 | this 1800 | thought 1801 | three 1802 | thrive 1803 | throw 1804 | thumb 1805 | thunder 1806 | ticket 1807 | tide 1808 | tiger 1809 | tilt 1810 | timber 1811 | time 1812 | tiny 1813 | tip 1814 | tired 1815 | tissue 1816 | title 1817 | toast 1818 | tobacco 1819 | today 1820 | toddler 1821 | toe 1822 | together 1823 | toilet 1824 | token 1825 | tomato 1826 | tomorrow 1827 | tone 1828 | tongue 1829 | tonight 1830 | tool 1831 | tooth 1832 | top 1833 | topic 1834 | topple 1835 | torch 1836 | tornado 1837 | tortoise 1838 | toss 1839 | total 1840 | tourist 1841 | toward 1842 | tower 1843 | town 1844 | toy 1845 | track 1846 | trade 1847 | traffic 1848 | tragic 1849 | train 1850 | transfer 1851 | trap 1852 | trash 1853 | travel 1854 | tray 1855 | treat 1856 | tree 1857 | trend 1858 | trial 1859 | tribe 1860 | trick 1861 | trigger 1862 | trim 1863 | trip 1864 | trophy 1865 | trouble 1866 | truck 1867 | true 1868 | truly 1869 | trumpet 1870 | trust 1871 | truth 1872 | try 1873 | tube 1874 | tuition 1875 | tumble 1876 | tuna 1877 | tunnel 1878 | turkey 1879 | turn 1880 | turtle 1881 | twelve 1882 | twenty 1883 | twice 1884 | twin 1885 | twist 1886 | two 1887 | type 1888 | typical 1889 | ugly 1890 | umbrella 1891 | unable 1892 | unaware 1893 | uncle 1894 | uncover 1895 | under 1896 | undo 1897 | unfair 1898 | unfold 1899 | unhappy 1900 | uniform 1901 | unique 1902 | unit 1903 | universe 1904 | unknown 1905 | unlock 1906 | until 1907 | unusual 1908 | unveil 1909 | update 1910 | upgrade 1911 | uphold 1912 | upon 1913 | upper 1914 | upset 1915 | urban 1916 | urge 1917 | usage 1918 | use 1919 | used 1920 | useful 1921 | useless 1922 | usual 1923 | utility 1924 | vacant 1925 | vacuum 1926 | vague 1927 | valid 1928 | valley 1929 | valve 1930 | van 1931 | vanish 1932 | vapor 1933 | various 1934 | vast 1935 | vault 1936 | vehicle 1937 | velvet 1938 | vendor 1939 | venture 1940 | venue 1941 | verb 1942 | verify 1943 | version 1944 | very 1945 | vessel 1946 | veteran 1947 | viable 1948 | vibrant 1949 | vicious 1950 | victory 1951 | video 1952 | view 1953 | village 1954 | vintage 1955 | violin 1956 | virtual 1957 | virus 1958 | visa 1959 | visit 1960 | visual 1961 | vital 1962 | vivid 1963 | vocal 1964 | voice 1965 | void 1966 | volcano 1967 | volume 1968 | vote 1969 | voyage 1970 | wage 1971 | wagon 1972 | wait 1973 | walk 1974 | wall 1975 | walnut 1976 | want 1977 | warfare 1978 | warm 1979 | warrior 1980 | wash 1981 | wasp 1982 | waste 1983 | water 1984 | wave 1985 | way 1986 | wealth 1987 | weapon 1988 | wear 1989 | weasel 1990 | weather 1991 | web 1992 | wedding 1993 | weekend 1994 | weird 1995 | welcome 1996 | west 1997 | wet 1998 | whale 1999 | what 2000 | wheat 2001 | wheel 2002 | when 2003 | where 2004 | whip 2005 | whisper 2006 | wide 2007 | width 2008 | wife 2009 | wild 2010 | will 2011 | win 2012 | window 2013 | wine 2014 | wing 2015 | wink 2016 | winner 2017 | winter 2018 | wire 2019 | wisdom 2020 | wise 2021 | wish 2022 | witness 2023 | wolf 2024 | woman 2025 | wonder 2026 | wood 2027 | wool 2028 | word 2029 | work 2030 | world 2031 | worry 2032 | worth 2033 | wrap 2034 | wreck 2035 | wrestle 2036 | wrist 2037 | write 2038 | wrong 2039 | yard 2040 | year 2041 | yellow 2042 | you 2043 | young 2044 | youth 2045 | zebra 2046 | zero 2047 | zone 2048 | zoo -------------------------------------------------------------------------------- /tests/TRC20Test.php: -------------------------------------------------------------------------------- 1 | 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', // USDT TRC20 14 | 'decimals' => 6, 15 | ]; 16 | 17 | $trc20Wallet = new TRC20(apiurl: URI, options: OPTIONS); 18 | 19 | //获取Token余额 20 | $balanceData = $trc20Wallet->balance($address); 21 | var_dump($balanceData); 22 | 23 | //转账 24 | $amount = 1; 25 | $from = $trc20Wallet->getAddressByPrivateKey($privateKey)->address; 26 | $transferData = $trc20Wallet->transferTRC20(self::PRIVATE_KEY, $from, self::ADDRESS, $amount); 27 | var_dump($transferData); 28 | -------------------------------------------------------------------------------- /tests/TRXTest.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | generateAddress(); 20 | // var_dump($addressData); 21 | 22 | //测试生成带助记词的地址 23 | // $addressData = $trxWallet->generateAddressWithMnemonic(); 24 | // var_dump($addressData); 25 | 26 | // //私钥生成地址 27 | // $privateKey = PRIVATE_KEY; 28 | // $addressData = $trxWallet->getAddressByPrivateKey($privateKey); 29 | // var_dump($addressData); 30 | 31 | // //获取钱包余额 32 | // $balanceData = $trxWallet->balance(ADDRESS); 33 | // var_dump($balanceData); 34 | 35 | // //转账 36 | // $amount = 0.1; 37 | // $from = $trxWallet->getAddressByPrivateKey(PRIVATE_KEY); 38 | // $transferData = $trxWallet->transfer(PRIVATE_KEY, $from->address, ADDRESS, $amount); 39 | // var_dump($transferData); 40 | 41 | // //获取当前最新区块 42 | // $blockData = $trxWallet->getNowBlock(); 43 | // var_dump($blockData); 44 | 45 | // //获取指定区块高度信息 46 | // $blockData = $trxWallet->getBlockByNum(BLOCK_ID); 47 | // var_dump($blockData); 48 | 49 | // //获取指定Hash交易信息 50 | $txData = $trxWallet->getTransactionById(TX_HASH); 51 | var_dump($txData); --------------------------------------------------------------------------------