Ethabi encodeParameters and SolidityType encode.

This commit is contained in:
sc0Vu 2017-12-29 15:34:05 +08:00
parent 51ece8f887
commit 518a72b422
4 changed files with 599 additions and 7 deletions

View File

@ -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; $i<count($encoded); $i++) {
if (isset($encoded[$i - 1])) {
$previousLength += abs($encoded[$i - 1][0]);
}
$result .= IntegerFormatter::format($offset + $i * $nestedStaticPartLength + $previousLength * 32);
}
}
for ($i=0; $i<count($encoded); $i++) {
// $bn = Utils::toBn($result);
// $divided = $bn->divide(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; $i<count($encoded); $i++) {
if (isset($encoded[$i - 1])) {
$previousLength += abs($encoded[$i - 1])[0];
}
$result .= IntegerFormatter::format($offset + $i * $nestedStaticPartLength + $previousLength * 32);
}
}
for ($i=0; $i<count($encoded); $i++) {
// $bn = Utils::toBn($result);
// $divided = $bn->divide(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;
}
}

View File

@ -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);
}
}

View File

@ -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]));
}
}
}

View File

@ -0,0 +1,131 @@
<?php
namespace Test\Unit;
use InvalidArgumentException;
use Test\TestCase;
use Web3\Contracts\SolidityType;
class SolidityTypeTest extends TestCase
{
/**
* type
*
* @var \Web3\Contracts\SolidityType
*/
protected $type;
/**
* setUp
*
* @return void
*/
public function setUp()
{
parent::setUp();
$this->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);
}
}