diff --git a/src/Contracts/Ethabi.php b/src/Contracts/Ethabi.php index cb23c97..2ac02c0 100644 --- a/src/Contracts/Ethabi.php +++ b/src/Contracts/Ethabi.php @@ -11,18 +11,32 @@ namespace Web3\Contracts; +use InvalidArgumentException; +use stdClass; use Web3\Utils; +use Web3\Formatters\Integer as IntegerFormatter; class Ethabi { + /** + * types + * + * @var array + */ + protected $types = []; + /** * construct * + * @param array $types * @return void */ - public function __contruct() + public function __construct($types=[]) { - // + if (!is_array($types)) { + $types = []; + } + $this->types = $types; } /** @@ -57,6 +71,18 @@ class Ethabi return false; } + /** + * callStatic + * + * @param string $name + * @param array $arguments + * @return void + */ + public static function __callStatic($name, $arguments) + { + // + } + /** * encodeFunctionSignature * @@ -84,4 +110,185 @@ class Ethabi } return Utils::sha3($functionName); } + + /** + * encodeParameter + * + * @param string $type + * @param mixed $param + * @return string + */ + public function encodeParameter($type, $param) + { + if (!is_string($type)) { + throw new InvalidArgumentException('The type to encodeParameter must be string.'); + } + return $this->encodeParameters([$type], [$param]); + } + + /** + * encodeParameters + * + * @param stdClass|array $types + * @param array $params + * @return string + */ + public function encodeParameters($types, $params) + { + if (count($types) !== count($params)) { + throw new InvalidArgumentException('encodeParameters number of types must equal to number of params.'); + } + // change json to array + if ($types instanceof stdClass && isset($types->inputs)) { + $types = Utils::jsonToArray($types, 2); + } + if (is_array($types) && isset($types['inputs'])) { + $inputTypes = $types; + $types = []; + + foreach ($inputTypes['inputs'] as $input) { + if (isset($input['type'])) { + $types[] = $input['type']; + } + } + } + $typesLength = count($types); + $solidityTypes = array_fill(0, $typesLength, 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; + } + } + } + $encodes = array_fill(0, $typesLength, ''); + + foreach ($solidityTypes as $key => $type) { + $encodes[$key] = call_user_func([$type, 'encode'], $params[$key], $types[$key]); + } + $dynamicOffset = 0; + + foreach ($solidityTypes as $key => $type) { + $staticPartLength = $type->staticPartLength($types[$key]); + $roundedStaticPartLength = floor(($staticPartLength + 31) / 32) * 32; + + if ($type->isDynamicType($types[$key]) || $type->isDynamicArray($types[$key])) { + $dynamicOffset += 32; + } else { + $dynamicOffset += $roundedStaticPartLength; + } + } + return '0x' . $this->encodeMultiWithOffset($types, $solidityTypes, $encodes, $dynamicOffset); + } + + /** + * encodeWithOffset + * + * @param string $type + * @param \Web3\Contracts\SolidityType $solidityType + * @param mixed $encode + * @param int $offset + * @return string + */ + protected function encodeWithOffset($type, $solidityType, $encoded, $offset) + { + if ($solidityType->isDynamicArray($type)) { + $nestedName = $solidityType->nestedName($type); + $nestedStaticPartLength = $solidityType->staticPartLength($type); + $result = $encoded[0]; + + if ($solidityType->isDynamicArray($nestedName)) { + $previousLength = 2; + + for ($i=0; $idivide(Utils::toBn(2)); + + // if (is_array($divided)) { + // $additionalOffset = (int) $divided[0]->toString(); + // } else { + // $additionalOffset = 0; + // } + $additionalOffset = floor(mb_strlen($result) / 2); + $result .= $this->encodeWithOffset($nestedName, $solidityType, $encoded[$i], $offset + $additionalOffset); + } + return mb_substr($result, 64); + } elseif ($solidityType->isStaticArray($type)) { + $nestedName = $solidityType->nestedName($type); + $nestedStaticPartLength = $solidityType->staticPartLength($type); + $result = ''; + + if ($solidityType->isDynamicArray($nestedName)) { + $previousLength = 0; + + for ($i=0; $idivide(Utils::toBn(2)); + + // if (is_array($divided)) { + // $additionalOffset = (int) $divided[0]->toString(); + // } else { + // $additionalOffset = 0; + // } + $additionalOffset = floor(mb_strlen($result) / 2); + $result .= $this->encodeWithOffset($nestedName, $solidityType, $encoded[$i], $offset + $additionalOffset); + } + return $result; + } + return $encoded; + } + + /** + * encodeMultiWithOffset + * + * @param array $types + * @param array $solidityTypes + * @param array $encodes + * @param int $dynamicOffset + * @return string + */ + protected function encodeMultiWithOffset($types, $solidityTypes, $encodes, $dynamicOffset) + { + $result = ''; + + foreach ($solidityTypes as $key => $type) { + if ($type->isDynamicType($types[$key]) || $type->isDynamicArray($types[$key])) { + $result .= IntegerFormatter::format($dynamicOffset); + $e = $this->encodeWithOffset($types[$key], $type, $encodes[$key], $dynamicOffset); + $dynamicOffset += floor(mb_strlen($e) / 2); + } else { + $result .= $this->encodeWithOffset($types[$key], $type, $encodes[$key], $dynamicOffset); + } + } + foreach ($solidityTypes as $key => $type) { + if ($type->isDynamicType($types[$key]) || $type->isDynamicArray($types[$key])) { + $e = $this->encodeWithOffset($types[$key], $type, $encodes[$key], $dynamicOffset); + // $dynamicOffset += floor(mb_strlen($e) / 2); + $result .= $e; + } + } + return $result; + } } \ No newline at end of file diff --git a/src/Contracts/SolidityType.php b/src/Contracts/SolidityType.php index dffacc7..11c7c5c 100644 --- a/src/Contracts/SolidityType.php +++ b/src/Contracts/SolidityType.php @@ -11,6 +11,9 @@ namespace Web3\Contracts; +use Web3\Utils; +use Web3\Formatters\Integer as IntegerFormatter; + class SolidityType { /** @@ -54,4 +57,162 @@ class SolidityType } return false; } + + /** + * callStatic + * + * @param string $name + * @param array $arguments + * @return void + */ + public static function __callStatic($name, $arguments) + { + // + } + + /** + * nestedTypes + * + * @param string $name + * @return mixed + */ + public function nestedTypes($name) + { + if (!is_string($name)) { + throw new InvalidArgumentException('nestedTypes name must string.'); + } + $matches = []; + + if (preg_match_all('/(\[[0-9]*\])/', $name, $matches, PREG_PATTERN_ORDER) >= 1) { + return $matches[0]; + } + return false; + } + + /** + * nestedName + * + * @param string $name + * @return string + */ + public function nestedName($name) + { + if (!is_string($name)) { + throw new InvalidArgumentException('nestedName name must string.'); + } + $nestedTypes = $this->nestedTypes($name); + + if ($nestedTypes === false) { + return $name; + } + return mb_substr($name, 0, mb_strlen($name) - mb_strlen($nestedTypes[count($nestedTypes) - 1])); + } + + /** + * isDynamicArray + * + * @param string $name + * @return bool + */ + public function isDynamicArray($name) + { + $nestedTypes = $this->nestedTypes($name); + + return $nestedTypes && preg_match('/[0-9]{1,}/', $nestedTypes[count($nestedTypes) - 1]) !== 1; + } + + /** + * isStaticArray + * + * @param string $name + * @return bool + */ + public function isStaticArray($name) + { + $nestedTypes = $this->nestedTypes($name); + + return $nestedTypes && preg_match('/[0-9]{1,}/', $nestedTypes[count($nestedTypes) - 1]) === 1; + } + + /** + * staticArrayLength + * + * @param string $name + * @return int + */ + public function staticArrayLength($name) + { + $nestedTypes = $this->nestedTypes($name); + + if ($nestedTypes === false) { + return 1; + } + $match = []; + + if (preg_match('/[0-9]{1,}/', $nestedTypes[count($nestedTypes) - 1], $match) === 1) { + return (int) $match[0]; + } + return 1; + } + + /** + * staticPartLength + * + * @param string $name + * @return int + */ + public function staticPartLength($name) + { + $nestedTypes = $this->nestedTypes($name); + + if ($nestedTypes === false) { + $nestedTypes = ['[1]']; + } + $count = 32; + + foreach ($nestedTypes as $type) { + $num = mb_substr($type, 1, 1); + + if (!is_numeric($num)) { + $num = 1; + } else { + $num = intval($num); + } + $count *= $num; + } + + return $count; + } + + /** + * encode + * + * @param mixed $value + * @param string $name + * @return string + */ + public function encode($value, $name) + { + if ($this->isDynamicArray($name)) { + $length = count($value); + $nestedName = $this->nestedName($name); + $result = []; + $result[] = IntegerFormatter::format($length); + + foreach ($value as $val) { + $result[] = $this->encode($val, $nestedName); + } + return $result; + } elseif ($this->isStaticArray($name)) { + $length = $this->staticArrayLength($name); + $nestedName = $this->nestedName($name); + $result = []; + + foreach ($value as $val) { + $result[] = $this->encode($val, $nestedName); + } + return $result; + } + return $this->inputFormat($value, $name); + } } \ No newline at end of file diff --git a/test/unit/EthabiTest.php b/test/unit/EthabiTest.php index 5e4ed04..d056f6b 100644 --- a/test/unit/EthabiTest.php +++ b/test/unit/EthabiTest.php @@ -6,9 +6,22 @@ use InvalidArgumentException; use Test\TestCase; use Web3\Utils; use Web3\Contracts\Ethabi; +use Web3\Contracts\Types\Address; +use Web3\Contracts\Types\Boolean; +use Web3\Contracts\Types\Bytes; +use Web3\Contracts\Types\Integer; +use Web3\Contracts\Types\Str; +use Web3\Contracts\Types\Uinteger; class EthabiTest extends TestCase { + /** + * abi + * + * @var \Web3\Contracts\Ethabi + */ + protected $abi; + /** * testJsonMethodString * from GameToken approve function @@ -36,15 +49,64 @@ class EthabiTest extends TestCase ], "payable": false, "stateMutability": "nonpayable", - "type": "function" + "type": "function", + "test": { + "name": "testObject" + } }'; /** - * abi + * tests + * from web3 eth.abi.encodeParameters test + * and web3 eth.abi.encodeParameter test * - * @var \Web3\Contracts\Ethabi + * @param array */ - protected $abi; + protected $tests = [ + [ + 'params' => [['uint256','string'], ['2345675643', 'Hello!%']], + 'result' => '0x000000000000000000000000000000000000000000000000000000008bd02b7b0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000748656c6c6f212500000000000000000000000000000000000000000000000000' + ], [ + 'params' => [['uint8[]','bytes32'], [['34','434'], '0x324567dfff']], + 'result' => '0x0000000000000000000000000000000000000000000000000000000000000040324567dfff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000001b2' + ], [ + 'params' => [['address','address','address', 'address'], ['0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1','','0x0', null]], + 'result' => '0x00000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + ], [ + 'params' => [['bool[2]', 'bool[3]'], [[true, false], [false, false, true]]], + 'result' => '0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001' + ], [ + 'params' => [['int'], [1]], + 'result' => '0x0000000000000000000000000000000000000000000000000000000000000001' + ], [ + 'params' => [['int'], [16]], + 'result' => '0x0000000000000000000000000000000000000000000000000000000000000010' + ], [ + 'params' => [['int'], [-1]], + 'result' => '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + ], [ + 'params' => [['int256'], [1]], + 'result' => '0x0000000000000000000000000000000000000000000000000000000000000001' + ], [ + 'params' => [['int256'], [16]], + 'result' => '0x0000000000000000000000000000000000000000000000000000000000000010' + ], [ + 'params' => [['int256'], [-1]], + 'result' => '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + ], [ + 'params' => [['int[]'], [[3]]], + 'result' => '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003' + ], [ + 'params' => [['int256[]'], [[3]]], + 'result' => '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003' + ], [ + 'params' => [['int256[]'], [[1,2,3]]], + 'result' => '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003' + ], [ + 'params' => [['int[]','int[]'], [[1,2],[3,4]]], + 'result' => '0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000004' + ] + ]; /** * setUp @@ -54,7 +116,24 @@ class EthabiTest extends TestCase public function setUp() { parent::setUp(); - $this->abi = new Ethabi(); + // Error: Using $this when not in object context + // $this->abi = new Ethabi([ + // 'address' => Address::class, + // 'bool' => Boolean::class, + // 'bytes' => Bytes::class, + // 'int' => Integer::class, + // 'string' => Str::class, + // 'uint' => Uinteger::class, + // ]); + + $this->abi = new Ethabi([ + 'address' => new Address, + 'bool' => new Boolean, + 'bytes' => new Bytes, + 'int' => new Integer, + 'string' => new Str, + 'uint' => new Uinteger, + ]); } /** @@ -102,4 +181,18 @@ class EthabiTest extends TestCase $this->assertEquals($str, '0x095ea7b334ae44009aa867bfb386f5c3b4b443ac6f0ee573fa91c4608fbadfba'); } + + /** + * testEncodeParameters + * + * @return void + */ + public function testEncodeParameters() + { + $abi = $this->abi; + + foreach ($this->tests as $test) { + $this->assertEquals($test['result'], $abi->encodeParameters($test['params'][0], $test['params'][1])); + } + } } \ No newline at end of file diff --git a/test/unit/SolidityTypeTest.php b/test/unit/SolidityTypeTest.php new file mode 100644 index 0000000..2cc5aab --- /dev/null +++ b/test/unit/SolidityTypeTest.php @@ -0,0 +1,131 @@ +type = new SolidityType(); + } + + /** + * testNestedTypes + * + * @return void + */ + public function testNestedTypes() + { + $type = $this->type; + + $this->assertEquals($type->nestedTypes('int[2][3][4]'), ['[2]', '[3]', '[4]']); + $this->assertEquals($type->nestedTypes('int[2][3][]'), ['[2]', '[3]', '[]']); + $this->assertEquals($type->nestedTypes('int[2][3]'), ['[2]', '[3]']); + $this->assertEquals($type->nestedTypes('int[2][]'), ['[2]', '[]']); + $this->assertEquals($type->nestedTypes('int[2]'), ['[2]']); + $this->assertEquals($type->nestedTypes('int[]'), ['[]']); + $this->assertEquals($type->nestedTypes('int'), false); + + } + + /** + * testNestedName + * + * @return void + */ + public function testNestedName() + { + $type = $this->type; + + $this->assertEquals($type->nestedName('int[2][3][4]'), 'int[2][3]'); + $this->assertEquals($type->nestedName('int[2][3][]'), 'int[2][3]'); + $this->assertEquals($type->nestedName('int[2][3]'), 'int[2]'); + $this->assertEquals($type->nestedName('int[2][]'), 'int[2]'); + $this->assertEquals($type->nestedName('int[2]'), 'int'); + $this->assertEquals($type->nestedName('int[]'), 'int'); + $this->assertEquals($type->nestedName('int'), 'int'); + } + + /** + * testIsDynamicArray + * + * @return void + */ + public function testIsDynamicArray() + { + $type = $this->type; + + $this->assertFalse($type->isDynamicArray('int[2][3][4]')); + $this->assertTrue($type->isDynamicArray('int[2][3][]')); + $this->assertFalse($type->isDynamicArray('int[2][3]')); + $this->assertTrue($type->isDynamicArray('int[2][]')); + $this->assertFalse($type->isDynamicArray('int[2]')); + $this->assertTrue($type->isDynamicArray('int[]')); + $this->assertFalse($type->isDynamicArray('int')); + } + + /** + * testIsStaticArray + * + * @return void + */ + public function testIsStaticArray() + { + $type = $this->type; + + $this->assertTrue($type->isStaticArray('int[2][3][4]')); + $this->assertFalse($type->isStaticArray('int[2][3][]')); + $this->assertTrue($type->isStaticArray('int[2][3]')); + $this->assertFalse($type->isStaticArray('int[2][]')); + $this->assertTrue($type->isStaticArray('int[2]')); + $this->assertFalse($type->isStaticArray('int[]')); + $this->assertFalse($type->isStaticArray('int')); + } + + /** + * testStaticArrayLength + * + * @return void + */ + public function testStaticArrayLength() + { + $type = $this->type; + + $this->assertEquals($type->staticArrayLength('int[2][3][4]'), 4); + $this->assertEquals($type->staticArrayLength('int[2][3][]'), 1); + $this->assertEquals($type->staticArrayLength('int[2][3]'), 3); + $this->assertEquals($type->staticArrayLength('int[2][]'), 1); + $this->assertEquals($type->staticArrayLength('int[2]'), 2); + $this->assertEquals($type->staticArrayLength('int[]'), 1); + $this->assertEquals($type->staticArrayLength('int'), 1); + + } + + /** + * testEncode + * + * @return void + */ + public function testEncode() + { + $type = $this->type; + $this->assertTrue(true); + } +} \ No newline at end of file