├── .gitignore ├── README.md ├── composer.json ├── src ├── Contract.php ├── Quantity.php ├── RLP │ ├── Buffer.php │ └── RLP.php ├── Utils.php ├── Wallet.php └── Web3.php └── test.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | /composer.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web3 2 | 3 | A simple encapsulation of Web3 framework in PHP environment. 4 | 5 | # install 6 | ```bash 7 | composer require kgs/web3:dev-master 8 | ``` 9 | ## Usage 10 | ### web3 11 | ```php 12 | $web3 = new Web3('https://kovan.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161'); 13 | var_dump($web3->accounts()); 14 | 15 | echo $web3->gasPrice(); 16 | echo \Web3\Utils::hexToDec($web3->getBalance("0xdB7D1B76D262D31c51d740C6fb98047B8498D851")); 17 | ``` 18 | 19 | 20 | ### Wallet 21 | ```php 22 | $key = "e872122c04df93040ede8996c0e738f35a0ea44e77642d97eb5c3deedbdd4201"; 23 | $wallet =Wallet::createByPrivate($key); 24 | echo $wallet->getAddress(); 25 | //0xdb7d1b76d262d31c51d740c6fb98047b8498d851 26 | $wallet = Wallet::create(); 27 | echo $wallet->getAddress(); 28 | echo $wallet->getPrivateKey(); 29 | 30 | 31 | ``` 32 | ### Contract 33 | ```php 34 | $key = "e872122c04df93040ede8996c0e738f35a0ea44e77642d97eb5c3deedbdd4201"; 35 | $wallet =Wallet::createByPrivate($key); 36 | $abi = '[{"inputs":[{"internalType":"string","name":"name","type":"string"},{"internalType":"string","name":"symbol","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]'; 37 | $web3 = new Web3('https://kovan.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161'); 38 | $contractAddress='0x7ef08Db1E4121b71177B828d5b5ff7a1BCB8305D'; 39 | $contract = Contract::at($web3,$abi,$contractAddress); 40 | $toAccount = "0x6aba7cd6750225f9d732a256F0f334916C866264"; 41 | $res =$contract->send($wallet,'transfer',[$toAccount,\Web3\Utils::ethToWei(1)]); 42 | echo $res; 43 | //0x43b287554146748780d00af8c7d9d42c499ba03759b759f0f4244b072ec0cab2y 44 | echo $contract->decodeEvent("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"); 45 | //Transfer 46 | 47 | $res =$contract->call('balanceOf',[$toAccount]); 48 | ``` 49 | 50 | 51 | # DONATE 52 | 53 | ``` 54 | eth/dai: 0x6aba7cd6750225f9d732a256F0f334916C866264 55 | ``` 56 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kgs/web3", 3 | "description": "A simple encapsulation of Web3 framework in PHP environment.\n", 4 | "type": "library", 5 | "keywords": ["eth", "web3", "contract", "sign", "usdt", "dai"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "yukaige", 10 | "email": "yukaige@yukaige.com" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": { 15 | "Web3\\": "src/" 16 | } 17 | }, 18 | "minimum-stability": "dev", 19 | "require": { 20 | "PHP": "^7.0|^8.0", 21 | "kornrunner/keccak": "^1.0", 22 | "simplito/elliptic-php": "^1.0", 23 | "guzzlehttp/guzzle": "^6.0|^7.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Contract.php: -------------------------------------------------------------------------------- 1 | abi = $abi; 19 | $this->web3 = $web3; 20 | $this->address = $address; 21 | 22 | $json = json_decode($this->abi, true); 23 | 24 | foreach ($json as $item) { 25 | switch ($item['type']) { 26 | case 'event': 27 | $this->event[$item['name']] = $item; 28 | $hash = $this->encodeEventSignature($this->getEventByName($item['name'])); 29 | $this->eventHash[$hash] = $item['name']; 30 | break; 31 | case 'function': 32 | $this->function[$item['name']] = $item; 33 | break; 34 | } 35 | } 36 | } 37 | 38 | public static function at(Web3 $web3, $abi, $address): Contract 39 | { 40 | return new Contract($web3, $abi, $address); 41 | } 42 | 43 | public function getFunctionArrayByName($name) 44 | { 45 | return $this->function[$name]; 46 | } 47 | 48 | public function getEventArrayByName($name) 49 | { 50 | return $this->event[$name]; 51 | } 52 | 53 | public function getWeb3(): Web3 54 | { 55 | return $this->web3; 56 | } 57 | 58 | public function getAddress() 59 | { 60 | return $this->address; 61 | } 62 | 63 | public function getFunctionByName($name): string 64 | { 65 | $inputs = $this->function[$name]['inputs']; 66 | return $this->extracted($name, $inputs); 67 | } 68 | 69 | public function getEventByName($name): string 70 | { 71 | $inputs = $this->event[$name]['inputs']; 72 | return $this->extracted($name, $inputs); 73 | } 74 | 75 | public function getFunctionName(): array 76 | { 77 | return array_keys($this->function); 78 | } 79 | 80 | public function getEventName(): array 81 | { 82 | return array_keys($this->event); 83 | } 84 | 85 | public function decodeEvent($hash) 86 | { 87 | if (!array_key_exists($hash, $this->eventHash)) { 88 | throw new \Exception("this hash not in the constract"); 89 | } 90 | return $this->eventHash[$hash]; 91 | } 92 | 93 | public function getAbi() 94 | { 95 | return $this->abi; 96 | } 97 | 98 | /** 99 | * @throws \Exception 100 | */ 101 | public function send(Wallet $wallet, $function, array $param, $config = []) 102 | { 103 | 104 | $data = $this->getData($function, $param, $wallet->getAddress()); 105 | $data['gas'] = dechex(hexdec( 106 | $this->web3->estimateGas($data['to'], $data['data'], $data['from'], null, $data['value'] 107 | )) * 1.5); 108 | $data['gasPrice'] = $this->web3->gasPrice(); 109 | $data['nonce'] = $this->web3->getTransactionCount($wallet->getAddress(), 'pending'); 110 | unset($data['from']); 111 | // $chainId = $this->web3->chainId(); 112 | $data = array_merge([ 113 | 'nonce' => '01', 114 | 'gasPrice' => '', 115 | 'gas' => '', 116 | 'to' => '', 117 | 'value' => '', 118 | 'data' => '', 119 | ], $data); 120 | $signature = $wallet->sign(Utils::rawEncode($data)); 121 | $chainId = 0; 122 | $data['v'] = dechex($signature->recoveryParam + 27 + ($chainId ? $chainId * 2 + 8 : 0)); 123 | $data['r'] = $signature->r->toString('hex'); 124 | $data['s'] = $signature->s->toString('hex'); 125 | $signRaw = Utils::add0x(Utils::rawEncode($data)); 126 | return $this->web3->sendRawTransaction($signRaw); 127 | } 128 | 129 | /** 130 | * @param $function 131 | * @param $param 132 | * @param null $from 133 | * @return array 134 | * @throws \Exception 135 | */ 136 | private function getData($function, $param, $from = null): array 137 | { 138 | if (!array_key_exists($function, $this->function)) { 139 | throw new \Exception(" function not in contract "); 140 | } 141 | $function = $this->getFunctionArrayByName($function); 142 | if (count($param) != count($function['inputs'])) { 143 | throw new \Exception("please send full param"); 144 | } 145 | $data = [ 146 | 'to' => $this->address, 147 | 'value' => '0x0' 148 | ]; 149 | if (!empty($from)) { 150 | $data['from'] = $from; 151 | } 152 | $hash = Keccak::hash($this->getFunctionByName($function['name']), 256); 153 | $hashSub = mb_substr($hash, 0, 8, 'utf-8'); 154 | $data['data'] = '0x' . $hashSub; 155 | $input = $function['inputs']; 156 | for ($i = 0; $i < count($param); $i++) { 157 | $value = ''; 158 | switch ($input[$i]['type']) { 159 | case 'address': 160 | $value = Utils::remove0x($param[$i]); 161 | break; 162 | case 'uint8': 163 | case 'uint16': 164 | case 'uint24': 165 | case 'uint32': 166 | case 'uint40': 167 | case 'uint48': 168 | case 'uint56': 169 | case 'uint64': 170 | case 'uint72': 171 | case 'uint80': 172 | case 'uint88': 173 | case 'uint96': 174 | case 'uint104': 175 | case 'uint112': 176 | case 'uint120': 177 | case 'uint128': 178 | case 'uint136': 179 | case 'uint144': 180 | case 'uint152': 181 | case 'uint160': 182 | case 'uint168': 183 | case 'uint176': 184 | case 'uint184': 185 | case 'uint192': 186 | case 'uint200': 187 | case 'uint208': 188 | case 'uint216': 189 | case 'uint224': 190 | case 'uint232': 191 | case 'uint240': 192 | case 'uint248': 193 | case 'uint256': 194 | $value = Utils::decToHex($param[$i], false); 195 | break; 196 | } 197 | $data['data'] = $data['data'] . Utils::fill0($value); 198 | } 199 | return $data; 200 | } 201 | 202 | /** 203 | * encodeEventSignature 204 | * TODO: Fix same event name with different params 205 | * 206 | * @param string|stdClass|array $functionName 207 | * @return string 208 | */ 209 | public function encodeEventSignature($functionName) 210 | { 211 | if (!is_string($functionName)) { 212 | $functionName = Utils::jsonMethodToString($functionName); 213 | } 214 | return Utils::sha3($functionName); 215 | } 216 | 217 | public function call($function, $param = [], $quantity = Quantity::latest) 218 | { 219 | $data = $this->getData($function, $param); 220 | return $this->web3->call($data['to'], $data['data'], null, null, null, null, $quantity); 221 | } 222 | 223 | 224 | /** 225 | * @param $name 226 | * @param $inputs 227 | * @return string 228 | */ 229 | public function extracted($name, $inputs): string 230 | { 231 | $res = $name . '('; 232 | foreach ($inputs as $input) { 233 | $res .= $input['type'] . ','; 234 | } 235 | if (count($inputs) > 0) { 236 | $res = substr($res, 0, strlen($res) - 1); 237 | } 238 | $res .= ')'; 239 | return $res; 240 | } 241 | } -------------------------------------------------------------------------------- /src/Quantity.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @author Peter Lai 9 | * @license MIT 10 | */ 11 | 12 | namespace Web3\RLP; 13 | 14 | use InvalidArgumentException; 15 | use ArrayAccess; 16 | 17 | class Buffer implements ArrayAccess 18 | { 19 | /** 20 | * data 21 | * 22 | * @var array 23 | */ 24 | protected $data = []; 25 | 26 | /** 27 | * encoding 28 | * 29 | * @var string 30 | */ 31 | protected $encoding = ''; 32 | 33 | /** 34 | * construct 35 | * 36 | * @param mixed $data 37 | * @param string $encoding the data encoding 38 | * @return void 39 | */ 40 | public function __construct($data=[], string $encoding='utf8') 41 | { 42 | $this->encoding = strtolower($encoding); 43 | 44 | if ($data) { 45 | $this->data = $this->decodeToData($data); 46 | } 47 | } 48 | 49 | /** 50 | * offsetSet 51 | * 52 | * @param mixed $offset 53 | * @param mixed $value 54 | * @return void 55 | */ 56 | public function offsetSet($offset, $value) 57 | { 58 | if (is_null($offset)) { 59 | $this->data[] = $value; 60 | } else { 61 | $this->data[$offset] = $value; 62 | } 63 | } 64 | 65 | /** 66 | * offsetExists 67 | * 68 | * @param mixed $offset 69 | * @return bool 70 | */ 71 | public function offsetExists($offset) 72 | { 73 | return isset($this->data[$offset]); 74 | } 75 | 76 | /** 77 | * offsetUnset 78 | * 79 | * @param mixed $offset 80 | * @return void 81 | */ 82 | public function offsetUnset($offset) 83 | { 84 | unset($this->data[$offset]); 85 | } 86 | 87 | /** 88 | * offsetGet 89 | * 90 | * @param mixed $offset 91 | * @return mixed 92 | */ 93 | public function offsetGet($offset) 94 | { 95 | return isset($this->data[$offset]) ? $this->data[$offset] : null; 96 | } 97 | 98 | /** 99 | * toString 100 | * 101 | * @param string $encoding 102 | * @return string 103 | */ 104 | public function toString(string $encoding='utf8') 105 | { 106 | $output = ''; 107 | $input = $this->data; 108 | 109 | switch ($encoding) { 110 | case 'hex': 111 | foreach ($input as $data) { 112 | $hex = dechex($data); 113 | 114 | // pad zero 115 | if ((strlen($hex) % 2) !== 0) { 116 | $hex = '0' . $hex; 117 | } 118 | $output .= $hex; 119 | } 120 | break; 121 | case 'ascii': 122 | foreach ($input as $data) { 123 | $output .= chr($data); 124 | } 125 | break; 126 | case 'utf8': 127 | // $length = count($input); 128 | 129 | // for ($i = array_keys($input)[0]; $i < $length; $i += 3) { 130 | // $output .= chr($input[$i]) . chr($input[$i + 1]) . chr($input[$i + 2]); 131 | // } 132 | // change to bytes string 133 | foreach ($input as $data) { 134 | $output .= chr($data); 135 | } 136 | break; 137 | default: 138 | throw new InvalidArgumentException('ToString encoding must be valid.'); 139 | break; 140 | } 141 | return $output; 142 | } 143 | 144 | /** 145 | * length 146 | * 147 | * @return int 148 | */ 149 | public function length() 150 | { 151 | return count($this->data); 152 | } 153 | 154 | /** 155 | * concat 156 | * 157 | * @param mixed $inputs 158 | * @return \RLP\Buffer 159 | */ 160 | public function concat() 161 | { 162 | $inputs = func_get_args(); 163 | 164 | foreach ($inputs as $input) { 165 | if (is_array($input)) { 166 | $input = new Buffer($input); 167 | } 168 | if ($input instanceof Buffer) { 169 | $length = $input->length(); 170 | 171 | for ($i = 0; $i < $length; $i++) { 172 | $this->data[] = $input[$i]; 173 | } 174 | } else { 175 | throw new InvalidArgumentException('Input must be array or Buffer when call concat.'); 176 | } 177 | } 178 | return $this; 179 | } 180 | 181 | /** 182 | * slice 183 | * 184 | * @param int $start 185 | * @param mixed $end 186 | * @return \RLP\Buffer 187 | */ 188 | public function slice(int $start=0, $end=null) 189 | { 190 | if ($end === null) { 191 | $end = $this->length(); 192 | } 193 | if ($end > 0) { 194 | $end -= $start; 195 | } elseif ($end === 0) { 196 | return new Buffer([]); 197 | } 198 | $sliced = array_slice($this->data, $start, $end); 199 | return new Buffer($sliced); 200 | } 201 | 202 | /** 203 | * decodeToData 204 | * 205 | * @param mixed $input 206 | * @return array 207 | */ 208 | protected function decodeToData($input) 209 | { 210 | $output = []; 211 | 212 | if (is_array($input)) { 213 | $output = $this->arrayToData($input); 214 | } elseif (is_int($input)) { 215 | $output = $this->intToData($input); 216 | } elseif (is_numeric($input)) { 217 | $output = $this->numericToData($input); 218 | } elseif (is_string($input)) { 219 | $output = $this->stringToData($input, $this->encoding); 220 | } 221 | return $output; 222 | } 223 | 224 | /** 225 | * arrayToData 226 | * 227 | * @param array $inputs 228 | * @return array 229 | */ 230 | protected function arrayToData(array $inputs) 231 | { 232 | $output = []; 233 | 234 | foreach ($inputs as $input) { 235 | if (is_array($input)) { 236 | // throw exception, maybe support future 237 | // $output[] = $this->arrayToData($input); 238 | throw new InvalidArgumentException('Do not use multidimensional array.'); 239 | } elseif (is_string($input)) { 240 | $output = array_merge($output, $this->stringToData($input, $this->encoding)); 241 | } elseif (is_numeric($input)) { 242 | $output = array_merge($output, $this->numericToData($input)); 243 | } 244 | } 245 | return $output; 246 | } 247 | 248 | /** 249 | * stringToData 250 | * 251 | * @param string $input 252 | * @param string $encoding 253 | * @return array 254 | */ 255 | protected function stringToData(string $input, string $encoding) 256 | { 257 | $output = []; 258 | 259 | switch ($encoding) { 260 | case 'hex': 261 | if (strpos($input, '0x') === 0) { 262 | // hex string 263 | $input = str_replace('0x', '', $input); 264 | } 265 | if (strlen($input) % 2 !== 0) { 266 | $input = '0' . $input; 267 | } 268 | // $splited = str_split($input, 2); 269 | 270 | // foreach ($splited as $data) { 271 | // $output[] = hexdec($data); 272 | // } 273 | $output = array_map('hexdec', str_split($input, 2)); 274 | 275 | break; 276 | case 'ascii': 277 | $output = array_map('ord', str_split($input, 1)); 278 | break; 279 | case 'utf8': 280 | $output = unpack('C*', $input); 281 | break; 282 | default: 283 | throw new InvalidArgumentException('StringToData encoding must be valid.'); 284 | break; 285 | } 286 | return $output; 287 | } 288 | 289 | /** 290 | * numericToData 291 | * 292 | * @param mixed $intput 293 | * @return array 294 | */ 295 | protected function numericToData($intput) 296 | { 297 | $output = (int) $intput; 298 | 299 | return [$output]; 300 | } 301 | 302 | /** 303 | * intToData 304 | * 305 | * @param mixed $intput 306 | * @return array 307 | */ 308 | protected function intToData($input) 309 | { 310 | return array_fill(0, $input, 0); 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/RLP/RLP.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @author Peter Lai 9 | * @license MIT 10 | */ 11 | 12 | namespace Web3\RLP; 13 | 14 | use InvalidArgumentException; 15 | use RuntimeException; 16 | 17 | class RLP 18 | { 19 | /** 20 | * encode 21 | * 22 | * @param mixed $inputs array of string 23 | * @return Buffer 24 | */ 25 | public function encode($inputs): Buffer 26 | { 27 | $output = new Buffer; 28 | if (is_array($inputs)) { 29 | $result = new Buffer; 30 | 31 | foreach ($inputs as $input) { 32 | $output->concat($this->encode($input)); 33 | } 34 | return $result->concat($this->encodeLength($output->length(), 192), $output); 35 | } 36 | $input = $this->toBuffer($inputs); 37 | $length = $input->length(); 38 | 39 | if ($length === 1 && $input[0] < 128) { 40 | return $input; 41 | } else { 42 | return $output->concat($this->encodeLength($length, 128), $input); 43 | } 44 | } 45 | 46 | /** 47 | * decode 48 | * Maybe use bignumber future. 49 | * 50 | * @param string $input 51 | * @return array 52 | */ 53 | public function decode(string $input): array 54 | { 55 | // if (!is_string($input)) { 56 | // throw new InvalidArgumentException('Input must be string when call decode.'); 57 | // } 58 | $input = $this->toBuffer($input); 59 | $decoded = $this->decodeData($input); 60 | 61 | return $decoded['data']; 62 | } 63 | 64 | /** 65 | * decodeData 66 | * Maybe use bignumber future. 67 | * 68 | * @param Buffer $input 69 | * @return array 70 | */ 71 | protected function decodeData(Buffer $input): array 72 | { 73 | $firstByte = $input[0]; 74 | $output = new Buffer; 75 | 76 | if ($firstByte <= 0x7f) { 77 | return [ 78 | 'data' => $input->slice(0, 1), 79 | 'remainder' => $input->slice(1) 80 | ]; 81 | } elseif ($firstByte <= 0xb7) { 82 | $length = $firstByte - 0x7f; 83 | $data = new Buffer([]); 84 | 85 | if ($firstByte !== 0x80) { 86 | // for ($i = 1; $i < $length; $i++) { 87 | // $data[] = $input[$i]; 88 | // } 89 | $data = $input->slice(1, $length); 90 | } 91 | if ($length === 2 && $data[0] < 0x80) { 92 | throw new RuntimeException('Byte must be less than 0x80.'); 93 | } 94 | return [ 95 | 'data' => $data, 96 | 'remainder' => $input->slice($length) 97 | ]; 98 | } elseif ($firstByte <= 0xbf) { 99 | $llength = $firstByte - 0xb6; 100 | $hexLength = $input->slice(1, $llength)->toString('hex'); 101 | 102 | if ($hexLength === '00') { 103 | throw new RuntimeException('Invalid RLP.'); 104 | } 105 | $length = hexdec($hexLength); 106 | $data = $input->slice($llength, $length + $llength); 107 | 108 | if ($data->length() < $length) { 109 | throw new RuntimeException('Invalid RLP.'); 110 | } 111 | return [ 112 | 'data' => $data, 113 | 'remainder' => $input->slice($length + $llength) 114 | ]; 115 | } elseif ($firstByte <= 0xf7) { 116 | $length = $firstByte - 0xbf; 117 | $innerRemainder = $input->slice(1, $length); 118 | $decoded = []; 119 | 120 | while ($innerRemainder->length()) { 121 | $data = $this->decodeData($innerRemainder); 122 | $decoded[] = $data['data']; 123 | $innerRemainder = $data['remainder']; 124 | } 125 | return [ 126 | 'data' => $decoded, 127 | 'remainder' => $input->slice($length) 128 | ]; 129 | } else { 130 | $llength = $firstByte - 0xf6; 131 | $hexLength = $input->slice(1, $llength)->toString('hex'); 132 | $decoded = []; 133 | 134 | if ($hexLength === '00') { 135 | throw new RuntimeException('Invalid RLP.'); 136 | } 137 | $length = hexdec($hexLength); 138 | $totalLength = $llength + $length; 139 | 140 | if ($totalLength > $input->length()) { 141 | throw new RuntimeException('Invalid RLP: total length is bigger than data length.'); 142 | } 143 | $innerRemainder = $input->slice($llength, $totalLength); 144 | 145 | if ($innerRemainder->length() === 0) { 146 | throw new RuntimeException('Invalid RLP: list has invalid length.'); 147 | } 148 | 149 | while ($innerRemainder->length()) { 150 | $data = $this->decodeData($innerRemainder); 151 | $decoded[] = $data['data']; 152 | $innerRemainder = $data['remainder']; 153 | } 154 | return [ 155 | 'data' => $decoded, 156 | 'remainder' => $input->slice($length) 157 | ]; 158 | } 159 | } 160 | 161 | /** 162 | * encodeLength 163 | * 164 | * @param int $length 165 | * @param int $offset 166 | * @return Buffer 167 | */ 168 | protected function encodeLength(int $length, int $offset): Buffer 169 | { 170 | // if (!is_int($length) || !is_int($offset)) { 171 | // throw new InvalidArgumentException('Length and offset must be int when call encodeLength.'); 172 | // } 173 | if ($length < 56) { 174 | // var_dump($length, $offset); 175 | return new Buffer(strval($length + $offset)); 176 | } 177 | $hexLength = $this->intToHex($length); 178 | $firstByte = $this->intToHex($offset + 55 + (strlen($hexLength) / 2)); 179 | return new Buffer(strval($firstByte . $hexLength), 'hex'); 180 | } 181 | 182 | /** 183 | * intToHex 184 | * 185 | * @param int $value 186 | * @return string 187 | */ 188 | protected function intToHex(int $value): string 189 | { 190 | // if (!is_int($value)) { 191 | // throw new InvalidArgumentException('Value must be int when call intToHex.'); 192 | // } 193 | $hex = dechex($value); 194 | 195 | return $this->padToEven($hex); 196 | } 197 | 198 | /** 199 | * padToEven 200 | * 201 | * @param string $value 202 | * @return string 203 | */ 204 | protected function padToEven(string $value): string 205 | { 206 | // if (!is_string($value)) { 207 | // throw new InvalidArgumentException('Value must be string when call padToEven.'); 208 | // } 209 | if ((strlen($value) % 2) !== 0 ) { 210 | $value = '0' . $value; 211 | } 212 | return $value; 213 | } 214 | 215 | /** 216 | * toArray 217 | * Format input to value, deprecated when we have toBuffer. 218 | * 219 | * @param mixed $input 220 | * @return array 221 | */ 222 | protected function toArray($input) 223 | { 224 | if (is_string($input)) { 225 | if (strpos($input, '0x') === 0) { 226 | // hex string 227 | $value = str_replace('0x', '', $input); 228 | return $input; 229 | } else { 230 | return str_split($input, 1); 231 | } 232 | } 233 | throw new InvalidArgumentException('The input type didn\'t support.'); 234 | } 235 | 236 | /** 237 | * toBuffer 238 | * Format input to buffer. 239 | * 240 | * @param mixed $input 241 | * @return Buffer 242 | */ 243 | protected function toBuffer($input): Buffer 244 | { 245 | if (is_string($input)) { 246 | if (strpos($input, '0x') === 0) { 247 | // hex string 248 | // $input = str_replace('0x', '', $input); 249 | return new Buffer($input, 'hex'); 250 | } 251 | return new Buffer(str_split($input, 1)); 252 | } elseif (is_numeric($input)) { 253 | if (!$input || $input < 0) { 254 | return new Buffer([]); 255 | } 256 | if (is_float($input)) { 257 | $input = number_format($input, 0, '', ''); 258 | var_dump($input); 259 | } 260 | $gmpInput = gmp_init($input, 10); 261 | return new Buffer('0x' . gmp_strval($gmpInput, 16), 'hex'); 262 | } elseif ($input === null) { 263 | return new Buffer([]); 264 | } elseif (is_array($input)) { 265 | return new Buffer($input); 266 | } elseif ($input instanceof Buffer) { 267 | return $input; 268 | } 269 | throw new InvalidArgumentException('The input type didn\'t support.'); 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/Utils.php: -------------------------------------------------------------------------------- 1 | encode($data)->toString('hex'); 42 | } 43 | 44 | /** 45 | * 46 | * @param string $str 47 | * @param int $bit 48 | * @return string 49 | */ 50 | public static function fill0($str, $bit = 64) 51 | { 52 | $str_len = strlen($str); 53 | $zero = ''; 54 | for ($i = $str_len; $i < $bit; $i++) { 55 | $zero .= "0"; 56 | } 57 | $real_str = $zero . $str; 58 | return $real_str; 59 | } 60 | 61 | /** 62 | * ether to wei 63 | */ 64 | public static function ethToWei($value, $hex = false) 65 | { 66 | $value = bcmul($value, '1000000000000000000'); 67 | if ($hex) { 68 | return self::decToHex($value, $hex); 69 | } 70 | return $value; 71 | } 72 | 73 | /** 74 | * wei to ether 75 | */ 76 | public static function weiToEth($value, $hex = false) 77 | { 78 | if (strtolower(substr($value, 0, 2)) == '0x') { 79 | $value = self::hexToDec(self::remove0x($value)); 80 | } 81 | $value = bcdiv($value, '1000000000000000000', 18); 82 | if ($hex) { 83 | return '0x' . self::decToHex($value); 84 | } 85 | return $value; 86 | } 87 | 88 | /** 89 | * change to hex(0x) 90 | * @param string|number $value 91 | * @param boolean $mark 92 | * @return string 93 | */ 94 | public static function decToHex($value, $mark = true) 95 | { 96 | $hexvalues = [ 97 | '0', '1', '2', '3', '4', '5', '6', '7', 98 | '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' 99 | ]; 100 | $hexval = ''; 101 | while ($value != '0') { 102 | $hexval = $hexvalues[bcmod($value, '16')] . $hexval; 103 | $value = bcdiv($value, '16', 0); 104 | } 105 | 106 | return ($mark ? '0x' . $hexval : $hexval); 107 | } 108 | 109 | /** 110 | * change to hex(0x) 111 | * @param string $number hex number 112 | * @return string 113 | */ 114 | public static function hexToDec($number) 115 | { 116 | // have 0x,remove it 117 | $number = self::remove0x(strtolower($number)); 118 | $decvalues = [ 119 | '0' => '0', '1' => '1', '2' => '2', 120 | '3' => '3', '4' => '4', '5' => '5', 121 | '6' => '6', '7' => '7', '8' => '8', 122 | '9' => '9', 'a' => '10', 'b' => '11', 123 | 'c' => '12', 'd' => '13', 'e' => '14', 124 | 'f' => '15']; 125 | $decval = '0'; 126 | $number = strrev($number); 127 | for ($i = 0; $i < strlen($number); $i++) { 128 | $decval = bcadd(bcmul(bcpow('16', $i, 0), $decvalues[$number[$i]]), $decval); 129 | } 130 | return $decval; 131 | } 132 | 133 | /** 134 | * jsonMethodToString 135 | * 136 | * @param stdClass|array $json 137 | * @return string 138 | */ 139 | public static function jsonMethodToString($json) 140 | { 141 | if ($json instanceof stdClass) { 142 | // one way to change whole json stdClass to array type 143 | // $jsonString = json_encode($json); 144 | 145 | // if (JSON_ERROR_NONE !== json_last_error()) { 146 | // throw new InvalidArgumentException('json_decode error: ' . json_last_error_msg()); 147 | // } 148 | // $json = json_decode($jsonString, true); 149 | 150 | // another way to change whole json to array type but need the depth 151 | // $json = self::jsonToArray($json, $depth) 152 | 153 | // another way to change json to array type but not whole json stdClass 154 | $json = (array)$json; 155 | $typeName = []; 156 | 157 | foreach ($json['inputs'] as $param) { 158 | if (isset($param->type)) { 159 | $typeName[] = $param->type; 160 | } 161 | } 162 | return $json['name'] . '(' . implode(',', $typeName) . ')'; 163 | } elseif (!is_array($json)) { 164 | throw new \Exception('jsonMethodToString json must be array or stdClass.'); 165 | } 166 | if (isset($json['name']) && strpos($json['name'], '(') > 0) { 167 | return $json['name']; 168 | } 169 | $typeName = []; 170 | 171 | foreach ($json['inputs'] as $param) { 172 | if (isset($param['type'])) { 173 | $typeName[] = $param['type']; 174 | } 175 | } 176 | return $json['name'] . '(' . implode(',', $typeName) . ')'; 177 | } 178 | 179 | /** 180 | * sha3 181 | * keccak256 182 | * 183 | * @param string $value 184 | * @return string 185 | */ 186 | public static function sha3($value) 187 | { 188 | if (!is_string($value)) { 189 | throw new InvalidArgumentException('The value to sha3 function must be string.'); 190 | } 191 | if (strpos($value, '0x') === 0) { 192 | $value = self::hexToBin($value); 193 | } 194 | $hash = Keccak::hash($value, 256); 195 | if ($hash === self::SHA3_NULL_HASH) { 196 | return null; 197 | } 198 | return '0x' . $hash; 199 | } 200 | /** 201 | * hexToBin 202 | * 203 | * @param string 204 | * @return string 205 | */ 206 | public static function hexToBin($value) 207 | { 208 | if (!is_string($value)) { 209 | throw new \Exception('The value to hexToBin function must be string.'); 210 | } 211 | if (self::isZeroPrefixed($value)) { 212 | $count = 1; 213 | $value = str_replace('0x', '', $value, $count); 214 | } 215 | return pack('H*', $value); 216 | } 217 | 218 | /** 219 | * isZeroPrefixed 220 | * 221 | * @param string 222 | * @return bool 223 | */ 224 | public static function isZeroPrefixed($value) 225 | { 226 | if (!is_string($value)) { 227 | throw new \Exception('The value to isZeroPrefixed function must be string.'); 228 | } 229 | return (strpos($value, '0x') === 0); 230 | } 231 | 232 | public static function hexToString($value){ 233 | return pack("H*",$value); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/Wallet.php: -------------------------------------------------------------------------------- 1 | address = $address; 17 | $this->privateKey = $privateKey; 18 | } 19 | 20 | static public function create(): Wallet 21 | { 22 | $ec = new EC('secp256k1'); 23 | $kp = $ec->genKeyPair(); 24 | $privateKey = $kp->getPrivate('hex'); 25 | $pub = $kp->getPublic('hex'); 26 | $address = Utils::pubKeyToAddress($pub); 27 | return new Wallet($address, $privateKey); 28 | } 29 | 30 | 31 | static public function createByPrivate($privateKey): Wallet 32 | { 33 | $ec = new EC('secp256k1'); 34 | // Generate keys 35 | $key = $ec->keyFromPrivate($privateKey); 36 | $pub = $key->getPublic('hex'); 37 | // get address based on public key 38 | return new Wallet(strtolower(Utils::pubKeyToAddress($pub)), $privateKey); 39 | } 40 | 41 | 42 | public function getAddress() 43 | { 44 | return $this->address; 45 | } 46 | 47 | public function getPrivateKey() 48 | { 49 | return Utils::add0x($this->privateKey); 50 | } 51 | 52 | /** 53 | * @throws \Exception 54 | */ 55 | public function sign($data) 56 | { 57 | 58 | if (empty($this->privateKey)) { 59 | throw new \Exception("please unlock this address"); 60 | } 61 | $hash = Keccak::hash(hex2bin($data), 256); 62 | $ec = new EC('secp256k1'); 63 | // Generate keys 64 | $key = $ec->keyFromPrivate($this->privateKey); 65 | // Sign message (can be hex sequence or array) 66 | return $key->sign($hash, ['canonical' => true]); 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /src/Web3.php: -------------------------------------------------------------------------------- 1 | provider; 17 | } 18 | 19 | public function __construct($url) 20 | { 21 | $this->url = $url; 22 | $this->client = new GuzzleHttp(array_merge(['timeout' => 60, 'verify' => false], ['base_uri' => $url])); 23 | } 24 | 25 | /** 26 | * Returns the current client version 27 | * @throws \Exception 28 | * Returns: String - the current client version 29 | */ 30 | public function clientVersion() 31 | { 32 | return $this->request('web3_clientVersion', []); 33 | } 34 | 35 | /** 36 | * Returns Keccak-256 (not the standardized SHA3-256) of the given data. 37 | * @param string data 38 | * @throws \Exception 39 | * Returns: DATA - The SHA3 result of the given string. 40 | */ 41 | public function sha3(string $data) 42 | { 43 | return $this->request('web3_sha3', [$data]); 44 | } 45 | 46 | /** 47 | * Returns the current network id. 48 | * @throws \Exception 49 | * Returns: String - The current network id. 50 | * "1": Ethereum Mainnet 51 | * "2": Morden Testnet (deprecated) 52 | * "3": Ropsten Testnet 53 | * "4": Rinkeby Testnet 54 | * "42": Kovan Testnet 55 | */ 56 | 57 | public function netVersion() 58 | { 59 | if (empty($this->chainId)) { 60 | $this->chainId = $this->request('net_version', []); 61 | } 62 | return $this->chainId; 63 | } 64 | 65 | /** 66 | * Returns true if client is actively listening for network connections. 67 | * @throws \Exception 68 | * Returns: Boolean - true when listening, otherwise false. 69 | */ 70 | 71 | public function netListening() 72 | { 73 | return $this->request('net_listening', []); 74 | } 75 | 76 | /** 77 | * Returns number of peers currently connected to the client. 78 | * @throws \Exception 79 | * Returns: QUANTITY - integer of the number of connected peers. 80 | */ 81 | 82 | public function netPeerCount() 83 | { 84 | return $this->request('net_peerCount', []); 85 | } 86 | 87 | /** 88 | * Returns the current ethereum protocol version. 89 | * @throws \Exception 90 | * Returns: String - The current ethereum protocol version 91 | */ 92 | 93 | public function protocolVersion() 94 | { 95 | return $this->request('eth_protocolVersion', []); 96 | } 97 | 98 | /** 99 | * Returns an object with data about the sync status or false. 100 | * @throws \Exception 101 | * Returns: Object|Boolean, An object with sync status data or FALSE, when not syncing: 102 | * startingBlock: QUANTITY - The block at which the import started (will only be reset, after the sync reached his head) 103 | * currentBlock: QUANTITY - The current block, same as eth_blockNumber 104 | * highestBlock: QUANTITY - The estimated highest block 105 | */ 106 | 107 | public function syncing() 108 | { 109 | return $this->request('eth_syncing', []); 110 | } 111 | 112 | /** 113 | * Returns the client coinbase address. 114 | * @throws \Exception 115 | * Returns: DATA, 20 bytes - the current coinbase address. 116 | */ 117 | 118 | public function coinbase() 119 | { 120 | return $this->request('eth_coinbase', []); 121 | } 122 | 123 | /** 124 | * Returns true if client is actively mining new blocks. 125 | * @throws \Exception 126 | * Returns: Boolean - returns true of the client is mining, otherwise false. 127 | */ 128 | 129 | public function mining() 130 | { 131 | return $this->request('eth_mining', []); 132 | } 133 | 134 | /** 135 | * Returns the number of hashes per second that the node is mining with. 136 | * @throws \Exception 137 | * Returns: QUANTITY - number of hashes per second. 138 | */ 139 | 140 | public function hashrate() 141 | { 142 | return $this->request('eth_hashrate', []); 143 | } 144 | 145 | /** 146 | * Returns the current price per gas in wei. 147 | * @throws \Exception 148 | * Returns: QUANTITY - integer of the current gas price in wei. 149 | */ 150 | 151 | public function gasPrice() 152 | { 153 | return $this->request('eth_gasPrice', []); 154 | } 155 | 156 | /** 157 | * Returns a list of addresses owned by client. 158 | * @throws \Exception 159 | * Returns: Array of DATA, 20 Bytes - addresses owned by the client. 160 | */ 161 | 162 | public function accounts() 163 | { 164 | return $this->request('eth_accounts', []); 165 | } 166 | 167 | /** 168 | * Returns the balance of the account of given address. 169 | * @param $address , 20 Bytes - address to check for balance. 170 | * @param string $block - integer block number, or the string "latest", "earliest" or "pending", see the default block parameter 171 | * @throws \Exception 172 | * Returns: QUANTITY - integer of the current balance in wei. 173 | */ 174 | 175 | public function getBalance($address, string $block = 'latest') 176 | { 177 | return $this->request('eth_getBalance', [$address, $block]); 178 | } 179 | 180 | /** 181 | * Returns the value from a storage position at a given address. 182 | * @param $data 183 | * @param $quantity1 184 | * @param string $quantity2 185 | * @return mixed 186 | * @throws \Exception 187 | * Returns: DATA - the value at this storage position. 188 | */ 189 | public function getStorageAt($data, $quantity1, string $quantity2 = Quantity::latest) 190 | { 191 | return $this->request('eth_getStorageAt', [$data, $quantity1, $quantity2]); 192 | } 193 | 194 | /** 195 | * Returns the number of transactions sent from an address. 196 | * @param $address 197 | * @param $quantity 198 | * @throws \Exception 199 | * Returns: QUANTITY - integer of the number of transactions send from this address. 200 | */ 201 | public function getTransactionCount($address, $quantity = Quantity::latest) 202 | { 203 | return $this->request('eth_getTransactionCount', [$address, $quantity]); 204 | } 205 | 206 | /** 207 | * Returns the number of transactions in a block from a block matching the given block hash. 208 | * @param $hash 209 | * @throws \Exception 210 | * Returns: QUANTITY - integer of the number of transactions in this block. 211 | */ 212 | public function getBlockTransactionCountByHash($hash) 213 | { 214 | return $this->request('eth_getBlockTransactionCountByHash', [$hash]); 215 | } 216 | 217 | /** 218 | * Returns the number of transactions in a block matching the given block number. 219 | * @param string $quantity 220 | * @throws \Exception 221 | * Returns: QUANTITY - integer of the number of transactions in this block. 222 | */ 223 | public function getBlockTransactionCountByNumber(string $quantity = Quantity::latest) 224 | { 225 | return $this->request('eth_getBlockTransactionCountByNumber', [$quantity]); 226 | } 227 | 228 | /** 229 | * Returns the number of uncles in a block from a block matching the given block hash. 230 | * @param $hash 231 | * @throws \Exception 232 | * Returns: QUANTITY - integer of the number of uncles in this block. 233 | */ 234 | public function getUncleCountByBlockHash($hash) 235 | { 236 | return $this->request('eth_getUncleCountByBlockHash', [$hash]); 237 | } 238 | 239 | /** 240 | * Returns the number of uncles in a block from a block matching the given block number. 241 | * @param $quantity 242 | * @throws \Exception 243 | * Returns: QUANTITY - integer of the number of uncles in this block. 244 | */ 245 | public function getUncleCountByBlockNumber($quantity = Quantity::latest) 246 | { 247 | return $this->request('eth_getUncleCountByBlockNumber', [$quantity]); 248 | } 249 | 250 | /** 251 | * Returns code at a given address. 252 | * @param $data 253 | * @param string $quantity 254 | * @throws \Exception 255 | * Returns: DATA - the code from the given address. 256 | */ 257 | public function getCode($data, string $quantity = Quantity::latest) 258 | { 259 | return $this->request('eth_getCode', [$data, $quantity]); 260 | } 261 | 262 | /** 263 | * The sign method calculates an Ethereum specific signature with: 264 | * sign(keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))). 265 | * 266 | * By adding a prefix to the message makes the calculated signature recognisable as an Ethereum specific signature. 267 | * This prevents misuse where a malicious DApp can sign arbitrary data (e.g. transaction) and use the signature to impersonate the victim. 268 | * 269 | * Note the address to sign with must be unlocked. 270 | * @param $address 271 | * @param $data 272 | * @throws \Exception 273 | * Returns: DATA: Signature 274 | */ 275 | public function sign($address, $data) 276 | { 277 | return $this->request('eth_sign', [$address, $data]); 278 | } 279 | 280 | /** 281 | * Creates new message call transaction or a contract creation, if the data field contains code. 282 | * @param $from 283 | * @param $to 284 | * @param $gas 285 | * @param $value 286 | * @param $data 287 | * @param $gasPrice 288 | * @param $nonce 289 | * @throws \Exception 290 | * Returns: DATA, 32 Bytes - the transaction hash, or the zero hash if the transaction is not yet available. 291 | * Use eth_getTransactionReceipt to get the contract address, after the transaction was mined, when you created a contract. 292 | */ 293 | public function sendTransaction($from, $to, $gas = 90000, $value = '0x0', $data = '0x0', $gasPrice = null, $nonce = null) 294 | { 295 | $data = [ 296 | 'from' => $from, 297 | 'to' => $to, 298 | 'gas' => $gas, 299 | 'value' => $value, 300 | 'data' => $data, 301 | ]; 302 | if (!empty($nonce)) { 303 | $data['nonce'] = $nonce; 304 | } 305 | if (!empty($gasPrice)) { 306 | $data['gasPrice'] = $gasPrice; 307 | } 308 | return $this->request('eth_sendTransaction', $data); 309 | } 310 | 311 | /** 312 | * Creates new message call transaction or a contract creation for signed transactions. 313 | * @param $data 314 | * @throws \Exception 315 | * Returns: DATA, 32 Bytes - the transaction hash, or the zero hash if the transaction is not yet available. 316 | * Use eth_getTransactionReceipt to get the contract address, after the transaction was mined, when you created a contract. 317 | */ 318 | public function sendRawTransaction($data) 319 | { 320 | return $this->request('eth_sendRawTransaction', [$data]); 321 | } 322 | 323 | /** 324 | * Executes a new message call immediately without creating a transaction on the block chain. 325 | * @param $to 326 | * @param string $data 327 | * @param $from 328 | * @param $gas 329 | * @param $value 330 | * @param $gasPrice 331 | * @return mixed 332 | * @throws \Exception Returns: DATA - the return value of executed contract. 333 | */ 334 | public function call( $to, string $data = '0x0', $from = null, $gas = null, $value = null, $gasPrice = null,$quantity=Quantity::latest) 335 | { 336 | $data0 = [ 337 | 'to' => $to, 338 | ]; 339 | 340 | if (!empty($gas)) { 341 | $data0['gas'] = $gas; 342 | } 343 | if (!empty($from)) { 344 | $data0['from'] = $from; 345 | } 346 | if (!empty($value)) { 347 | $data0['value'] = $value; 348 | } 349 | if (!empty($data)) { 350 | $data0['data'] = $data; 351 | } 352 | if (!empty($gasPrice)) { 353 | $data0['gasPrice'] = $gasPrice; 354 | } 355 | return $this->request('eth_call', [$data0,$quantity]); 356 | } 357 | 358 | /** 359 | * Generates and returns an estimate of how much gas is necessary to allow the transaction to complete.The transaction will not be added to the blockchain. 360 | * Note that the estimate may be significantly more than the amount of gas actually used by the transaction, for a variety of reasons including EVM mechanics and node performance. 361 | * @param $to 362 | * @param string $data 363 | * @param $from 364 | * @param $gas 365 | * @param $value 366 | * @param $gasPrice 367 | * @param $nonce 368 | * @throws \Exception 369 | * Returns: QUANTITY - the amount of gas used. 370 | */ 371 | public function estimateGas($to, string $data = '0x0', $from = null, $gas = null, $value = null, $gasPrice = null, $nonce = null) 372 | { 373 | $data0 = []; 374 | if (!empty($from)) { 375 | $data0['from'] = $from; 376 | } 377 | $data0['to'] = $to; 378 | if (!empty($gas)) { 379 | $data0['gas'] = $gas; 380 | } 381 | 382 | if (!empty($value)) { 383 | $data0['value'] = $value; 384 | } 385 | if (!empty($data)) { 386 | $data0['data'] = $data; 387 | } 388 | if (!empty($gasPrice)) { 389 | $data0['gasPrice'] = $gasPrice; 390 | } 391 | return $this->request('eth_estimateGas', [$data0]); 392 | } 393 | 394 | /** 395 | * Returns information about a block by hash. 396 | * @param $hash 397 | * @param $full If true it returns the full transaction objects, if false it returns only the hashes of the transactions. 398 | * @throws \Exception 399 | * Returns: Object - A block object, or null when no block was found: 400 | * number: QUANTITY - the block number. null when its pending block. 401 | * hash: DATA, 32 Bytes - hash of the block. null when its pending block. 402 | * parentHash: DATA, 32 Bytes - hash of the parent block. 403 | * nonce: DATA, 8 Bytes - hash of the generated proof-of-work. null when its pending block. 404 | * sha3Uncles: DATA, 32 Bytes - SHA3 of the uncles data in the block. 405 | * logsBloom: DATA, 256 Bytes - the bloom filter for the logs of the block. null when its pending block. 406 | * transactionsRoot: DATA, 32 Bytes - the root of the transaction trie of the block. 407 | * stateRoot: DATA, 32 Bytes - the root of the final state trie of the block. 408 | * receiptsRoot: DATA, 32 Bytes - the root of the receipts trie of the block. 409 | * miner: DATA, 20 Bytes - the address of the beneficiary to whom the mining rewards were given. 410 | * difficulty: QUANTITY - integer of the difficulty for this block. 411 | * totalDifficulty: QUANTITY - integer of the total difficulty of the chain until this block. 412 | * extraData: DATA - the “extra data” field of this block. 413 | * size: QUANTITY - integer the size of this block in bytes. 414 | * gasLimit: QUANTITY - the maximum gas allowed in this block. 415 | * gasUsed: QUANTITY - the total used gas by all transactions in this block. 416 | * timestamp: QUANTITY - the unix timestamp for when the block was collated. 417 | * transactions: Array - Array of transaction objects, or 32 Bytes transaction hashes depending on the last given parameter. 418 | * uncles: Array - Array of uncle hashes. 419 | */ 420 | public function getBlockByHash($hash,$full=false) 421 | { 422 | 423 | return $this->request('eth_getBlockByHash', [$hash,$full]); 424 | } 425 | 426 | /** 427 | * Returns information about a block by block number. 428 | * @param string $quantity 429 | * @param bool $allObject 430 | * @throws \Exception 431 | * Returns: See eth_getBlockByHash 432 | */ 433 | public function getBlockByNumber(string $quantity = Quantity::latest, bool $allObject = false) 434 | { 435 | 436 | return $this->request('eth_getBlockByNumber', [$quantity, $allObject]); 437 | } 438 | 439 | /** 440 | * Returns the information about a transaction requested by transaction hash. 441 | * @param $hash 442 | * @throws \Exception 443 | * Returns: Object - A transaction object, or null when no transaction was found: 444 | * blockHash: DATA, 32 Bytes - hash of the block where this transaction was in. null when its pending. 445 | * blockNumber: QUANTITY - block number where this transaction was in. null when its pending. 446 | * from: DATA, 20 Bytes - address of the sender. 447 | * gas: QUANTITY - gas provided by the sender. 448 | * gasPrice: QUANTITY - gas price provided by the sender in Wei. 449 | * hash: DATA, 32 Bytes - hash of the transaction. 450 | * input: DATA - the data send along with the transaction. 451 | * nonce: QUANTITY - the number of transactions made by the sender prior to this one. 452 | * to: DATA, 20 Bytes - address of the receiver. null when its a contract creation transaction. 453 | * transactionIndex: QUANTITY - integer of the transactions index position in the block. null when its pending. 454 | * value: QUANTITY - value transferred in Wei. 455 | * v: QUANTITY - ECDSA recovery id 456 | * r: DATA, 32 Bytes - ECDSA signature r 457 | * s: DATA, 32 Bytes - ECDSA signature s 458 | */ 459 | public function getTransactionByHash($hash) 460 | { 461 | 462 | return $this->request('eth_getTransactionByHash', [$hash]); 463 | } 464 | 465 | /** 466 | * Returns information about a transaction by block hash and transaction index position. 467 | * @param $hash 468 | * @param int $index 469 | * @throws \Exception 470 | * Returns: See eth_getTransactionByHash 471 | */ 472 | public function getTransactionByBlockHashAndIndex($hash, int $index = 0) 473 | { 474 | 475 | return $this->request('eth_getTransactionByBlockHashAndIndex', [$hash, $index]); 476 | } 477 | 478 | /** 479 | * Returns information about a transaction by block number and transaction index position. 480 | * @param string $quantity 481 | * @param int $index 482 | * @throws \Exception 483 | * Returns: See eth_getTransactionByHash 484 | */ 485 | public function getTransactionByBlockNumberAndIndex(string $quantity = Quantity::latest, int $index = 0) 486 | { 487 | 488 | return $this->request('eth_getTransactionByBlockNumberAndIndex', [$quantity, $index]); 489 | } 490 | 491 | /** 492 | * Returns the receipt of a transaction by transaction hash. 493 | * Note That the receipt is not available for pending transactions. 494 | * @param $hash 495 | * @throws \Exception 496 | * Returns: Object - A transaction receipt object, or null when no receipt was found: 497 | * transactionHash : DATA, 32 Bytes - hash of the transaction. 498 | * transactionIndex: QUANTITY - integer of the transactions index position in the block. 499 | * blockHash: DATA, 32 Bytes - hash of the block where this transaction was in. 500 | * blockNumber: QUANTITY - block number where this transaction was in. 501 | * from: DATA, 20 Bytes - address of the sender. 502 | * to: DATA, 20 Bytes - address of the receiver. null when its a contract creation transaction. 503 | * cumulativeGasUsed : QUANTITY - The total amount of gas used when this transaction was executed in the block. 504 | * gasUsed : QUANTITY - The amount of gas used by this specific transaction alone. 505 | * contractAddress : DATA, 20 Bytes - The contract address created, if the transaction was a contract creation, otherwise null. 506 | * logs: Array - Array of log objects, which this transaction generated. 507 | * logsBloom: DATA, 256 Bytes - Bloom filter for light clients to quickly retrieve related logs. 508 | * It also returns either : 509 | * root : DATA 32 bytes of post-transaction stateroot (pre Byzantium) 510 | * status: QUANTITY either 1 (success) or 0 (failure) 511 | */ 512 | public function getTransactionReceipt($hash) 513 | { 514 | 515 | return $this->request('eth_getTransactionReceipt', [$hash]); 516 | } 517 | 518 | /** 519 | * Returns information about a uncle of a block by hash and uncle index position. 520 | * @param $hash 521 | * @param int $index 522 | * @throws \Exception 523 | * Returns: See eth_getBlockByHash 524 | */ 525 | public function getUncleByBlockHashAndIndex($hash, int $index = 0) 526 | { 527 | 528 | return $this->request('eth_getUncleByBlockHashAndIndex', [$hash, $index]); 529 | } 530 | 531 | /** 532 | * Returns information about a uncle of a block by number and uncle index position. 533 | * @param string $quantity 534 | * @param int $index 535 | * @throws \Exception 536 | * Returns: See eth_getBlockByHash 537 | * Note: An uncle doesn’t contain individual transactions. 538 | */ 539 | public function getUncleByBlockNumberAndIndex(string $quantity = Quantity::latest, int $index = 0) 540 | { 541 | 542 | return $this->request('eth_getUncleByBlockNumberAndIndex', [$quantity, $index]); 543 | } 544 | 545 | /** 546 | * Returns a list of available compilers in the client. 547 | * @throws \Exception 548 | * Returns: Array - Array of available compilers. 549 | */ 550 | public function getCompilers() 551 | { 552 | 553 | return $this->request('eth_getCompilers', []); 554 | } 555 | 556 | /** 557 | * Returns compiled solidity code. 558 | * @param $source 559 | * @throws \Exception 560 | * Returns: DATA - The compiled source code. 561 | */ 562 | public function compileSolidity($source) 563 | { 564 | 565 | return $this->request('eth_compileSolidity', [$source]); 566 | } 567 | 568 | /** 569 | * Returns compiled LLL code. 570 | * @param $source 571 | * @throws \Exception 572 | * Returns: DATA - The compiled source code. 573 | */ 574 | public function compileLLL($source) 575 | { 576 | 577 | return $this->request('eth_compileLLL', [$source]); 578 | } 579 | 580 | /** 581 | * Returns compiled serpent code. 582 | * @param $source 583 | * @throws \Exception 584 | * Returns: DATA - The compiled source code. 585 | */ 586 | public function compileSerpent($source) 587 | { 588 | 589 | return $this->request('eth_compileSerpent', [$source]); 590 | } 591 | 592 | /** 593 | * Creates a filter object, based on filter options, to notify when the state changes (logs). 594 | * To check if the state has changed, call eth_getFilterChanges. 595 | * 596 | * A note on specifying topic filters: 597 | * Topics are order-dependent. A transaction with a log with topics [A, B] will be matched by the following topic filters: 598 | * [] “anything” 599 | * [A] “A in first position (and anything after)” 600 | * [null, B] “anything in first position AND B in second position (and anything after)” 601 | * [A, B] “A in first position AND B in second position (and anything after)” 602 | * [[A, B], [A, B]] “(A OR B) in first position AND (A OR B) in second position (and anything after)” 603 | * 604 | * @param $fromBlock 605 | * @param $toBlock 606 | * @param $address 607 | * @param $topic 608 | * @throws \Exception 609 | * Returns: QUANTITY - A filter id. 610 | */ 611 | public function newFilter($fromBlock = null, $toBlock = null, $address = null, $topic = []) 612 | { 613 | $data = $this->getData($fromBlock, $toBlock, $address, $topic); 614 | return $this->request('eth_newFilter', [$data]); 615 | } 616 | 617 | /** 618 | * Creates a filter in the node, to notify when a new block arrives.To check if the state has changed, call eth_getFilterChanges. 619 | * @throws \Exception 620 | * Returns: QUANTITY - A filter id. 621 | */ 622 | public function newBlockFilter() 623 | { 624 | return $this->request('eth_newBlockFilter', []); 625 | } 626 | 627 | /** 628 | * Creates a filter in the node, to notify when new pending transactions arrive.To check if the state has changed, call eth_getFilterChanges. 629 | * @throws \Exception 630 | * Returns: QUANTITY - A filter id. 631 | */ 632 | public function newPendingTransactionFilter() 633 | { 634 | return $this->request('eth_newPendingTransactionFilter', []); 635 | } 636 | 637 | /** 638 | * Uninstalls a filter with given id. Should always be called when watch is no longer needed. 639 | * Additonally Filters timeout when they aren’t requested with eth_getFilterChanges for a period of time. 640 | * @param $filterNumber 641 | * @throws \Exception 642 | * Returns: Boolean - true if the filter was successfully uninstalled, otherwise false. 643 | */ 644 | public function uninstallFilter($filterNumber) 645 | { 646 | return $this->request('eth_uninstallFilter', [$filterNumber]); 647 | } 648 | 649 | /** 650 | * Polling method for a filter, which returns an array of logs which occurred since last poll. 651 | * @param $filterNumber 652 | * @throws \Exception 653 | * Returns: Array - Array of log objects, or an empty array if nothing has changed since last poll. 654 | * For filters created with eth_newBlockFilter the return are block hashes (DATA, 32 Bytes), e.g. ["0x3454645634534..."]. 655 | * For filters created with eth_newPendingTransactionFilter the return are transaction hashes (DATA, 32 Bytes), e.g. ["0x6345343454645..."]. 656 | * For filters created with eth_newFilter logs are objects with following params: 657 | * removed: TAG - true when the log was removed, due to a chain reorganization. false if its a valid log. 658 | * logIndex: QUANTITY - integer of the log index position in the block. null when its pending log. 659 | * transactionIndex: QUANTITY - integer of the transactions index position log was created from. null when its pending log. 660 | * transactionHash: DATA, 32 Bytes - hash of the transactions this log was created from. null when its pending log. 661 | * blockHash: DATA, 32 Bytes - hash of the block where this log was in. null when its pending. null when its pending log. 662 | * blockNumber: QUANTITY - the block number where this log was in. null when its pending. null when its pending log. 663 | * address: DATA, 20 Bytes - address from which this log originated. 664 | * data: DATA - contains one or more 32 Bytes non-indexed arguments of the log. 665 | * topics: Array of DATA - Array of 0 to 4 32 Bytes DATA of indexed log arguments. 666 | * (In solidity: The first topic is the hash of the signature of the event (e.g. Deposit(address,bytes32,uint256)), except you declared the event with the anonymous specifier.) 667 | */ 668 | public function getFilterChanges($filterNumber) 669 | { 670 | return $this->request('eth_getFilterChanges', [$filterNumber]); 671 | } 672 | 673 | /** 674 | * Returns an array of all logs matching filter with given id. 675 | * @param $filterNumber 676 | * @throws \Exception 677 | * Returns: See eth_getFilterChanges 678 | */ 679 | public function getFilterLogs($filterNumber) 680 | { 681 | return $this->request('eth_getFilterLogs', [$filterNumber]); 682 | } 683 | 684 | /** 685 | * Returns an array of all logs matching a given filter object. 686 | * @param $fromBlock 687 | * @param $toBlock 688 | * @param $address 689 | * @param $topic 690 | * @throws \Exception 691 | * Returns: See eth_getFilterChanges 692 | */ 693 | public function getLogs(string $fromBlock = null, string $toBlock = null, $address = null, $topic = []) 694 | { 695 | $data = $this->getData($fromBlock, $toBlock, $address, $topic); 696 | return $this->request('eth_getLogs', [$data]); 697 | } 698 | 699 | /** 700 | * Returns the hash of the current block, the seedHash, and the boundary condition to be met (“target”). 701 | * @throws \Exception 702 | * Returns: Array - Array with the following properties: 703 | * DATA, 32 Bytes - current block header pow-hash 704 | * DATA, 32 Bytes - the seed hash used for the DAG. 705 | * DATA, 32 Bytes - the boundary condition (“target”), 2^256 / difficulty. 706 | */ 707 | public function getWork() 708 | { 709 | return $this->request('eth_getWork', []); 710 | } 711 | 712 | /** 713 | * Used for submitting a proof-of-work solution. 714 | * @param $data1 715 | * @param $data2 716 | * @param $data3 717 | * @throws \Exception 718 | * Returns: Boolean - returns true if the provided solution is valid, otherwise false. 719 | */ 720 | public function submitWork($data1, $data2, $data3) 721 | { 722 | return $this->request('eth_submitWork', [$data1, $data2 . $data3]); 723 | } 724 | 725 | /** 726 | * Used for submitting mining hashrate. 727 | * @param $hashRate 728 | * @param $id 729 | * @throws \Exception 730 | * Returns: Boolean - returns true if submitting went through succesfully and false otherwise. 731 | */ 732 | public function submitHashrate($hashRate, $id) 733 | { 734 | return $this->request('eth_submitHashrate', [$hashRate, $id]); 735 | } 736 | 737 | /** 738 | * Returns the number of most recent block. 739 | * @throws \Exception 740 | * Returns: QUANTITY - integer of the current block number the client is on. 741 | */ 742 | public function blockNumber() 743 | { 744 | return $this->request('eth_blockNumber', []); 745 | } 746 | 747 | /** 748 | * Returns the current whisper protocol version. 749 | * @throws \Exception 750 | * Returns: String - The current whisper protocol version 751 | */ 752 | public function shhVersion() 753 | { 754 | return $this->request('shh_version', []); 755 | } 756 | 757 | /** 758 | * Returns the current network id. 759 | * @throws \Exception 760 | * Returns: String - The current network id. 761 | * "1": Ethereum Mainnet 762 | * "2": Morden Testnet (deprecated) 763 | * "3": Ropsten Testnet 764 | * "4": Rinkeby Testnet 765 | * "42": Kovan Testnet 766 | */ 767 | 768 | public function chainId(): int 769 | { 770 | return $this->netVersion(); 771 | } 772 | 773 | public function request($method, $params = []){ 774 | 775 | $data = [ 776 | 'json' => [ 777 | 'jsonrpc' => '2.0', 778 | 'method' => $method, 779 | 'params' => $params, 780 | 'id' => $this->requestId++, 781 | ] 782 | ]; 783 | $res = $this->client->post('', $data); 784 | $body = json_decode($res->getBody()); 785 | if (isset($body->error) && !empty($body->error)) { 786 | throw new \Exception($body->error->message . " [Method] {$method}", $body->error->code); 787 | } 788 | return $body->result; 789 | } 790 | 791 | /** 792 | * @param $fromBlock 793 | * @param $toBlock 794 | * @param $address 795 | * @param $topic 796 | * @return array 797 | */ 798 | private function getData($fromBlock, $toBlock, $address, $topic): array 799 | { 800 | $data = []; 801 | 802 | if (!empty($fromBlock)) { 803 | $data['fromBlock'] = utils::decToHex($fromBlock); 804 | } else { 805 | $data['fromBlock'] = "0x0"; 806 | } 807 | if (!empty($toBlock)) { 808 | $data['toBlock'] = utils::decToHex($toBlock); 809 | } 810 | if (!empty($address)) { 811 | $data['address'] = $address; 812 | } 813 | if (!empty($topic)) { 814 | $topic = array_map(function ($item) { 815 | if (is_array($item)) { 816 | return array_map(function ($v) { 817 | return $v ? utils::fill0($v) : $v; 818 | }, $item); 819 | } else 820 | return $item ? utils::fill0($item) : $item; 821 | }, $topic); 822 | $data['topics'] = $topic; 823 | } 824 | return $data; 825 | } 826 | } 827 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 | send($wallet,'transfer',[$toAccount,\Web3\Utils::ethToWei(1)]); 16 | //echo $res; 17 | //$start = \Web3\Utils::decToHex(bcsub(\Web3\Utils::hexToDec($web3->blockNumber()),999)); 18 | //$res =$web3->getLogs($start,$web3->blockNumber(),$contractAddress); 19 | //var_dump($res); 20 | 21 | echo $res =$contract->call('balanceOf',[$toAccount]); --------------------------------------------------------------------------------