├── .gitignore ├── Core ├── ABI.php ├── Accounts.php ├── EIP712.php ├── SWeb3.php ├── SWeb3_Contract.php └── Utils.php ├── Example ├── example.account.php ├── example.batch.php ├── example.call.php ├── example.config.php ├── example.contract_creation.php ├── example.erc20.php ├── example.php ├── example.send.php ├── swp_contract.sol └── swp_contract_create.sol ├── LICENCE ├── README.md ├── Test ├── contract_test_mirror_tuple.sol ├── inc │ ├── inc.abitest.php │ └── inc.wtest.php ├── test.core.php ├── test.php └── test_rebuild_tuple.php └── composer.json /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | .phpintel/ 4 | /.idea/ 5 | composer.lock 6 | *.private.php 7 | /Errors/ -------------------------------------------------------------------------------- /Core/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_name)) 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::string_contains($abi_string_type, 'bytes')) return VariableType::BytesFixed; 121 | 122 | //dynamic 123 | else if (Utils::string_contains($abi_string_type, 'tuple')) return VariableType::Tuple; 124 | else if (Utils::string_contains($abi_string_type, 'string')) return VariableType::String; 125 | 126 | //static 127 | else if (Utils::string_contains($abi_string_type, 'uint') ) return VariableType::UInt; 128 | else if (Utils::string_contains($abi_string_type, 'int') ) return VariableType::Int; 129 | else if (Utils::string_contains($abi_string_type, 'fixed') ) return VariableType::Int; 130 | else if (Utils::string_contains($abi_string_type, 'bool')) return VariableType::Bool; 131 | else if (Utils::string_contains($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 | { 308 | $input_type = is_string($input) ? $input : $input->type; 309 | $varType = self::GetParameterType($input_type); 310 | 311 | // for non-tuple item, we'll have in-place value or offset 312 | if ($varType != VariableType::Tuple) { 313 | $staticInputCount++; 314 | continue; 315 | } 316 | 317 | // for tuple we'll have several in place values or one pointer to the start of several in-place values 318 | if (self::ExistsDynamicParameter($input->components)) { 319 | $staticInputCount++; 320 | } else { 321 | $staticInputCount += count($input->components); 322 | } 323 | } 324 | $currentDynamicIndex = $staticInputCount * self::NUM_ZEROS / 2; 325 | } 326 | 327 | //parameters 328 | $i = 0; 329 | foreach ($inputs as $pos => $input) 330 | { 331 | $var_name = $pos; 332 | if (is_object($input)) { 333 | if (isset($input->name)) $var_name = $input->name; 334 | } 335 | else if (is_string($input)){ 336 | $var_name = $input; 337 | } 338 | 339 | $inputData = is_object($data) ? $data->$var_name : (isset($data[$pos]) ? $data[$pos] : null); 340 | if (is_array($data) && $inputData === null) $inputData = $data[$var_name]; 341 | 342 | $hashData .= self::EncodeInput($input, $inputData, 1, $currentDynamicIndex); 343 | 344 | if (isset($input->hash)) $currentDynamicIndex += strlen($input->hash) / 2; 345 | $i++; 346 | } 347 | 348 | foreach($inputs as $pos => $input) { 349 | $hashData .= self::EncodeInput($input, null, 2, $currentDynamicIndex); 350 | } 351 | 352 | if (count($inputs) == 0) { 353 | $hashData .= self::NUM_ZEROS / 2; 354 | } 355 | 356 | return $hashData; 357 | } 358 | 359 | 360 | public static function EncodeParameters_External(array $input_types, array $data) : string 361 | { 362 | $inputs = array(); 363 | foreach($input_types as $it) { 364 | $input = new stdClass(); 365 | $input->name = $it; 366 | $input->type = $it; 367 | $inputs []= $input; 368 | } 369 | 370 | return '0x' . self::EncodeGroup($inputs, $data); 371 | } 372 | 373 | 374 | public static function EncodeParameter_External(string $input_name, $data) : string 375 | { 376 | $hashData = ""; 377 | $currentDynamicIndex = self::NUM_ZEROS / 2; 378 | 379 | $input = new stdClass(); 380 | $input->type = $input_name; 381 | 382 | $hashData .= self::EncodeInput($input, $data, 1, $currentDynamicIndex); 383 | 384 | if (isset($input->hash)) $currentDynamicIndex += strlen($input->hash) / 2; 385 | 386 | $hashData .= self::EncodeInput($input, null, 2, $currentDynamicIndex); 387 | 388 | return '0x' . $hashData; 389 | } 390 | 391 | 392 | public static function EncodePacked(array $inputs, array $data) : string 393 | { 394 | $res = ""; 395 | 396 | for ($i = 0; $i < count($inputs); $i++) 397 | { 398 | $type = $inputs[$i]; 399 | $val = $data[$i]; 400 | $varType = self::GetParameterType($type); 401 | 402 | if (Utils::string_contains($type, '[')) 403 | { 404 | throw new Exception($type . " - Not suported (EncodePacked)"); 405 | } 406 | else if ($varType == VariableType::String || $varType == VariableType::Bytes || $varType == VariableType::BytesFixed) 407 | { 408 | if (substr($val, 0, 2) == "0x") $res .= substr($val, 2); 409 | else $res .= bin2hex($val); 410 | } 411 | else if ($varType == VariableType::Int || $varType == VariableType::UInt) 412 | { 413 | $x = dechex($val); 414 | $fixedLength = (int)preg_replace('/[^0-9]/', '', $type,) / 4; 415 | if ($fixedLength <= 0) $fixedLength = 64; 416 | $res .= str_pad($x, $fixedLength, '0', STR_PAD_LEFT); 417 | } 418 | else if ($varType == VariableType::Address) 419 | { 420 | $res .= (substr($val, 0, 2) == "0x") ? substr($val, 2) : $val; 421 | } 422 | else 423 | { 424 | throw new Exception($type . " - Not suported (EncodePacked)"); 425 | } 426 | } 427 | 428 | return '0x' . $res; 429 | } 430 | 431 | 432 | 433 | private static function EncodeInput_Array($full_input, $inputData, $isStaticLength) 434 | { 435 | $inputs = []; 436 | $currentDynamicIndex = count($inputData) * self::NUM_ZEROS / 2; 437 | 438 | //prepare clean input 439 | $last_array_marker = strrpos($full_input->type, '['); 440 | $clean_type = substr($full_input->type, 0, $last_array_marker); 441 | 442 | $clean_internalType = ""; 443 | if (isset($full_input->internalType)) { 444 | $last_array_marker = strrpos($full_input->internalType, '['); 445 | $clean_internalType = substr($full_input->internalType, 0, $last_array_marker); 446 | } 447 | 448 | $hashData = ""; 449 | 450 | if (!$isStaticLength) { 451 | //add array length 452 | $hashData = self::EncodeInput_UInt(count($inputData)); 453 | } 454 | 455 | foreach ($inputData as $pos => $element) 456 | { 457 | $input = new stdClass(); 458 | $input->type = $clean_type; 459 | $input->internalType = $clean_internalType; 460 | if (isset($full_input->components)) { 461 | $input->components = $full_input->components; 462 | } 463 | $inputs []= $input; 464 | 465 | $hashData .= self::EncodeInput($input, $element, 1, $currentDynamicIndex); 466 | 467 | if (isset($input->hash)) $currentDynamicIndex += strlen($input->hash) / 2; 468 | } 469 | 470 | foreach($inputs as $pos => $input) 471 | { 472 | $data = $inputData[$pos]; 473 | $hashData .= self::EncodeInput($input, $data, 2, $currentDynamicIndex); 474 | } 475 | 476 | if (count($inputs) == 0) { 477 | $hashData .= self::NUM_ZEROS / 2; 478 | } 479 | 480 | return $hashData; 481 | } 482 | 483 | 484 | private static function EncodeInput($input, $inputData, $round, &$currentDynamicIndex) 485 | { 486 | $hash = ""; 487 | 488 | if($round == 1) 489 | { 490 | $input_type = is_string($input) ? $input : $input->type; 491 | $varType = self::GetParameterType($input_type); 492 | 493 | //dynamic 494 | if (Utils::string_contains($input_type, '[')) 495 | { 496 | //arrays with all static parameters have no initial array offset 497 | $isStaticArray = self::IsStaticParameter($varType); 498 | if ($varType == VariableType::Tuple) { 499 | $isStaticArray = !self::ExistsDynamicParameter($input->components); 500 | } 501 | $isStaticLength = $isStaticArray && !Utils::string_contains($input_type, '[]'); 502 | 503 | $res = self::EncodeInput_Array($input, $inputData, $isStaticLength); 504 | if (!$isStaticLength) { 505 | $input->hash = $res; 506 | return self::EncodeInput_UInt($currentDynamicIndex); 507 | } 508 | return $res; 509 | } 510 | else if ($varType == VariableType::Tuple) 511 | { 512 | $res = self::EncodeGroup($input->components, $inputData); 513 | 514 | // if the tuple is dynamic, we return offset and add tuple's data at the end 515 | if (self::ExistsDynamicParameter($input->components)) { 516 | $input->hash = $res; 517 | return self::EncodeInput_UInt($currentDynamicIndex); 518 | } 519 | return $res; 520 | } 521 | else if ($varType == VariableType::String) { 522 | $input->hash = self::EncodeInput_String($inputData); 523 | $res = self::EncodeInput_UInt($currentDynamicIndex); 524 | return $res; 525 | } 526 | else if ($varType == VariableType::Bytes) { 527 | $input->hash = self::EncodeInput_Bytes($inputData); 528 | $res = self::EncodeInput_UInt($currentDynamicIndex); 529 | return $res; 530 | } 531 | //static 532 | else if ($varType == VariableType::UInt) { 533 | return self::EncodeInput_UInt($inputData); 534 | } 535 | else if ($varType == VariableType::Int) { 536 | return self::EncodeInput_Int($inputData); 537 | } 538 | else if ($varType == VariableType::Bool) { 539 | return self::EncodeInput_Bool($inputData); 540 | } 541 | else if ($varType == VariableType::Address) { 542 | return self::EncodeInput_Address($inputData); 543 | } 544 | else if ($varType == VariableType::BytesFixed) { 545 | return self::EncodeInput_BytesFixed($inputData); 546 | } 547 | } 548 | else if($round == 2) 549 | { 550 | if (isset($input->hash) and $input->hash != '') { 551 | return $input->hash; 552 | } 553 | } 554 | 555 | return $hash; 556 | } 557 | 558 | private static function EncodeInput_UInt($data) 559 | { 560 | if (is_string($data) && ctype_digit($data)) { 561 | $bn = Utils::toBn($data); 562 | $hash = self::AddZeros($bn->toHex(true), true); 563 | } 564 | else if ($data instanceof BigNumber) { 565 | $hash = self::AddZeros($data->toHex(true), true); 566 | } 567 | else if (is_int($data) || is_long($data)) { 568 | $hash = self::AddZeros(dechex($data), true); 569 | } 570 | else { 571 | throw new Exception("EncodeInput_UInt, not valid input type"); 572 | } 573 | 574 | return $hash; 575 | } 576 | 577 | private static function EncodeInput_Int($data) 578 | { 579 | if (is_string($data) && ctype_digit($data)) { 580 | $bn = Utils::toBn($data); 581 | $hash = self::AddNegativeF($bn->toHex(true), true); 582 | } 583 | else if ($data instanceof BigNumber) { 584 | if($data->toString()[0] == '-') 585 | $hash = self::AddNegativeF($data->toHex(true), true); 586 | else 587 | $hash = self::AddZeros($data->toHex(true), true); 588 | } 589 | else if (is_int($data) || is_long($data)) { 590 | $hash = self::AddZerosOrF(dechex($data), true); 591 | } 592 | else { 593 | throw new Exception("EncodeInput_Int, not valid input type"); 594 | } 595 | 596 | return $hash; 597 | } 598 | 599 | private static function EncodeInput_Bool($data) 600 | { 601 | $hash = $data ? '1' : '0'; 602 | $hash = self::AddZeros($hash, true); 603 | return $hash; 604 | } 605 | 606 | private static function EncodeInput_Address($data) 607 | { 608 | $hash = self::AddZeros(substr($data, 2), true); 609 | return $hash; 610 | } 611 | 612 | private static function EncodeInput_String($data) 613 | { 614 | //length + hexa string 615 | $hash = self::EncodeInput_UInt(strlen($data)) . self::AddZeros(bin2hex($data), false); 616 | 617 | return $hash; 618 | } 619 | 620 | private static function EncodeInput_Bytes($data) 621 | { 622 | $hexa = $data; 623 | 624 | //I'm not proud of this. Official parsers seem to handle 0x as 0x0 when input is type bytes 625 | //I think it can cause problems when you want to use bytes as a string, because you can't save the string "0x" 626 | //but looking at issue #50 it seems clear that the current evm behaviour is this. 627 | if ($data == '0x') { 628 | $data = ''; 629 | } 630 | 631 | //if data is not a valid hexa, it means its a binary rep 632 | if (substr($data, 0, 2) != '0x' || !ctype_xdigit(substr($data, 2)) || strlen($data) % 2 != 0) { 633 | $hexa = bin2hex($data); 634 | } 635 | 636 | if (substr($hexa, 0, 2) == '0x') { 637 | $hexa = substr($hexa, 2); 638 | } 639 | 640 | //length + hexa string 641 | $hash = self::EncodeInput_UInt(strlen($hexa) / 2) . self::AddZeros($hexa, false); 642 | 643 | return $hash; 644 | } 645 | 646 | 647 | private static function EncodeInput_BytesFixed($data) 648 | { 649 | $hexa = $data; 650 | 651 | //if data is not a valid hexa, it means its a binary rep 652 | if (substr($data, 0, 2) != '0x' || !ctype_xdigit(substr($data, 2)) || strlen($data) % 2 != 0) { 653 | $hexa = bin2hex($data); 654 | } 655 | 656 | if (substr($hexa, 0, 2) == '0x') { 657 | $hexa = substr($hexa, 2); 658 | } 659 | 660 | //length + hexa string 661 | $hash = self::AddZeros($hexa, false); 662 | 663 | return $hash; 664 | } 665 | 666 | 667 | private static function AddZeros($data, $add_left) 668 | { 669 | $remaining = strlen($data); 670 | if ($remaining > self::NUM_ZEROS) $remaining %= self::NUM_ZEROS; 671 | $total = self::NUM_ZEROS - $remaining; 672 | 673 | $res = $data; 674 | 675 | if ($total > 0) { 676 | for($i=0; $i < $total; $i++) { 677 | if($add_left) $res = '0'.$res; 678 | else $res .= '0'; 679 | } 680 | } 681 | 682 | return $res; 683 | } 684 | 685 | 686 | private static function AddNegativeF($data, $add_left) 687 | { 688 | $remaining = strlen($data); 689 | if ($remaining > self::NUM_ZEROS) $remaining %= self::NUM_ZEROS; 690 | $total = self::NUM_ZEROS - $remaining; 691 | 692 | $res = $data; 693 | 694 | if ($total > 0) { 695 | for($i=0; $i < $total; $i++) { 696 | if($add_left) $res = 'f'.$res; 697 | else $res .= 'f'; 698 | } 699 | } 700 | 701 | return $res; 702 | } 703 | 704 | 705 | private static function AddZerosOrF($data, $add_left) 706 | { 707 | $valueToAdd = (strtolower($data[0]) == 'f' && strlen($data) == 16) ? 'f' : '0'; 708 | 709 | $remaining = strlen($data); 710 | if ($remaining > self::NUM_ZEROS) $remaining %= self::NUM_ZEROS; 711 | $total = self::NUM_ZEROS - $remaining; 712 | 713 | $res = $data; 714 | 715 | if ($total > 0) { 716 | for($i=0; $i < $total; $i++) { 717 | if($add_left) $res = $valueToAdd.$res; 718 | else $res .= $valueToAdd; 719 | } 720 | } 721 | 722 | return $res; 723 | } 724 | 725 | 726 | /***************************************DECODE */ 727 | 728 | public function DecodeData($function_name, $encoded) 729 | { 730 | $encoded = substr($encoded, 2); 731 | $function = $this->GetFunction($function_name); 732 | 733 | $decoded = self::DecodeGroup($function->outputs, $encoded, 0); 734 | 735 | return $decoded; 736 | } 737 | 738 | 739 | public static function DecodeGroup($outputs, $encoded, $index) 740 | { 741 | $group = new stdClass(); 742 | $first_index = $index; 743 | $elem_index = 1; 744 | $tuple_count = 1; 745 | $array_count = 1; 746 | $output_count = count($outputs); 747 | 748 | 749 | foreach ($outputs as $output) 750 | { 751 | $output_type = is_string($output) ? $output : $output->type; 752 | $varType = self::GetParameterType($output_type); 753 | $output_type_offset = self::GetOutputOffset($output); 754 | $var_name = ''; 755 | 756 | //dynamic 757 | if(Utils::string_contains($output->type, '[')) 758 | { 759 | $var_name = $output->name != '' ? $output->name : 'array_'.$array_count; 760 | 761 | //arrays with all static parameters have no initial array offset 762 | $isStaticArray = self::IsStaticParameter($varType); 763 | if ($varType == VariableType::Tuple) { 764 | $isStaticArray = !self::ExistsDynamicParameter($output->components); 765 | } 766 | $isStaticLength = $isStaticArray && !Utils::string_contains($output->type, '[]'); 767 | 768 | $dynamic_data_start = 0; 769 | if ($isStaticLength) $dynamic_data_start = $index; 770 | else $dynamic_data_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 771 | 772 | $group->$var_name = self::DecodeInput_Array($output, $encoded, $dynamic_data_start); 773 | $array_count++; 774 | } 775 | else if ($varType == VariableType::Tuple) 776 | { 777 | $var_name = $output->name != '' ? $output->name : 'tuple_'.$tuple_count; 778 | 779 | //tuples with only static parameters have no initial tuple offset 780 | $hasDynamicParameters = self::ExistsDynamicParameter($output->components); 781 | 782 | $dynamic_data_start = $index; 783 | if ($hasDynamicParameters) { 784 | $dynamic_data_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 785 | } 786 | 787 | $group->$var_name = self::DecodeGroup($output->components, $encoded, $dynamic_data_start); 788 | $tuple_count++; 789 | } 790 | else if ($varType == VariableType::String) 791 | { 792 | $var_name = $output->name != '' ? $output->name : 'elem_'.$elem_index; 793 | $dynamic_data_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 794 | $group->$var_name = self::DecodeInput_String($encoded, $dynamic_data_start); 795 | } 796 | else if ($varType == VariableType::Bytes) 797 | { 798 | $var_name = $output->name != '' ? $output->name : 'elem_'.$elem_index; 799 | $dynamic_data_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 800 | $group->$var_name = self::DecodeInput_Bytes($encoded, $dynamic_data_start); 801 | } 802 | //static 803 | else 804 | { 805 | $var_name = 'result'; 806 | if($output->name != '') $var_name = $output->name; 807 | else if($output_count > 1) $var_name = 'elem_'.$elem_index; 808 | 809 | $group->$var_name = self::DecodeInput_Generic($varType, $encoded, $index); 810 | } 811 | 812 | $elem_index++; 813 | $index += $output_type_offset * self::NUM_ZEROS; 814 | } 815 | 816 | return $group; 817 | } 818 | 819 | 820 | public static function DecodeParameter_External(string $output_type, string $encoded) 821 | { 822 | $output = new stdClass(); 823 | $output->type = $output_type; 824 | $varType = self::GetParameterType($output_type); 825 | $dynamic_data_start = self::NUM_ZEROS; 826 | 827 | $res = ""; 828 | 829 | if (substr($encoded, 0, 2) == '0x') { 830 | $encoded = substr($encoded, 2); 831 | } 832 | 833 | //dynamic 834 | if(Utils::string_contains($output->type, '[')) 835 | { 836 | //arrays with all static parameters have no initial array offset 837 | $isStaticArray = self::IsStaticParameter($varType); 838 | if ($varType == VariableType::Tuple) { 839 | $isStaticArray = !self::ExistsDynamicParameter($output->components); 840 | } 841 | $isStaticLength = $isStaticArray && !Utils::string_contains($output->type, '[]'); 842 | 843 | $dynamic_data_start = 0; 844 | if ($isStaticLength) $dynamic_data_start = 0; 845 | else $dynamic_data_start = 0 + self::DecodeInput_UInt_Internal($encoded, 0) * 2; 846 | 847 | $res = self::DecodeInput_Array($output, $encoded, $dynamic_data_start); 848 | } 849 | else if ($varType == VariableType::Tuple) 850 | { 851 | //tuples with only static parameters have no initial tuple offset 852 | $res = self::DecodeGroup($output->components, $encoded, $dynamic_data_start); 853 | } 854 | else if ($varType == VariableType::String) 855 | { 856 | $res = self::DecodeInput_String($encoded, $dynamic_data_start); 857 | } 858 | else if ($varType == VariableType::Bytes) 859 | { 860 | $res = self::DecodeInput_Bytes($encoded, $dynamic_data_start); 861 | } 862 | //simple 863 | else 864 | { 865 | $res = self::DecodeInput_Generic($varType, $encoded, 0); 866 | } 867 | 868 | return $res; 869 | } 870 | 871 | 872 | private static function DecodeInput_Array($output, $encoded, $index) 873 | { 874 | $array = []; 875 | $first_index = $index; 876 | 877 | $clean_output = clone $output; 878 | $last_array_marker = strrpos($clean_output->type, '['); 879 | $clean_output->type = substr($clean_output->type, 0, $last_array_marker); 880 | 881 | $varType = self::GetParameterType($clean_output->type); 882 | $isStaticType = self::IsStaticParameter($varType); 883 | if ($varType == VariableType::Tuple) { 884 | $isStaticType = !self::ExistsDynamicParameter($output->components); 885 | } 886 | 887 | $length = 0; 888 | if ($isStaticType) { 889 | $last_array_marker_end = strrpos($output->type, ']'); 890 | $length = (int) substr($output->type, $last_array_marker + 1, $last_array_marker_end - $last_array_marker - 1); 891 | } 892 | 893 | if ($length <= 0) { 894 | $length = self::DecodeInput_UInt_Internal($encoded, $first_index); 895 | $first_index += self::NUM_ZEROS; 896 | $index += self::NUM_ZEROS; 897 | } 898 | 899 | $element_offset = 1; 900 | if ($isStaticType) { 901 | $element_offset = self::GetOutputOffset($clean_output); 902 | } 903 | 904 | for ($i = 0; $i < $length; $i++) 905 | { 906 | $res = "error"; 907 | if (Utils::string_contains($clean_output->type, '[')) 908 | { 909 | $isStaticLength = $isStaticType && !Utils::string_contains($clean_output->type, '[]'); 910 | //arrays with all static parameters have no initial array offset 911 | $element_start = $index; 912 | if ($isStaticLength) { 913 | $element_start = $index; 914 | } 915 | else { 916 | $element_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 917 | } 918 | 919 | $res = self::DecodeInput_Array($clean_output, $encoded, $element_start); 920 | } 921 | else if($varType == VariableType::Tuple) 922 | { 923 | //tuple with all static parameters have no initial array offset 924 | if($isStaticType) { 925 | $element_start = $index; 926 | } 927 | else { 928 | $element_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 929 | } 930 | 931 | $res = self::DecodeGroup($clean_output->components, $encoded, $element_start); 932 | } 933 | else if($varType == VariableType::String) 934 | { 935 | $element_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 936 | $res = self::DecodeInput_String($encoded, $element_start); 937 | } 938 | else if($varType == VariableType::Bytes) 939 | { 940 | $element_start = $first_index + self::DecodeInput_UInt_Internal($encoded, $index) * 2; 941 | $res = self::DecodeInput_Bytes($encoded, $element_start); 942 | } 943 | else 944 | { 945 | $res = self::DecodeInput_Generic($varType, $encoded, $index); 946 | } 947 | 948 | $array []= $res; 949 | $index += self::NUM_ZEROS * $element_offset; 950 | } 951 | 952 | return $array; 953 | } 954 | 955 | 956 | private static function DecodeInput_Generic($variableType, $encoded, $start) 957 | { 958 | if($variableType == VariableType::String) { 959 | return self::DecodeInput_String($encoded, $start); 960 | } 961 | else if($variableType == VariableType::UInt) { 962 | return self::DecodeInput_UInt($encoded, $start); 963 | } 964 | else if($variableType == VariableType::Int) { 965 | return self::DecodeInput_Int($encoded, $start); 966 | } 967 | else if($variableType == VariableType::Bool) { 968 | return self::DecodeInput_Bool($encoded, $start); 969 | } 970 | else if($variableType == VariableType::Address) { 971 | return self::DecodeInput_Address($encoded, $start); 972 | } 973 | else if($variableType == VariableType::BytesFixed) { 974 | return self::DecodeInput_BytesFixed($encoded, $start); 975 | } 976 | else if($variableType == VariableType::Bytes) { 977 | return self::DecodeInput_Bytes($encoded, $start); 978 | } 979 | } 980 | 981 | 982 | private static function DecodeInput_UInt_Internal($encoded, $start) 983 | { 984 | $partial = substr($encoded, $start, 64); 985 | $partial = self::RemoveZeros($partial, true); 986 | 987 | return hexdec($partial); 988 | } 989 | 990 | 991 | private static function DecodeInput_UInt($encoded, $start) 992 | { 993 | $partial = substr($encoded, $start, 64); 994 | $partial = self::RemoveZeros($partial, true); 995 | 996 | $partial_big = new BigNumber($partial, 16); 997 | 998 | return $partial_big; 999 | } 1000 | 1001 | 1002 | private static function DecodeInput_Int($encoded, $start) 1003 | { 1004 | $partial = substr($encoded, $start, 64); 1005 | $partial_big = new BigNumber($partial, -16); 1006 | 1007 | return $partial_big; 1008 | } 1009 | 1010 | 1011 | private static function DecodeInput_Bool($encoded, $start) 1012 | { 1013 | $partial = substr($encoded, $start, 64); 1014 | $partial = self::RemoveZeros($partial, true); 1015 | return $partial == '1'; 1016 | } 1017 | 1018 | 1019 | private static function DecodeInput_Address($encoded, $start) 1020 | { 1021 | $partial = self::RemoveZeros(substr($encoded, $start, 64), true); 1022 | 1023 | //add zero padding from left for 20 bytes 1024 | $partial = str_pad($partial, 40, '0', STR_PAD_LEFT); 1025 | 1026 | return '0x'.$partial; 1027 | } 1028 | 1029 | 1030 | private static function DecodeInput_String($encoded, $start) 1031 | { 1032 | $length = self::DecodeInput_UInt_Internal($encoded, $start); 1033 | $start += self::NUM_ZEROS; 1034 | 1035 | $partial = substr($encoded, $start, $length * 2); 1036 | return hex2bin($partial); 1037 | } 1038 | 1039 | 1040 | private static function DecodeInput_BytesFixed($encoded, $start) 1041 | { 1042 | $partial = self::RemoveZeros(substr($encoded, $start, 64), false); 1043 | return hex2bin($partial); 1044 | } 1045 | 1046 | 1047 | private static function DecodeInput_Bytes($encoded, $start) 1048 | { 1049 | $length = self::DecodeInput_UInt_Internal($encoded, $start); 1050 | $start += self::NUM_ZEROS; 1051 | 1052 | $partial = substr($encoded, $start, $length * 2); 1053 | return hex2bin($partial); 1054 | } 1055 | 1056 | 1057 | private static function RemoveZeros($data, $remove_left) 1058 | { 1059 | $index = $remove_left ? 0 : strlen($data) - 1; 1060 | $current = substr($data, $index, 1); 1061 | while ($current == '0') 1062 | { 1063 | if ($remove_left) { 1064 | $data = substr($data, 1, strlen($data) - 1); 1065 | } 1066 | else { 1067 | $data = substr($data, 0, -1); 1068 | $index--; 1069 | } 1070 | $current = substr($data, $index, 1); 1071 | } 1072 | 1073 | return $data; 1074 | } 1075 | 1076 | 1077 | private static function GetOutputOffset ($output) : int 1078 | { 1079 | $output_type = is_string($output) ? $output : $output->type; 1080 | $varType = self::GetParameterType($output_type); 1081 | 1082 | if (Utils::string_contains($output_type, '[')) 1083 | { 1084 | $last_array_marker = strrpos($output->type, '['); 1085 | $last_array_marker_end = strrpos($output->type, ']'); 1086 | $length = (int) substr($output->type, $last_array_marker + 1, $last_array_marker_end - $last_array_marker - 1); 1087 | 1088 | if ($length > 0) 1089 | { 1090 | if ($varType == VariableType::Tuple) 1091 | { 1092 | if (!self::ExistsDynamicParameter($output->components)) { 1093 | return $length * self::GetOutputOffset_StaticComponents($output->components); 1094 | } 1095 | } 1096 | else if (self::IsStaticParameter($varType)) 1097 | { 1098 | return $length; 1099 | } 1100 | } 1101 | } 1102 | else if ($varType == VariableType::Tuple) 1103 | { 1104 | if (!self::ExistsDynamicParameter($output->components)) { 1105 | return self::GetOutputOffset_StaticComponents($output->components); 1106 | } 1107 | } 1108 | 1109 | return 1; 1110 | } 1111 | 1112 | 1113 | private static function GetOutputOffset_StaticComponents($components) : int 1114 | { 1115 | $offset = 0; 1116 | 1117 | foreach ($components as $comp) 1118 | { 1119 | $output_type = is_string($comp) ? $comp : $comp->type; 1120 | $varType = self::GetParameterType($output_type); 1121 | 1122 | if (Utils::string_contains($output_type, '[') || $varType == VariableType::Tuple) { 1123 | $offset += self::GetOutputOffset($comp); 1124 | } 1125 | else { 1126 | $offset++; 1127 | } 1128 | } 1129 | 1130 | return $offset; 1131 | } 1132 | 1133 | 1134 | 1135 | //EVENTS 1136 | 1137 | //parses event parameters 1138 | //event inputs are splitted between indexed topics and encoded data string 1139 | public function DecodeEvent($event_object, $log) : stdClass 1140 | { 1141 | $res = new stdClass(); 1142 | $res->indexed = array(); 1143 | $res->indexed []= $event_object->name; 1144 | 1145 | $res->data = array(); 1146 | 1147 | //split inputs between indexed and raw data 1148 | $indexed_index = 1; 1149 | $data_inputs = array(); 1150 | 1151 | foreach ($event_object->inputs as $input) 1152 | { 1153 | if ($input->indexed) 1154 | { 1155 | $input_type = is_string($input) ? $input : $input->type; 1156 | $varType = self::GetParameterType($input_type); 1157 | $res->indexed[$input->name] = $this->DecodeInput_Generic($varType, $log->topics[$indexed_index], 2); 1158 | 1159 | $indexed_index++; 1160 | } 1161 | else 1162 | { 1163 | $data_inputs []= $input; 1164 | } 1165 | } 1166 | 1167 | //parse raw data 1168 | $encoded = substr($log->data, 2); 1169 | $res->data = $this->DecodeGroup($data_inputs, $encoded, 0); 1170 | 1171 | //Return 1172 | return $res; 1173 | } 1174 | } 1175 | -------------------------------------------------------------------------------- /Core/Accounts.php: -------------------------------------------------------------------------------- 1 | signRaw($hash); 32 | $signature->message = $message; 33 | 34 | return $signature; 35 | } 36 | 37 | 38 | public function signRaw(string $hash) 39 | { 40 | //https://ethereum.stackexchange.com/questions/35425/web3-js-eth-sign-vs-eth-accounts-sign-producing-different-signatures 41 | $pk = $this->privateKey; 42 | if (substr($pk, 0, 2) != '0x') $pk = '0x' . $pk; 43 | 44 | // 64 hex characters + hex-prefix 45 | if (strlen($pk) != 66) { 46 | throw new Exception("Private key must be length 64 + 2 (" . strlen($pk) . " provided)"); 47 | } 48 | 49 | $ec = new EC('secp256k1'); 50 | try { 51 | $ecPrivateKey = $ec->keyFromPrivate(substr($pk, 2), 'hex'); 52 | } catch (Exception $e) { 53 | $ecPrivateKey = $ec->keyFromPrivate($pk, 'hex'); 54 | } 55 | 56 | //https://ethereum.stackexchange.com/questions/86485/create-signed-message-without-json-rpc-node-in-php 57 | $signature = $ecPrivateKey->sign($hash, ['canonical' => true, "n" => null,]); 58 | $r = str_pad($signature->r->toString(16), 64, '0', STR_PAD_LEFT); 59 | $s = str_pad($signature->s->toString(16), 64, '0', STR_PAD_LEFT); 60 | $v = dechex($signature->recoveryParam + 27); 61 | 62 | $res = new stdClass(); 63 | $res->messageHash = '0x'.$hash; 64 | $res->r = '0x'.$r; 65 | $res->s = '0x'.$s; 66 | $res->v = '0x'.$v; 67 | $res->signature = '0x'.$r.$s.$v;//$signature; 68 | 69 | return $res; 70 | 71 | //echo "Signed Hello world is:\n"; 72 | //echo "Using my script:\n"; 73 | //echo "0x$r$s$v\n"; 74 | //echo "Using MEW:\n"; 75 | //echo "0x2f52dfb196b75398b78c0e6c6aee8dc08d7279f2f88af5588ad7728f1e93dd0a479a710365c91ba649deb6c56e2e16836ffc5857cfd1130f159aebd05377d3a01c\n"; 76 | 77 | //web3.eth.accounts.sign('Some data', '0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318'); 78 | //> { 79 | // message: 'Some data', 80 | // messageHash: '0x1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655', 81 | // v: '0x1c', 82 | // r: '0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd', 83 | // s: '0x6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029', 84 | /// signature: '0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c' 85 | //} 86 | } 87 | 88 | } 89 | 90 | 91 | 92 | class Accounts 93 | { 94 | 95 | public static function create() 96 | { 97 | //Generates an account object with private key and public key. 98 | 99 | // Create the keypair 100 | $privateKey = Keccak::hash(Utils::GetRandomHex(128), 256); 101 | 102 | return self::privateKeyToAccount($privateKey); 103 | } 104 | 105 | 106 | public static function privateKeyToAccount(string $privateKey, bool $ignoreLength = false) 107 | { 108 | //Generates an account object with private key and public key. 109 | 110 | if (substr($privateKey, 0, 2) == '0x') { 111 | $privateKey = substr($privateKey, 2, strlen($privateKey) - 2); 112 | } 113 | 114 | // 64 hex characters + hex-prefix 115 | if (!$ignoreLength && strlen($privateKey) !== 64) { 116 | throw new Exception("Private key must be 32 bytes long (" . (strlen($privateKey) / 2) . " provided)"); 117 | } 118 | 119 | //get public key 120 | $ec = new EC('secp256k1'); 121 | $ec_priv = $ec->keyFromPrivate($privateKey); 122 | $publicKey = $ec_priv->getPublic(true, "hex"); 123 | 124 | // Returns a Web3 Account from a given privateKey 125 | $account = new Account(); 126 | $account->privateKey = '0x' . $privateKey; 127 | $account->publicKey = '0x' . $publicKey; 128 | $account->address = self::ecKeyToAddress($ec_priv->pub); 129 | 130 | return $account; 131 | } 132 | 133 | 134 | public static function hashMessage(string $message) : string 135 | { 136 | if (substr($message, 0, 2) == '0x' && strlen($message) % 2 == 0 && ctype_xdigit(substr($message, 2))) { 137 | $message = hex2bin(substr($message, 2)); 138 | } 139 | 140 | $messagelen = strlen($message); 141 | //"\x19Ethereum Signed Message:\n" + hash.length + hash and hashed using keccak256. 142 | $msg = hex2bin("19") . "Ethereum Signed Message:" . hex2bin("0A") . $messagelen . $message; 143 | $hash = Keccak::hash($msg, 256); 144 | 145 | return $hash; 146 | 147 | //https://github.com/web3/web3.js/blob/v1.2.11/packages/web3-eth-accounts/src/index.js (hashMessage) 148 | //web3.eth.accounts.hashMessage("Hello World") 149 | //"0xa1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2" 150 | } 151 | 152 | 153 | public static function ecKeyToAddress($pubEcKey) : string 154 | { 155 | return self::publicKeyToAddress($pubEcKey->encode("hex")); 156 | } 157 | 158 | 159 | public static function publicKeyToAddress(string $pubkey) 160 | { 161 | if (substr($pubkey, 0, 2) == '0x') $pubkey = substr($pubkey, 2); 162 | return "0x" . substr(Keccak::hash(substr(hex2bin($pubkey), 1), 256), 24); 163 | } 164 | 165 | 166 | public static function signedMessageToAddress(string $message, string $signature) : string 167 | { 168 | $hash = self::hashMessage($message); 169 | 170 | if (substr($signature, 0, 2) == '0x') { 171 | $signature = substr($signature, 2); 172 | } 173 | 174 | $sign = ["r" => substr($signature, 0, 64), "s" => substr($signature, 64, 64)]; 175 | $recid = ord(hex2bin(substr($signature, 128, 2))) - 27; 176 | 177 | if ($recid != ($recid & 1)) { 178 | throw new Exception("Signature recovery not valid"); 179 | } 180 | 181 | $ec = new EC('secp256k1'); 182 | $pubEcKey = $ec->recoverPubKey($hash, $sign, $recid); 183 | 184 | return self::publicKeyToAddress($pubEcKey->encode("hex")); 185 | } 186 | 187 | 188 | public static function verifySignatureWithAddress(string $message, string $signature, string $address) : bool 189 | { 190 | $message_address = self::signedMessageToAddress($message, $signature); 191 | 192 | return $address == $message_address; 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /Core/EIP712.php: -------------------------------------------------------------------------------- 1 | EIP712Domain = [ 25 | (object) ['name' => 'name', "type" => "string"], 26 | (object) ['name' => 'version', "type" => "string"], 27 | (object) ['name' => 'chainId', "type" => "uint256"], 28 | (object) ['name' => 'verifyingContract', "type" => "address"] 29 | ]; 30 | 31 | foreach ($types as $k => $t) { 32 | $finalTypes->$k = $t; 33 | } 34 | 35 | return $finalTypes; 36 | } 37 | 38 | 39 | private static function encodeType($typeName, $types) 40 | { 41 | $result = $typeName . '(' . implode(',', array_map(function ($type) { 42 | return $type->type . ' ' . $type->name; 43 | }, $types->$typeName)) . ')'; 44 | return Utils::sha3($result); 45 | } 46 | 47 | 48 | private static function encodeData($typeName, $data, $types) 49 | { 50 | $TYPEHASH = self::encodeType($typeName, $types); 51 | 52 | $types_inputs = ["bytes32"]; 53 | $data_inputs = [$TYPEHASH]; 54 | 55 | foreach ($types->$typeName as $field) 56 | { 57 | if ($field->type == "string" || Utils::string_contains($field->type, 'bytes')) { 58 | $types_inputs[] = "bytes32"; 59 | $data_inputs[] = Utils::sha3($data->{$field->name}); 60 | } 61 | else { 62 | $types_inputs[] = $field->type; 63 | $data_inputs[] = $data->{$field->name}; 64 | } 65 | } 66 | 67 | $encoded = ABI::EncodeParameters_External($types_inputs, $data_inputs); 68 | return Utils::sha3($encoded); 69 | } 70 | 71 | 72 | /* 73 | $types = [ 74 | "Message" => [ 75 | (object) [ "name" => "myName1", "type" => "uint256"] , 76 | (object) [ "name" => "myName2", "type" => "string"] 77 | ] 78 | ]; 79 | $domain = (object) [ 80 | "name" => "My DDapp", 81 | "version" => "1", 82 | "chainId" => 123, 83 | "verifyingContract" => "0xabc...123" 84 | ]; 85 | $data = (object) [ 86 | "myName1" => 123, 87 | "myName2" => "abc", 88 | ]; 89 | */ 90 | public static function signTypedData_digest(array $types, object $domain, object $message, string $primaryType = "") : string 91 | { 92 | if (empty($primaryType)) { 93 | $primaryType = array_keys($types)[0]; 94 | } 95 | 96 | $finalTypes = self::processTypes($types); 97 | $finalDomain = self::encodeData('EIP712Domain', $domain, $finalTypes); 98 | $finalHash = self::encodeData($primaryType, $message, $finalTypes); 99 | 100 | return Utils::sha3('0x1901' . Utils::stripZero($finalDomain) . Utils::stripZero($finalHash)); 101 | } 102 | 103 | } -------------------------------------------------------------------------------- /Core/SWeb3.php: -------------------------------------------------------------------------------- 1 | sweb3 = $sweb3; 37 | $this->address = $address; 38 | $this->privateKey = $privateKey; 39 | } 40 | 41 | function getNonce() 42 | { 43 | return $this->sweb3->getNonce($this->address); 44 | } 45 | } 46 | 47 | 48 | class SWeb3 49 | { 50 | private $provider; 51 | private $extra_curl_params; 52 | private $extra_headers; 53 | 54 | public $utils; 55 | 56 | public $personal; 57 | public $gasPrice; 58 | public $chainId; 59 | 60 | private $do_batch; 61 | private $batched_calls; 62 | 63 | 64 | function __construct(string $url_provider, array $extra_curl_params = null, array $extra_headers = null) 65 | { 66 | $this->provider = $url_provider; 67 | $this->extra_curl_params = $extra_curl_params; 68 | $this->extra_headers = $extra_headers; 69 | 70 | $this->utils = new Utils(); 71 | $this->gasPrice = null; 72 | 73 | $this->do_batch = false; 74 | $this->batched_calls = []; 75 | } 76 | 77 | 78 | function setPersonalData(string $address, string $privKey) 79 | { 80 | $this->personal = new PersonalData($this, $address, $privKey); 81 | } 82 | 83 | 84 | function call(string $method, ?array $params = null, $blockNumber = '') 85 | { 86 | //format api data 87 | $ethRequest = new Ethereum_CRPC(); 88 | $ethRequest->id = 1; 89 | $ethRequest->jsonrpc = '2.0'; 90 | $ethRequest->method = $method; 91 | 92 | $ethRequest->params = []; 93 | 94 | if ($params != null) { 95 | $ethRequest->params = $this->utils->forceAllNumbersHex($params); 96 | } 97 | 98 | if (!empty($blockNumber)) { 99 | $ethRequest->params []= $blockNumber; 100 | } 101 | 102 | if ($this->do_batch) { 103 | $this->batched_calls []= $ethRequest; 104 | return true; 105 | } 106 | else { 107 | $sendData = json_encode($ethRequest); 108 | return $this->makeCurl($sendData); 109 | } 110 | } 111 | 112 | 113 | function send($params) 114 | { 115 | if (!isset($params['gasPrice'])) $params['gasPrice'] = $this->getGasPrice(); 116 | if ($params != null) $params = $this->utils->forceAllNumbersHex($params); 117 | 118 | //prepare data 119 | $nonce = (isset($params['nonce'])) ? $params['nonce'] : ''; 120 | $gasPrice = (isset($params['gasPrice'])) ? $params['gasPrice'] : ''; 121 | $gasLimit = (isset($params['gasLimit'])) ? $params['gasLimit'] : ''; 122 | $to = (isset($params['to'])) ? $params['to'] : ''; 123 | $value = (isset($params['value'])) ? $params['value'] : ''; 124 | $data = (isset($params['data'])) ? $params['data'] : ''; 125 | $chainId = (isset($this->chainId)) ? $this->chainId : '0x0'; 126 | 127 | 128 | //sign transaction 129 | $transaction = new Transaction ($nonce, $gasPrice, $gasLimit, $to, $value, $data); 130 | $signedTransaction = '0x' . $transaction->getRaw ($this->personal->privateKey, $chainId); 131 | 132 | //SEND RAW TRANSACTION 133 | //format api data 134 | $ethRequest = new Ethereum_CRPC(); 135 | $ethRequest->id = 0; 136 | $ethRequest->jsonrpc = '2.0'; 137 | $ethRequest->method = 'eth_sendRawTransaction'; 138 | 139 | $ethRequest->params = [$signedTransaction]; 140 | 141 | if ($this->do_batch) { 142 | $this->batched_calls []= $ethRequest; 143 | return true; 144 | } 145 | else { 146 | $sendData = json_encode($ethRequest); 147 | //var_dump( $sendData); 148 | return $this->makeCurl($sendData); 149 | } 150 | } 151 | 152 | 153 | 154 | private function makeCurl(string $sendData) 155 | { 156 | //prepare curl 157 | $tuCurl = curl_init(); 158 | curl_setopt($tuCurl, CURLOPT_RETURNTRANSFER, true); 159 | curl_setopt($tuCurl, CURLOPT_URL, $this->provider); 160 | 161 | if ($this->extra_curl_params != null) { 162 | foreach ($this->extra_curl_params as $key => $param) { 163 | curl_setopt($tuCurl, $key, $param); 164 | } 165 | } 166 | 167 | 168 | //curl settings 169 | 170 | //curl port 171 | //curl_setopt($tuCurl, CURLOPT_PORT , 443); 172 | 173 | //post request 174 | curl_setopt($tuCurl, CURLOPT_POST, 1); 175 | 176 | //headers 177 | $headers = array("Content-Type: application/json", "Content-length: ".strlen($sendData)); 178 | if($this->extra_headers) { 179 | $headers = array_merge($headers, $this->extra_headers); 180 | } 181 | curl_setopt($tuCurl, CURLOPT_HTTPHEADER, $headers); 182 | 183 | //post data 184 | curl_setopt($tuCurl, CURLOPT_POSTFIELDS, $sendData); 185 | 186 | 187 | //execute call 188 | $tuData = curl_exec($tuCurl); 189 | if (!curl_errno($tuCurl)) 190 | $info = curl_getinfo($tuCurl); 191 | else 192 | throw new Exception('Curl send error: ' . curl_error($tuCurl)); 193 | 194 | curl_close($tuCurl); 195 | 196 | return json_decode($tuData); 197 | } 198 | 199 | 200 | function batch(bool $new_batch) 201 | { 202 | $this->do_batch = $new_batch; 203 | } 204 | 205 | 206 | function executeBatch() 207 | { 208 | if (!$this->do_batch) { 209 | return '{"error" : "SWeb3 not batching calls"}'; 210 | } 211 | if (count($this->batched_calls) <= 0) { 212 | return '{"error" : "SWeb3 no batched calls"}'; 213 | } 214 | 215 | $sendData = json_encode($this->batched_calls); 216 | $this->batched_calls = []; 217 | 218 | return $this->makeCurl($sendData); 219 | } 220 | 221 | 222 | function getNonce(string $address, $blockNumber = 'pending') 223 | { 224 | $transactionCount = $this->call('eth_getTransactionCount', [$address], $blockNumber); 225 | 226 | if(!isset($transactionCount->result)) { 227 | throw new Exception('getNonce error. from address: ' . $address); 228 | } 229 | 230 | return $this->utils->hexToBn($transactionCount->result); 231 | } 232 | 233 | 234 | 235 | function getGasPrice(bool $force_refresh = false) : BigNumber 236 | { 237 | if ($this->gasPrice == null || $force_refresh) { 238 | $gasPriceResult = $this->call('eth_gasPrice'); 239 | 240 | if(!isset($gasPriceResult->result)) { 241 | //var_dump($gasPriceResult); 242 | throw new Exception('getGasPrice error. '); 243 | } 244 | 245 | $this->gasPrice = $this->utils->hexToBn($gasPriceResult->result); 246 | } 247 | 248 | return $this->gasPrice; 249 | } 250 | 251 | 252 | //general info: https://docs.alchemy.com/alchemy/guides/eth_getlogs 253 | //default blocks: from-> 0x0 to-> latest 254 | //TOPICS: https://eth.wiki/json-rpc/API#a-note-on-specifying-topic-filters 255 | function getLogs(string $related_address, string $minBlock = null, string $maxBlock = null, $topics = null) 256 | { 257 | $data = new stdClass(); 258 | $data->address = $related_address; 259 | 260 | $data->fromBlock = ($minBlock != null) ? $minBlock : '0x0'; 261 | $data->toBlock = ($maxBlock != null) ? $maxBlock : 'latest'; 262 | if ($topics != null) $data->topics = $topics; 263 | 264 | $result = $this->call('eth_getLogs', [$data]); 265 | 266 | if (!isset($result->result) || !is_array($result->result)) { 267 | throw new Exception('getLogs error: ' . json_encode($result)); 268 | } 269 | 270 | return $result; 271 | } 272 | 273 | 274 | //general info: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionreceipt 275 | function getTransactionReceipt(string $transaction_hash) 276 | { 277 | $result = $this->call('eth_getTransactionReceipt', [$transaction_hash]); 278 | 279 | if(!isset($result->result)) { 280 | throw new Exception('getTransactionReceipt error: ' . json_encode($result)); 281 | } 282 | 283 | return $result; 284 | } 285 | 286 | } 287 | 288 | 289 | 290 | -------------------------------------------------------------------------------- /Core/SWeb3_Contract.php: -------------------------------------------------------------------------------- 1 | sweb3 = $sweb3; 31 | $this->address = $contractAddress; 32 | 33 | $this->ABI = new ABI(); 34 | $this->ABI->Init($contractABI); 35 | } 36 | 37 | 38 | function setBytecode($bytecode) 39 | { 40 | $this->bytecode = $bytecode; 41 | } 42 | 43 | 44 | function call(string $function_name, $callData = null, $extraParams = null, $blockNumber = 'latest') 45 | { 46 | if (!$this->ABI->isCallFunction($function_name)) { 47 | throw new Exception('ERROR: ' . $function_name . ' does not exist as a call function in this contract'); 48 | } 49 | 50 | $hashData = $this->ABI->EncodeData($function_name, $callData); 51 | 52 | if ($extraParams == null) $extraParams = []; 53 | $extraParams['to'] = $this->address; 54 | $extraParams['data'] = $hashData; 55 | 56 | $result = $this->sweb3->call('eth_call', [$extraParams], $blockNumber); 57 | 58 | if(isset($result->result)) 59 | return $this->DecodeData($function_name, $result->result); 60 | else 61 | return $result; 62 | } 63 | 64 | 65 | function send(string $function_name, $sendData, $extraParams = null) 66 | { 67 | if (!$this->ABI->isSendFunction($function_name)) { 68 | throw new Exception('ERROR: ' . $function_name . ' does not exist as a send function (changing state transaction) in this contract'); 69 | } 70 | 71 | $hashData = $this->ABI->EncodeData($function_name, $sendData); 72 | 73 | if ($extraParams == null) $extraParams = []; 74 | $extraParams['from'] = $this->sweb3->personal->address; 75 | $extraParams['to'] = $this->address; 76 | $extraParams['data'] = $hashData; 77 | 78 | if (!isset($extraParams['gasLimit'])) $extraParams['gasLimit'] = $this->estimateGas($extraParams); 79 | 80 | 81 | $result = $this->sweb3->send($extraParams); 82 | return $result; 83 | } 84 | 85 | 86 | function DecodeData(string $function_name, $data) 87 | { 88 | return $this->ABI->DecodeData($function_name, $data); 89 | } 90 | 91 | 92 | function estimateGas($extraParams) 93 | { 94 | $gasEstimateResult = $this->sweb3->call('eth_estimateGas', [$extraParams]); 95 | 96 | if(!isset($gasEstimateResult->result)) { 97 | throw new Exception('ERROR: estimateGas error: ' . $gasEstimateResult->error->message . ' (' . $gasEstimateResult->error->code . ')'); 98 | } 99 | 100 | $gasEstimate = $this->sweb3->utils->hexToBn($gasEstimateResult->result); 101 | 102 | return $gasEstimate; 103 | } 104 | 105 | 106 | function deployContract(array $inputs = [], array $extra_params = []) 107 | { 108 | if(!isset($this->bytecode)) { 109 | throw new Exception('ERROR: you need to initialize bytecode to deploy the contract'); 110 | } 111 | 112 | $count_expected = isset($this->ABI->constructor) && isset($this->ABI->constructor->inputs) ? count($this->ABI->constructor->inputs) : 0; 113 | $count_received = count($inputs); 114 | if ($count_expected != $count_received) { 115 | throw new Exception('ERROR: contract constructor inputs number does not match... Expecting: ' . $count_expected . ' Received: ' . $count_received); 116 | } 117 | 118 | $inputEncoded = $this->ABI->EncodeData('', $inputs); 119 | $extra_params['data'] = '0x' . $this->bytecode . Utils::stripZero($inputEncoded); 120 | 121 | //get function estimateGas 122 | if(!isset($extra_params['gasLimit'])) { 123 | $gasEstimateResult = $this->sweb3->call('eth_estimateGas', [$extra_params]); 124 | 125 | if(!isset($gasEstimateResult->result)) 126 | throw new Exception('estimation error: ' . json_encode($gasEstimateResult)); 127 | 128 | $extra_params['gasLimit'] = $this->sweb3->utils->hexToBn($gasEstimateResult->result); 129 | } 130 | 131 | //get gas price 132 | if(!isset($extra_params['gasPrice'])) $extra_params['gasPrice'] = $this->sweb3->getGasPrice(); 133 | 134 | return $this->sweb3->send($extra_params); 135 | } 136 | 137 | 138 | //EVENT LOGS 139 | 140 | //returns event ABI from event hash (encoded event name in transaction logs -> topics[0]) 141 | function GetEventFromLog($log_object) 142 | { 143 | return $this->ABI->GetEventFromHash($log_object->topics[0]); 144 | } 145 | 146 | 147 | //returns decoded topics/data from event object (in transaction logs ) 148 | function DecodeEvent($event_object, $log) 149 | { 150 | return $this->ABI->DecodeEvent($event_object, $log); 151 | } 152 | 153 | 154 | //returns all event logs. each with 2 extra parameters "decoded_data" and "event_anme" 155 | function getLogs(string $minBlock = null, string $maxBlock = null, $topics = null) 156 | { 157 | $result = $this->sweb3->getLogs($this->address, $minBlock, $maxBlock, $topics); 158 | $logs = $result->result; 159 | 160 | foreach($logs as $log) 161 | { 162 | $event = $this->GetEventFromLog($log); 163 | if($event != null) 164 | { 165 | $log->event_name = $event->name; 166 | $log->decoded_data = $this->DecodeEvent($event, $log); 167 | } 168 | else { 169 | $log->event_name = 'unknown'; 170 | } 171 | } 172 | 173 | return $logs; 174 | } 175 | 176 | 177 | } 178 | -------------------------------------------------------------------------------- /Core/Utils.php: -------------------------------------------------------------------------------- 1 | 14 | * 15 | * @author Peter Lai 16 | * @license MIT 17 | */ 18 | 19 | namespace SWeb3; 20 | 21 | 22 | use stdClass; 23 | use InvalidArgumentException; 24 | use kornrunner\Keccak; 25 | use phpseclib3\Math\BigInteger as BigNumber; 26 | 27 | class Utils 28 | { 29 | /** 30 | * SHA3_NULL_HASH 31 | * 32 | * @const string 33 | */ 34 | const SHA3_NULL_HASH = 'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'; 35 | 36 | /** 37 | * UNITS 38 | * from ethjs-unit 39 | * 40 | * @const array 41 | */ 42 | const UNITS = [ 43 | 'noether' => '0', 44 | 'wei' => '1', 45 | 'kwei' => '1000', 46 | 'Kwei' => '1000', 47 | 'babbage' => '1000', 48 | 'femtoether' => '1000', 49 | 'mwei' => '1000000', 50 | 'Mwei' => '1000000', 51 | 'lovelace' => '1000000', 52 | 'picoether' => '1000000', 53 | 'gwei' => '1000000000', 54 | 'Gwei' => '1000000000', 55 | 'shannon' => '1000000000', 56 | 'nanoether' => '1000000000', 57 | 'nano' => '1000000000', 58 | 'szabo' => '1000000000000', 59 | 'microether' => '1000000000000', 60 | 'micro' => '1000000000000', 61 | 'finney' => '1000000000000000', 62 | 'milliether' => '1000000000000000', 63 | 'milli' => '1000000000000000', 64 | 'ether' => '1000000000000000000', 65 | 'kether' => '1000000000000000000000', 66 | 'grand' => '1000000000000000000000', 67 | 'mether' => '1000000000000000000000000', 68 | 'gether' => '1000000000000000000000000000', 69 | 'tether' => '1000000000000000000000000000000' 70 | ]; 71 | 72 | 73 | 74 | /** 75 | * hexToBn 76 | * decoding hex number into decimal 77 | * 78 | * @param string $value 79 | * @return int 80 | */ 81 | public static function hexToBn($value) 82 | { 83 | $value = self::stripZero($value); 84 | return (new BigNumber($value, 16)); 85 | } 86 | 87 | /** 88 | * toHex 89 | * Encoding string or integer or numeric string(is not zero prefixed) or big number to hex. 90 | * 91 | * @param string|int|BigNumber $value 92 | * @param bool $isPrefix 93 | * @return string 94 | */ 95 | public static function toHex($value, $isPrefix=false) 96 | { 97 | if (is_numeric($value) && !is_float($value) && !is_double($value)) { 98 | // turn to hex number 99 | $bn = self::toBn($value); 100 | $hex = $bn->toHex(true); 101 | $hex = preg_replace('/^0+(?!$)/', '', $hex); 102 | } elseif (is_string($value)) { 103 | $value = self::stripZero($value); 104 | $hex = implode('', unpack('H*', $value)); 105 | } elseif ($value instanceof BigNumber) { 106 | $hex = $value->toHex(true); 107 | $hex = preg_replace('/^0+(?!$)/', '', $hex); 108 | } else { 109 | $type_error = gettype($value); 110 | 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."); 111 | } 112 | 113 | if ($isPrefix) { 114 | return self::addZeroPrefix($hex); 115 | } 116 | return $hex; 117 | } 118 | 119 | /** 120 | * hexToBin 121 | * 122 | * @param string 123 | * @return string 124 | */ 125 | public static function hexToBin($value) 126 | { 127 | if (!is_string($value)) { 128 | throw new InvalidArgumentException('The value to hexToBin function must be string.'); 129 | } 130 | if (self::isZeroPrefixed($value)) { 131 | $count = 1; 132 | $value = str_replace('0x', '', $value, $count); 133 | } 134 | return pack('H*', $value); 135 | } 136 | 137 | /** 138 | * isZeroPrefixed 139 | * 140 | * @param string 141 | * @return bool 142 | */ 143 | public static function isZeroPrefixed($value) 144 | { 145 | if (!is_string($value)) { 146 | //throw new InvalidArgumentException('The value to isZeroPrefixed function must be string.'); 147 | } 148 | return (strpos($value, '0x') === 0); 149 | } 150 | 151 | /** 152 | * stripZero 153 | * 154 | * @param string $value 155 | * @return string 156 | */ 157 | public static function stripZero($value) 158 | { 159 | if (self::isZeroPrefixed($value)) { 160 | $count = 1; 161 | return str_replace('0x', '', $value, $count); 162 | } 163 | return $value; 164 | } 165 | 166 | /** 167 | * addZeroPrefix 168 | * 169 | * @param string 170 | * @return string 171 | */ 172 | public static function addZeroPrefix($value) 173 | { 174 | $value = '' . $value; 175 | 176 | if (self::isZeroPrefixed($value)) return $value; 177 | 178 | //remove leading 0s 179 | $value = ltrim($value, "0"); 180 | 181 | return '0x' . $value; 182 | } 183 | 184 | /** 185 | * forceAllNumbersHex 186 | * 187 | * @param object[] 188 | * @return object[] 189 | */ 190 | public static function forceAllNumbersHex($params) 191 | { 192 | foreach($params as $key => $param) 193 | { 194 | if ($key !== 'chainId') 195 | { 196 | if(is_numeric($param) || $param instanceof BigNumber) 197 | { 198 | $params[$key] = self::toHex($param, true); 199 | } 200 | else if(is_array($param)) 201 | { 202 | foreach($param as $sub_key => $sub_param) 203 | { 204 | if ($sub_key !== 'chainId') 205 | { 206 | if(is_numeric($sub_param) || $sub_param instanceof BigNumber) { 207 | $param[$sub_key] = self::toHex($sub_param, true); 208 | } 209 | } 210 | } 211 | 212 | $params[$key] = $param; 213 | } 214 | } 215 | } 216 | 217 | return $params; 218 | } 219 | 220 | 221 | 222 | /** 223 | * isNegative 224 | * 225 | * @param string 226 | * @return bool 227 | */ 228 | public static function isNegative($value) 229 | { 230 | if (!is_string($value)) { 231 | throw new InvalidArgumentException('The value to isNegative function must be string.'); 232 | } 233 | return (strpos($value, '-') === 0); 234 | } 235 | 236 | /** 237 | * isAddress 238 | * 239 | * @param string $value 240 | * @return bool 241 | */ 242 | public static function isAddress($value) 243 | { 244 | if (!is_string($value)) { 245 | throw new InvalidArgumentException('The value to isAddress function must be string.'); 246 | } 247 | if (preg_match('/^(0x|0X)?[a-f0-9A-F]{40}$/', $value) !== 1) { 248 | return false; 249 | } elseif (preg_match('/^(0x|0X)?[a-f0-9]{40}$/', $value) === 1 || preg_match('/^(0x|0X)?[A-F0-9]{40}$/', $value) === 1) { 250 | return true; 251 | } 252 | return self::isAddressChecksum($value); 253 | } 254 | 255 | /** 256 | * isAddressChecksum 257 | * 258 | * @param string $value 259 | * @return bool 260 | */ 261 | public static function isAddressChecksum($value) 262 | { 263 | if (!is_string($value)) { 264 | throw new InvalidArgumentException('The value to isAddressChecksum function must be string.'); 265 | } 266 | $value = self::stripZero($value); 267 | $hash = self::stripZero(self::sha3(mb_strtolower($value))); 268 | 269 | for ($i = 0; $i < 40; $i++) { 270 | if ( 271 | (intval($hash[$i], 16) > 7 && mb_strtoupper($value[$i]) !== $value[$i]) || 272 | (intval($hash[$i], 16) <= 7 && mb_strtolower($value[$i]) !== $value[$i]) 273 | ) { 274 | return false; 275 | } 276 | } 277 | return true; 278 | } 279 | 280 | /** 281 | * toChecksumAddress 282 | * 283 | * @param string $value 284 | * @return string 285 | */ 286 | public static function toChecksumAddress($value) 287 | { 288 | if (!is_string($value)) { 289 | throw new InvalidArgumentException('The value to toChecksumAddress function must be string.'); 290 | } 291 | $value = self::stripZero(strtolower($value)); 292 | $hash = self::stripZero(self::sha3($value)); 293 | $ret = '0x'; 294 | 295 | for ($i = 0; $i < 40; $i++) { 296 | if (intval($hash[$i], 16) >= 8) { 297 | $ret .= strtoupper($value[$i]); 298 | } else { 299 | $ret .= $value[$i]; 300 | } 301 | } 302 | return $ret; 303 | } 304 | 305 | /** 306 | * isHex 307 | * 308 | * @param string $value 309 | * @return bool 310 | */ 311 | public static function isHex($value) 312 | { 313 | return (is_string($value) && preg_match('/^(0x)?[a-f0-9]*$/', $value) === 1); 314 | } 315 | 316 | /** 317 | * sha3 318 | * keccak256 319 | * 320 | * @param string $value 321 | * @return string 322 | */ 323 | public static function sha3($value) 324 | { 325 | if (!is_string($value)) { 326 | throw new InvalidArgumentException('The value to sha3 function must be string.'); 327 | } 328 | if (strpos($value, '0x') === 0) { 329 | $value = self::hexToBin($value); 330 | } 331 | $hash = Keccak::hash($value, 256); 332 | 333 | if ($hash === self::SHA3_NULL_HASH) { 334 | return null; 335 | } 336 | return '0x' . $hash; 337 | } 338 | 339 | /** 340 | * toString 341 | * 342 | * @param mixed $value 343 | * @return string 344 | */ 345 | public static function toString($value) 346 | { 347 | $value = (string) $value; 348 | 349 | return $value; 350 | } 351 | 352 | /** 353 | * toWei 354 | * Change number from unit to wei. 355 | * For example: 356 | * $wei = Utils::toWei('1', 'kwei'); 357 | * $wei->toString(); // 1000 358 | * 359 | * @param BigNumber|string $number 360 | * @param string $unit 361 | * @return \phpseclib3\Math\BigInteger 362 | */ 363 | public static function toWei($number, string $unit) 364 | { 365 | if (!isset(self::UNITS[$unit])) { 366 | throw new InvalidArgumentException('toWei doesn\'t support ' . $unit . ' unit.'); 367 | } 368 | 369 | return self::toWei_Internal($number, self::UNITS[$unit]); 370 | } 371 | 372 | 373 | /** 374 | * toWeiFromDecimals 375 | * Change number from unit that has decimals to wei. 376 | * For example: 377 | * $wei = Utils::toWeiFromDecimals('0.01', 8); //1000000 378 | * $wei->toString(); // 1000 379 | * 380 | * @param BigNumber|string $number 381 | * @param string $unit 382 | * @return \phpseclib3\Math\BigInteger 383 | */ 384 | public static function toWeiFromDecimals($number, int $numberOfDecimals) 385 | { 386 | $exponent = str_pad('1', $numberOfDecimals + 1, '0', STR_PAD_RIGHT); 387 | return self::toWei_Internal($number, $exponent); 388 | } 389 | 390 | 391 | /** 392 | * toWei_Internal 393 | * Internal private fucntion to convert a number in "unti" to string. 394 | * The unit string is 1000...000 having # decimal zero positions 395 | * 396 | * @param BigNumber|string $number 397 | * @param string $unit_value 398 | * @return \phpseclib3\Math\BigInteger 399 | */ 400 | private static function toWei_Internal($number, string $unit_value) 401 | { 402 | if (!is_string($number) && !($number instanceof BigNumber)) { 403 | throw new InvalidArgumentException('toWei number must be string or bignumber.'); 404 | } 405 | $bn = self::toBn($number); 406 | 407 | $bnt = new BigNumber($unit_value); 408 | 409 | if (is_array($bn)) { 410 | // fraction number 411 | list($whole, $fraction, $fractionLength, $negative1) = $bn; 412 | 413 | if ($fractionLength > strlen($unit_value)) { 414 | throw new InvalidArgumentException('toWei fraction part is out of limit.'); 415 | } 416 | $whole = $whole->multiply($bnt); 417 | 418 | $powerBase = (new BigNumber(10))->pow(new BigNumber($fractionLength)); 419 | $base = new BigNumber($powerBase->toString()); 420 | $fraction = $fraction->multiply($bnt)->divide($base)[0]; 421 | 422 | if ($negative1 !== false) { 423 | return $whole->add($fraction)->multiply($negative1); 424 | } 425 | return $whole->add($fraction); 426 | } 427 | 428 | return $bn->multiply($bnt); 429 | } 430 | 431 | 432 | /** 433 | * toEther 434 | * Change number from unit to ether. 435 | * For example: 436 | * list($bnq, $bnr) = Utils::toEther('1', 'kether'); 437 | * $bnq->toString(); // 1000 438 | * 439 | * @param BigNumber|string|int $number 440 | * @param string $unit 441 | * @return array 442 | */ 443 | public static function toEther($number, $unit) 444 | { 445 | // if ($unit === 'ether') { 446 | // throw new InvalidArgumentException('Please use another unit.'); 447 | // } 448 | $wei = self::toWei($number, $unit); 449 | $bnt = new BigNumber(self::UNITS['ether']); 450 | 451 | return $wei->divide($bnt); 452 | } 453 | 454 | 455 | /** 456 | * fromWei 457 | * Change number from wei to unit. 458 | * For example: 459 | * list($bnq, $bnr) = Utils::fromWei('1000', 'kwei'); 460 | * $bnq->toString(); // 1 461 | * 462 | * @param BigNumber|string|int $number 463 | * @param string $unit 464 | * @return \phpseclib3\Math\BigInteger 465 | */ 466 | public static function fromWei($number, $unit) 467 | { 468 | $bn = self::toBn($number); 469 | 470 | if (!is_string($unit)) { 471 | throw new InvalidArgumentException('fromWei unit must be string.'); 472 | } 473 | if (!isset(self::UNITS[$unit])) { 474 | throw new InvalidArgumentException('fromWei doesn\'t support ' . $unit . ' unit.'); 475 | } 476 | $bnt = new BigNumber(self::UNITS[$unit]); 477 | 478 | return $bn->divide($bnt); 479 | } 480 | 481 | 482 | 483 | /** 484 | * toWeiString 485 | * Change number from unit to wei. and show a string representation 486 | * For example: 487 | * $wei = Utils::toWeiString('1', 'kwei'); // 1000 488 | * 489 | * @param BigNumber|string $number 490 | * @param string $unit 491 | * @return string 492 | */ 493 | public static function toWeiString($number, $unit) : string 494 | { 495 | $conv = self::toWei($number, $unit); 496 | return $conv->toString(); 497 | } 498 | 499 | /** 500 | * toWeiStringFromDecimals 501 | * Change number from decimals to wei. and show a string representation 502 | * For example: 503 | * $wei = Utils::toWeiStringFromDecimals('1', 'kwei'); // 1000 504 | * 505 | * @param BigNumber|string $number 506 | * @param int $numberOfDecimals 507 | * @return string 508 | */ 509 | public static function toWeiStringFromDecimals($number, int $numberOfDecimals) : string 510 | { 511 | $conv = self::toWeiFromDecimals($number, $numberOfDecimals); 512 | return $conv->toString(); 513 | } 514 | 515 | 516 | /** 517 | * toEtherString 518 | * Change number from unit to ether. and show a string representation 519 | * For example: 520 | * $ether = Utils::toEtherString('1', 'kether'); // 1000 521 | * 522 | * @param BigNumber|string|int $number 523 | * @param string $unit 524 | * @return string 525 | */ 526 | public static function toEtherString($number, $unit) : string 527 | { 528 | $conversion = self::toEther($number, $unit); 529 | return self::transformDivisionToString($conversion, self::UNITS[$unit], self::UNITS['ether']); 530 | } 531 | 532 | 533 | /** 534 | * fromWeiToString 535 | * Change number from wei to unit. and show a string representation 536 | * For example: 537 | * $kwei = Utils::fromWei('1001', 'kwei'); // 1.001 538 | * 539 | * @param BigNumber|string|int $number 540 | * @param string $unit 541 | * @return \phpseclib3\Math\BigInteger 542 | */ 543 | public static function fromWeiToString($number, $unit) : string 544 | { 545 | $conversion = self::fromWei($number, $unit); 546 | return self::transformDivisionToString($conversion, self::UNITS['wei'], self::UNITS[$unit]); 547 | } 548 | 549 | 550 | /** 551 | * fromWeiToDecimalsString 552 | * Change number from wei to number of decimals. 553 | * For example: 554 | * $stringNumber = Utils::fromWeiToDecimalsString('1000000', 8); //0.01 555 | * 556 | * @param BigNumber|string|int $number 557 | * @param int $numberOfDecimals 558 | * @return string 559 | */ 560 | public static function fromWeiToDecimalsString($number, int $numberOfDecimals) : string 561 | { 562 | $bn = self::toBn($number); 563 | 564 | $exponent = str_pad('1', $numberOfDecimals + 1, '0', STR_PAD_RIGHT); 565 | 566 | $bnt = new BigNumber($exponent); 567 | 568 | $conversion = $bn->divide($bnt); 569 | 570 | return self::transformDivisionToString($conversion, self::UNITS['wei'], $exponent); 571 | } 572 | 573 | 574 | /** 575 | * transformDivisionToString 576 | * Internal private fucntion to convert a [quotient, remainder] BigNumber division result, 577 | * to a human readable unit.decimals (12.3920012000) 578 | * The unit string is 1000...000 having # decimal zero positions 579 | * 580 | * @param (\phpseclib3\Math\BigInteg, \phpseclib3\Math\BigInteg) $divisionArray 581 | * @param string $unitZerosOrigin string representing the origin unit's number of zeros 582 | * @param string $unitZerosOrigin string representing the origin unit's number of zeros 583 | * @return string 584 | */ 585 | private static function transformDivisionToString($divisionArray, $unitZerosOrigin, $unitZerosDestiny) : string 586 | { 587 | $left = $divisionArray[0]->toString(); 588 | $right = $divisionArray[1]->toString(); 589 | 590 | if ($right != "0") 591 | { 592 | $bnt_wei = new BigNumber($unitZerosOrigin); 593 | $bnt_unit = new BigNumber($unitZerosDestiny); 594 | 595 | $right_lead_zeros = strlen($bnt_unit->toString()) - strlen($bnt_wei->toString()) - strlen($right); 596 | 597 | for ($i = 0; $i < $right_lead_zeros; $i++) $right = '0' . $right; 598 | $right = rtrim($right, "0"); 599 | 600 | return $left . '.' . $right; 601 | } 602 | else 603 | { 604 | return $left; 605 | } 606 | } 607 | 608 | /** 609 | * jsonMethodToString 610 | * 611 | * @param stdClass|array $json 612 | * @return string 613 | */ 614 | public static function jsonMethodToString($json) : string 615 | { 616 | if ($json instanceof stdClass) { 617 | // one way to change whole json stdClass to array type 618 | // $jsonString = json_encode($json); 619 | 620 | // if (JSON_ERROR_NONE !== json_last_error()) { 621 | // throw new InvalidArgumentException('json_decode error: ' . json_last_error_msg()); 622 | // } 623 | // $json = json_decode($jsonString, true); 624 | 625 | // another way to change whole json to array type but need the depth 626 | // $json = self::jsonToArray($json, $depth) 627 | 628 | // another way to change json to array type but not whole json stdClass 629 | $json = (array) $json; 630 | $typeName = []; 631 | 632 | foreach ($json['inputs'] as $param) { 633 | if (isset($param->type)) { 634 | $typeName[] = $param->type; 635 | } 636 | } 637 | return $json['name'] . '(' . implode(',', $typeName) . ')'; 638 | } elseif (!is_array($json)) { 639 | throw new InvalidArgumentException('jsonMethodToString json must be array or stdClass.'); 640 | } 641 | if (isset($json['name']) && strpos($json['name'], '(') > 0) { 642 | return $json['name']; 643 | } 644 | $typeName = []; 645 | 646 | foreach ($json['inputs'] as $param) { 647 | if (isset($param['type'])) { 648 | $typeName[] = $param['type']; 649 | } 650 | } 651 | return $json['name'] . '(' . implode(',', $typeName) . ')'; 652 | } 653 | 654 | /** 655 | * jsonToArray 656 | * 657 | * @param stdClass|array $json 658 | * @return array 659 | */ 660 | public static function jsonToArray($json) 661 | { 662 | if ($json instanceof stdClass) { 663 | $json = (array) $json; 664 | $typeName = []; 665 | 666 | foreach ($json as $key => $param) { 667 | if (is_array($param)) { 668 | foreach ($param as $subKey => $subParam) { 669 | $json[$key][$subKey] = self::jsonToArray($subParam); 670 | } 671 | } elseif ($param instanceof stdClass) { 672 | $json[$key] = self::jsonToArray($param); 673 | } 674 | } 675 | } elseif (is_array($json)) { 676 | foreach ($json as $key => $param) { 677 | if (is_array($param)) { 678 | foreach ($param as $subKey => $subParam) { 679 | $json[$key][$subKey] = self::jsonToArray($subParam); 680 | } 681 | } elseif ($param instanceof stdClass) { 682 | $json[$key] = self::jsonToArray($param); 683 | } 684 | } 685 | } 686 | return $json; 687 | } 688 | 689 | /** 690 | * toBn 691 | * Change number or number string to bignumber. 692 | * 693 | * @param BigNumber|string|int $number 694 | * @return array|\phpseclib3\Math\BigInteger 695 | */ 696 | public static function toBn($number) 697 | { 698 | if ($number instanceof BigNumber){ 699 | $bn = $number; 700 | } 701 | elseif (is_int($number)) { 702 | $bn = new BigNumber($number); 703 | } 704 | elseif (is_numeric($number)) { 705 | $number = (string) $number; 706 | 707 | if (self::isNegative($number)) { 708 | $count = 1; 709 | $number = str_replace('-', '', $number, $count); 710 | $negative1 = new BigNumber(-1); 711 | } 712 | if (strpos($number, '.') > 0) { 713 | $comps = explode('.', $number); 714 | 715 | if (count($comps) > 2) { 716 | throw new InvalidArgumentException('toBn number must be a valid number.'); 717 | } 718 | $whole = $comps[0]; 719 | $fraction = $comps[1]; 720 | 721 | return [ 722 | new BigNumber($whole), 723 | new BigNumber($fraction), 724 | strlen($comps[1]), 725 | isset($negative1) ? $negative1 : false 726 | ]; 727 | } else { 728 | $bn = new BigNumber($number); 729 | } 730 | if (isset($negative1)) { 731 | $bn = $bn->multiply($negative1); 732 | } 733 | } 734 | elseif (is_string($number)) { 735 | $number = mb_strtolower($number); 736 | 737 | if (self::isNegative($number)) { 738 | $count = 1; 739 | $number = str_replace('-', '', $number, $count); 740 | $negative1 = new BigNumber(-1); 741 | } 742 | if (self::isZeroPrefixed($number) || preg_match('/^[0-9a-f]+$/i', $number) === 1) { 743 | $number = self::stripZero($number); 744 | $bn = new BigNumber($number, 16); 745 | } elseif (empty($number)) { 746 | $bn = new BigNumber(0); 747 | } else { 748 | throw new InvalidArgumentException('toBn number must be valid hex string.'); 749 | } 750 | if (isset($negative1)) { 751 | $bn = $bn->multiply($negative1); 752 | } 753 | } 754 | else { 755 | throw new InvalidArgumentException('toBn number must be BigNumber, string or int.'); 756 | } 757 | return $bn; 758 | } 759 | 760 | 761 | public static function GetRandomHex(int $length) 762 | { 763 | return bin2hex(openssl_random_pseudo_bytes($length / 2)); 764 | } 765 | 766 | 767 | public static function string_contains(string $haystack, string $needle) 768 | { 769 | return empty($needle) || strpos($haystack, $needle) !== false; 770 | } 771 | 772 | } -------------------------------------------------------------------------------- /Example/example.account.php: -------------------------------------------------------------------------------- 1 | privateKey); 35 | 36 | var_dump("ACCOUNT 2"); 37 | var_dump($account2); 38 | 39 | 40 | var_dump('HASH "Hello World", should be: 0xa1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2'); 41 | var_dump(Accounts::hashMessage("Hello World")); 42 | 43 | var_dump('HASH "Some data", should be: 0x1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655'); 44 | var_dump(Accounts::hashMessage('Some data')); 45 | 46 | 47 | var_dump("Sign message 'Some data' Should be:"); 48 | var_dump("signature: 0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c"); 49 | $account3 = Accounts::privateKeyToAccount('0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318'); 50 | $res = $account3->sign('Some data'); 51 | var_dump($res); 52 | 53 | 54 | var_dump("Reverse the last signed message. Should be:"); 55 | var_dump("address: $account3->address"); 56 | $address = Accounts::signedMessageToAddress('Some data', $res->signature); 57 | var_dump("res: $address"); 58 | 59 | 60 | var_dump("Check Signature With Address:"); 61 | $state = Accounts::verifySignatureWithAddress('Some data', $res->signature, $account3->address); 62 | var_dump($state ? "OK": "ERROR"); 63 | 64 | //EXIT 65 | exit(0); 66 | -------------------------------------------------------------------------------- /Example/example.batch.php: -------------------------------------------------------------------------------- 1 | chainId = '0x3';//ropsten 39 | $sweb3->setPersonalData(SWP_ADDRESS, SWP_PRIVATE_KEY); 40 | 41 | //enable batching 42 | $sweb3->batch(true); 43 | 44 | //we need the nonce for signing the send eth transaction 45 | $sweb3->call('eth_gasPrice'); 46 | $sweb3->call('eth_getTransactionCount', [$sweb3->personal->address], 'pending'); 47 | $res = $sweb3->executeBatch(); 48 | 49 | PrintCallResult('Gas price & nonce:', $res); 50 | 51 | $gasPrice = $sweb3->utils->hexToBn($res[0]->result); 52 | $nonce = $sweb3->utils->hexToBn($res[1]->result); 53 | 54 | 55 | //CALL 56 | 57 | //general ethereum block information 58 | $sweb3->call('eth_blockNumber', []); 59 | 60 | //contract: initialize contract from address and ABI string 61 | $contract = new SWeb3_contract($sweb3, SWP_Contract_Address, SWP_Contract_ABI); 62 | 63 | //contract: direct public variable 64 | $contract->call('autoinc_tuple_a'); 65 | 66 | //contract: input string[][] returns tuple[][] 67 | $contract->call('Mirror_StringArray', [['text1', 'text22'], ['text333', 'text4444'], ['text55555', 'text666666']]); 68 | 69 | 70 | //SEND 71 | //send 0.001 eth 72 | $sendParams = [ 73 | 'from' => $sweb3->personal->address, 74 | 'to' => '0x3Fc47d792BD1B0f423B0e850F4E2AD172d408447', 75 | 'gasPrice' => $gasPrice, 76 | 'gasLimit' => 21000, //good estimation for eth transaction only 77 | 'nonce' => $nonce, 78 | 'value' => $sweb3->utils->toWei('0.001', 'ether') 79 | ]; 80 | //$sweb3->send($sendParams); 81 | 82 | 83 | //EXECUTE 84 | 85 | //execute all batched calls 86 | $res = $sweb3->executeBatch(); 87 | 88 | PrintCallResult('Batched calls:', $res); 89 | 90 | 91 | PrintCallResult('contract => autoinc_tuple_a', $contract->DecodeData('autoinc_tuple_a', $res[1]->result)); 92 | PrintCallResult('contract => Mirror_StringArray', $contract->DecodeData('Mirror_StringArray', $res[2]->result)); 93 | 94 | exit(0); 95 | 96 | 97 | 98 | 99 | 100 | 101 | function PrintCallResult($callName, $result) 102 | { 103 | echo "
Call -> ". $callName . "
"; 104 | 105 | if(is_array($result)) 106 | { 107 | foreach($result as $key => $part) 108 | { 109 | echo "Part [" . $key . "]-> ". PrintObject($part) . "
"; 110 | } 111 | } 112 | else { 113 | echo "Result -> ". PrintObject($result) . "
"; 114 | } 115 | 116 | } 117 | 118 | 119 | 120 | function PrintObject($x, $tabs = 0) 121 | { 122 | if ($x instanceof BigNumber) 123 | { 124 | return $x; 125 | } 126 | 127 | if (is_object($x)) { 128 | $x = (array)($x); 129 | } 130 | 131 | if (is_array($x)) 132 | { 133 | $text = "["; 134 | $first = true; 135 | foreach($x as $key => $value) 136 | { 137 | if ($first) $first = false; 138 | else $text .= ", "; 139 | 140 | $text .= '
' . str_pad("", $tabs * 24, " ") . $key . " : " . PrintObject($value, $tabs + 1); 141 | } 142 | 143 | return $text . '
' . str_pad("", ($tabs - 1) * 24, " ") . "]"; 144 | } 145 | 146 | return $x . ''; 147 | } -------------------------------------------------------------------------------- /Example/example.call.php: -------------------------------------------------------------------------------- 1 | setPersonalData(SWP_ADDRESS, ''); 38 | 39 | //general ethereum block information 40 | $res = $sweb3->call('eth_blockNumber', []); 41 | PrintCallResult('eth_blockNumber', $res); 42 | 43 | //CONTRACT 44 | 45 | //initialize contract from address and ABI string 46 | $contract = new SWeb3_contract($sweb3, SWP_Contract_Address, SWP_Contract_ABI); 47 | 48 | //GETTERS - direct public variables 49 | $res = $contract->call('autoinc_tuple_a'); 50 | PrintCallResult('autoinc_tuple_a', $res); 51 | 52 | $res = $contract->call('public_uint'); 53 | PrintCallResult('public_uint', $res); 54 | 55 | $res = $contract->call('public_int'); 56 | PrintCallResult('public_int', $res); 57 | 58 | $res = $contract->call('public_string'); 59 | PrintCallResult('public_string', $res); 60 | 61 | 62 | //GETTERS 63 | 64 | //returns tuple[] 65 | $res = $contract->call('GetAllTuples_B'); 66 | PrintCallResult('GetAllTuples_B', $res); 67 | 68 | //input uint 69 | //returns tuple 70 | $res = $contract->call('GetTuple_A', [3]); 71 | PrintCallResult('GetTuple_A', $res); 72 | 73 | //input tuple 74 | //returns bool 75 | $callData = [new stdClass()]; 76 | //tuple(uint256, string, string[]) 77 | $callData[0]->uint_b = 3; 78 | $callData[0]->string_b = 'text3'; 79 | $callData[0]->string_array_b = ['text3', 'text3']; 80 | $res = $contract->call('ExistsTuple_B', $callData); 81 | PrintCallResult('ExistsTuple_B', $res); 82 | 83 | //input string[] 84 | //returns tuple[] 85 | $res = $contract->call('GetTuples_B', [['text1', 'text2']]); // *info_bit* 86 | PrintCallResult('GetTuples_B', $res); 87 | 88 | // *info_bit* 89 | //ideally all $callData in $contract->call should be wrapped in an array. 90 | //However we added some "security" to add the wrapping array before encoding the data 91 | 92 | //input string[][] 93 | //returns tuple[][] 94 | $res = $contract->call('Mirror_StringArray', [['text1', 'text22'], ['text333', 'text4444'], ['text55555', 'text666666']]); 95 | PrintCallResult('Mirror_StringArray', $res); 96 | 97 | 98 | //EVENTS 99 | $res = $contract->getLogs(); 100 | PrintCallResult('getLogs', $res); 101 | 102 | 103 | //EXIT 104 | exit(0); 105 | 106 | 107 | 108 | function PrintCallResult($callName, $result) 109 | { 110 | echo "
Call -> ". $callName . "
"; 111 | 112 | echo "Result -> " . PrintObject($result) . "
"; 113 | } 114 | 115 | function PrintObject($x, $tabs = 0) 116 | { 117 | if ($x instanceof BigNumber) 118 | { 119 | return $x; 120 | } 121 | 122 | if (is_object($x)) { 123 | $x = (array)($x); 124 | } 125 | 126 | if (is_array($x)) 127 | { 128 | $text = "["; 129 | $first = true; 130 | foreach($x as $key => $value) 131 | { 132 | if ($first) $first = false; 133 | else $text .= ", "; 134 | 135 | $text .= '
' . str_pad("", $tabs * 24, " ") . $key . " : " . PrintObject($value, $tabs + 1); 136 | } 137 | 138 | return $text . '
' . str_pad("", ($tabs - 1) * 24, " ") . "]"; 139 | } 140 | 141 | return $x . ''; 142 | } -------------------------------------------------------------------------------- /Example/example.config.php: -------------------------------------------------------------------------------- 1 | chainId = '0x3';//ropsten 39 | //set personal data for dending & signing 40 | $sweb3->setPersonalData(SWP_ADDRESS, SWP_PRIVATE_KEY); 41 | //refresh nonce 42 | $nonce = $sweb3->personal->getNonce(); 43 | 44 | //initialize contract with empty address and contract abi 45 | //contract used available at Examples/swp_contract_create.sol 46 | $creation_abi = '[{"inputs":[{"internalType":"uint256","name":"val","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"example_int","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]'; 47 | $contract = new SWeb3_contract($sweb3, '', $creation_abi); 48 | 49 | //set contract bytecode data 50 | $contract_bytecode = '608060405234801561001057600080fd5b5060405161016838038061016883398181016040528101906100329190610054565b80600081905550506100a7565b60008151905061004e81610090565b92915050565b60006020828403121561006a5761006961008b565b5b60006100788482850161003f565b91505092915050565b6000819050919050565b600080fd5b61009981610081565b81146100a457600080fd5b50565b60b3806100b56000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80633b5cbf1114602d575b600080fd5b60336047565b604051603e9190605a565b60405180910390f35b60005481565b6054816073565b82525050565b6000602082019050606d6000830184604d565b92915050565b600081905091905056fea2646970667358221220171f0c6da393c2e496a8253a903e831cfe3e35c23a55ab06552484e7f3f84ec664736f6c63430008070033'; 51 | $contract->setBytecode($contract_bytecode); 52 | 53 | //call contract deployment (note the array wrapping in the constructor params)! 54 | //if you don't provide explicit gas price, the system will update current gas price from the net (call) 55 | $extra_params = [ 56 | 'from' => SWP_ADDRESS, 57 | 'nonce' => $nonce 58 | ]; 59 | $result = $contract->deployContract( [123123], $extra_params); 60 | 61 | //check the result 62 | if(isset($result->result)) 63 | { 64 | echo 'Transaction succesfully sent: ' . $result->result . '
'; 65 | 66 | $newAddress = ''; 67 | //get the contract deployment transaction hash and wait untill it's finally confirmed 68 | do { 69 | echo 'Waiting 5 seconds...
'; 70 | sleep(5); 71 | $checkContract = $sweb3->call('eth_getTransactionReceipt', [$result->result]); 72 | if(isset($checkContract->result->contractAddress)) $newAddress = $checkContract->result->contractAddress; 73 | 74 | } while ($newAddress == ''); 75 | 76 | echo 'Contract created at: ' . $newAddress . '
'; 77 | 78 | //get a contract on the recently created address and query the value that we inserted 79 | $contract = new SWeb3_contract($sweb3, $newAddress, $creation_abi); 80 | $resultContract = $contract->call('example_int'); 81 | 82 | echo 'Contract value check:
'; 83 | echo json_encode($resultContract); 84 | } 85 | else 86 | { 87 | echo 'Transaction error
'; 88 | echo json_encode($result); 89 | } -------------------------------------------------------------------------------- /Example/example.erc20.php: -------------------------------------------------------------------------------- 1 | chainId = '0x3';//ropsten 38 | 39 | 40 | //GENERAL CONTRACT CONFIGURATION 41 | $config = new stdClass(); 42 | $config->personalAdress = "0xaaa...aaa"; 43 | $config->personalPrivateKey = "... [private key] ..."; 44 | $config->erc20Address = "0x123...123"; 45 | $config->erc20ABI = '[contract json ABI]'; 46 | $config->transferToAddress = "0xbbb...bbb"; 47 | 48 | 49 | //SET MY PERSONAL DATA 50 | $sweb3->setPersonalData($config->personalAdress, $config->personalPrivateKey); 51 | 52 | 53 | //CONTRACT 54 | //initialize contract from address and ABI string 55 | $contract = new SWeb3_contract($sweb3, $config->erc20Address, $config->erc20ABI); 56 | 57 | 58 | //QUERY BALANCE OF ADDRESS 59 | $res = $contract->call('balanceOf', [$config->personalAdress]); 60 | PrintCallResult('balanceOf Sender', $res); 61 | 62 | $res = $contract->call('balanceOf', [$config->transferToAddress]); 63 | PrintCallResult('balanceOf Receiver', $res); 64 | 65 | 66 | 67 | /// WARNING: AFTER THIS LINE CODE CAN SPEND ETHER AS SENDING TOKENS IS A SIGNED TRANSACTION (STATE CHANGE) 68 | //COMMENT THIS LINE BELOW TO ENABLE THE EXAMPLE 69 | 70 | exit; 71 | 72 | /// WARNING: END 73 | 74 | 75 | 76 | //SEND TOKENS FROM ME TO OTHER ADDRESS 77 | 78 | //nonce depends on the sender/signing address. it's the number of transactions made by this address, and can be used to override older transactions 79 | //it's used as a counter/queue 80 | //get nonce gives you the "desired next number" (makes a query to the provider), but you can setup more complex & efficient nonce handling ... at your own risk ;) 81 | $extra_data = [ 'nonce' => $sweb3->personal->getNonce() ]; 82 | 83 | //be carefull here. This contract has 18 decimal like ethers. So 1 token is 10^18 weis. 84 | $value = Utils::toWei('1', 'ether'); 85 | 86 | //$contract->send always populates: gasPrice, gasLimit, IF AND ONLY IF they are not already defined in $extra_data 87 | //$contract->send always populates: to (contract address), from (sweb3->personal->address), data (ABI encoded $sendData), these can NOT be defined from outside 88 | $result = $contract->send('transfer', [$config->transferToAddress, $value], $extra_data); 89 | 90 | PrintCallResult('transfer: ' . time(), $result); 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | function PrintCallResult($callName, $result) 99 | { 100 | echo "
ERC20 Token -> ". $callName . "
"; 101 | 102 | echo "Result -> " . PrintObject($result) . "
"; 103 | } 104 | 105 | 106 | function PrintObject($x, $tabs = 0) 107 | { 108 | if ($x instanceof BigNumber) 109 | { 110 | return $x; 111 | } 112 | 113 | if (is_object($x)) { 114 | $x = (array)($x); 115 | } 116 | 117 | if (is_array($x)) 118 | { 119 | $text = "["; 120 | $first = true; 121 | foreach($x as $key => $value) 122 | { 123 | if ($first) $first = false; 124 | else $text .= ", "; 125 | 126 | $text .= '
' . str_pad("", $tabs * 24, " ") . $key . " : " . PrintObject($value, $tabs + 1); 127 | } 128 | 129 | return $text . '
' . str_pad("", ($tabs - 1) * 24, " ") . "]"; 130 | } 131 | 132 | return $x . ''; 133 | } -------------------------------------------------------------------------------- /Example/example.php: -------------------------------------------------------------------------------- 1 | uint_b = 3; 121 | $callData[0]->string_b = 'text3'; 122 | $callData[0]->string_array_b = ['text3', 'text3']; 123 | return Ethereum_FContractCall(SWP_Contract_Address, SWP_Contract_ABI, 'ExistsTuple_B', $callData); 124 | } 125 | 126 | function SWP_GetTuples_B() 127 | { 128 | $callData = [['text1', 'text2']]; //string[] 129 | return Ethereum_FContractCall(SWP_Contract_Address, SWP_Contract_ABI, 'GetTuples_B', $callData); 130 | } 131 | 132 | 133 | function PrintCallResult($callName, $result) 134 | { 135 | echo "
Call -> ". $callName . "
"; 136 | echo "Result -> ". json_encode($result) . "
"; 137 | } -------------------------------------------------------------------------------- /Example/example.send.php: -------------------------------------------------------------------------------- 1 | chainId = '0x3';//ropsten 40 | 41 | $sweb3->setPersonalData(SWP_ADDRESS, SWP_PRIVATE_KEY); 42 | 43 | 44 | //GENERAL OPERATIONS 45 | 46 | //ALERT!! uncomment all functions you want to execute. mind that every call will make a state changing transaction to the selected net. 47 | 48 | //SendETH(); 49 | 50 | 51 | //CONTRACT 52 | //initialize contract from address and ABI string 53 | $contract = new SWeb3_contract($sweb3, SWP_Contract_Address, SWP_Contract_ABI); 54 | 55 | //ALERT!! uncomment all functions you want to execute. mind that every call will make a state changing transaction to the selected net. 56 | 57 | //Contract_Set_public_int(); 58 | //Contract_AddTupleA(); 59 | //Contract_AddTupleA_Params(); 60 | //AddTuple_B(); 61 | 62 | 63 | function SendETH() 64 | { 65 | global $sweb3; 66 | //send 0.001 eth to 0x3Fc47d792BD1B0f423B0e850F4E2AD172d408447 67 | 68 | //estimate gas cost 69 | //if you don't provide explicit gas price, the system will update current gas price from the net (call) 70 | $sendParams = [ 71 | 'from' => $sweb3->personal->address, 72 | 'to' => '0x3Fc47d792BD1B0f423B0e850F4E2AD172d408447', 73 | 'value' => $sweb3->utils->toWei('0.001', 'ether') 74 | ]; 75 | 76 | //get function estimateGas 77 | $gasEstimateResult = $sweb3->call('eth_estimateGas', [$sendParams]); 78 | 79 | if(!isset($gasEstimateResult->result)) 80 | throw new Exception('estimation error: ' . json_encode($gasEstimateResult)); 81 | 82 | $gasEstimate = $sweb3->utils->hexToBn($gasEstimateResult->result); 83 | 84 | //prepare sending 85 | $sendParams['nonce'] = $sweb3->personal->getNonce(); 86 | $sendParams['gasLimit'] = $gasEstimate; 87 | 88 | $result = $sweb3->send($sendParams); 89 | PrintCallResult('SendETH', $result); 90 | } 91 | 92 | 93 | function Contract_Set_public_uint() 94 | { 95 | global $sweb3, $contract; 96 | 97 | //nonce depends on the sender/signing address. it's the number of transactions made by this address, and can be used to override older transactions 98 | //it's used as a counter/queue 99 | //get nonce gives you the "desired next number" (makes a query to the provider), but you can setup more complex & efficient nonce handling ... at your own risk ;) 100 | $extra_data = ['nonce' => $sweb3->personal->getNonce()]; 101 | 102 | //$contract->send always populates: gasPrice, gasLimit, IF AND ONLY IF they are not already defined in $extra_data 103 | //$contract->send always populates: to (contract address), data (ABI encoded $sendData), these can NOT be defined from outside 104 | $result = $contract->send('Set_public_uint', time(), $extra_data); 105 | 106 | PrintCallResult('Contract_Set_public_uint: ' . time(), $result); 107 | } 108 | 109 | 110 | function Contract_Set_public_int() 111 | { 112 | global $sweb3, $contract; 113 | 114 | //nonce depends on the sender/signing address. it's the number of transactions made by this address, and can be used to override older transactions 115 | //it's used as a counter/queue 116 | //get nonce gives you the "desired next number" (makes a query to the provider), but you can setup more complex & efficient nonce handling ... at your own risk ;) 117 | $extra_data = ['nonce' => $sweb3->personal->getNonce()]; 118 | 119 | //$contract->send always populates: gasPrice, gasLimit, IF AND ONLY IF they are not already defined in $extra_data 120 | //$contract->send always populates: to (contract address), data (ABI encoded $sendData), these can NOT be defined from outside 121 | 122 | //the input data formatter accepts numbers and BigNumbers. Just mind that numbers might result in incorrect results for their limited max value. 123 | $raw_value = -time(); 124 | $format_value = Utils::toBn($raw_value); 125 | 126 | $result = $contract->send('Set_public_int', $format_value, $extra_data); 127 | //also valid: $result = $contract->send('Set_public_int', $raw_value, $extra_data); 128 | 129 | PrintCallResult('Contract_Set_public_int: ' . $raw_value, $result); 130 | } 131 | 132 | 133 | function Contract_AddTupleA() 134 | { 135 | global $sweb3, $contract; 136 | 137 | $send_data = new stdClass(); 138 | $send_data->uint_a = time(); 139 | $send_data->boolean_a = true; 140 | $send_data->address_a = SWP_ADDRESS; 141 | $send_data->bytes_a = 'Dynamic inserted tuple with SWP with tuple'; 142 | 143 | $extra_data = ['nonce' => $sweb3->personal->getNonce()]; 144 | $result = $contract->send('AddTupleA', $send_data, $extra_data); 145 | 146 | PrintCallResult('Contract_AddTupleA: ' . time(), $result); 147 | } 148 | 149 | function Contract_AddTupleA_Params() 150 | { 151 | global $sweb3, $contract; 152 | 153 | $send_data = []; 154 | $send_data['uint_a'] = time(); 155 | $send_data['boolean_a'] = true; 156 | $send_data['address_a'] = $sweb3->personal->personal->address; 157 | $send_data['bytes_a'] = 'Dynamic inserted tuple with SWP by params'; 158 | 159 | $extra_data = ['nonce' => $sweb3->personal->getNonce()]; 160 | $result = $contract->send('AddTupleA_Params', $send_data, $extra_data); 161 | 162 | PrintCallResult('Contract_AddTupleA_Params: ' . time(), $result); 163 | } 164 | 165 | 166 | function AddTuple_B() 167 | { 168 | global $sweb3, $contract; 169 | 170 | $send_data = new stdClass(); 171 | $send_data->uint_b = time(); 172 | $send_data->string_b = 'Dynamic inserted tuple with SWP with tuple'; 173 | $send_data->string_array_b = ['Dynamic', 'inserted', 'tuple', 'with', 'SWP', 'with', 'tuple']; 174 | 175 | $extra_data = ['nonce' => $sweb3->personal->getNonce()]; 176 | $result = $contract->send('AddTuple_B', $send_data, $extra_data); 177 | 178 | PrintCallResult('AddTuple_B: ' . time(), $result); 179 | } 180 | 181 | 182 | 183 | function PrintCallResult($callName, $result) 184 | { 185 | echo "
Send -> ". $callName . "
"; 186 | 187 | echo "Result -> " . PrintObject($result) . "
"; 188 | } 189 | 190 | 191 | function PrintObject($x, $tabs = 0) 192 | { 193 | if ($x instanceof BigNumber) 194 | { 195 | return $x; 196 | } 197 | 198 | if (is_object($x)) { 199 | $x = (array)($x); 200 | } 201 | 202 | if (is_array($x)) 203 | { 204 | $text = "["; 205 | $first = true; 206 | foreach($x as $key => $value) 207 | { 208 | if ($first) $first = false; 209 | else $text .= ", "; 210 | 211 | $text .= '
' . str_pad("", $tabs * 24, " ") . $key . " : " . PrintObject($value, $tabs + 1); 212 | } 213 | 214 | return $text . '
' . str_pad("", ($tabs - 1) * 24, " ") . "]"; 215 | } 216 | 217 | return $x . ''; 218 | } -------------------------------------------------------------------------------- /Example/swp_contract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity >=0.7.0 <0.9.0; 4 | 5 | /** 6 | * @title Owner 7 | * @dev Set & change owner 8 | */ 9 | contract SWP_contract 10 | { 11 | uint256 public autoinc_tuple_a; 12 | uint256 public public_uint; 13 | int256 public public_int; 14 | string public public_string; 15 | 16 | 17 | struct Tuple_A { 18 | uint uint_a; 19 | bool boolean_a; 20 | address address_a; 21 | bytes bytes_a; 22 | } 23 | 24 | struct Tuple_B { 25 | uint uint_b; 26 | string string_b; 27 | string[] string_array_b; 28 | } 29 | 30 | mapping(uint => Tuple_A) public map_tuples_a; 31 | Tuple_B[] private array_tuples_b; 32 | 33 | 34 | event Event_Set_public_uint(uint newValue); 35 | event Event_AddTuple_A(Tuple_A newObject); 36 | event Event_AddTuple_B(Tuple_B newObject); 37 | 38 | 39 | //CONSTRUCTOR 40 | constructor() 41 | { 42 | Set_public_uint(12342535644675); 43 | Set_public_int(-12342535644675); 44 | Set_public_string("Welcome to Simple Web3 PHP!"); 45 | 46 | 47 | AddTupleA_Params(111, true, 0x0729377BeCbd653b9F14fBf2956CfD317Bf2afAa, "hello my apple"); 48 | AddTupleA_Params(222, false, 0x0729377becbd653b9F14fBf2956cfd317bf2Afbb, "hello my banana"); 49 | AddTupleA_Params(333, true, 0x0729377becBD653b9f14fBf2956cFD317BF2aFcc, "hello my coconut"); 50 | AddTupleA_Params(444, false, 0x0729377becBd653b9f14fBF2956cfD317bF2aFDd, "hello my durian"); 51 | 52 | Tuple_B memory tb = Tuple_B(1, "text1", new string[](0)); 53 | AddTuple_B(tb); 54 | tb = Tuple_B(2, "text2", new string[](0)); 55 | AddTuple_B(tb); 56 | tb = Tuple_B(3, "text3", new string[](0)); 57 | AddTuple_B(tb); 58 | tb = Tuple_B(4, "text4", new string[](0)); 59 | AddTuple_B(tb); 60 | 61 | AddTuple_B_Params(5, "text1", new string[](0)); 62 | AddTuple_B_Params(6, "text1", new string[](0)); 63 | } 64 | 65 | 66 | 67 | //CALL (GETTERS) 68 | function GetAllTuples_B() public view returns (Tuple_B[] memory) 69 | { 70 | return array_tuples_b; 71 | } 72 | 73 | function GetTuple_A(uint id) public view returns (Tuple_A memory) 74 | { 75 | return map_tuples_a[id]; 76 | } 77 | 78 | function ExistsTuple_B(Tuple_B memory t1) public view returns (bool) 79 | { 80 | for(uint i = 0; i < array_tuples_b.length; i++) 81 | { 82 | Tuple_B memory t2 = array_tuples_b[i]; 83 | if (t1.uint_b == t2.uint_b && Compare(t1.string_b, t2.string_b)) { 84 | return true; 85 | } 86 | } 87 | return false; 88 | } 89 | 90 | function GetTuples_B(string[] calldata search) public view returns (Tuple_B[] memory) 91 | { 92 | //no dynamic memory arrays... so max 10 results.. 93 | uint8 maxResults = 10; 94 | uint8 currentResults = 0; 95 | 96 | Tuple_B[] memory hits = new Tuple_B[](maxResults); 97 | uint256 len = search.length; 98 | 99 | for (uint i = 0; i < array_tuples_b.length; i++) 100 | { 101 | Tuple_B memory t2 = array_tuples_b[i]; 102 | for (uint ii = 0; ii < len; ii++) 103 | { 104 | if (Compare(search[ii], t2.string_b)) { 105 | hits[currentResults] = t2; 106 | currentResults++; 107 | break; 108 | } 109 | 110 | } 111 | if(currentResults >= maxResults) break; 112 | } 113 | 114 | return hits; 115 | } 116 | 117 | 118 | //SEND TRANSACTION (SETTERS) 119 | function Set_public_uint(uint new_uint) public 120 | { 121 | public_uint = new_uint; 122 | emit Event_Set_public_uint(public_uint); 123 | } 124 | 125 | function Set_public_int(int new_int) public 126 | { 127 | public_int = new_int; 128 | } 129 | 130 | function Set_public_string(string memory new_string) public 131 | { 132 | public_string = new_string; 133 | } 134 | 135 | 136 | function AddTupleA(Tuple_A memory new_tuple_a) public 137 | { 138 | autoinc_tuple_a++; 139 | map_tuples_a[autoinc_tuple_a] = new_tuple_a; 140 | 141 | emit Event_AddTuple_A(new_tuple_a); 142 | } 143 | 144 | function AddTupleA_Params(uint uint_a, bool boolean_a, address address_a, bytes memory bytes_a) public 145 | { 146 | autoinc_tuple_a++; 147 | Tuple_A storage tuple = map_tuples_a[autoinc_tuple_a]; 148 | tuple.uint_a = uint_a; 149 | tuple.boolean_a = boolean_a; 150 | tuple.address_a = address_a; 151 | tuple.bytes_a = bytes_a; 152 | 153 | emit Event_AddTuple_A(tuple); 154 | } 155 | 156 | 157 | function AddTuple_B(Tuple_B memory new_tuple_b) public 158 | { 159 | array_tuples_b.push(new_tuple_b); 160 | 161 | array_tuples_b[array_tuples_b.length-1].string_array_b.push(new_tuple_b.string_b); 162 | array_tuples_b[array_tuples_b.length-1].string_array_b.push(new_tuple_b.string_b); 163 | 164 | emit Event_AddTuple_B(new_tuple_b); 165 | } 166 | 167 | function AddTuple_B_Params(uint uint_b, string memory string_b, string[] memory string_array_b) public 168 | { 169 | Tuple_B memory tuple; 170 | tuple.uint_b = uint_b; 171 | tuple.string_b = string_b; 172 | tuple.string_array_b = string_array_b; 173 | 174 | array_tuples_b.push(tuple); 175 | 176 | array_tuples_b[array_tuples_b.length-1].string_array_b.push(tuple.string_b); 177 | array_tuples_b[array_tuples_b.length-1].string_array_b.push(tuple.string_b); 178 | array_tuples_b[array_tuples_b.length-1].string_array_b.push(tuple.string_b); 179 | 180 | emit Event_AddTuple_B(tuple); 181 | } 182 | 183 | 184 | function Mirror_StringArray(string[][] memory sa) public pure returns (string[][] memory) 185 | { 186 | return sa; 187 | } 188 | 189 | 190 | //HELPERS 191 | function Compare(string memory a, string memory b) public pure returns (bool) 192 | { 193 | return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)); 194 | } 195 | 196 | 197 | } -------------------------------------------------------------------------------- /Example/swp_contract_create.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity >=0.7.0 <0.9.0; 4 | 5 | /** 6 | * @title Owner 7 | * @dev Set & change owner 8 | */ 9 | contract SWP_contract 10 | { 11 | uint256 public example_int; 12 | 13 | constructor(uint256 val) 14 | { 15 | example_int = val; 16 | } 17 | } -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Kuan-Cheng,Lai 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-web3-php 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/drlecks/simple-web3-php.svg?style=flat-square)](https://packagist.org/packages/drlecks/simple-web3-php) 4 | [![Join the chat at https://gitter.im/drlecks/Simple-Web3-Php](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg)](https://gitter.im/Simple-Web3-Php/community) 5 | [![Licensed under the MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/drlecks/Simple-Web3-Php/blob/master/LICENSE) 6 | 7 | 8 | A php interface for interacting with the Ethereum blockchain and ecosystem. 9 | 10 | 11 | # Features 12 | 13 | - PHP >= 7.4 14 | - Customizable curl calls 15 | - Call: get net state 16 | - Send signed transactions 17 | - Batch call requests and signed transactions 18 | - Address & private key creation 19 | - Message signing 20 | - Full ABIv2 encode/decode 21 | - Contract creation 22 | - Contract interaction (call/send) 23 | - Contract Events/logs with filters 24 | - Support for ERC20 contracts with non-nominative decimal values 25 | - Examples provided interacting with simple types, strings, tuples, arrays, arrays of tuples with arrays, multi-dimension arrays... 26 | - EIP712 Typed structured data hashing 27 | 28 | 29 | # Install 30 | 31 | ### Latest stable release 32 | ``` 33 | composer require drlecks/simple-web3-php "^0.10.0" 34 | ``` 35 | 36 | Or you can add this line in composer.json 37 | 38 | ``` 39 | "drlecks/simple-web3-php": "^0.10.0" 40 | ``` 41 | 42 | 43 | ### Development (main branch) 44 | ``` 45 | composer require drlecks/simple-web3-php dev-master 46 | ``` 47 | 48 | Or you can add this line in composer.json 49 | 50 | ``` 51 | "drlecks/simple-web3-php": "dev-master" 52 | ``` 53 | 54 | 55 | # Usage 56 | 57 | ### New instance 58 | ```php 59 | use SWeb3\SWeb3; 60 | //initialize SWeb3 main object 61 | $sweb3 = new SWeb3('http://ethereum.node.provider:optional.node.port'); 62 | 63 | //optional if not sending transactions 64 | $from_address = '0x0000000000000000000000000000000000000000'; 65 | $from_address_private_key = '345346245645435....'; 66 | $sweb3->setPersonalData($from_address, $from_address_private_key); 67 | ``` 68 | 69 | ### Convert values 70 | - Most calls return hex or BigNumber to represent numbers. 71 | - Most calls expect numeric parameters represented as BigNumbers. 72 | 73 | Hex to Big Number: 74 | ```php 75 | use SWeb3\Utils; 76 | 77 | $res = $sweb3->call('eth_blockNumber', []); 78 | $bigNum = Utils::hexToBn($res->result); 79 | ``` 80 | 81 | Number to BigNumber: 82 | ```php 83 | $bigNum = Utils::ToBn(123); 84 | ``` 85 | 86 | Get average-human readable string representation from Big Number: 87 | ```php 88 | $s_number = $bigNum->toString(); 89 | ``` 90 | 91 | Format 1 ether to wei (unit required for ether values in transactions): 92 | ```php 93 | Utils::toWei('0.001', 'ether'); 94 | ``` 95 | 96 | Get average-human readable string representation from a value conversion: 97 | ```php 98 | $s_val = Utils::fromWeiToString('1001', 'kwei'); // "1.001" 99 | 100 | $s_val = Utils::toWeiString('1.001', 'kwei'); // "1001" 101 | ``` 102 | 103 | ### ABI Encoding 104 | Manually encode parameters: 105 | ```php 106 | $abiEncoded = ABI::EncodeParameters_External(['address', 'uint256'], [$userAddress, 1]); 107 | ``` 108 | 109 | Keccak 256 hash: 110 | ```php 111 | $hash = Utils::sha3($abiEncoded); 112 | ``` 113 | 114 | ### General ethereum block information call: 115 | ```php 116 | $res = $sweb3->call('eth_blockNumber', []); 117 | ``` 118 | 119 | ### Refresh gas price 120 | ```php 121 | $gasPrice = $sweb3->getGasPrice(); 122 | ``` 123 | 124 | ### Estimate gas price (from send params) 125 | ```php 126 | $sweb3->chainId = '0x3';//ropsten 127 | $sendParams = [ 128 | 'from' => $sweb3->personal->address, 129 | 'to' => '0x1111111111111111111111111111111111111111', 130 | 'value' => Utils::toWei('0.001', 'ether'), 131 | 'nonce' => $sweb3->personal->getNonce() 132 | ]; 133 | $gasEstimateResult = $sweb3->call('eth_estimateGas', [$sendParams]); 134 | ``` 135 | 136 | ### Send 0.001 ether to address 137 | ```php 138 | //remember to set personal data first with a valid pair of address & private key 139 | $sendParams = [ 140 | 'from' => $sweb3->personal->address, 141 | 'to' => '0x1111111111111111111111111111111111111111', 142 | 'gasLimit' => 210000, 143 | 'value' => Utils::toWei('0.001', 'ether'), 144 | 'nonce' => $sweb3->personal->getNonce() 145 | ]; 146 | $result = $sweb3->send($sendParams); 147 | ``` 148 | 149 | ### Batch calls & transactions 150 | ```php 151 | //enable batching 152 | $sweb3->batch(true); 153 | 154 | $sweb3->call('eth_blockNumber', []); 155 | $sweb3->call('eth_getBalance', [$sweb3->personal->address], 'latest'); 156 | 157 | //execute all batched calls in one request 158 | $res = $sweb3->executeBatch(); 159 | 160 | //batching has to be manually disabled 161 | $sweb3->batch(false); 162 | ``` 163 | 164 | ### Account 165 | ```php 166 | use SWeb3\Accounts; 167 | use SWeb3\Account; 168 | 169 | //create new account privateKey/address (returns Account) 170 | $account = Accounts::create(); 171 | 172 | //retrieve account (address) from private key 173 | $account2 = Accounts::privateKeyToAccount('...private_key...'); 174 | 175 | //sign message with account 176 | $res = $account2->sign('Some data'); 177 | 178 | ``` 179 | 180 | 181 | ### Contract interaction 182 | 183 | ```php 184 | use SWeb3\SWeb3_Contract; 185 | 186 | $contract = new SWeb3_contract($sweb3, '0x2222222222222222222222222222222222222222', '[ABI...]'); //'0x2222...' is contract address 187 | 188 | // call contract function 189 | $res = $contract->call('autoinc_tuple_a'); 190 | 191 | // change function state 192 | //remember to set the sign values and chain id first: $sweb3->setPersonalData() & $sweb3->chainId 193 | $extra_data = ['nonce' => $sweb3->personal->getNonce()]; 194 | $result = $contract->send('Set_public_uint', 123, $extra_data); 195 | ``` 196 | 197 | ### Contract events (logs) 198 | 199 | ```php 200 | //optional parameters fromBlock, toBlock, topics 201 | //default values -> '0x0', 'latest', null (all events/logs from this contract) 202 | $res = $contract->getLogs(); 203 | ``` 204 | 205 | ### Contract creation (deployment) 206 | 207 | ```php 208 | use SWeb3\SWeb3_Contract; 209 | 210 | $creation_abi = '[abi...]'; 211 | $contract = new SWeb3_contract($sweb3, '', $creation_abi); 212 | 213 | //set contract bytecode data 214 | $contract_bytecode = '123456789....'; 215 | $contract->setBytecode($contract_bytecode); 216 | 217 | //remember to set the sign values and chain id first: $sweb3->setPersonalData() & $sweb3->chainId 218 | $extra_params = ['nonce' => $sweb3->personal->getNonce()]; 219 | $result = $contract->deployContract( [123123], $extra_params); 220 | ``` 221 | 222 | 223 | ### Usual required includes 224 | 225 | ```php 226 | use SWeb3\SWeb3; //always needed, to create the Web3 object 227 | use SWeb3\Utils; //sweb3 helper classes (for example, hex conversion operations) 228 | use SWeb3\SWeb3_Contract; //contract creation and interaction 229 | use SWeb3\Accounts; //account creation 230 | use SWeb3\Account; //single account management (signing) 231 | use phpseclib3\Math\BigInteger as BigNumber; //BigInt handling 232 | use stdClass; //for object interaction 233 | ``` 234 | 235 | # Provided Examples 236 | 237 | In the folder Examples/ there are some extended examples with call & send examples: 238 | 239 | - example.call.php 240 | - example.send.php 241 | - example.batch.php 242 | - example.account.php 243 | - example.contract_creation.php 244 | - example.erc20.php 245 | 246 | ### Example configuration 247 | 248 | To execute the examples you will need to add some data to the configuration file (example.config.php). 249 | 250 | The example is pre-configured to work with an infura endpoint: 251 | 252 | ```php 253 | define('INFURA_PROJECT_ID', 'XXXXXXXXX'); 254 | define('INFURA_PROJECT_SECRET', 'XXXXXXXXX'); 255 | define('ETHEREUM_NET_NAME', 'ropsten'); //ropsten , mainnet 256 | 257 | define('ETHEREUM_NET_ENDPOINT', 'https://'.ETHEREUM_NET_NAME.'.infura.io/v3/'.INFURA_PROJECT_ID); 258 | ``` 259 | Just add your infura project keys. If you have not configured the api secret key requisite, just ignore it. 260 | 261 | If you are using a private endpoint, just ignore all the infura definitions: 262 | 263 | ```php 264 | define('ETHEREUM_NET_ENDPOINT', 'https://123.123.40.123:1234'); 265 | ``` 266 | 267 | To enable contract interaction, set the contract data (address & ABI). The file is preconfigured to work with our example contract, already deployed on ropsten. 268 | ```php 269 | //swp_contract.sol is available on ropsten test net, address: 0x706986eEe8da42d9d85997b343834B38e34dc000 270 | define('SWP_Contract_Address','0x706986eEe8da42d9d85997b343834B38e34dc000'); 271 | $SWP_Contract_ABI = '[...]'; 272 | define('SWP_Contract_ABI', $SWP_Contract_ABI); 273 | ``` 274 | 275 | To enable transaction sending & signing, enter a valid pair of address and private key. Please take this advises before continuing: 276 | - The address must be active on the ropsten network and have some ether available to send the transactions. 277 | - Double check that you are using a test endpoint, otherwise you will be spending real eth to send the transactions 278 | - Be sure to keep your private key secret! 279 | 280 | ```php 281 | //SIGNING 282 | define('SWP_ADDRESS', 'XXXXXXXXXX'); 283 | define('SWP_PRIVATE_KEY', 'XXXXXXXXXX'); 284 | ``` 285 | 286 | ### Example contract 287 | 288 | The solidity contract used in this example is available too in the same folder: swp_contract.sol 289 | 290 | ### Example disclaimer 291 | 292 | Don't base your code structure on this example. This example does not represent clean / efficient / performant aproach to implement them in a production environment. It's only aim is to show some of the features of Simple Web3 Php. 293 | 294 | 295 | # Modules 296 | 297 | - Utils library forked & extended from web3p/web3.php 298 | - Transaction signing: kornrunner/ethereum-offline-raw-tx 299 | - sha3 encoding: from kornrunner/keccak 300 | - BigNumber interaction: phpseclib3\Math 301 | - Asymetric key handling: simplito/elliptic-php 302 | 303 | 304 | # TODO 305 | 306 | - Node accounts creation / interaction 307 | 308 | 309 | # License 310 | MIT 311 | 312 | 313 | # DONATIONS (ETH) 314 | 315 | ``` 316 | 0x4a890A7AFB7B1a4d49550FA81D5cdca09DC8606b 317 | ``` 318 | -------------------------------------------------------------------------------- /Test/contract_test_mirror_tuple.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity >=0.7.0 <0.9.0; 4 | 5 | contract contract_test_mirror_tuple 6 | { 7 | struct Tuple_A { 8 | uint uint_a; 9 | bool boolean_a; 10 | } 11 | 12 | struct Tuple_B { 13 | string string_b1; 14 | string string_b2; 15 | } 16 | 17 | struct Tuple_C { 18 | uint uint_c; 19 | string string_c; 20 | } 21 | 22 | function Mirror_TupleA(Tuple_A memory t) public pure returns (Tuple_A memory) 23 | { 24 | return t; 25 | } 26 | 27 | function Mirror_TupleB(Tuple_B memory t) public pure returns (Tuple_B memory) 28 | { 29 | return t; 30 | } 31 | 32 | function Mirror_TupleC(Tuple_C memory t) public pure returns (Tuple_C memory) 33 | { 34 | return t; 35 | } 36 | 37 | 38 | function Mirror_TupleArray(Tuple_C[] memory t) public pure returns (Tuple_C[] memory) 39 | { 40 | return t; 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /Test/inc/inc.abitest.php: -------------------------------------------------------------------------------- 1 | $v) { 14 | if ($v['type'] != 'function') 15 | continue; 16 | if ($filter_function !== false && strcasecmp($filter_function, $v['name']) != 0) 17 | continue; 18 | 19 | $v['outputs'] = $v['inputs']; 20 | $tmp[$v['name']] = $v; 21 | } 22 | return json_encode(array_values($tmp)); 23 | } 24 | 25 | static function showMessageByBlocks($msg, $title = "") 26 | { 27 | $title = "--{$title}"; 28 | if (strlen($title) < 30) { 29 | $title .= str_repeat('-', 30-strlen($title)); 30 | } 31 | 32 | $d = $msg; 33 | echo "
";
 34 | 		echo "\n{$title}\n";
 35 | 		$d = substr($d, 10);
 36 | 		$d = str_split($d, 64);
 37 | 		foreach ($d as $k => $v) {
 38 | 			
 39 | 			$addr = hexdec("0x{$v}");
 40 | 			$addr /= 32;
 41 | 			
 42 | 			if ($k < 10)
 43 | 				$k = "0{$k}";
 44 | 			echo "{$k}. {$v} {$addr}\n";
 45 | 		}
 46 | 		echo "\n
"; 47 | } 48 | 49 | static function trimFunctionName($msg) { 50 | if (stripos($msg, '0x') === 0) 51 | $msg = substr($msg, 2); 52 | return '0x'.substr($msg, 8); 53 | } 54 | 55 | static function stripTypeInfo($data) { 56 | foreach ($data as $k => $v) { 57 | if ($v instanceof stdClass) { 58 | $v = get_object_vars($v); 59 | $_t = []; 60 | foreach ($v as $kk=>$vv) 61 | $_t[] = "$vv"; 62 | $tmp[] = $_t; 63 | continue; 64 | } 65 | 66 | if (!is_array($v)) { 67 | $tmp[] = "{$v}"; 68 | continue; 69 | } 70 | 71 | foreach ($v as $kk=>$vv) 72 | $v[$kk] = "$vv"; 73 | $tmp[] = $v; 74 | } 75 | return $tmp; 76 | } 77 | 78 | private $data = []; 79 | function __construct($abi, $function_name, $sample_raw_data) { 80 | $this->data = [$abi, $function_name, $sample_raw_data]; 81 | } 82 | 83 | function getABIInputsCount($abi_fn = false) { 84 | [$abi_swapv3, $abi_fn_] = $this->data; 85 | $abi_swapv3 = json_decode($abi_swapv3, true); 86 | if ($abi_fn === false) 87 | $abi_fn = $abi_fn_; 88 | 89 | foreach ($abi_swapv3 as $v) { 90 | if (strcasecmp($v['type'], 'function') !== 0) 91 | continue; 92 | if (strcasecmp($v['name'], $abi_fn) !== 0) 93 | continue; 94 | return count($v['inputs'] ?? []); 95 | } 96 | return false; 97 | } 98 | 99 | function runTest($limitParams = false) { 100 | [$abi_swapv3, $abi_fn, $raw_data] = $this->data; 101 | $abi_swapv3 = self::abiForDecodingInput($abi_swapv3, $abi_fn); 102 | 103 | $aa = new ABI(); 104 | $aa->Init($abi_swapv3); 105 | $data = $aa->DecodeData($abi_fn, self::trimFunctionName($raw_data)); 106 | 107 | // re-encode, strip type info from data 108 | $data = self::stripTypeInfo($data); 109 | $param_count = $this->getABIInputsCount(); 110 | 111 | $aa = new ABI(); 112 | if ($limitParams !== false) { // maybe we need to test only some parameters, helps with debugging 113 | $abi_swapv3 = json_decode($abi_swapv3, true)[0]; 114 | $tmp = []; 115 | $inputs = $abi_swapv3['inputs']; 116 | foreach ($inputs as $k=>$v) { 117 | if ($k>=$limitParams) 118 | break; 119 | $tmp[] = $v; 120 | } 121 | $abi_swapv3['inputs'] = $tmp; 122 | $abi_swapv3 = json_encode([$abi_swapv3]); 123 | } 124 | $aa->Init($abi_swapv3); 125 | 126 | $reencoded = $aa->EncodeData('swapExactInput', $data); 127 | 128 | echo "
";
129 | 		$check = [];
130 | 		$check[] = substr(sha1($raw_data), 0, 15);
131 | 		$check[] = substr(sha1($reencoded), 0, 15);
132 | 		$chk = " {$abi_fn}";
133 | 		if (strlen($raw_data) != strlen($reencoded) && $limitParams < $param_count) {
134 | 			$chk .= " ({$limitParams} / {$param_count}) ";
135 | 			$chk .= "? Check manually";
136 | 		} else {
137 | 			$chk .= $check[0] == $check[1] ? " ✓ Valid " : " x Invalid";
138 | 		}
139 | 		
140 | 		if ($limitParams === false) {
141 | 			echo "Check: " . implode(" vs ", $check) . ' :' . $chk . "\n";
142 | 			echo "
"; 143 | self::showMessageByBlocks($raw_data, 'orin'); 144 | echo "
"; 145 | self::showMessageByBlocks($reencoded, 'new'); 146 | } else { 147 | self::showMessageByBlocks($reencoded, "new ({$limitParams}/{$param_count})"); 148 | } 149 | 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /Test/inc/inc.wtest.php: -------------------------------------------------------------------------------- 1 | ' . $name . ''; 22 | } 23 | 24 | 25 | private static function printResult(string $name, bool $res) 26 | { 27 | $res_html = '' . ($res ? 'OK': 'FAIL') . ''; 28 | echo '

' . $name . ': ' . $res_html . '

'; 29 | } 30 | 31 | 32 | public static function check(string $name, bool $comp) 33 | { 34 | self::printResult( $name, $comp); 35 | } 36 | 37 | 38 | } -------------------------------------------------------------------------------- /Test/test.core.php: -------------------------------------------------------------------------------- 1 | ' . $name . ''; 22 | } 23 | 24 | 25 | private static function printResult(string $name, bool $res) 26 | { 27 | $res_html = '' . ($res ? 'OK': 'FAIL') . ''; 28 | echo '

' . $name . ': ' . $res_html . '

'; 29 | } 30 | 31 | 32 | public static function check(string $name, bool $comp) 33 | { 34 | self::printResult( $name, $comp); 35 | } 36 | 37 | 38 | } -------------------------------------------------------------------------------- /Test/test.php: -------------------------------------------------------------------------------- 1 | Simple-Web3-PHP Test'; 46 | 47 | 48 | 49 | //ABI 50 | 51 | // 52 | WTest::printTitle('ABI - EncodeParameter_External'); 53 | 54 | $res = ABI::EncodeParameter_External('uint256', '2345675643'); 55 | WTest::check('uint256 (2345675643)', $res == "0x000000000000000000000000000000000000000000000000000000008bd02b7b"); 56 | 57 | $res = ABI::EncodeParameter_External('bytes32', '0xdf3234'); 58 | WTest::check('bytes32 (0xdf3234)', $res == "0xdf32340000000000000000000000000000000000000000000000000000000000"); 59 | 60 | $res = ABI::EncodeParameter_External('bytes', '0xdf3234'); 61 | WTest::check('bytes (0xdf3234)', $res == "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003df32340000000000000000000000000000000000000000000000000000000000"); 62 | 63 | $res = ABI::EncodeParameter_External('address', '0x00112233445566778899'); 64 | WTest::check('address (0x00112233445566778899)', $res == "0x0000000000000000000000000000000000000000000000112233445566778899"); 65 | 66 | $res = ABI::EncodeParameter_External('bytes32[]', ['0xdf3234', '0xfdfd']); 67 | WTest::check('bytes32[] ([\'0xdf3234\', \'0xfdfd\'])', $res == "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002df32340000000000000000000000000000000000000000000000000000000000fdfd000000000000000000000000000000000000000000000000000000000000"); 68 | 69 | 70 | // 71 | WTest::printTitle('ABI - EncodeParameters_External'); 72 | 73 | $res = ABI::EncodeParameters_External(['uint256','string'], ['2345675643', 'Hello!%']); 74 | WTest::check('[\'uint256\',\'string\'] ([\'2345675643\', \'Hello!%\'])', $res == '0x000000000000000000000000000000000000000000000000000000008bd02b7b0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000748656c6c6f212500000000000000000000000000000000000000000000000000'); 75 | 76 | $vals = json_decode('{"x1": true,"x2": ["0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000002"],"x3": 0,"x4": 20}'); 77 | $res = ABI::EncodeParameters_External(["bool","bytes32[2]","uint256","uint8"],array_values((array)$vals)); 78 | WTest::check('bytes32[2] -> fixed length array (fixed bytes)', $res == '0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014'); 79 | 80 | 81 | // 82 | WTest::printTitle('ABI - DecodeParameter_External'); 83 | 84 | $res = ABI::DecodeParameter_External('uint256', '0x0000000000000000000000000000000000000000000000000000000000000010'); 85 | WTest::check('uint256 (16)', $res->toString() == "16"); 86 | 87 | $res = ABI::DecodeParameter_External('string', '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000848656c6c6f212521000000000000000000000000000000000000000000000000'); 88 | WTest::check('string (Hello!%!)', $res == "Hello!%!"); 89 | 90 | $res = ABI::DecodeParameter_External('bytes', '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003df32340000000000000000000000000000000000000000000000000000000000'); 91 | WTest::check('bytes (df3234) ', bin2hex($res) == ("df3234")); 92 | 93 | $res = ABI::DecodeParameter_External('bytes32[]', '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002df32340000000000000000000000000000000000000000000000000000000000fdfd000000000000000000000000000000000000000000000000000000000000'); 94 | $res_format = [bin2hex($res[0]), bin2hex($res[1])]; 95 | WTest::check('bytes32[] ([\'df3234\', \'fdfd\']) ', $res_format == ['df3234','fdfd']); 96 | 97 | 98 | 99 | // 100 | WTest::printTitle('ABI - ENCODE / DECODE'); 101 | 102 | $original = 'sadfsaSAEFAW435456¿?!*_¨:_*'; 103 | $encoded = ABI::EncodeParameter_External('bytes32', $original); 104 | $decoded = ABI::DecodeParameter_External('bytes32', $encoded); 105 | WTest::check('bytes32 (sadfsaSAEFAW435456¿?!*_¨:_*) ', $original == $decoded); 106 | 107 | // 108 | WTest::printTitle('ABI - EncodeGroup'); 109 | 110 | $abi_tuples_raw = '[{"inputs":[{"components":[{"internalType":"uint256","name":"uint_a","type":"uint256"},{"internalType":"bool","name":"boolean_a","type":"bool"}],"internalType":"struct contract_test_mirror_tuple.Tuple_A","name":"t","type":"tuple"}],"name":"Mirror_TupleA","outputs":[{"components":[{"internalType":"uint256","name":"uint_a","type":"uint256"},{"internalType":"bool","name":"boolean_a","type":"bool"}],"internalType":"struct contract_test_mirror_tuple.Tuple_A","name":"","type":"tuple"}],"stateMutability":"pure","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"uint_c","type":"uint256"},{"internalType":"string","name":"string_c","type":"string"}],"internalType":"struct contract_test_mirror_tuple.Tuple_C[]","name":"t","type":"tuple[]"}],"name":"Mirror_TupleArray","outputs":[{"components":[{"internalType":"uint256","name":"uint_c","type":"uint256"},{"internalType":"string","name":"string_c","type":"string"}],"internalType":"struct contract_test_mirror_tuple.Tuple_C[]","name":"","type":"tuple[]"}],"stateMutability":"pure","type":"function"},{"inputs":[{"components":[{"internalType":"string","name":"string_b1","type":"string"},{"internalType":"string","name":"string_b2","type":"string"}],"internalType":"struct contract_test_mirror_tuple.Tuple_B","name":"t","type":"tuple"}],"name":"Mirror_TupleB","outputs":[{"components":[{"internalType":"string","name":"string_b1","type":"string"},{"internalType":"string","name":"string_b2","type":"string"}],"internalType":"struct contract_test_mirror_tuple.Tuple_B","name":"","type":"tuple"}],"stateMutability":"pure","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"uint_c","type":"uint256"},{"internalType":"string","name":"string_c","type":"string"}],"internalType":"struct contract_test_mirror_tuple.Tuple_C","name":"t","type":"tuple"}],"name":"Mirror_TupleC","outputs":[{"components":[{"internalType":"uint256","name":"uint_c","type":"uint256"},{"internalType":"string","name":"string_c","type":"string"}],"internalType":"struct contract_test_mirror_tuple.Tuple_C","name":"","type":"tuple"}],"stateMutability":"pure","type":"function"}]'; 111 | $abi_tuples = new ABI(); 112 | $abi_tuples->Init($abi_tuples_raw); 113 | 114 | $d = new stdClass(); 115 | $d->uint_a = 123; 116 | $d->boolean_a = false; 117 | $res = $abi_tuples->EncodeData('Mirror_TupleA', $d); 118 | WTest::check('Mirror_TupleA (full static)', $res == '0x1cdf9093000000000000000000000000000000000000000000000000000000000000007b0000000000000000000000000000000000000000000000000000000000000000'); 119 | 120 | $d = new stdClass(); 121 | $d->string_b1 = 'aaa'; 122 | $d->string_b2 = 'bbb'; 123 | $res = $abi_tuples->EncodeData('Mirror_TupleB', $d); 124 | WTest::check('Mirror_TupleB (full dynamic)', $res == '0x68116cf20000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003616161000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000036262620000000000000000000000000000000000000000000000000000000000'); 125 | 126 | $d = new stdClass(); 127 | $d->uint_c = 123; 128 | $d->string_c = 'ccc'; 129 | $res = $abi_tuples->EncodeData('Mirror_TupleC', $d); 130 | WTest::check('Mirror_TupleC (static/dynamic mix)', $res == '0x445cf8270000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000007b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000036363630000000000000000000000000000000000000000000000000000000000'); 131 | 132 | $ta1 = new stdClass(); $ta1->uint_c = 111; $ta1->string_c = 'aaa'; 133 | $ta2 = new stdClass(); $ta2->uint_c = 222; $ta2->string_c = 'bbb'; 134 | $res = $abi_tuples->EncodeData('Mirror_TupleArray', [$ta1, $ta2]); 135 | WTest::check('Mirror_TupleArray', $res == '0xf82fd7c900000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000006f00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000003616161000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000036262620000000000000000000000000000000000000000000000000000000000'); 136 | 137 | 138 | WTest::printTitle('ABI - DecodeGroup'); 139 | 140 | 141 | $res = $abi_tuples->DecodeData('Mirror_TupleA', '0x000000000000000000000000000000000000000000000000000000000000007b0000000000000000000000000000000000000000000000000000000000000000'); 142 | WTest::check('Mirror_TupleA (full static)', $res->tuple_1->uint_a->toString() == '123' && !$res->tuple_1->bool_a); 143 | 144 | $res = $abi_tuples->DecodeData('Mirror_TupleB', '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003616161000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000036262620000000000000000000000000000000000000000000000000000000000'); 145 | WTest::check('Mirror_TupleB (full dynamic)', $res->tuple_1->string_b1 == 'aaa' && $res->tuple_1->string_b2 == 'bbb'); 146 | 147 | $res = $abi_tuples->DecodeData('Mirror_TupleC', '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000007b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000036363630000000000000000000000000000000000000000000000000000000000'); 148 | WTest::check('Mirror_TupleC (static/dynamic mix)', $res->tuple_1->uint_c->toString() == '123' && $res->tuple_1->string_c == 'ccc'); 149 | 150 | $res = $abi_tuples->DecodeData('Mirror_TupleArray', '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000006f00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000003616161000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000036262620000000000000000000000000000000000000000000000000000000000'); 151 | WTest::check('Mirror_TupleArray', $res->array_1[0]->uint_c->toString() == '111' && $res->array_1[0]->string_c == 'aaa' && $res->array_1[1]->uint_c->toString() == '222' && $res->array_1[1]->string_c == 'bbb'); 152 | 153 | 154 | //UTILS 155 | 156 | // 157 | WTest::printTitle('Utils - units conversion'); 158 | 159 | $res = Utils::fromWeiToString('1001', 'kwei'); 160 | WTest::check('1001 wei -> kwei (1.001) ', $res == '1.001'); 161 | 162 | $res = Utils::toWeiString('1.001', 'kwei'); 163 | WTest::check('1.001 kwei -> wei (1.001) ', $res == '1001'); 164 | 165 | $res = Utils::toEtherString('100000000000000000', 'wei'); 166 | WTest::check('100000000000000000 wei-> ether (0.1) ', $res == '0.1'); 167 | 168 | 169 | 170 | //ACCOUNT 171 | WTest::printTitle('Accounts'); 172 | 173 | 174 | $res = Accounts::hashMessage("Hello World"); 175 | WTest::check('hashMessage "Hello World"', $res == 'a1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2'); 176 | 177 | $res = Accounts::hashMessage('Some data'); 178 | WTest::check('hashMessage "Some data"', $res == '1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655'); 179 | 180 | $account3 = Accounts::privateKeyToAccount('0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318'); 181 | WTest::check('privateKeyToAccount', $account3->address == '0x2c7536e3605d9c16a7a3d7b1898e529396a65c23'); 182 | 183 | $res_sign = $account3->sign('Some data'); 184 | WTest::check('sign "Some data"', $res_sign->signature == '0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c'); 185 | 186 | $res = Accounts::signedMessageToAddress('Some data', $res_sign->signature); 187 | WTest::check('signedMessageToAddress', $res == $account3->address); 188 | 189 | $res = Accounts::verifySignatureWithAddress('Some data', $res_sign->signature, $account3->address); 190 | WTest::check('verifySignatureWithAddress', $res); 191 | 192 | 193 | //RLP 194 | //https://toolkit.abdk.consulting/ethereum#rlp 195 | 196 | WTest::printTitle('RLP'); 197 | 198 | $rlp = new RLP; 199 | $data = ["0x00112233445566778899", "0xaaaa"]; 200 | $res = $rlp->encode($data); 201 | WTest::check('RLP encode address leading zero', $res == 'ce8a0011223344556677889982aaaa'); 202 | 203 | 204 | //EIP712 205 | WTest::printTitle('EIP712'); 206 | 207 | //https://eips.ethereum.org/EIPS/eip-712 208 | $types = [ 209 | "Message" => [ 210 | (object) [ "name" => "myName1", "type" => "uint256"] , 211 | (object) [ "name" => "myName2", "type" => "string"] 212 | ] 213 | ]; 214 | $domain = (object) [ 215 | "name" => "My DApp", 216 | "version" => "1", 217 | "chainId" => 123, 218 | "verifyingContract" => "0xeee45a4f343dcdc78d33818ad562bea5612c3b36" 219 | ]; 220 | $data = (object) [ 221 | "myName1" => 321, 222 | "myName2" => "abc", 223 | ]; 224 | $hash_eip712 = EIP712::signTypedData_digest($types, $domain, $data); 225 | WTest::check('EIP 712 Typed structured data hashing', $hash_eip712 == '0x31f5a9db3b0d503a462147fa6169261ab3598e36b0cb0e56ab9cf3e16772202a'); 226 | -------------------------------------------------------------------------------- /Test/test_rebuild_tuple.php: -------------------------------------------------------------------------------- 1 | Reencoding abi data"; 16 | echo "?limit=count - limit to X parameters for reencoding. Data format: [Block no, 32 byte content, possible offset]"; 17 | 18 | $t = new abitest($abi_swapv3, $abi_fn, $raw_data); 19 | if ($limit === $t->getABIInputsCount()) { 20 | echo "Too many parameters, setting to: {$limit}
"; 21 | } 22 | if ($limit === false) { 23 | $t->runTest(); 24 | 25 | echo "
 
"; 26 | for ($i = 1; $i <= $t->getABIInputsCount(); $i++) 27 | $t->runTest($i); 28 | } else { 29 | $t->runTest($limit); 30 | } 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drlecks/simple-web3-php", 3 | "description": "Web3 library in PHP", 4 | "keywords": ["ethereum", "transaction", "jsonRPC", "eth", "signed transaction", "php", "web3", "contract", "account", "ABIv2"], 5 | "type": "library", 6 | "license": "MIT", 7 | 8 | "authors": [ 9 | { 10 | "name": "drlecks", 11 | "email": "contacte@powdercode.com", 12 | "homepage": "https://github.com/drlecks/Simple-Web3-Php" 13 | } 14 | ], 15 | 16 | "require": { 17 | "web3p/rlp": "0.3.4", 18 | "kornrunner/ethereum-offline-raw-tx": "^0.6.0", 19 | "kornrunner/keccak": "~1", 20 | "phpseclib/phpseclib": "^3.0", 21 | "simplito/elliptic-php": "^1.0" 22 | }, 23 | 24 | "autoload": { 25 | "psr-4": { 26 | "SWeb3\\": "Core/" 27 | } 28 | } 29 | 30 | } 31 | --------------------------------------------------------------------------------