Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of Contract::getEventLogs() and improved general comments and error messages #196

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ $constructorData = $contract->bytecode($bytecode)->getData($params);

// get function data
$functionData = $contract->at($contractAddress)->getData($functionName, $params);

//get event log data
//$fromBlock and $toBlock are optional, default to 'latest' and accept block numbers integers
$events = $contract->getEventLogs($eventName, $fromBlock, $toBlock);
```

# Assign value to outside scope(from callback scope to outside scope)
Expand Down
166 changes: 131 additions & 35 deletions src/Contract.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php

/**
* This file is part of web3.php package.
* This file is part of the web3.php package.
*
* (c) Kuan-Cheng,Lai <[email protected]>
*
Expand All @@ -11,7 +11,8 @@

namespace Web3;

use InvalidArgumentException;
use \InvalidArgumentException;
use \RuntimeException;
use Web3\Providers\Provider;
use Web3\Providers\HttpProvider;
use Web3\RequestManagers\RequestManager;
Expand Down Expand Up @@ -175,7 +176,7 @@ public function __construct($provider, $abi, $defaultBlock = 'latest')
// public function __call($name, $arguments)
// {
// if (empty($this->provider)) {
// throw new \RuntimeException('Please set provider first.');
// throw new RuntimeException('Please set provider first.');
// }
// $class = explode('\\', get_class());
// if (preg_match('/^[a-zA-Z0-9]+$/', $name) === 1) {
Expand Down Expand Up @@ -267,6 +268,7 @@ public function setDefaultBlock($defaultBlock)

/**
* getFunctions
* get an array of all methods in the loaded contract
*
* @return array
*/
Expand All @@ -277,6 +279,7 @@ public function getFunctions()

/**
* getEvents
* get an array of all events (and their inputs) in the loaded contract
*
* @return array
*/
Expand Down Expand Up @@ -368,14 +371,15 @@ public function setToAddress($address)

/**
* at
* set the address of the deployed contract to make calls to
*
* @param string $address
* @return $this
*/
public function at($address)
{
if (AddressValidator::validate($address) === false) {
throw new InvalidArgumentException('Please make sure address is valid.');
throw new InvalidArgumentException('Please make sure the contract address is valid.');
}
$this->toAddress = AddressFormatter::format($address);

Expand All @@ -391,7 +395,7 @@ public function at($address)
public function bytecode($bytecode)
{
if (HexValidator::validate($bytecode) === false) {
throw new InvalidArgumentException('Please make sure bytecode is valid.');
throw new InvalidArgumentException('Please make sure the bytecode input is valid.');
}
$this->bytecode = Utils::stripZero($bytecode);

Expand All @@ -407,7 +411,7 @@ public function bytecode($bytecode)
public function abi($abi)
{
if (StringValidator::validate($abi) === false) {
throw new InvalidArgumentException('Please make sure abi is valid.');
throw new InvalidArgumentException('Please make sure the abi input is valid.');
}
$abiArray = [];
if (is_string($abi)) {
Expand Down Expand Up @@ -438,7 +442,7 @@ public function abi($abi)

/**
* new
* Deploy a contruct with params.
* Deploy a new contract, along with any relevant parameters for its constructor.
*
* @param mixed
* @return void
Expand All @@ -452,13 +456,13 @@ public function new()

$input_count = isset($constructor['inputs']) ? count($constructor['inputs']) : 0;
if (count($arguments) < $input_count) {
throw new InvalidArgumentException('Please make sure you have put all constructor params and callback.');
throw new InvalidArgumentException('Please make sure you have included all constructor parameters and a callback function.');
}
if (is_callable($callback) !== true) {
throw new \InvalidArgumentException('The last param must be callback function.');
throw new InvalidArgumentException('The last parameter must be a callback function.');
}
if (!isset($this->bytecode)) {
throw new \InvalidArgumentException('Please call bytecode($bytecode) before new().');
throw new InvalidArgumentException('Please call bytecode($bytecode) before new().');
}
$params = array_splice($arguments, 0, $input_count);
$data = $this->ethabi->encodeParameters($constructor, $params);
Expand All @@ -480,7 +484,8 @@ public function new()

/**
* send
* Send function method.
* Send inputs to a specific method of the deployed contract
* (interacts with chain data and can alter it: costs gas)
*
* @param mixed
* @return void
Expand All @@ -493,7 +498,7 @@ public function send()
$callback = array_pop($arguments);

if (!is_string($method)) {
throw new InvalidArgumentException('Please make sure the method is string.');
throw new InvalidArgumentException('Please make sure the method name is supplied as a string as the first parameter.');
}

$functions = [];
Expand All @@ -503,10 +508,10 @@ public function send()
}
};
if (count($functions) < 1) {
throw new InvalidArgumentException('Please make sure the method exists.');
throw new InvalidArgumentException('Please make sure the named method exists in the contract.');
}
if (is_callable($callback) !== true) {
throw new \InvalidArgumentException('The last param must be callback function.');
throw new InvalidArgumentException('The last parameter must be a callback function.');
}

// check the last one in arguments is transaction object
Expand Down Expand Up @@ -558,7 +563,7 @@ public function send()
break;
}
if (empty($data) || empty($functionName)) {
throw new InvalidArgumentException('Please make sure you have put all function params and callback.');
throw new InvalidArgumentException('Please make sure you have included all parameters of the method and a callback function.');
}
$functionSignature = $this->ethabi->encodeFunctionSignature($functionName);
$transaction['to'] = $this->toAddress;
Expand All @@ -575,7 +580,8 @@ public function send()

/**
* call
* Call function method.
* Call a specific method of the deployed contract
* (read-only, cannot alter chain data: does not cost gas)
*
* @param mixed
* @return void
Expand All @@ -588,7 +594,7 @@ public function call()
$callback = array_pop($arguments);

if (!is_string($method)) {
throw new InvalidArgumentException('Please make sure the method is string.');
throw new InvalidArgumentException('Please make sure the method name is supplied as a string as the first parameter.');
}

$functions = [];
Expand All @@ -598,10 +604,10 @@ public function call()
}
};
if (count($functions) < 1) {
throw new InvalidArgumentException('Please make sure the method exists.');
throw new InvalidArgumentException('Please make sure the named method exists in the contract.');
}
if (is_callable($callback) !== true) {
throw new \InvalidArgumentException('The last param must be callback function.');
throw new InvalidArgumentException('The last parameter must be a callback function.');
}

// check the arguments
Expand All @@ -623,7 +629,7 @@ public function call()
break;
}
if (empty($data) || empty($functionName)) {
throw new InvalidArgumentException('Please make sure you have put all function params and callback.');
throw new InvalidArgumentException('Please make sure you have included all parameters of the method and a callback function.');
}
// remove arguments
array_splice($arguments, 0, $paramsLen);
Expand Down Expand Up @@ -686,13 +692,13 @@ public function estimateGas()
$constructor = $this->constructor;

if (count($arguments) < count($constructor['inputs'])) {
throw new InvalidArgumentException('Please make sure you have put all constructor params and callback.');
throw new InvalidArgumentException('Please make sure you have included all constructor parameters and a callback function.');
}
if (is_callable($callback) !== true) {
throw new \InvalidArgumentException('The last param must be callback function.');
throw new InvalidArgumentException('The last parameter must be a callback function.');
}
if (!isset($this->bytecode)) {
throw new \InvalidArgumentException('Please call bytecode($bytecode) before estimateGas().');
throw new InvalidArgumentException('Please call bytecode($bytecode) before estimateGas().');
}
$params = array_splice($arguments, 0, count($constructor['inputs']));
$data = $this->ethabi->encodeParameters($constructor, $params);
Expand All @@ -706,7 +712,7 @@ public function estimateGas()
$method = array_splice($arguments, 0, 1)[0];

if (!is_string($method)) {
throw new InvalidArgumentException('Please make sure the method is string.');
throw new InvalidArgumentException('Please make sure the method name is supplied as a string as the first parameter.');
}

$functions = [];
Expand All @@ -716,10 +722,10 @@ public function estimateGas()
}
};
if (count($functions) < 1) {
throw new InvalidArgumentException('Please make sure the method exists.');
throw new InvalidArgumentException('Please make sure the named method exists in the contract.');
}
if (is_callable($callback) !== true) {
throw new \InvalidArgumentException('The last param must be callback function.');
throw new InvalidArgumentException('The last parameter must be a callback function.');
}

// check the last one in arguments is transaction object
Expand Down Expand Up @@ -771,7 +777,7 @@ public function estimateGas()
break;
}
if (empty($data) || empty($functionName)) {
throw new InvalidArgumentException('Please make sure you have put all function params and callback.');
throw new InvalidArgumentException('Please make sure you have included all parameters of the method and a callback function.');
}
$functionSignature = $this->ethabi->encodeFunctionSignature($functionName);
$transaction['to'] = $this->toAddress;
Expand All @@ -789,9 +795,9 @@ public function estimateGas()

/**
* getData
* Get the function method call data.
* With this function, you can send signed contract function transaction.
* 1. Get the funtion data with params.
* Get the contract method's call data.
* With this function, you can send signed contract method transactions.
* 1. Get the method data with parameters.
* 2. Sign the data with user private key.
* 3. Call sendRawTransaction.
*
Expand All @@ -808,10 +814,10 @@ public function getData()
$constructor = $this->constructor;

if (count($arguments) < count($constructor['inputs'])) {
throw new InvalidArgumentException('Please make sure you have put all constructor params and callback.');
throw new InvalidArgumentException('Please make sure you have included all constructor parameters and a callback function.');
}
if (!isset($this->bytecode)) {
throw new \InvalidArgumentException('Please call bytecode($bytecode) before getData().');
throw new InvalidArgumentException('Please call bytecode($bytecode) before getData().');
}
$params = array_splice($arguments, 0, count($constructor['inputs']));
$data = $this->ethabi->encodeParameters($constructor, $params);
Expand All @@ -820,7 +826,7 @@ public function getData()
$method = array_splice($arguments, 0, 1)[0];

if (!is_string($method)) {
throw new InvalidArgumentException('Please make sure the method is string.');
throw new InvalidArgumentException('Please make sure the method name is supplied as a string as the first parameter.');
}

$functions = [];
Expand All @@ -830,7 +836,7 @@ public function getData()
}
};
if (count($functions) < 1) {
throw new InvalidArgumentException('Please make sure the method exists.');
throw new InvalidArgumentException('Please make sure the named method exists in the contract.');
}

$params = $arguments;
Expand All @@ -849,12 +855,102 @@ public function getData()
break;
}
if (empty($data) || empty($functionName)) {
throw new InvalidArgumentException('Please make sure you have put all function params and callback.');
throw new InvalidArgumentException('Please make sure you have included all parameters of the method and a callback function.');
}
$functionSignature = $this->ethabi->encodeFunctionSignature($functionName);
$functionData = Utils::stripZero($functionSignature) . Utils::stripZero($data);
}
return $functionData;
}
}

/**
* getEventLogs
*
* @param string $eventName
* @param string|int $fromBlock
* @param string|int $toBlock
* @return array
*/
public function getEventLogs(string $eventName, $fromBlock = 'latest', $toBlock = 'latest')
{
//try to ensure block numbers are valid together
if ($fromBlock !== 'latest') {
if (!is_int($fromBlock) || $fromBlock < 1) {
throw new InvalidArgumentException('Please make sure fromBlock is a valid block number');
} else if ($toBlock !== 'latest' && $fromBlock > $toBlock) {
throw new InvalidArgumentException('Please make sure fromBlock is equal or less than toBlock');
}
}

if ($toBlock !== 'latest') {
if (!is_int($toBlock) || $toBlock < 1) {
throw new InvalidArgumentException('Please make sure toBlock is a valid block number');
} else if ($fromBlock === 'latest') {
throw new InvalidArgumentException('Please make sure toBlock is equal or greater than fromBlock');
}
}

$eventLogData = [];

//ensure the event actually exists before trying to filter for it
if (!array_key_exists($eventName, $this->events)) {
throw new InvalidArgumentException("'{$eventName}' does not exist in the ABI for this contract");
}

//indexed and non-indexed event parameters must be treated separately
//indexed parameters are stored in the 'topics' array
//non-indexed parameters are stored in the 'data' value
$eventParameterNames = [];
$eventParameterTypes = [];
$eventIndexedParameterNames = [];
$eventIndexedParameterTypes = [];

foreach ($this->events[$eventName]['inputs'] as $input) {
if ($input['indexed']) {
$eventIndexedParameterNames[] = $input['name'];
$eventIndexedParameterTypes[] = $input['type'];
} else {
$eventParameterNames[] = $input['name'];
$eventParameterTypes[] = $input['type'];
}
}

$numEventIndexedParameterNames = count($eventIndexedParameterNames);

//filter through log data to find any logs which match this event (topic) from
//this contract, between these specified blocks (defaulting to the latest block only)
$this->eth->getLogs([
'fromBlock' => (is_int($fromBlock)) ? '0x' . dechex($fromBlock) : $fromBlock,
'toBlock' => (is_int($toBlock)) ? '0x' . dechex($toBlock) : $toBlock,
'topics' => [$this->ethabi->encodeEventSignature($this->events[$eventName])],
'address' => $this->toAddress
],
function ($error, $result) use (&$eventLogData, $eventParameterTypes, $eventParameterNames, $eventIndexedParameterTypes, $eventIndexedParameterNames) {
if ($error !== null) {
throw new RuntimeException($error->getMessage());
}

foreach ($result as $object) {
//decode the data from the log into the expected formats, with its corresponding named key
$decodedData = array_combine($eventParameterNames, $this->ethabi->decodeParameters($eventParameterTypes, $object->data));

//decode the indexed parameter data
for ($i = 0; $i < $numEventIndexedParameterNames; $i++) {
Copy link

@davorminchorov davorminchorov Aug 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$numEventIndexedParameterNames is not defined anywhere.

//topics[0] is the event signature, so we start from $i + 1 for the indexed parameter data
$decodedData[$eventIndexedParameterNames[$i]] = $this->ethabi->decodeParameters([$eventIndexedParameterTypes[$i]], $object->topics[$i + 1])[0];
}

//include block metadata for context, along with event data
$eventLogData[] = [
'transactionHash' => $object->transactionHash,
'blockHash' => $object->blockHash,
'blockNumber' => hexdec($object->blockNumber),
'data' => $decodedData
];
}
});

return $eventLogData;
}
}
Loading