diff --git a/README.md b/README.md index 69fbef5..098e85e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![Build Status](https://travis-ci.org/sc0Vu/web3.php.svg?branch=master)](https://travis-ci.org/sc0Vu/web3.php) [![codecov](https://codecov.io/gh/sc0Vu/web3.php/branch/master/graph/badge.svg)](https://codecov.io/gh/sc0Vu/web3.php) [![Join the chat at https://gitter.im/web3-php/web3.php](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg)](https://gitter.im/web3-php/web3.php) +[![Licensed under the MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/sc0Vu/web3.php/blob/master/LICENSE) + A php interface for interacting with the Ethereum blockchain and ecosystem. @@ -173,7 +175,7 @@ $contract->at($contractAddress)->send($functionName, $params, $callback); $contract->bytecode($bytecode)->estimateGas($params, $callback); // estimate function gas -$contract->bytecode($bytecode)->estimateGas($functionName, $params, $callback); +$contract->at($contractAddress)->estimateGas($functionName, $params, $callback); ``` # Assign value to outside scope(from callback scope to outside scope) diff --git a/src/Contracts/Ethabi.php b/src/Contracts/Ethabi.php index c826d7d..e2d1f5f 100644 --- a/src/Contracts/Ethabi.php +++ b/src/Contracts/Ethabi.php @@ -153,7 +153,7 @@ class Ethabi throw new InvalidArgumentException('encodeParameters number of types must equal to number of params.'); } $typesLength = count($types); - $solidityTypes = array_fill(0, $typesLength, 0); + $solidityTypes = $this->getSolidityTypes($types); foreach ($types as $key => $type) { $match = []; @@ -189,6 +189,101 @@ class Ethabi return '0x' . $this->encodeMultiWithOffset($types, $solidityTypes, $encodes, $dynamicOffset); } + /** + * decodeParameter + * + * @param string $type + * @param mixed $param + * @return string + */ + public function decodeParameter($type, $param) + { + if (!is_string($type)) { + throw new InvalidArgumentException('The type to decodeParameter must be string.'); + } + return $this->decodeParameters([$type], $param)[0]; + } + + /** + * decodeParameters + * + * @param stdClass|array $type + * @param string $param + * @return string + */ + public function decodeParameters($types, $param) + { + if (!is_string($param)) { + throw new InvalidArgumentException('The type or param to decodeParameters must be string.'); + } + + // change json to array + if ($types instanceof stdClass && isset($types->outputs)) { + $types = Utils::jsonToArray($types, 2); + } + if (is_array($types) && isset($types['outputs'])) { + $outputTypes = $types; + $types = []; + + foreach ($outputTypes['outputs'] as $output) { + if (isset($output['type'])) { + $types[] = $output['type']; + } + } + } + $typesLength = count($types); + $solidityTypes = $this->getSolidityTypes($types); + $offsets = array_fill(0, $typesLength, 0); + + for ($i=0; $i<$typesLength; $i++) { + $offsets[$i] = $solidityTypes[$i]->staticPartLength($types[$i]); + } + for ($i=1; $i<$typesLength; $i++) { + $offsets[$i] += $offsets[$i - 1]; + } + for ($i=0; $i<$typesLength; $i++) { + $offsets[$i] -= $solidityTypes[$i]->staticPartLength($types[$i]); + } + $result = []; + $param = mb_strtolower(Utils::stripZero($param)); + + for ($i=0; $i<$typesLength; $i++) { + $result[$i] = $solidityTypes[$i]->decode($param, $offsets[$i], $types[$i]); + } + + return $result; + } + + /** + * getSolidityTypes + * + * @param array $types + * @return array + */ + protected function getSolidityTypes($types) + { + if (!is_array($types)) { + throw new InvalidArgumentException('Types must be array'); + } + $solidityTypes = array_fill(0, count($types), 0); + + foreach ($types as $key => $type) { + $match = []; + + if (preg_match('/^([a-zA-Z]+)/', $type, $match) === 1) { + if (isset($this->types[$match[0]])) { + $className = $this->types[$match[0]]; + + if (call_user_func([$this->types[$match[0]], 'isType'], $type) === false) { + throw new InvalidArgumentException('Unsupport solidity parameter type: ' . $type); + } + $solidityTypes[$key] = $className; + } + } + } + return $solidityTypes; + } + /** * encodeWithOffset * diff --git a/src/Contracts/SolidityType.php b/src/Contracts/SolidityType.php index 11c7c5c..98474ac 100644 --- a/src/Contracts/SolidityType.php +++ b/src/Contracts/SolidityType.php @@ -184,6 +184,16 @@ class SolidityType return $count; } + /** + * isDynamicType + * + * @return bool + */ + public function isDynamicType() + { + return false; + } + /** * encode * @@ -215,4 +225,54 @@ class SolidityType } return $this->inputFormat($value, $name); } + + /** + * decode + * + * @param mixed $value + * @param string $offset + * @param string $name + * @return array + */ + public function decode($value, $offset, $name) + { + if ($this->isDynamicArray($name)) { + $arrayOffset = (int) Utils::toBn('0x' . mb_substr($value, $offset * 2, 64))->toString(); + $length = (int) Utils::toBn('0x' . mb_substr($value, $arrayOffset * 2, 64))->toString(); + $arrayStart = $arrayOffset + 32; + + $nestedName = $this->nestedName($name); + $nestedStaticPartLength = $this->staticPartLength($nestedName); + $roundedNestedStaticPartLength = floor(($nestedStaticPartLength + 31) / 32) * 32; + $result = []; + + for ($i=0; $i<$length * $roundedNestedStaticPartLength; $i+=$roundedNestedStaticPartLength) { + $result[] = $this->decode($value, $arrayStart + $i, $nestedName); + } + return $result; + } elseif ($this->isStaticArray($name)) { + $length = $this->staticArrayLength($name); + $arrayStart = $offset; + + $nestedName = $this->nestedName($name); + $nestedStaticPartLength = $this->staticPartLength($nestedName); + $roundedNestedStaticPartLength = floor(($nestedStaticPartLength + 31) / 32) * 32; + $result = []; + + for ($i=0; $i<$length * $roundedNestedStaticPartLength; $i+=$roundedNestedStaticPartLength) { + $result[] = $this->decode($value, $arrayStart + $i, $nestedName); + } + return $result; + } elseif ($this->isDynamicType()) { + $dynamicOffset = (int) Utils::toBn('0x' . mb_substr($value, $offset * 2, 64))->toString(); + $length = (int) Utils::toBn('0x' . mb_substr($value, $dynamicOffset * 2, 64))->toString(); + $roundedLength = floor(($length + 31) / 32); + $param = mb_substr($value, $dynamicOffset * 2, ( 1 + $roundedLength) * 64); + return $this->outputFormat($param, $name); + } + $length = $this->staticPartLength($name); + $param = mb_substr($value, $offset * 2, $length * 2); + + return $this->outputFormat($param, $name); + } } \ No newline at end of file diff --git a/src/Contracts/Types/Address.php b/src/Contracts/Types/Address.php index f93e882..011679a 100644 --- a/src/Contracts/Types/Address.php +++ b/src/Contracts/Types/Address.php @@ -73,4 +73,16 @@ class Address extends SolidityType implements IType return $value; } + + /** + * outputFormat + * + * @param mixed $value + * @param string $name + * @return string + */ + public function outputFormat($value, $name) + { + return '0x' . mb_substr($value, 24, 40); + } } \ No newline at end of file diff --git a/src/Contracts/Types/Boolean.php b/src/Contracts/Types/Boolean.php index 6ec38c7..3850761 100644 --- a/src/Contracts/Types/Boolean.php +++ b/src/Contracts/Types/Boolean.php @@ -64,4 +64,18 @@ class Boolean extends SolidityType implements IType return '000000000000000000000000000000000000000000000000000000000000000' . $value; } + + /** + * outputFormat + * + * @param mixed $value + * @param string $name + * @return string + */ + public function outputFormat($value, $name) + { + $value = (int) mb_substr($value, 63, 1); + + return (bool) $value; + } } \ No newline at end of file diff --git a/src/Contracts/Types/Bytes.php b/src/Contracts/Types/Bytes.php index eba8a9c..55b13fc 100644 --- a/src/Contracts/Types/Bytes.php +++ b/src/Contracts/Types/Bytes.php @@ -75,4 +75,21 @@ class Bytes extends SolidityType implements IType return $value . implode('', array_fill(0, $padding, '0')); } + + /** + * outputFormat + * + * @param mixed $value + * @param string $name + * @return string + */ + public function outputFormat($value, $name) + { + $checkZero = str_replace('0', '', $value); + + if (empty($checkZero)) { + return '0'; + } + return '0x' . $value; + } } \ No newline at end of file diff --git a/src/Contracts/Types/DynamicBytes.php b/src/Contracts/Types/DynamicBytes.php index d95d50f..a0c8842 100644 --- a/src/Contracts/Types/DynamicBytes.php +++ b/src/Contracts/Types/DynamicBytes.php @@ -74,4 +74,21 @@ class DynamicBytes extends SolidityType implements IType return implode('', array_fill(0, 64-mb_strlen($bnHex), $padded)) . $bnHex . $value . implode('', array_fill(0, $padding, '0')); } + + /** + * outputFormat + * + * @param mixed $value + * @param string $name + * @return string + */ + public function outputFormat($value, $name) + { + $checkZero = str_replace('0', '', $value); + + if (empty($checkZero)) { + return '0'; + } + return '0x' . $value; + } } \ No newline at end of file diff --git a/src/Contracts/Types/Integer.php b/src/Contracts/Types/Integer.php index ca0e557..d2078a3 100644 --- a/src/Contracts/Types/Integer.php +++ b/src/Contracts/Types/Integer.php @@ -15,6 +15,7 @@ use Web3\Utils; use Web3\Contracts\SolidityType; use Web3\Contracts\Types\IType; use Web3\Formatters\Integer as IntegerFormatter; +use Web3\Formatters\BigNumberFormatter; class Integer extends SolidityType implements IType { @@ -60,4 +61,22 @@ class Integer extends SolidityType implements IType { return IntegerFormatter::format($value); } + + /** + * outputFormat + * + * @param mixed $value + * @param string $name + * @return string + */ + public function outputFormat($value, $name) + { + $match = []; + + if (preg_match('/^[0]+([a-f0-9]+)$/', $value, $match) === 1) { + // due to value without 0x prefix, we will parse as decimal + $value = '0x' . $match[1]; + } + return BigNumberFormatter::format($value); + } } \ No newline at end of file diff --git a/src/Contracts/Types/Str.php b/src/Contracts/Types/Str.php index 5691c6d..7a8a227 100644 --- a/src/Contracts/Types/Str.php +++ b/src/Contracts/Types/Str.php @@ -15,6 +15,7 @@ use Web3\Utils; use Web3\Contracts\SolidityType; use Web3\Contracts\Types\IType; use Web3\Formatters\Integer as IntegerFormatter; +use Web3\Formatters\BigNumberFormatter; class Str extends SolidityType implements IType { @@ -65,4 +66,25 @@ class Str extends SolidityType implements IType return $prefix . $value . implode('', array_fill(0, $padding, '0')); } + + /** + * outputFormat + * + * @param mixed $value + * @param string $name + * @return string + */ + public function outputFormat($value, $name) + { + $strLen = mb_substr($value, 0, 64); + $strValue = mb_substr($value, 64, 64); + $match = []; + + if (preg_match('/^[0]+([a-f0-9]+)$/', $strLen, $match) === 1) { + $strLen = BigNumberFormatter::format('0x' . $match[1])->toString(); + } + $strValue = mb_substr($strValue, 0, (int) $strLen * 2); + + return Utils::hexToBin($strValue); + } } \ No newline at end of file diff --git a/src/Contracts/Types/Uinteger.php b/src/Contracts/Types/Uinteger.php index b294666..d5dd4b2 100644 --- a/src/Contracts/Types/Uinteger.php +++ b/src/Contracts/Types/Uinteger.php @@ -15,6 +15,7 @@ use Web3\Utils; use Web3\Contracts\SolidityType; use Web3\Contracts\Types\IType; use Web3\Formatters\Integer as IntegerFormatter; +use Web3\Formatters\BigNumberFormatter; class Uinteger extends SolidityType implements IType { @@ -60,4 +61,22 @@ class Uinteger extends SolidityType implements IType { return IntegerFormatter::format($value); } + + /** + * outputFormat + * + * @param mixed $value + * @param string $name + * @return string + */ + public function outputFormat($value, $name) + { + $match = []; + + if (preg_match('/^[0]+([a-f0-9]+)$/', $value, $match) === 1) { + // due to value without 0x prefix, we will parse as decimal + $value = '0x' . $match[1]; + } + return BigNumberFormatter::format($value); + } } \ No newline at end of file diff --git a/test/unit/EthabiTest.php b/test/unit/EthabiTest.php index d056f6b..2d900ce 100644 --- a/test/unit/EthabiTest.php +++ b/test/unit/EthabiTest.php @@ -56,13 +56,14 @@ class EthabiTest extends TestCase }'; /** - * tests - * from web3 eth.abi.encodeParameters test + * encodingTests + * from web3 abi.encodeParameter.js test + * and web3 eth.abi.encodeParameters test * and web3 eth.abi.encodeParameter test * * @param array */ - protected $tests = [ + protected $encodingTests = [ [ 'params' => [['uint256','string'], ['2345675643', 'Hello!%']], 'result' => '0x000000000000000000000000000000000000000000000000000000008bd02b7b0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000748656c6c6f212500000000000000000000000000000000000000000000000000' @@ -108,6 +109,54 @@ class EthabiTest extends TestCase ] ]; + /** + * decodingTests + * from web3 abi.decodeParameter.js test + * and web3 eth.abi.decodeParameters test + * and web3 eth.abi.decodeParameter test + * + * @param array + */ + protected $decodingTests = [ + [ + 'params' => [['uint256'], '0x0000000000000000000000000000000000000000000000000000000000000010'], + 'result' => ['16'] + ], [ + 'params' => [['string'], '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000848656c6c6f212521000000000000000000000000000000000000000000000000'], + 'result' => ['Hello!%!'] + ], [ + 'params' => [['uint256','string'], '0x000000000000000000000000000000000000000000000000000000008bd02b7b0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000748656c6c6f212500000000000000000000000000000000000000000000000000'], + 'result' => ['2345675643', 'Hello!%'] + ], [ + 'params' => [['string'], '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'], + 'result' => [''] + ], [ + 'params' => [['int256'], '0x0000000000000000000000000000000000000000000000000000000000000000'], + 'result' => ['0'] + ], [ + 'params' => [['uint256'], '0x0000000000000000000000000000000000000000000000000000000000000000'], + 'result' => ['0'] + ], [ + 'params' => [['address'], '0x0000000000000000000000000000000000000000000000000000000000000000'], + 'result' => ['0x0000000000000000000000000000000000000000'] + ], [ + 'params' => [['bool'], '0x0000000000000000000000000000000000000000000000000000000000000000'], + 'result' => [false] + ], [ + 'params' => [['bytes'], '0x0000000000000000000000000000000000000000000000000000000000000000'], + 'result' => ['0'] + ], [ + 'params' => [['bytes32'], '0x0000000000000000000000000000000000000000000000000000000000000000'], + 'result' => ['0'] + ], [ + 'params' => [['bytes32'], '0xdf32340000000000000000000000000000000000000000000000000000000000'], + 'result' => ['0xdf32340000000000000000000000000000000000000000000000000000000000'] + ], [ + 'params' => [['bytes32[]'], '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002df32340000000000000000000000000000000000000000000000000000000000fdfd000000000000000000000000000000000000000000000000000000000000'], + 'result' => ['0xdf32340000000000000000000000000000000000000000000000000000000000', '0xfdfd000000000000000000000000000000000000000000000000000000000000'] + ] + ]; + /** * setUp * @@ -182,6 +231,18 @@ class EthabiTest extends TestCase $this->assertEquals($str, '0x095ea7b334ae44009aa867bfb386f5c3b4b443ac6f0ee573fa91c4608fbadfba'); } + /** + * testEncodeParameter + * + * @return void + */ + public function testEncodeParameter() + { + $abi = $this->abi; + + $this->assertEquals('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', $abi->encodeParameter('int256', '-1')); + } + /** * testEncodeParameters * @@ -191,8 +252,45 @@ class EthabiTest extends TestCase { $abi = $this->abi; - foreach ($this->tests as $test) { + foreach ($this->encodingTests as $test) { $this->assertEquals($test['result'], $abi->encodeParameters($test['params'][0], $test['params'][1])); } } + + /** + * testDecodeParameter + * + * @return void + */ + public function testDecodeParameter() + { + $abi = $this->abi; + + $this->assertEquals('16', $abi->decodeParameter('uint256', '0x0000000000000000000000000000000000000000000000000000000000000010')->toString()); + $this->assertEquals('16', $abi->decodeParameter('uint256', '0x0000000000000000000000000000000000000000000000000000000000000010')->toString()); + } + + /** + * testDecodeParameters + * + * @return void + */ + public function testDecodeParameters() + { + $abi = $this->abi; + + foreach ($this->decodingTests as $test) { + $decoded = $abi->decodeParameters($test['params'][0], $test['params'][1]); + + foreach ($decoded as $key => $decoding) { + if (!is_array($decoding)) { + $this->assertEquals($test['result'][$key], $decoding); + } else { + foreach ($test['result'] as $rKey => $expected) { + $this->assertEquals($expected, $decoding[$rKey]); + } + } + } + } + } } \ No newline at end of file diff --git a/test/unit/SolidityTypeTest.php b/test/unit/SolidityTypeTest.php index 2cc5aab..941deb2 100644 --- a/test/unit/SolidityTypeTest.php +++ b/test/unit/SolidityTypeTest.php @@ -123,9 +123,20 @@ class SolidityTypeTest extends TestCase * * @return void */ - public function testEncode() - { - $type = $this->type; - $this->assertTrue(true); - } + // public function testEncode() + // { + // $type = $this->type; + // $this->assertTrue(true); + // } + + /** + * testDecode + * + * @return void + */ + // public function testDecode() + // { + // $type = $this->type; + // $this->assertTrue(true); + // } } \ No newline at end of file