diff --git a/.gitignore b/.gitignore index 0aed48d..0fbfeab 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ composer.phar /vendor/ .phpintel/ /.idea/ +test.php # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file diff --git a/resources/abi.json b/resources/abi.json new file mode 100644 index 0000000..7d87527 --- /dev/null +++ b/resources/abi.json @@ -0,0 +1,217 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "remaining", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_from", + "type": "address" + }, + { + "indexed": true, + "name": "_to", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_owner", + "type": "address" + }, + { + "indexed": true, + "name": "_spender", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + } +] \ No newline at end of file diff --git a/src/AbiDecoder.php b/src/AbiDecoder.php new file mode 100644 index 0000000..4131ee8 --- /dev/null +++ b/src/AbiDecoder.php @@ -0,0 +1,229 @@ + + * @license MIT + */ + +namespace Web3; + +use Web3\Contracts\Ethabi; +use Web3\Contracts\Types\Address; +use Web3\Contracts\Types\Boolean; +use Web3\Contracts\Types\Bytes; +use Web3\Contracts\Types\DynamicBytes; +use Web3\Contracts\Types\Integer; +use Web3\Contracts\Types\Str; +use Web3\Contracts\Types\Uinteger; +use phpseclib\Math\BigInteger as BN; + +class AbiDecoder +{ + /** + * @var array + */ + private $abi = []; + + /** + * @var Ethabi + */ + private $ethAbi; + + /** + * @var array + */ + private $methodIds = []; + + /** + * @param array|null $abi + */ + public function __construct(?array $abi = null) + { + $this->abi = is_null($abi) ? json_decode(file_get_contents(dirname(__DIR__) . '/resources/abi.json')) : $abi; + + $this->ethAbi = new Ethabi([ + 'address' => new Address, + 'bool' => new Boolean, + 'bytes' => new Bytes, + 'dynamicBytes' => new DynamicBytes, + 'int' => new Integer, + 'string' => new Str, + 'uint' => new Uinteger, + ]); + + array_map([$this, 'parseMethodIds'], $this->abi); + } + + /** + * @param object $input + * @return string + */ + private function typeToString(object $input) : string + { + if ($input->type === "tuple") { + return "(" . implode(',', array_map([$this, 'typeToString'], $input->components)) . ")"; + } + + return $input->type; + } + + /** + * @param array $inputs + * @return string + */ + private function parseInputs(array $inputs) : string + { + return "(" . implode(',', array_map([$this, 'typeToString'], $inputs)) . ")"; + } + + /** + * @param object $obj + * @return void + */ + private function parseMethodIds(object $obj) + { + if (isset($obj->name)) { + $signature = Utils::sha3($obj->name . $this->parseInputs($obj->inputs)); + + if ($obj->type === "event") { + $this->methodIds[substr($signature, 2)] = $obj; + } else { + $this->methodIds[substr($signature, 2, 8)] = $obj; + } + } + } + + /** + * @param string $input + * @return object + */ + public function decodeInput(string $input) : object + { + $abiItem = $this->methodIds[substr($input, 2, 8)]; + $types = array_column($abiItem->inputs, 'type'); + + if ($abiItem) { + $decoded = $this->ethAbi->decodeParameters($types, substr($input, 10)); + + $retData = (object) [ + "name" => $abiItem->name, + "params" => [], + ]; + + for ($i = 0; $i < count($decoded); $i++) { + $param = $decoded[$i]; + $parsedParam = $param; + $isArray = is_array($param); + $isUint = strpos($abiItem->inputs[$i]->type, "uint") !== false; + $isInt = strpos($abiItem->inputs[$i]->type, "int") !== false; + $isAddress = strpos($abiItem->inputs[$i]->type, "address") !== false; + + if ($isUint || $isInt) { + if ($isArray) { + $parsedParam = array_map(function($val) { + return (new BN($val))->toString(); + }, $param); + } else { + $parsedParam = (new BN($param))->toString(); + } + } + + if ($isAddress) { + if ($isArray) { + $parsedParam = array_map('strtolower', $param); + } else { + $parsedParam = strtolower($param); + } + } + + $retData->params[] = [ + "name" => $abiItem->inputs[$i]->name, + "value" => $parsedParam, + "type" => $abiItem->inputs[$i]->type, + ]; + } + } + + return $retData; + } + + /** + * @param array $logs + * @return array + */ + public function decodeLogs(array $logs) : array + { + $logs = array_filter($logs, function($log) { + return count($log->topics) > 0; + }); + + return array_map(function($log) { + + $method = $this->methodIds[substr($log->topics[0], 2)]; + + if ($method) { + + $logData = $log->data; + $decodedParams = []; + $dataIndex = 0; + $topicsIndex = 1; + $dataTypes = []; + + array_map(function($input) use (&$dataTypes) { + if (!$input->indexed) { + $dataTypes[] = $input->type; + } + }, $method->inputs); + + $decoded = $this->ethAbi->decodeParameters($dataTypes, substr($logData, 2)); + + array_map(function($input) use ($log, &$decodedParams, &$topicsIndex, &$dataIndex, $decoded) { + + $decodedP = (object) [ + "name" => $input->name, + "type" => $input->type, + ]; + + if ($input->indexed) { + $decodedP->value = $log->topics[$topicsIndex]; + $topicsIndex++; + } else { + $decodedP->value = $decoded[$dataIndex]; + $dataIndex++; + } + + if ($input->type === "address") { + $decodedP->value = strtolower($decodedP->value); + if (strlen($decodedP->value) > 42) { + $toRemove = strlen($decodedP->value) - 42; + $decodedP->value = "0x" . substr($decodedP->value, -40); + } + } + + if ( + $input->type === "uint256" || + $input->type === "uint8" || + $input->type === "int" + ) { + if (is_string($decodedP->value) && str_starts_with($decodedP->value, '0x')) { + $decodedP->value = (new BN(substr($decodedP->value, 2), 16))->toString(10); + } else { + $decodedP->value = (new BN($decodedP->value))->toString(10); + } + } + + $decodedParams[] = $decodedP; + + }, $method->inputs); + + + return (object) [ + "name" => $method->name, + "events" => $decodedParams, + "address" => $log->address, + ]; + } + + }, $logs); + } +} \ No newline at end of file