Ethereum PHP
PHP interface to Ethereum JSON-RPC API.
Ethereum.php
Go to the documentation of this file.
1 <?php
2 namespace Ethereum;
3 
4 use Graze\GuzzleHttp\JsonRpc\Client as RpcClient;
5 
7 use Exception;
12 
39 class Ethereum extends EthereumStatic implements Web3Interface
40 {
41 
42  // Using a trait to enable Tools like PHP storm and doxygen understand PHP magic method calls.
43  use Web3Methods;
44 
45  private $definition;
46  private $methods;
47  private $id = 0;
48  public $client;
49  public $debugHtml = '';
50 
51 
69  public function __construct(string $url = 'http://localhost:8545')
70  {
71  // Require the workaround helpers, as autoload files in composer
72  // doesn't work as expected.
73  require_once __DIR__ . '/helpers/ethereum-client-workaround-helpers.php';
74 
75  $this->client = RpcClient::factory($url, [
76  // Debug JsonRPC requests.
77  'debug' => false,
78  'rpc_error' => true,
79  ]);
80 
81  $this->definition = self::getDefinition();
82 
83  foreach ($this->definition['methods'] as $name => $params) {
84  ${$name} = function () {
85 
86  $request_params = [];
87 
88  // Get name of called function.
89  $method = debug_backtrace()[2]['args'][0];
90  $this->debug('Called function name', $method);
91 
92  // Get call and return parameters and types.
93  $param_definition = $this->definition['methods'][$method];
94 
95  // Arguments send with function call.
96  $valid_arguments = $param_definition[0];
97  $argument_class_names = [];
98  if (count($valid_arguments)) {
99  $this->debug('Valid arguments', $valid_arguments);
100 
101  // Get argument definition Classes.
102  foreach ($valid_arguments as $type) {
103  $primitiveType = EthD::typeMap($type);
104  if ($primitiveType) {
105  $argument_class_names[] = $primitiveType;
106  } else {
107  $argument_class_names[] = $type;
108  }
109  }
110  $this->debug('Valid arguments class names', $argument_class_names);
111  }
112 
113  // Arguments send with function call.
114  $args = func_get_args();
115  if (count($args) && isset($argument_class_names)) {
116  $this->debug('Arguments', $args);
117 
118  // Validate arguments.
119  foreach ($args as $i => $arg) {
120  /* @var EthDataType $arg */
121 
122  if (is_subclass_of ($arg,'Ethereum\DataType\EthDataType')) {
123  // Former $arg->getType has been removed.
124  // Getting the basename of the class.
125  $argType = basename(str_replace('\\', '/', get_class($arg)));
126  if ($argument_class_names[$i] !== $argType) {
127  throw new \InvalidArgumentException("Argument $i is "
128  . $argType
129  . " but expected $argument_class_names[$i] in $method().");
130  }
131  else {
132  // Add value. Inconsistently booleans are not hexEncoded if they
133  // are not data like in eth_getBlockByHash().
134  if ($arg->isPrimitive() && $argType !== 'EthB') {
135 
136  if ($method === 'eth_getFilterChanges') {
137  // Filter Id is an un-padded value :(
138  $request_params[] = $arg->hexValUnpadded();
139  }
140  else {
141  $request_params[] = $arg->hexVal();
142  }
143  }
144  elseif ($arg->isPrimitive() && $argType === 'EthB') {
145  $request_params[] = $arg->val();
146  }
147  else {
148  $request_params[] = $arg->toArray();
149  }
150  }
151  }
152  else
153  {
154  throw new \InvalidArgumentException('Arg ' . $i . ' is not a EthDataType.');
155  }
156  }
157  }
158 
159  // Validate required parameters.
160  if (isset($param_definition[2])) {
161  $required_params = array_slice($param_definition[0], 0, $param_definition[2]);
162  $this->debug('Required Params', $required_params);
163  }
164 
165  if (isset($required_params) && count($required_params)) {
166  foreach ($required_params as $i => $param) {
167  if (!isset($request_params[$i])) {
168  throw new \InvalidArgumentException("Required argument $i $argument_class_names[$i] is missing in $method().");
169  }
170  }
171  }
172 
173  // Default block parameter required for function call.
174  // See: https://github.com/ethereum/wiki/wiki/JSON-RPC#the-default-block-parameter.
175  $require_default_block = false;
176  if (isset($param_definition[3])) {
177  $require_default_block = $param_definition[3];
178  $this->debug('Require default block parameter', $require_default_block);
179  }
180  if ($require_default_block) {
181  $arg_is_set = false;
182  foreach ($argument_class_names as $i => $class) {
183  if ($class === 'EthBlockParam' && !isset($request_params[$i])) {
184  $request_params[$i] = 'latest';
185  }
186  }
187  }
188 
189  // Return type.
190  $return_type = $param_definition[1];
191  $this->debug('Return value type', $return_type);
192 
193  $is_primitive = (is_array($return_type)) ? (bool)EthD::typeMap($return_type[0]) : (bool)EthD::typeMap($return_type);
194 
195  if (is_array($return_type)) {
196  $return_type_class = '[' . EthD::typeMap($return_type[0]) . ']';
197  } elseif ($is_primitive) {
198  $return_type_class = EthD::typeMap($return_type);
199  } else {
200  // Return Complex type.
201  $return_type_class = $return_type;
202  }
203  $this->debug('Return value Class name ', $return_type_class);
204 
205  // Call.
206  $this->debug('Final request params', $request_params);
207  $value = $this->etherRequest($method, $request_params);
208 
209  // Fix client specific flaws in src/helpers/helpers.php.
210  $functionName = 'eth_workaround_' . $method;
211  if (function_exists($functionName)) {
212  $value = call_user_func($functionName, $value);
213  }
214 
215  $return = $this->createReturnValue($value, $return_type_class, $method);
216  $this->debug('Final return object', $return);
217  $this->debug('<hr />');
218 
219  return $return;
220  };
221  // Binding above function.
222  $this->methods[$name] = \Closure::bind(${$name}, $this, get_class());
223  }
224  }
225 
226 
234  public function __call(string $method, array $args)
235  {
236  if (is_callable($this->methods[$method])) {
237  return call_user_func_array($this->methods[$method], $args);
238  } else {
239  throw new \InvalidArgumentException('Unknown Method: ' . $method);
240  }
241  }
242 
243 
259  private function createReturnValue($value, string $return_type_class, string $method)
260  {
261  $return = null;
262 
263  if (is_null($value)) {
264  return null;
265  }
266 
267  // Get return value type.
268  $class_name = '\\Ethereum\\DataType\\' . EthDataType::getTypeClass($return_type_class);
269  // Is array ?
270  $array_val = $this->isArrayType($return_type_class);
271  // Is primitive data type?
272  $is_primitive = $class_name::isPrimitive();
273 
274  // Primitive array Values.
275  if ($is_primitive && $array_val && is_array($value)) {
276  // According to schema array returns will always have primitive values.
277  $return = $this->valueArray($value, $class_name);
278  } elseif ($is_primitive && !$array_val && !is_array($value)) {
279  $return = new $class_name($value);
280  }
281 
282  // Complex array types.
283  if (!$is_primitive && !$array_val && is_array($value)) {
284  $return = $this->arrayToComplexType($class_name, $value);
285  }
286  elseif (!$is_primitive) {
287 
288  if ($array_val) {
289  if ($method === 'eth_getFilterChanges') {
290  // Only be [FilterChange| D32]
291  $return = $this->handleFilterChangeValues($value);
292  }
293  elseif ($method === 'shh_getFilterChanges') {
294  throw new \Exception('shh_getFilterChanges not implemented.');
295  }
296  else {
297  // There only should be [SSHFilterChange] left
298  throw new \Exception(' Return is a array of non primitive types. Method: ' . $method);
299  }
300  }
301  else {
302  $return = new $class_name();
303  }
304  }
305 
306  if (!$return && !is_array($return)) {
307  throw new Exception('Expected '
308  . $return_type_class
309  . ' at '
310  . $method
311  . ' (), couldn not be decoded. Value was: '
312  . print_r($value, true));
313  }
314  return $return;
315  }
316 
317 
326  protected static function isArrayType(string $type) {
327  return (strpos($type, '[') !== FALSE );
328  }
329 
339  public function request(string $method, array $params = [])
340  {
341  $this->id++;
342  return $this->client->send($this->client->request($this->id, $method, $params))->getRpcResult();
343  }
344 
345 
356  public function etherRequest(string $method, array $params = [])
357  {
358  try {
359  return $this->request($method, $params);
360  } catch (\Exception $e) {
361  if ($e->getCode() === 405) {
362  return [
363  'error' => true,
364  'code' => 405,
365  'message' => $e->getMessage(),
366  ];
367  } else {
368  throw $e;
369  }
370  }
371  }
372 
373 
385  public function debug(string $title, $content = null)
386  {
387  $return = '';
388  $return .= '<p style="margin-left: 1em"><b>' . $title . "</b></p>";
389  if ($content) {
390  $return .= '<pre style="background: rgba(0,0,0, .1); margin: .5em; padding: .25em; ">';
391  if (is_object($content) || is_array($content)) {
392  ob_start();
393  var_dump($content);
394  $return .= ob_get_clean();
395  } else {
396  $return .= ($content);
397  }
398  $return .= "</pre>";
399  }
400  $this->debugHtml .= $return;
401 
402  return $return;
403  }
404 
405 
420  protected static function handleFilterChangeValues(array $values) {
421 
422  $return = [];
423  foreach ($values as $val) {
424  // If $val is an array, we handle a change of eth_newFilter -> returns [FilterChange]
425  if (is_array($val)) {
426  $processed = [];
427  foreach (FilterChange::getTypeArray() as $key => $type) {
428 
429  if (substr($type, 0, 1) === '[') {
430  // param is an array. E.g topics.
431  $className = '\Ethereum\Datatype\\' . str_replace(['[', ']'], '', $type);
432  $sub = [];
433  foreach ($val[$key] as $subVal) {
434  $sub[] = new $className($subVal);
435  }
436  $processed[] = $sub;
437 
438  // @todo We'll need to decode the ABI of the values too!
439  }
440  else {
441  $className = '\Ethereum\Datatype\\' . $type;
442  $processed[] = isset($val[$key]) ? new $className($val[$key]) : null;
443  }
444  }
445  $return[] = new FilterChange(...$processed);
446  }
447  else {
448  // If $val not an array, we handle a change of
449  // eth_newBlockFilter (block hashes) -> returns [D32] or
450  // eth_newPendingTransactionFilter (transaction hashes) -> returns [D32]
451  $return[] = new EthD32($val);
452  }
453 
454  }
455  return $return;
456  }
457 
458 
473  protected static function arrayToComplexType(string $class_name, array $values)
474  {
475  $return = [];
476  $class_values = [];
477  if (!substr($class_name, 1,8) === __NAMESPACE__) {
478  $class_name = __NAMESPACE__ . "\\$class_name";
479  }
480 
481  // Applying workarounds.
482  $functionName = 'eth_workaround_' . strtolower(str_replace('\\', '_', substr($class_name,1)));
483  if (function_exists($functionName)) {
484  $values = call_user_func($functionName, $values);
485  }
486 
487 
489  $type_map = $class_name::getTypeArray();
490 
491  // Looping through the values of expected of $class_name.
492  foreach ($type_map as $name => $val_class) {
493 
494  if (isset($values[$name])) {
495  $val_class = '\\Ethereum\\DataType\\' . EthDataType::getTypeClass($val_class);
496 
497  // We might expect an array like logs=[FilterChange].
498  if (is_array($values[$name])) {
499  $sub_values = [];
500  foreach ($values[$name] as $sub_val) {
501 
502  // Working around the "DATA|Transaction" type in Blocks.
503  // This is a weired Ethereum problem. In eth_getBlockByHash
504  // and eth_getblockbynumber return type depends on the
505  // second param.
506  // @See: https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbynumber.
507  if (!is_array($sub_val) && $name === 'transactions') {
508  $val_class = '\\Ethereum\\DataType\\EthD32';
509  }
510 
511  // Work around testrpc giving not back an array.
512  if (is_array($sub_val)) {
513  $sub_values[] = self::arrayToComplexType($val_class, $sub_val);
514  }
515  else {
516  $sub_values[] = new $val_class($sub_val);
517  }
518  }
519  $class_values[] = $sub_values;
520  }
521  else {
522  $class_values[] = new $val_class($values[$name]);
523  }
524  }
525  else {
526  // In order to create a proper constructor null values are required.
527  $class_values[] = null;
528  }
529  }
530  $return = new $class_name(...$class_values);
531  if (!$return && !is_array($return)) {
532  throw new \Exception('Expected ' . $return_type_class . ' at ' . $method . ' (), couldn not be decoded. Value was: ' . print_r($value, TRUE));
533  }
534  return $return;
535  }
536 
537 
551  public static function personalEcRecover(string $message, EthD $signature) {
552  return EcRecover::personalEcRecover($message, $signature->hexVal());
553  }
554 
555 
571  public static function valueArray(array $values, string $typeClass)
572  {
573  $return = [];
574  if (!class_exists($typeClass)) {
575  $typeClass = '\\' . __NAMESPACE__ . '\\DataType\\' . $typeClass;
576  }
577  foreach ($values as $i => $val) {
578  if (is_object($val)) {
579  $return[$i] = $val->toArray();
580  }
581  if (is_array($val)) {
582  $return[$i] = self::arrayToComplexType($typeClass, $val);
583  }
584  $return[$i] = new $typeClass($val);
585  }
586  return $return;
587  }
588 
589 
590 }
static personalEcRecover(string $message, EthD $signature)
Definition: Ethereum.php:551
static handleFilterChangeValues(array $values)
Definition: Ethereum.php:420
static getTypeClass(string $type, bool $typed_constructor=false)
request(string $method, array $params=[])
Definition: Ethereum.php:339
static typeMap(string $type)
Definition: EthD.php:261
__construct(string $url='http://localhost:8545')
Definition: Ethereum.php:69
static valueArray(array $values, string $typeClass)
Definition: Ethereum.php:571
static isArrayType(string $type)
Definition: Ethereum.php:326
__call(string $method, array $args)
Definition: Ethereum.php:234
debug(string $title, $content=null)
Definition: Ethereum.php:385
Definition: Abi.php:3
etherRequest(string $method, array $params=[])
Definition: Ethereum.php:356