<commit_before><?php
require_once USERS_PLUGIN_DIR.'/vendor/autoload.php';
use Web3\Web3;
use Web3\Contract;
use Crypto\Keccak;
use Web3p\EthereumTx\Transaction;

/**
 * @module Users
 */
/**
 * Class for dealing with EVM blockchains
 *
 * @class Users_Web3
 */
class Users_Web3 extends Base_Users_Web3 {
	/**
	 * Used to execute methods on the blockchain.
	 * If you don't pass privateKey, then it returns the result of a "view" operation (possibly cached locally).
	 * If you pass a privateKey, then it returns the hash of a transaction submitted to be mined.
	 * @method execute
	 * @static
	 * @param {string|array} $contractAddress the contract address to call the method on,
	 *  or array($contractAddress, $abiContent) to specify custom ABI content (JSON),
	 *  useful for when you have many contracts with the same ABI produced by a
	 * @param {string} $methodName in the contract
	 * @param {string|array} [$params=array()] - params sent to contract method
	 * @param {string} [$appId=Q::app()] Indicate which entery in Users/apps config to use
	 * @param {boolean|null|callable} [$caching=true] Set false to ignore cache and request blockchain every time.
	 *  Set to null to cache any truthy result while not caching falsy results.
	 *  Or set to a callable function, to be passed the data as JSON, and return boolean indicating whether to cache or not.
	 * @param {integer} [$cacheDuration=3600] How many seconds in the past to look for a cache
	 * @param {string} [$defaultBlock='latest'] Can be one of 'latest', 'earliest', 'pending'
	 * @param {integer} [$delay=0] If not found in cache, set how many microseconds to delay before querying the blockchain
	 * @param {array} [$transaction=array()] Additional information for transaction, with possible keys 'from', 'gas', 'gasPrice', 'value', 'nonce'
	 * @param {string} [$privateKey=null] Optionally pass an ECDSA private key here, to sign the transaction with (e.g. to change blockchain state).
	 * @return {mixed} Returns the transaction hash, if privateKey was specified. Otherwise, returns the (possibly cached) result of "view" operation on the blockchain.
	 */
	static function execute (
		$contractAddress,
		$methodName,
		$params = array(),
		$appId = null,
		$caching = true,
		$cacheDuration = null,
		$defaultBlock = 'latest',
		$delay = 0,
		$transaction = array(),
		$privateKey = null)
	{
		if (is_array($contractAddress)) {
			list($contractAddress, $abi) = $contractAddress;
		}

		if (!isset($appId)) {
			$appId = Q::app();
		}

		$usersWeb3Config = Q_Config::get("Users", "web3", "chains", $appId, array());
		list($appId, $appInfo) = Users::appInfo('web3', $appId, true);
		$appInfo = array_merge($usersWeb3Config, $appInfo);
		if ($cacheDuration === null) {
			$cacheDuration = Q::ifset($appInfo, 'cacheDuration', 3600);
		}

		$chainId = Q::ifset($appInfo, 'chainId', Q::ifset($appInfo, 'appId', null));
		if (!$chainId) {
			throw new Q_Exception_MissingConfig(array(
				'fieldpath' => "'Users/apps/$appId/chainId'"
			));
		}

		if (!is_array($params)) {
			$params = array($params);
		}

		$from = Q::ifset($transaction, 'from', null);
		$cache = self::getCache($chainId, $contractAddress, $methodName, $params, $cacheDuration, $from);
		if ($caching !== false && $cacheDuration && $cache->wasRetrieved()) {
			return Q::json_decode($cache->result);
		}

		if ($delay) {
			usleep($delay);
		}

		if (empty($appInfo['rpcUrl'])) {
			throw new Q_Exception_MissingConfig(array(
				'fieldpath' => "Users/apps/$appId/rpcUrl"
			));
		}
		$infuraId = Q::ifset(
			$appInfo, 'providers', 'walletconnect', 'infura', 'projectId', null
		);
		$rpcUrl = Q::interpolate(
			$appInfo['rpcUrl'],
			compact('infuraId')
		);

		if (empty($abi)) {
			$abi = self::getABI($contractAddress, $chainId);
		}
		$data = array();
		$arguments = array($methodName);
		foreach ($params as $param) {
			$arguments[] = $param;
		}
		$callback = function ($err, $results) use (&$data) {
			if ($err) {
				$errMessage = Q::ifset($err, "message", null);
				if (!$errMessage) {
					$errMessage = $err->getMessage();
				}
				if ($errMessage) {
					throw new Exception($errMessage);
				}
			}

			if (empty($results)) {
				$data = $results;
				return;
			}
			if (sizeof($results) == 1) {
				$value = reset($results);
				if (is_array($value)) {
					foreach ($value as $result) {
						$data[] = $result->toString();
					}
				} else {
					$data = $value;
				}
			} else {
				$data = $results;
			}
		};

		$contract = (new Contract($rpcUrl, $abi, $defaultBlock))
			->at($contractAddress);
		if ($privateKey) {
			if (empty($transaction['from'])) {
				throw new Q_Exception_MissingObject(array(
					'name' => 'transaction.from'
				));
			}
			$eth = $contract->eth;
			$rawTransactionData = '0x' . 
				call_user_func_array([$contract, "getData"], $arguments);
			$transactionCount = null;
			$eth->getTransactionCount($transaction['from'],
			function ($err, $count) use(&$transactionCount) {
				if ($err) { 
					throw new Q_Exception('transaction count error: ' . $err->getMessage());
				}
				$transactionCount = $count;
			});
			if (!isset($transactionCount)) {
				return null;
			}
			$transactionParams = array_merge(array(
				'nonce' => "0x" . dechex($transactionCount->toString()),
				'from' => $transaction['from'],
				'to' =>  $contractAddress,
				// 'gas' =>  '0x' . dechex(8000000),
				'value' => '0x0',
				'data' => $rawTransactionData
			), $transaction);
			if (empty($transactionParams['gas'])) {
				$estimatedGas = null;
				$contract->estimateGas($transactionParams,
				function ($err, $gas) use (&$estimatedGas) {
					if ($err) {
						throw new Q_Exception('estimate gas error: ' . $err->getMessage());
					}
					$estimatedGas = $gas;
				});
				$transactionParams['gas'] = $estimatedGas;
			}
			$tx = new Transaction($transactionParams);
			$signedTx = '0x' . $tx->sign($privateKey);
			$transactionHash = null;
			$eth->sendRawTransaction($signedTx,
			function ($err, $txHash) use (&$transactionHash) {
				if ($err) { 
					throw new Q_Exception('transaction error: ' . $err->getMessage());
				} else {
					$transactionHash = $txHash;
				}
			});
			// NOTE: This hasn't been tested yet
			return $transactionHash;
		}
		$arguments[] = $transaction;
		$arguments[] = $defaultBlock;
		$arguments[] = $callback;
		// call contract function
		
		call_user_func_array([$contract, "call"], $arguments);

		if ($data instanceof \phpseclib\Math\BigInteger) {
			$data = $data->toString();
		} elseif (is_array($data)) {
			foreach ($data as $key => $item) {
				if ($item instanceof \phpseclib\Math\BigInteger) {
					$data[$key] = $item->toString();
				}
			}
		}

		if ($cache) {
			if ((
				is_callable($caching)
				and call_user_func_array($caching, array($data))
			) or (
				($data && $caching !== false)
				or (!$data && $caching === true)
			)) {
				$cache->result = Q::json_encode($data);
				$cache->save(true);
			}
		}

		return $data;
	}

	/**
	 * Get content of the ABI file for a contract.
	 * Taken from config Users/web3/contracts/$contractName/filename.
	 * As a fallback tries config Users/web3/contracts/$contractName/dir and if found,
	 * appends "/$contractAddress.json". As a last resort, tries
	 * Users/web3/contracts/$contractName/url and downloads it from that URL.
	 * You can interpolate "baseUrl" and "contractAddress" variables in the strings.
	 * 
	 * @method getABI
	 * @static
	 * @param {string} $contractAddress - The address of the contract. The chain doesn't matter
	 *  because we assume all contracts with same address have same code on all chains.
	 * @param {string} [$appId=Q::app()] Indicate the appId anyway, we need it to get config
	 * @param {Boolean} [$throwIfNotFound=true] - If true, throw exception if ABI file not found.
	 * @param {string} [$caching=true] - Set false to ignore cache and request URL every time.
	 * @return {array|null} Tries filename, then $dir/$contractAddress.json, then url from config
	 */
	static function getABI ($contractAddress, $appId, $throwIfNotFound=true, $caching=true)
	{
		/**
		 * @event Users/Web3/getABI {before}
		 * @param {string} $contractAddress
		 * @param {string} $appId
		 * @return {array} the actual ABI (decoded from JSON content)
		 */
		Q::event(
			'Users/Web3/getABI', compact('contractAddress', 'chainId'),
			'before', false, $ABI
		);
		if ($ABI) {
			return $ABI;
		}

		$baseUrl = Q_Request::baseUrl();
		$config = Q_Config::get(
			'Users', 'web3', 'contracts', array()
		);
		$ca = strtolower($contractAddress);
		$found = true;
		foreach ($config as $a => $info) {
			if (strtolower($a) === $ca) {
				$found = $info;
				break;
			}
		}
		if (!$found) {
			if ($throwIfNotFound) {
				if ($throwIfNotFound) {
					throw new Q_Exception_MissingConfig(array(
						'fieldpath' => "Users/web3/contracts/$ca"
					));
				}
			}
		}
		$filename = $ABI = $content = $cache = null;
		if (!empty($found['filename'])) {
			$filename = Q::interpolate($found['filename'], compact("contractAddress"));
			$filename = APP_WEB_DIR . DS . implode(DS, explode('/', $filename));
		} else if (!empty($found['dir'])) {
			$filename = APP_WEB_DIR . DS . implode(DS, explode('/', $found['dir']))
				. DS . "$contractAddress.json";
		}
		if ($filename) {
			if (!is_file($filename)) {
				if ($throwIfNotFound) {
					throw new Q_Exception_MissingFile(compact('filename'));
				} else {
					return null;
				}
			}
			$content = file_get_contents($filename);
		}
		if (!$content and !empty($found['url'])) {
			$url = Q_Uri::interpolateUrl($found['url'], compact("baseUrl", "contractAddress"));
			$methodName = "Q.Users.Web3.getABI";
			$cache = self::getCache('0x1', $contractAddress, $methodName, array($url));
			if ($caching && $cache->wasRetrieved()) {
				$content = $cache->result;
			} else {
				if ($filename = Q_Uri::filenameFromUrl($url)) {
					$content = file_get_contents($filename);
				} else {
					$content = Q_Utils::get($url);
					$ABI = Q::json_decode($content, true);
					if (!empty($ABI['result'])) {
						$ABI = $ABI["result"]; // from etherscan-like endpoint
						if (is_string($ABI)) {<commit_msg>Changed cache duration and methodName for getABI<commit_after><?php
require_once USERS_PLUGIN_DIR.'/vendor/autoload.php';
use Web3\Web3;
use Web3\Contract;
use Crypto\Keccak;
use Web3p\EthereumTx\Transaction;

/**
 * @module Users
 */
/**
 * Class for dealing with EVM blockchains
 *
 * @class Users_Web3
 */
class Users_Web3 extends Base_Users_Web3 {
	/**
	 * Used to execute methods on the blockchain.
	 * If you don't pass privateKey, then it returns the result of a "view" operation (possibly cached locally).
	 * If you pass a privateKey, then it returns the hash of a transaction submitted to be mined.
	 * @method execute
	 * @static
	 * @param {string|array} $contractAddress the contract address to call the method on,
	 *  or array($contractAddress, $abiContent) to specify custom ABI content (JSON),
	 *  useful for when you have many contracts with the same ABI produced by a
	 * @param {string} $methodName in the contract
	 * @param {string|array} [$params=array()] - params sent to contract method
	 * @param {string} [$appId=Q::app()] Indicate which entery in Users/apps config to use
	 * @param {boolean|null|callable} [$caching=true] Set false to ignore cache and request blockchain every time.
	 *  Set to null to cache any truthy result while not caching falsy results.
	 *  Or set to a callable function, to be passed the data as JSON, and return boolean indicating whether to cache or not.
	 * @param {integer} [$cacheDuration=3600] How many seconds in the past to look for a cache
	 * @param {string} [$defaultBlock='latest'] Can be one of 'latest', 'earliest', 'pending'
	 * @param {integer} [$delay=0] If not found in cache, set how many microseconds to delay before querying the blockchain
	 * @param {array} [$transaction=array()] Additional information for transaction, with possible keys 'from', 'gas', 'gasPrice', 'value', 'nonce'
	 * @param {string} [$privateKey=null] Optionally pass an ECDSA private key here, to sign the transaction with (e.g. to change blockchain state).
	 * @return {mixed} Returns the transaction hash, if privateKey was specified. Otherwise, returns the (possibly cached) result of "view" operation on the blockchain.
	 */
	static function execute (
		$contractAddress,
		$methodName,
		$params = array(),
		$appId = null,
		$caching = true,
		$cacheDuration = null,
		$defaultBlock = 'latest',
		$delay = 0,
		$transaction = array(),
		$privateKey = null)
	{
		if (is_array($contractAddress)) {
			list($contractAddress, $abi) = $contractAddress;
		}

		if (!isset($appId)) {
			$appId = Q::app();
		}

		$usersWeb3Config = Q_Config::get("Users", "web3", "chains", $appId, array());
		list($appId, $appInfo) = Users::appInfo('web3', $appId, true);
		$appInfo = array_merge($usersWeb3Config, $appInfo);
		if ($cacheDuration === null) {
			$cacheDuration = Q::ifset($appInfo, 'cacheDuration', 3600);
		}

		$chainId = Q::ifset($appInfo, 'chainId', Q::ifset($appInfo, 'appId', null));
		if (!$chainId) {
			throw new Q_Exception_MissingConfig(array(
				'fieldpath' => "'Users/apps/$appId/chainId'"
			));
		}

		if (!is_array($params)) {
			$params = array($params);
		}

		$from = Q::ifset($transaction, 'from', null);
		$cache = self::getCache($chainId, $contractAddress, $methodName, $params, $cacheDuration, $from);
		if ($caching !== false && $cacheDuration && $cache->wasRetrieved()) {
			return Q::json_decode($cache->result);
		}

		if ($delay) {
			usleep($delay);
		}

		if (empty($appInfo['rpcUrl'])) {
			throw new Q_Exception_MissingConfig(array(
				'fieldpath' => "Users/apps/$appId/rpcUrl"
			));
		}
		$infuraId = Q::ifset(
			$appInfo, 'providers', 'walletconnect', 'infura', 'projectId', null
		);
		$rpcUrl = Q::interpolate(
			$appInfo['rpcUrl'],
			compact('infuraId')
		);

		if (empty($abi)) {
			$abi = self::getABI($contractAddress, $chainId);
		}
		$data = array();
		$arguments = array($methodName);
		foreach ($params as $param) {
			$arguments[] = $param;
		}
		$callback = function ($err, $results) use (&$data) {
			if ($err) {
				$errMessage = Q::ifset($err, "message", null);
				if (!$errMessage) {
					$errMessage = $err->getMessage();
				}
				if ($errMessage) {
					throw new Exception($errMessage);
				}
			}

			if (empty($results)) {
				$data = $results;
				return;
			}
			if (sizeof($results) == 1) {
				$value = reset($results);
				if (is_array($value)) {
					foreach ($value as $result) {
						$data[] = $result->toString();
					}
				} else {
					$data = $value;
				}
			} else {
				$data = $results;
			}
		};

		$contract = (new Contract($rpcUrl, $abi, $defaultBlock))
			->at($contractAddress);
		if ($privateKey) {
			if (empty($transaction['from'])) {
				throw new Q_Exception_MissingObject(array(
					'name' => 'transaction.from'
				));
			}
			$eth = $contract->eth;
			$rawTransactionData = '0x' . 
				call_user_func_array([$contract, "getData"], $arguments);
			$transactionCount = null;
			$eth->getTransactionCount($transaction['from'],
			function ($err, $count) use(&$transactionCount) {
				if ($err) { 
					throw new Q_Exception('transaction count error: ' . $err->getMessage());
				}
				$transactionCount = $count;
			});
			if (!isset($transactionCount)) {
				return null;
			}
			$transactionParams = array_merge(array(
				'nonce' => "0x" . dechex($transactionCount->toString()),
				'from' => $transaction['from'],
				'to' =>  $contractAddress,
				// 'gas' =>  '0x' . dechex(8000000),
				'value' => '0x0',
				'data' => $rawTransactionData
			), $transaction);
			if (empty($transactionParams['gas'])) {
				$estimatedGas = null;
				$contract->estimateGas($transactionParams,
				function ($err, $gas) use (&$estimatedGas) {
					if ($err) {
						throw new Q_Exception('estimate gas error: ' . $err->getMessage());
					}
					$estimatedGas = $gas;
				});
				$transactionParams['gas'] = $estimatedGas;
			}
			$tx = new Transaction($transactionParams);
			$signedTx = '0x' . $tx->sign($privateKey);
			$transactionHash = null;
			$eth->sendRawTransaction($signedTx,
			function ($err, $txHash) use (&$transactionHash) {
				if ($err) { 
					throw new Q_Exception('transaction error: ' . $err->getMessage());
				} else {
					$transactionHash = $txHash;
				}
			});
			// NOTE: This hasn't been tested yet
			return $transactionHash;
		}
		$arguments[] = $transaction;
		$arguments[] = $defaultBlock;
		$arguments[] = $callback;
		// call contract function
		
		call_user_func_array([$contract, "call"], $arguments);

		if ($data instanceof \phpseclib\Math\BigInteger) {
			$data = $data->toString();
		} elseif (is_array($data)) {
			foreach ($data as $key => $item) {
				if ($item instanceof \phpseclib\Math\BigInteger) {
					$data[$key] = $item->toString();
				}
			}
		}

		if ($cache) {
			if ((
				is_callable($caching)
				and call_user_func_array($caching, array($data))
			) or (
				($data && $caching !== false)
				or (!$data && $caching === true)
			)) {
				$cache->result = Q::json_encode($data);
				$cache->save(true);
			}
		}

		return $data;
	}

	/**
	 * Get content of the ABI file for a contract.
	 * Taken from config Users/web3/contracts/$contractName/filename.
	 * As a fallback tries config Users/web3/contracts/$contractName/dir and if found,
	 * appends "/$contractAddress.json". As a last resort, tries
	 * Users/web3/contracts/$contractName/url and downloads it from that URL.
	 * You can interpolate "baseUrl" and "contractAddress" variables in the strings.
	 * 
	 * @method getABI
	 * @static
	 * @param {string} $contractAddress - The address of the contract. The chain doesn't matter
	 *  because we assume all contracts with same address have same code on all chains.
	 * @param {string} [$appId=Q::app()] Indicate the appId anyway, we need it to get config
	 * @param {Boolean} [$throwIfNotFound=true] - If true, throw exception if ABI file not found.
	 * @param {string} [$caching=true] - Set false to ignore cache and request URL every time.
	 * @return {array|null} Tries filename, then $dir/$contractAddress.json, then url from config
	 */
	static function getABI ($contractAddress, $appId, $throwIfNotFound=true, $caching=true)
	{
		/**
		 * @event Users/Web3/getABI {before}
		 * @param {string} $contractAddress
		 * @param {string} $appId
		 * @return {array} the actual ABI (decoded from JSON content)
		 */
		Q::event(
			'Users/Web3/getABI', compact('contractAddress', 'chainId'),
			'before', false, $ABI
		);
		if ($ABI) {
			return $ABI;
		}

		$baseUrl = Q_Request::baseUrl();
		$config = Q_Config::get(
			'Users', 'web3', 'contracts', array()
		);
		$ca = strtolower($contractAddress);
		$found = true;
		foreach ($config as $a => $info) {
			if (strtolower($a) === $ca) {
				$found = $info;
				break;
			}
		}
		if (!$found) {
			if ($throwIfNotFound) {
				if ($throwIfNotFound) {
					throw new Q_Exception_MissingConfig(array(
						'fieldpath' => "Users/web3/contracts/$ca"
					));
				}
			}
		}
		$filename = $ABI = $content = $cache = null;
		if (!empty($found['filename'])) {
			$filename = Q::interpolate($found['filename'], compact("contractAddress"));
			$filename = APP_WEB_DIR . DS . implode(DS, explode('/', $filename));
		} else if (!empty($found['dir'])) {
			$filename = APP_WEB_DIR . DS . implode(DS, explode('/', $found['dir']))
				. DS . "$contractAddress.json";
		}
		if ($filename) {
			if (!is_file($filename)) {
				if ($throwIfNotFound) {
					throw new Q_Exception_MissingFile(compact('filename'));
				} else {
					return null;
				}
			}
			$content = file_get_contents($filename);
		}
		if (!$content and !empty($found['url'])) {
			$url = Q_Uri::interpolateUrl($found['url'], compact("baseUrl", "contractAddress"));
			$methodName = "Users_Web3::getABI";
			$cacheDuration = 300000000;
			$cache = self::getCache('0x1', $contractAddress, $methodName, array($url), $cacheDuration);
			if ($caching && $cache->wasRetrieved()) {
				$content = $cache->result;
			} else {
				if ($filename = Q_Uri::filenameFromUrl($url)) {
					$content = file_get_contents($filename);
				} else {
					$content = Q_Utils::get($url);
					$ABI = Q::json_decode($content, true);
					if (!empty($ABI['result'])) {
						$ABI = $ABI["result"]; // from etherscan-like endpoint
						if (is_string($ABI)) {
{'fraction_non_alphanumeric': 0.16400844088923786, 'fraction_numerical': 0.004858418805516023, 'mean_word_length': 3.7008073817762397, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 67, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1075835', 'n_tokens_mistral': 7769, 'n_tokens_neox': 6560, 'n_words': 1775}